@tea-agent/loop-agent 0.40.0-next.4 → 0.40.0-next.5

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 (132) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/infrastructure/harness/atomic-write.js +23 -0
  4. package/dist/worker/console/chat/chat-event-store.js +62 -4
  5. package/dist/worker/console/chat/context-panel.js +7 -0
  6. package/dist/worker/console/chat/deferred-turn.js +12 -0
  7. package/dist/worker/console/chat/pi-runtime.js +264 -16
  8. package/dist/worker/console/chat/provider-error.js +98 -0
  9. package/dist/worker/console/chat/routes.js +292 -29
  10. package/dist/worker/console/chat/session-stats.js +179 -0
  11. package/dist/worker/console/chat/session-store.js +93 -63
  12. package/dist/worker/console/chat/todos.js +115 -0
  13. package/dist/worker/console/chat/tool-preview.js +42 -0
  14. package/dist/worker/console/chat/turn-process.js +20 -55
  15. package/dist/worker/console/chat/user-questions.js +266 -0
  16. package/dist/worker/console/chat/workspace-landing.js +14 -0
  17. package/dist/worker/console/native-directory-picker.js +13 -0
  18. package/dist/worker/console/routes.js +309 -1
  19. package/dist/worker/console/security.js +4 -4
  20. package/dist/worker/console/server.js +529 -186
  21. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CGZrpj0K.js} +1 -1
  22. package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-CwEyA8iq.js} +1 -1
  23. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-DLTkrEQ3.js} +1 -1
  24. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-4w12zR3t.js} +1 -1
  25. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-BppqLAjk.js} +1 -1
  26. package/dist/worker/console/static/assets/channel-TzuwQha5.js +1 -0
  27. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DO6U26Pm.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-DC7YEdOy.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtMYVQBw.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C5Ed-57N.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-BUY4oIzT.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-VnWHALIV.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-COV-RQk4.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-dSnLtUiz.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-CX6FDzWh.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D3mEgb6W.js} +1 -1
  37. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DCu69b3S.js +1 -0
  38. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DCu69b3S.js +1 -0
  39. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-QQBMUnAf.js} +1 -1
  40. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-BxSD-LVV.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-DxHNiNbk.js} +1 -1
  42. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-C83bYy8E.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-BhtZu2YY.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-K0P_ntRE.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-CXYy1Nib.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-D-CtIFkU.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BEK9-_7c.js} +1 -1
  48. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-L0EX-9wn.js} +1 -1
  49. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-9pt0fc_5.js} +1 -1
  50. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CZVeM_n1.js} +1 -1
  51. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-DqUKkAZP.js} +1 -1
  52. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-BYUuXEgp.js} +1 -1
  53. package/dist/worker/console/static/assets/index-B0jGzQj6.js +407 -0
  54. package/dist/worker/console/static/assets/index-BiYbPiTw.css +1 -0
  55. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-DNzKk7ah.js} +1 -1
  56. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-Clbyg5IP.js} +1 -1
  57. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-zDO_gZNO.js} +1 -1
  58. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-x3LpRYek.js} +1 -1
  59. package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CTpwZqKR.js} +1 -1
  60. package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-D_WXK6C7.js} +5 -5
  61. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-CWK-OYP5.js} +1 -1
  62. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-B9gtUPsM.js} +1 -1
  63. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-Dj2lzceR.js} +1 -1
  64. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-Nwljut7m.js} +1 -1
  65. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-BY-KNt93.js} +1 -1
  66. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Vx0wvpPy.js} +1 -1
  67. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B6grNXUk.js} +1 -1
  68. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-CteidBvh.js} +1 -1
  69. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Du4W8mFK.js} +1 -1
  70. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-jyI6Yh81.js} +1 -1
  71. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyQblISl.js +1 -0
  72. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-DYWs_fRv.js} +2 -2
  73. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DiygmgYP.js +8 -0
  74. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DO5LLy47.js} +1 -1
  75. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CLF2p1_L.js} +1 -1
  76. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-AwTaYBxA.js} +1 -1
  77. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-DzbblQRw.js} +1 -1
  78. package/dist/worker/console/static/index.html +2 -2
  79. package/dist/worker/console/static-src/app/console-types.js +29 -4
  80. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  81. package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
  82. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  83. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  84. package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
  85. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  86. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  87. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  88. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  89. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  90. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  91. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  92. package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
  93. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  94. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  95. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  96. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  97. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  98. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  99. package/dist/worker/console/workspace-context.js +234 -0
  100. package/dist/worker/console/workspace-registry.js +214 -0
  101. package/dist/worker/observe/server.js +4 -0
  102. package/dist/worker/observe/static/api.js +26 -6
  103. package/dist/worker/observe/static/app.js +18 -70
  104. package/dist/worker/observe/static/index.html +1 -1
  105. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  106. package/dist/worker/observe/static/operator-chrome.css +145 -20
  107. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  108. package/dist/worker/observe/static/operator-chrome.js +330 -100
  109. package/dist/worker/observe/static/router.d.ts +46 -0
  110. package/dist/worker/observe/static/router.js +25 -1
  111. package/dist/worker/observe/static/state.d.ts +50 -0
  112. package/dist/worker/observe/static/state.js +172 -0
  113. package/dist/worker/observe/static/views/dag-inspector.js +3 -2
  114. package/dist/worker/observe/static/views/dag.js +2 -1
  115. package/dist/worker/observe/static/views/dashboard.js +6 -4
  116. package/dist/worker/observe/static/views/night.js +2 -2
  117. package/dist/worker/observe/static/views/pool.js +1 -0
  118. package/dist/worker/observe/static/views/run.js +3 -1
  119. package/dist/worker/observe/static/views/task.js +1 -1
  120. package/dist/workflows/dag/interrupt-request.js +3 -3
  121. package/dist/workflows/dag/lifecycle.js +3 -3
  122. package/docs/README.md +1 -0
  123. package/docs/operations/local-development-environment.md +21 -0
  124. package/harness.json +3 -3
  125. package/package.json +3 -1
  126. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
  127. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
  128. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
  129. package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
  130. package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
  131. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
  132. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
