@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/commands/client-recovery.js +20 -55
  4. package/dist/executors/dag-pi-executor.js +27 -6
  5. package/dist/executors/shell-executor.js +3 -2
  6. package/dist/shared/pi-context-pressure/extension.js +26 -16
  7. package/dist/shared/pi-provider-recovery.js +182 -0
  8. package/dist/shared/pi-retry-settings.js +40 -0
  9. package/dist/worker/console/chat/browser-policy.js +6 -36
  10. package/dist/worker/console/chat/browser-routes.js +3 -11
  11. package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +2 -1
  12. package/dist/worker/console/chat/pi-runtime.js +11 -12
  13. package/dist/worker/console/chat/routes.js +1 -5
  14. package/dist/worker/console/chat/session-mode-view.js +5 -4
  15. package/dist/worker/console/chat/session-mode.js +11 -10
  16. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-D_fNbWc_.js → abnfDiagram-N423BO3Z-BEg5712w.js} +1 -1
  17. package/dist/worker/console/static/assets/{arc-_HuqLW6m.js → arc-CkQvaBGZ.js} +1 -1
  18. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Djy2qY1m.js → architectureDiagram-T3A2C74G-BrdwFNJ7.js} +1 -1
  19. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-bB-1TSyY.js → blockDiagram-VBNYF7ZC-M_LjP2kx.js} +1 -1
  20. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5Hmghrz.js → c4Diagram-5PPSVZJV-Bwr5Dtjc.js} +1 -1
  21. package/dist/worker/console/static/assets/channel-CKmyKgY-.js +1 -0
  22. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Duyl39oy.js → chunk-2GRJ4B5K-CDc44stW.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-x6mdnSyf.js → chunk-2Q5K7J3B-DZELwsTt.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-WlBuFj7W.js → chunk-5RXB4S5H-D7YI6SWX.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CTF429HX.js → chunk-5VM5RSS4-Bc46bEyj.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dwd6r-SF.js → chunk-6Q2QTUOP-Bj4vc-Dp.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C-Br82D1.js → chunk-GF5L2VYU-DRi-0nDX.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CuGp0DeE.js → chunk-JWPE2WC7-Dr-wFY60.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CJhFX6Lh.js → chunk-KBJHAD2P-CqIHE_D7.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DpGOa_jT.js → chunk-RYQCIY6F-C-Dl8wzk.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DXJaTynd.js → chunk-XXDRQBXY-CV4Mydz3.js} +1 -1
  32. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D8Wc3jJP.js +1 -0
  33. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D8Wc3jJP.js +1 -0
  34. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CRBZWjE4.js → cose-bilkent-JH36ORCC-C3MamSMJ.js} +1 -1
  35. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BdXcxtlb.js → cynefin-VYW2F7L2-zo_Jy2sf.js} +1 -1
  36. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-07UKsOts.js → cynefinDiagram-MW4NZA55-BB2PN1AG.js} +1 -1
  37. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-D38p9__S.js → dagre-VZM6K2ZE-BimYueoi.js} +1 -1
  38. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-nSiBNNjH.js → diagram-7IWD3JNH-Di1ObJRS.js} +1 -1
  39. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BFOTot5D.js → diagram-B4RE2ZJO-C83l-IJx.js} +1 -1
  40. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DypKaZ6k.js → diagram-LBJQPF4R-B3S3fOpm.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-B6yoOQeW.js → diagram-Q27KOJAE-urW_Zhe7.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BHYS6viX.js → diagram-UB23O5K3-nvIbCwF6.js} +1 -1
  43. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-D2VD5Snj.js → ebnfDiagram-BXEA7PRR-DG6O3KpP.js} +1 -1
  44. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-E98mWxN1.js → erDiagram-JOGREHBK-DpXdtdsj.js} +1 -1
  45. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CT1zxhqP.js → flowDiagram-UKHOOZJN-DvXWHrND.js} +1 -1
  46. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-6GcngNcy.js → ganttDiagram-PKOTCBZU-6F7SbvUS.js} +1 -1
  47. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCL6NvKe.js → gitGraphDiagram-DS77QQ5N-D1tZnFec.js} +1 -1
  48. package/dist/worker/console/static/assets/{index-24jp3aBh.js → index-CWarwRqc.js} +94 -94
  49. package/dist/worker/console/static/assets/{index-CY6y6I8M.css → index-SAANIK8g.css} +1 -1
  50. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C0627e-9.js → infoDiagram-6WML65LV-HN4qwJ2M.js} +1 -1
  51. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js → ishikawaDiagram-WSZJBQD7-OJ4qwDRo.js} +1 -1
  52. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DPDPv9HL.js → journeyDiagram-NVQOT4AX-CJZ6giQk.js} +1 -1
  53. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D3OHdWiO.js → kanban-definition-27J2QSJJ-co60m19S.js} +1 -1
  54. package/dist/worker/console/static/assets/{linear-DI88zD7n.js → linear-D1KeFqZY.js} +1 -1
  55. package/dist/worker/console/static/assets/{mermaid.core-p2l8IrWY.js → mermaid.core-i7dDGqov.js} +5 -5
  56. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-B1bAX-oS.js → mindmap-definition-FAOFIHXS-1SkQxBh1.js} +1 -1
  57. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DwOnWxdD.js → pegDiagram-VL7TDLO6-B10dhI-Y.js} +1 -1
  58. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-xRkTXnJC.js → pieDiagram-7S7Q4E2Y-CiUKUo6j.js} +1 -1
  59. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-_9UC5Zio.js → quadrantDiagram-CIZ2JOQS-C5vH4LXZ.js} +1 -1
  60. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CgEUirWJ.js → railroadDiagram-AXF67PYL-5nlWOCEj.js} +1 -1
  61. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Czbpwiyz.js → requirementDiagram-LRYGKXZP-CILd6QS3.js} +1 -1
  62. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-_j7_KiqY.js → sankeyDiagram-W5VNT64P-Dfp18ZO2.js} +1 -1
  63. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BNJa6Hyi.js → sequenceDiagram-SI44F4Z6-B9pkswBi.js} +1 -1
  64. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DedpKtOc.js → sizeCapture-X5ZJPWSS-C1-eSgx4.js} +1 -1
  65. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BBvthr7c.js → stateDiagram-OKZ733FA-COJsOoAF.js} +1 -1
  66. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Bpfb0q5s.js +1 -0
  67. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Ds7umgEM.js → swimlanes-SLNWSIFB-Dv5NXJUM.js} +2 -2
  68. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BEwGGabW.js +8 -0
  69. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKa-p1nF.js → timeline-definition-Z64GVDOM-DRdrZ7eR.js} +1 -1
  70. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LoUNR56o.js → vennDiagram-T6HMQDX7-0-2TlwCj.js} +1 -1
  71. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bqbl6ym7.js → wardleyDiagram-T6FBY63Y-3-o3q92P.js} +1 -1
  72. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B8dUg4lf.js → xychartDiagram-ELKLHX3M-DSgOgdUi.js} +1 -1
  73. package/dist/worker/console/static/index.html +2 -2
  74. package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +192 -0
  75. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +9 -1
  76. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +78 -35
  77. package/dist/worker/console/static-src/operator-chat/useChatStream.js +145 -70
  78. package/dist/worker/console/static-src/operator-chat/useComposer.js +182 -69
  79. package/dist/workflows/dag/frontend-closeout.js +1 -1
  80. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  81. package/dist/workflows/dag/frontend-review-context.js +2 -1
  82. package/dist/workflows/dag/frontend-review-scopes.js +2 -2
  83. package/dist/workflows/dag/frontend-verification-trace.js +1 -1
  84. package/dist/workflows/dag/init-hybrid.js +51 -159
  85. package/docs/architecture/runtime-boundaries.md +3 -3
  86. package/docs/templates/frontend-implementation-dag.json +9 -8
  87. package/package.json +1 -1
  88. package/skills/frontend-bounded-implement/SKILL.md +3 -3
  89. package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
  90. package/skills/frontend-contract/references/contract-protocol.md +3 -1
  91. package/skills/frontend-design-review/SKILL.md +12 -7
  92. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  93. package/skills/frontend-plan/SKILL.md +12 -0
  94. package/skills/frontend-plan/references/decision-contract.md +25 -19
  95. package/skills/frontend-review/SKILL.md +12 -18
  96. package/skills/frontend-review/references/review-findings.md +24 -43
  97. package/dist/worker/console/static/assets/channel-C0A6UEIa.js +0 -1
  98. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +0 -1
  99. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +0 -1
  100. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +0 -1
  101. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +0 -8
