@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
@@ -0,0 +1,48 @@
1
+ import { projectDeferredTurnSummaries, projectDeferredTurnSummary, } from "./deferred-turn.js";
2
+ /**
3
+ * Process-wide Session baseline, mirroring DSH's independent session.list.
4
+ * Full records stay in their fingerprint-isolated stores; this catalog only
5
+ * removes repeated filesystem discovery and resolves Session ownership.
6
+ */
7
+ export class ChatSessionCatalog {
8
+ entries = new Map();
9
+ upsert(record, ownerWorkspaceId) {
10
+ const workspaceId = ownerWorkspaceId?.trim() || record.workspaceId?.trim();
11
+ const existing = this.entries.get(record.sessionId);
12
+ if (existing &&
13
+ existing.repoFingerprint !== record.repoFingerprint) {
14
+ throw new Error(`chat session catalog conflict: ${record.sessionId} belongs to multiple repositories`);
15
+ }
16
+ this.entries.set(record.sessionId, {
17
+ sessionId: record.sessionId,
18
+ repoFingerprint: record.repoFingerprint,
19
+ repoRootDisplay: record.repoRootDisplay,
20
+ state: record.state,
21
+ ...(record.title !== undefined ? { title: record.title } : {}),
22
+ ...(record.displayTitle !== undefined
23
+ ? { displayTitle: record.displayTitle }
24
+ : {}),
25
+ createdAt: record.createdAt,
26
+ updatedAt: record.updatedAt,
27
+ ...(record.modelProvider !== undefined
28
+ ? { modelProvider: record.modelProvider }
29
+ : {}),
30
+ ...(record.modelId !== undefined ? { modelId: record.modelId } : {}),
31
+ ...(record.thinkingLevel !== undefined
32
+ ? { thinkingLevel: record.thinkingLevel }
33
+ : {}),
34
+ deferredTurn: projectDeferredTurnSummary(record.deferredTurns?.[0]),
35
+ deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
36
+ ...(workspaceId ? { workspaceId } : {}),
37
+ });
38
+ }
39
+ remove(sessionId) {
40
+ this.entries.delete(sessionId);
41
+ }
42
+ get(sessionId) {
43
+ return this.entries.get(sessionId);
44
+ }
45
+ list() {
46
+ return [...this.entries.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
47
+ }
48
+ }
@@ -156,6 +156,102 @@ export function deriveChatSessionTelemetry(events) {
156
156
  export function billedInputTokens(usage) {
157
157
  return usage.inputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
158
158
  }
159
+ export function deriveChatTurnTelemetry(events) {
160
+ const byTurn = new Map();
161
+ const turnOf = (turnId) => {
162
+ let entry = byTurn.get(turnId);
163
+ if (!entry) {
164
+ entry = {
165
+ turnId,
166
+ steps: 0,
167
+ llmMs: 0,
168
+ toolMs: 0,
169
+ toolCalls: 0,
170
+ samples: [],
171
+ };
172
+ byTurn.set(turnId, entry);
173
+ }
174
+ return entry;
175
+ };
176
+ const toolStarts = new Map();
177
+ for (const event of events) {
178
+ const at = eventTime(event);
179
+ const turn = turnOf(event.turnId);
180
+ if (event.kind === "usage") {
181
+ const sample = usageSample(event.data);
182
+ if (sample)
183
+ turn.samples.push(sample);
184
+ continue;
185
+ }
186
+ if (event.kind === "user_message") {
187
+ if (turn.startedAt === undefined && at !== undefined)
188
+ turn.startedAt = at;
189
+ continue;
190
+ }
191
+ if (event.kind === "agent_start") {
192
+ if (at !== undefined)
193
+ turn.stepStart = at;
194
+ continue;
195
+ }
196
+ if (event.kind === "tool_call") {
197
+ turn.toolCalls += 1;
198
+ const toolCallId = event.data.toolCallId;
199
+ if (typeof toolCallId === "string" && toolCallId && at !== undefined) {
200
+ toolStarts.set(`${event.turnId}\u0000${toolCallId}`, at);
201
+ }
202
+ continue;
203
+ }
204
+ if (event.kind === "tool_result") {
205
+ const toolCallId = event.data.toolCallId;
206
+ if (typeof toolCallId === "string" && toolCallId && at !== undefined) {
207
+ const key = `${event.turnId}\u0000${toolCallId}`;
208
+ const startedAt = toolStarts.get(key);
209
+ if (startedAt !== undefined && at >= startedAt) {
210
+ turn.toolMs += at - startedAt;
211
+ }
212
+ toolStarts.delete(key);
213
+ // Next assistant step within the same turn starts after this tool.
214
+ turn.stepStart = at;
215
+ }
216
+ continue;
217
+ }
218
+ if (event.kind !== "message_end")
219
+ continue;
220
+ turn.steps += 1;
221
+ const recorded = stepTiming(event.data);
222
+ if (recorded?.llmMs !== undefined)
223
+ turn.llmMs += recorded.llmMs;
224
+ else if (at !== undefined &&
225
+ turn.stepStart !== undefined &&
226
+ at >= turn.stepStart) {
227
+ turn.llmMs += at - turn.stepStart;
228
+ }
229
+ if (recorded?.ttftMs !== undefined) {
230
+ turn.ttftMs = (turn.ttftMs ?? 0) + recorded.ttftMs;
231
+ }
232
+ if (recorded?.decodeMs !== undefined &&
233
+ recorded.decodeMs > 0 &&
234
+ recorded.outputTokens !== undefined) {
235
+ turn.decodeMs = (turn.decodeMs ?? 0) + recorded.decodeMs;
236
+ turn.decodeTokens = (turn.decodeTokens ?? 0) + recorded.outputTokens;
237
+ }
238
+ turn.stepStart = undefined;
239
+ }
240
+ return [...byTurn.values()].map((entry) => ({
241
+ turnId: entry.turnId,
242
+ steps: entry.steps,
243
+ llmMs: entry.llmMs,
244
+ toolMs: entry.toolMs,
245
+ toolCalls: entry.toolCalls,
246
+ ...(entry.ttftMs !== undefined ? { ttftMs: entry.ttftMs } : {}),
247
+ ...(entry.decodeMs !== undefined ? { decodeMs: entry.decodeMs } : {}),
248
+ ...(entry.decodeTokens !== undefined
249
+ ? { decodeTokens: entry.decodeTokens }
250
+ : {}),
251
+ usage: aggregateUsage(entry.samples),
252
+ ...(entry.startedAt !== undefined ? { startedAt: entry.startedAt } : {}),
253
+ }));
254
+ }
159
255
  export function cacheHitPercent(usage) {
160
256
  const billed = billedInputTokens(usage);
161
257
  return billed > 0 ? Math.round((usage.cacheReadTokens / billed) * 100) : null;
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import path from "node:path";
14
14
  import { readdir, rm } from "node:fs/promises";
15
- import { readJsonIfExists, writeSecureJson, } from "../app-data.js";
15
+ import { readJsonIfExists, writeSecureJson, } from "../../../infrastructure/console/app-data.js";
16
16
  import { dispatchOperatorAction } from "../operator-actions.js";
17
17
  import { authorizeOperatorChatTool } from "./tools.js";
18
18
  import { projectOperationForChat } from "./chat-event-store.js";
@@ -40,14 +40,20 @@ function recordPath(appData, sessionId) {
40
40
  }
41
41
  export class ChatSessionStore {
42
42
  appData;
43
+ options;
43
44
  /**
44
45
  * Per-session write lock. Each mutating op chains onto the previous one so
45
46
  * concurrent appendMessage / recordToolInvocation calls do not lose updates
46
47
  * via last-write-wins on the same JSON record.
47
48
  */
48
49
  writeChains = new Map();
49
- constructor(appData) {
50
+ constructor(appData, options = {}) {
50
51
  this.appData = appData;
52
+ this.options = options;
53
+ }
54
+ async persist(record) {
55
+ await writeSecureJson(recordPath(this.appData, record.sessionId), record);
56
+ this.options.onRecordChange?.(record.sessionId, record);
51
57
  }
52
58
  /** Read + migrate legacy V1 records into the V2 in-memory shape. */
53
59
  async get(sessionId) {
@@ -147,7 +153,7 @@ export class ChatSessionStore {
147
153
  messages: [],
148
154
  toolInvocations: [],
149
155
  };
150
- await writeSecureJson(recordPath(this.appData, input.sessionId), record);
156
+ await this.persist(record);
151
157
  return record;
152
158
  });
153
159
  }
@@ -169,7 +175,7 @@ export class ChatSessionStore {
169
175
  messages: structuredClone(source.messages),
170
176
  toolInvocations: structuredClone(source.toolInvocations),
171
177
  };
172
- await writeSecureJson(recordPath(this.appData, input.targetSessionId), record);
178
+ await this.persist(record);
173
179
  return record;
174
180
  });
175
181
  }
