@tea-agent/loop-agent 0.43.0-next.11 → 0.43.0-next.13

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 (78) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/commands/client-recovery.js +20 -55
  4. package/dist/executors/shell-write-guard.js +27 -32
  5. package/dist/shared/pi-context-pressure/extension.js +26 -16
  6. package/dist/shared/pi-provider-recovery.js +182 -0
  7. package/dist/shared/pi-retry-settings.js +40 -0
  8. package/dist/worker/console/chat/pi-runtime.js +7 -5
  9. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTSK0xbU.js → abnfDiagram-N423BO3Z-BEg5712w.js} +1 -1
  10. package/dist/worker/console/static/assets/{arc-_2MfctFP.js → arc-CkQvaBGZ.js} +1 -1
  11. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DsDe2wTg.js → architectureDiagram-T3A2C74G-BrdwFNJ7.js} +1 -1
  12. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Dtz_779U.js → blockDiagram-VBNYF7ZC-M_LjP2kx.js} +1 -1
  13. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-LaULi-gw.js → c4Diagram-5PPSVZJV-Bwr5Dtjc.js} +1 -1
  14. package/dist/worker/console/static/assets/channel-CKmyKgY-.js +1 -0
  15. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DuHBqqX8.js → chunk-2GRJ4B5K-CDc44stW.js} +1 -1
  16. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BZgZs1dn.js → chunk-2Q5K7J3B-DZELwsTt.js} +1 -1
  17. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-tzwmlIO_.js → chunk-5RXB4S5H-D7YI6SWX.js} +1 -1
  18. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CeYrLkSn.js → chunk-5VM5RSS4-Bc46bEyj.js} +1 -1
  19. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C92cgilm.js → chunk-6Q2QTUOP-Bj4vc-Dp.js} +1 -1
  20. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-XOXpGC0F.js → chunk-GF5L2VYU-DRi-0nDX.js} +1 -1
  21. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CGHbJyCH.js → chunk-JWPE2WC7-Dr-wFY60.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bs2-LTzf.js → chunk-KBJHAD2P-CqIHE_D7.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-rN7up7E4.js → chunk-RYQCIY6F-C-Dl8wzk.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-B_fWjKpR.js → chunk-XXDRQBXY-CV4Mydz3.js} +1 -1
  25. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D8Wc3jJP.js +1 -0
  26. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D8Wc3jJP.js +1 -0
  27. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DE3SdJvB.js → cose-bilkent-JH36ORCC-C3MamSMJ.js} +1 -1
  28. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B-4yuRAX.js → cynefin-VYW2F7L2-zo_Jy2sf.js} +1 -1
  29. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-TiCPFuO8.js → cynefinDiagram-MW4NZA55-BB2PN1AG.js} +1 -1
  30. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-7crzLMzA.js → dagre-VZM6K2ZE-BimYueoi.js} +1 -1
  31. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Cvr-x6Fd.js → diagram-7IWD3JNH-Di1ObJRS.js} +1 -1
  32. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BzcRHUjH.js → diagram-B4RE2ZJO-C83l-IJx.js} +1 -1
  33. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BjkAXilh.js → diagram-LBJQPF4R-B3S3fOpm.js} +1 -1
  34. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BkzyATK7.js → diagram-Q27KOJAE-urW_Zhe7.js} +1 -1
  35. package/dist/worker/console/static/assets/{diagram-UB23O5K3-rZuTarYP.js → diagram-UB23O5K3-nvIbCwF6.js} +1 -1
  36. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CAYENaN1.js → ebnfDiagram-BXEA7PRR-DG6O3KpP.js} +1 -1
  37. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DCEam0Uq.js → erDiagram-JOGREHBK-DpXdtdsj.js} +1 -1
  38. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-e_dH4wVO.js → flowDiagram-UKHOOZJN-DvXWHrND.js} +1 -1
  39. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CvZa2zOK.js → ganttDiagram-PKOTCBZU-6F7SbvUS.js} +1 -1
  40. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CbT5TuKp.js → gitGraphDiagram-DS77QQ5N-D1tZnFec.js} +1 -1
  41. package/dist/worker/console/static/assets/{index-CUPeoE8W.js → index-CWarwRqc.js} +94 -94
  42. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BWzHQa_Z.js → infoDiagram-6WML65LV-HN4qwJ2M.js} +1 -1
  43. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BTjlovtU.js → ishikawaDiagram-WSZJBQD7-OJ4qwDRo.js} +1 -1
  44. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DkvQoCc1.js → journeyDiagram-NVQOT4AX-CJZ6giQk.js} +1 -1
  45. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CHSzGi3W.js → kanban-definition-27J2QSJJ-co60m19S.js} +1 -1
  46. package/dist/worker/console/static/assets/{linear-DZMkTCWH.js → linear-D1KeFqZY.js} +1 -1
  47. package/dist/worker/console/static/assets/{mermaid.core-BIMhty7A.js → mermaid.core-i7dDGqov.js} +5 -5
  48. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-Dx-tyMl3.js → mindmap-definition-FAOFIHXS-1SkQxBh1.js} +1 -1
  49. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-C6G34rbY.js → pegDiagram-VL7TDLO6-B10dhI-Y.js} +1 -1
  50. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CHK9XOfm.js → pieDiagram-7S7Q4E2Y-CiUKUo6j.js} +1 -1
  51. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CDARkI0w.js → quadrantDiagram-CIZ2JOQS-C5vH4LXZ.js} +1 -1
  52. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DiuUe0ZQ.js → railroadDiagram-AXF67PYL-5nlWOCEj.js} +1 -1
  53. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-RE61UR56.js → requirementDiagram-LRYGKXZP-CILd6QS3.js} +1 -1
  54. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-KDNLHMEG.js → sankeyDiagram-W5VNT64P-Dfp18ZO2.js} +1 -1
  55. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CeVgXnD9.js → sequenceDiagram-SI44F4Z6-B9pkswBi.js} +1 -1
  56. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CXwGyjjG.js → sizeCapture-X5ZJPWSS-C1-eSgx4.js} +1 -1
  57. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BjTfozab.js → stateDiagram-OKZ733FA-COJsOoAF.js} +1 -1
  58. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Bpfb0q5s.js +1 -0
  59. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BaXOB8GT.js → swimlanes-SLNWSIFB-Dv5NXJUM.js} +2 -2
  60. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BEwGGabW.js +8 -0
  61. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C3r2hqUU.js → timeline-definition-Z64GVDOM-DRdrZ7eR.js} +1 -1
  62. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-MWKgl1S9.js → vennDiagram-T6HMQDX7-0-2TlwCj.js} +1 -1
  63. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C9iFcZPk.js → wardleyDiagram-T6FBY63Y-3-o3q92P.js} +1 -1
  64. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DqnM24Jp.js → xychartDiagram-ELKLHX3M-DSgOgdUi.js} +1 -1
  65. package/dist/worker/console/static/index.html +1 -1
  66. package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +192 -0
  67. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +9 -1
  68. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +78 -35
  69. package/dist/worker/console/static-src/operator-chat/useChatStream.js +145 -70
  70. package/dist/worker/console/static-src/operator-chat/useComposer.js +182 -69
  71. package/dist/workflows/dag/frontend-repair.js +55 -76
  72. package/docs/architecture/runtime-boundaries.md +3 -3
  73. package/package.json +4 -1
  74. package/dist/worker/console/static/assets/channel-CqS4yBWT.js +0 -1
  75. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVOxNOq0.js +0 -1
  76. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVOxNOq0.js +0 -1
  77. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BXoYsyr2.js +0 -1
  78. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DfKliS4e.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,
