@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.
- package/CHANGELOG.md +20 -0
- package/dist/build-stamp.json +3 -3
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/worker/console/chat/chat-event-store.js +62 -4
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +264 -16
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +292 -29
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +14 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/routes.js +309 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +529 -186
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CGZrpj0K.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-CwEyA8iq.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-DLTkrEQ3.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-4w12zR3t.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-BppqLAjk.js} +1 -1
- package/dist/worker/console/static/assets/channel-TzuwQha5.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DO6U26Pm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-DC7YEdOy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtMYVQBw.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C5Ed-57N.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-BUY4oIzT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-VnWHALIV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-COV-RQk4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-dSnLtUiz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-CX6FDzWh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D3mEgb6W.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DCu69b3S.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DCu69b3S.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-QQBMUnAf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-BxSD-LVV.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-DxHNiNbk.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-C83bYy8E.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-BhtZu2YY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-K0P_ntRE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-CXYy1Nib.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-D-CtIFkU.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BEK9-_7c.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-L0EX-9wn.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-9pt0fc_5.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CZVeM_n1.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-DqUKkAZP.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-BYUuXEgp.js} +1 -1
- package/dist/worker/console/static/assets/index-B0jGzQj6.js +407 -0
- package/dist/worker/console/static/assets/index-BiYbPiTw.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-DNzKk7ah.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-Clbyg5IP.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-zDO_gZNO.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-x3LpRYek.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CTpwZqKR.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-D_WXK6C7.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-CWK-OYP5.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-B9gtUPsM.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-Dj2lzceR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-Nwljut7m.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-BY-KNt93.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Vx0wvpPy.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B6grNXUk.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-CteidBvh.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Du4W8mFK.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-jyI6Yh81.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyQblISl.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-DYWs_fRv.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DiygmgYP.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DO5LLy47.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CLF2p1_L.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-AwTaYBxA.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-DzbblQRw.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
- package/dist/worker/console/workspace-context.js +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +26 -6
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +3 -2
- package/dist/worker/observe/static/views/dag.js +2 -1
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/task.js +1 -1
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/docs/README.md +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/harness.json +3 -3
- package/package.json +3 -1
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- 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
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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(
|
|
1538
|
-
|
|
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?.
|
|
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))?.
|
|
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.
|
|
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.
|
|
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
|
|
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)) {
|