@hassangameryt/freeflow 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/NOTICE +14 -0
  6. package/README.md +26 -31
  7. package/capabilities/cognitive-routing/SKILL.md +106 -161
  8. package/capabilities/cognitive-routing/references/both-mode.md +36 -0
  9. package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
  10. package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
  11. package/capabilities/compaction/SKILL.md +49 -0
  12. package/capabilities/compaction/references/summary-format.md +239 -0
  13. package/command-surface.json +4 -0
  14. package/gemini-extension.json +1 -1
  15. package/hooks/shared/runtime-context.mjs +28 -30
  16. package/licenses/Apache-2.0.txt +55 -0
  17. package/package.json +22 -9
  18. package/pi-extension/README.md +41 -0
  19. package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
  20. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
  21. package/pi-extension/dist/cognitive-routing/economics.js +45 -0
  22. package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
  23. package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
  24. package/pi-extension/dist/cognitive-routing/gate.js +241 -0
  25. package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
  26. package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
  27. package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
  28. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  31. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
  32. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  33. package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
  34. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
  35. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  36. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
  37. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
  38. package/pi-extension/dist/compaction/carry.js +191 -0
  39. package/pi-extension/dist/compaction/config.js +22 -0
  40. package/pi-extension/dist/compaction/controller.js +471 -0
  41. package/pi-extension/dist/compaction/harness.js +77 -0
  42. package/pi-extension/dist/compaction/results.js +158 -0
  43. package/pi-extension/dist/compaction/thresholds.js +35 -0
  44. package/pi-extension/dist/compaction/tool.js +57 -0
  45. package/pi-extension/dist/host/branch.js +13 -0
  46. package/pi-extension/dist/host/canonical.js +12 -0
  47. package/pi-extension/dist/host/catalog.js +77 -0
  48. package/pi-extension/dist/host/config.js +374 -0
  49. package/pi-extension/dist/host/projection-tags.js +60 -0
  50. package/pi-extension/dist/host/prompts.js +82 -0
  51. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  52. package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
  53. package/pi-extension/dist/host/runtime-state.js +189 -0
  54. package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
  55. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  56. package/pi-extension/dist/host/staging.js +28 -0
  57. package/pi-extension/dist/host/status.js +101 -0
  58. package/pi-extension/dist/index.js +423 -173
  59. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  60. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  61. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  62. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  63. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  64. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  65. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  66. package/pi-extension/dist/provider-support/effort.js +13 -0
  67. package/pi-extension/dist/provider-support/index.js +44 -3
  68. package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
  69. package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
  70. package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
  71. package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
  72. package/pi-extension/dist/provider-support/routes.js +17 -0
  73. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  78. package/pi-extension/dist/tool-execution/background.js +238 -0
  79. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  80. package/pi-extension/dist/tool-execution/config.js +22 -0
  81. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  82. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  83. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  84. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  85. package/pi-extension/dist/tool-execution/tools.js +16 -0
  86. package/pi-extension/freeflow/README.md +9 -0
  87. package/plugin.json +1 -1
  88. package/runtime/prompts/cognitive-routing.md +4 -4
  89. package/runtime/prompts/core.md +22 -75
  90. package/runtime/prompts/interaction-contract.md +15 -8
  91. package/runtime/prompts/tool-execution.md +38 -0
  92. package/runtime/prompts/working-method.md +108 -0
  93. package/skills/action-selection/SKILL.md +39 -47
  94. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  95. package/skills/bypass/SKILL.md +2 -2
  96. package/skills/commit-work/SKILL.md +31 -83
  97. package/skills/commit-work/references/git-state-edges.md +1 -1
  98. package/skills/commit-work/references/simple-push.md +2 -0
  99. package/skills/commit-work/references/staging-decisions.md +1 -1
  100. package/skills/decision-gate/SKILL.md +31 -50
  101. package/skills/design-for-depth/SKILL.md +44 -53
  102. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  103. package/skills/design-for-depth/references/implementation-decisions.md +1 -1
  104. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  105. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  106. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  107. package/skills/diagnose-failure/SKILL.md +39 -24
  108. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  109. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  110. package/skills/discuss/SKILL.md +35 -11
  111. package/skills/discuss/references/discussion-continuity.md +7 -27
  112. package/skills/evaluate-skill/SKILL.md +49 -80
  113. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  114. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  115. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  116. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  117. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  118. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  119. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  120. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  121. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  122. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  123. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  124. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  125. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  126. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  127. package/skills/execute-work/SKILL.md +41 -9
  128. package/skills/execute-work/references/code-practices.md +2 -0
  129. package/skills/execute-work/references/execute-work-edges.md +11 -1
  130. package/skills/handoff/SKILL.md +1 -1
  131. package/skills/handoff/references/handoff-templates.md +1 -1
  132. package/skills/review-artifact/SKILL.md +42 -41
  133. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  134. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  135. package/skills/review-work/SKILL.md +38 -32
  136. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  137. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  138. package/skills/review-work/references/security-risk-lens.md +2 -4
  139. package/skills/setup-freeflow/SKILL.md +3 -1
  140. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  141. package/skills/setup-freeflow/references/host-setup.md +3 -3
  142. package/skills/track-work/SKILL.md +113 -161
  143. package/skills/track-work/references/working-record-format.md +189 -350
  144. package/skills/track-work/scripts/lib/document.mjs +62 -6
  145. package/skills/track-work/scripts/lib/format.mjs +16 -1
  146. package/skills/track-work/scripts/lib/store.mjs +9 -8
  147. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  148. package/skills/track-work/scripts/working-record.mjs +34 -12
  149. package/skills/verify-work/SKILL.md +29 -20
  150. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  151. package/skills/verify-work/references/integration-evidence.md +2 -0
  152. package/skills/verify-work/references/performance-evidence.md +3 -1
  153. package/skills/verify-work/references/test-design.md +3 -1
  154. package/skills/workflow/SKILL.md +65 -21
  155. package/skills/workflow/references/domain-skill-composition.md +5 -3
  156. package/skills/write-docs/SKILL.md +83 -0
  157. package/skills/write-docs/references/documentation-shapes.md +119 -0
  158. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  159. package/skills/write-plan/SKILL.md +40 -40
  160. package/skills/write-plan/references/plan-shapes.md +1 -1
  161. package/skills/write-skill/SKILL.md +107 -147
  162. package/skills/write-skill/references/activation-boundaries.md +2 -0
  163. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  164. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  165. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  166. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  167. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  168. package/skills/write-spec/SKILL.md +34 -41
  169. package/skills/write-spec/references/artifact-standards.md +1 -1
  170. package/skills/write-spec/references/decision-records.md +2 -0
  171. package/skills/write-spec/references/spec-shapes.md +1 -1
  172. package/capabilities/context-virtualization/SKILL.md +0 -103
  173. package/capabilities/conversation-history/SKILL.md +0 -53
  174. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  175. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  176. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
  177. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  178. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  179. package/pi-extension/dist/context-virtualization/runtime.js +0 -378
  180. package/pi-extension/dist/context-virtualization/state.js +0 -57
  181. package/pi-extension/dist/context-virtualization/types.js +0 -10
  182. package/pi-extension/dist/conversation-history/passages.js +0 -66
  183. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  184. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  185. package/pi-extension/dist/conversation-history/search.js +0 -56
  186. package/pi-extension/dist/conversation-history/types.js +0 -1
  187. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  188. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  189. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  190. package/pi-extension/dist/freeflow-context/tool.js +0 -314
  191. package/pi-extension/dist/freeflow-context/types.js +0 -12
  192. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  193. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  194. package/pi-extension/dist/runtime/runtime-context.js +0 -754
  195. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  196. package/pi-extension/dist/session-sources/provenance.js +0 -32
  197. package/pi-extension/dist/ui/text-component.js +0 -57
  198. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  199. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
  200. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
  201. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
  202. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
  203. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
  204. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
  205. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
  206. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
  207. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  208. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  209. package/pi-extension/src/context-virtualization/runtime.ts +0 -428
  210. package/pi-extension/src/context-virtualization/state.ts +0 -79
  211. package/pi-extension/src/context-virtualization/types.ts +0 -92
  212. package/pi-extension/src/conversation-history/passages.ts +0 -81
  213. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  214. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  215. package/pi-extension/src/conversation-history/search.ts +0 -79
  216. package/pi-extension/src/conversation-history/types.ts +0 -31
  217. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  218. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  219. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  220. package/pi-extension/src/freeflow-context/tool.ts +0 -366
  221. package/pi-extension/src/freeflow-context/types.ts +0 -70
  222. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  223. package/pi-extension/src/index.ts +0 -390
  224. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  225. package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
  226. package/pi-extension/src/provider-support/astra/history.ts +0 -145
  227. package/pi-extension/src/provider-support/astra/index.ts +0 -12
  228. package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
  229. package/pi-extension/src/provider-support/index.ts +0 -5
  230. package/pi-extension/src/runtime/request-history.ts +0 -243
  231. package/pi-extension/src/runtime/runtime-context.ts +0 -860
  232. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  233. package/pi-extension/src/session-sources/events.ts +0 -230
  234. package/pi-extension/src/session-sources/provenance.ts +0 -39
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -334
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1652
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cache-reuse/history.test.js +0 -117
  241. package/pi-extension/tests/cache-reuse/native.test.js +0 -91
  242. package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
  243. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
  244. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  245. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
  246. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
  247. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  248. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
  249. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  250. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
  251. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
  252. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
  253. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  254. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
  255. package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
  256. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  257. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
  258. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  259. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  260. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  261. package/pi-extension/tests/fixtures/routing-native.js +0 -191
  262. package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
  263. package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
  264. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
  265. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  266. package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
  267. package/pi-extension/tests/provider-support/astra.test.js +0 -306
  268. package/pi-extension/tests/provider-support/cli.test.js +0 -92
  269. package/pi-extension/tests/provider-support/native.test.js +0 -183
  270. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  271. package/pi-extension/tsconfig.json +0 -16
  272. package/runtime/prompts/context-virtualization.md +0 -3
  273. package/runtime/prompts/conversation-history.md +0 -3
  274. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  275. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
  276. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +0 -0