@@ -180,7 +186,7 @@ export class ChatSessionStore {
180
186
  throw new Error(`chat session not found: ${sessionId}`);
181
187
  record.title = title.trim().slice(0, 160) || undefined;
182
188
  record.updatedAt = new Date().toISOString();
183
- await writeSecureJson(recordPath(this.appData, sessionId), record);
189
+ await this.persist(record);
184
190
  return record;
185
191
  });
186
192
  }
@@ -195,7 +201,7 @@ export class ChatSessionStore {
195
201
  return undefined;
196
202
  record.title = cleaned;
197
203
  record.updatedAt = new Date().toISOString();
198
- await writeSecureJson(recordPath(this.appData, sessionId), record);
204
+ await this.persist(record);
199
205
  return record;
200
206
  });
201
207
  }
@@ -211,7 +217,7 @@ export class ChatSessionStore {
211
217
  if (selection.thinkingLevel !== undefined)
212
218
  record.thinkingLevel = selection.thinkingLevel;
213
219
  record.updatedAt = new Date().toISOString();
214
- await writeSecureJson(recordPath(this.appData, sessionId), record);
220
+ await this.persist(record);
215
221
  return record;
216
222
  });
217
223
  }
@@ -222,7 +228,7 @@ export class ChatSessionStore {
222
228
  throw new Error(`chat session not found: ${sessionId}`);
223
229
  record.state = state;
224
230
  record.updatedAt = new Date().toISOString();
225
- await writeSecureJson(recordPath(this.appData, sessionId), record);
231
+ await this.persist(record);
226
232
  return record;
227
233
  });