@@ -17,7 +17,7 @@ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_
17
17
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
18
18
  import { cleanChatTitle, acceptedOperationIdFromResult } from "./session-store.js";
19
19
  import { hashChatTurnPayload, projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
20
- import { projectDeferredTurnSummary, } from "./deferred-turn.js";
20
+ import { projectDeferredTurnSummary, projectDeferredTurnSummaries, } from "./deferred-turn.js";
21
21
  import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime.js";
22
22
  import { ResourcePreferencesError, setSkillAutoInvocationPreference, } from "./resource-preferences-store.js";
23
23
  import { ComposerDraftStore } from "./composer-draft-store.js";
@@ -33,9 +33,11 @@ import { dispatchOperatorAction } from "../operator-actions.js";
33
33
  import { makeHumanGateCard, } from "./human-gate-card.js";
34
34
  import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
35
35
  import { projectTaskContext } from "./context-panel.js";
36
+ import { deriveChatSessionTelemetry } from "./session-stats.js";
36
37
  import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
37
38
  import { sendJson } from "../routes.js";
38
39
  import { openSseResponse, writeSseEvent } from "../operation-sse.js";
40
+ import { buildTodoCompactionInstructions, projectCurrentTodos } from "./todos.js";
39
41
  function turnSummary(turn) {
40
42
  return {
41
43
  turnId: turn.turnId,
@@ -87,11 +89,38 @@ function turnEventToStorePartial(event) {
87
89
  case "message_update":
88
90
  case "message_end": {
89
91
  const content = parseAssistantContent(event.content);
92
+ const timingSource = event.type === "message_end" &&
93
+ event.timing &&
94
+ typeof event.timing === "object"
95
+ ? event.timing
96
+ : undefined;
97
+ const timing = timingSource &&
98
+ typeof timingSource.llmMs === "number" &&
99
+ Number.isFinite(timingSource.llmMs)
100
+ ? {
101
+ llmMs: Math.max(0, timingSource.llmMs),
102
+ ...(typeof timingSource.ttftMs === "number" &&
103
+ Number.isFinite(timingSource.ttftMs)
104
+ ? { ttftMs: Math.max(0, timingSource.ttftMs) }
105
+ : {}),
106
+ ...(typeof timingSource.decodeMs === "number" &&
107
+ Number.isFinite(timingSource.decodeMs)
108
+ ? { decodeMs: Math.max(0, timingSource.decodeMs) }
109
+ : {}),
110
+ ...(typeof timingSource.outputTokens === "number" &&
111
+ Number.isFinite(timingSource.outputTokens)
112
+ ? {
113
+ outputTokens: Math.max(0, timingSource.outputTokens),
114
+ }
115
+ : {}),
116
+ }
117
+ : undefined;
90
118
  return {
91
119
  kind: event.type,
92
120
  data: {
93
121
  text: String(event.text ?? ""),
94
122
  ...(content ? { content } : {}),
123
+ ...(timing ? { timing } : {}),
95
124
  },
96
125
  };
97
126
  }
@@ -123,6 +152,9 @@ function turnEventToStorePartial(event) {
123
152
  }),
124
153
  };
125
154
  }
155
+ case "todo_updated":
156
+ case "todo_cleared":
157
+ return { kind: event.type, data: { todos: Array.isArray(event.todos) ? event.todos : [], ...(event.toolCallId ? { toolCallId: String(event.toolCallId) } : {}) } };
126
158
  case "agent_end":
127
159
  return {
128
160
  kind: "agent_end",
@@ -130,8 +162,19 @@ function turnEventToStorePartial(event) {
130
162
  };
131
163
  case "agent_settled":
132
164
  return { kind: "agent_settled", data: {} };
133
- case "error":
134
- return { kind: "error", data: { message: String(event.message ?? "") } };
165
+ case "error": {
166
+ const content = parseAssistantContent(event.content);
167
+ return {
168
+ kind: "error",
169
+ data: {
170
+ message: String(event.message ?? ""),
171
+ ...(content ? { content } : {}),
172
+ ...(event.providerError && typeof event.providerError === "object"
173
+ ? { providerError: event.providerError }
174
+ : {}),
175
+ },
176
+ };
177
+ }
135
178
  default:
136
179
  return undefined;
137
180
  }
@@ -197,6 +240,13 @@ function ensureAgentEndEvent(deps, sessionId, turnId) {
197
240
  }
198
241
  }
