@tea-agent/loop-agent 0.40.0-next.9 → 0.41.0-next.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 (148) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +29 -20
  3. package/README.md +2 -2
  4. package/dist/application/dag/generate-task-dag.js +6 -2
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/command-definitions.js +9 -5
  7. package/dist/cli/program.js +38 -3
  8. package/dist/commands/client-recovery.js +8 -36
  9. package/dist/commands/init-model-catalog.js +22 -0
  10. package/dist/commands/task-advance.js +23 -1
  11. package/dist/commands/task-source-prepare.js +5 -0
  12. package/dist/commands/task-status.js +1 -0
  13. package/dist/executors/pi-sdk-executor.js +33 -5
  14. package/dist/executors/shell-executor.js +94 -20
  15. package/dist/governance/checks.js +1 -0
  16. package/dist/shared/operator/capabilities.js +9 -1
  17. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  18. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  19. package/dist/shared/pi-context-pressure/env.js +58 -0
  20. package/dist/shared/pi-context-pressure/extension.js +100 -0
  21. package/dist/shared/pi-context-pressure/index.js +7 -0
  22. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  23. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  24. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  25. package/dist/worker/console/chat/assistant-content.js +7 -0
  26. package/dist/worker/console/chat/chat-event-store.js +82 -11
  27. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  28. package/dist/worker/console/chat/goals.js +300 -0
  29. package/dist/worker/console/chat/pi-runtime.js +286 -8
  30. package/dist/worker/console/chat/provider-error.js +2 -1
  31. package/dist/worker/console/chat/repo-browser.js +94 -12
  32. package/dist/worker/console/chat/routes.js +495 -30
  33. package/dist/worker/console/chat/semantic-activity.js +12 -11
  34. package/dist/worker/console/chat/session-catalog.js +48 -0
  35. package/dist/worker/console/chat/session-stats.js +96 -0
  36. package/dist/worker/console/chat/session-store.js +27 -18
  37. package/dist/worker/console/chat/shortcuts.js +7 -0
  38. package/dist/worker/console/chat/sift-bridge.js +1 -0
  39. package/dist/worker/console/chat/turn-process.js +60 -7
  40. package/dist/worker/console/dag-execution-receipt.js +20 -2
  41. package/dist/worker/console/operator-actions.js +20 -5
  42. package/dist/worker/console/operator-selection.js +6 -2
  43. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  44. package/dist/worker/console/pi-plugins.js +179 -0
  45. package/dist/worker/console/routes.js +246 -6
  46. package/dist/worker/console/server.js +173 -42
  47. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-CV1qyRbn.js} +1 -1
  48. package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-B_ljC4ma.js} +1 -1
  49. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-DTBqsauL.js} +1 -1
  50. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-CMZj6nYE.js} +1 -1
  51. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-C4ey_S7v.js} +1 -1
  52. package/dist/worker/console/static/assets/channel-BvYm_83n.js +1 -0
  53. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-CGcAlY6Z.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-XRSM64J1.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-COSIFYbz.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-BwuaXiOK.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-DNbFuVlv.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-C4DZKffE.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-M3A6iczx.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-C5zDzQEM.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-uAcUDub8.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-EPpz2uSP.js} +1 -1
  63. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BNdy3f9F.js +1 -0
  64. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BNdy3f9F.js +1 -0
  65. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-BAkfFGTf.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-Cjb4d22A.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-Cf7QSgcu.js} +1 -1
  68. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-UlyVBCtl.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-DWdRAZ8p.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-Y2ag2Pm0.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-DBHcuPXI.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-BK7eno2C.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-Dbyku82r.js} +1 -1
  74. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR--rS0Q41A.js} +1 -1
  75. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-Cms8pJeK.js} +1 -1
  76. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-DouONwBy.js} +1 -1
  77. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-DuKB5yDe.js} +1 -1
  78. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-DE6zp0SI.js} +1 -1
  79. package/dist/worker/console/static/assets/index-C39zQYik.js +451 -0
  80. package/dist/worker/console/static/assets/index-Crixt9tN.css +1 -0
  81. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-BLETv9SH.js} +1 -1
  82. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-DyU-2lJb.js} +1 -1
  83. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-BotFSygt.js} +1 -1
  84. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-CqcAMgVk.js} +1 -1
  85. package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-Bf4U8h5G.js} +1 -1
  86. package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-D0JEVefG.js} +5 -5
  87. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-DEFgNgOo.js} +1 -1
  88. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-CirKhxo1.js} +1 -1
  89. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-BlaQYInF.js} +1 -1
  90. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-BbyzqH4U.js} +1 -1
  91. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-qA-XykV6.js} +1 -1
  92. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-Cc85Zcxc.js} +1 -1
  93. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-CS7BVnoH.js} +1 -1
  94. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-DMQ-kKmZ.js} +1 -1
  95. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-DXKs1dcE.js} +1 -1
  96. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-xG1ti87X.js} +1 -1
  97. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-tYINJZ8x.js +1 -0
  98. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-BQwmKGHd.js} +2 -2
  99. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-OoBOXPiG.js +8 -0
  100. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-BwL9oHtP.js} +1 -1
  101. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-BWk-i_Y1.js} +1 -1
  102. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-DAvETpEZ.js} +1 -1
  103. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-Cd4pASnF.js} +1 -1
  104. package/dist/worker/console/static/index.html +2 -2
  105. package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
  106. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  107. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  108. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
  109. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  110. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
  111. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  112. package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
  113. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  114. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  115. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  116. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
  117. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  118. package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
  119. package/dist/worker/console/workspace-context.js +28 -3
  120. package/dist/worker/console/workspace-registry.js +333 -60
  121. package/dist/worker/observability/read-model.js +4 -0
  122. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  123. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  124. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  125. package/dist/worker/observe/static/operator-chrome.css +23 -4
  126. package/dist/worker/observe/static/operator-chrome.js +7 -2
  127. package/dist/worker/observe/static/styles.css +87 -25
  128. package/dist/worker/observe/static/views/dag-inspector.js +42 -1
  129. package/dist/worker/observe/static/views/dags.js +7 -5
  130. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  131. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
  132. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  133. package/dist/workflows/dag/backend-test-result-contract.js +5 -1
  134. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  135. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  136. package/dist/workflows/dag/init-hybrid.js +5 -4
  137. package/docs/architecture/runtime-boundaries.md +2 -1
  138. package/docs/templates/backend-test-dag.json +6 -5
  139. package/docs/templates/init-managed-agents.md +1 -1
  140. package/package.json +3 -2
  141. package/skills/loop-agent/references/command-reference.md +15 -1
  142. package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
  144. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
  145. package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
  146. package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
  147. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
  148. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
