@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.10

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 (208) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +106 -109
  3. package/README.md +2 -2
  4. package/dist/application/task-lifecycle/advance.js +12 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/update/runtime-activity.js +1 -29
  7. package/dist/commands/dag-rerun.js +1 -1
  8. package/dist/executors/dag-pi-executor.js +69 -7
  9. package/dist/executors/pi-executor.js +4 -2
  10. package/dist/executors/pi-sdk-executor.js +46 -1
  11. package/dist/executors/pi-writer-tool-policy.js +57 -1
  12. package/dist/executors/shell-executor.js +43 -3
  13. package/dist/executors/workspace-write-snapshot.js +68 -0
  14. package/dist/infrastructure/harness/atomic-write.js +23 -0
  15. package/dist/shared/dag-prompt-override.js +4 -5
  16. package/dist/shared/update/npm-client.js +33 -11
  17. package/dist/task/config-types.js +10 -0
  18. package/dist/task/contract/apply.js +11 -1
  19. package/dist/task/contract/constants.js +2 -0
  20. package/dist/task/contract/hash.js +3 -0
  21. package/dist/task/contract/observe.js +25 -4
  22. package/dist/task/contract/paths.js +2 -1
  23. package/dist/task/contract/project.js +15 -1
  24. package/dist/task/contract/schema.js +2 -0
  25. package/dist/task/contract/transaction.js +12 -0
  26. package/dist/task/source-prepare/completeness.js +188 -0
  27. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  28. package/dist/task/source-prepare/index.js +3 -0
  29. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  30. package/dist/task/source-prepare/ledger-review.js +214 -0
  31. package/dist/task/source-prepare/ledger.js +545 -0
  32. package/dist/task/source-prepare/prepare.js +262 -1
  33. package/dist/task/source-prepare/semantic-intake.js +19 -3
  34. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  35. package/dist/task/source-prepare/types.js +22 -0
  36. package/dist/worker/cli.js +12 -26
  37. package/dist/worker/console/chat/chat-event-store.js +75 -5
  38. package/dist/worker/console/chat/context-panel.js +7 -0
  39. package/dist/worker/console/chat/deferred-turn.js +12 -0
  40. package/dist/worker/console/chat/pi-runtime.js +331 -18
  41. package/dist/worker/console/chat/provider-error.js +98 -0
  42. package/dist/worker/console/chat/routes.js +389 -56
  43. package/dist/worker/console/chat/session-stats.js +179 -0
  44. package/dist/worker/console/chat/session-store.js +93 -63
  45. package/dist/worker/console/chat/todos.js +115 -0
  46. package/dist/worker/console/chat/tool-preview.js +42 -0
  47. package/dist/worker/console/chat/turn-process.js +20 -55
  48. package/dist/worker/console/chat/user-questions.js +266 -0
  49. package/dist/worker/console/chat/workspace-landing.js +79 -0
  50. package/dist/worker/console/console-handoff.js +82 -0
  51. package/dist/worker/console/console-update-and-init.js +115 -0
  52. package/dist/worker/console/console-update-runtime.js +84 -0
  53. package/dist/worker/console/draft-store.js +49 -0
  54. package/dist/worker/console/native-directory-picker.js +13 -0
  55. package/dist/worker/console/operation-store.js +1 -1
  56. package/dist/worker/console/operator-actions.js +129 -4
  57. package/dist/worker/console/operator-surface-health.js +1 -0
  58. package/dist/worker/console/prd-intake-bridge.js +13 -0
  59. package/dist/worker/console/routes.js +312 -1
  60. package/dist/worker/console/security.js +4 -4
  61. package/dist/worker/console/server.js +583 -172
  62. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
  63. package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
  64. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
  65. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
  66. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
  67. package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
  68. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
  78. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
  79. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
  80. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
  81. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
  82. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
  83. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
  85. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
  86. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
  89. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
  90. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
  91. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
  92. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
  93. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
  94. package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
  95. package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
  96. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
  97. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
  98. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
  99. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
  100. package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
  101. package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
  102. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
  103. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
  104. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
  105. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
  106. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
  107. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
  108. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
  109. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
  111. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
  112. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
  113. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
  114. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
  115. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
  116. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
  117. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
  118. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
  119. package/dist/worker/console/static/index.html +2 -2
  120. package/dist/worker/console/static-src/app/console-types.js +68 -4
  121. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  122. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  123. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  124. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  125. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  126. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  127. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
  128. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  129. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  130. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  131. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  132. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  133. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  134. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  135. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
  136. package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
  137. package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
  138. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  139. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  140. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  141. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  142. package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
  143. package/dist/worker/console/workspace-context.js +234 -0
  144. package/dist/worker/console/workspace-registry.js +214 -0
  145. package/dist/worker/observability/init-runtime-activity.js +26 -0
  146. package/dist/worker/observe/node-input.js +88 -2
  147. package/dist/worker/observe/routes.js +188 -0
  148. package/dist/worker/observe/server.js +4 -0
  149. package/dist/worker/observe/static/api.js +29 -9
  150. package/dist/worker/observe/static/app.js +18 -70
  151. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  152. package/dist/worker/observe/static/dag-node-purpose.js +203 -0
  153. package/dist/worker/observe/static/format.js +37 -0
  154. package/dist/worker/observe/static/index.html +6 -1
  155. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  156. package/dist/worker/observe/static/operator-chrome.css +145 -20
  157. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  158. package/dist/worker/observe/static/operator-chrome.js +330 -100
  159. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  160. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  161. package/dist/worker/observe/static/router.d.ts +46 -0
  162. package/dist/worker/observe/static/router.js +25 -1
  163. package/dist/worker/observe/static/state.d.ts +50 -0
  164. package/dist/worker/observe/static/state.js +174 -2
  165. package/dist/worker/observe/static/styles.css +357 -35
  166. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  167. package/dist/worker/observe/static/views/dag-inspector.js +533 -79
  168. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  169. package/dist/worker/observe/static/views/dag.js +6 -4
  170. package/dist/worker/observe/static/views/dashboard.js +6 -4
  171. package/dist/worker/observe/static/views/night.js +2 -2
  172. package/dist/worker/observe/static/views/pool.js +1 -0
  173. package/dist/worker/observe/static/views/run.js +3 -1
  174. package/dist/worker/observe/static/views/task.js +56 -1
  175. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
  176. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  177. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  178. package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
  179. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  180. package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
  181. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  182. package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
  183. package/dist/workflows/dag/frontend-review-context.js +92 -10
  184. package/dist/workflows/dag/init-hybrid.js +238 -51
  185. package/dist/workflows/dag/interrupt-request.js +3 -3
  186. package/dist/workflows/dag/lifecycle.js +3 -3
  187. package/dist/workflows/dag/node-execution.js +45 -0
  188. package/dist/workflows/dag/types.js +62 -12
  189. package/docs/README.md +2 -0
  190. package/docs/architecture/evolution.md +45 -0
  191. package/docs/init-surface.manifest.json +1 -0
  192. package/docs/operations/local-development-environment.md +21 -0
  193. package/docs/templates/README.md +1 -1
  194. package/docs/templates/agent-dag.schema.json +18 -0
  195. package/docs/templates/backend-test-dag.json +265 -237
  196. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  197. package/harness.json +3 -3
  198. package/package.json +3 -1
  199. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  200. package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
  201. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
  202. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
  203. package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
  204. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
  205. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
  206. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
  207. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  208. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