199
242
  async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
243
+ // AC-R5-003: the multi-workspace dispatcher (server.ts readBodyOnce)
244
+ // pre-reads and JSON-parses the body for workspaceId resolution, stashing
245
+ // it on req.__preReadBody. Consume it first so the body is parsed exactly
246
+ // once per request; re-reading the consumed stream would yield {}.
247
+ const preRead = req.__preReadBody;
248
+ if (preRead !== undefined)
249
+ return preRead;
200
250
  const chunks = [];
201
251
  let total = 0;
202
252
  for await (const chunk of req) {
@@ -344,7 +394,10 @@ export async function handleChatCapabilities(_req, res, deps) {
344
394
  hasBash: true,
345
395
  });
346
396
  }
347
- export async function handleCreateChatSession(req, res, deps) {
397
+ export async function handleCreateChatSession(req, res, deps,
398
+ /** Explicit owning workspaceId from the multi-workspace dispatcher
399
+ * (AC-R7-002). The body still parses exactly once via req.__preReadBody. */
400
+ workspaceId) {
348
401
  const gate = gateMutation(req, deps);
349
402
  if (!gate.ok) {
350
403
  sendJson(res, gate.status, {
@@ -422,6 +475,10 @@ export async function handleCreateChatSession(req, res, deps) {
422
475
  sessionFile: handle.sessionFile,
423
476
  modelProvider: handle.model?.provider,
424
477
  modelId: handle.model?.modelId,
478
+ // AC-R7-002: explicit parameter from the dispatcher — the runtime
479
+ // store.create wrapper is gone; body workspaceId (if any) only acts as
480
+ // a fallback for direct (non-dispatcher) handler tests.
481
+ workspaceId: workspaceId ?? (typeof body.workspaceId === "string" && body.workspaceId ? body.workspaceId : undefined),
425
482
  });
426
483
  if (snapshot)
427
484
  deps.events.append(handle.sessionId, `${handle.sessionId}:snapshot`, {
@@ -646,8 +703,10 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
646
703
  await deps.operationLinker.recoverSession(sessionId);
647
704
  const composerDraft = await new ComposerDraftStore(deps.appData).get(sessionId);
648
705
  const snapshot = deps.events.snapshot(sessionId);
706
+ const telemetry = deriveChatSessionTelemetry(snapshot);
649
707
  const lastEvent = snapshot.at(-1);
650
708
  const activeTurn = deps.events.getActiveTurn(sessionId);
709
+ const pendingQuestion = await deps.questionRegistry?.get(sessionId);
651
710
  sendJson(res, 200, {
652
711
  ok: true,
653
712
  eventEpoch: deps.events.epoch(sessionId),
@@ -660,8 +719,19 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
660
719
  ? "stopping"
661
720
  : "running"
662
721
  : "idle",
722
+ pendingQuestion: pendingQuestion ?? null,
723
+ todos: projectCurrentTodos(snapshot) ?? [],
663
724
  },
664
725
  hasBash: true,
726
+ sessionStats: telemetry.stats,
727
+ usage: telemetry.usage,
728
+ compactions: snapshot
729
+ .filter((event) => event.kind === "compact")
730
+ .map((event) => ({
731
+ eventId: event.eventId,
732
+ at: event.at,
733
+ data: event.data,
734
+ })),
665
735
  lastEventId: lastEvent?.eventId ?? null,
666
736
  lastEventSeq: lastEvent?.seq ?? 0,
667
737
  });
@@ -769,11 +839,12 @@ export async function handlePutDeferredTurn(req, res, deps, sessionId) {
769
839
  : "待发送消息无效或会话不存在",
770
840
  },
771
841
  deferredTurn: result.deferredTurn ?? null,
842
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
772
843
  summary: projectDeferredTurnSummary(result.deferredTurn),
773
844
  });
774
845
  return;
775
846
  }
776
- emitDeferredTurnUpdate(deps, sessionId, blockedByTurnId, result.deferredTurn, result.deferredTurn.revision);
847
+ await emitDeferredTurnUpdate(deps, sessionId, blockedByTurnId, result.deferredTurn, result.deferredTurn.revision);
777
848
  sendJson(res, 200, {
778
849
  ok: true,
779
850
  accepted: true,
@@ -781,6 +852,7 @@ export async function handlePutDeferredTurn(req, res, deps, sessionId) {
781
852
  state: result.deferredTurn.state,
782
853
  revision: result.deferredTurn.revision,
783
854
  deferredTurn: result.deferredTurn,
855
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
784
856
  summary: projectDeferredTurnSummary(result.deferredTurn),
785
857
  });
786
858
  void dispatchDeferredTurn(deps, sessionId);
@@ -826,18 +898,90 @@ export async function handleDeleteDeferredTurn(req, res, deps, sessionId) {
826
898
  : "待发送消息已变化,请刷新后重试",
827
899
  },
828
900
  deferredTurn: result.deferredTurn ?? null,
901
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
829
902
  });
830
903
  return;
831
904
  }