@@ -296,13 +296,6 @@ function operationIdFromArgs(args) {
296
296
  const operationId = record?.operationId;
297
297
  return typeof operationId === "string" && operationId ? operationId : undefined;
298
298
  }
299
- function maxWaitSeconds(args) {
300
- const record = parseJsonRecord(args);
301
- const raw = record?.maxWaitMs;
302
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0)
303
- return undefined;
304
- return Math.max(1, Math.round(raw / 1000));
305
- }
306
299
  function activityRefForTool(tool) {
307
300
  // Plan A (2026-08-16): server backfills operationId on the persisted tool
308
301
  // entry (and live tool_result frames carry it in the result payload), so
@@ -313,6 +306,17 @@ function activityRefForTool(tool) {
313
306
  operationIdFromArgs(tool.resultText);
314
307
  return operationId ? `operation:${operationId}` : `tool:${tool.id}`;
315
308
  }
309
+ export function dagProgressActivityForTool(tool) {
310
+ if (tool.toolName !== "operationWait")
311
+ return undefined;
312
+ const activityRef = activityRefForTool(tool);
313
+ if (!activityRef.startsWith("operation:"))
314
+ return undefined;
315
+ return {
316
+ id: `dag-progress:${activityRef}`,
317
+ activityRef,
318
+ };
319
+ }
316
320
  function semanticKindForStatus(status) {
317
321
  if (status === "running")
318
322
  return "progress";
@@ -377,14 +381,11 @@ function nodeActivityFromMessages(messages, id, activityRef) {
377
381
  function operationWaitActivity(tool) {
378
382
  const activityRef = activityRefForTool(tool);
379
383
  const id = `activity:${tool.id}`;
380
- const seconds = maxWaitSeconds(tool.args);
381
384
  if (tool.status === "running") {
382
385
  return {
383
386
  id,
384
387
  kind: "progress",
385
- title: seconds
386
- ? `正在等待新的执行进展,最长等待时间 ${seconds} 秒`
387
- : "正在等待新的执行进展",
388
+ title: "正在等待新的执行进展",
388
389
  activityRef,
389
390
  };
390
391
  }
@@ -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;
@@ -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";
@@ -2,7 +2,7 @@
2
2
  * Turn grouping for Operator Chat inline process UX (pi-web ProcessDetails mind).
3
3
  */
4
4
  import { getDisplayableAssistantBlocks, joinAssistantText, parseAssistantContent, splitFinalAssistantBlocks, } from "./assistant-content.js";
5
- import { semanticActivityForTool, } from "./semantic-activity.js";
5
+ import { dagProgressActivityForTool, semanticActivityForTool, } from "./semantic-activity.js";
6
6
  function emptyGroup(key) {
7
7
  return {
8
8
  key,
@@ -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,17 +68,31 @@ 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}`;
75
+ // Prefer event-ring timestamps (tool_call at → tool_result at) so
76
+ // refreshed sessions keep real elapsed durations instead of 0ms.
77
+ const startedAtMs = item.startedAt ? Date.parse(item.startedAt) : NaN;
78
+ const finishedAtMs = item.finishedAt ? Date.parse(item.finishedAt) : NaN;
67
79
  const entry = {
68
80
  id: item.toolCallId || item.id,
69
81
  toolName: item.toolName || "tool",
70
82
  status: item.isError === true ? "error" : "ok",
71
83
  resultText: item.text,
72
84
  result: item.text,
73
- createdAt,
74
- finishedAt: createdAt,
75
- turnId,
85
+ createdAt: Number.isFinite(startedAtMs)
86
+ ? startedAtMs
87
+ : createdAt,
88
+ finishedAt: Number.isFinite(finishedAtMs)
89
+ ? finishedAtMs
90
+ : createdAt,
91
+ turnId: item.turnId ?? turnId,
92
+ ...(item.eventSeq === undefined ? {} : { eventSeq: item.eventSeq }),
93
+ ...(item.finishedSeq === undefined
94
+ ? {}
95
+ : { finishedSeq: item.finishedSeq }),
76
96
  ...(item.operationId ? { operationId: item.operationId } : {}),
77
97
  };
78
98
  toolCalls.push(entry);
@@ -106,6 +126,11 @@ function flushToolBuffer(processItems, buffer) {
106
126
  return;
107
127
  const sorted = sortToolsStable(buffer);
108
128
  for (const tool of sorted) {
129
+ const dagProgress = dagProgressActivityForTool(tool);
130
+ if (dagProgress) {
131
+ processItems.push({ kind: "dag-progress", progress: dagProgress });
132
+ continue;
133
+ }
109
134
  const activity = semanticActivityForTool(tool);
110
135
  if (activity) {
111
136
  processItems.push({ kind: "activity", activity });
@@ -115,6 +140,17 @@ function flushToolBuffer(processItems, buffer) {
115
140
  }
116
141
  }
117
142
  }
143
+ function collapseDagProgressItems(items) {
144
+ const seen = new Set();
145
+ return items.filter((item) => {
146
+ if (item.kind !== "dag-progress")
147
+ return true;
148
+ if (seen.has(item.progress.activityRef))
149
+ return false;
150
+ seen.add(item.progress.activityRef);
151
+ return true;
152
+ });
153
+ }
118
154
  function emitProcessBlocks(blocks, group, processItems, usedToolIds) {
119
155
  let buffer = [];
120
156
  for (const block of blocks) {
@@ -189,13 +225,15 @@ function fillProcessDetails(group) {
189
225
  const orphans = group.tools.filter((tool) => !usedToolIds.has(tool.id));
190
226
  if (orphans.length > 0)
191
227
  flushToolBuffer(processItems, orphans);
192
- group.processItems = processItems;
228
+ group.processItems = collapseDagProgressItems(processItems);
193
229
  if (!finalAssistant)
194
230
  return;
195
231
  if (blocks.length === 0) {
196
232
  group.answer = finalAssistant;
197
233
  return;
198
234
  }
235
+ if (split.answerBlocks.length === 0)
236
+ return;
199
237
  const answerText = joinAssistantText(split.answerBlocks);
200
238
  group.answer = {
201
239
  ...finalAssistant,
@@ -207,7 +245,22 @@ function fillProcessDetails(group) {
207
245
  * Build user → process → answer groups for the message timeline.
208
246
  */
209
247
  export function buildTurnProcessGroups(input) {
210
- const visible = input.messages.filter((m) => m.role === "user" || m.role === "assistant");
248
+ const visible = input.messages
249
+ .filter((m) => m.role === "user" || m.role === "assistant")
250
+ .sort((a, b) => {
251
+ if (a.eventSeq !== undefined &&
252
+ b.eventSeq !== undefined &&
253
+ a.eventSeq !== b.eventSeq) {
254
+ return a.eventSeq - b.eventSeq;
255
+ }
256
+ if (a.createdAt !== b.createdAt)
257
+ return a.createdAt - b.createdAt;
258
+ if (a.eventSeq !== undefined && b.eventSeq === undefined)
259
+ return 1;
260
+ if (a.eventSeq === undefined && b.eventSeq !== undefined)
261
+ return -1;
262
+ return 0;
263
+ });
211
264
  const groups = [];
212
265
  let current = null;
213
266
  const startGroup = (key) => {
@@ -176,8 +176,26 @@ export function deriveEligibilityFactsFromDagSpec(input) {
176
176
  }));
177
177
  const allWriteSetEntries = writers.flatMap((writer) => writer.writeSet);
178
178
  const broadWriteSetRisk = allWriteSetEntries.some(isBroadWriteSetEntry);
179
- const forbiddenOverlapRisk = tasks.some((task) => stringList(task.writeSet).some((entry) => [...stringList(task.forbiddenPaths), ...input.forbiddenPaths].some((forbidden) => pathMatchesPattern(entry, forbidden) ||
180
- pathMatchesPattern(forbidden, entry))));
179
+ const taskForbidden = input.forbiddenPaths.map(normalizePathEntry);
180
+ const forbiddenOverlapRisk = tasks.some((task) => stringList(task.writeSet).some((entry) => {
181
+ const normalizedEntry = normalizePathEntry(entry);
182
+ return stringList(task.forbiddenPaths).some((forbidden) => {
183
+ const normalizedForbidden = normalizePathEntry(forbidden);
184
+ const isTaskGlobal = taskForbidden.includes(normalizedForbidden);
185
+ if (isTaskGlobal) {
186
+ // Task-level forbidden paths are hard global boundaries. A broad
187
+ // writer that could include one is not autonomously eligible.
188
+ return (pathMatchesPattern(normalizedEntry, normalizedForbidden) ||
189
+ pathMatchesPattern(normalizedForbidden, normalizedEntry));
190
+ }
191
+ // Node-local forbidden paths may intentionally carve a narrower
192
+ // child (for example md/README.md) out of a broader writer
193
+ // writeSet. That exclusion is enforced by the runtime write guard.
194
+ // It is a conflict only when the writer entry itself is wholly
195
+ // inside the forbidden boundary.
196
+ return pathMatchesPattern(normalizedEntry, normalizedForbidden);
197
+ });
198
+ }));
181
199
  const hasStructuredVerification = tasks.some((task) => {
182
200
  if (task.executor !== "shell" || !task.shell)
183
201
  return false;
@@ -1148,14 +1148,14 @@ export async function dispatchOperatorAction(ctx, req) {
1148
1148
  if (usesUnifiedSurface) {
1149
1149
  const inspectPath = target
1150
1150
  ? buildInspectDeepLink(consoleOrigin, target).slice(consoleOrigin.length)
1151
- : "/inspect/#/";
1151
+ : "/#/inspect";
1152
1152
  return {
1153
1153
  kind: "sync",
1154
1154
  status: 200,
1155
1155
  body: operatorSucceeded("observeLink", {
1156
1156
  url: target
1157
1157
  ? buildInspectDeepLink(consoleOrigin, target)
1158
- : `${consoleOrigin.replace(/\/+$/, "")}/inspect/#/`,
1158
+ : `${consoleOrigin.replace(/\/+$/, "")}/#/inspect`,
1159
1159
  inspectPath,
1160
1160
  status: "same-origin",
1161
1161
  requiredCapability: requiredCapability ?? null,
@@ -1641,9 +1641,10 @@ export async function dispatchOperatorAction(ctx, req) {
1641
1641
  broadWriteSetRisk: typeof reviewPacket.broadWriteSetRisk === "boolean"
1642
1642
  ? reviewPacket.broadWriteSetRisk
1643
1643
  : derivedFacts.broadWriteSetRisk,
1644
- forbiddenOverlapRisk: typeof reviewPacket.forbiddenOverlapRisk === "boolean"
1645
- ? reviewPacket.forbiddenOverlapRisk
1646
- : derivedFacts.forbiddenOverlapRisk,
1644
+ // Always use the staged DAG + live task boundary derivation here.
1645
+ // The generation review packet lacks task-boundary scope and can
1646
+ // mistake a narrower node-local exclusion for a global overlap.
1647
+ forbiddenOverlapRisk: derivedFacts.forbiddenOverlapRisk,
1647
1648
  hasStructuredVerification: packetShellVerification,
1648
1649
  };
1649
1650
  const assessment = assessAutonomousExecutionEligibility(eligibility);
@@ -1816,6 +1817,18 @@ export async function dispatchOperatorAction(ctx, req) {
1816
1817
  const taskIdError = taskIdInputError(taskId);
1817
1818
  if (taskIdError)
1818
1819
  return invalid(action, taskIdError);
1820
+ // Optional write-set gate approval: the token is the server-issued
1821
+ // `<gate-id>:sha256:<hex>` that `task status` reports in
1822
+ // gate.approveCommand. Forwarded as --approve-gate so an operator can
1823
+ // complete the human approval through the Console operation path
1824
+ // instead of dropping to a raw terminal. This dispatch already sits
1825
+ // behind the Console mutation gate (boot cookie + confirmation
1826
+ // header), so it carries the same authority as typing the CLI flag.
1827
+ const approveGateRaw = str(p.approveGate);
1828
+ if (approveGateRaw &&
1829
+ !/^[A-Za-z0-9._-]+:sha256:[0-9a-f]{64}$/.test(approveGateRaw)) {
1830
+ return invalid(action, "approveGate must look like <gate-id>:sha256:<64-hex> (copy the token from task status gate.approveCommand)");
1831
+ }
1819
1832
  const args = ["task", "advance", taskId];
1820
1833
  const title = str(p.title);
1821
1834
  if (title)
@@ -1829,6 +1842,8 @@ export async function dispatchOperatorAction(ctx, req) {
1829
1842
  const profile = str(p.profile);
1830
1843
  if (profile)
1831
1844
  args.push("--profile", profile);
1845
+ if (approveGateRaw)
1846
+ args.push("--approve-gate", approveGateRaw);
1832
1847
  appendEngineeringCliArgs(args, parseEngineeringBoundaryFromParams(p));
1833
1848
  args.push("--json");
1834
1849
  return acceptOperation(ctx, {
@@ -27,7 +27,11 @@ export function encodeInspectHash(selection) {
27
27
  }
28
28
  }
29
29
  export function buildInspectPath(selection) {
30
- return `/inspect/${selection ? encodeInspectHash(selection) : "#/"}`;
30
+ if (!selection)
31
+ return "/#/inspect";
32
+ const hash = encodeInspectHash(selection);
33
+ const internal = hash.startsWith("#") ? hash.slice(1) : hash;
34
+ return `/#/inspect${internal}`;
31
35
  }
32
36
  export function buildInspectUrl(origin, selection) {
33
37
  const base = origin.trim().replace(/\/+$/, "");
@@ -99,7 +103,7 @@ export function selectionFromObserveLinkTarget(target) {
99
103
  export function normalizeLegacyConsoleDagHash(pathnameAndHash) {
100
104
  const [pathname, hash = ""] = pathnameAndHash.split("#", 2);
101
105
  if ((pathname === "/" || pathname === "") && /^\/dag\/[^/]+$/.test(hash)) {
102
- return `/inspect/#${hash}`;
106
+ return `/#/inspect${hash}`;
103
107
  }
104
108
  return pathnameAndHash;
105
109
  }