@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
@@ -0,0 +1,899 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { changeSelection, representationProblems } from "./projection.js";
3
+ import { bodyHash, isTaskEvidence } from "./sources.js";
4
+ import { RoutingError, canonical, emptySelection, idFor, isWorkerProfile, requireCondition as check } from "./types.js";
5
+ import { cachedBranch } from "../host/branch.js";
6
+ /**
7
+ * Runs the routing tools (freeflow_delegate, freeflow_return, freeflow_project, freeflow_unit) one at a time on the
8
+ * session's queue. Each accepted call is recorded once under its operation id, so a repeated call returns the saved
9
+ * result instead of acting again; a refused call returns a blocked result that names what to reconcile.
10
+ */
11
+ export class ToolHandlers {
12
+ session;
13
+ models;
14
+ gate;
15
+ assembler;
16
+ status;
17
+ constructor(session, models, gate, assembler, status) {
18
+ this.session = session;
19
+ this.models = models;
20
+ this.gate = gate;
21
+ this.assembler = assembler;
22
+ this.status = status;
23
+ }
24
+ resultGrants;
25
+ effectFence;
26
+ setResultGrantPort(port) {
27
+ this.resultGrants = port;
28
+ }
29
+ setEffectFencePort(port) {
30
+ this.effectFence = port;
31
+ }
32
+ callOperation(callId, name) {
33
+ check(this.session.turn && this.session.store, "execution_missing");
34
+ this.gate.batch(callId, name);
35
+ return JSON.stringify([this.session.store.reader.getSessionId(), this.session.turn.id, callId, name]);
36
+ }
37
+ result(value) {
38
+ return { content: [{ type: "text", text: JSON.stringify(value) }], details: value };
39
+ }
40
+ async invoke(name, callId, input, signal, ctx) {
41
+ return this.session.enqueue(async () => {
42
+ this.session.ctx = ctx;
43
+ try {
44
+ check(!signal?.aborted, "cancelled");
45
+ if (name === "freeflow_unit" && input.operation === "inspect") {
46
+ check(this.session.supported(), "routing_unavailable");
47
+ return this.result(this.inspectUnit(input));
48
+ }
49
+ const state = this.session.stateData();
50
+ const expected =
51
+ name === "freeflow_delegate" || name === "freeflow_unit" ? "coordinator" : this.session.assignedWorker(state);
52
+ this.session.assertAvailable(expected);
53
+ const op = this.callOperation(callId, name);
54
+ const cached = this.session.receipts.get(op);
55
+ if (cached) {
56
+ check(cached.input === canonical(input), "operation_conflict");
57
+ return this.result(structuredClone(cached.value));
58
+ }
59
+ const value =
60
+ name === "freeflow_delegate"
61
+ ? this.delegate(input, callId, op)
62
+ : name === "freeflow_return"
63
+ ? this.returnReport(input, callId, op)
64
+ : name === "freeflow_project"
65
+ ? this.project(input, op)
66
+ : name === "freeflow_unit"
67
+ ? await this.unit(input, callId, op)
68
+ : undefined;
69
+ if (value !== undefined) {
70
+ this.session.receipts.set(op, { input: canonical(input), value: structuredClone(value) });
71
+ return this.result(value);
72
+ }
73
+ throw new RoutingError("unknown_tool", name);
74
+ } catch (error) {
75
+ return this.result({
76
+ status: "blocked",
77
+ code: error instanceof RoutingError ? error.code : "operation_failed",
78
+ message: error instanceof Error ? error.message : String(error),
79
+ problems: error instanceof RoutingError ? error.problems : [],
80
+ observed: this.session.observed(),
81
+ expectedProfile:
82
+ name === "freeflow_unit" && input.operation === "inspect"
83
+ ? undefined
84
+ : name === "freeflow_delegate" || name === "freeflow_unit"
85
+ ? "coordinator"
86
+ : (() => {
87
+ try {
88
+ return this.session.assignedWorker(this.session.stateData());
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ })(),
93
+ recoveryAction:
94
+ error instanceof RoutingError &&
95
+ [
96
+ "cursor_expired",
97
+ "invalid_cursor",
98
+ "work_ref_required",
99
+ "invalid_work_ref",
100
+ "work_unavailable",
101
+ "invalid_inspection",
102
+ ].includes(error.code)
103
+ ? "Inspect again without a cursor and use a returned work ref. Lookup recovery does not require a new assignment."
104
+ : "Preserve saved work. Use freeflow_unit inspect; reconcile current input/control before retrying the named operation.",
105
+ });
106
+ }
107
+ });
108
+ }
109
+ duplicate(op) {
110
+ return [...this.session.stateData().events.values()].find(
111
+ (e) =>
112
+ e.operationId === op &&
113
+ [
114
+ "delegate-accepted",
115
+ "return-accepted",
116
+ "recovery-request-accepted",
117
+ "recovery-supplement-accepted",
118
+ "recovery-cancelled",
119
+ "handoff-retry-requested",
120
+ ].includes(e.data.type),
121
+ );
122
+ }
123
+ delegate(input, callId, op) {
124
+ const prior = this.duplicate(op);
125
+ if (prior) {
126
+ check(
127
+ prior.data.type === "delegate-accepted" &&
128
+ prior.data.assignment.contract === input.contract &&
129
+ (input.worker === undefined || prior.data.handoff.to === input.worker),
130
+ "operation_conflict",
131
+ );
132
+ return { status: "accepted", handoff: prior.data.handoff.id, unchanged: true };
133
+ }
134
+ const state = this.session.stateData();
135
+ const enabled = this.session.enabledWorkers();
136
+ let worker;
137
+ if (input.worker === undefined) {
138
+ check(enabled.length === 1, "worker_required", "Delegation mode 'both' requires choosing helper or executor.");
139
+ worker = enabled[0];
140
+ } else {
141
+ check(isWorkerProfile(input.worker), "invalid_worker");
142
+ worker = input.worker;
143
+ }
144
+ check(enabled.includes(worker), "worker_disabled", `${worker} is not enabled by delegation mode.`);
145
+ const unit = state.unitId
146
+ ? state.units.get(state.unitId)
147
+ : { id: randomUUID(), objective: input.contract, state: "open", assignmentIds: [] };
148
+ const old = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
149
+ if (input.operation === "replace")
150
+ check(old?.state === "outstanding" && !this.models.applying, "no_quiescent_assignment");
151
+ else
152
+ check(
153
+ input.operation === "assign" && old?.state !== "outstanding" && !state.pendingId && !state.recoveryId,
154
+ "assignment_outstanding",
155
+ );
156
+ const assignmentId = randomUUID(),
157
+ id = randomUUID();
158
+ const assignment = {
159
+ id: assignmentId,
160
+ unitId: unit.id,
161
+ contract: input.contract,
162
+ state: "outstanding",
163
+ delegateHandoffId: id,
164
+ basisUserEntryId: this.session.turn.basisUserEntryId,
165
+ };
166
+ const h = {
167
+ id,
168
+ kind: "delegate",
169
+ assignmentId,
170
+ text: input.contract,
171
+ from: "coordinator",
172
+ to: worker,
173
+ executionId: this.session.turn.id,
174
+ toolCallId: callId,
175
+ basisUserEntryId: this.session.turn.basisUserEntryId,
176
+ state: "pending",
177
+ reportRevision: 0,
178
+ limitations: [],
179
+ };
180
+ this.session.append(
181
+ {
182
+ type: "delegate-accepted",
183
+ unit: { ...unit, assignmentIds: [...unit.assignmentIds, assignmentId] },
184
+ assignment,
185
+ handoff: h,
186
+ ...(input.operation === "replace"
187
+ ? {
188
+ replacement: {
189
+ assignmentId: old.id,
190
+ reason: input.reason,
191
+ ...(state.pendingId ? { supersededHandoffId: state.pendingId } : {}),
192
+ },
193
+ }
194
+ : {}),
195
+ },
196
+ op,
197
+ );
198
+ return {
199
+ status: "accepted",
200
+ unit: unit.id,
201
+ assignment: assignmentId,
202
+ worker,
203
+ handoff: id,
204
+ transition: "pending",
205
+ createdUnit: !state.unitId,
206
+ ...(old && input.operation === "replace"
207
+ ? {
208
+ replacedAssignment: old.id,
209
+ continuity:
210
+ "Prior effects and evidence remain on native history. Reconcile uncertain effects before dependent work.",
211
+ }
212
+ : {}),
213
+ };
214
+ }
215
+ returnReport(input, callId, op) {
216
+ const state = this.session.stateData(),
217
+ a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
218
+ check(a, "assignment_missing");
219
+ const worker = this.session.assignedWorker(state, a.id);
220
+ const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
221
+ const original = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
222
+ const supplement = recovery?.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
223
+ const retryingSupplement = input.operation === "retry" && recovery?.state === "returning";
224
+ const saved = retryingSupplement || input.operation === "supplement" ? supplement : original;
225
+ const selection = state.selections.get(a.id) ?? emptySelection();
226
+ const duplicate = this.duplicate(op);
227
+ if (duplicate) {
228
+ if (input.operation === "retry") check(duplicate.data.type === "handoff-retry-requested", "operation_conflict");
229
+ else {
230
+ const expected = input.operation === "supplement" ? "recovery-supplement-accepted" : "return-accepted";
231
+ check(
232
+ duplicate.data.type === expected &&
233
+ duplicate.data.handoff.text === input.report &&
234
+ duplicate.data.handoff.outcome === input.outcome &&
235
+ canonical(duplicate.data.handoff.limitations) === canonical(input.limitations ?? []),
236
+ "operation_conflict",
237
+ );
238
+ }
239
+ const handoff = duplicate.data.handoffId ?? duplicate.data.handoff.id;
240
+ return { ...this.returnReadiness(handoff), unchanged: true };
241
+ }
242
+ if (input.operation === "retry") {
243
+ check(saved && ["blocked", "pending"].includes(saved.state), "saved_return_missing");
244
+ this.session.append(
245
+ {
246
+ type: "handoff-retry-requested",
247
+ handoffId: saved.id,
248
+ attemptId: randomUUID(),
249
+ executionId: this.session.turn.id,
250
+ toolCallId: callId,
251
+ reportRevision: saved.reportRevision,
252
+ selectionRevision: selection.revision,
253
+ },
254
+ op,
255
+ );
256
+ return {
257
+ ...this.returnReadiness(saved.id),
258
+ reportUnchanged: true,
259
+ supplementUnchanged: saved.kind === "recovery-return",
260
+ };
261
+ }
262
+ if (input.operation === "supplement") {
263
+ check(
264
+ recovery &&
265
+ ["reading", "returning"].includes(recovery.state) &&
266
+ (!supplement || ["pending", "blocked"].includes(supplement.state)),
267
+ "recovery_not_returnable",
268
+ );
269
+ const h = {
270
+ id: supplement?.id ?? randomUUID(),
271
+ kind: "recovery-return",
272
+ assignmentId: a.id,
273
+ text: input.report,
274
+ from: worker,
275
+ to: "coordinator",
276
+ executionId: this.session.turn.id,
277
+ toolCallId: callId,
278
+ basisUserEntryId: this.session.turn.basisUserEntryId,
279
+ state: "pending",
280
+ reportRevision: (supplement?.reportRevision ?? 0) + 1,
281
+ outcome: input.outcome,
282
+ limitations: input.limitations ?? [],
283
+ };
284
+ this.session.append({ type: "recovery-supplement-accepted", recoveryId: recovery.id, handoff: h }, op);
285
+ return this.returnReadiness(h.id);
286
+ }
287
+ check(input.operation === "submit", "invalid_return_operation");
288
+ check(
289
+ input.outcome !== "completed" || !this.effectFence?.status().unresolvedEffects,
290
+ "unresolved_effect",
291
+ "A clean completed return is unavailable while a live effect requires reconciliation.",
292
+ );
293
+ check(
294
+ a.state === "outstanding" ||
295
+ (a.state === "returned" && original && ["pending", "blocked"].includes(original.state)),
296
+ "assignment_task_ended",
297
+ );
298
+ const h = {
299
+ id: original?.id ?? randomUUID(),
300
+ kind: "return",
301
+ assignmentId: a.id,
302
+ text: input.report,
303
+ from: worker,
304
+ to: "coordinator",
305
+ executionId: this.session.turn.id,
306
+ toolCallId: callId,
307
+ basisUserEntryId: this.session.turn.basisUserEntryId,
308
+ state: "pending",
309
+ reportRevision: (original?.reportRevision ?? 0) + 1,
310
+ outcome: input.outcome,
311
+ limitations: input.limitations ?? [],
312
+ };
313
+ this.session.append({ type: "return-accepted", handoff: h }, op);
314
+ return this.returnReadiness(h.id);
315
+ }
316
+ returnReadiness(id) {
317
+ const state = this.session.stateData();
318
+ const h = state.handoffs.get(id);
319
+ const prepared = this.assembler.prepared("coordinator", this.session.messages, id);
320
+ const isSupplement = h.kind === "recovery-return";
321
+ const recovery = isSupplement
322
+ ? [...state.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id)
323
+ : undefined;
324
+ return {
325
+ status: "accepted",
326
+ reportSaved: !isSupplement,
327
+ supplementSaved: isSupplement,
328
+ handoff: id,
329
+ ...(recovery ? { recovery: recovery.id } : {}),
330
+ ...(isSupplement ? { supplementRevision: h.reportRevision } : { reportRevision: h.reportRevision }),
331
+ assignmentRef: `assignment:${h.assignmentId}`,
332
+ ...(isSupplement
333
+ ? { supplementRef: `supplement:${recovery?.id ?? "unknown"}:${h.reportRevision}` }
334
+ : { reportRef: `report:${h.id}:${h.reportRevision}` }),
335
+ producer: h.from,
336
+ outcome: h.outcome,
337
+ limitations: h.limitations,
338
+ ready: prepared.ready,
339
+ transition: prepared.ready ? "pending" : "blocked",
340
+ problems: prepared.problems,
341
+ warnings: prepared.warnings,
342
+ stage: "completed-input preparation",
343
+ finalExchangePending: true,
344
+ provisional:
345
+ "The finalized handoff exchange, target configuration and budget are revalidated at turn_end; this is not delivery evidence.",
346
+ // The text is already in the call arguments; the hash identifies it without a second copy.
347
+ ...(isSupplement ? { supplementSha256: bodyHash(h.text) } : { reportSha256: bodyHash(h.text) }),
348
+ evidence: this.assembler.evidenceFacts(state),
349
+ };
350
+ }
351
+ project(input, op) {
352
+ check(this.session.projectionEnabled, "projection_disabled");
353
+ const state = this.session.stateData(),
354
+ a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
355
+ check(a, "assignment_missing");
356
+ const sources = this.session.sources(state);
357
+ sources.associate(this.session.messages);
358
+ const selection = state.selections.get(a.id) ?? emptySelection();
359
+ let next = selection;
360
+ if (input.operation !== "inspect") {
361
+ check(["add", "remove"].includes(input.operation), "invalid_projection_operation");
362
+ next = changeSelection(selection, input, sources, state);
363
+ if (next !== selection)
364
+ this.session.append({ type: "selection-changed", assignmentId: a.id, selection: next }, op);
365
+ }
366
+ const prepared = this.assembler.prepared("coordinator", this.session.messages, a.returnHandoffId);
367
+ const scope = input.scope ?? "assignment";
368
+ let page;
369
+ if (input.operation === "inspect") {
370
+ const model = this.models.model("coordinator");
371
+ const checksFor = (s) => {
372
+ const eligibility = sources.selectionProblem(s.ref, state);
373
+ const limitations = [
374
+ ...(eligibility ? [eligibility] : []),
375
+ ...representationProblems(s, model),
376
+ ...sources.exchange(s).problems,
377
+ ];
378
+ return { eligible: !eligibility, targetReady: !limitations.length, limitations };
379
+ };
380
+ const rows = () =>
381
+ [...sources.byRef.values()]
382
+ .filter(
383
+ (s) =>
384
+ isWorkerProfile(s.producer) &&
385
+ (isTaskEvidence(s) || (scope === "selected" && next.selected.includes(s.ref))) &&
386
+ (scope === "selected"
387
+ ? next.selected.includes(s.ref)
388
+ : scope === "assignment"
389
+ ? s.assignmentId === a.id
390
+ : scope === "active"
391
+ ? s.active
392
+ : true),
393
+ )
394
+ .map((s) => {
395
+ const locator = sources.locator(s.ref);
396
+ return {
397
+ ref: s.ref,
398
+ kind: s.message.role,
399
+ producer: s.producer,
400
+ assignment: s.assignmentId,
401
+ toolName: s.message.toolName,
402
+ ...(locator ? { locator } : {}),
403
+ active: s.active,
404
+ selected: next.selected.includes(s.ref),
405
+ retainedSelection: next.selected.includes(s.ref) && !isTaskEvidence(s),
406
+ ...checksFor(s),
407
+ };
408
+ })
409
+ // Discovery offers usable candidates. Saved selections must remain
410
+ // inspectable even when a later source or target check fails.
411
+ .filter((row) => scope === "selected" || row.targetReady);
412
+ page = this.page(
413
+ `evidence:${scope}`,
414
+ rows,
415
+ input.cursor,
416
+ 30,
417
+ scope === "assignment" || scope === "selected" ? a.id : undefined,
418
+ scope === "active",
419
+ (rows) => ({
420
+ scopeCounts: {
421
+ candidates: rows.length,
422
+ eligible: rows.filter((r) => r.eligible).length,
423
+ targetReady: rows.filter((r) => r.targetReady).length,
424
+ },
425
+ otherAssignments: [...sources.byRef.values()].filter(
426
+ (s) =>
427
+ isWorkerProfile(s.producer) && s.assignmentId !== a.id && isTaskEvidence(s) && checksFor(s).targetReady,
428
+ ).length,
429
+ }),
430
+ );
431
+ }
432
+ return {
433
+ status: input.operation === "inspect" || next === selection ? "unchanged" : "saved",
434
+ ...(page
435
+ ? {
436
+ scope,
437
+ count: page.count,
438
+ returned: page.items.length,
439
+ candidates: page.items,
440
+ nextCursor: page.nextCursor,
441
+ ...page.summary,
442
+ pageCounts: {
443
+ candidates: page.items.length,
444
+ eligible: page.items.filter((r) => r.eligible).length,
445
+ targetReady: page.items.filter((r) => r.targetReady).length,
446
+ },
447
+ countsBasis:
448
+ "Inspection snapshot; page counts describe returned candidates, scope counts describe all candidates in this scope.",
449
+ historyHint: page.summary.otherAssignments
450
+ ? "Previously exposed task evidence exists in other assignments; inspect scope history when needed."
451
+ : undefined,
452
+ }
453
+ : {}),
454
+ revision: next.revision,
455
+ selected: next.selected,
456
+ selectedCount: next.selected.length,
457
+ unresolved: next.unresolved,
458
+ ready: !next.unresolved.length && prepared.ready,
459
+ problems: prepared.problems,
460
+ warnings: prepared.warnings,
461
+ evidence: this.assembler.evidenceFacts(this.session.stateData()),
462
+ items: (input.refs ? [...new Set(input.refs)] : []).map((ref) => ({
463
+ ref,
464
+ status: next.unresolved.some((p) => p.ref === ref)
465
+ ? "rejected"
466
+ : input.operation === "remove"
467
+ ? selection.selected.includes(ref)
468
+ ? "removed"
469
+ : selection.unresolved.some((p) => p.ref === ref)
470
+ ? "withdrawn"
471
+ : "unchanged"
472
+ : selection.selected.includes(ref)
473
+ ? "already_selected"
474
+ : "added",
475
+ })),
476
+ estimate: { tokens: prepared.estimatedTokens, method: prepared.estimateMethod },
477
+ };
478
+ }
479
+ async unit(input, callId, op) {
480
+ const state = this.session.stateData();
481
+ const prior = [...state.events.values()].find((e) => e.operationId === op);
482
+ if (prior) {
483
+ if (input.operation === "recover") {
484
+ check(
485
+ prior.data.type === "recovery-request-accepted" &&
486
+ prior.data.recovery.request === input.request &&
487
+ canonical(prior.data.recovery.requestedPaths) === canonical([...new Set(input.paths ?? [])]) &&
488
+ canonical(prior.data.recovery.requestedResults ?? []) === canonical(input.results ?? []),
489
+ "operation_conflict",
490
+ );
491
+ const { recovery, handoff } = prior.data;
492
+ return {
493
+ status: "accepted",
494
+ unchanged: true,
495
+ recovery: recovery.id,
496
+ handoff: handoff.id,
497
+ assignmentRef: `assignment:${recovery.assignmentId}`,
498
+ reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
499
+ paths: recovery.paths,
500
+ results: recovery.results ?? [],
501
+ transition: handoff.state,
502
+ };
503
+ }
504
+ if (input.operation === "cancel-recovery") {
505
+ check(prior.data.type === "recovery-cancelled" && prior.data.reason === input.reason, "operation_conflict");
506
+ return {
507
+ status: "cancelled",
508
+ unchanged: true,
509
+ recovery: prior.data.recoveryId,
510
+ reason: prior.data.reason,
511
+ assessment: state.assessment?.handoffId,
512
+ };
513
+ }
514
+ check(
515
+ (input.operation === "assess" && prior.data.type === "assessment-resumed") ||
516
+ (input.operation === "close" &&
517
+ prior.data.type === "unit-closed" &&
518
+ prior.data.outcome === input.outcome &&
519
+ prior.data.assessment === input.assessment),
520
+ "operation_conflict",
521
+ );
522
+ return { status: "unchanged", operation: input.operation };
523
+ }
524
+ if (input.operation === "recover") {
525
+ const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
526
+ const base = a?.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
527
+ check(
528
+ state.unitId &&
529
+ a?.state === "returned" &&
530
+ base?.kind === "return" &&
531
+ base.state === "configured" &&
532
+ state.assessment?.handoffId === base.id &&
533
+ !state.pendingId &&
534
+ !state.recoveryId &&
535
+ !this.models.applying,
536
+ "recovery_not_available",
537
+ );
538
+ const worker = this.session.assignedWorker(state, a.id);
539
+ const id = randomUUID(),
540
+ handoffId = randomUUID();
541
+ const paths = [...new Set((input.paths ?? []).map((path) => this.gate.canonicalRecoveryPath(path)))];
542
+ const requestedResults = [...(input.results ?? [])];
543
+ check(new Set(requestedResults).size === requestedResults.length, "duplicate_result_grant");
544
+ const results = [];
545
+ for (const resultId of requestedResults) {
546
+ // A context ref is evidence the worker selects, not a captured result the Coordinator grants.
547
+ check(
548
+ !idFor(resultId),
549
+ "result_unavailable",
550
+ `${resultId} is a context ref, not a captured result id: name it in the request instead, and the worker selects it as evidence with freeflow_project.`,
551
+ );
552
+ const grant = await this.resultGrants?.resolve(resultId, this.session.ctx);
553
+ check(grant?.id === resultId, "result_unavailable", `Captured result is unavailable: ${resultId}`);
554
+ results.push(grant);
555
+ }
556
+ const recovery = {
557
+ id,
558
+ assignmentId: a.id,
559
+ assessmentHandoffId: base.id,
560
+ baseReportRevision: base.reportRevision,
561
+ request: input.request,
562
+ requestedPaths: [...new Set(input.paths ?? [])],
563
+ paths,
564
+ requestedResults,
565
+ results,
566
+ state: "requested",
567
+ requestHandoffId: handoffId,
568
+ supplementRevision: 0,
569
+ };
570
+ const handoff = {
571
+ id: handoffId,
572
+ kind: "recovery-request",
573
+ assignmentId: a.id,
574
+ text: input.request,
575
+ from: "coordinator",
576
+ to: worker,
577
+ executionId: this.session.turn.id,
578
+ toolCallId: callId,
579
+ basisUserEntryId: this.session.turn.basisUserEntryId,
580
+ state: "pending",
581
+ reportRevision: 0,
582
+ limitations: [],
583
+ };
584
+ this.session.append({ type: "recovery-request-accepted", recovery, handoff }, op);
585
+ return {
586
+ status: "accepted",
587
+ recovery: id,
588
+ handoff: handoffId,
589
+ assignmentRef: `assignment:${a.id}`,
590
+ reportRef: `report:${base.id}:${base.reportRevision}`,
591
+ paths,
592
+ results,
593
+ transition: "pending",
594
+ };
595
+ }
596
+ if (input.operation === "cancel-recovery") {
597
+ check(state.recoveryId && !this.models.applying, "recovery_not_cancellable");
598
+ check(
599
+ ![...state.executions.values()].some(
600
+ (execution) =>
601
+ execution.recoveryId === state.recoveryId &&
602
+ isWorkerProfile(execution.profile) &&
603
+ !execution.assistantEntryId &&
604
+ !execution.interrupted,
605
+ ),
606
+ "recovery_not_cancellable",
607
+ );
608
+ this.session.append({ type: "recovery-cancelled", recoveryId: state.recoveryId, reason: input.reason }, op);
609
+ return {
610
+ status: "cancelled",
611
+ recovery: state.recoveryId,
612
+ reason: input.reason,
613
+ assessment: state.assessment?.handoffId,
614
+ };
615
+ }
616
+ if (input.operation === "assess") {
617
+ check(state.assessment, "assessment_missing");
618
+ check(!state.recoveryId, "recovery_outstanding");
619
+ if (state.assessment.view === "active") return { status: "unchanged", ready: true };
620
+ const prepared = this.assembler.prepared("coordinator", this.session.messages, state.assessment.handoffId, true);
621
+ if (!prepared.ready) return { status: "suspended", ready: false, problems: prepared.problems };
622
+ if (this.session.projectionEnabled) check(prepared.reservation, "reservation_missing");
623
+ this.session.append(
624
+ {
625
+ type: "assessment-resumed",
626
+ handoffId: state.assessment.handoffId,
627
+ basisUserEntryId: this.session.turn.basisUserEntryId,
628
+ ...(prepared.reservation ? { reservation: prepared.reservation } : {}),
629
+ },
630
+ op,
631
+ );
632
+ this.session.projectionError = undefined;
633
+ return {
634
+ status: "resumed",
635
+ ready: true,
636
+ stage: "prepared assessment; next request revalidated",
637
+ evidence: this.assembler.evidenceFacts(this.session.stateData()),
638
+ };
639
+ }
640
+ check(input.operation === "close" && state.unitId, "unit_missing");
641
+ const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
642
+ check(!this.models.applying, "not_quiescent");
643
+ check(
644
+ input.outcome !== "accepted" || !this.effectFence?.status().unresolvedEffects,
645
+ "unresolved_effect",
646
+ "Accepted closure is unavailable while a live effect requires reconciliation.",
647
+ );
648
+ if (state.recoveryId) {
649
+ check(input.outcome !== "accepted", "unfinished_work");
650
+ check(
651
+ ![...state.executions.values()].some(
652
+ (execution) =>
653
+ execution.recoveryId === state.recoveryId &&
654
+ isWorkerProfile(execution.profile) &&
655
+ !execution.assistantEntryId &&
656
+ !execution.interrupted,
657
+ ),
658
+ "not_quiescent",
659
+ );
660
+ }
661
+ this.session.append(
662
+ {
663
+ type: "unit-closed",
664
+ unitId: state.unitId,
665
+ outcome: input.outcome,
666
+ assessment: input.assessment,
667
+ ...(a?.state === "outstanding" && input.outcome !== "accepted" ? { supersededAssignmentId: a.id } : {}),
668
+ },
669
+ op,
670
+ );
671
+ return { status: "closed", unit: state.unitId, outcome: input.outcome, assessment: input.assessment };
672
+ }
673
+ page(scope, rows, cursor, size = 30, assignment, active = false, summarize) {
674
+ const branch = cachedBranch(this.session.ctx.sessionManager);
675
+ const compaction = branch.filter((e) => e.type === "compaction").at(-1)?.id;
676
+ let key,
677
+ offset = 0;
678
+ if (cursor) {
679
+ const match = /^([a-f0-9-]+):([0-9]+)$/.exec(cursor);
680
+ check(match, "invalid_cursor", "Use the returned cursor unchanged.");
681
+ key = match[1];
682
+ offset = Number(match[2]);
683
+ const page = this.session.pages.get(key);
684
+ check(
685
+ page &&
686
+ page.scope === scope &&
687
+ page.assignment === assignment &&
688
+ (!page.basis || branch.some((e) => e.id === page.basis)) &&
689
+ (!active || page.compaction === compaction),
690
+ "cursor_expired",
691
+ "History or scope changed. Inspect again without a cursor.",
692
+ );
693
+ check(
694
+ Number.isSafeInteger(offset) && offset >= 0 && offset < page.rows.length && offset % page.size === 0,
695
+ "invalid_cursor",
696
+ );
697
+ } else {
698
+ key = randomUUID();
699
+ if (this.session.pages.size >= 8) this.session.pages.delete(this.session.pages.keys().next().value);
700
+ const items = typeof rows === "function" ? rows() : rows;
701
+ this.session.pages.set(key, {
702
+ scope,
703
+ basis: branch.at(-1)?.id ?? null,
704
+ assignment,
705
+ compaction,
706
+ rows: items,
707
+ size,
708
+ summary: summarize?.(items),
709
+ });
710
+ }
711
+ const page = this.session.pages.get(key);
712
+ return {
713
+ scope,
714
+ count: page.rows.length,
715
+ offset,
716
+ summary: page.summary,
717
+ items: page.rows.slice(offset, offset + page.size),
718
+ nextCursor: offset + page.size < page.rows.length ? `${key}:${offset + page.size}` : undefined,
719
+ };
720
+ }
721
+ inspectUnit(input) {
722
+ const state = this.session.stateData(),
723
+ view = input.view ?? "current";
724
+ check(view === "detail" || !input.ref, "invalid_inspection", "A work ref belongs to view detail.");
725
+ check(
726
+ view === "history" || (!input.cursor && !input.limit),
727
+ "invalid_inspection",
728
+ "Pagination belongs to view history.",
729
+ );
730
+ if (view === "current")
731
+ return {
732
+ ...this.status.detail(),
733
+ status: "inspected",
734
+ view,
735
+ currentRef: state.assignmentId ? `assignment:${state.assignmentId}` : undefined,
736
+ };
737
+ if (view === "history") {
738
+ const numbers = new Map([...state.units.keys()].map((id, i) => [id, i + 1]));
739
+ const rows = () =>
740
+ [...state.assignments.values()].reverse().map((a) => {
741
+ const u = state.units.get(a.unitId),
742
+ h = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
743
+ const delegate = state.handoffs.get(a.delegateHandoffId);
744
+ return {
745
+ ref: `assignment:${a.id}`,
746
+ unitRef: `unit:${a.unitId}`,
747
+ unitNumber: numbers.get(a.unitId),
748
+ assignmentNumber: (u?.assignmentIds.indexOf(a.id) ?? 0) + 1,
749
+ state: a.state,
750
+ unitState: u?.state,
751
+ disposition: u?.disposition,
752
+ summary: a.contract.slice(0, 160),
753
+ reportAvailable: !!h,
754
+ outcome: h?.outcome,
755
+ from: delegate?.from,
756
+ to: delegate?.to,
757
+ };
758
+ });
759
+ const page = this.page("work-history", rows, input.cursor, input.limit ?? 20);
760
+ return {
761
+ status: "inspected",
762
+ view,
763
+ history: page.items,
764
+ count: page.count,
765
+ returned: page.items.length,
766
+ nextCursor: page.nextCursor,
767
+ };
768
+ }
769
+ check(
770
+ view === "detail" && typeof input.ref === "string",
771
+ "work_ref_required",
772
+ "Use a ref returned by history inspection.",
773
+ );
774
+ check(
775
+ /^(?:(?:unit|assignment|recovery):[^:\s]+|(?:report|supplement):[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
776
+ "invalid_work_ref",
777
+ "Use assignment, report, recovery, or supplement refs returned by a receipt or inspection, unchanged. A bare handoff ID is not a work ref.",
778
+ );
779
+ if (input.ref.startsWith("unit:")) {
780
+ const u = state.units.get(input.ref.slice(5));
781
+ check(u, "work_unavailable", "Unit is not on current ancestry.");
782
+ return {
783
+ status: "inspected",
784
+ view,
785
+ ref: input.ref,
786
+ unit: {
787
+ id: u.id,
788
+ state: u.state,
789
+ objective: u.objective,
790
+ disposition: u.disposition,
791
+ assessment: u.assessment,
792
+ },
793
+ assignmentCount: u.assignmentIds.length,
794
+ assignmentRefs: u.assignmentIds.slice(0, 30).map((id) => `assignment:${id}`),
795
+ remaining: Math.max(0, u.assignmentIds.length - 30),
796
+ historyHint: "Use paginated history inspection for all assignment refs.",
797
+ historical: true,
798
+ };
799
+ }
800
+ if (input.ref.startsWith("recovery:")) {
801
+ const recovery = state.recoveries.get(input.ref.slice(9));
802
+ check(recovery, "work_unavailable", "Recovery is not on current ancestry.");
803
+ const request = state.handoffs.get(recovery.requestHandoffId);
804
+ const supplement = recovery.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
805
+ return {
806
+ status: "inspected",
807
+ view,
808
+ ref: input.ref,
809
+ historical: recovery.id !== state.recoveryId,
810
+ recovery: {
811
+ id: recovery.id,
812
+ state: recovery.state,
813
+ assignmentId: recovery.assignmentId,
814
+ assessmentHandoffId: recovery.assessmentHandoffId,
815
+ baseReportRevision: recovery.baseReportRevision,
816
+ request: recovery.request,
817
+ paths: recovery.paths,
818
+ results: recovery.results ?? [],
819
+ requestHandoffState: request?.state,
820
+ supplementRevision: recovery.supplementRevision,
821
+ cancellationReason: recovery.cancellationReason,
822
+ },
823
+ supplementRef: supplement ? `supplement:${recovery.id}:${supplement.reportRevision}` : undefined,
824
+ sourceBoundary: "Saved recovery on current ancestry; historical content grants no current permission.",
825
+ };
826
+ }
827
+ if (input.ref.startsWith("supplement:")) {
828
+ const match = /^supplement:([^:]+):([0-9]+)$/.exec(input.ref);
829
+ check(match, "work_unavailable", "Use a supplement ref returned by a receipt or recovery inspection.");
830
+ const recovery = state.recoveries.get(match[1]);
831
+ let supplement;
832
+ for (const event of state.events.values())
833
+ if (
834
+ event.data.type === "recovery-supplement-accepted" &&
835
+ event.data.recoveryId === match[1] &&
836
+ event.data.handoff.reportRevision === Number(match[2])
837
+ )
838
+ supplement = event.data.handoff;
839
+ check(recovery && supplement, "work_unavailable", "Supplement revision is not on current ancestry.");
840
+ const base = state.handoffs.get(recovery.assessmentHandoffId);
841
+ return {
842
+ status: "inspected",
843
+ view,
844
+ ref: input.ref,
845
+ historical: true,
846
+ recoveryRef: `recovery:${recovery.id}`,
847
+ assignment: { id: recovery.assignmentId, state: state.assignments.get(recovery.assignmentId)?.state },
848
+ reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
849
+ report: base?.text,
850
+ supplement: supplement.text,
851
+ supplementRevision: supplement.reportRevision,
852
+ outcome: supplement.outcome,
853
+ limitations: supplement.limitations,
854
+ sourceBoundary:
855
+ "Saved supplement revision and original report linkage on current ancestry; historical content grants no current permission.",
856
+ };
857
+ }
858
+ let savedRevision;
859
+ if (input.ref.startsWith("report:")) {
860
+ const match = /^report:([^:]+):([0-9]+)$/.exec(input.ref);
861
+ check(match, "work_unavailable", "Use a report ref returned by detail inspection.");
862
+ for (const event of state.events.values())
863
+ if (
864
+ event.data.type === "return-accepted" &&
865
+ event.data.handoff.id === match[1] &&
866
+ event.data.handoff.reportRevision === Number(match[2])
867
+ )
868
+ savedRevision = event.data.handoff;
869
+ check(savedRevision, "work_unavailable", "Report revision is not on current ancestry.");
870
+ }
871
+ const a = savedRevision
872
+ ? state.assignments.get(savedRevision.assignmentId)
873
+ : input.ref.startsWith("assignment:")
874
+ ? state.assignments.get(input.ref.slice(11))
875
+ : undefined;
876
+ check(a, "work_unavailable", "Assignment is not on current ancestry.");
877
+ const latest = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
878
+ const h = savedRevision ?? latest;
879
+ return {
880
+ status: "inspected",
881
+ view,
882
+ ref: input.ref,
883
+ historical: a.id !== state.assignmentId || a.state !== "outstanding",
884
+ assignment: { id: a.id, unitId: a.unitId, state: a.state },
885
+ contract: a.contract,
886
+ report: h?.text,
887
+ reportRevision: h?.reportRevision,
888
+ latestReportRevision: latest?.reportRevision,
889
+ reportRef: h ? `report:${h.id}:${h.reportRevision}` : undefined,
890
+ previousReportRef: h && h.reportRevision > 1 ? `report:${h.id}:${h.reportRevision - 1}` : undefined,
891
+ outcome: h?.outcome,
892
+ limitations: h?.limitations,
893
+ handoff: h?.id,
894
+ currentSelection: state.selections.get(a.id) ?? emptySelection(),
895
+ sourceBoundary:
896
+ "Accepted report revision on current ancestry; assignment/selection show current recorded state. Historical content grants no current permission.",
897
+ };
898
+ }
899
+ }