@@ -24,6 +24,50 @@ export const frontendCommandCategorySchema = z.enum([
24
24
  "unit-test",
25
25
  "trace",
26
26
  ]);
27
+ /**
28
+ * Ordered string-blob rules. Order is the contract: the first matching rule wins,
29
+ * so rules that share keywords keep their original precedence. `nodeIdIncludes`
30
+ * and `blobIncludes` are alternatives within one rule (logical OR).
31
+ */
32
+ const BLOB_RULES = [
33
+ {
34
+ result: "contract",
35
+ nodeIdIncludes: ["contract"],
36
+ blobIncludes: ["contract mismatch", "source binding"],
37
+ },
38
+ { result: "trace", nodeIdIncludes: ["trace"], blobIncludes: ["trace:"] },
39
+ { result: "review", nodeIdIncludes: ["review"] },
40
+ {
41
+ result: "unit-test",
42
+ nodeIdIncludes: ["behavior"],
43
+ blobIncludes: ["vitest", "jest", "npm test"],
44
+ },
45
+ {
46
+ result: "typecheck",
47
+ blobIncludes: ["typecheck", "tsc"],
48
+ blobPatterns: [/error ts\d+/i],
49
+ },
50
+ { result: "lint", blobIncludes: ["eslint", "lint"] },
51
+ // "vite build"/"next build" are subsumed by "build".
52
+ { result: "build", blobIncludes: ["build"] },
53
+ {
54
+ result: "component-test",
55
+ blobIncludes: ["testing-library", "component", "render("],
56
+ },
57
+ {
58
+ result: "dependency",
59
+ blobIncludes: ["package.json", "unapproved dependency", "npm install"],
60
+ },
61
+ {
62
+ result: "credential",
63
+ blobIncludes: ["credential", "api key", "secret"],
64
+ },
65
+ { result: "deploy", blobIncludes: ["deploy", "production release"] },
66
+ {
67
+ result: "spec-unclear",
68
+ blobIncludes: ["spec unclear", "requirement ambiguous", "needs human"],
69
+ },
70
+ ];
27
71
  export function classifyFrontendFailure(input) {
28
72
  // AC-2: structured evidence takes priority over string-blob heuristics.
29
73
  if (input.failureCategory === "write-guard") {
@@ -39,86 +83,21 @@ export function classifyFrontendFailure(input) {
39
83
  return "trace";
40
84
  }
41
85
  if (input.commandCategory) {
42
- switch (input.commandCategory) {
43
- case "typecheck":
44
- return "typecheck";
45
- case "build":
46
- return "build";
47
- case "lint":
48
- return "lint";
49
- case "component-test":
50
- return "component-test";
51
- case "unit-test":
52
- return "unit-test";
53
- }
86
+ // "trace" already returned above, so the remaining categories map 1:1.
87
+ return input.commandCategory;
54
88
  }
55
- // Fallback: string-blob heuristic, preserved in its original branch order so
56
- // legacy assertions (error TS2304 → typecheck, write-guard → path, etc.) stay green.
89
+ // Fallback: ordered string-blob heuristics. Only the shell executor's
90
+ // structured category proves a write-boundary failure, so blob text never
91
+ // overrides the structured checks above.
57
92
  const blob = `${input.nodeId}\n${input.failureCategory ?? ""}\n${input.stdout ?? ""}\n${input.stderr ?? ""}`.toLowerCase();
58
- if (input.nodeId.includes("contract") ||
59
- blob.includes("contract mismatch") ||
60
- blob.includes("source binding")) {
61
- return "contract";
62
- }
63
- // Shell verification may contain test-runner logs from unrelated tests. Only
64
- // the shell executor's structured category proves a write-boundary failure.
65
- if (input.failureCategory === "write-guard") {
66
- return "path";
67
- }
68
- if (input.nodeId.includes("trace") || blob.includes("trace:")) {
69
- return "trace";
70
- }
71
- if (input.nodeId.includes("review")) {
72
- return "review";
73
- }
74
- // Classify the failed verification command before inspecting its output.
75
- // Full test output includes unrelated passing-test titles such as
76
- // "secret-shaped" and "write-guard failure".
77
- if (blob.includes("vitest") ||
78
- blob.includes("jest") ||
79
- blob.includes("npm test") ||
80
- input.nodeId.includes("behavior")) {
81
- return "unit-test";
82
- }
83
- if (blob.includes("typecheck") ||
84
- blob.includes("tsc") ||
85
- /error ts\d+/i.test(blob)) {
86
- return "typecheck";
87
- }
88
- if (blob.includes("eslint") || blob.includes("lint")) {
89
- return "lint";
90
- }
91
- if (blob.includes("build") ||
92
- blob.includes("vite build") ||
93
- blob.includes("next build")) {
94
- return "build";
95
- }
96
- if (blob.includes("testing-library") ||
97
- blob.includes("component") ||
98
- blob.includes("render(")) {
99
- return "component-test";
100
- }
101
- if (blob.includes("package.json") ||
102
- blob.includes("unapproved dependency") ||
103
- blob.includes("npm install")) {
104
- return "dependency";
105
- }
106
- if (blob.includes("credential") ||
107
- blob.includes("api key") ||
108
- blob.includes("secret")) {
109
- return "credential";
110
- }
111
- if (blob.includes("deploy") || blob.includes("production release")) {
112
- return "deploy";
113
- }
114
- if (blob.includes("spec unclear") ||
115
- blob.includes("requirement ambiguous") ||
116
- blob.includes("needs human")) {
117
- return "spec-unclear";
93
+ for (const rule of BLOB_RULES) {
94
+ if (rule.nodeIdIncludes?.some((keyword) => input.nodeId.includes(keyword)) ||
95
+ rule.blobIncludes?.some((keyword) => blob.includes(keyword)) ||
96
+ rule.blobPatterns?.some((pattern) => pattern.test(blob))) {
97
+ return rule.result;
98
+ }
118
99
  }
119
100
  if (input.failureCategory === "invalid-output")
120
101
  return "contract";
121
- if (input.failureCategory === "write-guard")
122
- return "path";
123
102
  return "unknown";
124
103
  }
@@ -205,15 +205,15 @@ Runtime 变更另需 `npm run typecheck` 及对应 targeted Vitest(见 exec pl
205
205
 
206
206
  | 面 | 路径 / 入口 | 边界 |
207
207
  | --- | --- | --- |
208
- | OpenCode transient-retry | `.opencode/plugins/loop-agent-transient-retry.js`(init surface `generated`) | 仅瞬态 UnknownError resume;busy/retry 门控 + backoff;overflow/401/配额/取消/业务标 permanent(`plugin-ignore-permanent-error`),**不** compact |
208
+ | OpenCode transient-retry | `.opencode/plugins/loop-agent-transient-retry.js`(init surface `generated`) | 瞬态 UnknownError / TypeValidation wrapper 及已知被裁剪的内部 provider 波动文案 resume;busy/retry 门控 + backoff;overflow/401/配额/取消/业务标 permanent(`plugin-ignore-permanent-error`),**不** compact |
209
209
  | OpenCode overflow-compact | `.opencode/plugins/loop-agent-context-overflow-compact.js`(init surface `generated`) | 仅 context overflow → `session.compact`(无则 `summarize`)+ 有限 `promptAsync` 续接;`MAX_OVERFLOW_RECOVERIES ≤ 2`;session 锁;非 overflow 不动作 |
210
210
  | Pi project extension | `.pi/extensions/loop-agent-context-overflow.js`(init surface `generated`) | `message_end` 上将 overflow `errorMessage` 幂等改写为 `context_length_exceeded:` 前缀,触发 Pi 原生 compact/retry;分类器与控制器共享 P1–P10 / N1–N5 语料(含嵌套 JSON);**不**写 `~/.pi` settings / compaction / 模型窗口,**不**投影 sift native |
211
- | Console / DAG 运行时 | npm 内置 inline extension + 可选 `@agent-context/sift` | 默认开启 L0/L1/L2;`HARNESS_CONTEXT_PRESSURE_MODE=off` L0/L2;`HARNESS_CONSOLE_SIFT` / `HARNESS_DAG_SIFT` 的 `0\|false\|off` 关 L1。DAG 不做 streaming 主动 compact。不进入目标项目 init 文件 |
211
+ | Console / DAG 运行时 | npm 内置 inline extension + 可选 `@agent-context/sift` | Console 无需 init 即启用内部 provider transient 规范化、字段级 retry process defaults 与 L0/L1/L2;项目显式 retry 字段优先于用户全局字段,再优先于推荐默认值。DAG 使用独立 in-memory retry/compaction,且不做 streaming 主动 compact。`HARNESS_CONTEXT_PRESSURE_MODE=off` 只关 L0/L2,不关闭 Console transient 恢复;`HARNESS_CONSOLE_SIFT` / `HARNESS_DAG_SIFT` 的 `0\|false\|off` 关 L1。不进入目标项目 init 文件 |
212
212
  | Pi 用户配置 | `~/.pi/agent/settings.json` | **不**进入项目 `.harness/init-surface.json` hash;仅 `--client-recovery=user` 可字段级补缺并原子写;`auto`/`project`/`off` 与 `check-update` 默认零写 home;读取时只有 `ENOENT` 视为缺文件,其他 I/O 错误 fail closed |
213
213
  | CLI mode | `--client-recovery=auto\|project\|user\|off`(默认 `auto`) | `auto`/`project` 安装上述三份项目产物;`user` = 三产物 + 显式 Pi retry 合并;`off` 全跳过 |
214
214
  | Ownership | recorded sha256 + apply-safe | 三份 generated 缺失可补、与 recorded hash 一致可升级;用户改过 → model merge / human decision,禁止静默覆盖 |
215
215
 
216
- 实现落点:`src/commands/client-recovery.ts`(共享 `isContextOverflow`、生成器与 install)+ `src/commands/init.ts`(surface 编排与 `written` 投影)。
216
+ 实现落点:`src/shared/pi-provider-recovery.ts`(内置规则、纯 `additionalRules` 扩展接口与 generated predicate)、`src/shared/pi-retry-settings.ts`(字段级 process defaults)、`src/shared/pi-context-pressure/` 与 `src/worker/console/chat/pi-runtime.ts`(Console inline normalization),以及 `src/commands/client-recovery.ts` / `src/commands/init.ts`(项目投影与 surface 编排)。
217
217
 
218
218
  ## 演进里程碑
219
219
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.43.0-next.11",
3
+ "version": "0.43.0-next.13",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -98,10 +98,13 @@
98
98
  },
99
99
  "devDependencies": {
100
100
  "@remixicon/react": "^4.9.0",
101
+ "@stryker-mutator/core": "^10.0.0",
102
+ "@stryker-mutator/vitest-runner": "^10.0.0",
101
103
  "@types/node": "^24.6.0",
102
104
  "@types/react": "^19.1.8",
103
105
  "@types/react-dom": "^19.1.6",
104
106
  "@types/semver": "^7.7.1",
107
+ "@vitest/coverage-v8": "^4.1.11",
105
108
  "react": "^19.1.0",
106
109
  "react-dom": "^19.1.0",
107
110
  "react-markdown": "^10.1.0",
@@ -1 +0,0 @@
1
- import{ag as o,ah as n}from"./mermaid.core-BIMhty7A.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-GF5L2VYU-XOXpGC0F.js";import{_ as i}from"./mermaid.core-BIMhty7A.js";import"./chunk-5VM5RSS4-CeYrLkSn.js";import"./chunk-XXDRQBXY-B_fWjKpR.js";import"./chunk-KBJHAD2P-Bs2-LTzf.js";import"./chunk-2GRJ4B5K-DuHBqqX8.js";import"./index-CUPeoE8W.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-GF5L2VYU-XOXpGC0F.js";import{_ as i}from"./mermaid.core-BIMhty7A.js";import"./chunk-5VM5RSS4-CeYrLkSn.js";import"./chunk-XXDRQBXY-B_fWjKpR.js";import"./chunk-KBJHAD2P-Bs2-LTzf.js";import"./chunk-2GRJ4B5K-DuHBqqX8.js";import"./index-CUPeoE8W.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};
@@ -1 +0,0 @@
1
- import{s as r,b as e,a,S as s}from"./chunk-5RXB4S5H-tzwmlIO_.js";import{_ as i}from"./mermaid.core-BIMhty7A.js";import"./chunk-XXDRQBXY-B_fWjKpR.js";import"./chunk-KBJHAD2P-Bs2-LTzf.js";import"./chunk-2GRJ4B5K-DuHBqqX8.js";import"./index-CUPeoE8W.js";var u={parser:a,get db(){return new s(2)},renderer:e,styles:r,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{u as diagram};
@@ -1,8 +0,0 @@
1
- import{c as r,s as e}from"./flowDiagram-UKHOOZJN-e_dH4wVO.js";import{_ as a}from"./mermaid.core-BIMhty7A.js";import"./chunk-5VM5RSS4-CeYrLkSn.js";import"./chunk-XXDRQBXY-B_fWjKpR.js";import"./chunk-KBJHAD2P-Bs2-LTzf.js";import"./chunk-2GRJ4B5K-DuHBqqX8.js";import"./channel-CqS4yBWT.js";import"./index-CUPeoE8W.js";var o=a(t=>`${e(t)}
2
- .swimlane.cluster rect {
3
- stroke: ${t.clusterBorder} !important;
4
- }
5
- [data-look="neo"].cluster rect {
6
- filter: none;
7
- }
8
- `,"getStyles"),s=o,n=r({defaultLayout:"swimlane",styles:s});export{n as diagram};