@tea-agent/loop-agent 0.43.0 → 0.44.0-next.2

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 (95) hide show
  1. package/CHANGELOG.md +226 -146
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/pi-sdk-executor.js +29 -1
  4. package/dist/shared/wizard-local-telemetry.js +50 -0
  5. package/dist/worker/console/chat/chat-event-store.js +3 -0
  6. package/dist/worker/console/chat/db-connections.js +758 -0
  7. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  8. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +627 -0
  9. package/dist/worker/console/chat/pi-runtime.js +66 -6
  10. package/dist/worker/console/chat/routes.js +154 -3
  11. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  12. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  13. package/dist/worker/console/chat/tools.js +34 -0
  14. package/dist/worker/console/chat/turn-process.js +43 -39
  15. package/dist/worker/console/dag-confirmation.js +3 -0
  16. package/dist/worker/console/dag-execution-receipt.js +3 -0
  17. package/dist/worker/console/operator-actions.js +32 -2
  18. package/dist/worker/console/server.js +5 -1
  19. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-B8EdIjdl.js} +1 -1
  20. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-CUaduV17.js} +1 -1
  21. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-bYAcxRaf.js} +1 -1
  22. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-FfhFUEMJ.js} +1 -1
  23. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-0Rqwn2_U.js} +1 -1
  24. package/dist/worker/console/static/assets/channel-ICokO4I1.js +1 -0
  25. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-D4FbDmsM.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-C2nu32ml.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-G2tHSClK.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-181nyzuH.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-D091VbiQ.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-BjrMCKTH.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-T3zFHTpb.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-CIB7wrYq.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-HwnAsgDw.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CQSdQAqo.js} +1 -1
  35. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0c0PBcA.js +1 -0
  36. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0c0PBcA.js +1 -0
  37. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-CyBVLpjP.js} +1 -1
  38. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-D2Pf3U7m.js} +1 -1
  39. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-CzXuQ1a2.js} +1 -1
  40. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-9cMmcOsQ.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-DTOr8GLs.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-B8saqFHU.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-Bvygp--w.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-CJyLwBhZ.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CkR1FDRN.js} +1 -1
  46. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-BsbNtwvi.js} +1 -1
  47. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-EVZo9AKy.js} +1 -1
  48. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-C1x91tWN.js} +1 -1
  49. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-CiNBUsrJ.js} +1 -1
  50. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-CSd50p3d.js} +1 -1
  51. package/dist/worker/console/static/assets/index-DHBWEwz_.css +1 -0
  52. package/dist/worker/console/static/assets/index-RFWQLUE-.js +478 -0
  53. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-1JDiYCuA.js} +1 -1
  54. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-BZLE6zpm.js} +1 -1
  55. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-Cg0nDmbe.js} +1 -1
  56. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-DcwEdKjq.js} +1 -1
  57. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-CXvVge5x.js} +1 -1
  58. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-5R4WTYSN.js} +5 -5
  59. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-By9pSeeq.js} +1 -1
  60. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DzbVZTqW.js} +1 -1
  61. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-B9nbfnAq.js} +1 -1
  62. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-D268jfWH.js} +1 -1
  63. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-oNOXPRNn.js} +1 -1
  64. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-DXdr8K-4.js} +1 -1
  65. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Dtm4uOJ0.js} +1 -1
  66. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-CeeFiof4.js} +1 -1
  67. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CK1gARll.js} +1 -1
  68. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-3jMoAcZw.js} +1 -1
  69. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cr_kaxrX.js +1 -0
  70. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-Div6kwoD.js} +2 -2
  71. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bl4fQmZh.js +8 -0
  72. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-B7P_6Up9.js} +1 -1
  73. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-eOPaGUOX.js} +1 -1
  74. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-tAcDnb31.js} +1 -1
  75. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-Dg_r_785.js} +1 -1
  76. package/dist/worker/console/static/index.html +2 -2
  77. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +33 -1
  78. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  79. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  80. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  81. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  82. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +15 -7
  83. package/dist/worker/console/static-src/operator-chat/useChatStream.js +47 -5
  84. package/dist/worker/console/static-src/operator-chat/useChatThread.js +24 -2
  85. package/dist/worker/console/workspace-context.js +1 -0
  86. package/dist/worker/scheduler/scheduled-goal-evidence.js +60 -2
  87. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  88. package/package.json +3 -1
  89. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  90. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  91. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  92. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  93. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  94. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  95. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { runtimeSelectionFromRecord } from "../../chat/runtime-selection.js";
