@tea-agent/loop-agent 0.36.4-beta.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +50 -68
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/infrastructure/harness/atomic-write.js +1 -2
  4. package/dist/task/contract/project.js +4 -0
  5. package/dist/task/frontend-project-capability.js +12 -162
  6. package/dist/task/source-prepare/completeness.js +36 -0
  7. package/dist/task/source-prepare/parse-intent.js +67 -11
  8. package/dist/task/source-prepare/prepare.js +102 -5
  9. package/dist/worker/console/chat/chat-event-store.js +91 -0
  10. package/dist/worker/console/chat/pi-runtime.js +25 -0
  11. package/dist/worker/console/chat/routes.js +7 -2
  12. package/dist/worker/console/chat/semantic-activity.js +7 -1
  13. package/dist/worker/console/chat/session-store.js +6 -1
  14. package/dist/worker/console/chat/turn-process.js +119 -12
  15. package/dist/worker/console/operator-actions.js +18 -1
  16. package/dist/worker/console/static/assets/abnfDiagram-N423BO3Z-DSvF6RXQ.js +1 -0
  17. package/dist/worker/console/static/assets/arc-DuI4ogtJ.js +1 -0
  18. package/dist/worker/console/static/assets/architectureDiagram-T3A2C74G-DdBbgD4y.js +36 -0
  19. package/dist/worker/console/static/assets/blockDiagram-VBNYF7ZC-xQC2HELm.js +132 -0
  20. package/dist/worker/console/static/assets/c4Diagram-5PPSVZJV-DPMSgwIZ.js +10 -0
  21. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +1 -0
  22. package/dist/worker/console/static/assets/chunk-2GRJ4B5K-WTEKTxin.js +1 -0
  23. package/dist/worker/console/static/assets/chunk-2Q5K7J3B-B6vZ0Pv_.js +1 -0
  24. package/dist/worker/console/static/assets/chunk-5RXB4S5H-BrerNAix.js +231 -0
  25. package/dist/worker/console/static/assets/chunk-5VM5RSS4-D_ch2aaI.js +15 -0
  26. package/dist/worker/console/static/assets/chunk-6Q2QTUOP-VvfacNrq.js +88 -0
  27. package/dist/worker/console/static/assets/chunk-GF5L2VYU-B-uGFIg2.js +206 -0
  28. package/dist/worker/console/static/assets/chunk-JWPE2WC7-BlysCtod.js +1 -0
  29. package/dist/worker/console/static/assets/chunk-KBJHAD2P-BjAUAfcT.js +1 -0
  30. package/dist/worker/console/static/assets/chunk-RYQCIY6F-BeZnBmb-.js +1 -0
  31. package/dist/worker/console/static/assets/chunk-XXDRQBXY-Bbfe3UJG.js +1 -0
  32. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +1 -0
  33. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +1 -0
  34. package/dist/worker/console/static/assets/cose-bilkent-JH36ORCC-BKJQnYVM.js +1 -0
  35. package/dist/worker/console/static/assets/cynefin-VYW2F7L2-B1wtI3hU.js +166 -0
  36. package/dist/worker/console/static/assets/cynefinDiagram-MW4NZA55-DCF6DBSu.js +62 -0
  37. package/dist/worker/console/static/assets/cytoscape.esm-yzknjiTM.js +321 -0
  38. package/dist/worker/console/static/assets/dagre-VZM6K2ZE-BJyNGRlP.js +4 -0
  39. package/dist/worker/console/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  40. package/dist/worker/console/static/assets/diagram-7IWD3JNH-B7j1-VsZ.js +30 -0
  41. package/dist/worker/console/static/assets/diagram-B4RE2ZJO-BREJ140a.js +3 -0
  42. package/dist/worker/console/static/assets/diagram-LBJQPF4R-CDPHmHGH.js +24 -0
  43. package/dist/worker/console/static/assets/diagram-Q27KOJAE-BtZLrez1.js +24 -0
  44. package/dist/worker/console/static/assets/diagram-UB23O5K3-mzybu8oU.js +41 -0
  45. package/dist/worker/console/static/assets/ebnfDiagram-BXEA7PRR-Dk3I2uO6.js +1 -0
  46. package/dist/worker/console/static/assets/erDiagram-JOGREHBK-BT97u_tQ.js +85 -0
  47. package/dist/worker/console/static/assets/flowDiagram-UKHOOZJN-CMOtYzui.js +156 -0
  48. package/dist/worker/console/static/assets/ganttDiagram-PKOTCBZU-DiNydkuY.js +292 -0
  49. package/dist/worker/console/static/assets/gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js +106 -0
  50. package/dist/worker/console/static/assets/graph-DOmOIIwC.js +1 -0
  51. package/dist/worker/console/static/assets/index-DQxiso1w.js +325 -0
  52. package/dist/worker/console/static/assets/index-_j0Hzo69.css +1 -0
  53. package/dist/worker/console/static/assets/infoDiagram-6WML65LV-AsGIGKKB.js +2 -0
  54. package/dist/worker/console/static/assets/init-Gi6I4Gst.js +1 -0
  55. package/dist/worker/console/static/assets/ishikawaDiagram-WSZJBQD7-BL8ITZh3.js +70 -0
  56. package/dist/worker/console/static/assets/journeyDiagram-NVQOT4AX-VPKAUX8M.js +139 -0
  57. package/dist/worker/console/static/assets/kanban-definition-27J2QSJJ-BL56hfGp.js +89 -0
  58. package/dist/worker/console/static/assets/layout-D-LzfAck.js +1 -0
  59. package/dist/worker/console/static/assets/linear-BmSG3nIZ.js +1 -0
  60. package/dist/worker/console/static/assets/map-DxJ2ADlA.js +1 -0
  61. package/dist/worker/console/static/assets/mermaid.core-DWGK9kwP.js +308 -0
  62. package/dist/worker/console/static/assets/mindmap-definition-FAOFIHXS--vvKLjo0.js +96 -0
  63. package/dist/worker/console/static/assets/ordinal-Cboi1Yqb.js +1 -0
  64. package/dist/worker/console/static/assets/pegDiagram-VL7TDLO6-B41mxb2-.js +1 -0
  65. package/dist/worker/console/static/assets/pieDiagram-7S7Q4E2Y-DTlu77sw.js +39 -0
  66. package/dist/worker/console/static/assets/quadrantDiagram-CIZ2JOQS-Bk5A7alv.js +7 -0
  67. package/dist/worker/console/static/assets/railroadDiagram-AXF67PYL-Csf9jzQy.js +1 -0
  68. package/dist/worker/console/static/assets/requirementDiagram-LRYGKXZP-CeptoSNx.js +84 -0
  69. package/dist/worker/console/static/assets/sankeyDiagram-W5VNT64P-A-DLCLer.js +40 -0
  70. package/dist/worker/console/static/assets/sequenceDiagram-SI44F4Z6-22VbPdOI.js +162 -0
  71. package/dist/worker/console/static/assets/sizeCapture-X5ZJPWSS-DWvAyBC9.js +1 -0
  72. package/dist/worker/console/static/assets/stateDiagram-OKZ733FA-qICZ75yT.js +1 -0
  73. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +1 -0
  74. package/dist/worker/console/static/assets/swimlanes-SLNWSIFB-xi7lcMyp.js +2 -0
  75. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +8 -0
  76. package/dist/worker/console/static/assets/timeline-definition-Z64GVDOM-Bu6Nl6O2.js +120 -0
  77. package/dist/worker/console/static/assets/vennDiagram-T6HMQDX7-BzMz4Bn9.js +34 -0
  78. package/dist/worker/console/static/assets/wardleyDiagram-T6FBY63Y-CxaJnzc5.js +78 -0
  79. package/dist/worker/console/static/assets/xychartDiagram-ELKLHX3M-5JK-omQK.js +7 -0
  80. package/dist/worker/console/static/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
  81. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
  82. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
  83. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
  84. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
  85. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
  86. package/dist/worker/console/static/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
  87. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
  88. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
  89. package/dist/worker/console/static/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
  90. package/dist/worker/console/static/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
  91. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
  92. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
  93. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
  94. package/dist/worker/console/static/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
  95. package/dist/worker/console/static/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
  96. package/dist/worker/console/static/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
  97. package/dist/worker/console/static/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
  98. package/dist/worker/console/static/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
  99. package/dist/worker/console/static/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
  100. package/dist/worker/console/static/index.html +2 -2
  101. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +25 -0
  102. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +37 -2
  103. package/dist/worker/observe/static/operator-chrome.js +4 -2
  104. package/dist/workflows/dag/failure-routing.js +4 -9
  105. package/dist/workflows/dag/frontend-prewrite-gate.js +8 -37
  106. package/dist/workflows/dag/init-hybrid.js +4 -73
  107. package/dist/workflows/dag/lifecycle.js +0 -4
  108. package/dist/workflows/dag/node-execution.js +6 -16
  109. package/dist/workflows/dag/report.js +0 -6
  110. package/dist/workflows/dag/retry-policy.js +0 -11
  111. package/dist/workflows/dag/runner.js +19 -3
  112. package/dist/workflows/dag/types.js +0 -2
  113. package/dist/workflows/dag/validate.js +2 -7
  114. package/docs/templates/frontend-design-contract.md +14 -34
  115. package/docs/templates/frontend-task-constraints.md +13 -33
  116. package/docs/templates/frontend-task-requirement.md +20 -54
  117. package/package.json +6 -1
  118. package/skills/loop-agent/references/command-reference.md +1 -0
  119. package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
  120. package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { lstat, readFile } from "node:fs/promises";
