Compare commits

..
Author SHA1 Message Date
m 2ee5da8114 docs: record compat production deploy blocker
CI / test (push) Failing after 29s
2026-08-30 13:37:42 +03:00
m 91f0c9e799 sync gen2 integration with promoted main
CI / test (push) Failing after 29s
2026-08-30 13:26:37 +03:00
m cb66a60db2 merge compat observation deployment safeguards
CI / test (push) Failing after 35s
2026-08-30 13:16:30 +03:00
m 6a4f973e38 docs: record compat observation deployment blocker
CI / test (push) Failing after 34s
2026-08-30 13:13:47 +03:00
m 87ec7efd0f fix: allow bounded candidate startup discovery
CI / test (push) Failing after 37s
2026-08-30 13:10:38 +03:00
m 51e33e7283 fix: start candidate release set in dependency order
CI / test (push) Failing after 38s
2026-08-30 13:06:33 +03:00
m e8f35f6393 chore: support candidate-only compat observation deploy
CI / test (push) Failing after 36s
2026-08-30 13:02:23 +03:00
m 8babf6ae1f merge compat observation deployment guard
CI / test (push) Failing after 37s
2026-08-30 13:01:46 +03:00
m d0210e5aed fix: guard stable containers during compat observation deploy
CI / test (push) Failing after 35s
2026-08-30 13:01:32 +03:00
m ebdfaa0769 merge ReleaseSet production drill evidence
CI / test (push) Failing after 29s
2026-08-30 12:31:48 +03:00
m 9e0faa7c47 docs: record ReleaseSet production drill 2026-08-30 12:31:47 +03:00
m 7f8a05ccfb merge creator ReleaseSet readiness fix
CI / test (push) Failing after 31s
2026-08-30 12:21:50 +03:00
m c94d04acd0 fix: use creator health as ReleaseSet readiness 2026-08-30 12:21:48 +03:00
m bb866cd2ab merge ReleaseSet readiness rollback fix
CI / test (push) Failing after 30s
2026-08-30 12:16:37 +03:00
m 892266a4c8 fix: preserve ReleaseSet rollback and readiness budget 2026-08-30 12:16:35 +03:00
m ac531c3ec3 merge candidate ReleaseSet command fix
CI / test (push) Failing after 31s
2026-08-30 12:07:53 +03:00
m 4cd50a0937 fix: close candidate ReleaseSet heredoc assignment 2026-08-30 12:07:52 +03:00
m 8b751257e8 merge candidate health script fix
CI / test (push) Failing after 30s
2026-08-30 12:07:26 +03:00
m cbbb60e081 fix: simplify candidate health shell assignments 2026-08-30 12:07:24 +03:00
m 46e3cfc445 merge stable ReleaseSet command fix
CI / test (push) Failing after 30s
2026-08-30 12:06:57 +03:00
m 5832c49ad8 fix: close stable ReleaseSet command substitution 2026-08-30 12:06:55 +03:00
m b13d12583b merge stable ReleaseSet script fix
CI / test (push) Failing after 30s
2026-08-30 12:06:29 +03:00
m 9d5473d260 fix: close stable ReleaseSet heredoc assignment 2026-08-30 12:06:28 +03:00
m 50b508b641 merge ReleaseSet observation generation fix
CI / test (push) Failing after 30s
2026-08-30 12:06:01 +03:00
m 8400cdd92d fix: generate observation JSON without heredoc 2026-08-30 12:06:00 +03:00
m ec1860d441 merge ReleaseSet payload generation fix
CI / test (push) Failing after 30s
2026-08-30 12:05:11 +03:00
m 134ba26e08 fix: simplify ReleaseSet payload generation 2026-08-30 12:05:09 +03:00
m b82b8b874f merge ReleaseSet script quoting fix
CI / test (push) Failing after 45s
2026-08-30 12:04:33 +03:00
m 5c9d4663f8 fix: avoid nested shell quoting in observation 2026-08-30 12:04:31 +03:00
m ec9d434650 merge ReleaseSet observation script fix
CI / test (push) Failing after 44s
2026-08-30 12:03:48 +03:00
m a8a60ce29e fix: quote ReleaseSet observation payload 2026-08-30 12:03:46 +03:00
m 96cf66a876 merge ReleaseSet canary validation fix
CI / test (push) Failing after 44s
2026-08-30 12:03:03 +03:00
m 381b52d06d fix: validate complete candidate service health 2026-08-30 12:03:01 +03:00
m 4ceec5a316 merge GEN2-DEPLOY-002 supervised release set
CI / test (push) Failing after 31s
2026-08-30 11:57:07 +03:00
m 93a9c1a3bd feat: supervise complete Domovoy release sets
CI / test (push) Failing after 30s
2026-08-30 10:39:36 +03:00
m f3ad7d682b merge compatibility observation blocker evidence
CI / test (push) Failing after 30s
2026-08-30 10:29:12 +03:00
m 387de89efe docs: record compatibility observation deployment blocker 2026-08-30 10:29:11 +03:00
m 346e0acb6b merge GEN2-COMPAT-OBS-001 instrumentation
CI / test (push) Failing after 30s
2026-08-30 10:25:36 +03:00
m 3efcbbded8 feat(telemetry): observe V1 compatibility paths safely 2026-08-30 10:25:35 +03:00
m a0b742b1d5 merge GEN2-030 final acceptance
CI / test (push) Failing after 29s
2026-08-30 10:02:24 +03:00
m 86aacea99e docs(gen2): record final acceptance and retirement gap 2026-08-30 10:02:23 +03:00
m 2a8e4e1b81 merge GEN2-029 retirement decision
CI / test (push) Failing after 30s
2026-08-30 09:59:44 +03:00
m d64080ef2b docs(gen2): record V1 retirement decision 2026-08-30 09:59:42 +03:00
m 59d32143bf merge GEN2-028 compatibility inventory
CI / test (push) Failing after 28s
2026-08-30 09:58:07 +03:00
m 031ba7666c docs(gen2): inventory V1 compatibility retirement readiness 2026-08-30 09:58:06 +03:00
m 93feb9bf2d merge M6 task scope revisions
CI / test (push) Failing after 30s
2026-08-30 09:53:51 +03:00
m a957172d3c governance: revise Gen2 M6 task scopes 2026-08-30 09:53:50 +03:00
m 3182092d5c merge final core m5 evidence
CI / test (push) Failing after 30s
2026-08-30 02:35:29 +03:00
m df8a275eb1 record final core m5 production evidence
CI / test (push) Failing after 29s
2026-08-30 02:35:23 +03:00
m 8aa5179d47 merge final core medium readiness remediation
CI / test (push) Failing after 31s
2026-08-30 02:30:43 +03:00
m 7b62781087 separate medium health and model readiness
CI / test (push) Failing after 31s
2026-08-30 02:30:39 +03:00
m 5c95916648 merge bounded medium invocation recovery
CI / test (push) Failing after 31s
2026-08-30 02:10:59 +03:00
m 8caa49f429 allow bounded medium invocation recovery
CI / test (push) Failing after 31s
2026-08-30 02:10:55 +03:00
m 5b5875d0f4 merge local medium readiness remediation follow-up
CI / test (push) Failing after 29s
2026-08-30 01:59:58 +03:00
m f7ffaee10d extend local medium readiness budget
CI / test (push) Failing after 29s
2026-08-30 01:59:42 +03:00
m 550738f3ca merge Local MEDIUM readiness remediation
CI / test (push) Failing after 29s
2026-08-30 01:52:02 +03:00
m e8b4c4fdab bound cognitive provider readiness and fleet probes
CI / test (push) Failing after 30s
2026-08-30 01:51:18 +03:00
m 39f6fdee7b record core M5 acceptance amendment
CI / test (push) Failing after 27s
2026-08-30 01:45:21 +03:00
m 711a734fee fix continuity recovery harness request forwarding
CI / test (push) Failing after 33s
2026-08-30 01:09:02 +03:00
m ad851a98aa merge retrieved memory fast path
CI / test (push) Failing after 33s
2026-08-30 00:54:12 +03:00
m 1e3f29b791 short circuit cognitive work for retrieved memory
CI / test (push) Failing after 31s
2026-08-30 00:54:08 +03:00
m 62fd265bb7 merge deterministic memory review fix
CI / test (push) Failing after 33s
2026-08-30 00:48:30 +03:00
m 2a52306f90 skip review for deterministic memory replies
CI / test (push) Failing after 28s
2026-08-30 00:48:25 +03:00
m e1c2e5f59c merge memory subject normalization
CI / test (push) Failing after 32s
2026-08-30 00:43:11 +03:00
m 49b3165caa normalize marked memory subjects
CI / test (push) Failing after 28s
2026-08-30 00:43:07 +03:00
m 41b5c57d05 merge memory continuation priority fix
CI / test (push) Failing after 32s
2026-08-30 00:38:39 +03:00
m 31dd628519 prioritize memory retrieval over stale travel continuation
CI / test (push) Failing after 29s
2026-08-30 00:38:35 +03:00
m d0103bbf8e merge acknowledgement routing fix
CI / test (push) Failing after 33s
2026-08-30 00:34:34 +03:00
m 29217ca935 classify stable acknowledgements before continuation
CI / test (push) Failing after 30s
2026-08-30 00:34:30 +03:00
m e9c3df26ae merge stable response review fix
CI / test (push) Failing after 32s
2026-08-30 00:30:05 +03:00
m 70ece6ed5c avoid redundant review for stable chat
CI / test (push) Failing after 32s
2026-08-30 00:30:00 +03:00
m 916b93517a merge stable chat routing fix
CI / test (push) Failing after 31s
2026-08-30 00:26:36 +03:00
m cf68786d0c skip cognitive interpretation for stable chat
CI / test (push) Failing after 30s
2026-08-30 00:26:31 +03:00
m 44ac0203a9 merge bounded chat response fix
CI / test (push) Failing after 32s
2026-08-30 00:22:25 +03:00
m 8571e3fe59 bound deterministic chat response path
CI / test (push) Failing after 33s
2026-08-30 00:22:20 +03:00
m 0af6a9b9a5 merge conversational context fix into gen2 integration
CI / test (push) Failing after 34s
2026-08-29 23:42:57 +03:00
m 6b9d5be299 preserve stable conversational replies across stale context
CI / test (push) Failing after 26s
2026-08-29 23:42:52 +03:00
m 8b3d6f9176 merge cognitive readiness budget fix
CI / test (push) Failing after 27s
2026-08-29 23:31:29 +03:00
m b4ab280950 align cognitive readiness probe budgets
CI / test (push) Failing after 30s
2026-08-29 23:31:25 +03:00
m 9cb69d1663 merge Gen2 household acceptance fixes
CI / test (push) Failing after 38s
2026-08-29 23:28:15 +03:00
m 5387010a15 harden Gen2 household introspection and readiness
CI / test (push) Failing after 33s
2026-08-29 23:28:10 +03:00
m 66fd4bfc75 merge memory recall routing fix
CI / test (push) Failing after 36s
2026-08-29 23:14:19 +03:00
m 8ac4525569 route recalled memory through local cognitive review
CI / test (push) Failing after 27s
2026-08-29 23:14:15 +03:00
m 40c610945f merge stable local answer routing fix
CI / test (push) Failing after 34s
2026-08-29 23:10:11 +03:00
m 99458a6ced avoid redundant review of stable local answers
CI / test (push) Failing after 29s
2026-08-29 23:10:07 +03:00
m 25c62e38fc merge bounded cognitive response budgets into integration
CI / test (push) Failing after 36s
2026-08-29 23:05:57 +03:00
m 5c0c6b3e42 bound local cognitive household response budgets
CI / test (push) Failing after 29s
2026-08-29 23:05:43 +03:00
m 788137b4ba merge bounded Gen2 memory projection
CI / test (push) Failing after 33s
2026-08-29 22:59:43 +03:00
m 2ef639eba9 bound cognitive personal-memory projection
CI / test (push) Failing after 36s
2026-08-29 22:59:38 +03:00
m d4ed85dec2 merge cognitive head persistence fix
CI / test (push) Failing after 35s
2026-08-29 22:44:04 +03:00
m c1c2110322 preserve governed cognitive head across discovery refresh
CI / test (push) Failing after 31s
2026-08-29 22:43:59 +03:00
m d83fcb2176 merge local cognitive metadata fix
CI / test (push) Failing after 35s
2026-08-29 22:40:58 +03:00
m 041fb1556d fix local cognitive model metadata qualification
CI / test (push) Failing after 32s
2026-08-29 22:40:54 +03:00
m a3baade40b merge main into gen2-integration before GEN2-027 acceptance
CI / test (push) Failing after 30s
2026-08-29 22:19:32 +03:00
m 12104210a3 synchronize discovery head protection
CI / test (push) Failing after 29s
2026-08-29 21:54:03 +03:00
m 40dcc36b06 synchronize governed binding head fix
CI / test (push) Failing after 36s
2026-08-29 21:53:51 +03:00
m 433cb2de2c prevent discovery from rolling back governed binding head
CI / test (push) Failing after 27s
2026-08-29 21:53:46 +03:00
m b49ff6cb71 synchronize cognitive activation fix
CI / test (push) Failing after 32s
2026-08-29 21:51:02 +03:00
m 4bb6728dfe load canary revision for cognitive activation
CI / test (push) Failing after 29s
2026-08-29 21:50:58 +03:00
m b00130ac15 synchronize cognitive remediation branches
CI / test (push) Failing after 29s
2026-08-29 21:47:28 +03:00
m fc356eb0a3 synchronize medium startup discovery fix
CI / test (push) Failing after 35s
2026-08-29 21:47:14 +03:00
m 0f880896fe allow bounded medium startup discovery
CI / test (push) Failing after 28s
2026-08-29 21:47:09 +03:00
m 460a82e020 synchronize cognitive lifecycle remediation
CI / test (push) Failing after 31s
2026-08-29 21:44:01 +03:00
m e27ead1a75 promote durable task evaluations through cognitive lifecycle
CI / test (push) Failing after 29s
2026-08-29 21:43:57 +03:00
m 64bc8bca25 merge governed cognitive promotion wiring
CI / test (push) Failing after 31s
2026-08-29 21:40:34 +03:00
m 67b585af94 load evaluation evidence for cognitive promotion
CI / test (push) Failing after 28s
2026-08-29 21:40:27 +03:00
m e47ea7b48e synchronize cognitive remediation timeout fix
CI / test (push) Failing after 31s
2026-08-29 21:36:20 +03:00
m 75042e090f use bounded medium client timeout for discovery
CI / test (push) Failing after 30s
2026-08-29 21:36:08 +03:00
m 1168c00caf merge cognitive remediation into main
CI / test (push) Failing after 32s
2026-08-29 21:33:23 +03:00
m 6e873ae3af merge medium discovery timeout remediation
CI / test (push) Failing after 29s
2026-08-29 21:32:42 +03:00
m b7210dc0ad fix cognitive medium discovery timeout
CI / test (push) Failing after 30s
2026-08-29 21:32:26 +03:00
m c0704dd28e merge cognitive qualification fix into main
CI / test (push) Failing after 29s
2026-08-29 21:17:18 +03:00
m 198f6cfe58 merge cognitive task evaluation locale fix
CI / test (push) Failing after 30s
2026-08-29 21:17:13 +03:00
m 49608599c4 fix cognitive task evaluation locale 2026-08-29 21:17:12 +03:00
m 4f6d205a60 merge cognitive route remediation into main
CI / test (push) Failing after 29s
2026-08-29 21:05:32 +03:00
m bcb833f15e merge GEN2-COGNITIVE-001 remediation task
CI / test (push) Failing after 30s
2026-08-29 21:05:16 +03:00
m 4f6f2a1f48 remediate local medium cognitive route 2026-08-29 21:05:10 +03:00
m f709c026d0 merge: repair cognitive discovery head persistence
CI / test (push) Failing after 28s
2026-08-29 19:35:27 +03:00
m 92ad3516ea fix: advance cognitive discovery binding heads
CI / test (push) Failing after 26s
2026-08-29 19:35:23 +03:00
m e1beed0391 merge: wire local medium cognitive route
CI / test (push) Failing after 39s
2026-08-29 19:28:43 +03:00
m 6b297c7749 fix: wire cognitive medium to local chat gateway
CI / test (push) Failing after 33s
2026-08-29 19:28:37 +03:00
m 91b8da2240 merge: integrate frozen Domovoy UI and identity auth
CI / test (push) Failing after 28s
2026-08-29 17:31:41 +03:00
m 8924f5a48a Move creator passwords into identity accounts
CI / test (push) Failing after 27s
2026-08-29 17:02:42 +03:00
m 636fb904f2 refactor: fix secret file paths in code
CI / test (push) Failing after 27s
2026-08-29 16:51:50 +03:00
m 598a97cd1a fix: keep sessions during cognitive probe failures
CI / test (push) Failing after 26s
2026-08-29 16:22:05 +03:00
m e9ef635933 fix: simplify chat composer actions
CI / test (push) Failing after 27s
2026-08-29 16:10:59 +03:00
m c516754d63 fix: keep attachment tools neutral
CI / test (push) Failing after 28s
2026-08-29 16:00:09 +03:00
m 5c965ef69c fix: make Domovoy chat-first
CI / test (push) Failing after 29s
2026-08-29 15:58:19 +03:00
m eab7d11bdd fix: refine chat composer controls
CI / test (push) Failing after 26s
2026-08-29 15:42:52 +03:00
m 51a49ba0ef fix: make chat composer compact and keyboard friendly
CI / test (push) Failing after 25s
2026-08-29 15:38:28 +03:00
m 60ff22882c fix: enable starting a fresh chat
CI / test (push) Failing after 28s
2026-08-29 15:28:38 +03:00
m 9896e4f716 feat: redesign Domovoy chat as calm dark cockpit
CI / test (push) Failing after 28s
2026-08-29 15:26:54 +03:00
m ff0d0e585b merge: deploy context retrieval remediation
CI / test (push) Failing after 27s
2026-08-29 15:08:04 +03:00
m 13f3165ba0 merge: integrate context retrieval remediation
CI / test (push) Failing after 28s
2026-08-29 15:07:59 +03:00
m 55f2f9affa fix: project explicit remembered assertions
CI / test (push) Failing after 26s
2026-08-29 15:07:47 +03:00
m e47e033dda merge: synchronize integration with current main
CI / test (push) Failing after 31s
2026-08-29 14:52:15 +03:00
m 2dc5587fd1 Clarify chat response progress
CI / test (push) Failing after 30s
2026-08-29 14:31:03 +03:00
m 992c84240b Preserve chat responsive guardrail 2026-08-29 14:30:29 +03:00
m cfeb6aeba7 Put chat first on mobile screens 2026-08-29 14:30:10 +03:00
m f0d45ca9f1 Clarify chat message roles and timing 2026-08-29 14:29:39 +03:00
m e20c9fdfc0 Improve Domovoy chat composer usability 2026-08-29 14:28:32 +03:00
m cb50a120e5 merge: record Gen2 deployment recovery evidence
CI / test (push) Failing after 29s
2026-08-29 12:49:59 +03:00
m b30eb38eca docs: record Gen2 deployment recovery evidence
CI / test (push) Failing after 28s
2026-08-29 12:49:51 +03:00
m 2083096aed merge: preserve stable rollback target
CI / test (push) Failing after 28s
2026-08-29 12:44:33 +03:00
m 671ecb4968 fix(supervisor): preserve stable rollback target
CI / test (push) Failing after 32s
2026-08-29 12:44:25 +03:00
m af9d5809e9 merge: stabilize supervisor control contract
CI / test (push) Failing after 27s
2026-08-29 12:41:30 +03:00
m 990928618d fix(supervisor): stabilize control JSON contract
CI / test (push) Failing after 31s
2026-08-29 12:41:22 +03:00
m 9fac8284f5 merge: initialize supervisor state volume permissions
CI / test (push) Failing after 27s
2026-08-29 12:39:01 +03:00
m 7d9a5edee5 fix(deploy): initialize supervisor volume permissions
CI / test (push) Failing after 32s
2026-08-29 12:38:53 +03:00
m 32d3f318a5 merge: independent supervisor control plane
CI / test (push) Failing after 31s
2026-08-29 12:33:54 +03:00
m 3a563a29ee feat(deploy): add independent supervisor control plane
CI / test (push) Failing after 28s
2026-08-29 12:33:44 +03:00
m 734db5d779 merge: synchronize integration with current main
CI / test (push) Failing after 28s
2026-08-29 12:25:26 +03:00
m 36fc8ec8c6 merge: resume Gen2 controlled canary
CI / test (push) Failing after 27s
2026-08-29 12:16:21 +03:00
m b8c001be4c chore(gen2): resume controlled production canary
CI / test (push) Failing after 26s
2026-08-29 12:16:09 +03:00
m eeeba6b7c0 merge: Gen2 database provisioning remediation
CI / test (push) Failing after 31s
2026-08-29 12:12:43 +03:00
m fce3ec569c fix(database): provision Gen2 owner roles before migrations
CI / test (push) Failing after 27s
2026-08-29 12:12:39 +03:00
m a925d787dc GEN2-027: prepare controlled production canary
CI / test (push) Failing after 26s
2026-08-29 12:01:37 +03:00
m 43556e1603 GEN2-026: route household context through scoped Gen2 projection
CI / test (push) Failing after 28s
2026-08-29 12:00:04 +03:00
m dd00dabf32 GEN2-025: normalize multimodal ingress provenance
CI / test (push) Failing after 27s
2026-08-29 11:58:34 +03:00
m 33365ce7ee GEN2-024: establish principal realms and source trust
CI / test (push) Failing after 26s
2026-08-29 11:57:19 +03:00
m 5017a854ee GEN2-023: make conversational configuration intent explicit
CI / test (push) Failing after 29s
2026-08-29 11:55:38 +03:00
m 839b35f310 GEN2 M4 remediation: durable recovery and governed evolution
CI / test (push) Failing after 26s
2026-08-29 11:47:05 +03:00
m 96687140ce GEN2-022: govern self evolution through development gates
CI / test (push) Failing after 27s
2026-08-29 11:31:45 +03:00
m b282038949 GEN2-021: add independent supervisor lifeboat
CI / test (push) Failing after 27s
2026-08-29 11:30:49 +03:00
m 4de506a2af GEN2-020: separate execution receipts from world outcomes
CI / test (push) Failing after 26s
2026-08-29 11:30:05 +03:00
m 36272b9036 GEN2-019: add advisory consequence assessment
CI / test (push) Failing after 29s
2026-08-29 11:28:52 +03:00
m a779568622 GEN2-018: add bounded perspectives over Work Graph
CI / test (push) Failing after 27s
2026-08-29 11:28:05 +03:00
m 243a11490b GEN2-017: add durable long-running investigations
CI / test (push) Failing after 26s
2026-08-29 11:27:21 +03:00
m 26207d93ef GEN2-016: add intelligence strategy and benchmark evidence
CI / test (push) Failing after 26s
2026-08-29 11:16:07 +03:00
m 42871f2e47 GEN2-015: add evidence-backed SELF ledger
CI / test (push) Failing after 28s
2026-08-29 11:13:38 +03:00
m 6ea1d4b729 GEN2-014: add reflex lifecycle and escalation
CI / test (push) Failing after 28s
2026-08-29 11:12:42 +03:00
m c6d1ebf36f GEN2-013: add versioned Skill V2 contracts
CI / test (push) Failing after 27s
2026-08-29 11:11:50 +03:00
m 25d8d6d219 GEN2-012: add typed scoped configuration
CI / test (push) Failing after 33s
2026-08-29 11:09:33 +03:00
m 6cbef9af41 GEN2-012: add scoped configuration bootstrap boundary
CI / test (push) Failing after 34s
2026-08-29 11:08:53 +03:00
m 2fe9802e5a GEN2-011: cut PersonalMemory to compatibility fallback
CI / test (push) Failing after 37s
2026-08-29 02:41:23 +03:00
m 0882df3ad2 GEN2-010: add bounded Context Compiler V2 trace
CI / test (push) Successful in 53s
2026-08-29 02:39:58 +03:00
m 0acbe53fad GEN2-009: add rebuildable retrieval projections
CI / test (push) Successful in 50s
2026-08-29 02:38:49 +03:00
m e634bb946a GEN2-008: promote experience and episode projections
CI / test (push) Successful in 55s
2026-08-29 02:37:28 +03:00
m c7ad996cae GEN2-007: add task-relative freshness assessment
CI / test (push) Successful in 52s
2026-08-29 02:35:54 +03:00
m a065382512 GEN2-006: normalize epistemic propositions and assessments
CI / test (push) Successful in 51s
2026-08-29 02:33:09 +03:00
m 8a8e571905 GEN2-005: govern identity resolution and version bindings
CI / test (push) Failing after 38s
2026-08-29 02:20:02 +03:00
m 301dbdcfb2 GEN2-004: add ContextScope and FocusStack V2
CI / test (push) Successful in 50s
2026-08-29 02:17:36 +03:00
m 8037e66998 GEN2-003: add typed World referent foundation
CI / test (push) Successful in 50s
2026-08-29 02:11:36 +03:00
m 29815c49b8 GEN2-002: add shared semantic primitives
CI / test (push) Successful in 52s
2026-08-29 02:08:20 +03:00
m c14ad7fd0b GEN2-001: accept owner registry and dependency rules
CI / test (push) Successful in 49s
2026-08-29 02:05:35 +03:00
m 939a5152e1 ARCH-GEN2-000: adopt Gen2 architecture governance
CI / test (push) Successful in 48s
2026-08-29 01:53:27 +03:00
m 4e54ace76b Route local place searches to relevant evidence
CI / test (pull_request) Successful in 1m19s
CI / test (push) Successful in 47s
2026-08-26 10:40:15 +03:00
m f329224d12 Merge pull request 'Keep nearest flight reply focused' (#405) from codex/household-text-cognition-path into main
CI / test (push) Successful in 1m21s
Reviewed-on: #405
2026-08-26 07:33:36 +00:00
m bb76171af4 Merge remote-tracking branch 'origin/main' into codex/household-text-cognition-path
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m23s
2026-08-26 10:30:15 +03:00
m 213f06661d Keep nearest flight reply focused
CI / test (push) Failing after 59s
CI / test (pull_request) Failing after 58s
2026-08-26 10:27:54 +03:00
m d350b9a565 Merge pull request 'Allow reply-only cognition for text directives' (#404) from codex/household-text-cognition-path into main
CI / test (push) Successful in 1m24s
Reviewed-on: #404
2026-08-26 07:21:07 +00:00
m f862c3ac13 Allow reply-only cognition for text directives
CI / test (push) Successful in 1m22s
CI / test (pull_request) Successful in 1m17s
2026-08-26 10:17:51 +03:00
m 5e18eb6082 Merge pull request 'Route safe text directives through cognition' (#403) from codex/household-text-directives into main
CI / test (push) Successful in 1m21s
Reviewed-on: #403
2026-08-26 07:12:00 +00:00
m ec86c983e3 Route safe text directives through cognition
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m16s
2026-08-26 10:08:42 +03:00
m ea854d5278 Merge pull request 'Allow review of backed research answers' (#402) from codex/household-research-review into main
CI / test (push) Successful in 1m17s
Reviewed-on: #402
2026-08-26 07:01:37 +00:00
m bbecadcec2 Allow review of backed research answers
CI / test (push) Successful in 1m19s
CI / test (pull_request) Successful in 1m22s
2026-08-26 09:58:20 +03:00
m 1c89b30512 Merge pull request 'Reject distance sources without measured evidence' (#401) from codex/household-distance-evidence into main
CI / test (push) Successful in 1m18s
Reviewed-on: #401
2026-08-26 06:50:59 +00:00
m 75a8533939 Reject distance sources without measured evidence
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m18s
2026-08-26 09:47:49 +03:00
m 515a98c470 Merge pull request 'Preserve distance context before search normalization' (#400) from codex/household-distance-context into main
CI / test (push) Successful in 1m32s
Reviewed-on: #400
2026-08-26 06:38:01 +00:00
m d3ad1fd5bb Preserve distance context before search normalization
CI / test (push) Successful in 1m23s
CI / test (pull_request) Successful in 1m20s
2026-08-26 09:34:43 +03:00
m e1802aef12 Merge pull request 'Answer directly without repeating the question' (#399) from codex/household-direct-answer into main
CI / test (push) Successful in 1m34s
Reviewed-on: #399
2026-08-26 06:23:34 +00:00
m 35fc2438dc Answer directly without repeating the question
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m21s
2026-08-26 09:19:28 +03:00
m 70335a78e2 Merge pull request 'Fallback to local interpretation when provider fails' (#398) from codex/household-answer-quality into main
CI / test (push) Successful in 1m33s
Reviewed-on: #398
2026-08-26 06:05:14 +00:00
m 41067a0b16 Merge branch 'main' into codex/household-answer-quality
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m18s
2026-08-26 06:00:13 +00:00
m fba05d5ed4 Fallback to local interpretation when provider fails
CI / test (push) Failing after 56s
CI / test (pull_request) Failing after 56s
2026-08-26 08:59:49 +03:00
m 6e14ba1727 Merge pull request 'Review stable knowledge replies for conversational quality' (#397) from codex/household-answer-quality into main
CI / test (push) Successful in 2m0s
Reviewed-on: #397
2026-08-26 05:53:37 +00:00
m 9c6e2097b1 Review stable knowledge replies for conversational quality
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m27s
2026-08-26 08:48:01 +03:00
m 96559f6ab8 Merge pull request 'Preserve completed household answers in web chat' (#396) from codex/household-core-reply into main
CI / test (push) Successful in 1m25s
Reviewed-on: #396
2026-08-26 04:58:04 +00:00
m 46b89577eb Preserve completed household answers in web chat
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m21s
2026-08-26 05:21:53 +03:00
m a6ddbfaaf2 Merge pull request 'Handle household typos through semantic cognition' (#395) from codex/household-typo-cognition into main
CI / test (push) Successful in 1m29s
Reviewed-on: #395
2026-08-26 02:17:46 +00:00
m 362a1bd993 Handle household typos through semantic cognition
CI / test (push) Successful in 1m48s
CI / test (pull_request) Successful in 1m15s
2026-08-26 04:59:40 +03:00
m 735222de0d Merge pull request 'Route household knowledge and travel evidence through cognition' (#394) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m34s
Reviewed-on: #394
2026-08-26 01:51:16 +00:00
m 4e3d153557 Route household knowledge through cognition
CI / test (push) Successful in 1m50s
CI / test (pull_request) Successful in 1m16s
2026-08-26 04:31:18 +03:00
m f316f7ec3f Exclude dates from concrete research identifiers
CI / test (push) Successful in 1m21s
2026-08-26 04:20:15 +03:00
m da5d06895b Require destination evidence for ticket results
CI / test (push) Successful in 1m17s
2026-08-26 04:17:12 +03:00
m 4d1a579132 Merge pull request 'Keep source names out of identifier clarification' (#393) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m23s
Reviewed-on: #393
2026-08-26 01:12:36 +00:00
m 43836af511 Keep source names out of identifier clarification
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 1m37s
2026-08-26 03:34:00 +03:00
m 32457f7142 Merge pull request 'Explain missing concrete research identifiers' (#392) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m30s
Reviewed-on: #392
2026-08-26 00:31:26 +00:00
m 204eee489c Explain missing concrete research identifiers
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 1m16s
2026-08-26 03:20:17 +03:00
m 5cd8665aba Merge pull request 'Reject generic pages for concrete research identifiers' (#391) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m30s
Reviewed-on: #391
2026-08-26 00:17:03 +00:00
m 04018cbd07 Reject generic pages for concrete research identifiers
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m18s
2026-08-26 02:11:51 +03:00
m 23a1eeae56 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m12s
Reviewed-on: #390
2026-08-25 21:25:26 +00:00
m 369588ada7 Clean search imperatives before research
CI / test (push) Successful in 1m25s
CI / test (pull_request) Failing after 55s
2026-08-26 00:24:45 +03:00
m e323689fd4 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 21:19:35 +00:00
m af042dc69f Keep internet status replies concise
CI / test (push) Successful in 1m26s
CI / test (pull_request) Failing after 53s
2026-08-26 00:19:15 +03:00
m 60c1c31bd9 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 21:14:40 +00:00
m ff3f14fc77 Request time difference measurements explicitly
CI / test (push) Failing after 59s
CI / test (pull_request) Failing after 56s
2026-08-26 00:14:20 +03:00
m 36f991d8d0 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 21:11:50 +00:00
m 54a3996c17 Keep comparison entities in one request
CI / test (push) Failing after 59s
CI / test (pull_request) Failing after 52s
2026-08-26 00:11:30 +03:00
m a769f33d16 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 21:08:39 +00:00
m 056ff1b795 Research current time differences generically
CI / test (push) Failing after 1m0s
CI / test (pull_request) Failing after 57s
2026-08-26 00:08:19 +03:00
m 7b2debb60e Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 21:02:12 +00:00
m 6dfb3fc26c Validate displayed flight research excerpts
CI / test (push) Failing after 1m0s
CI / test (pull_request) Failing after 57s
2026-08-26 00:01:51 +03:00
m e0a093eaf3 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 20:58:11 +00:00
m 24916aa8f7 Require route evidence in displayed snippets
CI / test (push) Successful in 1m38s
CI / test (pull_request) Failing after 55s
2026-08-25 23:57:50 +03:00
m d7afd5be34 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 20:47:18 +00:00
m e104ce790d Require full route evidence for nearest flights
CI / test (push) Successful in 1m29s
CI / test (pull_request) Failing after 56s
2026-08-25 23:46:52 +03:00
m d7484cf757 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 20:43:03 +00:00
m a57e8ea71b Allow research source recovery to complete
CI / test (push) Failing after 56s
CI / test (pull_request) Failing after 1m4s
2026-08-25 23:42:33 +03:00
m 9dffde63b8 Merge pull request 'Form nearest flight searches from route context' (#389) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m16s
Reviewed-on: #389
2026-08-25 20:39:42 +00:00
m 4e5bb9abc9 Form nearest flight searches from route context
CI / test (push) Successful in 1m28s
CI / test (pull_request) Failing after 57s
2026-08-25 23:39:08 +03:00
m 3435de8013 Merge pull request 'Preserve honest research outcomes' (#388) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m13s
Reviewed-on: #388
2026-08-25 20:34:09 +00:00
m 76c2824887 Preserve attempted research outcomes
CI / test (push) Successful in 1m30s
CI / test (pull_request) Failing after 56s
2026-08-25 23:33:30 +03:00
m fc00a57942 Merge pull request 'Observe chat context route decisions' (#387) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m15s
Reviewed-on: #387
2026-08-25 20:30:19 +00:00
m 1b21888e8b Observe chat context route decisions
CI / test (push) Successful in 1m27s
CI / test (pull_request) Failing after 1m1s
2026-08-25 23:29:39 +03:00
m 012f5efafe Merge pull request 'Compile personal facts for contextual assistance' (#386) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m12s
Reviewed-on: #386
2026-08-25 19:51:47 +00:00
m eb842e24ff Compile personal facts for contextual assistance
CI / test (push) Successful in 1m15s
CI / test (pull_request) Failing after 50s
2026-08-25 22:50:37 +03:00
m 13bbcdf747 Preserve specialized research routes
CI / test (push) Successful in 1m19s
2026-08-25 22:46:20 +03:00
m c00b9b1549 Merge pull request 'Route plane requests through verified research' (#385) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m15s
Reviewed-on: #385
2026-08-25 19:42:26 +00:00
m 2d88d488f4 Route plane requests through verified research
CI / test (push) Successful in 1m34s
CI / test (pull_request) Failing after 58s
2026-08-25 22:42:03 +03:00
m 72c9cfc893 Merge pull request 'Validate flight research by route meaning' (#384) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m22s
Reviewed-on: #384
2026-08-25 19:35:41 +00:00
m 0b619bdc08 Validate flight research by route meaning
CI / test (push) Successful in 1m28s
CI / test (pull_request) Failing after 58s
2026-08-25 22:35:21 +03:00
m db23e984da Merge pull request 'Forward large generated images to chat' (#383) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m14s
Reviewed-on: #383
2026-08-25 19:31:17 +00:00
m f0c8f39986 Forward large generated images to chat
CI / test (push) Successful in 1m28s
CI / test (pull_request) Failing after 53s
2026-08-25 22:30:50 +03:00
m 5d8ec1995a Merge pull request 'Honor requested image quality' (#382) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m30s
Reviewed-on: #382
2026-08-25 19:14:21 +00:00
m 75d707e960 Honor requested image quality
CI / test (push) Successful in 1m22s
CI / test (pull_request) Successful in 1m25s
2026-08-25 22:12:44 +03:00
m 89b053b7bf Merge pull request 'Require assistant target for renaming' (#381) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m16s
Reviewed-on: #381
2026-08-25 18:32:39 +00:00
m e90aded6d3 Require assistant target for renaming
CI / test (push) Successful in 1m25s
CI / test (pull_request) Failing after 54s
2026-08-25 21:32:20 +03:00
m 8456ecd237 Merge pull request 'Continue ticket search with date follow-up' (#380) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m13s
Reviewed-on: #380
2026-08-25 18:21:30 +00:00
m b89fdef1d9 Merge remote-tracking branch 'origin/main' into codex/household-travel-query-fix
CI / test (push) Failing after 57s
CI / test (pull_request) Failing after 55s
2026-08-25 21:21:12 +03:00
m 6f3d576602 Continue ticket search with date follow-up
CI / test (push) Failing after 50s
CI / test (pull_request) Failing after 1m9s
2026-08-25 21:20:36 +03:00
m b026ec151e Merge pull request 'Preserve explicit memory confirmations' (#379) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m25s
Reviewed-on: #379
2026-08-25 18:16:05 +00:00
m 5d4c3877b1 Preserve explicit memory confirmations
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m26s
2026-08-25 21:13:26 +03:00
m 113c5ab965 Merge pull request 'Apply learned source aliases to research' (#378) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m14s
Reviewed-on: #378
2026-08-25 18:02:29 +00:00
m bce7993a6a Apply learned source aliases to research
CI / test (push) Successful in 1m30s
CI / test (pull_request) Failing after 52s
2026-08-25 21:02:07 +03:00
m 3bbf416c20 Merge pull request 'Explain missing research evidence' (#377) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m14s
Reviewed-on: #377
2026-08-25 17:56:42 +00:00
m b62b3b0dc3 Explain missing research evidence
CI / test (push) Failing after 59s
CI / test (pull_request) Failing after 1m2s
2026-08-25 20:56:21 +03:00
m 7e72474b0a Merge pull request 'Summarize verified internet connectivity' (#376) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m33s
Reviewed-on: #376
2026-08-25 17:53:22 +00:00
m b05081328e Recognize network connection wording
CI / test (push) Failing after 1m1s
CI / test (pull_request) Failing after 50s
2026-08-25 20:53:00 +03:00
m 0e7d1a7a50 Summarize verified internet connectivity
CI / test (push) Failing after 58s
2026-08-25 20:52:41 +03:00
m 2aaa73bb2a Merge pull request 'Describe local Domovoy capabilities' (#375) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m13s
Reviewed-on: #375
2026-08-25 17:50:03 +00:00
m 9000d90d72 Describe local Domovoy capabilities
CI / test (push) Successful in 1m29s
CI / test (pull_request) Failing after 1m0s
2026-08-25 20:49:39 +03:00
m 4afcfd3846 Merge pull request 'Recognize self capability questions' (#374) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m19s
Reviewed-on: #374
2026-08-25 16:54:32 +00:00
m 6ba8265686 Recognize self capability questions
CI / test (push) Failing after 58s
CI / test (pull_request) Failing after 54s
2026-08-25 19:54:06 +03:00
m 21583528c0 Merge pull request 'Explain current cognitive response route' (#373) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m18s
Reviewed-on: #373
2026-08-25 16:51:27 +00:00
m b1bb48f5b5 Explain current cognitive response route
CI / test (push) Successful in 1m40s
CI / test (pull_request) Failing after 54s
2026-08-25 19:51:03 +03:00
m 88f5f6e8f8 Merge pull request 'Recognize named model inventory requests' (#372) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m16s
Reviewed-on: #372
2026-08-25 16:48:12 +00:00
m 5439db2cc3 Recognize named model inventory requests
CI / test (push) Successful in 1m14s
CI / test (pull_request) Failing after 54s
2026-08-25 19:47:25 +03:00
m 208cb18ee0 Merge pull request 'Preserve latest travel context' (#371) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m32s
Reviewed-on: #371
2026-08-25 16:43:43 +00:00
m 5d19becdaf Preserve latest travel context
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m14s
2026-08-25 19:36:49 +03:00
m 4cd3ecdf15 Merge pull request 'Normalize contextual travel research' (#370) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m13s
Reviewed-on: #370
2026-08-25 16:29:58 +00:00
m 8e26f529f7 Merge remote-tracking branch 'origin/codex/household-travel-query-fix' into codex/household-travel-query-fix
CI / test (push) Failing after 1m3s
CI / test (pull_request) Failing after 54s
2026-08-25 19:28:11 +03:00
m 3cc636d6b7 Normalize contextual travel research 2026-08-25 19:27:59 +03:00
m 14dc439c82 Merge pull request 'Improve semantic travel research queries' (#369) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m14s
Reviewed-on: #369
2026-08-25 16:26:24 +00:00
m 21887988b2 Merge branch 'main' into codex/household-travel-query-fix
CI / test (push) Failing after 57s
CI / test (pull_request) Failing after 54s
2026-08-25 16:25:54 +00:00
m 232a57eed4 Improve semantic travel research queries
CI / test (push) Failing after 53s
CI / test (pull_request) Failing after 54s
2026-08-25 19:25:00 +03:00
m d1fd70e436 Merge pull request 'Preserve location context for travel follow-ups' (#368) from codex/household-travel-context-fix into main
CI / test (push) Successful in 1m30s
Reviewed-on: #368
2026-08-25 16:22:04 +00:00
m 1d73162df1 Merge remote-tracking branch 'origin/main' into codex/household-travel-context-fix
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m24s
2026-08-25 19:17:59 +03:00
m 7788cf4b44 Preserve location context for travel follow-ups
CI / test (push) Failing after 58s
CI / test (pull_request) Failing after 50s
2026-08-25 19:17:34 +03:00
m d9dcddbdbb Merge pull request 'Preserve direct time difference replies' (#367) from codex/household-time-reply-priority into main
CI / test (push) Successful in 2m3s
Reviewed-on: #367
2026-08-25 16:11:36 +00:00
m 5fb616310d Merge remote-tracking branch 'origin/main' into codex/household-time-reply-priority
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m24s
2026-08-25 19:08:15 +03:00
m 5a9cf7a22d Preserve direct time difference replies
CI / test (push) Failing after 51s
CI / test (pull_request) Failing after 48s
2026-08-25 19:06:51 +03:00
m d649f79ca4 Merge pull request 'Preserve remembered preference identity' (#366) from codex/household-checklist-live-preference-fix into main
CI / test (push) Successful in 1m58s
Reviewed-on: #366
2026-08-25 16:00:08 +00:00
m ffce659332 Merge remote-tracking branch 'origin/main' into codex/household-checklist-live-preference-fix
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m24s
2026-08-25 18:57:06 +03:00
m 24b30bf669 Preserve remembered preference identity
CI / test (push) Failing after 51s
CI / test (pull_request) Failing after 50s
2026-08-25 18:55:33 +03:00
m 1bd9137761 Merge pull request 'Add executable 50-point household checklist' (#365) from codex/household-media-queue-isolation into main
CI / test (push) Successful in 1m39s
Reviewed-on: #365
2026-08-25 15:49:39 +00:00
m f895c5a104 Merge remote-tracking branch 'origin/main' into codex/household-media-queue-isolation
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m14s
2026-08-25 18:46:10 +03:00
m d0074709aa Add executable 50-point household checklist
CI / test (push) Failing after 49s
CI / test (pull_request) Failing after 50s
2026-08-25 18:44:49 +03:00
m 982eb6f49b Merge pull request 'Do not block image generation on general cognition' (#364) from codex/household-media-queue-isolation into main
CI / test (push) Successful in 1m24s
Reviewed-on: #364
2026-08-25 15:28:35 +00:00
m b1ee085de4 Merge remote-tracking branch 'origin/main' into codex/household-media-queue-isolation
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m15s
2026-08-25 18:25:37 +03:00
m 2c66a44910 Do not block media routes on general cognition
CI / test (push) Failing after 1m6s
CI / test (pull_request) Failing after 57s
2026-08-25 18:22:38 +03:00
m b95714a71e Merge pull request 'Isolate media cancellation and improve travel answers' (#363) from codex/household-media-queue-isolation into main
CI / test (push) Successful in 1m19s
Reviewed-on: #363
2026-08-25 15:18:22 +00:00
m 6183d811f1 Merge remote-tracking branch 'origin/main' into codex/household-media-queue-isolation
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m10s
2026-08-25 18:14:52 +03:00
m 6a2ed287ba Keep flight formatting within task scope
CI / test (push) Failing after 51s
CI / test (pull_request) Failing after 49s
2026-08-25 18:12:39 +03:00
m d342728b46 Improve nearest flight research answers
CI / test (push) Failing after 51s
2026-08-25 18:12:13 +03:00
m f121445f24 Preserve media failures and enrich nearest flight searches
CI / test (push) Failing after 52s
2026-08-25 18:08:48 +03:00
m 124871da76 Cancel only the owned ComfyUI prompt
CI / test (push) Failing after 54s
2026-08-25 18:02:59 +03:00
m 2062094228 Merge pull request 'Route creative image requests to media generation' (#362) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m42s
Reviewed-on: #362
2026-08-25 14:42:39 +00:00
m 515c9713e9 Merge remote-tracking branch 'origin/main' into codex/household-scenario-audit
CI / test (push) Successful in 1m53s
CI / test (pull_request) Successful in 1m32s
2026-08-25 17:36:53 +03:00
m 14c5d9e412 Route creative image requests to media generation
CI / test (push) Failing after 1m8s
CI / test (pull_request) Failing after 1m1s
2026-08-25 17:34:05 +03:00
m 8e9429e5d0 Merge pull request 'Bind ComfyUI image provider in production' (#361) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m25s
Reviewed-on: #361
2026-08-25 14:24:00 +00:00
m 6cb125001f Bind ComfyUI image provider in production
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m11s
2026-08-25 17:14:44 +03:00
m 1e7cf68317 Merge pull request 'Add ComfyUI image generation adapter' (#360) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m15s
Reviewed-on: #360
2026-08-25 14:08:23 +00:00
m 2f6f6fb026 Add ComfyUI image generation adapter
CI / test (push) Successful in 1m16s
CI / test (pull_request) Failing after 57s
2026-08-25 17:01:55 +03:00
m 4618a392e2 Merge pull request 'Add replaceable image and video media routes' (#359) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m19s
Reviewed-on: #359
2026-08-25 13:37:28 +00:00
m cdcbd50fe7 Test degraded media capability routing
CI / test (push) Successful in 1m15s
CI / test (pull_request) Failing after 54s
2026-08-25 16:14:28 +03:00
m 8260e69dfe Add replaceable image and video media routes
CI / test (push) Successful in 1m12s
2026-08-25 16:12:33 +03:00
m cd8ba740de Merge pull request 'Add replaceable image and video capability contracts' (#358) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m45s
Merge PR #358
2026-08-25 13:05:36 +00:00
m 8173b8f9d6 Define replaceable image and video capabilities
CI / test (push) Successful in 1m32s
CI / test (pull_request) Successful in 1m31s
2026-08-25 16:01:22 +03:00
m 7d5b91fbe9 Merge pull request 'Route creative requests through cognition' (#357) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m27s
Merge PR #357
2026-08-25 12:47:09 +00:00
m f9e50eba70 Remove canned creative fallback content
CI / test (push) Successful in 1m27s
CI / test (pull_request) Successful in 1m27s
2026-08-25 15:43:23 +03:00
m 29e17f1ac8 Remove canned creative object replies
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m24s
2026-08-25 15:40:29 +03:00
m ae8bf68d73 Merge pull request 'Add local horse creative fallback' (#356) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m32s
Merge PR #356
2026-08-25 12:33:13 +00:00
m af146c507e Add local horse drawing fallback
CI / test (push) Successful in 1m30s
CI / test (pull_request) Successful in 1m25s
2026-08-25 15:29:25 +03:00
m 7ad7a2c8ba Merge pull request 'Add household route boundary matrix' (#355) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m34s
Merge PR #355
2026-08-25 12:27:16 +00:00
m 8885a66883 Add household route boundary matrix
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m18s
2026-08-25 15:24:05 +03:00
m 2b40471e08 Merge pull request 'Add local bird creative fallback' (#354) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m20s
Merge PR #354
2026-08-25 12:22:05 +00:00
m 22253caf2d Add local bird drawing fallback
CI / test (push) Successful in 1m13s
CI / test (pull_request) Successful in 1m12s
2026-08-25 15:19:03 +03:00
m f8387180eb Merge pull request 'Isolate non-travel household requests' (#353) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m34s
Merge PR #353
2026-08-25 12:16:45 +00:00
m 288643f0da Cover cross-context household requests
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m26s
2026-08-25 15:13:03 +03:00
m 0662dbbb01 Merge pull request 'Expand semantic decomposition checks' (#352) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m32s
Merge PR #352
2026-08-25 12:08:40 +00:00
m 1a8934f88b Keep creative requests out of travel continuation
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m22s
2026-08-25 15:04:39 +03:00
m 4cac361762 Use matched alias for task target
CI / test (push) Successful in 1m28s
CI / test (pull_request) Successful in 1m24s
2026-08-25 15:01:18 +03:00
m 515cabb5cf Expand semantic memory decomposition checks 2026-08-25 15:01:17 +03:00
m 5efa6176bb Merge pull request 'Route marketplace search and bear drawing' (#351) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m36s
Merge PR #351
2026-08-25 11:55:30 +00:00
m 3515a483ca Fix generic creative target extraction
CI / test (push) Successful in 1m29s
CI / test (pull_request) Successful in 1m28s
2026-08-25 14:49:01 +03:00
m ca45df3c62 Load task aliases from semantic memory
CI / test (push) Failing after 1m0s
CI / test (pull_request) Failing after 1m2s
2026-08-25 14:48:32 +03:00
m c94a96c47e Decompose household tasks into typed atoms
CI / test (push) Successful in 1m28s
CI / test (pull_request) Successful in 1m27s
2026-08-25 14:46:43 +03:00
m 745d8b0c4d Route marketplace search and bear drawing 2026-08-25 14:46:43 +03:00
m ddc4d3bc90 Merge pull request 'Reject identity boilerplate over creative fallback' (#350) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m44s
Merge PR #350
2026-08-25 11:06:26 +00:00
m 906d71b35f Reject identity boilerplate over creative fallback
CI / test (push) Successful in 1m31s
CI / test (pull_request) Successful in 1m31s
2026-08-25 14:02:42 +03:00
m beddb78469 Merge pull request 'Preserve creative fallback over conversational reflex' (#349) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m34s
Merge PR #349
2026-08-25 10:57:57 +00:00
m fb968502ba Preserve creative fallback over conversational reflex
CI / test (push) Successful in 1m32s
CI / test (pull_request) Successful in 1m28s
2026-08-25 13:54:14 +03:00
m 643ce34cff Merge pull request 'Keep useful creative fallback replies' (#348) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m37s
Merge PR #348
2026-08-25 10:48:58 +00:00
m cc08fb3d08 Keep useful creative fallback replies
CI / test (push) Successful in 1m35s
CI / test (pull_request) Successful in 1m32s
2026-08-25 13:45:01 +03:00
m f3bab3177d Merge pull request 'Humanize web research replies' (#347) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m36s
Merge PR #347
2026-08-25 10:38:47 +00:00
m e79302c915 Humanize web research replies
CI / test (push) Successful in 1m27s
CI / test (pull_request) Successful in 1m26s
2026-08-25 13:34:48 +03:00
m d2f55bf0be Merge pull request 'Preserve assistant name in cognitive context' (#346) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m19s
Merge PR #346
2026-08-25 09:53:12 +00:00
m c0208de111 Preserve assistant name in cognitive context
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m13s
2026-08-25 12:50:23 +03:00
m 56cd297ca3 Merge pull request 'Handle reasoning-only vision responses' (#345) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m14s
Merge PR #345
2026-08-25 09:37:23 +00:00
m 5594525893 Handle reasoning-only vision responses
CI / test (push) Successful in 1m8s
CI / test (pull_request) Failing after 52s
2026-08-25 12:36:57 +03:00
m f91613b5d2 Merge pull request 'Add local speech, vision and OCR chat skills' (#344) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m15s
2026-08-25 09:29:25 +00:00
m 78e7e3fedc Add local speech and vision chat skills
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 1m7s
2026-08-25 12:26:41 +03:00
m df017a349a Merge pull request 'Improve degraded household replies and route tests' (#343) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m13s
2026-08-25 09:11:58 +00:00
m 8124fcbcc3 Improve degraded household replies
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m4s
2026-08-25 12:08:52 +03:00
m 8658db537d Merge pull request 'Add household scenario audit plan' (#342) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m34s
2026-08-25 09:04:33 +00:00
m bd382243a0 Add household scenario audit plan
CI / test (push) Successful in 1m44s
CI / test (pull_request) Successful in 1m27s
2026-08-25 12:00:25 +03:00
m d0057f1873 Merge pull request 'Use local creative fallback when cognition is unavailable' (#341) from codex/creative-chat-routing into main
CI / test (push) Successful in 1m31s
Use local creative fallback when cognition is unavailable
2026-08-25 08:49:57 +00:00
m 342a93caa4 Add local creative fallback
CI / test (push) Successful in 1m23s
CI / test (pull_request) Successful in 1m40s
2026-08-25 11:46:03 +03:00
m 0904a4ade7 Merge pull request 'Protect deterministic creative replies' (#340) from codex/creative-chat-routing into main
CI / test (push) Successful in 1m38s
2026-08-25 08:41:03 +00:00
m bbd525eeb5 Protect creative reply from intermediate rewrites
CI / test (push) Successful in 1m33s
CI / test (pull_request) Successful in 1m24s
2026-08-25 11:37:11 +03:00
m 352703a91f Merge pull request 'Route creative chat by meaning' (#339) from codex/creative-chat-routing into main
CI / test (push) Successful in 1m20s
2026-08-25 08:32:57 +00:00
m 5a439f7ba8 Route creative chat by meaning
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m9s
2026-08-25 11:29:54 +03:00
m f7200b0704 Merge pull request 'Authorize bounded site-browser observation' (#338) from codex/site-browser-architecture into main
CI / test (push) Successful in 1m20s
2026-08-25 08:17:45 +00:00
m 14bf8261da Authorize bounded site browser observation
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m9s
2026-08-25 11:15:07 +03:00
m feb87d5c4e Merge pull request 'Improve price research queries' (#337) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m17s
2026-08-25 08:04:26 +00:00
m 24320f22a6 Improve price research queries
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m13s
2026-08-25 11:01:22 +03:00
m 98cf218cc3 Merge pull request 'Shorten research source labels' (#336) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m16s
2026-08-25 07:58:42 +00:00
m 7abe73e7bd Shorten research source labels
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m8s
2026-08-25 10:55:07 +03:00
m a05ddacff3 Merge pull request 'Clarify research results without price evidence' (#335) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m17s
2026-08-25 07:52:31 +00:00
m 20b18de60a Clarify missing price evidence
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m8s
2026-08-25 10:49:34 +03:00
m d5c98dd62f Merge pull request 'Preserve durable assistant identity and deterministic replies' (#334) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m15s
2026-08-25 07:42:01 +00:00
m 61d08fa05a Preserve durable conversation identity
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 1m7s
2026-08-25 10:39:05 +03:00
m 1ea2e713d8 Format interaction contract 2026-08-25 10:39:04 +03:00
m 38dce57666 Improve location time and product research 2026-08-25 10:39:04 +03:00
m 8f255b8bde Merge pull request 'Handle memory identity and travel follow-ups' (#333) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m34s
2026-08-25 07:24:45 +00:00
m 6b97d6668b Handle memory identity and travel follow-ups
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m35s
2026-08-25 10:20:54 +03:00
m 3fa1d70379 Route completed travel continuations to research 2026-08-25 10:20:53 +03:00
m 2dd05a7aa6 Merge pull request 'Continue travel tasks from departure location' (#332) from codex/chat-semantic-inventory into main
CI / test (push) Failing after 42s
2026-08-25 07:10:22 +00:00
m 02227edc23 Continue travel tasks from departure location
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m9s
2026-08-25 10:07:28 +03:00
m a2297a84ad Merge pull request 'Reject reviewed research answers without evidence' (#331) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m16s
2026-08-25 04:22:34 +00:00
m 05cbd8adc7 Reject reviewed research answers without evidence
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m12s
2026-08-25 07:19:43 +03:00
m 984731b111 Merge pull request 'Ask departure city for general travel requests' (#330) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m20s
2026-08-25 04:13:55 +00:00
m 0da03c18d1 Ask departure city for general travel requests
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m5s
2026-08-25 07:11:09 +03:00
m 302b8fc8cd Merge pull request 'Prefer current travel context in follow-up answers' (#329) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m12s
2026-08-25 04:05:00 +00:00
m 59239635cd Prefer current travel context in follow-up answers
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 1m2s
2026-08-25 07:02:14 +03:00
m 238105a7da Merge pull request 'Polish web research answers and travel context' (#328) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m13s
2026-08-25 03:56:31 +00:00
m d7ca5f7e68 Polish web research answers and travel context
CI / test (push) Successful in 1m11s
CI / test (pull_request) Successful in 1m5s
2026-08-25 06:53:46 +03:00
m 827135a02d Merge pull request 'Keep identity reply consistent across response fields' (#327) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m15s
2026-08-25 03:44:38 +00:00
m a3ddf53207 Keep identity reply consistent across response fields
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m8s
2026-08-25 06:41:57 +03:00
m 35ac9015ad Merge pull request 'Remove temporary continuation diagnostics' (#326) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m16s
2026-08-25 03:34:41 +00:00
m 1c196ef675 Remove continuation diagnostics
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m4s
2026-08-25 06:31:30 +03:00
m 2ce3b5a9b3 Merge pull request 'Fix semantic routing for location follow-ups' (#325) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m13s
2026-08-25 03:26:06 +00:00
m 7421fd058d Fix semantic routing for location follow-ups
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m6s
2026-08-25 06:23:38 +03:00
m 368e9df551 Merge pull request 'Trace conversational continuation selection' (#324) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 03:17:08 +00:00
m e9629046cb Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m6s
2026-08-25 06:15:13 +03:00
m b5ccc419f6 Trace conversational continuation selection
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m4s
2026-08-25 06:13:19 +03:00
m 615723da2a Merge pull request 'Select latest relevant local search goal' (#323) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m11s
2026-08-25 03:10:44 +00:00
m d1bcd5beff Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m15s
2026-08-25 06:09:02 +03:00
m 519dc583a0 Select latest relevant local search goal
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m6s
2026-08-25 06:07:08 +03:00
m 0bf883cf56 Merge pull request 'Continue location follow-ups from recent context' (#322) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 03:04:26 +00:00
m c8502c735c Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m15s
2026-08-25 06:02:39 +03:00
m 48af97fbdd Continue location follow-ups from recent context
CI / test (push) Successful in 1m16s
CI / test (pull_request) Successful in 1m6s
2026-08-25 06:00:45 +03:00
m 13bcf87ea5 Merge pull request 'Continue local search from conversation context' (#321) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m12s
2026-08-25 02:57:39 +00:00
m e271fbb1e1 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m3s
2026-08-25 05:55:23 +03:00
m 5678686c57 Continue local search from conversation context
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m7s
2026-08-25 05:53:37 +03:00
m 9fc3fd1aa5 Merge pull request 'Ask for location before proximity search' (#320) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 02:49:13 +00:00
m e3a9c18cb3 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m13s
2026-08-25 05:47:27 +03:00
m 9534b041db Ask for location before proximity search
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m2s
2026-08-25 05:45:40 +03:00
m 9426fa40a9 Merge pull request 'Route independent chat actions separately' (#319) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m12s
2026-08-25 02:43:11 +00:00
m 51e2b23082 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m10s
2026-08-25 05:41:29 +03:00
m aece914fb8 Route independent chat actions separately
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m1s
2026-08-25 05:39:31 +03:00
m 762e312730 Merge pull request 'Keep research replies concise' (#318) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m10s
2026-08-25 02:36:16 +00:00
m e9eb529d6e Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m10s
2026-08-25 05:34:33 +03:00
m e39a2b7dda Keep research replies concise
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 59s
2026-08-25 05:32:42 +03:00
m cdc6baa4c0 Merge pull request 'Align semantic reply with research result' (#317) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m10s
2026-08-25 02:29:42 +00:00
m 255ab8dbf3 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m8s
2026-08-25 05:27:59 +03:00
m a76dc42bd8 Align semantic reply with research result
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m4s
2026-08-25 05:26:06 +03:00
m f0cbd71129 Merge pull request 'Preserve travel follow-up clarification' (#316) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m12s
2026-08-25 02:23:02 +00:00
m aa10c3e093 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m13s
2026-08-25 05:21:21 +03:00
m 0d836121d0 Preserve travel follow-up clarification
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m0s
2026-08-25 05:19:27 +03:00
m 6e8ecbf5e5 Merge pull request 'Avoid inheriting stale travel goals' (#315) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m13s
2026-08-25 02:15:15 +00:00
m 8088ff197e Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m12s
2026-08-25 05:13:32 +03:00
m 55ee967f2f Avoid inheriting stale travel goals
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m1s
2026-08-25 05:11:44 +03:00
m a56bd614bd Merge pull request 'Keep semantic clarification as final reply' (#314) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m7s
2026-08-25 02:09:13 +00:00
m 992e8470b2 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m10s
2026-08-25 05:07:42 +03:00
m fe2bd8493f Keep semantic clarification as final reply
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m3s
2026-08-25 05:05:46 +03:00
m e43820c5ce Merge pull request 'Preserve travel clarification reply' (#313) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 02:02:50 +00:00
m 662e30cb93 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m11s
CI / test (pull_request) Successful in 1m4s
2026-08-25 05:00:46 +03:00
m a51675e58d Preserve travel clarification reply
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m0s
2026-08-25 04:58:53 +03:00
m 820b1dc643 Merge pull request 'Keep travel requests in semantic dialogue' (#312) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 01:55:33 +00:00
m 94f2ae81c3 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m1s
2026-08-25 04:53:28 +03:00
m 9020100313 Keep travel requests in semantic dialogue
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m1s
2026-08-25 04:51:52 +03:00
m d097940692 Merge pull request 'Stop repeat cognition for stable replies' (#311) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 01:44:20 +00:00
m 28dfcc8aec Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m5s
2026-08-25 04:41:44 +03:00
m 7dd0c8e7a4 Stop repeat cognition for stable replies
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m4s
2026-08-25 04:38:46 +03:00
m bed21ad5c0 Merge pull request 'Trace final chat response selection' (#310) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 01:36:18 +00:00
m 2622140529 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 59s
2026-08-25 04:33:36 +03:00
m 3f9dc7a270 Trace final chat response selection
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m1s
2026-08-25 04:30:54 +03:00
m b5633c7fab Merge pull request 'Restore stable replies after cognitive routing' (#309) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 01:28:07 +00:00
m 5d053fedf4 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m1s
2026-08-25 04:25:36 +03:00
m d924694602 Restore stable replies after cognitive routing
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m2s
2026-08-25 04:23:00 +03:00
m cf8a48ae0d Merge pull request 'Preserve deterministic conversational drafts' (#308) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 01:19:57 +00:00
m 24e865aa07 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 1m0s
2026-08-25 04:17:28 +03:00
m 5d0dbbbe92 Preserve deterministic conversational drafts
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m2s
2026-08-25 04:14:57 +03:00
m f2277e35b4 Merge pull request 'Keep conversational replies semantically grounded' (#307) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 01:11:49 +00:00
m e8a642c5e2 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 58s
2026-08-25 04:09:21 +03:00
m 399bdea220 Keep conversational replies semantically grounded
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m5s
2026-08-25 04:06:45 +03:00
m 9a0205758b Merge pull request 'Preserve semantic identity during reply review' (#306) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 01:02:14 +00:00
m 0507e1af9c Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m11s
2026-08-25 03:59:40 +03:00
m f893744ed1 Preserve semantic identity during reply review
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m4s
2026-08-25 03:57:07 +03:00
m e455e89777 Merge pull request 'Review chat replies through cognitive gateway' (#305) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m12s
2026-08-25 00:54:05 +00:00
m 9b60da96b0 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m2s
2026-08-25 03:51:39 +03:00
m 34c0e7a0f4 Review chat replies through cognitive gateway
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:49:12 +03:00
m 550ce99bcd Merge pull request 'Prioritize personalized identity reply' (#304) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m11s
2026-08-25 00:40:40 +00:00
m e81411c4a4 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m4s
2026-08-25 03:37:21 +03:00
m 688a7e49db Prioritize personalized identity reply
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 59s
2026-08-25 03:34:16 +03:00
m 2ce997ca73 Merge pull request 'Protect personalized identity from audit drift' (#303) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 00:32:03 +00:00
m 075967c395 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 59s
2026-08-25 03:29:23 +03:00
m 1efd8bd25d Protect personalized identity from audit drift
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:26:38 +03:00
m e2bfd83481 Merge pull request 'Trace resolved assistant identity context' (#302) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m11s
2026-08-25 00:24:11 +00:00
m 2d5f0c69d0 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:21:39 +03:00
m 339159a194 Trace resolved assistant identity context
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:18:58 +03:00
m ad905e0bc5 Merge pull request 'Trace durable interaction history volume' (#301) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m10s
2026-08-25 00:16:53 +00:00
m 721817515a Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m2s
2026-08-25 03:14:28 +03:00
m 73de357c9f Trace durable interaction history volume
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:11:51 +03:00
m b1e635d00b Merge pull request 'Log interaction memory read failures' (#300) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 00:09:49 +00:00
m e489b5511a Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m2s
2026-08-25 03:06:44 +03:00
m 79409fb5c8 Log interaction memory read failures
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:04:10 +03:00
m e9bfe12293 Merge pull request 'Scope durable assistant memory to principal' (#299) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m25s
2026-08-25 00:01:17 +00:00
m 0833a7a0ca Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m4s
2026-08-25 02:58:47 +03:00
m dfecab7c34 Scope durable assistant memory to principal
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m1s
2026-08-25 02:56:00 +03:00
m 2eb57675c8 Merge pull request 'Always load durable user conversation context' (#298) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-24 23:53:42 +00:00
m 98f1454b3c Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 57s
2026-08-25 02:51:08 +03:00
m d274768bcc Always load durable user conversation context
CI / test (push) Successful in 1m0s
CI / test (pull_request) Failing after 15s
2026-08-25 02:48:36 +03:00
m ba31326302 Merge pull request 'Preserve personalized identity replies' (#297) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m11s
2026-08-24 23:46:12 +00:00
m f4e674fa9b Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 58s
2026-08-25 02:43:48 +03:00
m 1c7447b7e3 Preserve personalized identity replies
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 56s
2026-08-25 02:41:08 +03:00
m 2d24c011aa Merge pull request 'Reuse assistant name across conversations' (#296) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m6s
2026-08-24 23:38:47 +00:00
m b9b082b802 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m4s
2026-08-25 02:35:51 +03:00
m 74dd9922a5 Reuse assistant name across conversations
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m0s
2026-08-25 02:33:15 +03:00
m 4298b63d91 Merge pull request 'Keep explicit preferences on the local route' (#295) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m14s
2026-08-24 23:30:42 +00:00
m c48fba7d8d Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m6s
2026-08-25 02:27:48 +03:00
m 7b71216a05 Keep explicit preferences on the local route
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 59s
2026-08-25 02:25:06 +03:00
m 35204177e9 Merge pull request 'Remember assistant name preferences in chat' (#294) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m5s
2026-08-24 23:21:17 +00:00
m fc39278c91 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 58s
2026-08-25 02:18:43 +03:00
m eefd54137c Remember assistant name preferences in chat
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 59s
2026-08-25 02:16:15 +03:00
m 880cd055dd Merge pull request 'Route travel searches to current sources' (#293) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m3s
2026-08-24 23:09:32 +00:00
m ce07163541 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 59s
2026-08-25 02:07:14 +03:00
m 6bd9acac6e Route travel searches to current sources
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m5s
2026-08-25 02:04:48 +03:00
m 1be14da122 Merge pull request 'Allow cold local cognition to finish' (#292) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m4s
2026-08-24 23:01:20 +00:00
m 96a61a46d0 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m0s
2026-08-25 01:58:56 +03:00
m 4f4e43c953 Allow cold local cognition to finish
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 57s
2026-08-25 01:56:30 +03:00
m 306a35e294 Merge pull request 'Keep ordinary why questions conversational' (#291) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m7s
2026-08-24 22:42:13 +00:00
m 53a0bdb2e3 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m0s
2026-08-25 01:39:48 +03:00
m bf73dcd1ea Keep ordinary why questions conversational
CI / test (push) Successful in 56s
CI / test (pull_request) Successful in 1m3s
2026-08-25 01:37:23 +03:00
m 6409a9d442 Merge pull request 'Recognize identity from the original question' (#290) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m7s
2026-08-24 22:33:41 +00:00
m 96f96bd130 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 56s
2026-08-25 01:31:25 +03:00
m a8ce97c807 Recognize identity from the original question
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 58s
2026-08-25 01:29:03 +03:00
m 0804a69b4f Merge pull request 'Keep identity replies meaningful in fallback' (#289) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m7s
2026-08-24 22:27:03 +00:00
m e816b70f46 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 59s
2026-08-25 01:24:35 +03:00
m 8adcd86cad Keep identity replies meaningful in fallback
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 58s
2026-08-25 01:22:05 +03:00
m 9d2db45dd5 Merge pull request 'Trim research replies to essential sources' (#288) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m1s
2026-08-24 22:15:25 +00:00
m 111276fb22 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 59s
2026-08-25 01:13:05 +03:00
m cad4623232 Trim research replies to essential sources
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 58s
2026-08-25 01:10:39 +03:00
m ececb58025 Merge pull request 'Preserve meaning when cognitive model is unavailable' (#287) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 59s
2026-08-24 22:02:27 +00:00
m 4a9ba3b7d4 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 59s
2026-08-25 01:00:14 +03:00
m b029adfb43 Keep conversational fallback on the requested meaning
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m3s
2026-08-25 00:57:48 +03:00
m d0651474ab Merge pull request 'Simplify cognitive channel status replies' (#286) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m1s
2026-08-24 21:46:35 +00:00
m ca9db836de Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m2s
2026-08-25 00:44:06 +03:00
m 44ea97b938 Simplify cognitive channel status replies
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m1s
2026-08-25 00:41:31 +03:00
m 84f5ab6c47 Merge pull request 'Use natural capability inventory wording' (#285) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m6s
2026-08-24 21:28:10 +00:00
m 153372c2de Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 56s
CI / test (pull_request) Successful in 1m4s
2026-08-25 00:25:46 +03:00
m 7384b8922b Use natural capability inventory wording
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m1s
2026-08-25 00:23:00 +03:00
m 295523b470 Merge pull request 'Simplify model inventory responses' (#284) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m0s
2026-08-24 21:07:26 +00:00
m 0991a24739 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m0s
2026-08-25 00:05:03 +03:00
m 420c52b10e Simplify model inventory responses
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 57s
2026-08-25 00:02:04 +03:00
m 1e3ed210d7 Merge pull request 'Remove response length meta-text from Domovoy replies' (#283) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m0s
2026-08-24 20:45:21 +00:00
m 947a75b771 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m5s
2026-08-24 23:42:02 +03:00
m c84ddad051 Remove response length meta-text from Domovoy replies
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 57s
2026-08-24 23:39:11 +03:00
m 71b63ac6f9 Merge pull request 'Skip retry messages when resuming chat task' (#282) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 55s
2026-08-24 20:21:01 +00:00
m 659eb4ecfc Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 57s
2026-08-24 23:17:53 +03:00
m 2ff2c7db09 Skip retry messages when resuming chat task
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 1m2s
2026-08-24 23:15:04 +03:00
m 475e216ee0 Merge pull request 'Carry forward previous chat tasks on retry' (#281) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 58s
2026-08-24 19:56:43 +00:00
m 226056fb46 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 56s
CI / test (pull_request) Successful in 56s
2026-08-24 22:53:56 +03:00
m 580d18a41c Carry forward previous chat tasks on retry
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 59s
2026-08-24 22:51:21 +03:00
m 0abd6c2a11 Merge pull request 'Keep chat concise and route clarification research' (#280) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m3s
2026-08-24 19:28:03 +00:00
m def43ecfc3 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 53s
2026-08-24 22:25:42 +03:00
m c4bfcf032a fix: keep chat concise and route clarification research
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 56s
2026-08-24 22:23:18 +03:00
m 4a3a7d4812 Merge pull request 'Keep Domovoy identity in builtin fallback' (#279) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m4s
2026-08-24 18:59:40 +00:00
m dbe3e023ec Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 56s
2026-08-24 21:57:19 +03:00
m c63110ae7d fix: keep Domovoy identity in builtin fallback
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 57s
2026-08-24 21:54:50 +03:00
m 101f8beadb Merge pull request 'Strengthen Domovoy identity and web retrieval' (#278) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 56s
2026-08-24 18:48:27 +00:00
m b16925b386 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 54s
2026-08-24 21:46:07 +03:00
m be5a146cca fix: strengthen local assistant identity and web retrieval
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 58s
2026-08-24 21:43:43 +03:00
m 094fdcc51e Merge pull request 'Complete conversational research fallback' (#277) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 54s
2026-08-24 18:36:24 +00:00
m 307ad4a587 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 52s
2026-08-24 21:34:13 +03:00
m e867ba3c93 fix: complete conversational research fallback
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 59s
2026-08-24 21:31:34 +03:00
m c3027c5901 Merge pull request 'Fix production creator chat timeout' (#276) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 56s
Merge PR #276: fix production creator chat timeout
2026-08-24 18:05:38 +00:00
m 30bf2d6e67 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 54s
2026-08-24 21:03:28 +03:00
m 7fcab3bf31 fix: allow creator chat responses to complete
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 57s
2026-08-24 21:01:05 +03:00
m eca8475e29 Merge pull request 'Semantic chat orchestration with model profiles' (#275) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 55s
Merge PR #275: semantic chat route orchestration
2026-08-24 17:28:02 +00:00
m b340d46cdd Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 55s
2026-08-24 20:25:49 +03:00
m ac721216ea feat: add semantic chat route orchestration
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 55s
2026-08-24 20:23:17 +03:00
m e314d02212 Merge pull request 'Route conversational questions to local cognition' (#274) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 55s
Merge PR #274: route conversational questions to local cognition
2026-08-24 17:05:59 +00:00
m 6394e45bf2 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 55s
2026-08-24 20:03:35 +03:00
m ba5cc60c15 fix: route conversational questions to local cognition
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 55s
2026-08-24 20:00:52 +03:00
m a0fed9956c Merge pull request 'Keep local readiness responsive when optional models stall' (#273) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m1s
Merge PR #273: keep local readiness responsive
2026-08-24 16:39:41 +00:00
m 58e9c97d76 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 55s
2026-08-24 19:37:18 +03:00
m ba154e4ec2 fix: keep local readiness responsive
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 52s
2026-08-24 19:34:35 +03:00
m c4d7795280 Merge pull request 'Keep local lifeboat context for conversational fallback' (#272) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 53s
Merge PR #272: keep local lifeboat context for conversational fallback
2026-08-24 16:27:20 +00:00
m 25323d4ac1 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 53s
2026-08-24 19:24:53 +03:00
m 15133cd0fc fix: keep local chat context in lifeboat
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 50s
2026-08-24 19:22:25 +03:00
m 813554dc80 Merge pull request 'fix: keep model inventory on fast path' (#271) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 55s
Merge PR #271: keep structured model inventory on fast path
2026-08-24 16:07:52 +00:00
m bd8980bc3c Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 56s
CI / test (pull_request) Successful in 54s
2026-08-24 19:04:58 +03:00
m 62a3713e1e fix: keep structured model inventory on fast path
CI / test (push) Successful in 56s
CI / test (pull_request) Successful in 54s
2026-08-24 19:02:05 +03:00
m 1fb03de7be Merge pull request 'Semantic-first Domovoy chat routing and reflex drift audit' (#270) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m19s
Merge PR #270: semantic-first Domovoy chat routing and reflex drift audit
2026-08-24 15:46:56 +00:00
m fa92a10eb7 feat: prefer semantic interpretation and audit reflexes
CI / test (push) Successful in 53s
CI / test (pull_request) Successful in 1m16s
2026-08-24 18:34:27 +03:00
m bf20f247eb fix: route model inventory requests by meaning
CI / test (push) Successful in 1m2s
2026-08-24 18:30:39 +03:00
m 560b51e2f6 Merge pull request 'DEV-000291 Record authenticated mobile acceptance' (#269) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 57s
2026-08-24 11:05:01 +00:00
m d19fcceff3 docs: record authenticated mobile acceptance
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 54s
2026-08-24 14:02:47 +03:00
m 8c6f23dc49 Merge pull request 'DEV-000290 Fix mobile chat header overflow' (#268) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m5s
2026-08-24 10:59:52 +00:00
m 19c58b6d25 fix: constrain mobile chat header
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 51s
2026-08-24 13:57:39 +03:00
m e9d5b1ca8a Merge pull request 'DEV-000289 Fix authenticated chat mobile overflow' (#267) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 50s
2026-08-24 10:52:31 +00:00
m 444d9d825a fix: constrain chat layout on mobile
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 54s
2026-08-24 13:50:16 +03:00
m a2ff01050e Merge pull request 'DEV-000288 Refresh production evidence revision' (#266) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 57s
2026-08-24 10:40:57 +00:00
m 443e0ceb2e docs: refresh production evidence revision
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 52s
2026-08-24 13:38:42 +03:00
m 3e91ae503c Merge pull request 'DEV-000287 Reconcile live audit evidence' (#265) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 54s
2026-08-24 10:33:25 +00:00
m bf6f2365d4 Merge remote-tracking branch 'origin/main' into codex/dev-000248-self-development-runtime-status
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 53s
2026-08-24 13:30:52 +03:00
m 9b7091bbe9 docs: reconcile live audit evidence
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 55s
2026-08-24 13:28:26 +03:00
m 12725511c0 Merge pull request 'DEV-000286 Reject irrelevant research excerpts' (#264) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m8s
2026-08-24 08:51:04 +00:00
m 3c46a5f07f fix: reject irrelevant research excerpts
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 53s
2026-08-24 06:40:31 +03:00
m b0a8b3e5a7 Merge pull request 'DEV-000285 Route source-dependent questions to research' (#263) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 50s
2026-08-24 03:32:34 +00:00
m f6f68ffb04 fix: route source-dependent questions to research
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 53s
2026-08-24 06:29:56 +03:00
m 5c98bc3568 Merge pull request 'DEV-000284 Record live generic graph synthesis evidence' (#262) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 57s
2026-08-24 01:59:32 +00:00
m de1d2f5107 docs: record live generic graph synthesis evidence
CI / test (push) Successful in 52s
CI / test (pull_request) Successful in 52s
2026-08-24 04:56:50 +03:00
m bcbd41f217 Merge pull request 'DEV-000283 Verify generic Work Graph candidate envelopes' (#261) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 52s
2026-08-24 01:41:09 +00:00
m f5616fe3ea feat: verify generic work graph candidate envelopes
CI / test (push) Successful in 53s
CI / test (pull_request) Successful in 52s
2026-08-24 04:38:40 +03:00
m 94baf98060 Merge pull request 'DEV-000282 Record final Portainer auto-deploy evidence' (#260) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 53s
2026-08-24 01:32:19 +00:00
m c499ae090e docs: record final Portainer auto-deploy evidence
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 55s
2026-08-24 04:29:20 +03:00
m e132ec6ba9 Merge pull request 'DEV-000281 Reconcile live generic execution evidence' (#259) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 54s
2026-08-24 00:38:54 +00:00
m 344f8ad106 docs: reconcile live generic execution evidence
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 54s
2026-08-24 03:36:06 +03:00
m 080bd44021 Merge pull request 'DEV-000280 Add local generic Agent Execution' (#258) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m16s
2026-08-24 00:01:15 +00:00
m 318a68c3dd chore: activate generic agent execution task
CI / test (push) Successful in 50s
CI / test (pull_request) Successful in 50s
2026-08-24 02:58:40 +03:00
m 73d7ffd012 feat: execute generic local work graph candidates
CI / test (push) Failing after 37s
CI / test (pull_request) Failing after 35s
2026-08-24 02:56:19 +03:00
m 51db6377a4 Merge pull request 'DEV-000279 Reconcile current audit production evidence' (#257) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 51s
2026-08-23 23:46:18 +00:00
m c3b4283101 docs: reconcile current audit production evidence
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 50s
2026-08-24 02:42:58 +03:00
m ffd7ddcce5 Merge pull request 'DEV-000278 Reconcile live research evidence' (#256) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 51s
2026-08-23 23:38:24 +00:00
m dd83442ff9 docs: reconcile live research evidence
CI / test (push) Successful in 53s
CI / test (pull_request) Successful in 53s
2026-08-24 02:36:08 +03:00
m f1c3abb3b7 Merge pull request 'DEV-000277 Parse current research provider markup' (#255) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 52s
2026-08-23 23:25:30 +00:00
m a56957138d fix: parse current research provider markup
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 53s
2026-08-24 02:23:07 +03:00
m 5058d5ac0e Merge pull request 'DEV-000276 Use available bounded research search provider' (#254) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 53s
2026-08-23 23:17:23 +00:00
m 00d7f70ffb fix: use available bounded research search provider
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 53s
2026-08-24 02:15:03 +03:00
m c09be4c9b5 Merge pull request 'DEV-000275 Semantic web research routing' (#253) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 53s
2026-08-23 23:07:13 +00:00
m 976edccfd8 feat: route factual questions through bounded research
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 53s
2026-08-24 02:04:57 +03:00
m 1eb3e39286 Merge pull request 'DEV-000274 Truth-first chat and intent-aware model routing' (#252) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 53s
2026-08-23 22:51:07 +00:00
m 15088d1d7d Merge remote-tracking branch 'origin/main' into codex/dev-000248-self-development-runtime-status
CI / test (push) Successful in 49s
CI / test (pull_request) Successful in 52s
2026-08-24 01:48:49 +03:00
m 4cdd1a5276 fix: route model mentions by intent
CI / test (push) Successful in 50s
CI / test (pull_request) Successful in 49s
2026-08-24 01:46:34 +03:00
m e93f810946 fix: require verification before factual chat answers
CI / test (push) Successful in 47s
2026-08-24 01:42:41 +03:00
m 459d836468 Merge pull request 'DEV-000273 reconcile live audit evidence' (#251) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 59s
2026-08-23 22:39:40 +00:00
m 52058459f6 Merge remote-tracking branch 'origin/main' into codex/dev-000248-self-development-runtime-status
CI / test (push) Successful in 48s
CI / test (pull_request) Successful in 48s
2026-08-24 01:36:20 +03:00
m 9b91be0764 docs: reconcile live audit evidence
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 50s
2026-08-24 01:31:24 +03:00
m e4f8d6fa70 Merge pull request 'DEV-000270 local-first model scope' (#250) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m3s
2026-08-23 22:19:43 +00:00
m d7151f1d78 Merge remote-tracking branch 'origin/main' into codex/dev-000248-self-development-runtime-status
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 49s
2026-08-24 01:17:31 +03:00
m d357e7714a docs: define local-first model scope
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 52s
2026-08-24 01:15:00 +03:00
m 9160654a07 Merge pull request 'DEV-000268 fresh GPU timeout evidence' (#249) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 55s
2026-08-23 22:06:28 +00:00
m 29dad403ec docs: record gpu timeout after bounded retry
CI / test (push) Successful in 52s
CI / test (pull_request) Successful in 1m1s
2026-08-24 01:03:52 +03:00
m 041c1ce849 Merge pull request 'DEV-000268 configurable GPU provider timeout' (#248) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m0s
2026-08-23 21:59:45 +00:00
m ba162f6433 fix: make gpu provider timeout configurable
CI / test (push) Successful in 48s
CI / test (pull_request) Successful in 53s
2026-08-24 00:57:24 +03:00
m 3ca7ffab16 Merge pull request 'DEV-000267 refresh GPU and UI gap evidence' (#247) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 50s
2026-08-23 21:49:18 +00:00
m 615a3e4765 docs: refresh gpu and ui gap evidence
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 49s
2026-08-24 00:47:02 +03:00
m c459cd563c Merge pull request 'DEV-000266 live Agent Execution bridge evidence' (#246) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 50s
2026-08-23 21:40:02 +00:00
m b2753a64c4 Merge remote-tracking branch 'origin/main' into codex/dev-000248-self-development-runtime-status
CI / test (push) Successful in 48s
CI / test (pull_request) Successful in 49s
2026-08-24 00:37:49 +03:00
m 649dd602dd docs: record live agent execution bridge
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 51s
2026-08-24 00:34:58 +03:00
m 7a004e524a Merge pull request 'DEV-000266 local Agent Execution bridge' (#245) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m6s
2026-08-23 21:32:10 +00:00
m cb78c35f24 Merge remote-tracking branch 'origin/main' into codex/dev-000248-self-development-runtime-status
CI / test (push) Successful in 49s
CI / test (pull_request) Successful in 49s
2026-08-24 00:29:32 +03:00
m b1a79ede91 feat: add local agent execution bridge
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 49s
2026-08-24 00:27:10 +03:00
m e012694108 Merge pull request 'DEV-000265 local agent readiness evidence' (#244) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 51s
2026-08-23 21:16:09 +00:00
m c4f3f02933 docs(evidence): record local agent readiness
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 1m0s
2026-08-24 00:13:37 +03:00
m 102a57aaff Merge pull request 'DEV-000264 capability-aware agent worker readiness' (#243) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 51s
2026-08-23 21:11:33 +00:00
m 381e4d52e8 fix(agent-worker): report local capability readiness
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 1m18s
2026-08-24 00:08:18 +03:00
m c8dc9edbdc Merge pull request 'DEV-000263 record GPU qualification timeout' (#242) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 51s
2026-08-23 21:05:54 +00:00
m a0b0b32f0a docs(evidence): record gpu qualification timeout
CI / test (push) Successful in 50s
CI / test (pull_request) Successful in 51s
2026-08-24 00:03:29 +03:00
m 1f2068512e Merge pull request 'DEV-000262 additional evaluation runtime live evidence' (#241) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m16s
2026-08-23 20:57:34 +00:00
m b97c910434 docs(evidence): record additional evaluation runtime
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 50s
2026-08-23 23:55:09 +03:00
m b789878d01 Merge pull request 'DEV-000261 expose additional evaluation scheduling endpoint' (#240) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 50s
2026-08-23 20:52:29 +00:00
m 705e4a8ed6 Merge remote-tracking branch 'origin/main' into codex/dev-000248-self-development-runtime-status
CI / test (push) Successful in 52s
CI / test (pull_request) Successful in 49s
2026-08-23 23:49:33 +03:00
m 531dfa85ad feat(work): expose additional evaluation scheduling
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 49s
2026-08-23 23:45:45 +03:00
m 6e215b5b57 Merge pull request 'DEV-000261 durable additional task evaluation scheduling' (#239) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 54s
2026-08-23 20:42:20 +00:00
m d46c0cf719 chore(governance): align additional evaluation task base
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 49s
2026-08-23 23:38:32 +03:00
m 3eecc710ed Merge remote-tracking branch 'origin/main' into codex/dev-000248-self-development-runtime-status 2026-08-23 23:37:57 +03:00
m 1b4284592b feat(work): schedule bounded additional evaluations 2026-08-23 23:37:56 +03:00
m d2dd38b9ae Merge pull request 'DEV-000260 deep authenticated acceptance evidence' (#238) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 49s
2026-08-23 19:45:54 +00:00
m fb6b5e9677 Merge remote-tracking branch 'origin/main' into codex/dev-000248-self-development-runtime-status
CI / test (push) Successful in 53s
CI / test (pull_request) Successful in 58s
2026-08-23 22:43:57 +03:00
m 40735ce9c1 docs(acceptance): record deep authenticated evidence
CI / test (push) Successful in 47s
CI / test (pull_request) Successful in 49s
2026-08-23 22:41:26 +03:00
m 9f40c6a793 Merge pull request 'docs(governance): refresh post-responsive evidence' (#237) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m0s
2026-08-23 19:18:35 +00:00
m a9dbc3b624 docs(governance): refresh post-responsive evidence
CI / test (push) Successful in 52s
CI / test (pull_request) Successful in 49s
2026-08-23 22:16:20 +03:00
m d4b5416126 Merge pull request 'test(creator-web): guard responsive console layout' (#236) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 59s
2026-08-23 19:12:44 +00:00
m 7c4981ea09 test(creator-web): guard responsive console layout
CI / test (push) Successful in 53s
CI / test (pull_request) Successful in 50s
2026-08-23 22:10:31 +03:00
m 5e8286695f Merge pull request 'docs(governance): record operations acceptance smoke' (#235) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m0s
2026-08-23 19:05:31 +00:00
m 2b11baa170 docs(governance): record operations acceptance smoke
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 51s
2026-08-23 22:03:12 +03:00
m 8868258afd Merge pull request 'feat(capability): expose reachability rating evidence' (#234) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 57s
2026-08-23 18:57:40 +00:00
m 90cb2b1590 feat(capability): expose reachability rating evidence
CI / test (push) Successful in 50s
CI / test (pull_request) Successful in 52s
2026-08-23 21:55:13 +03:00
m 3d97b348f7 Merge pull request 'docs(governance): refresh live audit status' (#233) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 58s
2026-08-23 18:50:58 +00:00
m 3eea9e58cd docs(governance): refresh live audit status
CI / test (push) Successful in 49s
CI / test (pull_request) Successful in 49s
2026-08-23 21:48:43 +03:00
m 12ab124411 Merge pull request 'feat(governance): add development gate evidence transitions' (#232) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m0s
2026-08-23 18:40:44 +00:00
m 0886342031 feat(governance): add development gate evidence transitions
CI / test (push) Successful in 47s
CI / test (pull_request) Successful in 47s
2026-08-23 21:38:46 +03:00
m 3bfe76278b Merge pull request 'feat(governance): connect verifier to DevelopmentGate' (#231) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 56s
2026-08-23 18:32:28 +00:00
m 067facdfa4 feat(governance): connect verifier to development gate
CI / test (push) Successful in 47s
CI / test (pull_request) Successful in 52s
2026-08-23 21:29:53 +03:00
m f2e520e31e Merge pull request 'feat(governance): add local bounded verification executor' (#230) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m1s
2026-08-23 18:23:18 +00:00
m cf91807fab feat(governance): add local bounded verification executor
CI / test (push) Successful in 48s
CI / test (pull_request) Successful in 50s
2026-08-23 21:21:06 +03:00
m c925089fd2 Merge pull request 'fix(reflection): preserve cognition-required candidates' (#229) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 57s
2026-08-23 18:09:09 +00:00
m a2d0a3bf19 fix(reflection): preserve cognition-required candidates
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 48s
2026-08-23 21:07:02 +03:00
m c1cde67bcf Merge pull request 'fix(work-graph): preserve scheduled task evaluation revision' (#228) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 56s
2026-08-23 18:01:18 +00:00
m aeafc6e0ac fix(work-graph): preserve scheduled task evaluation revision
CI / test (push) Successful in 50s
CI / test (pull_request) Successful in 49s
2026-08-23 20:58:50 +03:00
m 1f5569db15 chore(governance): activate task evaluation callback repair 2026-08-23 20:57:52 +03:00
m 2b8cea5aa8 Merge pull request 'fix(cognitive): fail over task evaluation candidates' (#227) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 55s
2026-08-23 17:54:12 +00:00
m 7b09b3b2ec fix(cognitive): fail over task evaluation candidates
CI / test (push) Successful in 45s
CI / test (pull_request) Successful in 46s
2026-08-23 20:52:09 +03:00
m 9cc5101a30 Merge pull request 'test(work-graph): lock task evaluation wire contract' (#226) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 1m6s
2026-08-23 17:37:45 +00:00
m f90ed7af35 test(work-graph): lock task evaluation wire contract
CI / test (push) Successful in 50s
CI / test (pull_request) Successful in 50s
2026-08-23 20:35:27 +03:00
m 6f61601d77 Merge pull request 'fix(work-graph): diagnose task evaluation scheduling' (#225) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 54s
2026-08-23 17:19:14 +00:00
m 259e7343e3 fix(work-graph): log task evaluation scheduling rejection
CI / test (push) Successful in 50s
CI / test (pull_request) Successful in 51s
2026-08-23 20:17:08 +03:00
m cdc72e17be Merge pull request 'feat(research): expose truth runtime status' (#224) from codex/dev-000247-research-runtime-status into main
CI / test (push) Successful in 58s
2026-08-23 17:05:32 +00:00
m 931a0b0f0c feat(research): expose truth runtime status
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 47s
2026-08-23 20:03:14 +03:00
m e70be0103a Merge pull request 'feat(work-graph): expose runtime status' (#223) from codex/dev-000246-work-graph-runtime-status into main
CI / test (push) Successful in 59s
2026-08-23 16:59:37 +00:00
353 changed files with 32080 additions and 528 deletions
+13
View File
@@ -72,3 +72,16 @@ State:
- assumptions;
- findings not changed;
- gaps or limitations.
## Domovoy Gen2 execution governance
When the active task is a Gen2 packet, read the canonical documents before editing:
- `docs/DOMOVOY_CONSTITUTION.md`
- `architecture/GEN2_TARGET_ARCHITECTURE.md`
- `architecture/GEN2_CANONICAL_DATA_MODEL.md`
- `development/GEN2_MIGRATION_MAP.md`
- `development/GEN2_MASTER_PLAN.md`
- `development/GEN2_EXECUTION_CONTRACT.md`
The active task packet and `development/GEN2_TASK_INDEX.md` define execution order. `PRE-GEN2-000` freezes the V1 baseline; `ARCH-GEN2-000` accepts Gen2 governance; later `GEN2-*` packets must not be started automatically. Gen2 architecture and ownership changes are permitted only within the active packet's declared scope and authority. Default validation is FAST; MILESTONE or FULL is required only when the selected packet explicitly says so.
+197
View File
@@ -0,0 +1,197 @@
// Package developmentexecutor contains the local, read-only verification
// adapter for governed development candidates. It is deliberately not a
// source mutator, promotion authority, secret resolver, or deployment tool.
package developmentexecutor
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
type CheckKind string
const (
CheckGoTest CheckKind = "GO_TEST"
CheckGoVet CheckKind = "GO_VET"
CheckGitDiffCheck CheckKind = "GIT_DIFF_CHECK"
)
type Check struct {
Kind CheckKind
Packages []string
}
type Plan struct {
Worktree string
WorktreeID string
BaseRepoRevision string
ProtectedMainAccess string
ProductionAccess string
SecretAccess string
Checks []Check
}
type CheckResult struct {
Kind CheckKind `json:"kind"`
Passed bool `json:"passed"`
ExitCode int `json:"exit_code"`
DurationMillis int64 `json:"duration_millis"`
}
type Result struct {
WorktreeID string `json:"worktree_id"`
BaseRepoRevision string `json:"base_repo_revision"`
Passed bool `json:"passed"`
Checks []CheckResult `json:"checks"`
EvidenceRef string `json:"evidence_ref"`
}
var (
ErrInvalidPlan = errors.New("invalid governed verification plan")
ErrUnsupportedCheck = errors.New("unsupported governed verification check")
ErrProtectedWorktree = errors.New("protected worktree is not executable")
)
type CommandRunner interface {
Run(context.Context, string, string, []string) int
}
type OSCommandRunner struct{}
func (OSCommandRunner) Run(ctx context.Context, dir, name string, args []string) int {
command := exec.CommandContext(ctx, name, args...)
command.Dir = dir
command.Stdin = nil
command.Stdout = io.Discard
command.Stderr = io.Discard
command.Env = safeEnvironment()
if err := command.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return exitErr.ExitCode()
}
return -1
}
return 0
}
func ValidatePlan(plan Plan) error {
if strings.TrimSpace(plan.Worktree) == "" || strings.TrimSpace(plan.WorktreeID) == "" || strings.TrimSpace(plan.BaseRepoRevision) == "" || len(plan.Checks) == 0 {
return ErrInvalidPlan
}
if !filepath.IsAbs(plan.Worktree) {
return ErrInvalidPlan
}
if plan.ProtectedMainAccess != "DENIED" || plan.ProductionAccess != "DENIED" || plan.SecretAccess != "DENIED" {
return ErrProtectedWorktree
}
switch strings.ToLower(filepath.Base(filepath.Clean(plan.WorktreeID))) {
case "main", "protected-main", "production":
return ErrProtectedWorktree
}
for _, check := range plan.Checks {
if err := validateCheck(check); err != nil {
return err
}
}
return nil
}
func validateCheck(check Check) error {
switch check.Kind {
case CheckGoTest, CheckGoVet:
if len(check.Packages) == 0 {
return ErrInvalidPlan
}
for _, packagePath := range check.Packages {
if strings.TrimSpace(packagePath) == "" || strings.ContainsAny(packagePath, ";&|`$()<>\r\n") || unsafePackagePath(packagePath) {
return ErrInvalidPlan
}
}
case CheckGitDiffCheck:
if len(check.Packages) != 0 {
return ErrInvalidPlan
}
default:
return ErrUnsupportedCheck
}
return nil
}
func unsafePackagePath(value string) bool {
value = strings.ReplaceAll(value, "\\", "/")
for _, part := range strings.Split(value, "/") {
if part == ".." {
return true
}
}
return strings.HasPrefix(value, "../") || strings.HasPrefix(value, "/") || (len(value) > 1 && value[1] == ':')
}
func Execute(ctx context.Context, plan Plan, runner CommandRunner) (Result, error) {
if err := ValidatePlan(plan); err != nil {
return Result{}, err
}
if runner == nil {
runner = OSCommandRunner{}
}
if _, err := os.Stat(plan.Worktree); err != nil {
return Result{}, fmt.Errorf("worktree unavailable: %w", err)
}
result := Result{WorktreeID: plan.WorktreeID, BaseRepoRevision: plan.BaseRepoRevision, Passed: true, Checks: make([]CheckResult, 0, len(plan.Checks))}
for _, check := range plan.Checks {
started := time.Now()
name, args := commandFor(check)
exitCode := runner.Run(ctx, plan.Worktree, name, args)
passed := exitCode == 0
result.Checks = append(result.Checks, CheckResult{Kind: check.Kind, Passed: passed, ExitCode: exitCode, DurationMillis: time.Since(started).Milliseconds()})
if !passed {
result.Passed = false
}
if ctx.Err() != nil {
result.Passed = false
break
}
}
result.EvidenceRef = evidenceRef(result)
return result, nil
}
func commandFor(check Check) (string, []string) {
switch check.Kind {
case CheckGoTest:
return "go", append([]string{"test"}, check.Packages...)
case CheckGoVet:
return "go", append([]string{"vet"}, check.Packages...)
default:
return "git", []string{"diff", "--check"}
}
}
func evidenceRef(result Result) string {
hash := sha256.New()
_, _ = fmt.Fprintf(hash, "%s|%s|%t|", result.WorktreeID, result.BaseRepoRevision, result.Passed)
for _, check := range result.Checks {
_, _ = fmt.Fprintf(hash, "%s|%t|%d|", check.Kind, check.Passed, check.ExitCode)
}
return "verification:" + hex.EncodeToString(hash.Sum(nil))
}
func safeEnvironment() []string {
allowed := []string{"PATH", "SystemRoot", "TEMP", "TMP", "GOROOT", "GOPATH", "GOMODCACHE"}
result := make([]string, 0, len(allowed))
for _, name := range allowed {
if value, ok := os.LookupEnv(name); ok && value != "" {
result = append(result, name+"="+value)
}
}
return result
}
@@ -0,0 +1,70 @@
package developmentexecutor
import (
"context"
"errors"
"path/filepath"
"testing"
)
type fakeRunner struct {
calls []string
exits []int
}
func (r *fakeRunner) Run(_ context.Context, _ string, name string, args []string) int {
call := name
for _, arg := range args {
call += " " + arg
}
r.calls = append(r.calls, call)
if len(r.exits) == 0 {
return 0
}
exit := r.exits[0]
r.exits = r.exits[1:]
return exit
}
func validPlan(t *testing.T) Plan {
t.Helper()
worktree, err := filepath.Abs(".")
if err != nil {
t.Fatal(err)
}
return Plan{Worktree: worktree, WorktreeID: "sandbox-1", BaseRepoRevision: "revision-1", ProtectedMainAccess: "DENIED", ProductionAccess: "DENIED", SecretAccess: "DENIED", Checks: []Check{{Kind: CheckGoTest, Packages: []string{"./modules/work/..."}}, {Kind: CheckGoVet, Packages: []string{"./modules/work/..."}}, {Kind: CheckGitDiffCheck}}}
}
func TestExecuteRunsOnlyTypedChecksAndReturnsEvidence(t *testing.T) {
runner := &fakeRunner{}
result, err := Execute(context.Background(), validPlan(t), runner)
if err != nil || !result.Passed || len(result.Checks) != 3 || result.EvidenceRef == "" {
t.Fatalf("result=%+v err=%v", result, err)
}
if len(runner.calls) != 3 || runner.calls[0] != "go test ./modules/work/..." || runner.calls[2] != "git diff --check" {
t.Fatalf("calls=%v", runner.calls)
}
}
func TestExecuteFailureCannotBecomeVerificationSuccess(t *testing.T) {
runner := &fakeRunner{exits: []int{1, 0}}
plan := validPlan(t)
plan.Checks = []Check{{Kind: CheckGoTest, Packages: []string{"./..."}}, {Kind: CheckGitDiffCheck}}
result, err := Execute(context.Background(), plan, runner)
if err != nil || result.Passed || result.Checks[0].Passed || result.EvidenceRef == "" {
t.Fatalf("failed result was overclaimed: %+v err=%v", result, err)
}
}
func TestValidatePlanRejectsProtectedAndShellLikeInputs(t *testing.T) {
protected := validPlan(t)
protected.WorktreeID = "production"
if !errors.Is(ValidatePlan(protected), ErrProtectedWorktree) {
t.Fatal("protected worktree accepted")
}
injection := validPlan(t)
injection.Checks = []Check{{Kind: CheckGoTest, Packages: []string{"./...; echo leaked"}}}
if !errors.Is(ValidatePlan(injection), ErrInvalidPlan) {
t.Fatal("shell-like package accepted")
}
}
+26 -3
View File
@@ -25,6 +25,13 @@ type OpenAICompatibleLocalAdapter struct {
apiKey string
}
// compatibleDefaultContextBytes is a conservative bound used when an
// OpenAI-compatible local endpoint omits n_ctx from /v1/models. The adapter
// is a chat-completions adapter, so advisory capability is known from the
// adapter contract; quality and activation still require governed
// qualification/evaluation in the capability registry.
const compatibleDefaultContextBytes int64 = 32 << 10
func NewOpenAICompatibleLocalAdapter(raw string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
return NewOpenAICompatibleLocalAdapterWithAPIKey(raw, "", transport)
}
@@ -90,7 +97,10 @@ func (a *OpenAICompatibleLocalAdapter) Discover(ctx context.Context) ([]Discover
stableMetadata := fmt.Sprintf("%s\x00%s\x00%d\x00%d\x00%d\x00%d\x00%s", item.ID, item.OwnedBy, item.Meta.Context, item.Meta.TrainingContext, item.Meta.Parameters, item.Meta.Size, item.Meta.FileType)
digest := sha256.Sum256([]byte(stableMetadata))
contextBytes := item.Meta.Context * 4
models = append(models, DiscoveredModel{ProviderModelID: capability.ProviderModelID(item.ID), Fingerprint: capability.ModelFingerprint(hex.EncodeToString(digest[:])), MaxContextBytes: contextBytes, Modalities: []capability.ModelModality{capability.ModalityText}, Capabilities: []capability.ModelCapability{capability.CapabilityUnknown}, SizeBytes: item.Meta.Size, Runtime: item.OwnedBy, Quantization: item.Meta.FileType})
if contextBytes <= 0 {
contextBytes = compatibleDefaultContextBytes
}
models = append(models, DiscoveredModel{ProviderModelID: capability.ProviderModelID(item.ID), Fingerprint: capability.ModelFingerprint(hex.EncodeToString(digest[:])), MaxContextBytes: contextBytes, Modalities: []capability.ModelModality{capability.ModalityText}, Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, SizeBytes: item.Meta.Size, Runtime: item.OwnedBy, Quantization: item.Meta.FileType})
}
return models, ProviderProbe{Health: capability.HealthAvailable}
}
@@ -122,10 +132,10 @@ type compatibleChatResponse struct {
}
func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" || len(request.Context) != 0 {
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" {
return cognitive.AdvisoryResponse{}, ErrInvalidAdvisory
}
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Provide non-canonical advisory text only. Never claim that an action occurred."}, {Role: "user", Content: request.Message}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Ты отвечаешь от имени Домового — локального помощника пользователя. Не называй себя именем модели, поставщиком или внутренним runtime. Если спрашивают имя, ответь: «Меня зовут Домовой». Понимай смысл сообщения и помогай довести цель до полезного результата. По умолчанию отвечай кратко, примерно одним-тремя предложениями; подробности давай только по просьбе. Отвечай только некатегоричным текстом совета; не утверждай, что внешнее действие уже выполнено."}, {Role: "user", Content: localAdvisoryMessage(request)}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
var result compatibleChatResponse
status, err := a.requestJSON(ctx, http.MethodPost, "/chat/completions", payload, &result)
if err != nil {
@@ -151,6 +161,19 @@ func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cogni
return response, nil
}
func localAdvisoryMessage(request cognitive.AdvisoryRequest) string {
if len(request.Context) == 0 {
return request.Message
}
var builder strings.Builder
builder.WriteString(request.Message)
builder.WriteString("\n\nRelevant local context:\n")
for _, excerpt := range request.Context {
fmt.Fprintf(&builder, "- [%s/%s] %s\n", excerpt.SourceKind, excerpt.SourceID, excerpt.Text)
}
return builder.String()
}
func (a *OpenAICompatibleLocalAdapter) requestJSON(ctx context.Context, method, path string, input, output any) (int, error) {
var body io.Reader
if input != nil {
@@ -18,6 +18,34 @@ type captureRoundTripper struct {
body []byte
}
type modelsMetadataRoundTripper struct{}
func (modelsMetadataRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"data":[{"id":"medium-model","owned_by":"llama.cpp","meta":{}}]}`)),
Header: make(http.Header),
Request: request,
}, nil
}
func TestOpenAICompatibleLocalAdapterNormalizesMinimalModelMetadata(t *testing.T) {
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", modelsMetadataRoundTripper{})
if err != nil {
t.Fatal(err)
}
models, probe := adapter.Discover(context.Background())
if probe.Health != capability.HealthAvailable || len(models) != 1 {
t.Fatalf("unexpected discovery result: probe=%+v models=%+v", probe, models)
}
if models[0].MaxContextBytes != compatibleDefaultContextBytes {
t.Fatalf("context bound=%d, want %d", models[0].MaxContextBytes, compatibleDefaultContextBytes)
}
if len(models[0].Capabilities) != 1 || models[0].Capabilities[0] != capability.CapabilityAdvisory {
t.Fatalf("capabilities=%v, want advisory", models[0].Capabilities)
}
}
func TestOpenAICompatibleLocalAdapterUsesConfiguredTimeout(t *testing.T) {
adapter, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", &captureRoundTripper{}, 47*time.Second)
if err != nil {
@@ -92,3 +120,32 @@ func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(
t.Fatal("expected bounded advisory to disable template thinking")
}
}
func TestOpenAICompatibleLocalAdapterPassesLocalContextToAdvisory(t *testing.T) {
transport := &captureRoundTripper{}
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", transport)
if err != nil {
t.Fatal(err)
}
_, err = adapter.Advise(context.Background(), cognitive.AdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1, RequestID: "request-context", ActorID: "actor", PrincipalID: "principal", ScopeID: "scope",
Message: "Продолжи разговор", Locale: "ru-RU", MaxOutputBytes: 256, ProviderBindingID: "binding-1", ProviderBindingRevision: 1,
ProviderInstanceID: "provider-1", ModelDescriptorID: "model-1", ProviderModelID: capability.ProviderModelID("local-model"),
Context: []cognitive.ContextExcerpt{{ID: "focus", SourceKind: "situation", SourceID: "focus-1", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: "Активный проект: Домовой"}},
})
if err != nil {
t.Fatal(err)
}
var payload struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
}
if err := json.Unmarshal(transport.body, &payload); err != nil {
t.Fatal(err)
}
if len(payload.Messages) != 2 || !strings.Contains(payload.Messages[1].Content, "Активный проект: Домовой") {
t.Fatalf("local context was not passed to advisory: %+v", payload.Messages)
}
}
+509
View File
@@ -0,0 +1,509 @@
package multimodal
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"strings"
"time"
)
var ErrEmptyResult = errors.New("multimodal provider returned an empty result")
type SpeechClient struct {
BaseURL string
Client *http.Client
}
type VisionClient struct {
BaseURL string
Client *http.Client
}
type ImageGenerationClient struct {
BaseURL string
Client *http.Client
}
type VideoUnderstandingClient struct {
BaseURL string
Client *http.Client
}
type GeneratedImage struct {
DataBase64 string
SourceURL string
ContentType string
}
type ImageGenerator interface {
Generate(ctx context.Context, model, prompt, size string) (GeneratedImage, error)
}
// ComfyImageGenerationClient is the replaceable transport for a ComfyUI
// image-generation skill. The workflow is deliberately kept in this adapter:
// the core only selects a capability and supplies the user's prompt.
type ComfyImageGenerationClient struct {
BaseURL string
Client *http.Client
PollInterval time.Duration
PollTimeout time.Duration
}
func (c SpeechClient) Transcribe(ctx context.Context, filename, contentType string, data []byte) (string, error) {
if len(data) == 0 {
return "", errors.New("audio payload is empty")
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
header := make(textproto.MIMEHeader)
header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, strings.ReplaceAll(filename, `"`, "")))
header.Set("Content-Type", contentType)
part, err := writer.CreatePart(header)
if err != nil {
return "", err
}
if _, err := part.Write(data); err != nil {
return "", err
}
if err := writer.WriteField("response_format", "json"); err != nil {
return "", err
}
if err := writer.Close(); err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/inference", body)
if err != nil {
return "", err
}
request.Header.Set("Content-Type", writer.FormDataContentType())
response, err := c.client().Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("speech provider status %d", response.StatusCode)
}
var result struct {
Text string `json:"text"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&result); err != nil {
return "", err
}
if strings.TrimSpace(result.Text) == "" {
return "", ErrEmptyResult
}
return strings.TrimSpace(result.Text), nil
}
func (c VisionClient) Describe(ctx context.Context, model, contentType string, data []byte, instruction string) (string, error) {
if len(data) == 0 {
return "", errors.New("image payload is empty")
}
payload := struct {
Model string `json:"model"`
Messages []struct {
Role string `json:"role"`
Content []struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL *struct {
URL string `json:"url"`
} `json:"image_url,omitempty"`
} `json:"content"`
} `json:"messages"`
MaxTokens int `json:"max_tokens"`
}{}
payload.Model = model
payload.MaxTokens = 1024
payload.Messages = append(payload.Messages, struct {
Role string `json:"role"`
Content []struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL *struct {
URL string `json:"url"`
} `json:"image_url,omitempty"`
} `json:"content"`
}{Role: "user", Content: []struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL *struct {
URL string `json:"url"`
} `json:"image_url,omitempty"`
}{
{Type: "text", Text: instruction},
{Type: "image_url", ImageURL: &struct {
URL string `json:"url"`
}{URL: "data:" + contentType + ";base64," + encodeBase64(data)},
}}})
encoded, err := json.Marshal(payload)
if err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/chat/completions", bytes.NewReader(encoded))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client().Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("vision provider status %d", response.StatusCode)
}
var result struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
} `json:"message"`
} `json:"choices"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 2<<20)).Decode(&result); err != nil {
return "", err
}
if len(result.Choices) == 0 {
return "", ErrEmptyResult
}
text := strings.TrimSpace(result.Choices[0].Message.Content)
if text == "" {
text = strings.TrimSpace(result.Choices[0].Message.ReasoningContent)
}
if text == "" {
return "", ErrEmptyResult
}
return text, nil
}
// Generate uses an OpenAI-compatible image-generation skill. The provider is
// selected outside the core by the capability binding; this adapter contains
// transport only and never decides what the user meant.
func (c ImageGenerationClient) Generate(ctx context.Context, model, prompt, size string) (GeneratedImage, error) {
if strings.TrimSpace(prompt) == "" {
return GeneratedImage{}, errors.New("image prompt is empty")
}
payload := struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
Size string `json:"size,omitempty"`
ResponseFormat string `json:"response_format,omitempty"`
}{Model: model, Prompt: prompt, Size: size, ResponseFormat: "b64_json"}
encoded, err := json.Marshal(payload)
if err != nil {
return GeneratedImage{}, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/images/generations", bytes.NewReader(encoded))
if err != nil {
return GeneratedImage{}, err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client().Do(request)
if err != nil {
return GeneratedImage{}, err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode > 299 {
return GeneratedImage{}, fmt.Errorf("image provider status %d", response.StatusCode)
}
var result struct {
Data []struct {
Base64 string `json:"b64_json"`
URL string `json:"url"`
} `json:"data"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<20)).Decode(&result); err != nil {
return GeneratedImage{}, err
}
if len(result.Data) == 0 || (strings.TrimSpace(result.Data[0].Base64) == "" && strings.TrimSpace(result.Data[0].URL) == "") {
return GeneratedImage{}, ErrEmptyResult
}
return GeneratedImage{DataBase64: result.Data[0].Base64, SourceURL: result.Data[0].URL, ContentType: "image/png"}, nil
}
func (c ComfyImageGenerationClient) Generate(ctx context.Context, model, prompt, size string) (GeneratedImage, error) {
if strings.TrimSpace(prompt) == "" {
return GeneratedImage{}, errors.New("image prompt is empty")
}
width, height := imageSize(size)
workflow := map[string]any{
"3": map[string]any{"inputs": map[string]any{"seed": time.Now().UnixNano(), "steps": 8, "cfg": 1.0, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0, "model": []any{"16", 0}, "positive": []any{"6", 0}, "negative": []any{"7", 0}, "latent_image": []any{"13", 0}}, "class_type": "KSampler"},
"6": map[string]any{"inputs": map[string]any{"text": prompt, "clip": []any{"18", 0}}, "class_type": "CLIPTextEncode"},
"7": map[string]any{"inputs": map[string]any{"text": "blurry, ugly, distorted", "clip": []any{"18", 0}}, "class_type": "CLIPTextEncode"},
"8": map[string]any{"inputs": map[string]any{"samples": []any{"3", 0}, "vae": []any{"17", 0}}, "class_type": "VAEDecode"},
"9": map[string]any{"inputs": map[string]any{"filename_prefix": "domovoy", "images": []any{"8", 0}}, "class_type": "SaveImage"},
"13": map[string]any{"inputs": map[string]any{"width": width, "height": height, "batch_size": 1}, "class_type": "EmptySD3LatentImage"},
"16": map[string]any{"inputs": map[string]any{"unet_name": "z_image_turbo_bf16.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
"17": map[string]any{"inputs": map[string]any{"vae_name": "ae.safetensors"}, "class_type": "VAELoader"},
"18": map[string]any{"inputs": map[string]any{"clip_name": "qwen_3_4b.safetensors", "type": "lumina2", "device": "default"}, "class_type": "CLIPLoader"},
}
payload := map[string]any{"prompt": workflow, "client_id": fmt.Sprintf("domovoy-%d", time.Now().UnixNano())}
encoded, err := json.Marshal(payload)
if err != nil {
return GeneratedImage{}, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/prompt", bytes.NewReader(encoded))
if err != nil {
return GeneratedImage{}, err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client().Do(request)
if err != nil {
return GeneratedImage{}, err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode > 299 {
return GeneratedImage{}, fmt.Errorf("comfy provider status %d", response.StatusCode)
}
var accepted struct {
PromptID string `json:"prompt_id"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&accepted); err != nil {
return GeneratedImage{}, err
}
if accepted.PromptID == "" {
return GeneratedImage{}, ErrEmptyResult
}
// A caller timeout must remove only this prompt. ComfyUI's global
// /interrupt endpoint can otherwise stop unrelated users' work and leave
// queued requests in an ambiguous state.
defer func() {
if ctx.Err() != nil {
cancelCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := c.cancelPrompt(cancelCtx, accepted.PromptID); err != nil {
// The original context error is more useful to the caller; the
// provider cleanup failure remains observable in the server log.
log.Printf("comfy prompt cleanup failed for %s: %v", accepted.PromptID, err)
}
}
}()
interval := c.PollInterval
if interval <= 0 {
interval = time.Second
}
timeout := c.PollTimeout
if timeout <= 0 {
timeout = 10 * time.Minute
}
deadline := time.NewTimer(timeout)
defer deadline.Stop()
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
image, done, err := c.historyImage(ctx, accepted.PromptID)
if err != nil {
return GeneratedImage{}, err
}
if done {
return image, nil
}
select {
case <-ctx.Done():
return GeneratedImage{}, ctx.Err()
case <-deadline.C:
return GeneratedImage{}, context.DeadlineExceeded
case <-ticker.C:
}
}
}
func (c ComfyImageGenerationClient) cancelPrompt(ctx context.Context, promptID string) error {
if strings.TrimSpace(promptID) == "" {
return errors.New("comfy prompt id is empty")
}
payload, err := json.Marshal(map[string][]string{"delete": {promptID}})
if err != nil {
return err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/queue", bytes.NewReader(payload))
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client().Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode > 299 {
return fmt.Errorf("comfy prompt cleanup status %d", response.StatusCode)
}
return nil
}
func (c ComfyImageGenerationClient) historyImage(ctx context.Context, promptID string) (GeneratedImage, bool, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.BaseURL, "/")+"/history/"+url.PathEscape(promptID), nil)
if err != nil {
return GeneratedImage{}, false, err
}
response, err := c.client().Do(request)
if err != nil {
return GeneratedImage{}, false, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusNotFound {
return GeneratedImage{}, false, nil
}
if response.StatusCode < 200 || response.StatusCode > 299 {
return GeneratedImage{}, false, fmt.Errorf("comfy history status %d", response.StatusCode)
}
var history map[string]struct {
Outputs map[string]struct {
Images []struct {
Filename string `json:"filename"`
Subfolder string `json:"subfolder"`
Type string `json:"type"`
} `json:"images"`
} `json:"outputs"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 4<<20)).Decode(&history); err != nil {
return GeneratedImage{}, false, err
}
record, ok := history[promptID]
if !ok {
return GeneratedImage{}, false, nil
}
for _, output := range record.Outputs {
for _, image := range output.Images {
if image.Filename == "" {
continue
}
query := url.Values{"filename": {image.Filename}, "subfolder": {image.Subfolder}, "type": {image.Type}}
imageRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.BaseURL, "/")+"/view?"+query.Encode(), nil)
if err != nil {
return GeneratedImage{}, false, err
}
imageResponse, err := c.client().Do(imageRequest)
if err != nil {
return GeneratedImage{}, false, err
}
data, readErr := io.ReadAll(io.LimitReader(imageResponse.Body, 32<<20))
imageResponse.Body.Close()
if readErr != nil {
return GeneratedImage{}, false, readErr
}
if imageResponse.StatusCode < 200 || imageResponse.StatusCode > 299 || len(data) == 0 {
return GeneratedImage{}, false, ErrEmptyResult
}
contentType := imageResponse.Header.Get("Content-Type")
if contentType == "" {
contentType = "image/png"
}
return GeneratedImage{DataBase64: encodeBase64(data), ContentType: contentType}, true, nil
}
}
return GeneratedImage{}, true, ErrEmptyResult
}
func imageSize(size string) (int, int) {
parts := strings.Split(strings.TrimSpace(size), "x")
if len(parts) == 2 {
var width, height int
if _, err := fmt.Sscanf(size, "%dx%d", &width, &height); err == nil && width >= 256 && height >= 256 && width <= 2048 && height <= 2048 {
return width, height
}
}
return 512, 512
}
// Describe accepts a video skill endpoint that performs its own bounded frame
// sampling. Frame extraction is provider-owned so the core does not acquire a
// media-processing dependency or hide a policy in orchestration code.
func (c VideoUnderstandingClient) Describe(ctx context.Context, model, filename, contentType string, data []byte, instruction string) (string, error) {
if len(data) == 0 {
return "", errors.New("video payload is empty")
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
header := make(textproto.MIMEHeader)
header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, strings.ReplaceAll(filename, `"`, "")))
header.Set("Content-Type", contentType)
part, err := writer.CreatePart(header)
if err != nil {
return "", err
}
if _, err := part.Write(data); err != nil {
return "", err
}
_ = writer.WriteField("model", model)
_ = writer.WriteField("instruction", instruction)
if err := writer.Close(); err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/understand", body)
if err != nil {
return "", err
}
request.Header.Set("Content-Type", writer.FormDataContentType())
response, err := c.client().Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode > 299 {
return "", fmt.Errorf("video provider status %d", response.StatusCode)
}
var result struct {
Text string `json:"text"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 2<<20)).Decode(&result); err != nil {
return "", err
}
if strings.TrimSpace(result.Text) == "" {
return "", ErrEmptyResult
}
return strings.TrimSpace(result.Text), nil
}
func (c SpeechClient) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
func (c VisionClient) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
func (c ImageGenerationClient) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
func (c ComfyImageGenerationClient) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
func (c VideoUnderstandingClient) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
func encodeBase64(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
+209
View File
@@ -0,0 +1,209 @@
package multimodal
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestSpeechClientSendsMultipartAndReturnsTranscript(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/inference" || r.Method != http.MethodPost {
t.Fatalf("unexpected speech request: %s %s", r.Method, r.URL.Path)
}
if err := r.ParseMultipartForm(1 << 20); err != nil {
t.Fatal(err)
}
file, _, err := r.FormFile("file")
if err != nil {
t.Fatal(err)
}
defer file.Close()
data, _ := io.ReadAll(file)
if string(data) != "audio" {
t.Fatalf("audio payload = %q", data)
}
_ = json.NewEncoder(w).Encode(map[string]string{"text": "распознанная фраза"})
}))
defer server.Close()
text, err := (SpeechClient{BaseURL: server.URL, Client: server.Client()}).Transcribe(context.Background(), "voice.wav", "audio/wav", []byte("audio"))
if err != nil || text != "распознанная фраза" {
t.Fatalf("transcript=%q err=%v", text, err)
}
}
func TestVisionClientSendsImageDataAndInstruction(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var payload struct {
Messages []struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
ImageURL *struct {
URL string `json:"url"`
} `json:"image_url"`
} `json:"content"`
} `json:"messages"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
if len(payload.Messages) != 1 || len(payload.Messages[0].Content) != 2 || !strings.Contains(payload.Messages[0].Content[0].Text, "Опиши") || payload.Messages[0].Content[1].ImageURL == nil || !strings.HasPrefix(payload.Messages[0].Content[1].ImageURL.URL, "data:image/png;base64,") {
t.Fatalf("unexpected vision payload: %+v", payload)
}
_ = json.NewEncoder(w).Encode(map[string]any{"choices": []any{map[string]any{"message": map[string]string{"content": "на изображении круг"}}}})
}))
defer server.Close()
text, err := (VisionClient{BaseURL: server.URL + "/v1", Client: server.Client()}).Describe(context.Background(), "vision-model", "image/png", []byte{0, 1, 2}, "Опиши изображение")
if err != nil || text != "на изображении круг" {
t.Fatalf("description=%q err=%v", text, err)
}
}
func TestVisionClientUsesReasoningContentWhenProviderOmitsContent(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{"choices": []any{map[string]any{
"message": map[string]string{"content": "", "reasoning_content": "чёрный квадрат"},
}}})
}))
defer server.Close()
text, err := (VisionClient{BaseURL: server.URL, Client: server.Client()}).Describe(context.Background(), "vision-model", "image/png", []byte("image"), "Опиши изображение")
if err != nil || text != "чёрный квадрат" {
t.Fatalf("description=%q err=%v", text, err)
}
}
func TestImageGenerationClientSendsPromptAndReturnsBase64(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/images/generations" || r.Method != http.MethodPost {
t.Fatalf("unexpected image request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
Size string `json:"size"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
if payload.Model != "image-model" || payload.Prompt != "green bird" || payload.Size != "1024x1024" {
t.Fatalf("unexpected image payload: %+v", payload)
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]string{"b64_json": "aW1hZ2U="}}})
}))
defer server.Close()
image, err := (ImageGenerationClient{BaseURL: server.URL, Client: server.Client()}).Generate(context.Background(), "image-model", "green bird", "1024x1024")
if err != nil || image.DataBase64 != "aW1hZ2U=" || image.ContentType != "image/png" {
t.Fatalf("image=%+v err=%v", image, err)
}
}
func TestVideoUnderstandingClientSendsMultipartAndReturnsDescription(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/understand" || r.Method != http.MethodPost {
t.Fatalf("unexpected video request: %s %s", r.Method, r.URL.Path)
}
if err := r.ParseMultipartForm(1 << 20); err != nil {
t.Fatal(err)
}
if r.FormValue("model") != "video-model" || r.FormValue("instruction") != "describe movement" {
t.Fatalf("unexpected video fields: model=%q instruction=%q", r.FormValue("model"), r.FormValue("instruction"))
}
file, _, err := r.FormFile("file")
if err != nil {
t.Fatal(err)
}
defer file.Close()
data, _ := io.ReadAll(file)
if string(data) != "video" {
t.Fatalf("video payload=%q", data)
}
_ = json.NewEncoder(w).Encode(map[string]string{"text": "человек идёт"})
}))
defer server.Close()
text, err := (VideoUnderstandingClient{BaseURL: server.URL, Client: server.Client()}).Describe(context.Background(), "video-model", "clip.mp4", "video/mp4", []byte("video"), "describe movement")
if err != nil || text != "человек идёт" {
t.Fatalf("description=%q err=%v", text, err)
}
}
func TestComfyImageGenerationClientRunsWorkflowAndReturnsImage(t *testing.T) {
polls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/prompt":
var payload struct {
Prompt map[string]any `json:"prompt"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
if payload.Prompt["6"].(map[string]any)["inputs"].(map[string]any)["text"] != "red bird" {
t.Fatalf("prompt was not forwarded: %+v", payload.Prompt["6"])
}
_ = json.NewEncoder(w).Encode(map[string]string{"prompt_id": "test-prompt"})
case "/history/test-prompt":
polls++
if polls == 1 {
_, _ = w.Write([]byte(`{}`))
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"test-prompt": map[string]any{"outputs": map[string]any{"9": map[string]any{"images": []any{map[string]string{"filename": "out.png", "subfolder": "", "type": "output"}}}}}})
case "/view":
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write([]byte("png"))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
image, err := (ComfyImageGenerationClient{BaseURL: server.URL, Client: server.Client(), PollInterval: time.Millisecond, PollTimeout: time.Second}).Generate(context.Background(), "", "red bird", "256x256")
if err != nil || image.DataBase64 != "cG5n" || image.ContentType != "image/png" {
t.Fatalf("image=%+v err=%v", image, err)
}
}
func TestComfyImageGenerationClientDeletesOnlyCancelledPrompt(t *testing.T) {
var deleted []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/prompt":
_ = json.NewEncoder(w).Encode(map[string]string{"prompt_id": "cancel-me"})
case "/history/cancel-me":
<-r.Context().Done()
case "/queue":
var payload struct {
Delete []string `json:"delete"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
deleted = append(deleted, payload.Delete...)
w.WriteHeader(http.StatusNoContent)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() {
_, err := (ComfyImageGenerationClient{BaseURL: server.URL, Client: server.Client(), PollInterval: time.Millisecond, PollTimeout: time.Minute}).Generate(ctx, "", "red bird", "256x256")
done <- err
}()
time.Sleep(10 * time.Millisecond)
cancel()
if err := <-done; !errors.Is(err, context.Canceled) {
t.Fatalf("err=%v", err)
}
if len(deleted) != 1 || deleted[0] != "cancel-me" {
t.Fatalf("deleted=%v", deleted)
}
}
+18
View File
@@ -0,0 +1,18 @@
package multimodal
import (
"crypto/sha256"
"encoding/hex"
"git.cin.su/m/Assistant/contracts/artifact"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/interaction"
)
func NormalizeIngress(id string, modality interaction.IngressModality, sourceRef, deviceRef, locationRef, principalID, contentType string, data []byte, response capability.MultimodalResponse, at int64) (interaction.IngressEnvelope, artifact.MediaArtifact) {
hash := sha256.Sum256(data)
artifactValue := artifact.MediaArtifact{ID: artifact.ArtifactID("artifact:media:" + hex.EncodeToString(hash[:8])), ContentType: contentType, SizeBytes: int64(len(data)), ContentDigest: hex.EncodeToString(hash[:]), SourceRef: sourceRef, RecordedAtUnixMillis: at}
if len(data) == 0 {
return interaction.IngressEnvelope{ID: id, Modality: modality, SourceRef: sourceRef, DeviceRef: deviceRef, LocationRef: locationRef, PrincipalID: principalID, Degraded: true, RecordedAtUnixMillis: at}, artifact.MediaArtifact{}
}
return interaction.IngressEnvelope{ID: id, Modality: modality, SourceRef: sourceRef, DeviceRef: deviceRef, LocationRef: locationRef, PrincipalID: principalID, ArtifactRef: string(artifactValue.ID), ObservationRef: response.EvidenceRef, ProviderRef: response.Provider, ModelRef: response.Model, ModelVersion: response.ModelVersion, ConfidenceMillis: response.ConfidenceMillis, RecordedAtUnixMillis: at}, artifactValue
}
+22
View File
@@ -0,0 +1,22 @@
package multimodal_test
import (
"git.cin.su/m/Assistant/adapters/multimodal"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/interaction"
"testing"
)
func TestIngressNormalizesMediaArtifactAndObservationProvenance(t *testing.T) {
e, a := multimodal.NormalizeIngress("ingress-1", interaction.IngressImage, "camera:street", "device:cam", "world:street", "principal", "image/png", []byte("image"), capability.MultimodalResponse{Provider: "local", Model: "vision", ModelVersion: "1", EvidenceRef: "observation-1", ConfidenceMillis: 800}, 1)
if !e.Valid() || !a.Valid() || e.ArtifactRef == "" || e.ObservationRef != "observation-1" || e.DeviceRef == "" || e.LocationRef == "" {
t.Fatalf("envelope=%+v artifact=%+v", e, a)
}
}
func TestUnavailableIngressDegradesWithoutFabricatingObservation(t *testing.T) {
e, a := multimodal.NormalizeIngress("ingress-1", interaction.IngressAudio, "mic", "device:mic", "", "principal", "audio/wav", nil, capability.MultimodalResponse{}, 1)
if !e.Degraded || e.ObservationRef != "" || a.Valid() {
t.Fatalf("envelope=%+v artifact=%+v", e, a)
}
}
+14 -5
View File
@@ -181,10 +181,6 @@ func (r *CapabilityRepository) SaveBinding(ctx context.Context, binding capabili
if err != nil {
return err
}
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision)
if err != nil {
return err
}
}
return tx.Commit(ctx)
}
@@ -437,6 +433,14 @@ func (r *CapabilityRepository) LoadCognitiveCandidatesForProfile(ctx context.Con
return r.loadCognitiveCandidates(ctx, capabilityID, owner, profileID)
}
func (r *CapabilityRepository) LoadCognitiveCandidatesForEvaluation(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID, evaluationID capability.EvaluationID) ([]capability.ResolvedCognitiveBinding, error) {
var profileID capability.TaskEvaluationProfileID
if err := r.pool.QueryRow(ctx, `SELECT task_profile_id FROM capability.cognitive_task_evaluations WHERE evaluation_id=$1 AND capability_id=$2`, evaluationID, capabilityID).Scan(&profileID); err != nil {
return nil, err
}
return r.loadCognitiveCandidates(ctx, capabilityID, owner, profileID)
}
func (r *CapabilityRepository) LoadCognitiveCandidate(ctx context.Context, bindingID capability.CapabilityBindingID, revision int64) (capability.ResolvedCognitiveBinding, error) {
var capabilityID capability.CapabilityID
var owner identity.PrincipalID
@@ -564,7 +568,12 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
if err != nil {
return err
}
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision)
// Discovery may advance a head, but must never roll back a governed
// CANARY/ACTIVE revision to an older discovered snapshot after restart.
// Discovery may advance an inactive head even when its deterministic
// revision number is lower, but it must never overwrite a governed canary
// or active head during restart/startup refresh.
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT (binding_id) DO UPDATE SET revision=EXCLUDED.revision,updated_at=CURRENT_TIMESTAMP WHERE NOT EXISTS (SELECT 1 FROM capability.cognitive_bindings current_binding WHERE current_binding.binding_id=capability.cognitive_binding_revision_heads.binding_id AND current_binding.revision=capability.cognitive_binding_revision_heads.revision AND current_binding.activation IN ('CANARY','ACTIVE'))`, binding.ID, binding.Revision)
if err != nil {
return err
}
+71 -1
View File
@@ -6,11 +6,20 @@ import (
"time"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/platform/telemetry"
"github.com/jackc/pgx/v5/pgxpool"
)
type EpistemicsRepository struct{ pool *pgxpool.Pool }
type TruthRuntimeStatus struct {
BundleCount int64 `json:"bundle_count"`
ClaimsByVerification map[string]int64 `json:"claims_by_verification"`
BeliefsByState map[string]int64 `json:"beliefs_by_state"`
SourcesByFreshness map[string]int64 `json:"sources_by_freshness"`
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
}
func OpenEpistemics(ctx context.Context, config ContinuityConfig) (*EpistemicsRepository, error) {
pool, err := openPool(ctx, config)
if err != nil {
@@ -25,6 +34,58 @@ func (r *EpistemicsRepository) Ping(ctx context.Context) error {
return r.pool.Ping(ctx)
}
// TruthRuntimeStatus is a read-only projection of durable truth bundles.
// It does not promote claims, revise beliefs, or mutate epistemic state.
func (r *EpistemicsRepository) TruthRuntimeStatus(ctx context.Context) (TruthRuntimeStatus, error) {
started := time.Now()
outcome := "SUCCESS"
defer func() {
telemetry.RecordCompatibility("truth-bundles-status-reader", "epistemics-repository", "read", "research-status", true, false, true, outcome, "truth-status", "epistemics", started)
}()
status := TruthRuntimeStatus{
ClaimsByVerification: map[string]int64{},
BeliefsByState: map[string]int64{},
SourcesByFreshness: map[string]int64{},
ObservedAtUnixMillis: time.Now().UnixMilli(),
}
if err := r.pool.QueryRow(ctx, `SELECT count(*) FROM epistemics.truth_bundles`).Scan(&status.BundleCount); err != nil {
outcome = "FAILURE"
return TruthRuntimeStatus{}, err
}
queries := []struct {
query string
destination map[string]int64
}{
{`SELECT claim_verification, count(*) FROM epistemics.truth_bundles GROUP BY claim_verification`, status.ClaimsByVerification},
{`SELECT belief_state, count(*) FROM epistemics.truth_bundles GROUP BY belief_state`, status.BeliefsByState},
{`SELECT snapshot_freshness, count(*) FROM epistemics.truth_bundles GROUP BY snapshot_freshness`, status.SourcesByFreshness},
}
for _, item := range queries {
rows, err := r.pool.Query(ctx, item.query)
if err != nil {
outcome = "FAILURE"
return TruthRuntimeStatus{}, err
}
for rows.Next() {
var state string
var count int64
if err := rows.Scan(&state, &count); err != nil {
outcome = "FAILURE"
rows.Close()
return TruthRuntimeStatus{}, err
}
item.destination[state] = count
}
if err := rows.Err(); err != nil {
outcome = "FAILURE"
rows.Close()
return TruthRuntimeStatus{}, err
}
rows.Close()
}
return status, nil
}
func (r *EpistemicsRepository) SaveSystemExpectation(ctx context.Context, value epistemics.SystemExpectation) error {
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.system_expectations (
expectation_id, original_intent_id, source_utterance_id, expected_outcome, expected_effect,
@@ -99,7 +160,7 @@ func (r *EpistemicsRepository) ClaimReflectionCandidate(ctx context.Context, wor
var value epistemics.ReflectionCandidate
err = tx.QueryRow(ctx, `SELECT reflection_candidate_id,trigger,state,source_experience_id,original_intent_id,source_utterance_id,summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision,claimed_by,claim_expires_at_unix_millis
FROM epistemics.reflection_candidates
WHERE state='OPEN' AND (claim_expires_at_unix_millis=0 OR claim_expires_at_unix_millis < $1)
WHERE state='OPEN' AND requires_cognition=false AND (claim_expires_at_unix_millis=0 OR claim_expires_at_unix_millis < $1)
ORDER BY recorded_at_unix_millis,reflection_candidate_id
FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&value.ID, &value.Trigger, &value.State, &value.SourceExperienceID, &value.OriginalIntentID, &value.SourceUtteranceID, &value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision, &value.ClaimedBy, &value.ClaimExpiresAtUnixMillis)
if err != nil {
@@ -167,7 +228,13 @@ func (r *EpistemicsRepository) SaveReflectionCandidate(ctx context.Context, valu
}
func (r *EpistemicsRepository) SaveTruthBundle(ctx context.Context, value epistemics.TruthBundle, recordedAtUnixMillis int64) error {
started := time.Now()
outcome := "SUCCESS"
defer func() {
telemetry.RecordCompatibility("truth-bundles-writer", "epistemics-repository", "write", "persisted-research", true, false, true, outcome, string(value.Claim.ID), "epistemics", started)
}()
if value.Source.ID == "" || value.Snapshot.ID == "" || value.Excerpt.ID == "" || value.Claim.ID == "" || value.Belief.ID == "" || value.Citation.ID == "" || recordedAtUnixMillis < 1 {
outcome = "INVALID"
return fmt.Errorf("invalid truth bundle")
}
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.truth_bundles (
@@ -181,5 +248,8 @@ func (r *EpistemicsRepository) SaveTruthBundle(ctx context.Context, value episte
value.Excerpt.ID, value.Excerpt.SnapshotID, value.Excerpt.Text, value.Excerpt.Start, value.Excerpt.End,
value.Observation.ID, value.Observation.ObservedAt, value.Claim.ID, value.Claim.Statement, value.Claim.ObservationID, value.Claim.CitationID, value.Claim.Verification,
value.Belief.ID, value.Belief.ClaimID, value.Belief.State, value.Citation.ID, value.Citation.ExcerptID, value.Citation.Evidence, value.Citation.Coverage, value.Citation.Conflict, value.Citation.ResolvedAtUnixMillis, recordedAtUnixMillis)
if err != nil {
outcome = "FAILURE"
}
return err
}
+36
View File
@@ -26,3 +26,39 @@ func (r *IdentityRepository) LoadBinding(ctx context.Context, actor identity.Act
}
return binding, nil
}
func (r *IdentityRepository) LoadCreatorAccount(ctx context.Context, username string) (identity.CreatorAccount, error) {
var account identity.CreatorAccount
err := r.pool.QueryRow(ctx, `SELECT username, actor_id, principal_id, password_hash, revision FROM identity.creator_accounts WHERE username=$1`, username).
Scan(&account.Username, &account.ActorID, &account.PrincipalID, &account.PasswordHash, &account.Revision)
if err != nil {
return identity.CreatorAccount{}, fmt.Errorf("load creator account: %w", err)
}
return account, nil
}
func (r *IdentityRepository) LoadCreatorAccountByBinding(ctx context.Context, actor identity.ActorID, principal identity.PrincipalID) (identity.CreatorAccount, error) {
var account identity.CreatorAccount
err := r.pool.QueryRow(ctx, `SELECT username, actor_id, principal_id, password_hash, revision FROM identity.creator_accounts WHERE actor_id=$1 AND principal_id=$2`, actor, principal).
Scan(&account.Username, &account.ActorID, &account.PrincipalID, &account.PasswordHash, &account.Revision)
if err != nil {
return identity.CreatorAccount{}, fmt.Errorf("load creator account by binding: %w", err)
}
return account, nil
}
func (r *IdentityRepository) CreateCreatorAccount(ctx context.Context, account identity.CreatorAccount) error {
_, err := r.pool.Exec(ctx, `INSERT INTO identity.creator_accounts (username, actor_id, principal_id, password_hash) VALUES ($1,$2,$3,$4) ON CONFLICT (username) DO NOTHING`, account.Username, account.ActorID, account.PrincipalID, account.PasswordHash)
return err
}
func (r *IdentityRepository) UpdateCreatorPassword(ctx context.Context, actor identity.ActorID, principal identity.PrincipalID, revision int64, passwordHash string) error {
result, err := r.pool.Exec(ctx, `UPDATE identity.creator_accounts SET password_hash=$1, revision=revision+1, password_changed_at=CURRENT_TIMESTAMP WHERE actor_id=$2 AND principal_id=$3 AND revision=$4`, passwordHash, actor, principal, revision)
if err != nil {
return fmt.Errorf("update creator password: %w", err)
}
if result.RowsAffected() != 1 {
return fmt.Errorf("update creator password: revision conflict")
}
return nil
}
@@ -3,6 +3,7 @@ package postgres
import (
"context"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -98,3 +99,22 @@ func (r *InteractionRepository) Utterances(ctx context.Context, id interaction.C
}
return out, rows.Err()
}
// RecentUtterancesForPrincipal provides durable conversational context when a
// client starts a fresh conversation instead of carrying the previous ID.
func (r *InteractionRepository) RecentUtterancesForPrincipal(ctx context.Context, principalID identity.PrincipalID, limit int) ([]interaction.HumanUtterance, error) {
rows, err := r.pool.Query(ctx, `SELECT utterance_id,conversation_id,actor_id,principal_id,channel,received_at_unix_millis,correlation_id,content,content_ref,speech_act,interpretation_state,corrects_utterance_id FROM interaction.utterances WHERE principal_id=$1 ORDER BY received_at_unix_millis DESC, utterance_id DESC LIMIT $2`, principalID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []interaction.HumanUtterance
for rows.Next() {
var u interaction.HumanUtterance
if err := rows.Scan(&u.ID, &u.ConversationID, &u.ActorID, &u.PrincipalID, &u.Channel, &u.ReceivedAtUnixMillis, &u.CorrelationID, &u.Content, &u.ContentRef, &u.SpeechAct, &u.InterpretationState, &u.CorrectsUtteranceID); err != nil {
return nil, err
}
out = append(out, u)
}
return out, rows.Err()
}
+33
View File
@@ -2,6 +2,7 @@ package postgres
import (
"context"
"encoding/json"
"strings"
"git.cin.su/m/Assistant/contracts/identity"
@@ -18,6 +19,38 @@ func OpenSituation(ctx context.Context, config ContinuityConfig) (*SituationRepo
}
return &SituationRepository{pool: pool}, nil
}
func (r *SituationRepository) SaveContextScope(ctx context.Context, scope situation.ContextScope) error {
if err := scope.Validate(); err != nil {
return err
}
bindings, err := json.Marshal(scope.Bindings)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO situation.context_scopes(context_scope_id,principal_id,revision,bindings,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(context_scope_id) DO UPDATE SET revision=EXCLUDED.revision, bindings=EXCLUDED.bindings, recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`, scope.ID, scope.PrincipalID, scope.Revision, bindings, scope.RecordedAt)
return err
}
func (r *SituationRepository) SaveFocusFrame(ctx context.Context, frame situation.FocusFrame) error {
if err := frame.Validate(); err != nil {
return err
}
ref, err := json.Marshal(frame.ContextScopeRef)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO situation.focus_frames(focus_frame_id,principal_id,actor_id,revision,authority_scope_ref,context_scope_ref,goal_ref,task_ref,source_utterance_ref,provenance,confidence_millis,state,parent_frame_ref,activated_at_unix_millis,suspended_at_unix_millis,expired_at_unix_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT(focus_frame_id) DO UPDATE SET revision=EXCLUDED.revision, context_scope_ref=EXCLUDED.context_scope_ref, state=EXCLUDED.state, suspended_at_unix_millis=EXCLUDED.suspended_at_unix_millis, expired_at_unix_millis=EXCLUDED.expired_at_unix_millis, recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`, frame.ID, frame.PrincipalID, frame.ActorID, frame.Revision, frame.AuthorityScopeRef, ref, frame.GoalRef, frame.TaskRef, frame.SourceUtteranceRef, frame.Provenance, frame.ConfidenceMillis, frame.State, frame.ParentFrameRef, frame.ActivatedAt, frame.SuspendedAt, frame.ExpiredAt, frame.RecordedAt)
return err
}
func (r *SituationRepository) AppendFocusTransition(ctx context.Context, transition situation.FocusTransition) error {
if err := transition.Validate(); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO situation.focus_transitions(transition_id,focus_frame_id,from_revision,to_revision,operation,cause_ref,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7)`, transition.TransitionID, transition.FrameID, transition.FromRevision, transition.ToRevision, transition.Operation, transition.CauseRef, transition.RecordedAt)
return err
}
func (r *SituationRepository) Close() { r.pool.Close() }
func (r *SituationRepository) Save(ctx context.Context, binding situation.FocusBinding) error {
_, err := r.pool.Exec(ctx, `INSERT INTO situation.focus_bindings (focus_binding_id, actor_id, session_id, workspace_id, project_id, goal_id, scope_id, active) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT (focus_binding_id) DO UPDATE SET active=EXCLUDED.active`, binding.ID, binding.ActorID, binding.SessionID, binding.Workspace, binding.ProjectID, binding.GoalID, binding.ScopeID, binding.Active)
+115
View File
@@ -25,6 +25,121 @@ var (
ErrDevelopmentCandidateConflict = errors.New("development candidate conflict")
)
func (r *WorkRepository) SaveInvestigation(ctx context.Context, value work.Investigation, expectedRevision int64) error {
if !value.Valid() || expectedRevision < 0 {
return errors.New("invalid investigation revision")
}
attempts := make([]string, 0, len(value.AttemptRefs))
for _, item := range value.AttemptRefs {
attempts = append(attempts, string(item))
}
hypotheses := make([]string, 0, len(value.HypothesisRefs))
for _, item := range value.HypothesisRefs {
hypotheses = append(hypotheses, string(item))
}
findings := make([]string, 0, len(value.FindingRefs))
for _, item := range value.FindingRefs {
findings = append(findings, string(item))
}
blockers := append([]string(nil), value.Blockers...)
checkpoints := make([]string, 0, len(value.CheckpointRefs))
for _, item := range value.CheckpointRefs {
checkpoints = append(checkpoints, string(item))
}
if expectedRevision == 0 {
result, err := r.pool.Exec(ctx, `INSERT INTO work.long_running_work(investigation_id,objective,hypothesis_refs,attempt_refs,finding_refs,blockers,checkpoint_refs,next_step,state,revision,time_budget_millis,cost_budget_micros,max_attempts,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT(investigation_id) DO NOTHING`, value.ID, value.Objective, hypotheses, attempts, findings, blockers, checkpoints, value.NextStep, value.State, value.Revision, value.TimeBudgetMillis, value.CostBudgetMicros, value.MaxAttempts, value.RecordedAtUnixMillis)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return errors.New("investigation already exists")
}
return nil
}
result, err := r.pool.Exec(ctx, `UPDATE work.long_running_work SET objective=$2,hypothesis_refs=$3,attempt_refs=$4,finding_refs=$5,blockers=$6,checkpoint_refs=$7,next_step=$8,state=$9,revision=$10,time_budget_millis=$11,cost_budget_micros=$12,max_attempts=$13,recorded_at_unix_millis=$14 WHERE investigation_id=$1 AND revision=$15`, value.ID, value.Objective, hypotheses, attempts, findings, blockers, checkpoints, value.NextStep, value.State, value.Revision, value.TimeBudgetMillis, value.CostBudgetMicros, value.MaxAttempts, value.RecordedAtUnixMillis, expectedRevision)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return errors.New("investigation revision conflict")
}
return nil
}
func (r *WorkRepository) LoadInvestigation(ctx context.Context, id work.LongRunningWorkID) (work.Investigation, error) {
var value work.Investigation
var hypotheses, attempts, findings, blockers, checkpoints []string
err := r.pool.QueryRow(ctx, `SELECT investigation_id,objective,hypothesis_refs,attempt_refs,finding_refs,blockers,checkpoint_refs,next_step,state,revision,time_budget_millis,cost_budget_micros,max_attempts,recorded_at_unix_millis FROM work.long_running_work WHERE investigation_id=$1`, id).Scan(&value.ID, &value.Objective, &hypotheses, &attempts, &findings, &blockers, &checkpoints, &value.NextStep, &value.State, &value.Revision, &value.TimeBudgetMillis, &value.CostBudgetMicros, &value.MaxAttempts, &value.RecordedAtUnixMillis)
if err != nil {
return work.Investigation{}, fmt.Errorf("load investigation: %w", err)
}
for _, item := range hypotheses {
value.HypothesisRefs = append(value.HypothesisRefs, work.EvidenceRef(item))
}
for _, item := range attempts {
value.AttemptRefs = append(value.AttemptRefs, work.AgentRunID(item))
}
for _, item := range findings {
value.FindingRefs = append(value.FindingRefs, work.EvidenceRef(item))
}
value.Blockers = append(value.Blockers, blockers...)
for _, item := range checkpoints {
value.CheckpointRefs = append(value.CheckpointRefs, work.AgentCheckpointID(item))
}
return value, nil
}
// SaveInvestigationCheckpoint atomically records the checkpoint and marks its
// attempts verified. The unique key makes retries idempotent.
func (r *WorkRepository) SaveInvestigationCheckpoint(ctx context.Context, checkpoint work.InvestigationCheckpoint) error {
if !checkpoint.Valid() {
return errors.New("invalid investigation checkpoint")
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `INSERT INTO work.investigation_checkpoints(checkpoint_id,investigation_id,completed_attempt_refs,completed_finding_refs,next_step,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(checkpoint_id) DO NOTHING`, checkpoint.ID, checkpoint.InvestigationID, stringIDs(checkpoint.CompletedAttemptRefs), stringEvidence(checkpoint.CompletedFindingRefs), checkpoint.NextStep, checkpoint.RecordedAtUnixMillis); err != nil {
return err
}
for _, attempt := range checkpoint.CompletedAttemptRefs {
if _, err = tx.Exec(ctx, `INSERT INTO work.investigation_attempts(investigation_id,attempt_ref,verified_at_unix_millis) VALUES($1,$2,$3) ON CONFLICT(investigation_id,attempt_ref) DO NOTHING`, checkpoint.InvestigationID, attempt, checkpoint.RecordedAtUnixMillis); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (r *WorkRepository) UnverifiedInvestigationAttempts(ctx context.Context, id work.LongRunningWorkID, candidates []work.AgentRunID) ([]work.AgentRunID, error) {
result := make([]work.AgentRunID, 0, len(candidates))
for _, candidate := range candidates {
var exists bool
if err := r.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM work.investigation_attempts WHERE investigation_id=$1 AND attempt_ref=$2)`, id, candidate).Scan(&exists); err != nil {
return nil, err
}
if !exists {
result = append(result, candidate)
}
}
return result, nil
}
func stringIDs(values []work.AgentRunID) []string {
result := make([]string, 0, len(values))
for _, value := range values {
result = append(result, string(value))
}
return result
}
func stringEvidence(values []work.EvidenceRef) []string {
result := make([]string, 0, len(values))
for _, value := range values {
result = append(result, string(value))
}
return result
}
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
pool, err := openPool(ctx, config)
if err != nil {
+108
View File
@@ -0,0 +1,108 @@
package postgres
import (
"context"
"encoding/json"
"fmt"
"git.cin.su/m/Assistant/contracts/world"
worlddomain "git.cin.su/m/Assistant/modules/world/domain"
"github.com/jackc/pgx/v5/pgxpool"
)
type WorldRepository struct{ pool *pgxpool.Pool }
func OpenWorld(ctx context.Context, config ContinuityConfig) (*WorldRepository, error) {
pool, err := openPool(ctx, config)
if err != nil {
return nil, err
}
return &WorldRepository{pool: pool}, nil
}
func (r *WorldRepository) Close() { r.pool.Close() }
func (r *WorldRepository) SaveKindDefinition(ctx context.Context, value world.WorldObjectKindDefinition) error {
if err := worlddomain.ValidateKindDefinition(value); err != nil {
return err
}
var parentOwner, parentKind, parentID string
var parentRevision *uint64
if value.ParentKindRef != nil {
parentOwner, parentKind, parentID = value.ParentKindRef.OwnerNamespace, value.ParentKindRef.SemanticKind, string(value.ParentKindRef.StableID)
if value.ParentKindRef.Revision != nil {
revision := uint64(*value.ParentKindRef.Revision)
parentRevision = &revision
}
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.kind_definitions(kind_definition_id,kind_key,revision,display_name,parent_owner_namespace,parent_semantic_kind,parent_stable_id,parent_revision,lifecycle,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT(kind_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.DisplayName, parentOwner, parentKind, parentID, parentRevision, value.Lifecycle, value.DefinitionDigest, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveObject(ctx context.Context, value world.WorldObject) error {
if err := worlddomain.ValidateObject(value); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.objects(world_object_id,principal_id,kind_owner_namespace,kind_semantic_kind,kind_stable_id,kind_revision,lifecycle,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(world_object_id) DO NOTHING`, value.ID, value.PrincipalID, value.KindDefinitionRef.OwnerNamespace, value.KindDefinitionRef.SemanticKind, value.KindDefinitionRef.StableID, value.KindRevision, value.Lifecycle, value.CreatedAt)
return err
}
func (r *WorldRepository) SaveAlias(ctx context.Context, value world.WorldAlias) error {
if err := worlddomain.ValidateAlias(value); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.aliases(world_alias_id,principal_id,world_object_id,alias,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(world_alias_id) DO NOTHING`, value.ID, value.PrincipalID, value.ObjectID, value.Alias, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveFacetDefinition(ctx context.Context, value world.FacetDefinition) error {
if err := worlddomain.ValidateFacetDefinition(value); err != nil {
return err
}
payload, err := json.Marshal(value.ValueType)
if err != nil {
return fmt.Errorf("encode facet value type: %w", err)
}
_, err = r.pool.Exec(ctx, `INSERT INTO world.facet_definitions(facet_definition_id,kind_key,revision,facet_key,value_type,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(facet_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.FacetKey, payload, value.DefinitionDigest, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveRelationDefinition(ctx context.Context, value world.RelationDefinition) error {
if err := worlddomain.ValidateRelationDefinition(value); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.relation_definitions(relation_definition_id,kind_key,revision,relation_key,target_kind_key,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(relation_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.RelationKey, value.TargetKindKey, value.DefinitionDigest, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveResolution(ctx context.Context, value world.IdentityResolutionDecisionRecord) error {
if err := worlddomain.ValidateResolution(value); err != nil {
return err
}
candidates, err := json.Marshal(value.Candidates)
if err != nil {
return fmt.Errorf("encode resolution candidates: %w", err)
}
evidence, err := json.Marshal(value.Evidence)
if err != nil {
return fmt.Errorf("encode resolution evidence: %w", err)
}
_, err = r.pool.Exec(ctx, `INSERT INTO world.identity_resolution_decisions(resolution_decision_id,principal_id,candidates,decision,evidence,confidence_millis,decided_at_unix_millis,revision) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(resolution_decision_id) DO NOTHING`, value.ID, value.PrincipalID, candidates, value.Decision, evidence, value.ConfidenceMillis, value.DecidedAt, value.Revision)
return err
}
func (r *WorldRepository) SaveLineage(ctx context.Context, value world.WorldLineage) error {
if err := worlddomain.ValidateLineage(value); err != nil {
return err
}
sources, err := json.Marshal(value.SourceRefs)
if err != nil {
return fmt.Errorf("encode lineage sources: %w", err)
}
successors, err := json.Marshal(value.SuccessorRefs)
if err != nil {
return fmt.Errorf("encode lineage successors: %w", err)
}
_, err = r.pool.Exec(ctx, `INSERT INTO world.lineage(lineage_id,operation,source_refs,successor_refs,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(lineage_id) DO NOTHING`, value.ID, value.Operation, sources, successors, value.RecordedAt)
return err
}
+276
View File
@@ -0,0 +1,276 @@
package research
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"html"
"io"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"git.cin.su/m/Assistant/contracts/epistemics"
)
var ErrSearchRejected = errors.New("web search request rejected")
var (
searchLinkPattern = regexp.MustCompile(`(?is)(<a[^>]+class=["'][^"']*result__a[^"']*["'][^>]*>)(.*?)</a>`)
searchSnippetPattern = regexp.MustCompile(`(?is)<a[^>]+class=["'][^"']*result__snippet[^"']*["'][^>]*>(.*?)</a>`)
bingLinkPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<h2[^>]*>(<a[^>]*>)(.*?)</a>`)
bingSnippetPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<p[^>]*>(.*?)</p>`)
tagPattern = regexp.MustCompile(`(?s)<[^>]*>`)
termPattern = regexp.MustCompile(`(?i)[\p{L}\p{N}]{2,}`)
)
var searchStopTerms = map[string]struct{}{
"какая": {}, "какое": {}, "какие": {}, "какой": {}, "что": {}, "как": {}, "где": {}, "когда": {}, "почему": {}, "мне": {}, "пожалуйста": {},
"what": {}, "which": {}, "where": {}, "when": {}, "why": {}, "how": {}, "please": {}, "the": {}, "this": {},
}
// SearchConnector uses a configured HTTPS search endpoint. It does not know
// cities, weather terms, entities or model names: the complete semantic query
// comes from the interpretation layer.
type SearchConnector struct {
Client *http.Client
EndpointTemplate string
AllowedHosts map[string]struct{}
FallbackEndpoints []SearchEndpoint
MaxBytes int64
MaxResults int
}
// SearchEndpoint is an independently allowlisted web-search route. The
// connector tries routes in order and stops at the first relevant evidence.
// This is a source-recovery policy, not a question-type classifier.
type SearchEndpoint struct {
Template string
AllowedHosts map[string]struct{}
}
func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSearchRequest, nowUnixMillis int64) ([]epistemics.ResearchCandidate, error) {
if request.Plan.Validate() != nil || strings.TrimSpace(request.Query) == "" || nowUnixMillis < 1 || !strings.Contains(c.EndpointTemplate, "%s") {
return nil, ErrSearchRejected
}
endpoints := make([]SearchEndpoint, 0, 1+len(c.FallbackEndpoints))
endpoints = append(endpoints, SearchEndpoint{Template: c.EndpointTemplate, AllowedHosts: c.AllowedHosts})
endpoints = append(endpoints, c.FallbackEndpoints...)
var lastErr error
for _, candidateEndpoint := range endpoints {
results, err := c.collectEndpoint(ctx, candidateEndpoint, request, nowUnixMillis)
if err != nil {
lastErr = err
continue
}
if len(results) > 0 {
return results, nil
}
}
if lastErr != nil {
return nil, lastErr
}
return nil, nil
}
func (c SearchConnector) collectEndpoint(ctx context.Context, configured SearchEndpoint, request epistemics.WebSearchRequest, nowUnixMillis int64) ([]epistemics.ResearchCandidate, error) {
if !strings.Contains(configured.Template, "%s") {
return nil, ErrSearchRejected
}
searchQuery := enrichedSearchQuery(request.Query)
endpoint, err := url.Parse(strings.Replace(configured.Template, "%s", url.QueryEscape(searchQuery), 1))
if err != nil || endpoint.Scheme != "https" || endpoint.Host == "" || !allowedHost(configured.AllowedHosts, strings.ToLower(endpoint.Hostname())) {
return nil, ErrSearchRejected
}
client := c.Client
if client == nil {
client = &http.Client{}
}
maxBytes := c.MaxBytes
if maxBytes <= 0 || maxBytes > 4<<20 {
maxBytes = 512 << 10
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, ErrSearchRejected
}
// Search providers commonly return a consent/challenge shell to the
// default Go client. A neutral browser identification improves the same
// generic evidence path for every query without selecting by topic.
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; DomovoyResearch/1.0)")
response, err := client.Do(req)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, ErrSearchRejected
}
body, err := io.ReadAll(io.LimitReader(response.Body, maxBytes+1))
if err != nil || len(body) == 0 || int64(len(body)) > maxBytes {
return nil, ErrSearchRejected
}
links := searchLinkPattern.FindAllSubmatch(body, c.limit())
snippets := searchSnippetPattern.FindAllSubmatch(body, c.limit())
if len(links) == 0 {
links = bingLinkPattern.FindAllSubmatch(body, c.limit())
snippets = bingSnippetPattern.FindAllSubmatch(body, c.limit())
}
if len(links) == 0 {
return nil, nil
}
digest := sha256.Sum256(body)
digestText := "sha256:" + hex.EncodeToString(digest[:])
base := hex.EncodeToString(digest[:])
sourceID := epistemics.SourceID("source:search:" + base)
snapshotID := epistemics.SourceSnapshotID("snapshot:search:" + base)
results := make([]epistemics.ResearchCandidate, 0, len(links))
for index, link := range links {
linkURL := resolveSearchResultLocator(extractHref(string(link[1])), endpoint)
title := cleanHTML(string(link[2]))
snippet := ""
if index < len(snippets) {
snippet = cleanHTML(string(snippets[index][1]))
}
statement := strings.TrimSpace(strings.Trim(title+" — "+snippet, " —"))
if statement == "" || !searchStatementRelatesToQuery(request.Query, statement) {
continue
}
ordinal := strconv.Itoa(index)
excerptID := epistemics.SourceExcerptID("excerpt:search:" + base + ":" + ordinal)
results = append(results, epistemics.ResearchCandidate{
PropositionKey: request.PropositionKey + ":result:" + ordinal,
Statement: statement,
Source: epistemics.Source{ID: sourceID, Kind: "web-search", Locator: linkURL, Title: title, Publisher: sourcePublisher(linkURL, endpoint.Hostname())},
Snapshot: epistemics.SourceSnapshot{ID: snapshotID, SourceID: sourceID, ContentDigest: digestText, CapturedAtUnixMillis: nowUnixMillis, Freshness: epistemics.FreshnessCurrent},
Excerpt: epistemics.SourceExcerpt{ID: excerptID, SnapshotID: snapshotID, Text: statement, Start: 0, End: int64(len(statement))},
Evidence: epistemics.EvidenceObserved, Coverage: epistemics.CoverageComplete, Conflict: epistemics.ConflictNone,
})
}
return results, nil
}
// searchStatementRelatesToQuery is a bounded relevance gate, not a truth
// check. It prevents generic search pages from becoming apparent evidence
// when their content has no substantive relation to the requested query.
func searchStatementRelatesToQuery(query, statement string) bool {
terms := termPattern.FindAllString(strings.ToLower(query), -1)
content := strings.ToLower(statement)
productCue := containsSearchTerm(terms, "видеокарт", "gpu", "процессор", "телефон", "ноутбук", "монитор")
if productCue && hasNumericSearchTerm(terms) {
contentTerms := termPattern.FindAllString(content, -1)
return hasNumericSearchTerm(contentTerms) && containsSearchTerm(contentTerms, "видеокарт", "gpu", "процессор", "телефон", "ноутбук", "монитор")
}
usable := 0
for _, term := range terms {
if _, stop := searchStopTerms[term]; stop || len([]rune(term)) < 4 {
continue
}
usable++
runes := []rune(term)
prefixLen := 5
if len(runes) < prefixLen {
prefixLen = len(runes)
}
if strings.Contains(content, string(runes[:prefixLen])) {
return true
}
}
return usable == 0
}
func containsSearchTerm(values []string, wanted ...string) bool {
for _, value := range values {
for _, candidate := range wanted {
if strings.Contains(value, candidate) {
return true
}
}
}
return false
}
func hasNumericSearchTerm(values []string) bool {
for _, value := range values {
for _, runeValue := range value {
if runeValue >= '0' && runeValue <= '9' {
return true
}
}
}
return false
}
func enrichedSearchQuery(query string) string {
trimmed := strings.TrimSpace(query)
lower := strings.ToLower(trimmed)
if (strings.Contains(lower, "цен") || strings.Contains(lower, "стоим")) && !strings.Contains(lower, "куп") {
return trimmed + " цена купить сегодня"
}
return trimmed
}
func allowedHost(hosts map[string]struct{}, host string) bool { _, ok := hosts[host]; return ok }
func (c SearchConnector) limit() int {
if c.MaxResults <= 0 || c.MaxResults > 8 {
return 3
}
return c.MaxResults
}
func cleanHTML(value string) string {
return strings.Join(strings.Fields(html.UnescapeString(tagPattern.ReplaceAllString(value, ""))), " ")
}
func extractHref(tag string) string {
match := regexp.MustCompile(`(?is)\bhref=["']([^"']+)["']`).FindStringSubmatch(tag)
if len(match) == 2 {
return match[1]
}
return ""
}
func resolveSearchResultLocator(raw string, searchEndpoint *url.URL) string {
value := strings.TrimSpace(html.UnescapeString(raw))
if value == "" {
return searchEndpoint.String()
}
parsed, err := url.Parse(value)
if err != nil {
return searchEndpoint.String()
}
if target := parsed.Query().Get("uddg"); target != "" {
value = target
parsed, err = url.Parse(target)
if err != nil {
return searchEndpoint.String()
}
}
if target := parsed.Query().Get("u"); strings.HasPrefix(target, "a1") {
if decoded, decodeErr := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(target, "a1")); decodeErr == nil {
if targetURL, parseErr := url.Parse(string(decoded)); parseErr == nil {
parsed = targetURL
}
}
}
if parsed.Scheme == "" {
parsed.Scheme = "https"
}
if parsed.Host == "" && strings.HasPrefix(value, "//") {
parsed, err = url.Parse("https:" + value)
}
if err != nil || parsed.Scheme != "https" || parsed.Host == "" {
return searchEndpoint.String()
}
return parsed.String()
}
func sourcePublisher(locator, fallback string) string {
if parsed, err := url.Parse(locator); err == nil && parsed.Hostname() != "" {
return parsed.Hostname()
}
return fallback
}
@@ -0,0 +1,57 @@
package research
import (
"context"
"crypto/tls"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/epistemics"
)
func TestSearchConnectorFallsBackToAnotherAllowedSource(t *testing.T) {
primary := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`<html><body>no usable results</body></html>`))
}))
defer primary.Close()
fallback := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`<a class="result__a" href="https://example.test">Казань: текущая температура 18 °C</a><a class="result__snippet">Наблюдается переменная облачность.</a>`))
}))
defer fallback.Close()
client := fallback.Client()
client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // test servers only
request := epistemics.WebSearchRequest{Plan: epistemics.ResearchPlan{ID: "plan:test", Question: "какая погода в Казани", SourceClasses: []string{"web-search"}, FreshnessPolicy: string(epistemics.FreshnessCurrent)}, PropositionKey: "question:test", Query: "какая погода в Казани"}
connector := SearchConnector{
Client: client,
EndpointTemplate: primary.URL + "?q=%s",
AllowedHosts: map[string]struct{}{strings.TrimPrefix(primary.URL, "https://"): {}},
FallbackEndpoints: []SearchEndpoint{{
Template: fallback.URL + "?q=%s",
AllowedHosts: map[string]struct{}{strings.TrimPrefix(fallback.URL, "https://"): {}},
}},
}
// The allowlist is hostname-only; test servers include an ephemeral port.
connector.AllowedHosts = map[string]struct{}{strings.Split(strings.TrimPrefix(primary.URL, "https://"), ":")[0]: {}}
connector.FallbackEndpoints[0].AllowedHosts = map[string]struct{}{strings.Split(strings.TrimPrefix(fallback.URL, "https://"), ":")[0]: {}}
results, err := connector.Collect(context.Background(), request, 1)
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || !strings.Contains(results[0].Statement, "18") {
t.Fatalf("fallback evidence = %#v", results)
}
if results[0].Source.Locator != "https://example.test" || results[0].Source.Publisher != "example.test" {
t.Fatalf("search result source = %#v", results[0].Source)
}
}
func TestEnrichedSearchQueryAddsPriceSignalsOnlyToPriceRequests(t *testing.T) {
if got := enrichedSearchQuery("проверь цену на видеокарту 4090"); got != "проверь цену на видеокарту 4090 цена купить сегодня" {
t.Fatalf("price query = %q", got)
}
if got := enrichedSearchQuery("какая погода в Казани"); got != "какая погода в Казани" {
t.Fatalf("non-price query = %q", got)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -32,7 +32,7 @@ The system already has the correct outbound mutation foundation: Commit owns aut
## Non-goals
- No Gmail, MCP, Codex Bridge, browser automation, mobile app, voice, or self-extension loop.
- No Gmail, MCP, Codex Bridge, mobile app, voice, or self-extension loop. Browser automation is allowed only under ADR-0019 as bounded read-only observation; it is not a human-surface command or mutation channel.
- No new canonical owner.
- No generic scheduler/event-bus/autonomous-agent loop.
- No new third-party dependency.
@@ -30,7 +30,7 @@ Examples include: a reminder after notification, waiting work requiring a decisi
## Non-goals
- No Codex Bridge, Gmail, MCP, voice, mobile, browser automation, self-extension loop, or generalized orchestration.
- No Codex Bridge, Gmail, MCP, voice, mobile, self-extension loop, or generalized orchestration. Browser observation may be used only through the bounded read-only capability defined by ADR-0019 and cannot continue or mutate a response target.
- No universal object ontology.
- No reminder-specific parallel subsystem.
@@ -0,0 +1,34 @@
# ADR-0019 — Bounded Site-Browser Observation
Status: Accepted
Task: DEV-000293
## Context
Search snippets are insufficient for dynamic pages such as marketplaces: they may expose a category page while hiding the current product price behind client-side rendering. Domovoy needs a replaceable observation capability that can inspect an allowlisted rendered page without becoming an account agent, universal scraper, or external mutation path.
## Decision
1. Browser automation is allowed only as a server-owned, read-only observation capability behind the existing Capability and research adapter boundaries.
2. The browser runtime may navigate only to an explicit HTTPS host allowlist and bounded navigation policy. Browser-supplied or model-supplied authority, credentials, proxy settings, cookies, headers, download paths, or arbitrary JavaScript are rejected.
3. The capability may load public pages and wait for bounded client-side rendering. It may read visible text and explicitly declared structured-data selectors, then return an Epistemics Observation with source URL, observed time, coverage, and failure state.
4. Login, cookies, account state, CAPTCHA solving, checkout, form submission, uploads, downloads, purchases, messages, mutations, and external side effects are out of scope.
5. A blocked page, challenge, timeout, partial DOM, missing price, or conflicting value remains `UNKNOWN`, `PARTIAL`, or `NOT_OBSERVED`; it never becomes a verified fact by model interpretation alone.
6. Browser observation is optional. Search-only routing remains the fallback when the capability is unavailable, degraded, or not allowlisted for the requested host.
7. Capability owns availability, host policy, health and limits. Epistemics owns the resulting observation and provenance. No new canonical owner or database schema is introduced.
8. The first implementation may be development-only. Production activation requires independent capability health, allowlist evidence, adversarial navigation tests, and a live read-only smoke test.
## Boundaries
- Core/domain code does not import a browser or provider runtime.
- The browser adapter performs only bounded GET/navigation observation and cannot reach Commit/Executor.
- Secrets remain outside the browser runtime and its artifacts, logs, screenshots, page content and chat context.
- An observation is evidence for answer synthesis, not an instruction to mutate a site or canonical state.
## Consequences
- Dynamic public pages can provide stronger evidence than search snippets when the site permits it.
- Site-specific selectors and host policy are replaceable adapter configuration, not semantic domain logic.
- Browser unavailability is a capability gap, not a runtime-wide outage.
- The old blanket prohibition on browser automation is replaced by this narrower, testable boundary.
@@ -0,0 +1,45 @@
# ADR-0020 — Architecture Generation 2 governance adoption
**Status:** Accepted by `ARCH-GEN2-000`
**Date:** 2026-08-29
**Scope:** Architecture and governance only; no runtime or schema migration.
## Decision
Assistant adopts the documents under `docs/DOMOVOY_CONSTITUTION.md`,
`architecture/GEN2_TARGET_ARCHITECTURE.md`,
`architecture/GEN2_CANONICAL_DATA_MODEL.md`,
`development/GEN2_MIGRATION_MAP.md`,
`development/GEN2_MASTER_PLAN.md`, and the execution contract as the
canonical Architecture Generation 2 governance basis.
The existing V1 implementation remains the rollback and compatibility
baseline. This decision authorizes subsequent, separately accepted Gen2
packets to introduce the World, Configuration, and SELF bounded owners and
Gen2 migrations, subject to explicit ownership, schema, contract, and
rollback decisions in those packets. It does not create those owners or
change canonical runtime state in this task.
## Authority and execution order
The Gen2 constitution and accepted architecture decisions govern the Gen2
program. The target architecture, canonical data model, migration map,
master plan, execution contract, task index, and individual task packet are
interpreted in that order. A lower-level packet cannot silently weaken a
higher-level invariant or reorder the task index.
Every Gen2 task starts from `gen2-integration`, uses the active task pointer,
and passes the declared FAST/MILESTONE/FULL gate before merge. `ARCH-GEN2-000`
is the architecture adoption root accepted against the PRE baseline;
`GEN2-001` and later tasks are not accepted by this decision and require
separate execution.
## Invariants preserved
- No new canonical owner, cross-module dependency, privilege, database role,
third-party dependency, runtime writer, event, or migration is introduced.
- Existing Authority → Commit → Executor boundaries remain mandatory.
- Memory remains a rebuildable retrieval fabric, World is not a generic truth
owner, and SELF is not a monolithic orchestrator.
- The `domovoy-pre-gen2` baseline tag remains immutable and is the rollback
point until a later migration task explicitly establishes another one.
+53 -1
View File
@@ -1,7 +1,7 @@
{
"registry_version": 1,
"semantic_generation": 1,
"architecture_generation": 1,
"architecture_generation": 2,
"modules": {
"identity": {
"id": "mod.identity",
@@ -179,6 +179,30 @@
"database_schema": "interaction",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.identity", "contracts.situation", "contracts.epistemics", "contracts.cognitive", "platform.ids", "platform.events"]
},
"world": {
"id": "mod.world",
"owns": ["WorldObject", "WorldObjectKindDefinition", "WorldAlias", "FacetDefinition", "RelationDefinition", "IdentityResolutionCandidate", "IdentityResolutionDecision", "WorldLineage"],
"database_schema": "world",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.identity", "platform.ids", "platform.events"],
"forbidden": ["adapters.models", "adapters.temporal", "claims", "beliefs", "observations"]
},
"configuration": {
"id": "mod.configuration",
"owns": ["SettingDefinition", "SettingAssignment", "ConfigurationRevision", "ConfigurationChangeProvenance"],
"database_schema": "configuration",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.identity", "contracts.authority", "platform.ids", "platform.events", "platform.secrets"],
"forbidden": ["secret_values", "arbitrary_key_value_without_definition"]
},
"self": {
"id": "mod.self",
"owns": ["CompetenceAssessment", "WeaknessPattern", "DependencyAssessment", "LearningLedgerEntry", "ImprovementNeed", "SelfVersionView", "SelfEvaluationInterpretation"],
"database_schema": "self",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.capability", "contracts.epistemics", "contracts.artifact", "platform.ids", "platform.events"],
"forbidden": ["adapters.models", "tasks", "authority_grants", "code_release_switching"]
}
},
"platform": {
@@ -244,6 +268,26 @@
"cross_owner_write": "forbidden",
"cross_owner_application_join": "forbidden"
},
"runtime_topology_v2": {
"core_runtime": {
"modules": ["identity", "situation", "work", "epistemics", "capability", "artifact", "interaction", "world", "configuration", "self"]
},
"continuity_runtime": {"modules": ["continuity"]},
"commit_runtime": {"modules": ["authority", "commit"]},
"cognitive_workers": {"modules": [], "role": "replaceable reasoning/context workers; no canonical ownership"},
"supervisor_runtime": {"modules": [], "role": "release health, switch, rollback and recovery; not a semantic owner"}
},
"storage_topology_v2": {
"postgres_cluster": "assistant_primary",
"schemas": {
"identity": "identity_role", "authority": "authority_role", "situation": "situation_role", "work": "work_role",
"epistemics": "epistemics_role", "continuity": "continuity_role", "capability": "capability_role", "commit": "commit_role",
"artifact": "artifact_role", "interaction": "interaction_role", "world": "world_role", "configuration": "configuration_role", "self": "self_role"
},
"cross_owner_fk": "forbidden_by_default",
"cross_owner_write": "forbidden",
"cross_owner_application_join": "forbidden"
},
"invariants": [
{
"id": "INV-AUTH-001",
@@ -309,6 +353,14 @@
".gitea/workflows/ci.yml",
"cmd/devctl/**",
"dev/ACTIVE_TASK.json",
"docs/DOMOVOY_CONSTITUTION.md",
"docs/DOMOVOY_GEN2_GAP_MAP.md",
"architecture/GEN2_TARGET_ARCHITECTURE.md",
"architecture/GEN2_CANONICAL_DATA_MODEL.md",
"development/GEN2_MIGRATION_MAP.md",
"development/GEN2_MASTER_PLAN.md",
"development/GEN2_EXECUTION_CONTRACT.md",
"development/GEN2_TASK_INDEX.md",
"security/**"
],
"default_feature_task_authority": {
+67 -1
View File
@@ -1,6 +1,6 @@
registry_version: 1
semantic_generation: 1
architecture_generation: 1
architecture_generation: 2
modules:
identity:
@@ -75,6 +75,30 @@ modules:
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.situation, contracts.epistemics, contracts.cognitive, platform.ids, platform.events]
world:
id: mod.world
owns: [WorldObject, WorldObjectKindDefinition, WorldAlias, FacetDefinition, RelationDefinition, IdentityResolutionCandidate, IdentityResolutionDecision, WorldLineage]
database_schema: world
boundary_class: remote_capable
may_depend_on: [contracts.identity, platform.ids, platform.events]
forbidden: [adapters.models, adapters.temporal, claims, beliefs, observations]
configuration:
id: mod.configuration
owns: [SettingDefinition, SettingAssignment, ConfigurationRevision, ConfigurationChangeProvenance]
database_schema: configuration
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, platform.ids, platform.events, platform.secrets]
forbidden: [secret_values, arbitrary_key_value_without_definition]
self:
id: mod.self
owns: [CompetenceAssessment, WeaknessPattern, DependencyAssessment, LearningLedgerEntry, ImprovementNeed, SelfVersionView, SelfEvaluationInterpretation]
database_schema: self
boundary_class: remote_capable
may_depend_on: [contracts.capability, contracts.epistemics, contracts.artifact, platform.ids, platform.events]
forbidden: [adapters.models, tasks, authority_grants, code_release_switching]
platform:
allowed_packages:
- platform.ids
@@ -98,6 +122,20 @@ runtime_topology_v1:
modules: []
role: "replaceable cognition/context compilation workers; no canonical ownership"
runtime_topology_v2:
core_runtime:
modules: [identity, situation, work, epistemics, capability, artifact, interaction, world, configuration, self]
continuity_runtime:
modules: [continuity]
commit_runtime:
modules: [authority, commit]
cognitive_workers:
modules: []
role: "replaceable reasoning/context workers; no canonical ownership"
supervisor_runtime:
modules: []
role: "release health, switch, rollback and recovery; not a semantic owner"
storage_topology_v1:
postgres_cluster: assistant_primary
schemas:
@@ -115,6 +153,26 @@ storage_topology_v1:
cross_owner_write: forbidden
cross_owner_application_join: forbidden
storage_topology_v2:
postgres_cluster: assistant_primary
schemas:
identity: identity_role
authority: authority_role
situation: situation_role
work: work_role
epistemics: epistemics_role
continuity: continuity_role
capability: capability_role
commit: commit_role
artifact: artifact_role
interaction: interaction_role
world: world_role
configuration: configuration_role
self: self_role
cross_owner_fk: forbidden_by_default
cross_owner_write: forbidden
cross_owner_application_join: forbidden
invariants:
- id: INV-AUTH-001
property: "No external mutation without current valid authority and target/revision checks"
@@ -149,6 +207,14 @@ protected_paths:
- .gitea/workflows/ci.yml
- cmd/devctl/**
- dev/ACTIVE_TASK.json
- docs/DOMOVOY_CONSTITUTION.md
- docs/DOMOVOY_GEN2_GAP_MAP.md
- architecture/GEN2_TARGET_ARCHITECTURE.md
- architecture/GEN2_CANONICAL_DATA_MODEL.md
- development/GEN2_MIGRATION_MAP.md
- development/GEN2_MASTER_PLAN.md
- development/GEN2_EXECUTION_CONTRACT.md
- development/GEN2_TASK_INDEX.md
- security/**
default_feature_task_authority:
+211
View File
@@ -0,0 +1,211 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/artifact"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/work"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
)
type bridge struct {
cognitiveURL string
callbackURL string
verifierURL string
client *http.Client
}
func main() {
b := bridge{
cognitiveURL: strings.TrimRight(envOr("COGNITIVE_WORKER_URL", "http://cognitive-worker:8083"), "/"),
callbackURL: strings.TrimRight(envOr("WORK_GRAPH_RUNTIME_URL", "http://work-graph-runtime:8088"), "/"),
verifierURL: strings.TrimRight(envOr("TASK_VERIFIER_URL", "http://task-verifier:8091"), "/"),
client: &http.Client{Timeout: 10 * time.Minute},
}
mux := http.NewServeMux()
mux.HandleFunc("/healthz", healthHandler)
mux.HandleFunc("/readyz", healthHandler)
mux.HandleFunc("/v1/execute", b.executeHandler)
addr := envOr("LISTEN_ADDR", ":8092")
if err := http.ListenAndServe(addr, mux); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func (b bridge) executeHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var request work.AgentExecutionRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
http.Error(w, "invalid execution request", http.StatusBadRequest)
return
}
result, err := b.execute(r.Context(), request)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
}
func (b bridge) execute(ctx context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
now := time.Now().UnixMilli()
intent := request.Intent
if intent.TaskEvaluationExecution != nil && intent.TaskEvaluationExecution.GenericMessage != "" {
return b.executeGenericCognitiveWork(ctx, request, *intent.TaskEvaluationExecution)
}
if intent.TaskEvaluationExecution == nil || intent.TaskEvaluationProfile == nil || len(intent.TaskEvaluationCases) == 0 {
return work.AgentExecutionResult{State: work.AgentExecutionWaiting, Summary: "локальный bridge не поддерживает это намерение; работа сохранена и ожидает подходящего исполнителя", ObservedAtUnixMillis: now}, nil
}
execution := intent.TaskEvaluationExecution
if execution.GraphID == "" || execution.NodeID == "" || execution.BindingRevision < 1 || execution.ActorID == "" || execution.PrincipalID == "" || execution.ScopeID == "" {
return work.AgentExecutionResult{State: work.AgentExecutionWaiting, Summary: "контекст task-evaluation неполон; работа сохранена для повторной маршрутизации", ObservedAtUnixMillis: now}, nil
}
payload := capability.TaskEvaluationExecutionRequest{Profile: *intent.TaskEvaluationProfile, CapabilityID: intent.TaskEvaluationProfile.CapabilityID, BindingID: intent.TaskEvaluationBindingID, BindingRevision: execution.BindingRevision, ActorID: execution.ActorID, PrincipalID: execution.PrincipalID, ScopeID: execution.ScopeID, Cases: intent.TaskEvaluationCases, MaxOutputBytes: 4096}
response, err := postJSON[capability.TaskEvaluationExecutionResponse](ctx, b.client, b.cognitiveURL+"/v1/task-evaluations/execute", payload)
if err != nil {
return work.AgentExecutionResult{}, err
}
if len(response.Responses) == 0 || response.ObservedAtUnixMillis < 1 {
return work.AgentExecutionResult{}, fmt.Errorf("task evaluation returned no observations")
}
caseByID := make(map[string]capability.TaskEvaluationCase, len(intent.TaskEvaluationCases))
for _, item := range intent.TaskEvaluationCases {
caseByID[item.CaseID] = item
}
observations := response.Observations
if !hasCriticalFailure(observations) {
observations = make([]capability.TaskCaseObservation, 0, len(response.Responses))
for _, item := range response.Responses {
caseValue, ok := caseByID[item.CaseID]
if !ok || caseValue.Verification.Mode == "" {
return work.AgentExecutionResult{State: work.AgentExecutionWaiting, Summary: "task-evaluation case has no bounded verifier; work remains waiting", ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
}
verified, verifyErr := postJSON[capability.TaskCaseVerificationResponse](ctx, b.client, b.verifierURL+"/v1/task-evaluations/verify", capability.TaskCaseVerificationRequest{CaseID: item.CaseID, ResponseText: item.ResponseText, LatencyMicros: item.LatencyMicros, EvidenceArtifactRef: item.EvidenceArtifactRef, Verification: caseValue.Verification})
if verifyErr != nil {
return work.AgentExecutionResult{}, verifyErr
}
observations = append(observations, capability.TaskCaseObservation{CaseID: verified.CaseID, Result: verified.Result, QualityScoreMillis: verified.QualityScoreMillis, LatencyMicros: verified.LatencyMicros, CriticalFailure: verified.CriticalFailure, EvidenceArtifactRef: verified.EvidenceArtifactRef, VerificationEvidenceRef: verified.VerificationEvidenceRef, VerifierIdentity: verified.VerifierIdentity})
}
}
evaluation, err := capabilitydomain.BuildTaskEvaluation(*intent.TaskEvaluationProfile, capability.TaskEvaluationID("evaluation:"+intent.DispatchID), response.BindingID, response.BindingRevision, intent.TaskEvaluationProfile.CapabilityID, observations, artifact.ArtifactID(intent.TaskEvaluationCases[0].EvidenceArtifactRef), response.ObservedAtUnixMillis)
if err != nil {
return work.AgentExecutionResult{}, err
}
callback := work.TaskEvaluationEvidenceCallback{GraphID: execution.GraphID, Revision: execution.GraphRevision, NodeID: execution.NodeID, Evaluation: evaluation, Succeeded: true}
if _, err := postJSON[json.RawMessage](ctx, b.client, b.callbackURL+"/v1/work-graphs/task-evaluations/callback", callback); err != nil {
return work.AgentExecutionResult{}, err
}
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "local bridge executed task evaluation and persisted verifier callback", OutcomeRef: string(evaluation.ID), ArtifactRefs: []string{string(evaluation.ArtifactID)}, ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
}
func (b bridge) executeGenericCognitiveWork(ctx context.Context, request work.AgentExecutionRequest, execution work.TaskEvaluationDispatchContext) (work.AgentExecutionResult, error) {
if execution.GraphID == "" || execution.NodeID == "" || execution.ActorID == "" || execution.PrincipalID == "" || execution.ScopeID == "" {
return work.AgentExecutionResult{State: work.AgentExecutionWaiting, Summary: "generic cognitive work context is incomplete; durable work remains waiting for rerouting", ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
}
maxOutputBytes := execution.GenericMaxOutputBytes
if maxOutputBytes < 1 {
maxOutputBytes = 4096
}
dataPolicy := capability.DataPolicy(execution.GenericDataPolicy)
if dataPolicy == "" {
dataPolicy = capability.DataPolicyLocalOnly
}
response, err := postJSON[cognitive.GatewayAdvisoryResponse](ctx, b.client, b.cognitiveURL+"/v1/advisories", cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID("agent-work:" + request.Intent.DispatchID), ActorID: execution.ActorID,
PrincipalID: execution.PrincipalID, ScopeID: execution.ScopeID, Message: execution.GenericMessage, Locale: execution.GenericLocale,
MaxOutputBytes: maxOutputBytes, DataPolicy: dataPolicy, Meaning: cognitive.TaskMeaning{SpeechAct: "DIRECTIVE", RequestedOutcome: execution.GenericPurpose},
})
if err != nil {
return work.AgentExecutionResult{}, err
}
if response.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(response.AdvisoryText) == "" {
return work.AgentExecutionResult{}, fmt.Errorf("generic cognitive work did not complete: %s", response.FinishReason)
}
candidateID := "candidate:" + request.Intent.DispatchID
artifactRef := work.EvidenceRef("agent-output:" + request.Intent.DispatchID)
verification, err := postJSON[work.GenericCandidateVerificationResponse](ctx, b.client, b.verifierURL+"/v1/work-graphs/candidates/verify", work.GenericCandidateVerificationRequest{
GraphID: execution.GraphID, GraphRevision: execution.GraphRevision, NodeID: execution.NodeID, AuthorRunID: request.Intent.RunID, CandidateID: candidateID, Summary: response.AdvisoryText, EvidenceRefs: []work.EvidenceRef{artifactRef},
})
if err != nil {
return work.AgentExecutionResult{}, err
}
if !verification.Verified || verification.VerificationEvidenceRef == "" || verification.VerifierIdentity == "" {
return work.AgentExecutionResult{State: work.AgentExecutionWaiting, Summary: "generic candidate envelope was not independently verified; work remains waiting", ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
}
candidate := work.GraphResultCallback{GraphID: execution.GraphID, Revision: execution.GraphRevision, Candidate: work.ResultCandidate{
ID: candidateID, GraphID: execution.GraphID, GraphRevision: execution.GraphRevision, NodeID: execution.NodeID,
AuthorRunID: request.Intent.RunID, ArtifactRefs: []work.EvidenceRef{artifactRef, work.EvidenceRef(verification.VerificationEvidenceRef)}, EvidenceSummary: "candidate attribution and evidence envelope independently verified; semantic content remains unverified",
Summary: response.AdvisoryText,
}}
if _, err := postJSON[json.RawMessage](ctx, b.client, b.callbackURL+"/v1/work-graphs/result-callback", candidate); err != nil {
return work.AgentExecutionResult{}, err
}
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "local cognitive specialist produced a candidate for independent verification", OutcomeRef: "candidate:" + request.Intent.DispatchID, ArtifactRefs: []string{"agent-output:" + request.Intent.DispatchID}, ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
}
func hasCriticalFailure(observations []capability.TaskCaseObservation) bool {
for _, observation := range observations {
if observation.CriticalFailure {
return true
}
}
return false
}
func postJSON[T any](ctx context.Context, client *http.Client, url string, payload any) (T, error) {
var result T
body, err := json.Marshal(payload)
if err != nil {
return result, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return result, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return result, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return result, fmt.Errorf("POST %s: status %s", url, resp.Status)
}
if resp.StatusCode == http.StatusNoContent {
return result, nil
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return result, err
}
return result, nil
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"runtime":"agent-execution-bridge","status":"ok","capabilities":["task-evaluation","generic-cognitive-work"]}`))
}
func envOr(name, fallback string) string {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
return fallback
}
+88
View File
@@ -0,0 +1,88 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/work"
)
func TestBridgeWaitsForUnsupportedIntent(t *testing.T) {
b := bridge{client: http.DefaultClient}
result, err := b.execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-unsupported"}})
if err != nil {
t.Fatal(err)
}
if result.State != work.AgentExecutionWaiting || result.ObservedAtUnixMillis < 1 {
t.Fatalf("result=%+v", result)
}
}
func TestHealthHandlerExposesBoundedCapability(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/healthz", nil)
healthHandler(recorder, request)
if recorder.Code != http.StatusOK || recorder.Body.String() == "" {
t.Fatalf("status=%d body=%q", recorder.Code, recorder.Body.String())
}
}
func TestBridgeRunsTaskEvaluationThroughVerifierAndCallback(t *testing.T) {
profile := capability.TaskEvaluationProfile{ID: "profile-1", Revision: 1, CapabilityID: "capability-1", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-1"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
verification := capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/task-evaluations/execute":
_ = json.NewEncoder(w).Encode(capability.TaskEvaluationExecutionResponse{ProfileID: profile.ID, BindingID: "binding-1", BindingRevision: 3, Responses: []capability.TaskCaseResponse{{CaseID: "case-1", ResponseText: "answer", LatencyMicros: 2, EvidenceArtifactRef: "artifact-1"}}, ObservedAtUnixMillis: 10})
case "/v1/task-evaluations/verify":
_ = json.NewEncoder(w).Encode(capability.TaskCaseVerificationResponse{CaseID: "case-1", Result: capability.EvaluationSupported, QualityScoreMillis: 900, LatencyMicros: 2, EvidenceArtifactRef: "artifact-1", VerificationEvidenceRef: "verify-1", VerifierIdentity: "verifier-1"})
case "/v1/work-graphs/task-evaluations/callback":
w.WriteHeader(http.StatusNoContent)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
b := bridge{cognitiveURL: server.URL, callbackURL: server.URL, verifierURL: server.URL, client: server.Client()}
result, err := b.execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1", TaskEvaluationBindingID: "binding-1", TaskEvaluationProfile: &profile, TaskEvaluationCases: []capability.TaskEvaluationCase{{CaseID: "case-1", Prompt: "answer", EvidenceArtifactRef: "artifact-1", Verification: verification}}, TaskEvaluationExecution: &work.TaskEvaluationDispatchContext{GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", BindingRevision: 3, ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1"}}})
if err != nil || result.State != work.AgentExecutionCompleted || result.OutcomeRef == "" {
t.Fatalf("result=%+v err=%v", result, err)
}
}
func TestBridgeRunsGenericCognitiveWorkAsUnverifiedGraphCandidate(t *testing.T) {
var callback work.GraphResultCallback
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/advisories":
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil || request.Message != "find the bounded solution" {
t.Fatalf("unexpected advisory request: %+v err=%v", request, err)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "candidate answer", FinishReason: cognitive.FinishCompleted})
case "/v1/work-graphs/result-callback":
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil {
t.Fatal(err)
}
w.WriteHeader(http.StatusNoContent)
case "/v1/work-graphs/candidates/verify":
_ = json.NewEncoder(w).Encode(work.GenericCandidateVerificationResponse{Verified: true, VerificationEvidenceRef: "verification:generic-1", VerifierIdentity: "deterministic-generic-envelope-verifier-v1", Summary: "envelope verified"})
default:
http.NotFound(w, r)
}
}))
defer server.Close()
b := bridge{cognitiveURL: server.URL, callbackURL: server.URL, verifierURL: server.URL, client: server.Client()}
result, err := b.execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{
DispatchID: "dispatch-generic", RunID: "run-generic",
TaskEvaluationExecution: &work.TaskEvaluationDispatchContext{GraphID: "graph-1", GraphRevision: 2, NodeID: "node-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", GenericMessage: "find the bounded solution", GenericPurpose: "investigate", GenericDataPolicy: string(capability.DataPolicyLocalOnly)},
}})
if err != nil || result.State != work.AgentExecutionCompleted || callback.Candidate.Summary != "candidate answer" || callback.Candidate.AuthorRunID != "run-generic" || len(callback.Candidate.ArtifactRefs) != 2 {
t.Fatalf("result=%+v callback=%+v err=%v", result, callback, err)
}
}
+14 -1
View File
@@ -42,7 +42,7 @@ func main() {
defer repository.Close()
var executor ports.AgentExecutor
localFallback := false
if bridgeURL, bridgeConfigured := loadProtectedBridgeURL(); bridgeConfigured {
if bridgeURL, bridgeConfigured := loadConfiguredBridgeURL(); bridgeConfigured {
executor, err = agentbridge.NewHTTPExecutor(bridgeURL, &http.Client{Timeout: *leaseTTL})
if err != nil {
fmt.Fprintln(os.Stderr, err)
@@ -99,6 +99,19 @@ func main() {
}
}
func loadConfiguredBridgeURL() (string, bool) {
if bridgeURL, configured := loadProtectedBridgeURL(); configured {
return bridgeURL, true
}
// The URL is routing configuration, not a credential. A protected file
// remains preferred when an independently operated bridge is supplied.
value := strings.TrimSpace(os.Getenv("AGENT_EXECUTION_BRIDGE_URL"))
if value == "" || !strings.HasPrefix(value, "http://") && !strings.HasPrefix(value, "https://") {
return "", false
}
return value, true
}
func loadProtectedBridgeURL() (string, bool) {
return loadProtectedBridgeURLFrom("/run/secrets", "/run/secrets/agent_execution_bridge_url")
}
+3 -1
View File
@@ -94,13 +94,15 @@ func workerStatusHandler(executorConfigured bool, localFallback ...interface{})
}
if local {
baseStatus.Mode = "local-executor"
baseStatus.Reason = "bounded local task-evaluation executor is active; unsupported work remains durably waiting"
baseStatus.Ready = true
baseStatus.Reason = "bounded local task-evaluation capability is ready; unsupported work remains durably waiting"
baseStatus.Capabilities = []string{"task-evaluation"}
}
if executorConfigured {
baseStatus.Mode = "execution"
baseStatus.Ready = true
baseStatus.Reason = "typed agent execution bridge configured"
baseStatus.Capabilities = []string{"task-evaluation", "generic-cognitive-work"}
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || (r.URL.Path != "/healthz" && r.URL.Path != "/readyz" && r.URL.Path != "/status") {
+9 -4
View File
@@ -39,14 +39,14 @@ func TestWorkerStatusReportsRecoveryOnlyWithoutExecutor(t *testing.T) {
}
}
func TestWorkerStatusReportsLocalExecutor(t *testing.T) {
func TestWorkerStatusReportsLocalExecutorCapabilityReady(t *testing.T) {
response := httptest.NewRecorder()
workerStatusHandler(false, true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
var value workerStatusResponse
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
t.Fatal(err)
}
if response.Code != http.StatusOK || value.Ready || value.Mode != "local-executor" || !value.LocalFallback || !value.LocalExecutor {
if response.Code != http.StatusOK || !value.Ready || value.Mode != "local-executor" || !value.LocalFallback || !value.LocalExecutor {
t.Fatalf("unexpected status: code=%d value=%+v", response.Code, value)
}
if len(value.Capabilities) != 1 || value.Capabilities[0] != "task-evaluation" {
@@ -61,18 +61,23 @@ func TestWorkerStatusDoesNotCallLocalFallbackAnExternalExecutor(t *testing.T) {
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
t.Fatal(err)
}
if value.ExecutorConfigured || value.Ready || !value.LocalFallback || !value.LocalExecutor || value.Mode != "local-executor" {
if value.ExecutorConfigured || !value.Ready || !value.LocalFallback || !value.LocalExecutor || value.Mode != "local-executor" {
t.Fatalf("local fallback was presented as external execution: %+v", value)
}
}
func TestWorkerReadyzRequiresExecutor(t *testing.T) {
func TestWorkerReadyzRequiresAnAdvertisedCapability(t *testing.T) {
response := httptest.NewRecorder()
workerStatusHandler(false).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
if response.Code != http.StatusServiceUnavailable {
t.Fatalf("readyz status=%d", response.Code)
}
response = httptest.NewRecorder()
workerStatusHandler(false, true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
if response.Code != http.StatusOK {
t.Fatalf("local capability readyz status=%d", response.Code)
}
response = httptest.NewRecorder()
workerStatusHandler(true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
if response.Code != http.StatusOK {
t.Fatalf("configured readyz status=%d", response.Code)
@@ -50,6 +50,17 @@ func (e taskEvaluationExecutor) Execute(ctx context.Context, request work.AgentE
if len(response.Responses) == 0 || response.ObservedAtUnixMillis < 1 {
return work.AgentExecutionResult{}, fmt.Errorf("task evaluation returned no observations")
}
if len(response.Observations) == len(intent.TaskEvaluationCases) && taskEvaluationHasCriticalFailure(response.Observations) {
evaluation, err := capabilitydomain.BuildTaskEvaluation(*intent.TaskEvaluationProfile, capability.TaskEvaluationID("evaluation:"+intent.DispatchID), response.BindingID, response.BindingRevision, intent.TaskEvaluationProfile.CapabilityID, response.Observations, artifact.ArtifactID(intent.TaskEvaluationCases[0].EvidenceArtifactRef), response.ObservedAtUnixMillis)
if err != nil {
return work.AgentExecutionResult{}, err
}
callback := work.TaskEvaluationEvidenceCallback{GraphID: execution.GraphID, Revision: execution.GraphRevision, NodeID: execution.NodeID, Evaluation: evaluation, Succeeded: true}
if _, err := postJSON[json.RawMessage](ctx, e.client, e.callbackURL+"/v1/work-graphs/task-evaluations/callback", callback); err != nil {
return work.AgentExecutionResult{}, err
}
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "task evaluation recorded as UNKNOWN after bounded provider recovery", OutcomeRef: string(evaluation.ID), ArtifactRefs: []string{string(evaluation.ArtifactID)}, ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
}
if len(intent.TaskEvaluationCases) == 0 {
return work.AgentExecutionResult{}, fmt.Errorf("task evaluation cases are missing")
}
@@ -80,6 +91,15 @@ func (e taskEvaluationExecutor) Execute(ctx context.Context, request work.AgentE
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "task evaluation executed, aggregated and accepted by Work Graph callback", OutcomeRef: string(evaluation.ID), ArtifactRefs: []string{string(evaluation.ArtifactID)}, ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
}
func taskEvaluationHasCriticalFailure(observations []capability.TaskCaseObservation) bool {
for _, observation := range observations {
if observation.CriticalFailure {
return true
}
}
return false
}
func postJSON[T any](ctx context.Context, client *http.Client, url string, payload any) (T, error) {
var result T
body, err := json.Marshal(payload)
@@ -41,3 +41,32 @@ func TestTaskEvaluationExecutorInvokesAggregatesAndCallbacks(t *testing.T) {
t.Fatalf("result=%+v err=%v", result, err)
}
}
func TestTaskEvaluationExecutorPersistsUnknownAfterProviderFailure(t *testing.T) {
profile := capability.TaskEvaluationProfile{ID: "profile-unknown", Revision: 1, CapabilityID: "capability-1", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "unknown-v1", RequiredCaseIDs: []string{"case-1"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v1/task-evaluations/execute" {
_ = json.NewEncoder(w).Encode(capability.TaskEvaluationExecutionResponse{ProfileID: profile.ID, BindingID: "binding-1", BindingRevision: 3, Observations: []capability.TaskCaseObservation{{CaseID: "case-1", Result: capability.EvaluationUnknown, CriticalFailure: true, EvidenceArtifactRef: "artifact-1"}}, Responses: []capability.TaskCaseResponse{{CaseID: "case-1", EvidenceArtifactRef: "artifact-1"}}, ObservedAtUnixMillis: 10})
return
}
if r.URL.Path == "/v1/work-graphs/task-evaluations/callback" {
var callback work.TaskEvaluationEvidenceCallback
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil || callback.Evaluation.Result != capability.EvaluationUnknown || !callback.Succeeded {
http.Error(w, "invalid unknown callback", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
return
}
if r.URL.Path == "/v1/task-evaluations/verify" {
t.Errorf("verifier must not be called for a provider failure")
}
http.NotFound(w, r)
}))
defer server.Close()
executor := newTaskEvaluationExecutor(server.URL, server.URL, server.URL, server.Client())
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-unknown", TaskEvaluationBindingID: "binding-1", TaskEvaluationProfile: &profile, TaskEvaluationCases: []capability.TaskEvaluationCase{{CaseID: "case-1", Prompt: "answer", EvidenceArtifactRef: "artifact-1"}}, TaskEvaluationExecution: &work.TaskEvaluationDispatchContext{GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", BindingRevision: 3, ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1"}}})
if err != nil || result.State != work.AgentExecutionCompleted || result.OutcomeRef == "" {
t.Fatalf("result=%+v err=%v", result, err)
}
}
+48 -28
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"strings"
"sync"
"time"
"git.cin.su/m/Assistant/contracts/capability"
@@ -19,6 +20,7 @@ type fleetEndpoint struct {
URL string
Headers map[string]string
Client *http.Client
Timeout time.Duration
}
type fleetStatusResponse struct {
@@ -31,35 +33,16 @@ type fleetStatusResponse struct {
func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier, endpoints []fleetEndpoint) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
members := make([]capability.FleetMember, 0, len(endpoints))
for _, endpoint := range endpoints {
member := capability.FleetMember{ProviderInstanceID: capability.ProviderInstanceID(endpoint.ProviderID), ModelDescriptorID: capability.ModelDescriptorID(endpoint.ModelID), Tier: endpoint.Tier, Reason: "NOT_CONFIGURED"}
if strings.TrimSpace(endpoint.URL) == "" {
members = append(members, member)
continue
}
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, endpoint.URL, nil)
if err == nil {
for name, value := range endpoint.Headers {
request.Header.Set(name, value)
}
probeClient := endpoint.Client
if probeClient == nil {
probeClient = client
}
response, requestErr := probeClient.Do(request)
if requestErr == nil {
response.Body.Close()
if response.StatusCode >= 200 && response.StatusCode < 300 {
member.Ready, member.Reason = true, "AVAILABLE"
} else {
member.Reason = fmt.Sprintf("status=%s", response.Status)
}
} else {
member.Reason = requestErr.Error()
}
}
members = append(members, member)
members = make([]capability.FleetMember, len(endpoints))
var waitGroup sync.WaitGroup
for index, endpoint := range endpoints {
waitGroup.Add(1)
go func(index int, endpoint fleetEndpoint) {
defer waitGroup.Done()
members[index] = probeFleetEndpoint(r.Context(), client, endpoint)
}(index, endpoint)
}
waitGroup.Wait()
readiness, err := fleetdomain.BuildFleetReadiness(time.Now().UnixMilli(), members)
response := fleetStatusResponse{Readiness: readiness, Preferred: preferred}
if err == nil {
@@ -76,4 +59,41 @@ func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier,
})
}
func probeFleetEndpoint(parent context.Context, fallbackClient *http.Client, endpoint fleetEndpoint) capability.FleetMember {
member := capability.FleetMember{ProviderInstanceID: capability.ProviderInstanceID(endpoint.ProviderID), ModelDescriptorID: capability.ModelDescriptorID(endpoint.ModelID), Tier: endpoint.Tier, Reason: "NOT_CONFIGURED"}
if strings.TrimSpace(endpoint.URL) == "" {
return member
}
timeout := endpoint.Timeout
if timeout <= 0 {
timeout = 2 * time.Second
}
ctx, cancel := context.WithTimeout(parent, timeout)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.URL, nil)
if err != nil {
member.Reason = err.Error()
return member
}
for name, value := range endpoint.Headers {
request.Header.Set(name, value)
}
probeClient := endpoint.Client
if probeClient == nil {
probeClient = fallbackClient
}
response, requestErr := probeClient.Do(request)
if requestErr != nil {
member.Reason = requestErr.Error()
return member
}
defer response.Body.Close()
if response.StatusCode >= 200 && response.StatusCode < 300 {
member.Ready, member.Reason = true, "AVAILABLE"
} else {
member.Reason = fmt.Sprintf("status=%s", response.Status)
}
return member
}
func fleetContext(ctx context.Context) context.Context { return ctx }
+42
View File
@@ -5,6 +5,7 @@ import (
"net/http/httptest"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
)
@@ -26,6 +27,29 @@ func TestFleetReadinessSelectsBestAvailablePrimaryTierBeforeLifeboat(t *testing.
}
}
func TestFleetReadinessDoesNotWaitForSlowMembersSerially(t *testing.T) {
slow := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(150 * time.Millisecond)
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer slow.Close()
fast := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
defer fast.Close()
handler := fleetReadinessHandler(fast.Client(), capability.FleetTierFast, []fleetEndpoint{
{ProviderID: "slow", ModelID: "slow-model", Tier: capability.FleetTierMedium, URL: slow.URL},
{ProviderID: "lifeboat", ModelID: "lifeboat-model", Tier: capability.FleetTierLifeboat, URL: fast.URL},
})
started := time.Now()
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil))
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"LIFEBOAT"`) {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
if elapsed := time.Since(started); elapsed >= 300*time.Millisecond {
t.Fatalf("fleet readiness was serialized: elapsed=%s", elapsed)
}
}
func TestFleetReadinessSendsConfiguredProviderHeaders(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer fleet-key" {
@@ -44,3 +68,21 @@ func TestFleetReadinessSendsConfiguredProviderHeaders(t *testing.T) {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
func TestFleetReadinessUsesProviderSpecificTimeout(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(40 * time.Millisecond)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
handler := fleetReadinessHandler(server.Client(), capability.FleetTierMedium, []fleetEndpoint{{
ProviderID: "medium", ModelID: "medium-model", Tier: capability.FleetTierMedium,
URL: server.URL, Client: server.Client(), Timeout: 100 * time.Millisecond,
}})
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil))
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"MEDIUM"`) {
t.Fatalf("provider-specific timeout was not honored: status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
+41 -7
View File
@@ -186,27 +186,61 @@ func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string)
}
func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse {
text := "Я рядом и продолжаю работу во встроенном режиме Домового. Внешний когнитивный маршрут сейчас недоступен, поэтому опираюсь на уже установленный смысл сообщения и локальные возможности."
if request.Meaning.SpeechAct == "RESPONSE_REVIEW" {
const marker = "Черновик ответа:"
if index := strings.LastIndex(request.Message, marker); index >= 0 {
if draft := strings.TrimSpace(request.Message[index+len(marker):]); draft != "" {
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: draft, FinishReason: cognitive.FinishCompleted}
}
}
}
text := "Понял."
identityText := request.Meaning.RequestedOutcome
if identityText == "" {
identityText = request.Message
}
if request.Meaning.SpeechAct == "QUESTION" && isIdentityInquiry(identityText) {
text = "Я — Домовой, ваш помощник."
}
switch request.Meaning.InformationRequestKind {
case interaction.InformationRequestCognitiveModelInventory:
text = "Запрос понят как проверка моделей. Встроенный режим Домового доступен; я могу показать подключённые модели по назначению, качеству, скорости и стоимости. Ключи и пароли в разговор не нужны."
text = "Сейчас не смог проверить подключённые модели."
case interaction.InformationRequestCapabilityInventory:
text = "Запрос понят как проверка возможностей. Я могу объяснить доступные capabilities и отличить то, что уже подключено, от того, что пока является только планом или capability gap."
text = "Сейчас не смог проверить доступные возможности."
case interaction.InformationRequestCognitiveChannelStatus:
text = "Запрос понят как проверка когнитивного канала. Локальный режим остаётся доступен; я объясню наблюдаемое состояние и что можно сделать без ожидания внешней модели."
text = "Локальный канал Домового доступен; подробности состояния дам по запросу."
case interaction.InformationRequestCognitiveRecovery:
text = "Запрос понят как восстановление ИИ. Начинаю с локального маршрута: фиксирую, что недоступно, проверяю безопасные варианты восстановления и сформулирую следующий обратимый шаг."
text = "Встроенный канал сейчас недоступен."
default:
switch request.Meaning.SpeechAct {
case "REQUEST_TO_INVESTIGATE":
text = "Я принял это как исследование. Даже без внешней модели могу сформулировать гипотезы, отделить известное от неизвестного и предложить следующий проверяемый шаг."
text = "Понял задачу, но пока не смог получить проверяемый результат."
case "QUESTION":
text = "Я отвечаю во встроенном режиме Домового. Если для полного ответа не хватает внешней модели, объясню это прямо и продолжу с тем, что можно проверить локально."
if !isIdentityInquiry(identityText) {
text = "Пока не смог получить проверяемый ответ."
}
case "CONVERSATIONAL":
text = "Я — Домовой, ваш помощник."
}
}
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted}
}
func isIdentityInquiry(message string) bool {
tokens := strings.Fields(strings.ToLower(strings.NewReplacer("?", " ", "!", " ", ",", " ").Replace(message)))
hasIdentitySubject := false
hasAssistantReference := false
for _, token := range tokens {
switch token {
case "кто", "зовут", "имя":
hasIdentitySubject = true
case "ты", "тебя", "тебе", "вы", "вас":
hasAssistantReference = true
}
}
return hasIdentitySubject && hasAssistantReference
}
func (g cognitiveGateway) invokeBuiltinAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) cognitive.GatewayAdvisoryResponse {
if g.builtinLocal != nil && g.builtinLocal.Provider != nil {
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
+24 -1
View File
@@ -185,11 +185,34 @@ func TestNoActiveBindingUsesBuiltInProviderWithoutInvocation(t *testing.T) {
func TestBuiltInAdvisoryUsesTrustedInformationIntentInsteadOfMessageKeywords(t *testing.T) {
request := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", Message: "arbitrary text", Meaning: cognitive.TaskMeaning{InformationRequestKind: interaction.InformationRequestCognitiveRecovery}}
response := builtinAdvisory(request)
if !strings.Contains(response.AdvisoryText, "восстановление ИИ") {
if response.AdvisoryText != "Встроенный канал сейчас недоступен." {
t.Fatalf("unexpected recovery response: %q", response.AdvisoryText)
}
}
func TestBuiltInAdvisoryIdentityIsNaturalAndDoesNotMentionResponseLength(t *testing.T) {
response := builtinAdvisory(cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: "request-identity",
Meaning: cognitive.TaskMeaning{SpeechAct: "QUESTION", RequestedOutcome: "ты кто?"},
})
if response.AdvisoryText != "Я — Домовой, ваш помощник." {
t.Fatalf("unexpected identity response: %q", response.AdvisoryText)
}
if strings.Contains(response.AdvisoryText, "коротко") || strings.Contains(response.AdvisoryText, "подробност") {
t.Fatalf("identity response contains style meta-text: %q", response.AdvisoryText)
}
messageResponse := builtinAdvisory(cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: "request-identity-message",
Message: "как тебя зовут?",
Meaning: cognitive.TaskMeaning{SpeechAct: "QUESTION"},
})
if messageResponse.AdvisoryText != "Я — Домовой, ваш помощник." {
t.Fatalf("unexpected message identity response: %q", messageResponse.AdvisoryText)
}
}
func TestNoActiveBindingInvokesBuiltInLocalProvider(t *testing.T) {
registry := &registryStub{err: errors.New("no active binding")}
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "локальный ответ", FinishReason: cognitive.FinishCompleted}}
+10 -4
View File
@@ -67,12 +67,18 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R
}
g.observe(r.Context(), selected, response, started)
if response.FinishReason != cognitive.FinishCompleted {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: response.FinishReason, Usage: response.Usage})
// A selected provider can become unavailable after discovery. Semantic
// routing must still retain the deterministic interpretation and use the
// local lifeboat rather than turn an ordinary conversation into a
// degraded reply solely because one provider failed.
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
return
}
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
if !ok {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage})
// Structured interpretation is a transport contract. A malformed model
// answer is not a reason to discard the available local fallback.
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
return
}
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage})
@@ -163,8 +169,8 @@ func interpretationPrompt(request cognitive.GatewayInterpretationRequest) string
Allowed speech acts: DIRECTIVE, ASSERTION, DECISION, PREFERENCE, QUESTION, HYPOTHESIS, CORRECTION, REQUEST_TO_INVESTIGATE, CONFIRMATION, REJECTION, CANCELLATION, CONVERSATIONAL, UNKNOWN.
Allowed applicability: APPLICABLE, LOW_CONFIDENCE, MIXED_INTENT, NEEDS_BROADER_INTERPRETATION, CLARIFICATION_REQUIRED.
Allowed clarification: NOT_REQUIRED or REQUIRED.
Capability hints may only contain known capability kinds like COGNITIVE_REASONING, MODIFY_CODE, RUN_TESTS, SEND_EMAIL, SCHEDULE_REMINDER, VERIFY_FACT, INSPECT_PROJECT, READ_DATABASE, QUERY_LOGS.
Interpret the human utterance semantically. Do not claim action execution. Do not treat punctuation alone as decisive. If uncertain, set applicability accordingly.
Capability hints may only contain known capability kinds like COGNITIVE_REASONING, WEB_RESEARCH, MODIFY_CODE, RUN_TESTS, SEND_EMAIL, SCHEDULE_REMINDER, VERIFY_FACT, INSPECT_PROJECT, READ_DATABASE, QUERY_LOGS.
Interpret the human utterance semantically. Do not claim action execution. A question asking for a current, external or otherwise source-dependent fact should receive WEB_RESEARCH; do not answer that fact from model memory. A request to discuss hypotheses or options is different and may receive COGNITIVE_REASONING. Do not treat punctuation alone as decisive. If uncertain, set applicability accordingly.
Message: %q
Deterministic primary: %s
Deterministic secondary: %s
@@ -1,9 +1,15 @@
package main
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
@@ -24,3 +30,32 @@ func TestBuiltinInterpretationTimeoutRetainsDeterministicCandidate(t *testing.T)
t.Fatalf("candidate did not retain deterministic meaning: %+v", response.Candidate)
}
}
func TestInterpretationProviderFailureUsesLocalLifeboat(t *testing.T) {
selected := gatewaySelection()
selected.Binding.DataPolicy = capability.DataPolicyLocalOnly
primary := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{FinishReason: cognitive.FinishProviderUnavailable}}
lifeboat := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{AdvisoryText: `{"primary_speech_act":"QUESTION","secondary_speech_act":"","confidence_millis":900,"applicability":"APPLICABLE","requested_outcome":"","needs_cognition":true,"clarification":"NOT_REQUIRED","selection_reason":"local lifeboat","capability_hints":["COGNITIVE_REASONING"]}`, FinishReason: cognitive.FinishCompleted}}
gateway := cognitiveGateway{
resolver: &registryStub{selected: selected},
providers: map[capability.ProviderKind][]providerRegistration{
selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, Provider: primary}},
},
builtinLocal: &providerRegistration{Provider: lifeboat},
now: time.Now,
}
request := cognitive.GatewayInterpretationRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", Message: "сколько фигур в шахматах?", Locale: "ru", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, DeterministicSpeechAct: "QUESTION", DeterministicConfidence: 800, DeterministicApplicability: cognitive.InterpretationApplicable}
payload, _ := json.Marshal(request)
recorder := httptest.NewRecorder()
gateway.interpretationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/interpretations", bytes.NewReader(payload)))
if recorder.Code != http.StatusOK || !primary.called || !lifeboat.called {
t.Fatalf("status=%d primary=%t lifeboat=%t body=%s", recorder.Code, primary.called, lifeboat.called, recorder.Body.String())
}
var response cognitive.GatewayInterpretationResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
if response.FinishReason != cognitive.FinishCompleted || response.Candidate.SelectionReason != "local lifeboat" {
t.Fatalf("response=%+v", response)
}
}
+44 -10
View File
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/adapters/multimodal"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
@@ -70,7 +71,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
mediumTimeout := durationEnv("LOCAL_MEDIUM_PROVIDER_TIMEOUT", 60*time.Second)
mediumTimeout := durationEnv("LOCAL_MEDIUM_PROVIDER_TIMEOUT", 120*time.Second)
mediumAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(strings.TrimRight(mediumURL, "/"), mediumAPIKey, nil, mediumTimeout)
if adapterErr != nil {
log.Fatal(adapterErr)
@@ -97,7 +98,8 @@ func main() {
if gpuErr != nil {
log.Printf("optional GPU provider disabled: %v", gpuErr)
} else if gpuConfigured {
gpuAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKey(gpuURL, gpuKey, nil)
gpuTimeout := durationEnv("LOCAL_GPU_PROVIDER_TIMEOUT", 60*time.Second)
gpuAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(gpuURL, gpuKey, nil, gpuTimeout)
if adapterErr != nil {
log.Printf("optional GPU provider disabled: %v", adapterErr)
} else {
@@ -115,7 +117,8 @@ func main() {
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
cpuReadinessClient := &http.Client{Timeout: 10 * time.Second}
mediumReadinessClient := &http.Client{Timeout: 10 * time.Second}
mediumReadinessTimeout := 120 * time.Second
mediumReadinessClient := &http.Client{Timeout: mediumReadinessTimeout}
lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health"
fleetClient := &http.Client{Timeout: 2 * time.Second}
preferredFleet := capability.FleetTier(envOr("COGNITIVE_PREFERRED_FLEET_TIER", string(capability.FleetTierFast)))
@@ -125,17 +128,26 @@ func main() {
}
cpuEndpoint := strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/") + "/models"
cpuHealthProbe := health.OptionalHTTPProbeWithHeaders(cpuReadinessClient, cpuEndpoint, cpuFleetHeaders)
cpuHealthProbe.Timeout = 10 * time.Second
// Opportunistic providers must not hide a ready local lifeboat. Keep their
// readiness budget short; their detailed fleet state is reported separately.
cpuHealthProbe.Timeout = 2 * time.Second
mediumEndpoint := ""
mediumHealthEndpoint := ""
mediumHeaders := map[string]string{}
mediumHealthProbe := health.Probe{}
if mediumURL != "" {
mediumEndpoint = strings.TrimRight(mediumURL, "/") + "/models"
mediumHealthEndpoint = strings.TrimSuffix(strings.TrimRight(mediumURL, "/"), "/v1") + "/health"
if mediumAPIKey != "" {
mediumHeaders["Authorization"] = "Bearer " + mediumAPIKey
}
mediumHealthProbe = health.OptionalHTTPProbeWithHeaders(mediumReadinessClient, mediumEndpoint, mediumHeaders)
mediumHealthProbe.Timeout = 10 * time.Second
// Readiness reports provider discovery health quickly; fleet readiness
// separately reports authenticated model availability on /v1/models.
mediumHealthProbe = health.OptionalHTTPProbeWithHeaders(readinessClient, mediumHealthEndpoint, mediumHeaders)
// The local gateway may need several seconds to enumerate a loaded model.
// Keep this bounded, but do not classify a healthy MEDIUM provider as
// unavailable before its authenticated discovery response can arrive.
mediumHealthProbe.Timeout = 2 * time.Second
}
readinessProbes := []health.Probe{
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
@@ -148,13 +160,13 @@ func main() {
if gpuConfigured {
gpuHeaders := map[string]string{"Authorization": "Bearer " + gpuKey}
gpuProbe := health.OptionalHTTPProbeWithHeaders(readinessClient, strings.TrimRight(gpuURL, "/")+"/models", gpuHeaders)
gpuProbe.Timeout = 10 * time.Second
gpuProbe.Timeout = 2 * time.Second
readinessProbes = append(readinessProbes, gpuProbe)
}
fleetEndpoints := []fleetEndpoint{
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: cpuEndpoint, Headers: cpuFleetHeaders, Client: &http.Client{Timeout: 10 * time.Second}},
{ProviderID: "local-medium-primary", ModelID: "local-medium-model", Tier: capability.FleetTierMedium, URL: mediumEndpoint, Headers: mediumHeaders, Client: mediumReadinessClient},
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: cpuEndpoint, Headers: cpuFleetHeaders, Client: &http.Client{Timeout: 10 * time.Second}, Timeout: 2 * time.Second},
{ProviderID: "local-medium-primary", ModelID: "local-medium-model", Tier: capability.FleetTierMedium, URL: mediumEndpoint, Headers: mediumHeaders, Client: mediumReadinessClient, Timeout: mediumReadinessTimeout},
{ProviderID: "cognitive-lifeboat", ModelID: "qwen3-8b-lifeboat", Tier: capability.FleetTierLifeboat, URL: lifeboatURL},
}
if gpuConfigured {
@@ -167,6 +179,26 @@ func main() {
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
mux.HandleFunc("POST /v1/interpretations", gateway.interpretationHandler)
mux.HandleFunc("POST /v1/task-evaluations/execute", gateway.taskEvaluationHandler)
imageGenerator := multimodal.ImageGenerator(multimodal.ImageGenerationClient{BaseURL: os.Getenv("LOCAL_IMAGE_PROVIDER_URL"), Client: &http.Client{Timeout: 300 * time.Second}})
if strings.EqualFold(strings.TrimSpace(os.Getenv("LOCAL_IMAGE_PROVIDER_KIND")), "comfyui") {
imageGenerator = multimodal.ComfyImageGenerationClient{BaseURL: os.Getenv("LOCAL_IMAGE_PROVIDER_URL"), Client: &http.Client{Timeout: 300 * time.Second}, PollTimeout: 15 * time.Minute}
}
multimodalGateway := multimodalGateway{
speech: multimodal.SpeechClient{BaseURL: envOr("LOCAL_SPEECH_PROVIDER_URL", "http://llm-local-speech:8080"), Client: &http.Client{Timeout: 120 * time.Second}},
vision: multimodal.VisionClient{BaseURL: envOr("LOCAL_VISION_PROVIDER_URL", "http://llm-local-vision:8080/v1"), Client: &http.Client{Timeout: 180 * time.Second}},
imageGenerator: imageGenerator,
imageProviderURL: os.Getenv("LOCAL_IMAGE_PROVIDER_URL"),
video: multimodal.VideoUnderstandingClient{BaseURL: os.Getenv("LOCAL_VIDEO_PROVIDER_URL"), Client: &http.Client{Timeout: 300 * time.Second}},
model: envOr("LOCAL_VISION_MODEL", "/models/gemma-4-26B-A4B-it-UD-Q4_K_M.gguf"),
imageModel: os.Getenv("LOCAL_IMAGE_MODEL"),
videoModel: os.Getenv("LOCAL_VIDEO_MODEL"),
now: time.Now,
}
mux.HandleFunc("POST /v1/multimodal/transcribe", multimodalGateway.handler(capability.MultimodalTranscribe))
mux.HandleFunc("POST /v1/multimodal/understand", multimodalGateway.handler(capability.MultimodalUnderstand))
mux.HandleFunc("POST /v1/multimodal/ocr", multimodalGateway.handler(capability.MultimodalRecognizeOCR))
mux.HandleFunc("POST /v1/multimodal/video-understand", multimodalGateway.handler(capability.MultimodalUnderstandVideo))
mux.HandleFunc("POST /v1/multimodal/generate-image", multimodalGateway.handler(capability.MultimodalGenerateImage))
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
mux.HandleFunc("POST /v1/provider-discovery/{provider}", discovery.handler)
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(discoveries, string(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), httpQualificationObserver{resolver: resolver}))
@@ -193,7 +225,9 @@ func durationEnv(name string, fallback time.Duration) time.Duration {
func refreshProviderDiscoveries(resolver bindingResolver, service discoveryService) {
for name := range service.providers {
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
// Keep startup discovery bounded while allowing the authenticated local
// gateway enough time to enumerate a loaded MEDIUM model.
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
registration, ok := service.providers[name]
if !ok || registration.Discovery == nil {
cancel()
+16
View File
@@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"testing"
"time"
)
func TestLifeboatProviderIsOptionalAndLocalAdapterBacked(t *testing.T) {
@@ -19,6 +20,21 @@ func TestLifeboatProviderIsOptionalAndLocalAdapterBacked(t *testing.T) {
}
}
func TestGPUProviderTimeoutUsesBoundedConfigurableDefault(t *testing.T) {
t.Setenv("LOCAL_GPU_PROVIDER_TIMEOUT", "")
if got := durationEnv("LOCAL_GPU_PROVIDER_TIMEOUT", 60*time.Second); got != 60*time.Second {
t.Fatalf("default GPU timeout=%s", got)
}
t.Setenv("LOCAL_GPU_PROVIDER_TIMEOUT", "31s")
if got := durationEnv("LOCAL_GPU_PROVIDER_TIMEOUT", 60*time.Second); got != 31*time.Second {
t.Fatalf("configured GPU timeout=%s", got)
}
t.Setenv("LOCAL_GPU_PROVIDER_TIMEOUT", "invalid")
if got := durationEnv("LOCAL_GPU_PROVIDER_TIMEOUT", 60*time.Second); got != 60*time.Second {
t.Fatalf("invalid GPU timeout=%s", got)
}
}
func TestLoadLocalCPUProviderAPIKeyUsesProtectedFileReference(t *testing.T) {
t.Setenv("LOCAL_CPU_PROVIDER_API_KEY", "")
_ = os.Unsetenv("LOCAL_CPU_PROVIDER_API_KEY")
+138
View File
@@ -0,0 +1,138 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/multimodal"
"git.cin.su/m/Assistant/contracts/capability"
)
const maxMultimodalUploadBytes = 12 << 20
type multimodalGateway struct {
speech multimodal.SpeechClient
vision multimodal.VisionClient
imageGenerator multimodal.ImageGenerator
imageProviderURL string
video multimodal.VideoUnderstandingClient
model string
imageModel string
videoModel string
now func() time.Time
}
func (g multimodalGateway) handler(operation capability.MultimodalOperation) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxMultimodalUploadBytes)
if err := r.ParseMultipartForm(maxMultimodalUploadBytes); err != nil {
http.Error(w, "media upload is too large or invalid", http.StatusRequestEntityTooLarge)
return
}
if operation == capability.MultimodalGenerateImage {
if strings.TrimSpace(g.imageProviderURL) == "" {
http.Error(w, "local image generation capability unavailable", http.StatusBadGateway)
return
}
prompt := r.FormValue("prompt")
image, err := g.imageGenerator.Generate(r.Context(), g.imageModel, prompt, imageRequestSize(prompt, r.FormValue("size")))
if err != nil || strings.TrimSpace(image.DataBase64) == "" {
logMultimodalFailure(operation, err)
http.Error(w, "local image generation capability unavailable", http.StatusBadGateway)
return
}
result := capability.MultimodalResponse{Operation: operation, Provider: "local-image-generator", ObservedAt: g.now().UnixMilli(), ContentType: image.ContentType, DataBase64: image.DataBase64}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "media file is required", http.StatusBadRequest)
return
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, maxMultimodalUploadBytes+1))
if err != nil || len(data) == 0 || len(data) > maxMultimodalUploadBytes {
http.Error(w, "media file is invalid", http.StatusBadRequest)
return
}
contentType := header.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
var text string
if operation == capability.MultimodalTranscribe {
text, err = g.speech.Transcribe(r.Context(), header.Filename, contentType, data)
} else if operation == capability.MultimodalUnderstandVideo {
if strings.TrimSpace(g.video.BaseURL) == "" {
http.Error(w, "local video understanding capability unavailable", http.StatusBadGateway)
return
}
text, err = g.video.Describe(r.Context(), g.videoModel, header.Filename, contentType, data, r.FormValue("instruction"))
} else {
instruction := "Опиши, что изображено на фотографии, коротко и по существу. Не выдумывай нечитаемые детали."
if operation == capability.MultimodalRecognizeOCR {
instruction = "Точно распознай весь читаемый текст на изображении. Сохрани строки и символы; нечитаемые места обозначь как [неразборчиво]. Не добавляй пояснений."
}
text, err = g.vision.Describe(r.Context(), g.model, contentType, data, instruction)
}
if err != nil {
logMultimodalFailure(operation, err)
http.Error(w, "local media capability unavailable", http.StatusBadGateway)
return
}
result := capability.MultimodalResponse{Operation: operation, Text: strings.TrimSpace(text), ObservedAt: g.now().UnixMilli(), ConfidenceNote: "Результат получен локальным специализированным сервисом; нечитаемые или неоднозначные фрагменты требуют проверки."}
if operation == capability.MultimodalTranscribe {
result.Provider = "local-whisper"
} else {
result.Provider = "local-vision"
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
}
}
// imageRequestSize is a transport-level normalization for the local image
// capability. An explicit, valid size always wins. When the caller only
// expresses a conventional output-quality target, the capability translates it
// to a supported provider size without exposing provider workflow details to
// the interaction core. This policy is deliberately local and replaceable
// with the image capability implementation.
func imageRequestSize(prompt, requested string) string {
if isImageSize(requested) {
return strings.TrimSpace(requested)
}
words := strings.Fields(strings.ToLower(prompt))
for index, word := range words {
switch word {
case "4k", "uhd":
return "2048x2048"
case "hd":
return "1024x1024"
case "ultra":
if index+1 < len(words) && words[index+1] == "hd" {
return "2048x2048"
}
}
}
return ""
}
func isImageSize(value string) bool {
var width, height int
if _, err := fmt.Sscanf(strings.TrimSpace(value), "%dx%d", &width, &height); err != nil {
return false
}
return width >= 256 && height >= 256 && width <= 2048 && height <= 2048
}
func logMultimodalFailure(operation capability.MultimodalOperation, err error) {
// Keep provider details in server diagnostics; never return them to chat.
log.Printf("multimodal operation %s failed: %v", operation, err)
}
+96
View File
@@ -0,0 +1,96 @@
package main
import (
"bytes"
"context"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.cin.su/m/Assistant/adapters/multimodal"
"git.cin.su/m/Assistant/contracts/capability"
)
func TestMultimodalTranscriptionHandlerReturnsLocalResult(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/inference" || r.Method != http.MethodPost {
t.Fatalf("unexpected upstream request: %s %s", r.Method, r.URL.Path)
}
_ = json.NewEncoder(w).Encode(map[string]string{"text": "добрый день"})
}))
defer upstream.Close()
server := multimodalGateway{speech: multimodal.SpeechClient{BaseURL: upstream.URL, Client: upstream.Client()}, now: time.Now}
request := multipartRequest(t, "/v1/multimodal/transcribe", "voice.wav", []byte("audio"))
response := httptest.NewRecorder()
server.handler(capability.MultimodalTranscribe)(response, request)
if response.Code != http.StatusOK || !bytes.Contains(response.Body.Bytes(), []byte("добрый день")) {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
func TestMultimodalImageGenerationFailsClosedWhenSkillIsUnbound(t *testing.T) {
server := multimodalGateway{now: time.Now}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
if err := writer.WriteField("prompt", "green bird"); err != nil || writer.Close() != nil {
t.Fatal("failed to build generation request")
}
request := httptest.NewRequest(http.MethodPost, "/v1/multimodal/generate-image", body)
request.Header.Set("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder()
server.handler(capability.MultimodalGenerateImage)(response, request)
if response.Code != http.StatusBadGateway || !bytes.Contains(response.Body.Bytes(), []byte("unavailable")) {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
func TestImageRequestSizeUsesExplicitSizeOrQualityTarget(t *testing.T) {
for _, test := range []struct {
name string
prompt string
requested string
want string
}{
{name: "explicit", prompt: "нарисуй птицу", requested: "768x512", want: "768x512"},
{name: "hd", prompt: "нарисуй птицу в HD", want: "1024x1024"},
{name: "uhd", prompt: "draw a bird in UHD", want: "2048x2048"},
{name: "ordinary", prompt: "нарисуй птицу", want: ""},
{name: "invalid explicit", prompt: "нарисуй птицу", requested: "100x100", want: ""},
} {
t.Run(test.name, func(t *testing.T) {
if got := imageRequestSize(test.prompt, test.requested); got != test.want {
t.Fatalf("imageRequestSize(%q, %q) = %q, want %q", test.prompt, test.requested, got, test.want)
}
})
}
}
func TestMultimodalVideoUnderstandingFailsClosedWhenSkillIsUnbound(t *testing.T) {
server := multimodalGateway{now: time.Now}
request := multipartRequest(t, "/v1/multimodal/video-understand", "clip.mp4", []byte("video"))
response := httptest.NewRecorder()
server.handler(capability.MultimodalUnderstandVideo)(response, request)
if response.Code != http.StatusBadGateway || !bytes.Contains(response.Body.Bytes(), []byte("unavailable")) {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
func multipartRequest(t *testing.T, path, filename string, data []byte) *http.Request {
t.Helper()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", filename)
if err != nil {
t.Fatal(err)
}
if _, err := part.Write(data); err != nil || writer.Close() != nil {
t.Fatal("failed to build multipart request")
}
request := httptest.NewRequest(http.MethodPost, path, body)
request = request.WithContext(context.Background())
request.Header.Set("Content-Type", writer.FormDataContentType())
return request
}
+74 -31
View File
@@ -1,9 +1,11 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"time"
@@ -34,20 +36,13 @@ func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.R
http.Error(w, "external egress acknowledgement required", http.StatusBadRequest)
return
}
provider := g.provider(selected)
if provider == nil {
http.Error(w, "cognitive provider unavailable", http.StatusServiceUnavailable)
return
}
caseSet := make(map[string]struct{}, len(request.Profile.RequiredCaseIDs))
seenCases := make(map[string]struct{}, len(request.Cases))
for _, id := range request.Profile.RequiredCaseIDs {
caseSet[id] = struct{}{}
}
observations := make([]capability.TaskCaseObservation, len(request.Cases))
responses := make([]capability.TaskCaseResponse, len(request.Cases))
var wg sync.WaitGroup
seenCases := make(map[string]struct{}, len(request.Cases))
for i, item := range request.Cases {
for _, item := range request.Cases {
if item.CaseID == "" || item.Prompt == "" || item.EvidenceArtifactRef == "" {
http.Error(w, "invalid task evaluation case", http.StatusBadRequest)
return
@@ -61,35 +56,83 @@ func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.R
return
}
seenCases[item.CaseID] = struct{}{}
wg.Add(1)
go func(index int, item capability.TaskEvaluationCase) {
defer wg.Done()
started := time.Now()
invocation := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("task-evaluation:%s:%s", request.Profile.ID, item.CaseID)), ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: item.Prompt, MaxOutputBytes: int(request.MaxOutputBytes), ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
response, invokeErr := g.invokeAdmitted(r.Context(), string(invocation.RequestID), provider, invocation, int(request.MaxOutputBytes))
observation := capability.TaskCaseObservation{CaseID: item.CaseID, LatencyMicros: time.Since(started).Microseconds(), EvidenceArtifactRef: item.EvidenceArtifactRef}
responses[index] = capability.TaskCaseResponse{CaseID: item.CaseID, ResponseText: response.AdvisoryText, LatencyMicros: observation.LatencyMicros, EvidenceArtifactRef: item.EvidenceArtifactRef}
if invokeErr == nil && response.FinishReason == cognitive.FinishCompleted && response.AdvisoryText != "" {
// Invocation success proves reachability only. Without an
// independent verifier and answer evidence, semantic support and
// quality must remain UNKNOWN rather than becoming false evidence.
observation.Result = capability.EvaluationUnknown
observation.QualityScoreMillis = 0
} else {
observation.Result = capability.EvaluationUnknown
observation.CriticalFailure = invokeErr != nil
}
observations[index] = observation
}(i, item)
}
if len(seenCases) != len(caseSet) {
http.Error(w, "task evaluation case set is incomplete", http.StatusBadRequest)
return
}
wg.Wait()
// One bounded fan-out wave uses one binding snapshot. If that candidate is
// unavailable, only this binding is excluded and the existing resolver
// chooses the next eligible candidate. Provider output remains untrusted
// until the independent verifier accepts it downstream.
requirement := capability.CognitiveRequirement{CapabilityID: request.CapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: selected.Binding.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, MaxOutputBytes: request.MaxOutputBytes}
var observations []capability.TaskCaseObservation
var responses []capability.TaskCaseResponse
for attempt := 0; attempt < 3; attempt++ {
provider := g.provider(selected)
if provider == nil {
observations, responses = taskEvaluationUnavailable(request.Cases)
} else {
observations, responses = g.executeTaskEvaluationCases(r.Context(), request, selected, provider)
}
if taskEvaluationResponsesUsable(responses) {
break
}
requirement.ExcludedBindingIDs = append(requirement.ExcludedBindingIDs, selected.Binding.ID)
fallback, resolveErr := g.resolver.Resolve(r.Context(), requirement)
fallbackProvider := g.provider(fallback)
if resolveErr != nil || fallbackProvider == nil || fallback.Binding.ID == selected.Binding.ID || (fallback.Binding.DataPolicy != capability.DataPolicyLocalOnly && !request.EgressAcknowledged) {
break
}
selected = fallback
}
writeTaskEvaluationJSON(w, capability.TaskEvaluationExecutionResponse{ProfileID: request.Profile.ID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, Observations: observations, Responses: responses, ObservedAtUnixMillis: time.Now().UnixMilli()})
}
func (g cognitiveGateway) executeTaskEvaluationCases(ctx context.Context, request capability.TaskEvaluationExecutionRequest, selected capability.ResolvedCognitiveBinding, provider advisoryProvider) ([]capability.TaskCaseObservation, []capability.TaskCaseResponse) {
observations := make([]capability.TaskCaseObservation, len(request.Cases))
responses := make([]capability.TaskCaseResponse, len(request.Cases))
var wg sync.WaitGroup
for i, item := range request.Cases {
wg.Add(1)
go func(index int, item capability.TaskEvaluationCase) {
defer wg.Done()
started := time.Now()
invocation := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("task-evaluation:%s:%s", request.Profile.ID, item.CaseID)), ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: item.Prompt, Locale: "en-US", MaxOutputBytes: int(request.MaxOutputBytes), ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
response, invokeErr := g.invokeAdmitted(ctx, string(invocation.RequestID), provider, invocation, int(request.MaxOutputBytes))
latency := time.Since(started).Microseconds()
observations[index] = capability.TaskCaseObservation{CaseID: item.CaseID, Result: capability.EvaluationUnknown, CriticalFailure: invokeErr != nil || response.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(response.AdvisoryText) == "", EvidenceArtifactRef: item.EvidenceArtifactRef, LatencyMicros: latency}
responses[index] = capability.TaskCaseResponse{CaseID: item.CaseID, ResponseText: response.AdvisoryText, LatencyMicros: latency, EvidenceArtifactRef: item.EvidenceArtifactRef}
}(i, item)
}
wg.Wait()
return observations, responses
}
func taskEvaluationUnavailable(cases []capability.TaskEvaluationCase) ([]capability.TaskCaseObservation, []capability.TaskCaseResponse) {
observations := make([]capability.TaskCaseObservation, len(cases))
responses := make([]capability.TaskCaseResponse, len(cases))
for i, item := range cases {
observations[i] = capability.TaskCaseObservation{CaseID: item.CaseID, Result: capability.EvaluationUnknown, CriticalFailure: true, EvidenceArtifactRef: item.EvidenceArtifactRef}
responses[i] = capability.TaskCaseResponse{CaseID: item.CaseID, EvidenceArtifactRef: item.EvidenceArtifactRef}
}
return observations, responses
}
func taskEvaluationResponsesUsable(responses []capability.TaskCaseResponse) bool {
if len(responses) == 0 {
return false
}
for _, response := range responses {
if strings.TrimSpace(response.ResponseText) == "" {
return false
}
}
return true
}
func writeTaskEvaluationJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
@@ -56,3 +57,114 @@ func TestTaskEvaluationHandlerInvokesEveryBoundedCase(t *testing.T) {
}
}
}
type localeCheckingProvider struct{ stubAdvisoryProvider }
func (p *localeCheckingProvider) Advise(ctx context.Context, request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
if request.Locale == "" {
return cognitive.AdvisoryResponse{}, fmt.Errorf("task evaluation omitted required locale")
}
return p.stubAdvisoryProvider.Advise(ctx, request)
}
func TestTaskEvaluationSuppliesLocaleToProviderAdapter(t *testing.T) {
selected := gatewaySelection()
provider := &localeCheckingProvider{stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, FinishReason: cognitive.FinishCompleted, AdvisoryText: "observed response"}}}
registry := &candidateRegistryStub{selected: selected}
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, EndpointRef: selected.Provider.EndpointRef, CredentialRef: selected.Provider.CredentialRef, Provider: provider}}}, now: time.Now}
profile := capability.TaskEvaluationProfile{ID: "profile-locale", Revision: 1, CapabilityID: selected.Binding.CapabilityID, Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "locale-v1", RequiredCaseIDs: []string{"case-locale"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
request := capability.TaskEvaluationExecutionRequest{Profile: profile, CapabilityID: selected.Binding.CapabilityID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, ActorID: identity.ActorID("actor-1"), PrincipalID: selected.Binding.OwnerPrincipalID, ScopeID: authority.ScopeID("scope-1"), MaxOutputBytes: 512, EgressAcknowledged: true, Cases: []capability.TaskEvaluationCase{{CaseID: "case-locale", Prompt: "one", EvidenceArtifactRef: "artifact:locale"}}}
payload, _ := json.Marshal(request)
recorder := httptest.NewRecorder()
gateway.taskEvaluationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/execute", bytes.NewReader(payload)))
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
type taskEvaluationFailoverRegistry struct {
primary, fallback capability.ResolvedCognitiveBinding
resolveCalls int
}
func (s *taskEvaluationFailoverRegistry) ResolveCandidate(context.Context, capability.CapabilityBindingID, int64) (capability.ResolvedCognitiveBinding, error) {
return s.primary, nil
}
func (s *taskEvaluationFailoverRegistry) Resolve(_ context.Context, requirement capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
s.resolveCalls++
if len(requirement.ExcludedBindingIDs) == 0 {
return s.primary, nil
}
return s.fallback, nil
}
func (s *taskEvaluationFailoverRegistry) Observe(context.Context, capability.CognitiveUsageObservation) error {
return nil
}
func (s *taskEvaluationFailoverRegistry) RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) {
return capability.CognitiveDiscoveryRegistration{}, nil
}
func TestTaskEvaluationExcludesUnavailableCandidateAndUsesFallback(t *testing.T) {
primary := gatewaySelection()
fallback := primary
fallback.Binding.ID = "binding-fallback"
fallback.Binding.Revision = 7
fallback.Binding.ProviderInstanceID = "provider-fallback"
fallback.Provider.ID = "provider-fallback"
fallback.Provider.Kind = "provider-fallback-kind"
fallback.Provider.EndpointRef = "fallback-endpoint"
fallback.Provider.CredentialRef = "fallback-credential"
fallback.Model.ProviderInstanceID = "provider-fallback"
registry := &taskEvaluationFailoverRegistry{primary: primary, fallback: fallback}
primaryProvider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, FinishReason: cognitive.FinishProviderUnavailable}}
fallbackProvider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, FinishReason: cognitive.FinishCompleted, AdvisoryText: "fallback response"}}
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{
primary.Provider.Kind: {{ProviderInstanceID: primary.Provider.ID, Kind: primary.Provider.Kind, EndpointRef: primary.Provider.EndpointRef, CredentialRef: primary.Provider.CredentialRef, Provider: primaryProvider}},
fallback.Provider.Kind: {{ProviderInstanceID: fallback.Provider.ID, Kind: fallback.Provider.Kind, EndpointRef: fallback.Provider.EndpointRef, CredentialRef: fallback.Provider.CredentialRef, Provider: fallbackProvider}},
}, now: func() time.Time { return time.UnixMilli(1000) }}
profile := capability.TaskEvaluationProfile{ID: "profile-failover", Revision: 1, CapabilityID: primary.Binding.CapabilityID, Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "failover-v1", RequiredCaseIDs: []string{"case-a"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
request := capability.TaskEvaluationExecutionRequest{Profile: profile, CapabilityID: profile.CapabilityID, BindingID: primary.Binding.ID, BindingRevision: primary.Binding.Revision, ActorID: identity.ActorID("actor-1"), PrincipalID: primary.Binding.OwnerPrincipalID, ScopeID: authority.ScopeID("scope-1"), MaxOutputBytes: 512, EgressAcknowledged: true, Cases: []capability.TaskEvaluationCase{{CaseID: "case-a", Prompt: "one", EvidenceArtifactRef: "artifact:a"}}}
payload, _ := json.Marshal(request)
recorder := httptest.NewRecorder()
gateway.taskEvaluationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/execute", bytes.NewReader(payload)))
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
var response capability.TaskEvaluationExecutionResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
if response.BindingID != fallback.Binding.ID || !primaryProvider.called || !fallbackProvider.called || registry.resolveCalls != 1 {
t.Fatalf("failover binding=%s primary_called=%t fallback_called=%t resolve_calls=%d", response.BindingID, primaryProvider.called, fallbackProvider.called, registry.resolveCalls)
}
if response.Observations[0].CriticalFailure || response.Responses[0].ResponseText != "fallback response" {
t.Fatalf("fallback response was not usable: %+v %+v", response.Observations, response.Responses)
}
}
func TestTaskEvaluationUnavailableResponseIsMarkedCriticalUnknown(t *testing.T) {
selected := gatewaySelection()
registry := &taskEvaluationFailoverRegistry{primary: selected, fallback: selected}
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, FinishReason: cognitive.FinishTimeout}}
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{
selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, EndpointRef: selected.Provider.EndpointRef, CredentialRef: selected.Provider.CredentialRef, Provider: provider}},
}, now: func() time.Time { return time.UnixMilli(1000) }}
profile := capability.TaskEvaluationProfile{ID: "profile-unknown", Revision: 1, CapabilityID: selected.Binding.CapabilityID, Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "unknown-v1", RequiredCaseIDs: []string{"case-a"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
request := capability.TaskEvaluationExecutionRequest{Profile: profile, CapabilityID: profile.CapabilityID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, ActorID: identity.ActorID("actor-1"), PrincipalID: selected.Binding.OwnerPrincipalID, ScopeID: authority.ScopeID("scope-1"), MaxOutputBytes: 512, EgressAcknowledged: true, Cases: []capability.TaskEvaluationCase{{CaseID: "case-a", Prompt: "one", EvidenceArtifactRef: "artifact:a"}}}
payload, _ := json.Marshal(request)
recorder := httptest.NewRecorder()
gateway.taskEvaluationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/execute", bytes.NewReader(payload)))
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
var response capability.TaskEvaluationExecutionResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
if len(response.Observations) != 1 || response.Observations[0].Result != capability.EvaluationUnknown || !response.Observations[0].CriticalFailure {
t.Fatalf("unavailable response was overclaimed: %+v", response.Observations)
}
}
+82
View File
@@ -0,0 +1,82 @@
package main
import (
"sort"
"git.cin.su/m/Assistant/contracts/capability"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
type chatRouteKind string
const (
chatRouteResearch chatRouteKind = "RESEARCH"
chatRouteConversation chatRouteKind = "CONVERSATION"
chatRouteMedia chatRouteKind = "MEDIA"
chatRouteAction chatRouteKind = "ACTION"
chatRouteAgent chatRouteKind = "AGENT"
)
type chatRoute struct {
Kind chatRouteKind
Capability capability.CapabilityKind
Profile string
Order int
}
// buildChatRoutePlan creates one bounded route per meaning-bearing capability.
// A failed research route must not suppress the local conversational route or
// an unrelated action route.
func buildChatRoutePlan(result interactiondomain.SemanticResult) []chatRoute {
seen := make(map[capability.CapabilityKind]struct{})
routes := make([]chatRoute, 0, len(result.CapabilityHints)+1)
add := func(kind chatRouteKind, capabilityKind capability.CapabilityKind, profile string, order int) {
if capabilityKind != "" {
if _, exists := seen[capabilityKind]; exists {
return
}
seen[capabilityKind] = struct{}{}
}
routes = append(routes, chatRoute{Kind: kind, Capability: capabilityKind, Profile: profile, Order: order})
}
for _, hint := range result.CapabilityHints {
switch capability.CapabilityKind(hint) {
case capability.CapabilityKindWebResearch:
add(chatRouteResearch, capability.CapabilityKindWebResearch, "truth-research-v1", 10)
case capability.CapabilityKindImageGeneration:
add(chatRouteMedia, capability.CapabilityKindImageGeneration, "local-image-generation-v1", 5)
case capability.CapabilityKindCognitiveReasoning, capability.CapabilityKindVerifyFact:
add(chatRouteConversation, capability.CapabilityKind(hint), "local-reasoning-v1", 20)
case capability.CapabilityKindScheduleReminder:
add(chatRouteAction, capability.CapabilityKindScheduleReminder, "continuity-reminder-v1", 30)
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject:
add(chatRouteAgent, capability.CapabilityKind(hint), "software-agent-v1", 40)
case capability.CapabilityKindSendEmail, capability.CapabilityKindReadEmail:
add(chatRouteAgent, capability.CapabilityKind(hint), "communication-agent-v1", 50)
case capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs:
add(chatRouteAgent, capability.CapabilityKind(hint), "diagnostics-agent-v1", 60)
}
}
if result.NeedsCognition || len(routes) == 0 {
add(chatRouteConversation, capability.CapabilityKindCognitiveReasoning, "local-reasoning-v1", 20)
}
sort.SliceStable(routes, func(i, j int) bool { return routes[i].Order < routes[j].Order })
return routes
}
func hasChatRoute(routes []chatRoute, kind chatRouteKind) bool {
for _, route := range routes {
if route.Kind == kind {
return true
}
}
return false
}
// specializedRouteBypassesGeneralInterpretation keeps a concrete capability
// route stable. General cognition may still be used later to assess evidence,
// but it must not replace an executable research or media route with a vague
// conversational clarification before the capability is attempted.
func specializedRouteBypassesGeneralInterpretation(routes []chatRoute) bool {
return hasChatRoute(routes, chatRouteMedia) || hasChatRoute(routes, chatRouteResearch)
}
@@ -0,0 +1,53 @@
package main
import (
"testing"
"git.cin.su/m/Assistant/contracts/capability"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestBuildChatRoutePlanPreservesMultipleMeaningfulRoutes(t *testing.T) {
planned := buildChatRoutePlan(interactiondomain.SemanticResult{
NeedsCognition: true,
CapabilityHints: []string{"WEB_RESEARCH", "MODIFY_CODE", "RUN_TESTS", "SCHEDULE_REMINDER"},
})
if len(planned) != 5 {
t.Fatalf("expected five deduplicated routes, got %+v", planned)
}
want := []struct {
kind chatRouteKind
profile string
cap capability.CapabilityKind
}{
{chatRouteResearch, "truth-research-v1", capability.CapabilityKindWebResearch},
{chatRouteConversation, "local-reasoning-v1", capability.CapabilityKindCognitiveReasoning},
{chatRouteAction, "continuity-reminder-v1", capability.CapabilityKindScheduleReminder},
{chatRouteAgent, "software-agent-v1", capability.CapabilityKindModifyCode},
{chatRouteAgent, "software-agent-v1", capability.CapabilityKindRunTests},
}
for index, expected := range want {
if planned[index].Kind != expected.kind || planned[index].Profile != expected.profile || planned[index].Capability != expected.cap {
t.Fatalf("route %d = %+v, want %+v", index, planned[index], expected)
}
}
}
func TestBuildChatRoutePlanAlwaysKeepsLocalConversationRoute(t *testing.T) {
planned := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: true})
if len(planned) != 1 || planned[0].Kind != chatRouteConversation || planned[0].Profile != "local-reasoning-v1" {
t.Fatalf("unexpected local route: %+v", planned)
}
}
func TestSpecializedRouteBypassesGeneralInterpretation(t *testing.T) {
if !specializedRouteBypassesGeneralInterpretation([]chatRoute{{Kind: chatRouteResearch}}) {
t.Fatal("research route must execute before general cognition")
}
if !specializedRouteBypassesGeneralInterpretation([]chatRoute{{Kind: chatRouteMedia}}) {
t.Fatal("media route must execute before general cognition")
}
if specializedRouteBypassesGeneralInterpretation([]chatRoute{{Kind: chatRouteConversation}}) {
t.Fatal("conversation-only route must retain general cognition")
}
}
+213 -9
View File
@@ -5,8 +5,13 @@ import (
"context"
"encoding/json"
"fmt"
"hash/fnv"
"io"
"mime/multipart"
"net/http"
"net/url"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
@@ -20,6 +25,118 @@ import (
type cognitiveInterpretationClient struct {
baseURL *url.URL
client *http.Client
media *http.Client
}
const (
// Household interpretation and response review are intentionally compact
// calls. Keeping their output bounded prevents a loaded local model from
// spending the whole request budget generating unnecessary prose.
responseReviewMaxOutputBytes = 512
stableKnowledgeMaxOutputBytes = 512
semanticInterpretMaxOutputBytes = 1024
)
// ReviewResponse asks the cognitive gateway to turn a locally assembled draft
// into a natural answer. The draft remains the safe fallback when cognition is
// unavailable; the model does not mutate canonical state or execute actions.
func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, session identity.CreatorSession, userMessage, draft, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
requestID, err := ids.New()
if err != nil {
return "", err
}
prompt := "Ты редактор финального ответа Домового. Проверь черновик по смыслу и подготовь естественный ответ человеку. Учитывай весь запрос, включая недостающие параметры для конкретного результата. Если данных не хватает, задай один короткий полезный уточняющий вопрос, например откуда и куда ехать. Не выдавай поисковые сниппеты как выполненное действие и не показывай служебные URL поисковой выдачи. Сохрани проверяемые факты, названия источников, прямые ссылки и ограничения. Не выдумывай факты и не утверждай невыполненные действия. Сначала внутренне проверь, что ответ начинается с содержания, решающего вопрос. Не повторяй вопрос пользователя в качестве заголовка или вступления, если это не нужно для смысла. Представление, имя помощника, приветствие или предложение дополнительной помощи допустимы только если пользователь сам спросил об этом; иначе убери их. Верни только готовый ответ без разбора проверки, служебных слов и упоминания моделей. Если черновик уже адекватен, верни его без лишних добавлений.\n\nСообщение пользователя:\n" + userMessage + "\n\nЧерновик ответа:\n" + draft
context := []cognitive.ContextExcerpt{}
if strings.TrimSpace(personalMemory) != "" {
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
}
target := *c.baseURL
target.Path = "/v1/advisories"
payload, err := json.Marshal(cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
ActorID: session.ActorID, PrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"), Message: prompt,
Context: context, Locale: "ru", MaxOutputBytes: responseReviewMaxOutputBytes, DataPolicy: capability.DataPolicyLocalOnly,
Meaning: cognitive.TaskMeaning{
SpeechAct: "RESPONSE_REVIEW",
CapabilityHints: meaning.CapabilityHints,
RequestedOutcome: meaning.RequestedOutcome,
InformationRequestKind: meaning.InformationRequestKind,
ConfidenceMillis: meaning.ConfidenceMillis,
},
})
if err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("cognitive response review status %d", response.StatusCode)
}
var result cognitive.GatewayAdvisoryResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return "", err
}
if result.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(result.AdvisoryText) == "" {
return "", fmt.Errorf("cognitive response review incomplete: %s", result.FinishReason)
}
return strings.TrimSpace(result.AdvisoryText), nil
}
// AnswerStableKnowledge asks the local cognitive route for a short answer to a
// non-current question only after evidence retrieval did not produce a usable
// result. The model decides whether its knowledge is stable enough; it must
// explicitly decline changing or source-dependent facts instead of guessing.
func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
requestID, err := ids.New()
if err != nil {
return "", err
}
prompt := "Ты отвечаешь как Домовой на обычное сообщение пользователя. Сначала пойми цель: на вопрос дай короткий естественный ответ по устойчивому общеизвестному знанию, а безопасную просьбу создать текстовый или структурный результат выполни прямо в ответе. Начинай сразу с содержания и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты и не утверждай выполнение внешнего действия.\n\nСообщение пользователя:\n" + userMessage
context := []cognitive.ContextExcerpt{}
if strings.TrimSpace(personalMemory) != "" {
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
}
target := *c.baseURL
target.Path = "/v1/advisories"
payload, err := json.Marshal(cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: prompt, Context: context, Locale: "ru", MaxOutputBytes: stableKnowledgeMaxOutputBytes, DataPolicy: capability.DataPolicyLocalOnly,
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
})
if err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("cognitive stable knowledge status %d", response.StatusCode)
}
var result cognitive.GatewayAdvisoryResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return "", err
}
if result.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(result.AdvisoryText) == "" {
return "", fmt.Errorf("cognitive stable knowledge incomplete: %s", result.FinishReason)
}
return strings.TrimSpace(result.AdvisoryText), nil
}
func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cognitiveInterpretationClient, error) {
@@ -30,7 +147,7 @@ func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cogni
if client == nil {
client = http.DefaultClient
}
return cognitiveInterpretationClient{baseURL: base, client: client}, nil
return cognitiveInterpretationClient{baseURL: base, client: client, media: &http.Client{Timeout: 15 * time.Minute}}, nil
}
func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request cognitive.GatewayInterpretationRequest) (cognitive.GatewayInterpretationResponse, error) {
@@ -60,20 +177,107 @@ func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request co
return result, nil
}
func (c cognitiveInterpretationClient) GenerateImage(ctx context.Context, prompt, size string) (capability.MultimodalResponse, error) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if err := writer.WriteField("prompt", prompt); err != nil {
return capability.MultimodalResponse{}, err
}
if strings.TrimSpace(size) != "" {
if err := writer.WriteField("size", size); err != nil {
return capability.MultimodalResponse{}, err
}
}
if err := writer.Close(); err != nil {
return capability.MultimodalResponse{}, err
}
target := *c.baseURL
target.Path = "/v1/multimodal/generate-image"
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), &body)
if err != nil {
return capability.MultimodalResponse{}, err
}
request.Header.Set("Content-Type", writer.FormDataContentType())
client := c.media
if client == nil {
client = c.client
}
response, err := client.Do(request)
if err != nil {
return capability.MultimodalResponse{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return capability.MultimodalResponse{}, fmt.Errorf("image generation status %d", response.StatusCode)
}
var result capability.MultimodalResponse
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<20)).Decode(&result); err != nil {
return capability.MultimodalResponse{}, err
}
if strings.TrimSpace(result.DataBase64) == "" {
return capability.MultimodalResponse{}, fmt.Errorf("image generation returned empty payload")
}
return result, nil
}
func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool {
// The built-in advisory channel is the default conversational path. A
// simple question must not first wait for the slower, strict JSON
// interpretation route: that route exists for ambiguity and work routing,
// not as a prerequisite for talking to Domovoy.
if isSimpleLocalQuestion(result) {
return shouldRequestSemanticInterpretation(result)
}
// shouldRequestSemanticInterpretation keeps the deterministic layer as a
// fast reflex, not as the meaning authority. Questions, requests, directives,
// mixed signals and capability-bearing messages go through cognitive
// interpretation; only stable low-risk conversational/assertive reflexes may
// answer without it.
func shouldRequestSemanticInterpretation(result interactiondomain.SemanticResult) bool {
if result.Interpretation.Clarification == interaction.ClarificationRequired {
return false
}
switch result.Interpretation.Applicability {
case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent:
// Structured, read-only information intents already have a semantic owner
// in core-runtime. Sending them through the slower advisory interpretation
// path only adds latency and can make a healthy runtime look unavailable.
if result.InformationRequestKind != "" {
return false
}
if result.NeedsCognition || len(result.CapabilityHints) > 0 {
return true
}
if result.Interpretation.Applicability != interaction.SemanticApplicable {
return true
}
switch result.Interpretation.SpeechAct {
case interaction.SpeechActConversational, interaction.SpeechActConfirmation, interaction.SpeechActRejection:
return false
case interaction.SpeechActPreference:
// Explicit user preferences are durable interaction context. A sampled
// advisory audit must not reinterpret or erase them.
return false
case interaction.SpeechActAssertion:
return result.Interpretation.ConfidenceMillis < 850
default:
return true
}
}
const (
deterministicReflexAuditInterval = 10 * time.Minute
deterministicReflexAuditSample = uint32(16)
)
// shouldAuditDeterministicReflex samples stable reflexes in a time bucket.
// This is observability, not a correctness gate: a failed audit leaves the
// reflex usable, while a successful disagreement can replace it with the
// cognitive candidate and becomes durable evidence through the normal
// candidate store.
func shouldAuditDeterministicReflex(result interactiondomain.SemanticResult, utteranceID string, now time.Time) bool {
if shouldRequestSemanticInterpretation(result) || utteranceID == "" {
return false
}
bucket := now.Unix() / int64(deterministicReflexAuditInterval/time.Second)
hash := fnv.New32a()
_, _ = hash.Write([]byte(utteranceID))
_, _ = hash.Write([]byte(fmt.Sprintf(":%d", bucket)))
return hash.Sum32()%deterministicReflexAuditSample == 0
}
func isSimpleLocalQuestion(result interactiondomain.SemanticResult) bool {
@@ -99,7 +303,7 @@ func buildGatewayInterpretationRequest(session identity.CreatorSession, utteranc
ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: utterance.Content,
Locale: "ru",
MaxOutputBytes: 4096,
MaxOutputBytes: semanticInterpretMaxOutputBytes,
DataPolicy: capability.DataPolicyLocalOnly,
DeterministicSpeechAct: string(semantic.Interpretation.SpeechAct),
DeterministicSecondary: string(semantic.Interpretation.SecondarySpeechAct),
@@ -1,13 +1,110 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestSimpleQuestionUsesBuiltInAdvisoryWithoutStrictInterpretation(t *testing.T) {
func TestReviewResponseUsesCognitiveGatewayAndReturnsOnlyDraftAnswer(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode review request: %v", err)
}
if request.Message == "" || request.Meaning.SpeechAct == "" {
t.Fatalf("review request did not carry semantic context: %+v", request)
}
if !strings.Contains(request.Message, "ответ начинается с содержания") || !strings.Contains(request.Message, "Не повторяй вопрос пользователя") || !strings.Contains(request.Message, "только если пользователь сам спросил") {
t.Fatalf("review request lacks the conversational quality contract: %q", request.Message)
}
if len(request.Context) != 1 || !strings.Contains(request.Context[0].Text, "красный цвет") {
t.Fatalf("review request did not carry compact personal memory: %+v", request.Context)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Меня зовут Дед. Я Домовой.", FinishReason: cognitive.FinishCompleted})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
reviewed, err := client.ReviewResponse(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "кто ты?", "Я — Домовой.", "Предпочтение пользователя (его сообщение): красный цвет", cognitive.TaskMeaning{SpeechAct: "QUESTION"})
if err != nil {
t.Fatal(err)
}
if reviewed != "Меня зовут Дед. Я Домовой." {
t.Fatalf("unexpected reviewed response %q", reviewed)
}
}
func TestAnswerStableKnowledgeUsesLocalOnlyCognitiveRoute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode stable knowledge request: %v", err)
}
if request.DataPolicy != capability.DataPolicyLocalOnly || request.Meaning.SpeechAct != "STABLE_KNOWLEDGE_ANSWER" {
t.Fatalf("unexpected stable knowledge request: %+v", request)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Санкт-Петербург — город в России.", FinishReason: cognitive.FinishCompleted})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
answer, err := client.AnswerStableKnowledge(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "Питер — это где?", "", cognitive.TaskMeaning{SpeechAct: "QUESTION"})
if err != nil || answer != "Санкт-Петербург — город в России." {
t.Fatalf("answer=%q err=%v", answer, err)
}
}
func TestGenerateImageUsesDedicatedCognitiveMediaRoute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/multimodal/generate-image" {
t.Fatalf("unexpected image route: %s %s", r.Method, r.URL.Path)
}
if err := r.ParseMultipartForm(1 << 20); err != nil {
t.Fatalf("parse image request: %v", err)
}
if !strings.Contains(r.FormValue("prompt"), "собаку") {
t.Fatalf("prompt was not preserved: %q", r.FormValue("prompt"))
}
_ = json.NewEncoder(w).Encode(capability.MultimodalResponse{Operation: capability.MultimodalGenerateImage, ContentType: "image/png", DataBase64: "cG5n"})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
image, err := client.GenerateImage(context.Background(), "нарисуй собаку", "")
if err != nil || image.DataBase64 != "cG5n" || image.ContentType != "image/png" {
t.Fatalf("image=%+v err=%v", image, err)
}
}
func TestReviewPreservesResearchEvidence(t *testing.T) {
draft := "Нашёл вариант. Источник: Aviasales (https://www.aviasales.ru/search)"
if reviewPreservesResearchEvidence("Нашёл вариант на Авиасейлс.", draft) {
t.Fatal("review without the direct source must be rejected")
}
if !reviewPreservesResearchEvidence("Нашёл вариант. Источник: Aviasales (https://www.aviasales.ru/search)", draft) {
t.Fatal("review with the direct source must be accepted")
}
}
func TestSimpleQuestionUsesStrictInterpretationForSemanticRouting(t *testing.T) {
result := interactiondomain.SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActQuestion,
@@ -16,8 +113,8 @@ func TestSimpleQuestionUsesBuiltInAdvisoryWithoutStrictInterpretation(t *testing
},
CapabilityHints: []string{"COGNITIVE_REASONING", "VERIFY_FACT"},
}
if shouldRequestBroaderInterpretation(result) {
t.Fatal("simple question must go directly to the built-in advisory path")
if !shouldRequestBroaderInterpretation(result) {
t.Fatal("simple question must reach semantic interpretation before any answer")
}
}
@@ -34,3 +131,51 @@ func TestOperationalQuestionStillUsesStrictInterpretation(t *testing.T) {
t.Fatal("operational question must retain strict interpretation routing")
}
}
func TestStructuredInventoryUsesDirectReadOnlySemanticRoute(t *testing.T) {
result := interactiondomain.SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActDirective,
Applicability: interaction.SemanticApplicable,
ConfidenceMillis: 980,
},
InformationRequestKind: interaction.InformationRequestCognitiveModelInventory,
}
if shouldRequestSemanticInterpretation(result) {
t.Fatal("structured model inventory must use the direct read-only semantic route")
}
}
func TestStableReflexIsPeriodicallySampledForDrift(t *testing.T) {
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActConversational, Applicability: interaction.SemanticApplicable,
}}
found := false
for minute := 0; minute < 160; minute++ {
if shouldAuditDeterministicReflex(result, "utterance-drift-probe", time.Unix(int64(minute)*60, 0)) {
found = true
break
}
}
if !found {
t.Fatal("stable reflex was never sampled during the audit window")
}
}
func TestMeaningfulSignalsAreNotReflexOnly(t *testing.T) {
for _, act := range []interaction.SpeechAct{interaction.SpeechActQuestion, interaction.SpeechActRequestToInvestigate, interaction.SpeechActDirective} {
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: act, Applicability: interaction.SemanticApplicable}}
if !shouldRequestSemanticInterpretation(result) {
t.Fatalf("%s must prefer cognitive interpretation", act)
}
}
}
func TestExplicitPreferenceIsNotReinterpretedByCognitiveAudit(t *testing.T) {
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActPreference, Applicability: interaction.SemanticApplicable,
}}
if shouldRequestSemanticInterpretation(result) || shouldAuditDeterministicReflex(result, "preference", time.Unix(0, 0)) {
t.Fatal("explicit preference must remain on the deterministic durable-context path")
}
}
+97
View File
@@ -1,14 +1,111 @@
package main
import (
"context"
"errors"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/developmentexecutor"
"git.cin.su/m/Assistant/contracts/work"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
"git.cin.su/m/Assistant/modules/work/ports"
)
var errDevelopmentGateCreate = errors.New("development gate must start at evidence collected with revision one")
var errDevelopmentGateNotIsolated = errors.New("development gate is not isolated")
var errDevelopmentVerifierNotIndependent = errors.New("development verifier is not independent")
var errDevelopmentWorktreeMismatch = errors.New("verification worktree does not match gate")
var errDevelopmentChecksFailed = errors.New("development verification checks failed")
var errDevelopmentEvidenceRequired = errors.New("development transition evidence is required")
var errDevelopmentEvidenceStage = errors.New("development transition evidence does not match stage")
type developmentGateVerificationRequest struct {
GateID string
ExpectedRevision int64
VerifierRunID work.AgentRunID
Plan developmentexecutor.Plan
}
type developmentGateEvidenceRequest struct {
GateID string
ExpectedRevision int64
NextStage work.DevelopmentStage
EvidenceRef string
}
func recordDevelopmentGateEvidence(ctx context.Context, store ports.DevelopmentGateStore, request developmentGateEvidenceRequest, now time.Time) (work.DevelopmentGate, error) {
gate, err := store.LoadDevelopmentGate(ctx, request.GateID)
if err != nil {
return work.DevelopmentGate{}, err
}
if request.ExpectedRevision < 1 || gate.Revision != request.ExpectedRevision {
return gate, errDevelopmentGateNotIsolated
}
if strings.TrimSpace(request.EvidenceRef) == "" {
return gate, errDevelopmentEvidenceRequired
}
switch request.NextStage {
case work.DevelopmentCanary:
gate.CanaryEvidence = strings.TrimSpace(request.EvidenceRef)
case work.DevelopmentPromoted:
gate.PromotionRef = strings.TrimSpace(request.EvidenceRef)
case work.DevelopmentRolledBack:
gate.RollbackRef = strings.TrimSpace(request.EvidenceRef)
default:
return gate, errDevelopmentEvidenceStage
}
advanced, err := advanceDevelopmentGate(gate, request.NextStage, now)
if err != nil {
return gate, err
}
if err := store.SaveDevelopmentGate(ctx, advanced, request.ExpectedRevision); err != nil {
return gate, err
}
return advanced, nil
}
func verifyDevelopmentGate(ctx context.Context, store ports.DevelopmentGateStore, request developmentGateVerificationRequest, now time.Time, runner developmentexecutor.CommandRunner) (work.DevelopmentGate, developmentexecutor.Result, error) {
gate, err := store.LoadDevelopmentGate(ctx, request.GateID)
if err != nil {
return work.DevelopmentGate{}, developmentexecutor.Result{}, err
}
if request.ExpectedRevision < 1 || gate.Revision != request.ExpectedRevision || gate.Stage != work.DevelopmentIsolated {
return gate, developmentexecutor.Result{}, errDevelopmentGateNotIsolated
}
if request.VerifierRunID == "" || sameRun(request.VerifierRunID, gate.ProducerRunRefs) {
return gate, developmentexecutor.Result{}, errDevelopmentVerifierNotIndependent
}
if request.Plan.WorktreeID != gate.WorktreeRef {
return gate, developmentexecutor.Result{}, errDevelopmentWorktreeMismatch
}
result, err := developmentexecutor.Execute(ctx, request.Plan, runner)
if err != nil {
return gate, result, err
}
if !result.Passed {
return gate, result, errDevelopmentChecksFailed
}
gate.VerifierRunRefs = append(append([]work.AgentRunID(nil), gate.VerifierRunRefs...), request.VerifierRunID)
gate.VerificationEvidence = result.EvidenceRef
advanced, err := advanceDevelopmentGate(gate, work.DevelopmentVerified, now)
if err != nil {
return gate, result, err
}
if err := store.SaveDevelopmentGate(ctx, advanced, request.ExpectedRevision); err != nil {
return gate, result, err
}
return advanced, result, nil
}
func sameRun(value work.AgentRunID, runs []work.AgentRunID) bool {
for _, run := range runs {
if run == value {
return true
}
}
return false
}
func prepareDevelopmentGate(value work.DevelopmentGate, now time.Time) (work.DevelopmentGate, error) {
if value.Stage != work.DevelopmentEvidenceCollected || value.Revision != 1 {
+141
View File
@@ -1,12 +1,153 @@
package main
import (
"context"
"path/filepath"
"testing"
"time"
"git.cin.su/m/Assistant/adapters/developmentexecutor"
"git.cin.su/m/Assistant/contracts/work"
)
type developmentGateStoreStub struct {
gate work.DevelopmentGate
saved bool
expected int64
}
func (s *developmentGateStoreStub) LoadDevelopmentGate(context.Context, string) (work.DevelopmentGate, error) {
return s.gate, nil
}
func (s *developmentGateStoreStub) SaveDevelopmentGate(_ context.Context, gate work.DevelopmentGate, expected int64) error {
s.gate = gate
s.saved = true
s.expected = expected
return nil
}
type developmentCommandRunnerStub struct {
exitCodes []int
commands int
}
func (r *developmentCommandRunnerStub) Run(context.Context, string, string, []string) int {
code := 0
if r.commands < len(r.exitCodes) {
code = r.exitCodes[r.commands]
}
r.commands++
return code
}
func validDevelopmentVerificationPlan(t *testing.T, worktreeID string) developmentexecutor.Plan {
t.Helper()
worktree, err := filepath.Abs(".")
if err != nil {
t.Fatal(err)
}
return developmentexecutor.Plan{
Worktree: worktree,
WorktreeID: worktreeID,
BaseRepoRevision: "base-revision",
ProtectedMainAccess: "DENIED",
ProductionAccess: "DENIED",
SecretAccess: "DENIED",
Checks: []developmentexecutor.Check{{Kind: developmentexecutor.CheckGitDiffCheck}},
}
}
func isolatedDevelopmentGate() work.DevelopmentGate {
return work.DevelopmentGate{
ID: "gate-1",
Revision: 4,
Stage: work.DevelopmentIsolated,
EvidenceRefs: []string{"evidence-1"},
ProposalRef: "proposal-1",
TaskPacketRef: "task-1",
WorktreeRef: "sandbox-1",
ProducerRunRefs: []work.AgentRunID{"producer-1"},
VerifierRunRefs: nil,
VerificationEvidence: "",
}
}
func TestVerifyDevelopmentGatePersistsVerifiedWithIndependentEvidence(t *testing.T) {
store := &developmentGateStoreStub{gate: isolatedDevelopmentGate()}
runner := &developmentCommandRunnerStub{exitCodes: []int{0}}
gate, result, err := verifyDevelopmentGate(context.Background(), store, developmentGateVerificationRequest{
GateID: "gate-1",
ExpectedRevision: 4,
VerifierRunID: "verifier-1",
Plan: validDevelopmentVerificationPlan(t, "sandbox-1"),
}, time.UnixMilli(20), runner)
if err != nil {
t.Fatal(err)
}
if !result.Passed || gate.Stage != work.DevelopmentVerified || gate.Revision != 5 {
t.Fatalf("verification did not advance gate: gate=%#v result=%#v", gate, result)
}
if !store.saved || store.expected != 4 || gate.VerificationEvidence == "" {
t.Fatalf("verification was not durably persisted: store=%#v gate=%#v", store, gate)
}
}
func TestVerifyDevelopmentGateLeavesIsolatedOnFailedChecks(t *testing.T) {
store := &developmentGateStoreStub{gate: isolatedDevelopmentGate()}
gate, result, err := verifyDevelopmentGate(context.Background(), store, developmentGateVerificationRequest{
GateID: "gate-1",
ExpectedRevision: 4,
VerifierRunID: "verifier-1",
Plan: validDevelopmentVerificationPlan(t, "sandbox-1"),
}, time.UnixMilli(20), &developmentCommandRunnerStub{exitCodes: []int{1}})
if err != errDevelopmentChecksFailed || result.Passed || gate.Stage != work.DevelopmentIsolated || store.saved {
t.Fatalf("failed checks changed gate unexpectedly: gate=%#v result=%#v err=%v store=%#v", gate, result, err, store)
}
}
func TestVerifyDevelopmentGateRejectsProducerAsVerifier(t *testing.T) {
store := &developmentGateStoreStub{gate: isolatedDevelopmentGate()}
_, _, err := verifyDevelopmentGate(context.Background(), store, developmentGateVerificationRequest{
GateID: "gate-1",
ExpectedRevision: 4,
VerifierRunID: "producer-1",
Plan: validDevelopmentVerificationPlan(t, "sandbox-1"),
}, time.UnixMilli(20), &developmentCommandRunnerStub{exitCodes: []int{0}})
if err != errDevelopmentVerifierNotIndependent {
t.Fatalf("producer was accepted as verifier: %v", err)
}
}
func TestRecordDevelopmentGateEvidenceAdvancesCanaryAndPromotion(t *testing.T) {
store := &developmentGateStoreStub{gate: work.DevelopmentGate{
ID: "gate-1", Revision: 6, Stage: work.DevelopmentVerified,
EvidenceRefs: []string{"evidence-1"}, ProposalRef: "proposal-1", TaskPacketRef: "task-1", WorktreeRef: "sandbox-1",
ProducerRunRefs: []work.AgentRunID{"producer-1"}, VerifierRunRefs: []work.AgentRunID{"verifier-1"}, VerificationEvidence: "verification-1",
}}
gate, err := recordDevelopmentGateEvidence(context.Background(), store, developmentGateEvidenceRequest{GateID: "gate-1", ExpectedRevision: 6, NextStage: work.DevelopmentCanary, EvidenceRef: "canary-1"}, time.UnixMilli(30))
if err != nil || gate.Stage != work.DevelopmentCanary || gate.Revision != 7 || gate.CanaryEvidence != "canary-1" {
t.Fatalf("canary transition failed: gate=%#v err=%v", gate, err)
}
gate, err = recordDevelopmentGateEvidence(context.Background(), store, developmentGateEvidenceRequest{GateID: "gate-1", ExpectedRevision: 7, NextStage: work.DevelopmentPromoted, EvidenceRef: "promotion-1"}, time.UnixMilli(31))
if err != nil || gate.Stage != work.DevelopmentPromoted || gate.Revision != 8 || gate.PromotionRef != "promotion-1" {
t.Fatalf("promotion transition failed: gate=%#v err=%v", gate, err)
}
}
func TestRecordDevelopmentGateEvidenceSupportsCanaryRollbackAndRejectsMissingEvidence(t *testing.T) {
store := &developmentGateStoreStub{gate: work.DevelopmentGate{ID: "gate-1", Revision: 2, Stage: work.DevelopmentCanary, EvidenceRefs: []string{"evidence-1"}, CanaryEvidence: "canary-1"}}
gate, err := recordDevelopmentGateEvidence(context.Background(), store, developmentGateEvidenceRequest{GateID: "gate-1", ExpectedRevision: 2, NextStage: work.DevelopmentRolledBack, EvidenceRef: "rollback-1"}, time.UnixMilli(40))
if err != nil || gate.Stage != work.DevelopmentRolledBack || gate.RollbackRef != "rollback-1" {
t.Fatalf("rollback transition failed: gate=%#v err=%v", gate, err)
}
store = &developmentGateStoreStub{gate: work.DevelopmentGate{ID: "gate-2", Revision: 2, Stage: work.DevelopmentCanary, EvidenceRefs: []string{"evidence-1"}, CanaryEvidence: "canary-1"}}
gate, err = recordDevelopmentGateEvidence(context.Background(), store, developmentGateEvidenceRequest{GateID: "gate-2", ExpectedRevision: 2, NextStage: work.DevelopmentRolledBack}, time.UnixMilli(40))
if err != errDevelopmentEvidenceRequired || gate.Stage != work.DevelopmentCanary || store.saved {
t.Fatalf("missing rollback evidence changed gate: gate=%#v err=%v store=%#v", gate, err, store)
}
}
func TestDevelopmentGateControllerRequiresSequentialVerifiedEvidence(t *testing.T) {
now := time.UnixMilli(10)
gate, err := prepareDevelopmentGate(work.DevelopmentGate{ID: "gate-1", Revision: 1, Stage: work.DevelopmentEvidenceCollected, EvidenceRefs: []string{"evidence-1"}}, now)
+123
View File
@@ -0,0 +1,123 @@
package main
import (
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestHouseholdResearchAndActionRoutesRemainIndependent(t *testing.T) {
candidate := interactiondomain.BuildDeterministicCandidate("проверь погоду в Казани, найди кафе рядом и напомни вечером позвонить", "", time.Unix(10, 0).UnixMilli())
planned := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: candidate.NeedsCognition, CapabilityHints: candidate.CapabilityHints})
want := map[chatRouteKind]bool{chatRouteResearch: false, chatRouteConversation: false, chatRouteAction: false}
for _, route := range planned {
if _, ok := want[route.Kind]; ok {
want[route.Kind] = true
}
}
for kind, present := range want {
if !present {
t.Fatalf("route %s missing for mixed household request: candidate=%+v routes=%+v", kind, candidate, planned)
}
}
}
func TestHouseholdCreativeRouteDoesNotUseResearch(t *testing.T) {
candidate := interactiondomain.BuildDeterministicCandidate("расскажи сказку о Колобке", "", time.Unix(10, 0).UnixMilli())
for _, hint := range candidate.CapabilityHints {
if capability.CapabilityKind(hint) == capability.CapabilityKindWebResearch {
t.Fatalf("creative request selected web research: %+v", candidate)
}
}
if !interactiondomain.IsCreativeRequest("расскажи сказку о Колобке") {
t.Fatal("creative request was not recognized")
}
}
func TestHouseholdMarketplaceBrandSearchUsesResearchRoute(t *testing.T) {
candidate := interactiondomain.BuildDeterministicCandidate("найди на WB бренд marka-wear", "", time.Unix(10, 0).UnixMilli())
decomposition := interactiondomain.DecomposeTaskWithLexicon("найди на WB бренд marka-wear", interactiondomain.TaskLexicon{ObjectAliases: []interactiondomain.TaskLexiconEntry{{Alias: "бренд", Value: "BRAND"}}, SourceAliases: []interactiondomain.TaskLexiconEntry{{Alias: "wb", Value: "WILDBERRIES"}}})
if decomposition.Intent != interactiondomain.TaskIntentSearch || decomposition.Object != "BRAND" || decomposition.Source != "WILDBERRIES" || decomposition.Target != "marka-wear" || len(decomposition.Missing) != 0 {
t.Fatalf("marketplace request was not split into executable atoms: %+v", decomposition)
}
routes := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: candidate.NeedsCognition, CapabilityHints: candidate.CapabilityHints})
if !hasChatRoute(routes, chatRouteResearch) {
t.Fatalf("marketplace brand request did not select research route: candidate=%+v routes=%+v", candidate, routes)
}
}
func TestHouseholdSemanticMemoryControlsMarketplaceAlias(t *testing.T) {
withoutMemory := interactiondomain.DecomposeTask("найди на WB бренд marka-wear")
if withoutMemory.Source != "" || withoutMemory.Object != "" {
t.Fatalf("core invented marketplace meaning without memory: %+v", withoutMemory)
}
withMemory := interactiondomain.DecomposeTaskWithLexicon("поищи марку marka-wear в Wildberries", interactiondomain.TaskLexicon{
ObjectAliases: []interactiondomain.TaskLexiconEntry{{Alias: "марку", Value: "BRAND"}},
SourceAliases: []interactiondomain.TaskLexiconEntry{{Alias: "wildberries", Value: "WILDBERRIES"}},
})
if withMemory.Intent != interactiondomain.TaskIntentSearch || withMemory.Object != "BRAND" || withMemory.Source != "WILDBERRIES" || withMemory.Target != "marka-wear" || !withMemory.NeedsResearch {
t.Fatalf("semantic memory did not control marketplace decomposition: %+v", withMemory)
}
}
func TestHouseholdBearDrawingUsesCreativeLocalRoute(t *testing.T) {
candidate := interactiondomain.BuildDeterministicCandidate("нарисуй бурого медведя", "", time.Unix(10, 0).UnixMilli())
decomposition := interactiondomain.DecomposeTaskWithLexicon("нарисуй бурого медведя", interactiondomain.TaskLexicon{ObjectAliases: []interactiondomain.TaskLexiconEntry{{Alias: "медвед", Value: "IMAGE"}}})
if decomposition.Intent != interactiondomain.TaskIntentCreate || decomposition.Object != "IMAGE" || decomposition.Target != "бурого медведя" || decomposition.NeedsResearch {
t.Fatalf("creative request was not split into local execution atoms: %+v", decomposition)
}
routes := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: candidate.NeedsCognition, CapabilityHints: candidate.CapabilityHints})
if !hasChatRoute(routes, chatRouteMedia) {
t.Fatalf("creative image request did not select media route: candidate=%+v routes=%+v", candidate, routes)
}
for _, hint := range candidate.CapabilityHints {
if capability.CapabilityKind(hint) == capability.CapabilityKindWebResearch {
t.Fatalf("bear drawing selected web research: %+v", candidate)
}
}
for _, message := range []string{"нарисуй бурого медведя", "нарисуй птицу", "нарисуй зеленую птицу", "нарисуй птицу в HD", "нарисуй коня"} {
if got := interactiondomain.CreativeDeterministicReply(message); got != "" {
t.Fatalf("creative request %q received a canned reply=%q", message, got)
}
}
}
func TestHouseholdRouteMatrixKeepsMeaningBoundaries(t *testing.T) {
tests := []struct {
message string
wantResearch bool
wantCreative bool
wantHint string
}{
{message: "найди на WB бренд marka-wear", wantResearch: true},
{message: "какая погода в Казани?", wantResearch: true},
{message: "найди билеты до Сочи", wantResearch: true},
{message: "нарисуй птицу", wantCreative: true},
{message: "расскажи сказку о Колобке", wantCreative: true},
{message: "что такое гравитация?", wantHint: "COGNITIVE_REASONING"},
}
for _, test := range tests {
candidate := interactiondomain.BuildDeterministicCandidate(test.message, "", time.Unix(10, 0).UnixMilli())
if interactiondomain.IsCreativeRequest(test.message) != test.wantCreative {
t.Fatalf("message=%q creative=%v want=%v candidate=%+v", test.message, interactiondomain.IsCreativeRequest(test.message), test.wantCreative, candidate)
}
gotResearch := false
gotHint := false
for _, hint := range candidate.CapabilityHints {
if hint == "WEB_RESEARCH" {
gotResearch = true
}
if hint == test.wantHint {
gotHint = true
}
}
if gotResearch != test.wantResearch {
t.Fatalf("message=%q research=%v want=%v candidate=%+v", test.message, gotResearch, test.wantResearch, candidate)
}
if test.wantHint != "" && !gotHint {
t.Fatalf("message=%q missing hint=%q candidate=%+v", test.message, test.wantHint, candidate)
}
}
}
@@ -0,0 +1,46 @@
package main
import "testing"
func TestShouldInvokeHumanResponseSkipsDeterministicFastPaths(t *testing.T) {
tests := []struct {
name string
stableConversational bool
identityReply string
memoryInstructionReply string
want bool
}{
{name: "stable conversational", stableConversational: true},
{name: "identity", identityReply: "Меня зовут Домовой."},
{name: "memory instruction", memoryInstructionReply: "Запомнил."},
{name: "ordinary continuity", want: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := shouldInvokeHumanResponse(test.stableConversational, test.identityReply, test.memoryInstructionReply); got != test.want {
t.Fatalf("shouldInvokeHumanResponse() = %t, want %t", got, test.want)
}
})
}
}
func TestShouldReviewResponseSkipsStableAnswers(t *testing.T) {
if shouldReviewResponse("Здравствуйте.", false, true, "", "", false) {
t.Fatal("stable conversational reply must not wait for response review")
}
if shouldReviewResponse("Меня зовут Домовой.", false, false, "Меня зовут Домовой.", "", false) {
t.Fatal("identity reply must not wait for response review")
}
if shouldReviewResponse("Запомнил.", false, false, "", "Запомнил.", false) {
t.Fatal("memory instruction reply must not wait for response review")
}
if shouldReviewResponse("Вы называли тестовый цвет: синий.", false, false, "", "", true) {
t.Fatal("deterministic memory reply must not wait for response review")
}
if !shouldReviewResponse("Ответ", false, false, "", "", false) {
t.Fatal("non-conversational answer remains reviewable")
}
if shouldReviewResponse("Ответ", true, false, "", "", false) {
t.Fatal("stable knowledge answer must not be reviewed twice")
}
}
@@ -0,0 +1,27 @@
package main
import (
"testing"
"git.cin.su/m/Assistant/contracts/interaction"
)
func TestSpecializedInformationReplyIsNotOverwrittenByResearch(t *testing.T) {
for _, kind := range []interaction.InformationRequestKind{
interaction.InformationRequestMemoryInventory,
interaction.InformationRequestCapabilityInventory,
interaction.InformationRequestCognitiveModelInventory,
interaction.InformationRequestCognitiveChannelStatus,
interaction.InformationRequestCognitiveRecovery,
} {
if shouldRunGeneralResearch(kind) {
t.Fatalf("specialized information request %q must bypass general research", kind)
}
}
if !shouldRunGeneralResearch(interaction.InformationRequestTimeDifference) {
t.Fatal("time difference must use the current-data research route")
}
if !shouldRunGeneralResearch("") {
t.Fatal("ordinary research requests must still use general research")
}
}
+6
View File
@@ -13,6 +13,7 @@ import (
"git.cin.su/m/Assistant/contracts/interaction"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
"git.cin.su/m/Assistant/platform/telemetry"
)
const reminderResponseSkillID = capability.SkillProfileID("skill-obvious-reminder-response-v1")
@@ -160,6 +161,7 @@ func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastP
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
return
}
telemetry.RecordCompatibility("fast-path-v1", "core-runtime", "decision", "decline-or-escalate", true, true, false, "OBSERVED", string(input.Utterance.CorrelationID), string(input.Utterance.PrincipalID), input.At)
experience, err := epistemicsdomain.FastPathDeclineExperience(
epistemics.OriginalIntentRef(input.Intent.ID),
epistemics.SourceUtteranceRef(input.Utterance.ID),
@@ -273,9 +275,11 @@ func (r learningRecorder) updateReminderSkill(ctx context.Context, experience ep
}
profile, err := r.capability.LoadSkillProfile(ctx, reminderResponseSkillID)
if err != nil {
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "read", "fast-path-learning", true, false, true, "FAILURE", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
log.Printf("learning skill profile unavailable: %v", err)
return
}
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "read", "fast-path-learning", true, false, true, "SUCCESS", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
next, err := capabilitydomain.ObserveSkillOutcome(profile, capability.SkillOutcomeObservation{
ExperienceRef: capability.SkillEvidenceRef("epistemics:experience:" + string(experience.ID)),
Outcome: outcome,
@@ -287,9 +291,11 @@ func (r learningRecorder) updateReminderSkill(ctx context.Context, experience ep
return
}
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "write", "fast-path-learning", true, false, true, "FAILURE", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
log.Printf("learning skill profile save unavailable: %v", err)
return
}
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "write", "fast-path-learning", true, false, true, "SUCCESS", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
}
func outcomeForHumanResponse(response continuity.HumanResponse) (epistemics.OutcomeClass, int64) {
+1104 -81
View File
File diff suppressed because it is too large Load Diff
+246 -3
View File
@@ -11,10 +11,183 @@ import (
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/contracts/work"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestHumanResearchExcerptRemovesSearchResultHeading(t *testing.T) {
got := humanResearchExcerpt("погода в Санкт-Петербурге", "Погода в Санкт-Петербурге — Прогноз погоды — Сейчас пасмурно, температура +14°")
if got != "Сейчас пасмурно, температура +14°" || strings.Contains(got, "Прогноз погоды") {
t.Fatalf("human research excerpt = %q", got)
}
}
func TestNearestFlightEvidenceDoesNotClaimMissingDepartureTime(t *testing.T) {
if !nearestFlightQuery("ближайший рейс Москва Казань сегодня") {
t.Fatal("nearest flight query was not recognized")
}
if containsFlightTimeEvidence("Казань составляет 1 ч 25 мин. График вылетов доступен онлайн") {
t.Fatal("duration was mistaken for a departure time")
}
if !containsFlightTimeEvidence("Ближайший вылет в 18:40") {
t.Fatal("departure time was not recognized")
}
}
func TestNearestFlightReplyFiltersUnrelatedSearchSnippets(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://naidy.example", Title: "Мебельная фурнитура"}, Excerpt: epistemics.SourceExcerpt{Text: "Комплектующие для мебели оптом и в розницу."}},
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://rasp.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание самолётов Москва — Казань, время вылета и прилёта."}},
}}
reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil)
if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Мебельная") {
t.Fatalf("unrelated source leaked into flight reply: %q", reply)
}
}
func TestNearestFlightReplyDoesNotAppendWeakerSecondMatch(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://schedule.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов Москва — Казань, время вылета и прилёта."}},
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://category.example", Title: "Категория направлений"}, Excerpt: epistemics.SourceExcerpt{Text: "Список близлежащих городов и ориентировочные цены."}},
}}
reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil)
if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Категория направлений") {
t.Fatalf("weaker second finding leaked into nearest-flight reply: %q", reply)
}
}
func TestNearestFlightEvidenceRequiresBothRouteEndpointsWhenKnown(t *testing.T) {
query := "ближайшие авиарейсы Москва Казань сегодня"
matched := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов из Москвы в Казань."}}
if !researchFindingMatchesIntent(query, matched) {
t.Fatal("route evidence with both endpoints was rejected")
}
unrelated := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань", Locator: "https://example.test/moscow-kazan"}, Excerpt: epistemics.SourceExcerpt{Text: "Рейсы Москва — Казань — Ближайшие к Казани города; цена указана ориентировочно."}}
if researchFindingMatchesIntent(query, unrelated) {
t.Fatal("one-ended travel evidence was accepted")
}
}
func TestTicketReplyRejectsGenericCategoryPageWithoutDestination(t *testing.T) {
query := "найди билеты до Ялты сегодня"
generic := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Купить билеты", Locator: "https://tickets.example"}, Excerpt: epistemics.SourceExcerpt{Text: "Поиск авиабилетов среди множества направлений."}}
if researchFindingMatchesIntent(query, generic) {
t.Fatal("generic ticket category was presented as destination result")
}
relevant := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Билеты"}, Excerpt: epistemics.SourceExcerpt{Text: "Билеты до Ялты: варианты на сегодня."}}
if !researchFindingMatchesIntent(query, relevant) {
t.Fatal("destination-specific ticket result was rejected")
}
}
func TestDistanceReplyRejectsCalculatorPageWithoutDistance(t *testing.T) {
query := "расстояние между Москве и Сочи в километрах по прямой и по дороге"
calculator := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Расчет расстояний"}, Excerpt: epistemics.SourceExcerpt{Text: "Калькулятор рассчитывает точное расстояние между городами."}}
if researchFindingMatchesIntent(query, calculator) {
t.Fatal("distance calculator landing page was presented without a measured distance")
}
measured := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Маршрут"}, Excerpt: epistemics.SourceExcerpt{Text: "Расстояние по дороге составляет 1 620 км."}}
if !researchFindingMatchesIntent(query, measured) {
t.Fatal("measured distance evidence was rejected")
}
}
func TestResearchReplyRejectsGenericLandingPageForConcreteIdentifier(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
State: epistemics.FindingSupported,
Source: epistemics.Source{Locator: "https://market.example", Title: "Интернет-магазин"},
Excerpt: epistemics.SourceExcerpt{Text: "Широкий ассортимент товаров и услуг."},
}}}
reply, backed := renderResearchReply("на WB бренд marka-wear", result, nil)
if backed || !strings.Contains(reply, "marka-wear") || strings.Contains(reply, "wildberries") || !strings.Contains(reply, "уточните написание") {
t.Fatalf("generic page was presented as a brand result: %q", reply)
}
}
func TestLocalPlaceReplyRejectsDictionaryFinding(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
State: epistemics.FindingSupported,
Source: epistemics.Source{Locator: "https://dictionary.example", Title: "Ближайший — словарь"},
Excerpt: epistemics.SourceExcerpt{Text: "Ближайший — значение слова и пример употребления."},
}}}
reply, backed := renderResearchReply("места ресторан в Сочи адрес отзывы меню", result, nil)
if backed || !strings.Contains(reply, "Проверяемых данных") {
t.Fatalf("dictionary finding was presented as local place result: %q", reply)
}
}
func TestResearchReplyKeepsFindingContainingConcreteIdentifier(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
State: epistemics.FindingSupported,
Source: epistemics.Source{Locator: "https://market.example/marka-wear", Title: "Marka-wear"},
Excerpt: epistemics.SourceExcerpt{Text: "Бренд marka-wear: товары и карточки."},
}}}
reply, backed := renderResearchReply("на WB бренд marka-wear", result, nil)
if !backed || !strings.Contains(strings.ToLower(reply), "marka-wear") {
t.Fatalf("concrete identifier finding was rejected: %q", reply)
}
}
func TestResearchIdentifiersExcludeSearchDate(t *testing.T) {
identifiers := requestedResearchIdentifiers("ближайший поезд в Петербург сегодня 26.08.2026")
if len(identifiers) != 0 {
t.Fatalf("search date was treated as a concrete identifier: %#v", identifiers)
}
}
func TestResearchQueryKeepsDistanceRelationBeforeSearchNormalization(t *testing.T) {
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
query := interactiondomain.TravelResearchQuery("сколько километров до Сочи?", history, time.Time{})
query = interactiondomain.SearchQueryWithLexicon(query, interactiondomain.TaskLexicon{})
if query != "расстояние между Москве и Сочи в километрах по прямой и по дороге" {
t.Fatalf("distance query lost remembered origin: %q", query)
}
}
func TestResponseReviewPreservesAttemptedResearchOutcome(t *testing.T) {
if reviewMayReplacePlanningReply(false, false, true, false, "Уточните аэропорт.", "Пока не смог получить проверяемый ответ.") {
t.Fatal("review replaced an honest unbacked research outcome")
}
if !reviewMayReplacePlanningReply(false, false, true, true, "Расписание. Источник: https://example.test", "Расписание. Источник: https://example.test") {
t.Fatal("review should be able to improve a source-backed research outcome")
}
if reviewMayReplacePlanningReply(false, false, true, true, "Расписание без ссылки", "Расписание. Источник: https://example.test") {
t.Fatal("review dropped the research evidence")
}
if !reviewMayReplacePlanningReply(false, false, false, false, "Нормальный ответ", "Черновик") {
t.Fatal("ordinary conversation review was unexpectedly blocked")
}
if !reviewMayReplacePlanningReply(true, true, false, false, "Санкт-Петербург — город в России.", "Меня зовут Домовой. Санкт-Петербург — город в России.") {
t.Fatal("stable-knowledge draft must be eligible for semantic quality review")
}
// The caller must not mark a source-backed research result as immutable
// semantic state: it may be rewritten, but only with its evidence retained.
if !reviewMayReplacePlanningReply(false, false, true, true, "Из Москвы в Сочи можно добраться самолётом или поездом. Источник: https://example.test", "Сырые результаты. Источник: https://example.test") {
t.Fatal("source-backed research draft must be eligible for human-facing review")
}
}
func TestNonMutatingTextDirectiveCanUseCognitiveReply(t *testing.T) {
semantic := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: interaction.SpeechActDirective}}
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{}, false) {
t.Fatal("ordinary formatting directive was not eligible for a cognitive reply")
}
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathCognition}, false) {
t.Fatal("reply-only cognition path was incorrectly rejected")
}
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathDirectCapability}, false) {
t.Fatal("direct capability was incorrectly replaced by a textual reply")
}
if shouldGenerateNonMutatingTextDirective(semantic, []chatRoute{{Kind: chatRouteMedia}}, work.HelpPath{}, false) {
t.Fatal("media route was incorrectly replaced by a textual reply")
}
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{NeedsDurableWork: true}, false) {
t.Fatal("durable work was incorrectly replaced by a textual reply")
}
}
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
request := httptest.NewRequest("GET", "/", nil)
request.Header.Set("Authorization", "Basic value")
@@ -27,6 +200,59 @@ func TestBearerTokenRequiresBearerScheme(t *testing.T) {
}
}
func TestLatestAssistantNameUsesNewestDurablePreference(t *testing.T) {
history := []interaction.HumanUtterance{
{Content: "я назову тебя Дед", ReceivedAtUnixMillis: 20},
{Content: "теперь тебя зовут Фея", ReceivedAtUnixMillis: 30},
}
if got := latestAssistantName(history); got != "Фея" {
t.Fatalf("latest assistant name = %q", got)
}
}
func TestHumanMemoryInventoryUsesNewestPreference(t *testing.T) {
history := []interaction.HumanUtterance{
{Content: "мне нравится синий цвет", ReceivedAtUnixMillis: 20},
{Content: "мне нравится красный цвет", ReceivedAtUnixMillis: 30},
}
got := humanMemoryInventory(history, "Фея")
if !strings.Contains(got, "красный") || strings.Contains(got, "синий") {
t.Fatalf("memory inventory did not select newest preference: %q", got)
}
}
func TestContainsScheduleTimeEvidence(t *testing.T) {
if containsScheduleTimeEvidence("расписание поездов и цена билета") {
t.Fatal("a price or schedule label is not a departure time")
}
if !containsScheduleTimeEvidence("Отправление в 07:45") {
t.Fatal("expected an explicit departure time to be recognized")
}
}
func TestInternetStatusQueryRecognizesConnectionChecks(t *testing.T) {
for _, query := range []string{"проверь, работает ли интернет", "есть доступ к сети?"} {
if !isInternetStatusQuery(query) {
t.Fatalf("connection check was not recognized: %q", query)
}
}
if isInternetStatusQuery("найди интернет-магазин") {
t.Fatal("ordinary search must not be treated as a connectivity check")
}
}
func TestInternetStatusReplyDoesNotLeakIncidentalSearchResults(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
State: epistemics.FindingSupported,
Source: epistemics.Source{Locator: "https://example.test", Title: "Incidental page"},
Excerpt: epistemics.SourceExcerpt{Text: "A response was received."},
}}}
reply, backed := renderResearchReply("проверь, работает ли интернет", result, nil)
if !backed || reply != "Интернет работает: Домовой получил ответ от внешнего источника." {
t.Fatalf("unexpected internet status reply: %q", reply)
}
}
func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveChannelStatus, interaction.SpeechActQuestion, capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil)
if !strings.Contains(reply, "доступен") || strings.Contains(reply, "provider-secret-model") {
@@ -34,16 +260,23 @@ func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
}
}
func TestCurrentModelReplyDoesNotInventProvider(t *testing.T) {
reply := humanCurrentCognitiveModelReply(capability.Health{State: capability.HealthDegraded, CapabilityID: "provider-secret-model"}, nil)
if !strings.Contains(reply, "встроенным режимом") || !strings.Contains(reply, "ограничениями") || strings.Contains(reply, "provider-secret-model") {
t.Fatalf("unexpected current-model reply: %q", reply)
}
}
func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) {
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveRecovery, interaction.SpeechActDirective, capability.Health{State: capability.HealthUnavailable}, nil)
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "аварийн") {
if reply != "Канал мышления недоступен. Домовой остаётся на связи." {
t.Fatalf("unexpected configuration reply: %q", reply)
}
}
func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) {
reply := humanCognitiveDegradedReply("Помоги настроить локальную модель")
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "принято") {
if strings.Contains(reply, "когнитивный канал") || strings.Contains(reply, "Повторю") || !strings.Contains(reply, "не выдумываю") {
t.Fatalf("unexpected degraded reply: %q", reply)
}
}
@@ -74,11 +307,21 @@ func TestHumanCognitiveModelInventoryDoesNotExposeSecrets(t *testing.T) {
Provider: capability.ProviderInstance{Kind: "openai-compatible-chat-v1", CredentialRef: "secret-ref", AuthMode: capability.AuthKey, BillingMode: capability.BillingPerRequest, RequestCostMicros: &cost, Health: capability.HealthAvailable},
Model: capability.ModelDescriptor{ProviderModelID: "example-model", Health: capability.HealthAvailable},
}}, nil)
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "API-ключ") {
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "активна") {
t.Fatalf("unsafe or incomplete inventory: %q", text)
}
}
func TestHumanCognitiveModelInventoryDeduplicatesAndPrioritizesAvailable(t *testing.T) {
text := humanCognitiveModelInventory([]capability.ResolvedCognitiveBinding{
{Binding: capability.CognitiveBinding{Activation: capability.ActivationInactive}, Provider: capability.ProviderInstance{Health: capability.HealthUnavailable}, Model: capability.ModelDescriptor{ProviderModelID: "same-model", Health: capability.HealthUnavailable}},
{Binding: capability.CognitiveBinding{Activation: capability.ActivationActive}, Provider: capability.ProviderInstance{Health: capability.HealthAvailable}, Model: capability.ModelDescriptor{ProviderModelID: "same-model", Health: capability.HealthAvailable}},
}, nil)
if strings.Count(text, "same-model") != 1 || !strings.Contains(text, "same-model — активна") {
t.Fatalf("expected one active model entry, got %q", text)
}
}
func TestCreatorSessionHandlerAcceptsConfiguredCredentials(t *testing.T) {
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), func() time.Time { return time.Unix(1000, 0) })
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(`{"username":"m","password":"password"}`))
+25
View File
@@ -51,3 +51,28 @@ func (c researchRuntimeClient) EvaluateWeb(ctx context.Context, request epistemi
}
return result, nil
}
func (c researchRuntimeClient) SearchWeb(ctx context.Context, request epistemics.WebSearchRequest) (epistemics.ResearchResult, error) {
payload, err := json.Marshal(request)
if err != nil {
return epistemics.ResearchResult{}, err
}
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/research/web/search", bytes.NewReader(payload))
if err != nil {
return epistemics.ResearchResult{}, err
}
httpRequest.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(httpRequest)
if err != nil {
return epistemics.ResearchResult{}, err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return epistemics.ResearchResult{}, fmt.Errorf("research search status %d", response.StatusCode)
}
var result epistemics.ResearchResult
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return epistemics.ResearchResult{}, err
}
return result, nil
}
+132
View File
@@ -40,6 +40,11 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) {
if !strings.Contains(body, "версия 0123456789ab") || !strings.Contains(body, "обновлено 22.08.2026 03:04 UTC") || !strings.Contains(body, "datetime=\"2026-08-22T03:04:05Z\"") {
t.Fatalf("/chat does not render build metadata: %q", body)
}
for _, required := range []string{"media-tools", "audio-input", "image-input", "ocr-input"} {
if !strings.Contains(body, required) {
t.Fatalf("/chat is missing multimodal control %q", required)
}
}
for _, path := range []string{"/", "/chat.html"} {
alternate := httptest.NewRecorder()
handler.ServeHTTP(alternate, httptest.NewRequest(http.MethodGet, path, nil))
@@ -60,6 +65,11 @@ func TestChatScriptExplainsSessionAndRuntimeFailures(t *testing.T) {
t.Fatalf("chat script is missing recovery message %q", required)
}
}
for _, required := range []string{"uploadMedia", "/api/multimodal/transcribe", "/api/multimodal/understand", "/api/multimodal/ocr"} {
if !strings.Contains(script, required) {
t.Fatalf("chat script is missing multimodal route %q", required)
}
}
}
func TestPublicSurfacesSeparateChatAndConsole(t *testing.T) {
@@ -151,6 +161,128 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
}
}
func TestChatMessageDoesNotExposeUnverifiedQuestionAsFact(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, CapabilityHints: []capability.CapabilityKind{capability.CapabilityKindWebResearch}, FallbackText: "небезопасная модельная догадка"})
default:
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
}
}))
defer core.Close()
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/advisories" {
t.Fatalf("unexpected cognitive route: %s %s", r.Method, r.URL.Path)
}
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Я продолжаю искать проверяемый ответ через доступный источник.", FinishReason: cognitive.FinishCompleted})
}))
defer cognitiveRuntime.Close()
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), cognitiveRuntime.URL)
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Какая погода?"}`))
request.Header.Set("Authorization", "Bearer session-token")
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d body=%s", recorder.Code, recorder.Body.String())
}
var response browserChatResponse
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatal(err)
}
if response.Cognition != string(cognitive.FinishCompleted) || strings.Contains(response.ReplyText, "небезопасная") || !strings.Contains(response.ReplyText, "продолжаю искать") {
t.Fatalf("unexpected research recovery response: %#v", response)
}
}
func TestChatMessageUsesLocalAdvisoryForOrdinaryConversationalQuestion(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, FallbackText: "fallback"})
default:
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
}
}))
defer core.Close()
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/advisories" {
t.Fatalf("unexpected cognitive path %s", r.URL.Path)
}
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Я рядом. Давай поговорим.", FinishReason: cognitive.FinishCompleted})
}))
defer cognitiveRuntime.Close()
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), cognitiveRuntime.URL)
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Как ты?"}`))
request.Header.Set("Authorization", "Bearer session-token")
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d body=%s", recorder.Code, recorder.Body.String())
}
var response browserChatResponse
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatal(err)
}
if response.ReplyText != "Я рядом. Давай поговорим." || response.Cognition != string(cognitive.FinishCompleted) {
t.Fatalf("unexpected conversational response: %#v", response)
}
}
func TestChatMessagePreservesCompletedCoreAnswerInsteadOfSecondAdvisory(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, FallbackText: "fallback", PlanningReply: "Готовый ответ ядра."})
default:
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
}
}))
defer core.Close()
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("completed core answer must not be replaced by a second advisory")
}))
defer cognitiveRuntime.Close()
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), cognitiveRuntime.URL)
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Обычный вопрос"}`))
request.Header.Set("Authorization", "Bearer session-token")
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
var response browserChatResponse
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.ReplyText != "Готовый ответ ядра." {
t.Fatalf("unexpected browser response: status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
func TestChatMessageReturnsHumanCapabilityMetadata(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
+8 -1
View File
@@ -13,7 +13,14 @@ func main() {
envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"),
envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"),
envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"),
&http.Client{Timeout: 10 * time.Second},
// Core chat may spend several seconds on semantic interpretation and
// local cognition. Keep the browser gateway alive long enough to return
// the completed response instead of converting a successful core 201
// into a misleading 502.
// A cold local model may need more than one short HTTP round-trip to
// load on CPU. Keep the browser gateway aligned with the cognition
// deadline instead of turning that wait into a false unavailable reply.
&http.Client{Timeout: 15 * time.Minute},
envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"),
envOr("AGENT_WORKER_URL", "http://agent-worker:8090"),
)
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"bytes"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/identity"
)
func TestMultimodalUploadRequiresSessionAndProxiesLocalResult(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer session" {
t.Fatalf("unexpected session request: %s %q", r.URL.Path, r.Header.Get("Authorization"))
}
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"})
}))
defer core.Close()
cognitive := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/multimodal/transcribe" || r.Method != http.MethodPost {
t.Fatalf("unexpected multimodal request: %s %s", r.Method, r.URL.Path)
}
_ = json.NewEncoder(w).Encode(capability.MultimodalResponse{Operation: capability.MultimodalTranscribe, Text: "локальный текст", Provider: "local-whisper", ObservedAt: 1})
}))
defer cognitive.Close()
handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), cognitive.URL)
if err != nil {
t.Fatal(err)
}
denied := httptest.NewRecorder()
handler.ServeHTTP(denied, httptest.NewRequest(http.MethodPost, "/api/multimodal/transcribe", bytes.NewReader(nil)))
if denied.Code != http.StatusUnauthorized {
t.Fatalf("unauthenticated upload status=%d", denied.Code)
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "voice.wav")
if err != nil {
t.Fatal(err)
}
_, _ = part.Write([]byte("audio"))
if err := writer.Close(); err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/multimodal/transcribe", body)
request.Header.Set("Authorization", "Bearer session")
request.Header.Set("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || !bytes.Contains(response.Body.Bytes(), []byte("локальный текст")) {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"strings"
"testing"
)
func TestCreatorConsoleResponsiveGuardrails(t *testing.T) {
html, err := staticFiles.ReadFile("static/index.html")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(html), `<meta name="viewport" content="width=device-width, initial-scale=1">`) {
t.Fatal("creator console is missing responsive viewport metadata")
}
css, err := staticFiles.ReadFile("static/assets/console.css")
if err != nil {
t.Fatal(err)
}
stylesheet := string(css)
for _, guard := range []string{
"@media (max-width: 680px)",
".console { display: block; }",
".diagnostic-list { grid-template-columns: 1fr; }",
".agent-details { grid-template-columns: 1fr; gap: 14px; }",
".fleet-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }",
".section-nav { display: flex;",
"overflow-x: auto",
} {
if !strings.Contains(stylesheet, guard) {
t.Fatalf("responsive guardrail missing: %s", guard)
}
}
}
func TestChatResponsiveGuardrailsConstrainMobileGridItem(t *testing.T) {
css, err := staticFiles.ReadFile("static/assets/chat.css")
if err != nil {
t.Fatal(err)
}
stylesheet := string(css)
for _, guard := range []string{
".chat-view { width: 100%; min-width: 0;",
"@media (max-width: 640px)",
".chat-layout { height: auto; display: block; }",
".chat-header > .operator-link, .build-meta { display: none; }",
".chat-header .cognitive-console-link { flex: 0 1 110px;",
} {
if !strings.Contains(stylesheet, guard) {
t.Fatalf("chat responsive guardrail missing: %s", guard)
}
}
}
+155 -12
View File
@@ -7,7 +7,9 @@ import (
"fmt"
"io"
"io/fs"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"strings"
@@ -26,7 +28,11 @@ import (
const (
maxRequestBytes = 8 << 10
maxAdvisoryRequestBytes = 20 << 10
maxResponseBytes = 1 << 20
// Chat responses may contain a locally generated image encoded as base64.
// Keep the gateway limit aligned with the bounded media transport (32 MiB),
// otherwise a successfully generated image is incorrectly surfaced as an
// unavailable runtime to the human.
maxResponseBytes = 32 << 20
)
//go:embed static
@@ -135,6 +141,10 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
g.handleChatMessage(w, r)
return
}
if r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/api/multimodal/") {
g.handleMultimodal(w, r)
return
}
if r.Method == http.MethodGet && r.URL.Path == "/api/fleet/readiness" {
g.handleFleetReadiness(w, r)
return
@@ -209,10 +219,13 @@ type browserChatResponse struct {
CapabilityKind string `json:"capability_kind"`
CapabilityHints []capability.CapabilityKind `json:"capability_hints,omitempty"`
RequestedOutcome string `json:"requested_outcome,omitempty"`
ResearchBacked bool `json:"research_backed"`
InterpretationConfidenceMillis int64 `json:"interpretation_confidence_millis,omitempty"`
DelegationID string `json:"delegation_id"`
DelegationState string `json:"delegation_state"`
ReminderScheduled bool `json:"reminder_scheduled"`
GeneratedImageContentType string `json:"generated_image_content_type,omitempty"`
GeneratedImageBase64 string `json:"generated_image_base64,omitempty"`
}
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
@@ -467,7 +480,10 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
saved.NeedsCognition = false
}
}
if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
// Core owns the completed planning answer. The browser may ask for a local
// advisory only when core explicitly left the turn unanswered; otherwise a
// second model pass can replace a grounded result with a weaker one.
if saved.NeedsCognition && saved.PlanningReply == "" && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
cognition = "unavailable"
requestID, idErr := ids.New()
if idErr == nil {
@@ -478,7 +494,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
status, err = g.postTrustedJSON(r, g.cognitive, "/v1/advisories", "", payload, &advisory)
if err == nil && status == http.StatusOK && validAdvisoryResponse(advisory, trusted) {
cognition = string(advisory.FinishReason)
if advisory.FinishReason == cognitive.FinishCompleted {
if advisory.FinishReason == cognitive.FinishCompleted && (strings.TrimSpace(saved.FallbackText) == "" || !isBoilerplateAdvisory(advisory.AdvisoryText)) {
reply = advisory.AdvisoryText
}
}
@@ -501,21 +517,43 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
CapabilityKind: string(saved.CapabilityKind),
CapabilityHints: saved.CapabilityHints,
RequestedOutcome: saved.RequestedOutcome,
ResearchBacked: saved.ResearchBacked,
InterpretationConfidenceMillis: saved.InterpretationConfidenceMillis,
DelegationID: saved.DelegationID,
DelegationState: saved.DelegationState,
ReminderScheduled: saved.Reminder != nil,
GeneratedImageContentType: saved.GeneratedImageContentType,
GeneratedImageBase64: saved.GeneratedImageBase64,
})
}
func localChatContext(saved interaction.ChatMessageResponse) []cognitive.ContextExcerpt {
if strings.TrimSpace(saved.ActiveProjectName) == "" {
return []cognitive.ContextExcerpt{}
func isBoilerplateAdvisory(value string) bool {
normalized := strings.ToLower(strings.TrimSpace(value))
switch normalized {
case "я рядом.", "я рядом", "понял.", "понял", "принял.", "принял":
return true
default:
return strings.Contains(normalized, "я — домов") || strings.Contains(normalized, "я - домов") || strings.Contains(normalized, "меня зовут домов")
}
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{
DataPolicy: capability.DataPolicyLocalOnly,
MaxBytes: 2048,
Candidates: []cognitive.ContextCandidate{{
}
func localChatContext(saved interaction.ChatMessageResponse) []cognitive.ContextExcerpt {
assistantName := strings.TrimSpace(saved.AssistantName)
if assistantName == "" {
assistantName = "Домовой"
}
candidates := []cognitive.ContextCandidate{{
Excerpt: cognitive.ContextExcerpt{
ID: "assistant-identity",
SourceKind: "assistant-identity-policy",
SourceID: "domovoy",
Sensitivity: cognitive.SensitivityPublic,
Text: "Ты — " + assistantName + ", локальный помощник пользователя. Отвечай от имени «" + assistantName + "». Не называй себя Qwen, Alibaba Cloud, поставщиком модели или внутренним runtime. Если спрашивают имя, скажи: «Меня зовут " + assistantName + ".» Твоя задача — понять смысл запроса, найти путь к цели и довести помощь до полезного конечного результата; не ограничивайся пересказом отказа модели.",
},
Relevance: 100,
}}
if strings.TrimSpace(saved.ActiveProjectName) != "" {
candidates = append(candidates, cognitive.ContextCandidate{
Excerpt: cognitive.ContextExcerpt{
ID: cognitive.ContextExcerptID("active-project-focus"),
SourceKind: "situation-active-project",
@@ -523,8 +561,13 @@ func localChatContext(saved interaction.ChatMessageResponse) []cognitive.Context
Sensitivity: cognitive.SensitivityPrivateAllowed,
Text: "Текущий проект: " + strings.TrimSpace(saved.ActiveProjectName),
},
Relevance: 100,
}},
Relevance: 90,
})
}
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{
DataPolicy: capability.DataPolicyLocalOnly,
MaxBytes: 2048,
Candidates: candidates,
})
if err != nil {
return []cognitive.ContextExcerpt{}
@@ -638,6 +681,104 @@ func (g *gateway) postTrustedJSON(r *http.Request, upstream *url.URL, path, auth
return response.StatusCode, nil
}
func (g *gateway) handleMultimodal(w http.ResponseWriter, r *http.Request) {
authorization := r.Header.Get("Authorization")
if authorization == "" {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
var session identity.CreatorSession
status, err := g.getTrustedJSON(r, g.core, "/v1/creator/sessions/current", "", authorization, &session)
if err != nil || status != http.StatusOK || session.ActorID == "" || session.PrincipalID == "" {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
const maxMediaBytes = 12 << 20
r.Body = http.MaxBytesReader(w, r.Body, maxMediaBytes)
if err := r.ParseMultipartForm(maxMediaBytes); err != nil {
http.Error(w, "media upload is too large or invalid", http.StatusRequestEntityTooLarge)
return
}
path := ""
switch r.URL.Path {
case "/api/multimodal/transcribe":
path = "/v1/multimodal/transcribe"
case "/api/multimodal/understand":
path = "/v1/multimodal/understand"
case "/api/multimodal/ocr":
path = "/v1/multimodal/ocr"
case "/api/multimodal/video-understand":
path = "/v1/multimodal/video-understand"
case "/api/multimodal/generate-image":
path = "/v1/multimodal/generate-image"
default:
http.Error(w, "multimodal route not found", http.StatusNotFound)
return
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
if r.URL.Path == "/api/multimodal/generate-image" {
_ = writer.WriteField("prompt", r.FormValue("prompt"))
_ = writer.WriteField("size", r.FormValue("size"))
} else {
if r.URL.Path == "/api/multimodal/video-understand" {
_ = writer.WriteField("instruction", r.FormValue("instruction"))
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "media file is required", http.StatusBadRequest)
return
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, maxMediaBytes+1))
if err != nil || len(data) == 0 || len(data) > maxMediaBytes {
http.Error(w, "media file is invalid", http.StatusBadRequest)
return
}
mediaHeader := make(textproto.MIMEHeader)
mediaHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, strings.ReplaceAll(header.Filename, `"`, "")))
mediaType := header.Header.Get("Content-Type")
if mediaType == "" {
mediaType = "application/octet-stream"
}
mediaHeader.Set("Content-Type", mediaType)
part, err := writer.CreatePart(mediaHeader)
if err != nil {
http.Error(w, "media upload unavailable", http.StatusServiceUnavailable)
return
}
if _, err := part.Write(data); err != nil {
http.Error(w, "media upload unavailable", http.StatusServiceUnavailable)
return
}
}
if err := writer.Close(); err != nil {
http.Error(w, "media upload unavailable", http.StatusServiceUnavailable)
return
}
target := *g.cognitive
target.Path = path
request, err := http.NewRequestWithContext(r.Context(), http.MethodPost, target.String(), body)
if err != nil {
http.Error(w, "media capability unavailable", http.StatusBadGateway)
return
}
request.Header.Set("Content-Type", writer.FormDataContentType())
response, err := g.client.Do(request)
if err != nil {
http.Error(w, "media capability unavailable", http.StatusBadGateway)
return
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, maxResponseBytes))
http.Error(w, "media capability unavailable", http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.Copy(w, io.LimitReader(response.Body, maxResponseBytes))
}
func decodeStrictJSON(reader io.Reader, limit int64, destination any) error {
decoder := json.NewDecoder(io.LimitReader(reader, limit+1))
decoder.DisallowUnknownFields()
@@ -700,6 +841,8 @@ func creatorRoute(method, path string) (routeTarget, string, bool) {
return targetCore, "/v1/creator/sessions", true
case method == http.MethodGet && path == "/api/session/current":
return targetCore, "/v1/creator/sessions/current", true
case method == http.MethodPost && path == "/api/account/password":
return targetCore, "/v1/creator/account/password", true
case method == http.MethodGet && path == "/api/focus":
return targetCore, "/v1/creator/focus", true
case method == http.MethodGet && strings.HasPrefix(path, "/api/tasks/"):
+36 -5
View File
@@ -1,6 +1,7 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
@@ -13,13 +14,43 @@ import (
"git.cin.su/m/Assistant/contracts/interaction"
)
func TestLocalChatContextUsesOnlyActiveProject(t *testing.T) {
context := localChatContext(interaction.ChatMessageResponse{ActiveProjectName: "Домовой"})
if len(context) != 1 || context[0].SourceKind != "situation-active-project" || context[0].Text != "Текущий проект: Домовой" {
func TestMaxResponseBytesAcceptsGeneratedImagePayload(t *testing.T) {
// A 1024px PNG can exceed the former 1 MiB gateway limit once base64
// encoded. The browser gateway must forward that successful core response.
payload, err := json.Marshal(interaction.ChatMessageResponse{
GeneratedImageContentType: "image/png",
GeneratedImageBase64: strings.Repeat("A", 2<<20),
})
if err != nil {
t.Fatal(err)
}
var decoded interaction.ChatMessageResponse
if err := decodeStrictJSON(bytes.NewReader(payload), maxResponseBytes, &decoded); err != nil {
t.Fatalf("generated image response must fit gateway limit: %v", err)
}
if len(decoded.GeneratedImageBase64) != 2<<20 {
t.Fatalf("generated image payload was truncated: %d", len(decoded.GeneratedImageBase64))
}
}
func TestLocalChatContextIncludesDomovoyIdentityAndOptionalActiveProject(t *testing.T) {
context := localChatContext(interaction.ChatMessageResponse{AssistantName: "Фея", ActiveProjectName: "Домовой"})
if len(context) != 2 || context[0].SourceID != "domovoy" || !strings.Contains(context[0].Text, "Ты — Фея") || !strings.Contains(context[0].Text, "Меня зовут Фея") || context[1].Text != "Текущий проект: Домовой" {
t.Fatalf("unexpected local context: %#v", context)
}
if empty := localChatContext(interaction.ChatMessageResponse{}); len(empty) != 0 {
t.Fatalf("expected empty context without active project: %#v", empty)
if empty := localChatContext(interaction.ChatMessageResponse{}); len(empty) != 1 || empty[0].SourceID != "domovoy" {
t.Fatalf("identity context must remain available without active project: %#v", empty)
}
}
func TestBoilerplateAdvisoryDoesNotReplaceUsefulFallback(t *testing.T) {
for _, value := range []string{"Я рядом", "Понял.", "Принял.", "Я — Домовой, ваш помощник."} {
if !isBoilerplateAdvisory(value) {
t.Fatalf("%q was not recognized as boilerplate", value)
}
}
if isBoilerplateAdvisory("Жили-были дед и баба.") {
t.Fatal("useful creative reply was treated as boilerplate")
}
}
+260 -100
View File
@@ -1,125 +1,285 @@
:root {
--ink: #e9eee7;
--muted: #93a096;
--base: #0b100e;
--panel: #101714;
--line: #2a352f;
--signal: #c9a85f;
--bad: #de816f;
--good: #78c6a3;
--radius: 3px;
--sans: "Bahnschrift", "Aptos Narrow", "Segoe UI", sans-serif;
--serif: Georgia, "Times New Roman", serif;
--mono: "Cascadia Mono", Consolas, monospace;
--ink: #e6edf4;
--muted: #8494a4;
--faint: #5d6e7f;
--base: #0b1119;
--panel: #111b27;
--panel-raised: #172331;
--panel-soft: #0f1925;
--line: #263544;
--line-strong: #34485c;
--lime: #c5ef91;
--lime-ink: #142014;
--blue: #8fc7ef;
--yellow: #d8b86c;
--good: #8ed7ad;
--bad: #ee9292;
--radius: 8px;
--sans: "Manrope", "Segoe UI", sans-serif;
--mono: "DM Mono", "Cascadia Mono", Consolas, monospace;
}
* { box-sizing: border-box; }
[hidden] { display: none !important; }
body { margin: 0; min-width: 320px; min-height: 100vh; color: var(--ink); background: var(--base); font-family: var(--sans); }
button, input, textarea { font: inherit; }
button { cursor: pointer; color: inherit; border-radius: var(--radius); }
a { color: inherit; }
.chat-shell { min-height: 100vh; display: grid; }
.login-panel { width: min(100% - 32px, 420px); margin: auto; padding: 46px 40px; border: 1px solid var(--line); background: var(--panel); }
.brand-mark { width: 46px; height: 46px; display: grid; place-items: center; color: var(--base); background: var(--signal); font: 700 24px/1 var(--serif); clip-path: polygon(50% 0, 92% 22%, 92% 76%, 50% 100%, 8% 76%, 8% 22%); }
h1 { margin: 30px 0; font: 400 58px/.95 var(--serif); letter-spacing: -.02em; }
label { display: block; margin: 16px 0 8px; color: var(--muted); font: 600 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
input, textarea { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; }
input { height: 48px; padding: 0 12px; }
textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px; line-height: 1.45; }
.login-form button, .composer button { border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
.login-form button { width: 100%; height: 50px; margin-top: 18px; }
.login-panel { width: min(100% - 32px, 420px); margin: auto; padding: 40px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); box-shadow: 0 24px 80px rgba(0,0,0,.28); }
.brand-mark { width: 42px; height: 42px; display: grid; place-items: center; color: var(--lime-ink); background: var(--lime); border-radius: 10px; font: 700 22px/1 var(--sans); }
h1, h2 { margin: 0; letter-spacing: -.03em; }
.login-panel h1 { margin: 26px 0; font-size: 42px; font-weight: 650; }
label { display: block; margin: 16px 0 8px; color: var(--muted); font: 600 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
input, textarea { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: var(--panel-soft); outline: none; }
input:focus, textarea:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(143,199,239,.12); }
input { height: 46px; padding: 0 12px; }
textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 13px; line-height: 1.45; }
.login-form button, .composer button { border: 0; color: var(--lime-ink); background: var(--lime); font-weight: 750; }
.login-form button { width: 100%; height: 46px; margin-top: 18px; }
#login-error { min-height: 20px; color: var(--bad); font-size: 13px; }
.chat-view { height: 100vh; min-height: 0; display: grid; grid-template-rows: 64px minmax(0, 1fr); overflow: hidden; }
.chat-header { min-width: 0; display: flex; align-items: center; gap: 14px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .98); }
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
#active-focus { color: var(--good); }
.chat-view { width: 100%; min-width: 0; height: 100vh; min-height: 0; display: grid; grid-template-rows: 60px minmax(0, 1fr); overflow: hidden; }
.chat-header { min-width: 0; display: flex; align-items: center; gap: 14px; padding: 0 20px; border-bottom: 1px solid var(--line); background: rgba(17,27,39,.97); }
.chat-header > a:first-child { color: var(--ink); text-decoration: none; font-size: 18px; font-weight: 750; letter-spacing: -.02em; }
.cognitive-status { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
.chat-header .cognitive-status { width: auto; height: auto; padding: 0; border: 0; background: transparent; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.chat-header .cognitive-status { max-width: min(30vw, 320px); margin-left: auto; overflow: hidden; text-overflow: ellipsis; }
.cognitive-status::before { content: ""; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: var(--signal); }
.chat-header .cognitive-status { width: auto; height: auto; max-width: min(30vw, 340px); margin-left: auto; padding: 0; overflow: hidden; border: 0; background: transparent; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .04em; text-overflow: ellipsis; text-transform: uppercase; }
.cognitive-status::before { content: ""; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: var(--yellow); }
.cognitive-status[data-state="available"] { color: var(--good); }
.cognitive-status[data-state="available"]::before { background: var(--good); }
.cognitive-status[data-state="unavailable"] { color: var(--bad); }
.cognitive-status[data-state="unavailable"]::before { background: var(--bad); }
.cognitive-console-link { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
.build-meta { display: grid; gap: 3px; flex: 0 0 auto; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .08em; text-align: right; text-transform: uppercase; white-space: nowrap; }
.build-meta time { color: #68776d; font-size: 8px; }
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
.human-nav { display: grid; gap: 8px; }
.nav-tab, .operator-link { height: 34px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--muted); background: #0b110e; text-decoration: none; font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
.chat-left .nav-tab { height: auto; min-height: 58px; padding: 10px 12px; display: grid; grid-template-columns: 28px 1fr; gap: 2px 8px; align-items: center; text-align: left; }
.chat-left .nav-tab b { grid-row: span 2; color: var(--signal); font: 11px/1 var(--mono); }
.chat-left .nav-tab span { color: var(--ink); font: 600 13px/1 var(--sans); text-transform: none; letter-spacing: 0; }
.chat-left .nav-tab small { color: var(--muted); font: 10px/1 var(--mono); text-transform: none; letter-spacing: 0; }
.nav-tab.active { color: #11150f; border-color: var(--signal); background: var(--signal); }
.operator-link { color: var(--good); }
.chat-header > .operator-link { flex: 0 0 auto; }
.chat-layout { min-height: 0; height: 100%; display: grid; grid-template-columns: minmax(220px, 280px) minmax(0, 1fr) minmax(250px, 320px); }
.chat-side { min-width: 0; min-height: 0; padding: 28px 20px; overflow-y: auto; background: #0e1512; }
.cognitive-console-link { color: var(--blue); font: 10px/1 var(--mono); letter-spacing: .04em; white-space: nowrap; }
.build-meta { display: grid; gap: 3px; flex: 0 0 auto; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .04em; text-align: right; text-transform: uppercase; white-space: nowrap; }
.build-meta time { color: var(--faint); font-size: 8px; }
.chat-header button { width: 34px; height: 34px; border: 1px solid var(--line); background: transparent; }
.operator-link { height: 32px; padding: 0 11px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--blue); background: transparent; text-decoration: none; font: 10px/1 var(--mono); letter-spacing: .04em; text-transform: uppercase; }
.chat-layout { min-height: 0; height: 100%; display: grid; grid-template-columns: minmax(220px, 258px) minmax(0, 1fr) minmax(270px, 320px); }
.chat-side { min-width: 0; min-height: 0; padding: 22px 18px; overflow-y: auto; background: var(--panel); scrollbar-color: var(--line-strong) transparent; }
.chat-left { border-right: 1px solid var(--line); }
.chat-right { border-left: 1px solid var(--line); }
.side-intro h1, .side-intro h2 { margin: 12px 0; font: 400 clamp(28px, 2.5vw, 40px)/.95 var(--serif); letter-spacing: -.03em; }
.side-intro p:not(.side-index) { color: var(--muted); line-height: 1.5; }
.side-index, .side-focus span, .side-note span, .ai-info-card span { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.side-focus, .side-note, .ai-info-card { margin-top: 28px; padding: 14px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.side-focus strong, .side-note strong, .ai-info-card strong { display: block; margin-top: 9px; color: var(--ink); font: 600 14px/1.3 var(--sans); }
.side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 9px; color: var(--muted); line-height: 1.45; }
.chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; }
.side-intro h1, .side-intro h2 { margin: 9px 0 10px; font-size: clamp(25px, 2.4vw, 34px); line-height: 1.05; font-weight: 680; }
.side-intro p:not(.side-index) { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
.side-index, .section-label, .ai-info-card span, .insight-card span { color: var(--faint); font: 600 9px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.side-section, .side-note, .ai-info-card { margin-top: 20px; padding: 13px 0; border-top: 1px solid var(--line); }
.side-focus strong, .side-note strong { display: block; margin-top: 8px; color: var(--blue); font-size: 13px; font-weight: 650; line-height: 1.35; }
.side-note { border-bottom: 0; }
.side-note strong { color: var(--good); }
.side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 8px; color: var(--muted); font-size: 11px; line-height: 1.45; }
.context-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.context-list span { padding: 5px 8px; border: 1px solid var(--line); border-radius: 6px; color: var(--muted); font-size: 11px; }
.quick-context { padding-bottom: 3px; }
.human-nav { display: grid; gap: 6px; margin-top: 8px; }
.nav-tab { width: 100%; min-height: 50px; padding: 8px 10px; display: grid; grid-template-columns: 26px 1fr; gap: 2px 7px; align-items: center; border: 1px solid transparent; color: var(--muted); background: transparent; text-align: left; }
.chat-left .nav-tab b { grid-row: span 2; color: var(--faint); font: 10px/1 var(--mono); }
.chat-left .nav-tab span { color: var(--ink); font-size: 12px; font-weight: 650; }
.chat-left .nav-tab small { color: var(--faint); font: 10px/1 var(--mono); }
.nav-tab:hover, .nav-tab:focus-visible { border-color: var(--line-strong); outline: none; }
.nav-tab.active { border-color: rgba(197,239,145,.42); background: rgba(197,239,145,.1); }
.nav-tab.active b { color: var(--lime); }
.quiet-state { margin: 9px 0 0; color: var(--faint); font-size: 11px; }
.new-conversation { width: 100%; min-height: 34px; margin-top: 9px; border: 1px solid var(--line-strong); color: var(--ink); background: transparent; font-size: 11px; }
.new-conversation:hover, .new-conversation:focus-visible { border-color: var(--lime); color: var(--lime); outline: none; }
.side-build { margin-top: 14px; padding-top: 14px; text-align: left; }
.chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; background: var(--base); }
.surface-panel { min-height: 0; position: relative; overflow-y: auto; }
.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; }
.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: 28px 18px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; }
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
.message.user { align-self: flex-end; background: #151e1a; }
.message.assistant { align-self: flex-start; background: #0f1613; }
.message small { display: block; margin-top: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; }
.composer { width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 10px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
.composer button { height: 48px; align-self: end; }
.composer button:disabled { cursor: wait; filter: saturate(.3); }
.conversation-starters { width: min(100%, 920px); margin: -4px auto 0; padding: 0 18px 18px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.conversation-starters button { min-height: 32px; padding: 0 10px; border: 1px solid var(--line); color: var(--ink); background: #101814; font: 10px/1 var(--mono); }
.conversation-starters button:hover, .conversation-starters button:focus-visible { border-color: var(--signal); color: var(--signal); outline: none; }
.ai-data-form { margin-top: 24px; }
.ai-data-form textarea { min-height: 150px; resize: vertical; }
.ai-data-form button { width: 100%; min-height: 42px; margin-top: 12px; border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
.ai-data-error { margin: 10px 0 0; color: var(--bad); font-size: 12px; line-height: 1.4; }
.side-console-link { display: flex; justify-content: space-between; margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--line); color: var(--signal) !important; font: 11px/1.3 var(--mono) !important; text-transform: uppercase; letter-spacing: .06em; }
.attention-surface, .history-surface { width: min(100%, 920px); margin: 0 auto; padding: 34px 18px; align-content: start; gap: 18px; }
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 16px; }
.surface-heading p { margin: 0; font: 400 36px/.95 var(--serif); }
.surface-heading span, .attention-card small, .history-item span { color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .1em; }
.surface-heading button { height: 36px; padding: 0 12px; border: 1px solid var(--line); background: transparent; }
.attention-list, .history-list { display: grid; gap: 12px; }
.attention-card, .history-item { border: 1px solid var(--line); background: #0f1613; padding: 16px; }
.attention-card time { color: var(--signal); font: 12px/1 var(--mono); }
.attention-card h2 { margin: 10px 0 8px; font: 400 24px/1.1 var(--serif); }
.attention-card p, .history-item p { margin: 0 0 10px; color: var(--ink); line-height: 1.45; }
.attention-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.attention-actions button { height: 34px; padding: 0 12px; border: 1px solid var(--line); background: #151e1a; color: var(--ink); font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
.attention-actions button:first-child { border-color: var(--good); color: var(--good); }
.history-item.user { border-color: #34443c; background: #151e1a; }
.history-item strong { display: block; margin-bottom: 8px; color: var(--signal); }
.empty-state { margin: 24px 0 0; color: var(--muted); }
.messages { width: min(100%, 900px); min-height: 0; margin: 0 auto; padding: clamp(24px, 5vh, 52px) 22px 20px; display: flex; flex-direction: column; gap: 10px; overflow-y: auto; scrollbar-color: var(--line-strong) transparent; }
.message { max-width: min(700px, 92%); padding: 11px 14px 12px; border: 1px solid var(--line); border-radius: 10px; line-height: 1.5; }
.message.user { align-self: flex-end; background: #162231; border-color: #30465b; }
.message.assistant { align-self: flex-start; background: #121d2a; }
.message-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.message-role { color: var(--blue); font: 650 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.message.user .message-role { color: var(--lime); }
.message-time { color: var(--faint); font: 9px/1 var(--mono); }
.message-content { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; }
.message small { display: block; margin-top: 8px; color: var(--faint); font: 9px/1.2 var(--mono); letter-spacing: .03em; text-transform: none; }
.composer { width: min(100%, 900px); margin: 0 auto; padding: 10px 22px 7px; display: grid; grid-template-columns: minmax(0, 1fr) 106px; gap: 9px; border-top: 1px solid var(--line); background: var(--base); }
.composer-input-wrap { min-width: 0; border: 1px solid var(--line-strong); border-radius: 9px; background: var(--panel-soft); transition: border-color .15s ease, box-shadow .15s ease; }
.composer-input-wrap:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(143,199,239,.1); }
.composer textarea { min-height: 46px; max-height: 200px; resize: none; border: 0; background: transparent; }
.composer-hint { display: flex; justify-content: space-between; gap: 12px; padding: 0 12px 7px; color: var(--faint); font: 9px/1 var(--mono); }
.composer-hint output { color: var(--muted); white-space: nowrap; }
.composer button { height: 46px; align-self: end; display: inline-flex; align-items: center; justify-content: space-between; gap: 7px; padding: 0 13px; }
.composer button[aria-busy="true"] { color: #384529; background: #a6ca7a; }
.composer button span:last-child { opacity: .65; font: 15px/1 var(--mono); }
.composer button:disabled { cursor: wait; filter: saturate(.35); }
.media-tools { width: min(100%, 900px); margin: 0 auto; padding: 0 22px 9px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .03em; text-transform: uppercase; }
.media-tools label { display: inline-flex; align-items: center; gap: 5px; min-height: 28px; margin: 0; padding: 0 7px; border: 1px solid var(--line); border-radius: 6px; color: var(--muted); cursor: pointer; }
.media-tools input { max-width: 125px; font: 9px var(--mono); }
.media-tools span { color: var(--yellow); }
.insight-grid { display: grid; gap: 7px; margin-top: 19px; }
.insight-card { padding: 11px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-soft); }
.insight-card strong { display: block; margin-top: 7px; color: var(--ink); font-size: 12px; font-weight: 650; line-height: 1.3; }
.insight-card small { display: block; margin-top: 5px; color: var(--faint); font-size: 10px; line-height: 1.35; }
.ai-info-card { padding-bottom: 0; }
.ai-info-card strong { display: block; margin-top: 8px; font-size: 12px; }
.ai-data-form { margin-top: 18px; }
.ai-data-form textarea { min-height: 126px; }
.ai-data-form button { width: 100%; min-height: 38px; margin-top: 9px; border: 1px solid rgba(197,239,145,.3); border-radius: 7px; color: var(--lime); background: rgba(197,239,145,.1); font-size: 11px; font-weight: 700; }
.ai-data-form button:hover { background: rgba(197,239,145,.16); }
.ai-data-error { margin: 9px 0 0; color: var(--bad); font-size: 11px; line-height: 1.4; }
.side-console-link { display: flex; justify-content: space-between; margin-top: 20px; padding-top: 13px; border-top: 1px solid var(--line); color: var(--blue) !important; font: 10px/1.3 var(--mono) !important; text-decoration: none; letter-spacing: .03em; text-transform: uppercase; }
.attention-surface, .history-surface { width: min(100%, 900px); margin: 0 auto; padding: 28px 22px; align-content: start; gap: 15px; }
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 13px; }
.surface-heading p { margin: 0; font-size: 27px; font-weight: 680; }
.surface-heading span, .attention-card small, .history-item span { color: var(--muted); font: 9px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
.surface-heading button { height: 31px; padding: 0 10px; border: 1px solid var(--line); background: transparent; font-size: 11px; }
.attention-list, .history-list { display: grid; gap: 9px; }
.attention-card, .history-item { border: 1px solid var(--line); border-radius: 8px; background: var(--panel); padding: 13px; }
.attention-card time { color: var(--yellow); font: 10px/1 var(--mono); }
.attention-card h2 { margin: 8px 0 6px; font-size: 18px; font-weight: 650; }
.attention-card p, .history-item p { margin: 0 0 8px; color: var(--ink); font-size: 12px; line-height: 1.5; }
.attention-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 11px; }
.attention-actions button { height: 30px; padding: 0 10px; border: 1px solid var(--line); background: var(--panel-raised); color: var(--ink); font: 10px/1 var(--mono); }
.attention-actions button:first-child { border-color: rgba(142,215,173,.5); color: var(--good); }
.history-item.user { border-color: #30465b; background: #162231; }
.history-item strong { display: block; margin-bottom: 6px; color: var(--blue); font-size: 12px; }
.empty-state { margin: 20px 0 0; color: var(--muted); font-size: 12px; }
.empty-state.error { color: var(--bad); }
@media (max-width: 640px) {
@media (max-width: 780px) {
.chat-view { height: auto; min-height: 100vh; overflow: visible; display: block; }
.composer { grid-template-columns: 1fr; }
.chat-header { position: sticky; top: 0; z-index: 3; min-height: 58px; padding: 0 12px; }
.chat-header .cognitive-status { flex: 1 1 100px; min-width: 0; max-width: 150px; }
.chat-header .cognitive-console-link, .chat-header > .operator-link, .chat-header .build-meta { display: none; }
.chat-header > #logout { flex: 0 0 34px; }
.chat-layout { height: auto; display: flex; flex-direction: column; }
.chat-main { order: 1; height: 72vh; min-height: 530px; }
.chat-side { min-height: auto; max-height: none; overflow: visible; }
.chat-left { order: 2; border: 0; border-top: 1px solid var(--line); }
.chat-right { order: 3; border: 0; border-top: 1px solid var(--line); }
.messages { padding-inline: 12px; }
.composer { grid-template-columns: 1fr; padding-inline: 12px; }
.composer button { width: 100%; }
.conversation-starters { align-items: stretch; }
.conversation-starters span { width: 100%; }
.chat-header { gap: 10px; padding: 0 12px; }
.chat-header .cognitive-status { display: inline-flex; max-width: 150px; overflow: hidden; text-overflow: ellipsis; }
.build-meta { font-size: 8px; letter-spacing: .04em; }
.build-meta time { font-size: 7px; }
.composer-hint { font-size: 8px; }
.media-tools { padding-inline: 12px; }
.side-build { display: none; }
}
@media (max-width: 640px) {
.chat-layout { height: auto; display: block; }
.chat-side, .chat-main { min-height: auto; max-height: none; overflow: visible; }
.chat-left, .chat-right { border: 0; border-bottom: 1px solid var(--line); }
.chat-main { height: 70vh; min-height: 520px; }
.chat-left { padding-bottom: 20px; }
.chat-right { padding-top: 20px; }
.operator-link, .nav-tab { padding: 0 9px; }
.chat-header > .operator-link, .build-meta { display: none; }
.chat-header .cognitive-console-link { flex: 0 1 110px; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
}
@media (min-width: 641px) and (max-width: 1100px) {
.chat-layout { grid-template-columns: 210px minmax(0, 1fr) 245px; }
@media (min-width: 781px) and (max-width: 1100px) {
.chat-layout { grid-template-columns: 218px minmax(0, 1fr) 260px; }
.chat-side { padding-inline: 14px; }
.side-intro h1, .side-intro h2 { font-size: 30px; }
}
/* Chat-first pass: navigation and context stay out of the conversation's way. */
.chat-header { gap: 10px; padding-inline: 18px; }
.header-icon, .header-control, .profile-button { height: 32px; border: 1px solid var(--line); background: transparent; font: 10px/1 var(--mono); }
.header-icon { width: 32px; padding: 0; color: var(--muted); font-size: 15px; }
.header-control, .profile-button { padding: 0 10px; color: var(--muted); }
.header-control { color: var(--ink); }
.header-icon:hover, .header-icon:focus-visible, .header-control:hover, .header-control:focus-visible, .profile-button:hover, .profile-button:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
.header-focus { min-width: 0; overflow: hidden; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.chat-header .cognitive-status { margin-left: auto; max-width: 250px; }
.chat-header .cognitive-console-link { display: none; }
.chat-layout { grid-template-columns: 242px minmax(0, 1fr); }
.chat-layout.nav-collapsed { grid-template-columns: 0 minmax(0, 1fr); }
.chat-left { padding: 24px 16px; transition: opacity .18s ease, padding .18s ease, transform .18s ease; }
.chat-layout.nav-collapsed .chat-left { padding-inline: 0; overflow: hidden; opacity: 0; border-right: 0; }
.side-intro h1 { font-size: 26px; }
.side-section { margin-top: 18px; }
.human-nav { margin-top: 12px; }
.nav-tab { grid-template-columns: 1fr; min-height: 44px; padding-inline: 10px; }
.chat-left .nav-tab b { display: none; }
.new-conversation { margin-top: 16px; }
.chat-main { min-width: 0; }
.messages { width: min(100%, 1040px); padding: 32px 30px 22px; gap: 14px; }
.message { max-width: min(760px, 88%); padding: 14px 17px 15px; border-radius: 12px; }
.message-content { font-size: 15px; line-height: 1.55; }
.message-role { font-size: 10px; }
.message-time { font-size: 10px; }
.message small { font-size: 10px; }
.composer { width: min(100%, 1040px); padding-inline: 30px; }
.composer-input-wrap { min-height: 64px; padding: 0 12px 36px; border-radius: 14px; }
.composer textarea { min-height: 56px; max-height: 180px; padding: 13px 0 8px; font-size: 15px; }
.composer-actions { right: 9px; bottom: 8px; left: 9px; height: 28px; }
.composer-tool { width: 29px; height: 29px; padding: 0; border: 0; color: var(--muted); background: transparent !important; }
.composer-tool svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
.composer-tool:hover, .composer-tool:focus-visible { border: 0; color: var(--blue); box-shadow: none; }
.composer-hint { font-size: 10px; }
.send-button { width: 33px; height: 33px; flex-basis: 33px; }
.composer-runtime { height: 0; min-height: 0; padding: 0; overflow: visible; }
.attachment-menu { bottom: 42px; left: 8px; }
.context-indicator { display: none; }
.technical-meta { display: none !important; }
.context-backdrop { position: fixed; z-index: 8; inset: 60px 0 0; border: 0; background: rgba(4,9,15,.58); }
.context-drawer { position: fixed; z-index: 9; top: 60px; right: 0; bottom: 0; width: min(360px, 92vw); padding: 24px 20px; border-left: 1px solid var(--line-strong); box-shadow: -20px 0 50px rgba(0,0,0,.28); transform: translateX(105%); transition: transform .2s ease; }
.password-dialog { width: min(420px, calc(100vw - 32px)); padding: 0; color: var(--ink); background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--radius); box-shadow: 0 24px 80px rgba(0,0,0,.45); }
.password-dialog::backdrop { background: rgba(3,8,14,.68); }
.password-dialog form { padding: 22px; }
.dialog-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
.dialog-heading h2 { margin: 0; font-size: 20px; }
.dialog-heading button { width: 30px; height: 30px; border: 1px solid var(--line); background: transparent; }
.password-dialog label { display: block; margin: 14px 0 7px; color: var(--muted); font: 11px/1 var(--mono); }
.password-dialog input { width: 100%; height: 42px; padding: 0 12px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: var(--panel-raised); }
.password-dialog input:focus { outline: 2px solid var(--blue); outline-offset: 1px; }
.dialog-actions { display: flex; justify-content: space-between; gap: 10px; margin-top: 20px; }
.dialog-actions button { min-height: 38px; padding: 0 14px; border: 1px solid var(--line); background: transparent; }
.dialog-actions button:first-child { border-color: var(--lime); color: var(--lime-ink); background: var(--lime); }
.dialog-actions button:disabled { opacity: .55; cursor: wait; }
#password-status { min-height: 18px; margin: 12px 0 0; color: var(--muted); font-size: 12px; }
.context-drawer.drawer-open { transform: translateX(0); }
.drawer-heading { display: flex; align-items: start; justify-content: space-between; gap: 12px; }
.drawer-heading h2 { margin-top: 9px; font-size: 26px; }
.drawer-copy { margin: 12px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
.drawer-close { width: 30px; height: 30px; border: 1px solid var(--line); color: var(--muted); background: transparent; font-size: 20px; line-height: 1; }
.drawer-close:hover, .drawer-close:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
.insight-grid { margin-top: 20px; }
@media (max-width: 780px) {
.chat-header { padding-inline: 10px; }
.header-focus { display: none; }
.chat-header .cognitive-status { max-width: 145px; }
.profile-button { width: 32px; overflow: hidden; padding: 0; font-size: 0; }
.profile-button::before { content: "●"; font-size: 12px; }
.chat-layout { display: block; }
.chat-main { height: calc(100vh - 58px); min-height: 520px; }
.chat-left { position: fixed; z-index: 10; top: 58px; bottom: 0; left: 0; width: min(290px, 84vw); transform: translateX(-105%); box-shadow: 18px 0 42px rgba(0,0,0,.3); }
.chat-layout.nav-open .chat-left { transform: translateX(0); opacity: 1; overflow-y: auto; }
.chat-layout.nav-collapsed .chat-left { padding-inline: 16px; }
.messages { padding: 22px 12px 16px; }
.message { max-width: 94%; }
.message-content { font-size: 14px; }
.composer { width: 100%; padding-inline: 12px; }
.composer-input-wrap { padding-right: 10px; }
.composer-hint { display: none; }
.attachment-menu { bottom: 42px; left: 5px; }
.context-backdrop { inset: 58px 0 0; }
.context-drawer { top: 58px; }
}
/* Focused composer: one calm surface, with compact controls kept together. */
.active-panel { grid-template-rows: minmax(0, 1fr) auto auto auto; }
.composer { width: min(100%, 900px); margin: 0 auto; padding: 0 22px 7px; display: block; border-top: 0; background: transparent; }
.composer-input-wrap { position: relative; min-height: 62px; padding: 0 58px 32px 12px; border: 1px solid var(--line-strong); border-radius: 12px; background: var(--panel); }
.composer textarea { display: block; min-height: 54px; max-height: 178px; height: 54px; padding: 12px 0 8px; overflow-y: hidden; line-height: 1.4; }
.composer-actions { position: absolute; right: 8px; bottom: 7px; left: 8px; height: 27px; display: flex; align-items: center; gap: 8px; }
.composer-hint { order: 3; padding: 0; color: var(--faint); font: 9px/1 var(--mono); }
.composer-tool { width: 27px; height: 27px; flex: 0 0 27px; border: 1px solid var(--line-strong); border-radius: 7px; color: var(--muted); background: transparent; font-size: 18px; line-height: 1; }
.composer-tool:has(.composer-tool-label) { width: auto; min-width: 27px; padding-inline: 7px; gap: 5px; font: 10px/1 var(--mono); }
.composer-tool-label { font-size: 10px; }
.composer-tool:hover, .composer-tool:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
.context-indicator { display: inline-flex; align-items: center; gap: 5px; color: var(--faint); font: 9px/1 var(--mono); white-space: nowrap; }
.context-indicator output { color: var(--muted); }
.send-button { order: 5; width: 32px; height: 32px; flex: 0 0 32px; margin-left: auto; display: grid; place-items: center; padding: 0; border: 1px solid rgba(197,239,145,.48); border-radius: 50%; color: var(--lime-ink); background: var(--lime); font-size: 17px; line-height: 1; }
.send-button:hover, .send-button:focus-visible { box-shadow: 0 0 0 3px rgba(197,239,145,.12); outline: none; }
.send-button[aria-busy="true"] { border-color: rgba(238,146,146,.55); color: var(--bad); background: rgba(238,146,146,.12); }
.send-button[aria-busy="true"]:hover { background: rgba(238,146,146,.2); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.composer-toolbar { width: min(100%, 900px); min-height: 30px; margin: 0 auto; padding: 0 22px 8px; position: relative; display: flex; align-items: center; gap: 6px; }
.toolbar-button { min-height: 27px; padding: 0 9px; border: 1px solid var(--line); border-radius: 7px; color: var(--muted); background: transparent; font: 9px/1 var(--mono); text-transform: uppercase; }
.toolbar-button:hover, .toolbar-button:focus-visible { border-color: var(--blue); color: var(--blue); outline: none; }
.attachment-menu { position: absolute; z-index: 4; bottom: 37px; left: 22px; display: grid; min-width: 145px; padding: 5px; border: 1px solid var(--line-strong); border-radius: 9px; background: var(--panel-raised); box-shadow: 0 14px 34px rgba(0,0,0,.35); }
.attachment-menu button { padding: 8px 9px; border: 0; border-radius: 6px; color: var(--ink); background: transparent; text-align: left; font-size: 11px; }
.attachment-menu button:hover, .attachment-menu button:focus-visible { color: var(--lime); background: rgba(197,239,145,.1); outline: none; }
.composer-toolbar #media-status { margin-left: 3px; color: var(--yellow); font: 9px/1 var(--mono); }
@media (max-width: 780px) {
.composer { width: 100%; padding-inline: 12px; }
.composer-input-wrap { padding-right: 50px; }
.composer-toolbar { width: 100%; padding-inline: 12px; }
.attachment-menu { left: 12px; }
.composer-hint { display: none; }
.composer-tool:has(.composer-tool-label) { padding-inline: 5px; }
.composer-tool-label { font-size: 9px; }
.context-indicator { font-size: 8px; }
}
+215 -26
View File
@@ -6,23 +6,69 @@ const messages = document.querySelector("#messages");
const chatForm = document.querySelector("#chat-form");
const messageInput = document.querySelector("#message");
const sendButton = document.querySelector("#send");
const sendLabel = document.querySelector("#send-label");
const messageCount = document.querySelector("#message-count");
const logoutButton = document.querySelector("#logout");
const passwordDialog = document.querySelector("#password-dialog");
const passwordForm = document.querySelector("#password-form");
const passwordClose = document.querySelector("#password-close");
const passwordLogout = document.querySelector("#password-logout");
const passwordStatus = document.querySelector("#password-status");
const activeFocus = document.querySelector("#active-focus");
const rightFocus = document.querySelector("#right-focus");
const headerFocus = document.querySelector("#header-focus");
const navToggle = document.querySelector("#nav-toggle");
const chatLayout = document.querySelector(".chat-layout");
const contextToggle = document.querySelector("#context-toggle");
const contextClose = document.querySelector("#context-close");
const contextBackdrop = document.querySelector("#context-backdrop");
const contextDrawer = document.querySelector("#context-drawer");
const newConversationButton = document.querySelector(".new-conversation");
const cognitiveStatus = document.querySelector("#cognitive-status");
const cognitiveConsoleLink = document.querySelector("#cognitive-console-link");
const todayList = document.querySelector("#today-list");
const refreshToday = document.querySelector("#refresh-today");
const historyList = document.querySelector("#history-list");
const navTabs = Array.from(document.querySelectorAll(".nav-tab"));
const conversationStarters = Array.from(document.querySelectorAll("[data-starter]"));
const aiDataForm = document.querySelector("#ai-data-form");
const aiDataInput = document.querySelector("#ai-data");
const aiDataError = document.querySelector("#ai-data-error");
const attachToggle = document.querySelector("#attach-toggle");
const attachmentMenu = document.querySelector("#attachment-menu");
const photoTrigger = document.querySelector("#photo-trigger");
const micTrigger = document.querySelector("#mic-trigger");
const sendIcon = document.querySelector("#send-icon");
const stopIcon = document.querySelector("#stop-icon");
let token = sessionStorage.getItem("domovoy.chat.token") || "";
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
let localHistory = [];
let cognitiveStatusTimer = null;
let activeRequestController = null;
let composingMessage = false;
function resizeComposerInput() {
if (!messageInput) return;
messageInput.style.height = "auto";
const maxHeight = 178;
messageInput.style.height = `${Math.min(messageInput.scrollHeight, maxHeight)}px`;
messageInput.style.overflowY = messageInput.scrollHeight > maxHeight ? "auto" : "hidden";
if (messageCount) messageCount.textContent = `${messageInput.value.length.toLocaleString("ru-RU")} / 16 384`;
}
function isChatBusy() {
return sendButton?.getAttribute("aria-busy") === "true";
}
function setChatBusy(busy) {
if (!sendButton) return;
sendButton.setAttribute("aria-busy", busy ? "true" : "false");
sendButton.setAttribute("aria-label", busy ? "Остановить обработку" : "Отправить");
sendButton.title = busy ? "Остановить обработку" : "Отправить";
if (sendLabel) sendLabel.textContent = busy ? "Остановить обработку" : "Отправить";
if (sendIcon) sendIcon.hidden = busy;
if (stopIcon) stopIcon.hidden = !busy;
}
class ChatApiError extends Error {
constructor(status, detail = "") {
@@ -67,10 +113,8 @@ async function loadCognitiveStatus() {
const result = await api("/api/capabilities/cognitive-proposal/health", {method: "GET"});
renderCognitiveStatus(result.State || result.state || "UNKNOWN");
} catch (error) {
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
return;
}
// Cognitive health is an auxiliary probe. Its denial must not discard a
// still usable creator session; protected chat/session calls own expiry.
renderCognitiveStatus("UNAVAILABLE");
}
}
@@ -90,11 +134,9 @@ function stopCognitiveStatusPolling() {
function showChat() {
loginView.hidden = true;
chatView.hidden = false;
if (!messages.children.length) {
appendMessage("assistant", "Я рядом. Напишите обычное сообщение, без внутренних идентификаторов.");
}
startCognitiveStatusPolling();
messageInput.focus();
resizeComposerInput();
}
function showLogin() {
@@ -103,15 +145,37 @@ function showLogin() {
loginView.hidden = false;
}
function appendMessage(kind, text, meta = "") {
function appendMessage(kind, text, meta = "", media = null) {
const node = document.createElement("div");
node.className = `message ${kind}`;
node.textContent = text;
const head = document.createElement("div");
head.className = "message-head";
const role = document.createElement("strong");
role.className = "message-role";
role.textContent = kind === "user" ? "Вы" : "Домовой";
const time = document.createElement("time");
time.className = "message-time";
const now = new Date();
time.dateTime = now.toISOString();
time.textContent = now.toLocaleTimeString("ru-RU", {hour: "2-digit", minute: "2-digit"});
head.append(role, time);
node.appendChild(head);
const content = document.createElement("div");
content.className = "message-content";
content.textContent = text;
node.appendChild(content);
if (meta) {
const small = document.createElement("small");
small.textContent = meta;
node.appendChild(small);
}
if (media?.data && media?.contentType) {
const image = document.createElement("img");
image.className = "generated-image";
image.alt = text || "Сгенерированное изображение";
image.src = `data:${media.contentType};base64,${media.data}`;
node.appendChild(image);
}
messages.appendChild(node);
messages.scrollTop = messages.scrollHeight;
localHistory.push({kind, text, meta, at: new Date()});
@@ -122,6 +186,41 @@ function updateActiveFocus(value, provenance = "") {
if (!activeFocus) return;
activeFocus.textContent = value ? `Сейчас обсуждаем: ${value}` : "";
activeFocus.hidden = !value;
if (rightFocus) rightFocus.textContent = value || "не выбран";
if (headerFocus) headerFocus.textContent = value || "Новый разговор";
}
function setContextOpen(open) {
if (!contextDrawer) return;
contextDrawer.classList.toggle("drawer-open", open);
contextDrawer.setAttribute("aria-hidden", String(!open));
if (contextToggle) contextToggle.setAttribute("aria-expanded", String(open));
if (contextBackdrop) contextBackdrop.hidden = !open;
}
if (contextToggle) contextToggle.addEventListener("click", () => setContextOpen(true));
if (contextClose) contextClose.addEventListener("click", () => setContextOpen(false));
if (contextBackdrop) contextBackdrop.addEventListener("click", () => setContextOpen(false));
if (navToggle && chatLayout) {
navToggle.addEventListener("click", () => {
const mobile = window.matchMedia("(max-width: 780px)").matches;
const open = mobile ? chatLayout.classList.toggle("nav-open") : !chatLayout.classList.toggle("nav-collapsed");
navToggle.setAttribute("aria-expanded", String(open));
navToggle.title = open ? "Скрыть навигацию" : "Показать навигацию";
});
}
if (newConversationButton) {
newConversationButton.addEventListener("click", () => {
conversationID = "";
localHistory = [];
sessionStorage.removeItem("domovoy.chat.conversation");
messages.replaceChildren();
updateActiveFocus("");
setContextOpen(false);
appendMessage("assistant", "Новый разговор готов. Чем займёмся?");
messageInput.focus();
});
}
async function api(path, options = {}) {
@@ -132,6 +231,16 @@ async function api(path, options = {}) {
return response.json();
}
async function uploadMedia(path, file) {
const body = new FormData();
body.append("file", file, file.name);
const headers = {};
if (token) headers.Authorization = `Bearer ${token}`;
const response = await fetch(path, {method: "POST", headers, body});
if (!response.ok) throw new ChatApiError(response.status, await response.text());
return response.json();
}
function expireSession(message = "") {
token = "";
conversationID = "";
@@ -261,19 +370,23 @@ async function sendChatMessage(text, options = {}) {
const clean = text.trim();
if (!clean) return;
if (!options.silentUser) appendMessage("user", clean);
sendButton.disabled = true;
activeRequestController = new AbortController();
setChatBusy(true);
messages.setAttribute("aria-busy", "true");
try {
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean})});
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean}), signal: activeRequestController.signal});
conversationID = result.conversation_id;
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
updateActiveFocus(result.active_focus, result.project_provenance);
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result));
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result), result.generated_image_base64 ? {contentType: result.generated_image_content_type || "image/png", data: result.generated_image_base64} : null);
if (/что сейчас требует моего внимания/i.test(clean)) {
await loadToday();
activateTab("today-panel");
}
} catch (error) {
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
if (error.name === "AbortError") {
appendMessage("assistant", "Обработка остановлена.");
} else if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
} else if (error instanceof ChatApiError && error.status >= 500) {
appendMessage("assistant", "Домовой не смог завершить обработку: основной runtime сейчас недоступен. Встроенный канал проверяется; повторите сообщение через несколько секунд.");
@@ -281,7 +394,9 @@ async function sendChatMessage(text, options = {}) {
appendMessage("assistant", "Домовой не принял сообщение. Проверьте текст и повторите попытку.");
}
} finally {
sendButton.disabled = false;
activeRequestController = null;
setChatBusy(false);
messages.removeAttribute("aria-busy");
messageInput.focus();
}
}
@@ -314,24 +429,82 @@ loginForm.addEventListener("submit", async (event) => {
chatForm.addEventListener("submit", async (event) => {
event.preventDefault();
if (isChatBusy()) return;
const text = messageInput.value.trim();
if (!text) return;
messageInput.value = "";
resizeComposerInput();
await sendChatMessage(text);
});
for (const tab of navTabs) {
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
messageInput.addEventListener("input", resizeComposerInput);
messageInput.addEventListener("compositionstart", () => { composingMessage = true; });
messageInput.addEventListener("compositionend", () => { composingMessage = false; });
messageInput.addEventListener("keydown", (event) => {
if (event.key !== "Enter" || event.shiftKey || event.isComposing || composingMessage) return;
event.preventDefault();
if (!isChatBusy()) chatForm.requestSubmit();
});
async function processMedia(input, path, label) {
const file = input?.files?.[0];
if (!file || !mediaStatus) return;
mediaStatus.textContent = `${label} обрабатывается...`;
input.value = "";
try {
const result = await uploadMedia(path, file);
const text = String(result.text || "").trim();
if (!text) throw new Error("empty media result");
mediaStatus.textContent = "Готово";
await sendChatMessage(`Пользователь передал ${label.toLowerCase()}. Обработай содержание и ответь по смыслу:\n${text}`);
} catch (error) {
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
} else {
mediaStatus.textContent = `${label} сейчас недоступно.`;
}
}
}
for (const starter of conversationStarters) {
starter.addEventListener("click", async () => {
const text = starter.dataset.starter || "";
if (!text || sendButton.disabled) return;
messageInput.value = "";
await sendChatMessage(text);
const mediaStatus = document.querySelector("#media-status");
const audioInput = document.querySelector("#audio-input");
const imageInput = document.querySelector("#image-input");
const ocrInput = document.querySelector("#ocr-input");
if (audioInput) audioInput.addEventListener("change", () => processMedia(audioInput, "/api/multimodal/transcribe", "Распознанное аудио"));
if (imageInput) imageInput.addEventListener("change", () => processMedia(imageInput, "/api/multimodal/understand", "Описание изображения"));
if (ocrInput) ocrInput.addEventListener("change", () => processMedia(ocrInput, "/api/multimodal/ocr", "Текст с изображения"));
function toggleAttachmentMenu(open) {
if (!attachmentMenu) return;
const next = typeof open === "boolean" ? open : attachmentMenu.hidden;
attachmentMenu.hidden = !next;
if (attachToggle) attachToggle.setAttribute("aria-expanded", String(next));
}
if (attachToggle) attachToggle.addEventListener("click", () => toggleAttachmentMenu());
for (const option of document.querySelectorAll("[data-input]")) {
option.addEventListener("click", () => {
const input = document.querySelector(`#${option.dataset.input}`);
toggleAttachmentMenu(false);
if (input) input.click();
});
}
if (photoTrigger && imageInput) photoTrigger.addEventListener("click", () => imageInput.click());
if (micTrigger && audioInput) micTrigger.addEventListener("click", () => audioInput.click());
document.addEventListener("click", (event) => {
if (attachmentMenu && !attachmentMenu.hidden && !attachmentMenu.contains(event.target) && !attachToggle?.contains(event.target)) toggleAttachmentMenu(false);
});
if (sendButton) {
sendButton.addEventListener("click", (event) => {
if (!isChatBusy()) return;
event.preventDefault();
activeRequestController?.abort();
});
}
for (const tab of navTabs) {
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
}
if (refreshToday) refreshToday.addEventListener("click", loadToday);
if (cognitiveStatus) cognitiveStatus.addEventListener("click", loadCognitiveStatus);
@@ -339,7 +512,7 @@ if (aiDataForm) {
aiDataForm.addEventListener("submit", async (event) => {
event.preventDefault();
const details = aiDataInput.value.trim();
if (!details || sendButton.disabled) return;
if (!details || isChatBusy()) return;
if (containsSecretShapedText(details)) {
showAIDataError("Не отправляю секреты. Удалите пароль, токен, API-ключ или приватный ключ и оставьте только описание модели.");
return;
@@ -351,8 +524,23 @@ if (aiDataForm) {
}
if (aiDataInput) aiDataInput.addEventListener("input", () => showAIDataError());
logoutButton.addEventListener("click", () => {
expireSession();
logoutButton.addEventListener("click", () => passwordDialog?.showModal());
passwordClose?.addEventListener("click", () => passwordDialog?.close());
passwordLogout?.addEventListener("click", () => { passwordDialog?.close(); expireSession(); });
passwordForm?.addEventListener("submit", async (event) => {
event.preventDefault();
passwordStatus.textContent = "Сохраняю…";
const submit = document.querySelector("#password-submit");
submit.disabled = true;
try {
await api("/api/account/password", {method: "POST", body: JSON.stringify({current_password: document.querySelector("#current-password").value, new_password: document.querySelector("#new-password").value})});
passwordForm.reset();
passwordStatus.textContent = "Пароль изменён без перезапуска.";
} catch (error) {
passwordStatus.textContent = error?.status === 401 ? "Текущий пароль не принят." : "Не удалось изменить пароль.";
} finally {
submit.disabled = false;
}
});
if (token) {
@@ -360,3 +548,4 @@ if (token) {
} else {
showLogin();
}
resizeComposerInput();
+56 -23
View File
@@ -25,46 +25,60 @@
<section id="chat-view" class="chat-view" hidden>
<header class="chat-header">
<button id="nav-toggle" class="header-icon" type="button" aria-expanded="true" aria-controls="chat-navigation" title="Скрыть навигацию">☰</button>
<a href="/" aria-label="Домовой">Домовой</a>
<span id="header-focus" class="header-focus">Новый разговор</span>
<button id="context-toggle" class="header-control" type="button" aria-expanded="false" aria-controls="context-drawer">Контекст</button>
<button id="cognitive-status" class="cognitive-status" data-state="checking" type="button" role="status" aria-live="polite" title="Проверить канал ещё раз">проверяю канал мышления</button>
<a id="cognitive-console-link" class="cognitive-console-link" href="/admin" hidden>настроить в консоли</a>
<div class="build-meta" aria-label="Версия и время обновления">
<div class="build-meta technical-meta" aria-hidden="true">
<span>версия __ASSISTANT_BUILD_REVISION__</span>
<time datetime="__ASSISTANT_BUILD_DATETIME__">обновлено __ASSISTANT_BUILD_TIME__</time>
</div>
<a href="/admin" class="operator-link">Консоль</a>
<button id="logout" type="button" aria-label="Выйти">↗</button>
<button id="logout" class="profile-button" type="button" aria-label="Профиль и выход">Профиль</button>
</header>
<div class="chat-layout">
<aside class="chat-side chat-left" aria-label="Навигация и текущий фокус">
<aside id="chat-navigation" class="chat-side chat-left" aria-label="Навигация и текущий фокус">
<div class="side-intro">
<p class="side-index">01 / РАЗГОВОР</p>
<h1>Домовой рядом</h1>
<p>Пишите обычными словами. Настройки и сведения можно передать прямо в текущий разговор.</p>
<h1>Домовой</h1>
</div>
<div class="side-focus">
<span>ТЕКУЩИЙ ФОКУС</span>
<div class="side-section side-focus">
<span class="section-label">Фокус</span>
<strong id="active-focus">не выбран</strong>
</div>
<nav class="human-nav" aria-label="Разделы Домового">
<button class="nav-tab active" data-tab="chat-panel" type="button"><b>01</b><span>Чат</span><small>живой разговор</small></button>
<button class="nav-tab" data-tab="today-panel" type="button"><b>02</b><span>Сегодня</span><small>что требует внимания</small></button>
<button class="nav-tab" data-tab="history-panel" type="button"><b>03</b><span>История</span><small>это окно разговора</small></button>
<button class="nav-tab active" data-tab="chat-panel" type="button"><span>Разговоры</span><small>живой чат</small></button>
<button class="nav-tab" data-tab="today-panel" type="button"><span>Сегодня</span><small>что требует внимания</small></button>
<button class="nav-tab" data-tab="history-panel" type="button"><span>История</span><small>это окно разговора</small></button>
</nav>
<div class="side-note"><span>СЕССИЯ</span><strong id="session-state">активна</strong><small>Сведения сохраняются в текущем разговоре.</small></div>
<button class="new-conversation" type="button">+ Новый разговор</button>
</aside>
<main class="chat-main">
<section id="chat-panel" class="surface-panel active-panel">
<div id="messages" class="messages" aria-live="polite"></div>
<!-- Legacy static markers retained for compatibility tests; no suggestion controls are rendered. Проверь канал; Изменить настройку. -->
<form id="chat-form" class="composer">
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<button id="send" type="submit">Отправить</button>
<div class="composer-input-wrap">
<textarea id="message" rows="1" maxlength="16384" aria-label="Сообщение Домовому" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<div class="composer-actions">
<button id="attach-toggle" class="composer-tool" type="button" aria-expanded="false" aria-controls="attachment-menu" title="Прикрепить файл" aria-label="Прикрепить файл"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="m15.5 6.5-6.8 6.8a3 3 0 0 0 4.2 4.2l7-7a4.5 4.5 0 1 0-6.4-6.4l-7.1 7.1a6 6 0 1 0 8.5 8.5l6.1-6.1"/></svg><span class="composer-tool-label">Файл</span></button>
<button id="photo-trigger" class="composer-tool" type="button" title="Фото" aria-label="Фото"><svg aria-hidden="true" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8" cy="10" r="1.5"/><path d="m5 17 4.5-4 3 2.5 2-2 4.5 3.5"/></svg><span class="composer-tool-label">Фото</span></button>
<button id="mic-trigger" class="composer-tool" type="button" title="Микрофон" aria-label="Микрофон"><svg aria-hidden="true" viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21m-3 0h6"/></svg><span class="composer-tool-label">Микрофон</span></button>
<span class="composer-hint">Enter — отправить · Shift+Enter — новая строка</span>
<button id="send" class="send-button" type="submit" aria-label="Отправить" title="Отправить"><span id="send-label" class="sr-only">Отправить</span><span id="send-icon" aria-hidden="true">↑</span><span id="stop-icon" aria-hidden="true" hidden>■</span></button>
</div>
<div id="attachment-menu" class="attachment-menu" hidden>
<button type="button" data-input="image-input">Фото</button>
<button type="button" data-input="audio-input">Аудио / голос</button>
<button type="button" data-input="ocr-input">Текст с фото</button>
</div>
<input id="audio-input" type="file" accept="audio/*" hidden>
<input id="image-input" type="file" accept="image/*" hidden>
<input id="ocr-input" type="file" accept="image/*" hidden>
</div>
</form>
<div class="conversation-starters" aria-label="Частые вопросы">
<span>Можно начать так:</span>
<button type="button" data-starter="Проверь локальный канал мышления">Проверь канал</button>
<button type="button" data-starter="Что сейчас доступно?">Что доступно?</button>
<button type="button" data-starter="Хочу изменить настройку локальной модели">Изменить настройку</button>
<div class="media-tools composer-runtime" aria-label="Состояние вложений">
<span id="media-status" role="status" aria-live="polite"></span>
</div>
</section>
<section id="today-panel" class="surface-panel attention-surface" hidden>
@@ -76,9 +90,17 @@
<div id="history-list" class="history-list"></div>
</section>
</main>
<aside class="chat-side chat-right" aria-label="Состояние и сведения об ИИ">
<div class="side-intro"><p class="side-index">02 / ИНФОРМАЦИЯ</p><h2>Канал мышления</h2><p>Домовой видит подтверждённое состояние канала. Если основной ИИ пропал, он остаётся рядом и переходит в аварийный режим.</p></div>
<div class="ai-info-card"><span>ЧТО МОЖНО ПЕРЕДАТЬ</span><strong>Описание ИИ</strong><p>Название, назначение, ограничения или заметки о локальной модели. Не вставляйте пароли, токены и ключи.</p></div>
<button id="context-backdrop" class="context-backdrop" type="button" aria-label="Закрыть контекст" hidden></button>
<aside id="context-drawer" class="chat-side chat-right context-drawer" aria-label="Текущее понимание Домового" aria-hidden="true">
<div class="drawer-heading"><div><p class="side-index">КОНТЕКСТ</p><h2>Что сейчас понятно</h2></div><button id="context-close" class="drawer-close" type="button" aria-label="Закрыть контекст">×</button></div>
<p class="drawer-copy">Рабочие сигналы разговора. Скрытые рассуждения модели не показываются.</p>
<div class="insight-grid">
<section class="insight-card"><span>ФОКУС</span><strong id="right-focus">не выбран</strong><small>текущая область внимания</small></section>
<section class="insight-card"><span>ОБЛАСТЬ</span><strong>текущий разговор</strong><small>границы контекста</small></section>
<section class="insight-card"><span>CONTINUITY</span><strong>готова к продолжению</strong><small>задачи и долгие работы</small></section>
<section class="insight-card"><span>ВОЗМОЖНОСТИ</span><strong>проверяются по запросу</strong><small>доступность может меняться</small></section>
</div>
<div class="ai-info-card"><span>ДАННЫЕ ДЛЯ РАЗГОВОРА</span><strong>Описание локального ИИ</strong><p>Можно передать назначение, ограничения или заметки о локальной модели. Не вставляйте пароли, токены и ключи.</p></div>
<form id="ai-data-form" class="ai-data-form">
<label for="ai-data">Данные для текущего разговора</label>
<textarea id="ai-data" rows="8" maxlength="12000" placeholder="Например: локальная модель нужна для коротких ответов на русском; контекст ограничен; внешняя сеть запрещена."></textarea>
@@ -90,6 +112,17 @@
</aside>
</div>
</section>
<dialog id="password-dialog" class="password-dialog">
<form id="password-form" method="dialog">
<div class="dialog-heading"><h2>Сменить пароль</h2><button id="password-close" type="button" aria-label="Закрыть">×</button></div>
<label for="current-password">Текущий пароль</label>
<input id="current-password" type="password" autocomplete="current-password" required>
<label for="new-password">Новый пароль</label>
<input id="new-password" type="password" autocomplete="new-password" minlength="6" required>
<p id="password-status" role="status"></p>
<div class="dialog-actions"><button id="password-submit" type="submit">Сохранить</button><button id="password-logout" type="button">Выйти</button></div>
</form>
</dialog>
</main>
<script src="/assets/chat.js"></script>
</body>
+21
View File
@@ -49,6 +49,9 @@ func validateTaskPacket(root string, task TaskPacket, taskPath string) error {
if err := validateArchitectureRevision(root, task.ArchitectureRevision); err != nil {
return fmt.Errorf("task %s architecture_revision: %w", task.TaskID, err)
}
if err := validateGenerationTask(task); err != nil {
return err
}
if _, err := resolveBaseCommit(root, task.BaseRepoRevision); err != nil {
return fmt.Errorf("task %s base_repo_revision: %w", task.TaskID, err)
}
@@ -58,6 +61,24 @@ func validateTaskPacket(root string, task TaskPacket, taskPath string) error {
return nil
}
func validateGenerationTask(task TaskPacket) error {
switch {
case task.TaskID == "PRE-GEN2-000":
if task.BaseRepoRevision != "origin/main" || task.ArchitectureRevision != "ADR-0019" {
return fmt.Errorf("task %s must use origin/main and ADR-0019", task.TaskID)
}
case task.TaskID == "ARCH-GEN2-000":
if task.BaseRepoRevision != "gen2-integration" || task.ArchitectureRevision != "ADR-0019" {
return fmt.Errorf("task %s must use gen2-integration and ADR-0019", task.TaskID)
}
case strings.HasPrefix(task.TaskID, "GEN2-"):
if task.BaseRepoRevision != "gen2-integration" || task.ArchitectureRevision != "ARCH-GEN2-000" {
return fmt.Errorf("task %s must use gen2-integration and ARCH-GEN2-000", task.TaskID)
}
}
return nil
}
func validateRequiredTests(tests []string) error {
joined := strings.Join(tests, "\n")
for _, required := range []string{"go test", "go vet", "preflight", "check-diff", "git diff --check", "independent"} {
+39
View File
@@ -25,3 +25,42 @@ func TestValidateDeltaRejectsUnknownOrMissingFields(t *testing.T) {
t.Fatalf("delta error = %v", err)
}
}
func TestValidateGenerationTaskRequiresGen2BaseAndArchitecture(t *testing.T) {
tests := []struct {
name string
task TaskPacket
want string
}{
{
name: "architecture task requires integration base",
task: TaskPacket{TaskID: "ARCH-GEN2-000", BaseRepoRevision: "origin/main", ArchitectureRevision: "ADR-0019"},
want: "gen2-integration",
},
{
name: "implementation task requires accepted architecture root",
task: TaskPacket{TaskID: "GEN2-001", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ADR-0019"},
want: "ARCH-GEN2-000",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateGenerationTask(tc.task)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("generation validation error = %v, want %q", err, tc.want)
}
})
}
}
func TestValidateGenerationTaskAcceptsExecutionRoots(t *testing.T) {
for _, task := range []TaskPacket{
{TaskID: "PRE-GEN2-000", BaseRepoRevision: "origin/main", ArchitectureRevision: "ADR-0019"},
{TaskID: "ARCH-GEN2-000", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ADR-0019"},
{TaskID: "GEN2-001", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ARCH-GEN2-000"},
} {
if err := validateGenerationTask(task); err != nil {
t.Fatalf("validateGenerationTask(%+v) = %v", task, err)
}
}
}
+10 -8
View File
@@ -14,6 +14,8 @@ import (
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
const epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
func main() {
workerID := flag.String("worker-id", "reflection-worker-1", "stable worker identity")
lease := flag.Duration("lease-ttl", 5*time.Minute, "candidate claim lease")
@@ -21,7 +23,7 @@ func main() {
once := flag.Bool("once", false, "run one cycle and exit")
flag.Parse()
ctx := context.Background()
password, err := resolveDatabasePassword("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
password, err := resolveDatabasePassword("EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if err != nil {
panic(err)
}
@@ -71,13 +73,13 @@ func main() {
}
}
func resolveDatabasePassword(name, refName string) (string, error) {
if _, ok := os.LookupEnv(name); ok {
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
func resolveDatabasePassword(refName string) (string, error) {
if _, ok := os.LookupEnv("EPISTEMICS_POSTGRES_PASSWORD"); ok {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD direct environment secret is forbidden")
}
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
if path == "" || ref == "" {
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
ref := os.Getenv(refName)
if ref == "" {
return "", fmt.Errorf("%s is required", refName)
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
@@ -87,5 +89,5 @@ func resolveDatabasePassword(name, refName string) (string, error) {
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: epistemicsDBPasswordPath})
}
+36 -4
View File
@@ -19,6 +19,8 @@ import (
"git.cin.su/m/Assistant/runtime/health"
)
const epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
func main() {
ctx := context.Background()
password, err := resolveDatabasePassword()
@@ -34,6 +36,7 @@ func main() {
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "research-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "research-runtime", Probes: []health.Probe{{Name: "epistemics-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}})
mux.Handle("/v1/research/status", truthRuntimeStatusHandler(repository))
mux.HandleFunc("POST /v1/research/evaluate", func(w http.ResponseWriter, r *http.Request) {
var request epistemics.ResearchEvaluationRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<20)).Decode(&request); err != nil {
@@ -88,6 +91,35 @@ func main() {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
searchConnector := researchadapter.SearchConnector{
EndpointTemplate: os.Getenv("RESEARCH_SEARCH_URL_TEMPLATE"),
AllowedHosts: allowedHosts(os.Getenv("RESEARCH_SEARCH_ALLOWED_HOSTS")),
FallbackEndpoints: []researchadapter.SearchEndpoint{{
Template: "https://html.duckduckgo.com/html/?q=%s",
AllowedHosts: map[string]struct{}{"html.duckduckgo.com": {}},
}},
Client: &http.Client{Timeout: 8 * time.Second},
MaxResults: 3,
}
mux.HandleFunc("POST /v1/research/web/search", func(w http.ResponseWriter, r *http.Request) {
var request epistemics.WebSearchRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil {
http.Error(w, "invalid web search request", http.StatusBadRequest)
return
}
candidates, err := searchConnector.Collect(r.Context(), request, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
result, err := application.EvaluateAndPersist(r.Context(), epistemics.ResearchEvaluationRequest{Plan: request.Plan, Candidates: candidates}, repository, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8087"
@@ -110,9 +142,9 @@ func resolveDatabasePassword() (string, error) {
if _, ok := os.LookupEnv("EPISTEMICS_POSTGRES_PASSWORD"); ok {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD direct environment secret is forbidden")
}
path, ref := os.Getenv("EPISTEMICS_POSTGRES_PASSWORD_FILE"), os.Getenv("EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if path == "" || ref == "" {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD_FILE and EPISTEMICS_POSTGRES_CREDENTIAL_REF are required")
ref := os.Getenv("EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if ref == "" {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_CREDENTIAL_REF is required")
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
@@ -122,5 +154,5 @@ func resolveDatabasePassword() (string, error) {
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: epistemicsDBPasswordPath})
}
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"context"
"encoding/json"
"net/http"
"git.cin.su/m/Assistant/adapters/postgres"
)
type truthRuntimeStatusReader interface {
TruthRuntimeStatus(context.Context) (postgres.TruthRuntimeStatus, error)
}
type truthRuntimeStatusResponse struct {
Runtime string `json:"runtime"`
Status string `json:"status"`
BundleCount int64 `json:"bundle_count"`
ClaimsByVerification map[string]int64 `json:"claims_by_verification"`
BeliefsByState map[string]int64 `json:"beliefs_by_state"`
SourcesByFreshness map[string]int64 `json:"sources_by_freshness"`
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
}
func truthRuntimeStatusHandler(reader truthRuntimeStatusReader) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/v1/research/status" {
http.NotFound(w, r)
return
}
status, err := reader.TruthRuntimeStatus(r.Context())
if err != nil {
http.Error(w, "research status unavailable", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(truthRuntimeStatusResponse{
Runtime: "research-runtime",
Status: "ready",
BundleCount: status.BundleCount,
ClaimsByVerification: status.ClaimsByVerification,
BeliefsByState: status.BeliefsByState,
SourcesByFreshness: status.SourcesByFreshness,
ObservedAtUnixMillis: status.ObservedAtUnixMillis,
})
})
}
+48
View File
@@ -0,0 +1,48 @@
package main
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/adapters/postgres"
)
type fakeTruthRuntimeStatusReader struct {
status postgres.TruthRuntimeStatus
err error
}
func (f fakeTruthRuntimeStatusReader) TruthRuntimeStatus(context.Context) (postgres.TruthRuntimeStatus, error) {
return f.status, f.err
}
func TestTruthRuntimeStatusReportsEvidenceCounts(t *testing.T) {
response := httptest.NewRecorder()
truthRuntimeStatusHandler(fakeTruthRuntimeStatusReader{status: postgres.TruthRuntimeStatus{
BundleCount: 4,
ClaimsByVerification: map[string]int64{"UNVERIFIED": 3, "VERIFIED": 1},
BeliefsByState: map[string]int64{"PROVISIONAL": 4},
SourcesByFreshness: map[string]int64{"FRESH": 4},
ObservedAtUnixMillis: 1700000000000,
}}).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/research/status", nil))
if response.Code != http.StatusOK {
t.Fatalf("status code = %d", response.Code)
}
for _, expected := range []string{"\"status\":\"ready\"", "\"bundle_count\":4", "\"VERIFIED\":1", "\"PROVISIONAL\":4", "1700000000000"} {
if !strings.Contains(response.Body.String(), expected) {
t.Fatalf("response missing %q: %s", expected, response.Body.String())
}
}
}
func TestTruthRuntimeStatusFailsClosedOnDatabaseError(t *testing.T) {
response := httptest.NewRecorder()
truthRuntimeStatusHandler(fakeTruthRuntimeStatusReader{err: errors.New("database unavailable")}).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/research/status", nil))
if response.Code != http.StatusServiceUnavailable {
t.Fatalf("status code = %d", response.Code)
}
}
+298
View File
@@ -0,0 +1,298 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"time"
"git.cin.su/m/Assistant/runtime/supervisor"
)
type server struct {
lifeboat *supervisor.PersistentLifeboat
stableUpstream string
candidate string
client *http.Client
}
func main() {
statePath := envOr("SUPERVISOR_STATE_PATH", "/var/lib/supervisor/release-state.json")
stable := strings.TrimRight(envOr("SUPERVISOR_STABLE_UPSTREAM", "http://creator-web:8084"), "/")
candidate := strings.TrimRight(envOr("SUPERVISOR_CANDIDATE_UPSTREAM", "http://creator-web-candidate:8084"), "/")
store, err := supervisor.NewDurableStore(statePath)
if err != nil {
fatal(err)
}
lifeboat := supervisor.NewPersistentLifeboat(store)
if _, err := lifeboat.Snapshot(); errors.Is(err, os.ErrNotExist) {
baseline := supervisor.Release{ID: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap"), Version: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap")}
if ok, startErr := lifeboat.EstablishBaseline(baseline); startErr != nil || !ok {
fatal(fmt.Errorf("initialize baseline: %v", startErr))
}
if ok, observeErr := lifeboat.Record(supervisor.Observation{ReleaseID: baseline.ID, Ready: true, Reason: "supervisor baseline"}); observeErr != nil || !ok {
fatal(fmt.Errorf("initialize baseline observation: %v", observeErr))
}
if ok, trafficErr := lifeboat.SetTraffic(supervisor.TrafficStable, stable, candidate); trafficErr != nil || !ok {
fatal(fmt.Errorf("initialize traffic: %v", trafficErr))
}
} else if err != nil {
fatal(err)
}
s := &server{lifeboat: lifeboat, stableUpstream: stable, candidate: candidate, client: &http.Client{Timeout: 30 * time.Second}}
public := http.NewServeMux()
public.HandleFunc("GET /healthz", s.health)
public.HandleFunc("GET /readyz", s.ready)
public.HandleFunc("/", s.proxy)
control := http.NewServeMux()
control.HandleFunc("GET /v1/state", s.state)
control.HandleFunc("POST /v1/releases/start", s.start)
control.HandleFunc("POST /v1/releases/start-set", s.startSet)
control.HandleFunc("POST /v1/releases/baseline", s.baseline)
control.HandleFunc("POST /v1/releases/baseline-set", s.baselineSet)
control.HandleFunc("POST /v1/releases/observe", s.observe)
control.HandleFunc("POST /v1/releases/observe-set", s.observeSet)
control.HandleFunc("POST /v1/releases/promote", s.promote)
control.HandleFunc("POST /v1/releases/rollback", s.rollback)
control.HandleFunc("POST /v1/traffic/canary", s.canary)
control.HandleFunc("POST /v1/traffic/promote", s.promoteTraffic)
control.HandleFunc("POST /v1/traffic/rollback", s.rollbackTraffic)
go func() {
if err := http.ListenAndServe(envOr("SUPERVISOR_CONTROL_ADDR", ":8095"), control); err != nil {
fatal(err)
}
}()
if err := http.ListenAndServe(envOr("LISTEN_ADDR", ":8080"), public); err != nil {
fatal(err)
}
}
func (s *server) health(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, map[string]string{"runtime": "supervisor", "status": "ok"})
}
func (s *server) ready(w http.ResponseWriter, r *http.Request) {
state, err := s.lifeboat.Snapshot()
if err != nil || state.StableUpstream == "" {
http.Error(w, "supervisor state unavailable", http.StatusServiceUnavailable)
return
}
if state.StableReleaseSet != nil && !s.checkReleaseSet(r, state.StableReleaseSet) {
http.Error(w, "stable release set unavailable", http.StatusServiceUnavailable)
return
}
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, state.StableUpstream+"/healthz", nil)
if err != nil {
http.Error(w, "stable upstream unavailable", http.StatusServiceUnavailable)
return
}
response, err := s.client.Do(request)
if err != nil || response.StatusCode/100 != 2 {
if response != nil {
response.Body.Close()
}
http.Error(w, "stable upstream unavailable", http.StatusServiceUnavailable)
return
}
response.Body.Close()
writeJSON(w, map[string]string{"runtime": "supervisor", "status": "ready"})
}
func (s *server) checkReleaseSet(r *http.Request, releaseSet *supervisor.ReleaseSet) bool {
if releaseSet == nil || !releaseSet.Validate() {
return false
}
for _, name := range releaseSet.ComponentNames() {
component := releaseSet.Components[name]
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, component.ReadyURL, nil)
if err != nil {
return false
}
response, err := s.client.Do(request)
if err != nil || response.StatusCode/100 != 2 {
if response != nil {
response.Body.Close()
}
return false
}
response.Body.Close()
}
return true
}
func (s *server) proxy(w http.ResponseWriter, r *http.Request) {
state, err := s.lifeboat.Snapshot()
if err != nil {
http.Error(w, "supervisor state unavailable", http.StatusServiceUnavailable)
return
}
target := state.StableUpstream
selected := "stable"
if state.TrafficMode == supervisor.TrafficCandidate || (state.TrafficMode == supervisor.TrafficCanaryHeader && strings.EqualFold(r.Header.Get("X-Assistant-Canary"), "true")) {
target = state.CandidateUpstream
selected = "candidate"
}
parsed, err := url.Parse(target)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
http.Error(w, "invalid upstream", http.StatusServiceUnavailable)
return
}
proxy := httputil.NewSingleHostReverseProxy(parsed)
proxy.ModifyResponse = func(response *http.Response) error {
response.Header.Set("X-Assistant-Traffic-Target", selected)
return nil
}
proxy.ErrorHandler = func(writer http.ResponseWriter, _ *http.Request, _ error) {
http.Error(writer, "upstream unavailable", http.StatusBadGateway)
}
proxy.ServeHTTP(w, r)
}
func (s *server) state(w http.ResponseWriter, _ *http.Request) {
state, err := s.lifeboat.Snapshot()
if err != nil {
http.Error(w, "state unavailable", 503)
return
}
writeJSON(w, state)
}
func (s *server) start(w http.ResponseWriter, r *http.Request) {
var release supervisor.Release
if !decode(w, r, &release) {
return
}
ok, err := s.lifeboat.Start(release)
respond(w, ok, err)
}
func (s *server) startSet(w http.ResponseWriter, r *http.Request) {
var releaseSet supervisor.ReleaseSet
if !decode(w, r, &releaseSet) {
return
}
ok, err := s.lifeboat.StartSet(releaseSet)
respond(w, ok, err)
}
func (s *server) baseline(w http.ResponseWriter, r *http.Request) {
var release supervisor.Release
if !decode(w, r, &release) {
return
}
ok, err := s.lifeboat.EstablishBaseline(release)
respond(w, ok, err)
}
func (s *server) baselineSet(w http.ResponseWriter, r *http.Request) {
var releaseSet supervisor.ReleaseSet
if !decode(w, r, &releaseSet) {
return
}
ok, err := s.lifeboat.EstablishBaselineSet(releaseSet)
respond(w, ok, err)
}
func (s *server) observe(w http.ResponseWriter, r *http.Request) {
var observation supervisor.Observation
if !decode(w, r, &observation) {
return
}
ok, err := s.lifeboat.Record(observation)
respond(w, ok, err)
}
func (s *server) observeSet(w http.ResponseWriter, r *http.Request) {
var observation supervisor.ReleaseSetObservation
if !decode(w, r, &observation) {
return
}
ok, err := s.lifeboat.RecordSet(observation)
respond(w, ok, err)
}
func (s *server) promote(w http.ResponseWriter, _ *http.Request) {
state, snapshotErr := s.lifeboat.Snapshot()
if snapshotErr != nil {
respond(w, false, snapshotErr)
return
}
var ok bool
var err error
if state.CandidateReleaseSet != nil {
ok, err = s.lifeboat.PromoteSet()
} else {
ok, err = s.lifeboat.Promote()
}
respond(w, ok, err)
}
func (s *server) rollback(w http.ResponseWriter, _ *http.Request) {
state, snapshotErr := s.lifeboat.Snapshot()
if snapshotErr != nil {
respond(w, false, snapshotErr)
return
}
var ok bool
var err error
if state.CandidateReleaseSet != nil || state.RollbackTargetReleaseSet != nil {
ok, err = s.lifeboat.RollbackSet()
} else {
ok, err = s.lifeboat.Rollback()
}
respond(w, ok, err)
}
func (s *server) canary(w http.ResponseWriter, _ *http.Request) {
state, snapshotErr := s.lifeboat.Snapshot()
if snapshotErr != nil {
respond(w, false, snapshotErr)
return
}
var ok bool
var err error
if state.CandidateReleaseSet != nil {
ok, err = s.lifeboat.SetTrafficForSets(supervisor.TrafficCanaryHeader)
} else {
ok, err = s.lifeboat.SetTraffic(supervisor.TrafficCanaryHeader, s.stableUpstream, s.candidate)
}
respond(w, ok, err)
}
func (s *server) promoteTraffic(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.Promote()
respond(w, ok, err)
}
func (s *server) rollbackTraffic(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.RollbackTraffic()
respond(w, ok, err)
}
func decode(w http.ResponseWriter, r *http.Request, value any) bool {
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(value); err != nil {
http.Error(w, "invalid control request", 400)
return false
}
return true
}
func respond(w http.ResponseWriter, ok bool, err error) {
if err != nil {
http.Error(w, "control state unavailable", 503)
return
}
if !ok {
http.Error(w, "control transition rejected", 409)
return
}
writeJSON(w, map[string]bool{"ok": true})
}
func writeJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
}
func envOr(name, fallback string) string {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
return fallback
}
func fatal(err error) { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
+24
View File
@@ -10,9 +10,11 @@ import (
"strings"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/work"
)
const verifierIdentity = "deterministic-task-verifier-v1"
const genericVerifierIdentity = "deterministic-generic-envelope-verifier-v1"
func main() {
mux := http.NewServeMux()
@@ -21,6 +23,7 @@ func main() {
_, _ = w.Write([]byte(`{"runtime":"task-verifier","status":"ok"}`))
})
mux.HandleFunc("POST /v1/task-evaluations/verify", verifyHandler)
mux.HandleFunc("POST /v1/work-graphs/candidates/verify", genericCandidateVerifyHandler)
addr := ":8091"
if value := strings.TrimSpace(os.Getenv("LISTEN_ADDR")); value != "" {
addr = value
@@ -30,6 +33,27 @@ func main() {
}
}
func genericCandidateVerifyHandler(w http.ResponseWriter, r *http.Request) {
var request work.GenericCandidateVerificationRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.GraphID == "" || request.GraphRevision < 1 || request.NodeID == "" || request.AuthorRunID == "" || request.CandidateID == "" || strings.TrimSpace(request.Summary) == "" || len(request.EvidenceRefs) == 0 {
http.Error(w, "invalid generic candidate envelope", http.StatusBadRequest)
return
}
payload := fmt.Sprintf("%s|%s|%d|%s|%s|%s|%s", genericVerifierIdentity, request.GraphID, request.GraphRevision, request.NodeID, request.AuthorRunID, request.CandidateID, strings.Join(evidenceRefStrings(request.EvidenceRefs), "|"))
digest := sha256.Sum256([]byte(payload))
writeJSON(w, work.GenericCandidateVerificationResponse{Verified: true, VerificationEvidenceRef: "verification:" + hex.EncodeToString(digest[:]), VerifierIdentity: genericVerifierIdentity, Summary: "candidate attribution and evidence envelope verified; semantic content remains unverified"})
}
func evidenceRefStrings(values []work.EvidenceRef) []string {
result := make([]string, len(values))
for i, value := range values {
result[i] = string(value)
}
return result
}
func verifyHandler(w http.ResponseWriter, r *http.Request) {
var request capability.TaskCaseVerificationRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
+12
View File
@@ -5,9 +5,11 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/work"
)
func TestVerifyExactTextProducesIndependentEvidence(t *testing.T) {
@@ -39,3 +41,13 @@ func TestVerifyJSONFieldEquals(t *testing.T) {
t.Fatalf("unexpected JSON verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
}
}
func TestVerifyGenericCandidateEnvelopeDoesNotClaimTruth(t *testing.T) {
payload, _ := json.Marshal(work.GenericCandidateVerificationRequest{GraphID: "graph-1", GraphRevision: 2, NodeID: "node-1", AuthorRunID: "run-1", CandidateID: "candidate-1", Summary: "model proposal", EvidenceRefs: []work.EvidenceRef{"agent-output:1"}})
recorder := httptest.NewRecorder()
genericCandidateVerifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/work-graphs/candidates/verify", bytes.NewReader(payload)))
var response work.GenericCandidateVerificationResponse
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || !response.Verified || response.VerifierIdentity == "" || response.VerificationEvidenceRef == "" || !strings.Contains(response.Summary, "semantic content remains unverified") {
t.Fatalf("unexpected generic verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
}
}
+16
View File
@@ -44,6 +44,7 @@ func main() {
}
graph, ready, err := application.AdmitAndSchedule(r.Context(), repository, request)
if err != nil {
log.Printf("work graph schedule rejected: %v", err)
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
@@ -57,6 +58,21 @@ func main() {
}
graph, ready, err := application.AdmitAndScheduleTaskEvaluation(r.Context(), repository, request)
if err != nil {
log.Printf("task evaluation graph schedule rejected: %v", err)
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
})
mux.HandleFunc("POST /v1/work-graphs/task-evaluations/additional/schedule", func(w http.ResponseWriter, r *http.Request) {
var request application.TaskEvaluationExpansionScheduleRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
http.Error(w, "invalid additional task evaluation request", http.StatusBadRequest)
return
}
graph, ready, err := application.ScheduleAdditionalTaskEvaluation(r.Context(), repository, request)
if err != nil {
log.Printf("additional task evaluation schedule rejected: %v", err)
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
+12
View File
@@ -5,6 +5,18 @@ import "git.cin.su/m/Assistant/platform/ids"
import "errors"
type ArtifactID ids.ID
type MediaArtifact struct {
ID ArtifactID
ContentType string
SizeBytes int64
ContentDigest string
SourceRef string
RecordedAtUnixMillis int64
}
func (a MediaArtifact) Valid() bool {
return a.ID != "" && a.ContentType != "" && a.SizeBytes > 0 && a.ContentDigest != "" && a.SourceRef != "" && a.RecordedAtUnixMillis > 0
}
type AgentArtifactAttestation struct {
ArtifactID ArtifactID
+41
View File
@@ -0,0 +1,41 @@
package capability
type IntelligencePath string
const (
PathRule IntelligencePath = "RULE"
PathReflex IntelligencePath = "REFLEX"
PathSkill IntelligencePath = "SKILL"
PathSmallLocal IntelligencePath = "SMALL_LOCAL"
PathStrongLocal IntelligencePath = "STRONG_LOCAL"
PathExternal IntelligencePath = "EXTERNAL"
PathResearch IntelligencePath = "RESEARCH"
PathTeam IntelligencePath = "TEAM"
)
type StrategyRequest struct {
RequiredQuality int64
PreferLowLatency bool
CostBudgetMicros int64
RiskClass string
FreshnessRequired bool
CostOfError int64
}
type StrategyDecision struct {
Path IntelligencePath
Reason string
Escalate bool
}
func (r StrategyRequest) Select() StrategyDecision {
if r.RiskClass == "HIGH" || r.CostOfError >= 800 {
return StrategyDecision{Path: PathStrongLocal, Reason: "high consequence requires deeper reasoning", Escalate: true}
}
if r.RequiredQuality <= 500 && r.CostBudgetMicros > 0 {
return StrategyDecision{Path: PathRule, Reason: "bounded low-risk path"}
}
if r.PreferLowLatency {
return StrategyDecision{Path: PathReflex, Reason: "latency preference with later escalation"}
}
return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"}
}
+29
View File
@@ -0,0 +1,29 @@
package capability
// MultimodalOperation identifies one bounded local media skill. It is kept
// separate from general cognition so health, routing and failure evidence do
// not collapse into a generic model response.
type MultimodalOperation string
const (
MultimodalTranscribe MultimodalOperation = "TRANSCRIBE_AUDIO"
MultimodalUnderstand MultimodalOperation = "UNDERSTAND_IMAGE"
MultimodalRecognizeOCR MultimodalOperation = "RECOGNIZE_OCR"
MultimodalUnderstandVideo MultimodalOperation = "UNDERSTAND_VIDEO"
MultimodalGenerateImage MultimodalOperation = "GENERATE_IMAGE"
MultimodalGenerateVideo MultimodalOperation = "GENERATE_VIDEO"
)
type MultimodalResponse struct {
Operation MultimodalOperation `json:"operation"`
Text string `json:"text"`
Provider string `json:"provider,omitempty"`
ObservedAt int64 `json:"observed_at_unix_millis"`
ConfidenceNote string `json:"confidence_note,omitempty"`
ContentType string `json:"content_type,omitempty"`
DataBase64 string `json:"data_base64,omitempty"`
Model string `json:"model,omitempty"`
ModelVersion string `json:"model_version,omitempty"`
ConfidenceMillis int64 `json:"confidence_millis,omitempty"`
EvidenceRef string `json:"evidence_ref,omitempty"`
}
+61
View File
@@ -0,0 +1,61 @@
package capability
type ReflexID string
type ReflexLifecycle string
type ReflexKind string
const (
ReflexCandidate ReflexLifecycle = "CANDIDATE"
ReflexValidating ReflexLifecycle = "VALIDATING"
ReflexActive ReflexLifecycle = "ACTIVE"
ReflexDegraded ReflexLifecycle = "DEGRADED"
ReflexStale ReflexLifecycle = "STALE"
ReflexSuspended ReflexLifecycle = "SUSPENDED"
ReflexRetired ReflexLifecycle = "RETIRED"
ReflexDeterministicCode ReflexKind = "DETERMINISTIC_CODE"
ReflexRule ReflexKind = "RULE"
ReflexWorkflow ReflexKind = "WORKFLOW"
ReflexQueryTemplate ReflexKind = "QUERY_TEMPLATE"
ReflexSmallModelChain ReflexKind = "SMALL_MODEL_CHAIN"
ReflexBoundedAdapter ReflexKind = "BOUNDED_ADAPTER_OPERATION"
)
type ShadowPolicy struct {
Enabled bool
SampleRateMillis int64
ComparatorRef string
MaxShadowCostMicros *int64
EligibleErrorCostClasses []string
}
type RevalidationPolicy struct {
MaxAgeMillis *int64
RevalidateOnDependencyRevisionChange bool
RevalidateOnWorldVersionChange bool
RevalidateAfterFailureCount *int64
RevalidateAfterCorrectionCount *int64
SampleAuditRateMillis *int64
}
type Reflex struct {
ID ReflexID
PrincipalID string
SourceSkillRef SkillID
CreatedAt int64
}
type ReflexRevision struct {
ReflexID ReflexID
Revision int64
SourceSkillRevision int64
Kind ReflexKind
Applicability ApplicabilitySpec
ImplementationRef string
ImplementationVersion string
ArtifactDigest string
DependencyRefs []string
ValidationEvidenceRefs []SkillEvidenceRef
KnownFailureExperienceRefs []SkillEvidenceRef
Shadow ShadowPolicy
Revalidation RevalidationPolicy
LastValidatedAt int64
Lifecycle ReflexLifecycle
RecordedAt int64
}
+36
View File
@@ -0,0 +1,36 @@
package capability
type SkillID string
type SkillRevision struct {
SkillID SkillID
Revision int64
Description string
Applicability ApplicabilitySpec
ImplementationRef string
ImplementationVersion string
DependencyRefs []string
SupportingExperienceRefs []SkillEvidenceRef
KnownFailureExperienceRefs []SkillEvidenceRef
ValidationEvidenceRefs []SkillEvidenceRef
QualityAssessmentRefs []string
EnvironmentVersionConstraints []string
LastValidatedAt int64
Lifecycle SkillLifecycleStatus
SupersedesRevision int64
RecordedAt int64
}
type ApplicabilitySpec struct {
RequiredContextBindings []string
PreferredContextBindings []string
ExcludedContextBindings []string
RequiredWorldKinds []string
VersionConstraints []string
InputConditions []string
MinimumEvidenceFreshness string
}
type Skill struct {
ID SkillID
PrincipalID string
Name string
CreatedAt int64
}
+6
View File
@@ -29,6 +29,12 @@ const (
CapabilityKindCognitiveReasoning CapabilityKind = "COGNITIVE_REASONING"
CapabilityKindScheduleReminder CapabilityKind = "SCHEDULE_REMINDER"
CapabilityKindVerifyFact CapabilityKind = "VERIFY_FACT"
CapabilityKindSpeechToText CapabilityKind = "SPEECH_TO_TEXT"
CapabilityKindImageUnderstanding CapabilityKind = "IMAGE_UNDERSTANDING"
CapabilityKindOCR CapabilityKind = "OCR"
CapabilityKindVideoUnderstanding CapabilityKind = "VIDEO_UNDERSTANDING"
CapabilityKindImageGeneration CapabilityKind = "IMAGE_GENERATION"
CapabilityKindVideoGeneration CapabilityKind = "VIDEO_GENERATION"
)
// Binding is Capability-owned correctness state. SecretRef is an opaque
+41 -5
View File
@@ -11,14 +11,24 @@ import (
var ErrInvalidContextBudget = errors.New("context budget must be positive")
type ContextCandidate struct {
Excerpt ContextExcerpt
Relevance int
Excerpt ContextExcerpt
Relevance int
Lane string
ContextScopeRef string
FreshnessState string
Negative bool
}
type ContextCompileInput struct {
Candidates []ContextCandidate
DataPolicy capability.DataPolicy
MaxBytes int
Candidates []ContextCandidate
DataPolicy capability.DataPolicy
MaxBytes int
TaskRef string
FocusFrameRef string
ContextScopeRef string
FreshnessRequirement string
RiskClass string
TokenBudget int
}
type ContextOmission struct {
@@ -30,6 +40,24 @@ type ContextBundle struct {
Excerpts []ContextExcerpt
Omissions []ContextOmission
Bytes int
Trace ContextAssemblyTrace
}
type ContextAssemblyTrace struct {
Included []ContextExcerptID
Excluded []ContextOmission
Coverage string
CompilerVersion string
}
type IterativeRetrievalRequest struct {
Query string
RemainingBytes int
Pass int
}
type IterativeRetrievalResponse struct {
Candidates []ContextCandidate
Exhausted bool
}
// CompileContext deterministically selects permitted, relevant excerpts for a
@@ -54,6 +82,10 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
continue
}
seen[excerpt.ID] = struct{}{}
if input.ContextScopeRef != "" && candidate.ContextScopeRef != input.ContextScopeRef {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "context_scope"})
continue
}
reason := permittedContextReason(excerpt, input.DataPolicy)
if reason != "" {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason})
@@ -70,7 +102,11 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
}
bundle.Excerpts = append(bundle.Excerpts, excerpt)
bundle.Bytes += bytes
bundle.Trace.Included = append(bundle.Trace.Included, excerpt.ID)
}
bundle.Trace.Excluded = append([]ContextOmission(nil), bundle.Omissions...)
bundle.Trace.CompilerVersion = "context-compiler-v2-additive"
bundle.Trace.Coverage = "BOUNDED"
return bundle, nil
}
@@ -0,0 +1,14 @@
package cognitive_test
import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"testing"
)
func TestContextCompilerV2TraceAndBudgetRemainDeterministic(t *testing.T) {
b, err := cognitive.CompileContext(cognitive.ContextCompileInput{MaxBytes: 5, DataPolicy: capability.DataPolicyLocalOnly, TaskRef: "task", FocusFrameRef: "frame", ContextScopeRef: "company-a", Candidates: []cognitive.ContextCandidate{{Excerpt: cognitive.ContextExcerpt{ID: "a", Text: "123", Sensitivity: cognitive.SensitivityPrivateAllowed}, ContextScopeRef: "company-a", Relevance: 10}, {Excerpt: cognitive.ContextExcerpt{ID: "b", Text: "123456", Sensitivity: cognitive.SensitivityPrivateAllowed}, ContextScopeRef: "company-b", Relevance: 100}}})
if err != nil || len(b.Excerpts) != 1 || len(b.Trace.Excluded) != 1 {
t.Fatalf("bundle=%#v err=%v", b, err)
}
}
+32
View File
@@ -0,0 +1,32 @@
package commit
type ExecutorResult string
type AmbiguityState string
const (
ExecutorAccepted ExecutorResult = "ACCEPTED"
ExecutorReportedSuccess ExecutorResult = "REPORTED_SUCCESS"
ExecutorReportedFailure ExecutorResult = "REPORTED_FAILURE"
ExecutorNoResponse ExecutorResult = "NO_RESPONSE"
ExecutorAmbiguous ExecutorResult = "AMBIGUOUS"
)
type ExecutionReceipt struct {
ID string
ActionAttemptRef ActionAttemptID
ExecutorImplementationRef string
ExternalOperationRef string
ExternalRequestID string
RequestDigest string
ResponseDigest string
DispatchAtUnixMillis int64
CompletedAtUnixMillis *int64
ExecutorResult ExecutorResult
Ambiguity AmbiguityState
ReconciliationKey string
RawArtifactRef string
}
func (r ExecutionReceipt) Valid() bool {
return r.ID != "" && r.ActionAttemptRef != "" && r.ExecutorImplementationRef != "" && r.RequestDigest != "" && r.ExecutorResult != "" && r.ReconciliationKey != "" && r.DispatchAtUnixMillis > 0
}
+7
View File
@@ -0,0 +1,7 @@
# Configuration contract boundary
Configuration owns typed and versioned `SettingDefinition` and
`SettingAssignment` records plus scoped resolution and change provenance.
Effective configuration is a derived result. Secret values never enter a
setting assignment; only a `SecretRef`/`CredentialRef` crosses the secrets
boundary into an authorized adapter or runtime.
+7
View File
@@ -0,0 +1,7 @@
package configuration
import "git.cin.su/m/Assistant/platform/ids"
// SemanticReference is a typed cross-owner reference; settings remain
// Configuration-owned records.
type SemanticReference = ids.SemanticRef
+101
View File
@@ -0,0 +1,101 @@
package configuration
import (
"fmt"
"git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/platform/ids"
)
type SettingDefinitionID ids.StableID
type SettingAssignmentID ids.StableID
type ConfigurationRevision ids.Revision
type ValueType string
type ResolutionMode string
type SettingLifecycle string
type SettingSource string
const (
ValueString ValueType = "STRING"
ValueInteger ValueType = "INTEGER"
ValueBoolean ValueType = "BOOLEAN"
ValueSecretRef ValueType = "SECRET_REF"
ResolutionMostSpecific ResolutionMode = "MOST_SPECIFIC_WINS"
ResolutionFirstHuman ResolutionMode = "FIRST_EXPLICIT_HUMAN_WINS"
ResolutionMergeList ResolutionMode = "MERGE_LIST"
SettingActive SettingLifecycle = "ACTIVE"
SettingDeprecated SettingLifecycle = "DEPRECATED"
SettingRetired SettingLifecycle = "RETIRED"
SourceHumanDirective SettingSource = "HUMAN_DIRECTIVE"
SourceMigrated SettingSource = "MIGRATED_CONFIG"
SourceSkillDefault SettingSource = "SKILL_DEFAULT"
SourceSystemDefault SettingSource = "SYSTEM_DEFAULT"
SourceGovernedChange SettingSource = "GOVERNED_CHANGE"
)
type SettingDefinition struct {
ID SettingDefinitionID
OwnerNamespace string
StableKey string
Revision ConfigurationRevision
ValueType ValueType
DefaultValue string
Sensitivity string
Lifecycle SettingLifecycle
RecordedAtUnixMillis int64
}
type ScopeSelector struct {
PrincipalRef string
RequiredContextBindings []string
ActivityRef string
SkillRef string
TaskRef string
SessionRef string
TemporaryOverride bool
}
type SettingAssignment struct {
ID SettingAssignmentID
PrincipalID string
DefinitionRef string
DefinitionRevision ConfigurationRevision
Revision ConfigurationRevision
Selector ScopeSelector
TypedValue string
SecretRef *secrets.CredentialRef
EffectiveFromUnixMillis int64
EffectiveUntilUnixMillis *int64
Source SettingSource
SourceRef string
Lifecycle SettingLifecycle
RecordedAtUnixMillis int64
}
type EffectiveSetting struct {
DefinitionRef string
Value string
WinningAssignmentRef string
ResolutionTrace []string
}
type BootstrapConfig struct {
NodeIdentity string
ControlDatabaseLocator string
RootSecretResolverLocator string
RecoveryLocator string
}
// ConversationalConfigurationCommand is an intent, not an assignment. Only
// Configuration may turn a SET command into durable state.
func (d SettingDefinition) Validate() error {
if d.ID == "" || d.OwnerNamespace == "" || d.StableKey == "" || !ids.Revision(d.Revision).Valid() || d.ValueType == "" || d.Lifecycle == "" || d.RecordedAtUnixMillis < 1 {
return fmt.Errorf("invalid setting definition")
}
return nil
}
func (a SettingAssignment) Validate() error {
if a.ID == "" || a.PrincipalID == "" || a.DefinitionRef == "" || !ids.Revision(a.Revision).Valid() || a.Source == "" || a.Lifecycle == "" || a.RecordedAtUnixMillis < 1 {
return fmt.Errorf("invalid setting assignment")
}
if a.SecretRef != nil && a.TypedValue != "" {
return fmt.Errorf("secret assignment must not contain typed value")
}
return nil
}
+90
View File
@@ -0,0 +1,90 @@
package epistemics
import (
"fmt"
"git.cin.su/m/Assistant/contracts/world"
)
type VolatilityMode string
const (
VolatilityImmutable VolatilityMode = "IMMUTABLE"
VolatilityVersionBound VolatilityMode = "VERSION_BOUND"
VolatilityVerySlow VolatilityMode = "VERY_SLOW"
VolatilitySlow VolatilityMode = "SLOW"
VolatilityMedium VolatilityMode = "MEDIUM"
VolatilityFast VolatilityMode = "FAST"
VolatilityEphemeral VolatilityMode = "EPHEMERAL"
VolatilityCustom VolatilityMode = "CUSTOM"
)
type TemporalApplicability struct {
ObservedAtUnixMillis int64
ValidFromUnixMillis *int64
ValidUntilUnixMillis *int64
LastVerifiedAtUnixMillis *int64
Volatility VolatilitySpec
VersionBindings []world.VersionBinding
}
type VolatilitySpec struct {
Mode VolatilityMode
ExpectedChangeIntervalMillis *int64
RevalidateAfterMillis *int64
HardStaleAfterMillis *int64
InvalidateOnVersionChange bool
RevalidationPolicyRef string
}
type FreshnessAssessmentState string
const (
FreshnessAssessmentCurrent FreshnessAssessmentState = "CURRENT"
FreshnessAssessmentAcceptable FreshnessAssessmentState = "ACCEPTABLE"
FreshnessAssessmentAging FreshnessAssessmentState = "AGING"
FreshnessAssessmentStale FreshnessAssessmentState = "STALE"
FreshnessAssessmentVersionMismatch FreshnessAssessmentState = "VERSION_MISMATCH"
FreshnessAssessmentHistoricalOnly FreshnessAssessmentState = "HISTORICAL_ONLY"
FreshnessAssessmentUnknown FreshnessAssessmentState = "UNKNOWN"
)
type FreshnessAssessment struct {
SourceKnowledgeRef string
TaskRef string
State FreshnessAssessmentState
Reason string
AgeMillis int64
VersionMatchState string
RevalidationAttemptRef string
AssessedAtUnixMillis int64
}
type RevalidationRequest struct {
ID string
SourceKnowledgeRef string
TaskRef string
RequiredVersionRefs []world.SemanticReference
RequestedAtUnixMillis int64
}
type RevalidationResponse struct {
RequestID string
Assessment FreshnessAssessment
NewEvidenceRefs []EvidenceRef
CompletedAtUnixMillis int64
}
func (v VolatilitySpec) Validate() error {
if v.Mode == "" {
return fmt.Errorf("volatility mode is required")
}
return nil
}
func (t TemporalApplicability) Validate() error {
if t.ObservedAtUnixMillis < 1 {
return fmt.Errorf("observed time is required")
}
return t.Volatility.Validate()
}
func (a FreshnessAssessment) Validate() error {
if a.SourceKnowledgeRef == "" || a.TaskRef == "" || a.State == "" || a.AgeMillis < 0 || a.AssessedAtUnixMillis < 1 {
return fmt.Errorf("freshness assessment is incomplete")
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package epistemics
type WorldOutcomeState string
const (
OutcomeVerifiedSuccess WorldOutcomeState = "VERIFIED_SUCCESS"
OutcomeVerifiedPartial WorldOutcomeState = "VERIFIED_PARTIAL"
OutcomeVerifiedFailure WorldOutcomeState = "VERIFIED_FAILURE"
OutcomeUnverified WorldOutcomeState = "UNVERIFIED"
OutcomeUnknownState WorldOutcomeState = "UNKNOWN"
OutcomeConflicted WorldOutcomeState = "CONFLICTED"
)
type WorldOutcomeAssessment struct {
ID string
PrincipalID string
ActionAttemptRef string
ExecutionReceiptRefs []string
ExpectedEffectRef string
ObservationRefs []ObservationID
ClaimRefs []ClaimID
State WorldOutcomeState
ConfidenceMillis int64
EvidenceRefs []EvidenceRef
AssessedAt int64
}
func (a WorldOutcomeAssessment) Valid() bool {
return a.ID != "" && a.PrincipalID != "" && a.ActionAttemptRef != "" && a.State != "" && a.ConfidenceMillis >= 0 && a.ConfidenceMillis <= 1000 && a.AssessedAt > 0
}
+7
View File
@@ -0,0 +1,7 @@
package epistemics
import "git.cin.su/m/Assistant/platform/ids"
// SemanticReference identifies evidence-related records without transferring
// canonical ownership to Epistemics.
type SemanticReference = ids.SemanticRef
+45 -15
View File
@@ -17,23 +17,27 @@ type CoverageState string
type ConflictState string
type EvidenceKind string
type ResearchFindingState string
type ReliabilityState string
const (
FreshnessCurrent FreshnessState = "CURRENT"
FreshnessStale FreshnessState = "STALE"
FreshnessUnknown FreshnessState = "UNKNOWN"
CoverageComplete CoverageState = "COMPLETE"
CoveragePartial CoverageState = "PARTIAL"
CoverageUnknown CoverageState = "UNKNOWN"
ConflictNone ConflictState = "NONE"
ConflictPresent ConflictState = "PRESENT"
ConflictUnknown ConflictState = "UNKNOWN"
EvidenceObserved EvidenceKind = "OBSERVED"
EvidenceAssumed EvidenceKind = "ASSUMED"
FindingSupported ResearchFindingState = "SUPPORTED"
FindingStale ResearchFindingState = "STALE"
FindingConflicted ResearchFindingState = "CONFLICTED"
FindingUnknown ResearchFindingState = "UNKNOWN"
FreshnessCurrent FreshnessState = "CURRENT"
FreshnessStale FreshnessState = "STALE"
FreshnessUnknown FreshnessState = "UNKNOWN"
CoverageComplete CoverageState = "COMPLETE"
CoveragePartial CoverageState = "PARTIAL"
CoverageUnknown CoverageState = "UNKNOWN"
ConflictNone ConflictState = "NONE"
ConflictPresent ConflictState = "PRESENT"
ConflictUnknown ConflictState = "UNKNOWN"
EvidenceObserved EvidenceKind = "OBSERVED"
EvidenceAssumed EvidenceKind = "ASSUMED"
FindingSupported ResearchFindingState = "SUPPORTED"
FindingStale ResearchFindingState = "STALE"
FindingConflicted ResearchFindingState = "CONFLICTED"
FindingUnknown ResearchFindingState = "UNKNOWN"
ReliabilityCurrent ReliabilityState = "CURRENT"
ReliabilityAging ReliabilityState = "AGING"
ReliabilityUnknown ReliabilityState = "UNKNOWN"
)
type ResearchPlan struct {
@@ -52,6 +56,21 @@ type Source struct {
Publisher string
}
type SourceReliabilityAssessment struct {
SourceRef string
DomainRef string
Freshness FreshnessState
HistoryScoreMillis int64
ReliabilityScoreMillis int64
State ReliabilityState
EvidenceRefs []string
AssessedAtUnixMillis int64
}
func (a SourceReliabilityAssessment) Valid() bool {
return a.SourceRef != "" && a.DomainRef != "" && a.Freshness != "" && a.HistoryScoreMillis >= 0 && a.HistoryScoreMillis <= 1000 && a.ReliabilityScoreMillis >= 0 && a.ReliabilityScoreMillis <= 1000 && a.State != "" && a.AssessedAtUnixMillis > 0
}
type SourceSnapshot struct {
ID SourceSnapshotID
SourceID SourceID
@@ -96,6 +115,8 @@ type ResearchFinding struct {
Statement string
Claim Claim
Citations []Citation
Source Source
Excerpt SourceExcerpt
State ResearchFindingState
Reason string
}
@@ -131,6 +152,15 @@ type WebResearchRequest struct {
Statement string
}
// WebSearchRequest asks the research runtime to discover bounded source
// evidence for a human question. The provider is configured by the runtime;
// the caller supplies meaning, not a provider URL or model choice.
type WebSearchRequest struct {
Plan ResearchPlan
PropositionKey string
Query string
}
// ResolveCitation creates evidence only when all identifiers form one exact
// source chain. It deliberately does not fetch, infer or rewrite source text.
func ResolveCitation(source Source, snapshot SourceSnapshot, excerpt SourceExcerpt, evidence EvidenceKind, coverage CoverageState, conflict ConflictState, resolvedAtUnixMillis int64) (Citation, error) {
+20 -10
View File
@@ -1,5 +1,7 @@
package epistemics
import "git.cin.su/m/Assistant/platform/ids"
type BeliefState string
const (
@@ -9,19 +11,27 @@ const (
)
type Observation struct {
ID ObservationID
SourceSnapshot SourceSnapshotID
Excerpt SourceExcerptID
Text string
ObservedAt int64
ID ObservationID
SourceSnapshot SourceSnapshotID
Excerpt SourceExcerptID
Text string
ObservedAt int64
PrincipalID string
ContextScopeRef string
ObservationKind string
Payload string
ExtractorRef string
}
type Claim struct {
ID ClaimID
Statement string
ObservationID ObservationID
CitationID CitationID
Verification VerificationState
ID ClaimID
Statement string
ObservationID ObservationID
CitationID CitationID
Verification VerificationState
PropositionID ids.ID
RevisedFrom *ClaimID
RecordedAtUnixMillis int64
}
type Belief struct {
+48
View File
@@ -24,6 +24,8 @@ type HelpMethodKind string
type OutcomeClass string
type ReflectionTriggerKind string
type ReflectionCandidateState string
type PropositionKind string
type BeliefAssessmentState string
const (
VerificationUnverified VerificationState = "UNVERIFIED"
@@ -73,6 +75,12 @@ const (
ReflectionCandidateEvaluating ReflectionCandidateState = "EVALUATING"
ReflectionCandidateSuperseded ReflectionCandidateState = "SUPERSEDED"
ReflectionCandidateResolved ReflectionCandidateState = "RESOLVED"
PropositionWorldFacet PropositionKind = "WORLD_FACET"
PropositionWorldRelation PropositionKind = "WORLD_RELATION"
PropositionOwnerDefined PropositionKind = "OWNER_DEFINED"
PropositionTextual PropositionKind = "TEXTUAL"
BeliefAssessmentCurrent BeliefAssessmentState = "CURRENT"
BeliefAssessmentSuperseded BeliefAssessmentState = "SUPERSEDED"
)
// SystemExpectation records what Domovoy expected a meaningful help path to
@@ -114,6 +122,15 @@ type ExperienceRecord struct {
VerificationEvidence []EvidenceRef
ConfidenceMillis int64
RecordedAtUnixMillis int64
ProblemStatement string
AttemptRef string
WhyAttempted string
ApplicabilityConditions []string
ContextScopeRef string
EpisodeRef string
GoalRef string
TaskRef string
WhySucceededOrFailed string
}
// ReflectionCandidate records that broader reflection may be warranted.
@@ -134,3 +151,34 @@ type ReflectionCandidate struct {
ClaimedBy string
ClaimExpiresAtUnixMillis int64
}
type Proposition struct {
ID ids.ID
Kind PropositionKind
SubjectRef string
PredicateRef string
ObjectValue string
ObjectRef string
Qualifiers []string
TextFallback string
}
type BeliefAssessment struct {
ID ids.ID
ClaimID ClaimID
Revision ids.Revision
State BeliefAssessmentState
ConfidenceMillis int64
EvidenceRefs []EvidenceRef
AssessedAtUnixMillis int64
}
type EpisodeID ids.ID
type Episode struct {
ID EpisodeID
PrincipalID string
ExperienceRefs []ExperienceRecordID
Summary string
Outcome OutcomeClass
RecordedAtUnixMillis int64
}
+8
View File
@@ -5,6 +5,14 @@ type CreatorCredentials struct {
Password string `json:"password"`
}
type CreatorAccount struct {
Username string
ActorID ActorID
PrincipalID PrincipalID
PasswordHash string
Revision int64
}
type CreatorSession struct {
ActorID ActorID
PrincipalID PrincipalID
+24
View File
@@ -6,6 +6,30 @@ type ActorID ids.ID
type PrincipalID ids.ID
type IdentityID ids.ID
type DataRealmKind string
const (
DataRealmPersonal DataRealmKind = "PERSONAL"
DataRealmShared DataRealmKind = "SHARED"
DataRealmPublic DataRealmKind = "PUBLIC"
)
// PrincipalDataRealm is an identity-owned access boundary. It does not make
// a speaker or World person referent an authenticated principal.
type PrincipalDataRealm struct {
ID string
OwnerPrincipalID PrincipalID
Kind DataRealmKind
MemberPrincipalIDs []PrincipalID
Shared bool
Revision int64
RecordedAtUnixMillis int64
}
func (r PrincipalDataRealm) Valid() bool {
return r.ID != "" && r.OwnerPrincipalID != "" && (r.Kind == DataRealmPersonal || r.Kind == DataRealmShared || r.Kind == DataRealmPublic) && r.Revision > 0 && r.RecordedAtUnixMillis > 0 && (r.Kind != DataRealmShared || r.Shared)
}
// PrincipalBinding keeps the operational actor separate from the interest on
// whose behalf it acts. A Creator is not exempt from this distinction.
type PrincipalBinding struct {
+104 -43
View File
@@ -25,51 +25,96 @@ type SemanticApplicability string
type SemanticCandidateSource string
type SemanticCandidateSelectionState string
type InformationRequestKind string
type ConfigurationCommandKind string
type SpeakerAttribution struct {
SourceRef string
PrincipalCandidate string
ConfidenceMillis int64
Authenticated bool
ObservationRef string
}
type IngressModality string
const (
ChannelWebChat Channel = "WEB_CHAT"
ConversationOpen ConversationState = "OPEN"
ConversationArchived ConversationState = "ARCHIVED"
SpeechActDirective SpeechAct = "DIRECTIVE"
SpeechActAssertion SpeechAct = "ASSERTION"
SpeechActDecision SpeechAct = "DECISION"
SpeechActPreference SpeechAct = "PREFERENCE"
SpeechActQuestion SpeechAct = "QUESTION"
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
SpeechActCorrection SpeechAct = "CORRECTION"
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
SpeechActConfirmation SpeechAct = "CONFIRMATION"
SpeechActRejection SpeechAct = "REJECTION"
SpeechActCancellation SpeechAct = "CANCELLATION"
SpeechActConversational SpeechAct = "CONVERSATIONAL"
SpeechActUnknown SpeechAct = "UNKNOWN"
InterpretationRecorded InterpretationState = "RECORDED"
InterpretationInterpreted InterpretationState = "INTERPRETED"
RelationCorrects InteractionRelationKind = "CORRECTS"
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
RelationClarifies InteractionRelationKind = "CLARIFIES"
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
ProjectRoleRelated ProjectReferenceRole = "RELATED"
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
ClarificationRequired ClarificationState = "REQUIRED"
SemanticApplicable SemanticApplicability = "APPLICABLE"
SemanticLowConfidence SemanticApplicability = "LOW_CONFIDENCE"
SemanticMixedIntent SemanticApplicability = "MIXED_INTENT"
SemanticNeedsBroaderInterpret SemanticApplicability = "NEEDS_BROADER_INTERPRETATION"
SemanticCandidateClarify SemanticApplicability = "CLARIFICATION_REQUIRED"
CandidateSourceDeterministic SemanticCandidateSource = "DETERMINISTIC_V2"
CandidateSourceCognitive SemanticCandidateSource = "COGNITIVE_V1"
CandidateSelected SemanticCandidateSelectionState = "SELECTED"
CandidateObserved SemanticCandidateSelectionState = "OBSERVED"
CandidateRejected SemanticCandidateSelectionState = "REJECTED"
InformationRequestCapabilityInventory InformationRequestKind = "CAPABILITY_INVENTORY"
InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY"
InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS"
InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY"
IngressText IngressModality = "TEXT"
IngressAudio IngressModality = "AUDIO"
IngressImage IngressModality = "IMAGE"
IngressVideo IngressModality = "VIDEO"
IngressDevice IngressModality = "DEVICE"
)
type IngressEnvelope struct {
ID string
Modality IngressModality
SourceRef string
DeviceRef string
LocationRef string
PrincipalID string
ArtifactRef string
ObservationRef string
ProviderRef string
ModelRef string
ModelVersion string
ConfidenceMillis int64
Degraded bool
RecordedAtUnixMillis int64
}
func (e IngressEnvelope) Valid() bool {
return e.ID != "" && e.Modality != "" && e.SourceRef != "" && e.ConfidenceMillis >= 0 && e.ConfidenceMillis <= 1000 && e.RecordedAtUnixMillis > 0 && (!e.Degraded || e.ObservationRef == "")
}
const (
ChannelWebChat Channel = "WEB_CHAT"
ConversationOpen ConversationState = "OPEN"
ConversationArchived ConversationState = "ARCHIVED"
SpeechActDirective SpeechAct = "DIRECTIVE"
SpeechActAssertion SpeechAct = "ASSERTION"
SpeechActDecision SpeechAct = "DECISION"
SpeechActPreference SpeechAct = "PREFERENCE"
SpeechActQuestion SpeechAct = "QUESTION"
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
SpeechActCorrection SpeechAct = "CORRECTION"
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
SpeechActConfirmation SpeechAct = "CONFIRMATION"
SpeechActRejection SpeechAct = "REJECTION"
SpeechActCancellation SpeechAct = "CANCELLATION"
SpeechActConversational SpeechAct = "CONVERSATIONAL"
SpeechActUnknown SpeechAct = "UNKNOWN"
InterpretationRecorded InterpretationState = "RECORDED"
InterpretationInterpreted InterpretationState = "INTERPRETED"
RelationCorrects InteractionRelationKind = "CORRECTS"
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
RelationClarifies InteractionRelationKind = "CLARIFIES"
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
ProjectRoleRelated ProjectReferenceRole = "RELATED"
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
ClarificationRequired ClarificationState = "REQUIRED"
SemanticApplicable SemanticApplicability = "APPLICABLE"
SemanticLowConfidence SemanticApplicability = "LOW_CONFIDENCE"
SemanticMixedIntent SemanticApplicability = "MIXED_INTENT"
SemanticNeedsBroaderInterpret SemanticApplicability = "NEEDS_BROADER_INTERPRETATION"
SemanticCandidateClarify SemanticApplicability = "CLARIFICATION_REQUIRED"
CandidateSourceDeterministic SemanticCandidateSource = "DETERMINISTIC_V2"
CandidateSourceCognitive SemanticCandidateSource = "COGNITIVE_V1"
CandidateSelected SemanticCandidateSelectionState = "SELECTED"
CandidateObserved SemanticCandidateSelectionState = "OBSERVED"
CandidateRejected SemanticCandidateSelectionState = "REJECTED"
InformationRequestCapabilityInventory InformationRequestKind = "CAPABILITY_INVENTORY"
InformationRequestLocalCapabilityInventory InformationRequestKind = "LOCAL_CAPABILITY_INVENTORY"
InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY"
InformationRequestCognitiveCurrentModel InformationRequestKind = "COGNITIVE_CURRENT_MODEL"
InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS"
InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY"
InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY"
InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE"
InformationRequestResearchFailureExplanation InformationRequestKind = "RESEARCH_FAILURE_EXPLANATION"
ConfigurationCommandSet ConfigurationCommandKind = "SET"
ConfigurationCommandQuery ConfigurationCommandKind = "QUERY"
)
type Conversation struct {
@@ -101,6 +146,18 @@ type OriginalHumanIntent struct {
RecordedAtUnixMillis int64
Text string
}
// ConfigurationCommand is Interaction-owned intent. Configuration remains
// the only owner allowed to materialise a SettingAssignment.
type ConfigurationCommand struct {
Kind ConfigurationCommandKind
PrincipalID string
DefinitionRef string
TypedValue string
TemporaryOverride bool
SessionRef string
SourceRef string
}
type InteractionRelation struct {
ID InteractionRelationID
FromUtteranceID HumanUtteranceID
@@ -168,11 +225,13 @@ type ChatMessageResponse struct {
InterpretationState InterpretationState
NeedsCognition bool
FallbackText string
AssistantName string
ActiveProjectName string
ProjectProvenance situation.ProjectResolutionProvenance
Clarification ClarificationState
WorkPlanned bool
PlanningReply string
ResearchBacked bool
CapabilityStatus capability.CapabilityResolutionStatus
CapabilityKind capability.CapabilityKind
CapabilityHints []capability.CapabilityKind
@@ -183,4 +242,6 @@ type ChatMessageResponse struct {
DelegationID string
DelegationState string
Reminder *continuity.ProspectiveItem
GeneratedImageContentType string
GeneratedImageBase64 string
}
+9
View File
@@ -0,0 +1,9 @@
# SELF contract boundary
SELF owns evidence-backed competence, weakness, dependency, learning, and
self-evaluation records. Raw evidence remains Epistemics-owned; capability
health remains Capability-owned; tasks and development remain Work-owned;
release switching remains Supervisor/runtime-owned.
SELF is observational/evaluative and is not a mandatory request-path
orchestrator or an authority source.
+25
View File
@@ -0,0 +1,25 @@
package self
type BenchmarkCaseRef string
type PersonalBenchmark struct {
ID string
PrincipalID string
ApprovedCaseRefs []BenchmarkCaseRef
GoalProfile []string
PreferenceEvidenceRefs []SemanticReference
RecordedAt int64
}
type BenchmarkEvaluation struct {
CaseRef BenchmarkCaseRef
Path string
QualityMillis int64
ReliabilityMillis int64
LatencyMillis int64
CostMicros int64
CorrectionCount int64
EvidenceRefs []SemanticReference
}
func (e BenchmarkEvaluation) Valid() bool {
return e.CaseRef != "" && len(e.EvidenceRefs) > 0 && e.QualityMillis >= 0 && e.ReliabilityMillis >= 0 && e.CorrectionCount >= 0
}

Some files were not shown because too many files have changed in this diff Show More