@@ -1,2241 +0,0 @@
1
- import { estimateRequest } from "./budget.js";
2
- import { annotateSources } from "../session-sources/provenance.js";
3
- import { ROUTING_SCHEMAS } from "./schemas.js";
4
- import { randomUUID } from "node:crypto";
5
- import { existsSync, realpathSync } from "node:fs";
6
- import { relative, resolve as resolvePath } from "node:path";
7
- import { EventStore, type SessionReader } from "../session-sources/events.js";
8
- import { Sources, bodyHash, textRef, isTaskEvidence, type Source } from "../session-sources/sources.js";
9
- import { pairFromProfile, resolveCognitiveRoutingState, type CognitiveRoutingCapabilityState } from "./config.js";
10
- import { prepareView, changeSelection, representationProblems, type PreparedView } from "./projection.js";
11
- import { initialState } from "./state.js";
12
- import {
13
- ROUTING_ENTRY,
14
- ROUTING_MESSAGE,
15
- PROFILES,
16
- RoutingError,
17
- canonical,
18
- emptySelection,
19
- idFor,
20
- isWorkerProfile,
21
- requireCondition as check,
22
- samePair,
23
- workersForDelegation,
24
- type Profile,
25
- type WorkerProfile,
26
- type View,
27
- type Pair,
28
- type State,
29
- type Handoff,
30
- type Recovery,
31
- type NativeEntry,
32
- type EventData,
33
- type Execution,
34
- type Problem,
35
- } from "./types.js";
36
-
37
- export const ROUTING_TOOLS = ["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project"] as const;
38
- const HANDOFF_TOOLS = new Set(["freeflow_delegate", "freeflow_return"]);
39
- interface Turn {
40
- id: string;
41
- profile: View;
42
- assignmentId?: string;
43
- recoveryId?: string;
44
- basisUserEntryId: string | null;
45
- before: Set<string>;
46
- message?: any;
47
- bound?: string;
48
- sourceFingerprint?: string;
49
- opened?: boolean;
50
- pair: Pair;
51
- }
52
- interface Subject {
53
- token: string;
54
- revision: number;
55
- store: EventStore | undefined;
56
- leafId: string | null;
57
- userId: string | null;
58
- }
59
- export class RoutingRuntime {
60
- private store?: EventStore;
61
- private ctx: any;
62
- private capability?: CognitiveRoutingCapabilityState;
63
- private token = randomUUID();
64
- private turn?: Turn;
65
- private messages: any[] = [];
66
- private error?: string;
67
- private projectionError?: string;
68
- private suppressed = false;
69
- private applying?: Pair;
70
- private externalChange = false;
71
- private targetSignature?: string;
72
- private operations: Promise<unknown> = Promise.resolve();
73
- private revision = 0;
74
- private receipts = new Map<string, { input: string; value: any }>();
75
- private pages = new Map<
76
- string,
77
- {
78
- scope: string;
79
- basis: string | null;
80
- assignment?: string;
81
- compaction?: string;
82
- rows: any[];
83
- size: number;
84
- summary?: any;
85
- }
86
- >();
87
- private manualHold?: Profile;
88
- private automaticControl = false;
89
- private sourceCache?: { entries: NativeEntry[]; authors: number; source: Sources };
90
- constructor(
91
- private readonly pi: any,
92
- private readonly packageRoots: readonly string[] = [],
93
- ) {}
94
- private subject(): Subject {
95
- const branch = this.store?.reader.getBranch() ?? [];
96
- return {
97
- token: this.token,
98
- revision: this.revision,
99
- store: this.store,
100
- leafId: branch.at(-1)?.id ?? null,
101
- userId: branch.filter((e) => e.message?.role === "user").at(-1)?.id ?? null,
102
- };
103
- }
104
- private current(s: Subject): boolean {
105
- if (s.token !== this.token || s.revision !== this.revision || s.store !== this.store) return false;
106
- const branch = this.store?.reader.getBranch() ?? [];
107
- return (
108
- (!s.leafId || branch.some((e) => e.id === s.leafId)) &&
109
- (branch.filter((e) => e.message?.role === "user").at(-1)?.id ?? null) === s.userId
110
- );
111
- }
112
- private guard(s: Subject): void {
113
- check(
114
- this.current(s),
115
- "stale_operation",
116
- "Routing state changed while the operation was waiting; no further effects applied.",
117
- );
118
- }
119
- private taskBasis(state: State, assignmentId: string): string | null | undefined {
120
- return state.resumeBasis.has(assignmentId)
121
- ? state.resumeBasis.get(assignmentId)
122
- : state.assignments.get(assignmentId)?.basisUserEntryId;
123
- }
124
- private enabledWorkers(): readonly WorkerProfile[] {
125
- return workersForDelegation(this.capability?.delegation ?? "executor");
126
- }
127
- private assignedWorker(state: State, assignmentId = state.assignmentId): WorkerProfile {
128
- const assignment = assignmentId ? state.assignments.get(assignmentId) : undefined;
129
- const handoff = assignment ? state.handoffs.get(assignment.delegateHandoffId) : undefined;
130
- check(
131
- handoff?.kind === "delegate" && handoff.assignmentId === assignment?.id && isWorkerProfile(handoff.to),
132
- "assignment_worker_missing",
133
- "The current assignment has no valid recorded worker.",
134
- );
135
- return handoff.to;
136
- }
137
- private requiredProfiles(state = this.stateData()): Profile[] {
138
- const profiles = new Set<Profile>(["coordinator", ...this.enabledWorkers()]);
139
- if (state.profile) profiles.add(state.profile);
140
- if (this.manualHold) profiles.add(this.manualHold);
141
- const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
142
- if (assignment?.state === "outstanding" || state.recoveryId) profiles.add(this.assignedWorker(state));
143
- const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
144
- if (pending && isWorkerProfile(pending.to)) profiles.add(pending.to);
145
- return [...profiles];
146
- }
147
- private retireUnbound(reason: string): void {
148
- for (const execution of this.stateData().executions.values()) {
149
- if (!execution.assistantEntryId && !execution.interrupted)
150
- this.append({ type: "execution-interrupted", executionId: execution.id, reason });
151
- }
152
- this.turn = undefined;
153
- }
154
- private openTurn(): void {
155
- if (!this.turn || this.turn.opened) return;
156
- const t = this.turn;
157
- this.append({
158
- type: "execution-opened",
159
- execution: {
160
- id: t.id,
161
- profile: t.profile,
162
- assignmentId: t.assignmentId,
163
- recoveryId: t.recoveryId,
164
- basisUserEntryId: t.basisUserEntryId,
165
- pair: t.pair,
166
- resultEntryIds: [],
167
- },
168
- });
169
- t.opened = true;
170
- }
171
- async settled(ctx: any): Promise<void> {
172
- if (!this.store || !this.supported() || this.store.blocked || !ctx.isIdle?.()) return;
173
- this.ctx = ctx;
174
- this.retireUnbound("Native run settled without a complete source binding; prior effects remain unresolved.");
175
- }
176
- async beforeRun(ctx: any): Promise<void> {
177
- this.ctx = ctx;
178
- if (!this.store || !this.supported() || this.store.blocked || this.error) return;
179
- if (this.stateData().control === "automatic" && isWorkerProfile(this.stateData().profile)) {
180
- const result = await this.control("coordinator", false);
181
- check(result.status !== "blocked", "reconciliation_required", result.reason);
182
- }
183
- }
184
- private stateData(): State {
185
- return this.store?.state() ?? initialState();
186
- }
187
- private sources(state = this.stateData()): Sources {
188
- const entries = (this.ctx.sessionManager.getBranch() as NativeEntry[]).filter((e) =>
189
- ["message", "custom_message", "compaction", "branch_summary"].includes(e.type),
190
- );
191
- const cache = this.sourceCache;
192
- if (
193
- cache &&
194
- cache.authors === state.authors.size &&
195
- entries.length === cache.entries.length &&
196
- entries.every((e, i) => e === cache.entries[i])
197
- )
198
- return cache.source;
199
- const active = this.ctx.sessionManager.buildContextEntries?.();
200
- const activeIds = active ? new Set<string>(active.map((e: NativeEntry) => e.id)) : undefined;
201
- const source = cache?.source ?? new Sources([], state);
202
- source.refresh(entries, state, activeIds);
203
- this.sourceCache = { entries, authors: state.authors.size, source };
204
- return source;
205
- }
206
- private observed(ctx = this.ctx): Pair | undefined {
207
- return ctx?.model?.provider && ctx?.model?.id && ctx?.thinkingLevel
208
- ? { provider: ctx.model.provider, modelId: ctx.model.id, thinking: ctx.thinkingLevel }
209
- : undefined;
210
- }
211
- private supported(): boolean {
212
- return !!this.capability?.effective && !this.suppressed;
213
- }
214
- state() {
215
- let state: State;
216
- try {
217
- state = this.stateData();
218
- } catch {
219
- state = initialState();
220
- }
221
- const blocked = this.error ?? this.store?.blocked;
222
- return {
223
- effective: this.supported() && state.control !== "inactive" && !blocked,
224
- activeProfile: state.profile,
225
- delegation: this.capability?.delegation ?? "executor",
226
- controlMode: state.control === "manual" ? `manual-${state.profile}` : state.control,
227
- runtimeStatus: blocked
228
- ? ("blocked" as const)
229
- : this.supported() && state.control !== "inactive"
230
- ? ("active" as const)
231
- : ("inactive" as const),
232
- runtimeReason: blocked ?? (this.suppressed ? "startup_selection" : this.capability?.blockingReason.message),
233
- projectionFailure: this.projectionError,
234
- };
235
- }
236
- get projectionEnabled(): boolean {
237
- return this.supported() && this.capability?.projection === true && this.stateData().control === "automatic";
238
- }
239
- private append(data: EventData, op?: string, step?: string) {
240
- check(this.store, "routing_unavailable");
241
- return this.store.append(this.store.make(data, op, step));
242
- }
243
- private enqueue<T>(action: () => Promise<T> | T): Promise<T> {
244
- const token = this.token;
245
- const result = this.operations.then(() => {
246
- check(token === this.token, "stale_instance");
247
- return action();
248
- });
249
- this.operations = result.catch(() => {});
250
- return result;
251
- }
252
- private mark(error: unknown) {
253
- if (this.error && error instanceof RoutingError && error.code === "routing_blocked") return;
254
- this.error =
255
- error instanceof RoutingError
256
- ? `${error.code}: ${error.message}`
257
- : error instanceof Error
258
- ? error.message
259
- : String(error);
260
- }
261
- private assertAvailable(profile?: Profile) {
262
- check(
263
- this.supported() && this.store && !this.error && !this.store.blocked,
264
- "routing_unavailable",
265
- this.error ?? this.store?.blocked ?? "Routing is inactive.",
266
- );
267
- const state = this.stateData();
268
- check(state.control === "automatic", "manual_control");
269
- if (profile) check(state.profile === profile && this.turn?.profile === profile, "wrong_profile");
270
- if (state.profile) check(samePair(this.observed(), this.profilePair(state.profile)), "configuration_mismatch");
271
- }
272
- private configuredProfilePair(profile: Profile): Pair {
273
- const configured = this.capability?.profiles[profile];
274
- check(configured, "profile_missing");
275
- return pairFromProfile(configured);
276
- }
277
- private profilePair(profile: Profile): Pair {
278
- return this.stateData().profileOverrides.get(profile) ?? this.configuredProfilePair(profile);
279
- }
280
- private profilePairs(profiles = this.requiredProfiles()): Partial<Record<Profile, Pair>> {
281
- return Object.fromEntries(profiles.map((profile) => [profile, this.profilePair(profile)]));
282
- }
283
- private async validateProfilePairs(pairs: Partial<Record<Profile, Pair>>): Promise<void> {
284
- check(pairs.coordinator, "profile_missing", "Configure the coordinator profile.");
285
- const helper = pairs.helper !== undefined;
286
- const executor = pairs.executor !== undefined;
287
- check(helper || executor, "profile_missing", "Configure an enabled worker profile.");
288
- const delegation = helper && executor ? "both" : helper ? "helper" : "executor";
289
- const profiles = Object.fromEntries(
290
- Object.entries(pairs).map(([profile, pair]) => [
291
- profile,
292
- { provider: pair!.provider, model: pair!.modelId, thinking: pair!.thinking },
293
- ]),
294
- );
295
- const result = await resolveCognitiveRoutingState(
296
- { cognitiveRouting: { enabled: true, delegation, profiles } },
297
- {},
298
- this.ctx,
299
- );
300
- check(result.effective, result.blockingReason.code, result.blockingReason.message);
301
- }
302
- private model(profile: Profile) {
303
- const pair = this.profilePair(profile);
304
- const model = this.ctx?.modelRegistry?.find(pair.provider, pair.modelId);
305
- check(model, "profile_unavailable");
306
- return model;
307
- }
308
- async bind(ctx: any, capability: CognitiveRoutingCapabilityState, startup = false): Promise<void> {
309
- this.token = randomUUID();
310
- this.ctx = ctx;
311
- this.capability = capability;
312
- this.turn = undefined;
313
- this.manualHold = undefined;
314
- this.automaticControl = false;
315
- this.messages = [];
316
- this.sourceCache = undefined;
317
- this.receipts.clear();
318
- this.pages.clear();
319
- this.applying = undefined;
320
- this.externalChange = false;
321
- this.error = undefined;
322
- this.projectionError = undefined;
323
- this.operations = Promise.resolve();
324
- this.targetSignature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
325
- this.suppressed = startup && process.argv.some((a) => /^(--model|--thinking)(=|$)/.test(a));
326
- this.store = new EventStore(this.pi, ctx.sessionManager as SessionReader);
327
- const subject = this.subject();
328
- if (!capability.effective || this.suppressed) return;
329
- try {
330
- await this.store.reconcile();
331
- this.guard(subject);
332
- const state = this.stateData();
333
- this.manualHold = state.control === "manual" ? state.profile : undefined;
334
- this.automaticControl = !state.events.size || state.control === "automatic";
335
- await this.validateProfilePairs(this.profilePairs(this.requiredProfiles(state)));
336
- this.retireUnbound("Session rebind found an unfinished execution; no task effects replayed.");
337
- if (!state.events.size) {
338
- this.append({
339
- type: "control",
340
- control: "automatic",
341
- profile: "coordinator",
342
- reason: "Initial routing activation",
343
- });
344
- await this.applyPair(this.profilePair("coordinator"));
345
- } else if (
346
- state.control === "automatic" &&
347
- state.profile &&
348
- !samePair(this.observed(), this.profilePair(state.profile))
349
- ) {
350
- // Reload reconstructs responsibility; it does not silently reapply the last setter.
351
- this.append({
352
- type: "control",
353
- control: "automatic",
354
- profile: "coordinator",
355
- reason: "Rebind reconciles historical responsibility with current host control",
356
- });
357
- await this.applyPair(this.profilePair("coordinator"));
358
- }
359
- } catch (error) {
360
- if (this.current(subject)) this.mark(error);
361
- }
362
- }
363
- unbind(): void {
364
- this.token = randomUUID();
365
- this.store = undefined;
366
- this.turn = undefined;
367
- this.messages = [];
368
- this.ctx = undefined;
369
- this.sourceCache = undefined;
370
- this.receipts.clear();
371
- this.pages.clear();
372
- }
373
- async refresh(ctx: any, capability: CognitiveRoutingCapabilityState): Promise<void> {
374
- this.ctx = ctx;
375
- const was = this.capability?.effective === true;
376
- const signature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
377
- if (this.capability && (was !== capability.effective || signature !== this.targetSignature)) this.revision++;
378
- this.capability = capability;
379
- if (!capability.effective) {
380
- this.turn = undefined;
381
- return;
382
- }
383
- if (!this.store) {
384
- await this.bind(ctx, capability);
385
- return;
386
- }
387
- if (!was) {
388
- const subject = this.subject();
389
- this.suppressed = false;
390
- try {
391
- await this.store.reconcile();
392
- this.guard(subject);
393
- this.error = undefined;
394
- this.append({
395
- type: "control",
396
- control: "automatic",
397
- profile: "coordinator",
398
- reason: "Capability re-enabled; Coordinator reconciliation",
399
- });
400
- this.automaticControl = true;
401
- await this.applyPair(this.profilePair("coordinator"));
402
- } catch (error) {
403
- if (this.current(subject)) this.mark(error);
404
- }
405
- } else if (this.targetSignature && signature !== this.targetSignature) {
406
- this.error = "configuration_changed: reconcile profile configuration before automatic execution";
407
- }
408
- if (canonical({ delegation: this.capability?.delegation, profiles: this.capability?.profiles }) === signature)
409
- this.targetSignature = signature;
410
- }
411
- async ancestryChanged(ctx: any, navigation = true): Promise<void> {
412
- this.revision++;
413
- this.ctx = ctx;
414
- this.turn = undefined;
415
- this.messages = [];
416
- this.sourceCache = undefined;
417
- this.receipts.clear();
418
- this.pages.clear();
419
- if (!this.store || !this.supported()) return;
420
- const subject = this.subject();
421
- try {
422
- await this.store.reconcile();
423
- this.guard(subject);
424
- this.error = undefined;
425
- const state = this.stateData();
426
- await this.validateProfilePairs(this.profilePairs(this.requiredProfiles(state)));
427
- this.retireUnbound("Selected historical ancestry ends before execution binding; effects are not replayed.");
428
- if (this.manualHold) {
429
- this.append({
430
- type: "control",
431
- control: "manual",
432
- profile: this.manualHold,
433
- reason: "Current explicit manual hold survives navigation",
434
- });
435
- await this.applyPair(this.profilePair(this.manualHold));
436
- } else if (
437
- (this.automaticControl || !state.events.size) &&
438
- (navigation || !samePair(this.observed(), this.profilePair(state.profile ?? "coordinator")))
439
- ) {
440
- this.append({
441
- type: "control",
442
- control: "automatic",
443
- profile: "coordinator",
444
- reason: "Coordinator reconciles selected native ancestry",
445
- });
446
- await this.applyPair(this.profilePair("coordinator"));
447
- }
448
- } catch (error) {
449
- if (this.current(subject)) this.mark(error);
450
- }
451
- }
452
- async nativeChange(ctx: any): Promise<void> {
453
- this.ctx = ctx;
454
- if (!this.supported() || !this.store) return;
455
- if (this.applying) {
456
- if (ctx.model?.provider !== this.applying.provider || ctx.model?.id !== this.applying.modelId) {
457
- this.externalChange = true;
458
- this.revision++;
459
- }
460
- return; // Never await a queue already held by our setter.
461
- }
462
- const state = this.stateData();
463
- if (state.control !== "automatic" || !state.profile || samePair(this.observed(), this.profilePair(state.profile)))
464
- return;
465
- try {
466
- this.revision++;
467
- this.manualHold = undefined;
468
- this.automaticControl = false;
469
- this.append({ type: "control", control: "inactive", reason: "External native model/effort change" });
470
- } catch (error) {
471
- this.mark(error);
472
- }
473
- }
474
- private async applyPair(target: Pair): Promise<void> {
475
- const subject = this.subject(),
476
- token = this.token,
477
- prior = this.observed();
478
- if (samePair(prior, target)) return;
479
- const model = this.ctx.modelRegistry.find(target.provider, target.modelId);
480
- check(model, "profile_unavailable");
481
- const auth = await this.ctx.modelRegistry.getApiKeyAndHeaders(model);
482
- this.guard(subject);
483
- check(auth?.ok, "profile_unauthenticated");
484
- this.applying = target;
485
- let ownedPair = target;
486
- this.externalChange = false;
487
- try {
488
- check(await this.pi.setModel(model), "model_rejected");
489
- this.guard(subject);
490
- this.pi.setThinkingLevel(target.thinking);
491
- check(!this.externalChange && samePair(this.observed(), target), "configuration_mismatch");
492
- } catch (error) {
493
- if (this.current(subject) && prior && !this.externalChange) {
494
- const old = this.ctx.modelRegistry.find(prior.provider, prior.modelId);
495
- try {
496
- this.applying = prior;
497
- ownedPair = prior;
498
- check(old && (await this.pi.setModel(old)), "rollback_failed");
499
- this.guard(subject);
500
- this.pi.setThinkingLevel(prior.thinking);
501
- check(samePair(this.observed(), prior), "rollback_failed");
502
- } catch {
503
- if (this.current(subject)) this.error = "configuration_unknown: rollback could not be observed";
504
- }
505
- }
506
- throw error;
507
- } finally {
508
- if (token === this.token && this.applying === ownedPair) this.applying = undefined;
509
- }
510
- }
511
- private async control(profile: Profile, manual: boolean): Promise<{ status: string; reason?: string }> {
512
- this.revision++;
513
- const subject = this.subject();
514
- return this.enqueue(async () => {
515
- try {
516
- check(this.capability?.effective && this.store, "routing_unavailable");
517
- if (manual && isWorkerProfile(profile))
518
- check(
519
- this.enabledWorkers().includes(profile),
520
- "worker_disabled",
521
- `${profile} is not enabled by delegation mode.`,
522
- );
523
- await this.store.reconcile();
524
- this.guard(subject);
525
- this.error = undefined;
526
- this.suppressed = false;
527
- const previous = this.stateData();
528
- if (
529
- previous.control === (manual ? "manual" : "automatic") &&
530
- previous.profile === profile &&
531
- samePair(this.observed(), this.profilePair(profile))
532
- ) {
533
- this.manualHold = manual ? profile : undefined;
534
- this.automaticControl = !manual;
535
- return { status: manual ? "active" : "automatic" };
536
- }
537
- this.append({
538
- type: "control",
539
- control: manual ? "manual" : "automatic",
540
- profile,
541
- reason: manual ? "Explicit user manual hold" : "Explicit user automatic release/reconciliation",
542
- });
543
- try {
544
- await this.applyPair(this.profilePair(profile));
545
- this.guard(subject);
546
- } catch (error) {
547
- if (this.current(subject) && !this.error && !this.store.blocked)
548
- this.append({
549
- type: "control",
550
- control: previous.control,
551
- profile: previous.profile,
552
- reason: "Control request failed; prior control retained",
553
- });
554
- throw error;
555
- }
556
- this.manualHold = manual ? profile : undefined;
557
- this.automaticControl = !manual;
558
- return { status: manual ? "active" : "automatic" };
559
- } catch (error) {
560
- if (this.current(subject) && !(error instanceof RoutingError && error.code === "worker_disabled"))
561
- this.mark(error);
562
- return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
563
- }
564
- });
565
- }
566
- setManualProfile(profile: Profile, _mechanism?: string) {
567
- return this.control(profile, true);
568
- }
569
- sessionProfileOverrides(): Partial<Record<Profile, Pair>> {
570
- return Object.fromEntries(this.stateData().profileOverrides) as Partial<Record<Profile, Pair>>;
571
- }
572
- async setSessionProfileOverride(
573
- profile: Profile,
574
- override: Pair | null,
575
- mechanism = "Session profile override",
576
- ): Promise<{ status: string; reason?: string }> {
577
- this.revision++;
578
- const subject = this.subject();
579
- return this.enqueue(async () => {
580
- try {
581
- check(this.capability?.effective && this.store, "routing_unavailable");
582
- check(
583
- this.ctx?.isIdle?.() !== false,
584
- "host_busy",
585
- "Wait for Pi to become idle before changing session presets.",
586
- );
587
- await this.store.reconcile();
588
- this.guard(subject);
589
- const state = this.stateData();
590
- const previous = state.profileOverrides.get(profile);
591
- if ((override === null && !previous) || (override !== null && samePair(previous, override)))
592
- return { status: "unchanged" };
593
- const target = override ?? this.configuredProfilePair(profile);
594
- const profiles = new Set(this.requiredProfiles(state));
595
- profiles.add(profile);
596
- const pairs = this.profilePairs([...profiles]);
597
- pairs[profile] = target;
598
- await this.validateProfilePairs(pairs);
599
- this.append({
600
- type: "profile-overrides",
601
- overrides: { [profile]: override },
602
- reason: mechanism,
603
- });
604
- if (state.control !== "inactive" && state.profile === profile) {
605
- try {
606
- await this.applyPair(target);
607
- this.guard(subject);
608
- } catch (error) {
609
- if (this.current(subject) && !this.externalChange && !this.error && !this.store.blocked) {
610
- this.append({
611
- type: "profile-overrides",
612
- overrides: { [profile]: previous ?? null },
613
- reason: "Session profile application failed; prior override retained",
614
- });
615
- }
616
- throw error;
617
- }
618
- this.error = undefined;
619
- return { status: "active" };
620
- }
621
- this.error = undefined;
622
- return { status: "stored" };
623
- } catch (error) {
624
- if (this.current(subject)) this.mark(error);
625
- return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
626
- }
627
- });
628
- }
629
- async resetSessionProfileOverrides(
630
- mechanism = "Reset session profile overrides",
631
- ): Promise<{ status: string; reason?: string }> {
632
- this.revision++;
633
- const subject = this.subject();
634
- return this.enqueue(async () => {
635
- try {
636
- check(this.capability?.effective && this.store, "routing_unavailable");
637
- check(
638
- this.ctx?.isIdle?.() !== false,
639
- "host_busy",
640
- "Wait for Pi to become idle before resetting session presets.",
641
- );
642
- await this.store.reconcile();
643
- this.guard(subject);
644
- const state = this.stateData();
645
- if (state.profileOverrides.size === 0) return { status: "unchanged" };
646
- const previous = Object.fromEntries(state.profileOverrides) as Partial<Record<Profile, Pair>>;
647
- const required = this.requiredProfiles(state);
648
- const targetPairs = Object.fromEntries(
649
- required.map((profile) => [profile, this.configuredProfilePair(profile)]),
650
- ) as Partial<Record<Profile, Pair>>;
651
- await this.validateProfilePairs(targetPairs);
652
- this.append({
653
- type: "profile-overrides",
654
- overrides: { coordinator: null, helper: null, executor: null },
655
- reason: mechanism,
656
- });
657
- const active = state.control !== "inactive" ? state.profile : undefined;
658
- if (active && previous[active]) {
659
- try {
660
- await this.applyPair(targetPairs[active]!);
661
- this.guard(subject);
662
- } catch (error) {
663
- if (this.current(subject) && !this.externalChange && !this.error && !this.store.blocked) {
664
- this.append({
665
- type: "profile-overrides",
666
- overrides: {
667
- coordinator: previous.coordinator ?? null,
668
- helper: previous.helper ?? null,
669
- executor: previous.executor ?? null,
670
- },
671
- reason: "Session profile reset failed; prior overrides retained",
672
- });
673
- }
674
- throw error;
675
- }
676
- this.error = undefined;
677
- return { status: "active" };
678
- }
679
- this.error = undefined;
680
- return { status: "stored" };
681
- } catch (error) {
682
- if (this.current(subject)) this.mark(error);
683
- return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
684
- }
685
- });
686
- }
687
- setAutomaticControl(_mechanism?: string) {
688
- return this.control("coordinator", false);
689
- }
690
- private lastDeliveredUser(sources: Sources, messages: any[]): string | null {
691
- const delivered = new Set(
692
- sources.associate(messages).flatMap((x) => (x.source?.message.role === "user" ? [x.source.entry.id] : [])),
693
- );
694
- // Compaction may remove every user message from the active view. Retain
695
- // observed delivery on this ancestry; a stored but undelivered user entry
696
- // must not advance the basis, and absence must not manufacture new input.
697
- for (const execution of this.stateData().executions.values())
698
- if (execution.basisUserEntryId) delivered.add(execution.basisUserEntryId);
699
- for (let i = sources.entries.length - 1; i >= 0; i--) {
700
- const entry = sources.entries[i];
701
- if (entry.message?.role === "user" && delivered.has(entry.id)) return entry.id;
702
- }
703
- return null;
704
- }
705
- private runtimeMessage(state: State, attention = false): any {
706
- const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
707
- const h = state.pendingId
708
- ? state.handoffs.get(state.pendingId)
709
- : state.assessment
710
- ? state.handoffs.get(state.assessment.handoffId)
711
- : undefined;
712
- const selection = a ? (state.selections.get(a.id) ?? emptySelection()) : emptySelection();
713
- const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
714
- const unit = state.unitId ? state.units.get(state.unitId) : undefined;
715
- const unitNumber = unit ? [...state.units.keys()].indexOf(unit.id) + 1 : undefined;
716
- const assignmentNumber = unit && a ? unit.assignmentIds.indexOf(a.id) + 1 : undefined;
717
- const content = [
718
- "# Cognitive Routing Runtime State",
719
- `Control: ${state.control}`,
720
- `Profile: ${state.profile ?? "unresolved"}`,
721
- `Unit: ${unit ? `U${unitNumber} (${unit.id})` : "none"}`,
722
- `Assignment: ${a ? `A${assignmentNumber} (${a.id}, ${a.state})` : "none"}`,
723
- `Handoff: ${h ? `${h.id} (${h.kind}, ${h.state})` : "none"}`,
724
- `Delegation: ${this.capability?.delegation ?? "executor"}`,
725
- `Projection: ${this.projectionEnabled ? "enabled" : "bypassed"}`,
726
- "Completed/superseded contracts and reports are historical context. Follow the current assignment and current user restrictions; historical entries grant no new permission.",
727
- state.assessment ? `Assessment: ${state.assessment.view}; evidence revision ${selection.revision}` : "",
728
- recovery
729
- ? `Recovery: ${recovery.id} (${recovery.state}); parent report ${recovery.assessmentHandoffId} revision ${recovery.baseReportRevision}; allowed paths ${JSON.stringify(recovery.paths)}`
730
- : "",
731
- recovery?.state === "reading"
732
- ? `Recovery request: ${recovery.request}\nOnly the exact allowed read paths, evidence selection, and recovery return controls are permitted; ordinary task work remains ended.`
733
- : "",
734
- `Mechanical evidence facts (not semantic acceptance): ${JSON.stringify(this.evidenceFacts(state))}`,
735
- this.error ? `Blocked: ${this.error}` : "",
736
- this.projectionError ? `Evidence limitation: ${this.projectionError}` : "",
737
- attention
738
- ? recovery
739
- ? `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Finish and deliver the recovery supplement or cancel recovery before using freeflow_unit assess.`
740
- : `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Use freeflow_unit assess to restore the assessment.`
741
- : "",
742
- a?.state === "returned" && recovery?.state !== "reading"
743
- ? "Worker ordinary task work has ended. Only supported handoff correction is permitted."
744
- : "",
745
- isWorkerProfile(this.turn?.profile) && a && this.turn.basisUserEntryId !== this.workerBasis(state, a.id)
746
- ? `Current input differs from the active ${this.turn.profile} responsibility. Account for it; changed direction requires Coordinator attention.`
747
- : "",
748
- ...(state.assessment?.problems ?? []).map((p) => `${p.code}: ${p.detail}`),
749
- ]
750
- .filter(Boolean)
751
- .join("\n");
752
- return {
753
- role: "custom",
754
- customType: ROUTING_MESSAGE,
755
- content,
756
- display: false,
757
- timestamp: 0,
758
- details: { routingInstance: this.token },
759
- };
760
- }
761
- private evidenceFacts(state: State) {
762
- const selection = state.assignmentId
763
- ? (state.selections.get(state.assignmentId) ?? emptySelection())
764
- : emptySelection();
765
- const sources = this.sources(state);
766
- return {
767
- revision: selection.revision,
768
- selected: selection.selected.map((ref) => {
769
- const entry = this.ctx?.sessionManager.getEntry?.(ref.slice(4).replace(/#text$/, ""));
770
- const locator = sources.locator(ref);
771
- return {
772
- ref,
773
- kind: ref.endsWith("#text") ? "assistant-text" : (entry?.message?.role ?? "unknown"),
774
- toolName: entry?.message?.toolName,
775
- producer: state.authors.get(entry?.id)?.profile ?? "common",
776
- assignment: state.authors.get(entry?.id)?.assignmentId,
777
- ...(locator ? { locator } : {}),
778
- };
779
- }),
780
- unresolved: selection.unresolved,
781
- withdrawals: selection.withdrawals,
782
- preparedRevision: state.assessment?.reservation?.selectionRevision,
783
- limitations: state.assessment?.problems ?? [],
784
- };
785
- }
786
- private prepared(profile: Profile, input: any[], handoffId?: string, restoring = false): PreparedView {
787
- const state = this.stateData(),
788
- model = this.model(profile);
789
- return prepareView({
790
- messages: input,
791
- sources: this.sources(state),
792
- state,
793
- view: profile,
794
- projection: this.projectionEnabled,
795
- model,
796
- pair: this.profilePair(profile),
797
- systemPrompt: this.ctx.getSystemPrompt?.() ?? "",
798
- tools: (this.pi.getAllTools?.() ?? [])
799
- .filter((tool: any) => this.pi.getActiveTools?.().includes(tool.name))
800
- .map((tool: any) =>
801
- ROUTING_TOOLS.includes(tool.name) ? { ...tool, parameters: ROUTING_SCHEMAS[tool.name] } : tool,
802
- ),
803
- runtimeMessage: this.runtimeMessage(
804
- state,
805
- profile === "coordinator" && state.assessment?.view === "suspended" && !restoring && !handoffId,
806
- ),
807
- instance: this.token,
808
- preparingReturn: handoffId,
809
- restoring,
810
- });
811
- }
812
- budgetNotice(messages: any[], ctx: any): any | undefined {
813
- if (!this.supported()) return;
814
- const tools = (this.pi.getAllTools?.() ?? []).filter((tool: any) => this.pi.getActiveTools?.().includes(tool.name));
815
- const estimate = estimateRequest(ctx.getSystemPrompt?.() ?? "", tools, messages, ctx.model);
816
- if (!estimate.warnings.length) return;
817
- return {
818
- role: "custom",
819
- customType: "freeflow-routing-budget",
820
- display: false,
821
- content: estimate.warnings.map((warning) => warning.detail).join("\n"),
822
- timestamp: 0,
823
- };
824
- }
825
- private ordinaryContext(input: any[]): any[] {
826
- try {
827
- const sources = this.sources(),
828
- associated = sources.associate(input);
829
- const mapped = new Map(associated.filter((item) => item.source).map((item) => [item.message, item.source!]));
830
- return annotateSources(
831
- input,
832
- mapped,
833
- new Set([...mapped.values()].map((source) => source.ref)),
834
- sources,
835
- this.token,
836
- );
837
- } catch {
838
- return input;
839
- }
840
- }
841
- async context(ctx: any, incoming: any[]): Promise<any[]> {
842
- this.ctx = ctx;
843
- const input = incoming.filter(
844
- (m) =>
845
- !(
846
- m?.details?.routingInstance === this.token &&
847
- [ROUTING_MESSAGE, "freeflow-routing-v2-refs"].includes(m.customType)
848
- ),
849
- );
850
- this.messages = input;
851
- if (!this.supported() || !this.store) return this.ordinaryContext(input);
852
- try {
853
- const state = this.stateData();
854
- if (state.control !== "automatic") return this.ordinaryContext(input);
855
- check(!this.error && !this.store.blocked && state.profile, "routing_blocked", this.error ?? this.store.blocked);
856
- check(samePair(this.observed(), this.profilePair(state.profile!)), "prepared_pair_mismatch");
857
- const sources = this.sources(state),
858
- user = this.lastDeliveredUser(sources, input);
859
- if (state.assessment && user && user !== state.assessment.basisUserEntryId)
860
- this.append(
861
- {
862
- type: "assessment-suspended",
863
- handoffId: state.assessment.handoffId,
864
- reason: "user-attention",
865
- basisUserEntryId: user,
866
- problems: [],
867
- },
868
- JSON.stringify(["attention", state.assessment.handoffId, user]),
869
- );
870
- // New input reaching an already prepared worker request cannot be rerouted by changing only the live model.
871
- const outstanding = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
872
- const worker = outstanding ? this.assignedWorker(state, outstanding.id) : undefined;
873
- const interrupted =
874
- isWorkerProfile(state.profile) &&
875
- (state.profile !== worker || user !== (outstanding ? this.workerBasis(state, outstanding.id) : undefined));
876
- if (!this.turn || this.turn.bound) {
877
- this.receipts.clear();
878
- const execution: Execution = {
879
- id: randomUUID(),
880
- profile: state.profile!,
881
- assignmentId: state.assignmentId,
882
- recoveryId: state.recoveryId,
883
- basisUserEntryId: user,
884
- pair: this.observed()!,
885
- resultEntryIds: [],
886
- };
887
- const before = new Set((ctx.sessionManager.getBranch() as NativeEntry[]).map((e) => e.id));
888
- this.turn = {
889
- id: execution.id,
890
- profile: execution.profile,
891
- assignmentId: execution.assignmentId,
892
- recoveryId: execution.recoveryId,
893
- basisUserEntryId: user,
894
- before,
895
- pair: execution.pair,
896
- };
897
- this.openTurn();
898
- }
899
- let prepared = this.prepared(state.profile!, input);
900
- if (!prepared.ready && state.profile === "coordinator" && this.stateData().assessment) {
901
- const assessment = this.stateData().assessment!;
902
- this.append({
903
- type: "assessment-suspended",
904
- handoffId: assessment.handoffId,
905
- reason: "delivery-gap",
906
- basisUserEntryId: user,
907
- problems: prepared.problems,
908
- });
909
- this.projectionError = prepared.problems.map((p) => p.code).join(", ");
910
- prepared = this.prepared("coordinator", input);
911
- }
912
- check(prepared.ready, "context_unavailable", prepared.problems.map((p) => p.detail).join("; "));
913
- const known = this.stateData().exposure;
914
- const added = prepared.fullSources.filter((s) => known.get(s.ref) !== s.hash);
915
- if (isWorkerProfile(this.turn.profile))
916
- for (let i = 0; i < added.length; i += 128)
917
- this.append({
918
- type: "sources-exposed",
919
- executionId: this.turn.id,
920
- view: this.turn.profile,
921
- sources: added.slice(i, i + 128).map((s) => ({ ref: s.ref, bodyHash: s.hash })),
922
- });
923
- if (interrupted)
924
- prepared.messages.push({
925
- role: "custom",
926
- customType: ROUTING_MESSAGE,
927
- display: false,
928
- timestamp: 0,
929
- details: { routingInstance: this.token },
930
- content:
931
- "New delivered user input requires Coordinator attention. Do not run ordinary task tools; return the current partial result.",
932
- });
933
- return prepared.messages;
934
- } catch (error) {
935
- this.mark(error);
936
- ctx.abort?.();
937
- // Do not send an accidental full worker history on a projection failure.
938
- return [
939
- {
940
- role: "custom",
941
- customType: ROUTING_MESSAGE,
942
- content: `Automatic request blocked: ${this.error}`,
943
- display: false,
944
- timestamp: 0,
945
- details: { routingInstance: this.token, routingRequestBlocked: true },
946
- },
947
- ];
948
- }
949
- }
950
- messageEnd(message: any): void {
951
- if (message?.role === "assistant" && this.turn && !this.turn.bound) this.turn.message = structuredClone(message);
952
- }
953
- private contextOperation(name: string, input: any): boolean {
954
- return name === "freeflow_context" && ["archive", "restore", "search", "retrieve"].includes(input?.operation);
955
- }
956
- private handoffOperation(name: string, input: any): boolean {
957
- if (name === "freeflow_delegate") return true;
958
- if (name === "freeflow_return") return ["submit", "supplement", "retry"].includes(input?.operation);
959
- return name === "freeflow_unit" && input?.operation === "recover";
960
- }
961
- private stableRecoveryPath(path: string): boolean {
962
- return (
963
- !path.startsWith("@") &&
964
- path !== "~" &&
965
- !path.startsWith("~/") &&
966
- !path.startsWith("file://") &&
967
- !/[\u00a0\u2000-\u200a\u202f\u205f\u3000]/u.test(path) &&
968
- !(process.platform === "win32" && /^\/(?:mnt\/|cygdrive\/)?[a-z](?:\/|$)/i.test(path))
969
- );
970
- }
971
- private canonicalRecoveryPath(path: string): string {
972
- check(this.stableRecoveryPath(path), "recovery_path_unsupported", `Recovery path spelling is unsupported: ${path}`);
973
- const requested = resolvePath(this.ctx.cwd, path);
974
- check(existsSync(requested), "recovery_path_unavailable", `Recovery path is unavailable: ${path}`);
975
- try {
976
- return realpathSync(requested);
977
- } catch {
978
- throw new RoutingError("recovery_path_unavailable", `Recovery path is unavailable: ${path}`);
979
- }
980
- }
981
- private packagedInstruction(path: string): boolean {
982
- return this.packageRoots.some((root) => {
983
- const inside = relative(root, path);
984
- if (!inside || inside.startsWith("..") || resolvePath(root, inside) !== path) return false;
985
- return (
986
- /^(?:skills|capabilities)\/[^/]+\/SKILL\.md$/.test(inside) ||
987
- /^(?:skills|capabilities)\/[^/]+\/references\/[^/]+\.md$/.test(inside)
988
- );
989
- });
990
- }
991
- private recoveryReadAllowed(state: State, name: string, input: any): boolean {
992
- if (name !== "read" || typeof input?.path !== "string" || !state.recoveryId) return false;
993
- const recovery = state.recoveries.get(state.recoveryId);
994
- if (!recovery || recovery.state !== "reading" || !this.stableRecoveryPath(input.path)) return false;
995
- try {
996
- const path = this.canonicalRecoveryPath(input.path);
997
- return recovery.paths.includes(path) || this.packagedInstruction(path);
998
- } catch {
999
- return false;
1000
- }
1001
- }
1002
- private workerBasis(state: State, assignmentId: string): string | null {
1003
- const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
1004
- if (recovery?.state === "reading") return state.handoffs.get(recovery.requestHandoffId)?.basisUserEntryId ?? null;
1005
- return this.taskBasis(state, assignmentId);
1006
- }
1007
- private batch(callId: string, name: string): void {
1008
- check(this.turn?.message, "batch_unavailable");
1009
- const matches = (this.ctx.sessionManager.getBranch() as NativeEntry[]).filter(
1010
- (e) =>
1011
- !this.turn!.before.has(e.id) &&
1012
- e.type === "message" &&
1013
- e.message?.role === "assistant" &&
1014
- e.message.content?.some((b: any) => b.type === "toolCall" && b.id === callId),
1015
- );
1016
- check(matches.length === 1, "batch_source_changed");
1017
- const fingerprint = bodyHash(matches[0].message);
1018
- check(!this.turn.sourceFingerprint || this.turn.sourceFingerprint === fingerprint, "batch_source_changed");
1019
- // Later message_end handlers may legitimately replace the message. The first
1020
- // preflight freezes the actually persisted batch before any of its tools act.
1021
- this.turn.sourceFingerprint = fingerprint;
1022
- this.turn.message = structuredClone(matches[0].message);
1023
- const calls = (this.turn.message.content ?? []).filter((b: any) => b.type === "toolCall");
1024
- const handoffs = calls.filter((b: any) => this.handoffOperation(b.name, b.arguments));
1025
- check(
1026
- !handoffs.length ||
1027
- (handoffs.length === 1 &&
1028
- this.handoffOperation(calls.at(-1)?.name, calls.at(-1)?.arguments) &&
1029
- calls.every(
1030
- (b: any) =>
1031
- this.handoffOperation(b.name, b.arguments) ||
1032
- b.name === "freeflow_project" ||
1033
- this.contextOperation(b.name, b.arguments),
1034
- )),
1035
- "invalid_handoff_batch",
1036
- "A handoff must be last and cannot accompany ordinary task tools.",
1037
- );
1038
- check(
1039
- calls.some((b: any) => b.id === callId && b.name === name),
1040
- "call_not_in_batch",
1041
- );
1042
- }
1043
- preflight(event: any, ctx: any): any {
1044
- this.ctx = ctx;
1045
- const name = event.toolName,
1046
- isRouting = ROUTING_TOOLS.includes(name);
1047
- if (!this.supported()) return isRouting ? { block: true, reason: "Routing is unavailable." } : undefined;
1048
- try {
1049
- const state = this.stateData();
1050
- if (state.control === "manual")
1051
- return isRouting && name !== "freeflow_unit" ? { block: true, reason: "Manual control is active." } : undefined;
1052
- if (state.control !== "automatic")
1053
- return isRouting ? { block: true, reason: "Automatic routing is inactive." } : undefined;
1054
- this.batch(event.toolCallId, name);
1055
- check(!this.error && !this.store?.blocked, "routing_blocked");
1056
- this.openTurn();
1057
- if (isWorkerProfile(this.turn?.profile) && !isRouting && !this.contextOperation(name, event.input)) {
1058
- const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
1059
- const worker = a ? this.assignedWorker(state, a.id) : undefined;
1060
- const ordinary =
1061
- a?.state === "outstanding" &&
1062
- this.turn.basisUserEntryId === this.taskBasis(state, a.id) &&
1063
- state.profile === worker &&
1064
- this.turn.profile === worker;
1065
- const recovery =
1066
- a?.state === "returned" &&
1067
- this.turn.basisUserEntryId === this.workerBasis(state, a.id) &&
1068
- state.profile === worker &&
1069
- this.turn.profile === worker &&
1070
- this.recoveryReadAllowed(state, name, event.input);
1071
- check(ordinary || recovery, "worker_task_phase_ended");
1072
- }
1073
- return undefined;
1074
- } catch (error) {
1075
- return { block: true, reason: error instanceof Error ? error.message : String(error) };
1076
- }
1077
- }
1078
- private callOperation(callId: string, name: string): string {
1079
- check(this.turn && this.store, "execution_missing");
1080
- this.batch(callId, name);
1081
- return JSON.stringify([this.store.reader.getSessionId(), this.turn.id, callId, name]);
1082
- }
1083
- private result(value: any): any {
1084
- return { content: [{ type: "text", text: JSON.stringify(value) }], details: value };
1085
- }
1086
- async invoke(name: string, callId: string, input: any, signal: AbortSignal | undefined, ctx: any): Promise<any> {
1087
- return this.enqueue(async () => {
1088
- this.ctx = ctx;
1089
- try {
1090
- check(!signal?.aborted, "cancelled");
1091
- if (name === "freeflow_unit" && input.operation === "inspect") {
1092
- check(this.supported(), "routing_unavailable");
1093
- return this.result(this.inspectUnit(input));
1094
- }
1095
- const state = this.stateData();
1096
- const expected =
1097
- name === "freeflow_delegate" || name === "freeflow_unit" ? "coordinator" : this.assignedWorker(state);
1098
- this.assertAvailable(expected);
1099
- const op = this.callOperation(callId, name);
1100
- const cached = this.receipts.get(op);
1101
- if (cached) {
1102
- check(cached.input === canonical(input), "operation_conflict");
1103
- return this.result(structuredClone(cached.value));
1104
- }
1105
- const value =
1106
- name === "freeflow_delegate"
1107
- ? this.delegate(input, callId, op)
1108
- : name === "freeflow_return"
1109
- ? this.returnReport(input, callId, op)
1110
- : name === "freeflow_project"
1111
- ? this.project(input, op)
1112
- : name === "freeflow_unit"
1113
- ? this.unit(input, callId, op)
1114
- : undefined;
1115
- if (value !== undefined) {
1116
- this.receipts.set(op, { input: canonical(input), value: structuredClone(value) });
1117
- return this.result(value);
1118
- }
1119
- throw new RoutingError("unknown_tool", name);
1120
- } catch (error) {
1121
- return this.result({
1122
- status: "blocked",
1123
- code: error instanceof RoutingError ? error.code : "operation_failed",
1124
- message: error instanceof Error ? error.message : String(error),
1125
- problems: error instanceof RoutingError ? error.problems : [],
1126
- observed: this.observed(),
1127
- expectedProfile:
1128
- name === "freeflow_unit" && input.operation === "inspect"
1129
- ? undefined
1130
- : name === "freeflow_delegate" || name === "freeflow_unit"
1131
- ? "coordinator"
1132
- : (() => {
1133
- try {
1134
- return this.assignedWorker(this.stateData());
1135
- } catch {
1136
- return undefined;
1137
- }
1138
- })(),
1139
- recoveryAction:
1140
- error instanceof RoutingError &&
1141
- [
1142
- "cursor_expired",
1143
- "invalid_cursor",
1144
- "work_ref_required",
1145
- "invalid_work_ref",
1146
- "work_unavailable",
1147
- "invalid_inspection",
1148
- ].includes(error.code)
1149
- ? "Inspect again without a cursor and use a returned work ref. Lookup recovery does not require a new assignment."
1150
- : "Preserve saved work. Use freeflow_unit inspect; reconcile current input/control before retrying the named operation.",
1151
- });
1152
- }
1153
- });
1154
- }
1155
- private duplicate(op: string): any {
1156
- return [...this.stateData().events.values()].find(
1157
- (e) =>
1158
- e.operationId === op &&
1159
- [
1160
- "delegate-accepted",
1161
- "return-accepted",
1162
- "recovery-request-accepted",
1163
- "recovery-supplement-accepted",
1164
- "recovery-cancelled",
1165
- "handoff-retry-requested",
1166
- ].includes(e.data.type),
1167
- );
1168
- }
1169
- private delegate(input: any, callId: string, op: string) {
1170
- const prior = this.duplicate(op);
1171
- if (prior) {
1172
- check(
1173
- prior.data.type === "delegate-accepted" &&
1174
- prior.data.assignment.contract === input.contract &&
1175
- (input.worker === undefined || prior.data.handoff.to === input.worker),
1176
- "operation_conflict",
1177
- );
1178
- return { status: "accepted", handoff: prior.data.handoff.id, unchanged: true };
1179
- }
1180
- const state = this.stateData();
1181
- const enabled = this.enabledWorkers();
1182
- let worker: WorkerProfile;
1183
- if (input.worker === undefined) {
1184
- check(enabled.length === 1, "worker_required", "Delegation mode 'both' requires choosing helper or executor.");
1185
- worker = enabled[0]!;
1186
- } else {
1187
- check(isWorkerProfile(input.worker), "invalid_worker");
1188
- worker = input.worker;
1189
- }
1190
- check(enabled.includes(worker), "worker_disabled", `${worker} is not enabled by delegation mode.`);
1191
- const unit = state.unitId
1192
- ? state.units.get(state.unitId)!
1193
- : { id: randomUUID(), objective: input.contract, state: "open" as const, assignmentIds: [] };
1194
- const old = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
1195
- if (input.operation === "replace") check(old?.state === "outstanding" && !this.applying, "no_quiescent_assignment");
1196
- else
1197
- check(
1198
- input.operation === "assign" && old?.state !== "outstanding" && !state.pendingId && !state.recoveryId,
1199
- "assignment_outstanding",
1200
- );
1201
- const assignmentId = randomUUID(),
1202
- id = randomUUID();
1203
- const assignment = {
1204
- id: assignmentId,
1205
- unitId: unit.id,
1206
- contract: input.contract,
1207
- state: "outstanding" as const,
1208
- delegateHandoffId: id,
1209
- basisUserEntryId: this.turn!.basisUserEntryId,
1210
- };
1211
- const h: Handoff = {
1212
- id,
1213
- kind: "delegate",
1214
- assignmentId,
1215
- text: input.contract,
1216
- from: "coordinator",
1217
- to: worker,
1218
- executionId: this.turn!.id,
1219
- toolCallId: callId,
1220
- basisUserEntryId: this.turn!.basisUserEntryId,
1221
- state: "pending",
1222
- reportRevision: 0,
1223
- limitations: [],
1224
- };
1225
- this.append(
1226
- {
1227
- type: "delegate-accepted",
1228
- unit: { ...unit, assignmentIds: [...unit.assignmentIds, assignmentId] },
1229
- assignment,
1230
- handoff: h,
1231
- ...(input.operation === "replace"
1232
- ? {
1233
- replacement: {
1234
- assignmentId: old!.id,
1235
- reason: input.reason,
1236
- ...(state.pendingId ? { supersededHandoffId: state.pendingId } : {}),
1237
- },
1238
- }
1239
- : {}),
1240
- },
1241
- op,
1242
- );
1243
- return {
1244
- status: "accepted",
1245
- unit: unit.id,
1246
- assignment: assignmentId,
1247
- worker,
1248
- handoff: id,
1249
- transition: "pending",
1250
- contract: input.contract,
1251
- reason: input.reason,
1252
- createdUnit: !state.unitId,
1253
- ...(old && input.operation === "replace"
1254
- ? {
1255
- replacedAssignment: old.id,
1256
- continuity:
1257
- "Prior effects and evidence remain on native history. Reconcile uncertain effects before dependent work.",
1258
- }
1259
- : {}),
1260
- };
1261
- }
1262
- private returnReport(input: any, callId: string, op: string) {
1263
- const state = this.stateData(),
1264
- a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
1265
- check(a, "assignment_missing");
1266
- const worker = this.assignedWorker(state, a.id);
1267
- const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
1268
- const original = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1269
- const supplement = recovery?.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
1270
- const retryingSupplement = input.operation === "retry" && recovery?.state === "returning";
1271
- const saved = retryingSupplement || input.operation === "supplement" ? supplement : original;
1272
- const selection = state.selections.get(a.id) ?? emptySelection();
1273
- const duplicate = this.duplicate(op);
1274
- if (duplicate) {
1275
- if (input.operation === "retry") check(duplicate.data.type === "handoff-retry-requested", "operation_conflict");
1276
- else {
1277
- const expected = input.operation === "supplement" ? "recovery-supplement-accepted" : "return-accepted";
1278
- check(
1279
- duplicate.data.type === expected &&
1280
- duplicate.data.handoff.text === input.report &&
1281
- duplicate.data.handoff.outcome === input.outcome &&
1282
- canonical(duplicate.data.handoff.limitations) === canonical(input.limitations ?? []),
1283
- "operation_conflict",
1284
- );
1285
- }
1286
- const handoff = duplicate.data.handoffId ?? duplicate.data.handoff.id;
1287
- return { ...this.returnReadiness(handoff), unchanged: true };
1288
- }
1289
- if (input.operation === "retry") {
1290
- check(saved && ["blocked", "pending"].includes(saved.state), "saved_return_missing");
1291
- this.append(
1292
- {
1293
- type: "handoff-retry-requested",
1294
- handoffId: saved.id,
1295
- attemptId: randomUUID(),
1296
- executionId: this.turn!.id,
1297
- toolCallId: callId,
1298
- reportRevision: saved.reportRevision,
1299
- selectionRevision: selection.revision,
1300
- },
1301
- op,
1302
- );
1303
- return {
1304
- ...this.returnReadiness(saved.id),
1305
- reportUnchanged: true,
1306
- supplementUnchanged: saved.kind === "recovery-return",
1307
- };
1308
- }
1309
- if (input.operation === "supplement") {
1310
- check(
1311
- recovery &&
1312
- ["reading", "returning"].includes(recovery.state) &&
1313
- (!supplement || ["pending", "blocked"].includes(supplement.state)),
1314
- "recovery_not_returnable",
1315
- );
1316
- const h: Handoff = {
1317
- id: supplement?.id ?? randomUUID(),
1318
- kind: "recovery-return",
1319
- assignmentId: a.id,
1320
- text: input.report,
1321
- from: worker,
1322
- to: "coordinator",
1323
- executionId: this.turn!.id,
1324
- toolCallId: callId,
1325
- basisUserEntryId: this.turn!.basisUserEntryId,
1326
- state: "pending",
1327
- reportRevision: (supplement?.reportRevision ?? 0) + 1,
1328
- outcome: input.outcome,
1329
- limitations: input.limitations ?? [],
1330
- };
1331
- this.append({ type: "recovery-supplement-accepted", recoveryId: recovery.id, handoff: h }, op);
1332
- return this.returnReadiness(h.id);
1333
- }
1334
- check(input.operation === "submit", "invalid_return_operation");
1335
- check(
1336
- a.state === "outstanding" ||
1337
- (a.state === "returned" && original && ["pending", "blocked"].includes(original.state)),
1338
- "assignment_task_ended",
1339
- );
1340
- const h: Handoff = {
1341
- id: original?.id ?? randomUUID(),
1342
- kind: "return",
1343
- assignmentId: a.id,
1344
- text: input.report,
1345
- from: worker,
1346
- to: "coordinator",
1347
- executionId: this.turn!.id,
1348
- toolCallId: callId,
1349
- basisUserEntryId: this.turn!.basisUserEntryId,
1350
- state: "pending",
1351
- reportRevision: (original?.reportRevision ?? 0) + 1,
1352
- outcome: input.outcome,
1353
- limitations: input.limitations ?? [],
1354
- };
1355
- this.append({ type: "return-accepted", handoff: h }, op);
1356
- return this.returnReadiness(h.id);
1357
- }
1358
- private returnReadiness(id: string) {
1359
- const state = this.stateData();
1360
- const h = state.handoffs.get(id)!;
1361
- const prepared = this.prepared("coordinator", this.messages, id);
1362
- const isSupplement = h.kind === "recovery-return";
1363
- const recovery = isSupplement
1364
- ? [...state.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id)
1365
- : undefined;
1366
- return {
1367
- status: "accepted",
1368
- reportSaved: !isSupplement,
1369
- supplementSaved: isSupplement,
1370
- handoff: id,
1371
- ...(recovery ? { recovery: recovery.id } : {}),
1372
- ...(isSupplement ? { supplementRevision: h.reportRevision } : { reportRevision: h.reportRevision }),
1373
- assignmentRef: `assignment:${h.assignmentId}`,
1374
- ...(isSupplement
1375
- ? { supplementRef: `supplement:${recovery?.id ?? "unknown"}:${h.reportRevision}` }
1376
- : { reportRef: `report:${h.id}:${h.reportRevision}` }),
1377
- producer: h.from,
1378
- outcome: h.outcome,
1379
- limitations: h.limitations,
1380
- ready: prepared.ready,
1381
- transition: prepared.ready ? "pending" : "blocked",
1382
- problems: prepared.problems,
1383
- warnings: prepared.warnings,
1384
- stage: "completed-input preparation",
1385
- finalExchangePending: true,
1386
- provisional:
1387
- "The finalized handoff exchange, target configuration and budget are revalidated at turn_end; this is not delivery evidence.",
1388
- ...(isSupplement ? { supplement: h.text } : { report: h.text }),
1389
- evidence: this.evidenceFacts(state),
1390
- };
1391
- }
1392
- private project(input: any, op: string) {
1393
- check(this.projectionEnabled, "projection_disabled");
1394
- const state = this.stateData(),
1395
- a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
1396
- check(a, "assignment_missing");
1397
- const sources = this.sources(state);
1398
- sources.associate(this.messages);
1399
- const selection = state.selections.get(a.id) ?? emptySelection();
1400
- let next = selection;
1401
- if (input.operation !== "inspect") {
1402
- check(["add", "remove"].includes(input.operation), "invalid_projection_operation");
1403
- next = changeSelection(selection, input, sources, state);
1404
- if (next !== selection) this.append({ type: "selection-changed", assignmentId: a.id, selection: next }, op);
1405
- }
1406
- const prepared = this.prepared("coordinator", this.messages, a.returnHandoffId);
1407
- const scope = input.scope ?? "assignment";
1408
- let page: any;
1409
- if (input.operation === "inspect") {
1410
- const model = this.model("coordinator");
1411
- const checksFor = (s: Source) => {
1412
- const eligibility = sources.selectionProblem(s.ref, state);
1413
- const limitations = [
1414
- ...(eligibility ? [eligibility] : []),
1415
- ...representationProblems(s, model),
1416
- ...sources.exchange(s).problems,
1417
- ];
1418
- return { eligible: !eligibility, targetReady: !limitations.length, limitations };
1419
- };
1420
- const rows = () =>
1421
- [...sources.byRef.values()]
1422
- .filter(
1423
- (s) =>
1424
- isWorkerProfile(s.producer) &&
1425
- (isTaskEvidence(s) || (scope === "selected" && next.selected.includes(s.ref))) &&
1426
- (scope === "selected"
1427
- ? next.selected.includes(s.ref)
1428
- : scope === "assignment"
1429
- ? s.assignmentId === a.id
1430
- : scope === "active"
1431
- ? s.active
1432
- : true) &&
1433
- (s.original ||
1434
- s.message.role === "toolResult" ||
1435
- s.message.content?.some((b: any) => b.type === "text" && b.text?.trim())),
1436
- )
1437
- .map((s) => {
1438
- const locator = sources.locator(s.ref);
1439
- return {
1440
- ref: s.ref,
1441
- kind: s.original ? "assistant-text" : s.message.role,
1442
- producer: s.producer,
1443
- assignment: s.assignmentId,
1444
- toolName: s.message.toolName,
1445
- ...(locator ? { locator } : {}),
1446
- active: s.active,
1447
- selected: next.selected.includes(s.ref),
1448
- retainedSelection: next.selected.includes(s.ref) && !isTaskEvidence(s),
1449
- ...checksFor(s),
1450
- };
1451
- })
1452
- // Discovery offers usable candidates. Saved selections must remain
1453
- // inspectable even when a later source or target check fails.
1454
- .filter((row) => scope === "selected" || row.targetReady);
1455
- page = this.page(
1456
- `evidence:${scope}`,
1457
- rows,
1458
- input.cursor,
1459
- 30,
1460
- scope === "assignment" || scope === "selected" ? a.id : undefined,
1461
- scope === "active",
1462
- (rows) => ({
1463
- scopeCounts: {
1464
- candidates: rows.length,
1465
- eligible: rows.filter((r: any) => r.eligible).length,
1466
- targetReady: rows.filter((r: any) => r.targetReady).length,
1467
- },
1468
- otherAssignments: [...sources.byRef.values()].filter(
1469
- (s) =>
1470
- isWorkerProfile(s.producer) && s.assignmentId !== a.id && isTaskEvidence(s) && checksFor(s).targetReady,
1471
- ).length,
1472
- }),
1473
- );
1474
- }
1475
- return {
1476
- status: input.operation === "inspect" || next === selection ? "unchanged" : "saved",
1477
- ...(page
1478
- ? {
1479
- scope,
1480
- count: page.count,
1481
- returned: page.items.length,
1482
- candidates: page.items,
1483
- nextCursor: page.nextCursor,
1484
- ...page.summary,
1485
- pageCounts: {
1486
- candidates: page.items.length,
1487
- eligible: page.items.filter((r: any) => r.eligible).length,
1488
- targetReady: page.items.filter((r: any) => r.targetReady).length,
1489
- },
1490
- countsBasis:
1491
- "Inspection snapshot; page counts describe returned candidates, scope counts describe all candidates in this scope.",
1492
- historyHint: page.summary.otherAssignments
1493
- ? "Previously exposed task evidence exists in other assignments; inspect scope history when needed."
1494
- : undefined,
1495
- }
1496
- : {}),
1497
- revision: next.revision,
1498
- selected: next.selected,
1499
- selectedCount: next.selected.length,
1500
- unresolved: next.unresolved,
1501
- ready: !next.unresolved.length && prepared.ready,
1502
- problems: prepared.problems,
1503
- warnings: prepared.warnings,
1504
- evidence: this.evidenceFacts(this.stateData()),
1505
- items: (input.refs ? [...new Set<string>(input.refs)] : []).map((ref) => ({
1506
- ref,
1507
- status: next.unresolved.some((p) => p.ref === ref)
1508
- ? "rejected"
1509
- : input.operation === "remove"
1510
- ? selection.selected.includes(ref)
1511
- ? "removed"
1512
- : selection.unresolved.some((p) => p.ref === ref)
1513
- ? "withdrawn"
1514
- : "unchanged"
1515
- : selection.selected.includes(ref)
1516
- ? "already_selected"
1517
- : "added",
1518
- })),
1519
- estimate: { tokens: prepared.estimatedTokens, method: prepared.estimateMethod },
1520
- };
1521
- }
1522
- private unit(input: any, callId: string, op: string) {
1523
- const state = this.stateData();
1524
- const prior = [...state.events.values()].find((e) => e.operationId === op);
1525
- if (prior) {
1526
- if (input.operation === "recover") {
1527
- check(
1528
- prior.data.type === "recovery-request-accepted" &&
1529
- prior.data.recovery.request === input.request &&
1530
- canonical(prior.data.recovery.requestedPaths) === canonical([...new Set<string>(input.paths ?? [])]),
1531
- "operation_conflict",
1532
- );
1533
- const { recovery, handoff } = prior.data;
1534
- return {
1535
- status: "accepted",
1536
- unchanged: true,
1537
- recovery: recovery.id,
1538
- handoff: handoff.id,
1539
- assignmentRef: `assignment:${recovery.assignmentId}`,
1540
- reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
1541
- request: recovery.request,
1542
- paths: recovery.paths,
1543
- transition: handoff.state,
1544
- };
1545
- }
1546
- if (input.operation === "cancel-recovery") {
1547
- check(prior.data.type === "recovery-cancelled" && prior.data.reason === input.reason, "operation_conflict");
1548
- return {
1549
- status: "cancelled",
1550
- unchanged: true,
1551
- recovery: prior.data.recoveryId,
1552
- reason: prior.data.reason,
1553
- assessment: state.assessment?.handoffId,
1554
- };
1555
- }
1556
- check(
1557
- (input.operation === "assess" && prior.data.type === "assessment-resumed") ||
1558
- (input.operation === "close" &&
1559
- prior.data.type === "unit-closed" &&
1560
- prior.data.outcome === input.outcome &&
1561
- prior.data.assessment === input.assessment),
1562
- "operation_conflict",
1563
- );
1564
- return { status: "unchanged", operation: input.operation };
1565
- }
1566
- if (input.operation === "recover") {
1567
- const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
1568
- const base = a?.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1569
- check(
1570
- state.unitId &&
1571
- a?.state === "returned" &&
1572
- base?.kind === "return" &&
1573
- base.state === "configured" &&
1574
- state.assessment?.handoffId === base.id &&
1575
- !state.pendingId &&
1576
- !state.recoveryId &&
1577
- !this.applying,
1578
- "recovery_not_available",
1579
- );
1580
- const worker = this.assignedWorker(state, a.id);
1581
- const id = randomUUID(),
1582
- handoffId = randomUUID();
1583
- const paths = [...new Set<string>((input.paths ?? []).map((path: string) => this.canonicalRecoveryPath(path)))];
1584
- const recovery: Recovery = {
1585
- id,
1586
- assignmentId: a.id,
1587
- assessmentHandoffId: base.id,
1588
- baseReportRevision: base.reportRevision,
1589
- request: input.request,
1590
- requestedPaths: [...new Set<string>(input.paths ?? [])],
1591
- paths,
1592
- state: "requested",
1593
- requestHandoffId: handoffId,
1594
- supplementRevision: 0,
1595
- };
1596
- const handoff: Handoff = {
1597
- id: handoffId,
1598
- kind: "recovery-request",
1599
- assignmentId: a.id,
1600
- text: input.request,
1601
- from: "coordinator",
1602
- to: worker,
1603
- executionId: this.turn!.id,
1604
- toolCallId: callId,
1605
- basisUserEntryId: this.turn!.basisUserEntryId,
1606
- state: "pending",
1607
- reportRevision: 0,
1608
- limitations: [],
1609
- };
1610
- this.append({ type: "recovery-request-accepted", recovery, handoff }, op);
1611
- return {
1612
- status: "accepted",
1613
- recovery: id,
1614
- handoff: handoffId,
1615
- assignmentRef: `assignment:${a.id}`,
1616
- reportRef: `report:${base.id}:${base.reportRevision}`,
1617
- request: input.request,
1618
- paths,
1619
- transition: "pending",
1620
- };
1621
- }
1622
- if (input.operation === "cancel-recovery") {
1623
- check(state.recoveryId && !this.applying, "recovery_not_cancellable");
1624
- check(
1625
- ![...state.executions.values()].some(
1626
- (execution) =>
1627
- execution.recoveryId === state.recoveryId &&
1628
- isWorkerProfile(execution.profile) &&
1629
- !execution.assistantEntryId &&
1630
- !execution.interrupted,
1631
- ),
1632
- "recovery_not_cancellable",
1633
- );
1634
- this.append({ type: "recovery-cancelled", recoveryId: state.recoveryId, reason: input.reason }, op);
1635
- return {
1636
- status: "cancelled",
1637
- recovery: state.recoveryId,
1638
- reason: input.reason,
1639
- assessment: state.assessment?.handoffId,
1640
- };
1641
- }
1642
- if (input.operation === "assess") {
1643
- check(state.assessment, "assessment_missing");
1644
- check(!state.recoveryId, "recovery_outstanding");
1645
- if (state.assessment.view === "active") return { status: "unchanged", ready: true };
1646
- const prepared = this.prepared("coordinator", this.messages, state.assessment.handoffId, true);
1647
- if (!prepared.ready) return { status: "suspended", ready: false, problems: prepared.problems };
1648
- if (this.projectionEnabled) check(prepared.reservation, "reservation_missing");
1649
- this.append(
1650
- {
1651
- type: "assessment-resumed",
1652
- handoffId: state.assessment.handoffId,
1653
- basisUserEntryId: this.turn!.basisUserEntryId,
1654
- ...(prepared.reservation ? { reservation: prepared.reservation } : {}),
1655
- },
1656
- op,
1657
- );
1658
- this.projectionError = undefined;
1659
- return {
1660
- status: "resumed",
1661
- ready: true,
1662
- stage: "prepared assessment; next request revalidated",
1663
- evidence: this.evidenceFacts(this.stateData()),
1664
- };
1665
- }
1666
- check(input.operation === "close" && state.unitId, "unit_missing");
1667
- const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
1668
- check(!this.applying, "not_quiescent");
1669
- if (state.recoveryId) {
1670
- check(input.outcome !== "accepted", "unfinished_work");
1671
- check(
1672
- ![...state.executions.values()].some(
1673
- (execution) =>
1674
- execution.recoveryId === state.recoveryId &&
1675
- isWorkerProfile(execution.profile) &&
1676
- !execution.assistantEntryId &&
1677
- !execution.interrupted,
1678
- ),
1679
- "not_quiescent",
1680
- );
1681
- }
1682
- this.append(
1683
- {
1684
- type: "unit-closed",
1685
- unitId: state.unitId,
1686
- outcome: input.outcome,
1687
- assessment: input.assessment,
1688
- ...(a?.state === "outstanding" && input.outcome !== "accepted" ? { supersededAssignmentId: a.id } : {}),
1689
- },
1690
- op,
1691
- );
1692
- return { status: "closed", unit: state.unitId, outcome: input.outcome, assessment: input.assessment };
1693
- }
1694
- private page(
1695
- scope: string,
1696
- rows: any[] | (() => any[]),
1697
- cursor?: string,
1698
- size = 30,
1699
- assignment?: string,
1700
- active = false,
1701
- summarize?: (rows: any[]) => any,
1702
- ): any {
1703
- const branch = this.ctx.sessionManager.getBranch() as NativeEntry[];
1704
- const compaction = branch.filter((e) => e.type === "compaction").at(-1)?.id;
1705
- let key: string,
1706
- offset = 0;
1707
- if (cursor) {
1708
- const match = /^([a-f0-9-]+):([0-9]+)$/.exec(cursor);
1709
- check(match, "invalid_cursor", "Use the returned cursor unchanged.");
1710
- key = match[1];
1711
- offset = Number(match[2]);
1712
- const page = this.pages.get(key);
1713
- check(
1714
- page &&
1715
- page.scope === scope &&
1716
- page.assignment === assignment &&
1717
- (!page.basis || branch.some((e) => e.id === page.basis)) &&
1718
- (!active || page.compaction === compaction),
1719
- "cursor_expired",
1720
- "History or scope changed. Inspect again without a cursor.",
1721
- );
1722
- check(
1723
- Number.isSafeInteger(offset) && offset >= 0 && offset < page.rows.length && offset % page.size === 0,
1724
- "invalid_cursor",
1725
- );
1726
- } else {
1727
- key = randomUUID();
1728
- if (this.pages.size >= 8) this.pages.delete(this.pages.keys().next().value!);
1729
- const items = typeof rows === "function" ? rows() : rows;
1730
- this.pages.set(key, {
1731
- scope,
1732
- basis: branch.at(-1)?.id ?? null,
1733
- assignment,
1734
- compaction,
1735
- rows: items,
1736
- size,
1737
- summary: summarize?.(items),
1738
- });
1739
- }
1740
- const page = this.pages.get(key)!;
1741
- return {
1742
- scope,
1743
- count: page.rows.length,
1744
- offset,
1745
- summary: page.summary,
1746
- items: page.rows.slice(offset, offset + page.size),
1747
- nextCursor: offset + page.size < page.rows.length ? `${key}:${offset + page.size}` : undefined,
1748
- };
1749
- }
1750
- private inspectUnit(input: any): any {
1751
- const state = this.stateData(),
1752
- view = input.view ?? "current";
1753
- check(view === "detail" || !input.ref, "invalid_inspection", "A work ref belongs to view detail.");
1754
- check(
1755
- view === "history" || (!input.cursor && !input.limit),
1756
- "invalid_inspection",
1757
- "Pagination belongs to view history.",
1758
- );
1759
- if (view === "current")
1760
- return {
1761
- ...this.status(),
1762
- status: "inspected",
1763
- view,
1764
- currentRef: state.assignmentId ? `assignment:${state.assignmentId}` : undefined,
1765
- };
1766
- if (view === "history") {
1767
- const numbers = new Map([...state.units.keys()].map((id, i) => [id, i + 1]));
1768
- const rows = () =>
1769
- [...state.assignments.values()].reverse().map((a) => {
1770
- const u = state.units.get(a.unitId),
1771
- h = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1772
- const delegate = state.handoffs.get(a.delegateHandoffId);
1773
- return {
1774
- ref: `assignment:${a.id}`,
1775
- unitRef: `unit:${a.unitId}`,
1776
- unitNumber: numbers.get(a.unitId),
1777
- assignmentNumber: (u?.assignmentIds.indexOf(a.id) ?? 0) + 1,
1778
- state: a.state,
1779
- unitState: u?.state,
1780
- disposition: u?.disposition,
1781
- summary: a.contract.slice(0, 160),
1782
- reportAvailable: !!h,
1783
- outcome: h?.outcome,
1784
- from: delegate?.from,
1785
- to: delegate?.to,
1786
- };
1787
- });
1788
- const page = this.page("work-history", rows, input.cursor, input.limit ?? 20);
1789
- return {
1790
- status: "inspected",
1791
- view,
1792
- history: page.items,
1793
- count: page.count,
1794
- returned: page.items.length,
1795
- nextCursor: page.nextCursor,
1796
- };
1797
- }
1798
- check(
1799
- view === "detail" && typeof input.ref === "string",
1800
- "work_ref_required",
1801
- "Use a ref returned by history inspection.",
1802
- );
1803
- check(
1804
- /^(?:(?:unit|assignment|recovery):[^:\s]+|(?:report|supplement):[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
1805
- "invalid_work_ref",
1806
- "Use assignment, report, recovery, or supplement refs returned by a receipt or inspection, unchanged. A bare handoff ID is not a work ref.",
1807
- );
1808
- if (input.ref.startsWith("unit:")) {
1809
- const u = state.units.get(input.ref.slice(5));
1810
- check(u, "work_unavailable", "Unit is not on current ancestry.");
1811
- return {
1812
- status: "inspected",
1813
- view,
1814
- ref: input.ref,
1815
- unit: {
1816
- id: u.id,
1817
- state: u.state,
1818
- objective: u.objective,
1819
- disposition: u.disposition,
1820
- assessment: u.assessment,
1821
- },
1822
- assignmentCount: u.assignmentIds.length,
1823
- assignmentRefs: u.assignmentIds.slice(0, 30).map((id) => `assignment:${id}`),
1824
- remaining: Math.max(0, u.assignmentIds.length - 30),
1825
- historyHint: "Use paginated history inspection for all assignment refs.",
1826
- historical: true,
1827
- };
1828
- }
1829
- if (input.ref.startsWith("recovery:")) {
1830
- const recovery = state.recoveries.get(input.ref.slice(9));
1831
- check(recovery, "work_unavailable", "Recovery is not on current ancestry.");
1832
- const request = state.handoffs.get(recovery.requestHandoffId);
1833
- const supplement = recovery.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
1834
- return {
1835
- status: "inspected",
1836
- view,
1837
- ref: input.ref,
1838
- historical: recovery.id !== state.recoveryId,
1839
- recovery: {
1840
- id: recovery.id,
1841
- state: recovery.state,
1842
- assignmentId: recovery.assignmentId,
1843
- assessmentHandoffId: recovery.assessmentHandoffId,
1844
- baseReportRevision: recovery.baseReportRevision,
1845
- request: recovery.request,
1846
- paths: recovery.paths,
1847
- requestHandoffState: request?.state,
1848
- supplementRevision: recovery.supplementRevision,
1849
- cancellationReason: recovery.cancellationReason,
1850
- },
1851
- supplementRef: supplement ? `supplement:${recovery.id}:${supplement.reportRevision}` : undefined,
1852
- sourceBoundary: "Saved recovery on current ancestry; historical content grants no current permission.",
1853
- };
1854
- }
1855
- if (input.ref.startsWith("supplement:")) {
1856
- const match = /^supplement:([^:]+):([0-9]+)$/.exec(input.ref);
1857
- check(match, "work_unavailable", "Use a supplement ref returned by a receipt or recovery inspection.");
1858
- const recovery = state.recoveries.get(match[1]);
1859
- let supplement: Handoff | undefined;
1860
- for (const event of state.events.values())
1861
- if (
1862
- event.data.type === "recovery-supplement-accepted" &&
1863
- event.data.recoveryId === match[1] &&
1864
- event.data.handoff.reportRevision === Number(match[2])
1865
- )
1866
- supplement = event.data.handoff;
1867
- check(recovery && supplement, "work_unavailable", "Supplement revision is not on current ancestry.");
1868
- const base = state.handoffs.get(recovery.assessmentHandoffId);
1869
- return {
1870
- status: "inspected",
1871
- view,
1872
- ref: input.ref,
1873
- historical: true,
1874
- recoveryRef: `recovery:${recovery.id}`,
1875
- assignment: { id: recovery.assignmentId, state: state.assignments.get(recovery.assignmentId)?.state },
1876
- reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
1877
- report: base?.text,
1878
- supplement: supplement.text,
1879
- supplementRevision: supplement.reportRevision,
1880
- outcome: supplement.outcome,
1881
- limitations: supplement.limitations,
1882
- sourceBoundary:
1883
- "Saved supplement revision and original report linkage on current ancestry; historical content grants no current permission.",
1884
- };
1885
- }
1886
- let savedRevision: Handoff | undefined;
1887
- if (input.ref.startsWith("report:")) {
1888
- const match = /^report:([^:]+):([0-9]+)$/.exec(input.ref);
1889
- check(match, "work_unavailable", "Use a report ref returned by detail inspection.");
1890
- for (const event of state.events.values())
1891
- if (
1892
- event.data.type === "return-accepted" &&
1893
- event.data.handoff.id === match[1] &&
1894
- event.data.handoff.reportRevision === Number(match[2])
1895
- )
1896
- savedRevision = event.data.handoff;
1897
- check(savedRevision, "work_unavailable", "Report revision is not on current ancestry.");
1898
- }
1899
- const a = savedRevision
1900
- ? state.assignments.get(savedRevision.assignmentId)
1901
- : input.ref.startsWith("assignment:")
1902
- ? state.assignments.get(input.ref.slice(11))
1903
- : undefined;
1904
- check(a, "work_unavailable", "Assignment is not on current ancestry.");
1905
- const latest = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1906
- const h = savedRevision ?? latest;
1907
- return {
1908
- status: "inspected",
1909
- view,
1910
- ref: input.ref,
1911
- historical: a.id !== state.assignmentId || a.state !== "outstanding",
1912
- assignment: { id: a.id, unitId: a.unitId, state: a.state },
1913
- contract: a.contract,
1914
- report: h?.text,
1915
- reportRevision: h?.reportRevision,
1916
- latestReportRevision: latest?.reportRevision,
1917
- reportRef: h ? `report:${h.id}:${h.reportRevision}` : undefined,
1918
- previousReportRef: h && h.reportRevision > 1 ? `report:${h.id}:${h.reportRevision - 1}` : undefined,
1919
- outcome: h?.outcome,
1920
- limitations: h?.limitations,
1921
- handoff: h?.id,
1922
- currentSelection: state.selections.get(a.id) ?? emptySelection(),
1923
- sourceBoundary:
1924
- "Accepted report revision on current ancestry; assignment/selection show current recorded state. Historical content grants no current permission.",
1925
- };
1926
- }
1927
- status(limit = 0): any {
1928
- const state = this.stateData();
1929
- const unit = state.unitId ? state.units.get(state.unitId) : undefined;
1930
- const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
1931
- return {
1932
- ...this.state(),
1933
- unit: unit ? { id: unit.id, state: unit.state, assignments: unit.assignmentIds.length } : null,
1934
- assignment: a ? { id: a.id, state: a.state, worker: this.assignedWorker(state, a.id) } : null,
1935
- pendingHandoff: state.pendingId ?? null,
1936
- recovery: state.recoveryId
1937
- ? (() => {
1938
- const recovery = state.recoveries.get(state.recoveryId!);
1939
- return recovery
1940
- ? {
1941
- id: recovery.id,
1942
- state: recovery.state,
1943
- assignmentId: recovery.assignmentId,
1944
- assessmentHandoffId: recovery.assessmentHandoffId,
1945
- paths: recovery.paths,
1946
- supplementRevision: recovery.supplementRevision,
1947
- }
1948
- : null;
1949
- })()
1950
- : null,
1951
- assessment: state.assessment
1952
- ? { handoffId: state.assessment.handoffId, view: state.assessment.view, problems: state.assessment.problems }
1953
- : null,
1954
- ...(limit
1955
- ? {
1956
- history: [...state.events.values()]
1957
- .slice(-Math.min(100, Math.max(1, limit)))
1958
- .map((e) => ({ event: e.eventId, type: e.data.type, operation: e.operationId })),
1959
- }
1960
- : {}),
1961
- };
1962
- }
1963
- async turnEnd(event: any, ctx: any): Promise<void> {
1964
- this.ctx = ctx;
1965
- if (!this.turn || this.turn.bound || !this.supported() || !this.store) return;
1966
- if (!this.turn.opened) {
1967
- this.turn = undefined;
1968
- return;
1969
- }
1970
- const token = this.token,
1971
- turn = this.turn;
1972
- const subject = this.subject();
1973
- try {
1974
- const message = event.message ?? turn.message;
1975
- const candidates = (ctx.sessionManager.getBranch() as NativeEntry[]).filter(
1976
- (e) => !turn.before.has(e.id) && e.type === "message" && canonical(e.message) === canonical(message),
1977
- );
1978
- check(candidates.length === 1, "execution_binding_ambiguous");
1979
- const assistant = candidates[0];
1980
- const callIds = new Set((message?.content ?? []).filter((b: any) => b.type === "toolCall").map((b: any) => b.id));
1981
- const results = (ctx.sessionManager.getBranch() as NativeEntry[]).filter(
1982
- (e) => !turn.before.has(e.id) && e.message?.role === "toolResult" && callIds.has(e.message.toolCallId),
1983
- );
1984
- const outcome =
1985
- message?.stopReason === "error" ? "failed" : message?.stopReason === "aborted" ? "aborted" : "completed";
1986
- this.append(
1987
- {
1988
- type: "execution-bound",
1989
- executionId: turn.id,
1990
- assistantEntryId: assistant.id,
1991
- resultEntryIds: results.map((e) => e.id),
1992
- outcome,
1993
- },
1994
- JSON.stringify(["binding", turn.id]),
1995
- );
1996
- turn.bound = assistant.id;
1997
- const state = this.stateData(),
1998
- pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
1999
- if (!pending || pending.state === "superseded") return;
2000
- const attempt = state.attempts.get(pending.id);
2001
- if (pending.executionId !== turn.id && attempt?.executionId !== turn.id) return;
2002
- if (ctx.signal?.aborted || outcome === "aborted" || state.control !== "automatic") return;
2003
- const inputs = [...this.messages, message, ...results.map((e) => e.message)].filter(Boolean);
2004
- const latest = this.stateData();
2005
- const retry = latest.attempts.get(pending.id);
2006
- if (retry?.executionId === turn.id)
2007
- check(
2008
- retry.reportRevision === pending.reportRevision &&
2009
- retry.selectionRevision === (latest.selections.get(pending.assignmentId)?.revision ?? 0),
2010
- "stale_retry_revision",
2011
- );
2012
- await this.finishHandoff(pending, inputs, token);
2013
- } catch (error) {
2014
- if (this.current(subject)) {
2015
- this.mark(error);
2016
- ctx.abort?.();
2017
- }
2018
- }
2019
- }
2020
- private async finishHandoff(h: Handoff, inputs: any[], token: string): Promise<void> {
2021
- const subject = this.subject();
2022
- const state = this.stateData();
2023
- check(state.control === "automatic" && !this.store?.blocked && token === this.token, "transition_ineligible");
2024
- const execution = state.executions.get(h.executionId);
2025
- check(
2026
- execution?.assistantEntryId,
2027
- "source_turn_incomplete",
2028
- "Saved communication belongs to an incomplete historical turn. Reconcile its effects and replace or cancel the outstanding work; no tool result is fabricated.",
2029
- );
2030
- const source = new Sources(this.ctx.sessionManager.getBranch(), state);
2031
- const carrier = source.byRef.get(`ctx:${execution.assistantEntryId}`);
2032
- check(
2033
- carrier && !source.exchange(carrier).problems.length,
2034
- "source_exchange_incomplete",
2035
- "The saved handoff has an incomplete native exchange; reconcile or explicitly dispose of it.",
2036
- );
2037
- let attentionProblems: Problem[] = [];
2038
- if (["return", "recovery-return"].includes(h.kind) && this.projectionEnabled) {
2039
- const prepared = this.prepared("coordinator", inputs, h.id);
2040
- if (!prepared.ready) {
2041
- const attentionFallback = h.kind === "recovery-return" && h.outcome !== "completed";
2042
- this.projectionError = prepared.problems.map((p) => p.code).join(", ");
2043
- if (attentionFallback) attentionProblems = prepared.problems;
2044
- else {
2045
- this.append({
2046
- type: "handoff-state",
2047
- handoffId: h.id,
2048
- state: "blocked",
2049
- reason: prepared.problems
2050
- .map((p) => p.detail)
2051
- .join("; ")
2052
- .slice(0, 4096),
2053
- });
2054
- return;
2055
- }
2056
- } else {
2057
- check(prepared.reservation, "reservation_missing");
2058
- this.append({ type: "handoff-prepared", handoffId: h.id, reservation: prepared.reservation });
2059
- }
2060
- }
2061
- try {
2062
- const current = this.stateData();
2063
- check(current.pendingId === h.id && current.control === "automatic" && token === this.token, "stale_handoff");
2064
- await this.applyPair(this.profilePair(h.to));
2065
- this.guard(subject);
2066
- this.append({ type: "handoff-state", handoffId: h.id, state: "configured", observedPair: this.observed() });
2067
- const completed = this.stateData();
2068
- const reservation = completed.reservations.get(h.id);
2069
- const recovery =
2070
- h.kind === "recovery-return"
2071
- ? [...completed.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id)
2072
- : undefined;
2073
- if (h.kind === "recovery-return" && recovery && completed.assessment?.view === "suspended") {
2074
- if (attentionProblems.length) {
2075
- this.append({
2076
- type: "assessment-suspended",
2077
- handoffId: recovery.assessmentHandoffId,
2078
- reason: "delivery-gap",
2079
- basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
2080
- problems: attentionProblems,
2081
- });
2082
- } else if (completed.assessment.suspensionReason === "recovery") {
2083
- this.append({
2084
- type: "assessment-resumed",
2085
- handoffId: recovery.assessmentHandoffId,
2086
- basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
2087
- ...(reservation ? { reservation } : {}),
2088
- });
2089
- }
2090
- }
2091
- if (h.kind === "return" && completed.assessment?.view === "suspended" && reservation) {
2092
- this.append({
2093
- type: "assessment-resumed",
2094
- handoffId: h.id,
2095
- basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
2096
- reservation,
2097
- });
2098
- }
2099
- if (!attentionProblems.length) this.projectionError = undefined;
2100
- } catch (error) {
2101
- if (this.current(subject) && !this.store?.blocked && !this.error && this.stateData().pendingId === h.id) {
2102
- this.append({
2103
- type: "handoff-state",
2104
- handoffId: h.id,
2105
- state: "blocked",
2106
- reason: error instanceof Error ? error.message.slice(0, 4096) : "configuration_failed",
2107
- });
2108
- this.ctx.ui?.notify?.(
2109
- `Couldn’t switch to ${h.to.charAt(0).toUpperCase()}${h.to.slice(1)} — ${["return", "recovery-return"].includes(h.kind) ? "report" : "assignment"} saved.`,
2110
- "warning",
2111
- );
2112
- } else throw error;
2113
- }
2114
- }
2115
- async resume(ctx: any): Promise<void> {
2116
- this.ctx = ctx;
2117
- check(ctx.isIdle?.(), "not_idle");
2118
- check(this.store && this.capability?.effective, "routing_unavailable");
2119
- this.revision++;
2120
- const subject = this.subject();
2121
- return this.enqueue(() => this.resumeCurrent(ctx, subject));
2122
- }
2123
- private async resumeCurrent(ctx: any, subject: Subject): Promise<void> {
2124
- this.guard(subject);
2125
- check(this.store, "routing_unavailable");
2126
- await this.store.reconcile();
2127
- this.guard(subject);
2128
- this.error = undefined;
2129
- this.retireUnbound("Explicit resume retires an interrupted attempt without claiming its effects completed.");
2130
- const state = this.stateData();
2131
- check(state.control === "automatic", "manual_control");
2132
- check(
2133
- !ctx.hasPendingMessages?.(),
2134
- "pending_input",
2135
- "Queued input must be delivered and reconciled before resuming a worker.",
2136
- );
2137
- const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
2138
- if (pending) {
2139
- const source = ctx.sessionManager.buildSessionContext?.().messages;
2140
- check(Array.isArray(source) || this.messages.length, "resume_context_unavailable");
2141
- await this.finishHandoff(pending, source ?? this.messages, this.token);
2142
- this.guard(subject);
2143
- if (this.stateData().pendingId) {
2144
- check(
2145
- ["return", "recovery-return"].includes(pending.kind) && this.projectionError,
2146
- "handoff_still_blocked",
2147
- "Saved transfer is still blocked; reconcile its configuration before retrying.",
2148
- );
2149
- const worker = this.assignedWorker(this.stateData(), pending.assignmentId);
2150
- this.append({
2151
- type: "control",
2152
- control: "automatic",
2153
- profile: worker,
2154
- reason: "Explicit resume of saved-return evidence correction only; ordinary assignment work remains ended",
2155
- });
2156
- await this.applyPair(this.profilePair(worker));
2157
- }
2158
- } else {
2159
- const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
2160
- const input = ctx.sessionManager.buildSessionContext?.().messages ?? this.messages;
2161
- const user = this.lastDeliveredUser(new Sources(ctx.sessionManager.getBranch(), state), input);
2162
- const coordinatorSawInput = [...state.executions.values()].some(
2163
- (x) =>
2164
- x.profile === "coordinator" && x.basisUserEntryId === user && x.assistantEntryId && x.outcome === "completed",
2165
- );
2166
- const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
2167
- const worker = assignment ? this.assignedWorker(state, assignment.id) : undefined;
2168
- if (assignment?.state === "returned" && recovery?.state === "reading") {
2169
- check(
2170
- user === this.workerBasis(state, assignment.id) || coordinatorSawInput,
2171
- "input_unreconciled",
2172
- "New input needs Coordinator attention before evidence recovery can resume.",
2173
- );
2174
- this.append({
2175
- type: "control",
2176
- control: "automatic",
2177
- profile: worker!,
2178
- reason: "Explicit user resume of unchanged evidence recovery; ordinary assignment work remains ended",
2179
- });
2180
- await this.applyPair(this.profilePair(worker!));
2181
- } else if (assignment?.state === "outstanding") {
2182
- check(
2183
- user === this.taskBasis(state, assignment.id) || coordinatorSawInput,
2184
- "input_unreconciled",
2185
- "New input needs Coordinator attention before the unchanged assignment can resume.",
2186
- );
2187
- this.append({ type: "assignment-resumed", assignmentId: assignment.id, basisUserEntryId: user });
2188
- this.append({
2189
- type: "control",
2190
- control: "automatic",
2191
- profile: worker!,
2192
- reason: "Explicit user resume of the unchanged assignment; current restrictions remain applicable",
2193
- });
2194
- await this.applyPair(this.profilePair(worker!));
2195
- } else await this.applyPair(this.profilePair("coordinator"));
2196
- }
2197
- this.guard(subject);
2198
- this.pi.sendMessage(
2199
- {
2200
- customType: "freeflow-routing-v2-resume",
2201
- content:
2202
- "Explicit user resume of the saved routing responsibility. Recover the current contract and evidence before continuing.",
2203
- display: true,
2204
- },
2205
- { triggerTurn: true },
2206
- );
2207
- }
2208
- async command(args: string, ctx: any): Promise<boolean> {
2209
- const words = args.trim().split(/\s+/);
2210
- if (!["profile", "resume"].includes(words[0])) return false;
2211
- try {
2212
- if (words[0] === "profile" && words[1] === "history") {
2213
- check(words.length === 2 || (words.length === 3 && words[2] === "diagnostics"), "invalid_history_command");
2214
- ctx.ui.notify(
2215
- JSON.stringify(
2216
- words[2] === "diagnostics" ? this.status(30) : this.inspectUnit({ view: "history", limit: 30 }),
2217
- ),
2218
- "info",
2219
- );
2220
- return true;
2221
- }
2222
- check(ctx.isIdle?.(), "not_idle");
2223
- this.ctx = ctx;
2224
- if (words[0] === "resume" && words.length === 1) {
2225
- await this.resume(ctx);
2226
- return true;
2227
- }
2228
- check(
2229
- words.length === 2 && [...PROFILES, "auto"].includes(words[1]),
2230
- "invalid_profile_command",
2231
- "Use /freeflow profile coordinator|helper|executor|auto|history, or /freeflow resume.",
2232
- );
2233
- const result =
2234
- words[1] === "auto" ? await this.setAutomaticControl() : await this.setManualProfile(words[1] as Profile);
2235
- ctx.ui.notify(JSON.stringify(result), result.status === "blocked" ? "warning" : "info");
2236
- } catch (error) {
2237
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
2238
- }
2239
- return true;
2240
- }
2241
- }