3
3
  import { hydrateSessionThread } from "../../chat/turn-process.js";
4
4
  import { forgetActiveChatSession, readActiveChatSessionId, rememberActiveChatSession, } from "../../chat/workspace-landing.js";
@@ -179,10 +179,11 @@ function mergeCompactionMessages(messages, compactions) {
179
179
  * compact, plus browser-refresh recovery from the durable session store. */
180
180
  export function useChatSessions(params) {
181
181
  const { origin, refs, setSession, thread, composer, browser, setError, setNotice, onCompactionChange, setProcessOpen, setAutoScroll, } = params;
182
- const { applyActivatedSession, resetForNewSession, setMessages } = thread;
182
+ const { applyActivatedSession, resetForNewSession, setMessages, refreshSessionTelemetry, } = thread;
183
183
  const { seedSession, releaseSession } = composer;
184
184
  const { setPreviews, setActivePreviewPath } = browser;
185
185
  const { workspaceId, blocked } = params;
186
+ const visibleWorkspaceIds = useMemo(() => new Set(params.visibleWorkspaceIds), [params.visibleWorkspaceIds]);
186
187
  const workspaceQuery = workspaceId
187
188
  ? `?workspaceId=${encodeURIComponent(workspaceId)}`
188
189
  : "";
@@ -276,10 +277,13 @@ export function useChatSessions(params) {
276
277
  return;
277
278
  }
278
279
  const sessions = body.sessions ?? [];
279
- const durable = sessions.filter((item) => !pendingDeletedIdsRef.current.has(item.sessionId));
280
- const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => !durable.some((session) => session.sessionId === item.sessionId));
281
- // Preserve every workspace's summaries. ChatWorkspaceSidebar applies the
282
- // visible recent-item limit independently inside each workspace group.
280
+ const belongsToVisibleWorkspace = (item) => !item.workspaceId || visibleWorkspaceIds.has(item.workspaceId);
281
+ const durable = sessions.filter((item) => belongsToVisibleWorkspace(item) &&
282
+ !pendingDeletedIdsRef.current.has(item.sessionId));
283
+ const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => belongsToVisibleWorkspace(item) &&
284
+ !durable.some((session) => session.sessionId === item.sessionId));
285
+ // Preserve summaries for visible workspaces. ChatWorkspaceSidebar applies
286
+ // the recent-item limit independently inside each workspace group.
283
287
  const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
284
288
  sessionListRef.current = next;
285
289
  setSessionList(next);
@@ -292,7 +296,7 @@ export function useChatSessions(params) {
292
296
  return;
293
297
  setError(`会话列表加载失败:${error instanceof Error ? error.message : String(error)}`);
294
298
  }