832
905
  const clearedRevision = result.deferredTurn.revision + 1;
833
- emitDeferredTurnUpdate(deps, sessionId, result.deferredTurn.blockedByTurnId, undefined, clearedRevision);
906
+ await emitDeferredTurnUpdate(deps, sessionId, result.deferredTurn.blockedByTurnId, undefined, clearedRevision);
834
907
  sendJson(res, 200, {
835
908
  ok: true,
836
909
  cancelled: true,
837
910
  deferredTurn: result.deferredTurn,
911
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
838
912
  revision: clearedRevision,
839
913
  });
840
914
  }
915
+ /** Move one addressed FIFO item into Pi's live steering queue. */
916
+ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientRequestId) {
917
+ const gate = gateMutation(req, deps);
918
+ if (!gate.ok) {
919
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
920
+ return;
921
+ }
922
+ let body = {};
923
+ try {
924
+ body = await readJsonBody(req);
925
+ }
926
+ catch (error) {
927
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: error instanceof Error ? error.message : String(error) } });
928
+ return;
929
+ }
930
+ const expectedRevision = typeof body.expectedRevision === "number" && Number.isSafeInteger(body.expectedRevision)
931
+ ? body.expectedRevision
932
+ : -1;
933
+ const active = deps.events.getActiveTurn(sessionId);
934
+ if (!active) {
935
+ sendJson(res, 409, { ok: false, error: { code: "TURN_NOT_ACTIVE", message: "当前没有可插话的运行中回合" } });
936
+ return;
937
+ }
938
+ const claim = await deps.store.claimDeferredTurn(sessionId, expectedRevision, clientRequestId);
939
+ if (!claim.ok) {
940
+ sendJson(res, claim.code === "NOT_FOUND" ? 404 : 409, {
941
+ ok: false,
942
+ error: { code: claim.code, message: "排队消息已变化,请刷新后重试" },
943
+ });
944
+ return;
945
+ }
946
+ const item = claim.deferredTurn;
947
+ try {
948
+ await deps.runtime.steer(sessionId, item.text, item.images);
949
+ const userMessage = await deps.store.appendMessage(sessionId, {
950
+ role: "user",
951
+ text: item.text,
952
+ });
953
+ deps.events.append(sessionId, active.turnId, {
954
+ kind: "user_message",
955
+ data: {
956
+ clientRequestId: item.clientRequestId,
957
+ message: { id: userMessage.id, text: userMessage.text, at: userMessage.at },
958
+ },
959
+ });
960
+ await deps.store.clearDeferredTurnForRequest(sessionId, item.clientRequestId);
961
+ await emitDeferredTurnUpdate(deps, sessionId, active.turnId, undefined, item.revision + 2);
962
+ sendJson(res, 200, {
963
+ ok: true,
964
+ accepted: true,
965
+ deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
966
+ });
967
+ }
968
+ catch (error) {
969
+ const restored = await deps.store.transitionDeferredTurn(sessionId, {
970
+ expectedRevision: item.revision,
971
+ clientRequestId: item.clientRequestId,
972
+ state: "queued",
973
+ });
974
+ if (restored)
975
+ await emitDeferredTurnUpdate(deps, sessionId, active.turnId, restored, restored.revision);
976
+ sendJson(res, 409, {
977
+ ok: false,
978
+ error: {
979
+ code: "STEER_FAILED",
980
+ message: error instanceof Error ? error.message : "插话发送失败",
981
+ },
982
+ });
983
+ }
984
+ }
841
985
  /** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
842
986
  * get_state equivalent): exposes the active turn + the latest turn's durable
843
987
  * outcome WITHOUT the full message payload, so the UI can poll it on an
@@ -865,6 +1009,7 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
865
1009
  isBashRunning: false,
866
1010
  };
867
1011
  const runtimeBusy = Object.values(runtimeFacts).some((value) => value === true);
1012
+ const pendingQuestion = await deps.questionRegistry?.get(sessionId);
868
1013
  const turnPhase = active
869
1014
  ? active.abortRequestedAt
870
1015
  ? "stopping"
@@ -884,10 +1029,60 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
884
1029
  latestTurn: latest ? turnSummary(latest) : null,
885
1030
  runtime: runtimeFacts,
886
1031
  turnPhase,
887
- deferredTurn: projectDeferredTurnSummary(record.deferredTurn),
1032
+ deferredTurn: projectDeferredTurnSummary(record.deferredTurns?.[0]),
1033
+ deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
1034
+ pendingQuestion: pendingQuestion ?? null,
1035
+ todos: projectCurrentTodos(deps.events.snapshot(sessionId)) ?? [],
888
1036
  },
889
1037
  });
890
1038
  }
1039
+ export async function handleAnswerUserQuestion(req, res, deps, sessionId, requestId) {
1040
+ const gate = gateMutation(req, deps);
1041
+ if (!gate.ok) {
1042
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
1043
+ return;
1044
+ }
1045
+ if (!(await requireChatSession(res, deps, sessionId)))
1046
+ return;
1047
+ if (!deps.questionRegistry) {
1048
+ sendJson(res, 503, { ok: false, error: { code: "USER_QUESTIONS_UNAVAILABLE", message: "user question capability is unavailable" } });
1049
+ return;
1050
+ }
1051
+ try {
1052
+ const body = await readJsonBody(req);
1053
+ const question = await deps.questionRegistry.answer(sessionId, requestId, body);
1054
+ sendJson(res, 200, { ok: true, resolved: true, question });
1055
+ }
1056
+ catch (error) {
1057
+ const message = error instanceof Error ? error.message : String(error);
1058
+ const status = message === "USER_QUESTION_NOT_FOUND" ? 404 : message === "USER_QUESTION_STALE" ? 409 : 400;
1059
+ sendJson(res, status, { ok: false, error: { code: message, message } });
1060
+ }
1061
+ }
1062
+ export async function handleCancelUserQuestion(req, res, deps, sessionId, requestId) {
1063
+ const gate = gateMutation(req, deps);
1064
+ if (!gate.ok) {
1065
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
1066
+ return;
1067
+ }
1068
+ if (!(await requireChatSession(res, deps, sessionId)))
1069
+ return;
1070
+ if (!deps.questionRegistry) {
1071
+ sendJson(res, 503, { ok: false, error: { code: "USER_QUESTIONS_UNAVAILABLE", message: "user question capability is unavailable" } });
1072
+ return;
1073
+ }
1074
+ try {
1075
+ const body = await readJsonBody(req).catch(() => ({}));
1076
+ const reason = typeof body.reason === "string" && body.reason.trim() ? body.reason.trim().slice(0, 120) : "USER_CANCELLED";
1077
+ const question = await deps.questionRegistry.cancel(sessionId, requestId, reason);
1078
+ sendJson(res, 200, { ok: true, cancelled: true, question });
1079
+ }
1080
+ catch (error) {
1081
+ const message = error instanceof Error ? error.message : String(error);
1082
+ const status = message === "USER_QUESTION_NOT_FOUND" ? 404 : 409;
1083
+ sendJson(res, status, { ok: false, error: { code: message, message } });
1084
+ }
1085
+ }
891
1086
  export async function handleListChatSessions(_req, res, deps) {
892
1087
  const sessions = await deps.store.list({ includeArchived: true });
893
1088
  sendJson(res, 200, {
@@ -903,7 +1098,8 @@ export async function handleListChatSessions(_req, res, deps) {
903
1098
  : active
904
1099
  ? "running"
905
1100
  : "idle",
906
- deferredTurn: projectDeferredTurnSummary(session.deferredTurn),
1101
+ deferredTurn: projectDeferredTurnSummary(session.deferredTurns?.[0]),
1102
+ deferredTurns: projectDeferredTurnSummaries(session.deferredTurns ?? []),
907
1103
  };
908
1104
  }),
909
1105
  hasBash: true,
@@ -1150,6 +1346,7 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
1150
1346
  store: deps.store,
1151
1347
  actionContext,
1152
1348
  sessionId,
1349
+ turnId,
1153
1350
  text,
1154
1351
  images,
1155
1352
  signal: ac.signal,
@@ -1445,6 +1642,7 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1445
1642
  store: deps.store,
1446
1643
  actionContext,
1447
1644
  sessionId,
1645
+ turnId: turn.turnId,
1448
1646
  text,
1449
1647
  images,
1450
1648
  signal: controller.signal,
@@ -1530,12 +1728,16 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
1530
1728
  }
1531
1729
  })();
1532
1730
  }
1533
- function emitDeferredTurnUpdate(deps, sessionId, turnId, deferredTurn, revision) {
1731
+ async function emitDeferredTurnUpdate(deps, sessionId, turnId, _deferredTurn, _revision) {
1732
+ const record = await deps.store.get(sessionId);
1733
+ if (!record)
1734
+ return;
1534
1735
  deps.events.append(sessionId, turnId, {
1535
1736
  kind: "queue_update",
1536
1737
  data: {
1537
- deferredTurn: projectDeferredTurnSummary(deferredTurn),
1538
- revision,
1738
+ deferredTurn: projectDeferredTurnSummary(record.deferredTurns?.[0]),
1739
+ deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
1740
+ revision: record.deferredQueueRevision ?? 0,
1539
1741
  },
1540
1742
  });
1541
1743
  }
@@ -1571,7 +1773,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1571
1773
  deferredDispatchingSessions.add(sessionId);
1572
1774
  try {
1573
1775
  let record = await deps.store.get(sessionId);
1574
- let deferred = record?.deferredTurn;
1776
+ let deferred = record?.deferredTurns?.find((item) => item.state === "dispatching" || item.state === "queued");
1575
1777
  if (!deferred)
1576
1778
  return;
1577
1779
  if (deferred.state === "accepted")
@@ -1583,17 +1785,19 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1583
1785
  if (existing) {
1584
1786
  const accepted = await deps.store.transitionDeferredTurn(sessionId, {
1585
1787
  expectedRevision: deferred.revision,
1788
+ clientRequestId: deferred.clientRequestId,
1586
1789
  state: "accepted",
1587
1790
  acceptedTurnId: existing.turnId,
1588
1791
  });
1589
1792
  if (accepted)
1590
- emitDeferredTurnUpdate(deps, sessionId, existing.turnId, accepted, accepted.revision);
1793
+ await emitDeferredTurnUpdate(deps, sessionId, existing.turnId, accepted, accepted.revision);
1591
1794
  return;
1592
1795
  }
1593
1796
  if (deps.events.getActiveTurn(sessionId))
1594
1797
  return;
1595
1798
  const requeued = await deps.store.transitionDeferredTurn(sessionId, {
1596
1799
  expectedRevision: deferred.revision,
1800
+ clientRequestId: deferred.clientRequestId,
1597
1801
  state: "queued",
1598
1802
  });
1599
1803
  if (!requeued)
@@ -1604,11 +1808,11 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1604
1808
  const runtimeBusy = Boolean(runtimeFacts && Object.values(runtimeFacts).some((value) => value === true));
1605
1809
  if (deps.events.getActiveTurn(sessionId) || runtimeBusy)
1606
1810
  return;
1607
- const claim = await deps.store.claimDeferredTurn(sessionId, deferred.revision);
1811
+ const claim = await deps.store.claimDeferredTurn(sessionId, deferred.revision, deferred.clientRequestId);
1608
1812
  if (!claim.ok)
1609
1813
  return;
1610
1814
  deferred = claim.deferredTurn;
1611
- emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, deferred, deferred.revision);
1815
+ await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, deferred, deferred.revision);
1612
1816
  const started = await startTrustedChatTurn(deps, sessionId, {
1613
1817
  clientRequestId: deferred.clientRequestId,
1614
1818
  text: deferred.text,
@@ -1627,7 +1831,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1627
1831
  });
1628
1832
  const cleared = await deps.store.clearDeferredTurnForRequest(sessionId, deferred.clientRequestId);
1629
1833
  if (cleared)
1630
- emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, cleared.clearedRevision);
1834
+ await emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, cleared.clearedRevision);
1631
1835
  },
1632
1836
  });
1633
1837
  const accepted = started.body.accepted === true;
@@ -1637,11 +1841,12 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1637
1841
  if (accepted && turn?.turnId) {
1638
1842
  const next = await deps.store.transitionDeferredTurn(sessionId, {
1639
1843
  expectedRevision: deferred.revision,
1844
+ clientRequestId: deferred.clientRequestId,
1640
1845
  state: "accepted",
1641
1846
  acceptedTurnId: turn.turnId,
1642
1847
  });
1643
1848
  if (next)
1644
- emitDeferredTurnUpdate(deps, sessionId, turn.turnId, next, next.revision);
1849
+ await emitDeferredTurnUpdate(deps, sessionId, turn.turnId, next, next.revision);
1645
1850
  return;
1646
1851
  }
1647
1852
  const error = started.body.error && typeof started.body.error === "object"
@@ -1650,14 +1855,16 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1650
1855
  if (error.code === "TURN_ACTIVE") {
1651
1856
  const next = await deps.store.transitionDeferredTurn(sessionId, {
1652
1857
  expectedRevision: deferred.revision,
1858
+ clientRequestId: deferred.clientRequestId,
1653
1859
  state: "queued",
1654
1860
  });
1655
1861
  if (next)
1656
- emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, next, next.revision);
1862
+ await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, next, next.revision);
1657
1863
  return;
1658
1864
  }
1659
1865
  const failed = await deps.store.transitionDeferredTurn(sessionId, {
1660
1866
  expectedRevision: deferred.revision,
1867
+ clientRequestId: deferred.clientRequestId,
1661
1868
  state: "failed",
1662
1869
  lastError: {
1663
1870
  code: error.code ?? `HTTP_${started.status}`,
@@ -1665,17 +1872,18 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1665
1872
  },
1666
1873
  });
1667
1874
  if (failed)
1668
- emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, failed, failed.revision);
1875
+ await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, failed, failed.revision);
1669
1876
  }
1670
1877
  catch (error) {
1671
1878
  // Keep fire-and-forget callers safe and make a dispatch failure visible and
1672
1879
  // recoverable from the composer instead of leaving the slot indefinitely
1673
1880
  // in an ambiguous queued/dispatching state.
1674
1881
  try {
1675
- const current = (await deps.store.get(sessionId))?.deferredTurn;
1882
+ const current = (await deps.store.get(sessionId))?.deferredTurns?.find((item) => item.state === "queued" || item.state === "dispatching");
1676
1883
  if (current && (current.state === "queued" || current.state === "dispatching")) {
1677
1884
  const failed = await deps.store.transitionDeferredTurn(sessionId, {
1678
1885
  expectedRevision: current.revision,
1886
+ clientRequestId: current.clientRequestId,
1679
1887
  state: "failed",
1680
1888
  lastError: {
1681
1889
  code: "DEFERRED_TURN_DISPATCH_FAILED",
@@ -1683,7 +1891,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1683
1891
  },
1684
1892
  });
1685
1893
  if (failed)
1686
- emitDeferredTurnUpdate(deps, sessionId, failed.blockedByTurnId, failed, failed.revision);
1894
+ await emitDeferredTurnUpdate(deps, sessionId, failed.blockedByTurnId, failed, failed.revision);
1687
1895
  }
1688
1896
  }
1689
1897
  catch {
@@ -1698,12 +1906,9 @@ export async function dispatchDeferredTurn(deps, sessionId) {
1698
1906
  export async function recoverDeferredTurns(deps) {
1699
1907
  const sessions = await deps.store.list({ includeArchived: false });
1700
1908
  await Promise.all(sessions
1701
- .filter((session) => session.deferredTurn
1702
- ? session.deferredTurn.state === "queued" ||
1703
- session.deferredTurn.state === "dispatching"
1704
- : false)
1909
+ .filter((session) => (session.deferredTurns ?? []).some((item) => item.state === "queued" || item.state === "dispatching"))
1705
1910
  .map(async (session) => {
1706
- const deferred = session.deferredTurn;
1911
+ const deferred = session.deferredTurns.find((item) => item.state === "dispatching" || item.state === "queued");
1707
1912
  const active = deps.events.getActiveTurn(session.sessionId);
1708
1913
  // A process restart cannot retain AbortController ownership. When the
1709
1914
  // durable blocker was already stopping and no execution owns it now,
@@ -1771,6 +1976,7 @@ export async function handleAbortChatTurn(req, res, deps, sessionId, turnId) {
1771
1976
  return;
1772
1977
  }
1773
1978
  deps.events.markAbortRequested?.(sessionId, turnId);
1979
+ await deps.questionRegistry?.cancelForSession(sessionId, "TURN_ABORTED").catch(() => undefined);
1774
1980
  if (!execution.controller.signal.aborted)
1775
1981
  execution.controller.abort();
1776
1982
  const waited = await (deps.turnExecutions?.waitForCompletion(sessionId, turnId, DEFAULT_ABORT_WAIT_MS) ?? Promise.resolve({ status: "missing" }));
@@ -1921,6 +2127,8 @@ export async function handleDeleteChatSession(req, res, deps, sessionId) {
1921
2127
  });
1922
2128
  return;
1923
2129
  }
2130
+ await deps.questionRegistry?.cancelForSession(sessionId, "SESSION_DELETED").catch(() => undefined);
2131
+ deps.todoRegistry?.clear(sessionId);
1924
2132
  deps.runtime.dispose(sessionId);
1925
2133
  deps.operationLinker.clearSession(sessionId);
1926
2134
  deps.events.clear(sessionId);
@@ -2595,6 +2803,36 @@ async function handleLinkWorkspaceOperation(req, res, deps, sessionId) {
2595
2803
  });
2596
2804
  }
2597
2805
  }
2806
+ async function handleUnlinkWorkspaceOperation(req, res, deps, sessionId, operationId) {
2807
+ const gate = gateMutation(req, deps);
2808
+ if (!gate.ok) {
2809
+ sendJson(res, gate.status, {
2810
+ ok: false,
2811
+ error: { code: gate.code, message: gate.message },
2812
+ });
2813
+ return;
2814
+ }
2815
+ if (!(await requireChatSession(res, deps, sessionId)))
2816
+ return;
2817
+ // AC-R7-001 (2026-08-23): removing a workspace-linked DAG association is
2818
+ // idempotent — a missing link is already-removed (200), never an error.
2819
+ try {
2820
+ const removed = await deps.operationLinker.unlink({
2821
+ sessionId,
2822
+ operationId,
2823
+ });
2824
+ sendJson(res, 200, { ok: true, removed });
2825
+ }
2826
+ catch (error) {
2827
+ sendJson(res, 400, {
2828
+ ok: false,
2829
+ error: {
2830
+ code: "INVALID_INPUT",
2831
+ message: error instanceof Error ? error.message : String(error),
2832
+ },
2833
+ });
2834
+ }
2835
+ }
2598
2836
  async function handleStartInterview(req, res, deps, sessionId) {
2599
2837
  const gate = gateMutation(req, deps);
2600
2838
  if (!gate.ok) {
@@ -3247,7 +3485,8 @@ export async function handleChatCompact(req, res, deps, sessionId) {
3247
3485
  return;
3248
3486
  }
3249
3487
  const rawInstructions = typeof body.instructions === "string" ? body.instructions : undefined;
3250
- const customInstructions = rawInstructions?.trim() || undefined;
3488
+ const currentTodos = projectCurrentTodos(deps.events.snapshot(sessionId));
3489
+ const customInstructions = buildTodoCompactionInstructions(rawInstructions, currentTodos);
3251
3490
  try {
3252
3491
  const snapshot = await deps.runtime.compact(sessionId, customInstructions);
3253
3492
  const event = deps.events.append(sessionId, deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`, { kind: "compact", data: snapshot });
@@ -3283,8 +3522,10 @@ export async function handleChatMessages(res, deps, sessionId, url) {
3283
3522
  /**
3284
3523
  * Dispatch a chat HTTP request. Returns true if handled.
3285
3524
  * Mounted under /api/operator/v1/chat/*.
3525
+ * workspaceScope (AC-R7-002): the explicit owning workspaceId for session
3526
+ * create, forwarded to handleCreateChatSession → store.create.
3286
3527
  */
