@tea-agent/loop-agent 0.39.3 → 0.40.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -92
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +12 -0
- package/dist/cli/program.js +23 -1
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/executors/dag-pi-executor.js +108 -19
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +49 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +42 -2
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +35 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/operator/capabilities.js +14 -0
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +10 -0
- package/dist/task/source-prepare/fragment-inventory.js +1 -14
- package/dist/worker/console/chat/chat-event-store.js +77 -7
- 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 +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +389 -56
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +94 -64
- 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 +79 -0
- package/dist/worker/console/console-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +1 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -3
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +104 -3
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +128 -0
- package/dist/worker/console/routes.js +927 -8
- package/dist/worker/console/security.js +39 -4
- package/dist/worker/console/server.js +532 -187
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CpWvwARc.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DctXgO-g.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-D4mIkoQl.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-ClG0--KO.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-CwdLk78T.js} +1 -1
- package/dist/worker/console/static/assets/channel-C9VQZ-ru.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DKczcu-O.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-BV6F-vBI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtojVnvA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-BjDQRGnO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-I0fV6ngU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-BM8C-mmC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-BEK-a15e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-CQsg_fSR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-B-BcaUm3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D1Bu08fg.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-q8nwtxBR.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CruhpURw.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-CJne6iYF.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-CbeLvBU5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-DmCxH1Pe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-DJ5OYhtC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-B-UN-hln.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-CcEro8Ey.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BBBzDden.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-xKEyH-oo.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-C7xl63VC.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-nthQQ9nR.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CWN5mE33.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-Wn86tQ6N.js} +1 -1
- package/dist/worker/console/static/assets/index-D-0NVyMk.css +1 -0
- package/dist/worker/console/static/assets/index-gwyOxQiS.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-_WQymGtW.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-COBeivBm.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-CwGOJl3Q.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-Ci0CJ5Vi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CPjMq6vN.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-Z0wGoe0b.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-dISked0d.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-BJxgof7Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-CIblU8eW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-vwYiqh8E.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-_iOsVeFm.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Dk0MLxO_.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-Dx7HtLVW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-DRuWjlpL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Bhy9DQAA.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-DAx9twnU.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dmdf8cGy.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-okMlvVrM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-W1IcAFff.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DXqCKe37.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CeIUkwBh.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-BP76BKTN.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-BpVI0OpF.js} +1 -1
- package/dist/worker/console/static/index.html +7 -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 +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- 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/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- 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 +339 -0
- package/dist/worker/console/workspace-context.js +260 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +94 -10
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- 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 +175 -1
- package/dist/worker/observe/static/styles.css +1039 -128
- package/dist/worker/observe/static/views/dag-inspector.js +1218 -60
- 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/session-timeline.js +75 -10
- package/dist/worker/observe/static/views/task.js +1 -1
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
- package/dist/workflows/dag/backend-test-writer-completeness.js +7 -12
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +1 -89
- package/dist/workflows/dag/init-hybrid.js +178 -47
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +112 -27
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +29 -4
- package/dist/workflows/dag/rerun-plan.js +272 -0
- package/dist/workflows/dag/rerun-run.js +390 -12
- package/dist/workflows/dag/skill-snapshot.js +1 -0
- package/dist/workflows/dag/types.js +203 -8
- package/docs/README.md +1 -0
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/agent-dag.schema.json +50 -0
- package/docs/templates/backend-test-dag.json +265 -237
- package/harness.json +3 -3
- package/package.json +5 -1
- package/skills/loop-agent/references/command-reference.md +6 -0
- 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
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- /package/dist/{worker → infrastructure}/console/operation-store.js +0 -0
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
* Mutation routes go through the Console mutation gate (boot token + Origin).
|
|
13
13
|
* Read routes (GET capabilities / sessions) are open to loopback.
|
|
14
14
|
*/
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
15
16
|
import { parseAssistantContent } from "./assistant-content.js";
|
|
16
17
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
|
|
17
18
|
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
18
19
|
import { cleanChatTitle, acceptedOperationIdFromResult } from "./session-store.js";
|
|
19
20
|
import { hashChatTurnPayload, projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
|
|
20
|
-
import { projectDeferredTurnSummary, } from "./deferred-turn.js";
|
|
21
|
+
import { projectDeferredTurnSummary, projectDeferredTurnSummaries, } from "./deferred-turn.js";
|
|
21
22
|
import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime.js";
|
|
22
23
|
import { ResourcePreferencesError, setSkillAutoInvocationPreference, } from "./resource-preferences-store.js";
|
|
23
24
|
import { ComposerDraftStore } from "./composer-draft-store.js";
|
|
@@ -33,9 +34,11 @@ import { dispatchOperatorAction } from "../operator-actions.js";
|
|
|
33
34
|
import { makeHumanGateCard, } from "./human-gate-card.js";
|
|
34
35
|
import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
|
|
35
36
|
import { projectTaskContext } from "./context-panel.js";
|
|
37
|
+
import { deriveChatSessionTelemetry } from "./session-stats.js";
|
|
36
38
|
import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
|
|
37
39
|
import { sendJson } from "../routes.js";
|
|
38
40
|
import { openSseResponse, writeSseEvent } from "../operation-sse.js";
|
|
41
|
+
import { buildTodoCompactionInstructions, projectCurrentTodos } from "./todos.js";
|
|
39
42
|
function turnSummary(turn) {
|
|
40
43
|
return {
|
|
41
44
|
turnId: turn.turnId,
|
|
@@ -52,7 +55,6 @@ function turnSummary(turn) {
|
|
|
52
55
|
function isTerminalTurnState(state) {
|
|
53
56
|
return state === "settled" || state === "aborted" || state === "failed";
|
|
54
57
|
}
|
|
55
|
-
const DEFAULT_ABORT_WAIT_MS = 2_500;
|
|
56
58
|
/** Parse Chat's `<epoch>:<seq>` SSE cursor. Legacy `<sessionId>:<seq>` has
|
|
57
59
|
* the same wire shape; its prefix intentionally fails the current process
|
|
58
60
|
* epoch comparison and triggers safe snapshot recovery (M5.1). */
|
|
@@ -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,53 @@ function turnEventToStorePartial(event) {
|
|
|
130
162
|
};
|
|
131
163
|
case "agent_settled":
|
|
132
164
|
return { kind: "agent_settled", data: {} };
|
|
133
|
-
case "
|
|
134
|
-
|
|
165
|
+
case "compaction_start":
|
|
166
|
+
case "compaction_end": {
|
|
167
|
+
const reason = event.reason === "threshold" || event.reason === "overflow"
|
|
168
|
+
? event.reason
|
|
169
|
+
: "manual";
|
|
170
|
+
const status = event.type === "compaction_start"
|
|
171
|
+
? "running"
|
|
172
|
+
: event.status === "aborted" || event.status === "failed"
|
|
173
|
+
? event.status
|
|
174
|
+
: "completed";
|
|
175
|
+
const data = {
|
|
176
|
+
schemaVersion: 1,
|
|
177
|
+
compactionId: String(event.compactionId ?? ""),
|
|
178
|
+
reason,
|
|
179
|
+
status,
|
|
180
|
+
...(typeof event.summary === "string"
|
|
181
|
+
? { summary: event.summary }
|
|
182
|
+
: {}),
|
|
183
|
+
...(typeof event.firstKeptEntryId === "string"
|
|
184
|
+
? { firstKeptEntryId: event.firstKeptEntryId }
|
|
185
|
+
: {}),
|
|
186
|
+
...(typeof event.tokensBefore === "number"
|
|
187
|
+
? { tokensBefore: event.tokensBefore }
|
|
188
|
+
: {}),
|
|
189
|
+
...(typeof event.estimatedTokensAfter === "number"
|
|
190
|
+
? { estimatedTokensAfter: event.estimatedTokensAfter }
|
|
191
|
+
: {}),
|
|
192
|
+
willRetry: Boolean(event.willRetry),
|
|
193
|
+
...(typeof event.errorMessage === "string"
|
|
194
|
+
? { errorMessage: event.errorMessage }
|
|
195
|
+
: {}),
|
|
196
|
+
};
|
|
197
|
+
return { kind: event.type, data };
|
|
198
|
+
}
|
|
199
|
+
case "error": {
|
|
200
|
+
const content = parseAssistantContent(event.content);
|
|
201
|
+
return {
|
|
202
|
+
kind: "error",
|
|
203
|
+
data: {
|
|
204
|
+
message: String(event.message ?? ""),
|
|
205
|
+
...(content ? { content } : {}),
|
|
206
|
+
...(event.providerError && typeof event.providerError === "object"
|
|
207
|
+
? { providerError: event.providerError }
|
|
208
|
+
: {}),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
135
212
|
default:
|
|
136
213
|
return undefined;
|
|
137
214
|
}
|
|
@@ -197,6 +274,13 @@ function ensureAgentEndEvent(deps, sessionId, turnId) {
|
|
|
197
274
|
}
|
|
198
275
|
}
|
|
199
276
|
async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
|
|
277
|
+
// AC-R5-003: the multi-workspace dispatcher (server.ts readBodyOnce)
|
|
278
|
+
// pre-reads and JSON-parses the body for workspaceId resolution, stashing
|
|
279
|
+
// it on req.__preReadBody. Consume it first so the body is parsed exactly
|
|
280
|
+
// once per request; re-reading the consumed stream would yield {}.
|
|
281
|
+
const preRead = req.__preReadBody;
|
|
282
|
+
if (preRead !== undefined)
|
|
283
|
+
return preRead;
|
|
200
284
|
const chunks = [];
|
|
201
285
|
let total = 0;
|
|
202
286
|
for await (const chunk of req) {
|
|
@@ -344,7 +428,10 @@ export async function handleChatCapabilities(_req, res, deps) {
|
|
|
344
428
|
hasBash: true,
|
|
345
429
|
});
|
|
346
430
|
}
|
|
347
|
-
export async function handleCreateChatSession(req, res, deps
|
|
431
|
+
export async function handleCreateChatSession(req, res, deps,
|
|
432
|
+
/** Explicit owning workspaceId from the multi-workspace dispatcher
|
|
433
|
+
* (AC-R7-002). The body still parses exactly once via req.__preReadBody. */
|
|
434
|
+
workspaceId) {
|
|
348
435
|
const gate = gateMutation(req, deps);
|
|
349
436
|
if (!gate.ok) {
|
|
350
437
|
sendJson(res, gate.status, {
|
|
@@ -422,6 +509,10 @@ export async function handleCreateChatSession(req, res, deps) {
|
|
|
422
509
|
sessionFile: handle.sessionFile,
|
|
423
510
|
modelProvider: handle.model?.provider,
|
|
424
511
|
modelId: handle.model?.modelId,
|
|
512
|
+
// AC-R7-002: explicit parameter from the dispatcher — the runtime
|
|
513
|
+
// store.create wrapper is gone; body workspaceId (if any) only acts as
|
|
514
|
+
// a fallback for direct (non-dispatcher) handler tests.
|
|
515
|
+
workspaceId: workspaceId ?? (typeof body.workspaceId === "string" && body.workspaceId ? body.workspaceId : undefined),
|
|
425
516
|
});
|
|
426
517
|
if (snapshot)
|
|
427
518
|
deps.events.append(handle.sessionId, `${handle.sessionId}:snapshot`, {
|
|
@@ -646,8 +737,10 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
|
|
|
646
737
|
await deps.operationLinker.recoverSession(sessionId);
|
|
647
738
|
const composerDraft = await new ComposerDraftStore(deps.appData).get(sessionId);
|
|
648
739
|
const snapshot = deps.events.snapshot(sessionId);
|
|
740
|
+
const telemetry = deriveChatSessionTelemetry(snapshot);
|
|
649
741
|
const lastEvent = snapshot.at(-1);
|
|
650
742
|
const activeTurn = deps.events.getActiveTurn(sessionId);
|
|
743
|
+
const pendingQuestion = await deps.questionRegistry?.get(sessionId);
|
|
651
744
|
sendJson(res, 200, {
|
|
652
745
|
ok: true,
|
|
653
746
|
eventEpoch: deps.events.epoch(sessionId),
|
|
@@ -660,8 +753,22 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
|
|
|
660
753
|
? "stopping"
|
|
661
754
|
: "running"
|
|
662
755
|
: "idle",
|
|
756
|
+
pendingQuestion: pendingQuestion ?? null,
|
|
757
|
+
todos: projectCurrentTodos(snapshot) ?? [],
|
|
663
758
|
},
|
|
664
759
|
hasBash: true,
|
|
760
|
+
sessionStats: telemetry.stats,
|
|
761
|
+
usage: telemetry.usage,
|
|
762
|
+
compactions: snapshot
|
|
763
|
+
.filter((event) => event.kind === "compact" ||
|
|
764
|
+
event.kind === "compaction_start" ||
|
|
765
|
+
event.kind === "compaction_end")
|
|
766
|
+
.map((event) => ({
|
|
767
|
+
eventId: event.eventId,
|
|
768
|
+
kind: event.kind,
|
|
769
|
+
at: event.at,
|
|
770
|
+
data: event.data,
|
|
771
|
+
})),
|
|
665
772
|
lastEventId: lastEvent?.eventId ?? null,
|
|
666
773
|
lastEventSeq: lastEvent?.seq ?? 0,
|
|
667
774
|
});
|
|
@@ -769,11 +876,12 @@ export async function handlePutDeferredTurn(req, res, deps, sessionId) {
|
|
|
769
876
|
: "待发送消息无效或会话不存在",
|
|
770
877
|
},
|
|
771
878
|
deferredTurn: result.deferredTurn ?? null,
|
|
879
|
+
deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
|
|
772
880
|
summary: projectDeferredTurnSummary(result.deferredTurn),
|
|
773
881
|
});
|
|
774
882
|
return;
|
|
775
883
|
}
|
|
776
|
-
emitDeferredTurnUpdate(deps, sessionId, blockedByTurnId, result.deferredTurn, result.deferredTurn.revision);
|
|
884
|
+
await emitDeferredTurnUpdate(deps, sessionId, blockedByTurnId, result.deferredTurn, result.deferredTurn.revision);
|
|
777
885
|
sendJson(res, 200, {
|
|
778
886
|
ok: true,
|
|
779
887
|
accepted: true,
|
|
@@ -781,6 +889,7 @@ export async function handlePutDeferredTurn(req, res, deps, sessionId) {
|
|
|
781
889
|
state: result.deferredTurn.state,
|
|
782
890
|
revision: result.deferredTurn.revision,
|
|
783
891
|
deferredTurn: result.deferredTurn,
|
|
892
|
+
deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
|
|
784
893
|
summary: projectDeferredTurnSummary(result.deferredTurn),
|
|
785
894
|
});
|
|
786
895
|
void dispatchDeferredTurn(deps, sessionId);
|
|
@@ -826,18 +935,90 @@ export async function handleDeleteDeferredTurn(req, res, deps, sessionId) {
|
|
|
826
935
|
: "待发送消息已变化,请刷新后重试",
|
|
827
936
|
},
|
|
828
937
|
deferredTurn: result.deferredTurn ?? null,
|
|
938
|
+
deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
|
|
829
939
|
});
|
|
830
940
|
return;
|
|
831
941
|
}
|
|
832
942
|
const clearedRevision = result.deferredTurn.revision + 1;
|
|
833
|
-
emitDeferredTurnUpdate(deps, sessionId, result.deferredTurn.blockedByTurnId, undefined, clearedRevision);
|
|
943
|
+
await emitDeferredTurnUpdate(deps, sessionId, result.deferredTurn.blockedByTurnId, undefined, clearedRevision);
|
|
834
944
|
sendJson(res, 200, {
|
|
835
945
|
ok: true,
|
|
836
946
|
cancelled: true,
|
|
837
947
|
deferredTurn: result.deferredTurn,
|
|
948
|
+
deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
|
|
838
949
|
revision: clearedRevision,
|
|
839
950
|
});
|
|
840
951
|
}
|
|
952
|
+
/** Move one addressed FIFO item into Pi's live steering queue. */
|
|
953
|
+
export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientRequestId) {
|
|
954
|
+
const gate = gateMutation(req, deps);
|
|
955
|
+
if (!gate.ok) {
|
|
956
|
+
sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
let body = {};
|
|
960
|
+
try {
|
|
961
|
+
body = await readJsonBody(req);
|
|
962
|
+
}
|
|
963
|
+
catch (error) {
|
|
964
|
+
sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: error instanceof Error ? error.message : String(error) } });
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const expectedRevision = typeof body.expectedRevision === "number" && Number.isSafeInteger(body.expectedRevision)
|
|
968
|
+
? body.expectedRevision
|
|
969
|
+
: -1;
|
|
970
|
+
const active = deps.events.getActiveTurn(sessionId);
|
|
971
|
+
if (!active) {
|
|
972
|
+
sendJson(res, 409, { ok: false, error: { code: "TURN_NOT_ACTIVE", message: "当前没有可插话的运行中回合" } });
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const claim = await deps.store.claimDeferredTurn(sessionId, expectedRevision, clientRequestId);
|
|
976
|
+
if (!claim.ok) {
|
|
977
|
+
sendJson(res, claim.code === "NOT_FOUND" ? 404 : 409, {
|
|
978
|
+
ok: false,
|
|
979
|
+
error: { code: claim.code, message: "排队消息已变化,请刷新后重试" },
|
|
980
|
+
});
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const item = claim.deferredTurn;
|
|
984
|
+
try {
|
|
985
|
+
await deps.runtime.steer(sessionId, item.text, item.images);
|
|
986
|
+
const userMessage = await deps.store.appendMessage(sessionId, {
|
|
987
|
+
role: "user",
|
|
988
|
+
text: item.text,
|
|
989
|
+
});
|
|
990
|
+
deps.events.append(sessionId, active.turnId, {
|
|
991
|
+
kind: "user_message",
|
|
992
|
+
data: {
|
|
993
|
+
clientRequestId: item.clientRequestId,
|
|
994
|
+
message: { id: userMessage.id, text: userMessage.text, at: userMessage.at },
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
await deps.store.clearDeferredTurnForRequest(sessionId, item.clientRequestId);
|
|
998
|
+
await emitDeferredTurnUpdate(deps, sessionId, active.turnId, undefined, item.revision + 2);
|
|
999
|
+
sendJson(res, 200, {
|
|
1000
|
+
ok: true,
|
|
1001
|
+
accepted: true,
|
|
1002
|
+
deferredTurns: projectDeferredTurnSummaries(await deps.store.listDeferredTurns(sessionId)),
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
const restored = await deps.store.transitionDeferredTurn(sessionId, {
|
|
1007
|
+
expectedRevision: item.revision,
|
|
1008
|
+
clientRequestId: item.clientRequestId,
|
|
1009
|
+
state: "queued",
|
|
1010
|
+
});
|
|
1011
|
+
if (restored)
|
|
1012
|
+
await emitDeferredTurnUpdate(deps, sessionId, active.turnId, restored, restored.revision);
|
|
1013
|
+
sendJson(res, 409, {
|
|
1014
|
+
ok: false,
|
|
1015
|
+
error: {
|
|
1016
|
+
code: "STEER_FAILED",
|
|
1017
|
+
message: error instanceof Error ? error.message : "插话发送失败",
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
841
1022
|
/** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
|
|
842
1023
|
* get_state equivalent): exposes the active turn + the latest turn's durable
|
|
843
1024
|
* outcome WITHOUT the full message payload, so the UI can poll it on an
|
|
@@ -865,6 +1046,7 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
|
|
|
865
1046
|
isBashRunning: false,
|
|
866
1047
|
};
|
|
867
1048
|
const runtimeBusy = Object.values(runtimeFacts).some((value) => value === true);
|
|
1049
|
+
const pendingQuestion = await deps.questionRegistry?.get(sessionId);
|
|
868
1050
|
const turnPhase = active
|
|
869
1051
|
? active.abortRequestedAt
|
|
870
1052
|
? "stopping"
|
|
@@ -884,10 +1066,60 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
|
|
|
884
1066
|
latestTurn: latest ? turnSummary(latest) : null,
|
|
885
1067
|
runtime: runtimeFacts,
|
|
886
1068
|
turnPhase,
|
|
887
|
-
deferredTurn: projectDeferredTurnSummary(record.
|
|
1069
|
+
deferredTurn: projectDeferredTurnSummary(record.deferredTurns?.[0]),
|
|
1070
|
+
deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
|
|
1071
|
+
pendingQuestion: pendingQuestion ?? null,
|
|
1072
|
+
todos: projectCurrentTodos(deps.events.snapshot(sessionId)) ?? [],
|
|
888
1073
|
},
|
|
889
1074
|
});
|
|
890
1075
|
}
|
|
1076
|
+
export async function handleAnswerUserQuestion(req, res, deps, sessionId, requestId) {
|
|
1077
|
+
const gate = gateMutation(req, deps);
|
|
1078
|
+
if (!gate.ok) {
|
|
1079
|
+
sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
1083
|
+
return;
|
|
1084
|
+
if (!deps.questionRegistry) {
|
|
1085
|
+
sendJson(res, 503, { ok: false, error: { code: "USER_QUESTIONS_UNAVAILABLE", message: "user question capability is unavailable" } });
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
try {
|
|
1089
|
+
const body = await readJsonBody(req);
|
|
1090
|
+
const question = await deps.questionRegistry.answer(sessionId, requestId, body);
|
|
1091
|
+
sendJson(res, 200, { ok: true, resolved: true, question });
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1095
|
+
const status = message === "USER_QUESTION_NOT_FOUND" ? 404 : message === "USER_QUESTION_STALE" ? 409 : 400;
|
|
1096
|
+
sendJson(res, status, { ok: false, error: { code: message, message } });
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
export async function handleCancelUserQuestion(req, res, deps, sessionId, requestId) {
|
|
1100
|
+
const gate = gateMutation(req, deps);
|
|
1101
|
+
if (!gate.ok) {
|
|
1102
|
+
sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
1106
|
+
return;
|
|
1107
|
+
if (!deps.questionRegistry) {
|
|
1108
|
+
sendJson(res, 503, { ok: false, error: { code: "USER_QUESTIONS_UNAVAILABLE", message: "user question capability is unavailable" } });
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
try {
|
|
1112
|
+
const body = await readJsonBody(req).catch(() => ({}));
|
|
1113
|
+
const reason = typeof body.reason === "string" && body.reason.trim() ? body.reason.trim().slice(0, 120) : "USER_CANCELLED";
|
|
1114
|
+
const question = await deps.questionRegistry.cancel(sessionId, requestId, reason);
|
|
1115
|
+
sendJson(res, 200, { ok: true, cancelled: true, question });
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1119
|
+
const status = message === "USER_QUESTION_NOT_FOUND" ? 404 : 409;
|
|
1120
|
+
sendJson(res, status, { ok: false, error: { code: message, message } });
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
891
1123
|
export async function handleListChatSessions(_req, res, deps) {
|
|
892
1124
|
const sessions = await deps.store.list({ includeArchived: true });
|
|
893
1125
|
sendJson(res, 200, {
|
|
@@ -903,7 +1135,8 @@ export async function handleListChatSessions(_req, res, deps) {
|
|
|
903
1135
|
: active
|
|
904
1136
|
? "running"
|
|
905
1137
|
: "idle",
|
|
906
|
-
deferredTurn: projectDeferredTurnSummary(session.
|
|
1138
|
+
deferredTurn: projectDeferredTurnSummary(session.deferredTurns?.[0]),
|
|
1139
|
+
deferredTurns: projectDeferredTurnSummaries(session.deferredTurns ?? []),
|
|
907
1140
|
};
|
|
908
1141
|
}),
|
|
909
1142
|
hasBash: true,
|
|
@@ -1150,6 +1383,7 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
|
1150
1383
|
store: deps.store,
|
|
1151
1384
|
actionContext,
|
|
1152
1385
|
sessionId,
|
|
1386
|
+
turnId,
|
|
1153
1387
|
text,
|
|
1154
1388
|
images,
|
|
1155
1389
|
signal: ac.signal,
|
|
@@ -1445,6 +1679,7 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
|
|
|
1445
1679
|
store: deps.store,
|
|
1446
1680
|
actionContext,
|
|
1447
1681
|
sessionId,
|
|
1682
|
+
turnId: turn.turnId,
|
|
1448
1683
|
text,
|
|
1449
1684
|
images,
|
|
1450
1685
|
signal: controller.signal,
|
|
@@ -1530,12 +1765,16 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
|
|
|
1530
1765
|
}
|
|
1531
1766
|
})();
|
|
1532
1767
|
}
|
|
1533
|
-
function emitDeferredTurnUpdate(deps, sessionId, turnId,
|
|
1768
|
+
async function emitDeferredTurnUpdate(deps, sessionId, turnId, _deferredTurn, _revision) {
|
|
1769
|
+
const record = await deps.store.get(sessionId);
|
|
1770
|
+
if (!record)
|
|
1771
|
+
return;
|
|
1534
1772
|
deps.events.append(sessionId, turnId, {
|
|
1535
1773
|
kind: "queue_update",
|
|
1536
1774
|
data: {
|
|
1537
|
-
deferredTurn: projectDeferredTurnSummary(
|
|
1538
|
-
|
|
1775
|
+
deferredTurn: projectDeferredTurnSummary(record.deferredTurns?.[0]),
|
|
1776
|
+
deferredTurns: projectDeferredTurnSummaries(record.deferredTurns ?? []),
|
|
1777
|
+
revision: record.deferredQueueRevision ?? 0,
|
|
1539
1778
|
},
|
|
1540
1779
|
});
|
|
1541
1780
|
}
|
|
@@ -1571,7 +1810,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
|
|
|
1571
1810
|
deferredDispatchingSessions.add(sessionId);
|
|
1572
1811
|
try {
|
|
1573
1812
|
let record = await deps.store.get(sessionId);
|
|
1574
|
-
let deferred = record?.
|
|
1813
|
+
let deferred = record?.deferredTurns?.find((item) => item.state === "dispatching" || item.state === "queued");
|
|
1575
1814
|
if (!deferred)
|
|
1576
1815
|
return;
|
|
1577
1816
|
if (deferred.state === "accepted")
|
|
@@ -1583,17 +1822,19 @@ export async function dispatchDeferredTurn(deps, sessionId) {
|
|
|
1583
1822
|
if (existing) {
|
|
1584
1823
|
const accepted = await deps.store.transitionDeferredTurn(sessionId, {
|
|
1585
1824
|
expectedRevision: deferred.revision,
|
|
1825
|
+
clientRequestId: deferred.clientRequestId,
|
|
1586
1826
|
state: "accepted",
|
|
1587
1827
|
acceptedTurnId: existing.turnId,
|
|
1588
1828
|
});
|
|
1589
1829
|
if (accepted)
|
|
1590
|
-
emitDeferredTurnUpdate(deps, sessionId, existing.turnId, accepted, accepted.revision);
|
|
1830
|
+
await emitDeferredTurnUpdate(deps, sessionId, existing.turnId, accepted, accepted.revision);
|
|
1591
1831
|
return;
|
|
1592
1832
|
}
|
|
1593
1833
|
if (deps.events.getActiveTurn(sessionId))
|
|
1594
1834
|
return;
|
|
1595
1835
|
const requeued = await deps.store.transitionDeferredTurn(sessionId, {
|
|
1596
1836
|
expectedRevision: deferred.revision,
|
|
1837
|
+
clientRequestId: deferred.clientRequestId,
|
|
1597
1838
|
state: "queued",
|
|
1598
1839
|
});
|
|
1599
1840
|
if (!requeued)
|
|
@@ -1604,11 +1845,11 @@ export async function dispatchDeferredTurn(deps, sessionId) {
|
|
|
1604
1845
|
const runtimeBusy = Boolean(runtimeFacts && Object.values(runtimeFacts).some((value) => value === true));
|
|
1605
1846
|
if (deps.events.getActiveTurn(sessionId) || runtimeBusy)
|
|
1606
1847
|
return;
|
|
1607
|
-
const claim = await deps.store.claimDeferredTurn(sessionId, deferred.revision);
|
|
1848
|
+
const claim = await deps.store.claimDeferredTurn(sessionId, deferred.revision, deferred.clientRequestId);
|
|
1608
1849
|
if (!claim.ok)
|
|
1609
1850
|
return;
|
|
1610
1851
|
deferred = claim.deferredTurn;
|
|
1611
|
-
emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, deferred, deferred.revision);
|
|
1852
|
+
await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, deferred, deferred.revision);
|
|
1612
1853
|
const started = await startTrustedChatTurn(deps, sessionId, {
|
|
1613
1854
|
clientRequestId: deferred.clientRequestId,
|
|
1614
1855
|
text: deferred.text,
|
|
@@ -1627,7 +1868,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
|
|
|
1627
1868
|
});
|
|
1628
1869
|
const cleared = await deps.store.clearDeferredTurnForRequest(sessionId, deferred.clientRequestId);
|
|
1629
1870
|
if (cleared)
|
|
1630
|
-
emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, cleared.clearedRevision);
|
|
1871
|
+
await emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, cleared.clearedRevision);
|
|
1631
1872
|
},
|
|
1632
1873
|
});
|
|
1633
1874
|
const accepted = started.body.accepted === true;
|
|
@@ -1637,11 +1878,12 @@ export async function dispatchDeferredTurn(deps, sessionId) {
|
|
|
1637
1878
|
if (accepted && turn?.turnId) {
|
|
1638
1879
|
const next = await deps.store.transitionDeferredTurn(sessionId, {
|
|
1639
1880
|
expectedRevision: deferred.revision,
|
|
1881
|
+
clientRequestId: deferred.clientRequestId,
|
|
1640
1882
|
state: "accepted",
|
|
1641
1883
|
acceptedTurnId: turn.turnId,
|
|
1642
1884
|
});
|
|
1643
1885
|
if (next)
|
|
1644
|
-
emitDeferredTurnUpdate(deps, sessionId, turn.turnId, next, next.revision);
|
|
1886
|
+
await emitDeferredTurnUpdate(deps, sessionId, turn.turnId, next, next.revision);
|
|
1645
1887
|
return;
|
|
1646
1888
|
}
|
|
1647
1889
|
const error = started.body.error && typeof started.body.error === "object"
|
|
@@ -1650,14 +1892,16 @@ export async function dispatchDeferredTurn(deps, sessionId) {
|
|
|
1650
1892
|
if (error.code === "TURN_ACTIVE") {
|
|
1651
1893
|
const next = await deps.store.transitionDeferredTurn(sessionId, {
|
|
1652
1894
|
expectedRevision: deferred.revision,
|
|
1895
|
+
clientRequestId: deferred.clientRequestId,
|
|
1653
1896
|
state: "queued",
|
|
1654
1897
|
});
|
|
1655
1898
|
if (next)
|
|
1656
|
-
emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, next, next.revision);
|
|
1899
|
+
await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, next, next.revision);
|
|
1657
1900
|
return;
|
|
1658
1901
|
}
|
|
1659
1902
|
const failed = await deps.store.transitionDeferredTurn(sessionId, {
|
|
1660
1903
|
expectedRevision: deferred.revision,
|
|
1904
|
+
clientRequestId: deferred.clientRequestId,
|
|
1661
1905
|
state: "failed",
|
|
1662
1906
|
lastError: {
|
|
1663
1907
|
code: error.code ?? `HTTP_${started.status}`,
|
|
@@ -1665,17 +1909,18 @@ export async function dispatchDeferredTurn(deps, sessionId) {
|
|
|
1665
1909
|
},
|
|
1666
1910
|
});
|
|
1667
1911
|
if (failed)
|
|
1668
|
-
emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, failed, failed.revision);
|
|
1912
|
+
await emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, failed, failed.revision);
|
|
1669
1913
|
}
|
|
1670
1914
|
catch (error) {
|
|
1671
1915
|
// Keep fire-and-forget callers safe and make a dispatch failure visible and
|
|
1672
1916
|
// recoverable from the composer instead of leaving the slot indefinitely
|
|
1673
1917
|
// in an ambiguous queued/dispatching state.
|
|
1674
1918
|
try {
|
|
1675
|
-
const current = (await deps.store.get(sessionId))?.
|
|
1919
|
+
const current = (await deps.store.get(sessionId))?.deferredTurns?.find((item) => item.state === "queued" || item.state === "dispatching");
|
|
1676
1920
|
if (current && (current.state === "queued" || current.state === "dispatching")) {
|
|
1677
1921
|
const failed = await deps.store.transitionDeferredTurn(sessionId, {
|
|
1678
1922
|
expectedRevision: current.revision,
|
|
1923
|
+
clientRequestId: current.clientRequestId,
|
|
1679
1924
|
state: "failed",
|
|
1680
1925
|
lastError: {
|
|
1681
1926
|
code: "DEFERRED_TURN_DISPATCH_FAILED",
|
|
@@ -1683,7 +1928,7 @@ export async function dispatchDeferredTurn(deps, sessionId) {
|
|
|
1683
1928
|
},
|
|
1684
1929
|
});
|
|
1685
1930
|
if (failed)
|
|
1686
|
-
emitDeferredTurnUpdate(deps, sessionId, failed.blockedByTurnId, failed, failed.revision);
|
|
1931
|
+
await emitDeferredTurnUpdate(deps, sessionId, failed.blockedByTurnId, failed, failed.revision);
|
|
1687
1932
|
}
|
|
1688
1933
|
}
|
|
1689
1934
|
catch {
|
|
@@ -1698,12 +1943,9 @@ export async function dispatchDeferredTurn(deps, sessionId) {
|
|
|
1698
1943
|
export async function recoverDeferredTurns(deps) {
|
|
1699
1944
|
const sessions = await deps.store.list({ includeArchived: false });
|
|
1700
1945
|
await Promise.all(sessions
|
|
1701
|
-
.filter((session) => session.
|
|
1702
|
-
? session.deferredTurn.state === "queued" ||
|
|
1703
|
-
session.deferredTurn.state === "dispatching"
|
|
1704
|
-
: false)
|
|
1946
|
+
.filter((session) => (session.deferredTurns ?? []).some((item) => item.state === "queued" || item.state === "dispatching"))
|
|
1705
1947
|
.map(async (session) => {
|
|
1706
|
-
const deferred = session.
|
|
1948
|
+
const deferred = session.deferredTurns.find((item) => item.state === "dispatching" || item.state === "queued");
|
|
1707
1949
|
const active = deps.events.getActiveTurn(session.sessionId);
|
|
1708
1950
|
// A process restart cannot retain AbortController ownership. When the
|
|
1709
1951
|
// durable blocker was already stopping and no execution owns it now,
|
|
@@ -1770,34 +2012,24 @@ export async function handleAbortChatTurn(req, res, deps, sessionId, turnId) {
|
|
|
1770
2012
|
});
|
|
1771
2013
|
return;
|
|
1772
2014
|
}
|
|
1773
|
-
deps.events.markAbortRequested?.(sessionId, turnId);
|
|
2015
|
+
const stopping = deps.events.markAbortRequested?.(sessionId, turnId) ?? turn;
|
|
2016
|
+
// DSH-style acknowledgement: deliver the AbortSignal synchronously, then let
|
|
2017
|
+
// the detached Turn closeout own SDK/process cleanup and terminal persistence.
|
|
2018
|
+
// The HTTP request reports that Stop was accepted; it does not wait for Pi to
|
|
2019
|
+
// become idle. This keeps cancellation latency independent of slow providers,
|
|
2020
|
+
// tools and process-tree cleanup while preserving the active-Turn lease.
|
|
1774
2021
|
if (!execution.controller.signal.aborted)
|
|
1775
2022
|
execution.controller.abort();
|
|
1776
|
-
const
|
|
1777
|
-
if (
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
...turn,
|
|
1781
|
-
state: waited.outcome.state,
|
|
1782
|
-
};
|
|
1783
|
-
sendJson(res, 200, {
|
|
1784
|
-
ok: true,
|
|
1785
|
-
accepted: true,
|
|
1786
|
-
idempotent: false,
|
|
1787
|
-
turn: turnSummary(finalTurn),
|
|
1788
|
-
});
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
// Timeout or missing completion: keep non-terminal; client reconciles via /state.
|
|
1792
|
-
const pending = deps.events.getTurn(sessionId, turnId) ?? turn;
|
|
2023
|
+
const questionCancellation = deps.questionRegistry?.cancelForSession(sessionId, "TURN_ABORTED");
|
|
2024
|
+
if (questionCancellation)
|
|
2025
|
+
void questionCancellation.catch(() => undefined);
|
|
2026
|
+
const acceptedTurn = deps.events.getTurn(sessionId, turnId) ?? stopping;
|
|
1793
2027
|
sendJson(res, 202, {
|
|
1794
|
-
ok:
|
|
1795
|
-
accepted:
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
},
|
|
1800
|
-
turn: turnSummary(pending),
|
|
2028
|
+
ok: true,
|
|
2029
|
+
accepted: true,
|
|
2030
|
+
idempotent: false,
|
|
2031
|
+
cleanupPending: !isTerminalTurnState(acceptedTurn.state),
|
|
2032
|
+
turn: turnSummary(acceptedTurn),
|
|
1801
2033
|
});
|
|
1802
2034
|
}
|
|
1803
2035
|
export async function handleGetChatTurn(_req, res, deps, sessionId, turnId) {
|
|
@@ -1921,6 +2153,8 @@ export async function handleDeleteChatSession(req, res, deps, sessionId) {
|
|
|
1921
2153
|
});
|
|
1922
2154
|
return;
|
|
1923
2155
|
}
|
|
2156
|
+
await deps.questionRegistry?.cancelForSession(sessionId, "SESSION_DELETED").catch(() => undefined);
|
|
2157
|
+
deps.todoRegistry?.clear(sessionId);
|
|
1924
2158
|
deps.runtime.dispose(sessionId);
|
|
1925
2159
|
deps.operationLinker.clearSession(sessionId);
|
|
1926
2160
|
deps.events.clear(sessionId);
|
|
@@ -2595,6 +2829,36 @@ async function handleLinkWorkspaceOperation(req, res, deps, sessionId) {
|
|
|
2595
2829
|
});
|
|
2596
2830
|
}
|
|
2597
2831
|
}
|
|
2832
|
+
async function handleUnlinkWorkspaceOperation(req, res, deps, sessionId, operationId) {
|
|
2833
|
+
const gate = gateMutation(req, deps);
|
|
2834
|
+
if (!gate.ok) {
|
|
2835
|
+
sendJson(res, gate.status, {
|
|
2836
|
+
ok: false,
|
|
2837
|
+
error: { code: gate.code, message: gate.message },
|
|
2838
|
+
});
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
if (!(await requireChatSession(res, deps, sessionId)))
|
|
2842
|
+
return;
|
|
2843
|
+
// AC-R7-001 (2026-08-23): removing a workspace-linked DAG association is
|
|
2844
|
+
// idempotent — a missing link is already-removed (200), never an error.
|
|
2845
|
+
try {
|
|
2846
|
+
const removed = await deps.operationLinker.unlink({
|
|
2847
|
+
sessionId,
|
|
2848
|
+
operationId,
|
|
2849
|
+
});
|
|
2850
|
+
sendJson(res, 200, { ok: true, removed });
|
|
2851
|
+
}
|
|
2852
|
+
catch (error) {
|
|
2853
|
+
sendJson(res, 400, {
|
|
2854
|
+
ok: false,
|
|
2855
|
+
error: {
|
|
2856
|
+
code: "INVALID_INPUT",
|
|
2857
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2858
|
+
},
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2598
2862
|
async function handleStartInterview(req, res, deps, sessionId) {
|
|
2599
2863
|
const gate = gateMutation(req, deps);
|
|
2600
2864
|
if (!gate.ok) {
|
|
@@ -3247,14 +3511,59 @@ export async function handleChatCompact(req, res, deps, sessionId) {
|
|
|
3247
3511
|
return;
|
|
3248
3512
|
}
|
|
3249
3513
|
const rawInstructions = typeof body.instructions === "string" ? body.instructions : undefined;
|
|
3250
|
-
const
|
|
3514
|
+
const currentTodos = projectCurrentTodos(deps.events.snapshot(sessionId));
|
|
3515
|
+
const customInstructions = buildTodoCompactionInstructions(rawInstructions, currentTodos);
|
|
3516
|
+
const turnId = deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`;
|
|
3517
|
+
const compactionId = `${sessionId}:compaction:${randomUUID()}`;
|
|
3518
|
+
deps.events.append(sessionId, turnId, {
|
|
3519
|
+
kind: "compaction_start",
|
|
3520
|
+
data: {
|
|
3521
|
+
schemaVersion: 1,
|
|
3522
|
+
compactionId,
|
|
3523
|
+
reason: "manual",
|
|
3524
|
+
status: "running",
|
|
3525
|
+
willRetry: false,
|
|
3526
|
+
},
|
|
3527
|
+
});
|
|
3251
3528
|
try {
|
|
3252
3529
|
const snapshot = await deps.runtime.compact(sessionId, customInstructions);
|
|
3253
|
-
const event = deps.events.append(sessionId,
|
|
3530
|
+
const event = deps.events.append(sessionId, turnId, {
|
|
3531
|
+
kind: "compaction_end",
|
|
3532
|
+
data: {
|
|
3533
|
+
schemaVersion: 1,
|
|
3534
|
+
compactionId,
|
|
3535
|
+
reason: "manual",
|
|
3536
|
+
status: "completed",
|
|
3537
|
+
summary: snapshot.summary,
|
|
3538
|
+
...(snapshot.firstKeptEntryId
|
|
3539
|
+
? { firstKeptEntryId: snapshot.firstKeptEntryId }
|
|
3540
|
+
: {}),
|
|
3541
|
+
...(snapshot.tokensBefore === undefined
|
|
3542
|
+
? {}
|
|
3543
|
+
: { tokensBefore: snapshot.tokensBefore }),
|
|
3544
|
+
...(snapshot.estimatedTokensAfter === undefined
|
|
3545
|
+
? {}
|
|
3546
|
+
: { estimatedTokensAfter: snapshot.estimatedTokensAfter }),
|
|
3547
|
+
willRetry: false,
|
|
3548
|
+
},
|
|
3549
|
+
});
|
|
3254
3550
|
sendJson(res, 200, { ok: true, snapshot, eventId: event.eventId });
|
|
3255
3551
|
}
|
|
3256
3552
|
catch (error) {
|
|
3257
3553
|
const classified = classifyChatCompactionFailure(error);
|
|
3554
|
+
const aborted = error instanceof Error &&
|
|
3555
|
+
(error.name === "AbortError" || /cancel(?:led|ed)|aborted/i.test(error.message));
|
|
3556
|
+
deps.events.append(sessionId, turnId, {
|
|
3557
|
+
kind: "compaction_end",
|
|
3558
|
+
data: {
|
|
3559
|
+
schemaVersion: 1,
|
|
3560
|
+
compactionId,
|
|
3561
|
+
reason: "manual",
|
|
3562
|
+
status: aborted ? "aborted" : "failed",
|
|
3563
|
+
willRetry: false,
|
|
3564
|
+
...(aborted ? {} : { errorMessage: classified.message }),
|
|
3565
|
+
},
|
|
3566
|
+
});
|
|
3258
3567
|
sendJson(res, classified.status, {
|
|
3259
3568
|
ok: false,
|
|
3260
3569
|
error: { code: classified.code, message: classified.message },
|
|
@@ -3283,8 +3592,10 @@ export async function handleChatMessages(res, deps, sessionId, url) {
|
|
|
3283
3592
|
/**
|
|
3284
3593
|
* Dispatch a chat HTTP request. Returns true if handled.
|
|
3285
3594
|
* Mounted under /api/operator/v1/chat/*.
|
|
3595
|
+
* workspaceScope (AC-R7-002): the explicit owning workspaceId for session
|
|
3596
|
+
* create, forwarded to handleCreateChatSession → store.create.
|
|
3286
3597
|
*/
|
|
3287
|
-
export async function handleChatRequest(req, res, deps, pathname) {
|
|
3598
|
+
export async function handleChatRequest(req, res, deps, pathname, workspaceScope) {
|
|
3288
3599
|
if (!deps)
|
|
3289
3600
|
return false;
|
|
3290
3601
|
if (!pathname.startsWith("/api/operator/v1/chat/") &&
|
|
@@ -3362,6 +3673,16 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
3362
3673
|
await handleStartInterview(req, res, deps, decodeURIComponent(interviewStart[1]));
|
|
3363
3674
|
return true;
|
|
3364
3675
|
}
|
|
3676
|
+
const userQuestionAnswer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/questions\/([^/]+)\/answer$/);
|
|
3677
|
+
if (method === "POST" && userQuestionAnswer) {
|
|
3678
|
+
await handleAnswerUserQuestion(req, res, deps, decodeURIComponent(userQuestionAnswer[1]), decodeURIComponent(userQuestionAnswer[2]));
|
|
3679
|
+
return true;
|
|
3680
|
+
}
|
|
3681
|
+
const userQuestionCancel = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/questions\/([^/]+)\/cancel$/);
|
|
3682
|
+
if (method === "POST" && userQuestionCancel) {
|
|
3683
|
+
await handleCancelUserQuestion(req, res, deps, decodeURIComponent(userQuestionCancel[1]), decodeURIComponent(userQuestionCancel[2]));
|
|
3684
|
+
return true;
|
|
3685
|
+
}
|
|
3365
3686
|
const interviewAnswer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/interviews\/([^/]+)\/answers$/);
|
|
3366
3687
|
if (method === "POST" && interviewAnswer) {
|
|
3367
3688
|
await handleAnswerInterview(req, res, deps, decodeURIComponent(interviewAnswer[1]), decodeURIComponent(interviewAnswer[2]));
|
|
@@ -3468,6 +3789,11 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
3468
3789
|
await handleDeleteDeferredTurn(req, res, deps, decodeURIComponent(deferredTurn[1]));
|
|
3469
3790
|
return true;
|
|
3470
3791
|
}
|
|
3792
|
+
const deferredTurnSteer = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/deferred-turn\/([^/]+)\/steer$/);
|
|
3793
|
+
if (method === "POST" && deferredTurnSteer) {
|
|
3794
|
+
await handleSteerDeferredTurn(req, res, deps, decodeURIComponent(deferredTurnSteer[1]), decodeURIComponent(deferredTurnSteer[2]));
|
|
3795
|
+
return true;
|
|
3796
|
+
}
|
|
3471
3797
|
const sessionTurnAbort = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/turns\/([^/]+)\/abort$/);
|
|
3472
3798
|
if (method === "POST" && sessionTurnAbort) {
|
|
3473
3799
|
await handleAbortChatTurn(req, res, deps, decodeURIComponent(sessionTurnAbort[1]), decodeURIComponent(sessionTurnAbort[2]));
|
|
@@ -3483,6 +3809,13 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
3483
3809
|
await handleLinkWorkspaceOperation(req, res, deps, decodeURIComponent(operationLink[1]));
|
|
3484
3810
|
return true;
|
|
3485
3811
|
}
|
|
3812
|
+
const operationLinkItem = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/operation-links\/([^/]+)$/);
|
|
3813
|
+
if (method === "DELETE" && operationLinkItem) {
|
|
3814
|
+
// AC-R7-001: Details rail DAG remove — real DELETE of the workspace
|
|
3815
|
+
// operation-link association (idempotent).
|
|
3816
|
+
await handleUnlinkWorkspaceOperation(req, res, deps, decodeURIComponent(operationLinkItem[1]), decodeURIComponent(operationLinkItem[2]));
|
|
3817
|
+
return true;
|
|
3818
|
+
}
|
|
3486
3819
|
const sessionCompact = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/compact$/);
|
|
3487
3820
|
if (method === "POST" && sessionCompact) {
|
|
3488
3821
|
await handleChatCompact(req, res, deps, decodeURIComponent(sessionCompact[1]));
|
|
@@ -3530,7 +3863,7 @@ export async function handleChatRequest(req, res, deps, pathname) {
|
|
|
3530
3863
|
}
|
|
3531
3864
|
}
|
|
3532
3865
|
if (method === "POST" && pathname === "/api/operator/v1/chat/sessions") {
|
|
3533
|
-
await handleCreateChatSession(req, res, deps);
|
|
3866
|
+
await handleCreateChatSession(req, res, deps, workspaceScope);
|
|
3534
3867
|
return true;
|
|
3535
3868
|
}
|
|
3536
3869
|
if (isMutationMethod(method)) {
|