295
- }, [origin, blocked, setError]);
299
+ }, [origin, blocked, setError, visibleWorkspaceIds]);
296
300
  const deleteSession = useCallback(async (sessionId) => {
297
301
  if (pendingDeletedIdsRef.current.has(sessionId))
298
302
  return false;
@@ -500,6 +504,7 @@ export function useChatSessions(params) {
500
504
  sessionStats: body.sessionStats,
501
505
  usage: body.usage,
502
506
  turnTelemetry: body.turnTelemetry,
507
+ sddSessionIdentity: body.sddDataAlignment ?? null,
503
508
  streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
504
509
  turnPhase: body.session.turnPhase ??
505
510
  (body.session.activeTurnId ? "running" : "idle"),
@@ -634,6 +639,7 @@ export function useChatSessions(params) {
634
639
  sessionStats: body.sessionStats,
635
640
  usage: body.usage,
636
641
  turnTelemetry: body.turnTelemetry,
642
+ sddSessionIdentity: body.sddDataAlignment ?? null,
637
643
  streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
638
644
  turnPhase: body.session.turnPhase ??
639
645
  (body.session.activeTurnId ? "running" : "idle"),
@@ -760,6 +766,7 @@ export function useChatSessions(params) {
760
766
  setSelectedSession(created);
761
767
  rememberActiveChatSession(window.localStorage, created.workspaceId ?? options?.workspaceId ?? workspaceId, created.sessionId);
762
768
  setSession(created);
769
+ void refreshSessionTelemetry();
763
770
  return created;
764
771
  }
765
772
  catch (e) {
@@ -783,6 +790,7 @@ export function useChatSessions(params) {
783
790
  activateSession,
784
791
  blocked,
785
792
  workspaceId,
793
+ refreshSessionTelemetry,
786
794
  ]);
787
795
  // Phase-1: fork / in-session branch / mainline switch UI deferred; API routes remain.
788
796
  /** Single compact helper for toolbar + `/compact <instructions>` (AC-1). */
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect } from "react";
2
2
  import { formatOperatorUserError } from "../../operator-user-error.js";
3
+ import { inspectChatSseContinuity, readChatStreamReadyCursor, selectChatSseRepairBlocks, selectLaterChatEventId, } from "./chat-stream-continuity.js";
3
4
  import { confirmationToken, requestId, sleep } from "./format.js";
4
5
  import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
5
6
  import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
@@ -104,6 +105,7 @@ export function useChatStream(params) {
104
105
  * means replay cannot render duplicate messages while live text remains
105
106
  * smooth (M4.1/M4.3). */
106
107
  const openEventsStream = useCallback(async (sessionId, generation, signal) => {
108
+ const gapReplayBuffer = [];
107
109
  while (!signal.aborted) {
108
110
  const state = refs.eventsStreamStateRef.current;
109
111
  if (!state ||
@@ -129,21 +131,38 @@ export function useChatStream(params) {
129
131
  const decoder = new TextDecoder();
130
132
  let buffer = "";
131
133
  let replay = true;
134
+ let replayRepairNeeded = false;
135
+ let liveContinuityCursor = null;
132
136
  const fallbackAssistantId = requestId("a");
133
- for (;;) {
137
+ readLoop: for (;;) {
134
138
  const { done, value } = await reader.read();
135
139
  if (done)
136
140
  break;
137
141
  buffer += decoder.decode(value, { stream: true });
138
142
  const blocks = buffer.split("\n\n");
139
143
  buffer = blocks.pop() ?? "";
140
- for (const block of blocks) {
144
+ for (let blockIndex = 0; blockIndex < blocks.length; blockIndex += 1) {
145
+ const block = blocks[blockIndex];
146
+ if (block === undefined)
147
+ continue;
148
+ if (!replay) {
149
+ const continuity = inspectChatSseContinuity(block, liveContinuityCursor);
150
+ if (continuity.action === "reconnect") {
151
+ // Buffer this block and the rest of the decoded chunk before closing
152
+ // the reader. Retained replay repairs structure; the buffer preserves
153
+ // cumulative text frames that persistence may have coalesced away.
154
+ gapReplayBuffer.push(...blocks.slice(blockIndex));
155
+ replayRepairNeeded = true;
156
+ break readLoop;
157
+ }
158
+ }
141
159
  if (sseEventName(block) === "stream_ready") {
142
160
  const current = refs.eventsStreamStateRef.current;
143
161
  if (current?.sessionId === sessionId &&
144
162
  current.generation === generation)
145
163
  current.ready = true;
146
164
  replay = false;
165
+ liveContinuityCursor = readChatStreamReadyCursor(block);
147
166
  // Fall through: the applier owns the buffered-replay flush and
148
167
  // returns immediately for this event. `continue` here silently
149
168
  // dropped that flush, so a reconnect that landed after the
@@ -155,15 +174,38 @@ export function useChatStream(params) {
155
174
  targetSessionId: sessionId,
156
175
  generation,
157
176
  });
158
- if (eventId)
159
- refs.lastEventIdRef.current = eventId;
177
+ if (eventId) {
178
+ refs.lastEventIdRef.current = selectLaterChatEventId(refs.lastEventIdRef.current, eventId);
179
+ if (!replay)
180
+ liveContinuityCursor = selectLaterChatEventId(liveContinuityCursor, eventId);
181
+ }
182
+ if (sseEventName(block) === "stream_ready" &&
183
+ gapReplayBuffer.length > 0) {
184
+ const repairedBlocks = selectChatSseRepairBlocks(gapReplayBuffer.splice(0), liveContinuityCursor);
185
+ for (const repairedBlock of repairedBlocks) {
186
+ const repairedEventId = applySseBlock(repairedBlock, refs.assistantIdRef.current ?? fallbackAssistantId, {
187
+ replay: false,
188
+ targetSessionId: sessionId,
189
+ generation,
190
+ });
191
+ if (!repairedEventId)
192
+ continue;
193
+ refs.lastEventIdRef.current = selectLaterChatEventId(refs.lastEventIdRef.current, repairedEventId);
194
+ }
195
+ }
160
196
  }
161
197
  }
198
+ if (replayRepairNeeded) {
199
+ await reader.cancel().catch(() => undefined);
200
+ }
162
201
  const current = refs.eventsStreamStateRef.current;
163
202
  if (current?.sessionId === sessionId &&
164
203
  current.generation === generation)
165
204
  current.ready = false;
166
- await sleep(1_000, signal);
205
+ // A detected hole is expected to be immediately recoverable from the
206
+ // durable ring. Other EOFs retain the normal reconnect backoff.
207
+ if (!replayRepairNeeded)
208
+ await sleep(1_000, signal);
167
209
  }
168
210
  catch {
169
211
  if (signal.aborted)
@@ -140,6 +140,8 @@ export function useChatThread(params) {
140
140
  const [usage, setUsage] = useState(EMPTY_USAGE);
141
141
  const [sessionStats, setSessionStats] = useState(EMPTY_SESSION_STATS);
142
142
  const [turnTelemetry, setTurnTelemetry] = useState([]);
143
+ const [operatorActions, setOperatorActions] = useState([]);
144
+ const [sddSessionIdentity, setSddSessionIdentity] = useState(null);
143
145
  const [streaming, setStreaming] = useState(false);
144
146
  const [turnPhase, setTurnPhase] = useState("idle");
145
147
  const [deferredTurn, setDeferredTurn] = useState(null);
@@ -167,6 +169,7 @@ export function useChatThread(params) {
167
169
  if (body.usage)
168
170
  setUsage(body.usage);
169
171
  setTurnTelemetry(body.turnTelemetry ?? []);
172
+ setSddSessionIdentity(body.sddDataAlignment ?? null);
170
173
  }
171
174
  catch {
172
175
  // Telemetry is supplementary; the normal event stream remains authoritative.
@@ -188,6 +191,8 @@ export function useChatThread(params) {
188
191
  setPendingQuestion,
189
192
  setTodos,
190
193
  setGoal,
194
+ setOperatorActions,
195
+ setSddSessionIdentity,
191
196
  setStreamingAssistantId,
192
197
  setUnread,
193
198
  setOperationCards,
@@ -421,6 +426,8 @@ export function useChatThread(params) {
421
426
  setUsage(EMPTY_USAGE);
422
427
  setSessionStats(EMPTY_SESSION_STATS);
423
428
  setTurnTelemetry([]);
429
+ setOperatorActions([]);
430
+ setSddSessionIdentity(null);
424
431
  setStreaming(false);
425
432
  setTurnPhase("idle");
426
433
  setDeferredTurn(null);
@@ -477,6 +484,8 @@ export function useChatThread(params) {
477
484
  setUsage(next.usage ?? EMPTY_USAGE);
478
485
  setSessionStats(next.sessionStats ?? EMPTY_SESSION_STATS);
479
486
  setTurnTelemetry(next.turnTelemetry ?? []);
487
+ setOperatorActions([]);
488
+ setSddSessionIdentity(next.sddSessionIdentity ?? null);
480
489
  refs.usageResponseKeysRef.current = new Set();
481
490
  setStreaming(visuallyStopped ? false : next.streaming);
482
491
  setTurnPhase(visuallyStopped
@@ -500,13 +509,15 @@ export function useChatThread(params) {
500
509
  * operation/human-gate/artifact/interview cards, which the reconnected tail
501
510
  * stream will not replay (they live in the ring, already consumed). */
502
511
  const recoverSession = useCallback(async (targetSessionId) => {
512
+ const targetGeneration = refs.sessionGenerationRef.current;
503
513
  try {
504
514
  const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}`, { credentials: "include" });
505
515
  if (!res.ok)
506
516
  return false;
507
517
  const body = (await res.json());
508
518
  if (!body.session ||
509
- refs.sessionRef.current?.sessionId !== targetSessionId)
519
+ refs.sessionRef.current?.sessionId !== targetSessionId ||
520
+ refs.sessionGenerationRef.current !== targetGeneration)
510
521
  return false;
511
522
  const activeTurnId = body.session.activeTurnId ?? null;
512
523
  const visuallyStopped = body.session.turnPhase === "stopping";
@@ -526,6 +537,7 @@ export function useChatThread(params) {
526
537
  if (body.usage)
527
538
  setUsage(body.usage);
528
539
  setTurnTelemetry(body.turnTelemetry ?? []);
540
+ setSddSessionIdentity(body.sddDataAlignment ?? null);
529
541
  // Resume the events stream from the snapshot tail so the next
530
542
  // reconnect never immediately re-expires (AC-003). The refs below
531
543
  // are reset so any follow-up tail replay re-applies cleanly.
@@ -534,7 +546,13 @@ export function useChatThread(params) {
534
546
  refs.turnAssistantIdsRef.current = new Map();
535
547
  refs.sealedAssistantIdsRef.current = new Set();
536
548
  const hydrated = hydrateSessionThread(body.session.messages ?? []);
537
- const pending = refs.pendingSubmissionRef.current;
549
+ // Recovery is addressed to targetSessionId, so an optimistic
550
+ // submission from another session must never be projected into
551
+ // this snapshot (the activation path enforces the same owner gate).
552
+ const pending = refs.pendingSubmissionRef.current?.sessionId === targetSessionId &&
553
+ refs.pendingSubmissionRef.current.sessionGeneration === targetGeneration
554
+ ? refs.pendingSubmissionRef.current
555
+ : undefined;
538
556
  const recovered = mergePendingSubmissionOnHydration(hydrated.messages, pending
539
557
  ? {
540
558
  clientRequestId: pending.clientRequestId,
@@ -599,6 +617,9 @@ export function useChatThread(params) {
599
617
  usage,
600
618
  sessionStats,
601
619
  turnTelemetry,
620
+ operatorActions,
621
+ sddSessionIdentity,
622
+ setSddSessionIdentity,
602
623
  streaming,
603
624
  setStreaming,
604
625
  turnPhase,
@@ -621,6 +642,7 @@ export function useChatThread(params) {
621
642
  applySseBlock,
622
643
  loadTaskContext,
623
644
  applyOperationLinkMutation,
645
+ refreshSessionTelemetry,
624
646
  resetForNewSession,
625
647
  applyActivatedSession,
626
648
  recoverSession,
@@ -265,6 +265,7 @@ export async function createWorkspaceContext(record, deps) {
265
265
  const aimaxAuth = new AimaxAuthReader();
266
266
  await aimaxAuth.getToken();
267
267
  const sddDataAlignment = new SddDataAlignmentService(chatStore, aimaxAuth, deps.fetchImpl);
268
+ actionContext.sddDataAlignment = sddDataAlignment;
268
269
  const chatEventEpoch = randomBytes(12).toString("hex");
269
270
  const chatEvents = createChatEventStore({
270
271
  appData,
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { execFile } from "node:child_process";
2
+ import { execFile, spawn } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
  import { lstat, readFile, readlink, realpath } from "node:fs/promises";
5
5
  import path from "node:path";
@@ -7,6 +7,59 @@ import { pathMatchesPattern } from "../../shared/git-progress.js";
7
7
  import { scheduledGoalDraftSchema } from "./scheduled-goal-types.js";
8
8
  const execFileAsync = promisify(execFile);
9
9
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
10
+ /** Working-copy paths that are plain files, so Git can read them back. */
11
+ async function plainFileNames(root, names) {
12
+ const plain = [];
13
+ for (const name of names) {
14
+ const info = await lstat(path.resolve(root, name)).catch(() => undefined);
15
+ if (info?.isFile())
16
+ plain.push(name);
17
+ }
18
+ return plain;
19
+ }
20
+ /**
21
+ * Repository-form blob IDs for working-copy files, computed by Git itself so
22
+ * that `core.autocrlf` and `eol` attributes normalize exactly as a commit
23
+ * would. Hashing never consults the index, so staged-only and
24
+ * `assume-unchanged` paths keep their raw-byte meaning. A failure degrades to
25
+ * the caller's raw-byte blob, which can only over-report change, never hide it;
26
+ * `--stdin-paths` cannot carry names containing a newline.
27
+ */
28
+ async function gitNormalizedBlobIds(root, names) {
29
+ const ids = new Map();
30
+ const candidates = names.filter((name) => !name.includes("\n"));
31
+ if (candidates.length === 0)
32
+ return ids;
33
+ let stdout;
34
+ try {
35
+ stdout = await new Promise((resolve, reject) => {
36
+ const child = spawn("git", ["-C", root, "hash-object", "--stdin-paths"], { stdio: ["pipe", "pipe", "pipe"] });
37
+ let output = "";
38
+ let failure = "";
39
+ child.stdout.setEncoding("utf8");
40
+ child.stdout.on("data", (chunk) => { output += chunk; });
41
+ child.stderr.setEncoding("utf8");
42
+ child.stderr.on("data", (chunk) => { failure += chunk; });
43
+ child.on("error", reject);
44
+ child.on("close", (code) => {
45
+ if (code === 0)
46
+ resolve(output);
47
+ else
48
+ reject(new Error(`git hash-object failed: ${failure.trim()}`));
49
+ });
50
+ child.stdin.end(`${candidates.join("\n")}\n`);
51
+ });
52
+ }
53
+ catch {
54
+ return ids;
55
+ }
56
+ const hashes = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
57
+ if (hashes.length !== candidates.length)
58
+ return ids;
59
+ for (let index = 0; index < candidates.length; index += 1)
60
+ ids.set(candidates[index], hashes[index]);
61
+ return ids;
62
+ }
10
63
  /** Run only frozen AC commands. A timeout is uncertain execution, never a passing or repairable failure. */
11
64
  export async function evaluateScheduledGoalCriteria(frozen, options = {}) {
12
65
  frozen = scheduledGoalDraftSchema.pick({ worktreeRef: true, frozenBase: true, allowedPaths: true, forbiddenPaths: true, acceptance: true, time: true }).strip().parse(frozen);
@@ -115,6 +168,11 @@ export async function captureScheduledGoalTree(worktree, base, allowedPaths, for
115
168
  const blobId = (bytes) => createHash(objectFormat).update(`blob ${bytes.length}\0`).update(bytes).digest("hex");
116
169
  const names = [...new Set([...entries(tracked), ...entries(untracked), ...baseEntries.keys()])].sort();
117
170
  const deliveryNames = new Set(names);
171
+ // Compare content identity through Git's own normalization: a working copy
172
+ // materialized with `core.autocrlf` (or an `eol` attribute) is stored as LF
173
+ // while the checkout is CRLF, and raw bytes would report that as drift on
174
+ // every text file.
175
+ const normalizedIds = await gitNormalizedBlobIds(root, await plainFileNames(root, names));
118
176
  const files = [];
119
177
  const changedPaths = [];
120
178
  for (const name of names) {
@@ -144,7 +202,7 @@ export async function captureScheduledGoalTree(worktree, base, allowedPaths, for
144
202
  else if (stat.isFile()) {
145
203
  const bytes = await readFile(target);
146
204
  files.push({ name, kind: "file", hash: sha256(bytes), executable: Boolean(stat.mode & 0o111) });
147
- actualId = blobId(bytes);
205
+ actualId = normalizedIds.get(name) ?? blobId(bytes);
148
206
  actualMode = stat.mode & 0o111 ? "100755" : "100644";
149
207
  }
150
208
  else
@@ -16,6 +16,16 @@ async function syncDirectory(directoryPath) {
16
16
  try {
17
17
  await directory.sync();
18
18
  }
19
+ catch (error) {
20
+ // The file body is already durable from the file-level fsync above.
21
+ // Windows refuses to flush a directory handle (EPERM) and some
22
+ // filesystems reject the call outright, so a directory-only failure must
23
+ // not abort a transaction whose data is already on disk.
24
+ const code = error.code;
25
+ if (code !== "EPERM" && code !== "EACCES" && code !== "EINVAL"
26
+ && code !== "ENOTSUP" && code !== "EOPNOTSUPP" && code !== "ENOSYS")
27
+ throw error;
28
+ }
19
29
  finally {
20
30
  await directory.close();
21
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.43.0",
3
+ "version": "0.44.0-next.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -84,7 +84,9 @@
84
84
  "commander": "^12.1.0",
85
85
  "katex": "^0.16.47",
86
86
  "mermaid": "^11.16.1",
87
+ "mysql2": "^3.24.4",
87
88
  "node-pty": "^1.2.0-beta.15",
89
+ "pg": "^8.23.0",
88
90
  "playwright-core": "^1.63.0",
89
91
  "rehype-highlight": "^7.0.2",
90
92
  "rehype-katex": "^7.0.1",
@@ -1 +0,0 @@
1
- import{ag as o,ah as n}from"./mermaid.core-7pKqYtpZ.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-CxcKZ9mV.js";import{_ as i}from"./mermaid.core-7pKqYtpZ.js";import"./chunk-5VM5RSS4-XKxoJNJ4.js";import"./chunk-XXDRQBXY-DC_Ds11b.js";import"./chunk-KBJHAD2P-DebTtdFp.js";import"./chunk-2GRJ4B5K-YBHmhik1.js";import"./index-Cdkvw_H6.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-CxcKZ9mV.js";import{_ as i}from"./mermaid.core-7pKqYtpZ.js";import"./chunk-5VM5RSS4-XKxoJNJ4.js";import"./chunk-XXDRQBXY-DC_Ds11b.js";import"./chunk-KBJHAD2P-DebTtdFp.js";import"./chunk-2GRJ4B5K-YBHmhik1.js";import"./index-Cdkvw_H6.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};