@@ -1,31 +1,70 @@
1
- import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
1
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, } from "react";
2
2
  import { filterSlashCommands, mergeSlashCommands, } from "../../chat/shortcuts.js";
3
- import { confirmationToken } from "./format.js";
3
+ import { ComposerSessionLedger, composerSessionKey, } from "./composer-session-ledger.js";
4
+ import { confirmationToken, requestId } from "./format.js";
4
5
  import { clampShortcutIndex } from "./slash-palette-nav.js";
5
- import { createInputHistoryState, navigateInputHistory, recordInputHistoryEntry, resetInputHistoryBrowsing, } from "./input-history.js";
6
6
  import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
7
+ function sameComposerSession(left, right) {
8
+ if (!left || !right)
9
+ return left === right;
10
+ return composerSessionKey(left) === composerSessionKey(right);
11
+ }
12
+ function readPendingImage(file) {
13
+ return new Promise((resolve, reject) => {
14
+ const reader = new FileReader();
15
+ reader.onerror = () => reject(reader.error ?? new Error("图片读取失败"));
16
+ reader.onload = () => {
17
+ const value = String(reader.result ?? "");
18
+ resolve({
19
+ name: file.name,
20
+ type: "image",
21
+ mimeType: file.type,
22
+ data: value.slice(value.indexOf(",") + 1),
23
+ });
24
+ };
25
+ reader.readAsDataURL(file);
26
+ });
27
+ }
7
28
  /** Composer state: input text, @file palette, image attachments, /shortcuts,
8
29
  * IME composition guards, auto-grow and server-side draft sync. */