@@ -0,0 +1,384 @@
1
+ /**
2
+ * 生产 Source Fidelity Pi 适配器(AC-PROD-001..006 / AC-001..006)
3
+ *
4
+ * 为真实 `task advance` 的 prepare-contract 路径提供两个独立的 Pi 调用面:
5
+ * - reconcile:仅叙述性/歧义 ledger fragment 的顺序 reconciliation executor
6
+ * (结构化片段由 ledger-reconciliation.ts 的 isStructuredMappableFragment
7
+ * 走代码映射,零 Pi 调用);
8
+ * - review:context-only / duplicate / conflict 的独立 reviewer(与 reconciler
9
+ * 是不同调用阶段,最多一次有界修订,LEDGER_REVIEW_MAX_REVISIONS=1)。
10
+ *
11
+ * 模型队列镜像 semantic-intake.ts(MED→HIGH→LOW harness 档位 + Pi SDK 目录),
12
+ * 单次尝试沿用有界超时(SOURCE_FIDELITY_ATTEMPT_TIMEOUT_MS),工具面仅只读
13
+ * read/grep/find/ls(无写工具)。所有模型输出经 validateOutput + zod 严格校验:
14
+ * 无效 JSON/schema、越界 canonical ID、排除类 disposition 缺理由一律抛
15
+ * LedgerReconciliationError / LedgerReviewError,由 prepare.ts 既有 catch 转为
16
+ * 稳定 SOURCE_FIDELITY_* 阻断码,绝不回退确定性 heuristics。队列空 / 执行失败 /
17
+ * 超时同样 fail closed。
18
+ */
19
+ import { z } from "zod";
20
+ import { executePiStep } from "../../executors/pi-executor.js";
21
+ import { resolveDagPiModelConfig } from "../../executors/dag-pi-executor.js";
22
+ import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
23
+ import { listPiSdkAvailableModels } from "../../executors/pi-sdk-executor.js";
24
+ import { loadHarnessManifest } from "../../governance/harness.js";
25
+ import { loadTaskConfig } from "../runtime.js";
26
+ import { buildSemanticIntakeModelQueue, extractJsonObject, } from "./semantic-intake.js";
27
+ import { LEDGER_RECONCILIATION_DEFAULT_BUDGET_CHARS, LedgerReconciliationError, } from "./ledger-reconciliation.js";
28
+ import { LedgerReviewError, } from "./ledger-review.js";
29
+ /** 只读工具面(AC-003):source fidelity Pi 调用无任何写工具。 */
30
+ export const SOURCE_FIDELITY_PI_TOOL_NAMES = [
31
+ "read",
32
+ "grep",
33
+ "find",
34
+ "ls",
35
+ ];
36
+ /** 单次 reconciliation/review Pi 尝试的墙钟上限(镜像 SEMANTIC_INTAKE_ATTEMPT_TIMEOUT_MS)。 */
37
+ export const SOURCE_FIDELITY_ATTEMPT_TIMEOUT_MS = 300_000;
38
+ /** 接线侧门 taskKind(AC-PROD-005)。 */
39
+ export const SOURCE_FIDELITY_TASK_KIND = "frontend-implementation";
40
+ const FRAGMENT_DISPOSITIONS = [
41
+ "requirement",
42
+ "acceptance",
43
+ "constraint",
44
+ "non-goal",
45
+ "duplicate",
46
+ "context-only",
47
+ "unresolved",
48
+ ];
49
+ const EXCLUSION_DISPOSITIONS = new Set([
50
+ "context-only",
51
+ "duplicate",
52
+ "unresolved",
53
+ ]);
54
+ const reconciliationResultSchema = z.object({
55
+ fragmentId: z.string().min(1),
56
+ mappedIds: z.array(z.string().min(1)).default([]),
57
+ disposition: z.enum(FRAGMENT_DISPOSITIONS),
58
+ reason: z.string().min(1).optional(),
59
+ });
60
+ const reviewResponseSchema = z.object({
61
+ fragmentId: z.string().min(1),
62
+ approved: z.boolean(),
63
+ reason: z.string().min(1).optional(),
64
+ revision: z
65
+ .object({
66
+ disposition: z.enum(FRAGMENT_DISPOSITIONS).optional(),
67
+ reason: z.string().min(1).optional(),
68
+ })
69
+ .optional(),
70
+ });
71
+ /**
72
+ * 模型队列构建镜像 semantic-intake.ts:MED→HIGH→LOW(harness executors.pi)+
73
+ * Pi SDK 目录(已认证优先、非 chat 模型过滤、额外模型上限沿用
74
+ * SEMANTIC_INTAKE_MAX_EXTRA_MODELS)。队列空即 fail closed。
75
+ */
76
+ export function buildSourceFidelityModelQueue(input) {
77
+ return buildSemanticIntakeModelQueue(input);
78
+ }
79
+ async function buildProductionModelQueue(repoRoot, listAvailableModels) {
80
+ const manifest = await loadHarnessManifest(repoRoot);
81
+ const models = resolveExecutorModelMatrices(manifest);
82
+ let available = [];
83
+ try {
84
+ available = listAvailableModels
85
+ ? await listAvailableModels()
86
+ : await listPiSdkAvailableModels();
87
+ }
88
+ catch {
89
+ available = [];
90
+ }
91
+ return buildSourceFidelityModelQueue({ harness: models.pi, available });
92
+ }
93
+ function buildReconciliationPrompt(request) {
94
+ return [
95
+ "You are a read-only source-fidelity-reconcile agent for loop-agent task intake.",
96
+ "Map the source fragment to EXISTING canonical requirement ids only.",
97
+ "Return exactly one JSON object (optional json fence); no prose outside it.",
98
+ "Rules:",
99
+ "- mappedIds: canonical requirement ids from the provided list that this fragment directly supports; empty array when none.",
100
+ "- fragmentId: MUST equal the requested fragment id exactly (echo the `fragment id:` line verbatim); identity mismatch is rejected.",
101
+ "- disposition: exactly one of requirement|acceptance|constraint|non-goal|duplicate|context-only|unresolved.",
102
+ "- reason: REQUIRED (non-empty) when disposition is context-only, duplicate or unresolved; optional otherwise.",
103
+ "- Never invent canonical ids; only use ids present in the provided canonical requirements.",
104
+ "- Never invent requirements or human resolutions.",
105
+ JSON.stringify({
106
+ fragmentId: "REQ-SRC-AB12CD34EF56",
107
+ mappedIds: ["AC-001"],
108
+ disposition: "requirement",
109
+ reason: "optional; required for exclusion dispositions",
110
+ }, null, 2),
111
+ `fragment id: ${request.fragment.id}`,
112
+ `fragment path: ${request.fragment.path}`,
113
+ `fragment headingPath: ${request.fragment.headingPath}`,
114
+ `fragment lineRange: ${JSON.stringify(request.fragment.lineRange)}`,
115
+ `budgetChars: ${request.budgetChars}`,
116
+ "fragment text:",
117
+ "```markdown",
118
+ request.fragment.text,
119
+ "```",
120
+ "canonical requirements:",
121
+ JSON.stringify(request.canonicalRequirements, null, 2),
122
+ ].join("\n\n");
123
+ }
124
+ function buildReviewPrompt(request) {
125
+ return [
126
+ "You are an independent read-only source-fidelity-review agent for loop-agent task intake.",
127
+ "Review whether the given fragment's context-only / duplicate / conflict disposition is justified.",
128
+ "You are a separate stage from reconciliation: do not re-map fragments; only approve, reject, or propose at most one bounded revision.",
129
+ "Return exactly one JSON object (optional json fence); no prose outside it.",
130
+ "Rules:",
131
+ "- approved: true only when the exclusion/conflict handling is justified.",
132
+ "- fragmentId: MUST equal the requested fragment id exactly (echo the `fragment id:` line verbatim); identity mismatch is rejected.",
133
+ "- reason: recommended; explain the verdict.",
134
+ "- revision: optional single bounded revision {disposition, reason}; never invent requirements or human resolutions.",
135
+ JSON.stringify({
136
+ fragmentId: "REQ-SRC-AB12CD34EF56",
137
+ approved: true,
138
+ reason: "verdict rationale",
139
+ revision: {
140
+ disposition: "duplicate",
141
+ reason: "optional bounded revision",
142
+ },
143
+ }, null, 2),
144
+ `review kind: ${request.kind}`,
145
+ `fragment id: ${request.fragment.id}`,
146
+ `fragment disposition: ${request.fragment.disposition}`,
147
+ ...(request.fragment.reason
148
+ ? [`fragment reason: ${request.fragment.reason}`]
149
+ : []),
150
+ `fragment risk: ${request.fragment.risk}`,
151
+ `fragment canonicalRequirementIds: ${JSON.stringify(request.fragment.canonicalRequirementIds)}`,
152
+ ...(request.fragment.text.trim()
153
+ ? [
154
+ "fragment text:",
155
+ "```markdown",
156
+ request.fragment.text,
157
+ "```",
158
+ ]
159
+ : [
160
+ "fragment text: (bounded original text unavailable for this record)",
161
+ `fragment id: ${request.fragment.id}`,
162
+ ]),
163
+ "canonical requirements:",
164
+ JSON.stringify(request.canonicalRequirements, null, 2),
165
+ ...(request.resolutionAvailable
166
+ ? ["resolutionAvailable: true (conflict has a valid human resolution)"]
167
+ : []),
168
+ ].join("\n\n");
169
+ }
170
+ function validateReconciliationOutput(text, request) {
171
+ let raw;
172
+ try {
173
+ raw = extractJsonObject(text);
174
+ }
175
+ catch (error) {
176
+ throw new LedgerReconciliationError(`reconciliation output is not a JSON object: ${error instanceof Error ? error.message : String(error)}`);
177
+ }
178
+ let parsed;
179
+ try {
180
+ parsed = reconciliationResultSchema.parse(raw);
181
+ }
182
+ catch (error) {
183
+ throw new LedgerReconciliationError(`reconciliation output failed schema validation: ${error instanceof Error ? error.message : String(error)}`);
184
+ }
185
+ // 生产 fragmentId 绑定(AC-005 / AC-HARD-005):缺失/错配即 fail closed,
186
+ // 保证 reviewer/reconciler 决策绑定到实际请求的 source fragment。
187
+ if (parsed.fragmentId !== request.fragmentId) {
188
+ throw new LedgerReconciliationError(`reconciliation output fragmentId ${parsed.fragmentId} does not match requested fragment ${request.fragmentId}`);
189
+ }
190
+ const outOfBounds = parsed.mappedIds.filter((id) => !request.canonicalIds.has(id));
191
+ if (outOfBounds.length > 0) {
192
+ throw new LedgerReconciliationError(`reconciliation output contains out-of-bounds canonical ids: ${outOfBounds.join(", ")}`);
193
+ }
194
+ if (EXCLUSION_DISPOSITIONS.has(parsed.disposition) && !parsed.reason?.trim()) {
195
+ throw new LedgerReconciliationError(`reconciliation output omitted the required exclusion reason for disposition ${parsed.disposition}`);
196
+ }
197
+ return {
198
+ mappedIds: parsed.mappedIds,
199
+ disposition: parsed.disposition,
200
+ ...(parsed.reason ? { reason: parsed.reason } : {}),
201
+ };
202
+ }
203
+ function validateReviewOutput(text, request) {
204
+ let raw;
205
+ try {
206
+ raw = extractJsonObject(text);
207
+ }
208
+ catch (error) {
209
+ throw new LedgerReviewError(`review output is not a JSON object: ${error instanceof Error ? error.message : String(error)}`);
210
+ }
211
+ let parsed;
212
+ try {
213
+ parsed = reviewResponseSchema.parse(raw);
214
+ }
215
+ catch (error) {
216
+ throw new LedgerReviewError(`review output failed schema validation: ${error instanceof Error ? error.message : String(error)}`);
217
+ }
218
+ // 生产 fragmentId 绑定(AC-005 / AC-HARD-005):缺失/错配即 fail closed。
219
+ if (parsed.fragmentId !== request.fragmentId) {
220
+ throw new LedgerReviewError(`review output fragmentId ${parsed.fragmentId} does not match requested fragment ${request.fragmentId}`);
221
+ }
222
+ return {
223
+ approved: parsed.approved,
224
+ ...(parsed.reason ? { reason: parsed.reason } : {}),
225
+ ...(parsed.revision ? { revision: parsed.revision } : {}),
226
+ };
227
+ }
228
+ /**
229
+ * 执行一次生产 reconciliation:模型队列串行尝试(默认真实 executePiStep),
230
+ * 每个成功输出都经 validateOutput + zod 严格校验,全部失败抛
231
+ * LedgerReconciliationError(稳定 SOURCE_FIDELITY_RECONCILIATION_FAILED)。
232
+ */
233
+ export async function runProductionLedgerReconciliation(request, options) {
234
+ const canonicalIds = new Set(request.canonicalRequirements.map((requirement) => requirement.id));
235
+ const parseAndValidate = (text) => validateReconciliationOutput(text, {
236
+ fragmentId: request.fragment.id,
237
+ canonicalIds,
238
+ });
239
+ const queue = options.modelQueue ??
240
+ (await buildProductionModelQueue(options.repoRoot, options.listAvailableModels));
241
+ if (queue.length === 0) {
242
+ throw new LedgerReconciliationError("source fidelity reconciliation has no MED/HIGH/LOW or Pi-supported models to try (fail closed)");
243
+ }
244
+ const prompt = buildReconciliationPrompt(request);
245
+ const execute = options.executePi ?? executePiStep;
246
+ const timeoutMs = options.timeoutMs ?? SOURCE_FIDELITY_ATTEMPT_TIMEOUT_MS;
247
+ let lastFailure = "";
248
+ for (const modelRef of queue) {
249
+ try {
250
+ const result = await execute({
251
+ attachedFiles: [],
252
+ modelConfig: resolveDagPiModelConfig(modelRef),
253
+ prompt,
254
+ repoRoot: options.repoRoot,
255
+ step: "analyze",
256
+ toolNames: [...SOURCE_FIDELITY_PI_TOOL_NAMES],
257
+ timeoutMs,
258
+ stallTimeoutMs: timeoutMs,
259
+ userMessage: "Reconcile the source fragment into the required JSON only.",
260
+ validateOutput: (text) => {
261
+ try {
262
+ parseAndValidate(text);
263
+ return [];
264
+ }
265
+ catch (error) {
266
+ return [error instanceof Error ? error.message : String(error)];
267
+ }
268
+ },
269
+ });
270
+ if (!result.ok) {
271
+ lastFailure =
272
+ `${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
273
+ continue;
274
+ }
275
+ try {
276
+ return parseAndValidate(result.assistantText);
277
+ }
278
+ catch (error) {
279
+ // 模型返回成功但输出未通过严格校验:视为 invalid-output,尝试下一模型。
280
+ lastFailure =
281
+ `invalid-output: ${error instanceof Error ? error.message : String(error)}`;
282
+ }
283
+ }
284
+ catch (error) {
285
+ lastFailure =
286
+ `unknown: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2000);
287
+ }
288
+ }
289
+ throw new LedgerReconciliationError(`source fidelity reconciliation pi failed after ${queue.length} model(s): ${lastFailure || "no attempt produced valid output"}`);
290
+ }
291
+ /**
292
+ * 执行一次生产独立 review:模型队列串行尝试(默认真实 executePiStep),
293
+ * 输出经 validateOutput + zod 严格校验,全部失败抛 LedgerReviewError
294
+ * (稳定 SOURCE_FIDELITY_RECONCILIATION_FAILED,绝不回退默认 reviewer)。
295
+ */
296
+ export async function runProductionLedgerReview(request, options) {
297
+ const parseAndValidate = (text) => validateReviewOutput(text, { fragmentId: request.fragment.id });
298
+ const queue = options.modelQueue ??
299
+ (await buildProductionModelQueue(options.repoRoot, options.listAvailableModels));
300
+ if (queue.length === 0) {
301
+ throw new LedgerReviewError("source fidelity review has no MED/HIGH/LOW or Pi-supported models to try (fail closed)");
302
+ }
303
+ const prompt = buildReviewPrompt(request);
304
+ const execute = options.executePi ?? executePiStep;
305
+ const timeoutMs = options.timeoutMs ?? SOURCE_FIDELITY_ATTEMPT_TIMEOUT_MS;
306
+ let lastFailure = "";
307
+ for (const modelRef of queue) {
308
+ try {
309
+ const result = await execute({
310
+ attachedFiles: [],
311
+ modelConfig: resolveDagPiModelConfig(modelRef),
312
+ prompt,
313
+ repoRoot: options.repoRoot,
314
+ step: "analyze",
315
+ toolNames: [...SOURCE_FIDELITY_PI_TOOL_NAMES],
316
+ timeoutMs,
317
+ stallTimeoutMs: timeoutMs,
318
+ userMessage: "Review the fragment disposition into the required JSON only.",
319
+ validateOutput: (text) => {
320
+ try {
321
+ parseAndValidate(text);
322
+ return [];
323
+ }
324
+ catch (error) {
325
+ return [error instanceof Error ? error.message : String(error)];
326
+ }
327
+ },
328
+ });
329
+ if (!result.ok) {
330
+ lastFailure =
331
+ `${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
332
+ continue;
333
+ }
334
+ try {
335
+ return parseAndValidate(result.assistantText);
336
+ }
337
+ catch (error) {
338
+ lastFailure =
339
+ `invalid-output: ${error instanceof Error ? error.message : String(error)}`;
340
+ }
341
+ }
342
+ catch (error) {
343
+ lastFailure =
344
+ `unknown: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2000);
345
+ }
346
+ }
347
+ throw new LedgerReviewError(`source fidelity review pi failed after ${queue.length} model(s): ${lastFailure || "no attempt produced valid output"}`);
348
+ }
349
+ /**
350
+ * 生产适配器工厂:返回 prepare 可注入的 reconcile executor(budgetChars=4000)
351
+ * 与独立 reviewer 两个不同 Pi 调用面。
352
+ */
353
+ export function createProductionSourceFidelityPi(options) {
354
+ const reconcileExecutor = (request) => runProductionLedgerReconciliation(request, options);
355
+ const reviewer = (request) => runProductionLedgerReview(request, options);
356
+ return {
357
+ reconcile: {
358
+ executor: reconcileExecutor,
359
+ budgetChars: LEDGER_RECONCILIATION_DEFAULT_BUDGET_CHARS,
360
+ },
361
+ reviewer,
362
+ };
363
+ }
364
+ /**
365
+ * 接线侧门(AC-PROD-005):taskKind === 'frontend-implementation'(input.taskKind
366
+ * 优先,缺省回退 loadTaskConfig(repoRoot, taskId).taskKind)。ineligible 返回
367
+ * undefined 不接线;配合 prepare.ts 的 parseable requirement/acceptance 前置
368
+ * 双重保证非 eligible 路径零 source-fidelity Pi 调用。
369
+ */
370
+ export async function resolveProductionSourceFidelityPi(input) {
371
+ let effectiveTaskKind = input.taskKind?.trim();
372
+ if (!effectiveTaskKind) {
373
+ try {
374
+ const config = await loadTaskConfig(input.repoRoot, input.taskId);
375
+ effectiveTaskKind = config.taskKind?.trim();
376
+ }
377
+ catch {
378
+ effectiveTaskKind = undefined;
379
+ }
380
+ }
381
+ if (effectiveTaskKind !== SOURCE_FIDELITY_TASK_KIND)
382
+ return undefined;
383
+ return createProductionSourceFidelityPi({ repoRoot: input.repoRoot });
384
+ }
@@ -5,3 +5,25 @@ export const PREPARE_DEFAULT_FORBIDDEN_PATHS = [
5
5
  ".harness/**",
6
6
  "node_modules/**",
7
7
  ];