228
234
  }
@@ -232,6 +238,7 @@ export class ChatSessionStore {
232
238
  if (!record)
233
239
  return false;
234
240
  await rm(recordPath(this.appData, sessionId), { force: true });
241
+ this.options.onRecordChange?.(sessionId, undefined);
235
242
  return true;
236
243
  });
237
244
  }
@@ -285,7 +292,7 @@ export class ChatSessionStore {
285
292
  record.deferredTurn = queue[0];
286
293
  record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
287
294
  record.updatedAt = now;
288
- await writeSecureJson(recordPath(this.appData, sessionId), record);
295
+ await this.persist(record);
289
296
  return { ok: true, deferredTurn, idempotent: false };
290
297
  });
291
298
  }
@@ -313,7 +320,7 @@ export class ChatSessionStore {
313
320
  record.deferredTurn = queue[0];
314
321
  record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
315
322
  record.updatedAt = next.updatedAt;
316
- await writeSecureJson(recordPath(this.appData, sessionId), record);
323
+ await this.persist(record);
317
324
  return { ok: true, deferredTurn: next };
318
325
  });
319
326
  }
@@ -335,7 +342,7 @@ export class ChatSessionStore {
335
342
  record.deferredTurn = queue[0];
336
343
  record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
337
344
  record.updatedAt = next.updatedAt;
338
- await writeSecureJson(recordPath(this.appData, sessionId), record);
345
+ await this.persist(record);
339
346
  return next;
340
347
  });