9
30
  export function useComposer(params) {
10
- const { origin, sessionId, sessionMode, refs, setError, onUnrecoverableSession, } = params;
11
- const [input, setInputState] = useState("");
31
+ const { origin, sessionIdentity, sessionMode, sessionReady = true, refs, setError, onUnrecoverableSession, } = params;
32
+ const sessionId = sessionIdentity?.sessionId;
33
+ const ledgerRef = useRef(null);
34
+ if (!ledgerRef.current)
35
+ ledgerRef.current = new ComposerSessionLedger();
36
+ const ledger = ledgerRef.current;
37
+ const activeIdentityRef = useRef(sessionIdentity);
38
+ activeIdentityRef.current = sessionIdentity;
39
+ const [, forceRender] = useReducer((version) => version + 1, 0);
40
+ const activeSnapshot = sessionIdentity
41
+ ? ledger.snapshot(sessionIdentity)
42
+ : { draft: "", images: [] };
43
+ const input = activeSnapshot.draft;
44
+ const pendingImages = activeSnapshot.images;
12
45
  const [fileOptions, setFileOptions] = useState([]);
13
- const [pendingImages, setPendingImages] = useState([]);
14
46
  const [shortcutOpen, setShortcutOpen] = useState(false);
15
47
  /** Any composer text that starts with `/` should reveal the palette,
16
48
  * including draft restore. Escape can still close it without clearing. */
17
49
  const setInput = useCallback((value) => {
18
- if (typeof value === "function") {
19
- setInputState((prev) => {
20
- const next = value(prev);
21
- setShortcutOpen(next.startsWith("/"));
22
- return next;
23
- });
50
+ const identity = activeIdentityRef.current;
51
+ if (!identity)
24
52
  return;
25
- }
26
- setInputState(value);
27
- setShortcutOpen(value.startsWith("/"));
28
- }, []);
53
+ const previous = ledger.snapshot(identity).draft;
54
+ const next = typeof value === "function" ? value(previous) : value;
55
+ if (ledger.setDraft(identity, next))
56
+ forceRender();
57
+ setShortcutOpen(next.startsWith("/"));
58
+ }, [ledger]);
59
+ const setPendingImages = useCallback((value) => {
60
+ const identity = activeIdentityRef.current;
61
+ if (!identity)
62
+ return;
63
+ const previous = ledger.snapshot(identity).images;
64
+ const next = typeof value === "function" ? value(previous) : value;
65
+ ledger.setImages(identity, next);
66
+ forceRender();
67
+ }, [ledger]);
29
68
  const [shortcutIndex, setShortcutIndex] = useState(0);
