@tea-agent/loop-agent 0.41.1-beta.0 → 0.41.2
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 +61 -267
- package/dist/application/dag/generate-task-dag.js +19 -75
- package/dist/application/task-lifecycle/advance.js +5 -24
- package/dist/application/task-lifecycle/observe.js +17 -171
- package/dist/application/task-lifecycle/plan-transitions.js +7 -42
- package/dist/application/task-lifecycle/recommendations.js +2 -13
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +0 -7
- package/dist/cli/program.js +0 -5
- package/dist/commands/client-recovery.js +0 -3
- package/dist/commands/init.js +1 -27
- package/dist/commands/task-advance.js +0 -19
- package/dist/executors/dag-pi-executor.js +231 -3784
- package/dist/executors/pi-executor.js +4 -15
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/shell-executor.js +217 -945
- package/dist/executors/shell-write-guard.js +0 -7
- package/dist/shared/dag-failure-category.js +0 -12
- package/dist/shared/openspec-spec.js +4 -70
- package/dist/shared/operator/capabilities.js +0 -7
- package/dist/task/config-types.js +7 -34
- package/dist/task/contract/adopt.js +0 -4
- package/dist/task/contract/import-revision.js +0 -4
- package/dist/task/contract/project.js +0 -3
- package/dist/task/contract/schema.js +1 -2
- package/dist/task/frontend-project-capability.js +20 -203
- package/dist/task/runtime.js +2 -5
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +9 -15
- package/dist/task/source-prepare/prepare.js +1 -89
- package/dist/task/source-prepare/semantic-intake.js +2 -6
- package/dist/task/source-references.js +1 -22
- package/dist/worker/console/chat/chat-ui-policy.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +235 -13
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +86 -44
- package/dist/worker/console/chat/semantic-activity.js +12 -11
- package/dist/worker/console/chat/session-stats.js +96 -0
- package/dist/worker/console/chat/session-store.js +133 -70
- package/dist/worker/console/chat/shortcuts.js +1 -1
- package/dist/worker/console/chat/turn-process.js +139 -60
- package/dist/worker/console/index.js +0 -3
- package/dist/worker/console/operation-store.js +6 -226
- package/dist/worker/console/operator-actions.js +7 -169
- package/dist/worker/console/pi-plugins.js +3 -1
- package/dist/worker/console/server.js +19 -4
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DQ5jlYVU.js} +1 -1
- package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-BeTVCzH9.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-DassREBi.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-DyeNX8Ek.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-BXb_zxGx.js} +1 -1
- package/dist/worker/console/static/assets/channel-B_lofstT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-B9F9gxGC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-Byd5zHEQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-BqoxMJk9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-CT7d6YZw.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-CdiNyN0N.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-BGw9bpVc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-DiDVlVB1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-BhHf-4yK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-B79pEgwv.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-Cb70EcUV.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-ieUI0-eY.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-ieUI0-eY.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-DBHLnAf1.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-BT_JRsOH.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-Bss-msa3.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-B8Gr24pt.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-BrjU70Jw.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cp_JpPGd.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-BWKICEP-.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D3z_UNR_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-_Ywm390x.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-D9QYQQMB.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BNgXNKAr.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-QRLnkPCg.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-B9fOduNi.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-FSEdtPN2.js} +1 -1
- package/dist/worker/console/static/assets/index-Cs9JUSPs.css +1 -0
- package/dist/worker/console/static/assets/index-DtPrwu7M.js +451 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-D5Bjg9dm.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BlNRkWvf.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-BdJiw3SL.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-CsDNlYht.js} +1 -1
- package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-8g9dbEmX.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-ChyQH_ut.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-DwYoCTYt.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-T_b2rS17.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-1Cr7WMhH.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-Bq4qM59o.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-ytFgR1RL.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CnUdUVYX.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-_5rJqG6R.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-D5imyGbt.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-DvgDn3wL.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-BViu7duc.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-kNxnG4_w.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BcULKg2P.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DyTzlcWH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-DmPveIjt.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-9r-iFy0R.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-D3fIXdvM.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-BCSS9SBg.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
- package/dist/worker/console/workspace-context.js +1 -111
- package/dist/worker/observability/read-model.js +4 -0
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/dag-node-purpose.js +0 -5
- package/dist/worker/observe/static/views/dag-inspector.js +2 -7
- package/dist/worker/observe/static/views/session-timeline.js +46 -15
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
- package/dist/workflows/dag/frontend-plan-render.js +1 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
- package/dist/workflows/dag/frontend-recovery-run.js +20 -33
- package/dist/workflows/dag/frontend-repair.js +432 -1
- package/dist/workflows/dag/frontend-review-context.js +15 -261
- package/dist/workflows/dag/frontend-verification-trace.js +24 -249
- package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
- package/dist/workflows/dag/init-hybrid.js +494 -685
- package/dist/workflows/dag/interrupt-request.js +0 -7
- package/dist/workflows/dag/node-execution.js +4 -732
- package/dist/workflows/dag/prompt.js +1 -34
- package/dist/workflows/dag/report.js +1 -37
- package/dist/workflows/dag/rerun-feedback.js +1 -256
- package/dist/workflows/dag/rerun-plan.js +2 -10
- package/dist/workflows/dag/rerun-run.js +0 -23
- package/dist/workflows/dag/rerun-task.js +0 -29
- package/dist/workflows/dag/retry-policy.js +104 -214
- package/dist/workflows/dag/runner.js +122 -429
- package/dist/workflows/dag/scheduler.js +16 -114
- package/dist/workflows/dag/types.js +12 -239
- package/dist/workflows/dag/validate.js +12 -28
- package/docs/init-surface.manifest.json +12 -30
- package/docs/skills/vetted-skill-registry.md +2 -4
- package/docs/templates/README.md +0 -2
- package/docs/templates/agent-dag-report.schema.json +2 -8
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +1 -4
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +14 -15
- package/skills/frontend-design-review/SKILL.md +41 -22
- package/skills/frontend-implementation/SKILL.md +52 -0
- package/skills/frontend-implementation/references/code-standards.md +33 -0
- package/skills/frontend-implementation/references/design-spec.md +56 -0
- package/skills/frontend-implementation/references/node-contracts.md +31 -0
- package/skills/frontend-review/SKILL.md +15 -20
- package/skills/frontend-review/references/review-findings.md +7 -6
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +0 -1
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/commands/dag-follow-up.js +0 -138
- package/dist/executors/pi-read-budget-policy.js +0 -239
- package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
- package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
- package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
- package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
- package/dist/worker/console/static-src/active-run-badge.js +0 -17
- package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
- package/dist/workflows/dag/dag-retry-schema.js +0 -138
- package/dist/workflows/dag/frontend-closeout.js +0 -221
- package/dist/workflows/dag/frontend-design-policy.js +0 -400
- package/dist/workflows/dag/frontend-human-decision.js +0 -182
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
- package/dist/workflows/dag/frontend-read-budgets.js +0 -12
- package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
- package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
- package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
- package/dist/workflows/dag/frontend-review-findings.js +0 -270
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
- package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
- package/dist/workflows/dag/frontend-shape-facts.js +0 -409
- package/dist/workflows/dag/frontend-shape.js +0 -427
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
- package/dist/workflows/dag/frontend-split-application-service.js +0 -203
- package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
- package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
- package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
- package/dist/workflows/dag/frontend-writer-admission.js +0 -285
- package/dist/workflows/dag/frontend-writer-status.js +0 -256
- package/dist/workflows/dag/recovery-lease.js +0 -80
- package/docs/templates/frontend-implementation-dag.json +0 -89
- package/docs/templates/spec-registry.schema.json +0 -45
- package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
- package/skills/frontend-contract/SKILL.md +0 -23
- package/skills/frontend-contract/references/contract-protocol.md +0 -34
- package/skills/frontend-plan/SKILL.md +0 -26
- package/skills/frontend-plan/references/decision-contract.md +0 -37
- package/skills/frontend-plan/references/design-decisions.md +0 -17
- package/skills/frontend-scout/SKILL.md +0 -25
- package/skills/frontend-scout/references/design-evidence.md +0 -16
- package/skills/frontend-scout/references/scout-evidence.md +0 -23
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Read routes (GET capabilities / sessions) are open to loopback.
|
|
14
14
|
*/
|
|
15
15
|
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { access } from "node:fs/promises";
|
|
16
17
|
import { parseAssistantContent } from "./assistant-content.js";
|
|
17
18
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
|
|
18
19
|
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
@@ -34,7 +35,7 @@ import { dispatchOperatorAction } from "../operator-actions.js";
|
|
|
34
35
|
import { makeHumanGateCard, } from "./human-gate-card.js";
|
|
35
36
|
import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
|
|
36
37
|
import { projectTaskContext } from "./context-panel.js";
|
|
37
|
-
import { deriveChatSessionTelemetry } from "./session-stats.js";
|
|
38
|
+
import { deriveChatSessionTelemetry, deriveChatTurnTelemetry } from "./session-stats.js";
|
|
38
39
|
import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
|
|
39
40
|
import { sendJson } from "../routes.js";
|
|
40
41
|
import { openSseResponse, writeSseEvent } from "../operation-sse.js";
|
|
@@ -99,8 +100,16 @@ export function projectStoredMessagesWithEventOrder(messages, events) {
|
|
|
99
100
|
}
|
|
100
101
|
return {
|
|
101
102
|
...message,
|
|
102
|
-
...(start
|
|
103
|
-
|
|
103
|
+
...(start
|
|
104
|
+
? {
|
|
105
|
+
eventSeq: start.seq,
|
|
106
|
+
turnId: start.turnId,
|
|
107
|
+
startedAt: start.at,
|
|
108
|
+
}
|
|
109
|
+
: {}),
|
|
110
|
+
...(finish
|
|
111
|
+
? { finishedSeq: finish.seq, finishedAt: finish.at }
|
|
112
|
+
: {}),
|
|
104
113
|
};
|
|
105
114
|
});
|
|
106
115
|
}
|
|
@@ -460,7 +469,7 @@ function sendRuntimeUnavailable(res, error) {
|
|
|
460
469
|
* handled here — only reopen/init readiness failures.
|
|
461
470
|
*/
|
|
462
471
|
function mapReopenSessionFailure(reopen) {
|
|
463
|
-
if (reopen.code === "NO_SESSION_FILE") {
|
|
472
|
+
if (reopen.code === "NO_SESSION_FILE" || reopen.code === "SESSION_FILE_MISSING") {
|
|
464
473
|
return {
|
|
465
474
|
status: 404,
|
|
466
475
|
code: "PI_SESSION_NOT_FOUND",
|
|
@@ -480,6 +489,19 @@ function mapReopenSessionFailure(reopen) {
|
|
|
480
489
|
message: reopen.message,
|
|
481
490
|
};
|
|
482
491
|
}
|
|
492
|
+
/** Drop an orphan catalog record whose promised Pi JSONL is gone. */
|
|
493
|
+
async function healMissingSessionFile(deps, sessionId, reopen) {
|
|
494
|
+
if (reopen && reopen.code !== "NO_SESSION_FILE" && reopen.code !== "SESSION_FILE_MISSING") {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (typeof deps.store.remove !== "function")
|
|
498
|
+
return;
|
|
499
|
+
await deps.store.remove(sessionId).catch(() => undefined);
|
|
500
|
+
}
|
|
501
|
+
async function mapFailedReopen(deps, sessionId, reopen) {
|
|
502
|
+
await healMissingSessionFile(deps, sessionId, reopen);
|
|
503
|
+
return mapReopenSessionFailure(reopen);
|
|
504
|
+
}
|
|
483
505
|
export async function handleChatCapabilities(_req, res, deps) {
|
|
484
506
|
const toolSchemas = buildModelCallableToolSchemas();
|
|
485
507
|
sendJson(res, 200, {
|
|
@@ -723,10 +745,7 @@ export async function handleBranchChatSession(req, res, deps, sessionId) {
|
|
|
723
745
|
sessionFile: record.sessionFile,
|
|
724
746
|
});
|
|
725
747
|
if (!reopened.ok) {
|
|
726
|
-
const mapped =
|
|
727
|
-
code: reopened.code,
|
|
728
|
-
message: reopened.message,
|
|
729
|
-
});
|
|
748
|
+
const mapped = await mapFailedReopen(deps, sessionId, reopened);
|
|
730
749
|
sendJson(res, mapped.status, {
|
|
731
750
|
ok: false,
|
|
732
751
|
error: { code: mapped.code, message: mapped.message },
|
|
@@ -801,6 +820,29 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
|
|
|
801
820
|
});
|
|
802
821
|
return;
|
|
803
822
|
}
|
|
823
|
+
if (record.sessionFile) {
|
|
824
|
+
try {
|
|
825
|
+
await access(record.sessionFile);
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
828
|
+
const code = error && typeof error === "object" && "code" in error
|
|
829
|
+
? String(error.code ?? "")
|
|
830
|
+
: "";
|
|
831
|
+
if (code === "ENOENT") {
|
|
832
|
+
await healMissingSessionFile(deps, sessionId, {
|
|
833
|
+
code: "NO_SESSION_FILE",
|
|
834
|
+
});
|
|
835
|
+
sendJson(res, 404, {
|
|
836
|
+
ok: false,
|
|
837
|
+
error: {
|
|
838
|
+
code: "PI_SESSION_NOT_FOUND",
|
|
839
|
+
message: `Pi session file missing or not openable: ${record.sessionFile}`,
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
804
846
|
await deps.operationLinker.recoverSession(sessionId);
|
|
805
847
|
const composerDraft = await new ComposerDraftStore(deps.appData).get(sessionId);
|
|
806
848
|
const snapshot = deps.events.snapshot(sessionId);
|
|
@@ -828,6 +870,7 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
|
|
|
828
870
|
hasBash: true,
|
|
829
871
|
sessionStats: telemetry.stats,
|
|
830
872
|
usage: telemetry.usage,
|
|
873
|
+
turnTelemetry: deriveChatTurnTelemetry(snapshot),
|
|
831
874
|
contextPressure: typeof deps.runtime.getContextPressureTelemetry === "function"
|
|
832
875
|
? deps.runtime.getContextPressureTelemetry(sessionId)
|
|
833
876
|
: undefined,
|
|
@@ -1056,20 +1099,10 @@ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientR
|
|
|
1056
1099
|
}
|
|
1057
1100
|
const item = claim.deferredTurn;
|
|
1058
1101
|
try {
|
|
1059
|
-
await deps.runtime.steer(sessionId, item.text, item.images);
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
});
|
|
1064
|
-
deps.events.append(sessionId, active.turnId, {
|
|
1065
|
-
kind: "user_message",
|
|
1066
|
-
data: {
|
|
1067
|
-
clientRequestId: item.clientRequestId,
|
|
1068
|
-
message: { id: userMessage.id, text: userMessage.text, at: userMessage.at },
|
|
1069
|
-
},
|
|
1070
|
-
});
|
|
1071
|
-
await deps.store.clearDeferredTurnForRequest(sessionId, item.clientRequestId);
|
|
1072
|
-
await emitDeferredTurnUpdate(deps, sessionId, active.turnId, undefined, item.revision + 2);
|
|
1102
|
+
await deps.runtime.steer(sessionId, item.clientRequestId, item.text, item.images);
|
|
1103
|
+
// session.steer() only acknowledges queueing. Keep the durable item in
|
|
1104
|
+
// dispatching until Pi emits the corresponding user message_start.
|
|
1105
|
+
await emitDeferredTurnUpdate(deps, sessionId, active.turnId, item, item.revision);
|
|
1073
1106
|
sendJson(res, 200, {
|
|
1074
1107
|
ok: true,
|
|
1075
1108
|
accepted: true,
|
|
@@ -1093,6 +1126,21 @@ export async function handleSteerDeferredTurn(req, res, deps, sessionId, clientR
|
|
|
1093
1126
|
});
|
|
1094
1127
|
}
|
|
1095
1128
|
}
|
|
1129
|
+
async function persistAcceptedSteering(deps, sessionId, turnId, userMessage, clientRequestId) {
|
|
1130
|
+
deps.events.append(sessionId, turnId, {
|
|
1131
|
+
kind: "user_message",
|
|
1132
|
+
data: {
|
|
1133
|
+
clientRequestId,
|
|
1134
|
+
steering: true,
|
|
1135
|
+
message: {
|
|
1136
|
+
id: userMessage.id,
|
|
1137
|
+
text: userMessage.text,
|
|
1138
|
+
at: userMessage.at,
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
});
|
|
1142
|
+
await emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, 0);
|
|
1143
|
+
}
|
|
1096
1144
|
/** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
|
|
1097
1145
|
* get_state equivalent): exposes the active turn + the latest turn's durable
|
|
1098
1146
|
* outcome WITHOUT the full message payload, so the UI can poll it on an
|
|
@@ -1472,10 +1520,7 @@ export async function handleReopenChatSession(req, res, deps, sessionId) {
|
|
|
1472
1520
|
: {}),
|
|
1473
1521
|
});
|
|
1474
1522
|
if (!result.ok) {
|
|
1475
|
-
const mapped =
|
|
1476
|
-
code: result.code,
|
|
1477
|
-
message: result.message,
|
|
1478
|
-
});
|
|
1523
|
+
const mapped = await mapFailedReopen(deps, sessionId, result);
|
|
1479
1524
|
sendJson(res, mapped.status, {
|
|
1480
1525
|
ok: false,
|
|
1481
1526
|
error: { code: mapped.code, message: mapped.message },
|
|
@@ -1561,10 +1606,7 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
|
1561
1606
|
: {}),
|
|
1562
1607
|
});
|
|
1563
1608
|
if (!reopen.ok) {
|
|
1564
|
-
const mapped =
|
|
1565
|
-
code: reopen.code,
|
|
1566
|
-
message: reopen.message,
|
|
1567
|
-
});
|
|
1609
|
+
const mapped = await mapFailedReopen(deps, sessionId, reopen);
|
|
1568
1610
|
sendJson(res, mapped.status, {
|
|
1569
1611
|
ok: false,
|
|
1570
1612
|
error: { code: mapped.code, message: mapped.message },
|
|
@@ -1652,6 +1694,9 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
|
1652
1694
|
if (!res.writableEnded)
|
|
1653
1695
|
writeChatSse(res, persisted);
|
|
1654
1696
|
},
|
|
1697
|
+
onSteeringMessagePersisted: async (_record, userMessage, accepted) => {
|
|
1698
|
+
await persistAcceptedSteering(deps, sessionId, turnId, userMessage, accepted.clientRequestId);
|
|
1699
|
+
},
|
|
1655
1700
|
onOperationAccepted: (accepted) => deps.operationLinker.link({
|
|
1656
1701
|
sessionId,
|
|
1657
1702
|
turnId,
|
|
@@ -1830,10 +1875,7 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
|
|
|
1830
1875
|
: {}),
|
|
1831
1876
|
});
|
|
1832
1877
|
if (!reopen.ok) {
|
|
1833
|
-
const mapped =
|
|
1834
|
-
code: reopen.code,
|
|
1835
|
-
message: reopen.message,
|
|
1836
|
-
});
|
|
1878
|
+
const mapped = await mapFailedReopen(deps, sessionId, reopen);
|
|
1837
1879
|
sendJson(res, mapped.status, {
|
|
1838
1880
|
ok: false,
|
|
1839
1881
|
accepted: false,
|
|
@@ -1962,6 +2004,9 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
|
|
|
1962
2004
|
if (placeholder)
|
|
1963
2005
|
emitSessionTitle(deps, sessionId, turn.turnId, placeholder);
|
|
1964
2006
|
},
|
|
2007
|
+
onSteeringMessagePersisted: async (_record, userMessage, accepted) => {
|
|
2008
|
+
await persistAcceptedSteering(deps, sessionId, turn.turnId, userMessage, accepted.clientRequestId);
|
|
2009
|
+
},
|
|
1965
2010
|
persistUserMessage: trustedPayload?.persistUserMessage,
|
|
1966
2011
|
onOperationAccepted: (accepted) => deps.operationLinker.link({
|
|
1967
2012
|
sessionId,
|
|
@@ -2637,13 +2682,13 @@ async function handleRepoHtmlPreview(res, deps, rawPath) {
|
|
|
2637
2682
|
"Cache-Control": "no-store",
|
|
2638
2683
|
"X-Content-Type-Options": "nosniff",
|
|
2639
2684
|
"Referrer-Policy": "no-referrer",
|
|
2640
|
-
//
|
|
2641
|
-
// sandbox
|
|
2642
|
-
//
|
|
2685
|
+
// Project scripts may drive tabs and other local interactions, but the
|
|
2686
|
+
// opaque sandbox still withholds same-origin access, forms, popups and
|
|
2687
|
+
// top-level navigation. Network requests remain disabled below.
|
|
2643
2688
|
"Content-Security-Policy": [
|
|
2644
|
-
"sandbox",
|
|
2689
|
+
"sandbox allow-scripts",
|
|
2645
2690
|
"default-src 'none'",
|
|
2646
|
-
|
|
2691
|
+
`script-src 'unsafe-inline' ${origin}`,
|
|
2647
2692
|
"object-src 'none'",
|
|
2648
2693
|
"frame-src 'none'",
|
|
2649
2694
|
"form-action 'none'",
|
|
@@ -2979,7 +3024,7 @@ export async function ensureActiveSession(deps, sessionId) {
|
|
|
2979
3024
|
: {}),
|
|
2980
3025
|
});
|
|
2981
3026
|
if (!reopened.ok) {
|
|
2982
|
-
return { ok: false, ...
|
|
3027
|
+
return { ok: false, ...(await mapFailedReopen(deps, sessionId, reopened)) };
|
|
2983
3028
|
}
|
|
2984
3029
|
return { ok: true };
|
|
2985
3030
|
}
|
|
@@ -3926,10 +3971,7 @@ export async function handleChatCompact(req, res, deps, sessionId) {
|
|
|
3926
3971
|
: {}),
|
|
3927
3972
|
});
|
|
3928
3973
|
if (!reopened.ok) {
|
|
3929
|
-
const mapped =
|
|
3930
|
-
code: reopened.code,
|
|
3931
|
-
message: reopened.message,
|
|
3932
|
-
});
|
|
3974
|
+
const mapped = await mapFailedReopen(deps, sessionId, reopened);
|
|
3933
3975
|
sendJson(res, mapped.status, {
|
|
3934
3976
|
ok: false,
|
|
3935
3977
|
error: { code: mapped.code, message: mapped.message },
|
|
@@ -296,13 +296,6 @@ function operationIdFromArgs(args) {
|
|
|
296
296
|
const operationId = record?.operationId;
|
|
297
297
|
return typeof operationId === "string" && operationId ? operationId : undefined;
|
|
298
298
|
}
|
|
299
|
-
function maxWaitSeconds(args) {
|
|
300
|
-
const record = parseJsonRecord(args);
|
|
301
|
-
const raw = record?.maxWaitMs;
|
|
302
|
-
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0)
|
|
303
|
-
return undefined;
|
|
304
|
-
return Math.max(1, Math.round(raw / 1000));
|
|
305
|
-
}
|
|
306
299
|
function activityRefForTool(tool) {
|
|
307
300
|
// Plan A (2026-08-16): server backfills operationId on the persisted tool
|
|
308
301
|
// entry (and live tool_result frames carry it in the result payload), so
|
|
@@ -313,6 +306,17 @@ function activityRefForTool(tool) {
|
|
|
313
306
|
operationIdFromArgs(tool.resultText);
|
|
314
307
|
return operationId ? `operation:${operationId}` : `tool:${tool.id}`;
|
|
315
308
|
}
|
|
309
|
+
export function dagProgressActivityForTool(tool) {
|
|
310
|
+
if (tool.toolName !== "operationWait")
|
|
311
|
+
return undefined;
|
|
312
|
+
const activityRef = activityRefForTool(tool);
|
|
313
|
+
if (!activityRef.startsWith("operation:"))
|
|
314
|
+
return undefined;
|
|
315
|
+
return {
|
|
316
|
+
id: `dag-progress:${activityRef}`,
|
|
317
|
+
activityRef,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
316
320
|
function semanticKindForStatus(status) {
|
|
317
321
|
if (status === "running")
|
|
318
322
|
return "progress";
|
|
@@ -377,14 +381,11 @@ function nodeActivityFromMessages(messages, id, activityRef) {
|
|
|
377
381
|
function operationWaitActivity(tool) {
|
|
378
382
|
const activityRef = activityRefForTool(tool);
|
|
379
383
|
const id = `activity:${tool.id}`;
|
|
380
|
-
const seconds = maxWaitSeconds(tool.args);
|
|
381
384
|
if (tool.status === "running") {
|
|
382
385
|
return {
|
|
383
386
|
id,
|
|
384
387
|
kind: "progress",
|
|
385
|
-
title:
|
|
386
|
-
? `正在等待新的执行进展,最长等待时间 ${seconds} 秒`
|
|
387
|
-
: "正在等待新的执行进展",
|
|
388
|
+
title: "正在等待新的执行进展",
|
|
388
389
|
activityRef,
|
|
389
390
|
};
|
|
390
391
|
}
|
|
@@ -156,6 +156,102 @@ export function deriveChatSessionTelemetry(events) {
|
|
|
156
156
|
export function billedInputTokens(usage) {
|
|
157
157
|
return usage.inputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
158
158
|
}
|
|
159
|
+
export function deriveChatTurnTelemetry(events) {
|
|
160
|
+
const byTurn = new Map();
|
|
161
|
+
const turnOf = (turnId) => {
|
|
162
|
+
let entry = byTurn.get(turnId);
|
|
163
|
+
if (!entry) {
|
|
164
|
+
entry = {
|
|
165
|
+
turnId,
|
|
166
|
+
steps: 0,
|
|
167
|
+
llmMs: 0,
|
|
168
|
+
toolMs: 0,
|
|
169
|
+
toolCalls: 0,
|
|
170
|
+
samples: [],
|
|
171
|
+
};
|
|
172
|
+
byTurn.set(turnId, entry);
|
|
173
|
+
}
|
|
174
|
+
return entry;
|
|
175
|
+
};
|
|
176
|
+
const toolStarts = new Map();
|
|
177
|
+
for (const event of events) {
|
|
178
|
+
const at = eventTime(event);
|
|
179
|
+
const turn = turnOf(event.turnId);
|
|
180
|
+
if (event.kind === "usage") {
|
|
181
|
+
const sample = usageSample(event.data);
|
|
182
|
+
if (sample)
|
|
183
|
+
turn.samples.push(sample);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (event.kind === "user_message") {
|
|
187
|
+
if (turn.startedAt === undefined && at !== undefined)
|
|
188
|
+
turn.startedAt = at;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (event.kind === "agent_start") {
|
|
192
|
+
if (at !== undefined)
|
|
193
|
+
turn.stepStart = at;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (event.kind === "tool_call") {
|
|
197
|
+
turn.toolCalls += 1;
|
|
198
|
+
const toolCallId = event.data.toolCallId;
|
|
199
|
+
if (typeof toolCallId === "string" && toolCallId && at !== undefined) {
|
|
200
|
+
toolStarts.set(`${event.turnId}\u0000${toolCallId}`, at);
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (event.kind === "tool_result") {
|
|
205
|
+
const toolCallId = event.data.toolCallId;
|
|
206
|
+
if (typeof toolCallId === "string" && toolCallId && at !== undefined) {
|
|
207
|
+
const key = `${event.turnId}\u0000${toolCallId}`;
|
|
208
|
+
const startedAt = toolStarts.get(key);
|
|
209
|
+
if (startedAt !== undefined && at >= startedAt) {
|
|
210
|
+
turn.toolMs += at - startedAt;
|
|
211
|
+
}
|
|
212
|
+
toolStarts.delete(key);
|
|
213
|
+
// Next assistant step within the same turn starts after this tool.
|
|
214
|
+
turn.stepStart = at;
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (event.kind !== "message_end")
|
|
219
|
+
continue;
|
|
220
|
+
turn.steps += 1;
|
|
221
|
+
const recorded = stepTiming(event.data);
|
|
222
|
+
if (recorded?.llmMs !== undefined)
|
|
223
|
+
turn.llmMs += recorded.llmMs;
|
|
224
|
+
else if (at !== undefined &&
|
|
225
|
+
turn.stepStart !== undefined &&
|
|
226
|
+
at >= turn.stepStart) {
|
|
227
|
+
turn.llmMs += at - turn.stepStart;
|
|
228
|
+
}
|
|
229
|
+
if (recorded?.ttftMs !== undefined) {
|
|
230
|
+
turn.ttftMs = (turn.ttftMs ?? 0) + recorded.ttftMs;
|
|
231
|
+
}
|
|
232
|
+
if (recorded?.decodeMs !== undefined &&
|
|
233
|
+
recorded.decodeMs > 0 &&
|
|
234
|
+
recorded.outputTokens !== undefined) {
|
|
235
|
+
turn.decodeMs = (turn.decodeMs ?? 0) + recorded.decodeMs;
|
|
236
|
+
turn.decodeTokens = (turn.decodeTokens ?? 0) + recorded.outputTokens;
|
|
237
|
+
}
|
|
238
|
+
turn.stepStart = undefined;
|
|
239
|
+
}
|
|
240
|
+
return [...byTurn.values()].map((entry) => ({
|
|
241
|
+
turnId: entry.turnId,
|
|
242
|
+
steps: entry.steps,
|
|
243
|
+
llmMs: entry.llmMs,
|
|
244
|
+
toolMs: entry.toolMs,
|
|
245
|
+
toolCalls: entry.toolCalls,
|
|
246
|
+
...(entry.ttftMs !== undefined ? { ttftMs: entry.ttftMs } : {}),
|
|
247
|
+
...(entry.decodeMs !== undefined ? { decodeMs: entry.decodeMs } : {}),
|
|
248
|
+
...(entry.decodeTokens !== undefined
|
|
249
|
+
? { decodeTokens: entry.decodeTokens }
|
|
250
|
+
: {}),
|
|
251
|
+
usage: aggregateUsage(entry.samples),
|
|
252
|
+
...(entry.startedAt !== undefined ? { startedAt: entry.startedAt } : {}),
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
159
255
|
export function cacheHitPercent(usage) {
|
|
160
256
|
const billed = billedInputTokens(usage);
|
|
161
257
|
return billed > 0 ? Math.round((usage.cacheReadTokens / billed) * 100) : null;
|
|
@@ -38,6 +38,16 @@ export function cleanChatTitle(value) {
|
|
|
38
38
|
function recordPath(appData, sessionId) {
|
|
39
39
|
return path.join(appData.chats, `${sessionId}.json`);
|
|
40
40
|
}
|
|
41
|
+
/** True when `sessionFile` lives inside this store's dedicated Pi JSONL dir. */
|
|
42
|
+
export function isConsoleOwnedSessionFile(appData, sessionFile) {
|
|
43
|
+
const value = sessionFile?.trim();
|
|
44
|
+
if (!value)
|
|
45
|
+
return false;
|
|
46
|
+
const root = path.resolve(appData.chatSessions);
|
|
47
|
+
const target = path.resolve(value);
|
|
48
|
+
const relative = path.relative(root, target);
|
|
49
|
+
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
50
|
+
}
|
|
41
51
|
export class ChatSessionStore {
|
|
42
52
|
appData;
|
|
43
53
|
options;
|
|
@@ -237,8 +247,14 @@ export class ChatSessionStore {
|
|
|
237
247
|
const record = await this.get(sessionId);
|
|
238
248
|
if (!record)
|
|
239
249
|
return false;
|
|
250
|
+
const sessionFile = record.sessionFile;
|
|
240
251
|
await rm(recordPath(this.appData, sessionId), { force: true });
|
|
241
252
|
this.options.onRecordChange?.(sessionId, undefined);
|
|
253
|
+
// Record first, then best-effort JSONL unlink. Final consistency is
|
|
254
|
+
// "both gone" — a leftover JSONL is harmless, a leftover record is not.
|
|
255
|
+
if (isConsoleOwnedSessionFile(this.appData, sessionFile) && sessionFile) {
|
|
256
|
+
await rm(sessionFile, { force: true }).catch(() => undefined);
|
|
257
|
+
}
|
|
242
258
|
return true;
|
|
243
259
|
});
|
|
244
260
|
}
|
|
@@ -384,6 +400,44 @@ export class ChatSessionStore {
|
|
|
384
400
|
return { deferredTurn: existing, clearedRevision: existing.revision + 1 };
|
|
385
401
|
});
|
|
386
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* Commit Pi's steering acceptance as one durable record transition. Removing
|
|
405
|
+
* the dispatching queue item and appending its user message together prevents
|
|
406
|
+
* a crash between two writes from replaying or losing the accepted input.
|
|
407
|
+
*/
|
|
408
|
+
async acceptSteeringMessage(sessionId, clientRequestId, text) {
|
|
409
|
+
return this.serialize(sessionId, async () => {
|
|
410
|
+
const record = await this.get(sessionId);
|
|
411
|
+
if (!record)
|
|
412
|
+
throw new Error(`chat session not found: ${sessionId}`);
|
|
413
|
+
const queue = [...(record.deferredTurns ?? [])];
|
|
414
|
+
const index = queue.findIndex((item) => item.clientRequestId === clientRequestId);
|
|
415
|
+
const deferredTurn = index >= 0 ? queue[index] : undefined;
|
|
416
|
+
if (!deferredTurn || deferredTurn.state !== "dispatching")
|
|
417
|
+
return undefined;
|
|
418
|
+
const message = {
|
|
419
|
+
id: `m_${record.messages.length + 1}_${Math.random().toString(36).slice(2, 8)}`,
|
|
420
|
+
role: "user",
|
|
421
|
+
text,
|
|
422
|
+
at: new Date().toISOString(),
|
|
423
|
+
};
|
|
424
|
+
queue.splice(index, 1);
|
|
425
|
+
record.messages.push(message);
|
|
426
|
+
record.deferredTurns = queue;
|
|
427
|
+
record.deferredTurn = queue[0];
|
|
428
|
+
record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
|
|
429
|
+
if (!record.displayTitle)
|
|
430
|
+
record.displayTitle = cleanChatTitle(message.text);
|
|
431
|
+
record.updatedAt = message.at;
|
|
432
|
+
await this.persist(record);
|
|
433
|
+
return {
|
|
434
|
+
record,
|
|
435
|
+
message,
|
|
436
|
+
deferredTurn,
|
|
437
|
+
clearedRevision: deferredTurn.revision + 1,
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
}
|
|
387
441
|
async appendMessage(sessionId, message) {
|
|
388
442
|
return this.serialize(sessionId, async () => {
|
|
389
443
|
const record = await this.get(sessionId);
|
|
@@ -520,84 +574,91 @@ export async function runChatTurn(options) {
|
|
|
520
574
|
// observable to tests and to a reconnecting client reading the record).
|
|
521
575
|
const pending = [];
|
|
522
576
|
let settledEvent;
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
else {
|
|
532
|
-
options.onEvent?.(event);
|
|
577
|
+
let eventChain = Promise.resolve();
|
|
578
|
+
const processEvent = async (event) => {
|
|
579
|
+
if (event.type === "steering_accepted") {
|
|
580
|
+
const committed = await store.acceptSteeringMessage(sessionId, event.clientRequestId, event.text);
|
|
581
|
+
if (committed) {
|
|
582
|
+
await options.onSteeringMessagePersisted?.(committed.record, committed.message, {
|
|
583
|
+
clientRequestId: event.clientRequestId,
|
|
584
|
+
});
|
|
533
585
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
// execution happens in the SDK's customTools[].execute(), which
|
|
550
|
-
// routes to dispatchChatToolCall (Gate 3). Do NOT dispatch here —
|
|
551
|
-
// that would double-execute every tool call (P0.2).
|
|
552
|
-
break;
|
|
553
|
-
case "tool_result": {
|
|
554
|
-
const acceptedOperationId = acceptedOperationIdFromResult(event.toolName, event.result);
|
|
555
|
-
if (acceptedOperationId && options.onOperationAccepted) {
|
|
556
|
-
pending.push(Promise.resolve(options.onOperationAccepted({
|
|
557
|
-
sessionId,
|
|
558
|
-
toolCallId: event.toolCallId,
|
|
559
|
-
toolName: event.toolName,
|
|
560
|
-
operationId: acceptedOperationId,
|
|
561
|
-
})));
|
|
562
|
-
}
|
|
563
|
-
// tool_execution_end carries the real result of the tool call.
|
|
564
|
-
// Persist exactly once (the tool message + audit trail). This is
|
|
565
|
-
// the single point of record for tool outcomes (P0.2).
|
|
566
|
-
const resultText = safeStringifyResult(event.result);
|
|
567
|
-
// ChatSessionStore serializes writes per session, so these two calls
|
|
568
|
-
// are safe to fire concurrently — neither clobbers the other.
|
|
569
|
-
pending.push(store.appendMessage(sessionId, {
|
|
570
|
-
role: "tool",
|
|
571
|
-
text: resultText,
|
|
572
|
-
toolCallId: event.toolCallId,
|
|
573
|
-
toolName: event.toolName,
|
|
574
|
-
isError: event.isError,
|
|
575
|
-
// Durable tool→operation link (plan A): the restored timeline can
|
|
576
|
-
// resolve operation:<id> refs without the live operation-ref event.
|
|
577
|
-
...(acceptedOperationId
|
|
578
|
-
? { operationId: acceptedOperationId }
|
|
579
|
-
: {}),
|
|
580
|
-
}));
|
|
581
|
-
pending.push(store.recordToolInvocation(sessionId, {
|
|
582
|
-
toolCallId: event.toolCallId,
|
|
583
|
-
toolName: event.toolName,
|
|
584
|
-
ok: !event.isError,
|
|
585
|
-
}));
|
|
586
|
-
break;
|
|
587
|
-
}
|
|
588
|
-
case "error":
|
|
589
|
-
turnError = { code: "CHAT_TURN_ERROR", message: event.message };
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (event.type === "agent_settled") {
|
|
589
|
+
// The durable operation-ref linker may perform an async canonical
|
|
590
|
+
// operation read. Hold settled until those writes finish so replay
|
|
591
|
+
// never observes agent_settled before its accepted operation ref.
|
|
592
|
+
settledEvent = event;
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
options.onEvent?.(event);
|
|
596
|
+
}
|
|
597
|
+
switch (event.type) {
|
|
598
|
+
case "message_update":
|
|
599
|
+
case "message_end":
|
|
600
|
+
if (event.type === "message_end") {
|
|
590
601
|
pending.push(store.appendMessage(sessionId, {
|
|
591
602
|
role: "assistant",
|
|
592
|
-
|
|
593
|
-
text: event.providerError.message,
|
|
594
|
-
providerError: event.providerError,
|
|
603
|
+
text: event.text,
|
|
595
604
|
...(event.content && event.content.length > 0
|
|
596
605
|
? { content: event.content }
|
|
597
606
|
: {}),
|
|
598
607
|
}));
|
|
599
|
-
|
|
608
|
+
}
|
|
609
|
+
break;
|
|
610
|
+
case "tool_call":
|
|
611
|
+
// Forwarded to the caller (SSE) for UI feedback. The actual tool
|
|
612
|
+
// execution happens in the SDK's customTools[].execute(), which
|
|
613
|
+
// routes to dispatchChatToolCall (Gate 3). Do NOT dispatch here —
|
|
614
|
+
// that would double-execute every tool call (P0.2).
|
|
615
|
+
break;
|
|
616
|
+
case "tool_result": {
|
|
617
|
+
const acceptedOperationId = acceptedOperationIdFromResult(event.toolName, event.result);
|
|
618
|
+
if (acceptedOperationId && options.onOperationAccepted) {
|
|
619
|
+
pending.push(Promise.resolve(options.onOperationAccepted({
|
|
620
|
+
sessionId,
|
|
621
|
+
toolCallId: event.toolCallId,
|
|
622
|
+
toolName: event.toolName,
|
|
623
|
+
operationId: acceptedOperationId,
|
|
624
|
+
})));
|
|
625
|
+
}
|
|
626
|
+
const resultText = safeStringifyResult(event.result);
|
|
627
|
+
pending.push(store.appendMessage(sessionId, {
|
|
628
|
+
role: "tool",
|
|
629
|
+
text: resultText,
|
|
630
|
+
toolCallId: event.toolCallId,
|
|
631
|
+
toolName: event.toolName,
|
|
632
|
+
isError: event.isError,
|
|
633
|
+
...(acceptedOperationId ? { operationId: acceptedOperationId } : {}),
|
|
634
|
+
}));
|
|
635
|
+
pending.push(store.recordToolInvocation(sessionId, {
|
|
636
|
+
toolCallId: event.toolCallId,
|
|
637
|
+
toolName: event.toolName,
|
|
638
|
+
ok: !event.isError,
|
|
639
|
+
}));
|
|
640
|
+
break;
|
|
600
641
|
}
|
|
642
|
+
case "error":
|
|
643
|
+
turnError = { code: "CHAT_TURN_ERROR", message: event.message };
|
|
644
|
+
pending.push(store.appendMessage(sessionId, {
|
|
645
|
+
role: "assistant",
|
|
646
|
+
kind: "provider-error",
|
|
647
|
+
text: event.providerError.message,
|
|
648
|
+
providerError: event.providerError,
|
|
649
|
+
...(event.content && event.content.length > 0
|
|
650
|
+
? { content: event.content }
|
|
651
|
+
: {}),
|
|
652
|
+
}));
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
try {
|
|
657
|
+
await runtime.prompt(sessionId, text, (event) => {
|
|
658
|
+
// Pi's event callback is synchronous. Serialize projection work so the
|
|
659
|
+
// accepted steering user_message is durable and sequenced before any
|
|
660
|
+
// assistant/tool event that follows it.
|
|
661
|
+
eventChain = eventChain.then(() => processEvent(event)).catch(() => undefined);
|
|
601
662
|
}, {
|
|
602
663
|
signal: options.signal,
|
|
603
664
|
images: options.images,
|
|
@@ -605,10 +666,12 @@ export async function runChatTurn(options) {
|
|
|
605
666
|
});
|
|
606
667
|
}
|
|
607
668
|
catch (error) {
|
|
669
|
+
await eventChain;
|
|
608
670
|
await Promise.allSettled(pending);
|
|
609
671
|
const message = error instanceof Error ? error.message : String(error);
|
|
610
672
|
return { ok: false, error: { code: "CHAT_TURN_FAILED", message } };
|
|
611
673
|
}
|
|
674
|
+
await eventChain;
|
|
612
675
|
await Promise.allSettled(pending);
|
|
613
676
|
if (settledEvent)
|
|
614
677
|
options.onEvent?.(settledEvent);
|
|
@@ -37,7 +37,7 @@ export const SHORTCUTS = [
|
|
|
37
37
|
{
|
|
38
38
|
command: "/reload",
|
|
39
39
|
label: "Reload session",
|
|
40
|
-
description: "重新加载当前 Pi
|
|
40
|
+
description: "重新加载当前 Pi 会话的设置、扩展、技能、提示词、主题、上下文文件及模型与认证配置(不产生消息)",
|
|
41
41
|
action: "reload",
|
|
42
42
|
source: "operator",
|
|
43
43
|
},
|