Compare commits

..
Author SHA1 Message Date
m 3a8c6d4827 feat(secrets): add protected credential store
CI / test (push) Failing after 37s
2026-08-31 13:17:56 +03:00
m 8b1d75f337 feat(secrets): provision local machine credentials
CI / test (push) Failing after 39s
2026-08-31 13:17:33 +03:00
m cb50b7357b bound local advisory output
CI / test (push) Failing after 39s
2026-08-31 00:05:57 +03:00
m dfbee7568f fix cognitive local model discovery
CI / test (push) Failing after 36s
2026-08-30 23:54:18 +03:00
m f803cb67f0 merge: GEN2-EVOLVE-004 runtime wiring
CI / test (push) Failing after 37s
2026-08-30 22:48:53 +03:00
m 75fa8477d8 feat(core): wire durable self learning evidence 2026-08-30 22:48:46 +03:00
m b4c3479614 merge: GEN2-EVOLVE-004 SELF learning loop
CI / test (push) Failing after 35s
2026-08-30 22:26:18 +03:00
m 82b89a37d3 feat(self): add bounded improvement learning loop 2026-08-30 22:26:09 +03:00
m 4559e0439c merge: GEN2-EVOLVE-004 task packet
CI / test (push) Failing after 35s
2026-08-30 22:03:03 +03:00
m b39c3ed153 chore(gen2): define self improvement loop task 2026-08-30 22:02:57 +03:00
m dd04a67731 merge: GEN2-EVOLVE-003 runtime evidence
CI / test (push) Failing after 33s
2026-08-30 21:58:51 +03:00
m a0bd2a1831 feat(gen2): seed candidate skill from verified delegation 2026-08-30 21:58:49 +03:00
m c39126e0c1 merge: GEN2-EVOLVE-003 experience skill reflex lifecycle
CI / test (push) Failing after 36s
2026-08-30 21:56:51 +03:00
m 8ec7c7419f feat(gen2): add experience skill reflex lifecycle 2026-08-30 21:56:33 +03:00
m a07aba056d merge: GEN2-EVOLVE-002 iterative retrieval
CI / test (push) Failing after 35s
2026-08-30 20:47:05 +03:00
m 901d5e011d feat: add bounded iterative cognitive retrieval 2026-08-30 20:46:52 +03:00
m 74f326f11a chore: start GEN2-EVOLVE-002 2026-08-30 20:38:55 +03:00
m 850fe92ecb merge: GEN2-EVOLVE-001 adaptive retrieval
CI / test (push) Failing after 47s
2026-08-30 20:25:40 +03:00
m 9312441d00 feat: evolve adaptive Gen2 retrieval 2026-08-30 20:25:23 +03:00
m a6273d0134 feat: add scoped adaptive experience foundation 2026-08-30 20:06:58 +03:00
m e648d0423d chore: start GEN2-EVOLVE-001 2026-08-30 20:04:58 +03:00
m 07f0f6e380 merge: GEN2-COGNITIVE-002 core verified
CI / test (push) Failing after 36s
2026-08-30 19:55:11 +03:00
m e1b9c7ca84 docs: record cognitive core verification amendment
CI / test (push) Failing after 33s
2026-08-30 19:54:48 +03:00
m 00af176a20 fix: compile scoped interaction assertions
CI / test (push) Failing after 34s
2026-08-30 19:12:52 +03:00
m ce7000e78e chore: revise cognitive retrieval task scope
CI / test (push) Failing after 44s
2026-08-30 18:47:58 +03:00
m 6204e14177 fix: isolate candidate core cognitive route
CI / test (push) Failing after 36s
2026-08-30 16:02:11 +03:00
m 185317ee6a fix: revalidate stale cognitive binding health
CI / test (push) Failing after 35s
2026-08-30 15:56:34 +03:00
m 2e824b3c1f chore: start cognitive route remediation trace
CI / test (push) Failing after 35s
2026-08-30 15:33:59 +03:00
m f0360a8e13 merge GEN2-DEPLOY-003 isolated release slots
CI / test (push) Failing after 30s
2026-08-30 14:10:34 +03:00
m 5593f2ac2c docs: record isolated release slot drill
CI / test (push) Failing after 41s
2026-08-30 14:10:18 +03:00
m 216d76f1d8 docs: clarify stable updater isolation
CI / test (push) Failing after 28s
2026-08-30 13:59:24 +03:00
m 499b9678e1 test: support isolated canary rollback drill
CI / test (push) Failing after 28s
2026-08-30 13:55:35 +03:00
m 78a39ec011 fix: roll back release sets atomically
CI / test (push) Failing after 29s
2026-08-30 13:55:01 +03:00
m d120cc0958 fix: isolate candidate release slot
CI / test (push) Failing after 29s
2026-08-30 13:51:32 +03:00
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
179 changed files with 8830 additions and 448 deletions
+42 -2
View File
@@ -25,6 +25,15 @@ 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
const compatibleAdvisoryMaxTokens = 32
func NewOpenAICompatibleLocalAdapter(raw string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
return NewOpenAICompatibleLocalAdapterWithAPIKey(raw, "", transport)
}
@@ -58,6 +67,9 @@ type compatibleModelsResponse struct {
ID string `json:"id"`
Created int64 `json:"created"`
OwnedBy string `json:"owned_by"`
ModelKind string `json:"model_kind"`
Capabilities map[string]bool `json:"capabilities"`
Modalities map[string]bool `json:"modalities"`
Meta struct {
Context int64 `json:"n_ctx"`
TrainingContext int64 `json:"n_ctx_train"`
@@ -87,14 +99,36 @@ func (a *OpenAICompatibleLocalAdapter) Discover(ctx context.Context) ([]Discover
if item.ID == "" {
continue
}
// The gateway catalogue may contain image/audio runtimes beside local
// chat models. Discovery is not allowed to qualify those as text
// advisory models merely because they appear in /v1/models.
if !isTextAdvisoryModel(item.ModelKind, item.Capabilities, item.Modalities) {
continue
}
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}
}
func isTextAdvisoryModel(kind string, capabilities, modalities map[string]bool) bool {
if strings.EqualFold(strings.TrimSpace(kind), "image-generation") || strings.EqualFold(strings.TrimSpace(kind), "audio-transcription") || strings.EqualFold(strings.TrimSpace(kind), "text-to-speech") {
return false
}
if chat, known := capabilities["chat"]; known && !chat {
return false
}
if text, known := modalities["text"]; known && !text {
return false
}
return true
}
type compatibleChatRequest struct {
Model string `json:"model"`
Messages []compatibleMessage `json:"messages"`
@@ -125,7 +159,11 @@ func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cogni
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" {
return cognitive.AdvisoryResponse{}, ErrInvalidAdvisory
}
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}}
maxTokens := request.MaxOutputBytes / 4
if maxTokens <= 0 || maxTokens > compatibleAdvisoryMaxTokens {
maxTokens = compatibleAdvisoryMaxTokens
}
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Ты отвечаешь от имени Домового — локального помощника пользователя. Не называй себя именем модели, поставщиком или внутренним runtime. Если спрашивают имя, ответь: «Меня зовут Домовой». Понимай смысл сообщения и помогай довести цель до полезного результата. По умолчанию отвечай кратко, примерно одним-тремя предложениями; подробности давай только по просьбе. Отвечай только некатегоричным текстом совета; не утверждай, что внешнее действие уже выполнено."}, {Role: "user", Content: localAdvisoryMessage(request)}}, Temperature: 0, Stream: false, MaxTokens: maxTokens, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
var result compatibleChatResponse
status, err := a.requestJSON(ctx, http.MethodPost, "/chat/completions", payload, &result)
if err != nil {
@@ -226,6 +264,8 @@ func mapCompatibleStatus(status int) cognitive.FinishReason {
return cognitive.FinishContextTooLarge
case 429:
return cognitive.FinishRateLimited
case 400, 422:
return cognitive.FinishInvalidProviderResponse
default:
return cognitive.FinishProviderUnavailable
}
@@ -18,6 +18,65 @@ 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
}
type mixedModelsRoundTripper struct{}
func (mixedModelsRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"data":[{"id":"image-model","model_kind":"image-generation","capabilities":{"chat":false,"image_generation":true},"modalities":{"text":false,"image_generation":true}},{"id":"text-model","model_kind":"chat","capabilities":{"chat":true},"modalities":{"text":true},"meta":{}}]}`)),
Header: make(http.Header),
Request: request,
}, nil
}
func TestOpenAICompatibleLocalAdapterDoesNotQualifyNonTextModels(t *testing.T) {
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", mixedModelsRoundTripper{})
if err != nil {
t.Fatal(err)
}
models, probe := adapter.Discover(context.Background())
if probe.Health != capability.HealthAvailable || len(models) != 1 || models[0].ProviderModelID != "text-model" {
t.Fatalf("unexpected filtered discovery: probe=%+v models=%+v", probe, models)
}
}
func TestOpenAICompatibleLocalAdapterMapsImmediateBadRequest(t *testing.T) {
if got := mapCompatibleStatus(http.StatusBadRequest); got != cognitive.FinishInvalidProviderResponse {
t.Fatalf("400 finish=%s, want %s", got, cognitive.FinishInvalidProviderResponse)
}
if got := mapCompatibleStatus(http.StatusUnprocessableEntity); got != cognitive.FinishInvalidProviderResponse {
t.Fatalf("422 finish=%s, want %s", got, cognitive.FinishInvalidProviderResponse)
}
}
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 {
@@ -81,6 +140,7 @@ func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(
t.Fatal(err)
}
var payload struct {
MaxTokens int `json:"max_tokens"`
ChatTemplateKwargs struct {
EnableThinking bool `json:"enable_thinking"`
} `json:"chat_template_kwargs"`
@@ -88,6 +148,9 @@ func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(
if err := json.Unmarshal(transport.body, &payload); err != nil {
t.Fatal(err)
}
if payload.MaxTokens != compatibleAdvisoryMaxTokens {
t.Fatalf("max_tokens=%d, want bounded %d", payload.MaxTokens, compatibleAdvisoryMaxTokens)
}
if payload.ChatTemplateKwargs.EnableThinking {
t.Fatal("expected bounded advisory to disable template thinking")
}
+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)
}
}
+64 -12
View File
@@ -2,6 +2,7 @@ package postgres
import (
"context"
"encoding/json"
"errors"
"fmt"
@@ -181,10 +182,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)
}
@@ -309,12 +306,14 @@ func (r *CapabilityRepository) SaveSkillProfile(ctx context.Context, value capab
_, err = tx.Exec(ctx, `INSERT INTO capability.skill_profiles (
skill_profile_id,revision,name,description,applicable_context,implementation_ref,implementation_version,status,
supporting_experience_refs,known_failure_experience_refs,confidence_millis,last_validated_unix_millis,
observed_failure_count,observed_correction_count,recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT DO NOTHING`,
observed_failure_count,observed_correction_count,recorded_at_unix_millis,applicability,dependency_refs,validation_evidence_refs,
independent_evaluation_refs,known_failure_conditions,verified_outcome_count,must_not_worsen_satisfied)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22) ON CONFLICT DO NOTHING`,
value.ID, value.Revision, value.Name, value.Description, value.ApplicableContext, value.ImplementationRef,
value.ImplementationVersion, value.Status, value.SupportingExperienceRefs, value.KnownFailureExperienceRefs,
value.ConfidenceMillis, value.LastValidatedUnixMillis, value.ObservedFailureCount, value.ObservedCorrectionCount,
value.RecordedAtUnixMillis)
value.RecordedAtUnixMillis, mustJSON(value.Applicability), value.DependencyRefs, value.ValidationEvidenceRefs,
value.IndependentEvaluationRefs, value.KnownFailureConditions, value.VerifiedOutcomeCount, value.MustNotWorsenSatisfied)
if err != nil {
return err
}
@@ -333,13 +332,16 @@ func (r *CapabilityRepository) LoadSkillProfile(ctx context.Context, id capabili
err := r.pool.QueryRow(ctx, `SELECT
p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status,
p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis,
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis,p.applicability,p.dependency_refs,
p.validation_evidence_refs,p.independent_evaluation_refs,p.known_failure_conditions,p.verified_outcome_count,p.must_not_worsen_satisfied
FROM capability.skill_profile_heads h
JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision
WHERE p.skill_profile_id=$1`, id).Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext,
&value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs,
&value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis,
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis)
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis, newJSONTarget(&value.Applicability),
&value.DependencyRefs, &value.ValidationEvidenceRefs, &value.IndependentEvaluationRefs, &value.KnownFailureConditions,
&value.VerifiedOutcomeCount, &value.MustNotWorsenSatisfied)
if err != nil {
return capability.SkillProfile{}, fmt.Errorf("load skill profile: %w", err)
}
@@ -353,7 +355,8 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in
rows, err := r.pool.Query(ctx, `SELECT
p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status,
p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis,
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis,p.applicability,p.dependency_refs,
p.validation_evidence_refs,p.independent_evaluation_refs,p.known_failure_conditions,p.verified_outcome_count,p.must_not_worsen_satisfied
FROM capability.skill_profile_heads h
JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision
ORDER BY h.updated_at DESC, p.skill_profile_id LIMIT $1`, limit)
@@ -367,7 +370,9 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in
if err := rows.Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext,
&value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs,
&value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis,
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis); err != nil {
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis, newJSONTarget(&value.Applicability),
&value.DependencyRefs, &value.ValidationEvidenceRefs, &value.IndependentEvaluationRefs, &value.KnownFailureConditions,
&value.VerifiedOutcomeCount, &value.MustNotWorsenSatisfied); err != nil {
return nil, err
}
result = append(result, value)
@@ -375,6 +380,40 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in
return result, rows.Err()
}
func (r *CapabilityRepository) SaveReflexRevision(ctx context.Context, reflex capability.Reflex, value capability.ReflexRevision) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.reflex_revisions (
reflex_id,revision,principal_id,source_skill_id,source_skill_revision,kind,applicability,implementation_ref,implementation_version,artifact_digest,
dependency_refs,validation_evidence_refs,known_failure_experience_refs,shadow_policy,revalidation_policy,last_validated_at,lifecycle,
supersedes_revision,execution_count,failure_count,divergence_count,recorded_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22) ON CONFLICT DO NOTHING`,
value.ReflexID, value.Revision, reflex.PrincipalID, reflex.SourceSkillRef, value.SourceSkillRevision, value.Kind, mustJSON(value.Applicability), value.ImplementationRef, value.ImplementationVersion, value.ArtifactDigest,
value.DependencyRefs, value.ValidationEvidenceRefs, value.KnownFailureExperienceRefs, mustJSON(value.Shadow), mustJSON(value.Revalidation), value.LastValidatedAt, value.Lifecycle,
value.SupersedesRevision, value.ExecutionCount, value.FailureCount, value.DivergenceCount, value.RecordedAt)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO capability.reflex_heads(reflex_id,revision,lifecycle) VALUES($1,$2,$3)
ON CONFLICT(reflex_id) DO UPDATE SET revision=EXCLUDED.revision,lifecycle=EXCLUDED.lifecycle,updated_at=CURRENT_TIMESTAMP
WHERE capability.reflex_heads.revision < EXCLUDED.revision`, value.ReflexID, value.Revision, value.Lifecycle)
return err
}
func mustJSON(value any) []byte { encoded, _ := json.Marshal(value); return encoded }
type jsonTarget struct{ target any }
func newJSONTarget(target any) *jsonTarget { return &jsonTarget{target: target} }
func (v *jsonTarget) Scan(source any) error {
if source == nil {
return nil
}
bytes, ok := source.([]byte)
if !ok {
bytes = []byte(fmt.Sprint(source))
}
return json.Unmarshal(bytes, v.target)
}
func (r *CapabilityRepository) LoadBinding(ctx context.Context, id capability.CapabilityBindingID, revision int64) (capability.Binding, error) {
var binding capability.Binding
err := r.pool.QueryRow(ctx, `SELECT binding_id,capability_id,owner_principal_id,revision,state,https_origin,delivery_path,receipt_path,
@@ -437,6 +476,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 +611,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
}
+62 -9
View File
@@ -6,6 +6,7 @@ import (
"time"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/platform/telemetry"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -36,6 +37,11 @@ func (r *EpistemicsRepository) Ping(ctx context.Context) error {
// 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{},
@@ -43,6 +49,7 @@ func (r *EpistemicsRepository) TruthRuntimeStatus(ctx context.Context) (TruthRun
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 {
@@ -56,18 +63,21 @@ func (r *EpistemicsRepository) TruthRuntimeStatus(ctx context.Context) (TruthRun
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
}
@@ -91,10 +101,10 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
return nil, fmt.Errorf("invalid experience limit")
}
rows, err := r.pool.Query(ctx, `SELECT
experience_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
experience_id,principal_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
verification_evidence,confidence_millis,recorded_at_unix_millis
verification_evidence,confidence_millis,recorded_at_unix_millis,context_scope_ref,problem_statement,applicability_conditions,why_succeeded_or_failed
FROM epistemics.experience_records ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $1`, limit)
if err != nil {
return nil, err
@@ -103,10 +113,44 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
var result []epistemics.ExperienceRecord
for rows.Next() {
var value epistemics.ExperienceRecord
if err := rows.Scan(&value.ID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
if err := rows.Scan(&value.ID, &value.PrincipalID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis); err != nil {
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis, &value.ContextScopeRef, &value.ProblemStatement, &value.ApplicabilityConditions, &value.WhySucceededOrFailed); err != nil {
return nil, err
}
result = append(result, value)
}
return result, rows.Err()
}
// RecentExperienceRecordsForPrincipalAndScope is an owner-local Epistemics
// read. It intentionally applies Principal and ContextScope in SQL before any
// Core-side relevance ranking, so another realm's experience never becomes a
// retrieval candidate.
func (r *EpistemicsRepository) RecentExperienceRecordsForPrincipalAndScope(ctx context.Context, principalID, scopeRef string, limit int) ([]epistemics.ExperienceRecord, error) {
if principalID == "" || limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid scoped experience query")
}
rows, err := r.pool.Query(ctx, `SELECT
experience_id,principal_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
verification_evidence,confidence_millis,recorded_at_unix_millis,context_scope_ref,problem_statement,applicability_conditions,why_succeeded_or_failed
FROM epistemics.experience_records
WHERE principal_id=$1 AND ($2='' OR context_scope_ref=$2)
ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $3`, principalID, scopeRef, limit)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]epistemics.ExperienceRecord, 0)
for rows.Next() {
var value epistemics.ExperienceRecord
if err := rows.Scan(&value.ID, &value.PrincipalID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis, &value.ContextScopeRef, &value.ProblemStatement, &value.ApplicabilityConditions, &value.WhySucceededOrFailed); err != nil {
return nil, err
}
result = append(result, value)
@@ -195,15 +239,15 @@ func (r *EpistemicsRepository) WaitForReflectionCandidate(ctx context.Context, i
func (r *EpistemicsRepository) SaveExperienceRecord(ctx context.Context, value epistemics.ExperienceRecord) error {
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.experience_records (
experience_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
experience_id, principal_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
capability_implementation_ref, capability_usage_observation_ref, capability_gap_ref, skill_ref, expectation_id,
expected_outcome, actual_outcome, outcome_ref, outcome_detail, human_response_ref, correction_utterance_id,
verification_evidence, confidence_millis, recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21) ON CONFLICT DO NOTHING`,
value.ID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
verification_evidence, confidence_millis, recorded_at_unix_millis, context_scope_ref, problem_statement, applicability_conditions, why_succeeded_or_failed)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26) ON CONFLICT DO NOTHING`,
value.ID, value.PrincipalID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
value.CapabilityImplementationRef, value.CapabilityUsageObservationRef, value.CapabilityGapRef, value.SkillRef, value.ExpectationID,
value.ExpectedOutcome, value.ActualOutcome, value.OutcomeRef, value.OutcomeDetail, value.HumanResponseRef, value.CorrectionUtteranceID,
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis)
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis, value.ContextScopeRef, value.ProblemStatement, value.ApplicabilityConditions, value.WhySucceededOrFailed)
return err
}
@@ -218,7 +262,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 (
@@ -232,5 +282,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
}
@@ -100,6 +100,22 @@ func (r *InteractionRepository) Utterances(ctx context.Context, id interaction.C
return out, rows.Err()
}
// UtterancesWithProjectReferences returns canonical utterances together with
// their already-persisted Interaction-owned project references. The caller
// uses the pair only as a read projection; this method does not infer or write
// scope membership.
func (r *InteractionRepository) UtterancesWithProjectReferences(ctx context.Context, id interaction.ConversationID) ([]interaction.HumanUtterance, []interaction.ProjectReference, error) {
utterances, err := r.Utterances(ctx, id)
if err != nil {
return nil, nil, err
}
references, err := r.projectReferencesForUtterances(ctx, utterances)
if err != nil {
return nil, nil, err
}
return utterances, references, nil
}
// 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) {
@@ -118,3 +134,52 @@ func (r *InteractionRepository) RecentUtterancesForPrincipal(ctx context.Context
}
return out, rows.Err()
}
// RecentUtterancesForPrincipalWithProjectReferences keeps a new conversation
// within the same durable Interaction read boundary as its scoped context.
func (r *InteractionRepository) RecentUtterancesForPrincipalWithProjectReferences(ctx context.Context, principalID identity.PrincipalID, limit int) ([]interaction.HumanUtterance, []interaction.ProjectReference, error) {
utterances, err := r.RecentUtterancesForPrincipal(ctx, principalID, limit)
if err != nil {
return nil, nil, err
}
references, err := r.projectReferencesForUtterances(ctx, utterances)
if err != nil {
return nil, nil, err
}
return utterances, references, nil
}
func (r *InteractionRepository) projectReferencesForUtterances(ctx context.Context, utterances []interaction.HumanUtterance) ([]interaction.ProjectReference, error) {
if len(utterances) == 0 {
return nil, nil
}
ids := make([]string, 0, len(utterances))
seen := make(map[interaction.HumanUtteranceID]struct{}, len(utterances))
for _, utterance := range utterances {
if utterance.ID == "" {
continue
}
if _, exists := seen[utterance.ID]; exists {
continue
}
seen[utterance.ID] = struct{}{}
ids = append(ids, string(utterance.ID))
}
if len(ids) == 0 {
return nil, nil
}
rows, err := r.pool.Query(ctx, `SELECT project_reference_id,utterance_id,project_id,project_display_name,role,provenance,confidence_millis,recorded_at_unix_millis FROM interaction.project_references WHERE utterance_id = ANY($1::text[]) ORDER BY recorded_at_unix_millis,project_reference_id`, ids)
if err != nil {
return nil, err
}
defer rows.Close()
refs := make([]interaction.ProjectReference, 0)
for rows.Next() {
var ref interaction.ProjectReference
if err := rows.Scan(&ref.ID, &ref.UtteranceID, &ref.ProjectID, &ref.ProjectDisplayName, &ref.Role, &ref.Provenance, &ref.ConfidenceMillis, &ref.RecordedAtUnixMillis); err != nil {
return nil, err
}
refs = append(refs, ref)
}
return refs, rows.Err()
}
+148
View File
@@ -0,0 +1,148 @@
package postgres
import (
"context"
"encoding/json"
"fmt"
selfcontract "git.cin.su/m/Assistant/contracts/self"
"github.com/jackc/pgx/v5/pgxpool"
)
// SelfRepository persists only SELF-owned derived assessments and ledger
// evidence. It never evaluates, promotes or changes Capability state.
type SelfRepository struct{ pool *pgxpool.Pool }
func OpenSelf(ctx context.Context, config ContinuityConfig) (*SelfRepository, error) {
pool, err := openPool(ctx, config)
if err != nil {
return nil, err
}
return &SelfRepository{pool: pool}, nil
}
func (r *SelfRepository) Close() { r.pool.Close() }
func (r *SelfRepository) SaveImprovementNeed(ctx context.Context, value selfcontract.ImprovementNeed) error {
subject, err := json.Marshal(value.SubjectRef)
if err != nil {
return err
}
evidence, err := json.Marshal(value.EvidenceRefs)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.improvement_needs(improvement_need_id,principal_id,subject_ref,problem_summary,evidence_refs,expected_benefit,priority,state,created_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, subject, value.ProblemSummary, evidence, value.ExpectedBenefit, value.Priority, value.State, value.CreatedAt)
return err
}
func (r *SelfRepository) SaveLearningLedger(ctx context.Context, value selfcontract.LearningLedgerEntry) error {
evidence, err := json.Marshal(value.EvidenceRefs)
if err != nil {
return err
}
improvement, err := json.Marshal(value.ImprovementNeedRef)
if err != nil {
return err
}
proposed, err := json.Marshal(value.ProposedChangeRef)
if err != nil {
return err
}
evaluations, err := json.Marshal(value.ActualEvaluationRefs)
if err != nil {
return err
}
gate, err := json.Marshal(value.DevelopmentGateRef)
if err != nil {
return err
}
before, err := json.Marshal(value.BeforeVersionRefs)
if err != nil {
return err
}
after, err := json.Marshal(value.AfterVersionRefs)
if err != nil {
return err
}
sideEffects, err := json.Marshal(value.SideEffects)
if err != nil {
return err
}
rollback, err := json.Marshal(value.RollbackRef)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.learning_ledger(entry_id,principal_id,improvement_need_ref,observed_problem,evidence_refs,diagnosis,hypothesis,proposed_change_ref,expected_improvement,evaluation_refs,development_gate_ref,before_version_refs,after_version_refs,decision,side_effects,rollback_ref,recorded_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, improvement, value.ObservedProblem, evidence, value.Diagnosis, value.Hypothesis, proposed, value.ExpectedImprovement, evaluations, gate, before, after, value.Decision, sideEffects, rollback, value.RecordedAt)
return err
}
func (r *SelfRepository) SaveLearningSignal(ctx context.Context, value selfcontract.LearningSignal) error {
if err := value.Validate(); err != nil {
return err
}
subject, err := json.Marshal(value.SubjectRef)
if err != nil {
return err
}
evidence, err := json.Marshal(value.EvidenceRefs)
if err != nil {
return err
}
deps, err := json.Marshal(value.DependencyRefs)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.learning_signals(signal_id,principal_id,kind,task_pattern,scope_key,subject_ref,evidence_refs,expected_outcome,actual_outcome,dependency_refs,environment_version,observed_at_unix_millis,severity,user_impact,correction_count,decision_regret)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, value.Kind, value.TaskPattern, value.ScopeKey, subject, evidence, value.ExpectedOutcome, value.ActualOutcome, deps, value.EnvironmentVersion, value.ObservedAt, value.Severity, value.UserImpact, value.CorrectionCount, value.DecisionRegret)
return err
}
func (r *SelfRepository) SaveWeaknessPattern(ctx context.Context, value selfcontract.WeaknessPattern) error {
evidence, err := json.Marshal(value.DetectionEvidenceRefs)
if err != nil {
return err
}
deps, err := json.Marshal(value.DependencyRefs)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.weakness_patterns(assessment_id,principal_id,revision,pattern_key,task_pattern,scope_key,environment_version,dependency_refs,failure_mode,detection_evidence_refs,frequency,last_observed_at_unix_millis,severity,affected_component,affected_strategy,confidence_millis,epistemic_status,state,recorded_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, value.Revision, value.PatternKey, value.TaskPattern, value.ScopeKey, value.EnvironmentVersion, deps, value.FailureMode, evidence, value.Frequency, value.LastObservedAt, value.Severity, value.AffectedComponent, value.AffectedStrategy, value.ConfidenceMillis, value.EpistemicStatus, value.State, value.RecordedAt)
return err
}
func (r *SelfRepository) SaveImprovementCandidate(ctx context.Context, value selfcontract.ImprovementCandidate) error {
if err := value.Validate(); err != nil {
return err
}
need, err := json.Marshal(value.NeedRef)
if err != nil {
return err
}
must, err := json.Marshal(value.MustNotWorsen)
if err != nil {
return err
}
evidence, err := json.Marshal(value.EvidenceRefs)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.improvement_candidates(candidate_id,principal_id,need_ref,hypothesis,expected_effect,evaluation_plan,must_not_worsen,rollback_plan,resource_cost,creator,independent_evaluator,state,evidence_refs,created_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, need, value.Hypothesis, value.ExpectedEffect, value.EvaluationPlan, must, value.RollbackPlan, value.ResourceCost, value.Creator, value.IndependentEvaluator, value.State, evidence, value.CreatedAt)
return err
}
func (r *SelfRepository) SaveBenchmarkCase(ctx context.Context, value selfcontract.BenchmarkCase) error {
if !value.Valid() {
return fmt.Errorf("benchmark case is incomplete")
}
evidence, err := json.Marshal(value.EvidenceRefs)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.personal_benchmark_cases(case_id,principal_id,task_class,evidence_refs,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, value.TaskClass, evidence, value.RecordedAt)
return err
}
+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 {
+16 -3
View File
@@ -16,15 +16,17 @@ func (p *admissionTestProvider) Advise(context.Context, cognitive.AdvisoryReques
}
type admissionTestClient struct {
decision capability.AdmissionDecision
releases int
decision capability.AdmissionDecision
releases int
releaseContextErr error
}
func (c *admissionTestClient) Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error) {
return c.decision, capability.AdmissionLease{ID: "lease-1", PolicyID: "policy-1"}, nil
}
func (c *admissionTestClient) Release(context.Context, capability.AdmissionLease) error {
func (c *admissionTestClient) Release(ctx context.Context, _ capability.AdmissionLease) error {
c.releases++
c.releaseContextErr = ctx.Err()
return nil
}
@@ -47,3 +49,14 @@ func TestInvokeAdmittedReleasesDurableLeaseAfterProvider(t *testing.T) {
t.Fatalf("admitted lease was not released: response=%#v err=%v calls=%d releases=%d", response, err, provider.calls, client.releases)
}
}
func TestInvokeAdmittedReleasesLeaseAfterCallerCancellation(t *testing.T) {
provider := &admissionTestProvider{}
client := &admissionTestClient{decision: capability.AdmissionDecision{State: capability.AdmissionAdmitted, Reason: capability.AdmissionReasonNone, PolicyID: "policy-1"}}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _ = (&cognitiveGateway{admission: client}).invokeAdmitted(ctx, "request-1", provider, cognitive.AdvisoryRequest{Message: "hello"}, 100)
if client.releases != 1 || client.releaseContextErr != nil {
t.Fatalf("cancelled caller leaked durable lease: releases=%d release_context_err=%v", client.releases, client.releaseContextErr)
}
}
+16 -11
View File
@@ -1,7 +1,9 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"sort"
"time"
@@ -22,17 +24,7 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
s.statusHandler(w, r)
return
}
registration, ok := s.providers[r.PathValue("provider")]
if !ok || registration.Discovery == nil {
http.Error(w, "provider discovery not found", http.StatusNotFound)
return
}
discovered, probe := registration.Discovery.Discover(r.Context())
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, AuthMode: registration.AuthMode, BillingMode: registration.BillingMode, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: registration.DataPolicy, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
for _, model := range discovered {
snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, MaxContextBytes: model.MaxContextBytes, Modalities: model.Modalities, Capabilities: model.Capabilities, Installed: capability.OperationalYes, Loaded: capability.OperationalNotObserved, Invokable: capability.OperationalNotObserved})
}
result, err := s.resolver.RegisterDiscovery(r.Context(), snapshot)
result, err := s.register(r.Context(), r.PathValue("provider"))
if err != nil {
http.Error(w, "provider discovery registration unavailable", http.StatusBadGateway)
return
@@ -41,6 +33,19 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(result)
}
func (s discoveryService) register(ctx context.Context, provider string) (capability.CognitiveDiscoveryRegistration, error) {
registration, ok := s.providers[provider]
if !ok || registration.Discovery == nil {
return capability.CognitiveDiscoveryRegistration{}, fmt.Errorf("provider discovery not found")
}
discovered, probe := registration.Discovery.Discover(ctx)
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, AuthMode: registration.AuthMode, BillingMode: registration.BillingMode, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: registration.DataPolicy, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
for _, model := range discovered {
snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, MaxContextBytes: model.MaxContextBytes, Modalities: model.Modalities, Capabilities: model.Capabilities, Installed: capability.OperationalYes, Loaded: capability.OperationalNotObserved, Invokable: capability.OperationalNotObserved})
}
return s.resolver.RegisterDiscovery(ctx, snapshot)
}
type discoveryStatus struct {
Provider string `json:"provider"`
ProviderKind capability.ProviderKind `json:"provider_kind"`
+6 -1
View File
@@ -20,6 +20,7 @@ type fleetEndpoint struct {
URL string
Headers map[string]string
Client *http.Client
Timeout time.Duration
}
type fleetStatusResponse struct {
@@ -63,7 +64,11 @@ func probeFleetEndpoint(parent context.Context, fallbackClient *http.Client, end
if strings.TrimSpace(endpoint.URL) == "" {
return member
}
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
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 {
+18
View File
@@ -68,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())
}
}
+82 -4
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -20,6 +21,8 @@ import (
const creatorAdvisoryCapabilityID capability.CapabilityID = "creator-advisory"
var errNoInvocableCognitiveBinding = errors.New("no invocable cognitive binding")
type bindingResolver interface {
Resolve(context.Context, capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error)
Observe(context.Context, capability.CognitiveUsageObservation) error
@@ -56,6 +59,7 @@ type cognitiveGateway struct {
allowCanary bool
canaryMaxOutputBytes int
admission admissionClient
revalidatePrimary func(context.Context) error
}
func (g cognitiveGateway) policyHandler(w http.ResponseWriter, r *http.Request) {
@@ -89,7 +93,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
maxOutputBytes = g.canaryMaxOutputBytes
}
requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes), Objective: selectionObjectiveForMeaning(request.Meaning, request.Message)}
selected, err := g.resolver.Resolve(r.Context(), requirement)
selected, err := g.resolveWithRevalidation(r.Context(), requirement)
if err != nil {
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
return
@@ -131,7 +135,51 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
return
}
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage})
writeGatewayResponse(w, gatewayAdvisoryResponse(request, response))
}
const iterativeRetrievalMarker = "<domovoy-retrieval>"
// gatewayAdvisoryResponse is the only model-facing extraction point. The
// model may express a bounded retrieval intent, but it cannot provide scope,
// identity, policy or storage details; Core supplies those on continuation.
func gatewayAdvisoryResponse(request cognitive.GatewayAdvisoryRequest, response cognitive.AdvisoryResponse) cognitive.GatewayAdvisoryResponse {
result := cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}
if !request.AllowIterativeRetrieval || response.FinishReason != cognitive.FinishCompleted {
return result
}
intent, text, ok := parseIterativeRetrievalIntent(response.AdvisoryText)
if !ok {
return result
}
result.AdvisoryText = text
result.IterativeRetrieval = &intent
return result
}
func parseIterativeRetrievalIntent(text string) (cognitive.IterativeRetrievalRequest, string, bool) {
start := strings.LastIndex(text, iterativeRetrievalMarker)
if start < 0 {
return cognitive.IterativeRetrievalRequest{}, text, false
}
payload := strings.TrimSpace(text[start+len(iterativeRetrievalMarker):])
var intent cognitive.IterativeRetrievalRequest
if err := json.Unmarshal([]byte(payload), &intent); err != nil || !validIterativeRetrievalIntent(intent) {
return cognitive.IterativeRetrievalRequest{}, text, false
}
return intent, strings.TrimSpace(text[:start]), true
}
func validIterativeRetrievalIntent(intent cognitive.IterativeRetrievalRequest) bool {
if strings.TrimSpace(intent.Query) == "" || strings.TrimSpace(intent.Reason) == "" || len([]rune(intent.Query)) > 512 || len([]rune(intent.Reason)) > 512 {
return false
}
switch intent.Lane {
case cognitive.IterativeRetrievalCurrentFact, cognitive.IterativeRetrievalPositiveExperience, cognitive.IterativeRetrievalNegativeExperience, cognitive.IterativeRetrievalContradiction, cognitive.IterativeRetrievalHistoricalAnalogy:
return true
default:
return false
}
}
func (g cognitiveGateway) invokeAdmitted(ctx context.Context, leaseID string, provider advisoryProvider, invocation cognitive.AdvisoryRequest, maxOutputBytes int) (cognitive.AdvisoryResponse, error) {
@@ -145,11 +193,33 @@ func (g cognitiveGateway) invokeAdmitted(ctx context.Context, leaseID string, pr
return cognitive.AdvisoryResponse{Version: invocation.Version, RequestID: invocation.RequestID, ProviderBindingID: invocation.ProviderBindingID, ProviderBindingRevision: invocation.ProviderBindingRevision, FinishReason: cognitive.FinishRateLimited}, nil
}
lease = admittedLease
defer func() { _ = g.admission.Release(ctx, lease) }()
defer func() {
releaseCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer cancel()
_ = g.admission.Release(releaseCtx, lease)
}()
}
return provider.Advise(ctx, invocation)
}
// resolveWithRevalidation makes one bounded recovery attempt when Capability
// has an active/validated binding but its durable health projection rejected it
// as non-invocable. Discovery remains the canonical Capability write path; the
// retry observes the configured primary local provider and re-runs selection
// once. Provider invocation, admission, and fallback remain governed elsewhere.
func (g cognitiveGateway) resolveWithRevalidation(ctx context.Context, requirement capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
selected, err := g.resolver.Resolve(ctx, requirement)
if err == nil || !errors.Is(err, errNoInvocableCognitiveBinding) || g.revalidatePrimary == nil {
return selected, err
}
revalidationCtx, cancel := context.WithTimeout(ctx, 45*time.Second)
defer cancel()
if revalidationErr := g.revalidatePrimary(revalidationCtx); revalidationErr != nil {
return selected, err
}
return g.resolver.Resolve(ctx, requirement)
}
func selectionObjectiveForMessage(message string) capability.SelectionObjective {
lower := strings.ToLower(message)
if strings.Contains(lower, "дешев") || strings.Contains(lower, "бесплат") || strings.Contains(lower, "эконом") || strings.Contains(lower, "cost") || strings.Contains(lower, "cheap") {
@@ -374,6 +444,10 @@ type httpBindingResolver struct {
client *http.Client
}
type httpStatusError struct{ status int }
func (e httpStatusError) Error() string { return fmt.Sprintf("registry status %d", e.status) }
func newHTTPBindingResolver(raw string, client *http.Client) (*httpBindingResolver, error) {
base, err := url.Parse(raw)
if err != nil || base.Scheme == "" || base.Host == "" {
@@ -385,6 +459,10 @@ func newHTTPBindingResolver(raw string, client *http.Client) (*httpBindingResolv
func (r *httpBindingResolver) Resolve(ctx context.Context, value capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
var result capability.ResolvedCognitiveBinding
err := r.post(ctx, "/v1/cognitive-bindings/resolve", value, &result)
var statusErr httpStatusError
if errors.As(err, &statusErr) && statusErr.status == http.StatusNotFound {
return result, errNoInvocableCognitiveBinding
}
return result, err
}
@@ -437,7 +515,7 @@ func (r *httpBindingResolver) post(ctx context.Context, path string, value, dest
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096))
return fmt.Errorf("registry status %d", response.StatusCode)
return httpStatusError{status: response.StatusCode}
}
if destination != nil {
return json.NewDecoder(io.LimitReader(response.Body, 64<<10)).Decode(destination)
@@ -22,6 +22,26 @@ type registryStub struct {
observation *capability.CognitiveUsageObservation
}
type revalidatingRegistry struct {
selected capability.ResolvedCognitiveBinding
calls int
alwaysNo bool
}
func (s *revalidatingRegistry) Resolve(context.Context, capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
s.calls++
if s.alwaysNo || s.calls == 1 {
return capability.ResolvedCognitiveBinding{}, errNoInvocableCognitiveBinding
}
return s.selected, nil
}
func (*revalidatingRegistry) Observe(context.Context, capability.CognitiveUsageObservation) error {
return nil
}
func (*revalidatingRegistry) RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) {
return capability.CognitiveDiscoveryRegistration{}, nil
}
type failoverRegistry struct {
primary, fallback capability.ResolvedCognitiveBinding
observations []capability.CognitiveUsageObservation
@@ -88,6 +108,73 @@ func TestActiveRegistryBindingReachesProviderAdapter(t *testing.T) {
}
}
func TestGatewayExtractsOnlyBoundedTypedRetrievalIntent(t *testing.T) {
request := cognitive.GatewayAdvisoryRequest{AllowIterativeRetrieval: true}
response := cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request", FinishReason: cognitive.FinishCompleted, AdvisoryText: "<domovoy-retrieval>{\"Query\":\"sync catalog\",\"SemanticSubject\":\"catalog sync\",\"Lane\":\"NEGATIVE_EXPERIENCE\",\"Reason\":\"need known failure\"}"}
got := gatewayAdvisoryResponse(request, response)
if got.IterativeRetrieval == nil || got.IterativeRetrieval.Lane != cognitive.IterativeRetrievalNegativeExperience || got.AdvisoryText != "" {
t.Fatalf("response=%+v", got)
}
invalid := response
invalid.AdvisoryText = "<domovoy-retrieval>{\"Query\":\"all memory\",\"Lane\":\"UNRESTRICTED\",\"Reason\":\"everything\"}"
if got := gatewayAdvisoryResponse(request, invalid); got.IterativeRetrieval != nil {
t.Fatalf("invalid intent accepted: %+v", got)
}
}
func TestStaleUnavailableBindingRevalidatesOnceBeforeInvocation(t *testing.T) {
selected := gatewaySelection()
selected.Binding.DataPolicy = capability.DataPolicyLocalOnly
registry := &revalidatingRegistry{selected: selected}
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "ok", FinishReason: cognitive.FinishCompleted}}
revalidations := 0
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"provider-kind-1": {{EndpointRef: "endpoint-1", CredentialRef: "credential-1", Provider: provider}}}, now: time.Now, revalidatePrimary: func(context.Context) error { revalidations++; return nil }}
response := httptest.NewRecorder()
gateway.advisoryHandler(response, httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())))
if response.Code != http.StatusOK || !provider.called || revalidations != 1 || registry.calls != 2 {
t.Fatalf("status=%d provider_called=%t revalidations=%d resolves=%d", response.Code, provider.called, revalidations, registry.calls)
}
}
func TestRevalidationFailureDoesNotMakeBindingInvocable(t *testing.T) {
registry := &registryStub{err: errNoInvocableCognitiveBinding}
provider := &stubAdvisoryProvider{}
revalidations := 0
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"provider-kind-1": {{Provider: provider}}}, now: time.Now, revalidatePrimary: func(context.Context) error { revalidations++; return errors.New("provider unavailable") }}
gateway.advisoryHandler(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())))
if provider.called || revalidations != 1 {
t.Fatalf("failed provider revalidation was treated as invocable: provider_called=%t revalidations=%d", provider.called, revalidations)
}
}
func TestRevalidationIsNotRetriedInALoop(t *testing.T) {
registry := &revalidatingRegistry{alwaysNo: true}
revalidations := 0
gateway := cognitiveGateway{resolver: registry, now: time.Now, revalidatePrimary: func(context.Context) error { revalidations++; return nil }}
gateway.advisoryHandler(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())))
if revalidations != 1 || registry.calls != 2 {
t.Fatalf("revalidation loop: revalidations=%d resolves=%d", revalidations, registry.calls)
}
}
func TestHTTPResolverMarksNoInvocableBindingForSingleRevalidation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/cognitive-bindings/resolve" {
t.Fatalf("path=%s", r.URL.Path)
}
http.Error(w, "no invocable cognitive binding", http.StatusNotFound)
}))
defer server.Close()
resolver, err := newHTTPBindingResolver(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
_, err = resolver.Resolve(context.Background(), capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: "principal-1", MaxOutputBytes: 1})
if !errors.Is(err, errNoInvocableCognitiveBinding) {
t.Fatalf("resolve error=%v, want no-invocable sentinel", err)
}
}
func TestProviderUsesStableInstanceWhenCredentialMetadataIsStale(t *testing.T) {
provider := &stubAdvisoryProvider{}
gateway := cognitiveGateway{providers: map[capability.ProviderKind][]providerRegistration{
+21 -8
View File
@@ -71,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)
@@ -113,11 +113,16 @@ func main() {
kind := capability.ProviderKind(envOr("OPENAI_PROVIDER_KIND", "openai-responses-v1"))
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), AuthMode: capability.AuthKey, BillingMode: capability.BillingPerInput, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: openAIProvider})
}
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096, admission: newHTTPAdmissionClient(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 5 * time.Second})}
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096, admission: newHTTPAdmissionClient(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 5 * time.Second}), revalidatePrimary: func(ctx context.Context) error {
_, err := discovery.register(ctx, "medium")
return err
}}
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)))
@@ -131,14 +136,21 @@ func main() {
// 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)
// 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{
@@ -157,8 +169,8 @@ func main() {
}
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 {
@@ -191,7 +203,6 @@ func main() {
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}))
mux.HandleFunc("GET /v1/provider-discovery/status", discovery.statusHandler)
@@ -217,7 +228,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()
+1 -1
View File
@@ -100,7 +100,7 @@ func (g cognitiveGateway) executeTaskEvaluationCases(ctx context.Context, reques
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}
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}
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
@@ -57,6 +58,30 @@ 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
+211 -24
View File
@@ -16,8 +16,10 @@ import (
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
"git.cin.su/m/Assistant/platform/ids"
)
@@ -28,6 +30,154 @@ type cognitiveInterpretationClient struct {
media *http.Client
}
// compileInteractionContext is the Core-side handoff from the existing
// Interaction retrieval projection to the existing Context Compiler. The
// compiler receives only already-authorized, typed fragments and applies its
// scope and byte-budget guards before the cognitive gateway sees them.
func compileInteractionContext(projection interactiondomain.ContextProjection, focusProjectID string) (string, error) {
return compileInteractionContextWithExperience(projection, focusProjectID, nil, "")
}
func compileInteractionContextWithExperience(projection interactiondomain.ContextProjection, focusProjectID string, experiences []epistemics.ExperienceRecord, query string) (string, error) {
if len(projection.Fragments) == 0 && len(experiences) == 0 {
return projection.Text, nil
}
candidates := interactionContextCandidates(projection, focusProjectID, experiences, query)
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{Candidates: candidates, DataPolicy: capability.DataPolicyLocalOnly, MaxBytes: 2048, ContextScopeRef: focusProjectID, FreshnessRequirement: "conversation-current", RiskClass: "low"})
if err != nil {
return "", err
}
parts := make([]string, 0, len(bundle.Excerpts))
for _, excerpt := range bundle.Excerpts {
parts = append(parts, excerpt.Text)
}
return strings.Join(parts, "\n"), nil
}
func interactionContextCandidates(projection interactiondomain.ContextProjection, focusProjectID string, experiences []epistemics.ExperienceRecord, query string) []cognitive.ContextCandidate {
candidates := make([]cognitive.ContextCandidate, 0, len(projection.Fragments)+len(experiences))
for _, fragment := range projection.Fragments {
candidates = append(candidates, cognitive.ContextCandidate{
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
Relevance: fragment.Relevance,
Lane: "current-fact",
ContextScopeRef: fragment.ScopeRef,
FreshnessState: "CURRENT",
})
}
if needsHistoricalAnalogy(query) {
for _, fragment := range projection.Historical {
candidates = append(candidates, cognitive.ContextCandidate{
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:historical:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
Relevance: fragment.Relevance - 1, Lane: "historical-analogy", ContextScopeRef: fragment.ScopeRef, FreshnessState: "HISTORICAL_ONLY",
})
}
}
retrieved := epistemicsdomain.RetrieveExperience(experiences, "", focusProjectID, query, 4)
for _, record := range retrieved.Positive {
candidates = append(candidates, experienceContextCandidate(record, "positive-experience", false))
}
for _, record := range retrieved.Negative {
candidates = append(candidates, experienceContextCandidate(record, "negative-experience", true))
}
for _, record := range retrieved.Contradictions {
candidates = append(candidates, experienceContextCandidate(record, "contradiction", true))
}
return candidates
}
func needsHistoricalAnalogy(query string) bool {
for _, marker := range []string{"истор", "раньше", "предыдущ", "старое", "аналог"} {
if strings.Contains(strings.ToLower(query), marker) {
return true
}
}
return false
}
func experienceContextCandidate(record epistemics.ExperienceRecord, lane string, negative bool) cognitive.ContextCandidate {
text := strings.TrimSpace(record.ProblemStatement + ": " + record.OutcomeDetail)
if detail := strings.TrimSpace(record.WhySucceededOrFailed); detail != "" {
text += " (" + detail + ")"
}
if len(record.ApplicabilityConditions) > 0 {
text += " [conditions: " + strings.Join(record.ApplicabilityConditions, ", ") + "]"
}
return cognitive.ContextCandidate{Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("experience:" + string(record.ID)), SourceKind: "epistemics-experience", SourceID: "epistemics:experience-records", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: text}, Relevance: 1, Lane: lane, ContextScopeRef: record.ContextScopeRef, FreshnessState: "CURRENT", Negative: negative}
}
const (
iterativeRetrievalMaxRounds = 1
iterativeRetrievalMaxFragments = 2
iterativeRetrievalMaxAddedBytes = 1024
iterativeRetrievalMaxLatency = 1500 * time.Millisecond
)
func newIterativeContextRetriever(history []interaction.HumanUtterance, references []interaction.ProjectReference, principalID, focusProjectID string, experiences []epistemics.ExperienceRecord) iterativeContextRetriever {
seen := map[string]struct{}{}
return func(ctx context.Context, request cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse {
started := time.Now()
if request.Pass < 0 || request.Pass >= iterativeRetrievalMaxRounds || request.RemainingBytes <= 0 || request.RemainingBytes > iterativeRetrievalMaxAddedBytes {
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "budget"}
}
projection := interactiondomain.BuildScopedGen2ContextForQuery(history, references, principalID, focusProjectID, request.Query)
candidates := interactionContextCandidates(projection, focusProjectID, experiences, request.Query)
filtered := make([]cognitive.ContextCandidate, 0, len(candidates))
for _, candidate := range candidates {
if !iterativeLaneMatches(request.Lane, candidate.Lane) {
continue
}
key := string(request.Lane) + ":" + string(candidate.Excerpt.ID)
if _, duplicate := seen[key]; duplicate {
continue
}
filtered = append(filtered, candidate)
}
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{Candidates: filtered, DataPolicy: capability.DataPolicyLocalOnly, MaxBytes: request.RemainingBytes, ContextScopeRef: focusProjectID, TaskRef: "chat", FocusFrameRef: focusProjectID, FreshnessRequirement: "conversation-current"})
if err != nil || len(bundle.Excerpts) == 0 {
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "no_additional_evidence"}
}
out := make([]cognitive.ContextCandidate, 0, iterativeRetrievalMaxFragments)
for _, excerpt := range bundle.Excerpts {
if len(out) == iterativeRetrievalMaxFragments || time.Since(started) > iterativeRetrievalMaxLatency {
break
}
seen[string(request.Lane)+":"+string(excerpt.ID)] = struct{}{}
for _, candidate := range filtered {
if candidate.Excerpt.ID == excerpt.ID {
out = append(out, candidate)
break
}
}
}
if len(out) == 0 {
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "latency_budget"}
}
addedBytes := 0
for _, candidate := range out {
addedBytes += len([]byte(candidate.Excerpt.Text))
}
return cognitive.IterativeRetrievalResponse{Candidates: out, Exhausted: true, Reason: "completed", AddedBytes: addedBytes}
}
}
func iterativeLaneMatches(request cognitive.IterativeRetrievalLane, actual string) bool {
return (request == cognitive.IterativeRetrievalCurrentFact && actual == "current-fact") ||
(request == cognitive.IterativeRetrievalPositiveExperience && actual == "positive-experience") ||
(request == cognitive.IterativeRetrievalNegativeExperience && actual == "negative-experience") ||
(request == cognitive.IterativeRetrievalContradiction && actual == "contradiction") ||
(request == cognitive.IterativeRetrievalHistoricalAnalogy && actual == "historical-analogy")
}
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.
@@ -47,7 +197,7 @@ func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, sessi
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: 2048, DataPolicy: capability.DataPolicyLocalOnly,
Context: context, Locale: "ru", MaxOutputBytes: responseReviewMaxOutputBytes, DataPolicy: capability.DataPolicyLocalOnly,
Meaning: cognitive.TaskMeaning{
SpeechAct: "RESPONSE_REVIEW",
CapabilityHints: meaning.CapabilityHints,
@@ -86,43 +236,53 @@ func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, sessi
// 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.
type iterativeContextRetriever func(context.Context, cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse
func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
return c.AnswerStableKnowledgeWithContinuation(ctx, session, userMessage, personalMemory, meaning, nil)
}
// AnswerStableKnowledgeWithContinuation permits one model-requested, Core-run
// retrieval round. The callback receives only a typed intent and cannot alter
// identity, scope or provider selection.
func (c cognitiveInterpretationClient) AnswerStableKnowledgeWithContinuation(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning, retrieve iterativeContextRetriever) (string, error) {
requestID, err := ids.New()
if err != nil {
return "", err
}
prompt := "Ты отвечаешь как Домовой на обычное сообщение пользователя. Сначала пойми цель: на вопрос дай короткий естественный ответ по устойчивому общеизвестному знанию, а безопасную просьбу создать текстовый или структурный результат выполни прямо в ответе. Начинай сразу с содержания и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты и не утверждай выполнение внешнего действия.\n\nСообщение пользователя:\n" + userMessage
if retrieve != nil {
prompt += "\n\nЕсли именно для ответа не хватает ограниченного контекста, вместо ответа верни единственную строку <domovoy-retrieval>{\"Query\":\"...\",\"SemanticSubject\":\"...\",\"Lane\":\"CURRENT_FACT|POSITIVE_EXPERIENCE|NEGATIVE_EXPERIENCE|CONTRADICTION|HISTORICAL_ANALOGY\",\"Reason\":\"...\"}. Не запрашивай широкий дамп памяти."
}
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{
requestValue := 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: 1024, DataPolicy: capability.DataPolicyLocalOnly,
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
})
Message: prompt, Context: context, Locale: "ru", MaxOutputBytes: stableKnowledgeMaxOutputBytes, DataPolicy: capability.DataPolicyLocalOnly,
AllowIterativeRetrieval: retrieve != nil,
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
}
result, err := c.requestAdvisory(ctx, requestValue)
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.IterativeRetrieval != nil && retrieve != nil {
additional := retrieve(ctx, *result.IterativeRetrieval)
for _, candidate := range additional.Candidates {
context = append(context, candidate.Excerpt)
}
if len(additional.Candidates) == 0 {
context = append(context, cognitive.ContextExcerpt{ID: "continuation:no-additional-evidence", SourceKind: "iterative-retrieval", SourceID: "core", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: "No additional evidence was found; answer with uncertainty rather than inventing facts."})
}
requestValue.Context = context
requestValue.AllowIterativeRetrieval = false
result, err = c.requestAdvisory(ctx, requestValue)
if err != nil {
return "", err
}
}
if result.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(result.AdvisoryText) == "" {
return "", fmt.Errorf("cognitive stable knowledge incomplete: %s", result.FinishReason)
@@ -130,6 +290,33 @@ func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context
return strings.TrimSpace(result.AdvisoryText), nil
}
func (c cognitiveInterpretationClient) requestAdvisory(ctx context.Context, value cognitive.GatewayAdvisoryRequest) (cognitive.GatewayAdvisoryResponse, error) {
target := *c.baseURL
target.Path = "/v1/advisories"
payload, err := json.Marshal(value)
if err != nil {
return cognitive.GatewayAdvisoryResponse{}, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return cognitive.GatewayAdvisoryResponse{}, err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(request)
if err != nil {
return cognitive.GatewayAdvisoryResponse{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return cognitive.GatewayAdvisoryResponse{}, fmt.Errorf("cognitive stable knowledge status %d", response.StatusCode)
}
var result cognitive.GatewayAdvisoryResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return cognitive.GatewayAdvisoryResponse{}, err
}
return result, nil
}
func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cognitiveInterpretationClient, error) {
base, err := url.Parse(rawURL)
if err != nil || base.Scheme == "" || base.Host == "" {
@@ -294,7 +481,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),
@@ -11,6 +11,7 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
@@ -70,6 +71,84 @@ func TestAnswerStableKnowledgeUsesLocalOnlyCognitiveRoute(t *testing.T) {
}
}
func TestCompileInteractionContextAppliesTypedScopeBeforeGateway(t *testing.T) {
context, err := compileInteractionContext(interactiondomain.ContextProjection{Fragments: []interactiondomain.ContextFragment{
{UtteranceID: "a", Text: "контрольный код = ALPHA", ScopeRef: "company-a", Relevance: 2},
{UtteranceID: "b", Text: "контрольный код = BETA", ScopeRef: "company-b", Relevance: 3},
}}, "company-a")
if err != nil || context != "контрольный код = ALPHA" {
t.Fatalf("context=%q err=%v", context, err)
}
}
func TestCompileInteractionContextUsesHistoricalOnlyWhenRequested(t *testing.T) {
projection := interactiondomain.ContextProjection{Fragments: []interactiondomain.ContextFragment{{UtteranceID: "current", Text: "код = NEW", Relevance: 2}}, Historical: []interactiondomain.ContextFragment{{UtteranceID: "old", Text: "код = OLD", Relevance: 1, Historical: true}}}
current, err := compileInteractionContextWithExperience(projection, "", nil, "Какой текущий код?")
if err != nil || strings.Contains(current, "OLD") {
t.Fatalf("current context=%q err=%v", current, err)
}
historical, err := compileInteractionContextWithExperience(projection, "", nil, "Какой старый код был раньше?")
if err != nil || !strings.Contains(historical, "OLD") {
t.Fatalf("historical context=%q err=%v", historical, err)
}
}
func TestStableKnowledgeContinuesOnceWithBoundedAdditionalContext(t *testing.T) {
calls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
if calls == 1 {
if !request.AllowIterativeRetrieval {
t.Fatal("initial advisory did not permit bounded continuation")
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishCompleted, IterativeRetrieval: &cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "initial context is insufficient", RemainingBytes: 512}})
return
}
if request.AllowIterativeRetrieval || len(request.Context) != 1 || request.Context[0].Text != "контрольный код = ORBIT-731" {
t.Fatalf("continuation request=%+v", request)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishCompleted, AdvisoryText: "Контрольный код — ORBIT-731."})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
answer, err := client.AnswerStableKnowledgeWithContinuation(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "Какой контрольный код?", "", cognitive.TaskMeaning{SpeechAct: "QUESTION"}, func(context.Context, cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse {
return cognitive.IterativeRetrievalResponse{Candidates: []cognitive.ContextCandidate{{Excerpt: cognitive.ContextExcerpt{ID: "interaction:code", Text: "контрольный код = ORBIT-731", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact"}}, Exhausted: true, AddedBytes: 28}
})
if err != nil || answer != "Контрольный код — ORBIT-731." || calls != 2 {
t.Fatalf("answer=%q err=%v calls=%d", answer, err, calls)
}
}
func TestIterativeRetrieverPreservesScopeLanesAndBudgets(t *testing.T) {
history := []interaction.HumanUtterance{{ID: "a", PrincipalID: "p", Content: "контрольный код = ALPHA", ReceivedAtUnixMillis: 1}, {ID: "b", PrincipalID: "p", Content: "контрольный код = BETA", ReceivedAtUnixMillis: 2}}
references := []interaction.ProjectReference{{ID: "a-ref", UtteranceID: "a", ProjectID: "company-a"}, {ID: "b-ref", UtteranceID: "b", ProjectID: "company-b"}}
experiences := []epistemics.ExperienceRecord{{ID: "failure", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "catalog sync", ActualOutcome: epistemics.OutcomeFailed, OutcomeDetail: "old connector failed", RecordedAtUnixMillis: 1}, {ID: "conflict", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "catalog sync", ActualOutcome: epistemics.OutcomeConflict, OutcomeDetail: "conflicting result", RecordedAtUnixMillis: 2}}
retrieve := newIterativeContextRetriever(history, references, "p", "company-a", experiences)
current := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "need scoped fact", RemainingBytes: 512})
if len(current.Candidates) != 1 || !strings.Contains(current.Candidates[0].Excerpt.Text, "ALPHA") || strings.Contains(current.Candidates[0].Excerpt.Text, "BETA") {
t.Fatalf("scope leaked: %#v", current)
}
if duplicate := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "repeat", RemainingBytes: 512}); duplicate.Reason != "no_additional_evidence" {
t.Fatalf("duplicate was re-added: %#v", duplicate)
}
if negative := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "catalog sync", Lane: cognitive.IterativeRetrievalNegativeExperience, Reason: "need failure", RemainingBytes: 512}); len(negative.Candidates) < 1 || !strings.Contains(strings.Join([]string{negative.Candidates[0].Excerpt.Text, negative.Candidates[len(negative.Candidates)-1].Excerpt.Text}, " "), "old connector failed") {
t.Fatalf("negative lane=%#v", negative)
}
if contradiction := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "catalog sync", Lane: cognitive.IterativeRetrievalContradiction, Reason: "need conflict", RemainingBytes: 512}); len(contradiction.Candidates) != 1 || !strings.Contains(contradiction.Candidates[0].Excerpt.Text, "conflicting result") {
t.Fatalf("contradiction lane=%#v", contradiction)
}
if budget := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "anything", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "bad", RemainingBytes: 2048}); budget.Reason != "budget" {
t.Fatalf("budget=%#v", budget)
}
}
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" {
@@ -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")
}
}
+109 -1
View File
@@ -11,8 +11,12 @@ import (
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
selfcontract "git.cin.su/m/Assistant/contracts/self"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
selfdomain "git.cin.su/m/Assistant/modules/self/domain"
"git.cin.su/m/Assistant/platform/ids"
"git.cin.su/m/Assistant/platform/telemetry"
)
const reminderResponseSkillID = capability.SkillProfileID("skill-obvious-reminder-response-v1")
@@ -28,15 +32,55 @@ type learningCapabilityStore interface {
SaveSkillProfile(context.Context, capability.SkillProfile) error
}
type learningSelfStore interface {
SaveLearningSignal(context.Context, selfcontract.LearningSignal) error
SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error
SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error
}
type learningRecorder struct {
epistemics learningEpistemicsStore
capability capabilityLearningStore
self learningSelfStore
}
type capabilityLearningStore interface {
learningCapabilityStore
}
func (r learningRecorder) recordSelfSignal(ctx context.Context, signal selfcontract.LearningSignal) {
if r.self == nil {
return
}
if err := signal.Validate(); err != nil {
log.Printf("learning SELF signal invalid: %v", err)
return
}
if err := r.self.SaveLearningSignal(ctx, signal); err != nil {
log.Printf("learning SELF signal unavailable: %v", err)
return
}
diagnosis := selfdomain.AttributeFailure(signal)
need, err := selfdomain.BuildImprovementNeed(signal, diagnosis, string(signal.Kind))
if err != nil {
log.Printf("learning SELF improvement need invalid: %v", err)
return
}
if err := r.self.SaveImprovementNeed(ctx, need); err != nil {
log.Printf("learning SELF improvement need unavailable: %v", err)
return
}
needRef := selfcontract.SemanticReference{OwnerNamespace: "self", SemanticKind: "improvement_need", StableID: ids.StableID(need.ID)}
ledger := selfcontract.LearningLedgerEntry{ID: selfcontract.LearningLedgerEntryID("ledger:observed:" + signal.ID), PrincipalID: signal.PrincipalID, ImprovementNeedRef: &needRef, ObservedProblem: need.ProblemSummary, EvidenceRefs: signal.EvidenceRefs, Diagnosis: string(diagnosis.Class), Hypothesis: diagnosis.Rationale, ProposedChangeRef: signal.SubjectRef, ExpectedImprovement: need.ExpectedBenefit, Decision: "OBSERVED_REQUIRES_GOVERNED_REVIEW", RecordedAt: signal.ObservedAt}
if err := r.self.SaveLearningLedger(ctx, ledger); err != nil {
log.Printf("learning SELF ledger unavailable: %v", err)
}
}
func selfEvidence(owner, kind, stable string) selfcontract.SemanticReference {
return selfcontract.SemanticReference{OwnerNamespace: owner, SemanticKind: kind, StableID: ids.StableID(stable)}
}
type reminderResponseLearningInput struct {
Utterance interaction.HumanUtterance
Intent interaction.OriginalHumanIntent
@@ -112,6 +156,7 @@ func (r learningRecorder) recordReminderResponse(ctx context.Context, input remi
actual, confidence := outcomeForHumanResponse(input.Result.Response)
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceContinuity,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ContextRefs: contextRefs(
@@ -142,6 +187,7 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceInteraction,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodClarification,
@@ -154,12 +200,45 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:correction:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalUserCorrection,
TaskPattern: "interaction-correction", ScopeKey: "principal:" + string(input.Utterance.PrincipalID),
SubjectRef: selfEvidence("interaction", "intent", string(input.Intent.ID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))}, ObservedAt: input.At.UnixMilli(), Severity: 2, UserImpact: 2,
})
}
func (r learningRecorder) createDelegationCandidateSkill(ctx context.Context, input delegationLearningInput, experience epistemics.ExperienceRecord) {
if r.capability == nil || experience.ID == "" || len(experience.VerificationEvidence) == 0 {
return
}
// One verified result creates only a candidate. Repeated outcome evidence and
// independent validation are required by Capability before activation.
profile := capability.SkillProfile{
ID: capability.SkillProfileID("skill:delegation:" + string(input.ImplementationID)),
Revision: 1,
Name: "Verified bounded delegation strategy",
Description: "Observed verified delegation outcome; requires repeatability and independent validation.",
ApplicableContext: []string{"work:delegation"},
ImplementationRef: "work:agent-delegation-v1",
ImplementationVersion: "1",
Status: capability.SkillCandidate,
ConfidenceMillis: 500,
RecordedAtUnixMillis: experience.RecordedAtUnixMillis,
}
next, err := capabilitydomain.CandidateFromVerifiedExperience(profile, capability.SkillEvidenceRef("epistemics:experience:"+string(experience.ID)), true, experience.RecordedAtUnixMillis)
if err != nil {
return
}
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
log.Printf("learning candidate skill unavailable: %v", err)
}
}
func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastPathDeclineLearningInput) {
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),
@@ -173,10 +252,16 @@ func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastP
if input.Result.Response.ID != "" {
experience.ContextRefs = contextRefs("continuity:response-candidate:" + string(input.Result.Response.Operation))
}
experience.PrincipalID = string(input.Utterance.PrincipalID)
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok {
r.updateReminderSkill(ctx, saved, capability.UsageSkipped, false)
}
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:fast-path:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalSkillFallback,
TaskPattern: "fast-path", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "skill", string(reminderResponseSkillID)),
EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))}, ActualOutcome: string(input.Reason), ObservedAt: input.At.UnixMilli(), Severity: 1,
})
}
func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabilityLearningInput) {
@@ -194,6 +279,7 @@ func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabil
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceCapability,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodCapabilityExecution,
@@ -209,6 +295,13 @@ func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabil
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
if input.Outcome == capability.UsageFailed || input.Outcome == capability.UsageUnknown {
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:capability:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalVerifiedFailure,
TaskPattern: "capability-use", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "implementation", string(input.ImplementationID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))},
ActualOutcome: string(input.Outcome), ObservedAt: input.At.UnixMilli(), Severity: 2,
})
}
}
func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input delegationLearningInput) {
@@ -230,6 +323,7 @@ func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input del
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceCapability,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ContextRefs: contextRefs("work:delegation:"+input.DelegationID, "work:run:"+input.RunID),
@@ -244,7 +338,17 @@ func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input del
ConfidenceMillis: confidence,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok && input.Verified {
r.createDelegationCandidateSkill(ctx, input, saved)
}
if !input.Verified {
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:delegation:" + input.DelegationID, PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalVerifiedFailure,
TaskPattern: "delegation", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "implementation", string(input.ImplementationID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", input.OutcomeRef)},
ActualOutcome: string(actual), ObservedAt: input.At.UnixMilli(), Severity: 2,
})
}
}
func (r learningRecorder) saveExperienceAndReflection(ctx context.Context, value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, bool) {
@@ -273,9 +377,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 +393,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) {
+76 -1
View File
@@ -2,12 +2,14 @@ package main
import (
"context"
"errors"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
selfcontract "git.cin.su/m/Assistant/contracts/self"
)
type learningEpistemicsStub struct {
@@ -28,17 +30,60 @@ func (s *learningEpistemicsStub) SaveReflectionCandidate(context.Context, episte
type learningCapabilityStub struct {
loadCalls int
saveCalls int
saved []capability.SkillProfile
}
type learningSelfStub struct {
signals int
needs int
ledgers int
}
type failingLearningSelfStub struct{}
func (failingLearningSelfStub) SaveLearningSignal(context.Context, selfcontract.LearningSignal) error {
return errors.New("self repository unavailable")
}
func (failingLearningSelfStub) SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error {
return errors.New("self repository unavailable")
}
func (failingLearningSelfStub) SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error {
return errors.New("self repository unavailable")
}
func (s *learningSelfStub) SaveLearningSignal(context.Context, selfcontract.LearningSignal) error {
s.signals++
return nil
}
func (s *learningSelfStub) SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error {
s.needs++
return nil
}
func (s *learningSelfStub) SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error {
s.ledgers++
return nil
}
func (s *learningCapabilityStub) LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error) {
s.loadCalls++
return capability.SkillProfile{}, nil
}
func (s *learningCapabilityStub) SaveSkillProfile(context.Context, capability.SkillProfile) error {
func (s *learningCapabilityStub) SaveSkillProfile(_ context.Context, value capability.SkillProfile) error {
s.saveCalls++
s.saved = append(s.saved, value)
return nil
}
func TestVerifiedDelegationCreatesCandidateNotActiveSkill(t *testing.T) {
e := &learningEpistemicsStub{}
c := &learningCapabilityStub{}
r := learningRecorder{epistemics: e, capability: c}
r.recordDelegationOutcome(context.Background(), delegationLearningInput{Intent: interaction.OriginalHumanIntent{ID: "i"}, Utterance: interaction.HumanUtterance{ID: "u"}, DelegationID: "d", RunID: "r", ImplementationID: "impl", OutcomeRef: "outcome", VerificationRefs: []epistemics.EvidenceRef{"verified"}, Verified: true, At: time.UnixMilli(1000)})
if len(c.saved) != 1 || c.saved[0].Status != capability.SkillCandidate || c.saved[0].VerifiedOutcomeCount != 1 {
t.Fatalf("candidate=%#v", c.saved)
}
}
func TestGenericCorrectionDoesNotDegradeReminderSkill(t *testing.T) {
epistemicsStore := &learningEpistemicsStub{}
capabilityStore := &learningCapabilityStub{}
@@ -55,3 +100,33 @@ func TestGenericCorrectionDoesNotDegradeReminderSkill(t *testing.T) {
t.Fatalf("generic correction must remain evidence without a reminder skill reference: %#v", epistemicsStore.experiences)
}
}
func TestCorrectionWritesBoundedSelfEvidenceWithoutChangingResponsePath(t *testing.T) {
epistemicsStore := &learningEpistemicsStub{}
selfStore := &learningSelfStub{}
recorder := learningRecorder{epistemics: epistemicsStore, self: selfStore}
recorder.recordCorrection(context.Background(), correctionLearningInput{
Intent: interaction.OriginalHumanIntent{ID: "intent-2"},
Utterance: interaction.HumanUtterance{ID: "utterance-2", PrincipalID: "principal-2"},
At: time.UnixMilli(2000),
})
if selfStore.signals != 1 || selfStore.needs != 1 || selfStore.ledgers != 1 {
t.Fatalf("self evidence counts: signals=%d needs=%d ledgers=%d", selfStore.signals, selfStore.needs, selfStore.ledgers)
}
if len(epistemicsStore.experiences) != 1 {
t.Fatalf("completed learning path was changed")
}
}
func TestSelfRepositoryFailureDoesNotDiscardUserFacingLearningPath(t *testing.T) {
epistemicsStore := &learningEpistemicsStub{}
recorder := learningRecorder{epistemics: epistemicsStore, self: failingLearningSelfStub{}}
recorder.recordCorrection(context.Background(), correctionLearningInput{
Intent: interaction.OriginalHumanIntent{ID: "intent-failure"},
Utterance: interaction.HumanUtterance{ID: "utterance-failure", PrincipalID: "principal-failure"},
At: time.UnixMilli(3000),
})
if len(epistemicsStore.experiences) != 1 {
t.Fatalf("SELF repository failure must not discard the completed epistemics response path")
}
}
+279 -58
View File
@@ -24,6 +24,7 @@ import (
workdomain "git.cin.su/m/Assistant/modules/work/domain"
"git.cin.su/m/Assistant/platform/ids"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
"git.cin.su/m/Assistant/platform/telemetry"
"git.cin.su/m/Assistant/runtime/health"
"github.com/jackc/pgx/v5"
"io"
@@ -37,6 +38,15 @@ import (
"time"
)
const (
creatorPasswordPath = "/run/secrets/creator_password"
creatorBootstrapTokenPath = "/run/secrets/creator_bootstrap_token"
creatorSessionKeyPath = "/run/secrets/creator_session_signing_key"
epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
interactionDBPasswordPath = "/run/secrets/interaction_db_password"
selfDBPasswordPath = "/run/secrets/self_db_password"
)
const maxCreatorCredentialsBytes = 4 << 10
func main() {
@@ -44,20 +54,21 @@ func main() {
if addr == "" {
addr = ":8080"
}
bootstrapToken, err := readRequiredSecret("CREATOR_BOOTSTRAP_TOKEN")
bootstrapToken, err := readRequiredSecretFile("CREATOR_BOOTSTRAP_TOKEN", creatorBootstrapTokenPath)
if err != nil {
log.Fatal(err)
}
sessionKeyValue, err := readRequiredSecret("CREATOR_SESSION_SIGNING_KEY")
if err != nil {
log.Fatal(err)
}
creatorPassword, err := readRequiredSecret("CREATOR_PASSWORD")
sessionKeyValue, err := readRequiredSecretFile("CREATOR_SESSION_SIGNING_KEY", creatorSessionKeyPath)
if err != nil {
log.Fatal(err)
}
sessionKey := []byte(sessionKeyValue)
creatorUsername := os.Getenv("CREATOR_USERNAME")
if creatorUsername == "" {
log.Fatal("CREATOR_USERNAME is required")
}
telemetry.RecordCompatibility("non-bootstrap-env-consumers", "core-runtime", "read", "runtime-startup-configuration", true, true, true, "OBSERVED", "startup", "core-runtime", time.Now())
telemetry.RecordCompatibility("migration-compatibility-switches", "core-runtime", "observe", "runtime-startup-switch-scan", true, false, false, "NOT_OBSERVED", "startup", "core-runtime", time.Now())
repository, err := postgres.OpenSituation(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("POSTGRES_USER"), Password: os.Getenv("POSTGRES_PASSWORD")})
if err != nil {
log.Fatal(err)
@@ -73,7 +84,7 @@ func main() {
log.Fatal(err)
}
defer capabilityRepository.Close()
epistemicsPassword, err := readCredentialRefSecret("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
epistemicsPassword, err := readCredentialRefSecret("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF", epistemicsDBPasswordPath)
if err != nil {
log.Fatal(err)
}
@@ -91,7 +102,10 @@ func main() {
log.Fatal(err)
}
defer identityRepository.Close()
interactionPassword, err := readCredentialRefSecret("INTERACTION_POSTGRES_PASSWORD", "INTERACTION_POSTGRES_CREDENTIAL_REF")
if err := bootstrapCreatorAccount(context.Background(), identityRepository, creatorUsername, creatorPasswordPath); err != nil {
log.Fatal(err)
}
interactionPassword, err := readCredentialRefSecret("INTERACTION_POSTGRES_PASSWORD", "INTERACTION_POSTGRES_CREDENTIAL_REF", interactionDBPasswordPath)
if err != nil {
log.Fatal(err)
}
@@ -104,6 +118,25 @@ func main() {
log.Fatal(err)
}
defer interactionRepository.Close()
var selfRepository *postgres.SelfRepository
selfUser := os.Getenv("SELF_POSTGRES_USER")
selfPassword := ""
if selfUser != "" && os.Getenv("SELF_POSTGRES_CREDENTIAL_REF") != "" {
selfPassword, err = readCredentialRefSecret("SELF_POSTGRES_PASSWORD", "SELF_POSTGRES_CREDENTIAL_REF", selfDBPasswordPath)
if err != nil {
log.Printf("SELF learning repository unavailable; user responses remain unaffected: %v", err)
}
}
if selfUser != "" && selfPassword != "" {
selfRepository, err = postgres.OpenSelf(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: selfUser, Password: selfPassword})
if err != nil {
log.Printf("SELF learning repository unavailable; user responses remain unaffected: %v", err)
} else {
defer selfRepository.Close()
}
} else if selfUser == "" || os.Getenv("SELF_POSTGRES_CREDENTIAL_REF") == "" {
log.Printf("SELF learning repository is not configured; user responses remain unaffected")
}
reminderClient, err := newContinuityReminderClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second})
if err != nil {
log.Fatal(err)
@@ -124,7 +157,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository, self: selfRepository}
mux := http.NewServeMux()
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
@@ -144,10 +177,16 @@ func main() {
}
mux.Handle("/healthz", health.Handler{Runtime: "core-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", Probes: []health.Probe{
cognitiveReadinessClient := &http.Client{Timeout: 135 * time.Second}
cognitiveReadinessProbe := health.HTTPProbe(cognitiveReadinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/readyz")
cognitiveReadinessProbe.Timeout = 135 * time.Second
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", Timeout: 140 * time.Second, Probes: []health.Probe{
health.TCPProbe("postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/healthz"),
// A running worker is not necessarily ready to serve cognitive traffic.
// Keep deterministic paths alive, but do not advertise overall readiness
// while the cognitive dependency is still warming.
cognitiveReadinessProbe,
}})
admissionPolicy := capability.AdmissionPolicy{ID: envOr("COGNITIVE_ADMISSION_POLICY_ID", "creator-advisory-local-v1"), MaxConcurrent: envInt("COGNITIVE_ADMISSION_MAX_CONCURRENT", 1), MaxAcceptedPerHour: envInt("COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR", 60), MaxInputBytes: envInt("COGNITIVE_ADMISSION_MAX_INPUT_BYTES", 80<<10), MaxOutputBytes: envInt("COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES", 16<<10)}
mux.HandleFunc("POST /v1/internal/cognitive/admission", func(w http.ResponseWriter, r *http.Request) {
@@ -177,7 +216,39 @@ func main() {
}
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("POST /v1/creator/sessions", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now))
mux.HandleFunc("POST /v1/creator/sessions", creatorDatabaseSessionHandler(bootstrapToken, creatorUsername, identityRepository, sessionKey, time.Now))
mux.HandleFunc("POST /v1/creator/account/password", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
decoder := json.NewDecoder(io.LimitReader(r.Body, maxCreatorCredentialsBytes+1))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.CurrentPassword == "" || request.NewPassword == "" {
http.Error(w, "invalid password change request", http.StatusBadRequest)
return
}
account, err := identityRepository.LoadCreatorAccountByBinding(r.Context(), session.ActorID, session.PrincipalID)
if err != nil || !identitydomain.VerifyCreatorPassword(account.PasswordHash, request.CurrentPassword) {
http.Error(w, "current password denied", http.StatusUnauthorized)
return
}
newHash, err := identitydomain.HashCreatorPassword(request.NewPassword)
if err != nil {
http.Error(w, "invalid new password", http.StatusBadRequest)
return
}
if err := identityRepository.UpdateCreatorPassword(r.Context(), session.ActorID, session.PrincipalID, account.Revision, newHash); err != nil {
http.Error(w, "password change conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"status": "changed"})
})
mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
@@ -505,9 +576,13 @@ func main() {
storedFocus, err := repository.ActiveProjectFocus(r.Context(), session.ActorID, session.PrincipalID)
if err == nil {
previousFocus = &storedFocus
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "SUCCESS", utterance.CorrelationID, string(session.PrincipalID), now)
} else if !errors.Is(err, pgx.ErrNoRows) {
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "FAILURE", utterance.CorrelationID, string(session.PrincipalID), now)
http.Error(w, "project focus unavailable", http.StatusServiceUnavailable)
return
} else {
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "EMPTY", utterance.CorrelationID, string(session.PrincipalID), now)
}
var effectiveTaskMessage = interactiondomain.StripAssistantVocative(utterance.Content)
creativeUserRequest := interactiondomain.IsCreativeRequest(utterance.Content)
@@ -516,9 +591,10 @@ func main() {
travelContinuation := false
var previousTask *interaction.HumanUtterance
var conversationHistory []interaction.HumanUtterance
var contextProjectReferences []interaction.ProjectReference
if request.ConversationID != "" {
var historyErr error
conversationHistory, historyErr = interactionRepository.Utterances(r.Context(), conversation.ID)
conversationHistory, contextProjectReferences, historyErr = interactionRepository.UtterancesWithProjectReferences(r.Context(), conversation.ID)
if historyErr != nil {
log.Printf("interaction conversation history unavailable: %v", historyErr)
}
@@ -526,15 +602,38 @@ func main() {
// Keep durable user context available even when the web client carries a
// conversation ID; a new or rotated conversation must not erase learned
// preferences.
recentUserHistory, historyErr := interactionRepository.RecentUtterancesForPrincipal(r.Context(), session.PrincipalID, 64)
recentUserHistory, recentProjectReferences, historyErr := interactionRepository.RecentUtterancesForPrincipalWithProjectReferences(r.Context(), session.PrincipalID, 64)
if historyErr != nil {
log.Printf("interaction principal history unavailable: %v", historyErr)
} else {
log.Printf("interaction principal history loaded: %d utterances", len(recentUserHistory))
}
conversationHistory = append(conversationHistory, recentUserHistory...)
gen2Context := interactiondomain.BuildGen2Context(conversationHistory)
if !creativeUserRequest && locationReply == "" {
contextProjectReferences = append(contextProjectReferences, recentProjectReferences...)
focusProjectID := ""
if previousFocus != nil {
focusProjectID = string(previousFocus.ProjectID)
}
gen2Context := interactiondomain.BuildScopedGen2ContextForQuery(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, effectiveTaskMessage)
experienceRecords, experienceErr := epistemicsRepository.RecentExperienceRecordsForPrincipalAndScope(r.Context(), string(session.PrincipalID), focusProjectID, 16)
if experienceErr != nil {
log.Printf("epistemics experience context unavailable: %v", experienceErr)
}
if compiledContext, contextErr := compileInteractionContextWithExperience(gen2Context, focusProjectID, experienceRecords, effectiveTaskMessage); contextErr != nil {
log.Printf("interaction context compilation unavailable: %v", contextErr)
} else {
gen2Context.Text = compiledContext
}
iterativeRetriever := newIterativeContextRetriever(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, experienceRecords)
if gen2Context.UsedPersonalMemoryFallback {
telemetry.RecordCompatibility("personal-memory-fallback", "core-runtime", "read", "gen2-context-empty", true, true, false, "USED", utterance.CorrelationID, string(session.PrincipalID), now)
}
retrievedMemoryReply := interactiondomain.RecallPersonalPreference(conversationHistory, effectiveTaskMessage)
stableConversationalReply, stableConversational := interactiondomain.StableConversationalReply(
effectiveTaskMessage,
interactiondomain.ClassifySpeechAct(effectiveTaskMessage, utterance.CorrectsUtteranceID),
)
if !creativeUserRequest && locationReply == "" && !stableConversational && retrievedMemoryReply == "" {
if continuation := interactiondomain.ContinueLocalSearch(effectiveTaskMessage, conversationHistory); continuation != "" {
continuationMessage = continuation
effectiveTaskMessage = continuation
@@ -661,6 +760,14 @@ func main() {
semanticInterpretationRequested = false
semanticReflexAudit = false
}
if stableConversational {
semanticInterpretationRequested = false
semanticReflexAudit = false
}
if retrievedMemoryReply != "" {
semanticInterpretationRequested = false
semanticReflexAudit = false
}
if identityReply != "" || memoryInstructionReply != "" {
semanticReflexAudit = false
}
@@ -684,7 +791,7 @@ func main() {
cognitiveAvailable = false
}
}
if !creativeRequest {
if !creativeRequest && !stableConversational && retrievedMemoryReply == "" {
if travelReply := interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory); travelReply != "" {
semantic.Interpretation.Clarification = interaction.ClarificationRequired
semantic.Interpretation.Applicability = interaction.SemanticCandidateClarify
@@ -702,12 +809,16 @@ func main() {
http.Error(w, "save chat semantics", http.StatusInternalServerError)
return
}
responseResult, responseErr := responseClient.Respond(r.Context(), r.Header.Get("Authorization"), continuity.HumanResponseRequest{
SourceUtteranceID: string(utterance.ID),
SourceIntentID: string(intent.ID),
Message: effectiveTaskMessage,
RequestedAtUnixMillis: now.UnixMilli(),
})
var responseResult continuity.HumanResponseResult
var responseErr error
if shouldInvokeHumanResponse(stableConversational, identityReply, memoryInstructionReply) {
responseResult, responseErr = responseClient.Respond(r.Context(), r.Header.Get("Authorization"), continuity.HumanResponseRequest{
SourceUtteranceID: string(utterance.ID),
SourceIntentID: string(intent.ID),
Message: effectiveTaskMessage,
RequestedAtUnixMillis: now.UnixMilli(),
})
}
if semantic.Interpretation.SpeechAct == interaction.SpeechActCorrection {
learning.recordCorrection(r.Context(), correctionLearningInput{Utterance: utterance, Intent: intent, At: now})
}
@@ -715,9 +826,11 @@ func main() {
activeFocusProvenance := situation.ProjectResolutionProvenance("")
if semantic.ActiveFocus != nil {
if err := repository.SaveActiveProjectFocus(r.Context(), *semantic.ActiveFocus); err != nil {
telemetry.RecordCompatibility("active-project-focus-write", "core-runtime", "write", "semantic-focus-update", true, false, true, "FAILURE", utterance.CorrelationID, string(session.PrincipalID), now)
http.Error(w, "save project focus", http.StatusInternalServerError)
return
}
telemetry.RecordCompatibility("active-project-focus-write", "core-runtime", "write", "semantic-focus-update", true, false, true, "SUCCESS", utterance.CorrelationID, string(session.PrincipalID), now)
activeFocusName = semantic.ActiveFocus.ProjectDisplayName
activeFocusProvenance = semantic.ActiveFocus.Provenance
} else if len(semantic.Projects) > 0 && semantic.Projects[0].Provenance == situation.ProjectResolutionInheritedFocus && previousFocus != nil {
@@ -733,7 +846,7 @@ func main() {
} else if len(semantic.Projects) > 0 && semantic.Projects[0].Provenance == situation.ProjectResolutionInheritedFocus {
projectID = string(previousFocus.ProjectID)
}
if responseErr == nil && responseResult.Handled {
if responseErr == nil && responseResult.Handled && !stableConversational {
planningReply = responseResult.Reply
needsCognition = false
learning.recordReminderResponse(r.Context(), reminderResponseLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, At: now})
@@ -763,6 +876,12 @@ func main() {
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
}
helpPath := workdomain.DetermineHelpPath(planningInput)
// Introspection requests are read-only household UX. A cognitive
// candidate must not reinterpret them as an executable delegation.
switch semantic.InformationRequestKind {
case interaction.InformationRequestCapabilityInventory, interaction.InformationRequestLocalCapabilityInventory, interaction.InformationRequestCognitiveModelInventory, interaction.InformationRequestCognitiveCurrentModel, interaction.InformationRequestCognitiveChannelStatus, interaction.InformationRequestCognitiveRecovery, interaction.InformationRequestMemoryInventory:
helpPath = work.HelpPath{}
}
if responseErr == nil && responseResult.Handled {
helpPath = work.HelpPath{}
}
@@ -800,7 +919,11 @@ func main() {
researchBacked := false
researchAttempted := false
stableKnowledgeReply := false
if hasChatRoute(routePlan, chatRouteResearch) && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
if retrievedMemoryReply != "" {
planningReply = retrievedMemoryReply
needsCognition = false
}
if hasChatRoute(routePlan, chatRouteResearch) && len(gen2Context.Fragments) == 0 && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
researchAttempted = true
if planID, idErr := ids.New(); idErr == nil {
parts := interactiondomain.IndependentRequestParts(effectiveTaskMessage)
@@ -829,9 +952,9 @@ func main() {
reply, backed := renderResearchReply(searchQuery, result, researchErr)
if !backed && cognitiveAvailable {
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, part, gen2Context.Text, cognitive.TaskMeaning{
answer, answerErr := cognitiveClient.AnswerStableKnowledgeWithContinuation(answerCtx, session, part, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
})
}, iterativeRetriever)
cancelAnswer()
if answerErr == nil && stableKnowledgeAnswer(answer) {
reply, backed = answer, true
@@ -860,18 +983,18 @@ func main() {
planningReply = helpPath.Reply
needsCognition = false
}
if !creativeRequest {
if !creativeRequest && !stableConversational && retrievedMemoryReply == "" {
if travelReply := interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory); travelReply != "" {
planningReply = travelReply
needsCognition = false
}
}
canGenerateTextReply := shouldGenerateNonMutatingTextDirective(semantic, routePlan, helpPath, creativeRequest) && planningReply == deterministicReply
if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && !hasChatRoute(routePlan, chatRouteResearch) && !creativeRequest {
if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && (!hasChatRoute(routePlan, chatRouteResearch) || len(gen2Context.Fragments) > 0) && !creativeRequest {
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, effectiveTaskMessage, gen2Context.Text, cognitive.TaskMeaning{
answer, answerErr := cognitiveClient.AnswerStableKnowledgeWithContinuation(answerCtx, session, effectiveTaskMessage, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
})
}, iterativeRetriever)
cancelAnswer()
if answerErr == nil && stableKnowledgeAnswer(answer) {
planningReply = answer
@@ -990,7 +1113,11 @@ func main() {
planningReply = memoryInstructionReply
needsCognition = false
}
if strings.TrimSpace(planningReply) != "" {
// Stable-knowledge answers have already passed the local cognitive
// route with compact, source-aware context. Re-reviewing the same answer
// serially can exhaust the household request budget on a loaded local
// provider and turn a valid answer into a degraded response.
if shouldReviewResponse(planningReply, stableKnowledgeReply, stableConversational, identityReply, memoryInstructionReply, retrievedMemoryReply != "") {
reviewContext, cancelReview := context.WithTimeout(r.Context(), 12*time.Second)
reviewedReply, reviewErr := cognitiveClient.ReviewResponse(reviewContext, session, utterance.Content, planningReply, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct),
@@ -1016,13 +1143,8 @@ func main() {
}
}
if !mediaAttempted && !mediaHandled && identityReply == "" && creativeReply == "" && creativeFallbackReply == "" {
stableAct := interactiondomain.ClassifySpeechAct(utterance.Content, utterance.CorrectsUtteranceID)
travelClarification := ""
if !creativeRequest {
travelClarification = interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory)
}
if stableReply, stable := interactiondomain.StableConversationalReply(utterance.Content, stableAct); stable && travelClarification == "" {
planningReply = stableReply
if stableConversational {
planningReply = stableConversationalReply
needsCognition = false
}
}
@@ -1397,6 +1519,10 @@ func main() {
candidates, err = capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
}
if err != nil {
// The resolver is an internal capability boundary. Keep request identity
// and content out of logs, but preserve the repository error so a 503
// can be distinguished from a true empty/unsuitable binding set.
log.Printf("cognitive registry resolve unavailable: %v", err)
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
@@ -1486,7 +1612,7 @@ func main() {
}
request.RequestedByPrincipalID = session.PrincipalID
request.CapabilityID = "creator-advisory"
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
candidates, err := capabilityRepository.LoadCognitiveCandidatesForEvaluation(r.Context(), request.CapabilityID, session.PrincipalID, request.EvaluationID)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
@@ -1525,18 +1651,11 @@ func main() {
}
request.RequestedByPrincipalID = session.PrincipalID
request.CapabilityID = "creator-advisory"
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
current, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), request.BindingID, request.ExpectedBindingRevision)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
var current capability.ResolvedCognitiveBinding
for _, candidate := range candidates {
if candidate.Binding.ID == request.BindingID && candidate.Binding.Revision == request.ExpectedBindingRevision {
current = candidate
break
}
}
promotion, err := capabilitydomain.BuildActivationPromotion(request, current)
if err != nil {
http.Error(w, "cognitive activation precondition failed", http.StatusConflict)
@@ -1670,6 +1789,14 @@ func main() {
}
}
func shouldInvokeHumanResponse(stableConversational bool, identityReply, memoryInstructionReply string) bool {
return !stableConversational && identityReply == "" && memoryInstructionReply == ""
}
func shouldReviewResponse(planningReply string, stableKnowledgeReply, stableConversational bool, identityReply, memoryInstructionReply string, deterministicMemoryReply bool) bool {
return strings.TrimSpace(planningReply) != "" && !stableKnowledgeReply && !stableConversational && identityReply == "" && memoryInstructionReply == "" && !deterministicMemoryReply
}
func shouldRunGeneralResearch(kind interaction.InformationRequestKind) bool {
return kind == "" || kind == interaction.InformationRequestTimeDifference
}
@@ -1704,6 +1831,88 @@ func creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword stri
}
}
func creatorDatabaseSessionHandler(bootstrapToken, creatorUsername string, repository *postgres.IdentityRepository, sessionKey []byte, now func() time.Time) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if identitydomain.BootstrapMatches(bootstrapToken, bearerToken(r)) {
token, session, err := identitydomain.IssueCreatorSession(sessionKey, now(), 8*time.Hour)
if err != nil {
http.Error(w, "creator session unavailable", http.StatusServiceUnavailable)
return
}
writeCreatorSession(w, token, session)
return
}
var credentials identity.CreatorCredentials
decoder := json.NewDecoder(io.LimitReader(r.Body, maxCreatorCredentialsBytes+1))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&credentials); err != nil || decoder.Decode(&struct{}{}) != io.EOF {
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
return
}
account, err := repository.LoadCreatorAccount(r.Context(), credentials.Username)
if err != nil || !identitydomain.VerifyCreatorPassword(account.PasswordHash, credentials.Password) {
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
return
}
binding, err := repository.LoadBinding(r.Context(), account.ActorID, account.PrincipalID)
if err != nil || !binding.Active {
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
return
}
token, session, err := identitydomain.IssueCreatorSessionForBinding(sessionKey, account.ActorID, account.PrincipalID, now(), 8*time.Hour)
if err != nil {
http.Error(w, "creator session unavailable", http.StatusServiceUnavailable)
return
}
writeCreatorSession(w, token, session)
}
}
func writeCreatorSession(w http.ResponseWriter, token string, session identity.CreatorSession) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(struct {
Token string
Session identity.CreatorSession
}{token, session})
}
func bootstrapCreatorAccount(ctx context.Context, repository *postgres.IdentityRepository, username, passwordPath string) error {
if account, err := repository.LoadCreatorAccount(ctx, username); err == nil {
if account.ActorID == "" || account.PrincipalID == "" {
return errors.New("creator account binding is incomplete")
}
return nil
} else if !errors.Is(unwrapIdentityRepositoryError(err), pgx.ErrNoRows) {
return err
}
password, err := readRequiredSecretFile("CREATOR_PASSWORD", passwordPath)
if err != nil {
return fmt.Errorf("creator account is missing and bootstrap password is unavailable: %w", err)
}
hash, err := identitydomain.HashCreatorPassword(password)
if err != nil {
return fmt.Errorf("hash creator bootstrap password: %w", err)
}
if err := repository.CreateCreatorAccount(ctx, identity.CreatorAccount{Username: username, ActorID: "domovoy-creator-actor", PrincipalID: "domovoy-creator-principal", PasswordHash: hash}); err != nil {
return fmt.Errorf("create creator account: %w", err)
}
return nil
}
func unwrapIdentityRepositoryError(err error) error {
for err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return pgx.ErrNoRows
}
unwrapped := errors.Unwrap(err)
if unwrapped == nil {
break
}
err = unwrapped
}
return err
}
func bearerToken(r *http.Request) string {
value := r.Header.Get("Authorization")
if !strings.HasPrefix(value, "Bearer ") {
@@ -1726,10 +1935,7 @@ func readRequiredSecret(name string) (string, error) {
if err != nil {
return "", fmt.Errorf("read %s_FILE: %w", name, err)
}
direct = strings.TrimRight(string(contents), "\r\n")
if strings.ContainsAny(direct, "\r\n") {
return "", fmt.Errorf("%s_FILE must contain one line", name)
}
return normalizeSecretFile(name, contents)
}
if direct == "" {
return "", fmt.Errorf("%s is required", name)
@@ -1737,13 +1943,28 @@ func readRequiredSecret(name string) (string, error) {
return direct, nil
}
func readCredentialRefSecret(name, refName string) (string, error) {
if _, directSet := os.LookupEnv(name); directSet {
return "", fmt.Errorf("%s direct environment secret is forbidden; use %s_FILE", name, name)
func readRequiredSecretFile(name, path string) (string, error) {
contents, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read %s secret file: %w", name, err)
}
fileName := os.Getenv(name + "_FILE")
if fileName == "" {
return "", fmt.Errorf("%s_FILE is required", name)
return normalizeSecretFile(name, contents)
}
func normalizeSecretFile(name string, contents []byte) (string, error) {
value := strings.TrimRight(string(contents), "\r\n")
if strings.ContainsAny(value, "\r\n") {
return "", fmt.Errorf("%s secret file must contain one line", name)
}
if value == "" {
return "", fmt.Errorf("%s is required", name)
}
return value, nil
}
func readCredentialRefSecret(name, refName, fileName string) (string, error) {
if _, directSet := os.LookupEnv(name); directSet {
return "", fmt.Errorf("%s direct environment secret is forbidden; use the standard secret file", name)
}
ref := os.Getenv(refName)
if ref == "" {
+2
View File
@@ -841,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/"):
+260 -106
View File
@@ -1,131 +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 { width: 100%; min-width: 0; 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; }
.generated-image { display: block; max-width: min(100%, 640px); height: auto; margin-top: 12px; border: 1px solid var(--line); }
.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); }
.media-tools { width: min(100%, 920px); margin: 0 auto; padding: 0 18px 10px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .06em; }
.media-tools label { display: inline-flex; align-items: center; gap: 6px; min-height: 30px; padding: 0 8px; border: 1px solid var(--line); color: var(--ink); cursor: pointer; }
.media-tools input { max-width: 150px; font: 10px var(--mono); }
.media-tools span { color: var(--signal); }
.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; flex: 1 1 110px; min-width: 0; max-width: 110px; overflow: hidden; text-overflow: ellipsis; }
.chat-header .cognitive-console-link { flex: 0 1 110px; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.chat-header > .operator-link, .build-meta { display: none; }
.chat-header > #logout { flex: 0 0 38px; }
.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; }
}
+168 -24
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() {
@@ -106,7 +148,22 @@ function showLogin() {
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;
@@ -129,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 = {}) {
@@ -278,9 +370,11 @@ 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);
@@ -290,7 +384,9 @@ async function sendChatMessage(text, options = {}) {
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 сейчас недоступен. Встроенный канал проверяется; повторите сообщение через несколько секунд.");
@@ -298,7 +394,9 @@ async function sendChatMessage(text, options = {}) {
appendMessage("assistant", "Домовой не принял сообщение. Проверьте текст и повторите попытку.");
}
} finally {
sendButton.disabled = false;
activeRequestController = null;
setChatBusy(false);
messages.removeAttribute("aria-busy");
messageInput.focus();
}
}
@@ -331,12 +429,23 @@ 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);
});
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;
@@ -365,18 +474,37 @@ if (audioInput) audioInput.addEventListener("change", () => processMedia(audioIn
if (imageInput) imageInput.addEventListener("change", () => processMedia(imageInput, "/api/multimodal/understand", "Описание изображения"));
if (ocrInput) ocrInput.addEventListener("change", () => processMedia(ocrInput, "/api/multimodal/ocr", "Текст с изображения"));
for (const tab of navTabs) {
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
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));
}
for (const starter of conversationStarters) {
starter.addEventListener("click", async () => {
const text = starter.dataset.starter || "";
if (!text || sendButton.disabled) return;
messageInput.value = "";
await sendChatMessage(text);
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);
@@ -384,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;
@@ -396,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) {
@@ -405,3 +548,4 @@ if (token) {
} else {
showLogin();
}
resizeComposerInput();
+55 -28
View File
@@ -25,53 +25,61 @@
<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="media-tools" aria-label="Мультимодальные сообщения">
<label>Аудио <input id="audio-input" type="file" accept="audio/*"></label>
<label>Фото <input id="image-input" type="file" accept="image/*"></label>
<label>Текст с фото <input id="ocr-input" type="file" accept="image/*"></label>
<div class="media-tools composer-runtime" aria-label="Состояние вложений">
<span id="media-status" role="status" aria-live="polite"></span>
</div>
<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>
</section>
<section id="today-panel" class="surface-panel attention-surface" hidden>
<div class="surface-heading"><p>Сегодня / внимание</p><button id="refresh-today" type="button">Обновить</button></div>
@@ -82,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>
@@ -96,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>
+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})
}
+6 -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()
@@ -140,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 == "" {
@@ -152,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})
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
const (
defaultRoot = "/run/control-plane-secrets"
defaultRuntimeUID = 100
defaultRuntimeGID = 101
)
func main() {
reason := flag.String("reason", "", "rotation reason: owner-requested-rotation or exposure-detected")
flag.Parse()
if flag.NArg() != 1 {
fail("usage: secret-control-plane [ensure|rotate]")
}
store, err := platformsecrets.NewLocalMachineCredentialStore(defaultRoot, defaultRuntimeUID, defaultRuntimeGID, nil)
if err != nil {
fail("control-plane credential store unavailable")
}
spec := platformsecrets.ControlPlaneServiceTokenSpec()
switch strings.TrimSpace(flag.Arg(0)) {
case "ensure":
result, err := store.Ensure(spec)
if err != nil {
fail("control-plane credential provisioning failed")
}
if result.Created {
fmt.Println("control-plane credential provisioned")
return
}
fmt.Println("control-plane credential already active")
case "rotate":
if !validRotationReason(*reason) {
fail("rotation reason is invalid")
}
if _, err := store.Rotate(spec, *reason); err != nil {
fail("control-plane credential rotation failed")
}
fmt.Println("control-plane credential rotated")
default:
fail("unsupported control-plane credential operation")
}
}
func validRotationReason(reason string) bool {
switch strings.TrimSpace(reason) {
case "owner-requested-rotation", "exposure-detected":
return true
default:
return false
}
}
func fail(message string) {
fmt.Fprintln(os.Stderr, message)
os.Exit(1)
}
+15
View File
@@ -0,0 +1,15 @@
package main
import "testing"
func TestSecretControlPlaneDefaultsUseDedicatedProtectedRoot(t *testing.T) {
if defaultRoot != "/run/control-plane-secrets" || defaultRuntimeUID <= 0 || defaultRuntimeGID <= 0 {
t.Fatal("unsafe control-plane defaults")
}
}
func TestRotationReasonsAreBoundedAndNeverArbitraryDiagnosticText(t *testing.T) {
if !validRotationReason("owner-requested-rotation") || !validRotationReason("exposure-detected") || validRotationReason("Bearer secret-value") {
t.Fatal("unsafe rotation reason validation")
}
}
+311
View File
@@ -0,0 +1,311 @@
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) {
state, snapshotErr := s.lifeboat.Snapshot()
if snapshotErr != nil {
respond(w, false, snapshotErr)
return
}
// A ReleaseSet must roll back as a whole. The legacy release-only method
// would merely flip the traffic mode while retaining a candidate upstream.
var ok bool
var err error
if state.StableReleaseSet != nil {
ok, err = s.lifeboat.RollbackSet()
} else {
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) }
+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
+18 -3
View File
@@ -27,15 +27,30 @@ type StrategyDecision struct {
Escalate bool
}
// StrategyEligibility is supplied by Capability lifecycle resolution. It keeps
// a requested cheap route from becoming an entitlement to use stale evidence.
type StrategyEligibility struct {
RuleEligible bool
ReflexEligible bool
SkillEligible bool
}
func (r StrategyRequest) Select() StrategyDecision {
return r.SelectEligible(StrategyEligibility{RuleEligible: true, ReflexEligible: true, SkillEligible: true})
}
func (r StrategyRequest) SelectEligible(eligible StrategyEligibility) 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 {
if eligible.RuleEligible && r.RequiredQuality <= 500 && r.CostBudgetMicros > 0 {
return StrategyDecision{Path: PathRule, Reason: "bounded low-risk path"}
}
if r.PreferLowLatency {
if eligible.ReflexEligible && r.PreferLowLatency {
return StrategyDecision{Path: PathReflex, Reason: "latency preference with later escalation"}
}
return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"}
if eligible.SkillEligible {
return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"}
}
return StrategyDecision{Path: PathSmallLocal, Reason: "no eligible reflex or skill; escalate to reasoning", Escalate: true}
}
+11 -7
View File
@@ -15,11 +15,15 @@ const (
)
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"`
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"`
}
+16
View File
@@ -10,6 +10,7 @@ const (
ReflexActive ReflexLifecycle = "ACTIVE"
ReflexDegraded ReflexLifecycle = "DEGRADED"
ReflexStale ReflexLifecycle = "STALE"
ReflexRevalidating ReflexLifecycle = "REVALIDATING"
ReflexSuspended ReflexLifecycle = "SUSPENDED"
ReflexRetired ReflexLifecycle = "RETIRED"
ReflexDeterministicCode ReflexKind = "DETERMINISTIC_CODE"
@@ -58,4 +59,19 @@ type ReflexRevision struct {
LastValidatedAt int64
Lifecycle ReflexLifecycle
RecordedAt int64
SupersedesRevision int64
ExecutionCount int64
FailureCount int64
DivergenceCount int64
}
// ShadowComparison is evidence produced by a bounded shadow sample. It is not
// permission to rewrite a Reflex; SELF/governed evaluation decide next steps.
type ShadowComparison struct {
ReflexID ReflexID
ReflexRevision int64
EvidenceRefs []SkillEvidenceRef
VerifierPassed bool
Diverged bool
ObservedAt int64
}
+10
View File
@@ -70,6 +70,7 @@ const (
SkillCandidate SkillLifecycleStatus = "CANDIDATE"
SkillEvaluating SkillLifecycleStatus = "EVALUATING"
SkillValidated SkillLifecycleStatus = "VALIDATED"
SkillActive SkillLifecycleStatus = "ACTIVE"
SkillDegraded SkillLifecycleStatus = "DEGRADED"
SkillSuspended SkillLifecycleStatus = "SUSPENDED"
SkillRetired SkillLifecycleStatus = "RETIRED"
@@ -202,6 +203,15 @@ type SkillProfile struct {
ObservedFailureCount int64
ObservedCorrectionCount int64
RecordedAtUnixMillis int64
// The fields below are the additive Gen2 lifecycle view. The older fields
// remain a compatibility projection; they are not another Skill registry.
Applicability ApplicabilitySpec
DependencyRefs []string
ValidationEvidenceRefs []SkillEvidenceRef
IndependentEvaluationRefs []SkillEvidenceRef
KnownFailureConditions []string
VerifiedOutcomeCount int64
MustNotWorsenSatisfied bool
}
type SkillOutcomeObservation struct {
+87 -13
View File
@@ -36,6 +36,14 @@ type ContextOmission struct {
Reason string
}
// ContextInclusion records the bounded compiler decision without exposing
// source text. It is trace-only metadata and is never user-facing output.
type ContextInclusion struct {
ID ContextExcerptID
Lane string
Reason string
}
type ContextBundle struct {
Excerpts []ContextExcerpt
Omissions []ContextOmission
@@ -45,19 +53,60 @@ type ContextBundle struct {
type ContextAssemblyTrace struct {
Included []ContextExcerptID
Inclusions []ContextInclusion
Excluded []ContextOmission
Coverage string
CompilerVersion string
}
type IterativeRetrievalLane string
const (
IterativeRetrievalCurrentFact IterativeRetrievalLane = "CURRENT_FACT"
IterativeRetrievalPositiveExperience IterativeRetrievalLane = "POSITIVE_EXPERIENCE"
IterativeRetrievalNegativeExperience IterativeRetrievalLane = "NEGATIVE_EXPERIENCE"
IterativeRetrievalContradiction IterativeRetrievalLane = "CONTRADICTION"
IterativeRetrievalHistoricalAnalogy IterativeRetrievalLane = "HISTORICAL_ANALOGY"
)
// IterativeRetrievalRequest is the model-visible, storage-agnostic intent.
// Principal, Scope, Focus and policy are deliberately absent: Core binds them
// from the already-authorized request and never accepts them from a model.
type IterativeRetrievalRequest struct {
Query string
RemainingBytes int
Pass int
Query string
SemanticSubject string
Lane IterativeRetrievalLane
Reason string
RemainingBytes int
Pass int
}
// IterativeRetrievalBudget is a Core-enforced per-request budget.
type IterativeRetrievalBudget struct {
MaxRounds int
MaxFragments int
MaxAddedBytes int
MaxLatencyMillis int64
}
// IterativeRetrievalEnvelope is the trusted continuation input assembled by
// Core. It is never produced by a provider or model adapter.
type IterativeRetrievalEnvelope struct {
Request IterativeRetrievalRequest
PrincipalID string
ContextScopeRef string
FocusFrameRef string
TaskRef string
FreshnessRequirement string
DataPolicy capability.DataPolicy
Budget IterativeRetrievalBudget
SeenExcerptIDs []ContextExcerptID
}
type IterativeRetrievalResponse struct {
Candidates []ContextCandidate
Exhausted bool
Reason string
AddedBytes int
}
// CompileContext deterministically selects permitted, relevant excerpts for a
@@ -67,14 +116,9 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
return ContextBundle{}, ErrInvalidContextBudget
}
candidates := append([]ContextCandidate(nil), input.Candidates...)
sort.SliceStable(candidates, func(i, j int) bool {
if candidates[i].Relevance != candidates[j].Relevance {
return candidates[i].Relevance > candidates[j].Relevance
}
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
})
bundle := ContextBundle{Excerpts: []ContextExcerpt{}, Omissions: []ContextOmission{}}
seen := make(map[ContextExcerptID]struct{}, len(candidates))
eligible := make([]ContextCandidate, 0, len(candidates))
for _, candidate := range candidates {
excerpt := candidate.Excerpt
if _, ok := seen[excerpt.ID]; ok {
@@ -86,8 +130,7 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "context_scope"})
continue
}
reason := permittedContextReason(excerpt, input.DataPolicy)
if reason != "" {
if reason := permittedContextReason(excerpt, input.DataPolicy); reason != "" {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason})
continue
}
@@ -95,14 +138,45 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "empty"})
continue
}
eligible = append(eligible, candidate)
}
candidates = eligible
sort.SliceStable(candidates, func(i, j int) bool {
if candidates[i].Relevance != candidates[j].Relevance {
return candidates[i].Relevance > candidates[j].Relevance
}
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
})
included := make(map[ContextExcerptID]struct{}, len(candidates))
include := func(candidate ContextCandidate, reason string) bool {
excerpt := candidate.Excerpt
bytes := len([]byte(excerpt.Text))
if bundle.Bytes+bytes > input.MaxBytes {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "budget"})
continue
return false
}
bundle.Excerpts = append(bundle.Excerpts, excerpt)
bundle.Bytes += bytes
bundle.Trace.Included = append(bundle.Trace.Included, excerpt.ID)
bundle.Trace.Inclusions = append(bundle.Trace.Inclusions, ContextInclusion{ID: excerpt.ID, Lane: candidate.Lane, Reason: reason})
included[excerpt.ID] = struct{}{}
return true
}
// Preserve one candidate from each semantic lane before filling by score so
// many similar current facts cannot erase a relevant failure or conflict.
for _, lane := range []string{"current-fact", "positive-experience", "negative-experience", "contradiction", "historical-analogy", "interaction"} {
for _, candidate := range candidates {
if candidate.Lane == lane && include(candidate, "lane_coverage") {
break
}
}
}
for _, candidate := range candidates {
if _, ok := included[candidate.Excerpt.ID]; ok {
continue
}
if !include(candidate, "salience") {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: candidate.Excerpt.ID, Reason: "budget"})
}
}
bundle.Trace.Excluded = append([]ContextOmission(nil), bundle.Omissions...)
bundle.Trace.CompilerVersion = "context-compiler-v2-additive"
@@ -12,3 +12,19 @@ func TestContextCompilerV2TraceAndBudgetRemainDeterministic(t *testing.T) {
t.Fatalf("bundle=%#v err=%v", b, err)
}
}
func TestContextCompilerPreservesNegativeLaneWithinBoundedBudget(t *testing.T) {
b, err := cognitive.CompileContext(cognitive.ContextCompileInput{MaxBytes: 27, DataPolicy: capability.DataPolicyLocalOnly, Candidates: []cognitive.ContextCandidate{
{Excerpt: cognitive.ContextExcerpt{ID: "current", Text: "current-value", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact", Relevance: 100},
{Excerpt: cognitive.ContextExcerpt{ID: "failure", Text: "known-failure", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "negative-experience", Relevance: 1, Negative: true},
{Excerpt: cognitive.ContextExcerpt{ID: "crowd", Text: "similar-current", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact", Relevance: 99},
}})
if err != nil || len(b.Excerpts) != 2 || b.Excerpts[0].ID != "current" || b.Excerpts[1].ID != "failure" {
t.Fatalf("bundle=%#v err=%v", b, err)
}
// The negative record fits and is retained before a lower-value crowd of
// similar facts can consume the remaining bounded budget.
if len(b.Trace.Inclusions) != 2 || b.Trace.Inclusions[0].Reason != "lane_coverage" || b.Trace.Inclusions[1].Reason != "lane_coverage" {
t.Fatalf("trace=%#v", b.Trace)
}
}
+14 -12
View File
@@ -69,18 +69,19 @@ type AdvisoryRequest struct {
// GatewayAdvisoryRequest describes the task and policy requirement. It cannot
// name a provider, model or binding; Capability owns that selection.
type GatewayAdvisoryRequest struct {
Version ContractVersion
RequestID RequestID
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
Message string
Context []ContextExcerpt
Locale string
MaxOutputBytes int
DataPolicy capability.DataPolicy
EgressAcknowledged bool
Meaning TaskMeaning
Version ContractVersion
RequestID RequestID
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
Message string
Context []ContextExcerpt
Locale string
MaxOutputBytes int
DataPolicy capability.DataPolicy
EgressAcknowledged bool
AllowIterativeRetrieval bool
Meaning TaskMeaning
}
// TaskMeaning is the trusted semantic summary produced by Core. It is not a
@@ -177,4 +178,5 @@ type GatewayAdvisoryResponse struct {
ReferencedExcerptIDs []ContextExcerptID
FinishReason FinishReason
Usage *ProviderUsage
IterativeRetrieval *IterativeRetrievalRequest
}
+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
}
+3
View File
@@ -81,6 +81,9 @@ type BootstrapConfig struct {
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")
+18
View File
@@ -56,6 +56,24 @@ type FreshnessAssessment struct {
RevalidationAttemptRef string
AssessedAtUnixMillis int64
}
// FreshnessRequirement is supplied by the current task/focus owner. It is an
// evaluation input, not Epistemics-owned task state, so freshness remains
// relative to the decision being made.
type FreshnessRequirement struct {
Mode string
MaximumAgeMillis *int64
RequiredVersionRefs []SemanticReference
}
// WithExactVersionBinding is a typed contract helper for callers that need to
// attach an exact environment/version applicability constraint without taking
// a direct dependency on the World package.
func WithExactVersionBinding(value TemporalApplicability, subject, version SemanticReference) TemporalApplicability {
value.VersionBindings = append(value.VersionBindings, world.VersionBinding{SubjectRef: subject, VersionRef: version, MatchMode: world.VersionExact})
return value
}
type RevalidationRequest struct {
ID string
SourceKnowledgeRef string
+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
}
+34 -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
+10
View File
@@ -102,6 +102,7 @@ type SystemExpectation struct {
// state or grants authority.
type ExperienceRecord struct {
ID ExperienceRecordID
PrincipalID string
SourceKind ExperienceSourceKind
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
@@ -133,6 +134,15 @@ type ExperienceRecord struct {
WhySucceededOrFailed string
}
// ConsolidationCandidate is a rebuildable Epistemics projection. It preserves
// source ExperienceRecord references and cannot activate a Skill by itself.
type ConsolidationCandidate struct {
EpisodeRef string
ExperienceRefs []ExperienceRecordID
PropositionText string
SkillCandidate bool
}
// ReflectionCandidate records that broader reflection may be warranted.
// Cognitive reflection, if used, must go through Cognitive Gateway/Router and
// produce candidates only; it cannot directly rewrite canonical state.
+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 {
+52
View File
@@ -25,6 +25,44 @@ 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 (
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"
@@ -75,6 +113,8 @@ const (
InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY"
InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE"
InformationRequestResearchFailureExplanation InformationRequestKind = "RESEARCH_FAILURE_EXPLANATION"
ConfigurationCommandSet ConfigurationCommandKind = "SET"
ConfigurationCommandQuery ConfigurationCommandKind = "QUERY"
)
type Conversation struct {
@@ -106,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
+4 -2
View File
@@ -8,8 +8,9 @@ type CredentialRef string
type SecretPurpose string
const (
SecretPurposeDatabasePassword SecretPurpose = "DATABASE_PASSWORD"
SecretPurposeProviderAPIKey SecretPurpose = "PROVIDER_API_KEY"
SecretPurposeDatabasePassword SecretPurpose = "DATABASE_PASSWORD"
SecretPurposeProviderAPIKey SecretPurpose = "PROVIDER_API_KEY"
SecretPurposeServiceAuthentication SecretPurpose = "SERVICE_AUTHENTICATION"
)
// RuntimeConsumer identifies a trusted runtime allowed to resolve a credential.
@@ -17,6 +18,7 @@ type RuntimeConsumer string
const (
RuntimeConsumerCore RuntimeConsumer = "core-runtime"
RuntimeConsumerCommit RuntimeConsumer = "commit-runtime"
RuntimeConsumerWorkWorker RuntimeConsumer = "agent-worker"
RuntimeConsumerEpistemicsWorker RuntimeConsumer = "reflection-worker"
RuntimeConsumerCognitive RuntimeConsumer = "cognitive-worker"
+12
View File
@@ -1,6 +1,18 @@
package self
type BenchmarkCaseRef string
type BenchmarkCase struct {
ID string
PrincipalID string
TaskClass string
EvidenceRefs []SemanticReference
RecordedAt int64
}
func (c BenchmarkCase) Valid() bool {
return c.ID != "" && c.PrincipalID != "" && c.TaskClass != "" && len(c.EvidenceRefs) > 0 && c.RecordedAt > 0
}
type PersonalBenchmark struct {
ID string
PrincipalID string
+161 -9
View File
@@ -6,6 +6,58 @@ type AssessmentID string
type LearningLedgerEntryID string
type AssessmentState string
type LearningSignalKind string
type DiagnosisClass string
type EpistemicStatus string
type InterventionClass string
type DecisionRegretKind string
type CandidateState string
const (
SignalVerifiedFailure LearningSignalKind = "VERIFIED_FAILURE"
SignalUserCorrection LearningSignalKind = "USER_CORRECTION"
SignalRepeatedMistake LearningSignalKind = "REPEATED_KNOWN_MISTAKE"
SignalReflexDivergence LearningSignalKind = "REFLEX_DIVERGENCE"
SignalSkillFallback LearningSignalKind = "SKILL_FALLBACK"
SignalUnexpectedEscalation LearningSignalKind = "UNEXPECTED_ESCALATION"
SignalOutcomeMismatch LearningSignalKind = "OUTCOME_MISMATCH"
SignalRetryFallback LearningSignalKind = "RETRY_FALLBACK"
SignalStaleDependency LearningSignalKind = "STALE_DEPENDENCY"
SignalDecisionRegret LearningSignalKind = "DECISION_REGRET"
SignalBetterSolution LearningSignalKind = "BETTER_SOLUTION"
DiagnosisMemoryMissing DiagnosisClass = "MEMORY_MISSING"
DiagnosisMemoryNotRetrieved DiagnosisClass = "MEMORY_NOT_RETRIEVED"
DiagnosisWrongScope DiagnosisClass = "WRONG_SCOPE"
DiagnosisStaleKnowledge DiagnosisClass = "STALE_KNOWLEDGE"
DiagnosisContextSelection DiagnosisClass = "CONTEXT_SELECTION"
DiagnosisReasoningFailure DiagnosisClass = "REASONING_FAILURE"
DiagnosisSkillFailure DiagnosisClass = "SKILL_FAILURE"
DiagnosisReflexFailure DiagnosisClass = "REFLEX_FAILURE"
DiagnosisToolFailure DiagnosisClass = "TOOL_FAILURE"
DiagnosisProviderFailure DiagnosisClass = "PROVIDER_FAILURE"
DiagnosisOutcomeVerificationFailure DiagnosisClass = "OUTCOME_VERIFICATION_FAILURE"
DiagnosisRoutingInefficiency DiagnosisClass = "ROUTING_INEFFICIENCY"
DiagnosisUnknown DiagnosisClass = "UNKNOWN_CAUSE"
EpistemicKnown EpistemicStatus = "KNOWN"
EpistemicSupported EpistemicStatus = "SUPPORTED"
EpistemicHypothesis EpistemicStatus = "HYPOTHESIS"
EpistemicUnknown EpistemicStatus = "UNKNOWN"
InterventionConfiguration InterventionClass = "CONFIGURATION"
InterventionSkillReview InterventionClass = "SKILL_REVIEW"
InterventionReflexReview InterventionClass = "REFLEX_REVIEW"
InterventionRoutingReview InterventionClass = "ROUTING_REVIEW"
InterventionProviderReview InterventionClass = "PROVIDER_REVIEW"
InterventionContextReview InterventionClass = "CONTEXT_REVIEW"
RegretUnavailableThen DecisionRegretKind = "INFORMATION_UNAVAILABLE_THEN"
RegretAvailableUnused DecisionRegretKind = "INFORMATION_AVAILABLE_UNUSED"
RegretIgnoredInContext DecisionRegretKind = "EVIDENCE_IGNORED_IN_CONTEXT"
CandidateProposed CandidateState = "PROPOSED"
CandidateEvaluating CandidateState = "EVALUATING"
CandidateKept CandidateState = "KEPT"
CandidateRejected CandidateState = "REJECTED"
CandidateRolledBack CandidateState = "ROLLED_BACK"
)
const (
AssessmentCandidate AssessmentState = "CANDIDATE"
AssessmentConfirmed AssessmentState = "CONFIRMED"
@@ -37,6 +89,17 @@ type WeaknessPattern struct {
DetectionEvidenceRefs []SemanticReference
State AssessmentState
RecordedAt int64
TaskPattern string
ScopeKey string
EnvironmentVersion string
DependencyRefs []SemanticReference
Frequency int
LastObservedAt int64
Severity int
AffectedComponent string
AffectedStrategy string
ConfidenceMillis int64
EpistemicStatus EpistemicStatus
}
type DependencyAssessment struct {
ID AssessmentID
@@ -48,15 +111,83 @@ type DependencyAssessment struct {
AssessedAt int64
}
type ImprovementNeed struct {
ID AssessmentID
PrincipalID string
SubjectRef SemanticReference
ProblemSummary string
EvidenceRefs []SemanticReference
ExpectedBenefit string
Priority int
State string
CreatedAt int64
ID AssessmentID
PrincipalID string
SubjectRef SemanticReference
ProblemSummary string
EvidenceRefs []SemanticReference
ExpectedBenefit string
Priority int
State string
CreatedAt int64
Diagnosis DiagnosisClass
DiagnosisStatus EpistemicStatus
AffectedComponent string
ScopeKey string
Risk string
Intervention InterventionClass
Hypothesis string
EvaluationPlan string
MustNotWorsen string
RollbackPlan string
ResourceCost string
}
type LearningSignal struct {
ID string
PrincipalID string
Kind LearningSignalKind
TaskPattern string
ScopeKey string
SubjectRef SemanticReference
EvidenceRefs []SemanticReference
ExpectedOutcome string
ActualOutcome string
DependencyRefs []SemanticReference
EnvironmentVersion string
ObservedAt int64
Severity int
UserImpact int
CorrectionCount int
DecisionRegret DecisionRegretKind
}
type Diagnosis struct {
Class DiagnosisClass
Status EpistemicStatus
Rationale string
EvidenceRefs []SemanticReference
ConfidenceMillis int64
}
type ImprovementCandidate struct {
ID string
PrincipalID string
NeedRef SemanticReference
Hypothesis string
ExpectedEffect string
EvaluationPlan string
MustNotWorsen []string
RollbackPlan string
ResourceCost string
Creator string
IndependentEvaluator string
State CandidateState
EvidenceRefs []SemanticReference
CreatedAt int64
}
type ImprovementEvaluation struct {
CandidateRef SemanticReference
Evaluator string
EvidenceRefs []SemanticReference
ExpectedQuality int64
ActualQuality int64
ExpectedLatency int64
ActualLatency int64
MustNotWorsenPass bool
Decision CandidateState
EvaluatedAt int64
}
type LearningLedgerEntry struct {
ID LearningLedgerEntryID
@@ -90,3 +221,24 @@ func (l LearningLedgerEntry) Validate() error {
}
return nil
}
func (s LearningSignal) Validate() error {
if s.ID == "" || s.PrincipalID == "" || s.Kind == "" || s.TaskPattern == "" || s.ScopeKey == "" || !s.SubjectRef.Valid() || len(s.EvidenceRefs) == 0 || s.ObservedAt < 1 {
return fmt.Errorf("learning signal is incomplete")
}
return nil
}
func (d Diagnosis) Validate() error {
if d.Class == "" || d.Status == "" || len(d.EvidenceRefs) == 0 || d.ConfidenceMillis < 0 || d.ConfidenceMillis > 1000 {
return fmt.Errorf("diagnosis requires bounded evidence")
}
return nil
}
func (c ImprovementCandidate) Validate() error {
if c.ID == "" || c.PrincipalID == "" || !c.NeedRef.Valid() || c.Hypothesis == "" || c.ExpectedEffect == "" || c.EvaluationPlan == "" || c.MustNotWorsen == nil || c.RollbackPlan == "" || c.Creator == "" || c.IndependentEvaluator == "" || c.Creator == c.IndependentEvaluator || len(c.EvidenceRefs) == 0 || c.CreatedAt < 1 {
return fmt.Errorf("candidate lacks independent bounded evaluation")
}
return nil
}
+24
View File
@@ -0,0 +1,24 @@
package work
type ConsequenceAssessment struct {
ID string
TargetRefs []string
Impact int64
Reversibility int64
Uncertainty int64
Scope int64
Recoverability int64
CostOfError int64
ExpectedConsequences []string
MitigationOptions []string
RequiresDryRun bool
RequiresBackup bool
RecommendedConfirmation string
OwnerAccepted bool
EvidenceRefs []EvidenceRef
RecordedAtUnixMillis int64
}
func (a ConsequenceAssessment) Valid() bool {
return a.ID != "" && len(a.TargetRefs) > 0 && a.Impact >= 0 && a.Impact <= 1000 && a.Reversibility >= 0 && a.Reversibility <= 1000 && a.Uncertainty >= 0 && a.Uncertainty <= 1000 && a.Recoverability >= 0 && a.Recoverability <= 1000 && a.RecordedAtUnixMillis > 0
}
+39
View File
@@ -0,0 +1,39 @@
package work
type EvolutionRunStage string
const (
EvolutionNeedRecorded EvolutionRunStage = "IMPROVEMENT_NEED"
EvolutionCandidateBuilt EvolutionRunStage = "CANDIDATE"
EvolutionIndependentlyEvaluated EvolutionRunStage = "INDEPENDENT_EVALUATION"
EvolutionSandboxed EvolutionRunStage = "SANDBOX_BUILD"
EvolutionBenchmarked EvolutionRunStage = "BENCHMARK"
EvolutionCanaried EvolutionRunStage = "CANARY"
EvolutionPromoted EvolutionRunStage = "PROMOTION"
EvolutionRolledBack EvolutionRunStage = "ROLLBACK"
)
type GovernedEvolutionResult struct {
NeedRef string
CandidateRef string
EvaluationRef string
Stages []EvolutionRunStage
EvidenceRefs []EvidenceRef
LearningLedgerOutcomeRef string
Promoted bool
RollbackRef string
}
// GovernedEvolutionOps are explicit gates. The caller supplies existing
// development executor/supervisor operations; this interface grants no new
// authority and has no autonomous promotion implementation.
type GovernedEvolutionOps struct {
CreateCandidate func() (EvolutionCandidate, error)
Evaluate func(EvolutionCandidate) (EvolutionEvaluation, error)
SandboxBuild func(EvolutionCandidate) (EvidenceRef, error)
Benchmark func(EvolutionCandidate) (EvidenceRef, bool, error)
Canary func(EvolutionCandidate) (EvidenceRef, bool, error)
Promote func(EvolutionCandidate, EvolutionEvaluation) (string, error)
Rollback func(EvolutionCandidate) (string, error)
LearningLedgerOutcome func(GovernedEvolutionResult) (string, error)
}
+16
View File
@@ -30,6 +30,22 @@ type Investigation struct {
RecordedAtUnixMillis int64
}
// InvestigationCheckpoint is the durable boundary between attempts. A
// checkpoint is written only after an attempt has been verified, so a worker
// restart can resume at NextStep without replaying that attempt.
type InvestigationCheckpoint struct {
ID AgentCheckpointID
InvestigationID LongRunningWorkID
CompletedAttemptRefs []AgentRunID
CompletedFindingRefs []EvidenceRef
NextStep string
RecordedAtUnixMillis int64
}
func (c InvestigationCheckpoint) Valid() bool {
return c.ID != "" && c.InvestigationID != "" && c.NextStep != "" && c.RecordedAtUnixMillis > 0
}
func (w Investigation) Valid() bool {
return w.ID != "" && w.Objective != "" && w.State != "" && w.Revision > 0 && w.TimeBudgetMillis >= 0 && w.CostBudgetMicros >= 0 && w.MaxAttempts >= 0 && w.RecordedAtUnixMillis > 0
}
+7 -1
View File
@@ -4,7 +4,10 @@ type PerspectiveID string
type PerspectiveSpec struct {
ID PerspectiveID
ObjectiveLens string
FocusRef string
RetrievalProfileRef string
SkillRefs []string
ToolRefs []string
EvaluationCriteria []string
CapabilityRequirements []string
ExpectedOutputShape string
@@ -13,6 +16,9 @@ type PerspectiveSpec struct {
type SpecialistAssignment struct {
Perspective PerspectiveSpec
SubtaskRef string
FocusRef string
SkillRefs []string
ToolRefs []string
EvidenceRefs []EvidenceRef
}
type TeamDecision struct {
@@ -22,5 +28,5 @@ type TeamDecision struct {
}
func (p PerspectiveSpec) Valid() bool {
return p.ID != "" && p.ObjectiveLens != "" && len(p.EvaluationCriteria) > 0 && p.ExpectedOutputShape != ""
return p.ID != "" && p.ObjectiveLens != "" && p.FocusRef != "" && len(p.SkillRefs) > 0 && len(p.ToolRefs) > 0 && len(p.EvaluationCriteria) > 0 && p.ExpectedOutputShape != ""
}
+36
View File
@@ -0,0 +1,36 @@
package work
type EvolutionChangeLevel string
const (
EvolutionKnowledge EvolutionChangeLevel = "KNOWLEDGE"
EvolutionBehavior EvolutionChangeLevel = "BEHAVIOR"
EvolutionCapability EvolutionChangeLevel = "CAPABILITY"
EvolutionCore EvolutionChangeLevel = "CORE"
)
type EvolutionCandidate struct {
ID string
ProposalRef string
ChangeLevel EvolutionChangeLevel
CreatorIdentity string
BaseVersionRef string
CandidateVersionRef string
MustNotWorsen []string
SandboxEvidenceRefs []EvidenceRef
BenchmarkEvidenceRefs []EvidenceRef
RollbackRef string
State string
}
type EvolutionEvaluation struct {
CandidateRef string
EvaluatorIdentity string
Approved bool
EvidenceRefs []EvidenceRef
MustNotWorsenPassed bool
RecordedAtUnixMillis int64
}
func (e EvolutionEvaluation) ValidFor(c EvolutionCandidate) bool {
return e.CandidateRef == c.ID && e.EvaluatorIdentity != "" && e.EvaluatorIdentity != c.CreatorIdentity && e.Approved && e.MustNotWorsenPassed && len(e.EvidenceRefs) > 0 && e.RecordedAtUnixMillis > 0
}
+10
View File
@@ -104,6 +104,16 @@ type ValueTypeSpec struct {
type WorldObjectID ids.StableID
type KindDefinitionID ids.StableID
type WorldAliasID ids.StableID
type PersonReferent struct {
WorldObjectRef SemanticReference
DisplayName string
ConfidenceMillis int64
}
func (p PersonReferent) Valid() bool {
return p.WorldObjectRef.Valid() && p.DisplayName != "" && p.ConfidenceMillis >= 0 && p.ConfidenceMillis <= 1000
}
type FacetDefinitionID ids.StableID
type RelationDefinitionID ids.StableID
type ResolutionDecisionID ids.StableID
@@ -0,0 +1,58 @@
-- Gen2 owner-role provisioning for the dedicated bootstrap/migration principal.
-- This is intentionally separate from feature migrations and may be run repeatedly.
BEGIN;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'world_role') THEN
CREATE ROLE world_role LOGIN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'configuration_role') THEN
CREATE ROLE configuration_role LOGIN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'self_role') THEN
CREATE ROLE self_role LOGIN;
END IF;
END
$$;
DO $$
DECLARE
role_name TEXT;
can_login BOOLEAN;
BEGIN
FOREACH role_name IN ARRAY ARRAY['world_role', 'configuration_role', 'self_role'] LOOP
SELECT rolcanlogin INTO can_login FROM pg_roles WHERE rolname = role_name;
IF can_login IS DISTINCT FROM TRUE THEN
RAISE EXCEPTION 'Gen2 owner role % exists but is not a V1-compatible LOGIN role', role_name;
END IF;
END LOOP;
END
$$;
CREATE SCHEMA IF NOT EXISTS world AUTHORIZATION world_role;
CREATE SCHEMA IF NOT EXISTS configuration AUTHORIZATION configuration_role;
CREATE SCHEMA IF NOT EXISTS self AUTHORIZATION self_role;
REVOKE ALL ON SCHEMA world, configuration, self FROM PUBLIC;
DO $$
DECLARE
schema_name TEXT;
expected_owner TEXT;
actual_owner TEXT;
BEGIN
FOREACH schema_name IN ARRAY ARRAY['world', 'configuration', 'self'] LOOP
expected_owner := schema_name || '_role';
SELECT pg_get_userbyid(nspowner)
INTO actual_owner
FROM pg_namespace
WHERE nspname = schema_name;
IF actual_owner IS DISTINCT FROM expected_owner THEN
RAISE EXCEPTION 'Gen2 schema % is owned by %, expected %', schema_name, actual_owner, expected_owner;
END IF;
END LOOP;
END
$$;
COMMIT;
@@ -0,0 +1,22 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.long_running_work (
investigation_id TEXT PRIMARY KEY, objective TEXT NOT NULL,
hypothesis_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], attempt_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
finding_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], blockers TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
checkpoint_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], next_step TEXT NOT NULL, state TEXT NOT NULL,
revision BIGINT NOT NULL, time_budget_millis BIGINT NOT NULL, cost_budget_micros BIGINT NOT NULL,
max_attempts INTEGER NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (investigation_id <> ''), CHECK (objective <> ''), CHECK (next_step <> ''), CHECK (revision > 0), CHECK (time_budget_millis >= 0), CHECK (cost_budget_micros >= 0), CHECK (max_attempts >= 0), CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS work.investigation_checkpoints (
checkpoint_id TEXT PRIMARY KEY, investigation_id TEXT NOT NULL, completed_attempt_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
completed_finding_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], next_step TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (checkpoint_id <> ''), CHECK (investigation_id <> ''), CHECK (next_step <> ''), CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS work.investigation_attempts (
investigation_id TEXT NOT NULL, attempt_ref TEXT NOT NULL, verified_at_unix_millis BIGINT NOT NULL,
PRIMARY KEY (investigation_id, attempt_ref), CHECK (investigation_id <> ''), CHECK (attempt_ref <> ''), CHECK (verified_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,11 @@
BEGIN;
SET ROLE identity_role;
CREATE TABLE IF NOT EXISTS identity.principal_data_realms (
realm_id TEXT PRIMARY KEY, owner_principal_id TEXT NOT NULL, realm_kind TEXT NOT NULL,
member_principal_ids TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], shared BOOLEAN NOT NULL DEFAULT FALSE,
revision BIGINT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (realm_id <> ''), CHECK (owner_principal_id <> ''), CHECK (realm_kind IN ('PERSONAL','SHARED','PUBLIC')),
CHECK (realm_kind <> 'SHARED' OR shared), CHECK (revision > 0), CHECK (recorded_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,20 @@
BEGIN;
SET ROLE identity_role;
CREATE TABLE IF NOT EXISTS identity.creator_accounts (
username TEXT PRIMARY KEY,
actor_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
password_hash TEXT NOT NULL,
revision BIGINT NOT NULL DEFAULT 1,
password_changed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (username <> ''),
CHECK (actor_id <> ''),
CHECK (principal_id <> ''),
CHECK (password_hash <> ''),
CHECK (revision > 0),
UNIQUE (actor_id, principal_id)
);
CREATE INDEX IF NOT EXISTS creator_accounts_binding_idx ON identity.creator_accounts (actor_id, principal_id);
REVOKE ALL ON identity.creator_accounts FROM PUBLIC;
RESET ROLE;
COMMIT;
@@ -0,0 +1,17 @@
BEGIN;
SET ROLE epistemics_role;
ALTER TABLE epistemics.experience_records
ADD COLUMN IF NOT EXISTS principal_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS context_scope_ref TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS problem_statement TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS applicability_conditions TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
ADD COLUMN IF NOT EXISTS why_succeeded_or_failed TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS experience_records_principal_scope_recorded_idx
ON epistemics.experience_records (principal_id, context_scope_ref, recorded_at_unix_millis DESC);
RESET ROLE;
COMMIT;
@@ -0,0 +1,79 @@
BEGIN;
SET ROLE capability_role;
ALTER TABLE capability.skill_profiles
ADD COLUMN IF NOT EXISTS applicability JSONB NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS dependency_refs TEXT[] NOT NULL DEFAULT '{}',
ADD COLUMN IF NOT EXISTS validation_evidence_refs TEXT[] NOT NULL DEFAULT '{}',
ADD COLUMN IF NOT EXISTS independent_evaluation_refs TEXT[] NOT NULL DEFAULT '{}',
ADD COLUMN IF NOT EXISTS known_failure_conditions TEXT[] NOT NULL DEFAULT '{}',
ADD COLUMN IF NOT EXISTS verified_outcome_count BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS must_not_worsen_satisfied BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE capability.skill_profile_heads DROP CONSTRAINT IF EXISTS skill_profile_heads_status_check;
ALTER TABLE capability.skill_profile_heads
ADD CONSTRAINT skill_profile_heads_status_check CHECK (status IN ('CANDIDATE','EVALUATING','VALIDATED','ACTIVE','DEGRADED','SUSPENDED','RETIRED'));
CREATE TABLE IF NOT EXISTS capability.reflex_revisions (
reflex_id TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
principal_id TEXT NOT NULL,
source_skill_id TEXT NOT NULL,
source_skill_revision BIGINT NOT NULL CHECK (source_skill_revision > 0),
kind TEXT NOT NULL,
applicability JSONB NOT NULL,
implementation_ref TEXT NOT NULL,
implementation_version TEXT NOT NULL DEFAULT '',
artifact_digest TEXT NOT NULL DEFAULT '',
dependency_refs TEXT[] NOT NULL DEFAULT '{}',
validation_evidence_refs TEXT[] NOT NULL DEFAULT '{}',
known_failure_experience_refs TEXT[] NOT NULL DEFAULT '{}',
shadow_policy JSONB NOT NULL DEFAULT '{}'::jsonb,
revalidation_policy JSONB NOT NULL DEFAULT '{}'::jsonb,
last_validated_at BIGINT NOT NULL DEFAULT 0,
lifecycle TEXT NOT NULL,
supersedes_revision BIGINT NOT NULL DEFAULT 0,
execution_count BIGINT NOT NULL DEFAULT 0,
failure_count BIGINT NOT NULL DEFAULT 0,
divergence_count BIGINT NOT NULL DEFAULT 0,
recorded_at BIGINT NOT NULL CHECK (recorded_at > 0),
PRIMARY KEY (reflex_id, revision),
CHECK (lifecycle IN ('CANDIDATE','VALIDATING','ACTIVE','DEGRADED','STALE','REVALIDATING','SUSPENDED','RETIRED'))
);
CREATE TABLE IF NOT EXISTS capability.reflex_heads (
reflex_id TEXT PRIMARY KEY,
revision BIGINT NOT NULL,
lifecycle TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
GRANT SELECT, INSERT, UPDATE ON capability.reflex_revisions, capability.reflex_heads TO capability_role;
RESET ROLE;
SET ROLE self_role;
CREATE TABLE IF NOT EXISTS self.improvement_needs (
improvement_need_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
subject_ref JSONB NOT NULL,
problem_summary TEXT NOT NULL,
evidence_refs JSONB NOT NULL,
expected_benefit TEXT NOT NULL,
priority INTEGER NOT NULL,
state TEXT NOT NULL,
created_at_unix_millis BIGINT NOT NULL
);
ALTER TABLE self.learning_ledger
ADD COLUMN IF NOT EXISTS improvement_need_ref JSONB,
ADD COLUMN IF NOT EXISTS proposed_change_ref JSONB,
ADD COLUMN IF NOT EXISTS expected_improvement TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS evaluation_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS development_gate_ref JSONB,
ADD COLUMN IF NOT EXISTS before_version_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS after_version_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS side_effects JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS rollback_ref JSONB;
GRANT SELECT, INSERT, UPDATE ON self.improvement_needs, self.learning_ledger TO self_role;
RESET ROLE;
COMMIT;
@@ -0,0 +1,74 @@
BEGIN;
SET ROLE self_role;
CREATE TABLE IF NOT EXISTS self.learning_signals (
signal_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
kind TEXT NOT NULL,
task_pattern TEXT NOT NULL,
scope_key TEXT NOT NULL,
subject_ref JSONB NOT NULL,
evidence_refs JSONB NOT NULL,
expected_outcome TEXT NOT NULL DEFAULT '',
actual_outcome TEXT NOT NULL DEFAULT '',
dependency_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
environment_version TEXT NOT NULL DEFAULT '',
observed_at_unix_millis BIGINT NOT NULL,
severity INTEGER NOT NULL DEFAULT 0,
user_impact INTEGER NOT NULL DEFAULT 0,
correction_count INTEGER NOT NULL DEFAULT 0,
decision_regret TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS self.weakness_patterns (
assessment_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
pattern_key TEXT NOT NULL,
task_pattern TEXT NOT NULL,
scope_key TEXT NOT NULL,
environment_version TEXT NOT NULL DEFAULT '',
dependency_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
failure_mode TEXT NOT NULL,
detection_evidence_refs JSONB NOT NULL,
frequency INTEGER NOT NULL,
last_observed_at_unix_millis BIGINT NOT NULL,
severity INTEGER NOT NULL,
affected_component TEXT NOT NULL,
affected_strategy TEXT NOT NULL DEFAULT '',
confidence_millis BIGINT NOT NULL,
epistemic_status TEXT NOT NULL,
state TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS self.improvement_candidates (
candidate_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
need_ref JSONB NOT NULL,
hypothesis TEXT NOT NULL,
expected_effect TEXT NOT NULL,
evaluation_plan TEXT NOT NULL,
must_not_worsen JSONB NOT NULL,
rollback_plan TEXT NOT NULL,
resource_cost TEXT NOT NULL,
creator TEXT NOT NULL,
independent_evaluator TEXT NOT NULL,
state TEXT NOT NULL,
evidence_refs JSONB NOT NULL,
created_at_unix_millis BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS self.personal_benchmark_cases (
case_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
task_class TEXT NOT NULL,
evidence_refs JSONB NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL
);
GRANT SELECT, INSERT, UPDATE ON self.learning_signals, self.weakness_patterns, self.improvement_candidates, self.personal_benchmark_cases TO self_role;
RESET ROLE;
COMMIT;
+10
View File
@@ -25,6 +25,16 @@ psql_exec() {
docker exec -i "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_SUPERUSER" -d "$DATABASE" "$@"
}
# Owner roles and schemas are provisioned by the dedicated bootstrap boundary,
# before feature migrations run. This keeps role creation out of migrations and
# makes existing PostgreSQL volumes converge idempotently as well.
GEN2_BOOTSTRAP="$REPO_DIR/database/bootstrap/002_gen2_roles_and_schemas.sql"
if [[ ! -f "$GEN2_BOOTSTRAP" ]]; then
echo "migration runner: Gen2 bootstrap is missing" >&2
exit 1
fi
psql_exec < "$GEN2_BOOTSTRAP"
psql_exec -c "CREATE TABLE IF NOT EXISTS public.assistant_migration_history (migration_id TEXT PRIMARY KEY, applied_at_unix_millis BIGINT NOT NULL CHECK (applied_at_unix_millis > 0));"
if [[ -n "${MIGRATION_BASELINE:-}" ]]; then
+155
View File
@@ -0,0 +1,155 @@
# This file deliberately owns only one inactive release slot. It joins the
# stable project's shared network as an external resource, but has a distinct
# Compose project name supplied by run-production-canary.sh. Do not add
# durable services or stable services here.
services:
research-runtime:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: research-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: :8087
RESEARCH_SOURCE_ROOT: /var/lib/research-sources
RESEARCH_HTTP_ALLOWED_HOSTS: assistant.cin.su,git.cin.su
RESEARCH_SEARCH_URL_TEMPLATE: ${RESEARCH_SEARCH_URL_TEMPLATE:-https://www.bing.com/search?q=%s}
RESEARCH_SEARCH_ALLOWED_HOSTS: ${RESEARCH_SEARCH_ALLOWED_HOSTS:-www.bing.com}
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
SECRET_BACKEND_ROOT: /run/secrets
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
- assistant_research_sources:/var/lib/research-sources:ro
networks:
shared:
aliases: [research-runtime-candidate]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8087/healthz >/dev/null && wget -qO- http://localhost:8087/readyz >/dev/null"]
interval: 10s
timeout: 10s
retries: 20
core-runtime:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: core-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: ":8080"
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
POSTGRES_USER: situation_role
POSTGRES_PASSWORD: ${SITUATION_DB_PASSWORD}
IDENTITY_POSTGRES_USER: identity_role
IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD}
CREATOR_USERNAME: ${CREATOR_USERNAME}
WORK_POSTGRES_USER: work_role
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
CAPABILITY_POSTGRES_USER: capability_role
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
SELF_POSTGRES_USER: self_role
SELF_POSTGRES_CREDENTIAL_REF: credential://domovoy/self/db-password/v1
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
INTERACTION_POSTGRES_USER: interaction_role
INTERACTION_POSTGRES_CREDENTIAL_REF: credential://domovoy/interaction/db-password/v1
SECRET_BACKEND_ROOT: /run/secrets
COGNITIVE_ADMISSION_POLICY_ID: creator-advisory-local-v1
COGNITIVE_ADMISSION_MAX_CONCURRENT: ${COGNITIVE_ADMISSION_MAX_CONCURRENT:-1}
COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR: ${COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR:-60}
COGNITIVE_ADMISSION_MAX_INPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_INPUT_BYTES:-81920}
COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES:-16384}
COGNITIVE_RUNTIME_URL: http://cognitive-worker-candidate:8083
RESEARCH_RUNTIME_URL: http://research-runtime-candidate:8087
volumes:
- assistant_core_secrets:/run/secrets:ro
networks:
shared:
aliases: [core-runtime-candidate]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
interval: 10s
timeout: 20s
retries: 20
cognitive-worker:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: cognitive-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: ":8083"
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
LOCAL_CPU_PROVIDER_API_KEY_FILE: ${LOCAL_CPU_PROVIDER_API_KEY_FILE:-}
LOCAL_CPU_PROVIDER_CREDENTIAL_REF: ${LOCAL_CPU_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-cpu/provider-api-key/v1}
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://llm-local-gateway:8080/v1}
LOCAL_MEDIUM_PROVIDER_API_KEY_FILE: ${LOCAL_MEDIUM_PROVIDER_API_KEY_FILE:-/run/secrets/llm_local_gateway_api_key}
LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF: ${LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-medium/provider-api-key/v1}
LOCAL_GPU_PROVIDER_TIMEOUT: ${LOCAL_GPU_PROVIDER_TIMEOUT:-60s}
LOCAL_LIFEBOAT_PROVIDER_URL: http://cognitive-lifeboat:8086/v1
LOCAL_IMAGE_PROVIDER_KIND: ${LOCAL_IMAGE_PROVIDER_KIND:-comfyui}
LOCAL_IMAGE_PROVIDER_URL: ${LOCAL_IMAGE_PROVIDER_URL:-http://comfyui:8188}
LOCAL_IMAGE_MODEL: ${LOCAL_IMAGE_MODEL:-z_image_turbo_bf16.safetensors}
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
COGNITIVE_ALLOW_CANARY: "true"
volumes:
- assistant_core_secrets:/run/secrets:ro
networks:
shared:
aliases: [cognitive-worker-candidate]
llm-local: {}
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8083/healthz >/dev/null && wget -qO- http://localhost:8083/readyz >/dev/null"]
interval: 10s
timeout: 20s
retries: 20
creator-web:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: ":8084"
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
COMMIT_RUNTIME_URL: http://commit-runtime:8082
COGNITIVE_RUNTIME_URL: http://cognitive-worker-candidate:8083
AGENT_WORKER_URL: http://agent-worker:8090
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
networks:
shared:
aliases: [creator-web-candidate]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
interval: 10s
timeout: 5s
retries: 20
volumes:
assistant_core_secrets:
external: true
assistant_control_plane_service_secrets:
name: assistant_control_plane_service_secrets
external: true
assistant_research_sources:
name: assistant_research_sources
external: true
networks:
shared:
name: assistant_default
external: true
llm-local:
name: llm-local_default
external: true
+68 -13
View File
@@ -65,20 +65,17 @@ services:
POSTGRES_PASSWORD: ${SITUATION_DB_PASSWORD}
IDENTITY_POSTGRES_USER: identity_role
IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD}
CREATOR_BOOTSTRAP_TOKEN_FILE: /run/secrets/creator_bootstrap_token
CREATOR_SESSION_SIGNING_KEY_FILE: /run/secrets/creator_session_signing_key
CREATOR_USERNAME: ${CREATOR_USERNAME}
CREATOR_PASSWORD_FILE: /run/secrets/creator_password
WORK_POSTGRES_USER: work_role
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
CAPABILITY_POSTGRES_USER: capability_role
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
SELF_POSTGRES_USER: self_role
SELF_POSTGRES_CREDENTIAL_REF: credential://domovoy/self/db-password/v1
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
INTERACTION_POSTGRES_USER: interaction_role
INTERACTION_POSTGRES_CREDENTIAL_REF: credential://domovoy/interaction/db-password/v1
INTERACTION_POSTGRES_PASSWORD_FILE: /run/secrets/interaction_db_password
SECRET_BACKEND_ROOT: /run/secrets
COGNITIVE_ADMISSION_POLICY_ID: creator-advisory-local-v1
COGNITIVE_ADMISSION_MAX_CONCURRENT: ${COGNITIVE_ADMISSION_MAX_CONCURRENT:-1}
@@ -88,6 +85,12 @@ services:
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
interval: 10s
timeout: 20s
retries: 20
agent-canary:
profiles: ["agent-canary"]
@@ -182,7 +185,6 @@ services:
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
SECRET_BACKEND_ROOT: /run/secrets
depends_on: { postgres: { condition: service_healthy } }
volumes:
@@ -202,12 +204,16 @@ services:
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
SECRET_BACKEND_ROOT: /run/secrets
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_research_sources:/var/lib/research-sources:ro
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8087/healthz >/dev/null && wget -qO- http://localhost:8087/readyz >/dev/null"]
interval: 10s
timeout: 10s
retries: 20
continuity-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
@@ -242,8 +248,18 @@ services:
# External mutation credentials belong here only via Portainer/Docker secrets/env injection.
depends_on: [core-runtime, continuity-runtime, notification-receiver]
volumes:
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
- assistant_notification_writer_secret:/run/notification-writer:ro
secret-control-plane:
profiles: ["secret-control-plane"]
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: secret-control-plane, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
user: "0:0"
restart: "no"
command: ["ensure"]
volumes:
- assistant_control_plane_service_secrets:/run/control-plane-secrets
cognitive-worker:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: cognitive-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
@@ -253,8 +269,10 @@ services:
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
LOCAL_CPU_PROVIDER_API_KEY_FILE: ${LOCAL_CPU_PROVIDER_API_KEY_FILE:-}
LOCAL_CPU_PROVIDER_CREDENTIAL_REF: ${LOCAL_CPU_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-cpu/provider-api-key/v1}
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://llm-local-api:8080/v1}
LOCAL_MEDIUM_PROVIDER_API_KEY_FILE: ${LOCAL_MEDIUM_PROVIDER_API_KEY_FILE:-}
# The medium chat route is served by the authenticated local gateway.
# llm-local-api is the embeddings service and must not be used for chat.
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://llm-local-gateway:8080/v1}
LOCAL_MEDIUM_PROVIDER_API_KEY_FILE: ${LOCAL_MEDIUM_PROVIDER_API_KEY_FILE:-/run/secrets/llm_local_gateway_api_key}
LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF: ${LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-medium/provider-api-key/v1}
LOCAL_GPU_PROVIDER_TIMEOUT: ${LOCAL_GPU_PROVIDER_TIMEOUT:-60s}
LOCAL_LIFEBOAT_PROVIDER_URL: http://cognitive-lifeboat:8086/v1
@@ -263,10 +281,17 @@ services:
LOCAL_IMAGE_MODEL: ${LOCAL_IMAGE_MODEL:-z_image_turbo_bf16.safetensors}
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
COGNITIVE_ALLOW_CANARY: "true"
depends_on: [core-runtime, cognitive-lifeboat]
volumes:
- assistant_core_secrets:/run/secrets:ro
networks: [default, llm-local]
depends_on:
core-runtime: { condition: service_started }
cognitive-lifeboat: { condition: service_healthy }
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8083/healthz >/dev/null && wget -qO- http://localhost:8083/readyz >/dev/null"]
interval: 10s
timeout: 20s
retries: 20
cognitive-lifeboat:
image: ghcr.io/ggml-org/llama.cpp:server
@@ -277,7 +302,7 @@ services:
- --port
- "8086"
- --model
- /models/Qwen3-8B-Q4_K_M.gguf
- /models/Qwen3.8-27B-UD-Q2_K_XL.gguf
- --alias
- domovoy-lifeboat
- --jinja
@@ -299,6 +324,29 @@ services:
timeout: 3s
retries: 10
supervisor:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: supervisor, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
environment:
LISTEN_ADDR: ":8080"
SUPERVISOR_CONTROL_ADDR: ":8095"
SUPERVISOR_STATE_PATH: /var/lib/supervisor/release-state.json
SUPERVISOR_STABLE_UPSTREAM: http://creator-web:8084
SUPERVISOR_CANDIDATE_UPSTREAM: http://creator-web-candidate:8084
SUPERVISOR_BASELINE_RELEASE: ${ASSISTANT_SOURCE_REVISION:-unknown}
ports:
- "${CREATOR_WEB_PORT:-18080}:8080"
- "127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}:8095"
depends_on:
creator-web: { condition: service_healthy }
volumes:
- assistant_supervisor_state:/var/lib/supervisor
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
interval: 10s
timeout: 5s
retries: 20
creator-web:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
@@ -310,8 +358,11 @@ services:
COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083
AGENT_WORKER_URL: http://agent-worker:8090
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
ports: ["${CREATOR_WEB_PORT:-18080}:8084"]
depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker]
depends_on:
core-runtime: { condition: service_started }
continuity-runtime: { condition: service_started }
commit-runtime: { condition: service_started }
cognitive-worker: { condition: service_healthy }
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
interval: 10s
@@ -323,6 +374,9 @@ volumes:
assistant_nats:
assistant_core_secrets:
external: true
assistant_control_plane_service_secrets:
name: assistant_control_plane_service_secrets
external: true
assistant_notification_receipts:
assistant_notification_writer_secret:
external: true
@@ -333,6 +387,7 @@ volumes:
external: true
assistant_research_sources:
name: assistant_research_sources
assistant_supervisor_state:
networks:
llm-local:
+1
View File
@@ -4,5 +4,6 @@ LABEL org.opencontainers.image.revision=$SOURCE_REVISION \
org.opencontainers.image.source="https://git.cin.su/m/Assistant.git"
COPY database/bootstrap/001_roles_and_schemas.sql /docker-entrypoint-initdb.d/001_roles_and_schemas.sql
COPY database/bootstrap/002_gen2_roles_and_schemas.sql /docker-entrypoint-initdb.d/002_gen2_roles_and_schemas.sql
COPY deploy/images/002_owner_passwords.sh /docker-entrypoint-initdb.d/002_owner_passwords.sh
RUN chmod 0555 /docker-entrypoint-initdb.d/002_owner_passwords.sh
+1
View File
@@ -11,6 +11,7 @@ ARG SOURCE_REVISION=unknown
LABEL org.opencontainers.image.revision=$SOURCE_REVISION \
org.opencontainers.image.source="https://git.cin.su/m/Assistant.git"
RUN addgroup -S assistant && adduser -S -G assistant assistant
RUN mkdir -p /var/lib/supervisor && chown assistant:assistant /var/lib/supervisor
USER assistant
COPY --from=build /out/runtime /usr/local/bin/assistant-runtime
ENTRYPOINT ["/usr/local/bin/assistant-runtime"]
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
# Stable-only production updater. Candidate slots are managed exclusively by
# their isolated release command and Supervisor; this controller never starts
# or reconciles a candidate service.
BASE_DIR="${BASE_DIR:-/home/m/assistant-deploy}"
REPO_DIR="${REPO_DIR:-$BASE_DIR/repo}"
ENV_FILE="${ENV_FILE:-$BASE_DIR/production-compose.env}"
COMPOSE_FILE="$REPO_DIR/deploy/docker-compose.prod.yml"
LOG_FILE="${LOG_FILE:-$BASE_DIR/deploy.log}"
LOCK_FILE="${LOCK_FILE:-/tmp/assistant-stable-deploy.lock}"
stable_services=(postgres core-runtime continuity-runtime commit-runtime cognitive-worker creator-web notification-receiver research-runtime agent-worker work-graph-runtime)
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
log() { printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >>"$LOG_FILE"; }
fail() { log "FAILED: $*"; exit 1; }
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
cd "$REPO_DIR"
git fetch --prune origin main >>"$LOG_FILE" 2>&1
git merge --ff-only origin/main >>"$LOG_FILE" 2>&1 || fail "stable checkout is not a fast-forward to origin/main"
revision="$(git rev-parse HEAD)"
[[ "$revision" =~ ^[0-9a-f]{40}$ ]] || fail "invalid source revision"
if ! REPO_DIR="$REPO_DIR" ENV_FILE="$ENV_FILE" bash "$REPO_DIR/deploy/apply-migrations.sh" >>"$LOG_FILE" 2>&1; then
fail "migration application failed"
fi
compose config >/dev/null 2>>"$LOG_FILE" || fail "stable compose config validation failed"
all_current=1
for service in "${stable_services[@]}"; do
container="assistant-${service}-1"
label="$(docker inspect "$container" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null || true)"
[[ "$label" == "$revision" ]] || all_current=0
done
if [[ "$all_current" == 1 ]]; then log "SKIP stable revision=$revision"; exit 0; fi
log "BUILD stable revision=$revision"
ASSISTANT_SOURCE_REVISION="$revision" compose build --build-arg "SOURCE_REVISION=$revision" "${stable_services[@]}" >>"$LOG_FILE" 2>&1
log "UP stable revision=$revision"
ASSISTANT_SOURCE_REVISION="$revision" compose up -d --no-build "${stable_services[@]}" >>"$LOG_FILE" 2>&1
for attempt in $(seq 1 30); do
if curl -fsS --max-time 5 https://assistant.cin.su/healthz >/dev/null 2>&1; then break; fi
[[ "$attempt" == 30 ]] && fail "public health failed"
sleep 10
done
for service in "${stable_services[@]}"; do
container="assistant-${service}-1"
label="$(docker inspect "$container" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null || true)"
[[ "$label" == "$revision" ]] || fail "artifact drift container=$container"
done
log "OK stable revision=$revision"
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
# This is the only production writer for the dedicated control-plane machine
# credential volume. It never receives, prints, or exports a secret value.
BASE_DIR="${BASE_DIR:-/home/m/assistant-deploy}"
REPO_DIR="${REPO_DIR:-$BASE_DIR/repo}"
ENV_FILE="${ENV_FILE:-$BASE_DIR/production-compose.env}"
COMPOSE_FILE="$REPO_DIR/deploy/docker-compose.prod.yml"
VOLUME="assistant_control_plane_service_secrets"
ACTION="${1:-ensure}"
case "$ACTION" in
ensure|rotate) ;;
*) echo "unsupported machine credential action" >&2; exit 2 ;;
esac
[[ -f "$ENV_FILE" ]] || { echo "production compose environment is missing" >&2; exit 1; }
[[ -f "$COMPOSE_FILE" ]] || { echo "production compose file is missing" >&2; exit 1; }
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
# `docker volume create` is idempotent and does not inspect or expose content.
docker volume create "$VOLUME" >/dev/null
revision="$(git -C "$REPO_DIR" rev-parse HEAD)"
ASSISTANT_SOURCE_REVISION="$revision" compose build --build-arg "SOURCE_REVISION=$revision" secret-control-plane >/dev/null
if [[ "$ACTION" == "rotate" ]]; then
# Only bounded, non-secret rotation reasons can reach the audit trail.
reason="${ROTATION_REASON:-owner-requested-rotation}"
case "$reason" in
owner-requested-rotation|exposure-detected) ;;
*) echo "invalid rotation reason" >&2; exit 2 ;;
esac
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps secret-control-plane rotate -reason "$reason"
else
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps secret-control-plane ensure
fi
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
# Regression: the wrapper accepts only an operation name and never places a
# plaintext credential on a Docker/Compose command line.
script="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/provision-machine-credential.sh"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/bin" "$tmp/repo/deploy"
cp "$script" "$tmp/repo/deploy/provision-machine-credential.sh"
printf 'placeholder\n' > "$tmp/repo/deploy/docker-compose.prod.yml"
printf 'placeholder\n' > "$tmp/env"
cat > "$tmp/bin/docker" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "${CAPTURE:?}"
SH
cat > "$tmp/bin/git" <<'SH'
#!/usr/bin/env bash
printf '0123456789abcdef0123456789abcdef01234567\n'
SH
chmod +x "$tmp/bin/docker" "$tmp/bin/git" "$tmp/repo/deploy/provision-machine-credential.sh"
capture="$tmp/capture"
PATH="$tmp/bin:$PATH" CAPTURE="$capture" REPO_DIR="$tmp/repo" ENV_FILE="$tmp/env" "$tmp/repo/deploy/provision-machine-credential.sh" ensure >/dev/null
grep -qx 'volume create assistant_control_plane_service_secrets' "$capture"
grep -q -- '--profile secret-control-plane run --rm --no-deps secret-control-plane ensure' "$capture"
if grep -Eqi '(token=|password=|secret=|authorization:)' "$capture"; then
echo "provisioning wrapper passed credential material" >&2
exit 1
fi
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
set -euo pipefail
# Candidate services are never part of the stable Compose project. The slot
# Compose file may use only declared external networks and durable resources.
BASE_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
CANDIDATE_FILE="$BASE_DIR/deploy/docker-compose.candidate-slot.yml"
SUPERVISOR_CONTROL_URL="${SUPERVISOR_CONTROL_URL:-http://127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}}"
REVISION="${1:?candidate revision is required}"
SLOT="${CANDIDATE_SLOT:-b}"
STABLE_PROJECT="${STABLE_PROJECT:-assistant}"
CANDIDATE_PROJECT="${CANDIDATE_PROJECT:-assistant-slot-$SLOT}"
HEALTH_ATTEMPTS="${HEALTH_ATTEMPTS:-360}"
PROMOTE_AFTER_CANARY="${PROMOTE_AFTER_CANARY:-true}"
[[ "$SLOT" =~ ^[a-z0-9-]+$ ]] || { echo "invalid candidate slot" >&2; exit 2; }
[[ "$CANDIDATE_PROJECT" != "$STABLE_PROJECT" ]] || { echo "candidate and stable Compose projects must differ" >&2; exit 2; }
candidate_compose() { docker compose -p "$CANDIDATE_PROJECT" --env-file "$ENV_FILE" -f "$CANDIDATE_FILE" "$@"; }
control() { curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL$1" "${@:2}" >/dev/null; }
fail_rollback() {
control /v1/traffic/rollback || true
control /v1/releases/rollback || true
echo "isolated candidate ReleaseSet failed; stable ReleaseSet rollback requested" >&2
exit 1
}
services=(creator-web core-runtime cognitive-worker research-runtime)
stable_services=(creator-web core-runtime cognitive-worker research-runtime)
stable_ids() {
local service container id
for service in "${stable_services[@]}"; do
container="${STABLE_PROJECT}-${service}-1"
id=$(docker inspect -f '{{.Id}}' "$container" 2>/dev/null || true)
[[ -n "$id" ]] || { echo "stable container missing: $container" >&2; return 1; }
printf '%s=%s\n' "$service" "$id"
done
}
assert_stable_ids_unchanged() {
local after
after=$(stable_ids) || fail_rollback
[[ "$after" == "$stable_ids_before" ]] || {
echo "stable container IDs changed during isolated candidate operation" >&2
diff -u <(printf '%s\n' "$stable_ids_before") <(printf '%s\n' "$after") >&2 || true
fail_rollback
}
}
candidate_container() { printf '%s-%s-1' "$CANDIDATE_PROJECT" "$1"; }
wait_healthy() {
local container="$1" status="" ready=""
for attempt in $(seq 1 "$HEALTH_ATTEMPTS"); do
status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' "$container" 2>/dev/null || true)
if [[ "$status" == healthy ]]; then ready=true; break; fi
sleep 2
done
[[ "$ready" == true ]] || fail_rollback
}
stable_ids_before=$(stable_ids) || exit 1
candidate_compose config >/dev/null
candidate_compose build --build-arg "SOURCE_REVISION=$REVISION" "${services[@]}"
assert_stable_ids_unchanged
candidate_compose up -d --no-build --no-deps research-runtime core-runtime
wait_healthy "$(candidate_container research-runtime)"
wait_healthy "$(candidate_container core-runtime)"
candidate_compose up -d --no-build --no-deps cognitive-worker
wait_healthy "$(candidate_container cognitive-worker)"
candidate_compose up -d --no-build --no-deps creator-web
wait_healthy "$(candidate_container creator-web)"
assert_stable_ids_unchanged
for service in "${services[@]}"; do
image_revision=$(docker inspect -f '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$(candidate_container "$service")" 2>/dev/null || true)
[[ "$image_revision" == "$REVISION" ]] || fail_rollback
done
assert_stable_ids_unchanged
if [[ "${PREPARE_ONLY:-false}" == true ]]; then
echo "isolated candidate ReleaseSet prepared slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
exit 0
fi
release_json=$(cat <<JSON
{"id":"candidate-$SLOT-$REVISION","slot":"$SLOT","revision":"$REVISION","components":{"creator-web":{"name":"creator-web","revision":"$REVISION","upstream":"http://creator-web-candidate:8084","health_url":"http://creator-web-candidate:8084/healthz","ready_url":"http://creator-web-candidate:8084/healthz"},"core-runtime":{"name":"core-runtime","revision":"$REVISION","upstream":"http://core-runtime-candidate:8080","health_url":"http://core-runtime-candidate:8080/healthz","ready_url":"http://core-runtime-candidate:8080/readyz"},"cognitive-worker":{"name":"cognitive-worker","revision":"$REVISION","upstream":"http://cognitive-worker-candidate:8083","health_url":"http://cognitive-worker-candidate:8083/healthz","ready_url":"http://cognitive-worker-candidate:8083/readyz"},"research-runtime":{"name":"research-runtime","revision":"$REVISION","upstream":"http://research-runtime-candidate:8087","health_url":"http://research-runtime-candidate:8087/healthz","ready_url":"http://research-runtime-candidate:8087/readyz"}}}
JSON
)
stable_json=$(curl -fsS --max-time 10 "$SUPERVISOR_CONTROL_URL/v1/state")
stable_set=$(printf '%s' "$stable_json" | jq -c '.stable_release_set // empty')
if [[ -z "$stable_set" ]]; then
stable_revision=$(printf '%s' "$stable_json" | jq -r '.current.version // "unknown"')
stable_set=$(cat <<JSON
{"id":"stable-$stable_revision","slot":"legacy-stable","revision":"$stable_revision","components":{"creator-web":{"name":"creator-web","revision":"$stable_revision","upstream":"http://creator-web:8084","health_url":"http://creator-web:8084/healthz","ready_url":"http://creator-web:8084/healthz"},"core-runtime":{"name":"core-runtime","revision":"$stable_revision","upstream":"http://core-runtime:8080","health_url":"http://core-runtime:8080/healthz","ready_url":"http://core-runtime:8080/readyz"},"cognitive-worker":{"name":"cognitive-worker","revision":"$stable_revision","upstream":"http://cognitive-worker:8083","health_url":"http://cognitive-worker:8083/healthz","ready_url":"http://cognitive-worker:8083/readyz"},"research-runtime":{"name":"research-runtime","revision":"$stable_revision","upstream":"http://research-runtime:8087","health_url":"http://research-runtime:8087/healthz","ready_url":"http://research-runtime:8087/readyz"}}}
JSON
)
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/baseline-set" -H 'Content-Type: application/json' --data "$stable_set" >/dev/null || fail_rollback
fi
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/start-set" -H 'Content-Type: application/json' --data "$release_json" >/dev/null || fail_rollback
assert_stable_ids_unchanged
observation=$(printf '{"release_id":"candidate-%s-%s","ready":true,"reason":"isolated slot health and image revisions passed","observed_at_unix_millis":%s,"components":{"creator-web":{"revision":"%s","health":true,"ready":true},"core-runtime":{"revision":"%s","health":true,"ready":true},"cognitive-worker":{"revision":"%s","health":true,"ready":true},"research-runtime":{"revision":"%s","health":true,"ready":true}}}\n' "$SLOT" "$REVISION" "$(date +%s%3N)" "$REVISION" "$REVISION" "$REVISION" "$REVISION")
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/observe-set" -H 'Content-Type: application/json' --data "$observation" >/dev/null || fail_rollback
assert_stable_ids_unchanged
control /v1/traffic/canary || fail_rollback
assert_stable_ids_unchanged
headers=$(mktemp)
trap 'rm -f "$headers"' EXIT
if ! curl -fsS --max-time 10 -D "$headers" -H 'X-Assistant-Canary: true' "https://assistant.cin.su/login" -o /dev/null; then fail_rollback; fi
grep -qi '^X-Assistant-Traffic-Target: candidate' "$headers" || fail_rollback
if [[ "$PROMOTE_AFTER_CANARY" != true ]]; then
echo "isolated candidate ReleaseSet canary ready slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
exit 0
fi
control /v1/releases/promote || fail_rollback
assert_stable_ids_unchanged
echo "isolated candidate ReleaseSet promoted slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
COMPOSE_FILE="$BASE_DIR/deploy/docker-compose.prod.yml"
set -a
. "$ENV_FILE"
set +a
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
stable_services=(creator-web core-runtime cognitive-worker research-runtime)
stable_ids() {
local service container id
for service in "${stable_services[@]}"; do
container="assistant-${service}-1"
id=$(docker inspect -f '{{.Id}}' "$container" 2>/dev/null || true)
[[ -n "$id" ]] || { echo "stable container missing: $container" >&2; return 1; }
printf '%s=%s\n' "$service" "$id"
done
}
before=$(stable_ids)
compose up -d --no-build --no-deps supervisor
after=$(stable_ids)
[[ "$before" == "$after" ]] || {
echo "stable container IDs changed while updating supervisor" >&2
diff -u <(printf '%s\n' "$before") <(printf '%s\n' "$after") >&2 || true
exit 1
}
printf '%s\n' "$after"
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
# Safe diagnostics only: artifact existence/readability and mount topology.
# This script never reads or prints credential content.
TOKEN_PATH="/run/control-plane-secrets/control_plane_service_token"
VOLUME="assistant_control_plane_service_secrets"
authorized=(assistant-core-runtime-1 assistant-commit-runtime-1)
unauthorized=(assistant-creator-web-1 assistant-cognitive-worker-1 assistant-research-runtime-1 assistant-continuity-runtime-1 assistant-agent-worker-1)
for container in "${authorized[@]}"; do
docker inspect "$container" >/dev/null
docker exec "$container" sh -c "test -r '$TOKEN_PATH'"
docker inspect -f '{{range .Mounts}}{{if eq .Name "assistant_control_plane_service_secrets"}}{{.Destination}} {{.RW}}{{end}}{{end}}' "$container" | grep -qx '/run/control-plane-secrets false'
done
for container in "${unauthorized[@]}"; do
docker inspect "$container" >/dev/null
if docker inspect -f '{{range .Mounts}}{{println .Name}}{{end}}' "$container" | grep -qx "$VOLUME"; then
echo "unauthorized runtime has the control-plane credential mount" >&2
exit 1
fi
done
echo "machine credential mount topology verified"
+1 -1
View File
@@ -1 +1 @@
{"task_file":"dev/tasks/GEN2-018-add-perspective-and-dynamic-specialist-team-semantics-on-existing-work-g.json","task_id":"GEN2-018","task_revision":1}
{"task_file":"dev/tasks/GEN2-SECRETS-001-local-machine-credential-provisioning.json","task_id":"GEN2-SECRETS-001","task_revision":2}
@@ -1,6 +1,6 @@
{
"task_id": "GEN2-028",
"task_revision": 1,
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
@@ -16,7 +16,7 @@
"modules/**",
"contracts/**",
"cmd/**",
"adapters/**",
"adapters/postgres/**",
"runtime/**",
"platform/**",
"docs/**",
@@ -1,6 +1,6 @@
{
"task_id": "GEN2-029",
"task_revision": 1,
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
@@ -16,7 +16,7 @@
"modules/**",
"contracts/**",
"cmd/**",
"adapters/**",
"adapters/postgres/**",
"runtime/**",
"platform/**",
"database/migrations/*.sql",
@@ -1,6 +1,6 @@
{
"task_id": "GEN2-030",
"task_revision": 1,
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
@@ -20,7 +20,7 @@
"modules/**",
"contracts/**",
"cmd/**",
"adapters/**",
"adapters/postgres/**",
"runtime/**",
"platform/**",
"tests/**"
@@ -0,0 +1,66 @@
{
"task_id": "GEN2-COGNITIVE-001",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Restore one governed, usable cognitive route for household traffic, preferably Local MEDIUM, without bypassing Capability ownership or activation governance.",
"requirement_ids": ["REQ-GEN2-COGNITIVE-001-QUALIFICATION", "REQ-GEN2-COGNITIVE-001-LIFECYCLE", "REQ-GEN2-COGNITIVE-001-ROUTING"],
"allowed_change_scope": {
"modules": ["capability", "cognitive-worker", "core-runtime", "configuration", "production deployment"],
"paths": [
"adapters/postgres/capability_repository.go",
"cmd/cognitive-worker/**",
"cmd/core-runtime/**",
"contracts/capability/**",
"modules/capability/**",
"deploy/**",
"runtime/health/**",
"tests/**",
"docs/evidence/**",
"docs/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-COGNITIVE-001-qualify-and-promote-local-medium.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "database/**", "go.mod", "go.sum", ".gitea/workflows/**"]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 0
},
"contracts": {
"consume": ["Gen2 cognitive registry", "Capability repository boundary", "bounded cognitive gateway"],
"may_change": ["only typed qualification/selection wiring within the declared paths"]
},
"semantic_owner": "Capability owns provider, model and binding lifecycle; cognitive-worker produces bounded observations; Supervisor controls production release.",
"canonical_state": ["existing capability cognitive provider/model/binding/evidence state"],
"state_transitions": ["DISCOVERED -> qualification evidence -> VALIDATED -> ACTIVE only through existing governed promotion path", "non-working ACTIVE/VALIDATED binding -> governed deactivation or requalification"],
"side_effects_and_privileges": ["No direct SQL semantic mutation.", "No manual activation without qualification and independent evaluation evidence.", "No provider call outside cognitive adapter/gateway boundaries.", "No production secret values in source, logs, evidence or commits."],
"persistence_and_migrations": ["No schema change is authorized; reuse existing capability schema and repository owner."],
"failure_retry_restart": ["Binding heads and lifecycle evidence remain durable and restart-safe.", "CPU/GPU unavailability must not block a usable Local MEDIUM route.", "Lifeboat remains fallback, not primary."],
"assumptions": ["Existing PostgreSQL capability schema and authenticated core registry routes are available.", "Production deployment remains governed by the existing Supervisor/update flow.", "No database migration or new role is required."],
"acceptance_scenarios": [
"provider health and model discovery are observed through the governed worker path",
"Local MEDIUM bounded qualification records durable evidence",
"independent evaluation and activation promotion are accepted by existing Capability owner",
"ordinary cognitive request selects ACTIVE/VALIDATED Local MEDIUM when CPU/GPU are unavailable",
"production core and worker use the same qualified revision",
"restart preserves route usability and no stale binding is selected"
],
"required_tests": [
"FAST: go test ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./modules/capability/... ./adapters/postgres/...",
"FAST: targeted PostgreSQL repository tests",
"FAST: go vet ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./modules/capability/... ./adapters/postgres/...",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check",
"production: bounded qualification, governed promotion and cognitive invocation"
],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,64 @@
{
"task_id": "GEN2-COGNITIVE-002",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Восстановить доказанно рабочий production cognitive route для обычного household и memory-recall запроса через существующие Capability и Cognitive Gateway boundaries, без подмены primary route Lifeboat-ом.",
"requirement_ids": ["REQ-GEN2-COGNITIVE-002-TRACE", "REQ-GEN2-COGNITIVE-002-ROUTING", "REQ-GEN2-COGNITIVE-002-RECOVERY"],
"allowed_change_scope": {
"modules": ["capability", "cognitive-worker", "core-runtime", "interaction", "production deployment"],
"paths": [
"cmd/cognitive-worker/**",
"cmd/core-runtime/**",
"modules/interaction/domain/gen2_context.go",
"modules/interaction/domain/gen2_context_test.go",
"adapters/postgres/interaction_repository.go",
"contracts/capability/**",
"contracts/cognitive/**",
"modules/capability/**",
"adapters/models/**",
"adapters/postgres/capability_repository.go",
"deploy/run-production-canary.sh",
"deploy/docker-compose.candidate-slot.yml",
"tests/**",
"docs/evidence/**",
"docs/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-COGNITIVE-002-restore-production-cognitive-route.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "database/**", "go.mod", "go.sum", ".gitea/workflows/**", "GEN2-028", "GEN2-029", "GEN2-030"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {
"consume": ["existing Capability binding lifecycle", "existing cognitive gateway/advisory contracts", "existing Supervisor ReleaseSet control plane"],
"may_change": ["typed capability/provider selection and invocation wiring only within declared paths", "the existing Interaction Gen2 retrieval projection for bounded, scoped human assertions; no new Memory owner or reader"]
},
"semantic_owner": "Capability remains the sole owner of provider/model/binding lifecycle; cognitive-worker invokes providers through adapters; Core consumes the governed gateway; Supervisor alone controls traffic promotion and rollback.",
"canonical_state": ["existing Capability provider/model/binding/evaluation evidence", "canonical Interaction human utterance history and existing typed ProjectReference read through the existing Gen2 retrieval projection", "no new canonical writer, owner, schema, role, or migration"],
"state_transitions": ["trace stable and candidate loaded binding heads -> correct stale/divergent lifecycle or invocation defect -> governed repair/qualification -> scoped Interaction projection selects bounded current human assertions -> isolated candidate validation -> Supervisor canary -> promote or rollback"],
"side_effects_and_privileges": ["production candidate deployment and Supervisor canary/promotion are explicitly authorized", "adapters/models/** may change only after trace proves a model adapter, provider endpoint, injection, timeout, rate-limit, context budget, or invocation defect there", "cmd/core-runtime/** may change only after trace proves Core-to-worker request construction, route selection, or response handling is causal", "modules/interaction/domain/gen2_context.go may change only to extend the existing bounded Gen2 read projection over canonical Interaction utterances with Principal hard filtering, current applicability and relevance selection; it must not write state, become a Memory owner, or promote PersonalMemory V1 to primary", "adapters/postgres/interaction_repository.go may only read the already-persisted typed interaction.ProjectReference records for the selected canonical Interaction utterances; it may not create schema, write state, or bypass Interaction ownership", "no direct SQL semantic mutation", "no credential, cookie, token, prompt, or user content read/emitted", "Lifeboat may be observed as fallback but cannot mask Local MEDIUM primary failure"],
"persistence_and_migrations": ["no schema/migration change is authorized", "adapters/postgres/capability_repository.go may change only after trace proves durable Capability head/state loading diverges from the correct persisted state; reuse its existing repository boundary"],
"failure_retry_restart": ["discovery health, invocation health, and availability remain distinct", "CPU/GPU gaps must not suppress a valid Local MEDIUM", "restart reloads a durable active/validated binding head", "failed candidate rolls traffic to Supervisor LKG without stable recreation"],
"assumptions": ["isolated candidate slot B and Supervisor controls from GEN2-DEPLOY-003 are available", "an existing authenticated production session may be used only through ordinary runtime interaction", "GEN2-COMPAT-OBS-001 remains paused and V1 paths remain unchanged"],
"acceptance_scenarios": [
"COGNITIVE-002-SCN-01: factual stable/candidate trace records Core request, requirement, binding selection, loaded binding head, fleet availability, provider eligibility, invocation result, and response/error without sensitive content",
"COGNITIVE-002-SCN-02: Local MEDIUM is shown ACTIVE, VALIDATED, AVAILABLE, and actually invoked for bounded OK, Russian household, and prepared-context recall requests",
"COGNITIVE-002-SCN-03: health endpoint success alone is not used as inference proof; discovery and invocation failures are separately classified",
"COGNITIVE-002-SCN-04: candidate four-component health/revision check, real canary household request, scoped bounded arbitrary-assertion memory write/multiple turns/recall, and candidate Core plus cognitive-worker restart/recovery pass",
"COGNITIVE-002-SCN-05: only after candidate success, Supervisor promotes it; evidence retains LKG and rollback readiness",
"COGNITIVE-002-EXIT: ordinary household and memory-recall response use Local MEDIUM through the governed route and remain usable after candidate restart"
],
"required_tests": [
"FAST: targeted go test ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./modules/capability/... ./adapters/models/... ./adapters/postgres/... only for changed packages",
"FAST: targeted go vet for changed packages",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"production: stable/candidate trace, isolated candidate health/canary, Local MEDIUM invocation, restart recovery, and Supervisor promotion/rollback evidence"
],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,78 @@
{
"task_id": "GEN2-COGNITIVE-003",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Restore bounded OpenAI-compatible Local MEDIUM invocation without changing canonical ownership or Core semantics.",
"requirement_ids": ["REQ-GEN2-EVOLVE-004-LOCAL-MEDIUM-INVOCATION"],
"allowed_change_scope": {
"modules": ["cognitive-worker provider invocation", "existing local OpenAI-compatible adapter", "targeted evidence"],
"paths": [
"cmd/cognitive-worker/main.go",
"cmd/cognitive-worker/advisory.go",
"cmd/cognitive-worker/gateway_registry.go",
"cmd/cognitive-worker/*_test.go",
"adapters/models/openai_compatible_local.go",
"adapters/models/openai_compatible_local_test.go",
"cmd/core-runtime/learning_test.go",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-COGNITIVE-003-restore-local-medium-invocation.json",
"docs/evidence/**"
]
},
"forbidden_change_scope": {
"paths": [
"cmd/core-runtime/capability_chat.go",
"cmd/core-runtime/capability_chat_test.go",
"cmd/core-runtime/chat_orchestrator.go",
"cmd/core-runtime/chat_orchestrator_test.go",
"cmd/core-runtime/cognitive_interpretation.go",
"cmd/core-runtime/cognitive_interpretation_test.go",
"cmd/core-runtime/development_gate.go",
"cmd/core-runtime/development_gate_test.go",
"cmd/core-runtime/household_scenario_test.go",
"cmd/core-runtime/human_response_gate_test.go",
"cmd/core-runtime/information_request_test.go",
"cmd/core-runtime/learning.go",
"cmd/core-runtime/main.go",
"cmd/core-runtime/main_test.go",
"cmd/core-runtime/research_client.go",
"cmd/core-runtime/research_client_test.go",
"adapters/postgres/**",
"modules/**",
"contracts/**",
"architecture/**",
"database/**",
"security/**",
"deploy/**",
"go.mod",
"go.sum",
"UI",
"V1 retirement",
"GEN2-EVOLVE-005",
"Gen3/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 0
},
"contracts": {
"consume": ["existing cognitive advisory contract", "existing capability binding/profile metadata", "existing OpenAI-compatible local adapter"],
"may_change": ["provider request normalization and typed immediate failure mapping only"]
},
"semantic_owner": "Cognitive gateway remains the sole owner of provider invocation and route selection.",
"canonical_state": ["discovery is not invocation qualification", "optional provider parameters are capability-gated", "immediate provider rejection is not reported as timeout"],
"state_transitions": ["binding selection -> normalized request -> provider invocation -> typed result"],
"side_effects_and_privileges": ["no secrets or authorization headers in evidence", "no direct database access", "no provider calls outside the existing gateway boundary"],
"persistence_and_migrations": ["no schema change"],
"failure_retry_restart": ["bounded invocation; no timeout inflation; no infinite retry; preserve fail-closed/fallback semantics"],
"assumptions": ["Local MEDIUM gateway remains the existing provider endpoint", "candidate slot B is available for isolated verification", "cmd/core-runtime/learning_test.go is a preserved carry-forward regression from the preceding runtime task"],
"acceptance_scenarios": ["minimal invocation completes", "ordinary and bounded-context invocation complete", "immediate provider 4xx maps to typed rejection without timeout", "qualification requires bounded invocation evidence"],
"required_tests": ["targeted go test", "targeted go vet", "preflight", "check-diff", "independent", "architecture checks", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "REQUIRES_MIGRATION_DECISION", "BLOCKED_BY_DEPLOYMENT_CONTROL"]
}
@@ -0,0 +1,65 @@
{
"task_id": "GEN2-COMPAT-OBS-001",
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Добавить bounded redacted production telemetry для оставшихся V1 compatibility paths без изменения их поведения, owners или canonical storage.",
"requirement_ids": ["REQ-GEN2-COMPAT-OBS-001-TELEMETRY", "REQ-GEN2-COMPAT-OBS-001-OBSERVATION", "REQ-GEN2-COMPAT-OBS-001-ROLLBACK"],
"allowed_change_scope": {
"modules": [],
"paths": [
"platform/telemetry/**",
"cmd/core-runtime/**",
"cmd/research-runtime/**",
"modules/interaction/domain/**",
"modules/capability/domain/**",
"adapters/postgres/**",
"docs/**",
"tests/**",
"deploy/run-production-canary.sh",
"deploy/update-supervisor.sh",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-COMPAT-OBS-001-production-compatibility-observation.json"
]
},
"forbidden_change_scope": {
"paths": ["security/**", "go.mod", "go.sum", "architecture/**", ".gitea/workflows/**", "database/**", "adapters/models/**"]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 1,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 0
},
"contracts": {
"consume": ["existing runtime logs and capability usage observation mechanisms"],
"may_change": ["telemetry event shape only; no semantic/domain contract change"]
},
"semantic_owner": "Migration/Governance; telemetry is an operational projection and never a canonical semantic owner.",
"canonical_state": ["no change", "V1 compatibility behavior and Gen2 canonical writers remain unchanged"],
"state_transitions": ["path invocation -> redacted telemetry event -> bounded aggregation/observation -> retirement classification"],
"side_effects_and_privileges": ["stdout/stderr telemetry only; no external mutation, secret access, or user content logging"],
"persistence_and_migrations": ["no schema or migration change; telemetry is emitted to existing process logs"],
"failure_retry_restart": ["telemetry failure must never change or block the compatibility path", "rollback is code revert to the pre-instrumentation commit"],
"assumptions": ["production Supervisor deployment is authorized by this stage", "observation window is bounded and redacted", "no credentials are read"],
"acceptance_scenarios": [
"COMPAT-OBS-001-SCN-01: each listed path has a typed redacted event shape",
"COMPAT-OBS-001-SCN-02: events include invocation, component, read/write, reason, Gen2 availability, fallback/primary, outcome and safe correlation",
"COMPAT-OBS-001-SCN-03: no event contains content, secrets, tokens or raw user identifiers",
"COMPAT-OBS-001-SCN-04: FAST tests prove instrumentation is non-blocking and path behavior is unchanged",
"COMPAT-OBS-001-SCN-05: deployed production smoke produces observable compatibility telemetry",
"COMPAT-OBS-001-EXIT: production observation can classify each path ACTIVE_REQUIRED, ACTIVE_FALLBACK, SHADOW_ONLY, UNUSED or UNKNOWN without claiming retirement from absence of evidence"
],
"required_tests": [
"FAST: targeted go test for changed runtime/domain/adapter packages",
"FAST: targeted go vet for changed packages",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"production smoke: Supervisor candidate deployment and redacted log observation"
],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,42 @@
{
"task_id": "GEN2-DB-001",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Исправить provisioning Gen2 owner roles/schemas до применения migrations 046-050 без ослабления V1 DB isolation.",
"requirement_ids": ["REQ-GEN2-DB-001-ROLES", "REQ-GEN2-DB-001-OWNERSHIP", "REQ-GEN2-DB-001-IDEMPOTENCY", "REQ-GEN2-DB-001-RECOVERY"],
"allowed_change_scope": {
"modules": ["database bootstrap", "deployment migration runner"],
"paths": [
"database/bootstrap/002_gen2_roles_and_schemas.sql",
"deploy/apply-migrations.sh",
"deploy/images/postgres.Dockerfile",
"dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json"
],
},
"forbidden_change_scope": {
"paths": [
"GEN2-028",
"production-canary-before-provisioning",
"feature-migration-role-creation",
"cross-owner-grants",
"new canonical owner",
"production canary before provisioning"
]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["V1 database ownership/ACL", "GEN2 canonical owner registry", "GEN2 execution contract"], "may_change": ["Bootstrap provisioning only; feature migration semantics are unchanged."]},
"semantic_owner": "Dedicated PostgreSQL bootstrap/migration principal; semantic ownership remains World, Configuration, SELF and existing V1 owners.",
"canonical_state": ["world schema owned by world_role", "configuration schema owned by configuration_role", "self schema owned by self_role", "no cross-owner canonical writes"],
"state_transitions": ["missing Gen2 roles/schemas -> idempotently provisioned -> migrations 046-050 apply -> restart/recovery preserves schema and migration state"],
"side_effects_and_privileges": ["Create only the three declared LOGIN owner roles when absent.", "Revoke PUBLIC schema privileges and fail closed on an unexpected schema owner.", "Do not invent or print credentials; do not grant cross-owner access."],
"persistence_and_migrations": ["Provisioning runs in the dedicated bootstrap boundary before feature migrations.", "Migrations 046-050 remain unchanged and are applied by the existing migration history runner."],
"failure_retry_restart": ["Provisioning is safe to run repeatedly.", "Unexpected existing role/schema ownership fails closed and requires an architecture decision.", "PostgreSQL restart/recovery must retain owner roles, schemas, ACL and migration history."],
"assumptions": ["This is a user-authorized remediation task for the previously stopped GEN2-027 migration decision.", "No Gen2 runtime consumer currently requires a new database password; credentials are not invented by this task.", "GEN2-027 resumes only after provisioning and recovery evidence; GEN2-028 is not started."],
"acceptance_scenarios": ["GEN2-DB-001-SCN-01: all roles required by migrations 046-050 are identified", "GEN2-DB-001-SCN-02: provisioning succeeds twice without duplicate objects", "GEN2-DB-001-SCN-03: migrations 046-050 apply in the controlled deployment path", "GEN2-DB-001-SCN-04: schema owners and PUBLIC ACL are confirmed", "GEN2-DB-001-SCN-05: PostgreSQL restart/recovery preserves the result", "GEN2-DB-001-EXIT: migration blocker is removed without weakening V1 isolation"],
"required_tests": ["FAST governance: go run ./cmd/devctl preflight -task dev/tasks/GEN2-DB-001-provision-gen2-owner-roles.json", "FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "FAST whitespace: git diff --check gen2-integration --", "TARGETED: provisioning repeated twice, migrations 046-050, ACL/ownership verification and PostgreSQL restart/recovery"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,36 @@
{
"task_id": "GEN2-DEPLOY-001",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Развернуть независимый Supervisor control plane с durable release state, controlled traffic switching и доказуемым rollback перед GEN2-027.",
"requirement_ids": ["REQ-GEN2-DEPLOY-001-SUPERVISOR", "REQ-GEN2-DEPLOY-001-TRAFFIC", "REQ-GEN2-DEPLOY-001-ROLLBACK", "REQ-GEN2-DEPLOY-001-LIFEBOAT"],
"allowed_change_scope": {
"modules": ["deployment control", "runtime supervisor", "runtime health"],
"paths": [
"deploy/**",
"runtime/supervisor/**",
"runtime/health/**",
"cmd/supervisor/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-DEPLOY-001-supervisor-and-production-rollback.json",
"docs/evidence/**"
]
},
"forbidden_change_scope": {
"paths": ["GEN2-028", "database/**", "security/**", "architecture/**", "Authority/Commit bypass", "cognitive-lifeboat as Supervisor"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["GEN2-027 production canary contract", "runtime/supervisor durable state", "existing deployment update controller"], "may_change": ["deployment topology, Supervisor HTTP control contract and traffic routing only"]},
"semantic_owner": "Operations/Supervisor; not a semantic owner and not a cognitive provider.",
"canonical_state": ["durable release/current/last-known-good evidence owned by Supervisor control plane", "traffic target is Supervisor-owned operational state"],
"state_transitions": ["baseline -> candidate -> observed healthy -> header canary -> promoted OR independent rollback to last-known-good"],
"side_effects_and_privileges": ["Control API is bound only to the production-internal loopback port.", "Supervisor has no database, Authority or Commit credentials.", "Candidate traffic is explicit and rollback remains available."],
"persistence_and_migrations": ["No database changes; durable state is stored in a dedicated named volume outside candidate containers."],
"failure_retry_restart": ["Supervisor restart reloads release and traffic state from its volume.", "Candidate failure must not erase last-known-good or block stable traffic rollback.", "Lifeboat model failure remains a separately reported degraded provider condition."],
"assumptions": ["GEN2-027 resumes only after this task's production evidence; GEN2-028 is not started.", "The existing public ingress forwards to the configured creator web host port."],
"acceptance_scenarios": ["GEN2-DEPLOY-001-SCN-01: Supervisor is a separate healthy production container", "GEN2-DEPLOY-001-SCN-02: release state survives Supervisor restart", "GEN2-DEPLOY-001-SCN-03: canary header reaches candidate while default traffic stays stable", "GEN2-DEPLOY-001-SCN-04: candidate failure rolls traffic back to last-known-good", "GEN2-DEPLOY-001-SCN-05: cognitive lifeboat diagnosis and degraded role are explicit", "GEN2-DEPLOY-001-EXIT: GEN2-027 rollback prerequisites are proven"],
"required_tests": ["FAST: go test ./runtime/supervisor/... ./runtime/health/... ./cmd/supervisor/...", "FAST: go vet ./runtime/supervisor/... ./runtime/health/... ./cmd/supervisor/...", "FAST governance: preflight", "FAST scope: check-diff", "FAST independent: independent", "FAST whitespace: git diff --check", "TARGETED recovery: Supervisor restart, canary and rollback on docker.cin.su"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,23 @@
{
"task_id": "GEN2-DEPLOY-002",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Расширить существующий Supervisor до безопасного управления единым candidate/stable ReleaseSet из creator-web, core-runtime, cognitive-worker и research-runtime без изменения Gen2 semantic ownership.",
"requirement_ids": ["REQ-GEN2-DEPLOY-002-RELEASE-SET", "REQ-GEN2-DEPLOY-002-CANDIDATE-TOPOLOGY", "REQ-GEN2-DEPLOY-002-ROLLBACK", "REQ-GEN2-DEPLOY-002-COMPAT-OBS"],
"allowed_change_scope": {"modules": ["runtime/supervisor"], "paths": ["runtime/supervisor/**", "cmd/supervisor/**", "deploy/**", "docs/**", "tests/**", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-DEPLOY-002-supervised-release-set.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "modules/**", "contracts/**", "security/**", "go.mod", "go.sum", ".gitea/workflows/**", "adapters/models/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["existing Supervisor durable state and production compose", "existing canary header and stable rollback controls"], "may_change": ["Supervisor release control payloads", "deployment topology and candidate routing configuration"]},
"semantic_owner": "Supervisor/deployment control plane; ReleaseSet is operational release state and not semantic canonical state.",
"canonical_state": ["no semantic owner or schema change", "previous stable release and evidence remain retained"],
"state_transitions": ["candidate ReleaseSet -> component health/revision checks -> canary -> observation -> promotion or whole-set rollback"],
"side_effects_and_privileges": ["production deployment only through approved control plane; no direct stable replacement; no credential reads"],
"persistence_and_migrations": ["durable Supervisor release state remains in existing external Supervisor volume; no database migration"],
"assumptions": ["the existing production Supervisor and compose control plane remain authoritative for deployment", "candidate services may share existing durable infrastructure only under current ownership and compatibility rules", "no credentials are read by this task"],
"failure_retry_restart": ["candidate component failure routes all traffic to previous stable ReleaseSet", "Supervisor restart reloads ReleaseSet state", "previous stable is retained until promotion is complete"],
"acceptance_scenarios": ["DEPLOY-002-SCN-01: complete ReleaseSet has all four components and one revision", "DEPLOY-002-SCN-02: candidate dependencies resolve only to candidate services", "DEPLOY-002-SCN-03: health/readiness/revision checks cover all required components", "DEPLOY-002-SCN-04: canary and stable paths are isolated", "DEPLOY-002-SCN-05: forced candidate failure rolls back the complete ReleaseSet", "DEPLOY-002-SCN-06: Supervisor restart preserves stable/candidate/LKG state", "DEPLOY-002-SCN-07: production telemetry revision starts bounded observation after controlled deployment"],
"required_tests": ["go test ./runtime/supervisor ./cmd/supervisor", "FAST: targeted supervisor/release-set tests", "FAST: compose config validation", "FAST: routing and revision consistency tests", "FAST: rollback/restart persistence tests", "FAST: targeted go vet", "FAST governance: preflight/check-diff/independent", "FAST whitespace: git diff --check", "PRODUCTION: candidate ReleaseSet drill and telemetry observation start"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,59 @@
{
"task_id": "GEN2-DEPLOY-003",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Изолировать stable и candidate ReleaseSet в независимые Compose deployment projects/slots, чтобы подготовка candidate не могла пересоздать stable containers, сохранив Supervisor как единственный traffic/promotion/rollback control plane.",
"requirement_ids": ["REQ-GEN2-DEPLOY-003-SLOT-ISOLATION", "REQ-GEN2-DEPLOY-003-UPDATER-OWNERSHIP", "REQ-GEN2-DEPLOY-003-ROLLBACK"],
"allowed_change_scope": {
"modules": ["deployment control", "runtime supervisor"],
"paths": [
"deploy/**",
"runtime/supervisor/**",
"cmd/supervisor/**",
"docs/evidence/**",
"docs/DEPLOYMENT.md",
"tests/deployment/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-DEPLOY-003-isolated-release-slots.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "database/**", "modules/**", "contracts/**", "security/**", "adapters/**", "go.mod", "go.sum", ".gitea/workflows/**", "GEN2-028", "GEN2-029", "GEN2-030"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {
"consume": ["existing Supervisor durable ReleaseSet state", "existing production compose stack", "existing shared durable PostgreSQL/volumes"],
"may_change": ["deployment slot topology", "deployment-controller scripts", "Supervisor operational ReleaseSet identity fields"]
},
"semantic_owner": "Operations/Supervisor; slot and ReleaseSet identity are runtime-only operational control-plane state, not semantic canonical state.",
"canonical_state": ["no semantic owner, canonical writer, schema or data migration change", "Supervisor durable release evidence/LKG remains retained outside candidate containers"],
"state_transitions": ["stable slot retained -> isolated candidate slot start -> four-component health/revision validation -> Supervisor canary -> promote or traffic rollback -> inactive slot retained for rollback"],
"side_effects_and_privileges": ["production operations only through Docker/Compose and Supervisor control plane explicitly authorized for this remediation", "candidate preparation must never directly replace/recreate stable", "no credential reads or outputs", "external updater pause is allowed only as an explicit bounded operation with restore evidence"],
"persistence_and_migrations": ["no database migration", "shared PostgreSQL, named volumes and durable services are attached as existing external resources and are never duplicated or deleted by candidate cleanup"],
"failure_retry_restart": ["capture stable container IDs before every candidate operation and abort before canary when any changes", "candidate component failure returns traffic to LKG without container recreation", "Supervisor restart reloads ReleaseSet/slot/LKG evidence", "updater ownership must be project-scoped so it cannot reconcile the other slot"],
"assumptions": ["current stable may remain in its existing project during the one-time transition; it must not be recreated merely to rename a slot", "external deployment controller can be changed only when its exact owner/process is evidenced", "GEN2-COMPAT-OBS-001, V1 retirement and Gen3 remain stopped until this task passes"],
"acceptance_scenarios": [
"DEPLOY-003-SCN-01: stable and candidate Compose projects, service/container identities and updater ownership are distinct",
"DEPLOY-003-SCN-02: candidate dependencies resolve only to candidate services plus declared shared external resources",
"DEPLOY-003-SCN-03: candidate operation leaves every captured stable container ID unchanged; deviation aborts before canary",
"DEPLOY-003-SCN-04: all four candidate components report health/readiness and one revision",
"DEPLOY-003-SCN-05: Supervisor records ReleaseSet slot identity/revision/LKG and canaries the candidate",
"DEPLOY-003-SCN-06: forced candidate component failure rolls all traffic back to previous stable without stable recreate",
"DEPLOY-003-SCN-07: Supervisor restart preserves LKG/rollback target and candidate recovery does not alter stable IDs",
"DEPLOY-003-EXIT: isolated production ReleaseSet drill passes, after which GEN2-COMPAT-OBS-001 may resume without V1 retirement"
],
"required_tests": [
"FAST: compose config for stable and candidate slots",
"FAST: targeted go test ./runtime/supervisor ./cmd/supervisor",
"FAST: targeted go vet ./runtime/supervisor ./cmd/supervisor",
"FAST: deployment shell regression for project/identity isolation and stable ID invariance",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"PRODUCTION: isolated candidate ReleaseSet health, canary, forced rollback, Supervisor restart/LKG and stable-ID drill"
],
"terminal_gap_states": ["BLOCKED_BY_DEPLOYMENT_CONTROL", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,78 @@
{
"task_id": "GEN2-EVOLVE-001",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Extend the existing Gen2 retrieval fabric with task-relative freshness, bounded salience, scoped positive/negative experience, contradiction candidates and rebuildable consolidation candidates without creating a Memory owner or retiring V1 compatibility.",
"requirement_ids": ["REQ-GEN2-EVOLVE-001-ADAPTIVE-RETRIEVAL", "REQ-GEN2-EVOLVE-001-EXPERIENCE", "REQ-GEN2-EVOLVE-001-CONSOLIDATION"],
"allowed_change_scope": {
"modules": ["interaction", "epistemics", "core-runtime", "cognitive-context", "production evidence"],
"paths": [
"modules/interaction/domain/gen2_context.go",
"modules/interaction/domain/gen2_context_test.go",
"modules/epistemics/domain/freshness.go",
"modules/epistemics/domain/freshness_test.go",
"modules/epistemics/domain/experience.go",
"modules/epistemics/domain/experience_test.go",
"modules/epistemics/domain/adaptive_retrieval.go",
"modules/epistemics/domain/adaptive_retrieval_test.go",
"modules/epistemics/domain/consolidation.go",
"modules/epistemics/domain/consolidation_test.go",
"contracts/epistemics/freshness.go",
"contracts/epistemics/types.go",
"contracts/cognitive/context_compiler.go",
"contracts/cognitive/context_compiler_v2_test.go",
"adapters/postgres/epistemics_repository.go",
"adapters/postgres/interaction_repository.go",
"cmd/core-runtime/main.go",
"cmd/core-runtime/learning.go",
"cmd/core-runtime/cognitive_interpretation.go",
"cmd/core-runtime/cognitive_interpretation_test.go",
"database/migrations/053_gen2_adaptive_experience_scope.sql",
"development/GEN2_MIGRATION_MAP.md",
"docs/evidence/**",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "go.mod", "go.sum", ".gitea/workflows/**", "cmd/supervisor/**", "deploy/**", "GEN2-028", "GEN2-029", "GEN2-030", "PersonalMemory write paths", "Gen3/**"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {
"consume": ["existing Interaction utterance and ProjectReference projection", "existing Epistemics ExperienceRecord and Freshness contracts", "existing Cognitive Context Compiler V2"],
"may_change": ["typed Epistemics retrieval and temporal fields", "typed Context Compiler lane and omission metadata", "no public or cross-owner contract"]
},
"semantic_owner": "Interaction remains canonical owner of human utterances and ProjectReference; Epistemics remains canonical owner of ExperienceRecord and task-relative freshness evidence; Context Compiler remains a rebuildable cognitive projection; Core only composes existing owner reads through established repository boundaries.",
"canonical_state": ["Interaction utterance history and ProjectReference", "Epistemics ExperienceRecord and temporal applicability", "no new Memory canonical state", "no direct cross-owner SQL join or write"],
"persistence_and_migrations": ["add only owner-local Epistemics columns for principal and ContextScope references required to retrieve ExperienceRecord without cross-owner joins", "existing rows remain historical and are not deleted", "migration must be additive and rollback-safe", "migration number 053 is assigned because global migrations 051 and 052 already exist on the task base; no existing migration is changed", "cmd/core-runtime/learning.go is included only to persist the existing authenticated principal into the additive Epistemics metadata written by this task"],
"side_effects_and_privileges": ["no external mutation", "no provider override", "no autonomous Skill or Reflex activation", "no scheduler", "no V1 retirement", "no browser acceptance required"],
"state_transitions": [
"task-relative freshness uses observed/verified/validity/volatility/version/provenance inputs and never turns a stale assessment into permanent deletion",
"current versus historical assertion resolution preserves historical lineage and evaluates applicability before recency",
"ProjectReference and principal filtering precede semantic relevance ranking",
"Epistemics positive and negative ExperienceRecord retrieval is separately lane-classified and preserves failure applicability conditions",
"bounded contradiction candidates are retrieved after the primary candidates without confirmation-only selection",
"Context Compiler receives bounded current-fact, experience, contradiction and historical-analogy lanes with explainable inclusions and omissions",
"consolidation is a deterministic rebuildable projection from raw records to episode/proposition/experience/Skill-candidate evidence; it does not write or activate Skills"
],
"failure_retry_restart": ["bounded retrieval never loops", "missing or old Experience metadata remains historical or excluded rather than treated as current", "rebuild/restart recomputes projections from canonical utterance and Epistemics evidence", "no candidate failure changes stable production traffic"],
"assumptions": ["GEN2-COGNITIVE-002 scoped Interaction retrieval remains the primary assertion reader", "Epistemics owns additive ExperienceRecord metadata", "COMPAT observation and V1 retirement remain deferred by owner", "no browser or production promotion is required for this task"],
"acceptance_scenarios": [
"EVOLVE-001-SCN-01 current versus historical value returns the applicable current value while retaining historical evidence",
"EVOLVE-001-SCN-02 fast-aging and slow-aging inputs receive distinct task-relative freshness assessments",
"EVOLVE-001-SCN-03 Company A and Company B are hard-isolated through typed scope references before ranking",
"EVOLVE-001-SCN-04 relevant memory is selected and irrelevant memory is excluded under a bounded context budget with an explainable omission",
"EVOLVE-001-SCN-05 similar work retrieves both successful and failed applicable ExperienceRecord lanes",
"EVOLVE-001-SCN-06 contradiction retrieval emits a bounded opposing candidate without replacing canonical evidence",
"EVOLVE-001-SCN-07 rebuildable consolidation retains source evidence references and survives repository reload without raw-evidence loss"
],
"required_tests": [
"FAST: targeted go test and go vet for changed interaction, epistemics, cognitive context, core-runtime and postgres packages",
"FAST: devctl preflight, check-diff, independent governance, git diff --check",
"MILESTONE: changed owners plus migration consistency and focused restart/rebuild retrieval check; no browser or FULL suite"
],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,23 @@
{
"task_id": "GEN2-EVOLVE-002",
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Wire one bounded cognitive memory-continuation round through the existing Interaction/Epistemics retrieval fabric and Context Compiler without a new Memory owner, V1 retirement, UI change, direct model storage access or an unrestricted memory tool.",
"requirement_ids": ["REQ-GEN2-EVOLVE-002-ITERATIVE-RETRIEVAL"],
"allowed_change_scope": {"modules": ["cognitive contracts", "core-runtime", "cognitive-worker", "Gen2 governance and evidence"], "paths": ["contracts/cognitive/context_compiler.go", "contracts/cognitive/context_compiler_v2_test.go", "contracts/cognitive/types.go", "cmd/core-runtime/cognitive_interpretation.go", "cmd/core-runtime/cognitive_interpretation_test.go", "cmd/core-runtime/main.go", "cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/gateway_registry_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-EVOLVE-002-iterative-retrieval-and-cognitive-memory-continuation.json", "docs/evidence/**", "docs/PROJECT_STATUS.md"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "database/**", "deploy/**", "cmd/supervisor/**", "cmd/creator-web/**", "PersonalMemory write paths", "GEN2-028", "GEN2-029", "GEN2-030", "Gen3/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["existing Interaction and Epistemics retrieval projections", "existing Context Compiler V2"], "may_change": ["Cognitive iterative-retrieval request, inherited continuation envelope and advisory response fields only"]},
"semantic_owner": "Interaction remains canonical owner of utterance assertions and ProjectReference; Epistemics remains canonical owner of experience. Core validates and binds continuation identity/scope/policy before calling existing owner reads; Context Compiler remains the only context selector; cognitive-worker may emit only a typed retrieval intent and never reads storage.",
"canonical_state": ["no new state is written", "no new Memory owner", "no direct model or cognitive-worker storage access", "no cross-owner SQL join"],
"persistence_and_migrations": ["no schema or migration change"],
"side_effects_and_privileges": ["no external mutation", "no provider override", "no UI/browser change", "no V1 retirement"],
"state_transitions": ["initial Context Compiler context -> cognitive-worker may emit a typed retrieval intent -> Core binds inherited Principal/Scope/Focus/task/freshness/policy -> existing owner retrieval hard-filters scope before ranking -> Context Compiler compiles unique bounded additions -> one continuation advisory request", "no result -> explicit no-additional-evidence continuation", "contradiction or negative experience is lane-classified rather than suppressed"],
"failure_retry_restart": ["one bounded continuation round only", "round/bytes/fragments/latency and duplicate budgets fail closed", "missing evidence remains unknown", "continuation state is request-scoped; durable Work/Investigation remains responsible for any restartable task"],
"assumptions": ["the task branch starts at gen2-integration 850fe92", "UI/browser acceptance remains deferred by owner", "the traced existing IterativeRetrievalRequest lacks the required structured lane/reason and inherited runtime envelope, so this revision changes only those Cognitive contract fields"],
"acceptance_scenarios": ["EVOLVE-002-SCN-01 insufficient initial context can obtain one additional current fact", "EVOLVE-002-SCN-02 negative and contradiction lanes are separately requested", "EVOLVE-002-SCN-03 inherited Company A scope rejects Company B regardless of request", "EVOLVE-002-SCN-04 duplicate excerpts and rounds/byte limits are rejected", "EVOLVE-002-SCN-05 no result preserves an explicit unknown/no-additional-evidence state"],
"required_tests": ["FAST: targeted go test and go vet for changed cognitive contract, core-runtime and cognitive-worker packages", "FAST: devctl preflight, check-diff, independent governance, git diff --check", "MILESTONE: changed owner/runtime tests plus focused bounded continuation and restart-state check; no browser or FULL suite"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,39 @@
{
"task_id": "GEN2-EVOLVE-003",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Implement the bounded evidence-governed Experience to Skill to Reflex learning lifecycle using existing Epistemics, Capability and SELF owners: candidate, independent validation, applicable routing, bounded shadow/revalidation, downgrade/retirement and Learning Ledger evidence, without autonomous production mutation.",
"requirement_ids": ["REQ-GEN2-EVOLVE-003-EXPERIENCE-SKILL-REFLEX"],
"allowed_change_scope": {
"modules": ["capability", "self", "core-runtime learning", "Gen2 evidence and governance"],
"paths": [
"contracts/capability/skill_v2.go", "contracts/capability/reflex_v2.go", "contracts/capability/resolution.go", "contracts/capability/intelligence.go", "contracts/self/types.go",
"modules/capability/domain/skill.go", "modules/capability/domain/skill_test.go", "modules/capability/domain/reflex.go", "modules/capability/domain/reflex_test.go", "modules/capability/domain/intelligence_test.go",
"modules/self/domain/self.go", "modules/self/domain/self_test.go",
"adapters/postgres/capability_repository.go", "adapters/postgres/self_repository.go", "adapters/postgres/capability_repository_test.go", "adapters/postgres/self_repository_test.go",
"database/migrations/054_experience_skill_reflex_learning_lifecycle.sql",
"cmd/core-runtime/learning.go", "cmd/core-runtime/learning_test.go",
"dev/ACTIVE_TASK.json", "dev/tasks/GEN2-EVOLVE-003-experience-skill-reflex-learning-lifecycle.json", "docs/evidence/**", "docs/PROJECT_STATUS.md"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "deploy/**", "cmd/creator-web/**", "cmd/cognitive-worker/**", "adapters/models/**", "modules/epistemics/**", "adapters/postgres/epistemics_repository.go", "PersonalMemory write paths", "GEN2-028", "GEN2-029", "GEN2-030", "Gen3/**"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {
"consume": ["existing Epistemics ExperienceRecord/ReflectionCandidate", "existing Capability Skill V2/Reflex V2", "existing SELF LearningLedger/ImprovementNeed", "existing Intelligence Strategy"],
"may_change": ["Capability and SELF typed lifecycle/evidence fields necessary for bounded candidate, validation, routing and revalidation semantics only"]
},
"semantic_owner": "Epistemics remains canonical owner of Experience and Outcome evidence; Capability remains sole canonical owner of Skill and Reflex lifecycle; SELF remains canonical owner of ImprovementNeed, competence/weakness assessments and Learning Ledger. Core Runtime only records evidence through owner repositories and never self-promotes production behavior.",
"canonical_state": ["Experience/Outcome evidence is referenced, never copied as a new source of truth", "Capability writes Skill/Reflex revisions and heads; SELF writes Learning Ledger and ImprovementNeed", "candidate, validation, shadow and revalidation never grant independent production mutation authority", "no new Memory owner, parallel Skill registry or cross-owner SQL join"],
"side_effects_and_privileges": ["No external mutation may bypass Authority -> Commit -> Executor -> verified Outcome.", "SELF may observe, evaluate and propose only; production promotion remains behind the existing governed development gates and independent evaluation.", "No provider invocation, privilege expansion, UI/browser change or V1 retirement is authorized."],
"persistence_and_migrations": ["054 is the next verified global migration number after 053; it is additive and uses existing capability_role and self_role only", "revision 3 sets devctl's database-surface compatibility delta to one solely because devctl requires it for any migration; no database owner role is created", "persist Skill/Reflex revision/head and SELF learning evidence needed for restart-safe lifecycle state; existing historical records remain untouched", "rollback disables/refuses non-active Reflex selection and returns to existing Skill/reasoning path without deleting evidence"],
"state_transitions": ["verified positive Experience with a bounded pattern and provenance -> Candidate Skill", "independent validation/replay evidence and must-not-worsen criteria -> Validated Skill -> Active eligible routing", "frequent stable Active Skill with deterministic bounded implementation and verifier -> Candidate Reflex -> Validated/Active Reflex", "dependency change, stale evidence, correction, failure trend or shadow divergence -> Stale/Suspicious -> revalidation request -> Active, rewrite proposal or Retired", "Reflex decline/failure -> Skill -> reasoning; no execution can bypass Authority/Commit/Executor"],
"failure_retry_restart": ["one success never promotes Candidate directly to Active Skill or Reflex", "shadow is sampled/bounded and never delays ordinary low-risk response; divergence records evidence and ImprovementNeed rather than rewriting Reflex", "revalidation is bounded and never retries endlessly; real failures remain fail-closed and escalate", "durable revisions, validation evidence and Ledger survive restart; no direct provider invocation or process-local canonical state"],
"assumptions": ["task branch starts at gen2-integration a07aba0", "UI/browser acceptance remains deferred by owner; V1 compatibility remains retained", "a verified inspection found 054 free, existing SkillProfile persistence, Reflex V2 contract without durable lifecycle persistence, and SELF typed ledger without a repository", "revision 2 adds only contracts/capability/resolution.go because the existing canonical SkillProfile is declared there and its durable evidence fields must be additive rather than creating a parallel Skill registry", "revision 3 satisfies devctl's migration compatibility gate without creating any role or privilege", "automatic arbitrary production code generation is not in scope; candidate executable strategies must use an existing bounded implementation reference and governed development remains the only production-promotion route"],
"acceptance_scenarios": ["EVOLVE-003-SCN-01 verified outcome and pattern create Candidate Skill with source Experience/Outcome references", "EVOLVE-003-SCN-02 independent replay/evaluation evidence promotes Skill only after repeatability and must-not-worsen checks", "EVOLVE-003-SCN-03 applicable fresh Active Skill is routed; inapplicable, stale or weak Skill escalates to reasoning", "EVOLVE-003-SCN-04 sufficient stable evidence creates Candidate Reflex; verified bounded Reflex becomes Active and retains fallback", "EVOLVE-003-SCN-05 dependency change, correction, failure trend and stale validation trigger bounded revalidation/downgrade rather than silent use", "EVOLVE-003-SCN-06 sampled shadow divergence records ImprovementNeed/LearningLedger evidence and does not rewrite production Reflex", "EVOLVE-003-SCN-07 user correction links the affected Skill/Reflex revision and preserves conditional known failure evidence", "EVOLVE-003-SCN-08 retired Reflex is excluded from routing while all evidence/history remains queryable"],
"required_tests": ["FAST: targeted go test and go vet for changed capability, self, core-runtime and postgres packages", "FAST: devctl preflight, check-diff, independent governance, architecture check, git diff --check", "MILESTONE: focused lifecycle/persistence/restart and escalation tests only; no browser, production canary or FULL suite"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,23 @@
{
"task_id": "GEN2-EVOLVE-004-RUNTIME-001",
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Wire the existing bounded SELF learning loop into the Core composition root without creating a new owner or bypassing existing ports.",
"requirement_ids": ["REQ-GEN2-EVOLVE-004-SELF-RUNTIME-WIRING"],
"allowed_change_scope": {"modules": ["core-runtime learning composition", "SELF persistence integration", "production secret reference wiring", "targeted evidence"], "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/learning.go", "cmd/core-runtime/learning_test.go", "adapters/postgres/self_repository.go", "adapters/postgres/self_repository_test.go", "deploy/docker-compose.prod.yml", "deploy/docker-compose.candidate-slot.yml", "docs/CREDENTIALS_AND_SECRETS.md", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-EVOLVE-004-RUNTIME-001-wire-self-learning-loop.json", "docs/evidence/**", "docs/PROJECT_STATUS.md"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "cmd/cognitive-worker/**", "modules/**", "adapters/models/**", "database/**", "deploy/caddy/**", "deploy/images/**", "deploy/windows/**", "deploy/PORTAINER.md", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["existing learningRecorder", "existing SELF repository methods", "existing SELF domain contracts"], "may_change": ["Core composition wiring only"]},
"semantic_owner": "SELF remains the sole owner of derived learning state; Core only coordinates evidence recording.",
"canonical_state": ["SELF write failures never replace a completed user response with an error", "no direct database access outside SelfRepository", "no automatic promotion or production mutation"],
"state_transitions": ["learning signal -> SelfRepository -> bounded derived evidence -> ledger"],
"side_effects_and_privileges": ["use existing self_role repository boundary; do not read or print secrets", "the positive new_privileges value is only the devctl deployment-surface compatibility gate; no privilege is granted and self_role grants are unchanged"],
"persistence_and_migrations": ["no schema change; consume migration 055 tables"],
"failure_retry_restart": ["recording is best-effort after response completion and durable when repository is available; restart reads remain repository-owned"],
"assumptions": ["existing external assistant_core_secrets control plane can materialize self_db_password without exposing its value", "existing production database has migration 055"],
"acceptance_scenarios": ["correction/failure reaches SelfRepository", "repository failure is diagnostic and non-fatal", "durable evidence remains available after repository reopen"],
"required_tests": ["targeted go test", "targeted go vet", "preflight", "check-diff", "independent", "git diff --check", "focused MILESTONE"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,42 @@
{
"task_id": "GEN2-EVOLVE-004",
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Implement a bounded evidence-only SELF learning loop: observed signals -> contextual weakness diagnosis -> ImprovementNeed -> candidate proposal -> independent evaluation plan -> Learning Ledger, without autonomous code generation, production promotion or a new owner.",
"requirement_ids": ["REQ-GEN2-EVOLVE-004-SELF-IMPROVEMENT-LOOP"],
"allowed_change_scope": {
"modules": ["self", "self benchmark", "core-runtime learning evidence", "Gen2 evidence and governance"],
"paths": [
"contracts/self/types.go",
"contracts/self/benchmark.go",
"modules/self/domain/self.go",
"modules/self/domain/self_test.go",
"adapters/postgres/self_repository.go",
"adapters/postgres/self_repository_test.go",
"database/migrations/055_self_weakness_diagnosis_and_benchmark.sql",
"cmd/core-runtime/learning.go",
"cmd/core-runtime/learning_test.go",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json",
"docs/evidence/**",
"docs/PROJECT_STATUS.md"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "deploy/**", "cmd/creator-web/**", "cmd/cognitive-worker/**", "adapters/models/**", "modules/capability/**", "modules/epistemics/**", "database/migrations/001-054", "PersonalMemory write paths", "GEN2-028", "GEN2-029", "GEN2-030", "Gen3/**"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["existing Epistemics Experience/Outcome evidence", "existing Capability Skill/Reflex references", "existing SELF CompetenceAssessment/WeaknessPattern/ImprovementNeed/LearningLedger/PersonalBenchmark", "existing Work governed-evolution gates"], "may_change": ["SELF diagnosis, bounded candidate-proposal and benchmark case typed fields only"]},
"semantic_owner": "SELF remains the sole owner of derived competence, weakness, ImprovementNeed, PersonalBenchmark and Learning Ledger state. Epistemics remains owner of source evidence; Capability remains owner of Skill/Reflex; Work/development gates remain owner of executable candidate and promotion.",
"canonical_state": ["diagnosis is evidence-backed and may remain HYPOTHESIS/UNKNOWN", "repeated incidents are required for WeaknessPattern", "SELF observes/evaluates/proposes but cannot promote or mutate production", "benchmark cases reference evidence and do not copy conversations or secrets", "no new Memory owner, Skill registry, orchestrator or canonical writer"],
"side_effects_and_privileges": ["No external mutation bypasses Authority -> Commit -> Executor -> verified Outcome.", "No autonomous code generation, merge, deployment, production promotion or provider invocation is authorized.", "Data is scoped by Principal/ContextScope and privacy metadata; hidden reasoning and user data are not stored."],
"persistence_and_migrations": ["055 is the next free global migration after 054 and is additive under existing self_role only", "the positive new_database_owner_roles value is a devctl database-surface compatibility gate, not authorization for a new role; no new owner role is created", "persist contextual weakness incidents, ImprovementNeed diagnosis/candidate metadata, bounded benchmark references and Learning Ledger outcomes; preserve all historical evidence", "rollback disables proposal generation and leaves observations/evidence queryable"],
"state_transitions": ["verified failure/correction/fallback/divergence signal -> scoped evidence", "repeated scoped evidence -> WeaknessPattern; one random failure remains an incident", "supported diagnosis -> ImprovementNeed with intervention class, risk and expected effect", "ImprovementNeed -> candidate proposal/evaluation plan only", "independent evaluation and existing governed development gates decide promotion; actual result is appended to Learning Ledger"],
"failure_retry_restart": ["bounded diagnosis/candidate/evaluation budgets and deduplication prevent self-review loops", "provider outage is not competence loss; diagnosis distinguishes unavailable dependency", "stale/unknown/hypothesis diagnosis is never presented as established fact", "durable SELF state survives restart; no process-local canonical state"],
"assumptions": ["task branch starts at gen2-integration dd04a67", "UI/browser acceptance remains deferred and V1 compatibility remains retained", "existing Work governed-evolution contract is consumed rather than duplicated", "automatic executable production changes remain a future governed task"],
"acceptance_scenarios": ["EVOLVE-004-SCN-01 repeated user corrections attach to the affected scoped Skill/Reflex decision", "EVOLVE-004-SCN-02 repeated incidents create a WeaknessPattern while one random failure does not", "EVOLVE-004-SCN-03 diagnosis distinguishes retrieval/context/skill/reflex/provider/tool/outcome causes and preserves confidence/evidence", "EVOLVE-004-SCN-04 competence is contextual and evidence-derived, never self-declared", "EVOLVE-004-SCN-05 ImprovementNeed produces a bounded candidate proposal with risk, expected effect, evaluation and rollback", "EVOLVE-004-SCN-06 Creator and Evaluator are distinct; must-not-worsen can reject a candidate", "EVOLVE-004-SCN-07 Personal Benchmark cases reference confirmed evidence for memory, scope, failure avoidance, household, technical, continuity and authority/outcome classes", "EVOLVE-004-SCN-08 expected versus actual benefit is appended to Learning Ledger; no autonomous promotion occurs", "EVOLVE-004-SCN-09 diagnosis/candidate budgets and deduplication prevent infinite self-review"],
"required_tests": ["FAST: targeted go test and go vet for changed self, core-runtime, contracts and postgres packages", "FAST: devctl preflight, check-diff, independent governance, architecture check, git diff --check", "MILESTONE: focused evidence attribution, diagnosis, deduplication, contextual competence, candidate/evaluation, benchmark and Learning Ledger tests; no FULL suite, deployment or browser acceptance"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,97 @@
{
"task_id": "GEN2-REM-CORE-M5-LOCAL-MEDIUM-001",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Restore the existing Local MEDIUM cognitive path and make discovery, invocation, model availability and readiness evidence bounded and distinct for Core M5 acceptance.",
"requirement_ids": [
"REQ-GEN2-027-GOAL",
"REQ-GEN2-027-BOUNDARY",
"REQ-GEN2-027-GATE"
],
"allowed_change_scope": {
"modules": ["configuration", "capability", "operations/supervisor"],
"paths": [
"cmd/cognitive-worker/**",
"cmd/core-runtime/**",
"runtime/health/**",
"runtime/supervisor/**",
"deploy/**",
"modules/configuration/**",
"contracts/configuration/**",
"tests/**",
"docs/evidence/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-REM-CORE-M5-LOCAL-MEDIUM-001.json"
]
},
"forbidden_change_scope": {
"paths": [
"security/**",
"architecture/**",
"database/**",
"adapters/models/**",
"go.mod",
"go.sum",
".gitea/workflows/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_database_owner_roles": 0,
"new_third_party_dependencies": 0
},
"contracts": {
"consume": ["contracts.capability@gen2", "contracts.configuration@gen2"],
"may_change": ["Only bounded readiness and provider-routing behavior in the declared paths."]
},
"semantic_owner": "Capability and Operations/Supervisor; no new semantic owner.",
"canonical_state": [
"Existing Capability registry remains authoritative for binding qualification and activation.",
"Lifeboat remains fallback only; it does not replace Local MEDIUM."
],
"state_transitions": [
"provider discovery -> qualified binding -> active route -> unavailable/degraded -> recovered active route"
],
"side_effects_and_privileges": [
"No external mutation; no credential value is read into evidence; no new privilege or database change."
],
"persistence_and_migrations": [
"No database schema or migration change. Existing provider and capability state remain authoritative."
],
"assumptions": [
"The existing llm-local gateway and protected provider references are the approved Local MEDIUM deployment path.",
"Production data and secret values are not copied or exposed."
],
"failure_retry_restart": [
"Provider probes have bounded per-provider timeouts and do not block unrelated healthy paths.",
"Core readiness is ready when required infrastructure and cognitive worker readiness are available; optional provider loss is represented as degraded fleet state.",
"Restart must re-run discovery and restore the same routing without changing canonical state."
],
"acceptance_scenarios": [
"SQLite/gateway contention is identified without reading credentials.",
"Local MEDIUM discovery health, invocation health and model availability are separately observable.",
"Local MEDIUM authenticated model probe and a real cognitive invocation succeed.",
"CPU/GPU loss does not prevent MEDIUM selection; Lifeboat is fallback.",
"MEDIUM loss selects Lifeboat and recovery returns selection to MEDIUM.",
"Core and cognitive-worker restart restore readiness and routing."
],
"required_tests": [
"FAST: go test ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/health/... ./runtime/supervisor/...",
"FAST: go vet ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/health/... ./runtime/supervisor/...",
"FAST governance: go run ./cmd/devctl preflight -task dev/tasks/GEN2-REM-CORE-M5-LOCAL-MEDIUM-001.json",
"FAST scope: go run ./cmd/devctl check-diff -task dev/tasks/GEN2-REM-CORE-M5-LOCAL-MEDIUM-001.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"Targeted production provider/restart/recovery smoke"
],
"terminal_gap_states": [
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,28 @@
{
"task_id": "GEN2-SECRETS-001",
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Provide an idempotent, local-only, cryptographically secure machine-credential provisioner and rotation mechanism on the existing protected Docker secret control plane, initially for the delegated control-plane service token, without creating a second semantic owner or exposing a secret value.",
"requirement_ids": ["REQ-GEN2-SECRETS-001-LOCAL-MACHINE-CREDENTIALS", "REQ-GEN2-AUTHORITY-001-DELEGATED-CONTROL-PLANE"],
"allowed_change_scope": {
"modules": ["technical protected secret file backend", "production deployment/bootstrap secret provisioning", "targeted Core/Commit service-authentication mount wiring", "targeted tests and evidence"],
"paths": ["platform/secrets/**", "contracts/secrets/types.go", "cmd/secret-control-plane/**", "cmd/core-runtime/main.go", "cmd/commit-runtime/control_plane_authority.go", "deploy/images/runtime.Dockerfile", "deploy/docker-compose.prod.yml", "deploy/docker-compose.candidate-slot.yml", "deploy/provision-machine-credential.sh", "deploy/provision-machine-credential_test.sh", "deploy/verify-machine-credential.sh", "docs/CREDENTIALS_AND_SECRETS.md", "docs/evidence/**", "development/GEN2_TASK_INDEX.md", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-SECRETS-001-local-machine-credential-provisioning.json"]
},
"forbidden_change_scope": {
"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "cmd/devctl/**", "contracts/authority/**", "modules/authority/**", "adapters/postgres/**", "database/**", "modules/identity/**", "modules/capability/**", "cmd/cognitive-worker/**", "cmd/creator-web/**", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**", "go.mod", "go.sum"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["existing CredentialRef and FileResolver", "existing Docker protected-volume deployment mechanism"], "may_change": ["typed SecretPurpose/consumer metadata only if required for the provisioner"]},
"semantic_owner": "No new semantic owner is introduced. platform/secrets remains a technical bootstrap/resolution mechanism; Identity, Authority, Capability and Configuration ownership remain unchanged.",
"canonical_state": ["credential values live only in protected local artifacts, never in Configuration, Memory, context, PostgreSQL, Compose environment, Git, logs or evidence", "machine authentication is distinct from Authority grants", "metadata/audit is separate from the value and contains no plaintext or reversible material", "the control-plane token is mounted read-only only to Core and Commit runtimes"],
"state_transitions": ["ABSENT -> ACTIVE version one via idempotent CSPRNG provisioning", "ACTIVE -> ROTATING -> ACTIVE new version after atomic artifact replacement and controlled verification", "ACTIVE -> COMPROMISED/REVOKED -> replacement version with audit retained"],
"side_effects_and_privileges": ["a one-shot local control-plane container may create or rotate only its dedicated external Docker volume", "Core and Commit get read-only mounts; all other runtimes receive no mount", "no plaintext environment variable is added"],
"persistence_and_migrations": ["no application database schema or canonical writer changes", "secret metadata and append-only audit reside separately from the secret value inside the protected control-plane volume", "rollback retains prior metadata and revokes only after a verified replacement"],
"failure_retry_restart": ["restart is idempotent and preserves the active artifact", "partial writes are avoided by atomic replacement", "a missing or unreadable artifact fails service authentication closed", "rotation never emits a credential and leaves a valid active artifact on failure"],
"assumptions": ["docker.cin.su permits creation of a dedicated external Docker volume by the existing production deployment control plane", "the runtime image can execute a one-shot local provisioner as the control-plane writer", "the existing protected secret volume pattern is the approved production mechanism"],
"acceptance_scenarios": ["missing credential is generated once with crypto/rand and then reused across restart", "metadata and audit are separate from the value", "Core and Commit mounts are read-only and no other runtime mounts the dedicated token volume", "rotation accepts the new credential and rejects the old credential", "logs and diagnostics redact known secret headers/fields", "a valid service credential alone does not bypass the existing delegated Authority grant checks"],
"required_tests": ["targeted go test ./platform/secrets ./cmd/secret-control-plane", "targeted go vet ./platform/secrets ./cmd/secret-control-plane", "compose config validation", "provisioner shell regression", "preflight", "check-diff", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
+1 -1
View File
@@ -1641,7 +1641,7 @@ Historical migrations are evidence. Gen2 migration starts at the next available
# 41. New Gen2 schema migration groups
Exact migration numbers are assigned after current repository HEAD is fixed.
Exact migration numbers are assigned after current repository HEAD is fixed. The repository uses one global sequence: `051_identity_principal_realms.sql` and `052_identity_creator_accounts.sql` occupy their numbers. GEN2-EVOLVE-001 therefore adds owner-local Epistemics metadata as additive migration `053_gen2_adaptive_experience_scope.sql`; it does not alter either existing migration.
Recommended groups:
+4
View File
@@ -1,5 +1,9 @@
# DOMOVOY Gen2 — Task Index
## GEN2-SECRETS-001 — Local Machine Credential Provisioning & Rotation
Bounded post-M5 remediation. Provides the technical protected-secret control-plane mechanism required by `GEN2-AUTHORITY-001`; it does not change semantic ownership or grant Authority.
Это исполняемый индекс task packets. `ARCH-GEN2-000` соответствует разделу `GEN2-000` Master Plan: префикс `ARCH-` нужен текущему `devctl`, чтобы последующие packets могли ссылаться на него через `architecture_revision`.
| Порядок | Task | Milestone | Gate | Base ref | Зависимость | Файл |
+6
View File
@@ -21,6 +21,12 @@ Do not guess Portainer credentials and do not search other project source trees
## Production services
Each privileged runtime gets only its required secret/capability bindings. `commit-runtime` is intentionally isolated from the general Core Runtime.
The Core SELF learning repository uses the existing `assistant_core_secrets` volume and credential-reference mechanism with `self_role` and `credential://domovoy/self/db-password/v1`. The secret is consumed from the fixed `/run/secrets/self_db_password` path; its value is never placed in Compose environment, Git, logs or ordinary configuration. The role and schema already exist; this wiring does not create a role or broaden privileges.
Delegated non-interactive control-plane work is separately authenticated. The local `secret-control-plane` one-shot control-plane service provisions `control_plane_service_token` with `crypto/rand` into the dedicated external `assistant_control_plane_service_secrets` volume. The token artifact is mode `0400`; metadata and audit are separate protected files and contain no value or reversible fingerprint. Only `core-runtime` and the existing Authority/Commit runtime mount that volume read-only at `/run/control-plane-secrets`; no model, Skill, SELF, creator-web, cognitive-worker or research-runtime receives the mount. Compose carries only the non-secret credential reference `credential://domovoy/control-plane/service-token/v1`.
The provisioner is idempotent: restart reuses an existing safe active artifact. Rotation creates a new value by atomic replacement, records non-secret metadata/audit, and never accepts a plaintext environment variable. The service token is not a Creator session, cannot impersonate a human, and never grants Authority by itself. Removing or rotating the token disables/refreshes service authentication; revoking the durable Authority grant also blocks new delegated operations without deleting audit history.
Creator authentication secrets are stored in the external Docker volume `assistant_core_secrets` and mounted read-only only at `/run/secrets` in `core-runtime`. The production container environment contains `_FILE` paths, not the secret values. Files are owned by runtime uid 100/gid 101 with mode 0400. `CREATOR_USERNAME` is not secret and remains an environment value. Development may continue to use direct environment values, but a direct value and its matching `_FILE` setting must never be configured together.
The PostgreSQL off-host backup passphrase is generated randomly and stored as CurrentUser DPAPI ciphertext at `%LOCALAPPDATA%\Domovoy\postgres-backup-key.dpapi.json`. An atomic copy of that DPAPI ciphertext, never the plaintext passphrase, is retained at `\\nas\MST\codex\_backups\domovoy\keys\postgres-backup-key.dpapi.json`. The passphrase is never stored in Git, the production Docker host, a scheduled-task argument, or NAS plaintext. The NAS backup directory receives only GnuPG AES-256 ciphertext and a non-secret integrity manifest. Recovery therefore requires both the NAS artifacts and the authorized Windows DPAPI profile.

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