30
69
  const [dynamicCommands, setDynamicCommands] = useState([]);
31
70
  const [commandCapabilities, setCommandCapabilities] = useState({
@@ -36,30 +75,29 @@ export function useComposer(params) {
36
75
  const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
37
76
  const [commandsEpoch, setCommandsEpoch] = useState(0);
38
77
  const textareaRef = useRef(null);
39
- /** Arrow-key input history; unsent drafts survive history browsing. */
40
- const historyRef = useRef(createInputHistoryState());
41
78
  /** Active session generation for /commands; bumped immediately on switch (AC-2). */
42
79
  const commandsSessionRef = useRef(sessionId);
43
80
  const commandsGenerationRef = useRef(0);
44
81
  const handleInputChange = useCallback((value) => {
45
82
  setInput(value);
46
- setShortcutOpen(value.startsWith("/"));
47
83
  setShortcutIndex(0);
48
- // Manual typing edits a recalled entry in place → back to present draft.
49
- historyRef.current = resetInputHistoryBrowsing(historyRef.current);
50
- }, []);
84
+ }, [setInput]);
51
85
  /** Recall an earlier input via ↑/↓; returns true when the key was consumed. */
52
86
  const navigateHistory = useCallback((direction) => {
87
+ const identity = activeIdentityRef.current;
88
+ if (!identity)
89
+ return false;
90
+ const current = ledger.snapshot(identity).draft;
53
91
  const textarea = textareaRef.current;
54
- const decision = navigateInputHistory(historyRef.current, direction, {
55
- text: input,
56
- caretStart: textarea?.selectionStart ?? input.length,
57
- caretEnd: textarea?.selectionEnd ?? input.length,
92
+ const decision = ledger.navigateHistory(identity, direction, {
93
+ text: current,
94
+ caretStart: textarea?.selectionStart ?? current.length,
95
+ caretEnd: textarea?.selectionEnd ?? current.length,
58
96
  });
59
97
  if (!decision.handled)
60
98
  return false;
61
- historyRef.current = decision.state;
62
- setInput(decision.text);
99
+ forceRender();
100
+ setShortcutOpen(decision.text.startsWith("/"));
63
101
  const caret = decision.text.length;
64
102
  queueMicrotask(() => {
65
103
  const el = textareaRef.current;
@@ -68,22 +106,76 @@ export function useComposer(params) {
68
106
  }
69
107
  });
70
108
  return true;
71
- }, [input, setInput]);
72
- /** Record a submitted text into history (consecutive duplicates collapse). */
109
+ }, [ledger]);
110
+ /** Record a submitted text into the selected session's history. */
73
111
  const recordHistory = useCallback((text) => {
74
- historyRef.current = recordInputHistoryEntry(historyRef.current, text);
75
- }, []);
112
+ const identity = activeIdentityRef.current;
113
+ if (identity)
114
+ ledger.recordHistory(identity, text);
115
+ }, [ledger]);
116
+ const claimSubmission = useCallback(() => {
117
+ const identity = activeIdentityRef.current;
118
+ if (!identity)
119
+ return null;
120
+ const attempt = ledger.beginSubmission(identity, refs.sessionGenerationRef.current, requestId("chat-submit"));
121
+ if (!attempt)
122
+ return null;
123
+ forceRender();
124
+ setShortcutOpen(false);
125
+ setShortcutIndex(0);
126
+ return attempt;
127
+ }, [ledger, refs]);
128
+ const claimSessionSubmission = useCallback((identity, text, images = []) => {
129
+ ledger.setDraft(identity, text);
130
+ ledger.setImages(identity, images);
131
+ const attempt = ledger.beginSubmission(identity, refs.sessionGenerationRef.current, requestId("chat-submit"));
132
+ if (sameComposerSession(activeIdentityRef.current, identity))
133
+ forceRender();
134
+ return attempt;
135
+ }, [ledger, refs]);
136
+ const settleSubmission = useCallback((attempt, outcome) => {
137
+ const result = ledger.settle(attempt, outcome);
138
+ if (sameComposerSession(activeIdentityRef.current, attempt.identity))
139
+ forceRender();
140
+ return result;
141
+ }, [ledger]);
142
+ const settleSubmissionByRequest = useCallback((identity, clientRequestId, outcome) => {
143
+ const result = ledger.settleByRequest(identity, clientRequestId, outcome);
144
+ if (sameComposerSession(activeIdentityRef.current, identity))
145
+ forceRender();
146
+ return result;
147
+ }, [ledger]);
148
+ const seedSession = useCallback((identity, draft) => {
149
+ const seeded = ledger.seedDraft(identity, draft);
150
+ if (seeded && sameComposerSession(activeIdentityRef.current, identity))
151
+ forceRender();
152
+ return seeded;
153
+ }, [ledger]);
154
+ const replaceSessionDraft = useCallback((identity, draft, images) => {
155
+ ledger.setDraft(identity, draft);
156
+ ledger.setImages(identity, images);
157
+ if (sameComposerSession(activeIdentityRef.current, identity))
158
+ forceRender();
159
+ }, [ledger]);
160
+ const releaseSession = useCallback((identity) => {
161
+ ledger.release(identity);
162
+ if (sameComposerSession(activeIdentityRef.current, identity))
163
+ forceRender();
164
+ }, [ledger]);
76
165
  // Immediate invalidation on A→B even when B palette never opens (AC-2).
