@tea-agent/loop-agent 0.43.0-next.10 → 0.43.0-next.12
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 +18 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/client-recovery.js +20 -55
- package/dist/executors/dag-pi-executor.js +27 -6
- package/dist/executors/shell-executor.js +3 -2
- package/dist/shared/pi-context-pressure/extension.js +26 -16
- package/dist/shared/pi-provider-recovery.js +182 -0
- package/dist/shared/pi-retry-settings.js +40 -0
- package/dist/worker/console/chat/browser-policy.js +6 -36
- package/dist/worker/console/chat/browser-routes.js +3 -11
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +2 -1
- package/dist/worker/console/chat/pi-runtime.js +11 -12
- package/dist/worker/console/chat/routes.js +1 -5
- package/dist/worker/console/chat/session-mode-view.js +5 -4
- package/dist/worker/console/chat/session-mode.js +11 -10
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-D_fNbWc_.js → abnfDiagram-N423BO3Z-BEg5712w.js} +1 -1
- package/dist/worker/console/static/assets/{arc-_HuqLW6m.js → arc-CkQvaBGZ.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Djy2qY1m.js → architectureDiagram-T3A2C74G-BrdwFNJ7.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-bB-1TSyY.js → blockDiagram-VBNYF7ZC-M_LjP2kx.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5Hmghrz.js → c4Diagram-5PPSVZJV-Bwr5Dtjc.js} +1 -1
- package/dist/worker/console/static/assets/channel-CKmyKgY-.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Duyl39oy.js → chunk-2GRJ4B5K-CDc44stW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-x6mdnSyf.js → chunk-2Q5K7J3B-DZELwsTt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-WlBuFj7W.js → chunk-5RXB4S5H-D7YI6SWX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CTF429HX.js → chunk-5VM5RSS4-Bc46bEyj.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dwd6r-SF.js → chunk-6Q2QTUOP-Bj4vc-Dp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C-Br82D1.js → chunk-GF5L2VYU-DRi-0nDX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CuGp0DeE.js → chunk-JWPE2WC7-Dr-wFY60.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CJhFX6Lh.js → chunk-KBJHAD2P-CqIHE_D7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DpGOa_jT.js → chunk-RYQCIY6F-C-Dl8wzk.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DXJaTynd.js → chunk-XXDRQBXY-CV4Mydz3.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D8Wc3jJP.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D8Wc3jJP.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CRBZWjE4.js → cose-bilkent-JH36ORCC-C3MamSMJ.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BdXcxtlb.js → cynefin-VYW2F7L2-zo_Jy2sf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-07UKsOts.js → cynefinDiagram-MW4NZA55-BB2PN1AG.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-D38p9__S.js → dagre-VZM6K2ZE-BimYueoi.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-nSiBNNjH.js → diagram-7IWD3JNH-Di1ObJRS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BFOTot5D.js → diagram-B4RE2ZJO-C83l-IJx.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DypKaZ6k.js → diagram-LBJQPF4R-B3S3fOpm.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-B6yoOQeW.js → diagram-Q27KOJAE-urW_Zhe7.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BHYS6viX.js → diagram-UB23O5K3-nvIbCwF6.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-D2VD5Snj.js → ebnfDiagram-BXEA7PRR-DG6O3KpP.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-E98mWxN1.js → erDiagram-JOGREHBK-DpXdtdsj.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CT1zxhqP.js → flowDiagram-UKHOOZJN-DvXWHrND.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-6GcngNcy.js → ganttDiagram-PKOTCBZU-6F7SbvUS.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCL6NvKe.js → gitGraphDiagram-DS77QQ5N-D1tZnFec.js} +1 -1
- package/dist/worker/console/static/assets/{index-24jp3aBh.js → index-CWarwRqc.js} +94 -94
- package/dist/worker/console/static/assets/{index-CY6y6I8M.css → index-SAANIK8g.css} +1 -1
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C0627e-9.js → infoDiagram-6WML65LV-HN4qwJ2M.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js → ishikawaDiagram-WSZJBQD7-OJ4qwDRo.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DPDPv9HL.js → journeyDiagram-NVQOT4AX-CJZ6giQk.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D3OHdWiO.js → kanban-definition-27J2QSJJ-co60m19S.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DI88zD7n.js → linear-D1KeFqZY.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-p2l8IrWY.js → mermaid.core-i7dDGqov.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-B1bAX-oS.js → mindmap-definition-FAOFIHXS-1SkQxBh1.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DwOnWxdD.js → pegDiagram-VL7TDLO6-B10dhI-Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-xRkTXnJC.js → pieDiagram-7S7Q4E2Y-CiUKUo6j.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-_9UC5Zio.js → quadrantDiagram-CIZ2JOQS-C5vH4LXZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CgEUirWJ.js → railroadDiagram-AXF67PYL-5nlWOCEj.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Czbpwiyz.js → requirementDiagram-LRYGKXZP-CILd6QS3.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-_j7_KiqY.js → sankeyDiagram-W5VNT64P-Dfp18ZO2.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BNJa6Hyi.js → sequenceDiagram-SI44F4Z6-B9pkswBi.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DedpKtOc.js → sizeCapture-X5ZJPWSS-C1-eSgx4.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BBvthr7c.js → stateDiagram-OKZ733FA-COJsOoAF.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Bpfb0q5s.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Ds7umgEM.js → swimlanes-SLNWSIFB-Dv5NXJUM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BEwGGabW.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKa-p1nF.js → timeline-definition-Z64GVDOM-DRdrZ7eR.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LoUNR56o.js → vennDiagram-T6HMQDX7-0-2TlwCj.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bqbl6ym7.js → wardleyDiagram-T6FBY63Y-3-o3q92P.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B8dUg4lf.js → xychartDiagram-ELKLHX3M-DSgOgdUi.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +192 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +9 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +78 -35
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +145 -70
- package/dist/worker/console/static-src/operator-chat/useComposer.js +182 -69
- package/dist/workflows/dag/frontend-closeout.js +1 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
- package/dist/workflows/dag/frontend-review-context.js +2 -1
- package/dist/workflows/dag/frontend-review-scopes.js +2 -2
- package/dist/workflows/dag/frontend-verification-trace.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +51 -159
- package/docs/architecture/runtime-boundaries.md +3 -3
- package/docs/templates/frontend-implementation-dag.json +9 -8
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +3 -3
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/references/contract-protocol.md +3 -1
- package/skills/frontend-design-review/SKILL.md +12 -7
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +12 -0
- package/skills/frontend-plan/references/decision-contract.md +25 -19
- package/skills/frontend-review/SKILL.md +12 -18
- package/skills/frontend-review/references/review-findings.md +24 -43
- package/dist/worker/console/static/assets/channel-C0A6UEIa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +0 -8
|
@@ -64,7 +64,7 @@ function clearEmptyAssistant(setMessages, assistantId) {
|
|
|
64
64
|
export function useChatStream(params) {
|
|
65
65
|
const { origin, refs, session, thread, composer, setError, setAutoScroll, setNotice, recoveryVersion = 0, watchSessionTitle, } = params;
|
|
66
66
|
const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, setDeferredTurns, } = thread;
|
|
67
|
-
const {
|
|
67
|
+
const { settleSubmission, settleSubmissionByRequest, replaceSessionDraft, } = composer;
|
|
68
68
|
const showNotice = useCallback((message) => {
|
|
69
69
|
if (!setNotice)
|
|
70
70
|
return;
|
|
@@ -223,15 +223,22 @@ export function useChatStream(params) {
|
|
|
223
223
|
}, [origin]);
|
|
224
224
|
const postCreateTurn = useCallback(async (input) => {
|
|
225
225
|
try {
|
|
226
|
+
// Resolve against the configured Console origin and fail closed if a
|
|
227
|
+
// future URL helper ever escapes that trusted same-origin boundary.
|
|
228
|
+
const consoleOrigin = new URL(origin, window.location.href);
|
|
229
|
+
const turnsUrl = new URL(operatorChatTurnsUrl(origin, input.sessionId), consoleOrigin);
|
|
230
|
+
if (turnsUrl.origin !== consoleOrigin.origin)
|
|
231
|
+
throw new Error("create-turn URL escaped the Console origin");
|
|
226
232
|
// Real create-turn path remains `${origin}/api/operator/v1/chat/sessions/:id/turns`
|
|
227
233
|
// (via operatorChatTurnsUrl); source contract anchors on /turns`.
|
|
228
|
-
const res = await fetch(
|
|
234
|
+
const res = await fetch(turnsUrl, {
|
|
229
235
|
method: "POST",
|
|
230
236
|
headers: {
|
|
231
237
|
"Content-Type": "application/json",
|
|
232
238
|
"x-loop-console-confirmation": confirmationToken(),
|
|
233
239
|
},
|
|
234
240
|
credentials: "include",
|
|
241
|
+
signal: input.signal,
|
|
235
242
|
body: JSON.stringify({
|
|
236
243
|
clientRequestId: input.clientRequestId,
|
|
237
244
|
text: input.text.trim() || "请查看附件图片。",
|
|
@@ -350,9 +357,6 @@ export function useChatStream(params) {
|
|
|
350
357
|
bag.sessionId = refs.sessionRef.current?.sessionId ?? null;
|
|
351
358
|
bag.sessionGeneration = refs.sessionGenerationRef.current;
|
|
352
359
|
bag.submitGeneration = refs.submitGenerationRef.current;
|
|
353
|
-
const setPendingImagesUi = (images) => {
|
|
354
|
-
setPendingImages(images);
|
|
355
|
-
};
|
|
356
360
|
settleCreateTurnOutcome(bag, outcome, {
|
|
357
361
|
assistantId: ctx.assistantId,
|
|
358
362
|
text: ctx.text,
|
|
@@ -363,13 +367,13 @@ export function useChatStream(params) {
|
|
|
363
367
|
acceptedUi: {
|
|
364
368
|
setStreaming,
|
|
365
369
|
setStreamingAssistantId,
|
|
366
|
-
setPendingImages:
|
|
370
|
+
setPendingImages: () => undefined,
|
|
367
371
|
watchSessionTitle,
|
|
368
372
|
},
|
|
369
373
|
adoptUi: {
|
|
370
374
|
clearEmptyAssistant: (assistantId) => clearEmptyAssistant(setMessages, assistantId),
|
|
371
|
-
setInput,
|
|
372
|
-
setPendingImages:
|
|
375
|
+
setInput: () => undefined,
|
|
376
|
+
setPendingImages: () => undefined,
|
|
373
377
|
setStreaming,
|
|
374
378
|
setStreamingAssistantId,
|
|
375
379
|
removePendingMessage: (requestId) => setMessages((messages) => removePendingSubmission(messages, requestId)),
|
|
@@ -379,8 +383,8 @@ export function useChatStream(params) {
|
|
|
379
383
|
setStreaming,
|
|
380
384
|
setStreamingAssistantId,
|
|
381
385
|
clearEmptyAssistant: (assistantId) => clearEmptyAssistant(setMessages, assistantId),
|
|
382
|
-
setInput,
|
|
383
|
-
setPendingImages:
|
|
386
|
+
setInput: () => undefined,
|
|
387
|
+
setPendingImages: () => undefined,
|
|
384
388
|
formatError: formatOperatorUserError,
|
|
385
389
|
removePendingMessage: (requestId) => setMessages((messages) => removePendingSubmission(messages, requestId)),
|
|
386
390
|
},
|
|
@@ -389,6 +393,14 @@ export function useChatStream(params) {
|
|
|
389
393
|
void postAbortForTurn(ctx.sessionId, ctx.generation, turnId);
|
|
390
394
|
},
|
|
391
395
|
});
|
|
396
|
+
if (outcome.kind === "accepted") {
|
|
397
|
+
settleSubmission(ctx.attempt, "accepted");
|
|
398
|
+
}
|
|
399
|
+
else if (outcome.kind === "adopt-active" ||
|
|
400
|
+
outcome.kind === "rejected" ||
|
|
401
|
+
outcome.kind === "stale") {
|
|
402
|
+
settleSubmission(ctx.attempt, "rejected");
|
|
403
|
+
}
|
|
392
404
|
applyOwnershipBagToRefs(refs, bag);
|
|
393
405
|
const ownershipPhase = turnPhaseFromOwnership(bag);
|
|
394
406
|
setTurnPhase(visualTurnPhaseFromOwnership(bag));
|
|
@@ -403,68 +415,84 @@ export function useChatStream(params) {
|
|
|
403
415
|
setStreaming,
|
|
404
416
|
setTurnPhase,
|
|
405
417
|
setStreamingAssistantId,
|
|
406
|
-
|
|
418
|
+
settleSubmission,
|
|
407
419
|
watchSessionTitle,
|
|
408
420
|
setMessages,
|
|
409
|
-
setInput,
|
|
410
421
|
setError,
|
|
411
422
|
postAbortForTurn,
|
|
412
423
|
]);
|
|
413
424
|
const queueDeferredTurn = useCallback(async (input) => {
|
|
425
|
+
const { attempt } = input;
|
|
414
426
|
const activeSession = refs.sessionRef.current;
|
|
415
|
-
|
|
427
|
+
const isCurrent = () => refs.sessionRef.current?.sessionId === attempt.identity.sessionId &&
|
|
428
|
+
refs.sessionGenerationRef.current === attempt.sessionGeneration;
|
|
429
|
+
if (!activeSession ||
|
|
430
|
+
activeSession.sessionId !== attempt.identity.sessionId ||
|
|
431
|
+
!isCurrent()) {
|
|
432
|
+
settleSubmission(attempt, "rejected");
|
|
416
433
|
return { action: "handled" };
|
|
434
|
+
}
|
|
435
|
+
const showError = (message) => {
|
|
436
|
+
if (isCurrent())
|
|
437
|
+
setError(message);
|
|
438
|
+
};
|
|
417
439
|
const acceptDeferred = (deferredTurn, deferredTurns) => {
|
|
440
|
+
settleSubmission(attempt, "accepted");
|
|
441
|
+
if (!isCurrent())
|
|
442
|
+
return;
|
|
418
443
|
setDeferredTurn(deferredTurns?.[0] ?? deferredTurn);
|
|
419
444
|
setDeferredTurns(deferredTurns ?? [deferredTurn]);
|
|
420
|
-
setInput("");
|
|
421
|
-
setPendingImages([]);
|
|
422
445
|
setError(null);
|
|
423
446
|
};
|
|
424
447
|
const acceptDispatched = () => {
|
|
448
|
+
settleSubmission(attempt, "accepted");
|
|
449
|
+
if (!isCurrent())
|
|
450
|
+
return;
|
|
425
451
|
setDeferredTurn(null);
|
|
426
452
|
setDeferredTurns([]);
|
|
427
|
-
setInput("");
|
|
428
|
-
setPendingImages([]);
|
|
429
453
|
setError(null);
|
|
430
454
|
};
|
|
431
455
|
const putOnce = async () => {
|
|
432
|
-
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(
|
|
456
|
+
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(attempt.identity.sessionId)}/deferred-turn`, {
|
|
433
457
|
method: "PUT",
|
|
434
458
|
headers: {
|
|
435
459
|
"Content-Type": "application/json",
|
|
436
460
|
"x-loop-console-confirmation": confirmationToken(),
|
|
437
461
|
},
|
|
438
462
|
credentials: "include",
|
|
463
|
+
signal: attempt.signal,
|
|
439
464
|
body: JSON.stringify({
|
|
440
|
-
clientRequestId:
|
|
465
|
+
clientRequestId: attempt.clientRequestId,
|
|
441
466
|
blockedByTurnId: input.blockedByTurnId,
|
|
442
|
-
text:
|
|
443
|
-
images:
|
|
467
|
+
text: attempt.draftSnapshot,
|
|
468
|
+
images: attempt.imageSnapshot,
|
|
444
469
|
}),
|
|
445
470
|
});
|
|
446
471
|
const body = (await response.json().catch(() => ({})));
|
|
447
472
|
return { response, body };
|
|
448
473
|
};
|
|
449
474
|
try {
|
|
450
|
-
for (let
|
|
475
|
+
for (let retry = 0; retry < 2; retry += 1) {
|
|
451
476
|
const { response, body } = await putOnce();
|
|
452
477
|
if (response.ok && body.accepted && body.deferredTurn) {
|
|
453
478
|
acceptDeferred(body.deferredTurn, body.deferredTurns);
|
|
454
479
|
return { action: "handled" };
|
|
455
480
|
}
|
|
456
|
-
if (body.deferredTurns)
|
|
457
|
-
setDeferredTurns(body.deferredTurns);
|
|
458
|
-
if (body.deferredTurn)
|
|
459
|
-
setDeferredTurn(body.deferredTurn);
|
|
460
481
|
if (body.error?.code !== "TURN_NOT_ACTIVE") {
|
|
461
|
-
|
|
482
|
+
if (isCurrent()) {
|
|
483
|
+
if (body.deferredTurns)
|
|
484
|
+
setDeferredTurns(body.deferredTurns);
|
|
485
|
+
if (body.deferredTurn)
|
|
486
|
+
setDeferredTurn(body.deferredTurn);
|
|
487
|
+
}
|
|
488
|
+
settleSubmission(attempt, "rejected");
|
|
489
|
+
showError(body.error?.message ??
|
|
462
490
|
`待发送消息未被服务端接受(HTTP ${response.status})`);
|
|
463
491
|
return { action: "handled" };
|
|
464
492
|
}
|
|
465
|
-
const canonical = await fetchSessionState(
|
|
493
|
+
const canonical = await fetchSessionState(attempt.identity.sessionId);
|
|
466
494
|
const decision = reconcileDeferredSubmissionFromState({
|
|
467
|
-
clientRequestId:
|
|
495
|
+
clientRequestId: attempt.clientRequestId,
|
|
468
496
|
blockedByTurnId: input.blockedByTurnId,
|
|
469
497
|
state: canonical ?? undefined,
|
|
470
498
|
});
|
|
@@ -476,20 +504,29 @@ export function useChatStream(params) {
|
|
|
476
504
|
acceptDispatched();
|
|
477
505
|
return { action: "handled" };
|
|
478
506
|
}
|
|
479
|
-
if (decision.action === "retry-deferred" &&
|
|
507
|
+
if (decision.action === "retry-deferred" && retry === 0)
|
|
480
508
|
continue;
|
|
481
|
-
if (decision.action === "send-now")
|
|
509
|
+
if (decision.action === "send-now")
|
|
482
510
|
return { action: "send-now" };
|
|
511
|
+
if (isCurrent()) {
|
|
512
|
+
if (body.deferredTurns)
|
|
513
|
+
setDeferredTurns(body.deferredTurns);
|
|
514
|
+
if (body.deferredTurn)
|
|
515
|
+
setDeferredTurn(body.deferredTurn);
|
|
483
516
|
}
|
|
484
|
-
|
|
517
|
+
settleSubmission(attempt, "rejected");
|
|
518
|
+
showError(body.error?.message ?? "停止状态已变化,当前消息仍保留在输入框");
|
|
485
519
|
return { action: "handled" };
|
|
486
520
|
}
|
|
521
|
+
settleSubmission(attempt, "rejected");
|
|
487
522
|
return { action: "handled" };
|
|
488
523
|
}
|
|
489
524
|
catch (error) {
|
|
490
|
-
|
|
525
|
+
if (attempt.signal.aborted)
|
|
526
|
+
return { action: "handled" };
|
|
527
|
+
const canonical = await fetchSessionState(attempt.identity.sessionId);
|
|
491
528
|
const decision = reconcileDeferredSubmissionFromState({
|
|
492
|
-
clientRequestId:
|
|
529
|
+
clientRequestId: attempt.clientRequestId,
|
|
493
530
|
blockedByTurnId: input.blockedByTurnId,
|
|
494
531
|
state: canonical ?? undefined,
|
|
495
532
|
});
|
|
@@ -501,10 +538,10 @@ export function useChatStream(params) {
|
|
|
501
538
|
acceptDispatched();
|
|
502
539
|
return { action: "handled" };
|
|
503
540
|
}
|
|
504
|
-
if (decision.action === "send-now")
|
|
541
|
+
if (decision.action === "send-now")
|
|
505
542
|
return { action: "send-now" };
|
|
506
|
-
|
|
507
|
-
|
|
543
|
+
settleSubmission(attempt, "rejected");
|
|
544
|
+
showError(error instanceof Error
|
|
508
545
|
? `无法确认消息是否已排队:${error.message}`
|
|
509
546
|
: "无法确认消息是否已排队,请刷新后查看服务端状态");
|
|
510
547
|
return { action: "handled" };
|
|
@@ -515,8 +552,7 @@ export function useChatStream(params) {
|
|
|
515
552
|
thread.deferredTurn,
|
|
516
553
|
setDeferredTurn,
|
|
517
554
|
setDeferredTurns,
|
|
518
|
-
|
|
519
|
-
setPendingImages,
|
|
555
|
+
settleSubmission,
|
|
520
556
|
setError,
|
|
521
557
|
fetchSessionState,
|
|
522
558
|
]);
|
|
@@ -529,6 +565,10 @@ export function useChatStream(params) {
|
|
|
529
565
|
: thread.deferredTurns[0] ?? thread.deferredTurn;
|
|
530
566
|
if (!activeSession || !deferred)
|
|
531
567
|
return;
|
|
568
|
+
const identity = { ...activeSession };
|
|
569
|
+
const generation = refs.sessionGenerationRef.current;
|
|
570
|
+
const isCurrent = () => refs.sessionRef.current?.sessionId === identity.sessionId &&
|
|
571
|
+
refs.sessionGenerationRef.current === generation;
|
|
532
572
|
try {
|
|
533
573
|
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
|
|
534
574
|
method: "DELETE",
|
|
@@ -544,20 +584,25 @@ export function useChatStream(params) {
|
|
|
544
584
|
});
|
|
545
585
|
const body = (await response.json().catch(() => ({})));
|
|
546
586
|
if (!response.ok || !body.cancelled) {
|
|
547
|
-
|
|
587
|
+
if (isCurrent())
|
|
588
|
+
setError(body.error?.message ?? "待发送消息撤回失败");
|
|
548
589
|
return;
|
|
549
590
|
}
|
|
591
|
+
if (mode === "restore") {
|
|
592
|
+
replaceSessionDraft(identity, deferred.text ?? body.deferredTurn?.text ?? "", deferred.images ??
|
|
593
|
+
body.deferredTurn?.images ??
|
|
594
|
+
[]);
|
|
595
|
+
}
|
|
596
|
+
if (!isCurrent())
|
|
597
|
+
return;
|
|
550
598
|
const remaining = thread.deferredTurns.filter((item) => item.clientRequestId !== deferred.clientRequestId);
|
|
551
599
|
setDeferredTurns(remaining);
|
|
552
600
|
setDeferredTurn(remaining[0] ?? null);
|
|
553
|
-
if (mode === "restore") {
|
|
554
|
-
setInput(deferred.text ?? body.deferredTurn?.text ?? "");
|
|
555
|
-
setPendingImages(deferred.images ?? body.deferredTurn?.images ?? []);
|
|
556
|
-
}
|
|
557
601
|
setError(null);
|
|
558
602
|
}
|
|
559
603
|
catch (error) {
|
|
560
|
-
|
|
604
|
+
if (isCurrent())
|
|
605
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
561
606
|
}
|
|
562
607
|
}, [
|
|
563
608
|
origin,
|
|
@@ -566,8 +611,7 @@ export function useChatStream(params) {
|
|
|
566
611
|
thread.deferredTurns,
|
|
567
612
|
setDeferredTurn,
|
|
568
613
|
setDeferredTurns,
|
|
569
|
-
|
|
570
|
-
setPendingImages,
|
|
614
|
+
replaceSessionDraft,
|
|
571
615
|
setError,
|
|
572
616
|
]);
|
|
573
617
|
const cancelDeferredTurn = useCallback(() => requestDeferredTurnDelete("restore"), [requestDeferredTurnDelete]);
|
|
@@ -576,6 +620,9 @@ export function useChatStream(params) {
|
|
|
576
620
|
const activeSession = refs.sessionRef.current;
|
|
577
621
|
if (!activeSession)
|
|
578
622
|
return;
|
|
623
|
+
const generation = refs.sessionGenerationRef.current;
|
|
624
|
+
const isCurrent = () => refs.sessionRef.current?.sessionId === activeSession.sessionId &&
|
|
625
|
+
refs.sessionGenerationRef.current === generation;
|
|
579
626
|
const item = thread.deferredTurns.find((entry) => entry.clientRequestId === target.clientRequestId);
|
|
580
627
|
if (!item)
|
|
581
628
|
return;
|
|
@@ -587,6 +634,8 @@ export function useChatStream(params) {
|
|
|
587
634
|
body: JSON.stringify({ clientRequestId: target.clientRequestId, blockedByTurnId: item.blockedByTurnId, text, images: item.images ?? [], expectedRevision: target.revision }),
|
|
588
635
|
});
|
|
589
636
|
const body = (await response.json().catch(() => ({})));
|
|
637
|
+
if (!isCurrent())
|
|
638
|
+
return;
|
|
590
639
|
if (!response.ok)
|
|
591
640
|
throw new Error(body.error?.message ?? "编辑排队消息失败");
|
|
592
641
|
if (body.deferredTurns)
|
|
@@ -596,13 +645,17 @@ export function useChatStream(params) {
|
|
|
596
645
|
setError(null);
|
|
597
646
|
}
|
|
598
647
|
catch (error) {
|
|
599
|
-
|
|
648
|
+
if (isCurrent())
|
|
649
|
+
setError(error instanceof Error ? error.message : "编辑排队消息失败");
|
|
600
650
|
}
|
|
601
651
|
}, [origin, refs, thread.deferredTurns, setDeferredTurn, setDeferredTurns, setError]);
|
|
602
652
|
const steerDeferredTurn = useCallback(async (target) => {
|
|
603
653
|
const activeSession = refs.sessionRef.current;
|
|
604
654
|
if (!activeSession)
|
|
605
655
|
return;
|
|
656
|
+
const generation = refs.sessionGenerationRef.current;
|
|
657
|
+
const isCurrent = () => refs.sessionRef.current?.sessionId === activeSession.sessionId &&
|
|
658
|
+
refs.sessionGenerationRef.current === generation;
|
|
606
659
|
try {
|
|
607
660
|
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn/${encodeURIComponent(target.clientRequestId)}/steer`, {
|
|
608
661
|
method: "POST",
|
|
@@ -611,6 +664,8 @@ export function useChatStream(params) {
|
|
|
611
664
|
body: JSON.stringify({ expectedRevision: target.revision }),
|
|
612
665
|
});
|
|
613
666
|
const body = (await response.json().catch(() => ({})));
|
|
667
|
+
if (!isCurrent())
|
|
668
|
+
return;
|
|
614
669
|
if (!response.ok)
|
|
615
670
|
throw new Error(body.error?.message ?? "插话发送失败");
|
|
616
671
|
const remaining = body.deferredTurns ?? thread.deferredTurns.filter((item) => item.clientRequestId !== target.clientRequestId);
|
|
@@ -619,33 +674,39 @@ export function useChatStream(params) {
|
|
|
619
674
|
setError(null);
|
|
620
675
|
}
|
|
621
676
|
catch (error) {
|
|
622
|
-
|
|
677
|
+
if (isCurrent())
|
|
678
|
+
setError(error instanceof Error ? error.message : "插话发送失败");
|
|
623
679
|
}
|
|
624
680
|
}, [origin, refs, thread.deferredTurns, setDeferredTurn, setDeferredTurns, setError]);
|
|
625
|
-
const sendPrompt = useCallback(async (
|
|
681
|
+
const sendPrompt = useCallback(async (attempt) => {
|
|
682
|
+
const text = attempt.draftSnapshot;
|
|
683
|
+
const images = attempt.imageSnapshot;
|
|
626
684
|
const initialSession = refs.sessionRef.current;
|
|
627
|
-
if (!initialSession ||
|
|
685
|
+
if (!initialSession ||
|
|
686
|
+
initialSession.sessionId !== attempt.identity.sessionId ||
|
|
687
|
+
refs.sessionGenerationRef.current !== attempt.sessionGeneration ||
|
|
688
|
+
(!text.trim() && images.length === 0)) {
|
|
689
|
+
settleSubmission(attempt, "rejected");
|
|
628
690
|
return;
|
|
691
|
+
}
|
|
629
692
|
const ownership = ownershipBagFromRefs(refs);
|
|
630
693
|
const route = decideSubmissionRoute({
|
|
631
694
|
turnPhase: turnPhaseFromOwnership(ownership),
|
|
632
695
|
blockedByTurnId: ownership.stoppingTurnId ?? ownership.activeTurnId,
|
|
633
696
|
});
|
|
634
|
-
const clientRequestId =
|
|
697
|
+
const clientRequestId = attempt.clientRequestId;
|
|
635
698
|
if (route.action === "hold") {
|
|
636
699
|
const phase = turnPhaseFromOwnership(ownership);
|
|
637
700
|
if (phase === "stopping")
|
|
638
701
|
showNotice("正在确认上一轮停止状态,当前消息已保留");
|
|
639
702
|
else if (phase === "running")
|
|
640
703
|
showNotice("正在确认当前回合状态,当前消息已保留");
|
|
704
|
+
settleSubmission(attempt, "rejected");
|
|
641
705
|
return;
|
|
642
706
|
}
|
|
643
|
-
recordHistory?.(text);
|
|
644
707
|
if (route.action === "queue-deferred") {
|
|
645
708
|
const deferred = await queueDeferredTurn({
|
|
646
|
-
|
|
647
|
-
images,
|
|
648
|
-
clientRequestId,
|
|
709
|
+
attempt,
|
|
649
710
|
blockedByTurnId: route.blockedByTurnId,
|
|
650
711
|
});
|
|
651
712
|
if (deferred.action !== "send-now")
|
|
@@ -655,19 +716,22 @@ export function useChatStream(params) {
|
|
|
655
716
|
const generation = refs.sessionGenerationRef.current;
|
|
656
717
|
if (!activeSession ||
|
|
657
718
|
activeSession.sessionId !== initialSession.sessionId ||
|
|
719
|
+
generation !== attempt.sessionGeneration ||
|
|
658
720
|
(!text.trim() && images.length === 0) ||
|
|
659
721
|
refs.streamingRef.current ||
|
|
660
722
|
refs.submitInFlightRef.current ||
|
|
661
723
|
// AC-R5-002: ownership lease blocks send until terminal release.
|
|
662
|
-
refs.pendingSubmissionRef.current)
|
|
724
|
+
refs.pendingSubmissionRef.current) {
|
|
725
|
+
settleSubmission(attempt, "rejected");
|
|
663
726
|
return;
|
|
727
|
+
}
|
|
664
728
|
// AC-001: acquire synchronous submit ownership before the first await.
|
|
665
729
|
refs.submitInFlightRef.current = true;
|
|
666
730
|
const submitGeneration = ++refs.submitGenerationRef.current;
|
|
667
731
|
const userMessageId = requestId("u");
|
|
668
732
|
const assistantId = requestId("a");
|
|
669
733
|
const draftText = text.trim();
|
|
670
|
-
const draftImages = images
|
|
734
|
+
const draftImages = [...images];
|
|
671
735
|
const pending = createPendingSubmission({
|
|
672
736
|
clientRequestId,
|
|
673
737
|
submitGeneration,
|
|
@@ -699,7 +763,6 @@ export function useChatStream(params) {
|
|
|
699
763
|
user: userMsg,
|
|
700
764
|
assistant: assistantMsg,
|
|
701
765
|
}));
|
|
702
|
-
setInput("");
|
|
703
766
|
setAutoScroll(true);
|
|
704
767
|
setError(null);
|
|
705
768
|
try {
|
|
@@ -713,8 +776,7 @@ export function useChatStream(params) {
|
|
|
713
776
|
releaseSubmitOwnership(clientRequestId);
|
|
714
777
|
setMessages((messages) => removePendingSubmission(messages, clientRequestId));
|
|
715
778
|
clearEmptyAssistant(setMessages, assistantId);
|
|
716
|
-
|
|
717
|
-
setPendingImages(draftImages);
|
|
779
|
+
settleSubmission(attempt, "rejected");
|
|
718
780
|
return;
|
|
719
781
|
}
|
|
720
782
|
if (refs.sessionRef.current?.sessionId !== activeSession.sessionId ||
|
|
@@ -722,13 +784,11 @@ export function useChatStream(params) {
|
|
|
722
784
|
releaseSubmitOwnership(clientRequestId);
|
|
723
785
|
if (refs.sessionRef.current?.sessionId === activeSession.sessionId) {
|
|
724
786
|
// Same session, newer generation: the activation snapshot replaced
|
|
725
|
-
// the thread, so
|
|
726
|
-
// instead of losing the user's text silently (2026-09-08).
|
|
787
|
+
// the thread, so remove its obsolete optimistic projection.
|
|
727
788
|
setMessages((messages) => removePendingSubmission(messages, clientRequestId));
|
|
728
789
|
clearEmptyAssistant(setMessages, assistantId);
|
|
729
|
-
setInput(draftText);
|
|
730
|
-
setPendingImages(draftImages);
|
|
731
790
|
}
|
|
791
|
+
settleSubmission(attempt, "rejected");
|
|
732
792
|
return;
|
|
733
793
|
}
|
|
734
794
|
// Attachments stay until accepted (AC/UIS-accepted).
|
|
@@ -744,6 +804,7 @@ export function useChatStream(params) {
|
|
|
744
804
|
assistantId,
|
|
745
805
|
text: draftText,
|
|
746
806
|
images: draftImages,
|
|
807
|
+
attempt,
|
|
747
808
|
pending,
|
|
748
809
|
};
|
|
749
810
|
const first = await postCreateTurn({
|
|
@@ -751,6 +812,7 @@ export function useChatStream(params) {
|
|
|
751
812
|
clientRequestId,
|
|
752
813
|
text: draftText,
|
|
753
814
|
images: draftImages,
|
|
815
|
+
signal: attempt.signal,
|
|
754
816
|
});
|
|
755
817
|
if (!isSubmissionCurrent({
|
|
756
818
|
pending,
|
|
@@ -760,6 +822,7 @@ export function useChatStream(params) {
|
|
|
760
822
|
})) {
|
|
761
823
|
pending.resolveOutcome({ kind: "stale" });
|
|
762
824
|
releaseSubmitOwnership(clientRequestId);
|
|
825
|
+
settleSubmission(attempt, first.httpOk && first.body.accepted ? "accepted" : "rejected");
|
|
763
826
|
return;
|
|
764
827
|
}
|
|
765
828
|
let outcome = first.networkError
|
|
@@ -786,6 +849,7 @@ export function useChatStream(params) {
|
|
|
786
849
|
clientRequestId,
|
|
787
850
|
text: draftText,
|
|
788
851
|
images: draftImages,
|
|
852
|
+
signal: attempt.signal,
|
|
789
853
|
});
|
|
790
854
|
if (!isSubmissionCurrent({
|
|
791
855
|
pending,
|
|
@@ -795,6 +859,7 @@ export function useChatStream(params) {
|
|
|
795
859
|
})) {
|
|
796
860
|
pending.resolveOutcome({ kind: "stale" });
|
|
797
861
|
releaseSubmitOwnership(clientRequestId);
|
|
862
|
+
settleSubmission(attempt, retry.httpOk && retry.body.accepted ? "accepted" : "rejected");
|
|
798
863
|
return;
|
|
799
864
|
}
|
|
800
865
|
outcome = retry.networkError
|
|
@@ -825,14 +890,13 @@ export function useChatStream(params) {
|
|
|
825
890
|
releaseSubmitOwnership,
|
|
826
891
|
showNotice,
|
|
827
892
|
setMessages,
|
|
828
|
-
|
|
893
|
+
settleSubmission,
|
|
829
894
|
setAutoScroll,
|
|
830
895
|
setError,
|
|
831
896
|
setStreaming,
|
|
832
897
|
setTurnPhase,
|
|
833
898
|
setStreamingAssistantId,
|
|
834
899
|
queueDeferredTurn,
|
|
835
|
-
recordHistory,
|
|
836
900
|
]);
|
|
837
901
|
const handleStop = useCallback(async () => {
|
|
838
902
|
// AC-R5-001: durable stopRequested is set synchronously before any await so
|
|
@@ -888,8 +952,9 @@ export function useChatStream(params) {
|
|
|
888
952
|
// Intent remains; /state bind-active will fire exactly one abort.
|
|
889
953
|
return;
|
|
890
954
|
}
|
|
891
|
-
|
|
892
|
-
|
|
955
|
+
if (outcome.kind === "rejected" ||
|
|
956
|
+
outcome.kind === "stale") {
|
|
957
|
+
// Rejected/stale settlement released this submission's stop intent.
|
|
893
958
|
refs.stopRequestedRef.current = false;
|
|
894
959
|
return;
|
|
895
960
|
}
|
|
@@ -933,6 +998,7 @@ export function useChatStream(params) {
|
|
|
933
998
|
if (!session || (!streaming && !hasOwnershipWork))
|
|
934
999
|
return;
|
|
935
1000
|
const sessionId = session.sessionId;
|
|
1001
|
+
const sessionIdentity = { ...session };
|
|
936
1002
|
const generation = refs.sessionGenerationRef.current;
|
|
937
1003
|
let cancelled = false;
|
|
938
1004
|
let syncInFlight = null;
|
|
@@ -975,11 +1041,15 @@ export function useChatStream(params) {
|
|
|
975
1041
|
if (state && "deferredTurn" in state)
|
|
976
1042
|
setDeferredTurn(state.deferredTurn ?? null);
|
|
977
1043
|
if (result === "bound" && ownershipPhase !== "stopping") {
|
|
1044
|
+
settleSubmissionByRequest(sessionIdentity, pending.clientRequestId, "accepted");
|
|
978
1045
|
// US-BUSY-ACTIVE-BOUND: keep streaming/busy with same id.
|
|
979
1046
|
setStreaming(true);
|
|
980
1047
|
return;
|
|
981
1048
|
}
|
|
982
1049
|
if (result === "released") {
|
|
1050
|
+
settleSubmissionByRequest(sessionIdentity, pending.clientRequestId, decision.action === "release-not-accepted"
|
|
1051
|
+
? "rejected"
|
|
1052
|
+
: "accepted");
|
|
983
1053
|
return;
|
|
984
1054
|
}
|
|
985
1055
|
// hold without acceptedTurnId: keep pending + busy.
|
|
@@ -1002,6 +1072,7 @@ export function useChatStream(params) {
|
|
|
1002
1072
|
stillPending.clientRequestId &&
|
|
1003
1073
|
isTerminalTurnState(state?.activeTurn?.state);
|
|
1004
1074
|
if (latestMatches || activeMatches) {
|
|
1075
|
+
settleSubmissionByRequest(sessionIdentity, stillPending.clientRequestId, "accepted");
|
|
1005
1076
|
const termTurnId = activeMatches
|
|
1006
1077
|
? (state?.activeTurn?.turnId ?? null)
|
|
1007
1078
|
: (state?.latestTurn?.turnId ??
|
|
@@ -1073,7 +1144,10 @@ export function useChatStream(params) {
|
|
|
1073
1144
|
stopRetryIndex >= STOP_RECONCILE_DELAYS_MS.length ||
|
|
1074
1145
|
(!refs.stopRequestedRef.current && !refs.stoppingTurnIdRef.current))
|
|
1075
1146
|
return;
|
|
1076
|
-
const delay = STOP_RECONCILE_DELAYS_MS[stopRetryIndex
|
|
1147
|
+
const delay = STOP_RECONCILE_DELAYS_MS[stopRetryIndex];
|
|
1148
|
+
stopRetryIndex += 1;
|
|
1149
|
+
if (delay === undefined)
|
|
1150
|
+
return;
|
|
1077
1151
|
stopRetryTimer = window.setTimeout(() => {
|
|
1078
1152
|
void sync().finally(scheduleStopRetry);
|
|
1079
1153
|
}, delay);
|
|
@@ -1095,6 +1169,7 @@ export function useChatStream(params) {
|
|
|
1095
1169
|
refs,
|
|
1096
1170
|
session,
|
|
1097
1171
|
streaming,
|
|
1172
|
+
settleSubmissionByRequest,
|
|
1098
1173
|
setTurnPhase,
|
|
1099
1174
|
markIdleIfCurrent,
|
|
1100
1175
|
releaseSubmitOwnership,
|