3
+ import path from "node:path";
3
4
  import { ContractMutationError, applyTaskContract, logicalRevisionForState, observeTaskContractState, projectConstraintsMarkdown, projectRequirementMarkdown, sha256OfCanonicalJson, validateDraftForTask, } from "../contract/index.js";
4
5
  import { TASK_CONTRACT_DRAFT_SCHEMA_VERSION } from "../contract/constants.js";
5
6
  import { getTaskContractPaths } from "../contract/paths.js";
@@ -28,26 +29,114 @@ async function loadExistingTaskConfig(repoRoot, taskId) {
28
29
  return null;
29
30
  }
30
31
  }
32
+ /**
33
+ * Check whether a "## References" section in a managed 需求.md matches the
34
+ * projector's canonical form: every entry is `- [role] <manifest-path>` and
35
+ * every path exists in source-manifest.json. The projector itself writes
36
+ * this section, so matching entries are the writer's own round-trip output
37
+ * and must be accepted (2026-08-16 L1). Unknown/free-form entries stay
38
+ * blocking — they indicate hand-edited provenance.
39
+ */
40
+ function isProjectedReferencesSection(sectionBody, manifestPaths) {
41
+ const entryRe = /^- \[(?<role>[^\]]+)\] (?<ref>\S.*)$/;
42
+ const lines = sectionBody
43
+ .split(/\r?\n/)
44
+ .map((line) => line.trim())
45
+ .filter(Boolean);
46
+ if (lines.length === 0)
47
+ return true; // empty section: nothing to reject
48
+ return lines.every((line) => {
49
+ const match = entryRe.exec(line);
50
+ if (!match?.groups)
51
+ return false;
52
+ const ref = match.groups.ref?.replace(/\\/g, "/").trim() ?? "";
53
+ return manifestPaths.has(ref);
54
+ });
55
+ }
56
+ /**
57
+ * Source Resolutions canonical projector shape:
58
+ * `- <sourceRef>: <conflict> → <decision> (<reason>; decidedBy=<who>)`.
59
+ */
60
+ function isProjectedSourceResolutionsSection(sectionBody) {
61
+ const entryRe = /^- \S[^:]*: .+ → .+ \(.+; decidedBy=.+\)$/u;
62
+ const lines = sectionBody
63
+ .split(/\r?\n/)
64
+ .map((line) => line.trim())
65
+ .filter(Boolean);
66
+ if (lines.length === 0)
67
+ return true;
68
+ return lines.every((line) => entryRe.test(line));
69
+ }
70
+ /** Section titles the read-back validator may inspect (static whitelist). */
71
+ const MANAGED_PROJECTION_SECTION_TITLES = [
72
+ "References",
73
+ "Source Resolutions",
74
+ ];
75
+ /** Extract the body of the first `## <Title>` section, if present. */
76
+ function extractSectionBody(markdown, title) {
77
+ const re = new RegExp(`^##\\s+${title}\\s*$`, "im");
78
+ const match = re.exec(markdown);
79
+ if (!match)
80
+ return null;
81
+ const rest = markdown.slice(match.index + match[0].length);
82
+ const nextHeading = /^##\s+/m.exec(rest);
83
+ return nextHeading ? rest.slice(0, nextHeading.index) : rest;
84
+ }
85
+ /** Materialized paths registered in the task's source-manifest.json. */
86
+ async function loadSourceManifestPaths(requirementPath) {
87
+ const manifestPath = path.join(path.dirname(requirementPath), "source-manifest.json");
88
+ try {
89
+ const raw = JSON.parse(await readFile(manifestPath, "utf-8"));
90
+ return new Set((raw.documents ?? [])
91
+ .map((doc) => doc.materializedPath?.replace(/\\/g, "/"))
92
+ .filter((p) => Boolean(p)));
93
+ }
94
+ catch {
95
+ return new Set();
96
+ }
97
+ }
31
98
  async function loadManagedProjectionDraft(input) {
32
99
  const config = input.existingTaskConfig;
33
100
  if (!config) {
34
101
  return { ok: false, message: "managed task config could not be loaded" };
35
102
  }
36
103
  try {
37
- const [requirement, constraints] = await Promise.all([
104
+ const [requirementRaw, constraints] = await Promise.all([
38
105
  readFile(input.requirementPath, "utf-8"),
39
106
  readFile(input.constraintsPath, "utf-8"),
40
107
  ]);
41
- const requirementFacts = extractRequirementFactsFromMarkdown(requirement);
42
- const constraintFacts = extractRequirementFactsFromMarkdown(constraints);
43
- if (/^##\s+(?:References|Source Resolutions)\s*$/im.test(requirement)) {
108
+ // L1 (2026-08-16): the requirement projector itself writes `## References`
109
+ // (from source-manifest) and `## Source Resolutions` sections into
110
+ // 需求.md. The read-back validator used to reject ANY such section —
111
+ // writer/reader contract drift that deadlocked every PRD import whose
112
+ // draft carried references. Now accept the projector's own canonical
113
+ // forms: References entries must all be `- [role] <path>` with paths
114
+ // present in source-manifest.json; Source Resolutions entries must
115
+ // match the projector's `- ref: conflict → decision (...)` shape.
116
+ // Free-form/hand-edited entries remain blocking.
117
+ const manifest = await loadSourceManifestPaths(input.requirementPath);
118
+ const referencesBody = extractSectionBody(requirementRaw, "References");
119
+ if (referencesBody !== null &&
120
+ !isProjectedReferencesSection(referencesBody, manifest)) {
121
+ return {
122
+ ok: false,
123
+ message: "managed requirement projection contains hand-edited or unknown References entries (not backed by source-manifest); use loop-agent task advance " +
124
+ input.taskId +
125
+ " --from-draft <reviewed-draft.json> --json",
126
+ };
127
+ }
128
+ const sourceResolutionsBody = extractSectionBody(requirementRaw, "Source Resolutions");
129
+ if (sourceResolutionsBody !== null &&
130
+ !isProjectedSourceResolutionsSection(sourceResolutionsBody)) {
44
131
  return {
45
132
  ok: false,
46
- message: "managed requirement projection contains unsupported References or Source Resolutions; use loop-agent task advance " +
133
+ message: "managed requirement projection contains hand-edited or malformed Source Resolutions entries; use loop-agent task advance " +
47
134
  input.taskId +
48
135
  " --from-draft <reviewed-draft.json> --json",
49
136
  };
50
137
  }
138
+ const requirementFacts = extractRequirementFactsFromMarkdown(requirementRaw);
139
+ const constraintFacts = extractRequirementFactsFromMarkdown(constraints);
51
140
  if (!requirementFacts.objective?.trim()) {
52
141
  return {
53
142
  ok: false,
@@ -517,6 +606,14 @@ export async function prepareTaskSource(input) {
517
606
  code: "MANAGED_PROJECTION_INCOMPLETE",
518
607
  level: "blocking",
519
608
  message: managedProjectionError,
609
+ selfHealing: {
610
+ action: "adopt-draft",
611
+ steps: [
612
+ "task status --json 读取 contract.observedCanonicalHash / observedSourceHashes,确认现有源文件状态",
613
+ "从 Console drafts(<app-data>/repos/<repo>/drafts/<taskId>.json)或 semantic intake 产物(artifacts/intake/semantic-draft.json 的 draft 字段)取回结构化 draft",
614
+ "补充缺失字段(scope/invariants 等)后:task advance --from-draft <reviewed-draft.json> --json",
615
+ ],
616
+ },
520
617
  });
521
618
  }
522
619
  // If --use-imported-prd and no parseable requirement and no AC from flags/text → blocking
@@ -152,6 +152,75 @@ export function projectOperationForChat(operation) {
152
152
  previewHash,
153
153
  };
154
154
  }
155
+ /** Action families that leave a user-inspectable durable outcome. */
156
+ const ARTIFACT_EMITTING_ACTIONS = new Set([
157
+ "taskAdvance",
158
+ "taskCreate",
159
+ "contractApply",
160
+ "dagReport",
161
+ "taskImportPrd",
162
+ "importPrd",
163
+ ]);
164
+ /**
165
+ * Project a terminal operation into an「产物」rail card (2026-08-16).
166
+ * Mirrors `ArtifactCardState` (schemaVersion 1) so the existing frontend
167
+ * parser renders it without any change. Best-effort: operations without a
168
+ * durable outcome (pure status reads) return undefined.
169
+ */
170
+ function artifactFromTerminalOperation(operation, projection) {
171
+ if (!ARTIFACT_EMITTING_ACTIONS.has(operation.action))
172
+ return undefined;
173
+ // Failed/interrupted actions leave nothing inspectable as a deliverable.
174
+ if (operation.state !== "succeeded")
175
+ return undefined;
176
+ const taskId = projection.taskId;
177
+ const kind = taskId
178
+ ? "contract"
179
+ : "operation";
180
+ const title = taskId ? `任务 ${taskId}` : `执行 ${operation.action}`;
181
+ const summary = projection.runSummary?.status
182
+ ? `运行 ${projection.runSummary.runId} · ${projection.runSummary.status}`
183
+ : projection.summaryPreview || undefined;
184
+ const reportLines = [];
185
+ if (projection.runSummary) {
186
+ const rs = projection.runSummary;
187
+ reportLines.push(`运行: ${rs.runId}`);
188
+ reportLines.push(`状态: ${rs.status}`);
189
+ if (rs.verdict)
190
+ reportLines.push(`结论: ${rs.verdict}`);
191
+ if (rs.failureCategory)
192
+ reportLines.push(`失败归类: ${rs.failureCategory}`);
193
+ if (Array.isArray(rs.nodes)) {
194
+ for (const node of rs.nodes.slice(0, 30)) {
195
+ reportLines.push(`- ${node.id}: ${node.status}`);
196
+ }
197
+ }
198
+ }
199
+ if (projection.stdoutPreview) {
200
+ reportLines.push("", "--- stdout 摘要 ---", projection.stdoutPreview);
201
+ }
202
+ const previewHash = createHash("sha256")
203
+ .update(JSON.stringify({
204
+ operationId: operation.operationId,
205
+ state: operation.state,
206
+ taskId: taskId ?? null,
207
+ runSummary: projection.runSummary ?? null,
208
+ stdoutPreview: projection.stdoutPreview ?? null,
209
+ }))
210
+ .digest("hex");
211
+ return {
212
+ schemaVersion: 1,
213
+ kind,
214
+ artifactId: `operation-${operation.operationId}`,
215
+ title,
216
+ ...(summary ? { summary } : {}),
217
+ ...(reportLines.length > 0
218
+ ? { reportText: reportLines.join("\n").slice(0, 8000) }
219
+ : {}),
220
+ inspectHref: projection.inspectHref,
221
+ previewHash,
222
+ };
223
+ }
155
224
  export function projectCompactSnapshot(value) {
156
225
  const record = value && typeof value === "object"
157
226
  ? value
@@ -254,6 +323,7 @@ export function projectChatToolEventPayload(input) {
254
323
  resultPreview: preview,
255
324
  previewHash,
256
325
  isError: Boolean(input.isError),
326
+ ...(input.operationId ? { operationId: input.operationId } : {}),
257
327
  };
258
328
  }
259
329
  export function projectOperationEventSummary(event) {
@@ -411,6 +481,27 @@ export function createChatOperationLinker(options) {
411
481
  },
412
482
  });
413
483
  }
484
+ // Artifact projection (2026-08-16): the sidebar「产物」tab was permanently
485
+ // empty because nothing in the server ever emitted `artifact-ref`. On a
486
+ // terminal operation with a durable outcome (task created / advanced /
487
+ // contract applied / report produced) project one artifact card so the
488
+ // user can inspect the deliverable straight from the rail.
489
+ if (isTerminalOperationState(operation.state)) {
490
+ const artifact = artifactFromTerminalOperation(operation, projection);
491
+ if (artifact) {
492
+ const artifactDuplicate = options.chatEvents
493
+ .snapshot(input.sessionId)
494
+ .some((event) => event.kind === "artifact-ref" &&
495
+ event.data
496
+ .artifact?.previewHash === artifact.previewHash);
497
+ if (!artifactDuplicate) {
498
+ options.chatEvents.append(input.sessionId, input.turnId, {
499
+ kind: "artifact-ref",
500
+ data: { artifact },
501
+ });
502
+ }
503
+ }
504
+ }
414
505
  if (isTerminalOperationState(operation.state)) {
415
506
  const key = keyFor(input.sessionId, input.operationId);
416
507
  followers.get(key)?.();
@@ -1963,6 +1963,31 @@ export class ConsolePiRuntime {
1963
1963
  if (usage)
1964
1964
  onEvent({ type: "usage", sessionId, usage });
1965
1965
  }
1966
+ // Context usage rides the usage event: refresh on agent_settled so the
1967
+ // bar reflects the post-turn context window fill (pi-web parity).
1968
+ if (event.type === "agent_settled") {
1969
+ try {
1970
+ const stats = session.getSessionStats?.();
1971
+ const contextUsage = stats?.contextUsage;
1972
+ if (contextUsage) {
1973
+ onEvent({
1974
+ type: "usage",
1975
+ sessionId,
1976
+ usage: {
1977
+ inputTokens: 0,
1978
+ outputTokens: 0,
1979
+ totalTokens: 0,
1980
+ ...(contextUsage.tokens === null
1981
+ ? {}
1982
+ : { contextUsage }),
1983
+ },
1984
+ });
1985
+ }
1986
+ }
1987
+ catch {
1988
+ // Stats getter is best-effort; never break the event stream.
1989
+ }
1990
+ }
1966
1991
  });
1967
1992
  options?.signal?.addEventListener("abort", onAbort);
1968
1993
  if (options?.signal?.aborted) {
@@ -15,7 +15,7 @@
15
15
  import { parseAssistantContent } from "./assistant-content.js";
16
16
  import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
17
17
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
18
- import { cleanChatTitle } from "./session-store.js";
18
+ import { cleanChatTitle, acceptedOperationIdFromResult } from "./session-store.js";
19
19
  import { hashChatTurnPayload, projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
20
20
  import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime.js";
21
21
  import { ResourcePreferencesError, setSkillAutoInvocationPreference, } from "./resource-preferences-store.js";
@@ -106,7 +106,10 @@ function turnEventToStorePartial(event) {
106
106
  value: event.args,
107
107
  }),
108
108
  };
109
- case "tool_result":
109
+ case "tool_result": {
110
+ // Plan A (2026-08-16): surface the accepted-operation link on the live
111
+ // SSE frame so the frontend semantic event can resolve operation:<id>.
112
+ const resultOperationId = acceptedOperationIdFromResult(String(event.toolName ?? ""), event.result);
110
113
  return {
111
114
  kind: "tool_result",
112
115
  data: projectChatToolEventPayload({
@@ -115,8 +118,10 @@ function turnEventToStorePartial(event) {
115
118
  toolName: String(event.toolName ?? ""),
116
119
  value: event.result,
117
120
  isError: Boolean(event.isError),
121
+ ...(resultOperationId ? { operationId: resultOperationId } : {}),
118
122
  }),
119
123
  };
124
+ }
120
125
  case "agent_end":
121
126
  return {
122
127
  kind: "agent_end",
@@ -292,7 +292,13 @@ function maxWaitSeconds(args) {
292
292
  return Math.max(1, Math.round(raw / 1000));
293
293
  }
294
294
  function activityRefForTool(tool) {
295
- const operationId = operationIdFromArgs(tool.args);
295
+ // Plan A (2026-08-16): server backfills operationId on the persisted tool
296
+ // entry (and live tool_result frames carry it in the result payload), so
297
+ // prefer the authoritative link before parsing args/result fallbacks.
298
+ const operationId = tool.operationId ??
299
+ operationIdFromArgs(tool.args) ??
300
+ operationIdFromArgs(tool.result) ??
301
+ operationIdFromArgs(tool.resultText);
296
302
  return operationId ? `operation:${operationId}` : `tool:${tool.id}`;
297
303
  }
298
304
  function semanticKindForStatus(status) {
@@ -402,6 +402,11 @@ export async function runChatTurn(options) {
402
402
  toolCallId: event.toolCallId,
403
403
  toolName: event.toolName,
404
404
  isError: event.isError,
405
+ // Durable tool→operation link (plan A): the restored timeline can
406
+ // resolve operation:<id> refs without the live operation-ref event.
407
+ ...(acceptedOperationId
408
+ ? { operationId: acceptedOperationId }
409
+ : {}),
405
410
  }));
406
411
  pending.push(store.recordToolInvocation(sessionId, {
407
412
  toolCallId: event.toolCallId,
@@ -431,7 +436,7 @@ export async function runChatTurn(options) {
431
436
  * huge operator-action payload (e.g. a full dagReport) cannot bloat the chat
432
437
  * history record.
433
438
  */
434
- function acceptedOperationIdFromResult(toolName, result) {
439
+ export function acceptedOperationIdFromResult(toolName, result) {
435
440
  const candidates = [result];
436
441
  if (result && typeof result === "object") {
437
442
  const record = result;
@@ -70,6 +70,7 @@ export function hydrateSessionThread(raw) {
70
70
  createdAt,
71
71
  finishedAt: createdAt,
72
72
  turnId,
73
+ ...(item.operationId ? { operationId: item.operationId } : {}),
73
74
  };
74
75
  toolCalls.push(entry);
75
76
  pendingTools.push(entry);
@@ -77,30 +78,89 @@ export function hydrateSessionThread(raw) {
77
78
  }
78
79
  return { messages, toolCalls };
79
80
  }
80
- function pushToolProcessItem(processItems, tool) {
81
- const activity = semanticActivityForTool(tool);
82
- if (activity)
83
- processItems.push({ kind: "activity", activity });
84
- else
85
- processItems.push({ kind: "tool", tool });
81
+ /** Stable tool order: server eventSeq first, createdAt fallback, id tiebreak. */
82
+ function sortToolsStable(tools) {
83
+ return [...tools].sort((a, b) => {
84
+ if (a.eventSeq !== undefined &&
85
+ b.eventSeq !== undefined &&
86
+ a.eventSeq !== b.eventSeq) {
87
+ return a.eventSeq - b.eventSeq;
88
+ }
89
+ if (a.createdAt !== b.createdAt)
90
+ return a.createdAt - b.createdAt;
91
+ return a.id < b.id ? -1 : 1;
92
+ });
93
+ }
94
+ /**
95
+ * Flush buffered consecutive tools into process items. Semantic operator
96
+ * tools stay as individual activity events; plain tools group into a
97
+ * `tool-group` ("并行调用 · N 项") when two or more land back to back.
98
+ * Thinking/text/activity boundaries break the buffer upstream.
99
+ */
100
+ function flushToolBuffer(processItems, buffer) {
101
+ if (buffer.length === 0)
102
+ return;
103
+ const sorted = sortToolsStable(buffer);
104
+ let plainGroup = [];
105
+ const flushPlain = () => {
106
+ if (plainGroup.length === 1) {
107
+ processItems.push({ kind: "tool", tool: plainGroup[0] });
108
+ }
109
+ else if (plainGroup.length > 1) {
110
+ processItems.push({ kind: "tool-group", tools: plainGroup });
111
+ }
112
+ plainGroup = [];
113
+ };
114
+ for (const tool of sorted) {
115
+ const activity = semanticActivityForTool(tool);
116
+ if (activity) {
117
+ flushPlain();
118
+ processItems.push({ kind: "activity", activity });
119
+ }
120
+ else {
121
+ plainGroup.push(tool);
122
+ }
123
+ }
124
+ flushPlain();
86
125
  }
87
126
  function emitProcessBlocks(blocks, group, processItems, usedToolIds) {
127
+ let buffer = [];
88
128
  for (const block of blocks) {
89
129
  if (block.type === "thinking") {
130
+ flushToolBuffer(processItems, buffer);
131
+ buffer = [];
90
132
  processItems.push({ kind: "thinking", thinking: block.thinking });
91
133
  continue;
92
134
  }
93
135
  if (block.type === "text") {
94
- if (block.text.trim())
136
+ flushToolBuffer(processItems, buffer);
137
+ buffer = [];
138
+ if (block.text.trim()) {
95
139
  processItems.push({ kind: "text", text: block.text });
140
+ }
96
141
  continue;
97
142
  }
98
143
  const tool = group.tools.find((entry) => entry.id === block.toolCallId);
99
- if (tool) {
100
- pushToolProcessItem(processItems, tool);
144
+ if (tool && !usedToolIds.has(tool.id)) {
145
+ buffer.push(tool);
101
146
  usedToolIds.add(tool.id);
102
147
  }
103
148
  }
149
+ flushToolBuffer(processItems, buffer);
150
+ }
151
+ /**
152
+ * Insert tools attributed to this assistant stage but without a matching
153
+ * content toolCall block (live SSE tool_call frames often arrive before the
154
+ * assistant content does). They follow the stage's thinking/text instead of
155
+ * sinking to the end of the whole turn.
156
+ */
157
+ function appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds) {
158
+ const unmatched = group.tools.filter((tool) => !usedToolIds.has(tool.id) && tool.assistantMessageId === assistant.id);
159
+ if (unmatched.length === 0)
160
+ return;
161
+ for (const tool of unmatched)
162
+ usedToolIds.add(tool.id);
163
+ flushToolBuffer(processItems, unmatched);
104
164
  }
105
165
  function fillProcessDetails(group) {
106
166
  const usedToolIds = new Set();
@@ -109,14 +169,20 @@ function fillProcessDetails(group) {
109
169
  const finalAssistant = group.assistants.at(-1);
110
170
  for (const assistant of prior) {
111
171
  emitProcessBlocks(getDisplayableAssistantBlocks(assistant.content ?? []), group, processItems, usedToolIds);
172
+ appendAssistantUnmatchedTools(group, assistant, processItems, usedToolIds);
112
173
  }
113
174
  const blocks = finalAssistant?.content ?? [];
114
175
  const split = splitFinalAssistantBlocks(blocks, { isStreaming: group.live });
115
176
  emitProcessBlocks(split.processBlocks, group, processItems, usedToolIds);
116
- for (const tool of group.tools) {
117
- if (!usedToolIds.has(tool.id))
118
- pushToolProcessItem(processItems, tool);
177
+ if (finalAssistant) {
178
+ appendAssistantUnmatchedTools(group, finalAssistant, processItems, usedToolIds);
119
179
  }
180
+ // Bounded fallback for legacy restored rows with no assistant attribution:
181
+ // keep them at this turn's end in stable server order instead of randomly
182
+ // interleaving them into earlier stages.
183
+ const orphans = group.tools.filter((tool) => !usedToolIds.has(tool.id));
184
+ if (orphans.length > 0)
185
+ flushToolBuffer(processItems, orphans);
120
186
  group.processItems = processItems;
121
187
  if (!finalAssistant)
122
188
  return;
@@ -149,6 +215,14 @@ export function buildTurnProcessGroups(input) {
149
215
  current.user = message;
150
216
  continue;
151
217
  }
218
+ if (message.kind === "compaction") {
219
+ // Compaction is a standalone timeline event. Starting a fresh group
220
+ // prevents it from replacing the previous turn's assistant answer.
221
+ current = startGroup(`compaction-${message.id}`);
222
+ current.assistants.push(message);
223
+ current = null;
224
+ continue;
225
+ }
152
226
  if (!current)
153
227
  current = startGroup(`assistant-${message.id}`);
154
228
  current.assistants.push(message);
@@ -250,3 +324,36 @@ export function processGroupSummary(tools, options) {
250
324
  parts.push("运行中");
251
325
  return { count, errorCount, runningCount, stepCount, label: parts.join(" · ") };
252
326
  }
327
+ /** Summary over rendered process items (tool-group tools count individually). */
328
+ export function summarizeProcessItems(items) {
329
+ const tools = [];
330
+ let thinkingCount = 0;
331
+ let textCount = 0;
332
+ let activityCount = 0;
333
+ let activityErrorCount = 0;
334
+ let activityRunningCount = 0;
335
+ for (const item of items) {
336
+ if (item.kind === "thinking")
337
+ thinkingCount += 1;
338
+ else if (item.kind === "text")
339
+ textCount += 1;
340
+ else if (item.kind === "activity") {
341
+ activityCount += 1;
342
+ if (item.activity.kind === "error")
343
+ activityErrorCount += 1;
344
+ else if (item.activity.kind === "progress")
345
+ activityRunningCount += 1;
346
+ }
347
+ else if (item.kind === "tool")
348
+ tools.push(item.tool);
349
+ else
350
+ tools.push(...item.tools);
351
+ }
352
+ return processGroupSummary(tools, {
353
+ thinkingCount,
354
+ textCount,
355
+ activityCount,
356
+ activityErrorCount,
357
+ activityRunningCount,
358
+ });
359
+ }
@@ -1,7 +1,7 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import { buildOperatorCapabilitiesDocument } from "../../shared/operator/capabilities.js";
3
3
  import { buildOperatorResult, operatorFailed, operatorSucceeded, } from "../../shared/operator/envelope.js";
4
- import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
4
+ import { CANONICAL_TASK_ID_PATTERN, formatLocalBusinessDate, getTaskPaths, loadTaskConfig, } from "../../task/runtime.js";
5
5
  import { diffTaskContract, validateDraftForTask, } from "../../task/contract/index.js";
6
6
  import { stageUtf8Text } from "./app-data.js";
7
7
  import { ALL_CONFIRMATION_CHALLENGES, hashDagBytes, } from "./dag-confirmation.js";
@@ -63,6 +63,8 @@ function str(v) {
63
63
  return typeof v === "string" && v.trim() ? v.trim() : undefined;
64
64
  }
65
65
  const TASK_ID_PLACEHOLDER_RE = /[<>]|^(?:id|task-?id|your-task-id|todo|tbd)$/i;
66
+ /** Mirror of runtime GENERIC_TASK_SLUGS (not exported there); keep in sync. */
67
+ const GENERIC_SLUG_SET = new Set(["test", "fix", "tmp", "new-task"]);
66
68
  const TASK_ADVANCE_ALIAS_ACTIONS = new Set(["promoteRun", "closeoutTask"]);
67
69
  function taskIdInputError(taskId) {
68
70
  if (!TASK_ID_PLACEHOLDER_RE.test(taskId))
@@ -1502,6 +1504,21 @@ export async function dispatchOperatorAction(ctx, req) {
1502
1504
  const taskIdError = taskIdInputError(taskId);
1503
1505
  if (taskIdError)
1504
1506
  return invalid(action, taskIdError);
1507
+ // Canonical id gate at admission (2026-08-16): non-canonical ids were
1508
+ // accepted here (validateNewTaskId only warns) and every downstream
1509
+ // advance/import then flailed on the malformed task directory. Fail
1510
+ // fast with a concrete suggested id instead.
1511
+ if (!CANONICAL_TASK_ID_PATTERN.test(taskId)) {
1512
+ const slug = taskId
1513
+ .toLowerCase()
1514
+ .replace(/[^a-z0-9]+/g, "-")
1515
+ .replace(/^-+|-+$/g, "")
1516
+ .slice(0, 48);
1517
+ const suggested = slug && !GENERIC_SLUG_SET.has(slug)
1518
+ ? `${formatLocalBusinessDate()}-${slug}`
1519
+ : `${formatLocalBusinessDate()}-${slug || "task"}-r2`;
1520
+ return invalid(action, `taskId must be canonical YYYY-MM-DD-<kebab-slug> (local business date); got "${taskId}" — suggested id: "${suggested}" (e.g. 2026-08-16-tool-preview-edge-test); retry newTask with that id`);
1521
+ }
1505
1522
  return acceptOperation(ctx, {
1506
1523
  action,
1507
1524
  actionParams: p,
@@ -0,0 +1 @@
1
+ import{g as p,r as u,d as a}from"./chunk-6Q2QTUOP-VvfacNrq.js";import{p as f}from"./chunk-JWPE2WC7-BlysCtod.js";import{_ as n,l as o}from"./mermaid.core-DWGK9kwP.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-B1wtI3hU.js";import"./index-DQxiso1w.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},B={parser:R,db:a,renderer:u,styles:p};export{B as diagram};
@@ -0,0 +1 @@
1
+ import{Y as ln,$ as an,a0 as F,a1 as q,a2 as j,a3 as un,a4 as y,a5 as tn,a6 as J,a7 as _,a8 as rn,a9 as o,aa as on,ab as sn,ac as fn}from"./mermaid.core-DWGK9kwP.js";function cn(l){return l.innerRadius}function yn(l){return l.outerRadius}function gn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,I,D,v,A,z,a){var O=I-l,i=D-h,n=z-v,d=a-A,u=d*O-n*i;if(!(u*u<y))return u=(n*(h-A)-d*(l-v))/u,[l+u*O,h+u*i]}function U(l,h,I,D,v,A,z){var a=l-I,O=h-D,i=(z?A:-A)/J(a*a+O*O),n=i*O,d=-i*a,u=l+n,s=h+d,f=I+n,c=D+d,B=(u+f)/2,t=(s+c)/2,m=f-u,g=c-s,R=m*m+g*g,T=v-A,P=u*c-f*s,S=(g<0?-1:1)*J(on(0,T*T*R-P*P)),Y=(P*g-m*S)/R,$=(-P*m-g*S)/R,w=(P*g+m*S)/R,p=(-P*m+g*S)/R,x=Y-B,e=$-t,r=w-B,C=p-t;return x*x+e*e>r*r+C*C&&(Y=w,$=p),{cx:Y,cy:$,x01:-n,y01:-d,x11:Y*(v/T-1),y11:$*(v/T-1)}}function hn(){var l=cn,h=yn,I=j(0),D=null,v=gn,A=dn,z=mn,a=null,O=ln(i);function i(){var n,d,u=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-un,c=A.apply(this,arguments)-un,B=rn(c-f),t=c>f;if(a||(a=n=O()),s<u&&(d=s,s=u,u=d),!(s>y))a.moveTo(0,0);else if(B>tn-y)a.moveTo(s*F(f),s*q(f)),a.arc(0,0,s,f,c,!t),u>y&&(a.moveTo(u*F(c),u*q(c)),a.arc(0,0,u,c,f,t));else{var m=f,g=c,R=f,T=c,P=B,S=B,Y=z.apply(this,arguments)/2,$=Y>y&&(D?+D.apply(this,arguments):J(u*u+s*s)),w=_(rn(s-u)/2,+I.apply(this,arguments)),p=w,x=w,e,r;if($>y){var C=sn($/u*q(Y)),K=sn($/s*q(Y));(P-=C*2)>y?(C*=t?1:-1,R+=C,T-=C):(P=0,R=T=(f+c)/2),(S-=K*2)>y?(K*=t?1:-1,m+=K,g-=K):(S=0,m=g=(f+c)/2)}var G=s*F(m),H=s*q(m),L=u*F(T),M=u*q(T);if(w>y){var N=s*F(g),Q=s*q(g),V=u*F(R),W=u*q(R),E;if(B<an)if(E=pn(G,H,V,W,N,Q,L,M)){var X=G-E[0],Z=H-E[1],b=N-E[0],k=Q-E[1],nn=1/q(fn((X*b+Z*k)/(J(X*X+Z*Z)*J(b*b+k*k)))/2),en=J(E[0]*E[0]+E[1]*E[1]);p=_(w,(u-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}S>y?x>y?(e=U(V,W,G,H,s,x,t),r=U(N,Q,L,M,s,x,t),a.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),a.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(a.moveTo(G,H),a.arc(0,0,s,m,g,!t)):a.moveTo(G,H),!(u>y)||!(P>y)?a.lineTo(L,M):p>y?(e=U(L,M,N,Q,u,-p,t),r=U(G,H,V,W,u,-p,t),a.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,u,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),a.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):a.arc(0,0,u,T,R,t)}if(a.closePath(),n)return a=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +A.apply(this,arguments))/2-an/2;return[F(d)*n,q(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:j(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:j(+n),i):h},i.cornerRadius=function(n){return arguments.length?(I=typeof n=="function"?n:j(+n),i):I},i.padRadius=function(n){return arguments.length?(D=n==null?null:typeof n=="function"?n:j(+n),i):D},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:j(+n),i):v},i.endAngle=function(n){return arguments.length?(A=typeof n=="function"?n:j(+n),i):A},i.padAngle=function(n){return arguments.length?(z=typeof n=="function"?n:j(+n),i):z},i.context=function(n){return arguments.length?(a=n??null,i):a},i}export{hn as d};