8
+ // ---------------------------------------------------------------------------
9
+ // Source Fidelity Ledger (20260824-frontend-source-fidelity-ledger-v1)
10
+ // ---------------------------------------------------------------------------
11
+ // 原始 requirement/acceptance 文档保持语义权威;派生 source/需求.md 仅作导航视图。
12
+ // ledger 是二者之间确定性、可审计的映射账本:完整性由可审计账本、来源绑定和
13
+ // writer 前确定性门禁证明,摘要遗漏不再静默通过。
14
+ /** 稳定机器可判读错误码(SOURCE_FIDELITY_* 类目),writer 前门禁/修复路由消费。 */
15
+ export const SOURCE_FIDELITY_ERROR_CODES = [
16
+ "SOURCE_FIDELITY_LEDGER_MISSING",
17
+ "SOURCE_FIDELITY_LEDGER_STALE",
18
+ "SOURCE_FIDELITY_UNRESOLVED",
19
+ "SOURCE_FIDELITY_CONFLICT",
20
+ "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED",
21
+ "SOURCE_FIDELITY_CONTEXT_ONLY_UNREVIEWED",
22
+ "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED",
23
+ "SOURCE_FIDELITY_SOURCE_REF_INVALID",
24
+ "SOURCE_FIDELITY_FRAGMENT_INVALID",
25
+ "SOURCE_FIDELITY_RECONCILIATION_FAILED",
26
+ ];
27
+ export const LEDGER_SCHEMA_VERSION = 1;
28
+ export const LEDGER_SCHEMA_RECONCILER_VERSION = "ledger-reconciler-v1";
29
+ export const REQUIREMENT_LEDGER_FILE_NAME = "requirement-ledger.json";
@@ -1,11 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from "node:fs/promises";
3
- import os from "node:os";
4
3
  import path from "node:path";