3287
- export async function handleChatRequest(req, res, deps, pathname) {
3528
+ export async function handleChatRequest(req, res, deps, pathname, workspaceScope) {
3288
3529
  if (!deps)
3289
3530
  return false;
3290
3531
  if (!pathname.startsWith("/api/operator/v1/chat/") &&
@@ -3362,6 +3603,16 @@ export async function handleChatRequest(req, res, deps, pathname) {
3362
3603
  await handleStartInterview(req, res, deps, decodeURIComponent(interviewStart[1]));
3363
3604
  return true;
3364
3605
  }
3606
+ const userQuestionAnswer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/questions\/([^/]+)\/answer$/);
3607
+ if (method === "POST" && userQuestionAnswer) {
3608
+ await handleAnswerUserQuestion(req, res, deps, decodeURIComponent(userQuestionAnswer[1]), decodeURIComponent(userQuestionAnswer[2]));
3609
+ return true;
3610
+ }
3611
+ const userQuestionCancel = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/questions\/([^/]+)\/cancel$/);
3612
+ if (method === "POST" && userQuestionCancel) {
3613
+ await handleCancelUserQuestion(req, res, deps, decodeURIComponent(userQuestionCancel[1]), decodeURIComponent(userQuestionCancel[2]));
3614
+ return true;
3615
+ }
3365
3616
  const interviewAnswer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/interviews\/([^/]+)\/answers$/);