77
166
  useLayoutEffect(() => {
78
167
  commandsSessionRef.current = sessionId;
79
168
  commandsGenerationRef.current += 1;
169
+ refs.isComposingRef.current = false;
170
+ setFileOptions([]);
171
+ setShortcutOpen(input.startsWith("/"));
80
172
  setDynamicCommands([]);
81
173
  setCommandCapabilities({ sddBingo: false, sessionMode });
82
174
  setDynamicCommandsError(null);
83
175
  setDynamicCommandsLoading(false);
84
176
  }, [sessionId, sessionMode]);
85
177
  const loadDynamicCommands = useCallback(async () => {
86
- if (!sessionId) {
178
+ if (!sessionId || !sessionReady) {
87
179
  setDynamicCommands([]);
88
180
  setDynamicCommandsError(null);
89
181
  setDynamicCommandsLoading(false);
@@ -147,21 +239,21 @@ export function useComposer(params) {
147
239
  setDynamicCommandsLoading(false);
148
240
  }
149
241
  }
150
- }, [origin, sessionId, sessionMode]);
242
+ }, [origin, sessionId, sessionMode, sessionReady]);
151
243
  // Lazy-load dynamic commands when slash palette opens or session changes.
152
244
  useEffect(() => {
153
- if (!sessionId)
245
+ if (!sessionId || !sessionReady)
154
246
  return;
155
247
  if (!shortcutOpen && commandsEpoch === 0)
156
248
  return;
157
249
  void loadDynamicCommands();
158
- }, [sessionId, shortcutOpen, commandsEpoch, loadDynamicCommands]);
250
+ }, [sessionId, sessionReady, shortcutOpen, commandsEpoch, loadDynamicCommands]);
159
251
  const retryDynamicCommands = useCallback(() => {
160
252
  setCommandsEpoch((n) => n + 1);
161
253
  void loadDynamicCommands();
162
254
  }, [loadDynamicCommands]);