341
348
  }
@@ -356,7 +363,7 @@ export class ChatSessionStore {
356
363
  record.deferredTurn = queue[0];
357
364
  record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
358
365
  record.updatedAt = new Date().toISOString();
359
- await writeSecureJson(recordPath(this.appData, sessionId), record);
366
+ await this.persist(record);
360
367
  return { ok: true, deferredTurn: existing };
361
368
  });
362
369
  }
@@ -373,7 +380,7 @@ export class ChatSessionStore {
373
380
  record.deferredTurn = queue[0];
374
381
  record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
375
382
  record.updatedAt = new Date().toISOString();
376
- await writeSecureJson(recordPath(this.appData, sessionId), record);
383
+ await this.persist(record);
377
384
  return { deferredTurn: existing, clearedRevision: existing.revision + 1 };
378
385
  });
379
386
  }
@@ -392,7 +399,7 @@ export class ChatSessionStore {
392
399
  record.displayTitle = cleanChatTitle(stored.text);
393
400
  }
394
401
  record.updatedAt = stored.at;
395
- await writeSecureJson(recordPath(this.appData, sessionId), record);
402
+ await this.persist(record);
396
403
  return stored;
397
404
  });
398
405
  }
@@ -403,7 +410,7 @@ export class ChatSessionStore {
403
410
  return;
404
411
  record.toolInvocations.push({ ...entry, at: new Date().toISOString() });
405
412
  record.updatedAt = new Date().toISOString();
406
- await writeSecureJson(recordPath(this.appData, sessionId), record);
413
+ await this.persist(record);
407
414
  });
408
415
  }
409
416
  }
@@ -501,10 +508,12 @@ export async function runChatTurn(options) {
501
508
  },
502
509
  };
503
510
  }
504
- const userMessage = await store.appendMessage(sessionId, { role: "user", text });
505
- const afterUserMessage = await store.get(sessionId);
506
- if (afterUserMessage)
507
- await options.onUserMessagePersisted?.(afterUserMessage, userMessage);
511
+ if (options.persistUserMessage !== false) {
512
+ const userMessage = await store.appendMessage(sessionId, { role: "user", text });
513
+ const afterUserMessage = await store.get(sessionId);
514
+ if (afterUserMessage)
515
+ await options.onUserMessagePersisted?.(afterUserMessage, userMessage);
516
+ }
508
517
  let turnError;
509
518
  // Collect persistence promises so runChatTurn does not resolve before the
510
519
  // chat history / audit trail is durably written (also makes the outcome
@@ -44,6 +44,13 @@ export const SHORTCUTS = [
44
44
  ];
45
45
  /** Pi Web commands with real browser-equivalent behavior (UI-11). */
