@tea-agent/loop-agent 0.39.3 → 0.40.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -92
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +12 -0
- package/dist/cli/program.js +23 -1
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/executors/dag-pi-executor.js +108 -19
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +49 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +42 -2
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +35 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/operator/capabilities.js +14 -0
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +10 -0
- package/dist/task/source-prepare/fragment-inventory.js +1 -14
- package/dist/worker/console/chat/chat-event-store.js +77 -7
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +389 -56
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +94 -64
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- package/dist/worker/console/console-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +1 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -3
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +104 -3
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +128 -0
- package/dist/worker/console/routes.js +927 -8
- package/dist/worker/console/security.js +39 -4
- package/dist/worker/console/server.js +532 -187
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CpWvwARc.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DctXgO-g.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-D4mIkoQl.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-ClG0--KO.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-CwdLk78T.js} +1 -1
- package/dist/worker/console/static/assets/channel-C9VQZ-ru.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DKczcu-O.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-BV6F-vBI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtojVnvA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-BjDQRGnO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-I0fV6ngU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-BM8C-mmC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-BEK-a15e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-CQsg_fSR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-B-BcaUm3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D1Bu08fg.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BsCkw5MF.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-q8nwtxBR.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CruhpURw.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-CJne6iYF.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-CbeLvBU5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-DmCxH1Pe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-DJ5OYhtC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-B-UN-hln.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-CcEro8Ey.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BBBzDden.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-xKEyH-oo.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-C7xl63VC.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-nthQQ9nR.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CWN5mE33.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-Wn86tQ6N.js} +1 -1
- package/dist/worker/console/static/assets/index-D-0NVyMk.css +1 -0
- package/dist/worker/console/static/assets/index-gwyOxQiS.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-_WQymGtW.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-COBeivBm.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-CwGOJl3Q.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-Ci0CJ5Vi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CPjMq6vN.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-Z0wGoe0b.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-dISked0d.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-BJxgof7Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-CIblU8eW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-vwYiqh8E.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-_iOsVeFm.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Dk0MLxO_.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-Dx7HtLVW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-DRuWjlpL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Bhy9DQAA.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-DAx9twnU.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dmdf8cGy.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-okMlvVrM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-W1IcAFff.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DXqCKe37.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CeIUkwBh.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-BP76BKTN.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-BpVI0OpF.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +260 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +94 -10
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +175 -1
- package/dist/worker/observe/static/styles.css +1039 -128
- package/dist/worker/observe/static/views/dag-inspector.js +1218 -60
- package/dist/worker/observe/static/views/dag.js +2 -1
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/session-timeline.js +75 -10
- package/dist/worker/observe/static/views/task.js +1 -1
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
- package/dist/workflows/dag/backend-test-writer-completeness.js +7 -12
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +1 -89
- package/dist/workflows/dag/init-hybrid.js +178 -47
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +112 -27
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +29 -4
- package/dist/workflows/dag/rerun-plan.js +272 -0
- package/dist/workflows/dag/rerun-run.js +390 -12
- package/dist/workflows/dag/skill-snapshot.js +1 -0
- package/dist/workflows/dag/types.js +203 -8
- package/docs/README.md +1 -0
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/agent-dag.schema.json +50 -0
- package/docs/templates/backend-test-dag.json +265 -237
- package/harness.json +3 -3
- package/package.json +5 -1
- package/skills/loop-agent/references/command-reference.md +6 -0
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- /package/dist/{worker → infrastructure}/console/operation-store.js +0 -0
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect } from "react";
|
|
|
2
2
|
import { formatOperatorUserError } from "../../operator-user-error.js";
|
|
3
3
|
import { confirmationToken, requestId, sleep } from "./format.js";
|
|
4
4
|
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, } from "./turn-stream-controller.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
6
|
/** Project ChatRefs into the executable ownership bag used by the controller. */
|
|
7
7
|
function ownershipBagFromRefs(refs) {
|
|
8
8
|
return {
|
|
@@ -46,6 +46,7 @@ function sseEventName(block) {
|
|
|
46
46
|
function sleepBriefly() {
|
|
47
47
|
return new Promise((resolve) => window.setTimeout(resolve, 25));
|
|
48
48
|
}
|
|
49
|
+
const STOP_RECONCILE_DELAYS_MS = [100, 250, 500, 1_000, 2_000];
|
|
49
50
|
function clearEmptyAssistant(setMessages, assistantId) {
|
|
50
51
|
if (!assistantId)
|
|
51
52
|
return;
|
|
@@ -57,7 +58,7 @@ function clearEmptyAssistant(setMessages, assistantId) {
|
|
|
57
58
|
* detached Turn resource only; it never opens a second inline SSE consumer. */
|
|
58
59
|
export function useChatStream(params) {
|
|
59
60
|
const { origin, refs, session, thread, composer, setError, setAutoScroll, setNotice, recoveryVersion = 0, watchSessionTitle, } = params;
|
|
60
|
-
const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, } = thread;
|
|
61
|
+
const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, setDeferredTurns, } = thread;
|
|
61
62
|
const { setInput, setPendingImages, recordHistory } = composer;
|
|
62
63
|
const showNotice = useCallback((message) => {
|
|
63
64
|
if (!setNotice)
|
|
@@ -73,15 +74,26 @@ export function useChatStream(params) {
|
|
|
73
74
|
if (refs.sessionRef.current?.sessionId !== sessionId ||
|
|
74
75
|
refs.sessionGenerationRef.current !== generation)
|
|
75
76
|
return;
|
|
77
|
+
const pendingClientRequestId = refs.pendingSubmissionRef.current?.clientRequestId;
|
|
78
|
+
if (pendingClientRequestId)
|
|
79
|
+
releaseSubmitOwnership(pendingClientRequestId);
|
|
76
80
|
refs.streamingRef.current = false;
|
|
77
81
|
refs.activeTurnIdRef.current = null;
|
|
78
82
|
refs.assistantIdRef.current = null;
|
|
79
83
|
refs.stoppingTurnIdRef.current = null;
|
|
84
|
+
refs.stopRequestedRef.current = false;
|
|
80
85
|
setStreaming(false);
|
|
81
86
|
setTurnPhase("idle");
|
|
82
87
|
setStreamingAssistantId(null);
|
|
83
88
|
clearEmptyAssistant(setMessages, assistantId);
|
|
84
|
-
}, [
|
|
89
|
+
}, [
|
|
90
|
+
refs,
|
|
91
|
+
releaseSubmitOwnership,
|
|
92
|
+
setStreaming,
|
|
93
|
+
setTurnPhase,
|
|
94
|
+
setStreamingAssistantId,
|
|
95
|
+
setMessages,
|
|
96
|
+
]);
|
|
85
97
|
/** Follow one session's event stream until it is explicitly aborted. The
|
|
86
98
|
* server's `stream_ready` marker separates replay from live events; this
|
|
87
99
|
* means replay cannot render duplicate messages while live text remains
|
|
@@ -243,12 +255,12 @@ export function useChatStream(params) {
|
|
|
243
255
|
/** Issue real POST /abort for a known turnId (Stop never disposes Session). */
|
|
244
256
|
const postAbortForTurn = useCallback(async (sessionId, generation, turnId) => {
|
|
245
257
|
refs.stoppingTurnIdRef.current = turnId;
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
258
|
+
// DSH-style visual stop: the composer becomes idle immediately. Hidden
|
|
259
|
+
// ownership remains stopping so a new message routes to the durable queue
|
|
260
|
+
// until terminal evidence proves the old Turn is gone.
|
|
249
261
|
refs.streamingRef.current = false;
|
|
250
262
|
setStreaming(false);
|
|
251
|
-
setTurnPhase("
|
|
263
|
+
setTurnPhase("idle");
|
|
252
264
|
setStreamingAssistantId(null);
|
|
253
265
|
try {
|
|
254
266
|
const res = await fetch(operatorChatAbortUrl(origin, sessionId, turnId), {
|
|
@@ -277,13 +289,17 @@ export function useChatStream(params) {
|
|
|
277
289
|
const state = await fetchSessionState(sessionId);
|
|
278
290
|
const stillCurrent = refs.sessionRef.current?.sessionId === sessionId &&
|
|
279
291
|
refs.sessionGenerationRef.current === generation;
|
|
292
|
+
// Terminal SSE may settle the Turn while /state is in flight. Do not
|
|
293
|
+
// write the pre-request ownership snapshot back after that newer fact.
|
|
294
|
+
if (!stillCurrent || refs.stoppingTurnIdRef.current !== turnId)
|
|
295
|
+
return;
|
|
280
296
|
applyAbortDecision(bag, decision, noticeSink(), {
|
|
281
|
-
sessionStillCurrent:
|
|
297
|
+
sessionStillCurrent: true,
|
|
282
298
|
runtimeBusy: isRuntimeBusyUnion(state ?? undefined),
|
|
283
299
|
markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
|
|
284
300
|
});
|
|
285
301
|
applyOwnershipBagToRefs(refs, bag);
|
|
286
|
-
setTurnPhase(
|
|
302
|
+
setTurnPhase(visualTurnPhaseFromOwnership(bag));
|
|
287
303
|
return;
|
|
288
304
|
}
|
|
289
305
|
applyAbortDecision(bag, decision, noticeSink(), {
|
|
@@ -291,11 +307,7 @@ export function useChatStream(params) {
|
|
|
291
307
|
markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
|
|
292
308
|
});
|
|
293
309
|
applyOwnershipBagToRefs(refs, bag);
|
|
294
|
-
setTurnPhase(
|
|
295
|
-
// Sync the post-decision busy state back to React: optimistic stop
|
|
296
|
-
// flipped it idle up front; pending/keep-busy outcomes restore it.
|
|
297
|
-
if (bag.streaming)
|
|
298
|
-
setStreaming(true);
|
|
310
|
+
setTurnPhase(visualTurnPhaseFromOwnership(bag));
|
|
299
311
|
}
|
|
300
312
|
catch {
|
|
301
313
|
// Abort network failure: keep busy (UIS-abort-network-fail).
|
|
@@ -308,11 +320,11 @@ export function useChatStream(params) {
|
|
|
308
320
|
networkError: true,
|
|
309
321
|
}), noticeSink());
|
|
310
322
|
applyOwnershipBagToRefs(refs, bag);
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
refs.streamingRef.current =
|
|
314
|
-
setStreaming(
|
|
315
|
-
setTurnPhase("
|
|
323
|
+
// Transport failure is ambiguous: keep the visual composer available,
|
|
324
|
+
// retain hidden ownership, and let the short /state backoff converge.
|
|
325
|
+
refs.streamingRef.current = false;
|
|
326
|
+
setStreaming(false);
|
|
327
|
+
setTurnPhase("idle");
|
|
316
328
|
}
|
|
317
329
|
}, [
|
|
318
330
|
origin,
|
|
@@ -368,7 +380,13 @@ export function useChatStream(params) {
|
|
|
368
380
|
},
|
|
369
381
|
});
|
|
370
382
|
applyOwnershipBagToRefs(refs, bag);
|
|
371
|
-
|
|
383
|
+
const ownershipPhase = turnPhaseFromOwnership(bag);
|
|
384
|
+
setTurnPhase(visualTurnPhaseFromOwnership(bag));
|
|
385
|
+
if (ownershipPhase === "stopping") {
|
|
386
|
+
refs.streamingRef.current = false;
|
|
387
|
+
setStreaming(false);
|
|
388
|
+
setStreamingAssistantId(null);
|
|
389
|
+
}
|
|
372
390
|
}, [
|
|
373
391
|
refs,
|
|
374
392
|
noticeSink,
|
|
@@ -386,17 +404,18 @@ export function useChatStream(params) {
|
|
|
386
404
|
const activeSession = refs.sessionRef.current;
|
|
387
405
|
if (!activeSession || (!input.text.trim() && input.images.length === 0))
|
|
388
406
|
return { action: "handled" };
|
|
389
|
-
const existing = thread.deferredTurn;
|
|
390
407
|
showNotice("正在排队…");
|
|
391
|
-
const acceptDeferred = (deferredTurn) => {
|
|
392
|
-
setDeferredTurn(deferredTurn);
|
|
408
|
+
const acceptDeferred = (deferredTurn, deferredTurns) => {
|
|
409
|
+
setDeferredTurn(deferredTurns?.[0] ?? deferredTurn);
|
|
410
|
+
setDeferredTurns(deferredTurns ?? [deferredTurn]);
|
|
393
411
|
setInput("");
|
|
394
412
|
setPendingImages([]);
|
|
395
413
|
setError(null);
|
|
396
|
-
showNotice(
|
|
414
|
+
showNotice("已排队,将在当前回合结束后自动发送");
|
|
397
415
|
};
|
|
398
416
|
const acceptDispatched = () => {
|
|
399
417
|
setDeferredTurn(null);
|
|
418
|
+
setDeferredTurns([]);
|
|
400
419
|
setInput("");
|
|
401
420
|
setPendingImages([]);
|
|
402
421
|
setError(null);
|
|
@@ -415,7 +434,6 @@ export function useChatStream(params) {
|
|
|
415
434
|
blockedByTurnId: input.blockedByTurnId,
|
|
416
435
|
text: input.text,
|
|
417
436
|
images: input.images,
|
|
418
|
-
...(existing ? { expectedRevision: existing.revision } : {}),
|
|
419
437
|
}),
|
|
420
438
|
});
|
|
421
439
|
const body = (await response.json().catch(() => ({})));
|
|
@@ -425,9 +443,11 @@ export function useChatStream(params) {
|
|
|
425
443
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
426
444
|
const { response, body } = await putOnce();
|
|
427
445
|
if (response.ok && body.accepted && body.deferredTurn) {
|
|
428
|
-
acceptDeferred(body.deferredTurn);
|
|
446
|
+
acceptDeferred(body.deferredTurn, body.deferredTurns);
|
|
429
447
|
return { action: "handled" };
|
|
430
448
|
}
|
|
449
|
+
if (body.deferredTurns)
|
|
450
|
+
setDeferredTurns(body.deferredTurns);
|
|
431
451
|
if (body.deferredTurn)
|
|
432
452
|
setDeferredTurn(body.deferredTurn);
|
|
433
453
|
if (body.error?.code !== "TURN_NOT_ACTIVE") {
|
|
@@ -442,7 +462,7 @@ export function useChatStream(params) {
|
|
|
442
462
|
state: canonical ?? undefined,
|
|
443
463
|
});
|
|
444
464
|
if (decision.action === "deferred") {
|
|
445
|
-
acceptDeferred(decision.deferredTurn);
|
|
465
|
+
acceptDeferred(decision.deferredTurn, canonical?.deferredTurns);
|
|
446
466
|
return { action: "handled" };
|
|
447
467
|
}
|
|
448
468
|
if (decision.action === "accepted") {
|
|
@@ -451,7 +471,7 @@ export function useChatStream(params) {
|
|
|
451
471
|
}
|
|
452
472
|
if (decision.action === "retry-deferred" && attempt === 0)
|
|
453
473
|
continue;
|
|
454
|
-
if (decision.action === "send-now"
|
|
474
|
+
if (decision.action === "send-now") {
|
|
455
475
|
showNotice("上一轮已停止,正在发送当前消息…");
|
|
456
476
|
return { action: "send-now" };
|
|
457
477
|
}
|
|
@@ -468,14 +488,14 @@ export function useChatStream(params) {
|
|
|
468
488
|
state: canonical ?? undefined,
|
|
469
489
|
});
|
|
470
490
|
if (decision.action === "deferred") {
|
|
471
|
-
acceptDeferred(decision.deferredTurn);
|
|
491
|
+
acceptDeferred(decision.deferredTurn, canonical?.deferredTurns);
|
|
472
492
|
return { action: "handled" };
|
|
473
493
|
}
|
|
474
494
|
if (decision.action === "accepted") {
|
|
475
495
|
acceptDispatched();
|
|
476
496
|
return { action: "handled" };
|
|
477
497
|
}
|
|
478
|
-
if (decision.action === "send-now"
|
|
498
|
+
if (decision.action === "send-now") {
|
|
479
499
|
showNotice("上一轮已停止,正在发送当前消息…");
|
|
480
500
|
return { action: "send-now" };
|
|
481
501
|
}
|
|
@@ -489,6 +509,7 @@ export function useChatStream(params) {
|
|
|
489
509
|
refs,
|
|
490
510
|
thread.deferredTurn,
|
|
491
511
|
setDeferredTurn,
|
|
512
|
+
setDeferredTurns,
|
|
492
513
|
setInput,
|
|
493
514
|
setPendingImages,
|
|
494
515
|
setError,
|
|
@@ -497,9 +518,11 @@ export function useChatStream(params) {
|
|
|
497
518
|
]);
|
|
498
519
|
/** DELETE the queued turn; "restore" moves its text back into the composer,
|
|
499
520
|
* "discard" drops it entirely (2026-08-23 queue chip actions). */
|
|
500
|
-
const requestDeferredTurnDelete = useCallback(async (mode) => {
|
|
521
|
+
const requestDeferredTurnDelete = useCallback(async (mode, target) => {
|
|
501
522
|
const activeSession = refs.sessionRef.current;
|
|
502
|
-
const deferred =
|
|
523
|
+
const deferred = target
|
|
524
|
+
? thread.deferredTurns.find((item) => item.clientRequestId === target.clientRequestId)
|
|
525
|
+
: thread.deferredTurns[0] ?? thread.deferredTurn;
|
|
503
526
|
if (!activeSession || !deferred)
|
|
504
527
|
return;
|
|
505
528
|
try {
|
|
@@ -520,10 +543,12 @@ export function useChatStream(params) {
|
|
|
520
543
|
setError(body.error?.message ?? "待发送消息撤回失败");
|
|
521
544
|
return;
|
|
522
545
|
}
|
|
523
|
-
|
|
546
|
+
const remaining = thread.deferredTurns.filter((item) => item.clientRequestId !== deferred.clientRequestId);
|
|
547
|
+
setDeferredTurns(remaining);
|
|
548
|
+
setDeferredTurn(remaining[0] ?? null);
|
|
524
549
|
if (mode === "restore") {
|
|
525
|
-
setInput(body.deferredTurn?.text ?? "");
|
|
526
|
-
setPendingImages(body.deferredTurn?.images ?? []);
|
|
550
|
+
setInput(deferred.text ?? body.deferredTurn?.text ?? "");
|
|
551
|
+
setPendingImages(deferred.images ?? body.deferredTurn?.images ?? []);
|
|
527
552
|
showNotice("待发送消息已移回输入框");
|
|
528
553
|
}
|
|
529
554
|
else {
|
|
@@ -538,14 +563,66 @@ export function useChatStream(params) {
|
|
|
538
563
|
origin,
|
|
539
564
|
refs,
|
|
540
565
|
thread.deferredTurn,
|
|
566
|
+
thread.deferredTurns,
|
|
541
567
|
setDeferredTurn,
|
|
568
|
+
setDeferredTurns,
|
|
542
569
|
setInput,
|
|
543
570
|
setPendingImages,
|
|
544
571
|
setError,
|
|
545
572
|
showNotice,
|
|
546
573
|
]);
|
|
547
574
|
const cancelDeferredTurn = useCallback(() => requestDeferredTurnDelete("restore"), [requestDeferredTurnDelete]);
|
|
548
|
-
const discardDeferredTurn = useCallback(() => requestDeferredTurnDelete("discard"), [requestDeferredTurnDelete]);
|
|
575
|
+
const discardDeferredTurn = useCallback((target) => requestDeferredTurnDelete("discard", target), [requestDeferredTurnDelete]);
|
|
576
|
+
const editDeferredTurn = useCallback(async (target, text) => {
|
|
577
|
+
const activeSession = refs.sessionRef.current;
|
|
578
|
+
if (!activeSession)
|
|
579
|
+
return;
|
|
580
|
+
const item = thread.deferredTurns.find((entry) => entry.clientRequestId === target.clientRequestId);
|
|
581
|
+
if (!item)
|
|
582
|
+
return;
|
|
583
|
+
try {
|
|
584
|
+
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
|
|
585
|
+
method: "PUT",
|
|
586
|
+
headers: { "Content-Type": "application/json", "x-loop-console-confirmation": confirmationToken() },
|
|
587
|
+
credentials: "include",
|
|
588
|
+
body: JSON.stringify({ clientRequestId: target.clientRequestId, blockedByTurnId: item.blockedByTurnId, text, images: item.images ?? [], expectedRevision: target.revision }),
|
|
589
|
+
});
|
|
590
|
+
const body = (await response.json().catch(() => ({})));
|
|
591
|
+
if (!response.ok)
|
|
592
|
+
throw new Error(body.error?.message ?? "编辑排队消息失败");
|
|
593
|
+
if (body.deferredTurns)
|
|
594
|
+
setDeferredTurns(body.deferredTurns);
|
|
595
|
+
if (body.deferredTurn)
|
|
596
|
+
setDeferredTurn(body.deferredTurn);
|
|
597
|
+
setError(null);
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
setError(error instanceof Error ? error.message : "编辑排队消息失败");
|
|
601
|
+
}
|
|
602
|
+
}, [origin, refs, thread.deferredTurns, setDeferredTurn, setDeferredTurns, setError]);
|
|
603
|
+
const steerDeferredTurn = useCallback(async (target) => {
|
|
604
|
+
const activeSession = refs.sessionRef.current;
|
|
605
|
+
if (!activeSession)
|
|
606
|
+
return;
|
|
607
|
+
try {
|
|
608
|
+
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn/${encodeURIComponent(target.clientRequestId)}/steer`, {
|
|
609
|
+
method: "POST",
|
|
610
|
+
headers: { "Content-Type": "application/json", "x-loop-console-confirmation": confirmationToken() },
|
|
611
|
+
credentials: "include",
|
|
612
|
+
body: JSON.stringify({ expectedRevision: target.revision }),
|
|
613
|
+
});
|
|
614
|
+
const body = (await response.json().catch(() => ({})));
|
|
615
|
+
if (!response.ok)
|
|
616
|
+
throw new Error(body.error?.message ?? "插话发送失败");
|
|
617
|
+
const remaining = body.deferredTurns ?? thread.deferredTurns.filter((item) => item.clientRequestId !== target.clientRequestId);
|
|
618
|
+
setDeferredTurns(remaining);
|
|
619
|
+
setDeferredTurn(remaining[0] ?? null);
|
|
620
|
+
setError(null);
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
setError(error instanceof Error ? error.message : "插话发送失败");
|
|
624
|
+
}
|
|
625
|
+
}, [origin, refs, thread.deferredTurns, setDeferredTurn, setDeferredTurns, setError]);
|
|
549
626
|
const sendPrompt = useCallback(async (text, images) => {
|
|
550
627
|
const initialSession = refs.sessionRef.current;
|
|
551
628
|
if (!initialSession || (!text.trim() && images.length === 0))
|
|
@@ -747,10 +824,12 @@ export function useChatStream(params) {
|
|
|
747
824
|
// Decide + mark intent/coalesce only; POST is issued once below.
|
|
748
825
|
const stopResult = requestStopOnOwnership(bag, {
|
|
749
826
|
postAbort: () => { },
|
|
750
|
-
notice: noticeSink(),
|
|
751
827
|
});
|
|
752
828
|
applyOwnershipBagToRefs(refs, bag);
|
|
753
|
-
|
|
829
|
+
refs.streamingRef.current = false;
|
|
830
|
+
setStreaming(false);
|
|
831
|
+
setStreamingAssistantId(null);
|
|
832
|
+
setTurnPhase("idle");
|
|
754
833
|
// Immediate abort already scheduled for a known turnId — coalesce via
|
|
755
834
|
// abortPostedTurnIds; stopInFlight only gates concurrent in-flight POSTs.
|
|
756
835
|
if (stopResult.abortedTurnId) {
|
|
@@ -800,7 +879,6 @@ export function useChatStream(params) {
|
|
|
800
879
|
markStopRequested(lateBag);
|
|
801
880
|
const late = requestStopOnOwnership(lateBag, {
|
|
802
881
|
postAbort: () => { },
|
|
803
|
-
notice: noticeSink(),
|
|
804
882
|
});
|
|
805
883
|
applyOwnershipBagToRefs(refs, lateBag);
|
|
806
884
|
if (late.abortedTurnId) {
|
|
@@ -810,7 +888,13 @@ export function useChatStream(params) {
|
|
|
810
888
|
finally {
|
|
811
889
|
refs.stopInFlightRef.current = false;
|
|
812
890
|
}
|
|
813
|
-
}, [
|
|
891
|
+
}, [
|
|
892
|
+
refs,
|
|
893
|
+
postAbortForTurn,
|
|
894
|
+
setStreaming,
|
|
895
|
+
setStreamingAssistantId,
|
|
896
|
+
setTurnPhase,
|
|
897
|
+
]);
|
|
814
898
|
// State reconciliation is only a status fence: it never consumes/creates
|
|
815
899
|
// message events, and a generation check discards late responses (M5.3).
|
|
816
900
|
// Busy truth = durable activeTurn union runtime busy facts (AC-003/AC-006).
|
|
@@ -830,7 +914,8 @@ export function useChatStream(params) {
|
|
|
830
914
|
const sessionId = session.sessionId;
|
|
831
915
|
const generation = refs.sessionGenerationRef.current;
|
|
832
916
|
let cancelled = false;
|
|
833
|
-
|
|
917
|
+
let syncInFlight = null;
|
|
918
|
+
const runSync = async () => {
|
|
834
919
|
try {
|
|
835
920
|
const res = await fetch(operatorChatStateUrl(origin, sessionId), {
|
|
836
921
|
credentials: "include",
|
|
@@ -859,10 +944,16 @@ export function useChatStream(params) {
|
|
|
859
944
|
},
|
|
860
945
|
});
|
|
861
946
|
applyOwnershipBagToRefs(refs, bag);
|
|
862
|
-
|
|
947
|
+
const ownershipPhase = turnPhaseFromOwnership(bag);
|
|
948
|
+
setTurnPhase(visualTurnPhaseFromOwnership(bag));
|
|
949
|
+
if (ownershipPhase === "stopping") {
|
|
950
|
+
refs.streamingRef.current = false;
|
|
951
|
+
setStreaming(false);
|
|
952
|
+
setStreamingAssistantId(null);
|
|
953
|
+
}
|
|
863
954
|
if (state && "deferredTurn" in state)
|
|
864
955
|
setDeferredTurn(state.deferredTurn ?? null);
|
|
865
|
-
if (result === "bound") {
|
|
956
|
+
if (result === "bound" && ownershipPhase !== "stopping") {
|
|
866
957
|
// US-BUSY-ACTIVE-BOUND: keep streaming/busy with same id.
|
|
867
958
|
setStreaming(true);
|
|
868
959
|
return;
|
|
@@ -940,19 +1031,41 @@ export function useChatStream(params) {
|
|
|
940
1031
|
// offline/background case: interval, visibility and online retry.
|
|
941
1032
|
}
|
|
942
1033
|
};
|
|
1034
|
+
const sync = () => {
|
|
1035
|
+
if (syncInFlight)
|
|
1036
|
+
return syncInFlight;
|
|
1037
|
+
syncInFlight = runSync().finally(() => {
|
|
1038
|
+
syncInFlight = null;
|
|
1039
|
+
});
|
|
1040
|
+
return syncInFlight;
|
|
1041
|
+
};
|
|
943
1042
|
const onVisible = () => {
|
|
944
1043
|
if (document.visibilityState === "visible")
|
|
945
1044
|
void sync();
|
|
946
1045
|
};
|
|
947
1046
|
const onOnline = () => void sync();
|
|
948
1047
|
const interval = window.setInterval(() => void sync(), 15_000);
|
|
1048
|
+
let stopRetryTimer;
|
|
1049
|
+
let stopRetryIndex = 0;
|
|
1050
|
+
const scheduleStopRetry = () => {
|
|
1051
|
+
if (cancelled ||
|
|
1052
|
+
stopRetryIndex >= STOP_RECONCILE_DELAYS_MS.length ||
|
|
1053
|
+
(!refs.stopRequestedRef.current && !refs.stoppingTurnIdRef.current))
|
|
1054
|
+
return;
|
|
1055
|
+
const delay = STOP_RECONCILE_DELAYS_MS[stopRetryIndex++];
|
|
1056
|
+
stopRetryTimer = window.setTimeout(() => {
|
|
1057
|
+
void sync().finally(scheduleStopRetry);
|
|
1058
|
+
}, delay);
|
|
1059
|
+
};
|
|
949
1060
|
document.addEventListener("visibilitychange", onVisible);
|
|
950
1061
|
window.addEventListener("online", onOnline);
|
|
951
1062
|
// Immediate tick so terminal-before-streaming-false races converge promptly.
|
|
952
|
-
void sync();
|
|
1063
|
+
void sync().finally(scheduleStopRetry);
|
|
953
1064
|
return () => {
|
|
954
1065
|
cancelled = true;
|
|
955
1066
|
window.clearInterval(interval);
|
|
1067
|
+
if (stopRetryTimer !== undefined)
|
|
1068
|
+
window.clearTimeout(stopRetryTimer);
|
|
956
1069
|
document.removeEventListener("visibilitychange", onVisible);
|
|
957
1070
|
window.removeEventListener("online", onOnline);
|
|
958
1071
|
};
|
|
@@ -988,6 +1101,8 @@ export function useChatStream(params) {
|
|
|
988
1101
|
handleStop,
|
|
989
1102
|
cancelDeferredTurn,
|
|
990
1103
|
discardDeferredTurn,
|
|
1104
|
+
editDeferredTurn,
|
|
1105
|
+
steerDeferredTurn,
|
|
991
1106
|
openEventsStream,
|
|
992
1107
|
};
|
|
993
1108
|
}
|