3366
3617
  if (method === "POST" && interviewAnswer) {
3367
3618
  await handleAnswerInterview(req, res, deps, decodeURIComponent(interviewAnswer[1]), decodeURIComponent(interviewAnswer[2]));
@@ -3468,6 +3719,11 @@ export async function handleChatRequest(req, res, deps, pathname) {
3468
3719
  await handleDeleteDeferredTurn(req, res, deps, decodeURIComponent(deferredTurn[1]));
3469
3720
  return true;
3470
3721
  }
3722
+ const deferredTurnSteer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/deferred-turn\/([^/]+)\/steer$/);
3723
+ if (method === "POST" && deferredTurnSteer) {
3724
+ await handleSteerDeferredTurn(req, res, deps, decodeURIComponent(deferredTurnSteer[1]), decodeURIComponent(deferredTurnSteer[2]));
3725
+ return true;
3726
+ }
3471
3727
  const sessionTurnAbort = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/turns\/([^/]+)\/abort$/);
3472
3728
  if (method === "POST" && sessionTurnAbort) {
3473
3729
  await handleAbortChatTurn(req, res, deps, decodeURIComponent(sessionTurnAbort[1]), decodeURIComponent(sessionTurnAbort[2]));
@@ -3483,6 +3739,13 @@ export async function handleChatRequest(req, res, deps, pathname) {
3483
3739
  await handleLinkWorkspaceOperation(req, res, deps, decodeURIComponent(operationLink[1]));
3484
3740
  return true;
3485
3741
  }
3742
+ const operationLinkItem = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/operation-links\/([^/]+)$/);
3743
+ if (method === "DELETE" && operationLinkItem) {
3744
+ // AC-R7-001: Details rail DAG remove — real DELETE of the workspace
3745
+ // operation-link association (idempotent).
3746
+ await handleUnlinkWorkspaceOperation(req, res, deps, decodeURIComponent(operationLinkItem[1]), decodeURIComponent(operationLinkItem[2]));
3747
+ return true;
3748
+ }
3486
3749
  const sessionCompact = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/compact$/);
3487
3750
  if (method === "POST" && sessionCompact) {
3488
3751
  await handleChatCompact(req, res, deps, decodeURIComponent(sessionCompact[1]));
@@ -3530,7 +3793,7 @@ export async function handleChatRequest(req, res, deps, pathname) {
3530
3793
  }
3531
3794
  }
3532
3795
  if (method === "POST" && pathname === "/api/operator/v1/chat/sessions") {
3533
- await handleCreateChatSession(req, res, deps);
3796
+ await handleCreateChatSession(req, res, deps, workspaceScope);
3534
3797
  return true;
3535
3798
  }
3536
3799
  if (isMutationMethod(method)) {