163
255
  useEffect(() => {
164
- if (!sessionId)
256
+ if (!sessionId || !sessionIdentity || !sessionReady)
165
257
  return;
166
258
  const match = input
167
259
  .slice(0, textareaRef.current?.selectionStart ?? input.length)
@@ -170,13 +262,29 @@ export function useComposer(params) {
170
262
  setFileOptions([]);
171
263
  return;
172
264
  }
265
+ const owner = { ...sessionIdentity };
266
+ const ownerKey = composerSessionKey(owner);
267
+ const generation = refs.sessionGenerationRef.current;
268
+ const controller = new AbortController();
173
269
  const timer = window.setTimeout(() => {
174
- void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/files?q=${encodeURIComponent(match[1] ?? "")}`)
270
+ void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/files?q=${encodeURIComponent(match[1] ?? "")}`, { signal: controller.signal })
175
271
  .then((r) => (r.ok ? r.json() : null))
176
- .then((body) => setFileOptions((body?.files ?? []).map((item) => item.path)));
272
+ .then((body) => {
273
+ const active = activeIdentityRef.current;
274
+ if (controller.signal.aborted ||
275
+ !active ||
276
+ composerSessionKey(active) !== ownerKey ||
277
+ refs.sessionGenerationRef.current !== generation)
278
+ return;
279
+ setFileOptions((body?.files ?? []).map((item) => item.path));
280
+ })
281
+ .catch(() => undefined);
177
282
  }, 150);
178
- return () => window.clearTimeout(timer);
179
- }, [origin, sessionId, input]);
283
+ return () => {
284
+ window.clearTimeout(timer);
285
+ controller.abort();
286
+ };
287
+ }, [origin, sessionId, sessionIdentity, sessionReady, input, refs]);
180
288
  const insertFileReference = useCallback((file) => {
181
289
  const caret = textareaRef.current?.selectionStart ?? input.length;
182
290
  const before = input.slice(0, caret);
@@ -185,33 +293,31 @@ export function useComposer(params) {
185
293
  return;
186
294
  setInput(`${input.slice(0, start)}@${file} ${input.slice(caret)}`);
187
295
  setFileOptions([]);
188
- }, [input]);
296
+ }, [input, setInput]);
189
297
  const addImagesFromFiles = useCallback((files) => {
190
- for (const file of Array.from(files).slice(0, 4)) {
191
- if (!file.type.startsWith("image/") || file.size > 5 * 1024 * 1024) {
192
- setError("图片仅支持 PNG/JPEG/GIF/WebP,单张不超过 5 MiB");
193
- continue;
194
- }
195
- const reader = new FileReader();
196
- reader.onload = () => {
197
- const value = String(reader.result ?? "");
198
- const data = value.slice(value.indexOf(",") + 1);
199
- setPendingImages((items) => [
200
- ...items,
201
- {
202
- name: file.name,
203
- type: "image",
204
- mimeType: file.type,
205
- data,
206
- },
207
- ].slice(0, 4));
208
- };
209
- reader.readAsDataURL(file);
210
- }
211
- }, [setError]);
298
+ const owner = activeIdentityRef.current;
299
+ if (!owner)
300
+ return;
301
+ const ownerIdentity = { ...owner };
302
+ const selected = Array.from(files).slice(0, 4);
303
+ const valid = selected.filter((file) => file.type.startsWith("image/") && file.size <= 5 * 1024 * 1024);
304
+ if (valid.length !== selected.length)
305
+ setError("图片仅支持 PNG/JPEG/GIF/WebP,单张不超过 5 MiB");
306
+ void Promise.all(valid.map(readPendingImage)).then((loaded) => {
307
+ if (!ledger.has(ownerIdentity) || loaded.length === 0)
308
+ return;
309
+ const current = ledger.snapshot(ownerIdentity).images;
310
+ ledger.setImages(ownerIdentity, [...current, ...loaded].slice(0, 4));
311
+ if (sameComposerSession(activeIdentityRef.current, ownerIdentity))
312
+ forceRender();
313
+ }, (error) => {
314
+ if (sameComposerSession(activeIdentityRef.current, ownerIdentity))
315
+ setError(error instanceof Error ? error.message : "图片读取失败");
316
+ });
317
+ }, [ledger, setError]);
212
318
  const removeImage = useCallback((image) => {
213
319
  setPendingImages((items) => items.filter((item) => item !== image));
214
- }, []);
320
+ }, [setPendingImages]);
215
321
  // Chromium handles growth in CSS without forcing a layout read on every key.
