Compare commits

..
Author SHA1 Message Date
m 990928618d fix(supervisor): stabilize control JSON contract
CI / test (push) Failing after 31s
2026-08-29 12:41:22 +03:00
m 9fac8284f5 merge: initialize supervisor state volume permissions
CI / test (push) Failing after 27s
2026-08-29 12:39:01 +03:00
m 7d9a5edee5 fix(deploy): initialize supervisor volume permissions
CI / test (push) Failing after 32s
2026-08-29 12:38:53 +03:00
m 32d3f318a5 merge: independent supervisor control plane
CI / test (push) Failing after 31s
2026-08-29 12:33:54 +03:00
m 3a563a29ee feat(deploy): add independent supervisor control plane
CI / test (push) Failing after 28s
2026-08-29 12:33:44 +03:00
m 734db5d779 merge: synchronize integration with current main
CI / test (push) Failing after 28s
2026-08-29 12:25:26 +03:00
m 36fc8ec8c6 merge: resume Gen2 controlled canary
CI / test (push) Failing after 27s
2026-08-29 12:16:21 +03:00
m b8c001be4c chore(gen2): resume controlled production canary
CI / test (push) Failing after 26s
2026-08-29 12:16:09 +03:00
m eeeba6b7c0 merge: Gen2 database provisioning remediation
CI / test (push) Failing after 31s
2026-08-29 12:12:43 +03:00
m fce3ec569c fix(database): provision Gen2 owner roles before migrations
CI / test (push) Failing after 27s
2026-08-29 12:12:39 +03:00
m a925d787dc GEN2-027: prepare controlled production canary
CI / test (push) Failing after 26s
2026-08-29 12:01:37 +03:00
m 43556e1603 GEN2-026: route household context through scoped Gen2 projection
CI / test (push) Failing after 28s
2026-08-29 12:00:04 +03:00
m dd00dabf32 GEN2-025: normalize multimodal ingress provenance
CI / test (push) Failing after 27s
2026-08-29 11:58:34 +03:00
m 33365ce7ee GEN2-024: establish principal realms and source trust
CI / test (push) Failing after 26s
2026-08-29 11:57:19 +03:00
m 5017a854ee GEN2-023: make conversational configuration intent explicit
CI / test (push) Failing after 29s
2026-08-29 11:55:38 +03:00
m 839b35f310 GEN2 M4 remediation: durable recovery and governed evolution
CI / test (push) Failing after 26s
2026-08-29 11:47:05 +03:00
m 96687140ce GEN2-022: govern self evolution through development gates
CI / test (push) Failing after 27s
2026-08-29 11:31:45 +03:00
m b282038949 GEN2-021: add independent supervisor lifeboat
CI / test (push) Failing after 27s
2026-08-29 11:30:49 +03:00
m 4de506a2af GEN2-020: separate execution receipts from world outcomes
CI / test (push) Failing after 26s
2026-08-29 11:30:05 +03:00
m 36272b9036 GEN2-019: add advisory consequence assessment
CI / test (push) Failing after 29s
2026-08-29 11:28:52 +03:00
m a779568622 GEN2-018: add bounded perspectives over Work Graph
CI / test (push) Failing after 27s
2026-08-29 11:28:05 +03:00
m 243a11490b GEN2-017: add durable long-running investigations
CI / test (push) Failing after 26s
2026-08-29 11:27:21 +03:00
m 26207d93ef GEN2-016: add intelligence strategy and benchmark evidence
CI / test (push) Failing after 26s
2026-08-29 11:16:07 +03:00
m 42871f2e47 GEN2-015: add evidence-backed SELF ledger
CI / test (push) Failing after 28s
2026-08-29 11:13:38 +03:00
m 6ea1d4b729 GEN2-014: add reflex lifecycle and escalation
CI / test (push) Failing after 28s
2026-08-29 11:12:42 +03:00
m c6d1ebf36f GEN2-013: add versioned Skill V2 contracts
CI / test (push) Failing after 27s
2026-08-29 11:11:50 +03:00
m 25d8d6d219 GEN2-012: add typed scoped configuration
CI / test (push) Failing after 33s
2026-08-29 11:09:33 +03:00
m 6cbef9af41 GEN2-012: add scoped configuration bootstrap boundary
CI / test (push) Failing after 34s
2026-08-29 11:08:53 +03:00
m 2fe9802e5a GEN2-011: cut PersonalMemory to compatibility fallback
CI / test (push) Failing after 37s
2026-08-29 02:41:23 +03:00
m 0882df3ad2 GEN2-010: add bounded Context Compiler V2 trace
CI / test (push) Successful in 53s
2026-08-29 02:39:58 +03:00
m 0acbe53fad GEN2-009: add rebuildable retrieval projections
CI / test (push) Successful in 50s
2026-08-29 02:38:49 +03:00
m e634bb946a GEN2-008: promote experience and episode projections
CI / test (push) Successful in 55s
2026-08-29 02:37:28 +03:00
m c7ad996cae GEN2-007: add task-relative freshness assessment
CI / test (push) Successful in 52s
2026-08-29 02:35:54 +03:00
m a065382512 GEN2-006: normalize epistemic propositions and assessments
CI / test (push) Successful in 51s
2026-08-29 02:33:09 +03:00
m 8a8e571905 GEN2-005: govern identity resolution and version bindings
CI / test (push) Failing after 38s
2026-08-29 02:20:02 +03:00
m 301dbdcfb2 GEN2-004: add ContextScope and FocusStack V2
CI / test (push) Successful in 50s
2026-08-29 02:17:36 +03:00
m 8037e66998 GEN2-003: add typed World referent foundation
CI / test (push) Successful in 50s
2026-08-29 02:11:36 +03:00
m 29815c49b8 GEN2-002: add shared semantic primitives
CI / test (push) Successful in 52s
2026-08-29 02:08:20 +03:00
m c14ad7fd0b GEN2-001: accept owner registry and dependency rules
CI / test (push) Successful in 49s
2026-08-29 02:05:35 +03:00
m 939a5152e1 ARCH-GEN2-000: adopt Gen2 architecture governance
CI / test (push) Successful in 48s
2026-08-29 01:53:27 +03:00
m 4e54ace76b Route local place searches to relevant evidence
CI / test (pull_request) Successful in 1m19s
CI / test (push) Successful in 47s
2026-08-26 10:40:15 +03:00
m f329224d12 Merge pull request 'Keep nearest flight reply focused' (#405) from codex/household-text-cognition-path into main
CI / test (push) Successful in 1m21s
Reviewed-on: #405
2026-08-26 07:33:36 +00:00
m bb76171af4 Merge remote-tracking branch 'origin/main' into codex/household-text-cognition-path
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m23s
2026-08-26 10:30:15 +03:00
m 213f06661d Keep nearest flight reply focused
CI / test (push) Failing after 59s
CI / test (pull_request) Failing after 58s
2026-08-26 10:27:54 +03:00
m d350b9a565 Merge pull request 'Allow reply-only cognition for text directives' (#404) from codex/household-text-cognition-path into main
CI / test (push) Successful in 1m24s
Reviewed-on: #404
2026-08-26 07:21:07 +00:00
m f862c3ac13 Allow reply-only cognition for text directives
CI / test (push) Successful in 1m22s
CI / test (pull_request) Successful in 1m17s
2026-08-26 10:17:51 +03:00
m 5e18eb6082 Merge pull request 'Route safe text directives through cognition' (#403) from codex/household-text-directives into main
CI / test (push) Successful in 1m21s
Reviewed-on: #403
2026-08-26 07:12:00 +00:00
m ec86c983e3 Route safe text directives through cognition
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m16s
2026-08-26 10:08:42 +03:00
m ea854d5278 Merge pull request 'Allow review of backed research answers' (#402) from codex/household-research-review into main
CI / test (push) Successful in 1m17s
Reviewed-on: #402
2026-08-26 07:01:37 +00:00
m bbecadcec2 Allow review of backed research answers
CI / test (push) Successful in 1m19s
CI / test (pull_request) Successful in 1m22s
2026-08-26 09:58:20 +03:00
m 1c89b30512 Merge pull request 'Reject distance sources without measured evidence' (#401) from codex/household-distance-evidence into main
CI / test (push) Successful in 1m18s
Reviewed-on: #401
2026-08-26 06:50:59 +00:00
m 75a8533939 Reject distance sources without measured evidence
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m18s
2026-08-26 09:47:49 +03:00
m 515a98c470 Merge pull request 'Preserve distance context before search normalization' (#400) from codex/household-distance-context into main
CI / test (push) Successful in 1m32s
Reviewed-on: #400
2026-08-26 06:38:01 +00:00
m d3ad1fd5bb Preserve distance context before search normalization
CI / test (push) Successful in 1m23s
CI / test (pull_request) Successful in 1m20s
2026-08-26 09:34:43 +03:00
m e1802aef12 Merge pull request 'Answer directly without repeating the question' (#399) from codex/household-direct-answer into main
CI / test (push) Successful in 1m34s
Reviewed-on: #399
2026-08-26 06:23:34 +00:00
m 35fc2438dc Answer directly without repeating the question
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m21s
2026-08-26 09:19:28 +03:00
m 70335a78e2 Merge pull request 'Fallback to local interpretation when provider fails' (#398) from codex/household-answer-quality into main
CI / test (push) Successful in 1m33s
Reviewed-on: #398
2026-08-26 06:05:14 +00:00
m 41067a0b16 Merge branch 'main' into codex/household-answer-quality
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m18s
2026-08-26 06:00:13 +00:00
m fba05d5ed4 Fallback to local interpretation when provider fails
CI / test (push) Failing after 56s
CI / test (pull_request) Failing after 56s
2026-08-26 08:59:49 +03:00
m 6e14ba1727 Merge pull request 'Review stable knowledge replies for conversational quality' (#397) from codex/household-answer-quality into main
CI / test (push) Successful in 2m0s
Reviewed-on: #397
2026-08-26 05:53:37 +00:00
m 9c6e2097b1 Review stable knowledge replies for conversational quality
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m27s
2026-08-26 08:48:01 +03:00
m 96559f6ab8 Merge pull request 'Preserve completed household answers in web chat' (#396) from codex/household-core-reply into main
CI / test (push) Successful in 1m25s
Reviewed-on: #396
2026-08-26 04:58:04 +00:00
m 46b89577eb Preserve completed household answers in web chat
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m21s
2026-08-26 05:21:53 +03:00
m a6ddbfaaf2 Merge pull request 'Handle household typos through semantic cognition' (#395) from codex/household-typo-cognition into main
CI / test (push) Successful in 1m29s
Reviewed-on: #395
2026-08-26 02:17:46 +00:00
m 362a1bd993 Handle household typos through semantic cognition
CI / test (push) Successful in 1m48s
CI / test (pull_request) Successful in 1m15s
2026-08-26 04:59:40 +03:00
m 735222de0d Merge pull request 'Route household knowledge and travel evidence through cognition' (#394) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m34s
Reviewed-on: #394
2026-08-26 01:51:16 +00:00
m 4e3d153557 Route household knowledge through cognition
CI / test (push) Successful in 1m50s
CI / test (pull_request) Successful in 1m16s
2026-08-26 04:31:18 +03:00
m f316f7ec3f Exclude dates from concrete research identifiers
CI / test (push) Successful in 1m21s
2026-08-26 04:20:15 +03:00
m da5d06895b Require destination evidence for ticket results
CI / test (push) Successful in 1m17s
2026-08-26 04:17:12 +03:00
m 4d1a579132 Merge pull request 'Keep source names out of identifier clarification' (#393) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m23s
Reviewed-on: #393
2026-08-26 01:12:36 +00:00
m 43836af511 Keep source names out of identifier clarification
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 1m37s
2026-08-26 03:34:00 +03:00
m 32457f7142 Merge pull request 'Explain missing concrete research identifiers' (#392) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m30s
Reviewed-on: #392
2026-08-26 00:31:26 +00:00
m 204eee489c Explain missing concrete research identifiers
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 1m16s
2026-08-26 03:20:17 +03:00
m 5cd8665aba Merge pull request 'Reject generic pages for concrete research identifiers' (#391) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m30s
Reviewed-on: #391
2026-08-26 00:17:03 +00:00
m 04018cbd07 Reject generic pages for concrete research identifiers
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m18s
2026-08-26 02:11:51 +03:00
m 23a1eeae56 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m12s
Reviewed-on: #390
2026-08-25 21:25:26 +00:00
m 369588ada7 Clean search imperatives before research
CI / test (push) Successful in 1m25s
CI / test (pull_request) Failing after 55s
2026-08-26 00:24:45 +03:00
m e323689fd4 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 21:19:35 +00:00
m af042dc69f Keep internet status replies concise
CI / test (push) Successful in 1m26s
CI / test (pull_request) Failing after 53s
2026-08-26 00:19:15 +03:00
m 60c1c31bd9 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 21:14:40 +00:00
m ff3f14fc77 Request time difference measurements explicitly
CI / test (push) Failing after 59s
CI / test (pull_request) Failing after 56s
2026-08-26 00:14:20 +03:00
m 36f991d8d0 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 21:11:50 +00:00
m 54a3996c17 Keep comparison entities in one request
CI / test (push) Failing after 59s
CI / test (pull_request) Failing after 52s
2026-08-26 00:11:30 +03:00
m a769f33d16 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 21:08:39 +00:00
m 056ff1b795 Research current time differences generically
CI / test (push) Failing after 1m0s
CI / test (pull_request) Failing after 57s
2026-08-26 00:08:19 +03:00
m 7b2debb60e Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 21:02:12 +00:00
m 6dfb3fc26c Validate displayed flight research excerpts
CI / test (push) Failing after 1m0s
CI / test (pull_request) Failing after 57s
2026-08-26 00:01:51 +03:00
m e0a093eaf3 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 20:58:11 +00:00
m 24916aa8f7 Require route evidence in displayed snippets
CI / test (push) Successful in 1m38s
CI / test (pull_request) Failing after 55s
2026-08-25 23:57:50 +03:00
m d7afd5be34 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 20:47:18 +00:00
m e104ce790d Require full route evidence for nearest flights
CI / test (push) Successful in 1m29s
CI / test (pull_request) Failing after 56s
2026-08-25 23:46:52 +03:00
m d7484cf757 Merge pull request 'Allow research source recovery to complete' (#390) from codex/household-travel-query-fix into main
Reviewed-on: #390
2026-08-25 20:43:03 +00:00
m a57e8ea71b Allow research source recovery to complete
CI / test (push) Failing after 56s
CI / test (pull_request) Failing after 1m4s
2026-08-25 23:42:33 +03:00
m 9dffde63b8 Merge pull request 'Form nearest flight searches from route context' (#389) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m16s
Reviewed-on: #389
2026-08-25 20:39:42 +00:00
m 4e5bb9abc9 Form nearest flight searches from route context
CI / test (push) Successful in 1m28s
CI / test (pull_request) Failing after 57s
2026-08-25 23:39:08 +03:00
m 3435de8013 Merge pull request 'Preserve honest research outcomes' (#388) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m13s
Reviewed-on: #388
2026-08-25 20:34:09 +00:00
m 76c2824887 Preserve attempted research outcomes
CI / test (push) Successful in 1m30s
CI / test (pull_request) Failing after 56s
2026-08-25 23:33:30 +03:00
m fc00a57942 Merge pull request 'Observe chat context route decisions' (#387) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m15s
Reviewed-on: #387
2026-08-25 20:30:19 +00:00
m 1b21888e8b Observe chat context route decisions
CI / test (push) Successful in 1m27s
CI / test (pull_request) Failing after 1m1s
2026-08-25 23:29:39 +03:00
m 012f5efafe Merge pull request 'Compile personal facts for contextual assistance' (#386) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m12s
Reviewed-on: #386
2026-08-25 19:51:47 +00:00
m eb842e24ff Compile personal facts for contextual assistance
CI / test (push) Successful in 1m15s
CI / test (pull_request) Failing after 50s
2026-08-25 22:50:37 +03:00
m 13bbcdf747 Preserve specialized research routes
CI / test (push) Successful in 1m19s
2026-08-25 22:46:20 +03:00
m c00b9b1549 Merge pull request 'Route plane requests through verified research' (#385) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m15s
Reviewed-on: #385
2026-08-25 19:42:26 +00:00
m 2d88d488f4 Route plane requests through verified research
CI / test (push) Successful in 1m34s
CI / test (pull_request) Failing after 58s
2026-08-25 22:42:03 +03:00
m 72c9cfc893 Merge pull request 'Validate flight research by route meaning' (#384) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m22s
Reviewed-on: #384
2026-08-25 19:35:41 +00:00
m 0b619bdc08 Validate flight research by route meaning
CI / test (push) Successful in 1m28s
CI / test (pull_request) Failing after 58s
2026-08-25 22:35:21 +03:00
m db23e984da Merge pull request 'Forward large generated images to chat' (#383) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m14s
Reviewed-on: #383
2026-08-25 19:31:17 +00:00
m f0c8f39986 Forward large generated images to chat
CI / test (push) Successful in 1m28s
CI / test (pull_request) Failing after 53s
2026-08-25 22:30:50 +03:00
m 5d8ec1995a Merge pull request 'Honor requested image quality' (#382) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m30s
Reviewed-on: #382
2026-08-25 19:14:21 +00:00
m 75d707e960 Honor requested image quality
CI / test (push) Successful in 1m22s
CI / test (pull_request) Successful in 1m25s
2026-08-25 22:12:44 +03:00
m 89b053b7bf Merge pull request 'Require assistant target for renaming' (#381) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m16s
Reviewed-on: #381
2026-08-25 18:32:39 +00:00
m e90aded6d3 Require assistant target for renaming
CI / test (push) Successful in 1m25s
CI / test (pull_request) Failing after 54s
2026-08-25 21:32:20 +03:00
m 8456ecd237 Merge pull request 'Continue ticket search with date follow-up' (#380) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m13s
Reviewed-on: #380
2026-08-25 18:21:30 +00:00
m b89fdef1d9 Merge remote-tracking branch 'origin/main' into codex/household-travel-query-fix
CI / test (push) Failing after 57s
CI / test (pull_request) Failing after 55s
2026-08-25 21:21:12 +03:00
m 6f3d576602 Continue ticket search with date follow-up
CI / test (push) Failing after 50s
CI / test (pull_request) Failing after 1m9s
2026-08-25 21:20:36 +03:00
m b026ec151e Merge pull request 'Preserve explicit memory confirmations' (#379) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m25s
Reviewed-on: #379
2026-08-25 18:16:05 +00:00
m 5d4c3877b1 Preserve explicit memory confirmations
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m26s
2026-08-25 21:13:26 +03:00
m 113c5ab965 Merge pull request 'Apply learned source aliases to research' (#378) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m14s
Reviewed-on: #378
2026-08-25 18:02:29 +00:00
m bce7993a6a Apply learned source aliases to research
CI / test (push) Successful in 1m30s
CI / test (pull_request) Failing after 52s
2026-08-25 21:02:07 +03:00
m 3bbf416c20 Merge pull request 'Explain missing research evidence' (#377) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m14s
Reviewed-on: #377
2026-08-25 17:56:42 +00:00
m b62b3b0dc3 Explain missing research evidence
CI / test (push) Failing after 59s
CI / test (pull_request) Failing after 1m2s
2026-08-25 20:56:21 +03:00
m 7e72474b0a Merge pull request 'Summarize verified internet connectivity' (#376) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m33s
Reviewed-on: #376
2026-08-25 17:53:22 +00:00
m b05081328e Recognize network connection wording
CI / test (push) Failing after 1m1s
CI / test (pull_request) Failing after 50s
2026-08-25 20:53:00 +03:00
m 0e7d1a7a50 Summarize verified internet connectivity
CI / test (push) Failing after 58s
2026-08-25 20:52:41 +03:00
m 2aaa73bb2a Merge pull request 'Describe local Domovoy capabilities' (#375) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m13s
Reviewed-on: #375
2026-08-25 17:50:03 +00:00
m 9000d90d72 Describe local Domovoy capabilities
CI / test (push) Successful in 1m29s
CI / test (pull_request) Failing after 1m0s
2026-08-25 20:49:39 +03:00
m 4afcfd3846 Merge pull request 'Recognize self capability questions' (#374) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m19s
Reviewed-on: #374
2026-08-25 16:54:32 +00:00
m 6ba8265686 Recognize self capability questions
CI / test (push) Failing after 58s
CI / test (pull_request) Failing after 54s
2026-08-25 19:54:06 +03:00
m 21583528c0 Merge pull request 'Explain current cognitive response route' (#373) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m18s
Reviewed-on: #373
2026-08-25 16:51:27 +00:00
m b1bb48f5b5 Explain current cognitive response route
CI / test (push) Successful in 1m40s
CI / test (pull_request) Failing after 54s
2026-08-25 19:51:03 +03:00
m 88f5f6e8f8 Merge pull request 'Recognize named model inventory requests' (#372) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m16s
Reviewed-on: #372
2026-08-25 16:48:12 +00:00
m 5439db2cc3 Recognize named model inventory requests
CI / test (push) Successful in 1m14s
CI / test (pull_request) Failing after 54s
2026-08-25 19:47:25 +03:00
m 208cb18ee0 Merge pull request 'Preserve latest travel context' (#371) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m32s
Reviewed-on: #371
2026-08-25 16:43:43 +00:00
m 5d19becdaf Preserve latest travel context
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m14s
2026-08-25 19:36:49 +03:00
m 4cd3ecdf15 Merge pull request 'Normalize contextual travel research' (#370) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m13s
Reviewed-on: #370
2026-08-25 16:29:58 +00:00
m 8e26f529f7 Merge remote-tracking branch 'origin/codex/household-travel-query-fix' into codex/household-travel-query-fix
CI / test (push) Failing after 1m3s
CI / test (pull_request) Failing after 54s
2026-08-25 19:28:11 +03:00
m 3cc636d6b7 Normalize contextual travel research 2026-08-25 19:27:59 +03:00
m 14dc439c82 Merge pull request 'Improve semantic travel research queries' (#369) from codex/household-travel-query-fix into main
CI / test (push) Successful in 1m14s
Reviewed-on: #369
2026-08-25 16:26:24 +00:00
m 21887988b2 Merge branch 'main' into codex/household-travel-query-fix
CI / test (push) Failing after 57s
CI / test (pull_request) Failing after 54s
2026-08-25 16:25:54 +00:00
m 232a57eed4 Improve semantic travel research queries
CI / test (push) Failing after 53s
CI / test (pull_request) Failing after 54s
2026-08-25 19:25:00 +03:00
m d1fd70e436 Merge pull request 'Preserve location context for travel follow-ups' (#368) from codex/household-travel-context-fix into main
CI / test (push) Successful in 1m30s
Reviewed-on: #368
2026-08-25 16:22:04 +00:00
m 1d73162df1 Merge remote-tracking branch 'origin/main' into codex/household-travel-context-fix
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m24s
2026-08-25 19:17:59 +03:00
m 7788cf4b44 Preserve location context for travel follow-ups
CI / test (push) Failing after 58s
CI / test (pull_request) Failing after 50s
2026-08-25 19:17:34 +03:00
m d9dcddbdbb Merge pull request 'Preserve direct time difference replies' (#367) from codex/household-time-reply-priority into main
CI / test (push) Successful in 2m3s
Reviewed-on: #367
2026-08-25 16:11:36 +00:00
m 5fb616310d Merge remote-tracking branch 'origin/main' into codex/household-time-reply-priority
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m24s
2026-08-25 19:08:15 +03:00
m 5a9cf7a22d Preserve direct time difference replies
CI / test (push) Failing after 51s
CI / test (pull_request) Failing after 48s
2026-08-25 19:06:51 +03:00
m d649f79ca4 Merge pull request 'Preserve remembered preference identity' (#366) from codex/household-checklist-live-preference-fix into main
CI / test (push) Successful in 1m58s
Reviewed-on: #366
2026-08-25 16:00:08 +00:00
m ffce659332 Merge remote-tracking branch 'origin/main' into codex/household-checklist-live-preference-fix
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m24s
2026-08-25 18:57:06 +03:00
m 24b30bf669 Preserve remembered preference identity
CI / test (push) Failing after 51s
CI / test (pull_request) Failing after 50s
2026-08-25 18:55:33 +03:00
m 1bd9137761 Merge pull request 'Add executable 50-point household checklist' (#365) from codex/household-media-queue-isolation into main
CI / test (push) Successful in 1m39s
Reviewed-on: #365
2026-08-25 15:49:39 +00:00
m f895c5a104 Merge remote-tracking branch 'origin/main' into codex/household-media-queue-isolation
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m14s
2026-08-25 18:46:10 +03:00
m d0074709aa Add executable 50-point household checklist
CI / test (push) Failing after 49s
CI / test (pull_request) Failing after 50s
2026-08-25 18:44:49 +03:00
m 982eb6f49b Merge pull request 'Do not block image generation on general cognition' (#364) from codex/household-media-queue-isolation into main
CI / test (push) Successful in 1m24s
Reviewed-on: #364
2026-08-25 15:28:35 +00:00
m b1ee085de4 Merge remote-tracking branch 'origin/main' into codex/household-media-queue-isolation
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m15s
2026-08-25 18:25:37 +03:00
m 2c66a44910 Do not block media routes on general cognition
CI / test (push) Failing after 1m6s
CI / test (pull_request) Failing after 57s
2026-08-25 18:22:38 +03:00
m b95714a71e Merge pull request 'Isolate media cancellation and improve travel answers' (#363) from codex/household-media-queue-isolation into main
CI / test (push) Successful in 1m19s
Reviewed-on: #363
2026-08-25 15:18:22 +00:00
m 6183d811f1 Merge remote-tracking branch 'origin/main' into codex/household-media-queue-isolation
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m10s
2026-08-25 18:14:52 +03:00
m 6a2ed287ba Keep flight formatting within task scope
CI / test (push) Failing after 51s
CI / test (pull_request) Failing after 49s
2026-08-25 18:12:39 +03:00
m d342728b46 Improve nearest flight research answers
CI / test (push) Failing after 51s
2026-08-25 18:12:13 +03:00
m f121445f24 Preserve media failures and enrich nearest flight searches
CI / test (push) Failing after 52s
2026-08-25 18:08:48 +03:00
m 124871da76 Cancel only the owned ComfyUI prompt
CI / test (push) Failing after 54s
2026-08-25 18:02:59 +03:00
m 2062094228 Merge pull request 'Route creative image requests to media generation' (#362) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m42s
Reviewed-on: #362
2026-08-25 14:42:39 +00:00
m 515c9713e9 Merge remote-tracking branch 'origin/main' into codex/household-scenario-audit
CI / test (push) Successful in 1m53s
CI / test (pull_request) Successful in 1m32s
2026-08-25 17:36:53 +03:00
m 14c5d9e412 Route creative image requests to media generation
CI / test (push) Failing after 1m8s
CI / test (pull_request) Failing after 1m1s
2026-08-25 17:34:05 +03:00
m 8e9429e5d0 Merge pull request 'Bind ComfyUI image provider in production' (#361) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m25s
Reviewed-on: #361
2026-08-25 14:24:00 +00:00
m 6cb125001f Bind ComfyUI image provider in production
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m11s
2026-08-25 17:14:44 +03:00
m 1e7cf68317 Merge pull request 'Add ComfyUI image generation adapter' (#360) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m15s
Reviewed-on: #360
2026-08-25 14:08:23 +00:00
m 2f6f6fb026 Add ComfyUI image generation adapter
CI / test (push) Successful in 1m16s
CI / test (pull_request) Failing after 57s
2026-08-25 17:01:55 +03:00
m 4618a392e2 Merge pull request 'Add replaceable image and video media routes' (#359) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m19s
Reviewed-on: #359
2026-08-25 13:37:28 +00:00
m cdcbd50fe7 Test degraded media capability routing
CI / test (push) Successful in 1m15s
CI / test (pull_request) Failing after 54s
2026-08-25 16:14:28 +03:00
m 8260e69dfe Add replaceable image and video media routes
CI / test (push) Successful in 1m12s
2026-08-25 16:12:33 +03:00
m cd8ba740de Merge pull request 'Add replaceable image and video capability contracts' (#358) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m45s
Merge PR #358
2026-08-25 13:05:36 +00:00
m 8173b8f9d6 Define replaceable image and video capabilities
CI / test (push) Successful in 1m32s
CI / test (pull_request) Successful in 1m31s
2026-08-25 16:01:22 +03:00
m 7d5b91fbe9 Merge pull request 'Route creative requests through cognition' (#357) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m27s
Merge PR #357
2026-08-25 12:47:09 +00:00
m f9e50eba70 Remove canned creative fallback content
CI / test (push) Successful in 1m27s
CI / test (pull_request) Successful in 1m27s
2026-08-25 15:43:23 +03:00
m 29e17f1ac8 Remove canned creative object replies
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m24s
2026-08-25 15:40:29 +03:00
m ae8bf68d73 Merge pull request 'Add local horse creative fallback' (#356) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m32s
Merge PR #356
2026-08-25 12:33:13 +00:00
m af146c507e Add local horse drawing fallback
CI / test (push) Successful in 1m30s
CI / test (pull_request) Successful in 1m25s
2026-08-25 15:29:25 +03:00
m 7ad7a2c8ba Merge pull request 'Add household route boundary matrix' (#355) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m34s
Merge PR #355
2026-08-25 12:27:16 +00:00
m 8885a66883 Add household route boundary matrix
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m18s
2026-08-25 15:24:05 +03:00
m 2b40471e08 Merge pull request 'Add local bird creative fallback' (#354) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m20s
Merge PR #354
2026-08-25 12:22:05 +00:00
m 22253caf2d Add local bird drawing fallback
CI / test (push) Successful in 1m13s
CI / test (pull_request) Successful in 1m12s
2026-08-25 15:19:03 +03:00
m f8387180eb Merge pull request 'Isolate non-travel household requests' (#353) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m34s
Merge PR #353
2026-08-25 12:16:45 +00:00
m 288643f0da Cover cross-context household requests
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m26s
2026-08-25 15:13:03 +03:00
m 0662dbbb01 Merge pull request 'Expand semantic decomposition checks' (#352) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m32s
Merge PR #352
2026-08-25 12:08:40 +00:00
m 1a8934f88b Keep creative requests out of travel continuation
CI / test (push) Successful in 1m25s
CI / test (pull_request) Successful in 1m22s
2026-08-25 15:04:39 +03:00
m 4cac361762 Use matched alias for task target
CI / test (push) Successful in 1m28s
CI / test (pull_request) Successful in 1m24s
2026-08-25 15:01:18 +03:00
m 515cabb5cf Expand semantic memory decomposition checks 2026-08-25 15:01:17 +03:00
m 5efa6176bb Merge pull request 'Route marketplace search and bear drawing' (#351) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m36s
Merge PR #351
2026-08-25 11:55:30 +00:00
m 3515a483ca Fix generic creative target extraction
CI / test (push) Successful in 1m29s
CI / test (pull_request) Successful in 1m28s
2026-08-25 14:49:01 +03:00
m ca45df3c62 Load task aliases from semantic memory
CI / test (push) Failing after 1m0s
CI / test (pull_request) Failing after 1m2s
2026-08-25 14:48:32 +03:00
m c94a96c47e Decompose household tasks into typed atoms
CI / test (push) Successful in 1m28s
CI / test (pull_request) Successful in 1m27s
2026-08-25 14:46:43 +03:00
m 745d8b0c4d Route marketplace search and bear drawing 2026-08-25 14:46:43 +03:00
m ddc4d3bc90 Merge pull request 'Reject identity boilerplate over creative fallback' (#350) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m44s
Merge PR #350
2026-08-25 11:06:26 +00:00
m 906d71b35f Reject identity boilerplate over creative fallback
CI / test (push) Successful in 1m31s
CI / test (pull_request) Successful in 1m31s
2026-08-25 14:02:42 +03:00
m beddb78469 Merge pull request 'Preserve creative fallback over conversational reflex' (#349) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m34s
Merge PR #349
2026-08-25 10:57:57 +00:00
m fb968502ba Preserve creative fallback over conversational reflex
CI / test (push) Successful in 1m32s
CI / test (pull_request) Successful in 1m28s
2026-08-25 13:54:14 +03:00
m 643ce34cff Merge pull request 'Keep useful creative fallback replies' (#348) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m37s
Merge PR #348
2026-08-25 10:48:58 +00:00
m cc08fb3d08 Keep useful creative fallback replies
CI / test (push) Successful in 1m35s
CI / test (pull_request) Successful in 1m32s
2026-08-25 13:45:01 +03:00
m f3bab3177d Merge pull request 'Humanize web research replies' (#347) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m36s
Merge PR #347
2026-08-25 10:38:47 +00:00
m e79302c915 Humanize web research replies
CI / test (push) Successful in 1m27s
CI / test (pull_request) Successful in 1m26s
2026-08-25 13:34:48 +03:00
m d2f55bf0be Merge pull request 'Preserve assistant name in cognitive context' (#346) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m19s
Merge PR #346
2026-08-25 09:53:12 +00:00
m c0208de111 Preserve assistant name in cognitive context
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m13s
2026-08-25 12:50:23 +03:00
m 56cd297ca3 Merge pull request 'Handle reasoning-only vision responses' (#345) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m14s
Merge PR #345
2026-08-25 09:37:23 +00:00
m 5594525893 Handle reasoning-only vision responses
CI / test (push) Successful in 1m8s
CI / test (pull_request) Failing after 52s
2026-08-25 12:36:57 +03:00
m f91613b5d2 Merge pull request 'Add local speech, vision and OCR chat skills' (#344) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m15s
2026-08-25 09:29:25 +00:00
m 78e7e3fedc Add local speech and vision chat skills
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 1m7s
2026-08-25 12:26:41 +03:00
m df017a349a Merge pull request 'Improve degraded household replies and route tests' (#343) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m13s
2026-08-25 09:11:58 +00:00
m 8124fcbcc3 Improve degraded household replies
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m4s
2026-08-25 12:08:52 +03:00
m 8658db537d Merge pull request 'Add household scenario audit plan' (#342) from codex/household-scenario-audit into main
CI / test (push) Successful in 1m34s
2026-08-25 09:04:33 +00:00
m bd382243a0 Add household scenario audit plan
CI / test (push) Successful in 1m44s
CI / test (pull_request) Successful in 1m27s
2026-08-25 12:00:25 +03:00
m d0057f1873 Merge pull request 'Use local creative fallback when cognition is unavailable' (#341) from codex/creative-chat-routing into main
CI / test (push) Successful in 1m31s
Use local creative fallback when cognition is unavailable
2026-08-25 08:49:57 +00:00
m 342a93caa4 Add local creative fallback
CI / test (push) Successful in 1m23s
CI / test (pull_request) Successful in 1m40s
2026-08-25 11:46:03 +03:00
m 0904a4ade7 Merge pull request 'Protect deterministic creative replies' (#340) from codex/creative-chat-routing into main
CI / test (push) Successful in 1m38s
2026-08-25 08:41:03 +00:00
m bbd525eeb5 Protect creative reply from intermediate rewrites
CI / test (push) Successful in 1m33s
CI / test (pull_request) Successful in 1m24s
2026-08-25 11:37:11 +03:00
m 352703a91f Merge pull request 'Route creative chat by meaning' (#339) from codex/creative-chat-routing into main
CI / test (push) Successful in 1m20s
2026-08-25 08:32:57 +00:00
m 5a439f7ba8 Route creative chat by meaning
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m9s
2026-08-25 11:29:54 +03:00
m f7200b0704 Merge pull request 'Authorize bounded site-browser observation' (#338) from codex/site-browser-architecture into main
CI / test (push) Successful in 1m20s
2026-08-25 08:17:45 +00:00
m 14bf8261da Authorize bounded site browser observation
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m9s
2026-08-25 11:15:07 +03:00
m feb87d5c4e Merge pull request 'Improve price research queries' (#337) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m17s
2026-08-25 08:04:26 +00:00
m 24320f22a6 Improve price research queries
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m13s
2026-08-25 11:01:22 +03:00
m 98cf218cc3 Merge pull request 'Shorten research source labels' (#336) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m16s
2026-08-25 07:58:42 +00:00
m 7abe73e7bd Shorten research source labels
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m8s
2026-08-25 10:55:07 +03:00
m a05ddacff3 Merge pull request 'Clarify research results without price evidence' (#335) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m17s
2026-08-25 07:52:31 +00:00
m 20b18de60a Clarify missing price evidence
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m8s
2026-08-25 10:49:34 +03:00
m d5c98dd62f Merge pull request 'Preserve durable assistant identity and deterministic replies' (#334) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m15s
2026-08-25 07:42:01 +00:00
m 61d08fa05a Preserve durable conversation identity
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 1m7s
2026-08-25 10:39:05 +03:00
m 1ea2e713d8 Format interaction contract 2026-08-25 10:39:04 +03:00
m 38dce57666 Improve location time and product research 2026-08-25 10:39:04 +03:00
m 8f255b8bde Merge pull request 'Handle memory identity and travel follow-ups' (#333) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m34s
2026-08-25 07:24:45 +00:00
m 6b97d6668b Handle memory identity and travel follow-ups
CI / test (push) Successful in 1m18s
CI / test (pull_request) Successful in 1m35s
2026-08-25 10:20:54 +03:00
m 3fa1d70379 Route completed travel continuations to research 2026-08-25 10:20:53 +03:00
m 2dd05a7aa6 Merge pull request 'Continue travel tasks from departure location' (#332) from codex/chat-semantic-inventory into main
CI / test (push) Failing after 42s
2026-08-25 07:10:22 +00:00
m 02227edc23 Continue travel tasks from departure location
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m9s
2026-08-25 10:07:28 +03:00
m a2297a84ad Merge pull request 'Reject reviewed research answers without evidence' (#331) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m16s
2026-08-25 04:22:34 +00:00
m 05cbd8adc7 Reject reviewed research answers without evidence
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m12s
2026-08-25 07:19:43 +03:00
m 984731b111 Merge pull request 'Ask departure city for general travel requests' (#330) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m20s
2026-08-25 04:13:55 +00:00
m 0da03c18d1 Ask departure city for general travel requests
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m5s
2026-08-25 07:11:09 +03:00
m 302b8fc8cd Merge pull request 'Prefer current travel context in follow-up answers' (#329) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m12s
2026-08-25 04:05:00 +00:00
m 59239635cd Prefer current travel context in follow-up answers
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 1m2s
2026-08-25 07:02:14 +03:00
m 238105a7da Merge pull request 'Polish web research answers and travel context' (#328) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m13s
2026-08-25 03:56:31 +00:00
m d7ca5f7e68 Polish web research answers and travel context
CI / test (push) Successful in 1m11s
CI / test (pull_request) Successful in 1m5s
2026-08-25 06:53:46 +03:00
m 827135a02d Merge pull request 'Keep identity reply consistent across response fields' (#327) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m15s
2026-08-25 03:44:38 +00:00
m a3ddf53207 Keep identity reply consistent across response fields
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m8s
2026-08-25 06:41:57 +03:00
m 35ac9015ad Merge pull request 'Remove temporary continuation diagnostics' (#326) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m16s
2026-08-25 03:34:41 +00:00
m 1c196ef675 Remove continuation diagnostics
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m4s
2026-08-25 06:31:30 +03:00
m 2ce3b5a9b3 Merge pull request 'Fix semantic routing for location follow-ups' (#325) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m13s
2026-08-25 03:26:06 +00:00
m 7421fd058d Fix semantic routing for location follow-ups
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m6s
2026-08-25 06:23:38 +03:00
m 368e9df551 Merge pull request 'Trace conversational continuation selection' (#324) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 03:17:08 +00:00
m e9629046cb Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m6s
2026-08-25 06:15:13 +03:00
m b5ccc419f6 Trace conversational continuation selection
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m4s
2026-08-25 06:13:19 +03:00
m 615723da2a Merge pull request 'Select latest relevant local search goal' (#323) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m11s
2026-08-25 03:10:44 +00:00
m d1bcd5beff Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m15s
2026-08-25 06:09:02 +03:00
m 519dc583a0 Select latest relevant local search goal
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m6s
2026-08-25 06:07:08 +03:00
m 0bf883cf56 Merge pull request 'Continue location follow-ups from recent context' (#322) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 03:04:26 +00:00
m c8502c735c Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m15s
2026-08-25 06:02:39 +03:00
m 48af97fbdd Continue location follow-ups from recent context
CI / test (push) Successful in 1m16s
CI / test (pull_request) Successful in 1m6s
2026-08-25 06:00:45 +03:00
m 13bcf87ea5 Merge pull request 'Continue local search from conversation context' (#321) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m12s
2026-08-25 02:57:39 +00:00
m e271fbb1e1 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m3s
2026-08-25 05:55:23 +03:00
m 5678686c57 Continue local search from conversation context
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m7s
2026-08-25 05:53:37 +03:00
m 9fc3fd1aa5 Merge pull request 'Ask for location before proximity search' (#320) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 02:49:13 +00:00
m e3a9c18cb3 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m13s
2026-08-25 05:47:27 +03:00
m 9534b041db Ask for location before proximity search
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m2s
2026-08-25 05:45:40 +03:00
m 9426fa40a9 Merge pull request 'Route independent chat actions separately' (#319) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m12s
2026-08-25 02:43:11 +00:00
m 51e2b23082 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m10s
2026-08-25 05:41:29 +03:00
m aece914fb8 Route independent chat actions separately
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m1s
2026-08-25 05:39:31 +03:00
m 762e312730 Merge pull request 'Keep research replies concise' (#318) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m10s
2026-08-25 02:36:16 +00:00
m e9eb529d6e Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m10s
2026-08-25 05:34:33 +03:00
m e39a2b7dda Keep research replies concise
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 59s
2026-08-25 05:32:42 +03:00
m cdc6baa4c0 Merge pull request 'Align semantic reply with research result' (#317) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m10s
2026-08-25 02:29:42 +00:00
m 255ab8dbf3 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m8s
2026-08-25 05:27:59 +03:00
m a76dc42bd8 Align semantic reply with research result
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m4s
2026-08-25 05:26:06 +03:00
m f0cbd71129 Merge pull request 'Preserve travel follow-up clarification' (#316) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m12s
2026-08-25 02:23:02 +00:00
m aa10c3e093 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m13s
2026-08-25 05:21:21 +03:00
m 0d836121d0 Preserve travel follow-up clarification
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m0s
2026-08-25 05:19:27 +03:00
m 6e8ecbf5e5 Merge pull request 'Avoid inheriting stale travel goals' (#315) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m13s
2026-08-25 02:15:15 +00:00
m 8088ff197e Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m12s
2026-08-25 05:13:32 +03:00
m 55ee967f2f Avoid inheriting stale travel goals
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m1s
2026-08-25 05:11:44 +03:00
m a56bd614bd Merge pull request 'Keep semantic clarification as final reply' (#314) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m7s
2026-08-25 02:09:13 +00:00
m 992e8470b2 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m10s
2026-08-25 05:07:42 +03:00
m fe2bd8493f Keep semantic clarification as final reply
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m3s
2026-08-25 05:05:46 +03:00
m e43820c5ce Merge pull request 'Preserve travel clarification reply' (#313) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 02:02:50 +00:00
m 662e30cb93 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m11s
CI / test (pull_request) Successful in 1m4s
2026-08-25 05:00:46 +03:00
m a51675e58d Preserve travel clarification reply
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m0s
2026-08-25 04:58:53 +03:00
m 820b1dc643 Merge pull request 'Keep travel requests in semantic dialogue' (#312) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 01:55:33 +00:00
m 94f2ae81c3 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m1s
2026-08-25 04:53:28 +03:00
m 9020100313 Keep travel requests in semantic dialogue
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m1s
2026-08-25 04:51:52 +03:00
m d097940692 Merge pull request 'Stop repeat cognition for stable replies' (#311) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 01:44:20 +00:00
m 28dfcc8aec Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m5s
2026-08-25 04:41:44 +03:00
m 7dd0c8e7a4 Stop repeat cognition for stable replies
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m4s
2026-08-25 04:38:46 +03:00
m bed21ad5c0 Merge pull request 'Trace final chat response selection' (#310) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 01:36:18 +00:00
m 2622140529 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 59s
2026-08-25 04:33:36 +03:00
m 3f9dc7a270 Trace final chat response selection
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m1s
2026-08-25 04:30:54 +03:00
m b5633c7fab Merge pull request 'Restore stable replies after cognitive routing' (#309) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 01:28:07 +00:00
m 5d053fedf4 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m1s
2026-08-25 04:25:36 +03:00
m d924694602 Restore stable replies after cognitive routing
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m2s
2026-08-25 04:23:00 +03:00
m cf8a48ae0d Merge pull request 'Preserve deterministic conversational drafts' (#308) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 01:19:57 +00:00
m 24e865aa07 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 1m0s
2026-08-25 04:17:28 +03:00
m 5d0dbbbe92 Preserve deterministic conversational drafts
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m2s
2026-08-25 04:14:57 +03:00
m f2277e35b4 Merge pull request 'Keep conversational replies semantically grounded' (#307) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 01:11:49 +00:00
m e8a642c5e2 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 58s
2026-08-25 04:09:21 +03:00
m 399bdea220 Keep conversational replies semantically grounded
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m5s
2026-08-25 04:06:45 +03:00
m 9a0205758b Merge pull request 'Preserve semantic identity during reply review' (#306) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 01:02:14 +00:00
m 0507e1af9c Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m11s
2026-08-25 03:59:40 +03:00
m f893744ed1 Preserve semantic identity during reply review
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m4s
2026-08-25 03:57:07 +03:00
m e455e89777 Merge pull request 'Review chat replies through cognitive gateway' (#305) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m12s
2026-08-25 00:54:05 +00:00
m 9b60da96b0 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m2s
2026-08-25 03:51:39 +03:00
m 34c0e7a0f4 Review chat replies through cognitive gateway
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:49:12 +03:00
m 550ce99bcd Merge pull request 'Prioritize personalized identity reply' (#304) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m11s
2026-08-25 00:40:40 +00:00
m e81411c4a4 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m4s
2026-08-25 03:37:21 +03:00
m 688a7e49db Prioritize personalized identity reply
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 59s
2026-08-25 03:34:16 +03:00
m 2ce997ca73 Merge pull request 'Protect personalized identity from audit drift' (#303) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-25 00:32:03 +00:00
m 075967c395 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 59s
2026-08-25 03:29:23 +03:00
m 1efd8bd25d Protect personalized identity from audit drift
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:26:38 +03:00
m e2bfd83481 Merge pull request 'Trace resolved assistant identity context' (#302) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m11s
2026-08-25 00:24:11 +00:00
m 2d5f0c69d0 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:21:39 +03:00
m 339159a194 Trace resolved assistant identity context
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:18:58 +03:00
m ad905e0bc5 Merge pull request 'Trace durable interaction history volume' (#301) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m10s
2026-08-25 00:16:53 +00:00
m 721817515a Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m2s
2026-08-25 03:14:28 +03:00
m 73de357c9f Trace durable interaction history volume
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:11:51 +03:00
m b1e635d00b Merge pull request 'Log interaction memory read failures' (#300) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m8s
2026-08-25 00:09:49 +00:00
m e489b5511a Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m2s
2026-08-25 03:06:44 +03:00
m 79409fb5c8 Log interaction memory read failures
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m0s
2026-08-25 03:04:10 +03:00
m e9bfe12293 Merge pull request 'Scope durable assistant memory to principal' (#299) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m25s
2026-08-25 00:01:17 +00:00
m 0833a7a0ca Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m4s
2026-08-25 02:58:47 +03:00
m dfecab7c34 Scope durable assistant memory to principal
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m1s
2026-08-25 02:56:00 +03:00
m 2eb57675c8 Merge pull request 'Always load durable user conversation context' (#298) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m9s
2026-08-24 23:53:42 +00:00
m 98f1454b3c Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 57s
2026-08-25 02:51:08 +03:00
m d274768bcc Always load durable user conversation context
CI / test (push) Successful in 1m0s
CI / test (pull_request) Failing after 15s
2026-08-25 02:48:36 +03:00
m ba31326302 Merge pull request 'Preserve personalized identity replies' (#297) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m11s
2026-08-24 23:46:12 +00:00
m f4e674fa9b Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 58s
2026-08-25 02:43:48 +03:00
m 1c7447b7e3 Preserve personalized identity replies
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 56s
2026-08-25 02:41:08 +03:00
m 2d24c011aa Merge pull request 'Reuse assistant name across conversations' (#296) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m6s
2026-08-24 23:38:47 +00:00
m b9b082b802 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m4s
2026-08-25 02:35:51 +03:00
m 74dd9922a5 Reuse assistant name across conversations
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m0s
2026-08-25 02:33:15 +03:00
m 4298b63d91 Merge pull request 'Keep explicit preferences on the local route' (#295) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m14s
2026-08-24 23:30:42 +00:00
m c48fba7d8d Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m6s
2026-08-25 02:27:48 +03:00
m 7b71216a05 Keep explicit preferences on the local route
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 59s
2026-08-25 02:25:06 +03:00
m 35204177e9 Merge pull request 'Remember assistant name preferences in chat' (#294) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m5s
2026-08-24 23:21:17 +00:00
m fc39278c91 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 58s
2026-08-25 02:18:43 +03:00
m eefd54137c Remember assistant name preferences in chat
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 59s
2026-08-25 02:16:15 +03:00
m 880cd055dd Merge pull request 'Route travel searches to current sources' (#293) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m3s
2026-08-24 23:09:32 +00:00
m ce07163541 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 59s
2026-08-25 02:07:14 +03:00
m 6bd9acac6e Route travel searches to current sources
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m5s
2026-08-25 02:04:48 +03:00
m 1be14da122 Merge pull request 'Allow cold local cognition to finish' (#292) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m4s
2026-08-24 23:01:20 +00:00
m 96a61a46d0 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m0s
2026-08-25 01:58:56 +03:00
m 4f4e43c953 Allow cold local cognition to finish
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 57s
2026-08-25 01:56:30 +03:00
m 306a35e294 Merge pull request 'Keep ordinary why questions conversational' (#291) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m7s
2026-08-24 22:42:13 +00:00
m 53a0bdb2e3 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m0s
2026-08-25 01:39:48 +03:00
m bf73dcd1ea Keep ordinary why questions conversational
CI / test (push) Successful in 56s
CI / test (pull_request) Successful in 1m3s
2026-08-25 01:37:23 +03:00
m 6409a9d442 Merge pull request 'Recognize identity from the original question' (#290) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m7s
2026-08-24 22:33:41 +00:00
m 96f96bd130 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 56s
2026-08-25 01:31:25 +03:00
m a8ce97c807 Recognize identity from the original question
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 58s
2026-08-25 01:29:03 +03:00
m 0804a69b4f Merge pull request 'Keep identity replies meaningful in fallback' (#289) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m7s
2026-08-24 22:27:03 +00:00
m e816b70f46 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 59s
2026-08-25 01:24:35 +03:00
m 8adcd86cad Keep identity replies meaningful in fallback
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 58s
2026-08-25 01:22:05 +03:00
m 9d2db45dd5 Merge pull request 'Trim research replies to essential sources' (#288) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m1s
2026-08-24 22:15:25 +00:00
m 111276fb22 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 59s
2026-08-25 01:13:05 +03:00
m cad4623232 Trim research replies to essential sources
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 58s
2026-08-25 01:10:39 +03:00
m ececb58025 Merge pull request 'Preserve meaning when cognitive model is unavailable' (#287) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 59s
2026-08-24 22:02:27 +00:00
m 4a9ba3b7d4 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 59s
2026-08-25 01:00:14 +03:00
m b029adfb43 Keep conversational fallback on the requested meaning
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m3s
2026-08-25 00:57:48 +03:00
m d0651474ab Merge pull request 'Simplify cognitive channel status replies' (#286) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m1s
2026-08-24 21:46:35 +00:00
m ca9db836de Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m2s
2026-08-25 00:44:06 +03:00
m 44ea97b938 Simplify cognitive channel status replies
CI / test (push) Successful in 1m4s
CI / test (pull_request) Successful in 1m1s
2026-08-25 00:41:31 +03:00
m 84f5ab6c47 Merge pull request 'Use natural capability inventory wording' (#285) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m6s
2026-08-24 21:28:10 +00:00
m 153372c2de Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 56s
CI / test (pull_request) Successful in 1m4s
2026-08-25 00:25:46 +03:00
m 7384b8922b Use natural capability inventory wording
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m1s
2026-08-25 00:23:00 +03:00
m 295523b470 Merge pull request 'Simplify model inventory responses' (#284) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m0s
2026-08-24 21:07:26 +00:00
m 0991a24739 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m0s
2026-08-25 00:05:03 +03:00
m 420c52b10e Simplify model inventory responses
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 57s
2026-08-25 00:02:04 +03:00
m 1e3ed210d7 Merge pull request 'Remove response length meta-text from Domovoy replies' (#283) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m0s
2026-08-24 20:45:21 +00:00
m 947a75b771 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m1s
CI / test (pull_request) Successful in 1m5s
2026-08-24 23:42:02 +03:00
m c84ddad051 Remove response length meta-text from Domovoy replies
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 57s
2026-08-24 23:39:11 +03:00
m 71b63ac6f9 Merge pull request 'Skip retry messages when resuming chat task' (#282) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 55s
2026-08-24 20:21:01 +00:00
m 659eb4ecfc Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 57s
2026-08-24 23:17:53 +03:00
m 2ff2c7db09 Skip retry messages when resuming chat task
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 1m2s
2026-08-24 23:15:04 +03:00
m 475e216ee0 Merge pull request 'Carry forward previous chat tasks on retry' (#281) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 58s
2026-08-24 19:56:43 +00:00
m 226056fb46 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 56s
CI / test (pull_request) Successful in 56s
2026-08-24 22:53:56 +03:00
m 580d18a41c Carry forward previous chat tasks on retry
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 59s
2026-08-24 22:51:21 +03:00
m 0abd6c2a11 Merge pull request 'Keep chat concise and route clarification research' (#280) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m3s
2026-08-24 19:28:03 +00:00
m def43ecfc3 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 53s
2026-08-24 22:25:42 +03:00
m c4bfcf032a fix: keep chat concise and route clarification research
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 56s
2026-08-24 22:23:18 +03:00
m 4a3a7d4812 Merge pull request 'Keep Domovoy identity in builtin fallback' (#279) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m4s
2026-08-24 18:59:40 +00:00
m dbe3e023ec Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 56s
2026-08-24 21:57:19 +03:00
m c63110ae7d fix: keep Domovoy identity in builtin fallback
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 57s
2026-08-24 21:54:50 +03:00
m 101f8beadb Merge pull request 'Strengthen Domovoy identity and web retrieval' (#278) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 56s
2026-08-24 18:48:27 +00:00
m b16925b386 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 54s
2026-08-24 21:46:07 +03:00
m be5a146cca fix: strengthen local assistant identity and web retrieval
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 58s
2026-08-24 21:43:43 +03:00
m 094fdcc51e Merge pull request 'Complete conversational research fallback' (#277) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 54s
2026-08-24 18:36:24 +00:00
m 307ad4a587 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 52s
2026-08-24 21:34:13 +03:00
m e867ba3c93 fix: complete conversational research fallback
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 59s
2026-08-24 21:31:34 +03:00
m c3027c5901 Merge pull request 'Fix production creator chat timeout' (#276) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 56s
Merge PR #276: fix production creator chat timeout
2026-08-24 18:05:38 +00:00
m 30bf2d6e67 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 54s
2026-08-24 21:03:28 +03:00
m 7fcab3bf31 fix: allow creator chat responses to complete
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 57s
2026-08-24 21:01:05 +03:00
m eca8475e29 Merge pull request 'Semantic chat orchestration with model profiles' (#275) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 55s
Merge PR #275: semantic chat route orchestration
2026-08-24 17:28:02 +00:00
m b340d46cdd Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 55s
2026-08-24 20:25:49 +03:00
m ac721216ea feat: add semantic chat route orchestration
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 55s
2026-08-24 20:23:17 +03:00
m e314d02212 Merge pull request 'Route conversational questions to local cognition' (#274) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 55s
Merge PR #274: route conversational questions to local cognition
2026-08-24 17:05:59 +00:00
m 6394e45bf2 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 55s
2026-08-24 20:03:35 +03:00
m ba5cc60c15 fix: route conversational questions to local cognition
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 55s
2026-08-24 20:00:52 +03:00
m a0fed9956c Merge pull request 'Keep local readiness responsive when optional models stall' (#273) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m1s
Merge PR #273: keep local readiness responsive
2026-08-24 16:39:41 +00:00
m 58e9c97d76 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 55s
2026-08-24 19:37:18 +03:00
m ba154e4ec2 fix: keep local readiness responsive
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 52s
2026-08-24 19:34:35 +03:00
m c4d7795280 Merge pull request 'Keep local lifeboat context for conversational fallback' (#272) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 53s
Merge PR #272: keep local lifeboat context for conversational fallback
2026-08-24 16:27:20 +00:00
m 25323d4ac1 Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 53s
2026-08-24 19:24:53 +03:00
m 15133cd0fc fix: keep local chat context in lifeboat
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 50s
2026-08-24 19:22:25 +03:00
m 813554dc80 Merge pull request 'fix: keep model inventory on fast path' (#271) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 55s
Merge PR #271: keep structured model inventory on fast path
2026-08-24 16:07:52 +00:00
m bd8980bc3c Merge remote-tracking branch 'origin/main' into codex/chat-semantic-inventory
CI / test (push) Successful in 56s
CI / test (pull_request) Successful in 54s
2026-08-24 19:04:58 +03:00
m 62a3713e1e fix: keep structured model inventory on fast path
CI / test (push) Successful in 56s
CI / test (pull_request) Successful in 54s
2026-08-24 19:02:05 +03:00
m 1fb03de7be Merge pull request 'Semantic-first Domovoy chat routing and reflex drift audit' (#270) from codex/chat-semantic-inventory into main
CI / test (push) Successful in 1m19s
Merge PR #270: semantic-first Domovoy chat routing and reflex drift audit
2026-08-24 15:46:56 +00:00
m fa92a10eb7 feat: prefer semantic interpretation and audit reflexes
CI / test (push) Successful in 53s
CI / test (pull_request) Successful in 1m16s
2026-08-24 18:34:27 +03:00
m bf20f247eb fix: route model inventory requests by meaning
CI / test (push) Successful in 1m2s
2026-08-24 18:30:39 +03:00
m 560b51e2f6 Merge pull request 'DEV-000291 Record authenticated mobile acceptance' (#269) from codex/dev-000248-self-development-runtime-status into main
CI / test (push) Successful in 57s
2026-08-24 11:05:01 +00:00
215 changed files with 25877 additions and 252 deletions
+13
View File
@@ -72,3 +72,16 @@ State:
- assumptions;
- findings not changed;
- gaps or limitations.
## Domovoy Gen2 execution governance
When the active task is a Gen2 packet, read the canonical documents before editing:
- `docs/DOMOVOY_CONSTITUTION.md`
- `architecture/GEN2_TARGET_ARCHITECTURE.md`
- `architecture/GEN2_CANONICAL_DATA_MODEL.md`
- `development/GEN2_MIGRATION_MAP.md`
- `development/GEN2_MASTER_PLAN.md`
- `development/GEN2_EXECUTION_CONTRACT.md`
The active task packet and `development/GEN2_TASK_INDEX.md` define execution order. `PRE-GEN2-000` freezes the V1 baseline; `ARCH-GEN2-000` accepts Gen2 governance; later `GEN2-*` packets must not be started automatically. Gen2 architecture and ownership changes are permitted only within the active packet's declared scope and authority. Default validation is FAST; MILESTONE or FULL is required only when the selected packet explicitly says so.
+15 -2
View File
@@ -122,10 +122,10 @@ type compatibleChatResponse struct {
}
func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" || len(request.Context) != 0 {
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" {
return cognitive.AdvisoryResponse{}, ErrInvalidAdvisory
}
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Provide non-canonical advisory text only. Never claim that an action occurred."}, {Role: "user", Content: request.Message}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Ты отвечаешь от имени Домового — локального помощника пользователя. Не называй себя именем модели, поставщиком или внутренним runtime. Если спрашивают имя, ответь: «Меня зовут Домовой». Понимай смысл сообщения и помогай довести цель до полезного результата. По умолчанию отвечай кратко, примерно одним-тремя предложениями; подробности давай только по просьбе. Отвечай только некатегоричным текстом совета; не утверждай, что внешнее действие уже выполнено."}, {Role: "user", Content: localAdvisoryMessage(request)}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
var result compatibleChatResponse
status, err := a.requestJSON(ctx, http.MethodPost, "/chat/completions", payload, &result)
if err != nil {
@@ -151,6 +151,19 @@ func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cogni
return response, nil
}
func localAdvisoryMessage(request cognitive.AdvisoryRequest) string {
if len(request.Context) == 0 {
return request.Message
}
var builder strings.Builder
builder.WriteString(request.Message)
builder.WriteString("\n\nRelevant local context:\n")
for _, excerpt := range request.Context {
fmt.Fprintf(&builder, "- [%s/%s] %s\n", excerpt.SourceKind, excerpt.SourceID, excerpt.Text)
}
return builder.String()
}
func (a *OpenAICompatibleLocalAdapter) requestJSON(ctx context.Context, method, path string, input, output any) (int, error) {
var body io.Reader
if input != nil {
@@ -92,3 +92,32 @@ func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(
t.Fatal("expected bounded advisory to disable template thinking")
}
}
func TestOpenAICompatibleLocalAdapterPassesLocalContextToAdvisory(t *testing.T) {
transport := &captureRoundTripper{}
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", transport)
if err != nil {
t.Fatal(err)
}
_, err = adapter.Advise(context.Background(), cognitive.AdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1, RequestID: "request-context", ActorID: "actor", PrincipalID: "principal", ScopeID: "scope",
Message: "Продолжи разговор", Locale: "ru-RU", MaxOutputBytes: 256, ProviderBindingID: "binding-1", ProviderBindingRevision: 1,
ProviderInstanceID: "provider-1", ModelDescriptorID: "model-1", ProviderModelID: capability.ProviderModelID("local-model"),
Context: []cognitive.ContextExcerpt{{ID: "focus", SourceKind: "situation", SourceID: "focus-1", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: "Активный проект: Домовой"}},
})
if err != nil {
t.Fatal(err)
}
var payload struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
}
if err := json.Unmarshal(transport.body, &payload); err != nil {
t.Fatal(err)
}
if len(payload.Messages) != 2 || !strings.Contains(payload.Messages[1].Content, "Активный проект: Домовой") {
t.Fatalf("local context was not passed to advisory: %+v", payload.Messages)
}
}
+509
View File
@@ -0,0 +1,509 @@
package multimodal
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"strings"
"time"
)
var ErrEmptyResult = errors.New("multimodal provider returned an empty result")
type SpeechClient struct {
BaseURL string
Client *http.Client
}
type VisionClient struct {
BaseURL string
Client *http.Client
}
type ImageGenerationClient struct {
BaseURL string
Client *http.Client
}
type VideoUnderstandingClient struct {
BaseURL string
Client *http.Client
}
type GeneratedImage struct {
DataBase64 string
SourceURL string
ContentType string
}
type ImageGenerator interface {
Generate(ctx context.Context, model, prompt, size string) (GeneratedImage, error)
}
// ComfyImageGenerationClient is the replaceable transport for a ComfyUI
// image-generation skill. The workflow is deliberately kept in this adapter:
// the core only selects a capability and supplies the user's prompt.
type ComfyImageGenerationClient struct {
BaseURL string
Client *http.Client
PollInterval time.Duration
PollTimeout time.Duration
}
func (c SpeechClient) Transcribe(ctx context.Context, filename, contentType string, data []byte) (string, error) {
if len(data) == 0 {
return "", errors.New("audio payload is empty")
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
header := make(textproto.MIMEHeader)
header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, strings.ReplaceAll(filename, `"`, "")))
header.Set("Content-Type", contentType)
part, err := writer.CreatePart(header)
if err != nil {
return "", err
}
if _, err := part.Write(data); err != nil {
return "", err
}
if err := writer.WriteField("response_format", "json"); err != nil {
return "", err
}
if err := writer.Close(); err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/inference", body)
if err != nil {
return "", err
}
request.Header.Set("Content-Type", writer.FormDataContentType())
response, err := c.client().Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("speech provider status %d", response.StatusCode)
}
var result struct {
Text string `json:"text"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&result); err != nil {
return "", err
}
if strings.TrimSpace(result.Text) == "" {
return "", ErrEmptyResult
}
return strings.TrimSpace(result.Text), nil
}
func (c VisionClient) Describe(ctx context.Context, model, contentType string, data []byte, instruction string) (string, error) {
if len(data) == 0 {
return "", errors.New("image payload is empty")
}
payload := struct {
Model string `json:"model"`
Messages []struct {
Role string `json:"role"`
Content []struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL *struct {
URL string `json:"url"`
} `json:"image_url,omitempty"`
} `json:"content"`
} `json:"messages"`
MaxTokens int `json:"max_tokens"`
}{}
payload.Model = model
payload.MaxTokens = 1024
payload.Messages = append(payload.Messages, struct {
Role string `json:"role"`
Content []struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL *struct {
URL string `json:"url"`
} `json:"image_url,omitempty"`
} `json:"content"`
}{Role: "user", Content: []struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL *struct {
URL string `json:"url"`
} `json:"image_url,omitempty"`
}{
{Type: "text", Text: instruction},
{Type: "image_url", ImageURL: &struct {
URL string `json:"url"`
}{URL: "data:" + contentType + ";base64," + encodeBase64(data)},
}}})
encoded, err := json.Marshal(payload)
if err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/chat/completions", bytes.NewReader(encoded))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client().Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("vision provider status %d", response.StatusCode)
}
var result struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
} `json:"message"`
} `json:"choices"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 2<<20)).Decode(&result); err != nil {
return "", err
}
if len(result.Choices) == 0 {
return "", ErrEmptyResult
}
text := strings.TrimSpace(result.Choices[0].Message.Content)
if text == "" {
text = strings.TrimSpace(result.Choices[0].Message.ReasoningContent)
}
if text == "" {
return "", ErrEmptyResult
}
return text, nil
}
// Generate uses an OpenAI-compatible image-generation skill. The provider is
// selected outside the core by the capability binding; this adapter contains
// transport only and never decides what the user meant.
func (c ImageGenerationClient) Generate(ctx context.Context, model, prompt, size string) (GeneratedImage, error) {
if strings.TrimSpace(prompt) == "" {
return GeneratedImage{}, errors.New("image prompt is empty")
}
payload := struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
Size string `json:"size,omitempty"`
ResponseFormat string `json:"response_format,omitempty"`
}{Model: model, Prompt: prompt, Size: size, ResponseFormat: "b64_json"}
encoded, err := json.Marshal(payload)
if err != nil {
return GeneratedImage{}, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/images/generations", bytes.NewReader(encoded))
if err != nil {
return GeneratedImage{}, err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client().Do(request)
if err != nil {
return GeneratedImage{}, err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode > 299 {
return GeneratedImage{}, fmt.Errorf("image provider status %d", response.StatusCode)
}
var result struct {
Data []struct {
Base64 string `json:"b64_json"`
URL string `json:"url"`
} `json:"data"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<20)).Decode(&result); err != nil {
return GeneratedImage{}, err
}
if len(result.Data) == 0 || (strings.TrimSpace(result.Data[0].Base64) == "" && strings.TrimSpace(result.Data[0].URL) == "") {
return GeneratedImage{}, ErrEmptyResult
}
return GeneratedImage{DataBase64: result.Data[0].Base64, SourceURL: result.Data[0].URL, ContentType: "image/png"}, nil
}
func (c ComfyImageGenerationClient) Generate(ctx context.Context, model, prompt, size string) (GeneratedImage, error) {
if strings.TrimSpace(prompt) == "" {
return GeneratedImage{}, errors.New("image prompt is empty")
}
width, height := imageSize(size)
workflow := map[string]any{
"3": map[string]any{"inputs": map[string]any{"seed": time.Now().UnixNano(), "steps": 8, "cfg": 1.0, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0, "model": []any{"16", 0}, "positive": []any{"6", 0}, "negative": []any{"7", 0}, "latent_image": []any{"13", 0}}, "class_type": "KSampler"},
"6": map[string]any{"inputs": map[string]any{"text": prompt, "clip": []any{"18", 0}}, "class_type": "CLIPTextEncode"},
"7": map[string]any{"inputs": map[string]any{"text": "blurry, ugly, distorted", "clip": []any{"18", 0}}, "class_type": "CLIPTextEncode"},
"8": map[string]any{"inputs": map[string]any{"samples": []any{"3", 0}, "vae": []any{"17", 0}}, "class_type": "VAEDecode"},
"9": map[string]any{"inputs": map[string]any{"filename_prefix": "domovoy", "images": []any{"8", 0}}, "class_type": "SaveImage"},
"13": map[string]any{"inputs": map[string]any{"width": width, "height": height, "batch_size": 1}, "class_type": "EmptySD3LatentImage"},
"16": map[string]any{"inputs": map[string]any{"unet_name": "z_image_turbo_bf16.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
"17": map[string]any{"inputs": map[string]any{"vae_name": "ae.safetensors"}, "class_type": "VAELoader"},
"18": map[string]any{"inputs": map[string]any{"clip_name": "qwen_3_4b.safetensors", "type": "lumina2", "device": "default"}, "class_type": "CLIPLoader"},
}
payload := map[string]any{"prompt": workflow, "client_id": fmt.Sprintf("domovoy-%d", time.Now().UnixNano())}
encoded, err := json.Marshal(payload)
if err != nil {
return GeneratedImage{}, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/prompt", bytes.NewReader(encoded))
if err != nil {
return GeneratedImage{}, err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client().Do(request)
if err != nil {
return GeneratedImage{}, err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode > 299 {
return GeneratedImage{}, fmt.Errorf("comfy provider status %d", response.StatusCode)
}
var accepted struct {
PromptID string `json:"prompt_id"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&accepted); err != nil {
return GeneratedImage{}, err
}
if accepted.PromptID == "" {
return GeneratedImage{}, ErrEmptyResult
}
// A caller timeout must remove only this prompt. ComfyUI's global
// /interrupt endpoint can otherwise stop unrelated users' work and leave
// queued requests in an ambiguous state.
defer func() {
if ctx.Err() != nil {
cancelCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := c.cancelPrompt(cancelCtx, accepted.PromptID); err != nil {
// The original context error is more useful to the caller; the
// provider cleanup failure remains observable in the server log.
log.Printf("comfy prompt cleanup failed for %s: %v", accepted.PromptID, err)
}
}
}()
interval := c.PollInterval
if interval <= 0 {
interval = time.Second
}
timeout := c.PollTimeout
if timeout <= 0 {
timeout = 10 * time.Minute
}
deadline := time.NewTimer(timeout)
defer deadline.Stop()
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
image, done, err := c.historyImage(ctx, accepted.PromptID)
if err != nil {
return GeneratedImage{}, err
}
if done {
return image, nil
}
select {
case <-ctx.Done():
return GeneratedImage{}, ctx.Err()
case <-deadline.C:
return GeneratedImage{}, context.DeadlineExceeded
case <-ticker.C:
}
}
}
func (c ComfyImageGenerationClient) cancelPrompt(ctx context.Context, promptID string) error {
if strings.TrimSpace(promptID) == "" {
return errors.New("comfy prompt id is empty")
}
payload, err := json.Marshal(map[string][]string{"delete": {promptID}})
if err != nil {
return err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/queue", bytes.NewReader(payload))
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client().Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode > 299 {
return fmt.Errorf("comfy prompt cleanup status %d", response.StatusCode)
}
return nil
}
func (c ComfyImageGenerationClient) historyImage(ctx context.Context, promptID string) (GeneratedImage, bool, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.BaseURL, "/")+"/history/"+url.PathEscape(promptID), nil)
if err != nil {
return GeneratedImage{}, false, err
}
response, err := c.client().Do(request)
if err != nil {
return GeneratedImage{}, false, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusNotFound {
return GeneratedImage{}, false, nil
}
if response.StatusCode < 200 || response.StatusCode > 299 {
return GeneratedImage{}, false, fmt.Errorf("comfy history status %d", response.StatusCode)
}
var history map[string]struct {
Outputs map[string]struct {
Images []struct {
Filename string `json:"filename"`
Subfolder string `json:"subfolder"`
Type string `json:"type"`
} `json:"images"`
} `json:"outputs"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 4<<20)).Decode(&history); err != nil {
return GeneratedImage{}, false, err
}
record, ok := history[promptID]
if !ok {
return GeneratedImage{}, false, nil
}
for _, output := range record.Outputs {
for _, image := range output.Images {
if image.Filename == "" {
continue
}
query := url.Values{"filename": {image.Filename}, "subfolder": {image.Subfolder}, "type": {image.Type}}
imageRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.BaseURL, "/")+"/view?"+query.Encode(), nil)
if err != nil {
return GeneratedImage{}, false, err
}
imageResponse, err := c.client().Do(imageRequest)
if err != nil {
return GeneratedImage{}, false, err
}
data, readErr := io.ReadAll(io.LimitReader(imageResponse.Body, 32<<20))
imageResponse.Body.Close()
if readErr != nil {
return GeneratedImage{}, false, readErr
}
if imageResponse.StatusCode < 200 || imageResponse.StatusCode > 299 || len(data) == 0 {
return GeneratedImage{}, false, ErrEmptyResult
}
contentType := imageResponse.Header.Get("Content-Type")
if contentType == "" {
contentType = "image/png"
}
return GeneratedImage{DataBase64: encodeBase64(data), ContentType: contentType}, true, nil
}
}
return GeneratedImage{}, true, ErrEmptyResult
}
func imageSize(size string) (int, int) {
parts := strings.Split(strings.TrimSpace(size), "x")
if len(parts) == 2 {
var width, height int
if _, err := fmt.Sscanf(size, "%dx%d", &width, &height); err == nil && width >= 256 && height >= 256 && width <= 2048 && height <= 2048 {
return width, height
}
}
return 512, 512
}
// Describe accepts a video skill endpoint that performs its own bounded frame
// sampling. Frame extraction is provider-owned so the core does not acquire a
// media-processing dependency or hide a policy in orchestration code.
func (c VideoUnderstandingClient) Describe(ctx context.Context, model, filename, contentType string, data []byte, instruction string) (string, error) {
if len(data) == 0 {
return "", errors.New("video payload is empty")
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
header := make(textproto.MIMEHeader)
header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, strings.ReplaceAll(filename, `"`, "")))
header.Set("Content-Type", contentType)
part, err := writer.CreatePart(header)
if err != nil {
return "", err
}
if _, err := part.Write(data); err != nil {
return "", err
}
_ = writer.WriteField("model", model)
_ = writer.WriteField("instruction", instruction)
if err := writer.Close(); err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/understand", body)
if err != nil {
return "", err
}
request.Header.Set("Content-Type", writer.FormDataContentType())
response, err := c.client().Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode > 299 {
return "", fmt.Errorf("video provider status %d", response.StatusCode)
}
var result struct {
Text string `json:"text"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 2<<20)).Decode(&result); err != nil {
return "", err
}
if strings.TrimSpace(result.Text) == "" {
return "", ErrEmptyResult
}
return strings.TrimSpace(result.Text), nil
}
func (c SpeechClient) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
func (c VisionClient) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
func (c ImageGenerationClient) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
func (c ComfyImageGenerationClient) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
func (c VideoUnderstandingClient) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
func encodeBase64(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
+209
View File
@@ -0,0 +1,209 @@
package multimodal
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestSpeechClientSendsMultipartAndReturnsTranscript(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/inference" || r.Method != http.MethodPost {
t.Fatalf("unexpected speech request: %s %s", r.Method, r.URL.Path)
}
if err := r.ParseMultipartForm(1 << 20); err != nil {
t.Fatal(err)
}
file, _, err := r.FormFile("file")
if err != nil {
t.Fatal(err)
}
defer file.Close()
data, _ := io.ReadAll(file)
if string(data) != "audio" {
t.Fatalf("audio payload = %q", data)
}
_ = json.NewEncoder(w).Encode(map[string]string{"text": "распознанная фраза"})
}))
defer server.Close()
text, err := (SpeechClient{BaseURL: server.URL, Client: server.Client()}).Transcribe(context.Background(), "voice.wav", "audio/wav", []byte("audio"))
if err != nil || text != "распознанная фраза" {
t.Fatalf("transcript=%q err=%v", text, err)
}
}
func TestVisionClientSendsImageDataAndInstruction(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var payload struct {
Messages []struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
ImageURL *struct {
URL string `json:"url"`
} `json:"image_url"`
} `json:"content"`
} `json:"messages"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
if len(payload.Messages) != 1 || len(payload.Messages[0].Content) != 2 || !strings.Contains(payload.Messages[0].Content[0].Text, "Опиши") || payload.Messages[0].Content[1].ImageURL == nil || !strings.HasPrefix(payload.Messages[0].Content[1].ImageURL.URL, "data:image/png;base64,") {
t.Fatalf("unexpected vision payload: %+v", payload)
}
_ = json.NewEncoder(w).Encode(map[string]any{"choices": []any{map[string]any{"message": map[string]string{"content": "на изображении круг"}}}})
}))
defer server.Close()
text, err := (VisionClient{BaseURL: server.URL + "/v1", Client: server.Client()}).Describe(context.Background(), "vision-model", "image/png", []byte{0, 1, 2}, "Опиши изображение")
if err != nil || text != "на изображении круг" {
t.Fatalf("description=%q err=%v", text, err)
}
}
func TestVisionClientUsesReasoningContentWhenProviderOmitsContent(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{"choices": []any{map[string]any{
"message": map[string]string{"content": "", "reasoning_content": "чёрный квадрат"},
}}})
}))
defer server.Close()
text, err := (VisionClient{BaseURL: server.URL, Client: server.Client()}).Describe(context.Background(), "vision-model", "image/png", []byte("image"), "Опиши изображение")
if err != nil || text != "чёрный квадрат" {
t.Fatalf("description=%q err=%v", text, err)
}
}
func TestImageGenerationClientSendsPromptAndReturnsBase64(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/images/generations" || r.Method != http.MethodPost {
t.Fatalf("unexpected image request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
Size string `json:"size"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
if payload.Model != "image-model" || payload.Prompt != "green bird" || payload.Size != "1024x1024" {
t.Fatalf("unexpected image payload: %+v", payload)
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]string{"b64_json": "aW1hZ2U="}}})
}))
defer server.Close()
image, err := (ImageGenerationClient{BaseURL: server.URL, Client: server.Client()}).Generate(context.Background(), "image-model", "green bird", "1024x1024")
if err != nil || image.DataBase64 != "aW1hZ2U=" || image.ContentType != "image/png" {
t.Fatalf("image=%+v err=%v", image, err)
}
}
func TestVideoUnderstandingClientSendsMultipartAndReturnsDescription(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/understand" || r.Method != http.MethodPost {
t.Fatalf("unexpected video request: %s %s", r.Method, r.URL.Path)
}
if err := r.ParseMultipartForm(1 << 20); err != nil {
t.Fatal(err)
}
if r.FormValue("model") != "video-model" || r.FormValue("instruction") != "describe movement" {
t.Fatalf("unexpected video fields: model=%q instruction=%q", r.FormValue("model"), r.FormValue("instruction"))
}
file, _, err := r.FormFile("file")
if err != nil {
t.Fatal(err)
}
defer file.Close()
data, _ := io.ReadAll(file)
if string(data) != "video" {
t.Fatalf("video payload=%q", data)
}
_ = json.NewEncoder(w).Encode(map[string]string{"text": "человек идёт"})
}))
defer server.Close()
text, err := (VideoUnderstandingClient{BaseURL: server.URL, Client: server.Client()}).Describe(context.Background(), "video-model", "clip.mp4", "video/mp4", []byte("video"), "describe movement")
if err != nil || text != "человек идёт" {
t.Fatalf("description=%q err=%v", text, err)
}
}
func TestComfyImageGenerationClientRunsWorkflowAndReturnsImage(t *testing.T) {
polls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/prompt":
var payload struct {
Prompt map[string]any `json:"prompt"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
if payload.Prompt["6"].(map[string]any)["inputs"].(map[string]any)["text"] != "red bird" {
t.Fatalf("prompt was not forwarded: %+v", payload.Prompt["6"])
}
_ = json.NewEncoder(w).Encode(map[string]string{"prompt_id": "test-prompt"})
case "/history/test-prompt":
polls++
if polls == 1 {
_, _ = w.Write([]byte(`{}`))
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"test-prompt": map[string]any{"outputs": map[string]any{"9": map[string]any{"images": []any{map[string]string{"filename": "out.png", "subfolder": "", "type": "output"}}}}}})
case "/view":
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write([]byte("png"))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
image, err := (ComfyImageGenerationClient{BaseURL: server.URL, Client: server.Client(), PollInterval: time.Millisecond, PollTimeout: time.Second}).Generate(context.Background(), "", "red bird", "256x256")
if err != nil || image.DataBase64 != "cG5n" || image.ContentType != "image/png" {
t.Fatalf("image=%+v err=%v", image, err)
}
}
func TestComfyImageGenerationClientDeletesOnlyCancelledPrompt(t *testing.T) {
var deleted []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/prompt":
_ = json.NewEncoder(w).Encode(map[string]string{"prompt_id": "cancel-me"})
case "/history/cancel-me":
<-r.Context().Done()
case "/queue":
var payload struct {
Delete []string `json:"delete"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
deleted = append(deleted, payload.Delete...)
w.WriteHeader(http.StatusNoContent)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() {
_, err := (ComfyImageGenerationClient{BaseURL: server.URL, Client: server.Client(), PollInterval: time.Millisecond, PollTimeout: time.Minute}).Generate(ctx, "", "red bird", "256x256")
done <- err
}()
time.Sleep(10 * time.Millisecond)
cancel()
if err := <-done; !errors.Is(err, context.Canceled) {
t.Fatalf("err=%v", err)
}
if len(deleted) != 1 || deleted[0] != "cancel-me" {
t.Fatalf("deleted=%v", deleted)
}
}
+18
View File
@@ -0,0 +1,18 @@
package multimodal
import (
"crypto/sha256"
"encoding/hex"
"git.cin.su/m/Assistant/contracts/artifact"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/interaction"
)
func NormalizeIngress(id string, modality interaction.IngressModality, sourceRef, deviceRef, locationRef, principalID, contentType string, data []byte, response capability.MultimodalResponse, at int64) (interaction.IngressEnvelope, artifact.MediaArtifact) {
hash := sha256.Sum256(data)
artifactValue := artifact.MediaArtifact{ID: artifact.ArtifactID("artifact:media:" + hex.EncodeToString(hash[:8])), ContentType: contentType, SizeBytes: int64(len(data)), ContentDigest: hex.EncodeToString(hash[:]), SourceRef: sourceRef, RecordedAtUnixMillis: at}
if len(data) == 0 {
return interaction.IngressEnvelope{ID: id, Modality: modality, SourceRef: sourceRef, DeviceRef: deviceRef, LocationRef: locationRef, PrincipalID: principalID, Degraded: true, RecordedAtUnixMillis: at}, artifact.MediaArtifact{}
}
return interaction.IngressEnvelope{ID: id, Modality: modality, SourceRef: sourceRef, DeviceRef: deviceRef, LocationRef: locationRef, PrincipalID: principalID, ArtifactRef: string(artifactValue.ID), ObservationRef: response.EvidenceRef, ProviderRef: response.Provider, ModelRef: response.Model, ModelVersion: response.ModelVersion, ConfidenceMillis: response.ConfidenceMillis, RecordedAtUnixMillis: at}, artifactValue
}
+22
View File
@@ -0,0 +1,22 @@
package multimodal_test
import (
"git.cin.su/m/Assistant/adapters/multimodal"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/interaction"
"testing"
)
func TestIngressNormalizesMediaArtifactAndObservationProvenance(t *testing.T) {
e, a := multimodal.NormalizeIngress("ingress-1", interaction.IngressImage, "camera:street", "device:cam", "world:street", "principal", "image/png", []byte("image"), capability.MultimodalResponse{Provider: "local", Model: "vision", ModelVersion: "1", EvidenceRef: "observation-1", ConfidenceMillis: 800}, 1)
if !e.Valid() || !a.Valid() || e.ArtifactRef == "" || e.ObservationRef != "observation-1" || e.DeviceRef == "" || e.LocationRef == "" {
t.Fatalf("envelope=%+v artifact=%+v", e, a)
}
}
func TestUnavailableIngressDegradesWithoutFabricatingObservation(t *testing.T) {
e, a := multimodal.NormalizeIngress("ingress-1", interaction.IngressAudio, "mic", "device:mic", "", "principal", "audio/wav", nil, capability.MultimodalResponse{}, 1)
if !e.Degraded || e.ObservationRef != "" || a.Valid() {
t.Fatalf("envelope=%+v artifact=%+v", e, a)
}
}
@@ -3,6 +3,7 @@ package postgres
import (
"context"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -98,3 +99,22 @@ func (r *InteractionRepository) Utterances(ctx context.Context, id interaction.C
}
return out, rows.Err()
}
// RecentUtterancesForPrincipal provides durable conversational context when a
// client starts a fresh conversation instead of carrying the previous ID.
func (r *InteractionRepository) RecentUtterancesForPrincipal(ctx context.Context, principalID identity.PrincipalID, limit int) ([]interaction.HumanUtterance, error) {
rows, err := r.pool.Query(ctx, `SELECT utterance_id,conversation_id,actor_id,principal_id,channel,received_at_unix_millis,correlation_id,content,content_ref,speech_act,interpretation_state,corrects_utterance_id FROM interaction.utterances WHERE principal_id=$1 ORDER BY received_at_unix_millis DESC, utterance_id DESC LIMIT $2`, principalID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []interaction.HumanUtterance
for rows.Next() {
var u interaction.HumanUtterance
if err := rows.Scan(&u.ID, &u.ConversationID, &u.ActorID, &u.PrincipalID, &u.Channel, &u.ReceivedAtUnixMillis, &u.CorrelationID, &u.Content, &u.ContentRef, &u.SpeechAct, &u.InterpretationState, &u.CorrectsUtteranceID); err != nil {
return nil, err
}
out = append(out, u)
}
return out, rows.Err()
}
+33
View File
@@ -2,6 +2,7 @@ package postgres
import (
"context"
"encoding/json"
"strings"
"git.cin.su/m/Assistant/contracts/identity"
@@ -18,6 +19,38 @@ func OpenSituation(ctx context.Context, config ContinuityConfig) (*SituationRepo
}
return &SituationRepository{pool: pool}, nil
}
func (r *SituationRepository) SaveContextScope(ctx context.Context, scope situation.ContextScope) error {
if err := scope.Validate(); err != nil {
return err
}
bindings, err := json.Marshal(scope.Bindings)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO situation.context_scopes(context_scope_id,principal_id,revision,bindings,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(context_scope_id) DO UPDATE SET revision=EXCLUDED.revision, bindings=EXCLUDED.bindings, recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`, scope.ID, scope.PrincipalID, scope.Revision, bindings, scope.RecordedAt)
return err
}
func (r *SituationRepository) SaveFocusFrame(ctx context.Context, frame situation.FocusFrame) error {
if err := frame.Validate(); err != nil {
return err
}
ref, err := json.Marshal(frame.ContextScopeRef)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO situation.focus_frames(focus_frame_id,principal_id,actor_id,revision,authority_scope_ref,context_scope_ref,goal_ref,task_ref,source_utterance_ref,provenance,confidence_millis,state,parent_frame_ref,activated_at_unix_millis,suspended_at_unix_millis,expired_at_unix_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT(focus_frame_id) DO UPDATE SET revision=EXCLUDED.revision, context_scope_ref=EXCLUDED.context_scope_ref, state=EXCLUDED.state, suspended_at_unix_millis=EXCLUDED.suspended_at_unix_millis, expired_at_unix_millis=EXCLUDED.expired_at_unix_millis, recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`, frame.ID, frame.PrincipalID, frame.ActorID, frame.Revision, frame.AuthorityScopeRef, ref, frame.GoalRef, frame.TaskRef, frame.SourceUtteranceRef, frame.Provenance, frame.ConfidenceMillis, frame.State, frame.ParentFrameRef, frame.ActivatedAt, frame.SuspendedAt, frame.ExpiredAt, frame.RecordedAt)
return err
}
func (r *SituationRepository) AppendFocusTransition(ctx context.Context, transition situation.FocusTransition) error {
if err := transition.Validate(); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO situation.focus_transitions(transition_id,focus_frame_id,from_revision,to_revision,operation,cause_ref,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7)`, transition.TransitionID, transition.FrameID, transition.FromRevision, transition.ToRevision, transition.Operation, transition.CauseRef, transition.RecordedAt)
return err
}
func (r *SituationRepository) Close() { r.pool.Close() }
func (r *SituationRepository) Save(ctx context.Context, binding situation.FocusBinding) error {
_, err := r.pool.Exec(ctx, `INSERT INTO situation.focus_bindings (focus_binding_id, actor_id, session_id, workspace_id, project_id, goal_id, scope_id, active) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT (focus_binding_id) DO UPDATE SET active=EXCLUDED.active`, binding.ID, binding.ActorID, binding.SessionID, binding.Workspace, binding.ProjectID, binding.GoalID, binding.ScopeID, binding.Active)
+115
View File
@@ -25,6 +25,121 @@ var (
ErrDevelopmentCandidateConflict = errors.New("development candidate conflict")
)
func (r *WorkRepository) SaveInvestigation(ctx context.Context, value work.Investigation, expectedRevision int64) error {
if !value.Valid() || expectedRevision < 0 {
return errors.New("invalid investigation revision")
}
attempts := make([]string, 0, len(value.AttemptRefs))
for _, item := range value.AttemptRefs {
attempts = append(attempts, string(item))
}
hypotheses := make([]string, 0, len(value.HypothesisRefs))
for _, item := range value.HypothesisRefs {
hypotheses = append(hypotheses, string(item))
}
findings := make([]string, 0, len(value.FindingRefs))
for _, item := range value.FindingRefs {
findings = append(findings, string(item))
}
blockers := append([]string(nil), value.Blockers...)
checkpoints := make([]string, 0, len(value.CheckpointRefs))
for _, item := range value.CheckpointRefs {
checkpoints = append(checkpoints, string(item))
}
if expectedRevision == 0 {
result, err := r.pool.Exec(ctx, `INSERT INTO work.long_running_work(investigation_id,objective,hypothesis_refs,attempt_refs,finding_refs,blockers,checkpoint_refs,next_step,state,revision,time_budget_millis,cost_budget_micros,max_attempts,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT(investigation_id) DO NOTHING`, value.ID, value.Objective, hypotheses, attempts, findings, blockers, checkpoints, value.NextStep, value.State, value.Revision, value.TimeBudgetMillis, value.CostBudgetMicros, value.MaxAttempts, value.RecordedAtUnixMillis)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return errors.New("investigation already exists")
}
return nil
}
result, err := r.pool.Exec(ctx, `UPDATE work.long_running_work SET objective=$2,hypothesis_refs=$3,attempt_refs=$4,finding_refs=$5,blockers=$6,checkpoint_refs=$7,next_step=$8,state=$9,revision=$10,time_budget_millis=$11,cost_budget_micros=$12,max_attempts=$13,recorded_at_unix_millis=$14 WHERE investigation_id=$1 AND revision=$15`, value.ID, value.Objective, hypotheses, attempts, findings, blockers, checkpoints, value.NextStep, value.State, value.Revision, value.TimeBudgetMillis, value.CostBudgetMicros, value.MaxAttempts, value.RecordedAtUnixMillis, expectedRevision)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return errors.New("investigation revision conflict")
}
return nil
}
func (r *WorkRepository) LoadInvestigation(ctx context.Context, id work.LongRunningWorkID) (work.Investigation, error) {
var value work.Investigation
var hypotheses, attempts, findings, blockers, checkpoints []string
err := r.pool.QueryRow(ctx, `SELECT investigation_id,objective,hypothesis_refs,attempt_refs,finding_refs,blockers,checkpoint_refs,next_step,state,revision,time_budget_millis,cost_budget_micros,max_attempts,recorded_at_unix_millis FROM work.long_running_work WHERE investigation_id=$1`, id).Scan(&value.ID, &value.Objective, &hypotheses, &attempts, &findings, &blockers, &checkpoints, &value.NextStep, &value.State, &value.Revision, &value.TimeBudgetMillis, &value.CostBudgetMicros, &value.MaxAttempts, &value.RecordedAtUnixMillis)
if err != nil {
return work.Investigation{}, fmt.Errorf("load investigation: %w", err)
}
for _, item := range hypotheses {
value.HypothesisRefs = append(value.HypothesisRefs, work.EvidenceRef(item))
}
for _, item := range attempts {
value.AttemptRefs = append(value.AttemptRefs, work.AgentRunID(item))
}
for _, item := range findings {
value.FindingRefs = append(value.FindingRefs, work.EvidenceRef(item))
}
value.Blockers = append(value.Blockers, blockers...)
for _, item := range checkpoints {
value.CheckpointRefs = append(value.CheckpointRefs, work.AgentCheckpointID(item))
}
return value, nil
}
// SaveInvestigationCheckpoint atomically records the checkpoint and marks its
// attempts verified. The unique key makes retries idempotent.
func (r *WorkRepository) SaveInvestigationCheckpoint(ctx context.Context, checkpoint work.InvestigationCheckpoint) error {
if !checkpoint.Valid() {
return errors.New("invalid investigation checkpoint")
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `INSERT INTO work.investigation_checkpoints(checkpoint_id,investigation_id,completed_attempt_refs,completed_finding_refs,next_step,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(checkpoint_id) DO NOTHING`, checkpoint.ID, checkpoint.InvestigationID, stringIDs(checkpoint.CompletedAttemptRefs), stringEvidence(checkpoint.CompletedFindingRefs), checkpoint.NextStep, checkpoint.RecordedAtUnixMillis); err != nil {
return err
}
for _, attempt := range checkpoint.CompletedAttemptRefs {
if _, err = tx.Exec(ctx, `INSERT INTO work.investigation_attempts(investigation_id,attempt_ref,verified_at_unix_millis) VALUES($1,$2,$3) ON CONFLICT(investigation_id,attempt_ref) DO NOTHING`, checkpoint.InvestigationID, attempt, checkpoint.RecordedAtUnixMillis); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (r *WorkRepository) UnverifiedInvestigationAttempts(ctx context.Context, id work.LongRunningWorkID, candidates []work.AgentRunID) ([]work.AgentRunID, error) {
result := make([]work.AgentRunID, 0, len(candidates))
for _, candidate := range candidates {
var exists bool
if err := r.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM work.investigation_attempts WHERE investigation_id=$1 AND attempt_ref=$2)`, id, candidate).Scan(&exists); err != nil {
return nil, err
}
if !exists {
result = append(result, candidate)
}
}
return result, nil
}
func stringIDs(values []work.AgentRunID) []string {
result := make([]string, 0, len(values))
for _, value := range values {
result = append(result, string(value))
}
return result
}
func stringEvidence(values []work.EvidenceRef) []string {
result := make([]string, 0, len(values))
for _, value := range values {
result = append(result, string(value))
}
return result
}
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
pool, err := openPool(ctx, config)
if err != nil {
+108
View File
@@ -0,0 +1,108 @@
package postgres
import (
"context"
"encoding/json"
"fmt"
"git.cin.su/m/Assistant/contracts/world"
worlddomain "git.cin.su/m/Assistant/modules/world/domain"
"github.com/jackc/pgx/v5/pgxpool"
)
type WorldRepository struct{ pool *pgxpool.Pool }
func OpenWorld(ctx context.Context, config ContinuityConfig) (*WorldRepository, error) {
pool, err := openPool(ctx, config)
if err != nil {
return nil, err
}
return &WorldRepository{pool: pool}, nil
}
func (r *WorldRepository) Close() { r.pool.Close() }
func (r *WorldRepository) SaveKindDefinition(ctx context.Context, value world.WorldObjectKindDefinition) error {
if err := worlddomain.ValidateKindDefinition(value); err != nil {
return err
}
var parentOwner, parentKind, parentID string
var parentRevision *uint64
if value.ParentKindRef != nil {
parentOwner, parentKind, parentID = value.ParentKindRef.OwnerNamespace, value.ParentKindRef.SemanticKind, string(value.ParentKindRef.StableID)
if value.ParentKindRef.Revision != nil {
revision := uint64(*value.ParentKindRef.Revision)
parentRevision = &revision
}
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.kind_definitions(kind_definition_id,kind_key,revision,display_name,parent_owner_namespace,parent_semantic_kind,parent_stable_id,parent_revision,lifecycle,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT(kind_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.DisplayName, parentOwner, parentKind, parentID, parentRevision, value.Lifecycle, value.DefinitionDigest, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveObject(ctx context.Context, value world.WorldObject) error {
if err := worlddomain.ValidateObject(value); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.objects(world_object_id,principal_id,kind_owner_namespace,kind_semantic_kind,kind_stable_id,kind_revision,lifecycle,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(world_object_id) DO NOTHING`, value.ID, value.PrincipalID, value.KindDefinitionRef.OwnerNamespace, value.KindDefinitionRef.SemanticKind, value.KindDefinitionRef.StableID, value.KindRevision, value.Lifecycle, value.CreatedAt)
return err
}
func (r *WorldRepository) SaveAlias(ctx context.Context, value world.WorldAlias) error {
if err := worlddomain.ValidateAlias(value); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.aliases(world_alias_id,principal_id,world_object_id,alias,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(world_alias_id) DO NOTHING`, value.ID, value.PrincipalID, value.ObjectID, value.Alias, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveFacetDefinition(ctx context.Context, value world.FacetDefinition) error {
if err := worlddomain.ValidateFacetDefinition(value); err != nil {
return err
}
payload, err := json.Marshal(value.ValueType)
if err != nil {
return fmt.Errorf("encode facet value type: %w", err)
}
_, err = r.pool.Exec(ctx, `INSERT INTO world.facet_definitions(facet_definition_id,kind_key,revision,facet_key,value_type,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(facet_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.FacetKey, payload, value.DefinitionDigest, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveRelationDefinition(ctx context.Context, value world.RelationDefinition) error {
if err := worlddomain.ValidateRelationDefinition(value); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `INSERT INTO world.relation_definitions(relation_definition_id,kind_key,revision,relation_key,target_kind_key,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(relation_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.RelationKey, value.TargetKindKey, value.DefinitionDigest, value.RecordedAt)
return err
}
func (r *WorldRepository) SaveResolution(ctx context.Context, value world.IdentityResolutionDecisionRecord) error {
if err := worlddomain.ValidateResolution(value); err != nil {
return err
}
candidates, err := json.Marshal(value.Candidates)
if err != nil {
return fmt.Errorf("encode resolution candidates: %w", err)
}
evidence, err := json.Marshal(value.Evidence)
if err != nil {
return fmt.Errorf("encode resolution evidence: %w", err)
}
_, err = r.pool.Exec(ctx, `INSERT INTO world.identity_resolution_decisions(resolution_decision_id,principal_id,candidates,decision,evidence,confidence_millis,decided_at_unix_millis,revision) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(resolution_decision_id) DO NOTHING`, value.ID, value.PrincipalID, candidates, value.Decision, evidence, value.ConfidenceMillis, value.DecidedAt, value.Revision)
return err
}
func (r *WorldRepository) SaveLineage(ctx context.Context, value world.WorldLineage) error {
if err := worlddomain.ValidateLineage(value); err != nil {
return err
}
sources, err := json.Marshal(value.SourceRefs)
if err != nil {
return fmt.Errorf("encode lineage sources: %w", err)
}
successors, err := json.Marshal(value.SuccessorRefs)
if err != nil {
return fmt.Errorf("encode lineage successors: %w", err)
}
_, err = r.pool.Exec(ctx, `INSERT INTO world.lineage(lineage_id,operation,source_refs,successor_refs,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(lineage_id) DO NOTHING`, value.ID, value.Operation, sources, successors, value.RecordedAt)
return err
}
+138 -12
View File
@@ -3,6 +3,7 @@ package research
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"html"
@@ -19,9 +20,9 @@ import (
var ErrSearchRejected = errors.New("web search request rejected")
var (
searchLinkPattern = regexp.MustCompile(`(?is)<a[^>]+class=["'][^"']*result__a[^"']*["'][^>]*>(.*?)</a>`)
searchLinkPattern = regexp.MustCompile(`(?is)(<a[^>]+class=["'][^"']*result__a[^"']*["'][^>]*>)(.*?)</a>`)
searchSnippetPattern = regexp.MustCompile(`(?is)<a[^>]+class=["'][^"']*result__snippet[^"']*["'][^>]*>(.*?)</a>`)
bingLinkPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<h2[^>]*><a[^>]*>(.*?)</a>`)
bingLinkPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<h2[^>]*>(<a[^>]*>)(.*?)</a>`)
bingSnippetPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<p[^>]*>(.*?)</p>`)
tagPattern = regexp.MustCompile(`(?s)<[^>]*>`)
termPattern = regexp.MustCompile(`(?i)[\p{L}\p{N}]{2,}`)
@@ -36,19 +37,53 @@ var searchStopTerms = map[string]struct{}{
// cities, weather terms, entities or model names: the complete semantic query
// comes from the interpretation layer.
type SearchConnector struct {
Client *http.Client
EndpointTemplate string
AllowedHosts map[string]struct{}
MaxBytes int64
MaxResults int
Client *http.Client
EndpointTemplate string
AllowedHosts map[string]struct{}
FallbackEndpoints []SearchEndpoint
MaxBytes int64
MaxResults int
}
// SearchEndpoint is an independently allowlisted web-search route. The
// connector tries routes in order and stops at the first relevant evidence.
// This is a source-recovery policy, not a question-type classifier.
type SearchEndpoint struct {
Template string
AllowedHosts map[string]struct{}
}
func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSearchRequest, nowUnixMillis int64) ([]epistemics.ResearchCandidate, error) {
if request.Plan.Validate() != nil || strings.TrimSpace(request.Query) == "" || nowUnixMillis < 1 || !strings.Contains(c.EndpointTemplate, "%s") {
return nil, ErrSearchRejected
}
endpoint, err := url.Parse(strings.Replace(c.EndpointTemplate, "%s", url.QueryEscape(strings.TrimSpace(request.Query)), 1))
if err != nil || endpoint.Scheme != "https" || endpoint.Host == "" || !c.allowedHost(strings.ToLower(endpoint.Hostname())) {
endpoints := make([]SearchEndpoint, 0, 1+len(c.FallbackEndpoints))
endpoints = append(endpoints, SearchEndpoint{Template: c.EndpointTemplate, AllowedHosts: c.AllowedHosts})
endpoints = append(endpoints, c.FallbackEndpoints...)
var lastErr error
for _, candidateEndpoint := range endpoints {
results, err := c.collectEndpoint(ctx, candidateEndpoint, request, nowUnixMillis)
if err != nil {
lastErr = err
continue
}
if len(results) > 0 {
return results, nil
}
}
if lastErr != nil {
return nil, lastErr
}
return nil, nil
}
func (c SearchConnector) collectEndpoint(ctx context.Context, configured SearchEndpoint, request epistemics.WebSearchRequest, nowUnixMillis int64) ([]epistemics.ResearchCandidate, error) {
if !strings.Contains(configured.Template, "%s") {
return nil, ErrSearchRejected
}
searchQuery := enrichedSearchQuery(request.Query)
endpoint, err := url.Parse(strings.Replace(configured.Template, "%s", url.QueryEscape(searchQuery), 1))
if err != nil || endpoint.Scheme != "https" || endpoint.Host == "" || !allowedHost(configured.AllowedHosts, strings.ToLower(endpoint.Hostname())) {
return nil, ErrSearchRejected
}
client := c.Client
@@ -63,6 +98,10 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
if err != nil {
return nil, ErrSearchRejected
}
// Search providers commonly return a consent/challenge shell to the
// default Go client. A neutral browser identification improves the same
// generic evidence path for every query without selecting by topic.
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; DomovoyResearch/1.0)")
response, err := client.Do(req)
if err != nil {
return nil, err
@@ -91,7 +130,8 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
snapshotID := epistemics.SourceSnapshotID("snapshot:search:" + base)
results := make([]epistemics.ResearchCandidate, 0, len(links))
for index, link := range links {
title := cleanHTML(string(link[1]))
linkURL := resolveSearchResultLocator(extractHref(string(link[1])), endpoint)
title := cleanHTML(string(link[2]))
snippet := ""
if index < len(snippets) {
snippet = cleanHTML(string(snippets[index][1]))
@@ -105,7 +145,7 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
results = append(results, epistemics.ResearchCandidate{
PropositionKey: request.PropositionKey + ":result:" + ordinal,
Statement: statement,
Source: epistemics.Source{ID: sourceID, Kind: "web-search", Locator: endpoint.String(), Title: title, Publisher: endpoint.Hostname()},
Source: epistemics.Source{ID: sourceID, Kind: "web-search", Locator: linkURL, Title: title, Publisher: sourcePublisher(linkURL, endpoint.Hostname())},
Snapshot: epistemics.SourceSnapshot{ID: snapshotID, SourceID: sourceID, ContentDigest: digestText, CapturedAtUnixMillis: nowUnixMillis, Freshness: epistemics.FreshnessCurrent},
Excerpt: epistemics.SourceExcerpt{ID: excerptID, SnapshotID: snapshotID, Text: statement, Start: 0, End: int64(len(statement))},
Evidence: epistemics.EvidenceObserved, Coverage: epistemics.CoverageComplete, Conflict: epistemics.ConflictNone,
@@ -120,6 +160,11 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
func searchStatementRelatesToQuery(query, statement string) bool {
terms := termPattern.FindAllString(strings.ToLower(query), -1)
content := strings.ToLower(statement)
productCue := containsSearchTerm(terms, "видеокарт", "gpu", "процессор", "телефон", "ноутбук", "монитор")
if productCue && hasNumericSearchTerm(terms) {
contentTerms := termPattern.FindAllString(content, -1)
return hasNumericSearchTerm(contentTerms) && containsSearchTerm(contentTerms, "видеокарт", "gpu", "процессор", "телефон", "ноутбук", "монитор")
}
usable := 0
for _, term := range terms {
if _, stop := searchStopTerms[term]; stop || len([]rune(term)) < 4 {
@@ -138,7 +183,38 @@ func searchStatementRelatesToQuery(query, statement string) bool {
return usable == 0
}
func (c SearchConnector) allowedHost(host string) bool { _, ok := c.AllowedHosts[host]; return ok }
func containsSearchTerm(values []string, wanted ...string) bool {
for _, value := range values {
for _, candidate := range wanted {
if strings.Contains(value, candidate) {
return true
}
}
}
return false
}
func hasNumericSearchTerm(values []string) bool {
for _, value := range values {
for _, runeValue := range value {
if runeValue >= '0' && runeValue <= '9' {
return true
}
}
}
return false
}
func enrichedSearchQuery(query string) string {
trimmed := strings.TrimSpace(query)
lower := strings.ToLower(trimmed)
if (strings.Contains(lower, "цен") || strings.Contains(lower, "стоим")) && !strings.Contains(lower, "куп") {
return trimmed + " цена купить сегодня"
}
return trimmed
}
func allowedHost(hosts map[string]struct{}, host string) bool { _, ok := hosts[host]; return ok }
func (c SearchConnector) limit() int {
if c.MaxResults <= 0 || c.MaxResults > 8 {
return 3
@@ -148,3 +224,53 @@ func (c SearchConnector) limit() int {
func cleanHTML(value string) string {
return strings.Join(strings.Fields(html.UnescapeString(tagPattern.ReplaceAllString(value, ""))), " ")
}
func extractHref(tag string) string {
match := regexp.MustCompile(`(?is)\bhref=["']([^"']+)["']`).FindStringSubmatch(tag)
if len(match) == 2 {
return match[1]
}
return ""
}
func resolveSearchResultLocator(raw string, searchEndpoint *url.URL) string {
value := strings.TrimSpace(html.UnescapeString(raw))
if value == "" {
return searchEndpoint.String()
}
parsed, err := url.Parse(value)
if err != nil {
return searchEndpoint.String()
}
if target := parsed.Query().Get("uddg"); target != "" {
value = target
parsed, err = url.Parse(target)
if err != nil {
return searchEndpoint.String()
}
}
if target := parsed.Query().Get("u"); strings.HasPrefix(target, "a1") {
if decoded, decodeErr := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(target, "a1")); decodeErr == nil {
if targetURL, parseErr := url.Parse(string(decoded)); parseErr == nil {
parsed = targetURL
}
}
}
if parsed.Scheme == "" {
parsed.Scheme = "https"
}
if parsed.Host == "" && strings.HasPrefix(value, "//") {
parsed, err = url.Parse("https:" + value)
}
if err != nil || parsed.Scheme != "https" || parsed.Host == "" {
return searchEndpoint.String()
}
return parsed.String()
}
func sourcePublisher(locator, fallback string) string {
if parsed, err := url.Parse(locator); err == nil && parsed.Hostname() != "" {
return parsed.Hostname()
}
return fallback
}
@@ -0,0 +1,57 @@
package research
import (
"context"
"crypto/tls"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/epistemics"
)
func TestSearchConnectorFallsBackToAnotherAllowedSource(t *testing.T) {
primary := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`<html><body>no usable results</body></html>`))
}))
defer primary.Close()
fallback := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`<a class="result__a" href="https://example.test">Казань: текущая температура 18 °C</a><a class="result__snippet">Наблюдается переменная облачность.</a>`))
}))
defer fallback.Close()
client := fallback.Client()
client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // test servers only
request := epistemics.WebSearchRequest{Plan: epistemics.ResearchPlan{ID: "plan:test", Question: "какая погода в Казани", SourceClasses: []string{"web-search"}, FreshnessPolicy: string(epistemics.FreshnessCurrent)}, PropositionKey: "question:test", Query: "какая погода в Казани"}
connector := SearchConnector{
Client: client,
EndpointTemplate: primary.URL + "?q=%s",
AllowedHosts: map[string]struct{}{strings.TrimPrefix(primary.URL, "https://"): {}},
FallbackEndpoints: []SearchEndpoint{{
Template: fallback.URL + "?q=%s",
AllowedHosts: map[string]struct{}{strings.TrimPrefix(fallback.URL, "https://"): {}},
}},
}
// The allowlist is hostname-only; test servers include an ephemeral port.
connector.AllowedHosts = map[string]struct{}{strings.Split(strings.TrimPrefix(primary.URL, "https://"), ":")[0]: {}}
connector.FallbackEndpoints[0].AllowedHosts = map[string]struct{}{strings.Split(strings.TrimPrefix(fallback.URL, "https://"), ":")[0]: {}}
results, err := connector.Collect(context.Background(), request, 1)
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || !strings.Contains(results[0].Statement, "18") {
t.Fatalf("fallback evidence = %#v", results)
}
if results[0].Source.Locator != "https://example.test" || results[0].Source.Publisher != "example.test" {
t.Fatalf("search result source = %#v", results[0].Source)
}
}
func TestEnrichedSearchQueryAddsPriceSignalsOnlyToPriceRequests(t *testing.T) {
if got := enrichedSearchQuery("проверь цену на видеокарту 4090"); got != "проверь цену на видеокарту 4090 цена купить сегодня" {
t.Fatalf("price query = %q", got)
}
if got := enrichedSearchQuery("какая погода в Казани"); got != "какая погода в Казани" {
t.Fatalf("non-price query = %q", got)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -32,7 +32,7 @@ The system already has the correct outbound mutation foundation: Commit owns aut
## Non-goals
- No Gmail, MCP, Codex Bridge, browser automation, mobile app, voice, or self-extension loop.
- No Gmail, MCP, Codex Bridge, mobile app, voice, or self-extension loop. Browser automation is allowed only under ADR-0019 as bounded read-only observation; it is not a human-surface command or mutation channel.
- No new canonical owner.
- No generic scheduler/event-bus/autonomous-agent loop.
- No new third-party dependency.
@@ -30,7 +30,7 @@ Examples include: a reminder after notification, waiting work requiring a decisi
## Non-goals
- No Codex Bridge, Gmail, MCP, voice, mobile, browser automation, self-extension loop, or generalized orchestration.
- No Codex Bridge, Gmail, MCP, voice, mobile, self-extension loop, or generalized orchestration. Browser observation may be used only through the bounded read-only capability defined by ADR-0019 and cannot continue or mutate a response target.
- No universal object ontology.
- No reminder-specific parallel subsystem.
@@ -0,0 +1,34 @@
# ADR-0019 — Bounded Site-Browser Observation
Status: Accepted
Task: DEV-000293
## Context
Search snippets are insufficient for dynamic pages such as marketplaces: they may expose a category page while hiding the current product price behind client-side rendering. Domovoy needs a replaceable observation capability that can inspect an allowlisted rendered page without becoming an account agent, universal scraper, or external mutation path.
## Decision
1. Browser automation is allowed only as a server-owned, read-only observation capability behind the existing Capability and research adapter boundaries.
2. The browser runtime may navigate only to an explicit HTTPS host allowlist and bounded navigation policy. Browser-supplied or model-supplied authority, credentials, proxy settings, cookies, headers, download paths, or arbitrary JavaScript are rejected.
3. The capability may load public pages and wait for bounded client-side rendering. It may read visible text and explicitly declared structured-data selectors, then return an Epistemics Observation with source URL, observed time, coverage, and failure state.
4. Login, cookies, account state, CAPTCHA solving, checkout, form submission, uploads, downloads, purchases, messages, mutations, and external side effects are out of scope.
5. A blocked page, challenge, timeout, partial DOM, missing price, or conflicting value remains `UNKNOWN`, `PARTIAL`, or `NOT_OBSERVED`; it never becomes a verified fact by model interpretation alone.
6. Browser observation is optional. Search-only routing remains the fallback when the capability is unavailable, degraded, or not allowlisted for the requested host.
7. Capability owns availability, host policy, health and limits. Epistemics owns the resulting observation and provenance. No new canonical owner or database schema is introduced.
8. The first implementation may be development-only. Production activation requires independent capability health, allowlist evidence, adversarial navigation tests, and a live read-only smoke test.
## Boundaries
- Core/domain code does not import a browser or provider runtime.
- The browser adapter performs only bounded GET/navigation observation and cannot reach Commit/Executor.
- Secrets remain outside the browser runtime and its artifacts, logs, screenshots, page content and chat context.
- An observation is evidence for answer synthesis, not an instruction to mutate a site or canonical state.
## Consequences
- Dynamic public pages can provide stronger evidence than search snippets when the site permits it.
- Site-specific selectors and host policy are replaceable adapter configuration, not semantic domain logic.
- Browser unavailability is a capability gap, not a runtime-wide outage.
- The old blanket prohibition on browser automation is replaced by this narrower, testable boundary.
@@ -0,0 +1,45 @@
# ADR-0020 — Architecture Generation 2 governance adoption
**Status:** Accepted by `ARCH-GEN2-000`
**Date:** 2026-08-29
**Scope:** Architecture and governance only; no runtime or schema migration.
## Decision
Assistant adopts the documents under `docs/DOMOVOY_CONSTITUTION.md`,
`architecture/GEN2_TARGET_ARCHITECTURE.md`,
`architecture/GEN2_CANONICAL_DATA_MODEL.md`,
`development/GEN2_MIGRATION_MAP.md`,
`development/GEN2_MASTER_PLAN.md`, and the execution contract as the
canonical Architecture Generation 2 governance basis.
The existing V1 implementation remains the rollback and compatibility
baseline. This decision authorizes subsequent, separately accepted Gen2
packets to introduce the World, Configuration, and SELF bounded owners and
Gen2 migrations, subject to explicit ownership, schema, contract, and
rollback decisions in those packets. It does not create those owners or
change canonical runtime state in this task.
## Authority and execution order
The Gen2 constitution and accepted architecture decisions govern the Gen2
program. The target architecture, canonical data model, migration map,
master plan, execution contract, task index, and individual task packet are
interpreted in that order. A lower-level packet cannot silently weaken a
higher-level invariant or reorder the task index.
Every Gen2 task starts from `gen2-integration`, uses the active task pointer,
and passes the declared FAST/MILESTONE/FULL gate before merge. `ARCH-GEN2-000`
is the architecture adoption root accepted against the PRE baseline;
`GEN2-001` and later tasks are not accepted by this decision and require
separate execution.
## Invariants preserved
- No new canonical owner, cross-module dependency, privilege, database role,
third-party dependency, runtime writer, event, or migration is introduced.
- Existing Authority → Commit → Executor boundaries remain mandatory.
- Memory remains a rebuildable retrieval fabric, World is not a generic truth
owner, and SELF is not a monolithic orchestrator.
- The `domovoy-pre-gen2` baseline tag remains immutable and is the rollback
point until a later migration task explicitly establishes another one.
+53 -1
View File
@@ -1,7 +1,7 @@
{
"registry_version": 1,
"semantic_generation": 1,
"architecture_generation": 1,
"architecture_generation": 2,
"modules": {
"identity": {
"id": "mod.identity",
@@ -179,6 +179,30 @@
"database_schema": "interaction",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.identity", "contracts.situation", "contracts.epistemics", "contracts.cognitive", "platform.ids", "platform.events"]
},
"world": {
"id": "mod.world",
"owns": ["WorldObject", "WorldObjectKindDefinition", "WorldAlias", "FacetDefinition", "RelationDefinition", "IdentityResolutionCandidate", "IdentityResolutionDecision", "WorldLineage"],
"database_schema": "world",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.identity", "platform.ids", "platform.events"],
"forbidden": ["adapters.models", "adapters.temporal", "claims", "beliefs", "observations"]
},
"configuration": {
"id": "mod.configuration",
"owns": ["SettingDefinition", "SettingAssignment", "ConfigurationRevision", "ConfigurationChangeProvenance"],
"database_schema": "configuration",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.identity", "contracts.authority", "platform.ids", "platform.events", "platform.secrets"],
"forbidden": ["secret_values", "arbitrary_key_value_without_definition"]
},
"self": {
"id": "mod.self",
"owns": ["CompetenceAssessment", "WeaknessPattern", "DependencyAssessment", "LearningLedgerEntry", "ImprovementNeed", "SelfVersionView", "SelfEvaluationInterpretation"],
"database_schema": "self",
"boundary_class": "remote_capable",
"may_depend_on": ["contracts.capability", "contracts.epistemics", "contracts.artifact", "platform.ids", "platform.events"],
"forbidden": ["adapters.models", "tasks", "authority_grants", "code_release_switching"]
}
},
"platform": {
@@ -244,6 +268,26 @@
"cross_owner_write": "forbidden",
"cross_owner_application_join": "forbidden"
},
"runtime_topology_v2": {
"core_runtime": {
"modules": ["identity", "situation", "work", "epistemics", "capability", "artifact", "interaction", "world", "configuration", "self"]
},
"continuity_runtime": {"modules": ["continuity"]},
"commit_runtime": {"modules": ["authority", "commit"]},
"cognitive_workers": {"modules": [], "role": "replaceable reasoning/context workers; no canonical ownership"},
"supervisor_runtime": {"modules": [], "role": "release health, switch, rollback and recovery; not a semantic owner"}
},
"storage_topology_v2": {
"postgres_cluster": "assistant_primary",
"schemas": {
"identity": "identity_role", "authority": "authority_role", "situation": "situation_role", "work": "work_role",
"epistemics": "epistemics_role", "continuity": "continuity_role", "capability": "capability_role", "commit": "commit_role",
"artifact": "artifact_role", "interaction": "interaction_role", "world": "world_role", "configuration": "configuration_role", "self": "self_role"
},
"cross_owner_fk": "forbidden_by_default",
"cross_owner_write": "forbidden",
"cross_owner_application_join": "forbidden"
},
"invariants": [
{
"id": "INV-AUTH-001",
@@ -309,6 +353,14 @@
".gitea/workflows/ci.yml",
"cmd/devctl/**",
"dev/ACTIVE_TASK.json",
"docs/DOMOVOY_CONSTITUTION.md",
"docs/DOMOVOY_GEN2_GAP_MAP.md",
"architecture/GEN2_TARGET_ARCHITECTURE.md",
"architecture/GEN2_CANONICAL_DATA_MODEL.md",
"development/GEN2_MIGRATION_MAP.md",
"development/GEN2_MASTER_PLAN.md",
"development/GEN2_EXECUTION_CONTRACT.md",
"development/GEN2_TASK_INDEX.md",
"security/**"
],
"default_feature_task_authority": {
+67 -1
View File
@@ -1,6 +1,6 @@
registry_version: 1
semantic_generation: 1
architecture_generation: 1
architecture_generation: 2
modules:
identity:
@@ -75,6 +75,30 @@ modules:
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.situation, contracts.epistemics, contracts.cognitive, platform.ids, platform.events]
world:
id: mod.world
owns: [WorldObject, WorldObjectKindDefinition, WorldAlias, FacetDefinition, RelationDefinition, IdentityResolutionCandidate, IdentityResolutionDecision, WorldLineage]
database_schema: world
boundary_class: remote_capable
may_depend_on: [contracts.identity, platform.ids, platform.events]
forbidden: [adapters.models, adapters.temporal, claims, beliefs, observations]
configuration:
id: mod.configuration
owns: [SettingDefinition, SettingAssignment, ConfigurationRevision, ConfigurationChangeProvenance]
database_schema: configuration
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, platform.ids, platform.events, platform.secrets]
forbidden: [secret_values, arbitrary_key_value_without_definition]
self:
id: mod.self
owns: [CompetenceAssessment, WeaknessPattern, DependencyAssessment, LearningLedgerEntry, ImprovementNeed, SelfVersionView, SelfEvaluationInterpretation]
database_schema: self
boundary_class: remote_capable
may_depend_on: [contracts.capability, contracts.epistemics, contracts.artifact, platform.ids, platform.events]
forbidden: [adapters.models, tasks, authority_grants, code_release_switching]
platform:
allowed_packages:
- platform.ids
@@ -98,6 +122,20 @@ runtime_topology_v1:
modules: []
role: "replaceable cognition/context compilation workers; no canonical ownership"
runtime_topology_v2:
core_runtime:
modules: [identity, situation, work, epistemics, capability, artifact, interaction, world, configuration, self]
continuity_runtime:
modules: [continuity]
commit_runtime:
modules: [authority, commit]
cognitive_workers:
modules: []
role: "replaceable reasoning/context workers; no canonical ownership"
supervisor_runtime:
modules: []
role: "release health, switch, rollback and recovery; not a semantic owner"
storage_topology_v1:
postgres_cluster: assistant_primary
schemas:
@@ -115,6 +153,26 @@ storage_topology_v1:
cross_owner_write: forbidden
cross_owner_application_join: forbidden
storage_topology_v2:
postgres_cluster: assistant_primary
schemas:
identity: identity_role
authority: authority_role
situation: situation_role
work: work_role
epistemics: epistemics_role
continuity: continuity_role
capability: capability_role
commit: commit_role
artifact: artifact_role
interaction: interaction_role
world: world_role
configuration: configuration_role
self: self_role
cross_owner_fk: forbidden_by_default
cross_owner_write: forbidden
cross_owner_application_join: forbidden
invariants:
- id: INV-AUTH-001
property: "No external mutation without current valid authority and target/revision checks"
@@ -149,6 +207,14 @@ protected_paths:
- .gitea/workflows/ci.yml
- cmd/devctl/**
- dev/ACTIVE_TASK.json
- docs/DOMOVOY_CONSTITUTION.md
- docs/DOMOVOY_GEN2_GAP_MAP.md
- architecture/GEN2_TARGET_ARCHITECTURE.md
- architecture/GEN2_CANONICAL_DATA_MODEL.md
- development/GEN2_MIGRATION_MAP.md
- development/GEN2_MASTER_PLAN.md
- development/GEN2_EXECUTION_CONTRACT.md
- development/GEN2_TASK_INDEX.md
- security/**
default_feature_task_authority:
+43 -28
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"strings"
"sync"
"time"
"git.cin.su/m/Assistant/contracts/capability"
@@ -31,35 +32,16 @@ type fleetStatusResponse struct {
func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier, endpoints []fleetEndpoint) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
members := make([]capability.FleetMember, 0, len(endpoints))
for _, endpoint := range endpoints {
member := capability.FleetMember{ProviderInstanceID: capability.ProviderInstanceID(endpoint.ProviderID), ModelDescriptorID: capability.ModelDescriptorID(endpoint.ModelID), Tier: endpoint.Tier, Reason: "NOT_CONFIGURED"}
if strings.TrimSpace(endpoint.URL) == "" {
members = append(members, member)
continue
}
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, endpoint.URL, nil)
if err == nil {
for name, value := range endpoint.Headers {
request.Header.Set(name, value)
}
probeClient := endpoint.Client
if probeClient == nil {
probeClient = client
}
response, requestErr := probeClient.Do(request)
if requestErr == nil {
response.Body.Close()
if response.StatusCode >= 200 && response.StatusCode < 300 {
member.Ready, member.Reason = true, "AVAILABLE"
} else {
member.Reason = fmt.Sprintf("status=%s", response.Status)
}
} else {
member.Reason = requestErr.Error()
}
}
members = append(members, member)
members = make([]capability.FleetMember, len(endpoints))
var waitGroup sync.WaitGroup
for index, endpoint := range endpoints {
waitGroup.Add(1)
go func(index int, endpoint fleetEndpoint) {
defer waitGroup.Done()
members[index] = probeFleetEndpoint(r.Context(), client, endpoint)
}(index, endpoint)
}
waitGroup.Wait()
readiness, err := fleetdomain.BuildFleetReadiness(time.Now().UnixMilli(), members)
response := fleetStatusResponse{Readiness: readiness, Preferred: preferred}
if err == nil {
@@ -76,4 +58,37 @@ func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier,
})
}
func probeFleetEndpoint(parent context.Context, fallbackClient *http.Client, endpoint fleetEndpoint) capability.FleetMember {
member := capability.FleetMember{ProviderInstanceID: capability.ProviderInstanceID(endpoint.ProviderID), ModelDescriptorID: capability.ModelDescriptorID(endpoint.ModelID), Tier: endpoint.Tier, Reason: "NOT_CONFIGURED"}
if strings.TrimSpace(endpoint.URL) == "" {
return member
}
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.URL, nil)
if err != nil {
member.Reason = err.Error()
return member
}
for name, value := range endpoint.Headers {
request.Header.Set(name, value)
}
probeClient := endpoint.Client
if probeClient == nil {
probeClient = fallbackClient
}
response, requestErr := probeClient.Do(request)
if requestErr != nil {
member.Reason = requestErr.Error()
return member
}
defer response.Body.Close()
if response.StatusCode >= 200 && response.StatusCode < 300 {
member.Ready, member.Reason = true, "AVAILABLE"
} else {
member.Reason = fmt.Sprintf("status=%s", response.Status)
}
return member
}
func fleetContext(ctx context.Context) context.Context { return ctx }
+24
View File
@@ -5,6 +5,7 @@ import (
"net/http/httptest"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
)
@@ -26,6 +27,29 @@ func TestFleetReadinessSelectsBestAvailablePrimaryTierBeforeLifeboat(t *testing.
}
}
func TestFleetReadinessDoesNotWaitForSlowMembersSerially(t *testing.T) {
slow := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(150 * time.Millisecond)
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer slow.Close()
fast := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
defer fast.Close()
handler := fleetReadinessHandler(fast.Client(), capability.FleetTierFast, []fleetEndpoint{
{ProviderID: "slow", ModelID: "slow-model", Tier: capability.FleetTierMedium, URL: slow.URL},
{ProviderID: "lifeboat", ModelID: "lifeboat-model", Tier: capability.FleetTierLifeboat, URL: fast.URL},
})
started := time.Now()
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil))
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"LIFEBOAT"`) {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
if elapsed := time.Since(started); elapsed >= 300*time.Millisecond {
t.Fatalf("fleet readiness was serialized: elapsed=%s", elapsed)
}
}
func TestFleetReadinessSendsConfiguredProviderHeaders(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer fleet-key" {
+41 -7
View File
@@ -186,27 +186,61 @@ func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string)
}
func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse {
text := "Я рядом и продолжаю работу во встроенном режиме Домового. Внешний когнитивный маршрут сейчас недоступен, поэтому опираюсь на уже установленный смысл сообщения и локальные возможности."
if request.Meaning.SpeechAct == "RESPONSE_REVIEW" {
const marker = "Черновик ответа:"
if index := strings.LastIndex(request.Message, marker); index >= 0 {
if draft := strings.TrimSpace(request.Message[index+len(marker):]); draft != "" {
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: draft, FinishReason: cognitive.FinishCompleted}
}
}
}
text := "Понял."
identityText := request.Meaning.RequestedOutcome
if identityText == "" {
identityText = request.Message
}
if request.Meaning.SpeechAct == "QUESTION" && isIdentityInquiry(identityText) {
text = "Я — Домовой, ваш помощник."
}
switch request.Meaning.InformationRequestKind {
case interaction.InformationRequestCognitiveModelInventory:
text = "Запрос понят как проверка моделей. Встроенный режим Домового доступен; я могу показать подключённые модели по назначению, качеству, скорости и стоимости. Ключи и пароли в разговор не нужны."
text = "Сейчас не смог проверить подключённые модели."
case interaction.InformationRequestCapabilityInventory:
text = "Запрос понят как проверка возможностей. Я могу объяснить доступные capabilities и отличить то, что уже подключено, от того, что пока является только планом или capability gap."
text = "Сейчас не смог проверить доступные возможности."
case interaction.InformationRequestCognitiveChannelStatus:
text = "Запрос понят как проверка когнитивного канала. Локальный режим остаётся доступен; я объясню наблюдаемое состояние и что можно сделать без ожидания внешней модели."
text = "Локальный канал Домового доступен; подробности состояния дам по запросу."
case interaction.InformationRequestCognitiveRecovery:
text = "Запрос понят как восстановление ИИ. Начинаю с локального маршрута: фиксирую, что недоступно, проверяю безопасные варианты восстановления и сформулирую следующий обратимый шаг."
text = "Встроенный канал сейчас недоступен."
default:
switch request.Meaning.SpeechAct {
case "REQUEST_TO_INVESTIGATE":
text = "Я принял это как исследование. Даже без внешней модели могу сформулировать гипотезы, отделить известное от неизвестного и предложить следующий проверяемый шаг."
text = "Понял задачу, но пока не смог получить проверяемый результат."
case "QUESTION":
text = "Я отвечаю во встроенном режиме Домового. Если для полного ответа не хватает внешней модели, объясню это прямо и продолжу с тем, что можно проверить локально."
if !isIdentityInquiry(identityText) {
text = "Пока не смог получить проверяемый ответ."
}
case "CONVERSATIONAL":
text = "Я — Домовой, ваш помощник."
}
}
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted}
}
func isIdentityInquiry(message string) bool {
tokens := strings.Fields(strings.ToLower(strings.NewReplacer("?", " ", "!", " ", ",", " ").Replace(message)))
hasIdentitySubject := false
hasAssistantReference := false
for _, token := range tokens {
switch token {
case "кто", "зовут", "имя":
hasIdentitySubject = true
case "ты", "тебя", "тебе", "вы", "вас":
hasAssistantReference = true
}
}
return hasIdentitySubject && hasAssistantReference
}
func (g cognitiveGateway) invokeBuiltinAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) cognitive.GatewayAdvisoryResponse {
if g.builtinLocal != nil && g.builtinLocal.Provider != nil {
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
+24 -1
View File
@@ -185,11 +185,34 @@ func TestNoActiveBindingUsesBuiltInProviderWithoutInvocation(t *testing.T) {
func TestBuiltInAdvisoryUsesTrustedInformationIntentInsteadOfMessageKeywords(t *testing.T) {
request := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", Message: "arbitrary text", Meaning: cognitive.TaskMeaning{InformationRequestKind: interaction.InformationRequestCognitiveRecovery}}
response := builtinAdvisory(request)
if !strings.Contains(response.AdvisoryText, "восстановление ИИ") {
if response.AdvisoryText != "Встроенный канал сейчас недоступен." {
t.Fatalf("unexpected recovery response: %q", response.AdvisoryText)
}
}
func TestBuiltInAdvisoryIdentityIsNaturalAndDoesNotMentionResponseLength(t *testing.T) {
response := builtinAdvisory(cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: "request-identity",
Meaning: cognitive.TaskMeaning{SpeechAct: "QUESTION", RequestedOutcome: "ты кто?"},
})
if response.AdvisoryText != "Я — Домовой, ваш помощник." {
t.Fatalf("unexpected identity response: %q", response.AdvisoryText)
}
if strings.Contains(response.AdvisoryText, "коротко") || strings.Contains(response.AdvisoryText, "подробност") {
t.Fatalf("identity response contains style meta-text: %q", response.AdvisoryText)
}
messageResponse := builtinAdvisory(cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: "request-identity-message",
Message: "как тебя зовут?",
Meaning: cognitive.TaskMeaning{SpeechAct: "QUESTION"},
})
if messageResponse.AdvisoryText != "Я — Домовой, ваш помощник." {
t.Fatalf("unexpected message identity response: %q", messageResponse.AdvisoryText)
}
}
func TestNoActiveBindingInvokesBuiltInLocalProvider(t *testing.T) {
registry := &registryStub{err: errors.New("no active binding")}
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "локальный ответ", FinishReason: cognitive.FinishCompleted}}
+8 -2
View File
@@ -67,12 +67,18 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R
}
g.observe(r.Context(), selected, response, started)
if response.FinishReason != cognitive.FinishCompleted {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: response.FinishReason, Usage: response.Usage})
// A selected provider can become unavailable after discovery. Semantic
// routing must still retain the deterministic interpretation and use the
// local lifeboat rather than turn an ordinary conversation into a
// degraded reply solely because one provider failed.
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
return
}
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
if !ok {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage})
// Structured interpretation is a transport contract. A malformed model
// answer is not a reason to discard the available local fallback.
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
return
}
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage})
@@ -1,9 +1,15 @@
package main
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
@@ -24,3 +30,32 @@ func TestBuiltinInterpretationTimeoutRetainsDeterministicCandidate(t *testing.T)
t.Fatalf("candidate did not retain deterministic meaning: %+v", response.Candidate)
}
}
func TestInterpretationProviderFailureUsesLocalLifeboat(t *testing.T) {
selected := gatewaySelection()
selected.Binding.DataPolicy = capability.DataPolicyLocalOnly
primary := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{FinishReason: cognitive.FinishProviderUnavailable}}
lifeboat := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{AdvisoryText: `{"primary_speech_act":"QUESTION","secondary_speech_act":"","confidence_millis":900,"applicability":"APPLICABLE","requested_outcome":"","needs_cognition":true,"clarification":"NOT_REQUIRED","selection_reason":"local lifeboat","capability_hints":["COGNITIVE_REASONING"]}`, FinishReason: cognitive.FinishCompleted}}
gateway := cognitiveGateway{
resolver: &registryStub{selected: selected},
providers: map[capability.ProviderKind][]providerRegistration{
selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, Provider: primary}},
},
builtinLocal: &providerRegistration{Provider: lifeboat},
now: time.Now,
}
request := cognitive.GatewayInterpretationRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", Message: "сколько фигур в шахматах?", Locale: "ru", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, DeterministicSpeechAct: "QUESTION", DeterministicConfidence: 800, DeterministicApplicability: cognitive.InterpretationApplicable}
payload, _ := json.Marshal(request)
recorder := httptest.NewRecorder()
gateway.interpretationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/interpretations", bytes.NewReader(payload)))
if recorder.Code != http.StatusOK || !primary.called || !lifeboat.called {
t.Fatalf("status=%d primary=%t lifeboat=%t body=%s", recorder.Code, primary.called, lifeboat.called, recorder.Body.String())
}
var response cognitive.GatewayInterpretationResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
if response.FinishReason != cognitive.FinishCompleted || response.Candidate.SelectionReason != "local lifeboat" {
t.Fatalf("response=%+v", response)
}
}
+26 -3
View File
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/adapters/multimodal"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
@@ -126,7 +127,9 @@ func main() {
}
cpuEndpoint := strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/") + "/models"
cpuHealthProbe := health.OptionalHTTPProbeWithHeaders(cpuReadinessClient, cpuEndpoint, cpuFleetHeaders)
cpuHealthProbe.Timeout = 10 * time.Second
// Opportunistic providers must not hide a ready local lifeboat. Keep their
// readiness budget short; their detailed fleet state is reported separately.
cpuHealthProbe.Timeout = 2 * time.Second
mediumEndpoint := ""
mediumHeaders := map[string]string{}
mediumHealthProbe := health.Probe{}
@@ -136,7 +139,7 @@ func main() {
mediumHeaders["Authorization"] = "Bearer " + mediumAPIKey
}
mediumHealthProbe = health.OptionalHTTPProbeWithHeaders(mediumReadinessClient, mediumEndpoint, mediumHeaders)
mediumHealthProbe.Timeout = 10 * time.Second
mediumHealthProbe.Timeout = 2 * time.Second
}
readinessProbes := []health.Probe{
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
@@ -149,7 +152,7 @@ func main() {
if gpuConfigured {
gpuHeaders := map[string]string{"Authorization": "Bearer " + gpuKey}
gpuProbe := health.OptionalHTTPProbeWithHeaders(readinessClient, strings.TrimRight(gpuURL, "/")+"/models", gpuHeaders)
gpuProbe.Timeout = 10 * time.Second
gpuProbe.Timeout = 2 * time.Second
readinessProbes = append(readinessProbes, gpuProbe)
}
@@ -168,6 +171,26 @@ func main() {
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
mux.HandleFunc("POST /v1/interpretations", gateway.interpretationHandler)
mux.HandleFunc("POST /v1/task-evaluations/execute", gateway.taskEvaluationHandler)
imageGenerator := multimodal.ImageGenerator(multimodal.ImageGenerationClient{BaseURL: os.Getenv("LOCAL_IMAGE_PROVIDER_URL"), Client: &http.Client{Timeout: 300 * time.Second}})
if strings.EqualFold(strings.TrimSpace(os.Getenv("LOCAL_IMAGE_PROVIDER_KIND")), "comfyui") {
imageGenerator = multimodal.ComfyImageGenerationClient{BaseURL: os.Getenv("LOCAL_IMAGE_PROVIDER_URL"), Client: &http.Client{Timeout: 300 * time.Second}, PollTimeout: 15 * time.Minute}
}
multimodalGateway := multimodalGateway{
speech: multimodal.SpeechClient{BaseURL: envOr("LOCAL_SPEECH_PROVIDER_URL", "http://llm-local-speech:8080"), Client: &http.Client{Timeout: 120 * time.Second}},
vision: multimodal.VisionClient{BaseURL: envOr("LOCAL_VISION_PROVIDER_URL", "http://llm-local-vision:8080/v1"), Client: &http.Client{Timeout: 180 * time.Second}},
imageGenerator: imageGenerator,
imageProviderURL: os.Getenv("LOCAL_IMAGE_PROVIDER_URL"),
video: multimodal.VideoUnderstandingClient{BaseURL: os.Getenv("LOCAL_VIDEO_PROVIDER_URL"), Client: &http.Client{Timeout: 300 * time.Second}},
model: envOr("LOCAL_VISION_MODEL", "/models/gemma-4-26B-A4B-it-UD-Q4_K_M.gguf"),
imageModel: os.Getenv("LOCAL_IMAGE_MODEL"),
videoModel: os.Getenv("LOCAL_VIDEO_MODEL"),
now: time.Now,
}
mux.HandleFunc("POST /v1/multimodal/transcribe", multimodalGateway.handler(capability.MultimodalTranscribe))
mux.HandleFunc("POST /v1/multimodal/understand", multimodalGateway.handler(capability.MultimodalUnderstand))
mux.HandleFunc("POST /v1/multimodal/ocr", multimodalGateway.handler(capability.MultimodalRecognizeOCR))
mux.HandleFunc("POST /v1/multimodal/video-understand", multimodalGateway.handler(capability.MultimodalUnderstandVideo))
mux.HandleFunc("POST /v1/multimodal/generate-image", multimodalGateway.handler(capability.MultimodalGenerateImage))
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
mux.HandleFunc("POST /v1/provider-discovery/{provider}", discovery.handler)
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(discoveries, string(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), httpQualificationObserver{resolver: resolver}))
+138
View File
@@ -0,0 +1,138 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/multimodal"
"git.cin.su/m/Assistant/contracts/capability"
)
const maxMultimodalUploadBytes = 12 << 20
type multimodalGateway struct {
speech multimodal.SpeechClient
vision multimodal.VisionClient
imageGenerator multimodal.ImageGenerator
imageProviderURL string
video multimodal.VideoUnderstandingClient
model string
imageModel string
videoModel string
now func() time.Time
}
func (g multimodalGateway) handler(operation capability.MultimodalOperation) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxMultimodalUploadBytes)
if err := r.ParseMultipartForm(maxMultimodalUploadBytes); err != nil {
http.Error(w, "media upload is too large or invalid", http.StatusRequestEntityTooLarge)
return
}
if operation == capability.MultimodalGenerateImage {
if strings.TrimSpace(g.imageProviderURL) == "" {
http.Error(w, "local image generation capability unavailable", http.StatusBadGateway)
return
}
prompt := r.FormValue("prompt")
image, err := g.imageGenerator.Generate(r.Context(), g.imageModel, prompt, imageRequestSize(prompt, r.FormValue("size")))
if err != nil || strings.TrimSpace(image.DataBase64) == "" {
logMultimodalFailure(operation, err)
http.Error(w, "local image generation capability unavailable", http.StatusBadGateway)
return
}
result := capability.MultimodalResponse{Operation: operation, Provider: "local-image-generator", ObservedAt: g.now().UnixMilli(), ContentType: image.ContentType, DataBase64: image.DataBase64}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "media file is required", http.StatusBadRequest)
return
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, maxMultimodalUploadBytes+1))
if err != nil || len(data) == 0 || len(data) > maxMultimodalUploadBytes {
http.Error(w, "media file is invalid", http.StatusBadRequest)
return
}
contentType := header.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
var text string
if operation == capability.MultimodalTranscribe {
text, err = g.speech.Transcribe(r.Context(), header.Filename, contentType, data)
} else if operation == capability.MultimodalUnderstandVideo {
if strings.TrimSpace(g.video.BaseURL) == "" {
http.Error(w, "local video understanding capability unavailable", http.StatusBadGateway)
return
}
text, err = g.video.Describe(r.Context(), g.videoModel, header.Filename, contentType, data, r.FormValue("instruction"))
} else {
instruction := "Опиши, что изображено на фотографии, коротко и по существу. Не выдумывай нечитаемые детали."
if operation == capability.MultimodalRecognizeOCR {
instruction = "Точно распознай весь читаемый текст на изображении. Сохрани строки и символы; нечитаемые места обозначь как [неразборчиво]. Не добавляй пояснений."
}
text, err = g.vision.Describe(r.Context(), g.model, contentType, data, instruction)
}
if err != nil {
logMultimodalFailure(operation, err)
http.Error(w, "local media capability unavailable", http.StatusBadGateway)
return
}
result := capability.MultimodalResponse{Operation: operation, Text: strings.TrimSpace(text), ObservedAt: g.now().UnixMilli(), ConfidenceNote: "Результат получен локальным специализированным сервисом; нечитаемые или неоднозначные фрагменты требуют проверки."}
if operation == capability.MultimodalTranscribe {
result.Provider = "local-whisper"
} else {
result.Provider = "local-vision"
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
}
}
// imageRequestSize is a transport-level normalization for the local image
// capability. An explicit, valid size always wins. When the caller only
// expresses a conventional output-quality target, the capability translates it
// to a supported provider size without exposing provider workflow details to
// the interaction core. This policy is deliberately local and replaceable
// with the image capability implementation.
func imageRequestSize(prompt, requested string) string {
if isImageSize(requested) {
return strings.TrimSpace(requested)
}
words := strings.Fields(strings.ToLower(prompt))
for index, word := range words {
switch word {
case "4k", "uhd":
return "2048x2048"
case "hd":
return "1024x1024"
case "ultra":
if index+1 < len(words) && words[index+1] == "hd" {
return "2048x2048"
}
}
}
return ""
}
func isImageSize(value string) bool {
var width, height int
if _, err := fmt.Sscanf(strings.TrimSpace(value), "%dx%d", &width, &height); err != nil {
return false
}
return width >= 256 && height >= 256 && width <= 2048 && height <= 2048
}
func logMultimodalFailure(operation capability.MultimodalOperation, err error) {
// Keep provider details in server diagnostics; never return them to chat.
log.Printf("multimodal operation %s failed: %v", operation, err)
}
+96
View File
@@ -0,0 +1,96 @@
package main
import (
"bytes"
"context"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.cin.su/m/Assistant/adapters/multimodal"
"git.cin.su/m/Assistant/contracts/capability"
)
func TestMultimodalTranscriptionHandlerReturnsLocalResult(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/inference" || r.Method != http.MethodPost {
t.Fatalf("unexpected upstream request: %s %s", r.Method, r.URL.Path)
}
_ = json.NewEncoder(w).Encode(map[string]string{"text": "добрый день"})
}))
defer upstream.Close()
server := multimodalGateway{speech: multimodal.SpeechClient{BaseURL: upstream.URL, Client: upstream.Client()}, now: time.Now}
request := multipartRequest(t, "/v1/multimodal/transcribe", "voice.wav", []byte("audio"))
response := httptest.NewRecorder()
server.handler(capability.MultimodalTranscribe)(response, request)
if response.Code != http.StatusOK || !bytes.Contains(response.Body.Bytes(), []byte("добрый день")) {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
func TestMultimodalImageGenerationFailsClosedWhenSkillIsUnbound(t *testing.T) {
server := multimodalGateway{now: time.Now}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
if err := writer.WriteField("prompt", "green bird"); err != nil || writer.Close() != nil {
t.Fatal("failed to build generation request")
}
request := httptest.NewRequest(http.MethodPost, "/v1/multimodal/generate-image", body)
request.Header.Set("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder()
server.handler(capability.MultimodalGenerateImage)(response, request)
if response.Code != http.StatusBadGateway || !bytes.Contains(response.Body.Bytes(), []byte("unavailable")) {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
func TestImageRequestSizeUsesExplicitSizeOrQualityTarget(t *testing.T) {
for _, test := range []struct {
name string
prompt string
requested string
want string
}{
{name: "explicit", prompt: "нарисуй птицу", requested: "768x512", want: "768x512"},
{name: "hd", prompt: "нарисуй птицу в HD", want: "1024x1024"},
{name: "uhd", prompt: "draw a bird in UHD", want: "2048x2048"},
{name: "ordinary", prompt: "нарисуй птицу", want: ""},
{name: "invalid explicit", prompt: "нарисуй птицу", requested: "100x100", want: ""},
} {
t.Run(test.name, func(t *testing.T) {
if got := imageRequestSize(test.prompt, test.requested); got != test.want {
t.Fatalf("imageRequestSize(%q, %q) = %q, want %q", test.prompt, test.requested, got, test.want)
}
})
}
}
func TestMultimodalVideoUnderstandingFailsClosedWhenSkillIsUnbound(t *testing.T) {
server := multimodalGateway{now: time.Now}
request := multipartRequest(t, "/v1/multimodal/video-understand", "clip.mp4", []byte("video"))
response := httptest.NewRecorder()
server.handler(capability.MultimodalUnderstandVideo)(response, request)
if response.Code != http.StatusBadGateway || !bytes.Contains(response.Body.Bytes(), []byte("unavailable")) {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
func multipartRequest(t *testing.T, path, filename string, data []byte) *http.Request {
t.Helper()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", filename)
if err != nil {
t.Fatal(err)
}
if _, err := part.Write(data); err != nil || writer.Close() != nil {
t.Fatal("failed to build multipart request")
}
request := httptest.NewRequest(http.MethodPost, path, body)
request = request.WithContext(context.Background())
request.Header.Set("Content-Type", writer.FormDataContentType())
return request
}
+82
View File
@@ -0,0 +1,82 @@
package main
import (
"sort"
"git.cin.su/m/Assistant/contracts/capability"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
type chatRouteKind string
const (
chatRouteResearch chatRouteKind = "RESEARCH"
chatRouteConversation chatRouteKind = "CONVERSATION"
chatRouteMedia chatRouteKind = "MEDIA"
chatRouteAction chatRouteKind = "ACTION"
chatRouteAgent chatRouteKind = "AGENT"
)
type chatRoute struct {
Kind chatRouteKind
Capability capability.CapabilityKind
Profile string
Order int
}
// buildChatRoutePlan creates one bounded route per meaning-bearing capability.
// A failed research route must not suppress the local conversational route or
// an unrelated action route.
func buildChatRoutePlan(result interactiondomain.SemanticResult) []chatRoute {
seen := make(map[capability.CapabilityKind]struct{})
routes := make([]chatRoute, 0, len(result.CapabilityHints)+1)
add := func(kind chatRouteKind, capabilityKind capability.CapabilityKind, profile string, order int) {
if capabilityKind != "" {
if _, exists := seen[capabilityKind]; exists {
return
}
seen[capabilityKind] = struct{}{}
}
routes = append(routes, chatRoute{Kind: kind, Capability: capabilityKind, Profile: profile, Order: order})
}
for _, hint := range result.CapabilityHints {
switch capability.CapabilityKind(hint) {
case capability.CapabilityKindWebResearch:
add(chatRouteResearch, capability.CapabilityKindWebResearch, "truth-research-v1", 10)
case capability.CapabilityKindImageGeneration:
add(chatRouteMedia, capability.CapabilityKindImageGeneration, "local-image-generation-v1", 5)
case capability.CapabilityKindCognitiveReasoning, capability.CapabilityKindVerifyFact:
add(chatRouteConversation, capability.CapabilityKind(hint), "local-reasoning-v1", 20)
case capability.CapabilityKindScheduleReminder:
add(chatRouteAction, capability.CapabilityKindScheduleReminder, "continuity-reminder-v1", 30)
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject:
add(chatRouteAgent, capability.CapabilityKind(hint), "software-agent-v1", 40)
case capability.CapabilityKindSendEmail, capability.CapabilityKindReadEmail:
add(chatRouteAgent, capability.CapabilityKind(hint), "communication-agent-v1", 50)
case capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs:
add(chatRouteAgent, capability.CapabilityKind(hint), "diagnostics-agent-v1", 60)
}
}
if result.NeedsCognition || len(routes) == 0 {
add(chatRouteConversation, capability.CapabilityKindCognitiveReasoning, "local-reasoning-v1", 20)
}
sort.SliceStable(routes, func(i, j int) bool { return routes[i].Order < routes[j].Order })
return routes
}
func hasChatRoute(routes []chatRoute, kind chatRouteKind) bool {
for _, route := range routes {
if route.Kind == kind {
return true
}
}
return false
}
// specializedRouteBypassesGeneralInterpretation keeps a concrete capability
// route stable. General cognition may still be used later to assess evidence,
// but it must not replace an executable research or media route with a vague
// conversational clarification before the capability is attempted.
func specializedRouteBypassesGeneralInterpretation(routes []chatRoute) bool {
return hasChatRoute(routes, chatRouteMedia) || hasChatRoute(routes, chatRouteResearch)
}
@@ -0,0 +1,53 @@
package main
import (
"testing"
"git.cin.su/m/Assistant/contracts/capability"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestBuildChatRoutePlanPreservesMultipleMeaningfulRoutes(t *testing.T) {
planned := buildChatRoutePlan(interactiondomain.SemanticResult{
NeedsCognition: true,
CapabilityHints: []string{"WEB_RESEARCH", "MODIFY_CODE", "RUN_TESTS", "SCHEDULE_REMINDER"},
})
if len(planned) != 5 {
t.Fatalf("expected five deduplicated routes, got %+v", planned)
}
want := []struct {
kind chatRouteKind
profile string
cap capability.CapabilityKind
}{
{chatRouteResearch, "truth-research-v1", capability.CapabilityKindWebResearch},
{chatRouteConversation, "local-reasoning-v1", capability.CapabilityKindCognitiveReasoning},
{chatRouteAction, "continuity-reminder-v1", capability.CapabilityKindScheduleReminder},
{chatRouteAgent, "software-agent-v1", capability.CapabilityKindModifyCode},
{chatRouteAgent, "software-agent-v1", capability.CapabilityKindRunTests},
}
for index, expected := range want {
if planned[index].Kind != expected.kind || planned[index].Profile != expected.profile || planned[index].Capability != expected.cap {
t.Fatalf("route %d = %+v, want %+v", index, planned[index], expected)
}
}
}
func TestBuildChatRoutePlanAlwaysKeepsLocalConversationRoute(t *testing.T) {
planned := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: true})
if len(planned) != 1 || planned[0].Kind != chatRouteConversation || planned[0].Profile != "local-reasoning-v1" {
t.Fatalf("unexpected local route: %+v", planned)
}
}
func TestSpecializedRouteBypassesGeneralInterpretation(t *testing.T) {
if !specializedRouteBypassesGeneralInterpretation([]chatRoute{{Kind: chatRouteResearch}}) {
t.Fatal("research route must execute before general cognition")
}
if !specializedRouteBypassesGeneralInterpretation([]chatRoute{{Kind: chatRouteMedia}}) {
t.Fatal("media route must execute before general cognition")
}
if specializedRouteBypassesGeneralInterpretation([]chatRoute{{Kind: chatRouteConversation}}) {
t.Fatal("conversation-only route must retain general cognition")
}
}
+207 -5
View File
@@ -5,8 +5,13 @@ import (
"context"
"encoding/json"
"fmt"
"hash/fnv"
"io"
"mime/multipart"
"net/http"
"net/url"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
@@ -20,6 +25,109 @@ import (
type cognitiveInterpretationClient struct {
baseURL *url.URL
client *http.Client
media *http.Client
}
// ReviewResponse asks the cognitive gateway to turn a locally assembled draft
// into a natural answer. The draft remains the safe fallback when cognition is
// unavailable; the model does not mutate canonical state or execute actions.
func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, session identity.CreatorSession, userMessage, draft, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
requestID, err := ids.New()
if err != nil {
return "", err
}
prompt := "Ты редактор финального ответа Домового. Проверь черновик по смыслу и подготовь естественный ответ человеку. Учитывай весь запрос, включая недостающие параметры для конкретного результата. Если данных не хватает, задай один короткий полезный уточняющий вопрос, например откуда и куда ехать. Не выдавай поисковые сниппеты как выполненное действие и не показывай служебные URL поисковой выдачи. Сохрани проверяемые факты, названия источников, прямые ссылки и ограничения. Не выдумывай факты и не утверждай невыполненные действия. Сначала внутренне проверь, что ответ начинается с содержания, решающего вопрос. Не повторяй вопрос пользователя в качестве заголовка или вступления, если это не нужно для смысла. Представление, имя помощника, приветствие или предложение дополнительной помощи допустимы только если пользователь сам спросил об этом; иначе убери их. Верни только готовый ответ без разбора проверки, служебных слов и упоминания моделей. Если черновик уже адекватен, верни его без лишних добавлений.\n\nСообщение пользователя:\n" + userMessage + "\n\nЧерновик ответа:\n" + draft
context := []cognitive.ContextExcerpt{}
if strings.TrimSpace(personalMemory) != "" {
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
}
target := *c.baseURL
target.Path = "/v1/advisories"
payload, err := json.Marshal(cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
ActorID: session.ActorID, PrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"), Message: prompt,
Context: context, Locale: "ru", MaxOutputBytes: 2048, DataPolicy: capability.DataPolicyLocalOnly,
Meaning: cognitive.TaskMeaning{
SpeechAct: "RESPONSE_REVIEW",
CapabilityHints: meaning.CapabilityHints,
RequestedOutcome: meaning.RequestedOutcome,
InformationRequestKind: meaning.InformationRequestKind,
ConfidenceMillis: meaning.ConfidenceMillis,
},
})
if err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("cognitive response review status %d", response.StatusCode)
}
var result cognitive.GatewayAdvisoryResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return "", err
}
if result.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(result.AdvisoryText) == "" {
return "", fmt.Errorf("cognitive response review incomplete: %s", result.FinishReason)
}
return strings.TrimSpace(result.AdvisoryText), nil
}
// AnswerStableKnowledge asks the local cognitive route for a short answer to a
// non-current question only after evidence retrieval did not produce a usable
// result. The model decides whether its knowledge is stable enough; it must
// explicitly decline changing or source-dependent facts instead of guessing.
func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
requestID, err := ids.New()
if err != nil {
return "", err
}
prompt := "Ты отвечаешь как Домовой на обычное сообщение пользователя. Сначала пойми цель: на вопрос дай короткий естественный ответ по устойчивому общеизвестному знанию, а безопасную просьбу создать текстовый или структурный результат выполни прямо в ответе. Начинай сразу с содержания и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты и не утверждай выполнение внешнего действия.\n\nСообщение пользователя:\n" + userMessage
context := []cognitive.ContextExcerpt{}
if strings.TrimSpace(personalMemory) != "" {
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
}
target := *c.baseURL
target.Path = "/v1/advisories"
payload, err := json.Marshal(cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: prompt, Context: context, Locale: "ru", MaxOutputBytes: 1024, DataPolicy: capability.DataPolicyLocalOnly,
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
})
if err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("cognitive stable knowledge status %d", response.StatusCode)
}
var result cognitive.GatewayAdvisoryResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return "", err
}
if result.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(result.AdvisoryText) == "" {
return "", fmt.Errorf("cognitive stable knowledge incomplete: %s", result.FinishReason)
}
return strings.TrimSpace(result.AdvisoryText), nil
}
func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cognitiveInterpretationClient, error) {
@@ -30,7 +138,7 @@ func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cogni
if client == nil {
client = http.DefaultClient
}
return cognitiveInterpretationClient{baseURL: base, client: client}, nil
return cognitiveInterpretationClient{baseURL: base, client: client, media: &http.Client{Timeout: 15 * time.Minute}}, nil
}
func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request cognitive.GatewayInterpretationRequest) (cognitive.GatewayInterpretationResponse, error) {
@@ -60,13 +168,107 @@ func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request co
return result, nil
}
func (c cognitiveInterpretationClient) GenerateImage(ctx context.Context, prompt, size string) (capability.MultimodalResponse, error) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if err := writer.WriteField("prompt", prompt); err != nil {
return capability.MultimodalResponse{}, err
}
if strings.TrimSpace(size) != "" {
if err := writer.WriteField("size", size); err != nil {
return capability.MultimodalResponse{}, err
}
}
if err := writer.Close(); err != nil {
return capability.MultimodalResponse{}, err
}
target := *c.baseURL
target.Path = "/v1/multimodal/generate-image"
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), &body)
if err != nil {
return capability.MultimodalResponse{}, err
}
request.Header.Set("Content-Type", writer.FormDataContentType())
client := c.media
if client == nil {
client = c.client
}
response, err := client.Do(request)
if err != nil {
return capability.MultimodalResponse{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return capability.MultimodalResponse{}, fmt.Errorf("image generation status %d", response.StatusCode)
}
var result capability.MultimodalResponse
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<20)).Decode(&result); err != nil {
return capability.MultimodalResponse{}, err
}
if strings.TrimSpace(result.DataBase64) == "" {
return capability.MultimodalResponse{}, fmt.Errorf("image generation returned empty payload")
}
return result, nil
}
func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool {
switch result.Interpretation.Applicability {
case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent:
return true
default:
return shouldRequestSemanticInterpretation(result)
}
// shouldRequestSemanticInterpretation keeps the deterministic layer as a
// fast reflex, not as the meaning authority. Questions, requests, directives,
// mixed signals and capability-bearing messages go through cognitive
// interpretation; only stable low-risk conversational/assertive reflexes may
// answer without it.
func shouldRequestSemanticInterpretation(result interactiondomain.SemanticResult) bool {
if result.Interpretation.Clarification == interaction.ClarificationRequired {
return false
}
// Structured, read-only information intents already have a semantic owner
// in core-runtime. Sending them through the slower advisory interpretation
// path only adds latency and can make a healthy runtime look unavailable.
if result.InformationRequestKind != "" {
return false
}
if result.NeedsCognition || len(result.CapabilityHints) > 0 {
return true
}
if result.Interpretation.Applicability != interaction.SemanticApplicable {
return true
}
switch result.Interpretation.SpeechAct {
case interaction.SpeechActConversational, interaction.SpeechActConfirmation, interaction.SpeechActRejection:
return false
case interaction.SpeechActPreference:
// Explicit user preferences are durable interaction context. A sampled
// advisory audit must not reinterpret or erase them.
return false
case interaction.SpeechActAssertion:
return result.Interpretation.ConfidenceMillis < 850
default:
return true
}
}
const (
deterministicReflexAuditInterval = 10 * time.Minute
deterministicReflexAuditSample = uint32(16)
)
// shouldAuditDeterministicReflex samples stable reflexes in a time bucket.
// This is observability, not a correctness gate: a failed audit leaves the
// reflex usable, while a successful disagreement can replace it with the
// cognitive candidate and becomes durable evidence through the normal
// candidate store.
func shouldAuditDeterministicReflex(result interactiondomain.SemanticResult, utteranceID string, now time.Time) bool {
if shouldRequestSemanticInterpretation(result) || utteranceID == "" {
return false
}
bucket := now.Unix() / int64(deterministicReflexAuditInterval/time.Second)
hash := fnv.New32a()
_, _ = hash.Write([]byte(utteranceID))
_, _ = hash.Write([]byte(fmt.Sprintf(":%d", bucket)))
return hash.Sum32()%deterministicReflexAuditSample == 0
}
func isSimpleLocalQuestion(result interactiondomain.SemanticResult) bool {
@@ -1,12 +1,109 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestReviewResponseUsesCognitiveGatewayAndReturnsOnlyDraftAnswer(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode review request: %v", err)
}
if request.Message == "" || request.Meaning.SpeechAct == "" {
t.Fatalf("review request did not carry semantic context: %+v", request)
}
if !strings.Contains(request.Message, "ответ начинается с содержания") || !strings.Contains(request.Message, "Не повторяй вопрос пользователя") || !strings.Contains(request.Message, "только если пользователь сам спросил") {
t.Fatalf("review request lacks the conversational quality contract: %q", request.Message)
}
if len(request.Context) != 1 || !strings.Contains(request.Context[0].Text, "красный цвет") {
t.Fatalf("review request did not carry compact personal memory: %+v", request.Context)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Меня зовут Дед. Я Домовой.", FinishReason: cognitive.FinishCompleted})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
reviewed, err := client.ReviewResponse(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "кто ты?", "Я — Домовой.", "Предпочтение пользователя (его сообщение): красный цвет", cognitive.TaskMeaning{SpeechAct: "QUESTION"})
if err != nil {
t.Fatal(err)
}
if reviewed != "Меня зовут Дед. Я Домовой." {
t.Fatalf("unexpected reviewed response %q", reviewed)
}
}
func TestAnswerStableKnowledgeUsesLocalOnlyCognitiveRoute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode stable knowledge request: %v", err)
}
if request.DataPolicy != capability.DataPolicyLocalOnly || request.Meaning.SpeechAct != "STABLE_KNOWLEDGE_ANSWER" {
t.Fatalf("unexpected stable knowledge request: %+v", request)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Санкт-Петербург — город в России.", FinishReason: cognitive.FinishCompleted})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
answer, err := client.AnswerStableKnowledge(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "Питер — это где?", "", cognitive.TaskMeaning{SpeechAct: "QUESTION"})
if err != nil || answer != "Санкт-Петербург — город в России." {
t.Fatalf("answer=%q err=%v", answer, err)
}
}
func TestGenerateImageUsesDedicatedCognitiveMediaRoute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/multimodal/generate-image" {
t.Fatalf("unexpected image route: %s %s", r.Method, r.URL.Path)
}
if err := r.ParseMultipartForm(1 << 20); err != nil {
t.Fatalf("parse image request: %v", err)
}
if !strings.Contains(r.FormValue("prompt"), "собаку") {
t.Fatalf("prompt was not preserved: %q", r.FormValue("prompt"))
}
_ = json.NewEncoder(w).Encode(capability.MultimodalResponse{Operation: capability.MultimodalGenerateImage, ContentType: "image/png", DataBase64: "cG5n"})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
image, err := client.GenerateImage(context.Background(), "нарисуй собаку", "")
if err != nil || image.DataBase64 != "cG5n" || image.ContentType != "image/png" {
t.Fatalf("image=%+v err=%v", image, err)
}
}
func TestReviewPreservesResearchEvidence(t *testing.T) {
draft := "Нашёл вариант. Источник: Aviasales (https://www.aviasales.ru/search)"
if reviewPreservesResearchEvidence("Нашёл вариант на Авиасейлс.", draft) {
t.Fatal("review without the direct source must be rejected")
}
if !reviewPreservesResearchEvidence("Нашёл вариант. Источник: Aviasales (https://www.aviasales.ru/search)", draft) {
t.Fatal("review with the direct source must be accepted")
}
}
func TestSimpleQuestionUsesStrictInterpretationForSemanticRouting(t *testing.T) {
result := interactiondomain.SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
@@ -34,3 +131,51 @@ func TestOperationalQuestionStillUsesStrictInterpretation(t *testing.T) {
t.Fatal("operational question must retain strict interpretation routing")
}
}
func TestStructuredInventoryUsesDirectReadOnlySemanticRoute(t *testing.T) {
result := interactiondomain.SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActDirective,
Applicability: interaction.SemanticApplicable,
ConfidenceMillis: 980,
},
InformationRequestKind: interaction.InformationRequestCognitiveModelInventory,
}
if shouldRequestSemanticInterpretation(result) {
t.Fatal("structured model inventory must use the direct read-only semantic route")
}
}
func TestStableReflexIsPeriodicallySampledForDrift(t *testing.T) {
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActConversational, Applicability: interaction.SemanticApplicable,
}}
found := false
for minute := 0; minute < 160; minute++ {
if shouldAuditDeterministicReflex(result, "utterance-drift-probe", time.Unix(int64(minute)*60, 0)) {
found = true
break
}
}
if !found {
t.Fatal("stable reflex was never sampled during the audit window")
}
}
func TestMeaningfulSignalsAreNotReflexOnly(t *testing.T) {
for _, act := range []interaction.SpeechAct{interaction.SpeechActQuestion, interaction.SpeechActRequestToInvestigate, interaction.SpeechActDirective} {
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: act, Applicability: interaction.SemanticApplicable}}
if !shouldRequestSemanticInterpretation(result) {
t.Fatalf("%s must prefer cognitive interpretation", act)
}
}
}
func TestExplicitPreferenceIsNotReinterpretedByCognitiveAudit(t *testing.T) {
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActPreference, Applicability: interaction.SemanticApplicable,
}}
if shouldRequestSemanticInterpretation(result) || shouldAuditDeterministicReflex(result, "preference", time.Unix(0, 0)) {
t.Fatal("explicit preference must remain on the deterministic durable-context path")
}
}
+123
View File
@@ -0,0 +1,123 @@
package main
import (
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestHouseholdResearchAndActionRoutesRemainIndependent(t *testing.T) {
candidate := interactiondomain.BuildDeterministicCandidate("проверь погоду в Казани, найди кафе рядом и напомни вечером позвонить", "", time.Unix(10, 0).UnixMilli())
planned := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: candidate.NeedsCognition, CapabilityHints: candidate.CapabilityHints})
want := map[chatRouteKind]bool{chatRouteResearch: false, chatRouteConversation: false, chatRouteAction: false}
for _, route := range planned {
if _, ok := want[route.Kind]; ok {
want[route.Kind] = true
}
}
for kind, present := range want {
if !present {
t.Fatalf("route %s missing for mixed household request: candidate=%+v routes=%+v", kind, candidate, planned)
}
}
}
func TestHouseholdCreativeRouteDoesNotUseResearch(t *testing.T) {
candidate := interactiondomain.BuildDeterministicCandidate("расскажи сказку о Колобке", "", time.Unix(10, 0).UnixMilli())
for _, hint := range candidate.CapabilityHints {
if capability.CapabilityKind(hint) == capability.CapabilityKindWebResearch {
t.Fatalf("creative request selected web research: %+v", candidate)
}
}
if !interactiondomain.IsCreativeRequest("расскажи сказку о Колобке") {
t.Fatal("creative request was not recognized")
}
}
func TestHouseholdMarketplaceBrandSearchUsesResearchRoute(t *testing.T) {
candidate := interactiondomain.BuildDeterministicCandidate("найди на WB бренд marka-wear", "", time.Unix(10, 0).UnixMilli())
decomposition := interactiondomain.DecomposeTaskWithLexicon("найди на WB бренд marka-wear", interactiondomain.TaskLexicon{ObjectAliases: []interactiondomain.TaskLexiconEntry{{Alias: "бренд", Value: "BRAND"}}, SourceAliases: []interactiondomain.TaskLexiconEntry{{Alias: "wb", Value: "WILDBERRIES"}}})
if decomposition.Intent != interactiondomain.TaskIntentSearch || decomposition.Object != "BRAND" || decomposition.Source != "WILDBERRIES" || decomposition.Target != "marka-wear" || len(decomposition.Missing) != 0 {
t.Fatalf("marketplace request was not split into executable atoms: %+v", decomposition)
}
routes := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: candidate.NeedsCognition, CapabilityHints: candidate.CapabilityHints})
if !hasChatRoute(routes, chatRouteResearch) {
t.Fatalf("marketplace brand request did not select research route: candidate=%+v routes=%+v", candidate, routes)
}
}
func TestHouseholdSemanticMemoryControlsMarketplaceAlias(t *testing.T) {
withoutMemory := interactiondomain.DecomposeTask("найди на WB бренд marka-wear")
if withoutMemory.Source != "" || withoutMemory.Object != "" {
t.Fatalf("core invented marketplace meaning without memory: %+v", withoutMemory)
}
withMemory := interactiondomain.DecomposeTaskWithLexicon("поищи марку marka-wear в Wildberries", interactiondomain.TaskLexicon{
ObjectAliases: []interactiondomain.TaskLexiconEntry{{Alias: "марку", Value: "BRAND"}},
SourceAliases: []interactiondomain.TaskLexiconEntry{{Alias: "wildberries", Value: "WILDBERRIES"}},
})
if withMemory.Intent != interactiondomain.TaskIntentSearch || withMemory.Object != "BRAND" || withMemory.Source != "WILDBERRIES" || withMemory.Target != "marka-wear" || !withMemory.NeedsResearch {
t.Fatalf("semantic memory did not control marketplace decomposition: %+v", withMemory)
}
}
func TestHouseholdBearDrawingUsesCreativeLocalRoute(t *testing.T) {
candidate := interactiondomain.BuildDeterministicCandidate("нарисуй бурого медведя", "", time.Unix(10, 0).UnixMilli())
decomposition := interactiondomain.DecomposeTaskWithLexicon("нарисуй бурого медведя", interactiondomain.TaskLexicon{ObjectAliases: []interactiondomain.TaskLexiconEntry{{Alias: "медвед", Value: "IMAGE"}}})
if decomposition.Intent != interactiondomain.TaskIntentCreate || decomposition.Object != "IMAGE" || decomposition.Target != "бурого медведя" || decomposition.NeedsResearch {
t.Fatalf("creative request was not split into local execution atoms: %+v", decomposition)
}
routes := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: candidate.NeedsCognition, CapabilityHints: candidate.CapabilityHints})
if !hasChatRoute(routes, chatRouteMedia) {
t.Fatalf("creative image request did not select media route: candidate=%+v routes=%+v", candidate, routes)
}
for _, hint := range candidate.CapabilityHints {
if capability.CapabilityKind(hint) == capability.CapabilityKindWebResearch {
t.Fatalf("bear drawing selected web research: %+v", candidate)
}
}
for _, message := range []string{"нарисуй бурого медведя", "нарисуй птицу", "нарисуй зеленую птицу", "нарисуй птицу в HD", "нарисуй коня"} {
if got := interactiondomain.CreativeDeterministicReply(message); got != "" {
t.Fatalf("creative request %q received a canned reply=%q", message, got)
}
}
}
func TestHouseholdRouteMatrixKeepsMeaningBoundaries(t *testing.T) {
tests := []struct {
message string
wantResearch bool
wantCreative bool
wantHint string
}{
{message: "найди на WB бренд marka-wear", wantResearch: true},
{message: "какая погода в Казани?", wantResearch: true},
{message: "найди билеты до Сочи", wantResearch: true},
{message: "нарисуй птицу", wantCreative: true},
{message: "расскажи сказку о Колобке", wantCreative: true},
{message: "что такое гравитация?", wantHint: "COGNITIVE_REASONING"},
}
for _, test := range tests {
candidate := interactiondomain.BuildDeterministicCandidate(test.message, "", time.Unix(10, 0).UnixMilli())
if interactiondomain.IsCreativeRequest(test.message) != test.wantCreative {
t.Fatalf("message=%q creative=%v want=%v candidate=%+v", test.message, interactiondomain.IsCreativeRequest(test.message), test.wantCreative, candidate)
}
gotResearch := false
gotHint := false
for _, hint := range candidate.CapabilityHints {
if hint == "WEB_RESEARCH" {
gotResearch = true
}
if hint == test.wantHint {
gotHint = true
}
}
if gotResearch != test.wantResearch {
t.Fatalf("message=%q research=%v want=%v candidate=%+v", test.message, gotResearch, test.wantResearch, candidate)
}
if test.wantHint != "" && !gotHint {
t.Fatalf("message=%q missing hint=%q candidate=%+v", test.message, test.wantHint, candidate)
}
}
}
@@ -0,0 +1,27 @@
package main
import (
"testing"
"git.cin.su/m/Assistant/contracts/interaction"
)
func TestSpecializedInformationReplyIsNotOverwrittenByResearch(t *testing.T) {
for _, kind := range []interaction.InformationRequestKind{
interaction.InformationRequestMemoryInventory,
interaction.InformationRequestCapabilityInventory,
interaction.InformationRequestCognitiveModelInventory,
interaction.InformationRequestCognitiveChannelStatus,
interaction.InformationRequestCognitiveRecovery,
} {
if shouldRunGeneralResearch(kind) {
t.Fatalf("specialized information request %q must bypass general research", kind)
}
}
if !shouldRunGeneralResearch(interaction.InformationRequestTimeDifference) {
t.Fatal("time difference must use the current-data research route")
}
if !shouldRunGeneralResearch("") {
t.Fatal("ordinary research requests must still use general research")
}
}
+809 -57
View File
File diff suppressed because it is too large Load Diff
+246 -3
View File
@@ -11,10 +11,183 @@ import (
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/contracts/work"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestHumanResearchExcerptRemovesSearchResultHeading(t *testing.T) {
got := humanResearchExcerpt("погода в Санкт-Петербурге", "Погода в Санкт-Петербурге — Прогноз погоды — Сейчас пасмурно, температура +14°")
if got != "Сейчас пасмурно, температура +14°" || strings.Contains(got, "Прогноз погоды") {
t.Fatalf("human research excerpt = %q", got)
}
}
func TestNearestFlightEvidenceDoesNotClaimMissingDepartureTime(t *testing.T) {
if !nearestFlightQuery("ближайший рейс Москва Казань сегодня") {
t.Fatal("nearest flight query was not recognized")
}
if containsFlightTimeEvidence("Казань составляет 1 ч 25 мин. График вылетов доступен онлайн") {
t.Fatal("duration was mistaken for a departure time")
}
if !containsFlightTimeEvidence("Ближайший вылет в 18:40") {
t.Fatal("departure time was not recognized")
}
}
func TestNearestFlightReplyFiltersUnrelatedSearchSnippets(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://naidy.example", Title: "Мебельная фурнитура"}, Excerpt: epistemics.SourceExcerpt{Text: "Комплектующие для мебели оптом и в розницу."}},
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://rasp.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание самолётов Москва — Казань, время вылета и прилёта."}},
}}
reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil)
if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Мебельная") {
t.Fatalf("unrelated source leaked into flight reply: %q", reply)
}
}
func TestNearestFlightReplyDoesNotAppendWeakerSecondMatch(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://schedule.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов Москва — Казань, время вылета и прилёта."}},
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://category.example", Title: "Категория направлений"}, Excerpt: epistemics.SourceExcerpt{Text: "Список близлежащих городов и ориентировочные цены."}},
}}
reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil)
if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Категория направлений") {
t.Fatalf("weaker second finding leaked into nearest-flight reply: %q", reply)
}
}
func TestNearestFlightEvidenceRequiresBothRouteEndpointsWhenKnown(t *testing.T) {
query := "ближайшие авиарейсы Москва Казань сегодня"
matched := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов из Москвы в Казань."}}
if !researchFindingMatchesIntent(query, matched) {
t.Fatal("route evidence with both endpoints was rejected")
}
unrelated := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань", Locator: "https://example.test/moscow-kazan"}, Excerpt: epistemics.SourceExcerpt{Text: "Рейсы Москва — Казань — Ближайшие к Казани города; цена указана ориентировочно."}}
if researchFindingMatchesIntent(query, unrelated) {
t.Fatal("one-ended travel evidence was accepted")
}
}
func TestTicketReplyRejectsGenericCategoryPageWithoutDestination(t *testing.T) {
query := "найди билеты до Ялты сегодня"
generic := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Купить билеты", Locator: "https://tickets.example"}, Excerpt: epistemics.SourceExcerpt{Text: "Поиск авиабилетов среди множества направлений."}}
if researchFindingMatchesIntent(query, generic) {
t.Fatal("generic ticket category was presented as destination result")
}
relevant := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Билеты"}, Excerpt: epistemics.SourceExcerpt{Text: "Билеты до Ялты: варианты на сегодня."}}
if !researchFindingMatchesIntent(query, relevant) {
t.Fatal("destination-specific ticket result was rejected")
}
}
func TestDistanceReplyRejectsCalculatorPageWithoutDistance(t *testing.T) {
query := "расстояние между Москве и Сочи в километрах по прямой и по дороге"
calculator := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Расчет расстояний"}, Excerpt: epistemics.SourceExcerpt{Text: "Калькулятор рассчитывает точное расстояние между городами."}}
if researchFindingMatchesIntent(query, calculator) {
t.Fatal("distance calculator landing page was presented without a measured distance")
}
measured := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Маршрут"}, Excerpt: epistemics.SourceExcerpt{Text: "Расстояние по дороге составляет 1 620 км."}}
if !researchFindingMatchesIntent(query, measured) {
t.Fatal("measured distance evidence was rejected")
}
}
func TestResearchReplyRejectsGenericLandingPageForConcreteIdentifier(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
State: epistemics.FindingSupported,
Source: epistemics.Source{Locator: "https://market.example", Title: "Интернет-магазин"},
Excerpt: epistemics.SourceExcerpt{Text: "Широкий ассортимент товаров и услуг."},
}}}
reply, backed := renderResearchReply("на WB бренд marka-wear", result, nil)
if backed || !strings.Contains(reply, "marka-wear") || strings.Contains(reply, "wildberries") || !strings.Contains(reply, "уточните написание") {
t.Fatalf("generic page was presented as a brand result: %q", reply)
}
}
func TestLocalPlaceReplyRejectsDictionaryFinding(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
State: epistemics.FindingSupported,
Source: epistemics.Source{Locator: "https://dictionary.example", Title: "Ближайший — словарь"},
Excerpt: epistemics.SourceExcerpt{Text: "Ближайший — значение слова и пример употребления."},
}}}
reply, backed := renderResearchReply("места ресторан в Сочи адрес отзывы меню", result, nil)
if backed || !strings.Contains(reply, "Проверяемых данных") {
t.Fatalf("dictionary finding was presented as local place result: %q", reply)
}
}
func TestResearchReplyKeepsFindingContainingConcreteIdentifier(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
State: epistemics.FindingSupported,
Source: epistemics.Source{Locator: "https://market.example/marka-wear", Title: "Marka-wear"},
Excerpt: epistemics.SourceExcerpt{Text: "Бренд marka-wear: товары и карточки."},
}}}
reply, backed := renderResearchReply("на WB бренд marka-wear", result, nil)
if !backed || !strings.Contains(strings.ToLower(reply), "marka-wear") {
t.Fatalf("concrete identifier finding was rejected: %q", reply)
}
}
func TestResearchIdentifiersExcludeSearchDate(t *testing.T) {
identifiers := requestedResearchIdentifiers("ближайший поезд в Петербург сегодня 26.08.2026")
if len(identifiers) != 0 {
t.Fatalf("search date was treated as a concrete identifier: %#v", identifiers)
}
}
func TestResearchQueryKeepsDistanceRelationBeforeSearchNormalization(t *testing.T) {
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
query := interactiondomain.TravelResearchQuery("сколько километров до Сочи?", history, time.Time{})
query = interactiondomain.SearchQueryWithLexicon(query, interactiondomain.TaskLexicon{})
if query != "расстояние между Москве и Сочи в километрах по прямой и по дороге" {
t.Fatalf("distance query lost remembered origin: %q", query)
}
}
func TestResponseReviewPreservesAttemptedResearchOutcome(t *testing.T) {
if reviewMayReplacePlanningReply(false, false, true, false, "Уточните аэропорт.", "Пока не смог получить проверяемый ответ.") {
t.Fatal("review replaced an honest unbacked research outcome")
}
if !reviewMayReplacePlanningReply(false, false, true, true, "Расписание. Источник: https://example.test", "Расписание. Источник: https://example.test") {
t.Fatal("review should be able to improve a source-backed research outcome")
}
if reviewMayReplacePlanningReply(false, false, true, true, "Расписание без ссылки", "Расписание. Источник: https://example.test") {
t.Fatal("review dropped the research evidence")
}
if !reviewMayReplacePlanningReply(false, false, false, false, "Нормальный ответ", "Черновик") {
t.Fatal("ordinary conversation review was unexpectedly blocked")
}
if !reviewMayReplacePlanningReply(true, true, false, false, "Санкт-Петербург — город в России.", "Меня зовут Домовой. Санкт-Петербург — город в России.") {
t.Fatal("stable-knowledge draft must be eligible for semantic quality review")
}
// The caller must not mark a source-backed research result as immutable
// semantic state: it may be rewritten, but only with its evidence retained.
if !reviewMayReplacePlanningReply(false, false, true, true, "Из Москвы в Сочи можно добраться самолётом или поездом. Источник: https://example.test", "Сырые результаты. Источник: https://example.test") {
t.Fatal("source-backed research draft must be eligible for human-facing review")
}
}
func TestNonMutatingTextDirectiveCanUseCognitiveReply(t *testing.T) {
semantic := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: interaction.SpeechActDirective}}
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{}, false) {
t.Fatal("ordinary formatting directive was not eligible for a cognitive reply")
}
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathCognition}, false) {
t.Fatal("reply-only cognition path was incorrectly rejected")
}
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathDirectCapability}, false) {
t.Fatal("direct capability was incorrectly replaced by a textual reply")
}
if shouldGenerateNonMutatingTextDirective(semantic, []chatRoute{{Kind: chatRouteMedia}}, work.HelpPath{}, false) {
t.Fatal("media route was incorrectly replaced by a textual reply")
}
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{NeedsDurableWork: true}, false) {
t.Fatal("durable work was incorrectly replaced by a textual reply")
}
}
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
request := httptest.NewRequest("GET", "/", nil)
request.Header.Set("Authorization", "Basic value")
@@ -27,6 +200,59 @@ func TestBearerTokenRequiresBearerScheme(t *testing.T) {
}
}
func TestLatestAssistantNameUsesNewestDurablePreference(t *testing.T) {
history := []interaction.HumanUtterance{
{Content: "я назову тебя Дед", ReceivedAtUnixMillis: 20},
{Content: "теперь тебя зовут Фея", ReceivedAtUnixMillis: 30},
}
if got := latestAssistantName(history); got != "Фея" {
t.Fatalf("latest assistant name = %q", got)
}
}
func TestHumanMemoryInventoryUsesNewestPreference(t *testing.T) {
history := []interaction.HumanUtterance{
{Content: "мне нравится синий цвет", ReceivedAtUnixMillis: 20},
{Content: "мне нравится красный цвет", ReceivedAtUnixMillis: 30},
}
got := humanMemoryInventory(history, "Фея")
if !strings.Contains(got, "красный") || strings.Contains(got, "синий") {
t.Fatalf("memory inventory did not select newest preference: %q", got)
}
}
func TestContainsScheduleTimeEvidence(t *testing.T) {
if containsScheduleTimeEvidence("расписание поездов и цена билета") {
t.Fatal("a price or schedule label is not a departure time")
}
if !containsScheduleTimeEvidence("Отправление в 07:45") {
t.Fatal("expected an explicit departure time to be recognized")
}
}
func TestInternetStatusQueryRecognizesConnectionChecks(t *testing.T) {
for _, query := range []string{"проверь, работает ли интернет", "есть доступ к сети?"} {
if !isInternetStatusQuery(query) {
t.Fatalf("connection check was not recognized: %q", query)
}
}
if isInternetStatusQuery("найди интернет-магазин") {
t.Fatal("ordinary search must not be treated as a connectivity check")
}
}
func TestInternetStatusReplyDoesNotLeakIncidentalSearchResults(t *testing.T) {
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
State: epistemics.FindingSupported,
Source: epistemics.Source{Locator: "https://example.test", Title: "Incidental page"},
Excerpt: epistemics.SourceExcerpt{Text: "A response was received."},
}}}
reply, backed := renderResearchReply("проверь, работает ли интернет", result, nil)
if !backed || reply != "Интернет работает: Домовой получил ответ от внешнего источника." {
t.Fatalf("unexpected internet status reply: %q", reply)
}
}
func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveChannelStatus, interaction.SpeechActQuestion, capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil)
if !strings.Contains(reply, "доступен") || strings.Contains(reply, "provider-secret-model") {
@@ -34,16 +260,23 @@ func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
}
}
func TestCurrentModelReplyDoesNotInventProvider(t *testing.T) {
reply := humanCurrentCognitiveModelReply(capability.Health{State: capability.HealthDegraded, CapabilityID: "provider-secret-model"}, nil)
if !strings.Contains(reply, "встроенным режимом") || !strings.Contains(reply, "ограничениями") || strings.Contains(reply, "provider-secret-model") {
t.Fatalf("unexpected current-model reply: %q", reply)
}
}
func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) {
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveRecovery, interaction.SpeechActDirective, capability.Health{State: capability.HealthUnavailable}, nil)
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "аварийн") {
if reply != "Канал мышления недоступен. Домовой остаётся на связи." {
t.Fatalf("unexpected configuration reply: %q", reply)
}
}
func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) {
reply := humanCognitiveDegradedReply("Помоги настроить локальную модель")
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "принято") {
if strings.Contains(reply, "когнитивный канал") || strings.Contains(reply, "Повторю") || !strings.Contains(reply, "не выдумываю") {
t.Fatalf("unexpected degraded reply: %q", reply)
}
}
@@ -74,11 +307,21 @@ func TestHumanCognitiveModelInventoryDoesNotExposeSecrets(t *testing.T) {
Provider: capability.ProviderInstance{Kind: "openai-compatible-chat-v1", CredentialRef: "secret-ref", AuthMode: capability.AuthKey, BillingMode: capability.BillingPerRequest, RequestCostMicros: &cost, Health: capability.HealthAvailable},
Model: capability.ModelDescriptor{ProviderModelID: "example-model", Health: capability.HealthAvailable},
}}, nil)
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "API-ключ") {
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "активна") {
t.Fatalf("unsafe or incomplete inventory: %q", text)
}
}
func TestHumanCognitiveModelInventoryDeduplicatesAndPrioritizesAvailable(t *testing.T) {
text := humanCognitiveModelInventory([]capability.ResolvedCognitiveBinding{
{Binding: capability.CognitiveBinding{Activation: capability.ActivationInactive}, Provider: capability.ProviderInstance{Health: capability.HealthUnavailable}, Model: capability.ModelDescriptor{ProviderModelID: "same-model", Health: capability.HealthUnavailable}},
{Binding: capability.CognitiveBinding{Activation: capability.ActivationActive}, Provider: capability.ProviderInstance{Health: capability.HealthAvailable}, Model: capability.ModelDescriptor{ProviderModelID: "same-model", Health: capability.HealthAvailable}},
}, nil)
if strings.Count(text, "same-model") != 1 || !strings.Contains(text, "same-model — активна") {
t.Fatalf("expected one active model entry, got %q", text)
}
}
func TestCreatorSessionHandlerAcceptsConfiguredCredentials(t *testing.T) {
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), func() time.Time { return time.Unix(1000, 0) })
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(`{"username":"m","password":"password"}`))
+98 -4
View File
@@ -40,6 +40,11 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) {
if !strings.Contains(body, "версия 0123456789ab") || !strings.Contains(body, "обновлено 22.08.2026 03:04 UTC") || !strings.Contains(body, "datetime=\"2026-08-22T03:04:05Z\"") {
t.Fatalf("/chat does not render build metadata: %q", body)
}
for _, required := range []string{"media-tools", "audio-input", "image-input", "ocr-input"} {
if !strings.Contains(body, required) {
t.Fatalf("/chat is missing multimodal control %q", required)
}
}
for _, path := range []string{"/", "/chat.html"} {
alternate := httptest.NewRecorder()
handler.ServeHTTP(alternate, httptest.NewRequest(http.MethodGet, path, nil))
@@ -60,6 +65,11 @@ func TestChatScriptExplainsSessionAndRuntimeFailures(t *testing.T) {
t.Fatalf("chat script is missing recovery message %q", required)
}
}
for _, required := range []string{"uploadMedia", "/api/multimodal/transcribe", "/api/multimodal/understand", "/api/multimodal/ocr"} {
if !strings.Contains(script, required) {
t.Fatalf("chat script is missing multimodal route %q", required)
}
}
}
func TestPublicSurfacesSeparateChatAndConsole(t *testing.T) {
@@ -158,14 +168,21 @@ func TestChatMessageDoesNotExposeUnverifiedQuestionAsFact(t *testing.T) {
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, FallbackText: "небезопасная модельная догадка"})
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, CapabilityHints: []capability.CapabilityKind{capability.CapabilityKindWebResearch}, FallbackText: "небезопасная модельная догадка"})
default:
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
}
}))
defer core.Close()
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("unverified question must not reach advisory: %s %s", r.Method, r.URL.Path)
if r.Method != http.MethodPost || r.URL.Path != "/v1/advisories" {
t.Fatalf("unexpected cognitive route: %s %s", r.Method, r.URL.Path)
}
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Я продолжаю искать проверяемый ответ через доступный источник.", FinishReason: cognitive.FinishCompleted})
}))
defer cognitiveRuntime.Close()
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), cognitiveRuntime.URL)
@@ -184,8 +201,85 @@ func TestChatMessageDoesNotExposeUnverifiedQuestionAsFact(t *testing.T) {
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatal(err)
}
if response.Cognition != "verification_required" || strings.Contains(response.ReplyText, "небезопасная") || !strings.Contains(response.ReplyText, "не буду придумывать") {
t.Fatalf("unexpected truth-first response: %#v", response)
if response.Cognition != string(cognitive.FinishCompleted) || strings.Contains(response.ReplyText, "небезопасная") || !strings.Contains(response.ReplyText, "продолжаю искать") {
t.Fatalf("unexpected research recovery response: %#v", response)
}
}
func TestChatMessageUsesLocalAdvisoryForOrdinaryConversationalQuestion(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, FallbackText: "fallback"})
default:
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
}
}))
defer core.Close()
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/advisories" {
t.Fatalf("unexpected cognitive path %s", r.URL.Path)
}
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Я рядом. Давай поговорим.", FinishReason: cognitive.FinishCompleted})
}))
defer cognitiveRuntime.Close()
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), cognitiveRuntime.URL)
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Как ты?"}`))
request.Header.Set("Authorization", "Bearer session-token")
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d body=%s", recorder.Code, recorder.Body.String())
}
var response browserChatResponse
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatal(err)
}
if response.ReplyText != "Я рядом. Давай поговорим." || response.Cognition != string(cognitive.FinishCompleted) {
t.Fatalf("unexpected conversational response: %#v", response)
}
}
func TestChatMessagePreservesCompletedCoreAnswerInsteadOfSecondAdvisory(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, FallbackText: "fallback", PlanningReply: "Готовый ответ ядра."})
default:
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
}
}))
defer core.Close()
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("completed core answer must not be replaced by a second advisory")
}))
defer cognitiveRuntime.Close()
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), cognitiveRuntime.URL)
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Обычный вопрос"}`))
request.Header.Set("Authorization", "Bearer session-token")
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
var response browserChatResponse
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.ReplyText != "Готовый ответ ядра." {
t.Fatalf("unexpected browser response: status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
+8 -1
View File
@@ -13,7 +13,14 @@ func main() {
envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"),
envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"),
envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"),
&http.Client{Timeout: 10 * time.Second},
// Core chat may spend several seconds on semantic interpretation and
// local cognition. Keep the browser gateway alive long enough to return
// the completed response instead of converting a successful core 201
// into a misleading 502.
// A cold local model may need more than one short HTTP round-trip to
// load on CPU. Keep the browser gateway aligned with the cognition
// deadline instead of turning that wait into a false unavailable reply.
&http.Client{Timeout: 15 * time.Minute},
envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"),
envOr("AGENT_WORKER_URL", "http://agent-worker:8090"),
)
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"bytes"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/identity"
)
func TestMultimodalUploadRequiresSessionAndProxiesLocalResult(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer session" {
t.Fatalf("unexpected session request: %s %q", r.URL.Path, r.Header.Get("Authorization"))
}
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"})
}))
defer core.Close()
cognitive := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/multimodal/transcribe" || r.Method != http.MethodPost {
t.Fatalf("unexpected multimodal request: %s %s", r.Method, r.URL.Path)
}
_ = json.NewEncoder(w).Encode(capability.MultimodalResponse{Operation: capability.MultimodalTranscribe, Text: "локальный текст", Provider: "local-whisper", ObservedAt: 1})
}))
defer cognitive.Close()
handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), cognitive.URL)
if err != nil {
t.Fatal(err)
}
denied := httptest.NewRecorder()
handler.ServeHTTP(denied, httptest.NewRequest(http.MethodPost, "/api/multimodal/transcribe", bytes.NewReader(nil)))
if denied.Code != http.StatusUnauthorized {
t.Fatalf("unauthenticated upload status=%d", denied.Code)
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "voice.wav")
if err != nil {
t.Fatal(err)
}
_, _ = part.Write([]byte("audio"))
if err := writer.Close(); err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/api/multimodal/transcribe", body)
request.Header.Set("Authorization", "Bearer session")
request.Header.Set("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || !bytes.Contains(response.Body.Bytes(), []byte("локальный текст")) {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
}
+148 -17
View File
@@ -7,7 +7,9 @@ import (
"fmt"
"io"
"io/fs"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"strings"
@@ -26,7 +28,11 @@ import (
const (
maxRequestBytes = 8 << 10
maxAdvisoryRequestBytes = 20 << 10
maxResponseBytes = 1 << 20
// Chat responses may contain a locally generated image encoded as base64.
// Keep the gateway limit aligned with the bounded media transport (32 MiB),
// otherwise a successfully generated image is incorrectly surfaced as an
// unavailable runtime to the human.
maxResponseBytes = 32 << 20
)
//go:embed static
@@ -135,6 +141,10 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
g.handleChatMessage(w, r)
return
}
if r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/api/multimodal/") {
g.handleMultimodal(w, r)
return
}
if r.Method == http.MethodGet && r.URL.Path == "/api/fleet/readiness" {
g.handleFleetReadiness(w, r)
return
@@ -214,6 +224,8 @@ type browserChatResponse struct {
DelegationID string `json:"delegation_id"`
DelegationState string `json:"delegation_state"`
ReminderScheduled bool `json:"reminder_scheduled"`
GeneratedImageContentType string `json:"generated_image_content_type,omitempty"`
GeneratedImageBase64 string `json:"generated_image_base64,omitempty"`
}
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
@@ -468,11 +480,10 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
saved.NeedsCognition = false
}
}
if saved.SpeechAct == interaction.SpeechActQuestion && saved.InformationRequestKind == "" && !saved.ResearchBacked && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired && !asksAttention(browserRequest.Message) {
reply = truthfulVerificationRequiredReply()
cognition = "verification_required"
saved.NeedsCognition = false
} else if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
// Core owns the completed planning answer. The browser may ask for a local
// advisory only when core explicitly left the turn unanswered; otherwise a
// second model pass can replace a grounded result with a weaker one.
if saved.NeedsCognition && saved.PlanningReply == "" && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
cognition = "unavailable"
requestID, idErr := ids.New()
if idErr == nil {
@@ -483,7 +494,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
status, err = g.postTrustedJSON(r, g.cognitive, "/v1/advisories", "", payload, &advisory)
if err == nil && status == http.StatusOK && validAdvisoryResponse(advisory, trusted) {
cognition = string(advisory.FinishReason)
if advisory.FinishReason == cognitive.FinishCompleted {
if advisory.FinishReason == cognitive.FinishCompleted && (strings.TrimSpace(saved.FallbackText) == "" || !isBoilerplateAdvisory(advisory.AdvisoryText)) {
reply = advisory.AdvisoryText
}
}
@@ -511,21 +522,38 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
DelegationID: saved.DelegationID,
DelegationState: saved.DelegationState,
ReminderScheduled: saved.Reminder != nil,
GeneratedImageContentType: saved.GeneratedImageContentType,
GeneratedImageBase64: saved.GeneratedImageBase64,
})
}
func truthfulVerificationRequiredReply() string {
return "Я не могу сейчас подтвердить этот факт по проверенному источнику, поэтому не буду придумывать ответ. Для проверки используйте официальный сайт соответствующей службы или надёжный сервис с текущими данными; когда подключён research-маршрут, Домовой сможет проверить это сам."
func isBoilerplateAdvisory(value string) bool {
normalized := strings.ToLower(strings.TrimSpace(value))
switch normalized {
case "я рядом.", "я рядом", "понял.", "понял", "принял.", "принял":
return true
default:
return strings.Contains(normalized, "я — домов") || strings.Contains(normalized, "я - домов") || strings.Contains(normalized, "меня зовут домов")
}
}
func localChatContext(saved interaction.ChatMessageResponse) []cognitive.ContextExcerpt {
if strings.TrimSpace(saved.ActiveProjectName) == "" {
return []cognitive.ContextExcerpt{}
assistantName := strings.TrimSpace(saved.AssistantName)
if assistantName == "" {
assistantName = "Домовой"
}
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{
DataPolicy: capability.DataPolicyLocalOnly,
MaxBytes: 2048,
Candidates: []cognitive.ContextCandidate{{
candidates := []cognitive.ContextCandidate{{
Excerpt: cognitive.ContextExcerpt{
ID: "assistant-identity",
SourceKind: "assistant-identity-policy",
SourceID: "domovoy",
Sensitivity: cognitive.SensitivityPublic,
Text: "Ты — " + assistantName + ", локальный помощник пользователя. Отвечай от имени «" + assistantName + "». Не называй себя Qwen, Alibaba Cloud, поставщиком модели или внутренним runtime. Если спрашивают имя, скажи: «Меня зовут " + assistantName + ".» Твоя задача — понять смысл запроса, найти путь к цели и довести помощь до полезного конечного результата; не ограничивайся пересказом отказа модели.",
},
Relevance: 100,
}}
if strings.TrimSpace(saved.ActiveProjectName) != "" {
candidates = append(candidates, cognitive.ContextCandidate{
Excerpt: cognitive.ContextExcerpt{
ID: cognitive.ContextExcerptID("active-project-focus"),
SourceKind: "situation-active-project",
@@ -533,8 +561,13 @@ func localChatContext(saved interaction.ChatMessageResponse) []cognitive.Context
Sensitivity: cognitive.SensitivityPrivateAllowed,
Text: "Текущий проект: " + strings.TrimSpace(saved.ActiveProjectName),
},
Relevance: 100,
}},
Relevance: 90,
})
}
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{
DataPolicy: capability.DataPolicyLocalOnly,
MaxBytes: 2048,
Candidates: candidates,
})
if err != nil {
return []cognitive.ContextExcerpt{}
@@ -648,6 +681,104 @@ func (g *gateway) postTrustedJSON(r *http.Request, upstream *url.URL, path, auth
return response.StatusCode, nil
}
func (g *gateway) handleMultimodal(w http.ResponseWriter, r *http.Request) {
authorization := r.Header.Get("Authorization")
if authorization == "" {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
var session identity.CreatorSession
status, err := g.getTrustedJSON(r, g.core, "/v1/creator/sessions/current", "", authorization, &session)
if err != nil || status != http.StatusOK || session.ActorID == "" || session.PrincipalID == "" {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
const maxMediaBytes = 12 << 20
r.Body = http.MaxBytesReader(w, r.Body, maxMediaBytes)
if err := r.ParseMultipartForm(maxMediaBytes); err != nil {
http.Error(w, "media upload is too large or invalid", http.StatusRequestEntityTooLarge)
return
}
path := ""
switch r.URL.Path {
case "/api/multimodal/transcribe":
path = "/v1/multimodal/transcribe"
case "/api/multimodal/understand":
path = "/v1/multimodal/understand"
case "/api/multimodal/ocr":
path = "/v1/multimodal/ocr"
case "/api/multimodal/video-understand":
path = "/v1/multimodal/video-understand"
case "/api/multimodal/generate-image":
path = "/v1/multimodal/generate-image"
default:
http.Error(w, "multimodal route not found", http.StatusNotFound)
return
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
if r.URL.Path == "/api/multimodal/generate-image" {
_ = writer.WriteField("prompt", r.FormValue("prompt"))
_ = writer.WriteField("size", r.FormValue("size"))
} else {
if r.URL.Path == "/api/multimodal/video-understand" {
_ = writer.WriteField("instruction", r.FormValue("instruction"))
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "media file is required", http.StatusBadRequest)
return
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, maxMediaBytes+1))
if err != nil || len(data) == 0 || len(data) > maxMediaBytes {
http.Error(w, "media file is invalid", http.StatusBadRequest)
return
}
mediaHeader := make(textproto.MIMEHeader)
mediaHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, strings.ReplaceAll(header.Filename, `"`, "")))
mediaType := header.Header.Get("Content-Type")
if mediaType == "" {
mediaType = "application/octet-stream"
}
mediaHeader.Set("Content-Type", mediaType)
part, err := writer.CreatePart(mediaHeader)
if err != nil {
http.Error(w, "media upload unavailable", http.StatusServiceUnavailable)
return
}
if _, err := part.Write(data); err != nil {
http.Error(w, "media upload unavailable", http.StatusServiceUnavailable)
return
}
}
if err := writer.Close(); err != nil {
http.Error(w, "media upload unavailable", http.StatusServiceUnavailable)
return
}
target := *g.cognitive
target.Path = path
request, err := http.NewRequestWithContext(r.Context(), http.MethodPost, target.String(), body)
if err != nil {
http.Error(w, "media capability unavailable", http.StatusBadGateway)
return
}
request.Header.Set("Content-Type", writer.FormDataContentType())
response, err := g.client.Do(request)
if err != nil {
http.Error(w, "media capability unavailable", http.StatusBadGateway)
return
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, maxResponseBytes))
http.Error(w, "media capability unavailable", http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.Copy(w, io.LimitReader(response.Body, maxResponseBytes))
}
func decodeStrictJSON(reader io.Reader, limit int64, destination any) error {
decoder := json.NewDecoder(io.LimitReader(reader, limit+1))
decoder.DisallowUnknownFields()
+36 -5
View File
@@ -1,6 +1,7 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
@@ -13,13 +14,43 @@ import (
"git.cin.su/m/Assistant/contracts/interaction"
)
func TestLocalChatContextUsesOnlyActiveProject(t *testing.T) {
context := localChatContext(interaction.ChatMessageResponse{ActiveProjectName: "Домовой"})
if len(context) != 1 || context[0].SourceKind != "situation-active-project" || context[0].Text != "Текущий проект: Домовой" {
func TestMaxResponseBytesAcceptsGeneratedImagePayload(t *testing.T) {
// A 1024px PNG can exceed the former 1 MiB gateway limit once base64
// encoded. The browser gateway must forward that successful core response.
payload, err := json.Marshal(interaction.ChatMessageResponse{
GeneratedImageContentType: "image/png",
GeneratedImageBase64: strings.Repeat("A", 2<<20),
})
if err != nil {
t.Fatal(err)
}
var decoded interaction.ChatMessageResponse
if err := decodeStrictJSON(bytes.NewReader(payload), maxResponseBytes, &decoded); err != nil {
t.Fatalf("generated image response must fit gateway limit: %v", err)
}
if len(decoded.GeneratedImageBase64) != 2<<20 {
t.Fatalf("generated image payload was truncated: %d", len(decoded.GeneratedImageBase64))
}
}
func TestLocalChatContextIncludesDomovoyIdentityAndOptionalActiveProject(t *testing.T) {
context := localChatContext(interaction.ChatMessageResponse{AssistantName: "Фея", ActiveProjectName: "Домовой"})
if len(context) != 2 || context[0].SourceID != "domovoy" || !strings.Contains(context[0].Text, "Ты — Фея") || !strings.Contains(context[0].Text, "Меня зовут Фея") || context[1].Text != "Текущий проект: Домовой" {
t.Fatalf("unexpected local context: %#v", context)
}
if empty := localChatContext(interaction.ChatMessageResponse{}); len(empty) != 0 {
t.Fatalf("expected empty context without active project: %#v", empty)
if empty := localChatContext(interaction.ChatMessageResponse{}); len(empty) != 1 || empty[0].SourceID != "domovoy" {
t.Fatalf("identity context must remain available without active project: %#v", empty)
}
}
func TestBoilerplateAdvisoryDoesNotReplaceUsefulFallback(t *testing.T) {
for _, value := range []string{"Я рядом", "Понял.", "Принял.", "Я — Домовой, ваш помощник."} {
if !isBoilerplateAdvisory(value) {
t.Fatalf("%q was not recognized as boilerplate", value)
}
}
if isBoilerplateAdvisory("Жили-были дед и баба.") {
t.Fatal("useful creative reply was treated as boilerplate")
}
}
+5
View File
@@ -56,6 +56,7 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.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); }
.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; }
@@ -75,6 +76,10 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.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; }
+47 -2
View File
@@ -103,7 +103,7 @@ function showLogin() {
loginView.hidden = false;
}
function appendMessage(kind, text, meta = "") {
function appendMessage(kind, text, meta = "", media = null) {
const node = document.createElement("div");
node.className = `message ${kind}`;
node.textContent = text;
@@ -112,6 +112,13 @@ function appendMessage(kind, text, meta = "") {
small.textContent = meta;
node.appendChild(small);
}
if (media?.data && media?.contentType) {
const image = document.createElement("img");
image.className = "generated-image";
image.alt = text || "Сгенерированное изображение";
image.src = `data:${media.contentType};base64,${media.data}`;
node.appendChild(image);
}
messages.appendChild(node);
messages.scrollTop = messages.scrollHeight;
localHistory.push({kind, text, meta, at: new Date()});
@@ -132,6 +139,16 @@ async function api(path, options = {}) {
return response.json();
}
async function uploadMedia(path, file) {
const body = new FormData();
body.append("file", file, file.name);
const headers = {};
if (token) headers.Authorization = `Bearer ${token}`;
const response = await fetch(path, {method: "POST", headers, body});
if (!response.ok) throw new ChatApiError(response.status, await response.text());
return response.json();
}
function expireSession(message = "") {
token = "";
conversationID = "";
@@ -267,7 +284,7 @@ async function sendChatMessage(text, options = {}) {
conversationID = result.conversation_id;
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
updateActiveFocus(result.active_focus, result.project_provenance);
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result));
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result), result.generated_image_base64 ? {contentType: result.generated_image_content_type || "image/png", data: result.generated_image_base64} : null);
if (/что сейчас требует моего внимания/i.test(clean)) {
await loadToday();
activateTab("today-panel");
@@ -320,6 +337,34 @@ chatForm.addEventListener("submit", async (event) => {
await sendChatMessage(text);
});
async function processMedia(input, path, label) {
const file = input?.files?.[0];
if (!file || !mediaStatus) return;
mediaStatus.textContent = `${label} обрабатывается...`;
input.value = "";
try {
const result = await uploadMedia(path, file);
const text = String(result.text || "").trim();
if (!text) throw new Error("empty media result");
mediaStatus.textContent = "Готово";
await sendChatMessage(`Пользователь передал ${label.toLowerCase()}. Обработай содержание и ответь по смыслу:\n${text}`);
} catch (error) {
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
} else {
mediaStatus.textContent = `${label} сейчас недоступно.`;
}
}
}
const mediaStatus = document.querySelector("#media-status");
const audioInput = document.querySelector("#audio-input");
const imageInput = document.querySelector("#image-input");
const ocrInput = document.querySelector("#ocr-input");
if (audioInput) audioInput.addEventListener("change", () => processMedia(audioInput, "/api/multimodal/transcribe", "Распознанное аудио"));
if (imageInput) imageInput.addEventListener("change", () => processMedia(imageInput, "/api/multimodal/understand", "Описание изображения"));
if (ocrInput) ocrInput.addEventListener("change", () => processMedia(ocrInput, "/api/multimodal/ocr", "Текст с изображения"));
for (const tab of navTabs) {
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
}
+6
View File
@@ -60,6 +60,12 @@
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<button id="send" type="submit">Отправить</button>
</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>
<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>
+21
View File
@@ -49,6 +49,9 @@ func validateTaskPacket(root string, task TaskPacket, taskPath string) error {
if err := validateArchitectureRevision(root, task.ArchitectureRevision); err != nil {
return fmt.Errorf("task %s architecture_revision: %w", task.TaskID, err)
}
if err := validateGenerationTask(task); err != nil {
return err
}
if _, err := resolveBaseCommit(root, task.BaseRepoRevision); err != nil {
return fmt.Errorf("task %s base_repo_revision: %w", task.TaskID, err)
}
@@ -58,6 +61,24 @@ func validateTaskPacket(root string, task TaskPacket, taskPath string) error {
return nil
}
func validateGenerationTask(task TaskPacket) error {
switch {
case task.TaskID == "PRE-GEN2-000":
if task.BaseRepoRevision != "origin/main" || task.ArchitectureRevision != "ADR-0019" {
return fmt.Errorf("task %s must use origin/main and ADR-0019", task.TaskID)
}
case task.TaskID == "ARCH-GEN2-000":
if task.BaseRepoRevision != "gen2-integration" || task.ArchitectureRevision != "ADR-0019" {
return fmt.Errorf("task %s must use gen2-integration and ADR-0019", task.TaskID)
}
case strings.HasPrefix(task.TaskID, "GEN2-"):
if task.BaseRepoRevision != "gen2-integration" || task.ArchitectureRevision != "ARCH-GEN2-000" {
return fmt.Errorf("task %s must use gen2-integration and ARCH-GEN2-000", task.TaskID)
}
}
return nil
}
func validateRequiredTests(tests []string) error {
joined := strings.Join(tests, "\n")
for _, required := range []string{"go test", "go vet", "preflight", "check-diff", "git diff --check", "independent"} {
+39
View File
@@ -25,3 +25,42 @@ func TestValidateDeltaRejectsUnknownOrMissingFields(t *testing.T) {
t.Fatalf("delta error = %v", err)
}
}
func TestValidateGenerationTaskRequiresGen2BaseAndArchitecture(t *testing.T) {
tests := []struct {
name string
task TaskPacket
want string
}{
{
name: "architecture task requires integration base",
task: TaskPacket{TaskID: "ARCH-GEN2-000", BaseRepoRevision: "origin/main", ArchitectureRevision: "ADR-0019"},
want: "gen2-integration",
},
{
name: "implementation task requires accepted architecture root",
task: TaskPacket{TaskID: "GEN2-001", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ADR-0019"},
want: "ARCH-GEN2-000",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateGenerationTask(tc.task)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("generation validation error = %v, want %q", err, tc.want)
}
})
}
}
func TestValidateGenerationTaskAcceptsExecutionRoots(t *testing.T) {
for _, task := range []TaskPacket{
{TaskID: "PRE-GEN2-000", BaseRepoRevision: "origin/main", ArchitectureRevision: "ADR-0019"},
{TaskID: "ARCH-GEN2-000", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ADR-0019"},
{TaskID: "GEN2-001", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ARCH-GEN2-000"},
} {
if err := validateGenerationTask(task); err != nil {
t.Fatalf("validateGenerationTask(%+v) = %v", task, err)
}
}
}
+10 -1
View File
@@ -89,7 +89,16 @@ func main() {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
searchConnector := researchadapter.SearchConnector{EndpointTemplate: os.Getenv("RESEARCH_SEARCH_URL_TEMPLATE"), AllowedHosts: allowedHosts(os.Getenv("RESEARCH_SEARCH_ALLOWED_HOSTS")), Client: &http.Client{Timeout: 8 * time.Second}, MaxResults: 3}
searchConnector := researchadapter.SearchConnector{
EndpointTemplate: os.Getenv("RESEARCH_SEARCH_URL_TEMPLATE"),
AllowedHosts: allowedHosts(os.Getenv("RESEARCH_SEARCH_ALLOWED_HOSTS")),
FallbackEndpoints: []researchadapter.SearchEndpoint{{
Template: "https://html.duckduckgo.com/html/?q=%s",
AllowedHosts: map[string]struct{}{"html.duckduckgo.com": {}},
}},
Client: &http.Client{Timeout: 8 * time.Second},
MaxResults: 3,
}
mux.HandleFunc("POST /v1/research/web/search", func(w http.ResponseWriter, r *http.Request) {
var request epistemics.WebSearchRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil {
+203
View File
@@ -0,0 +1,203 @@
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.Start(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: 5 * 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/observe", s.observe)
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
}
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) 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) 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) promote(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.Promote()
respond(w, ok, err)
}
func (s *server) rollback(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.Rollback()
respond(w, ok, err)
}
func (s *server) canary(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.SetTraffic(supervisor.TrafficCanaryHeader, s.stableUpstream, s.candidate)
respond(w, ok, err)
}
func (s *server) promoteTraffic(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.Promote()
respond(w, ok, err)
}
func (s *server) rollbackTraffic(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.RollbackTraffic()
respond(w, ok, err)
}
func decode(w http.ResponseWriter, r *http.Request, value any) bool {
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(value); err != nil {
http.Error(w, "invalid control request", 400)
return false
}
return true
}
func respond(w http.ResponseWriter, ok bool, err error) {
if err != nil {
http.Error(w, "control state unavailable", 503)
return
}
if !ok {
http.Error(w, "control transition rejected", 409)
return
}
writeJSON(w, map[string]bool{"ok": true})
}
func writeJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
}
func envOr(name, fallback string) string {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
return fallback
}
func fatal(err error) { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
+12
View File
@@ -5,6 +5,18 @@ import "git.cin.su/m/Assistant/platform/ids"
import "errors"
type ArtifactID ids.ID
type MediaArtifact struct {
ID ArtifactID
ContentType string
SizeBytes int64
ContentDigest string
SourceRef string
RecordedAtUnixMillis int64
}
func (a MediaArtifact) Valid() bool {
return a.ID != "" && a.ContentType != "" && a.SizeBytes > 0 && a.ContentDigest != "" && a.SourceRef != "" && a.RecordedAtUnixMillis > 0
}
type AgentArtifactAttestation struct {
ArtifactID ArtifactID
+41
View File
@@ -0,0 +1,41 @@
package capability
type IntelligencePath string
const (
PathRule IntelligencePath = "RULE"
PathReflex IntelligencePath = "REFLEX"
PathSkill IntelligencePath = "SKILL"
PathSmallLocal IntelligencePath = "SMALL_LOCAL"
PathStrongLocal IntelligencePath = "STRONG_LOCAL"
PathExternal IntelligencePath = "EXTERNAL"
PathResearch IntelligencePath = "RESEARCH"
PathTeam IntelligencePath = "TEAM"
)
type StrategyRequest struct {
RequiredQuality int64
PreferLowLatency bool
CostBudgetMicros int64
RiskClass string
FreshnessRequired bool
CostOfError int64
}
type StrategyDecision struct {
Path IntelligencePath
Reason string
Escalate bool
}
func (r StrategyRequest) Select() StrategyDecision {
if r.RiskClass == "HIGH" || r.CostOfError >= 800 {
return StrategyDecision{Path: PathStrongLocal, Reason: "high consequence requires deeper reasoning", Escalate: true}
}
if r.RequiredQuality <= 500 && r.CostBudgetMicros > 0 {
return StrategyDecision{Path: PathRule, Reason: "bounded low-risk path"}
}
if r.PreferLowLatency {
return StrategyDecision{Path: PathReflex, Reason: "latency preference with later escalation"}
}
return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"}
}
+29
View File
@@ -0,0 +1,29 @@
package capability
// MultimodalOperation identifies one bounded local media skill. It is kept
// separate from general cognition so health, routing and failure evidence do
// not collapse into a generic model response.
type MultimodalOperation string
const (
MultimodalTranscribe MultimodalOperation = "TRANSCRIBE_AUDIO"
MultimodalUnderstand MultimodalOperation = "UNDERSTAND_IMAGE"
MultimodalRecognizeOCR MultimodalOperation = "RECOGNIZE_OCR"
MultimodalUnderstandVideo MultimodalOperation = "UNDERSTAND_VIDEO"
MultimodalGenerateImage MultimodalOperation = "GENERATE_IMAGE"
MultimodalGenerateVideo MultimodalOperation = "GENERATE_VIDEO"
)
type MultimodalResponse struct {
Operation MultimodalOperation `json:"operation"`
Text string `json:"text"`
Provider string `json:"provider,omitempty"`
ObservedAt int64 `json:"observed_at_unix_millis"`
ConfidenceNote string `json:"confidence_note,omitempty"`
ContentType string `json:"content_type,omitempty"`
DataBase64 string `json:"data_base64,omitempty"`
Model string `json:"model,omitempty"`
ModelVersion string `json:"model_version,omitempty"`
ConfidenceMillis int64 `json:"confidence_millis,omitempty"`
EvidenceRef string `json:"evidence_ref,omitempty"`
}
+61
View File
@@ -0,0 +1,61 @@
package capability
type ReflexID string
type ReflexLifecycle string
type ReflexKind string
const (
ReflexCandidate ReflexLifecycle = "CANDIDATE"
ReflexValidating ReflexLifecycle = "VALIDATING"
ReflexActive ReflexLifecycle = "ACTIVE"
ReflexDegraded ReflexLifecycle = "DEGRADED"
ReflexStale ReflexLifecycle = "STALE"
ReflexSuspended ReflexLifecycle = "SUSPENDED"
ReflexRetired ReflexLifecycle = "RETIRED"
ReflexDeterministicCode ReflexKind = "DETERMINISTIC_CODE"
ReflexRule ReflexKind = "RULE"
ReflexWorkflow ReflexKind = "WORKFLOW"
ReflexQueryTemplate ReflexKind = "QUERY_TEMPLATE"
ReflexSmallModelChain ReflexKind = "SMALL_MODEL_CHAIN"
ReflexBoundedAdapter ReflexKind = "BOUNDED_ADAPTER_OPERATION"
)
type ShadowPolicy struct {
Enabled bool
SampleRateMillis int64
ComparatorRef string
MaxShadowCostMicros *int64
EligibleErrorCostClasses []string
}
type RevalidationPolicy struct {
MaxAgeMillis *int64
RevalidateOnDependencyRevisionChange bool
RevalidateOnWorldVersionChange bool
RevalidateAfterFailureCount *int64
RevalidateAfterCorrectionCount *int64
SampleAuditRateMillis *int64
}
type Reflex struct {
ID ReflexID
PrincipalID string
SourceSkillRef SkillID
CreatedAt int64
}
type ReflexRevision struct {
ReflexID ReflexID
Revision int64
SourceSkillRevision int64
Kind ReflexKind
Applicability ApplicabilitySpec
ImplementationRef string
ImplementationVersion string
ArtifactDigest string
DependencyRefs []string
ValidationEvidenceRefs []SkillEvidenceRef
KnownFailureExperienceRefs []SkillEvidenceRef
Shadow ShadowPolicy
Revalidation RevalidationPolicy
LastValidatedAt int64
Lifecycle ReflexLifecycle
RecordedAt int64
}
+36
View File
@@ -0,0 +1,36 @@
package capability
type SkillID string
type SkillRevision struct {
SkillID SkillID
Revision int64
Description string
Applicability ApplicabilitySpec
ImplementationRef string
ImplementationVersion string
DependencyRefs []string
SupportingExperienceRefs []SkillEvidenceRef
KnownFailureExperienceRefs []SkillEvidenceRef
ValidationEvidenceRefs []SkillEvidenceRef
QualityAssessmentRefs []string
EnvironmentVersionConstraints []string
LastValidatedAt int64
Lifecycle SkillLifecycleStatus
SupersedesRevision int64
RecordedAt int64
}
type ApplicabilitySpec struct {
RequiredContextBindings []string
PreferredContextBindings []string
ExcludedContextBindings []string
RequiredWorldKinds []string
VersionConstraints []string
InputConditions []string
MinimumEvidenceFreshness string
}
type Skill struct {
ID SkillID
PrincipalID string
Name string
CreatedAt int64
}
+6
View File
@@ -29,6 +29,12 @@ const (
CapabilityKindCognitiveReasoning CapabilityKind = "COGNITIVE_REASONING"
CapabilityKindScheduleReminder CapabilityKind = "SCHEDULE_REMINDER"
CapabilityKindVerifyFact CapabilityKind = "VERIFY_FACT"
CapabilityKindSpeechToText CapabilityKind = "SPEECH_TO_TEXT"
CapabilityKindImageUnderstanding CapabilityKind = "IMAGE_UNDERSTANDING"
CapabilityKindOCR CapabilityKind = "OCR"
CapabilityKindVideoUnderstanding CapabilityKind = "VIDEO_UNDERSTANDING"
CapabilityKindImageGeneration CapabilityKind = "IMAGE_GENERATION"
CapabilityKindVideoGeneration CapabilityKind = "VIDEO_GENERATION"
)
// Binding is Capability-owned correctness state. SecretRef is an opaque
+41 -5
View File
@@ -11,14 +11,24 @@ import (
var ErrInvalidContextBudget = errors.New("context budget must be positive")
type ContextCandidate struct {
Excerpt ContextExcerpt
Relevance int
Excerpt ContextExcerpt
Relevance int
Lane string
ContextScopeRef string
FreshnessState string
Negative bool
}
type ContextCompileInput struct {
Candidates []ContextCandidate
DataPolicy capability.DataPolicy
MaxBytes int
Candidates []ContextCandidate
DataPolicy capability.DataPolicy
MaxBytes int
TaskRef string
FocusFrameRef string
ContextScopeRef string
FreshnessRequirement string
RiskClass string
TokenBudget int
}
type ContextOmission struct {
@@ -30,6 +40,24 @@ type ContextBundle struct {
Excerpts []ContextExcerpt
Omissions []ContextOmission
Bytes int
Trace ContextAssemblyTrace
}
type ContextAssemblyTrace struct {
Included []ContextExcerptID
Excluded []ContextOmission
Coverage string
CompilerVersion string
}
type IterativeRetrievalRequest struct {
Query string
RemainingBytes int
Pass int
}
type IterativeRetrievalResponse struct {
Candidates []ContextCandidate
Exhausted bool
}
// CompileContext deterministically selects permitted, relevant excerpts for a
@@ -54,6 +82,10 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
continue
}
seen[excerpt.ID] = struct{}{}
if input.ContextScopeRef != "" && candidate.ContextScopeRef != input.ContextScopeRef {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "context_scope"})
continue
}
reason := permittedContextReason(excerpt, input.DataPolicy)
if reason != "" {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason})
@@ -70,7 +102,11 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
}
bundle.Excerpts = append(bundle.Excerpts, excerpt)
bundle.Bytes += bytes
bundle.Trace.Included = append(bundle.Trace.Included, excerpt.ID)
}
bundle.Trace.Excluded = append([]ContextOmission(nil), bundle.Omissions...)
bundle.Trace.CompilerVersion = "context-compiler-v2-additive"
bundle.Trace.Coverage = "BOUNDED"
return bundle, nil
}
@@ -0,0 +1,14 @@
package cognitive_test
import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"testing"
)
func TestContextCompilerV2TraceAndBudgetRemainDeterministic(t *testing.T) {
b, err := cognitive.CompileContext(cognitive.ContextCompileInput{MaxBytes: 5, DataPolicy: capability.DataPolicyLocalOnly, TaskRef: "task", FocusFrameRef: "frame", ContextScopeRef: "company-a", Candidates: []cognitive.ContextCandidate{{Excerpt: cognitive.ContextExcerpt{ID: "a", Text: "123", Sensitivity: cognitive.SensitivityPrivateAllowed}, ContextScopeRef: "company-a", Relevance: 10}, {Excerpt: cognitive.ContextExcerpt{ID: "b", Text: "123456", Sensitivity: cognitive.SensitivityPrivateAllowed}, ContextScopeRef: "company-b", Relevance: 100}}})
if err != nil || len(b.Excerpts) != 1 || len(b.Trace.Excluded) != 1 {
t.Fatalf("bundle=%#v err=%v", b, err)
}
}
+32
View File
@@ -0,0 +1,32 @@
package commit
type ExecutorResult string
type AmbiguityState string
const (
ExecutorAccepted ExecutorResult = "ACCEPTED"
ExecutorReportedSuccess ExecutorResult = "REPORTED_SUCCESS"
ExecutorReportedFailure ExecutorResult = "REPORTED_FAILURE"
ExecutorNoResponse ExecutorResult = "NO_RESPONSE"
ExecutorAmbiguous ExecutorResult = "AMBIGUOUS"
)
type ExecutionReceipt struct {
ID string
ActionAttemptRef ActionAttemptID
ExecutorImplementationRef string
ExternalOperationRef string
ExternalRequestID string
RequestDigest string
ResponseDigest string
DispatchAtUnixMillis int64
CompletedAtUnixMillis *int64
ExecutorResult ExecutorResult
Ambiguity AmbiguityState
ReconciliationKey string
RawArtifactRef string
}
func (r ExecutionReceipt) Valid() bool {
return r.ID != "" && r.ActionAttemptRef != "" && r.ExecutorImplementationRef != "" && r.RequestDigest != "" && r.ExecutorResult != "" && r.ReconciliationKey != "" && r.DispatchAtUnixMillis > 0
}
+7
View File
@@ -0,0 +1,7 @@
# Configuration contract boundary
Configuration owns typed and versioned `SettingDefinition` and
`SettingAssignment` records plus scoped resolution and change provenance.
Effective configuration is a derived result. Secret values never enter a
setting assignment; only a `SecretRef`/`CredentialRef` crosses the secrets
boundary into an authorized adapter or runtime.
+7
View File
@@ -0,0 +1,7 @@
package configuration
import "git.cin.su/m/Assistant/platform/ids"
// SemanticReference is a typed cross-owner reference; settings remain
// Configuration-owned records.
type SemanticReference = ids.SemanticRef
+101
View File
@@ -0,0 +1,101 @@
package configuration
import (
"fmt"
"git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/platform/ids"
)
type SettingDefinitionID ids.StableID
type SettingAssignmentID ids.StableID
type ConfigurationRevision ids.Revision
type ValueType string
type ResolutionMode string
type SettingLifecycle string
type SettingSource string
const (
ValueString ValueType = "STRING"
ValueInteger ValueType = "INTEGER"
ValueBoolean ValueType = "BOOLEAN"
ValueSecretRef ValueType = "SECRET_REF"
ResolutionMostSpecific ResolutionMode = "MOST_SPECIFIC_WINS"
ResolutionFirstHuman ResolutionMode = "FIRST_EXPLICIT_HUMAN_WINS"
ResolutionMergeList ResolutionMode = "MERGE_LIST"
SettingActive SettingLifecycle = "ACTIVE"
SettingDeprecated SettingLifecycle = "DEPRECATED"
SettingRetired SettingLifecycle = "RETIRED"
SourceHumanDirective SettingSource = "HUMAN_DIRECTIVE"
SourceMigrated SettingSource = "MIGRATED_CONFIG"
SourceSkillDefault SettingSource = "SKILL_DEFAULT"
SourceSystemDefault SettingSource = "SYSTEM_DEFAULT"
SourceGovernedChange SettingSource = "GOVERNED_CHANGE"
)
type SettingDefinition struct {
ID SettingDefinitionID
OwnerNamespace string
StableKey string
Revision ConfigurationRevision
ValueType ValueType
DefaultValue string
Sensitivity string
Lifecycle SettingLifecycle
RecordedAtUnixMillis int64
}
type ScopeSelector struct {
PrincipalRef string
RequiredContextBindings []string
ActivityRef string
SkillRef string
TaskRef string
SessionRef string
TemporaryOverride bool
}
type SettingAssignment struct {
ID SettingAssignmentID
PrincipalID string
DefinitionRef string
DefinitionRevision ConfigurationRevision
Revision ConfigurationRevision
Selector ScopeSelector
TypedValue string
SecretRef *secrets.CredentialRef
EffectiveFromUnixMillis int64
EffectiveUntilUnixMillis *int64
Source SettingSource
SourceRef string
Lifecycle SettingLifecycle
RecordedAtUnixMillis int64
}
type EffectiveSetting struct {
DefinitionRef string
Value string
WinningAssignmentRef string
ResolutionTrace []string
}
type BootstrapConfig struct {
NodeIdentity string
ControlDatabaseLocator string
RootSecretResolverLocator string
RecoveryLocator string
}
// ConversationalConfigurationCommand is an intent, not an assignment. Only
// Configuration may turn a SET command into durable state.
func (d SettingDefinition) Validate() error {
if d.ID == "" || d.OwnerNamespace == "" || d.StableKey == "" || !ids.Revision(d.Revision).Valid() || d.ValueType == "" || d.Lifecycle == "" || d.RecordedAtUnixMillis < 1 {
return fmt.Errorf("invalid setting definition")
}
return nil
}
func (a SettingAssignment) Validate() error {
if a.ID == "" || a.PrincipalID == "" || a.DefinitionRef == "" || !ids.Revision(a.Revision).Valid() || a.Source == "" || a.Lifecycle == "" || a.RecordedAtUnixMillis < 1 {
return fmt.Errorf("invalid setting assignment")
}
if a.SecretRef != nil && a.TypedValue != "" {
return fmt.Errorf("secret assignment must not contain typed value")
}
return nil
}
+90
View File
@@ -0,0 +1,90 @@
package epistemics
import (
"fmt"
"git.cin.su/m/Assistant/contracts/world"
)
type VolatilityMode string
const (
VolatilityImmutable VolatilityMode = "IMMUTABLE"
VolatilityVersionBound VolatilityMode = "VERSION_BOUND"
VolatilityVerySlow VolatilityMode = "VERY_SLOW"
VolatilitySlow VolatilityMode = "SLOW"
VolatilityMedium VolatilityMode = "MEDIUM"
VolatilityFast VolatilityMode = "FAST"
VolatilityEphemeral VolatilityMode = "EPHEMERAL"
VolatilityCustom VolatilityMode = "CUSTOM"
)
type TemporalApplicability struct {
ObservedAtUnixMillis int64
ValidFromUnixMillis *int64
ValidUntilUnixMillis *int64
LastVerifiedAtUnixMillis *int64
Volatility VolatilitySpec
VersionBindings []world.VersionBinding
}
type VolatilitySpec struct {
Mode VolatilityMode
ExpectedChangeIntervalMillis *int64
RevalidateAfterMillis *int64
HardStaleAfterMillis *int64
InvalidateOnVersionChange bool
RevalidationPolicyRef string
}
type FreshnessAssessmentState string
const (
FreshnessAssessmentCurrent FreshnessAssessmentState = "CURRENT"
FreshnessAssessmentAcceptable FreshnessAssessmentState = "ACCEPTABLE"
FreshnessAssessmentAging FreshnessAssessmentState = "AGING"
FreshnessAssessmentStale FreshnessAssessmentState = "STALE"
FreshnessAssessmentVersionMismatch FreshnessAssessmentState = "VERSION_MISMATCH"
FreshnessAssessmentHistoricalOnly FreshnessAssessmentState = "HISTORICAL_ONLY"
FreshnessAssessmentUnknown FreshnessAssessmentState = "UNKNOWN"
)
type FreshnessAssessment struct {
SourceKnowledgeRef string
TaskRef string
State FreshnessAssessmentState
Reason string
AgeMillis int64
VersionMatchState string
RevalidationAttemptRef string
AssessedAtUnixMillis int64
}
type RevalidationRequest struct {
ID string
SourceKnowledgeRef string
TaskRef string
RequiredVersionRefs []world.SemanticReference
RequestedAtUnixMillis int64
}
type RevalidationResponse struct {
RequestID string
Assessment FreshnessAssessment
NewEvidenceRefs []EvidenceRef
CompletedAtUnixMillis int64
}
func (v VolatilitySpec) Validate() error {
if v.Mode == "" {
return fmt.Errorf("volatility mode is required")
}
return nil
}
func (t TemporalApplicability) Validate() error {
if t.ObservedAtUnixMillis < 1 {
return fmt.Errorf("observed time is required")
}
return t.Volatility.Validate()
}
func (a FreshnessAssessment) Validate() error {
if a.SourceKnowledgeRef == "" || a.TaskRef == "" || a.State == "" || a.AgeMillis < 0 || a.AssessedAtUnixMillis < 1 {
return fmt.Errorf("freshness assessment is incomplete")
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package epistemics
type WorldOutcomeState string
const (
OutcomeVerifiedSuccess WorldOutcomeState = "VERIFIED_SUCCESS"
OutcomeVerifiedPartial WorldOutcomeState = "VERIFIED_PARTIAL"
OutcomeVerifiedFailure WorldOutcomeState = "VERIFIED_FAILURE"
OutcomeUnverified WorldOutcomeState = "UNVERIFIED"
OutcomeUnknownState WorldOutcomeState = "UNKNOWN"
OutcomeConflicted WorldOutcomeState = "CONFLICTED"
)
type WorldOutcomeAssessment struct {
ID string
PrincipalID string
ActionAttemptRef string
ExecutionReceiptRefs []string
ExpectedEffectRef string
ObservationRefs []ObservationID
ClaimRefs []ClaimID
State WorldOutcomeState
ConfidenceMillis int64
EvidenceRefs []EvidenceRef
AssessedAt int64
}
func (a WorldOutcomeAssessment) Valid() bool {
return a.ID != "" && a.PrincipalID != "" && a.ActionAttemptRef != "" && a.State != "" && a.ConfidenceMillis >= 0 && a.ConfidenceMillis <= 1000 && a.AssessedAt > 0
}
+7
View File
@@ -0,0 +1,7 @@
package epistemics
import "git.cin.su/m/Assistant/platform/ids"
// SemanticReference identifies evidence-related records without transferring
// canonical ownership to Epistemics.
type SemanticReference = ids.SemanticRef
+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
+20 -10
View File
@@ -1,5 +1,7 @@
package epistemics
import "git.cin.su/m/Assistant/platform/ids"
type BeliefState string
const (
@@ -9,19 +11,27 @@ const (
)
type Observation struct {
ID ObservationID
SourceSnapshot SourceSnapshotID
Excerpt SourceExcerptID
Text string
ObservedAt int64
ID ObservationID
SourceSnapshot SourceSnapshotID
Excerpt SourceExcerptID
Text string
ObservedAt int64
PrincipalID string
ContextScopeRef string
ObservationKind string
Payload string
ExtractorRef string
}
type Claim struct {
ID ClaimID
Statement string
ObservationID ObservationID
CitationID CitationID
Verification VerificationState
ID ClaimID
Statement string
ObservationID ObservationID
CitationID CitationID
Verification VerificationState
PropositionID ids.ID
RevisedFrom *ClaimID
RecordedAtUnixMillis int64
}
type Belief struct {
+48
View File
@@ -24,6 +24,8 @@ type HelpMethodKind string
type OutcomeClass string
type ReflectionTriggerKind string
type ReflectionCandidateState string
type PropositionKind string
type BeliefAssessmentState string
const (
VerificationUnverified VerificationState = "UNVERIFIED"
@@ -73,6 +75,12 @@ const (
ReflectionCandidateEvaluating ReflectionCandidateState = "EVALUATING"
ReflectionCandidateSuperseded ReflectionCandidateState = "SUPERSEDED"
ReflectionCandidateResolved ReflectionCandidateState = "RESOLVED"
PropositionWorldFacet PropositionKind = "WORLD_FACET"
PropositionWorldRelation PropositionKind = "WORLD_RELATION"
PropositionOwnerDefined PropositionKind = "OWNER_DEFINED"
PropositionTextual PropositionKind = "TEXTUAL"
BeliefAssessmentCurrent BeliefAssessmentState = "CURRENT"
BeliefAssessmentSuperseded BeliefAssessmentState = "SUPERSEDED"
)
// SystemExpectation records what Domovoy expected a meaningful help path to
@@ -114,6 +122,15 @@ type ExperienceRecord struct {
VerificationEvidence []EvidenceRef
ConfidenceMillis int64
RecordedAtUnixMillis int64
ProblemStatement string
AttemptRef string
WhyAttempted string
ApplicabilityConditions []string
ContextScopeRef string
EpisodeRef string
GoalRef string
TaskRef string
WhySucceededOrFailed string
}
// ReflectionCandidate records that broader reflection may be warranted.
@@ -134,3 +151,34 @@ type ReflectionCandidate struct {
ClaimedBy string
ClaimExpiresAtUnixMillis int64
}
type Proposition struct {
ID ids.ID
Kind PropositionKind
SubjectRef string
PredicateRef string
ObjectValue string
ObjectRef string
Qualifiers []string
TextFallback string
}
type BeliefAssessment struct {
ID ids.ID
ClaimID ClaimID
Revision ids.Revision
State BeliefAssessmentState
ConfidenceMillis int64
EvidenceRefs []EvidenceRef
AssessedAtUnixMillis int64
}
type EpisodeID ids.ID
type Episode struct {
ID EpisodeID
PrincipalID string
ExperienceRefs []ExperienceRecordID
Summary string
Outcome OutcomeClass
RecordedAtUnixMillis int64
}
+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 {
+103 -43
View File
@@ -25,51 +25,96 @@ type SemanticApplicability string
type SemanticCandidateSource string
type SemanticCandidateSelectionState string
type InformationRequestKind string
type ConfigurationCommandKind string
type SpeakerAttribution struct {
SourceRef string
PrincipalCandidate string
ConfidenceMillis int64
Authenticated bool
ObservationRef string
}
type IngressModality string
const (
ChannelWebChat Channel = "WEB_CHAT"
ConversationOpen ConversationState = "OPEN"
ConversationArchived ConversationState = "ARCHIVED"
SpeechActDirective SpeechAct = "DIRECTIVE"
SpeechActAssertion SpeechAct = "ASSERTION"
SpeechActDecision SpeechAct = "DECISION"
SpeechActPreference SpeechAct = "PREFERENCE"
SpeechActQuestion SpeechAct = "QUESTION"
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
SpeechActCorrection SpeechAct = "CORRECTION"
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
SpeechActConfirmation SpeechAct = "CONFIRMATION"
SpeechActRejection SpeechAct = "REJECTION"
SpeechActCancellation SpeechAct = "CANCELLATION"
SpeechActConversational SpeechAct = "CONVERSATIONAL"
SpeechActUnknown SpeechAct = "UNKNOWN"
InterpretationRecorded InterpretationState = "RECORDED"
InterpretationInterpreted InterpretationState = "INTERPRETED"
RelationCorrects InteractionRelationKind = "CORRECTS"
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
RelationClarifies InteractionRelationKind = "CLARIFIES"
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
ProjectRoleRelated ProjectReferenceRole = "RELATED"
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
ClarificationRequired ClarificationState = "REQUIRED"
SemanticApplicable SemanticApplicability = "APPLICABLE"
SemanticLowConfidence SemanticApplicability = "LOW_CONFIDENCE"
SemanticMixedIntent SemanticApplicability = "MIXED_INTENT"
SemanticNeedsBroaderInterpret SemanticApplicability = "NEEDS_BROADER_INTERPRETATION"
SemanticCandidateClarify SemanticApplicability = "CLARIFICATION_REQUIRED"
CandidateSourceDeterministic SemanticCandidateSource = "DETERMINISTIC_V2"
CandidateSourceCognitive SemanticCandidateSource = "COGNITIVE_V1"
CandidateSelected SemanticCandidateSelectionState = "SELECTED"
CandidateObserved SemanticCandidateSelectionState = "OBSERVED"
CandidateRejected SemanticCandidateSelectionState = "REJECTED"
InformationRequestCapabilityInventory InformationRequestKind = "CAPABILITY_INVENTORY"
InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY"
InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS"
InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY"
IngressText IngressModality = "TEXT"
IngressAudio IngressModality = "AUDIO"
IngressImage IngressModality = "IMAGE"
IngressVideo IngressModality = "VIDEO"
IngressDevice IngressModality = "DEVICE"
)
type IngressEnvelope struct {
ID string
Modality IngressModality
SourceRef string
DeviceRef string
LocationRef string
PrincipalID string
ArtifactRef string
ObservationRef string
ProviderRef string
ModelRef string
ModelVersion string
ConfidenceMillis int64
Degraded bool
RecordedAtUnixMillis int64
}
func (e IngressEnvelope) Valid() bool {
return e.ID != "" && e.Modality != "" && e.SourceRef != "" && e.ConfidenceMillis >= 0 && e.ConfidenceMillis <= 1000 && e.RecordedAtUnixMillis > 0 && (!e.Degraded || e.ObservationRef == "")
}
const (
ChannelWebChat Channel = "WEB_CHAT"
ConversationOpen ConversationState = "OPEN"
ConversationArchived ConversationState = "ARCHIVED"
SpeechActDirective SpeechAct = "DIRECTIVE"
SpeechActAssertion SpeechAct = "ASSERTION"
SpeechActDecision SpeechAct = "DECISION"
SpeechActPreference SpeechAct = "PREFERENCE"
SpeechActQuestion SpeechAct = "QUESTION"
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
SpeechActCorrection SpeechAct = "CORRECTION"
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
SpeechActConfirmation SpeechAct = "CONFIRMATION"
SpeechActRejection SpeechAct = "REJECTION"
SpeechActCancellation SpeechAct = "CANCELLATION"
SpeechActConversational SpeechAct = "CONVERSATIONAL"
SpeechActUnknown SpeechAct = "UNKNOWN"
InterpretationRecorded InterpretationState = "RECORDED"
InterpretationInterpreted InterpretationState = "INTERPRETED"
RelationCorrects InteractionRelationKind = "CORRECTS"
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
RelationClarifies InteractionRelationKind = "CLARIFIES"
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
ProjectRoleRelated ProjectReferenceRole = "RELATED"
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
ClarificationRequired ClarificationState = "REQUIRED"
SemanticApplicable SemanticApplicability = "APPLICABLE"
SemanticLowConfidence SemanticApplicability = "LOW_CONFIDENCE"
SemanticMixedIntent SemanticApplicability = "MIXED_INTENT"
SemanticNeedsBroaderInterpret SemanticApplicability = "NEEDS_BROADER_INTERPRETATION"
SemanticCandidateClarify SemanticApplicability = "CLARIFICATION_REQUIRED"
CandidateSourceDeterministic SemanticCandidateSource = "DETERMINISTIC_V2"
CandidateSourceCognitive SemanticCandidateSource = "COGNITIVE_V1"
CandidateSelected SemanticCandidateSelectionState = "SELECTED"
CandidateObserved SemanticCandidateSelectionState = "OBSERVED"
CandidateRejected SemanticCandidateSelectionState = "REJECTED"
InformationRequestCapabilityInventory InformationRequestKind = "CAPABILITY_INVENTORY"
InformationRequestLocalCapabilityInventory InformationRequestKind = "LOCAL_CAPABILITY_INVENTORY"
InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY"
InformationRequestCognitiveCurrentModel InformationRequestKind = "COGNITIVE_CURRENT_MODEL"
InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS"
InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY"
InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY"
InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE"
InformationRequestResearchFailureExplanation InformationRequestKind = "RESEARCH_FAILURE_EXPLANATION"
ConfigurationCommandSet ConfigurationCommandKind = "SET"
ConfigurationCommandQuery ConfigurationCommandKind = "QUERY"
)
type Conversation struct {
@@ -101,6 +146,18 @@ type OriginalHumanIntent struct {
RecordedAtUnixMillis int64
Text string
}
// ConfigurationCommand is Interaction-owned intent. Configuration remains
// the only owner allowed to materialise a SettingAssignment.
type ConfigurationCommand struct {
Kind ConfigurationCommandKind
PrincipalID string
DefinitionRef string
TypedValue string
TemporaryOverride bool
SessionRef string
SourceRef string
}
type InteractionRelation struct {
ID InteractionRelationID
FromUtteranceID HumanUtteranceID
@@ -168,6 +225,7 @@ type ChatMessageResponse struct {
InterpretationState InterpretationState
NeedsCognition bool
FallbackText string
AssistantName string
ActiveProjectName string
ProjectProvenance situation.ProjectResolutionProvenance
Clarification ClarificationState
@@ -184,4 +242,6 @@ type ChatMessageResponse struct {
DelegationID string
DelegationState string
Reminder *continuity.ProspectiveItem
GeneratedImageContentType string
GeneratedImageBase64 string
}
+9
View File
@@ -0,0 +1,9 @@
# SELF contract boundary
SELF owns evidence-backed competence, weakness, dependency, learning, and
self-evaluation records. Raw evidence remains Epistemics-owned; capability
health remains Capability-owned; tasks and development remain Work-owned;
release switching remains Supervisor/runtime-owned.
SELF is observational/evaluative and is not a mandatory request-path
orchestrator or an authority source.
+25
View File
@@ -0,0 +1,25 @@
package self
type BenchmarkCaseRef string
type PersonalBenchmark struct {
ID string
PrincipalID string
ApprovedCaseRefs []BenchmarkCaseRef
GoalProfile []string
PreferenceEvidenceRefs []SemanticReference
RecordedAt int64
}
type BenchmarkEvaluation struct {
CaseRef BenchmarkCaseRef
Path string
QualityMillis int64
ReliabilityMillis int64
LatencyMillis int64
CostMicros int64
CorrectionCount int64
EvidenceRefs []SemanticReference
}
func (e BenchmarkEvaluation) Valid() bool {
return e.CaseRef != "" && len(e.EvidenceRefs) > 0 && e.QualityMillis >= 0 && e.ReliabilityMillis >= 0 && e.CorrectionCount >= 0
}
+7
View File
@@ -0,0 +1,7 @@
package self
import "git.cin.su/m/Assistant/platform/ids"
// SemanticReference lets SELF cite evidence and capabilities without owning
// their canonical state.
type SemanticReference = ids.SemanticRef
+92
View File
@@ -0,0 +1,92 @@
package self
import "fmt"
type AssessmentID string
type LearningLedgerEntryID string
type AssessmentState string
const (
AssessmentCandidate AssessmentState = "CANDIDATE"
AssessmentConfirmed AssessmentState = "CONFIRMED"
AssessmentMonitoring AssessmentState = "MONITORING"
AssessmentResolved AssessmentState = "RESOLVED"
)
type CompetenceAssessment struct {
ID AssessmentID
PrincipalID string
SubjectRef SemanticReference
Revision uint64
ContextMatcher []string
QualityMillis int64
ReliabilityMillis int64
ConfidenceMillis int64
EvidenceRefs []SemanticReference
LastValidatedAt int64
AssessedAt int64
SupersedesRef *SemanticReference
}
type WeaknessPattern struct {
ID AssessmentID
PrincipalID string
Revision uint64
PatternKey string
ContextMatcher []string
FailureMode string
DetectionEvidenceRefs []SemanticReference
State AssessmentState
RecordedAt int64
}
type DependencyAssessment struct {
ID AssessmentID
SubjectRef SemanticReference
DependencyRef SemanticReference
ImpactClass string
CurrentEffect string
EvidenceRefs []SemanticReference
AssessedAt int64
}
type ImprovementNeed struct {
ID AssessmentID
PrincipalID string
SubjectRef SemanticReference
ProblemSummary string
EvidenceRefs []SemanticReference
ExpectedBenefit string
Priority int
State string
CreatedAt int64
}
type LearningLedgerEntry struct {
ID LearningLedgerEntryID
PrincipalID string
ImprovementNeedRef *SemanticReference
ObservedProblem string
EvidenceRefs []SemanticReference
Diagnosis string
Hypothesis string
ProposedChangeRef SemanticReference
ExpectedImprovement string
ActualEvaluationRefs []SemanticReference
DevelopmentGateRef *SemanticReference
BeforeVersionRefs []SemanticReference
AfterVersionRefs []SemanticReference
Decision string
SideEffects []string
RollbackRef *SemanticReference
RecordedAt int64
}
func (a CompetenceAssessment) Validate() error {
if a.ID == "" || a.PrincipalID == "" || !a.SubjectRef.Valid() || a.Revision < 1 || a.ConfidenceMillis < 0 || a.ConfidenceMillis > 1000 || len(a.EvidenceRefs) == 0 || a.AssessedAt < 1 {
return fmt.Errorf("competence requires evidence")
}
return nil
}
func (l LearningLedgerEntry) Validate() error {
if l.ID == "" || l.PrincipalID == "" || l.ObservedProblem == "" || len(l.EvidenceRefs) == 0 || l.Decision == "" || l.RecordedAt < 1 {
return fmt.Errorf("learning ledger entry is incomplete")
}
return nil
}
+141
View File
@@ -0,0 +1,141 @@
package situation
import (
"fmt"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/platform/clock"
"git.cin.su/m/Assistant/platform/ids"
)
type ContextScopeID ids.StableID
type FocusFrameID ids.StableID
type FocusTransitionID ids.StableID
type FocusRevision ids.Revision
type RoleKey string
type MatchPolicy string
type FocusFrameState string
type FocusProvenance string
type FocusTransitionOperation string
const (
MatchRequired MatchPolicy = "REQUIRED"
MatchPreferred MatchPolicy = "PREFERRED"
MatchRelated MatchPolicy = "RELATED"
MatchExcluded MatchPolicy = "EXCLUDED"
RolePerson RoleKey = "PERSON"
RoleOrganization RoleKey = "ORGANIZATION"
RoleFamily RoleKey = "FAMILY"
RoleEnvironment RoleKey = "ENVIRONMENT"
RoleLocation RoleKey = "LOCATION"
RoleSystem RoleKey = "SYSTEM"
RoleDatabase RoleKey = "DATABASE"
RoleRepository RoleKey = "REPOSITORY"
RoleBranch RoleKey = "BRANCH"
RoleVersion RoleKey = "VERSION"
RoleActivity RoleKey = "ACTIVITY"
RoleProject RoleKey = "PROJECT"
RoleActiveEntity RoleKey = "ACTIVE_ENTITY"
FocusCandidate FocusFrameState = "CANDIDATE"
FocusActive FocusFrameState = "ACTIVE"
FocusSuspended FocusFrameState = "SUSPENDED"
FocusExpired FocusFrameState = "EXPIRED"
FocusSuperseded FocusFrameState = "SUPERSEDED"
FocusExplicitHuman FocusProvenance = "EXPLICIT_HUMAN"
FocusUserConfirmed FocusProvenance = "USER_CONFIRMED"
FocusInherited FocusProvenance = "INHERITED"
FocusEntityReference FocusProvenance = "ENTITY_REFERENCE"
FocusTaskReference FocusProvenance = "TASK_REFERENCE"
FocusRepositoryReference FocusProvenance = "REPOSITORY_REFERENCE"
FocusEnvironmentObservation FocusProvenance = "ENVIRONMENT_OBSERVATION"
FocusInferred FocusProvenance = "INFERRED"
TransitionActivate FocusTransitionOperation = "ACTIVATE"
TransitionSuspend FocusTransitionOperation = "SUSPEND"
TransitionResume FocusTransitionOperation = "RESUME"
TransitionSupersede FocusTransitionOperation = "SUPERSEDE"
TransitionExpire FocusTransitionOperation = "EXPIRE"
)
type ContextBinding struct {
RoleKey RoleKey
TargetRef SemanticReference
MatchPolicy MatchPolicy
Provenance string
ConfidenceMillis int64
SourceRef *SemanticReference
}
type ContextScope struct {
ID ContextScopeID
PrincipalID identity.PrincipalID
Revision FocusRevision
Bindings []ContextBinding
RecordedAt clock.Instant
}
type FocusFrame struct {
ID FocusFrameID
PrincipalID identity.PrincipalID
ActorID identity.ActorID
Revision FocusRevision
AuthorityScopeRef authority.ScopeID
ContextScopeRef SemanticReference
GoalRef *work.GoalID
TaskRef *work.TaskID
SourceUtteranceRef string
Provenance FocusProvenance
ConfidenceMillis int64
State FocusFrameState
ParentFrameRef *FocusFrameID
ActivatedAt clock.Instant
SuspendedAt *clock.Instant
ExpiredAt *clock.Instant
RecordedAt clock.Instant
}
type FocusTransition struct {
TransitionID FocusTransitionID
FrameID FocusFrameID
FromRevision FocusRevision
ToRevision FocusRevision
Operation FocusTransitionOperation
CauseRef string
RecordedAt clock.Instant
}
func (s ContextScope) Validate() error {
if s.ID == "" || s.PrincipalID == "" || !ids.Revision(s.Revision).Valid() || !s.RecordedAt.Valid() {
return fmt.Errorf("invalid context scope")
}
for _, b := range s.Bindings {
if b.RoleKey == "" || !b.TargetRef.Valid() || !validMatchPolicy(b.MatchPolicy) {
return fmt.Errorf("invalid context binding")
}
}
return nil
}
func (f FocusFrame) Validate() error {
if f.ID == "" || f.PrincipalID == "" || f.ActorID == "" || !ids.Revision(f.Revision).Valid() || !f.ContextScopeRef.Valid() || f.State == "" || f.Provenance == "" || !f.RecordedAt.Valid() {
return fmt.Errorf("invalid focus frame")
}
return nil
}
func (t FocusTransition) Validate() error {
if t.TransitionID == "" || t.FrameID == "" || !ids.Revision(t.FromRevision).Valid() || !ids.Revision(t.ToRevision).Valid() || t.Operation == "" || !t.RecordedAt.Valid() {
return fmt.Errorf("invalid focus transition")
}
return nil
}
func validMatchPolicy(p MatchPolicy) bool {
return p == MatchRequired || p == MatchPreferred || p == MatchRelated || p == MatchExcluded
}
+22
View File
@@ -0,0 +1,22 @@
package situation_test
import (
"testing"
"git.cin.su/m/Assistant/contracts/identity"
contract "git.cin.su/m/Assistant/contracts/situation"
"git.cin.su/m/Assistant/platform/clock"
"git.cin.su/m/Assistant/platform/ids"
)
func TestFocusV2ContractsValidateTypedScopeAndFrame(t *testing.T) {
rev := ids.Revision(1)
scope := contract.ContextScope{ID: "scope", PrincipalID: identity.PrincipalID("company-a"), Revision: contract.FocusRevision(rev), RecordedAt: clock.Instant(1), Bindings: []contract.ContextBinding{{RoleKey: contract.RoleOrganization, TargetRef: contract.SemanticReference{OwnerNamespace: "world", SemanticKind: "organization", StableID: "a"}, MatchPolicy: contract.MatchRequired}}}
if err := scope.Validate(); err != nil {
t.Fatal(err)
}
frame := contract.FocusFrame{ID: "frame", PrincipalID: scope.PrincipalID, ActorID: "actor", Revision: 1, ContextScopeRef: contract.SemanticReference{OwnerNamespace: "situation", SemanticKind: "context_scope", StableID: ids.StableID(scope.ID), Revision: &rev}, State: contract.FocusActive, Provenance: contract.FocusExplicitHuman, RecordedAt: 1}
if err := frame.Validate(); err != nil {
t.Fatal(err)
}
}
+7
View File
@@ -0,0 +1,7 @@
package situation
import "git.cin.su/m/Assistant/platform/ids"
// SemanticReference keeps Situation references transport-neutral while
// ContextScope semantics remain owned by Situation.
type SemanticReference = ids.SemanticRef
+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
}
+28
View File
@@ -0,0 +1,28 @@
package work
import "git.cin.su/m/Assistant/contracts/world"
type FreshnessRequirementMode string
type FreshnessFallbackPolicy string
const (
FreshnessAny FreshnessRequirementMode = "ANY"
FreshnessPreferCurrent FreshnessRequirementMode = "PREFER_CURRENT"
FreshnessRequireCurrent FreshnessRequirementMode = "REQUIRE_CURRENT"
FreshnessRequireWithinAge FreshnessRequirementMode = "REQUIRE_WITHIN_AGE"
FreshnessRequireVersionMatch FreshnessRequirementMode = "REQUIRE_VERSION_MATCH"
FreshnessLiveRequired FreshnessRequirementMode = "LIVE_REQUIRED"
FallbackAllowStaleDisclosure FreshnessFallbackPolicy = "ALLOW_STALE_WITH_DISCLOSURE"
FallbackHypothesisOnly FreshnessFallbackPolicy = "USE_AS_HYPOTHESIS_ONLY"
FallbackWaitForSource FreshnessFallbackPolicy = "WAIT_FOR_SOURCE"
FallbackAskOwner FreshnessFallbackPolicy = "ASK_OWNER"
FallbackStopUnknown FreshnessFallbackPolicy = "STOP_WITH_UNKNOWN"
)
type TaskFreshnessRequirement struct {
Mode FreshnessRequirementMode
MaximumAgeMillis *int64
RequiredVersionRefs []world.SemanticReference
RevalidationRequired bool
FallbackPolicy FreshnessFallbackPolicy
}
+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)
}
+51
View File
@@ -0,0 +1,51 @@
package work
type LongRunningWorkID string
type LongRunningState string
const (
LongRunningProposed LongRunningState = "PROPOSED"
LongRunningRunning LongRunningState = "RUNNING"
LongRunningPaused LongRunningState = "PAUSED"
LongRunningBlocked LongRunningState = "BLOCKED"
LongRunningCompleted LongRunningState = "COMPLETED"
LongRunningExhausted LongRunningState = "EXHAUSTED"
LongRunningCancelled LongRunningState = "CANCELLED"
)
type Investigation struct {
ID LongRunningWorkID
Objective string
HypothesisRefs []EvidenceRef
AttemptRefs []AgentRunID
FindingRefs []EvidenceRef
Blockers []string
CheckpointRefs []AgentCheckpointID
NextStep string
State LongRunningState
Revision int64
TimeBudgetMillis int64
CostBudgetMicros int64
MaxAttempts int
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
}
+32
View File
@@ -0,0 +1,32 @@
package work
type PerspectiveID string
type PerspectiveSpec struct {
ID PerspectiveID
ObjectiveLens string
FocusRef string
RetrievalProfileRef string
SkillRefs []string
ToolRefs []string
EvaluationCriteria []string
CapabilityRequirements []string
ExpectedOutputShape string
Critic bool
}
type SpecialistAssignment struct {
Perspective PerspectiveSpec
SubtaskRef string
FocusRef string
SkillRefs []string
ToolRefs []string
EvidenceRefs []EvidenceRef
}
type TeamDecision struct {
UseTeam bool
Reason string
Assignments []SpecialistAssignment
}
func (p PerspectiveSpec) Valid() bool {
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
}
+9
View File
@@ -0,0 +1,9 @@
# World contract boundary
World owns typed, versioned referents and vocabulary: `WorldObject`, kind and
facet/relation definitions, aliases, identity-resolution decisions, and
merge/split lineage. It does not own observations, claims, beliefs, or the
truth of values about a referent; those remain Epistemics-owned evidence.
Cross-owner references are typed semantic references. They do not create
cross-owner foreign keys, joins, writes, or a generic `Entity(any)` API.
+322
View File
@@ -0,0 +1,322 @@
package world
import (
"fmt"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/clock"
"git.cin.su/m/Assistant/platform/ids"
)
// SemanticReference is the transport envelope used by World contracts. It is
// a reference only; it does not expose another owner's storage.
type SemanticReference = ids.SemanticRef
// VersionBinding is a typed applicability constraint. Absence of VersionRef
// means unknown, never an implicit "latest".
type VersionMatchMode string
const (
VersionExact VersionMatchMode = "EXACT"
VersionCompatibleFamily VersionMatchMode = "COMPATIBLE_FAMILY"
VersionAtOrBefore VersionMatchMode = "AT_OR_BEFORE"
VersionUnknown VersionMatchMode = "UNKNOWN"
)
type VersionBinding struct {
SubjectRef SemanticReference
VersionRef SemanticReference
MatchMode VersionMatchMode
}
type EnvironmentReference struct {
EnvironmentRef SemanticReference
VersionRef *SemanticReference
}
func (b VersionBinding) Validate() error {
if !b.SubjectRef.Valid() || !b.VersionRef.Valid() {
return fmt.Errorf("version binding references are invalid")
}
if b.MatchMode != VersionExact && b.MatchMode != VersionCompatibleFamily && b.MatchMode != VersionAtOrBefore && b.MatchMode != VersionUnknown {
return fmt.Errorf("version binding match mode is invalid")
}
return nil
}
func (r EnvironmentReference) Validate() error {
if !r.EnvironmentRef.Valid() {
return fmt.Errorf("environment reference is invalid")
}
if r.VersionRef != nil && !r.VersionRef.Valid() {
return fmt.Errorf("environment version reference is invalid")
}
return nil
}
type PrincipalRealm string
const (
RealmPersonal PrincipalRealm = "PERSONAL"
RealmShared PrincipalRealm = "SHARED"
RealmPublic PrincipalRealm = "PUBLIC"
RealmSystem PrincipalRealm = "SYSTEM"
)
type SensitivityClass string
const (
SensitivityPublic SensitivityClass = "PUBLIC"
SensitivityPrivate SensitivityClass = "PRIVATE"
SensitivityRestricted SensitivityClass = "RESTRICTED"
SensitivitySecretReference SensitivityClass = "SECRET_REFERENCE"
)
type ValueTypeKind string
const (
ValueString ValueTypeKind = "STRING"
ValueInteger ValueTypeKind = "INTEGER"
ValueDecimal ValueTypeKind = "DECIMAL"
ValueBoolean ValueTypeKind = "BOOLEAN"
ValueTime ValueTypeKind = "TIME"
ValueDuration ValueTypeKind = "DURATION"
ValueEnum ValueTypeKind = "ENUM"
ValueReference ValueTypeKind = "REFERENCE"
ValueBinaryRef ValueTypeKind = "BINARY_REF"
ValueList ValueTypeKind = "LIST"
ValueStruct ValueTypeKind = "STRUCT"
)
type ValueField struct {
Name string
Type ValueTypeSpec
}
type ValueTypeSpec struct {
Kind ValueTypeKind
Element *ValueTypeSpec
Fields []ValueField
EnumValues []string
ReferenceKind string
}
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
type LineageID ids.StableID
type LifecycleState string
const (
LifecycleActive LifecycleState = "ACTIVE"
LifecycleMerged LifecycleState = "MERGED"
LifecycleSplit LifecycleState = "SPLIT"
LifecycleRetired LifecycleState = "RETIRED"
LifecycleUnknownIdentity LifecycleState = "UNKNOWN_IDENTITY"
)
type WorldObject struct {
ID WorldObjectID
PrincipalID identity.PrincipalID
KindDefinitionRef SemanticReference
KindRevision ids.Revision
Lifecycle LifecycleState
CreatedAt clock.Instant
}
type WorldObjectKindDefinition struct {
ID KindDefinitionID
KindKey string
Revision ids.Revision
DisplayName string
ParentKindRef *SemanticReference
Lifecycle LifecycleState
DefinitionDigest string
RecordedAt clock.Instant
}
type WorldAlias struct {
ID WorldAliasID
PrincipalID identity.PrincipalID
ObjectID WorldObjectID
Alias string
RecordedAt clock.Instant
}
type FacetDefinition struct {
ID FacetDefinitionID
KindKey string
Revision ids.Revision
FacetKey string
ValueType ValueTypeSpec
DefinitionDigest string
RecordedAt clock.Instant
}
type RelationDefinition struct {
ID RelationDefinitionID
KindKey string
Revision ids.Revision
RelationKey string
TargetKindKey string
DefinitionDigest string
RecordedAt clock.Instant
}
type ResolutionDecision string
const (
ResolutionSame ResolutionDecision = "SAME"
ResolutionDistinct ResolutionDecision = "DISTINCT"
ResolutionMerge ResolutionDecision = "MERGE"
ResolutionSplit ResolutionDecision = "SPLIT"
ResolutionUnresolved ResolutionDecision = "UNRESOLVED"
)
type IdentityResolutionCandidate struct {
ObjectRef SemanticReference
Evidence []SemanticReference
Confidence int64
}
type IdentityResolutionDecisionRecord struct {
ID ResolutionDecisionID
PrincipalID identity.PrincipalID
Candidates []IdentityResolutionCandidate
Decision ResolutionDecision
Evidence []SemanticReference
ConfidenceMillis int64
DecidedAt clock.Instant
Revision ids.Revision
}
type WorldLineage struct {
ID LineageID
Operation ResolutionDecision
SourceRefs []SemanticReference
SuccessorRefs []SemanticReference
RecordedAt clock.Instant
}
func (w WorldObject) Validate() error {
if w.ID == "" || !w.KindDefinitionRef.Valid() || !w.KindRevision.Valid() || !validLifecycle(w.Lifecycle) || !w.CreatedAt.Valid() {
return fmt.Errorf("world object is incomplete")
}
return nil
}
func (k WorldObjectKindDefinition) Validate() error {
if k.ID == "" || k.KindKey == "" || !k.Revision.Valid() || k.DisplayName == "" || !validLifecycle(k.Lifecycle) || k.DefinitionDigest == "" || !k.RecordedAt.Valid() {
return fmt.Errorf("world kind definition is incomplete")
}
if k.ParentKindRef != nil && !k.ParentKindRef.Valid() {
return fmt.Errorf("world kind parent reference is invalid")
}
return nil
}
func (a WorldAlias) Validate() error {
if a.ID == "" || a.ObjectID == "" || a.Alias == "" || !a.RecordedAt.Valid() {
return fmt.Errorf("world alias is incomplete")
}
return nil
}
func (f FacetDefinition) Validate() error {
if f.ID == "" || f.KindKey == "" || !f.Revision.Valid() || f.FacetKey == "" || f.DefinitionDigest == "" || !f.RecordedAt.Valid() {
return fmt.Errorf("facet definition is incomplete")
}
return f.ValueType.Validate()
}
func (r RelationDefinition) Validate() error {
if r.ID == "" || r.KindKey == "" || !r.Revision.Valid() || r.RelationKey == "" || r.TargetKindKey == "" || r.DefinitionDigest == "" || !r.RecordedAt.Valid() {
return fmt.Errorf("relation definition is incomplete")
}
return nil
}
func (d IdentityResolutionDecisionRecord) Validate() error {
if d.ID == "" || !validDecision(d.Decision) || len(d.Candidates) == 0 || d.ConfidenceMillis < 0 || !d.DecidedAt.Valid() || !d.Revision.Valid() {
return fmt.Errorf("identity resolution decision is incomplete")
}
for _, candidate := range d.Candidates {
if !candidate.ObjectRef.Valid() || candidate.Confidence < 0 || candidate.Confidence > 1000 {
return fmt.Errorf("identity resolution candidate is invalid")
}
for _, evidence := range candidate.Evidence {
if !evidence.Valid() {
return fmt.Errorf("identity resolution evidence reference is invalid")
}
}
}
return nil
}
func (l WorldLineage) Validate() error {
if l.ID == "" || (l.Operation != ResolutionMerge && l.Operation != ResolutionSplit) || len(l.SourceRefs) == 0 || len(l.SuccessorRefs) == 0 || !l.RecordedAt.Valid() {
return fmt.Errorf("world lineage is incomplete")
}
return nil
}
func validLifecycle(value LifecycleState) bool {
return value == LifecycleActive || value == LifecycleMerged || value == LifecycleSplit || value == LifecycleRetired || value == LifecycleUnknownIdentity
}
func validDecision(value ResolutionDecision) bool {
return value == ResolutionSame || value == ResolutionDistinct || value == ResolutionMerge || value == ResolutionSplit || value == ResolutionUnresolved
}
func (s ValueTypeSpec) Validate() error {
if s.Kind == "" {
return fmt.Errorf("value type kind is required")
}
switch s.Kind {
case ValueList:
if s.Element == nil {
return fmt.Errorf("list element type is required")
}
return s.Element.Validate()
case ValueStruct:
if len(s.Fields) == 0 {
return fmt.Errorf("struct fields are required")
}
seen := map[string]struct{}{}
for _, field := range s.Fields {
if field.Name == "" {
return fmt.Errorf("struct field name is required")
}
if _, ok := seen[field.Name]; ok {
return fmt.Errorf("duplicate struct field %q", field.Name)
}
seen[field.Name] = struct{}{}
if err := field.Type.Validate(); err != nil {
return fmt.Errorf("field %s: %w", field.Name, err)
}
}
case ValueEnum:
if len(s.EnumValues) == 0 {
return fmt.Errorf("enum values are required")
}
case ValueReference:
if s.ReferenceKind == "" {
return fmt.Errorf("reference kind is required")
}
}
return nil
}
+17
View File
@@ -0,0 +1,17 @@
package world
import "testing"
func TestValueTypeSpecValidatesTypedRecursiveShape(t *testing.T) {
spec := ValueTypeSpec{Kind: ValueStruct, Fields: []ValueField{{Name: "name", Type: ValueTypeSpec{Kind: ValueString}}, {Name: "refs", Type: ValueTypeSpec{Kind: ValueList, Element: &ValueTypeSpec{Kind: ValueReference, ReferenceKind: "world.object"}}}}}
if err := spec.Validate(); err != nil {
t.Fatalf("ValueTypeSpec.Validate() = %v", err)
}
}
func TestValueTypeSpecRejectsDuplicateFields(t *testing.T) {
spec := ValueTypeSpec{Kind: ValueStruct, Fields: []ValueField{{Name: "same", Type: ValueTypeSpec{Kind: ValueString}}, {Name: "same", Type: ValueTypeSpec{Kind: ValueString}}}}
if err := spec.Validate(); err == nil {
t.Fatal("duplicate fields were accepted")
}
}
+22
View File
@@ -0,0 +1,22 @@
package world
import (
"testing"
"git.cin.su/m/Assistant/platform/clock"
"git.cin.su/m/Assistant/platform/ids"
)
func TestWorldObjectValidationKeepsReferentSeparateFromClaims(t *testing.T) {
object := WorldObject{ID: "object-1", KindDefinitionRef: ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "kind", StableID: "kind-1"}, KindRevision: 1, Lifecycle: LifecycleActive, CreatedAt: clock.Instant(1)}
if err := object.Validate(); err != nil {
t.Fatalf("WorldObject.Validate() = %v", err)
}
}
func TestIdentityResolutionRequiresEvidenceBackedCandidates(t *testing.T) {
decision := IdentityResolutionDecisionRecord{ID: "decision-1", Decision: ResolutionSame, Candidates: []IdentityResolutionCandidate{{ObjectRef: ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "object", StableID: "object-1"}, Confidence: 900}}, DecidedAt: clock.Instant(1), Revision: 1}
if err := decision.Validate(); err != nil {
t.Fatalf("IdentityResolutionDecisionRecord.Validate() = %v", err)
}
}
@@ -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,82 @@
BEGIN;
CREATE SCHEMA IF NOT EXISTS world;
SET ROLE world_role;
CREATE TABLE IF NOT EXISTS world.kind_definitions (
kind_definition_id TEXT PRIMARY KEY,
kind_key TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
display_name TEXT NOT NULL,
parent_owner_namespace TEXT NOT NULL DEFAULT '',
parent_semantic_kind TEXT NOT NULL DEFAULT '',
parent_stable_id TEXT NOT NULL DEFAULT '',
parent_revision BIGINT,
lifecycle TEXT NOT NULL,
definition_digest TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
UNIQUE (kind_key, revision)
);
CREATE TABLE IF NOT EXISTS world.objects (
world_object_id TEXT PRIMARY KEY,
principal_id TEXT,
kind_owner_namespace TEXT NOT NULL,
kind_semantic_kind TEXT NOT NULL,
kind_stable_id TEXT NOT NULL,
kind_revision BIGINT,
lifecycle TEXT NOT NULL,
created_at_unix_millis BIGINT NOT NULL CHECK (created_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS world.aliases (
world_alias_id TEXT PRIMARY KEY,
principal_id TEXT,
world_object_id TEXT NOT NULL,
alias TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS world.facet_definitions (
facet_definition_id TEXT PRIMARY KEY,
kind_key TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
facet_key TEXT NOT NULL,
value_type JSONB NOT NULL,
definition_digest TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
UNIQUE (kind_key, facet_key, revision)
);
CREATE TABLE IF NOT EXISTS world.relation_definitions (
relation_definition_id TEXT PRIMARY KEY,
kind_key TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
relation_key TEXT NOT NULL,
target_kind_key TEXT NOT NULL,
definition_digest TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
UNIQUE (kind_key, relation_key, revision)
);
CREATE TABLE IF NOT EXISTS world.identity_resolution_decisions (
resolution_decision_id TEXT PRIMARY KEY,
principal_id TEXT,
candidates JSONB NOT NULL,
decision TEXT NOT NULL,
evidence JSONB NOT NULL,
confidence_millis BIGINT NOT NULL CHECK (confidence_millis >= 0),
decided_at_unix_millis BIGINT NOT NULL CHECK (decided_at_unix_millis > 0),
revision BIGINT NOT NULL CHECK (revision > 0)
);
CREATE TABLE IF NOT EXISTS world.lineage (
lineage_id TEXT PRIMARY KEY,
operation TEXT NOT NULL,
source_refs JSONB NOT NULL,
successor_refs JSONB NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,47 @@
BEGIN;
SET ROLE situation_role;
CREATE TABLE IF NOT EXISTS situation.context_scopes (
context_scope_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
bindings JSONB NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS situation.focus_frames (
focus_frame_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
authority_scope_ref TEXT NOT NULL,
context_scope_ref JSONB NOT NULL,
goal_ref TEXT,
task_ref TEXT,
source_utterance_ref TEXT,
provenance TEXT NOT NULL,
confidence_millis BIGINT NOT NULL CHECK (confidence_millis >= 0),
state TEXT NOT NULL,
parent_frame_ref TEXT,
activated_at_unix_millis BIGINT NOT NULL CHECK (activated_at_unix_millis > 0),
suspended_at_unix_millis BIGINT,
expired_at_unix_millis BIGINT,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS situation.focus_transitions (
transition_id TEXT PRIMARY KEY,
focus_frame_id TEXT NOT NULL,
from_revision BIGINT NOT NULL CHECK (from_revision > 0),
to_revision BIGINT NOT NULL CHECK (to_revision > 0),
operation TEXT NOT NULL,
cause_ref TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
);
CREATE INDEX IF NOT EXISTS focus_transitions_frame_order ON situation.focus_transitions(focus_frame_id, recorded_at_unix_millis, transition_id);
GRANT SELECT, INSERT, UPDATE ON situation.context_scopes, situation.focus_frames, situation.focus_transitions TO situation_role;
RESET ROLE;
COMMIT;
@@ -0,0 +1,7 @@
BEGIN;
SET ROLE configuration_role;
CREATE TABLE IF NOT EXISTS configuration.setting_definitions (setting_definition_id TEXT PRIMARY KEY, owner_namespace TEXT NOT NULL, stable_key TEXT NOT NULL, revision BIGINT NOT NULL CHECK(revision>0), value_type TEXT NOT NULL, default_value TEXT, sensitivity TEXT NOT NULL, lifecycle TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL);
CREATE TABLE IF NOT EXISTS configuration.setting_assignments (setting_assignment_id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, definition_ref JSONB NOT NULL, revision BIGINT NOT NULL CHECK(revision>0), selector JSONB NOT NULL, typed_value TEXT, secret_ref TEXT, effective_from_unix_millis BIGINT NOT NULL, effective_until_unix_millis BIGINT, source TEXT NOT NULL, source_ref TEXT NOT NULL, lifecycle TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL);
GRANT SELECT, INSERT, UPDATE ON configuration.setting_definitions, configuration.setting_assignments TO configuration_role;
RESET ROLE;
COMMIT;
@@ -0,0 +1,7 @@
BEGIN;
SET ROLE self_role;
CREATE TABLE IF NOT EXISTS self.competence_assessments (assessment_id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, subject_ref JSONB NOT NULL, revision BIGINT NOT NULL CHECK(revision>0), context_matcher JSONB NOT NULL, confidence_millis BIGINT NOT NULL, evidence_refs JSONB NOT NULL, assessed_at_unix_millis BIGINT NOT NULL);
CREATE TABLE IF NOT EXISTS self.learning_ledger (entry_id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, observed_problem TEXT NOT NULL, evidence_refs JSONB NOT NULL, diagnosis TEXT NOT NULL, hypothesis TEXT NOT NULL, decision TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL);
GRANT SELECT, INSERT, UPDATE ON self.competence_assessments, self.learning_ledger TO self_role;
RESET ROLE;
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;
+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
+3
View File
@@ -102,8 +102,11 @@ services:
LISTEN_ADDR: ":8083"
CORE_RUNTIME_URL: http://core-runtime:8080
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://docker.cin.su:18082/v1}
LOCAL_MEDIUM_PROVIDER_TIMEOUT: ${LOCAL_MEDIUM_PROVIDER_TIMEOUT:-120s}
LOCAL_LIFEBOAT_PROVIDER_URL: ${LOCAL_LIFEBOAT_PROVIDER_URL:-}
LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434
COGNITIVE_ALLOW_CANARY: "true"
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
ports: ["19083:8083"]
+34 -2
View File
@@ -258,6 +258,9 @@ services:
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"
depends_on: [core-runtime, cognitive-lifeboat]
@@ -274,7 +277,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
@@ -296,6 +299,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
@@ -307,7 +333,6 @@ 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]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
@@ -315,6 +340,12 @@ services:
timeout: 3s
retries: 10
creator-web-candidate:
profiles: ["assistant-canary"]
extends:
service: creator-web
restart: "no"
volumes:
assistant_pg:
assistant_nats:
@@ -330,6 +361,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"]
+42
View File
@@ -0,0 +1,42 @@
#!/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"
SUPERVISOR_CONTROL_URL="${SUPERVISOR_CONTROL_URL:-http://127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}}"
REVISION="${1:?candidate revision is required}"
set -a
. "$ENV_FILE"
set +a
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_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 "production canary failed; stable traffic rollback requested" >&2
exit 1
}
compose --profile assistant-canary build --build-arg "SOURCE_REVISION=$REVISION" creator-web-candidate
compose --profile assistant-canary up -d --no-build creator-web-candidate
for attempt in $(seq 1 30); do
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' assistant-creator-web-candidate-1 2>/dev/null || true)"
[[ "$status" == healthy ]] && break
[[ "$attempt" == 30 ]] && fail_rollback
sleep 2
done
control /v1/releases/start -H 'Content-Type: application/json' --data "{\"id\":\"candidate-$REVISION\",\"version\":\"$REVISION\"}" || fail_rollback
control /v1/releases/observe -H 'Content-Type: application/json' --data "{\"release_id\":\"candidate-$REVISION\",\"ready\":true,\"reason\":\"candidate healthcheck passed\",\"observed_at_unix_millis\":$(date +%s%3N)}" || fail_rollback
control /v1/traffic/canary || fail_rollback
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
control /v1/releases/promote || fail_rollback
echo "production canary promoted revision=$REVISION"

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