@tea-agent/loop-agent 0.40.0-next.9 → 0.41.0-beta.1

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 (218) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +130 -3
  3. package/README.md +2 -2
  4. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  5. package/dist/adapters/context-transfer/pi-session.js +61 -0
  6. package/dist/application/dag/generate-task-dag.js +6 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +21 -5
  9. package/dist/cli/program.js +61 -4
  10. package/dist/commands/client-recovery.js +8 -36
  11. package/dist/commands/dag-artifact.js +284 -0
  12. package/dist/commands/dag-context.js +184 -0
  13. package/dist/commands/dag-rerun.js +206 -1
  14. package/dist/commands/init-model-catalog.js +22 -0
  15. package/dist/commands/task-advance.js +23 -1
  16. package/dist/commands/task-source-prepare.js +5 -0
  17. package/dist/commands/task-status.js +1 -0
  18. package/dist/executors/dag-pi-executor.js +39 -12
  19. package/dist/executors/pi-sdk-executor.js +36 -5
  20. package/dist/executors/shell-executor.js +64 -17
  21. package/dist/governance/checks.js +1 -0
  22. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  23. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  24. package/dist/infrastructure/console/context-export-store.js +160 -0
  25. package/dist/infrastructure/console/dir-lock.js +132 -0
  26. package/dist/{worker → infrastructure}/console/operation-store.js +28 -0
  27. package/dist/infrastructure/harness/artifact-store.js +10 -1
  28. package/dist/infrastructure/harness/atomic-write.js +12 -2
  29. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  30. package/dist/shared/context-transfer.js +418 -0
  31. package/dist/shared/operator/capabilities.js +23 -1
  32. package/dist/shared/operator/safe-run-summary.js +1 -0
  33. package/dist/shared/path-safety.js +93 -0
  34. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  35. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  36. package/dist/shared/pi-context-pressure/env.js +58 -0
  37. package/dist/shared/pi-context-pressure/extension.js +100 -0
  38. package/dist/shared/pi-context-pressure/index.js +7 -0
  39. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  40. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  41. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  42. package/dist/shared/preview.js +28 -4
  43. package/dist/task/config-types.js +2 -1
  44. package/dist/task/task-demand-routing.js +3 -0
  45. package/dist/worker/console/chat/assistant-content.js +7 -0
  46. package/dist/worker/console/chat/chat-event-store.js +84 -13
  47. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  48. package/dist/worker/console/chat/goals.js +300 -0
  49. package/dist/worker/console/chat/pi-runtime.js +285 -8
  50. package/dist/worker/console/chat/provider-error.js +2 -1
  51. package/dist/worker/console/chat/repo-browser.js +94 -12
  52. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  53. package/dist/worker/console/chat/routes.js +487 -30
  54. package/dist/worker/console/chat/session-catalog.js +48 -0
  55. package/dist/worker/console/chat/session-stats.js +96 -0
  56. package/dist/worker/console/chat/session-store.js +28 -19
  57. package/dist/worker/console/chat/shortcuts.js +7 -0
  58. package/dist/worker/console/chat/sift-bridge.js +1 -0
  59. package/dist/worker/console/chat/turn-process.js +32 -3
  60. package/dist/worker/console/chat/user-questions.js +1 -1
  61. package/dist/worker/console/console-update-runtime.js +1 -1
  62. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  63. package/dist/worker/console/dag-confirmation.js +1 -1
  64. package/dist/worker/console/dag-execution-receipt.js +21 -3
  65. package/dist/worker/console/doctor.js +1 -1
  66. package/dist/worker/console/draft-store.js +1 -1
  67. package/dist/worker/console/human-gate-token.js +1 -1
  68. package/dist/worker/console/index.js +3 -3
  69. package/dist/worker/console/interview/assessment.js +1 -1
  70. package/dist/worker/console/interview/session.js +1 -1
  71. package/dist/worker/console/operation-runner.js +45 -4
  72. package/dist/worker/console/operation-sse.js +1 -1
  73. package/dist/worker/console/operation-wait.js +1 -1
  74. package/dist/worker/console/operator-actions.js +124 -8
  75. package/dist/worker/console/operator-selection.js +6 -2
  76. package/dist/worker/console/operator-surface-health.js +1 -1
  77. package/dist/worker/console/operator-user-error.js +169 -0
  78. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  79. package/dist/worker/console/pi-plugins.js +179 -0
  80. package/dist/worker/console/routes.js +864 -13
  81. package/dist/worker/console/security.js +35 -0
  82. package/dist/worker/console/server.js +177 -44
  83. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-DSVcg1YO.js} +1 -1
  84. package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-BtXhIqui.js} +1 -1
  85. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-B-nh-ilJ.js} +1 -1
  86. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-BIUSVdZI.js} +1 -1
  87. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-Cmw3L4qM.js} +1 -1
  88. package/dist/worker/console/static/assets/channel-BhZFNefx.js +1 -0
  89. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-CGpMMjGm.js} +1 -1
  90. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-BNBkEpEJ.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-ubln3eaU.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-_XcBTt1i.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-Ln30Pgrh.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-ESH3lhSx.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-DAJt-qXB.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-Cx10LIcH.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-CENeOxoJ.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-DXqK-D38.js} +1 -1
  99. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Sbv-NJni.js +1 -0
  100. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Sbv-NJni.js +1 -0
  101. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-CE4dmvGY.js} +1 -1
  102. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-CRacN1zK.js} +1 -1
  103. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-Cneukjrt.js} +1 -1
  104. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-DPFboRoL.js} +1 -1
  105. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-DIAQgcOP.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-VrYMKxuy.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-CLHvBghn.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-DVCWBif8.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-BTMmbBzJ.js} +1 -1
  110. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-BUZsKJ56.js} +1 -1
  111. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-BsBZ5NXC.js} +1 -1
  112. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-D7fH_kDa.js} +1 -1
  113. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-Bza5OohY.js} +1 -1
  114. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-DpkoGR_Y.js} +1 -1
  115. package/dist/worker/console/static/assets/index-Ius3vxhf.css +1 -0
  116. package/dist/worker/console/static/assets/index-yASzcbBr.js +451 -0
  117. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-BYm3g6LT.js} +1 -1
  118. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-C2RS_H4v.js} +1 -1
  119. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-B4slKpCo.js} +1 -1
  120. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-B7orRNpI.js} +1 -1
  121. package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-D1BeBxLQ.js} +1 -1
  122. package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-0TtMP8k4.js} +5 -5
  123. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-XL5EbCtT.js} +1 -1
  124. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-CwF7uas5.js} +1 -1
  125. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-A_Jo-72f.js} +1 -1
  126. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-CxOa4gKL.js} +1 -1
  127. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-DoAoswIH.js} +1 -1
  128. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-C1Lc1MlD.js} +1 -1
  129. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-DFXOkq0U.js} +1 -1
  130. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-B0ufUCuW.js} +1 -1
  131. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-iPbW55g3.js} +1 -1
  132. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-CHiXzEKP.js} +1 -1
  133. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BtMgzo_H.js +1 -0
  134. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-LyFs7TpX.js} +2 -2
  135. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cb-OVd9Y.js +8 -0
  136. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-tNkJnb6z.js} +1 -1
  137. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-DFm7Oo8o.js} +1 -1
  138. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-tpblTp9i.js} +1 -1
  139. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-BCDLWR_D.js} +1 -1
  140. package/dist/worker/console/static/index.html +7 -2
  141. package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
  142. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  143. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  144. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  145. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
  146. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  147. package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
  148. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  149. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  150. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  151. package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
  152. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  153. package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
  154. package/dist/worker/console/workspace-context.js +56 -5
  155. package/dist/worker/console/workspace-registry.js +335 -62
  156. package/dist/worker/continuation/worker-continuation.js +278 -0
  157. package/dist/worker/observe/health.js +1 -1
  158. package/dist/worker/observe/node-transparency.js +572 -0
  159. package/dist/worker/observe/routes.js +51 -1
  160. package/dist/worker/observe/static/api.js +69 -5
  161. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  162. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  163. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  164. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  165. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  166. package/dist/worker/observe/static/operator-chrome.css +23 -4
  167. package/dist/worker/observe/static/operator-chrome.js +7 -2
  168. package/dist/worker/observe/static/state.js +3 -1
  169. package/dist/worker/observe/static/styles.css +1108 -135
  170. package/dist/worker/observe/static/views/dag-inspector.js +1257 -59
  171. package/dist/worker/observe/static/views/dags.js +7 -5
  172. package/dist/worker/observe/static/views/session-timeline.js +210 -10
  173. package/dist/worker/outcomes/adapters.js +19 -0
  174. package/dist/worker/outcomes/types.js +1 -0
  175. package/dist/worker/pool/attempt-lease.js +97 -66
  176. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  177. package/dist/worker/pool/reconcile.js +46 -1
  178. package/dist/worker/pool/run-store.js +2 -0
  179. package/dist/worker/pool/state-projection.js +2 -0
  180. package/dist/worker/task-spec/workflow-routing.js +8 -3
  181. package/dist/workflows/dag/artifact-bindings.js +149 -0
  182. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  183. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
  184. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  185. package/dist/workflows/dag/backend-test-result-contract.js +4 -0
  186. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  187. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  188. package/dist/workflows/dag/budget-enforcement.js +10 -0
  189. package/dist/workflows/dag/context-receipt.js +305 -0
  190. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  191. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  192. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  193. package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
  194. package/dist/workflows/dag/init-hybrid.js +151 -4
  195. package/dist/workflows/dag/node-execution.js +112 -27
  196. package/dist/workflows/dag/path-safety.js +1 -0
  197. package/dist/workflows/dag/prompt.js +29 -4
  198. package/dist/workflows/dag/rerun-plan.js +555 -1
  199. package/dist/workflows/dag/rerun-run.js +601 -15
  200. package/dist/workflows/dag/runner.js +20 -1
  201. package/dist/workflows/dag/skill-snapshot.js +17 -0
  202. package/dist/workflows/dag/types.js +183 -7
  203. package/docs/architecture/runtime-boundaries.md +8 -7
  204. package/docs/architecture/worker-and-feature.md +1 -1
  205. package/docs/templates/agent-dag.schema.json +32 -0
  206. package/docs/templates/backend-test-dag.json +6 -5
  207. package/docs/templates/init-managed-agents.md +1 -1
  208. package/docs/templates/product-line/task.yaml +1 -1
  209. package/package.json +5 -2
  210. package/skills/loop-agent/references/command-reference.md +21 -1
  211. package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
  212. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
  213. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
  214. package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
  215. package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
  216. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
  217. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
  218. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -19,8 +19,8 @@