46
46
  export const WEB_SLASH_COMMANDS = [
47
+ {
48
+ command: "/goal",
49
+ label: "持续目标",
50
+ description: "创建或编辑一个跨多个回合持续推进的目标",
51
+ action: "goal",
52
+ source: "web",
53
+ },
47
54
  {
48
55
  command: "/compact",
49
56
  label: "压缩上下文",
@@ -0,0 +1 @@
1
+ export { createSiftBridgeState, parseSiftBridgeConfig, compressProviderPayload, executeSiftRetrieve, housekeepSiftStash, createSiftRetrieveToolDefinition, SIFT_RETRIEVE_TOOL_NAME, SIFT_POLICY_VERSION, } from "../../../shared/pi-context-pressure/sift-bridge.js";
@@ -34,17 +34,21 @@ export function hydrateSessionThread(raw) {
34
34
  for (const item of raw) {
35
35
  const createdAt = Date.parse(item.at) || Date.now();
36
36
  if (item.role === "user") {
37
- turnId = `restored-${item.id}`;
37
+ turnId = item.turnId ?? `restored-${item.id}`;
38
38
  pendingTools = [];
39
39
  messages.push({
40
40
  id: item.id,
41
41
  role: "user",
42
42
  text: item.text,
43
43
  createdAt,
44
+ ...(item.eventSeq === undefined ? {} : { eventSeq: item.eventSeq }),
45
+ ...(item.turnId ? { turnId: item.turnId } : {}),
44
46
  });
45
47
  continue;
46
48
  }
47
49
  if (item.role === "assistant") {
50
+ if (item.turnId)
51
+ turnId = item.turnId;
48
52
  if (!turnId)
49
53
  turnId = `restored-${item.id}`;
50
54
  const content = parseAssistantContent(item.content);
@@ -53,6 +57,8 @@ export function hydrateSessionThread(raw) {
53
57
  role: "assistant",
54
58
  text: item.text,
55
59
  createdAt,
60
+ ...(item.eventSeq === undefined ? {} : { eventSeq: item.eventSeq }),
61
+ ...(item.turnId ? { turnId: item.turnId } : {}),
56
62
  ...(item.kind === "provider-error" && item.providerError
57
63
  ? { kind: item.kind, providerError: item.providerError }
58
64
  : {}),
@@ -62,6 +68,8 @@ export function hydrateSessionThread(raw) {
62
68
  continue;
63
69
  }
64
70
  if (item.role === "tool") {
71
+ if (item.turnId)
72
+ turnId = item.turnId;
65
73
  if (!turnId)
66
74
  turnId = `restored-tool-${item.id}`;
67
75
  const entry = {
@@ -72,7 +80,11 @@ export function hydrateSessionThread(raw) {
72
80
  result: item.text,
73
81
  createdAt,
74
82
  finishedAt: createdAt,
75
- turnId,
83
+ turnId: item.turnId ?? turnId,
84
+ ...(item.eventSeq === undefined ? {} : { eventSeq: item.eventSeq }),
85
+ ...(item.finishedSeq === undefined
86
+ ? {}
87
+ : { finishedSeq: item.finishedSeq }),
76
88
  ...(item.operationId ? { operationId: item.operationId } : {}),
77
89
  };
78
90
  toolCalls.push(entry);
@@ -196,6 +208,8 @@ function fillProcessDetails(group) {
196
208
  group.answer = finalAssistant;
197
209
  return;
198
210
  }
211
+ if (split.answerBlocks.length === 0)
212
+ return;
199
213
  const answerText = joinAssistantText(split.answerBlocks);
200
214
  group.answer = {
201
215
  ...finalAssistant,
@@ -207,7 +221,22 @@ function fillProcessDetails(group) {
207
221
  * Build user → process → answer groups for the message timeline.
208
222
  */
209
223
  export function buildTurnProcessGroups(input) {
210
- const visible = input.messages.filter((m) => m.role === "user" || m.role === "assistant");
224
+ const visible = input.messages
225
+ .filter((m) => m.role === "user" || m.role === "assistant")
226
+ .sort((a, b) => {
227
+ if (a.eventSeq !== undefined &&
228
+ b.eventSeq !== undefined &&
229
+ a.eventSeq !== b.eventSeq) {
230
+ return a.eventSeq - b.eventSeq;
231
+ }
232
+ if (a.createdAt !== b.createdAt)
233
+ return a.createdAt - b.createdAt;
234
+ if (a.eventSeq !== undefined && b.eventSeq === undefined)
235
+ return 1;
236
+ if (a.eventSeq === undefined && b.eventSeq !== undefined)
237
+ return -1;
238
+ return 0;
239
+ });
211
240
  const groups = [];
212
241
  let current = null;
213
242
  const startGroup = (key) => {
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import path from "node:path";
3
- import { readJsonIfExists, writeSecureJson } from "../app-data.js";
3
+ import { readJsonIfExists, writeSecureJson, } from "../../../infrastructure/console/app-data.js";
4
4
  function questionFile(root, sessionId) {
5
5
  const safe = sessionId.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 180);
6
6
  return path.join(root, `${safe}.json`);
@@ -4,7 +4,7 @@ import { createNpmUpdateClient } from "../../shared/update/npm-client.js";
4
4
  import { acquireUpdateLock, updateLockPath } from "../../shared/update/state.js";
5
5
  import { readInitRuntimeActivity } from "../observability/init-runtime-activity.js";
6
6
  import { performConsoleUpdateAndInit, } from "./console-update-and-init.js";
7
- import { TERMINAL_OPERATION_STATES } from "./operation-store.js";
7
+ import { TERMINAL_OPERATION_STATES } from "../../infrastructure/console/operation-store.js";
8
8
  export function createConsoleUpdateExecutor(input) {
9
9
  return async (channel) => {
10
10
  const currentVersion = readPackageVersion(input.packageRoot);
@@ -0,0 +1,198 @@
1
+ import path from "node:path";
2
+ import { appendFile, chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { randomBytes } from "node:crypto";
4
+ import { assertSafeAppDataPath, readJsonIfExists, writeSecureJson, } from "../../infrastructure/console/app-data.js";
5
+ const diagnosticWriteChains = new Map();
6
+ function serializeDiagnostic(filePath, operation) {
7
+ const previous = diagnosticWriteChains.get(filePath) ?? Promise.resolve();
8
+ const next = previous.then(operation, operation);
9
+ diagnosticWriteChains.set(filePath, next.then(() => undefined, () => undefined));
10
+ return next;
11
+ }
12
+ function diagnosticsPath(appData) {
13
+ const repoNamespace = path.dirname(appData.operations);
14
+ return assertSafeAppDataPath(repoNamespace, path.join(repoNamespace, "context-transfer-diagnostics.json"));
15
+ }
16
+ function safeNonNegative(value) {
17
+ return Number.isFinite(value) && (value ?? 0) >= 0 ? Math.round(value ?? 0) : 0;
18
+ }
19
+ function safeReasonCode(value) {
20
+ if (!value)
21
+ return undefined;
22
+ const normalized = value.trim().slice(0, 96);
23
+ return /^[A-Za-z0-9_.-]+$/.test(normalized) ? normalized : "UNCLASSIFIED";
24
+ }
25
+ export async function recordContextTransferDiagnostic(input) {
26
+ const filePath = diagnosticsPath(input.appData);
27
+ return serializeDiagnostic(filePath, async () => {
28
+ const current = (await readJsonIfExists(filePath)) ?? {
29
+ schemaVersion: 1,
30
+ updatedAt: (input.now ?? new Date()).toISOString(),
31
+ counters: [],
32
+ };
33
+ const dimensions = {
34
+ outcome: input.dimensions.outcome,
35
+ ...(input.dimensions.target ? { target: input.dimensions.target } : {}),
36
+ ...(input.dimensions.profile ? { profile: input.dimensions.profile } : {}),
37
+ ...(input.dimensions.diffKind ? { diffKind: input.dimensions.diffKind } : {}),
38
+ ...(safeReasonCode(input.dimensions.reasonCode)
39
+ ? { reasonCode: safeReasonCode(input.dimensions.reasonCode) }
40
+ : {}),
41
+ };
42
+ const key = JSON.stringify({ event: input.event, ...dimensions });
43
+ const existing = current.counters.find((counter) => JSON.stringify({ event: counter.event, ...dimensionsFromCounter(counter) }) === key);
44
+ if (existing) {
45
+ existing.count += 1;
46
+ existing.bytes += safeNonNegative(input.dimensions.bytes);
47
+ existing.durationMs += safeNonNegative(input.dimensions.durationMs);
48
+ existing.affectedNodeCount += safeNonNegative(input.dimensions.affectedNodeCount);
49
+ existing.importedNodeCount += safeNonNegative(input.dimensions.importedNodeCount);
50
+ }
51
+ else {
52
+ current.counters.push({
53
+ event: input.event,
54
+ ...dimensions,
55
+ count: 1,
56
+ bytes: safeNonNegative(input.dimensions.bytes),
57
+ durationMs: safeNonNegative(input.dimensions.durationMs),
58
+ affectedNodeCount: safeNonNegative(input.dimensions.affectedNodeCount),
59
+ importedNodeCount: safeNonNegative(input.dimensions.importedNodeCount),
60
+ });
61
+ }
62
+ current.updatedAt = (input.now ?? new Date()).toISOString();
63
+ await writeSecureJson(filePath, current);
64
+ return current;
65
+ });
66
+ }
67
+ function dimensionsFromCounter(counter) {
68
+ return {
69
+ outcome: counter.outcome,
70
+ ...(counter.target ? { target: counter.target } : {}),
71
+ ...(counter.profile ? { profile: counter.profile } : {}),
72
+ ...(counter.diffKind ? { diffKind: counter.diffKind } : {}),
73
+ ...(counter.reasonCode ? { reasonCode: counter.reasonCode } : {}),
74
+ };
75
+ }
76
+ export async function readContextTransferDiagnostics(appData) {
77
+ return readJsonIfExists(diagnosticsPath(appData));
78
+ }
79
+ /**
80
+ * Append-only lifecycle event log for context-transfer mutations
81
+ * (exec-plan observability: context_export.* / artifact_revision.* /
82
+ * artifact_rerun.*). Events carry ids, counts, bytes, duration and reason
83
+ * codes only — never prompt text, artifact content, secret finding values or
84
+ * host absolute paths. The Console emits only stages it truly observes;
85
+ * CLI-kernel-internal stages (capture, secret scan, adapters) stay in the
86
+ * kernel's own evidence.
87
+ */
88
+ export const CONTEXT_TRANSFER_EVENT_LOG_LIMIT = 512;
89
+ const CONTEXT_TRANSFER_EVENT_NAME_PATTERN = /^(?:context_export|artifact_revision|artifact_rerun)\.[a-z][a-z_]{0,47}$/;
90
+ const CONTEXT_TRANSFER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
91
+ export async function recordContextTransferEvent(input) {
92
+ const at = (input.now ?? new Date()).toISOString();
93
+ if (!CONTEXT_TRANSFER_EVENT_NAME_PATTERN.test(input.event)) {
94
+ return { recorded: false, total: 0 };
95
+ }
96
+ const fields = input.fields ?? {};
97
+ const entry = {
98
+ schemaVersion: 1,
99
+ at,
100
+ event: input.event,
101
+ ...sanitizeLifecycleFields(fields),
102
+ };
103
+ const filePath = lifecycleEventsPath(input.appData);
104
+ return serializeDiagnostic(filePath, async () => {
105
+ await appendLifecycleEvent(filePath, entry);
106
+ const total = await countLifecycleEvents(filePath);
107
+ if (total > CONTEXT_TRANSFER_EVENT_LOG_LIMIT) {
108
+ await trimLifecycleEvents(filePath, CONTEXT_TRANSFER_EVENT_LOG_LIMIT);
109
+ return { recorded: true, total: CONTEXT_TRANSFER_EVENT_LOG_LIMIT };
110
+ }
111
+ return { recorded: true, total };
112
+ });
113
+ }
114
+ export async function readContextTransferEvents(appData) {
115
+ const filePath = lifecycleEventsPath(appData);
116
+ const raw = await readFile(filePath, "utf8").catch(() => "");
117
+ const events = [];
118
+ for (const line of raw.split("\n")) {
119
+ if (!line.trim())
120
+ continue;
121
+ try {
122
+ const parsed = JSON.parse(line);
123
+ if (parsed &&
124
+ typeof parsed === "object" &&
125
+ parsed.schemaVersion === 1 &&
126
+ typeof parsed.event === "string") {
127
+ events.push(parsed);
128
+ }
129
+ }
130
+ catch {
131
+ // A torn final line after a crash must not break readers.
132
+ }
133
+ }
134
+ return events;
135
+ }
136
+ function lifecycleEventsPath(appData) {
137
+ const repoNamespace = path.dirname(appData.operations);
138
+ return assertSafeAppDataPath(repoNamespace, path.join(repoNamespace, "context-transfer-events.jsonl"));
139
+ }
140
+ function safeLifecycleId(value) {
141
+ if (!value)
142
+ return undefined;
143
+ return CONTEXT_TRANSFER_ID_PATTERN.test(value) ? value : undefined;
144
+ }
145
+ function sanitizeLifecycleFields(fields) {
146
+ return {
147
+ ...(safeLifecycleId(fields.runId) ? { runId: fields.runId } : {}),
148
+ ...(safeLifecycleId(fields.nodeId) ? { nodeId: fields.nodeId } : {}),
149
+ ...(safeLifecycleId(fields.exportId) ? { exportId: fields.exportId } : {}),
150
+ ...(safeLifecycleId(fields.revisionId)
151
+ ? { revisionId: fields.revisionId }
152
+ : {}),
153
+ ...(safeLifecycleId(fields.operationId)
154
+ ? { operationId: fields.operationId }
155
+ : {}),
156
+ ...(fields.profile === "redacted" || fields.profile === "raw"
157
+ ? { profile: fields.profile }
158
+ : {}),
159
+ ...(fields.target === "generic" ||
160
+ fields.target === "pi-session" ||
161
+ fields.target === "pi-handoff" ||
162
+ fields.target === "readable"
163
+ ? { target: fields.target }
164
+ : {}),
165
+ ...(fields.bytes !== undefined
166
+ ? { bytes: safeNonNegative(fields.bytes) }
167
+ : {}),
168
+ ...(fields.durationMs !== undefined
169
+ ? { durationMs: safeNonNegative(fields.durationMs) }
170
+ : {}),
171
+ ...(safeReasonCode(fields.reasonCode)
172
+ ? { reasonCode: safeReasonCode(fields.reasonCode) }
173
+ : {}),
174
+ };
175
+ }
176
+ async function appendLifecycleEvent(filePath, entry) {
177
+ await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
178
+ await appendFile(filePath, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
179
+ await chmod(filePath, 0o600).catch(() => undefined);
180
+ }
181
+ async function countLifecycleEvents(filePath) {
182
+ const raw = await readFile(filePath, "utf8").catch(() => "");
183
+ let total = 0;
184
+ for (const line of raw.split("\n")) {
185
+ if (line.trim())
186
+ total += 1;
187
+ }
188
+ return total;
189
+ }
190
+ async function trimLifecycleEvents(filePath, limit) {
191
+ const raw = await readFile(filePath, "utf8").catch(() => "");
192
+ const lines = raw.split("\n").filter((line) => line.trim());
193
+ const kept = lines.slice(-limit);
194
+ const tmp = `${filePath}.${randomBytes(8).toString("hex")}.tmp`;
195
+ await writeFile(tmp, `${kept.join("\n")}\n`, { mode: 0o600, flag: "wx" });
196
+ await rename(tmp, filePath);
197
+ await chmod(filePath, 0o600).catch(() => undefined);
198
+ }
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import path from "node:path";
3
- import { readJsonIfExists, sha256Utf8, writeSecureJson, } from "./app-data.js";
3
+ import { readJsonIfExists, sha256Utf8, writeSecureJson, } from "../../infrastructure/console/app-data.js";
4
4
  import { confirmationPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "./human-gate-token.js";
5
5
  export const ALL_CONFIRMATION_CHALLENGES = [
6
6
  "source",