216
322
  // Keep a deferred fallback for browsers that do not support field-sizing.
217
323
  useEffect(() => {
@@ -231,7 +337,7 @@ export function useComposer(params) {
231
337
  }, [input]);
232
338
  // Debounced server-side composer draft sync.
233
339
  useEffect(() => {
234
- if (!sessionId)
340
+ if (!sessionId || !sessionReady)
235
341
  return;
236
342
  const timer = window.setTimeout(() => {
237
343
  void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/draft`, {
@@ -245,7 +351,7 @@ export function useComposer(params) {
245
351
  });
246
352
  }, 400);
247
353
  return () => window.clearTimeout(timer);
248
- }, [origin, sessionId, input]);
354
+ }, [origin, sessionId, sessionReady, input]);
249
355
  const mergedCommands = useMemo(() => mergeSlashCommands(dynamicCommands, commandCapabilities), [dynamicCommands, commandCapabilities]);
250
356
  const filteredShortcuts = useMemo(() => input.startsWith("/")
251
357
  ? filterSlashCommands(mergedCommands, input)
@@ -279,17 +385,24 @@ export function useComposer(params) {
279
385
  useEffect(() => {
280
386
  attachRuntimeSnapshotController({
281
387
  origin,
282
- sessionId,
388
+ sessionId: sessionReady ? sessionId : undefined,
283
389
  refs,
284
390
  onUnrecoverableSession,
285
391
  });
286
- }, [origin, sessionId, refs, onUnrecoverableSession]);
392
+ }, [origin, sessionId, sessionReady, refs, onUnrecoverableSession]);
287
393
  return {
288
394
  input,
289
395
  setInput,
290
396
  handleInputChange,
291
397
  navigateHistory,
292
398
  recordHistory,
399
+ claimSubmission,
400
+ claimSessionSubmission,
401
+ settleSubmission,
402
+ settleSubmissionByRequest,
403
+ seedSession,
404
+ replaceSessionDraft,
405
+ releaseSession,
293
406
  fileOptions,
294
407
  pendingImages,
295
408
  setPendingImages,
@@ -114,7 +114,7 @@ export function buildFrontendTestHandoff(input) {
114
114
  * required verification evidence. Anything else downgrades to pending.
115
115
  */
116
116
  export function deriveFrontendIntegrationFacts(input) {
117
- const requiredEvidenceSatisfied = input.verificationTargets.length > 0 &&
117
+ const requiredEvidenceSatisfied = input.verificationTargets.some((target) => target.mode === "behavior") &&
118
118
  input.verificationTargets.every((target) => target.status === "ok");
119
119
  if (input.realApiEvidence.present &&
120
120
  input.realApiEvidence.passed &&
@@ -702,10 +702,10 @@ export async function runFrontendPrewriteGate(input) {
702
702
  failureCode: "mock-strategy-outside-allowed",
703
703
  });
704
704
  }
705
- // Mock command binding (main): a non-not-needed strategy can only be proven
706
- // by the DAG's frozen Mock verification commands. Block before any write is
707
- // authorized when none were materialized at generation time.
705
+ // Behavior verification needs bound Mock evidence. Build-only implementation
706
+ // does not claim tested Mock behavior and may defer those commands.
708
707
  if (mockStrategy !== "not-needed" &&
708
+ analysis.canonical.verificationTargets.some((target) => target.mode === "behavior") &&
709
709
  (input.config.mockCommandLabels?.length ?? 0) === 0) {
710
710
  return finalizePrewrite(input, {
711
711
  ...basePending,
@@ -246,7 +246,8 @@ function assertReviewEvidence(input) {
246
246
  const trace = input.verificationTrace;
247
247
  const expectedTargets = new Map(contract.data.verificationTargets.map((target) => [target.id, target]));
248
248
  const actualTargets = Array.isArray(trace?.targets) ? trace.targets : [];
249
- const mockEvidenceBound = contract.data.mockApi.strategy === "not-needed" ||
249
+ const mockEvidenceBound = contract.data.verificationTargets.every((target) => target.mode === "static") ||
250
+ contract.data.mockApi.strategy === "not-needed" ||
250
251
  (typeof trace?.mockCommandNodeId === "string" && trace.mockCommandNodeId.length > 0 &&
251
252
  Array.isArray(trace?.mockCommandLabels) && trace.mockCommandLabels.length > 0 &&
252
253
  Array.isArray(trace?.mockCommandTexts) && trace.mockCommandTexts.length === trace.mockCommandLabels.length &&
@@ -121,11 +121,11 @@ export function createFrontendReviewScopeProtocol(input) {
121
121
  committedFacts: () => readCommittedEvents(input.getStore(), input.attemptId),
122
122
  assertComplete() { const missing = input.inventory?.scopes.filter(s => !completedScopeIds().has(s.id)) ?? []; if (missing.length)
123
123
  throw Object.assign(Error(`FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${missing.map(s => s.id).join(", ")}`), { code: "FRONTEND_REVIEW_SCOPE_INCOMPLETE" }); },
124
- customTools: !input.inventory ? [] : [{ name: "complete_review_scope", label: "complete_review_scope", description: "After reviewing the supplied complete scope and saving its findings, checkpoint its exact runtime id. This does not approve the implementation or design.", parameters: { type: "object", properties: { id: { type: "string", minLength: 1 } }, required: ["id"], additionalProperties: false },
124
+ customTools: !input.inventory ? [] : [{ name: "complete_review_scope", label: "complete_review_scope", description: "After reviewing the supplied complete scope and saving its findings, checkpoint its exact runtime id. This does not approve the implementation or design.", parameters: { type: "object", properties: { id: { type: "string", minLength: 1, enum: input.inventory.scopes.map(scope => scope.id) } }, required: ["id"], additionalProperties: false },
125
125
  async execute(callId, params) {
126
126
  const scope = input.inventory.scopes.find(s => s.id === params.id);
127
127
  if (!scope || !active.has(params.id))
128
- return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "Only the supplied active review scope may be completed" });
128
+ return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "Only the supplied active review scope may be completed; use an exact activeScopeIds entry, not the node name", activeScopeIds: [...active], completedScopeIds: [...completedScopeIds()] });
129
129
  await input.inventory.validate();
130
130
  const store = input.getStore();
131
131
  const requestId = `${input.attemptId}:${callId}:${randomUUID()}`;
@@ -462,7 +462,7 @@ export async function runFrontendVerificationTraceGate(input) {
462
462
  const mockCommandLabels = input.evidence?.mock?.commandLabels ?? [];
463
463
  const mockCommandNodeId = input.evidence?.mock?.nodeId;
464
464
  const mockCommandTexts = input.evidence?.mock?.commandTexts ?? [];
465
- if (input.evidence && contract.mockApi.strategy !== "not-needed" && mockCommandLabels.length === 0) {
465
+ if (input.evidence && contract.verificationTargets.some((target) => target.mode === "behavior") && contract.mockApi.strategy !== "not-needed" && mockCommandLabels.length === 0) {
466
466
  throw new Error("trace: selected Mock strategy requires successful Mock verification commands");
467
467
  }
468
468
  if (input.evidence) {