@tea-agent/loop-agent 0.43.0 → 0.44.0-next.10
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 +408 -367
- package/dist/application/evaluation/budget.js +21 -0
- package/dist/application/evaluation/corpus-hash.js +10 -15
- package/dist/application/evaluation/corpus.js +2 -1
- package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
- package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
- package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
- package/dist/application/evaluation/frontend-pair-registration.js +143 -0
- package/dist/application/evaluation/frontend-paired-summary.js +150 -0
- package/dist/application/evaluation/frontend-run-observation.js +144 -0
- package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
- package/dist/application/evaluation/types.js +45 -12
- package/dist/application/task-lifecycle/observe.js +26 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +11 -0
- package/dist/cli/program.js +4 -0
- package/dist/commands/task-advance.js +3 -5
- package/dist/commands/task-source-prepare.js +13 -1
- package/dist/executors/dag-pi/guards.js +4 -0
- package/dist/executors/dag-pi/model-config.js +30 -0
- package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
- package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
- package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
- package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
- package/dist/executors/dag-pi/plan/record-tools.js +861 -0
- package/dist/executors/dag-pi/plan/schema.js +129 -0
- package/dist/executors/dag-pi/plan/session.js +1 -0
- package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
- package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
- package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
- package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
- package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
- package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
- package/dist/executors/dag-pi-executor.js +398 -3384
- package/dist/executors/pi-executor.js +6 -1
- package/dist/executors/pi-sdk-executor.js +112 -19
- package/dist/executors/shell-executor.js +88 -12
- package/dist/executors/shell-presets.js +1 -1
- package/dist/executors/shell-write-guard.js +26 -3
- package/dist/infrastructure/console/operation-store.js +48 -14
- package/dist/infrastructure/evaluation/corpus-store.js +37 -12
- package/dist/shared/file-identity.js +23 -0
- package/dist/shared/git-workspace-status.js +482 -0
- package/dist/shared/operator/capabilities.js +10 -5
- package/dist/shared/path-safety.js +2 -2
- package/dist/shared/wizard-local-telemetry.js +51 -0
- package/dist/task/config-types.js +22 -8
- package/dist/task/contract/project.js +13 -0
- package/dist/task/contract/schema.js +2 -7
- package/dist/task/frontend-preflight.js +1 -1
- package/dist/task/source-prepare/parse-intent.js +124 -5
- package/dist/task/source-prepare/prepare.js +6 -1
- package/dist/task/source-prepare/semantic-intake.js +18 -3
- package/dist/task/source-prepare/source-execution.js +65 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
- package/dist/task/source-prepare/source-provider-budget.js +290 -0
- package/dist/worker/console/chat/assistant-content.js +5 -34
- package/dist/worker/console/chat/chat-event-store.js +62 -10
- package/dist/worker/console/chat/context-insights.js +723 -0
- package/dist/worker/console/chat/conversation-nodes.js +383 -0
- package/dist/worker/console/chat/db-connections.js +854 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
- package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
- package/dist/worker/console/chat/pi-runtime.js +445 -36
- package/dist/worker/console/chat/provider-error.js +0 -12
- package/dist/worker/console/chat/repo-browser.js +52 -4
- package/dist/worker/console/chat/routes.js +577 -72
- package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
- package/dist/worker/console/chat/session-store.js +147 -19
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/chat/timeline-message-order.js +22 -0
- package/dist/worker/console/chat/tools.js +34 -0
- package/dist/worker/console/chat/turn-process.js +237 -66
- package/dist/worker/console/chat/user-turn-outline.js +19 -0
- package/dist/worker/console/console-update-runtime.js +2 -2
- package/dist/worker/console/dag-confirmation.js +3 -0
- package/dist/worker/console/dag-execution-receipt.js +3 -0
- package/dist/worker/console/operator-actions.js +32 -3
- package/dist/worker/console/operator-user-error.js +28 -1
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/prd-intake-bridge.js +5 -12
- package/dist/worker/console/recovery-cta.js +14 -14
- package/dist/worker/console/server.js +5 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
- package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
- package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
- package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
- package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -1
- package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
- package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
- package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
- package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
- package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
- package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
- package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
- package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
- package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
- package/dist/worker/console/workspace-context.js +2 -0
- package/dist/worker/console/workspace-initialization.js +3 -3
- package/dist/worker/observability/read-model.js +23 -2
- package/dist/worker/observe/node-context-insights.js +479 -0
- package/dist/worker/observe/static/console-theme.css +174 -0
- package/dist/worker/observe/static/console-theme.js +36 -0
- package/dist/worker/observe/static/operator-chrome.css +85 -0
- package/dist/worker/observe/static/operator-chrome.js +51 -0
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
- package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
- package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
- package/dist/workflows/dag/budget-enforcement.js +31 -2
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-contract-facts.js +11 -8
- package/dist/workflows/dag/frontend-design-policy.js +6 -6
- package/dist/workflows/dag/frontend-durable-tools.js +90 -70
- package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
- package/dist/workflows/dag/frontend-plan-canary.js +66 -16
- package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
- package/dist/workflows/dag/frontend-plan-progress.js +92 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
- package/dist/workflows/dag/frontend-recovery-run.js +51 -46
- package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
- package/dist/workflows/dag/frontend-review-context.js +29 -1
- package/dist/workflows/dag/frontend-review-scopes.js +92 -2
- package/dist/workflows/dag/frontend-risk.js +8 -3
- package/dist/workflows/dag/frontend-root-observation.js +261 -0
- package/dist/workflows/dag/frontend-session-budget.js +52 -39
- package/dist/workflows/dag/frontend-session-context.js +10 -0
- package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
- package/dist/workflows/dag/frontend-verification-trace.js +6 -0
- package/dist/workflows/dag/frontend-writer-admission.js +3 -1
- package/dist/workflows/dag/init-hybrid.js +108 -37
- package/dist/workflows/dag/node-execution.js +11 -11
- package/dist/workflows/dag/output-protocol.js +1 -0
- package/dist/workflows/dag/rerun-feedback.js +35 -4
- package/dist/workflows/dag/rerun-task.js +62 -66
- package/dist/workflows/dag/runner.js +58 -12
- package/dist/workflows/dag/types.js +15 -8
- package/dist/workflows/loop/benchmark.js +11 -8
- package/docs/README.md +1 -1
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/governance/harness-methodology-verification.md +11 -0
- package/docs/templates/backend-test-dag.json +5 -4
- package/docs/templates/frontend-implementation-contract.schema.json +31 -11
- package/package.json +6 -4
- package/skills/frontend-design-review/SKILL.md +8 -11
- package/skills/frontend-design-review/references/review-checklist.md +3 -4
- package/skills/frontend-review/SKILL.md +5 -1
- package/skills/loop-agent/references/command-reference.md +8 -0
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
- package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
- package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { runtimeSelectionFromRecord } from "../../chat/runtime-selection.js";
|
|
3
3
|
import { hydrateSessionThread } from "../../chat/turn-process.js";
|
|
4
4
|
import { forgetActiveChatSession, readActiveChatSessionId, rememberActiveChatSession, } from "../../chat/workspace-landing.js";
|
|
@@ -8,6 +8,47 @@ import { isLostChatSessionError } from "./runtime-snapshot-store.js";
|
|
|
8
8
|
import { upsertCompactionMessage, } from "./compaction-message.js";
|
|
9
9
|
import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
|
|
10
10
|
import { SessionTitleWatcher } from "./session-title-watcher.js";
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function isSessionListItem(value) {
|
|
15
|
+
if (!isRecord(value))
|
|
16
|
+
return false;
|
|
17
|
+
const optionalString = (field) => field === undefined || typeof field === "string";
|
|
18
|
+
return (typeof value.sessionId === "string" &&
|
|
19
|
+
value.sessionId.trim().length > 0 &&
|
|
20
|
+
(value.state === "active" || value.state === "archived") &&
|
|
21
|
+
typeof value.updatedAt === "string" &&
|
|
22
|
+
optionalString(value.workspaceId) &&
|
|
23
|
+
optionalString(value.title) &&
|
|
24
|
+
optionalString(value.displayTitle) &&
|
|
25
|
+
optionalString(value.activeTurnId) &&
|
|
26
|
+
(value.isBlank === undefined || typeof value.isBlank === "boolean") &&
|
|
27
|
+
(value.sessionMode === undefined ||
|
|
28
|
+
value.sessionMode === "workflow" ||
|
|
29
|
+
value.sessionMode === "pi") &&
|
|
30
|
+
(value.turnPhase === undefined ||
|
|
31
|
+
value.turnPhase === "idle" ||
|
|
32
|
+
value.turnPhase === "running" ||
|
|
33
|
+
value.turnPhase === "stopping"));
|
|
34
|
+
}
|
|
35
|
+
/** A 2xx response is authoritative only after its versioned payload validates. */
|
|
36
|
+
export function parseSessionListBaseline(value) {
|
|
37
|
+
if (!isRecord(value) || value.schemaVersion !== 2) {
|
|
38
|
+
throw new Error("会话列表响应版本无效");
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(value.sessions) || !value.sessions.every(isSessionListItem)) {
|
|
41
|
+
throw new Error("会话列表响应缺少有效的 sessions");
|
|
42
|
+
}
|
|
43
|
+
const workspaceErrors = value.workspaceErrors ?? [];
|
|
44
|
+
if (!Array.isArray(workspaceErrors) || !workspaceErrors.every(isRecord)) {
|
|
45
|
+
throw new Error("会话列表响应包含无效的 workspaceErrors");
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
sessions: value.sessions,
|
|
49
|
+
workspaceErrors: workspaceErrors,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
11
52
|
/** Pure list mutations keep optimistic UI deterministic and easy to verify. */
|
|
12
53
|
export function insertOptimisticSession(list, row) {
|
|
13
54
|
return [row, ...list.filter((item) => item.sessionId !== row.sessionId)];
|
|
@@ -179,10 +220,11 @@ function mergeCompactionMessages(messages, compactions) {
|
|
|
179
220
|
* compact, plus browser-refresh recovery from the durable session store. */
|
|
180
221
|
export function useChatSessions(params) {
|
|
181
222
|
const { origin, refs, setSession, thread, composer, browser, setError, setNotice, onCompactionChange, setProcessOpen, setAutoScroll, } = params;
|
|
182
|
-
const { applyActivatedSession, resetForNewSession, setMessages } = thread;
|
|
223
|
+
const { applyActivatedSession, resetForNewSession, setMessages, refreshSessionTelemetry, } = thread;
|
|
183
224
|
const { seedSession, releaseSession } = composer;
|
|
184
225
|
const { setPreviews, setActivePreviewPath } = browser;
|
|
185
226
|
const { workspaceId, blocked } = params;
|
|
227
|
+
const visibleWorkspaceIds = useMemo(() => new Set(params.visibleWorkspaceIds), [params.visibleWorkspaceIds]);
|
|
186
228
|
const workspaceQuery = workspaceId
|
|
187
229
|
? `?workspaceId=${encodeURIComponent(workspaceId)}`
|
|
188
230
|
: "";
|
|
@@ -198,11 +240,19 @@ export function useChatSessions(params) {
|
|
|
198
240
|
return owner ? `?workspaceId=${encodeURIComponent(owner)}` : "";
|
|
199
241
|
}, [refs]);
|
|
200
242
|
const [sessionList, setSessionList] = useState([]);
|
|
243
|
+
const [sessionListStatus, setSessionListStatus] = useState({
|
|
244
|
+
phase: "pending",
|
|
245
|
+
state: "idle",
|
|
246
|
+
error: null,
|
|
247
|
+
});
|
|
201
248
|
const [selectedSession, setSelectedSession] = useState(null);
|
|
202
249
|
const selectedSessionRef = useRef(null);
|
|
203
250
|
selectedSessionRef.current = selectedSession;
|
|
204
251
|
const sessionListRef = useRef([]);
|
|
205
252
|
const sessionListRequestVersionRef = useRef(0);
|
|
253
|
+
const sessionListRefreshRef = useRef(null);
|
|
254
|
+
const sessionListRefreshQueuedRef = useRef(false);
|
|
255
|
+
const sessionListGlobalErrorRef = useRef(null);
|
|
206
256
|
const pendingDeletedIdsRef = useRef(new Set());
|
|
207
257
|
const pendingCreatedRowsRef = useRef(new Map());
|
|
208
258
|
const sessionMutationVersionRef = useRef(0);
|
|
@@ -256,43 +306,101 @@ export function useChatSessions(params) {
|
|
|
256
306
|
setProcessOpen,
|
|
257
307
|
setAutoScroll,
|
|
258
308
|
]);
|
|
259
|
-
const refreshSessionList = useCallback(
|
|
309
|
+
const refreshSessionList = useCallback(() => {
|
|
260
310
|
if (blocked)
|
|
261
|
-
return; // AC-R5-002: fail-closed, zero
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/sessions`, {
|
|
269
|
-
credentials: "include",
|
|
270
|
-
});
|
|
271
|
-
const body = (await res.json().catch(() => ({})));
|
|
272
|
-
if (version !== sessionListRequestVersionRef.current)
|
|
273
|
-
return;
|
|
274
|
-
if (!res.ok) {
|
|
275
|
-
setError(body.error?.message ?? `会话列表加载失败(HTTP ${res.status})`);
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
const sessions = body.sessions ?? [];
|
|
279
|
-
const durable = sessions.filter((item) => !pendingDeletedIdsRef.current.has(item.sessionId));
|
|
280
|
-
const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => !durable.some((session) => session.sessionId === item.sessionId));
|
|
281
|
-
// Preserve every workspace's summaries. ChatWorkspaceSidebar applies the
|
|
282
|
-
// visible recent-item limit independently inside each workspace group.
|
|
283
|
-
const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
|
|
284
|
-
sessionListRef.current = next;
|
|
285
|
-
setSessionList(next);
|
|
286
|
-
if (body.workspaceErrors?.length) {
|
|
287
|
-
setError(`部分工作区的会话加载失败(${body.workspaceErrors.length} 个)`);
|
|
288
|
-
}
|
|
311
|
+
return Promise.resolve(); // AC-R5-002: fail-closed, zero requests
|
|
312
|
+
if (sessionListRefreshRef.current) {
|
|
313
|
+
// The newest refresh may carry a different visible-workspace projection.
|
|
314
|
+
// Do not briefly commit the older scope before the queued baseline runs.
|
|
315
|
+
sessionListRequestVersionRef.current += 1;
|
|
316
|
+
sessionListRefreshQueuedRef.current = true;
|
|
317
|
+
return sessionListRefreshRef.current;
|
|
289
318
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
319
|
+
const request = (async () => {
|
|
320
|
+
do {
|
|
321
|
+
sessionListRefreshQueuedRef.current = false;
|
|
322
|
+
const version = ++sessionListRequestVersionRef.current;
|
|
323
|
+
setSessionListStatus((current) => ({
|
|
324
|
+
...current,
|
|
325
|
+
state: "loading",
|
|
326
|
+
error: null,
|
|
327
|
+
}));
|
|
328
|
+
try {
|
|
329
|
+
// The sidebar is one unscoped multi-workspace index. Session detail
|
|
330
|
+
// and mutations remain owner-scoped.
|
|
331
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions`, {
|
|
332
|
+
credentials: "include",
|
|
333
|
+
});
|
|
334
|
+
let rawBody;
|
|
335
|
+
try {
|
|
336
|
+
rawBody = await res.json();
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
throw new Error("会话列表响应不是有效 JSON");
|
|
340
|
+
}
|
|
341
|
+
if (version !== sessionListRequestVersionRef.current) {
|
|
342
|
+
sessionListRefreshQueuedRef.current = true;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
const message = isRecord(rawBody) &&
|
|
347
|
+
isRecord(rawBody.error) &&
|
|
348
|
+
typeof rawBody.error.message === "string"
|
|
349
|
+
? rawBody.error.message
|
|
350
|
+
: `会话列表加载失败(HTTP ${res.status})`;
|
|
351
|
+
throw new Error(message);
|
|
352
|
+
}
|
|
353
|
+
const baseline = parseSessionListBaseline(rawBody);
|
|
354
|
+
if (baseline.workspaceErrors.length > 0) {
|
|
355
|
+
throw new Error(`部分工作区的会话加载失败(${baseline.workspaceErrors.length} 个)`);
|
|
356
|
+
}
|
|
357
|
+
const belongsToVisibleWorkspace = (item) => params.workspacesLoading ||
|
|
358
|
+
!item.workspaceId ||
|
|
359
|
+
visibleWorkspaceIds.has(item.workspaceId);
|
|
360
|
+
const durable = baseline.sessions.filter((item) => belongsToVisibleWorkspace(item) &&
|
|
361
|
+
!pendingDeletedIdsRef.current.has(item.sessionId));
|
|
362
|
+
const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => belongsToVisibleWorkspace(item) &&
|
|
363
|
+
!durable.some((session) => session.sessionId === item.sessionId));
|
|
364
|
+
const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
|
|
365
|
+
sessionListRef.current = next;
|
|
366
|
+
setSessionList(next);
|
|
367
|
+
setSessionListStatus({ phase: "ready", state: "idle", error: null });
|
|
368
|
+
const previousListError = sessionListGlobalErrorRef.current;
|
|
369
|
+
if (previousListError) {
|
|
370
|
+
setError((current) => current === previousListError ? null : current);
|
|
371
|
+
sessionListGlobalErrorRef.current = null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (version !== sessionListRequestVersionRef.current) {
|
|
376
|
+
sessionListRefreshQueuedRef.current = true;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const message = `会话列表加载失败:${error instanceof Error ? error.message : String(error)}`;
|
|
380
|
+
setSessionListStatus((current) => ({
|
|
381
|
+
...current,
|
|
382
|
+
state: "error",
|
|
383
|
+
error: message,
|
|
384
|
+
}));
|
|
385
|
+
sessionListGlobalErrorRef.current = message;
|
|
386
|
+
setError(message);
|
|
387
|
+
}
|
|
388
|
+
} while (sessionListRefreshQueuedRef.current && !blocked);
|
|
389
|
+
})();
|
|
390
|
+
sessionListRefreshRef.current = request;
|
|
391
|
+
void request.finally(() => {
|
|
392
|
+
if (sessionListRefreshRef.current === request) {
|
|
393
|
+
sessionListRefreshRef.current = null;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
return request;
|
|
397
|
+
}, [origin, blocked, setError, visibleWorkspaceIds, params.workspacesLoading]);
|
|
398
|
+
const invalidateSessionListBaseline = useCallback(() => {
|
|
399
|
+
sessionListRequestVersionRef.current += 1;
|
|
400
|
+
if (sessionListRefreshRef.current) {
|
|
401
|
+
sessionListRefreshQueuedRef.current = true;
|
|
294
402
|
}
|
|
295
|
-
}, [
|
|
403
|
+
}, []);
|
|
296
404
|
const deleteSession = useCallback(async (sessionId) => {
|
|
297
405
|
if (pendingDeletedIdsRef.current.has(sessionId))
|
|
298
406
|
return false;
|
|
@@ -302,7 +410,7 @@ export function useChatSessions(params) {
|
|
|
302
410
|
if (!removed || removedIndex < 0)
|
|
303
411
|
return false;
|
|
304
412
|
// No pre-mutation snapshot may overwrite this deletion or its rollback.
|
|
305
|
-
|
|
413
|
+
invalidateSessionListBaseline();
|
|
306
414
|
pendingDeletedIdsRef.current.add(sessionId);
|
|
307
415
|
deleteRequestVersionsRef.current.set(sessionId, requestId);
|
|
308
416
|
const activeSession = refs.sessionRef.current?.sessionId === sessionId
|
|
@@ -335,7 +443,7 @@ export function useChatSessions(params) {
|
|
|
335
443
|
const restore = () => {
|
|
336
444
|
if (deleteRequestVersionsRef.current.get(sessionId) !== requestId)
|
|
337
445
|
return;
|
|
338
|
-
|
|
446
|
+
invalidateSessionListBaseline();
|
|
339
447
|
pendingDeletedIdsRef.current.delete(sessionId);
|
|
340
448
|
deleteRequestVersionsRef.current.delete(sessionId);
|
|
341
449
|
const restored = restoreSessionAt(sessionListRef.current.filter((item) => item.sessionId !== sessionId), removed, removedIndex);
|
|
@@ -406,6 +514,7 @@ export function useChatSessions(params) {
|
|
|
406
514
|
setError,
|
|
407
515
|
sessionOwnerQuery,
|
|
408
516
|
workspaceId,
|
|
517
|
+
invalidateSessionListBaseline,
|
|
409
518
|
]);
|
|
410
519
|
const forgetLostSession = useCallback((sessionId, ownerWorkspaceId) => {
|
|
411
520
|
const active = refs.sessionRef.current;
|
|
@@ -418,6 +527,7 @@ export function useChatSessions(params) {
|
|
|
418
527
|
forgetActiveChatSession(window.localStorage, scope, sessionId);
|
|
419
528
|
releaseSession(identity);
|
|
420
529
|
pendingDeletedIdsRef.current.add(sessionId);
|
|
530
|
+
invalidateSessionListBaseline();
|
|
421
531
|
const { next } = removeSessionById(sessionListRef.current, sessionId);
|
|
422
532
|
sessionListRef.current = next;
|
|
423
533
|
setSessionList(next);
|
|
@@ -430,7 +540,15 @@ export function useChatSessions(params) {
|
|
|
430
540
|
setSession(null);
|
|
431
541
|
}
|
|
432
542
|
void refreshSessionList();
|
|
433
|
-
}, [
|
|
543
|
+
}, [
|
|
544
|
+
refs,
|
|
545
|
+
releaseSession,
|
|
546
|
+
resetActiveProjection,
|
|
547
|
+
setSession,
|
|
548
|
+
workspaceId,
|
|
549
|
+
refreshSessionList,
|
|
550
|
+
invalidateSessionListBaseline,
|
|
551
|
+
]);
|
|
434
552
|
const activateSession = useCallback(async (sessionId, ownerWorkspaceId) => {
|
|
435
553
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
436
554
|
const listedOwner = sessionListRef.current.find((item) => item.sessionId === sessionId)?.workspaceId;
|
|
@@ -497,9 +615,13 @@ export function useChatSessions(params) {
|
|
|
497
615
|
applyActivatedSession({
|
|
498
616
|
messages: hydrated.messages,
|
|
499
617
|
toolCalls: hydrated.toolCalls,
|
|
618
|
+
turnStates: body.session.turnStates,
|
|
619
|
+
conversationEvents: body.conversationEvents,
|
|
500
620
|
sessionStats: body.sessionStats,
|
|
501
621
|
usage: body.usage,
|
|
502
622
|
turnTelemetry: body.turnTelemetry,
|
|
623
|
+
userTurnOutline: body.userTurnOutline,
|
|
624
|
+
sddSessionIdentity: body.sddDataAlignment ?? null,
|
|
503
625
|
streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
|
|
504
626
|
turnPhase: body.session.turnPhase ??
|
|
505
627
|
(body.session.activeTurnId ? "running" : "idle"),
|
|
@@ -535,6 +657,32 @@ export function useChatSessions(params) {
|
|
|
535
657
|
forgetLostSession,
|
|
536
658
|
]);
|
|
537
659
|
activateSessionRef.current = activateSession;
|
|
660
|
+
const forkSessionAt = useCallback(async (throughMessageId) => {
|
|
661
|
+
const current = refs.sessionRef.current;
|
|
662
|
+
if (!current || refs.activeTurnIdRef.current)
|
|
663
|
+
return false;
|
|
664
|
+
try {
|
|
665
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(current.sessionId)}/fork${sessionOwnerQuery(current.workspaceId)}`, {
|
|
666
|
+
method: "POST",
|
|
667
|
+
credentials: "include",
|
|
668
|
+
headers: {
|
|
669
|
+
"content-type": "application/json",
|
|
670
|
+
"x-loop-console-confirmation": confirmationToken(),
|
|
671
|
+
},
|
|
672
|
+
body: JSON.stringify({ throughMessageId }),
|
|
673
|
+
});
|
|
674
|
+
const body = (await res.json().catch(() => ({})));
|
|
675
|
+
if (!res.ok || !body.sessionId) {
|
|
676
|
+
throw new Error(body.error?.message ?? "创建分叉会话失败");
|
|
677
|
+
}
|
|
678
|
+
await refreshSessionList();
|
|
679
|
+
return activateSession(body.sessionId, current.workspaceId);
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}, [activateSession, origin, refreshSessionList, refs, sessionOwnerQuery, setError]);
|
|
538
686
|
useEffect(() => {
|
|
539
687
|
void refreshSessionList();
|
|
540
688
|
}, [refreshSessionList]);
|
|
@@ -631,9 +779,13 @@ export function useChatSessions(params) {
|
|
|
631
779
|
applyActivatedSession({
|
|
632
780
|
messages: hydrated.messages,
|
|
633
781
|
toolCalls: hydrated.toolCalls,
|
|
782
|
+
turnStates: body.session.turnStates,
|
|
783
|
+
conversationEvents: body.conversationEvents,
|
|
634
784
|
sessionStats: body.sessionStats,
|
|
635
785
|
usage: body.usage,
|
|
636
786
|
turnTelemetry: body.turnTelemetry,
|
|
787
|
+
userTurnOutline: body.userTurnOutline,
|
|
788
|
+
sddSessionIdentity: body.sddDataAlignment ?? null,
|
|
637
789
|
streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
|
|
638
790
|
turnPhase: body.session.turnPhase ??
|
|
639
791
|
(body.session.activeTurnId ? "running" : "idle"),
|
|
@@ -688,6 +840,7 @@ export function useChatSessions(params) {
|
|
|
688
840
|
optimisticState: "creating",
|
|
689
841
|
};
|
|
690
842
|
pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
|
|
843
|
+
invalidateSessionListBaseline();
|
|
691
844
|
const optimisticList = insertOptimisticSession(sessionListRef.current, optimisticRow);
|
|
692
845
|
sessionListRef.current = optimisticList;
|
|
693
846
|
setSessionList(optimisticList);
|
|
@@ -703,6 +856,14 @@ export function useChatSessions(params) {
|
|
|
703
856
|
creatingRef: creatingSessionRef,
|
|
704
857
|
});
|
|
705
858
|
pendingCreatedRowsRef.current.delete(optimisticSessionId);
|
|
859
|
+
const durableRow = sessionListRef.current.find((item) => item.sessionId === created.sessionId);
|
|
860
|
+
if (durableRow) {
|
|
861
|
+
pendingCreatedRowsRef.current.set(created.sessionId, durableRow);
|
|
862
|
+
invalidateSessionListBaseline();
|
|
863
|
+
void refreshSessionList().finally(() => {
|
|
864
|
+
pendingCreatedRowsRef.current.delete(created.sessionId);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
706
867
|
return created;
|
|
707
868
|
}
|
|
708
869
|
catch (error) {
|
|
@@ -712,7 +873,13 @@ export function useChatSessions(params) {
|
|
|
712
873
|
setSessionList(cleaned);
|
|
713
874
|
throw error;
|
|
714
875
|
}
|
|
715
|
-
}, [
|
|
876
|
+
}, [
|
|
877
|
+
origin,
|
|
878
|
+
refreshSessionList,
|
|
879
|
+
workspaceId,
|
|
880
|
+
setError,
|
|
881
|
+
invalidateSessionListBaseline,
|
|
882
|
+
]);
|
|
716
883
|
const createSession = useCallback(async (options) => {
|
|
717
884
|
// Suppress concurrent create from any entry (sidebar / Landing / chip).
|
|
718
885
|
if (creatingSessionRef.current)
|
|
@@ -760,6 +927,7 @@ export function useChatSessions(params) {
|
|
|
760
927
|
setSelectedSession(created);
|
|
761
928
|
rememberActiveChatSession(window.localStorage, created.workspaceId ?? options?.workspaceId ?? workspaceId, created.sessionId);
|
|
762
929
|
setSession(created);
|
|
930
|
+
void refreshSessionTelemetry();
|
|
763
931
|
return created;
|
|
764
932
|
}
|
|
765
933
|
catch (e) {
|
|
@@ -783,6 +951,7 @@ export function useChatSessions(params) {
|
|
|
783
951
|
activateSession,
|
|
784
952
|
blocked,
|
|
785
953
|
workspaceId,
|
|
954
|
+
refreshSessionTelemetry,
|
|
786
955
|
]);
|
|
787
956
|
// Phase-1: fork / in-session branch / mainline switch UI deferred; API routes remain.
|
|
788
957
|
/** Single compact helper for toolbar + `/compact <instructions>` (AC-1). */
|
|
@@ -892,11 +1061,13 @@ export function useChatSessions(params) {
|
|
|
892
1061
|
};
|
|
893
1062
|
return {
|
|
894
1063
|
sessionList,
|
|
1064
|
+
sessionListStatus,
|
|
895
1065
|
selectedSession,
|
|
896
1066
|
sessionSwitching: Boolean(selectedSession &&
|
|
897
1067
|
refs.sessionRef.current?.sessionId !== selectedSession.sessionId),
|
|
898
1068
|
creatingSession,
|
|
899
1069
|
activateSession,
|
|
1070
|
+
forkSessionAt,
|
|
900
1071
|
refreshSessionList,
|
|
901
1072
|
deleteSession,
|
|
902
1073
|
createSession,
|
|
@@ -1,9 +1,52 @@
|
|
|
1
1
|
import { useCallback, useEffect } from "react";
|
|
2
2
|
import { formatOperatorUserError } from "../../operator-user-error.js";
|
|
3
|
+
import { inspectChatSseContinuity, readChatStreamReadyCursor, selectChatSseRepairBlocks, selectLaterChatEventId, } from "./chat-stream-continuity.js";
|
|
3
4
|
import { confirmationToken, requestId, sleep } from "./format.js";
|
|
4
5
|
import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
|
|
5
|
-
import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
|
|
6
|
+
import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, createBoundedOwnerSignal, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
|
|
6
7
|
import { appendPendingSubmission, removePendingSubmission } from "./pending-user-message.js";
|
|
8
|
+
import { createTurnStateReconcileQueue, turnStateReconcileTrigger, } from "./turn-state-projection.js";
|
|
9
|
+
const STATE_REQUEST_TIMEOUT_MS = 2_000;
|
|
10
|
+
async function fetchReconciledSessionState(origin, sessionId, options) {
|
|
11
|
+
const bounded = createBoundedOwnerSignal({
|
|
12
|
+
ownerSignal: options?.ownerSignal,
|
|
13
|
+
timeoutMs: STATE_REQUEST_TIMEOUT_MS,
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(operatorChatStateUrl(origin, sessionId, options?.turnIds), {
|
|
17
|
+
credentials: "include",
|
|
18
|
+
signal: bounded.signal,
|
|
19
|
+
});
|
|
20
|
+
const body = (await res.json().catch(() => ({})));
|
|
21
|
+
if (!res.ok)
|
|
22
|
+
return null;
|
|
23
|
+
return body.state ?? null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
bounded.dispose();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function readChatSseTurnId(block) {
|
|
33
|
+
const data = block
|
|
34
|
+
.split("\n")
|
|
35
|
+
.filter((line) => line.startsWith("data: "))
|
|
36
|
+
.map((line) => line.slice(6))
|
|
37
|
+
.join("");
|
|
38
|
+
if (!data)
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(data);
|
|
42
|
+
return typeof parsed.turnId === "string" && parsed.turnId
|
|
43
|
+
? parsed.turnId
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
7
50
|
/** Project ChatRefs into the executable ownership bag used by the controller. */
|
|
8
51
|
function ownershipBagFromRefs(refs) {
|
|
9
52
|
return {
|
|
@@ -104,6 +147,18 @@ export function useChatStream(params) {
|
|
|
104
147
|
* means replay cannot render duplicate messages while live text remains
|
|
105
148
|
* smooth (M4.1/M4.3). */
|
|
106
149
|
const openEventsStream = useCallback(async (sessionId, generation, signal) => {
|
|
150
|
+
const gapReplayBuffer = [];
|
|
151
|
+
const turnStateReconciles = createTurnStateReconcileQueue({
|
|
152
|
+
read: (turnIds) => fetchReconciledSessionState(origin, sessionId, {
|
|
153
|
+
turnIds,
|
|
154
|
+
ownerSignal: signal,
|
|
155
|
+
}),
|
|
156
|
+
apply: (summaries) => thread.reconcileTurnStates([...summaries]),
|
|
157
|
+
wait: (delayMs) => sleep(delayMs, signal),
|
|
158
|
+
isCancelled: () => signal.aborted ||
|
|
159
|
+
refs.sessionRef.current?.sessionId !== sessionId ||
|
|
160
|
+
refs.sessionGenerationRef.current !== generation,
|
|
161
|
+
});
|
|
107
162
|
while (!signal.aborted) {
|
|
108
163
|
const state = refs.eventsStreamStateRef.current;
|
|
109
164
|
if (!state ||
|
|
@@ -129,21 +184,48 @@ export function useChatStream(params) {
|
|
|
129
184
|
const decoder = new TextDecoder();
|
|
130
185
|
let buffer = "";
|
|
131
186
|
let replay = true;
|
|
187
|
+
let replayRepairNeeded = false;
|
|
188
|
+
let liveContinuityCursor = null;
|
|
132
189
|
const fallbackAssistantId = requestId("a");
|
|
133
|
-
for (;;) {
|
|
190
|
+
readLoop: for (;;) {
|
|
134
191
|
const { done, value } = await reader.read();
|
|
135
192
|
if (done)
|
|
136
193
|
break;
|
|
137
194
|
buffer += decoder.decode(value, { stream: true });
|
|
138
195
|
const blocks = buffer.split("\n\n");
|
|
139
196
|
buffer = blocks.pop() ?? "";
|
|
140
|
-
for (
|
|
141
|
-
|
|
197
|
+
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex += 1) {
|
|
198
|
+
const block = blocks[blockIndex];
|
|
199
|
+
if (block === undefined)
|
|
200
|
+
continue;
|
|
201
|
+
const eventName = sseEventName(block);
|
|
202
|
+
const reconcileTrigger = turnStateReconcileTrigger(replay, eventName);
|
|
203
|
+
if (reconcileTrigger) {
|
|
204
|
+
const settledTurnId = readChatSseTurnId(block);
|
|
205
|
+
if (settledTurnId) {
|
|
206
|
+
turnStateReconciles.enqueue(settledTurnId, {
|
|
207
|
+
defer: reconcileTrigger === "after-replay",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!replay) {
|
|
212
|
+
const continuity = inspectChatSseContinuity(block, liveContinuityCursor);
|
|
213
|
+
if (continuity.action === "reconnect") {
|
|
214
|
+
// Buffer this block and the rest of the decoded chunk before closing
|
|
215
|
+
// the reader. Retained replay repairs structure; the buffer preserves
|
|
216
|
+
// cumulative text frames that persistence may have coalesced away.
|
|
217
|
+
gapReplayBuffer.push(...blocks.slice(blockIndex));
|
|
218
|
+
replayRepairNeeded = true;
|
|
219
|
+
break readLoop;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (eventName === "stream_ready") {
|
|
142
223
|
const current = refs.eventsStreamStateRef.current;
|
|
143
224
|
if (current?.sessionId === sessionId &&
|
|
144
225
|
current.generation === generation)
|
|
145
226
|
current.ready = true;
|
|
146
227
|
replay = false;
|
|
228
|
+
liveContinuityCursor = readChatStreamReadyCursor(block);
|
|
147
229
|
// Fall through: the applier owns the buffered-replay flush and
|
|
148
230
|
// returns immediately for this event. `continue` here silently
|
|
149
231
|
// dropped that flush, so a reconnect that landed after the
|
|
@@ -155,15 +237,41 @@ export function useChatStream(params) {
|
|
|
155
237
|
targetSessionId: sessionId,
|
|
156
238
|
generation,
|
|
157
239
|
});
|
|
158
|
-
if (eventId)
|
|
159
|
-
refs.lastEventIdRef.current = eventId;
|
|
240
|
+
if (eventId) {
|
|
241
|
+
refs.lastEventIdRef.current = selectLaterChatEventId(refs.lastEventIdRef.current, eventId);
|
|
242
|
+
if (!replay)
|
|
243
|
+
liveContinuityCursor = selectLaterChatEventId(liveContinuityCursor, eventId);
|
|
244
|
+
}
|
|
245
|
+
if (eventName === "stream_ready" &&
|
|
246
|
+
gapReplayBuffer.length > 0) {
|
|
247
|
+
const repairedBlocks = selectChatSseRepairBlocks(gapReplayBuffer.splice(0), liveContinuityCursor);
|
|
248
|
+
for (const repairedBlock of repairedBlocks) {
|
|
249
|
+
const repairedEventId = applySseBlock(repairedBlock, refs.assistantIdRef.current ?? fallbackAssistantId, {
|
|
250
|
+
replay: false,
|
|
251
|
+
targetSessionId: sessionId,
|
|
252
|
+
generation,
|
|
253
|
+
});
|
|
254
|
+
if (!repairedEventId)
|
|
255
|
+
continue;
|
|
256
|
+
refs.lastEventIdRef.current = selectLaterChatEventId(refs.lastEventIdRef.current, repairedEventId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (eventName === "stream_ready") {
|
|
260
|
+
void turnStateReconciles.start();
|
|
261
|
+
}
|
|
160
262
|
}
|
|
161
263
|
}
|
|
264
|
+
if (replayRepairNeeded) {
|
|
265
|
+
await reader.cancel().catch(() => undefined);
|
|
266
|
+
}
|
|
162
267
|
const current = refs.eventsStreamStateRef.current;
|
|
163
268
|
if (current?.sessionId === sessionId &&
|
|
164
269
|
current.generation === generation)
|
|
165
270
|
current.ready = false;
|
|
166
|
-
|
|
271
|
+
// A detected hole is expected to be immediately recoverable from the
|
|
272
|
+
// durable ring. Other EOFs retain the normal reconnect backoff.
|
|
273
|
+
if (!replayRepairNeeded)
|
|
274
|
+
await sleep(1_000, signal);
|
|
167
275
|
}
|
|
168
276
|
catch {
|
|
169
277
|
if (signal.aborted)
|
|
@@ -171,7 +279,7 @@ export function useChatStream(params) {
|
|
|
171
279
|
await sleep(2_000, signal);
|
|
172
280
|
}
|
|
173
281
|
}
|
|
174
|
-
}, [origin, refs, applySseBlock]);
|
|
282
|
+
}, [origin, refs, applySseBlock, thread.reconcileTurnStates]);
|
|
175
283
|
const startEventsStream = useCallback((sessionId, generation) => {
|
|
176
284
|
const existing = refs.eventsStreamStateRef.current;
|
|
177
285
|
if (existing?.sessionId === sessionId &&
|
|
@@ -207,20 +315,7 @@ export function useChatStream(params) {
|
|
|
207
315
|
}
|
|
208
316
|
throw new Error("事件连接未就绪,请检查 Console 连接后重试");
|
|
209
317
|
}, [refs, startEventsStream]);
|
|
210
|
-
const fetchSessionState = useCallback(
|
|
211
|
-
try {
|
|
212
|
-
const res = await fetch(operatorChatStateUrl(origin, sessionId), {
|
|
213
|
-
credentials: "include",
|
|
214
|
-
});
|
|
215
|
-
const body = (await res.json().catch(() => ({})));
|
|
216
|
-
if (!res.ok)
|
|
217
|
-
return null;
|
|
218
|
-
return body.state ?? null;
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
}, [origin]);
|
|
318
|
+
const fetchSessionState = useCallback((sessionId) => fetchReconciledSessionState(origin, sessionId), [origin]);
|
|
224
319
|
const postCreateTurn = useCallback(async (input) => {
|
|
225
320
|
try {
|
|
226
321
|
// Resolve against the configured Console origin and fail closed if a
|
|
@@ -1000,20 +1095,20 @@ export function useChatStream(params) {
|
|
|
1000
1095
|
const sessionId = session.sessionId;
|
|
1001
1096
|
const sessionIdentity = { ...session };
|
|
1002
1097
|
const generation = refs.sessionGenerationRef.current;
|
|
1098
|
+
const stateReconcileAbort = new AbortController();
|
|
1003
1099
|
let cancelled = false;
|
|
1004
1100
|
let syncInFlight = null;
|
|
1005
1101
|
const runSync = async () => {
|
|
1006
1102
|
try {
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1103
|
+
const state = await fetchReconciledSessionState(origin, sessionId, {
|
|
1104
|
+
ownerSignal: stateReconcileAbort.signal,
|
|
1009
1105
|
});
|
|
1010
|
-
const body = (await res.json().catch(() => ({})));
|
|
1011
1106
|
if (cancelled ||
|
|
1012
|
-
!
|
|
1107
|
+
!state ||
|
|
1013
1108
|
refs.sessionRef.current?.sessionId !== sessionId ||
|
|
1014
1109
|
refs.sessionGenerationRef.current !== generation)
|
|
1015
1110
|
return;
|
|
1016
|
-
|
|
1111
|
+
thread.reconcileTurnStates(state?.turnStates);
|
|
1017
1112
|
const pending = refs.pendingSubmissionRef.current;
|
|
1018
1113
|
// Pending ownership reconcile. Controller owns bind/release; same
|
|
1019
1114
|
// clientRequestId only. Re-enter after acceptedTurnId so terminal
|
|
@@ -1158,6 +1253,7 @@ export function useChatStream(params) {
|
|
|
1158
1253
|
void sync().finally(scheduleStopRetry);
|
|
1159
1254
|
return () => {
|
|
1160
1255
|
cancelled = true;
|
|
1256
|
+
stateReconcileAbort.abort();
|
|
1161
1257
|
window.clearInterval(interval);
|
|
1162
1258
|
if (stopRetryTimer !== undefined)
|
|
1163
1259
|
window.clearTimeout(stopRetryTimer);
|
|
@@ -1174,6 +1270,7 @@ export function useChatStream(params) {
|
|
|
1174
1270
|
markIdleIfCurrent,
|
|
1175
1271
|
releaseSubmitOwnership,
|
|
1176
1272
|
postAbortForTurn,
|
|
1273
|
+
thread.reconcileTurnStates,
|
|
1177
1274
|
]);
|
|
1178
1275
|
// Exactly one owner starts/aborts GET /events. Switching sessions or
|
|
1179
1276
|
// recovering a cursor aborts the prior stream before the next one is made.
|