5
4
  import { fileURLToPath } from "node:url";
6
5
  import { Command } from "commander";
7
- import { createNpmUpdateClient } from "../shared/update/npm-client.js";
8
- import { runConsoleDailyUpdateCheck } from "../shared/update/console-notifier.js";
9
6
  import { readPackageVersion } from "../shared/package-metadata.js";
10
7
  import YAML from "yaml";
11
8
  import { LoopAgentClient } from "./loop-agent/loop-agent-client.js";
@@ -844,37 +841,26 @@ function parseOptionalTimeoutMs(raw) {
844
841
  export async function runConsoleServe(options) {
845
842
  const { createConsoleServer } = await import("./console/server.js");
846
843
  const { maybeOpenConsoleBrowser } = await import("./console/open-browser.js");
844
+ const { readConsoleHandoff, waitForOldConsoleExit } = await import("./console/console-handoff.js");
847
845
  try {
848
- try {
849
- const packageRoot = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
850
- const currentVersion = readPackageVersion(packageRoot) ?? "0.0.0";
851
- await runConsoleDailyUpdateCheck({
852
- currentVersion,
853
- packageRoot,
854
- homeDir: os.homedir(),
855
- cwd: options.repoRoot,
856
- env: process.env,
857
- stdinIsTTY: process.stdin.isTTY === true,
858
- stderrIsTTY: process.stderr.isTTY === true,
859
- client: createNpmUpdateClient({
860
- currentVersion,
861
- packageRoot,
862
- cwd: options.repoRoot,
863
- env: process.env,
864
- }),
865
- });
866
- }
867
- catch (error) {
868
- const detail = error instanceof Error ? error.message : String(error);
869
- process.stderr.write(`loop-agent 更新检查已跳过:${detail}\n`);
846
+ const handoff = readConsoleHandoff(process.env);
847
+ const appDataRoot = options.appDataRoot ?? handoff?.appDataRoot;
848
+ if (handoff) {
849
+ if (path.resolve(handoff.repoRoot) !== path.resolve(options.repoRoot) ||
850
+ handoff.host !== options.host ||
851
+ handoff.port !== options.port) {
852
+ throw new Error("console handoff does not match requested repo/host/port");
853
+ }
854
+ await waitForOldConsoleExit(handoff.oldPid);
870
855
  }
871
856
  const server = await createConsoleServer({
872
857
  repoRoot: options.repoRoot,
873
858
  host: options.host,
874
859
  port: options.port,
875
860
  debug: options.debug === true,
876
- appDataRoot: options.appDataRoot,
861
+ appDataRoot,
877
862
  defaultTimeoutMs: parseOptionalTimeoutMs(options.defaultTimeoutMsRaw),
863
+ ...(handoff ? { consoleInstanceId: handoff.consoleInstanceId } : {}),
878
864
  });
879
865
  // Keep stdout bind URL contract (may include 0.0.0.0 / ::); browser URL is normalized separately.
880
866
  process.stdout.write(`${server.url}
@@ -231,6 +231,10 @@ export function projectCompactSnapshot(value) {
231
231
  Number.isFinite(record.tokensBefore)
232
232
  ? Math.max(0, Math.floor(record.tokensBefore))
233
233
  : undefined;
234
+ const estimatedTokensAfter = typeof record.estimatedTokensAfter === "number" &&
235
+ Number.isFinite(record.estimatedTokensAfter)
236
+ ? Math.max(0, Math.floor(record.estimatedTokensAfter))
237
+ : undefined;
234
238
  const details = record.details === undefined
235
239
  ? undefined
236
240
  : boundedRedactedJson(record.details).preview;
@@ -239,9 +243,16 @@ export function projectCompactSnapshot(value) {
239
243
  summary,
240
244
  ...(firstKeptEntryId ? { firstKeptEntryId } : {}),
241
245
  ...(tokensBefore !== undefined ? { tokensBefore } : {}),
246
+ ...(estimatedTokensAfter !== undefined ? { estimatedTokensAfter } : {}),
242
247
  ...(details ? { details } : {}),
243
248
  previewHash: createHash("sha256")
244
- .update(JSON.stringify({ summary, firstKeptEntryId, tokensBefore, details }))
249
+ .update(JSON.stringify({
250
+ summary,
251
+ firstKeptEntryId,
252
+ tokensBefore,
253
+ estimatedTokensAfter,
254
+ details,
255
+ }))
245
256
  .digest("hex"),
246
257
  };
247
258
  }
@@ -433,6 +444,20 @@ function safeSessionId(sessionId) {
433
444
  * Link durable Chat refs to the existing canonical operation store/event ring.
434
445
  * This owns no operation state and schedules no work; it only projects facts.
435
446
  */
447
+ /** R8 AC-R8-001: operationIds whose operation-ref has been overshadowed by
448
+ * a later operation-unlinked tombstone in the same ring. A tombstoned
449
+ * association must never be restored by recoverSession / replay / fork. */
450
+ function tombstonedOperationIds(events) {
451
+ const tombstones = new Set();
452
+ for (const event of events) {
453
+ if (event.kind !== "operation-unlinked")
454
+ continue;
455
+ const operationId = event.data.operationId;
456
+ if (typeof operationId === "string" && operationId)
457
+ tombstones.add(operationId);
458
+ }
459
+ return tombstones;
460
+ }
436
461
  export function createChatOperationLinker(options) {
437
462
  const followers = new Map();
438
463
  const keyFor = (sessionId, operationId) => `${sessionId}\u0000${operationId}`;
@@ -535,14 +560,37 @@ export function createChatOperationLinker(options) {
535
560
  }
536
561
  return {
537
562
  link,
563
+ async unlink(input) {
564
+ // Idempotent removal (AC-R7-001): drop the live follower and the
565
+ // persisted operation-ref event so the association is truly gone.
566
+ const key = keyFor(input.sessionId, input.operationId);
567
+ followers.get(key)?.();
568
+ followers.delete(key);
569
+ const ring = options.chatEvents.snapshot(input.sessionId);
570
+ const refs = ring.filter((event) => event.kind === "operation-ref" &&
571
+ event.data.operationId ===
572
+ input.operationId);
573
+ for (const event of refs) {
574
+ options.chatEvents.append(input.sessionId, event.turnId, {
575
+ kind: "operation-unlinked",
576
+ data: { operationId: input.operationId, unlinkedAt: new Date().toISOString() },
577
+ });
578
+ }
579
+ return refs.length > 0;
580
+ },
538
581
  async recoverSession(sessionId) {
539
- const refs = options.chatEvents
540
- .snapshot(sessionId)
541
- .filter((event) => event.kind === "operation-ref");
582
+ const snapshot = options.chatEvents.snapshot(sessionId);
583
+ // R8 AC-R8-001: a tombstoned (unlinked) operationId must NOT be
584
+ // re-linked on recovery — the persisted operation-unlinked event
585
+ // overshadows any earlier operation-ref in the same ring.
586
+ const tombstones = tombstonedOperationIds(snapshot);
587
+ const refs = snapshot.filter((event) => event.kind === "operation-ref");
542
588
  for (const event of refs) {
543
589
  const data = event.data;
544
590
  if (!data.operationId || !data.toolCallId)
545
591
  continue;
592
+ if (tombstones.has(data.operationId))
593
+ continue;
546
594
  await link({
547
595
  sessionId,
548
596
  turnId: event.turnId,
@@ -566,6 +614,19 @@ export function createChatOperationLinker(options) {
566
614
  },
567
615
  };
568
616
  }
617
+ /** R8 AC-R8-001 (fork): same tombstone scan as the linker — kept local to
618
+ * the store so forkContextRefs needs no linker dependency. */
619
+ function forkTombstonedOperationIds(events) {
620
+ const tombstones = new Set();
621
+ for (const event of events) {
622
+ if (event.kind !== "operation-unlinked")
623
+ continue;
624
+ const operationId = event.data.operationId;
625
+ if (typeof operationId === "string" && operationId)
626
+ tombstones.add(operationId);
627
+ }
628
+ return tombstones;
629
+ }
569
630
  export function createChatEventStore(options) {
570
631
  const maxEvents = options?.maxEventsPerSession ?? DEFAULT_MAX_EVENTS;
571
632
  const maxBytes = options?.maxBytesPerSession ?? DEFAULT_MAX_BYTES;
@@ -735,13 +796,22 @@ export function createChatEventStore(options) {
735
796
  "assessment",
736
797
  "taskKind-confirmed",
737
798
  "human-gate-card",
799
+ "compaction_end",
738
800
  "compact",
739
801
  "artifact-ref",
740
802
  ]);
741
803
  let copiedCount = 0;
742
- for (const event of this.snapshot(input.sourceSessionId)) {
804
+ const source = this.snapshot(input.sourceSessionId);
805
+ // R8 AC-R8-001: fork excludes tombstoned associations — an unlinked
806
+ // operationId must not leak its operation-ref into a forked session.
807
+ const tombstones = forkTombstonedOperationIds(source);
808
+ for (const event of source) {
743
809
  if (!allowed.has(event.kind))
744
810
  continue;
811
+ if ((event.kind === "operation-ref" ||
812
+ event.kind === "operation-status") &&
813
+ tombstones.has(event.data.operationId ?? ""))
814
+ continue;
745
815
  const data = structuredClone(event.data);
746
816
  if (event.kind === "human-gate-card") {
747
817
  delete data.humanGateToken;
@@ -38,6 +38,13 @@ export function projectTaskContext(input) {
38
38
  const previous = gates.get(card.cardId);
39
39
  gates.set(card.cardId, safeGate({ ...card, ...(previous?.dagSpine && !card.dagSpine ? { dagSpine: previous.dagSpine } : {}) }));
40
40
  }
41
+ else if (event.kind === "operation-unlinked") {
42
+ // R8 AC-001/AC-R8-001: tombstone — an operation-unlinked event appended
43
+ // after its operation-ref removes the card from the projection for good.
44
+ const operationId = typeof data.operationId === "string" ? data.operationId : "";
45
+ if (operationId)
46
+ view.operations = view.operations.filter((operation) => operation.operationId !== operationId);
47
+ }
41
48
  else if (event.kind === "operation-ref" || event.kind === "operation-status") {
42
49
  const operation = operationFromEventPayload(data);
43
50
  if (operation && (!activeTaskId || !operation.taskId || operation.taskId === activeTaskId))
@@ -81,11 +81,23 @@ export function projectDeferredTurnSummary(deferredTurn) {
81
81
  blockedByTurnId: deferredTurn.blockedByTurnId,
82
82
  state: deferredTurn.state,
83
83
  updatedAt: deferredTurn.updatedAt,
84
+ text: deferredTurn.text,
84
85
  textPreview: deferredTurn.text.replace(/\s+/g, " ").trim().slice(0, 120),
85
86
  imageCount: deferredTurn.images.length,
87
+ images: structuredClone(deferredTurn.images),
86
88
  ...(deferredTurn.acceptedTurnId
87
89
  ? { acceptedTurnId: deferredTurn.acceptedTurnId }
88
90
  : {}),
89
91
  ...(deferredTurn.lastError ? { lastError: deferredTurn.lastError } : {}),
90
92
  };
91
93
  }
94
+ export function parseDeferredTurns(value) {
95
+ if (!Array.isArray(value))
96
+ return [];
97
+ return value
98
+ .map((item) => parseDeferredTurn(item))
99
+ .filter((item) => item !== undefined);
100
+ }
101
+ export function projectDeferredTurnSummaries(deferredTurns) {
102
+ return deferredTurns.map((item) => projectDeferredTurnSummary(item));
103
+ }