19
19
  import { createHash } from "node:crypto";
20
20
  import { existsSync, readFileSync, rmSync } from "node:fs";
21
21
  import path from "node:path";
22
- import { ensureSecureDir, writeSecureJsonSync, } from "../app-data.js";
23
- import { isTerminalOperationState, } from "../operation-store.js";
22
+ import { ensureSecureDir, writeSecureJsonSync, } from "../../../infrastructure/console/app-data.js";
23
+ import { isTerminalOperationState, } from "../../../infrastructure/console/operation-store.js";
24
24
  import { scrubSecrets } from "./explore-tools.js";
25
25
  const OPERATION_PREVIEW_MAX_CHARS = 1200;
26
26
  function boundedRedactedText(value, maxChars = OPERATION_PREVIEW_MAX_CHARS) {
@@ -136,7 +136,7 @@ export function projectOperationForChat(operation) {
136
136
  state: operation.state,
137
137
  createdAt: operation.createdAt,
138
138
  updatedAt: operation.finishedAt ?? operation.startedAt ?? operation.createdAt,
139
- inspectHref: `/inspect/?operationId=${encodeURIComponent(operation.operationId)}#/`,
139
+ inspectHref: `/#/inspect?operationId=${encodeURIComponent(operation.operationId)}`,
140
140
  ...(taskId ? { taskId } : {}),
141
141
  ...(dagRunId ? { dagRunId } : {}),
142
142
  ...(featureId ? { featureId } : {}),
@@ -381,12 +381,26 @@ function dropOneByPriority(ring) {
381
381
  const events = ring.events;
382
382
  if (events.length === 0)
383
383
  return undefined;
384
- const textIndex = events.findIndex((event) => event.kind === "message_update");
384
+ // Keep the newest Goal snapshot/tombstone as the session's durable Goal
385
+ // source of truth. Otherwise a long thread could forget an active Goal or
386
+ // resurrect a cleared one after ring eviction.
387
+ let protectedGoalIndex = -1;
388
+ for (let index = events.length - 1; index >= 0; index -= 1) {
389
+ if (events[index].kind === "goal_changed" ||
390
+ events[index].kind === "goal_cleared") {
391
+ protectedGoalIndex = index;
392
+ break;
393
+ }
394
+ }
395
+ const textIndex = events.findIndex((event, index) => index !== protectedGoalIndex && event.kind === "message_update");
385
396
  if (textIndex !== -1)
386
397
  return events.splice(textIndex, 1)[0];
387
- const structuralIndex = events.findIndex((event) => event.kind !== "message_end");
398
+ const structuralIndex = events.findIndex((event, index) => index !== protectedGoalIndex && event.kind !== "message_end");
388
399
  if (structuralIndex !== -1)
389
400
  return events.splice(structuralIndex, 1)[0];
401
+ const anyUnprotected = events.findIndex((_event, index) => index !== protectedGoalIndex);
402
+ if (anyUnprotected !== -1)
403
+ return events.splice(anyUnprotected, 1)[0];
390
404
  return events.shift();
391
405
  }
392
406
  /**
@@ -430,6 +444,13 @@ const DEFAULT_MAX_EVENTS = 2000;
430
444
  const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
431
445
  /** Same-turn `message_update` events inside this window coalesce into one persisted event. */
432
446
  const DEFAULT_COALESCE_WINDOW_MS = 100;
447
+ /** B5-1: `message_update` appends persist at most once per this window (driven
448
+ * by the event `at` clock, same as coalescing — no timers involved). */
449
+ const DEFAULT_PERSIST_DEBOUNCE_MS = 250;
450
+ /** B5-1: hard cap on pending debounced `message_update` appends before an
451
+ * interim flush is forced, so a fast stream never widens the loss window
452
+ * unboundedly even if the `at` clock stalls. */
453
+ const DEFAULT_PERSIST_MAX_PENDING_UPDATES = 250;
433
454
  /** Conservative per-event scale for the byte pre-check gate (UTF-8 multibyte ≤ 4 bytes/char × pretty-print expansion). */
434
455
  const BYTE_GATE_SCALE = 12;
435
456
  /** Fixed envelope slack (schema keys, turnCount, nextSeq, minSeq, turns) for the byte pre-check gate. */
@@ -632,6 +653,9 @@ export function createChatEventStore(options) {
632
653
  const maxBytes = options?.maxBytesPerSession ?? DEFAULT_MAX_BYTES;
633
654
  const coalesceWindowMs = options?.coalesceWindowMs ?? DEFAULT_COALESCE_WINDOW_MS;
634
655
  const eventEpoch = options?.epoch?.trim() || LEGACY_EPOCH;
656
+ const persistWriter = options?.persistWriter ?? writeSecureJsonSync;
657
+ const persistDebounceMs = options?.persistDebounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS;
658
+ const persistMaxPendingUpdates = options?.persistMaxPendingUpdates ?? DEFAULT_PERSIST_MAX_PENDING_UPDATES;
635
659
  const persistenceDir = options?.appData?.chatEvents
636
660
  ? path.resolve(options.appData.chatEvents)
637
661
  : undefined;
@@ -642,6 +666,9 @@ export function createChatEventStore(options) {
642
666
  const turnCounts = new Map();
643
667
  /** Live subscribers per session. */
644
668
  const subscribers = new Map();
669
+ /** B5-1: sessions whose latest `message_update` appends are still waiting
670
+ * for the next persist (debounced; structural events / turn changes flush). */
671
+ const pendingPersists = new Map();
645
672
  function persistedPath(sessionId) {
646
673
  return persistenceDir
647
674
  ? path.join(persistenceDir, `${safeSessionId(sessionId)}.json`)
@@ -709,7 +736,7 @@ export function createChatEventStore(options) {
709
736
  // current limits if it was written by an older config or hand-edited.
710
737
  enforceBounds(sessionId, ring);
711
738
  if (orphaned)
712
- persist(sessionId, ring);
739
+ persistNow(sessionId, ring);
713
740
  return ring;
714
741
  }
715
742
  catch (error) {
@@ -763,11 +790,36 @@ export function createChatEventStore(options) {
763
790
  ring.minSeq = ring.events[0].seq;
764
791
  }
765
792
  }
766
- function persist(sessionId, ring) {
793
+ /** Persist the full ring synchronously (atomic tmp+rename) and clear any
794
+ * B5-1 pending-debounce marker for this session. */
795
+ function persistNow(sessionId, ring) {
767
796
  const file = persistedPath(sessionId);
797
+ pendingPersists.delete(sessionId);
768
798
  if (!file)
769
799
  return;
770
- writeSecureJsonSync(file, ringPayload(sessionId, ring));
800
+ persistWriter(file, ringPayload(sessionId, ring));
801
+ }
802
+ /** B5-1: defer the persist of a `message_update` append. The window is driven
803
+ * by the append `at` clock (mirroring `shouldCoalesce`) — a structural event,
804
+ * turn change, elapsed window or the pending-count cap each force a real
805
+ * flush, so a streaming turn no longer serializes the whole ring per token. */
806
+ function persistDebounced(sessionId, ring, atMs) {
807
+ const file = persistedPath(sessionId);
808
+ if (!file)
809
+ return;
810
+ const pending = pendingPersists.get(sessionId);
811
+ if (!pending) {
812
+ pendingPersists.set(sessionId, {
813
+ firstPendingAtMs: atMs,
814
+ pendingUpdates: 1,
815
+ });
816
+ return;
817
+ }
818
+ pending.pendingUpdates += 1;
819
+ if (atMs - pending.firstPendingAtMs >= persistDebounceMs ||
820
+ pending.pendingUpdates >= persistMaxPendingUpdates) {
821
+ persistNow(sessionId, ring);
822
+ }
771
823
  }
772
824
  function ringFor(sessionId) {
773
825
  let ring = rings.get(sessionId);
@@ -870,7 +922,7 @@ export function createChatEventStore(options) {
870
922
  };
871
923
  ring.turns.push(turn);
872
924
  ring.approxBytes += byteHint(turn);
873
- persist(sessionId, ring);
925
+ persistNow(sessionId, ring);
874
926
  return { ok: true, turn: { ...turn }, created: true, idempotent: false };
875
927
  },
876
928
  setTurnState(sessionId, turnId, state, error) {
@@ -889,7 +941,7 @@ export function createChatEventStore(options) {
889
941
  turn.error = error;
890
942
  ring.approxBytes +=
891
943
  (JSON.stringify(turn).length - before) * BYTE_GATE_SCALE;
892
- persist(sessionId, ring);
944
+ persistNow(sessionId, ring);
893
945
  return { ...turn };
894
946
  },
895
947
  markAbortRequested(sessionId, turnId, at) {
@@ -902,7 +954,7 @@ export function createChatEventStore(options) {
902
954
  turn.abortRequestedAt = at ?? new Date().toISOString();
903
955
  ring.approxBytes +=
904
956
  (JSON.stringify(turn).length - before) * BYTE_GATE_SCALE;
905
- persist(sessionId, ring);
957
+ persistNow(sessionId, ring);
906
958
  return { ...turn };
907
959
  },
908
960
  findTurnByClientRequestId(sessionId, clientRequestId) {
@@ -962,7 +1014,23 @@ export function createChatEventStore(options) {
962
1014
  ring.approxBytes += byteHint(live);
963
1015
  }
964
1016
  enforceBounds(sessionId, ring);
965
- persist(sessionId, ring);
1017
+ // B5-1: only high-frequency `message_update` appends defer the persist;
1018
+ // every other kind (message_end / tool_call / error / turn signals …)
1019
+ // still persists synchronously. Because persistNow writes the FULL
1020
+ // in-memory ring, one flush also lands any earlier pending updates in
1021
+ // order — no two-phase flush is needed.
1022
+ if (partial.kind === "message_update") {
1023
+ const atMs = partial.at ? Date.parse(partial.at) : Date.now();
1024
+ if (!Number.isNaN(atMs)) {
1025
+ persistDebounced(sessionId, ring, atMs);
1026
+ }
1027
+ else {
1028
+ persistNow(sessionId, ring);
1029
+ }
1030
+ }
1031
+ else {
1032
+ persistNow(sessionId, ring);
1033
+ }
966
1034
  const subs = subscribers.get(sessionId);
967
1035
  if (subs) {
968
1036
  for (const fn of subs) {
@@ -1017,7 +1085,7 @@ export function createChatEventStore(options) {
1017
1085
  const ring = ringFor(sessionId);
1018
1086
  const next = (turnCounts.get(sessionId) ?? 0) + 1;
1019
1087
  turnCounts.set(sessionId, next);
1020
- persist(sessionId, ring);
1088
+ persistNow(sessionId, ring);
1021
1089
  return next;
1022
1090
  },
1023
1091
  latestTurnId(sessionId) {
@@ -1043,6 +1111,9 @@ export function createChatEventStore(options) {
1043
1111
  rings.delete(sessionId);
1044
1112
  turnCounts.delete(sessionId);
1045
1113
  subscribers.delete(sessionId);
1114
+ // B5-1: drop any debounced (not yet written) updates with the ring itself —
1115
+ // the file is being deleted, so flushing first would only resurrect it.
1116
+ pendingPersists.delete(sessionId);
1046
1117
  const file = persistedPath(sessionId);
1047
1118
  if (file)
1048
1119
  rmSync(file, { force: true });
@@ -0,0 +1,72 @@
1
+ export function classifyGoalRoundAdmission(input) {
2
+ if (input.phase !== "active")
3
+ return { action: "idle", reason: "not-active" };
4
+ if (!input.armed)
5
+ return { action: "idle", reason: "disarmed" };
6
+ if (input.round >= input.maxRounds)
7
+ return { action: "idle", reason: "round-limit" };
8
+ if (input.hasQueuedUserTurn)
9
+ return { action: "wait", reason: "user-queue" };
10
+ if (input.hasActiveTurn || input.runtimeBusy)
11
+ return { action: "wait", reason: "turn-busy" };
12
+ if (input.operationBusy)
13
+ return { action: "wait", reason: "operation-busy" };
14
+ return { action: "ready" };
15
+ }
16
+ /**
17
+ * Small process-local wake coordinator. Durable Goal phase lives in the event
18
+ * stream; timers deliberately do not. A Console restart therefore cannot
19
+ * resume work until a human explicitly arms the Goal again.
20
+ */
21
+ export class GoalRoundDriver {
22
+ drive;
23
+ waitMs;
24
+ timers = new Map();
25
+ running = new Set();
26
+ disposed = false;
27
+ constructor(drive, waitMs = 1_500) {
28
+ this.drive = drive;
29
+ this.waitMs = waitMs;
30
+ }
31
+ wake(sessionId, delayMs = 0) {
32
+ if (this.disposed || this.timers.has(sessionId) || this.running.has(sessionId))
33
+ return;
34
+ const timer = setTimeout(() => {
35
+ this.timers.delete(sessionId);
36
+ void this.run(sessionId);
37
+ }, Math.max(0, delayMs));
38
+ timer.unref?.();
39
+ this.timers.set(sessionId, timer);
40
+ }
41
+ cancel(sessionId) {
42
+ const timer = this.timers.get(sessionId);
43
+ if (timer)
44
+ clearTimeout(timer);
45
+ this.timers.delete(sessionId);
46
+ }
47
+ dispose() {
48
+ this.disposed = true;
49
+ for (const timer of this.timers.values())
50
+ clearTimeout(timer);
51
+ this.timers.clear();
52
+ }
53
+ async run(sessionId) {
54
+ if (this.disposed || this.running.has(sessionId))
55
+ return;
56
+ this.running.add(sessionId);
57
+ let retry = false;
58
+ try {
59
+ const result = await this.drive(sessionId);
60
+ retry = result === "wait";
61
+ }
62
+ catch {
63
+ // The route-level driver disarms on unsafe failures. A thrown observer or
64
+ // transient read must not create a hot retry loop.
65
+ }
66
+ finally {
67
+ this.running.delete(sessionId);
68
+ if (retry)
69
+ this.wake(sessionId, this.waitMs);
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,300 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export const DEFAULT_GOAL_MAX_ROUNDS = 32;
3
+ function cloneGoal(goal) {
4
+ return structuredClone(goal);
5
+ }
6
+ export function normalizeGoalObjective(value) {
7
+ if (typeof value !== "string")
8
+ throw new Error("objective must be a string");
9
+ const objective = value.replace(/\r\n/g, "\n").trim();
10
+ if (!objective)
11
+ throw new Error("objective must not be empty");
12
+ if (objective.length > 8_000)
13
+ throw new Error("objective exceeds 8000 characters");
14
+ return objective;
15
+ }
16
+ export function isExplicitGoalCreationRequest(text) {
17
+ const normalized = text.trim().toLowerCase();
18
+ return (/^\/goal(?:\s|$)/i.test(normalized) ||
19
+ /(创建|新建|启动|开启|设定|设置).{0,10}(持续目标|goal)/i.test(normalized) ||
20
+ /(create|start|set up|enable).{0,16}\bgoal\b/i.test(normalized));
21
+ }
22
+ export function parseGoalSnapshot(value) {
23
+ if (!value || typeof value !== "object")
24
+ return undefined;
25
+ const item = value;
26
+ if (item.schemaVersion !== 1 ||
27
+ typeof item.goalId !== "string" ||
28
+ !item.goalId ||
29
+ !Number.isSafeInteger(item.revision) ||
30
+ (item.revision ?? 0) < 1 ||
31
+ typeof item.objective !== "string" ||
32
+ !item.objective.trim() ||
33
+ !["active", "paused", "blocked", "complete"].includes(item.phase) ||
34
+ !Number.isSafeInteger(item.round) ||
35
+ (item.round ?? -1) < 0 ||
36
+ !Number.isSafeInteger(item.maxRounds) ||
37
+ (item.maxRounds ?? 0) < 1 ||
38
+ typeof item.createdAt !== "string" ||
39
+ typeof item.updatedAt !== "string") {
40
+ return undefined;
41
+ }
42
+ return {
43
+ schemaVersion: 1,
44
+ goalId: item.goalId,
45
+ revision: item.revision,
46
+ objective: item.objective.trim(),
47
+ phase: item.phase,
48
+ round: item.round,
49
+ maxRounds: item.maxRounds,
50
+ createdAt: item.createdAt,
51
+ updatedAt: item.updatedAt,
52
+ ...(typeof item.lastTurnId === "string" && item.lastTurnId
53
+ ? { lastTurnId: item.lastTurnId }
54
+ : {}),
55
+ ...(typeof item.blocker === "string" && item.blocker.trim()
56
+ ? { blocker: item.blocker.trim() }
57
+ : {}),
58
+ };
59
+ }
60
+ export function projectCurrentGoal(events) {
61
+ let current = null;
62
+ for (const event of events) {
63
+ if (event.kind === "goal_cleared")
64
+ current = null;
65
+ if (event.kind === "goal_changed") {
66
+ const parsed = parseGoalSnapshot(event.data.goal);
67
+ if (parsed)
68
+ current = parsed;
69
+ }
70
+ }
71
+ return current ? cloneGoal(current) : null;
72
+ }
73
+ export function buildGoalCompactionInstructions(rawInstructions, goal) {
74
+ const reminder = goal
75
+ ? [
76
+ "Preserve this durable Goal snapshot in the compaction summary. Do not mark it complete or change its phase unless the Goal tool already did so:",
77
+ JSON.stringify(goal),
78
+ ].join("\n")
79
+ : undefined;
80
+ return [rawInstructions?.trim(), reminder]
81
+ .filter((item) => Boolean(item))
82
+ .join("\n\n") || undefined;
83
+ }
84
+ export function buildGoalContinuationContext(goal) {
85
+ if (!goal)
86
+ return undefined;
87
+ return [
88
+ "<current_goal>",
89
+ "This is the durable cross-turn Goal for this session. Continue working toward its objective without broadening user authorization.",
90
+ JSON.stringify(goal),
91
+ "Use get_goal to refresh it. Use update_goal with status=complete only when the objective is genuinely achieved and no required work remains; use blocked only when safe progress cannot continue.",
92
+ "</current_goal>",
93
+ ].join("\n");
94
+ }
95
+ export class GoalRegistry {
96
+ events;
97
+ armed = new Set();
98
+ modelCreateGrants = new Set();
99
+ constructor(events) {
100
+ this.events = events;
101
+ }
102
+ get(sessionId) {
103
+ const goal = projectCurrentGoal(this.events.snapshot(sessionId));
104
+ return goal ? { ...goal, armed: this.armed.has(sessionId) } : null;
105
+ }
106
+ startTurn(sessionId, userText) {
107
+ if (isExplicitGoalCreationRequest(userText))
108
+ this.modelCreateGrants.add(sessionId);
109
+ else
110
+ this.modelCreateGrants.delete(sessionId);
111
+ }
112
+ consumeModelCreateGrant(sessionId) {
113
+ const granted = this.modelCreateGrants.has(sessionId);
114
+ this.modelCreateGrants.delete(sessionId);
115
+ return granted;
116
+ }
117
+ create(input) {
118
+ let objective;
119
+ try {
120
+ objective = normalizeGoalObjective(input.objective);
121
+ }
122
+ catch (error) {
123
+ return {
124
+ ok: false,
125
+ code: "GOAL_INVALID_INPUT",
126
+ message: error instanceof Error ? error.message : String(error),
127
+ };
128
+ }
129
+ const existing = this.get(input.sessionId);
130
+ if (existing && existing.phase !== "complete") {
131
+ return {
132
+ ok: false,
133
+ code: "GOAL_ALREADY_EXISTS",
134
+ message: "an unfinished Goal already exists for this session",
135
+ goal: existing,
136
+ };
137
+ }
138
+ const maxRounds = Number.isSafeInteger(input.maxRounds)
139
+ ? Math.max(1, Math.min(128, input.maxRounds))
140
+ : DEFAULT_GOAL_MAX_ROUNDS;
141
+ const now = new Date().toISOString();
142
+ const goal = {
143
+ schemaVersion: 1,
144
+ goalId: randomUUID(),
145
+ revision: 1,
146
+ objective,
147
+ phase: "active",
148
+ round: 0,
149
+ maxRounds,
150
+ createdAt: now,
151
+ updatedAt: now,
152
+ };
153
+ this.events.append(input.sessionId, input.turnId ?? `${input.sessionId}:goal`, { kind: "goal_changed", data: { goal } });
154
+ if (input.arm !== false)
155
+ this.armed.add(input.sessionId);
156
+ return { ok: true, goal: { ...goal, armed: this.armed.has(input.sessionId) } };
157
+ }
158
+ update(input) {
159
+ const current = this.get(input.sessionId);
160
+ if (!current)
161
+ return { ok: false, code: "GOAL_NOT_FOUND", message: "Goal not found" };
162
+ if (current.goalId !== input.goalId)
163
+ return {
164
+ ok: false,
165
+ code: "GOAL_ID_CONFLICT",
166
+ message: "Goal id does not match the current session Goal",
167
+ goal: current,
168
+ };
169
+ if (current.revision !== input.expectedRevision)
170
+ return {
171
+ ok: false,
172
+ code: "GOAL_REVISION_CONFLICT",
173
+ message: `Goal revision changed: expected ${input.expectedRevision}, current ${current.revision}`,
174
+ goal: current,
175
+ };
176
+ let objective = current.objective;
177
+ if (input.objective !== undefined) {
178
+ try {
179
+ objective = normalizeGoalObjective(input.objective);
180
+ }
181
+ catch (error) {
182
+ return {
183
+ ok: false,
184
+ code: "GOAL_INVALID_INPUT",
185
+ message: error instanceof Error ? error.message : String(error),
186
+ goal: current,
187
+ };
188
+ }
189
+ }
190
+ const phase = input.phase ?? current.phase;
191
+ const blocker = typeof input.blocker === "string" ? input.blocker.trim().slice(0, 2_000) : "";
192
+ if (phase === "blocked" && !blocker && !current.blocker) {
193
+ return {
194
+ ok: false,
195
+ code: "GOAL_INVALID_INPUT",
196
+ message: "blocked Goal requires a concrete blocker",
197
+ goal: current,
198
+ };
199
+ }
200
+ const next = {
201
+ ...current,
202
+ revision: current.revision + 1,
203
+ objective,
204
+ phase,
205
+ updatedAt: new Date().toISOString(),
206
+ ...(phase === "blocked" && blocker
207
+ ? { blocker }
208
+ : current.blocker && phase === "blocked"
209
+ ? { blocker: current.blocker }
210
+ : {}),
211
+ };
212
+ delete next.armed;
213
+ if (phase !== "blocked")
214
+ delete next.blocker;
215
+ this.events.append(input.sessionId, input.turnId ?? current.lastTurnId ?? `${input.sessionId}:goal`, { kind: "goal_changed", data: { goal: next } });
216
+ if (phase !== "active" || input.arm === false)
217
+ this.armed.delete(input.sessionId);
218
+ else if (input.arm === true)
219
+ this.armed.add(input.sessionId);
220
+ return { ok: true, goal: { ...next, armed: this.armed.has(input.sessionId) } };
221
+ }
222
+ beginRound(input) {
223
+ const current = this.get(input.sessionId);
224
+ if (!current)
225
+ return { ok: false, code: "GOAL_NOT_FOUND", message: "Goal not found" };
226
+ if (current.round >= current.maxRounds) {
227
+ this.armed.delete(input.sessionId);
228
+ return {
229
+ ok: false,
230
+ code: "GOAL_ROUND_LIMIT",
231
+ message: `Goal reached its ${current.maxRounds}-round safety limit`,
232
+ goal: { ...current, armed: false },
233
+ };
234
+ }
235
+ if (current.goalId !== input.goalId)
236
+ return {
237
+ ok: false,
238
+ code: "GOAL_ID_CONFLICT",
239
+ message: "Goal id changed before the next round started",
240
+ goal: current,
241
+ };
242
+ if (current.revision !== input.expectedRevision)
243
+ return {
244
+ ok: false,
245
+ code: "GOAL_REVISION_CONFLICT",
246
+ message: `Goal revision changed: expected ${input.expectedRevision}, current ${current.revision}`,
247
+ goal: current,
248
+ };
249
+ const next = {
250
+ ...current,
251
+ revision: current.revision + 1,
252
+ round: current.round + 1,
253
+ lastTurnId: input.turnId,
254
+ updatedAt: new Date().toISOString(),
255
+ };
256
+ delete next.armed;
257
+ this.events.append(input.sessionId, input.turnId, {
258
+ kind: "goal_changed",
259
+ data: { goal: next },
260
+ });
261
+ return { ok: true, goal: { ...next, armed: this.armed.has(input.sessionId) } };
262
+ }
263
+ clear(input) {
264
+ const current = this.get(input.sessionId);
265
+ if (!current)
266
+ return { ok: false, code: "GOAL_NOT_FOUND", message: "Goal not found" };
267
+ if (current.goalId !== input.goalId || current.revision !== input.expectedRevision) {
268
+ return {
269
+ ok: false,
270
+ code: current.goalId !== input.goalId
271
+ ? "GOAL_ID_CONFLICT"
272
+ : "GOAL_REVISION_CONFLICT",
273
+ message: "Goal changed before it could be cleared",
274
+ goal: current,
275
+ };
276
+ }
277
+ this.armed.delete(input.sessionId);
278
+ this.events.append(input.sessionId, input.turnId ?? current.lastTurnId ?? `${input.sessionId}:goal`, { kind: "goal_cleared", data: {} });
279
+ return { ok: true, goal: { ...current, armed: false } };
280
+ }
281
+ arm(sessionId) {
282
+ const goal = this.get(sessionId);
283
+ if (!goal || goal.phase !== "active" || goal.round >= goal.maxRounds)
284
+ return goal;
285
+ this.armed.add(sessionId);
286
+ return { ...goal, armed: true };
287
+ }
288
+ disarm(sessionId) {
289
+ this.armed.delete(sessionId);
290
+ const goal = this.get(sessionId);
291
+ return goal ? { ...goal, armed: false } : null;
292
+ }
293
+ isArmed(sessionId) {
294
+ return this.armed.has(sessionId);
295
+ }
296
+ clearRuntime(sessionId) {
297
+ this.armed.delete(sessionId);
298
+ this.modelCreateGrants.delete(sessionId);
299
+ }
300
+ }