@tea-agent/loop-agent 0.44.0-next.6 → 0.44.0-next.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +285 -454
- package/dist/build-stamp.json +3 -3
- package/dist/shared/git-workspace-status.js +2 -0
- package/dist/worker/console/chat/assistant-content.js +5 -34
- package/dist/worker/console/chat/chat-event-store.js +27 -10
- package/dist/worker/console/chat/db-connections.js +72 -8
- package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +52 -24
- package/dist/worker/console/chat/pi-runtime.js +34 -0
- package/dist/worker/console/chat/routes.js +138 -9
- package/dist/worker/console/chat/session-store.js +147 -19
- package/dist/worker/console/chat/turn-process.js +69 -23
- package/dist/worker/console/chat/user-turn-outline.js +19 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Brn2sPzL.js → abnfDiagram-N423BO3Z-DTuku_-7.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BIMNTG0W.js → arc-CQbCsSqb.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G--WtcgR_A.js → architectureDiagram-T3A2C74G-C-xUntTR.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BT_Ztog1.js → blockDiagram-VBNYF7ZC-DhNlwGAl.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5hbVr-l.js → c4Diagram-5PPSVZJV-B7j3nx8o.js} +1 -1
- package/dist/worker/console/static/assets/channel-5_os7t_x.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BKKnyR9Z.js → chunk-2GRJ4B5K-BCrcxEh2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BHs6biqA.js → chunk-2Q5K7J3B-CsaePDb3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BdSFy_vP.js → chunk-5RXB4S5H--SRQc1rN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BpJIdTKV.js → chunk-5VM5RSS4-C5BFY2Po.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C_iWJhkT.js → chunk-6Q2QTUOP-Caso6uN7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-BTbmx3-k.js → chunk-GF5L2VYU-AY2ZSAx2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D4Ee6DmP.js → chunk-JWPE2WC7-DiMqZQ_8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B487SORR.js → chunk-KBJHAD2P-gpvvriOp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BKYHjNrr.js → chunk-RYQCIY6F-CUdyoJhI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DrTdoYZG.js → chunk-XXDRQBXY-Bp1xW3T4.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CttUY_Jo.js → cose-bilkent-JH36ORCC-D-JIcOIT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B4Rm5gnR.js → cynefin-VYW2F7L2-C-NKFflx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-ByxHBq_S.js → cynefinDiagram-MW4NZA55-CSOcvOAA.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DGrfNFA0.js → dagre-VZM6K2ZE-BQSBltMy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-KjrUPquv.js → diagram-7IWD3JNH-B0LVC45G.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-VS4mlm75.js → diagram-B4RE2ZJO-Cypg3nqy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BLXYmFTM.js → diagram-LBJQPF4R-BBDU88XJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C6xoFAqH.js → diagram-Q27KOJAE-BZusLqSb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-C478dCe7.js → diagram-UB23O5K3-Cfolotcr.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DHEpW5zN.js → ebnfDiagram-BXEA7PRR-CjfyUy0m.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CA5GKfo9.js → erDiagram-JOGREHBK-DJdz7txE.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-zysi0Fl-.js → flowDiagram-UKHOOZJN-CQqgPO6y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DGzcKA8Y.js → ganttDiagram-PKOTCBZU-BOpGfS2m.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CHsuIG1w.js → gitGraphDiagram-DS77QQ5N-C6UCu0qx.js} +1 -1
- package/dist/worker/console/static/assets/index-CUT440A8.css +1 -0
- package/dist/worker/console/static/assets/index-DWKejNVJ.js +486 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BexZHPCp.js → infoDiagram-6WML65LV-BWeaVEUB.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Gkx8q-DB.js → ishikawaDiagram-WSZJBQD7-C8gmlBbk.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-XDW3qrJ7.js → journeyDiagram-NVQOT4AX-DF3bm8Bp.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DJ07eEaN.js → kanban-definition-27J2QSJJ-D2fG2Vxi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DL-zeqOn.js → linear-BGKgMc2T.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-C0HdXtOf.js → mermaid.core-DF6mMPJ9.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CqRX0lrt.js → mindmap-definition-FAOFIHXS-AMhePheC.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVM9JZ2H.js → pegDiagram-VL7TDLO6-DN1Cl0W0.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BA3ozdKk.js → pieDiagram-7S7Q4E2Y-Cx9ngUeN.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B-0_p9_4.js → quadrantDiagram-CIZ2JOQS-BcYPTQJb.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DHRzPWFc.js → railroadDiagram-AXF67PYL-BqHUZWnH.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-9BEC_PcW.js → requirementDiagram-LRYGKXZP-CmqWZ0-B.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DMxeW4rN.js → sankeyDiagram-W5VNT64P--4YetH1y.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DixP6LIU.js → sequenceDiagram-SI44F4Z6-DGD4VMfD.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B_EifJBo.js → sizeCapture-X5ZJPWSS-QVw9q7rh.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BwikwWbX.js → stateDiagram-OKZ733FA-CuemV9pe.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C162l9o1.js → swimlanes-SLNWSIFB-C7oKzZKs.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-mpe_u1i4.js → timeline-definition-Z64GVDOM-C2Of60nT.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DyUk54HX.js → vennDiagram-T6HMQDX7-DHtpQHyL.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-HYyMkzfm.js → wardleyDiagram-T6FBY63Y-COi9OhSy.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DRoh7skR.js → xychartDiagram-ELKLHX3M-DPHQfRrt.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +58 -3
- package/dist/worker/console/static-src/operator-chat/process-label.js +19 -6
- package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -0
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
- package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +202 -43
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -23
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +55 -18
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +5 -2
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
- package/dist/worker/scheduler/scheduled-goal-evidence.js +6 -1
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-DNWB4hpR.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-COhFgJji.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-COhFgJji.js +0 -1
- package/dist/worker/console/static/assets/index-CJh6pVLm.js +0 -482
- package/dist/worker/console/static/assets/index-C_kvKmFM.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-S2ZHMo1c.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BZm4ByfV.js +0 -8
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export const TURN_STATE_RECONCILE_DELAYS_MS = [0, 75, 200, 500];
|
|
2
|
+
export const TURN_STATE_QUEUE_RETRY_DELAYS_MS = [0, 75, 200, 500, 2_000];
|
|
3
|
+
const MAX_TURN_STATE_QUERY_BATCH = 8;
|
|
4
|
+
/** Normalize server turn facts. Queued is externally still an open/running Turn. */
|
|
5
|
+
export function normalizeDurableTurnState(state) {
|
|
6
|
+
if (state === "queued" || state === "running")
|
|
7
|
+
return "running";
|
|
8
|
+
if (state === "settled" || state === "aborted" || state === "failed") {
|
|
9
|
+
return state;
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Merge authoritative summaries without deleting older paginated Turn facts. */
|
|
14
|
+
export function mergeTurnStateSummaries(current, summaries) {
|
|
15
|
+
if (!summaries?.length)
|
|
16
|
+
return current;
|
|
17
|
+
let next;
|
|
18
|
+
for (const summary of summaries) {
|
|
19
|
+
if (typeof summary.turnId !== "string" || !summary.turnId)
|
|
20
|
+
continue;
|
|
21
|
+
const state = normalizeDurableTurnState(summary.state);
|
|
22
|
+
const previous = next?.[summary.turnId] ?? current[summary.turnId];
|
|
23
|
+
const merged = mergeDurableTurnState(previous, state);
|
|
24
|
+
if (!merged || previous === merged)
|
|
25
|
+
continue;
|
|
26
|
+
next ??= { ...current };
|
|
27
|
+
next[summary.turnId] = merged;
|
|
28
|
+
}
|
|
29
|
+
return next ?? current;
|
|
30
|
+
}
|
|
31
|
+
export function setProjectedTurnState(current, turnId, state) {
|
|
32
|
+
if (!turnId)
|
|
33
|
+
return current;
|
|
34
|
+
const normalized = normalizeDurableTurnState(state);
|
|
35
|
+
const merged = mergeDurableTurnState(current[turnId], normalized);
|
|
36
|
+
if (!merged || current[turnId] === merged) {
|
|
37
|
+
return current;
|
|
38
|
+
}
|
|
39
|
+
return { ...current, [turnId]: merged };
|
|
40
|
+
}
|
|
41
|
+
function mergeDurableTurnState(current, incoming) {
|
|
42
|
+
if (!incoming)
|
|
43
|
+
return current;
|
|
44
|
+
if (!current)
|
|
45
|
+
return incoming;
|
|
46
|
+
if (current === "failed" || current === "aborted")
|
|
47
|
+
return current;
|
|
48
|
+
if (current === "settled" && incoming === "running")
|
|
49
|
+
return current;
|
|
50
|
+
return incoming;
|
|
51
|
+
}
|
|
52
|
+
export function turnStateReconcileTrigger(replay, eventName) {
|
|
53
|
+
if (eventName !== "agent_settled")
|
|
54
|
+
return undefined;
|
|
55
|
+
return replay ? "after-replay" : "immediate";
|
|
56
|
+
}
|
|
57
|
+
export async function reconcileTurnStateWithBackoff(input) {
|
|
58
|
+
const delays = input.delaysMs ?? TURN_STATE_RECONCILE_DELAYS_MS;
|
|
59
|
+
const wait = input.wait ??
|
|
60
|
+
((delayMs) => new Promise((resolve) => globalThis.setTimeout(resolve, delayMs)));
|
|
61
|
+
for (const delayMs of delays) {
|
|
62
|
+
if (delayMs > 0)
|
|
63
|
+
await wait(delayMs);
|
|
64
|
+
if (input.isCancelled?.())
|
|
65
|
+
return "cancelled";
|
|
66
|
+
let snapshot = null;
|
|
67
|
+
try {
|
|
68
|
+
snapshot = await input.read();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// A transport failure consumes one bounded attempt; later attempts or an
|
|
72
|
+
// SSE reconnect retain ownership of the pending reconciliation.
|
|
73
|
+
}
|
|
74
|
+
if (input.isCancelled?.())
|
|
75
|
+
return "cancelled";
|
|
76
|
+
if (!snapshot?.turnStates)
|
|
77
|
+
continue;
|
|
78
|
+
input.apply(snapshot.turnStates);
|
|
79
|
+
const state = snapshot.turnStates
|
|
80
|
+
.filter((summary) => summary.turnId === input.turnId)
|
|
81
|
+
.map((summary) => normalizeDurableTurnState(summary.state))
|
|
82
|
+
.find((candidate) => candidate !== undefined);
|
|
83
|
+
if (state && state !== "running")
|
|
84
|
+
return "terminal";
|
|
85
|
+
}
|
|
86
|
+
return "pending";
|
|
87
|
+
}
|
|
88
|
+
/** Serialize targeted Turn-state reads, batching replay work and retaining
|
|
89
|
+
* unresolved Turns until durable terminal truth arrives or the owner ends. */
|
|
90
|
+
export function createTurnStateReconcileQueue(input) {
|
|
91
|
+
const pending = new Set();
|
|
92
|
+
const wait = input.wait ??
|
|
93
|
+
((delayMs) => new Promise((resolve) => globalThis.setTimeout(resolve, delayMs)));
|
|
94
|
+
const retryDelays = input.retryDelaysMs?.length
|
|
95
|
+
? input.retryDelaysMs
|
|
96
|
+
: TURN_STATE_QUEUE_RETRY_DELAYS_MS;
|
|
97
|
+
let worker = null;
|
|
98
|
+
const run = async () => {
|
|
99
|
+
let retryIndex = 0;
|
|
100
|
+
while (pending.size > 0 && !input.isCancelled?.()) {
|
|
101
|
+
const delayMs = retryDelays[Math.min(retryIndex, retryDelays.length - 1)] ?? 0;
|
|
102
|
+
if (delayMs > 0) {
|
|
103
|
+
try {
|
|
104
|
+
await wait(delayMs);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
if (input.isCancelled?.())
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (input.isCancelled?.())
|
|
112
|
+
return;
|
|
113
|
+
const batch = [...pending].slice(0, MAX_TURN_STATE_QUERY_BATCH);
|
|
114
|
+
let snapshot = null;
|
|
115
|
+
try {
|
|
116
|
+
snapshot = await input.read(batch);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// The queue owns retry cadence; a transient read failure keeps the batch pending.
|
|
120
|
+
}
|
|
121
|
+
if (input.isCancelled?.())
|
|
122
|
+
return;
|
|
123
|
+
const summaries = snapshot?.turnStates;
|
|
124
|
+
if (summaries?.length)
|
|
125
|
+
input.apply(summaries);
|
|
126
|
+
let completed = 0;
|
|
127
|
+
for (const turnId of batch) {
|
|
128
|
+
const state = summaries
|
|
129
|
+
?.filter((summary) => summary.turnId === turnId)
|
|
130
|
+
.map((summary) => normalizeDurableTurnState(summary.state))
|
|
131
|
+
.find((candidate) => candidate !== undefined);
|
|
132
|
+
if (state && state !== "running") {
|
|
133
|
+
pending.delete(turnId);
|
|
134
|
+
completed += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Rotate unresolved ids so batches larger than the server query cap cannot starve.
|
|
138
|
+
pending.delete(turnId);
|
|
139
|
+
pending.add(turnId);
|
|
140
|
+
}
|
|
141
|
+
retryIndex = completed > 0 ? 0 : retryIndex + 1;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const start = () => {
|
|
145
|
+
if (worker)
|
|
146
|
+
return worker;
|
|
147
|
+
if (pending.size === 0 || input.isCancelled?.())
|
|
148
|
+
return Promise.resolve();
|
|
149
|
+
worker = run().finally(() => {
|
|
150
|
+
worker = null;
|
|
151
|
+
if (pending.size > 0 && !input.isCancelled?.())
|
|
152
|
+
void start();
|
|
153
|
+
});
|
|
154
|
+
return worker;
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
enqueue: (turnId, options) => {
|
|
158
|
+
if (!turnId || input.isCancelled?.())
|
|
159
|
+
return;
|
|
160
|
+
pending.add(turnId);
|
|
161
|
+
if (!options?.defer)
|
|
162
|
+
void start();
|
|
163
|
+
},
|
|
164
|
+
start,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -19,11 +19,65 @@ export const OPERATOR_CHAT_REAL_PATHS = {
|
|
|
19
19
|
events: "/api/operator/v1/chat/sessions/:sessionId/events",
|
|
20
20
|
abort: "/api/operator/v1/chat/sessions/:sessionId/turns/:turnId/abort",
|
|
21
21
|
};
|
|
22
|
+
const MAX_STATE_TURN_IDS = 8;
|
|
23
|
+
const MAX_STATE_TURN_ID_CHARS = 240;
|
|
24
|
+
function boundedStateTurnIds(turnIds) {
|
|
25
|
+
const result = [];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
for (const raw of turnIds ?? []) {
|
|
28
|
+
const turnId = raw.trim();
|
|
29
|
+
if (!turnId ||
|
|
30
|
+
turnId.length > MAX_STATE_TURN_ID_CHARS ||
|
|
31
|
+
seen.has(turnId)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
seen.add(turnId);
|
|
35
|
+
result.push(turnId);
|
|
36
|
+
if (result.length >= MAX_STATE_TURN_IDS)
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
22
41
|
export function operatorChatTurnsUrl(origin, sessionId) {
|
|
23
42
|
return `${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/turns`;
|
|
24
43
|
}
|
|
25
|
-
export function operatorChatStateUrl(origin, sessionId) {
|
|
26
|
-
|
|
44
|
+
export function operatorChatStateUrl(origin, sessionId, turnIds) {
|
|
45
|
+
const base = `${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/state`;
|
|
46
|
+
const bounded = boundedStateTurnIds(turnIds);
|
|
47
|
+
if (bounded.length === 0)
|
|
48
|
+
return base;
|
|
49
|
+
const query = new URLSearchParams();
|
|
50
|
+
for (const turnId of bounded)
|
|
51
|
+
query.append("turnId", turnId);
|
|
52
|
+
return `${base}?${query.toString()}`;
|
|
53
|
+
}
|
|
54
|
+
/** Compose session ownership with a short request timeout and explicit cleanup. */
|
|
55
|
+
export function createBoundedOwnerSignal(input) {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const abortFromOwner = () => controller.abort(input.ownerSignal?.reason);
|
|
58
|
+
if (input.ownerSignal?.aborted)
|
|
59
|
+
abortFromOwner();
|
|
60
|
+
else
|
|
61
|
+
input.ownerSignal?.addEventListener("abort", abortFromOwner, { once: true });
|
|
62
|
+
const schedule = input.schedule ??
|
|
63
|
+
((callback, delayMs) => globalThis.setTimeout(callback, delayMs));
|
|
64
|
+
const cancel = input.cancel ??
|
|
65
|
+
((handle) => globalThis.clearTimeout(handle));
|
|
66
|
+
const timer = controller.signal.aborted
|
|
67
|
+
? undefined
|
|
68
|
+
: schedule(() => controller.abort(new Error("Operator Chat state request timed out")), input.timeoutMs);
|
|
69
|
+
let disposed = false;
|
|
70
|
+
return {
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
dispose: () => {
|
|
73
|
+
if (disposed)
|
|
74
|
+
return;
|
|
75
|
+
disposed = true;
|
|
76
|
+
if (timer !== undefined)
|
|
77
|
+
cancel(timer);
|
|
78
|
+
input.ownerSignal?.removeEventListener("abort", abortFromOwner);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
27
81
|
}
|
|
28
82
|
export function operatorChatEventsUrl(origin, sessionId) {
|
|
29
83
|
return `${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/events`;
|
|
@@ -8,6 +8,47 @@ import { isLostChatSessionError } from "./runtime-snapshot-store.js";
|
|
|
8
8
|
import { upsertCompactionMessage, } from "./compaction-message.js";
|
|
9
9
|
import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
|
|
10
10
|
import { SessionTitleWatcher } from "./session-title-watcher.js";
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function isSessionListItem(value) {
|
|
15
|
+
if (!isRecord(value))
|
|
16
|
+
return false;
|
|
17
|
+
const optionalString = (field) => field === undefined || typeof field === "string";
|
|
18
|
+
return (typeof value.sessionId === "string" &&
|
|
19
|
+
value.sessionId.trim().length > 0 &&
|
|
20
|
+
(value.state === "active" || value.state === "archived") &&
|
|
21
|
+
typeof value.updatedAt === "string" &&
|
|
22
|
+
optionalString(value.workspaceId) &&
|
|
23
|
+
optionalString(value.title) &&
|
|
24
|
+
optionalString(value.displayTitle) &&
|
|
25
|
+
optionalString(value.activeTurnId) &&
|
|
26
|
+
(value.isBlank === undefined || typeof value.isBlank === "boolean") &&
|
|
27
|
+
(value.sessionMode === undefined ||
|
|
28
|
+
value.sessionMode === "workflow" ||
|
|
29
|
+
value.sessionMode === "pi") &&
|
|
30
|
+
(value.turnPhase === undefined ||
|
|
31
|
+
value.turnPhase === "idle" ||
|
|
32
|
+
value.turnPhase === "running" ||
|
|
33
|
+
value.turnPhase === "stopping"));
|
|
34
|
+
}
|
|
35
|
+
/** A 2xx response is authoritative only after its versioned payload validates. */
|
|
36
|
+
export function parseSessionListBaseline(value) {
|
|
37
|
+
if (!isRecord(value) || value.schemaVersion !== 2) {
|
|
38
|
+
throw new Error("会话列表响应版本无效");
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(value.sessions) || !value.sessions.every(isSessionListItem)) {
|
|
41
|
+
throw new Error("会话列表响应缺少有效的 sessions");
|
|
42
|
+
}
|
|
43
|
+
const workspaceErrors = value.workspaceErrors ?? [];
|
|
44
|
+
if (!Array.isArray(workspaceErrors) || !workspaceErrors.every(isRecord)) {
|
|
45
|
+
throw new Error("会话列表响应包含无效的 workspaceErrors");
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
sessions: value.sessions,
|
|
49
|
+
workspaceErrors: workspaceErrors,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
11
52
|
/** Pure list mutations keep optimistic UI deterministic and easy to verify. */
|
|
12
53
|
export function insertOptimisticSession(list, row) {
|
|
13
54
|
return [row, ...list.filter((item) => item.sessionId !== row.sessionId)];
|
|
@@ -199,11 +240,19 @@ export function useChatSessions(params) {
|
|
|
199
240
|
return owner ? `?workspaceId=${encodeURIComponent(owner)}` : "";
|
|
200
241
|
}, [refs]);
|
|
201
242
|
const [sessionList, setSessionList] = useState([]);
|
|
243
|
+
const [sessionListStatus, setSessionListStatus] = useState({
|
|
244
|
+
phase: "pending",
|
|
245
|
+
state: "idle",
|
|
246
|
+
error: null,
|
|
247
|
+
});
|
|
202
248
|
const [selectedSession, setSelectedSession] = useState(null);
|
|
203
249
|
const selectedSessionRef = useRef(null);
|
|
204
250
|
selectedSessionRef.current = selectedSession;
|
|
205
251
|
const sessionListRef = useRef([]);
|
|
206
252
|
const sessionListRequestVersionRef = useRef(0);
|
|
253
|
+
const sessionListRefreshRef = useRef(null);
|
|
254
|
+
const sessionListRefreshQueuedRef = useRef(false);
|
|
255
|
+
const sessionListGlobalErrorRef = useRef(null);
|
|
207
256
|
const pendingDeletedIdsRef = useRef(new Set());
|
|
208
257
|
const pendingCreatedRowsRef = useRef(new Map());
|
|
209
258
|
const sessionMutationVersionRef = useRef(0);
|
|
@@ -257,48 +306,101 @@ export function useChatSessions(params) {
|
|
|
257
306
|
setProcessOpen,
|
|
258
307
|
setAutoScroll,
|
|
259
308
|
]);
|
|
260
|
-
const refreshSessionList = useCallback(
|
|
309
|
+
const refreshSessionList = useCallback(() => {
|
|
261
310
|
if (blocked)
|
|
262
|
-
return; // AC-R5-002: fail-closed, zero
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/sessions`, {
|
|
270
|
-
credentials: "include",
|
|
271
|
-
});
|
|
272
|
-
const body = (await res.json().catch(() => ({})));
|
|
273
|
-
if (version !== sessionListRequestVersionRef.current)
|
|
274
|
-
return;
|
|
275
|
-
if (!res.ok) {
|
|
276
|
-
setError(body.error?.message ?? `会话列表加载失败(HTTP ${res.status})`);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
const sessions = body.sessions ?? [];
|
|
280
|
-
const belongsToVisibleWorkspace = (item) => params.workspacesLoading ||
|
|
281
|
-
!item.workspaceId ||
|
|
282
|
-
visibleWorkspaceIds.has(item.workspaceId);
|
|
283
|
-
const durable = sessions.filter((item) => belongsToVisibleWorkspace(item) &&
|
|
284
|
-
!pendingDeletedIdsRef.current.has(item.sessionId));
|
|
285
|
-
const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => belongsToVisibleWorkspace(item) &&
|
|
286
|
-
!durable.some((session) => session.sessionId === item.sessionId));
|
|
287
|
-
// Preserve summaries for visible workspaces. ChatWorkspaceSidebar applies
|
|
288
|
-
// the recent-item limit independently inside each workspace group.
|
|
289
|
-
const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
|
|
290
|
-
sessionListRef.current = next;
|
|
291
|
-
setSessionList(next);
|
|
292
|
-
if (body.workspaceErrors?.length) {
|
|
293
|
-
setError(`部分工作区的会话加载失败(${body.workspaceErrors.length} 个)`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
catch (error) {
|
|
297
|
-
if (version !== sessionListRequestVersionRef.current)
|
|
298
|
-
return;
|
|
299
|
-
setError(`会话列表加载失败:${error instanceof Error ? error.message : String(error)}`);
|
|
311
|
+
return Promise.resolve(); // AC-R5-002: fail-closed, zero requests
|
|
312
|
+
if (sessionListRefreshRef.current) {
|
|
313
|
+
// The newest refresh may carry a different visible-workspace projection.
|
|
314
|
+
// Do not briefly commit the older scope before the queued baseline runs.
|
|
315
|
+
sessionListRequestVersionRef.current += 1;
|
|
316
|
+
sessionListRefreshQueuedRef.current = true;
|
|
317
|
+
return sessionListRefreshRef.current;
|
|
300
318
|
}
|
|
319
|
+
const request = (async () => {
|
|
320
|
+
do {
|
|
321
|
+
sessionListRefreshQueuedRef.current = false;
|
|
322
|
+
const version = ++sessionListRequestVersionRef.current;
|
|
323
|
+
setSessionListStatus((current) => ({
|
|
324
|
+
...current,
|
|
325
|
+
state: "loading",
|
|
326
|
+
error: null,
|
|
327
|
+
}));
|
|
328
|
+
try {
|
|
329
|
+
// The sidebar is one unscoped multi-workspace index. Session detail
|
|
330
|
+
// and mutations remain owner-scoped.
|
|
331
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions`, {
|
|
332
|
+
credentials: "include",
|
|
333
|
+
});
|
|
334
|
+
let rawBody;
|
|
335
|
+
try {
|
|
336
|
+
rawBody = await res.json();
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
throw new Error("会话列表响应不是有效 JSON");
|
|
340
|
+
}
|
|
341
|
+
if (version !== sessionListRequestVersionRef.current) {
|
|
342
|
+
sessionListRefreshQueuedRef.current = true;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
const message = isRecord(rawBody) &&
|
|
347
|
+
isRecord(rawBody.error) &&
|
|
348
|
+
typeof rawBody.error.message === "string"
|
|
349
|
+
? rawBody.error.message
|
|
350
|
+
: `会话列表加载失败(HTTP ${res.status})`;
|
|
351
|
+
throw new Error(message);
|
|
352
|
+
}
|
|
353
|
+
const baseline = parseSessionListBaseline(rawBody);
|
|
354
|
+
if (baseline.workspaceErrors.length > 0) {
|
|
355
|
+
throw new Error(`部分工作区的会话加载失败(${baseline.workspaceErrors.length} 个)`);
|
|
356
|
+
}
|
|
357
|
+
const belongsToVisibleWorkspace = (item) => params.workspacesLoading ||
|
|
358
|
+
!item.workspaceId ||
|
|
359
|
+
visibleWorkspaceIds.has(item.workspaceId);
|
|
360
|
+
const durable = baseline.sessions.filter((item) => belongsToVisibleWorkspace(item) &&
|
|
361
|
+
!pendingDeletedIdsRef.current.has(item.sessionId));
|
|
362
|
+
const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => belongsToVisibleWorkspace(item) &&
|
|
363
|
+
!durable.some((session) => session.sessionId === item.sessionId));
|
|
364
|
+
const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
|
|
365
|
+
sessionListRef.current = next;
|
|
366
|
+
setSessionList(next);
|
|
367
|
+
setSessionListStatus({ phase: "ready", state: "idle", error: null });
|
|
368
|
+
const previousListError = sessionListGlobalErrorRef.current;
|
|
369
|
+
if (previousListError) {
|
|
370
|
+
setError((current) => current === previousListError ? null : current);
|
|
371
|
+
sessionListGlobalErrorRef.current = null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (version !== sessionListRequestVersionRef.current) {
|
|
376
|
+
sessionListRefreshQueuedRef.current = true;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const message = `会话列表加载失败:${error instanceof Error ? error.message : String(error)}`;
|
|
380
|
+
setSessionListStatus((current) => ({
|
|
381
|
+
...current,
|
|
382
|
+
state: "error",
|
|
383
|
+
error: message,
|
|
384
|
+
}));
|
|
385
|
+
sessionListGlobalErrorRef.current = message;
|
|
386
|
+
setError(message);
|
|
387
|
+
}
|
|
388
|
+
} while (sessionListRefreshQueuedRef.current && !blocked);
|
|
389
|
+
})();
|
|
390
|
+
sessionListRefreshRef.current = request;
|
|
391
|
+
void request.finally(() => {
|
|
392
|
+
if (sessionListRefreshRef.current === request) {
|
|
393
|
+
sessionListRefreshRef.current = null;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
return request;
|
|
301
397
|
}, [origin, blocked, setError, visibleWorkspaceIds, params.workspacesLoading]);
|
|
398
|
+
const invalidateSessionListBaseline = useCallback(() => {
|
|
399
|
+
sessionListRequestVersionRef.current += 1;
|
|
400
|
+
if (sessionListRefreshRef.current) {
|
|
401
|
+
sessionListRefreshQueuedRef.current = true;
|
|
402
|
+
}
|
|
403
|
+
}, []);
|
|
302
404
|
const deleteSession = useCallback(async (sessionId) => {
|
|
303
405
|
if (pendingDeletedIdsRef.current.has(sessionId))
|
|
304
406
|
return false;
|
|
@@ -308,7 +410,7 @@ export function useChatSessions(params) {
|
|
|
308
410
|
if (!removed || removedIndex < 0)
|
|
309
411
|
return false;
|
|
310
412
|
// No pre-mutation snapshot may overwrite this deletion or its rollback.
|
|
311
|
-
|
|
413
|
+
invalidateSessionListBaseline();
|
|
312
414
|
pendingDeletedIdsRef.current.add(sessionId);
|
|
313
415
|
deleteRequestVersionsRef.current.set(sessionId, requestId);
|
|
314
416
|
const activeSession = refs.sessionRef.current?.sessionId === sessionId
|
|
@@ -341,7 +443,7 @@ export function useChatSessions(params) {
|
|
|
341
443
|
const restore = () => {
|
|
342
444
|
if (deleteRequestVersionsRef.current.get(sessionId) !== requestId)
|
|
343
445
|
return;
|
|
344
|
-
|
|
446
|
+
invalidateSessionListBaseline();
|
|
345
447
|
pendingDeletedIdsRef.current.delete(sessionId);
|
|
346
448
|
deleteRequestVersionsRef.current.delete(sessionId);
|
|
347
449
|
const restored = restoreSessionAt(sessionListRef.current.filter((item) => item.sessionId !== sessionId), removed, removedIndex);
|
|
@@ -412,6 +514,7 @@ export function useChatSessions(params) {
|
|
|
412
514
|
setError,
|
|
413
515
|
sessionOwnerQuery,
|
|
414
516
|
workspaceId,
|
|
517
|
+
invalidateSessionListBaseline,
|
|
415
518
|
]);
|
|
416
519
|
const forgetLostSession = useCallback((sessionId, ownerWorkspaceId) => {
|
|
417
520
|
const active = refs.sessionRef.current;
|
|
@@ -424,6 +527,7 @@ export function useChatSessions(params) {
|
|
|
424
527
|
forgetActiveChatSession(window.localStorage, scope, sessionId);
|
|
425
528
|
releaseSession(identity);
|
|
426
529
|
pendingDeletedIdsRef.current.add(sessionId);
|
|
530
|
+
invalidateSessionListBaseline();
|
|
427
531
|
const { next } = removeSessionById(sessionListRef.current, sessionId);
|
|
428
532
|
sessionListRef.current = next;
|
|
429
533
|
setSessionList(next);
|
|
@@ -436,7 +540,15 @@ export function useChatSessions(params) {
|
|
|
436
540
|
setSession(null);
|
|
437
541
|
}
|
|
438
542
|
void refreshSessionList();
|
|
439
|
-
}, [
|
|
543
|
+
}, [
|
|
544
|
+
refs,
|
|
545
|
+
releaseSession,
|
|
546
|
+
resetActiveProjection,
|
|
547
|
+
setSession,
|
|
548
|
+
workspaceId,
|
|
549
|
+
refreshSessionList,
|
|
550
|
+
invalidateSessionListBaseline,
|
|
551
|
+
]);
|
|
440
552
|
const activateSession = useCallback(async (sessionId, ownerWorkspaceId) => {
|
|
441
553
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
442
554
|
const listedOwner = sessionListRef.current.find((item) => item.sessionId === sessionId)?.workspaceId;
|
|
@@ -503,10 +615,12 @@ export function useChatSessions(params) {
|
|
|
503
615
|
applyActivatedSession({
|
|
504
616
|
messages: hydrated.messages,
|
|
505
617
|
toolCalls: hydrated.toolCalls,
|
|
618
|
+
turnStates: body.session.turnStates,
|
|
506
619
|
conversationEvents: body.conversationEvents,
|
|
507
620
|
sessionStats: body.sessionStats,
|
|
508
621
|
usage: body.usage,
|
|
509
622
|
turnTelemetry: body.turnTelemetry,
|
|
623
|
+
userTurnOutline: body.userTurnOutline,
|
|
510
624
|
sddSessionIdentity: body.sddDataAlignment ?? null,
|
|
511
625
|
streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
|
|
512
626
|
turnPhase: body.session.turnPhase ??
|
|
@@ -543,6 +657,32 @@ export function useChatSessions(params) {
|
|
|
543
657
|
forgetLostSession,
|
|
544
658
|
]);
|
|
545
659
|
activateSessionRef.current = activateSession;
|
|
660
|
+
const forkSessionAt = useCallback(async (throughMessageId) => {
|
|
661
|
+
const current = refs.sessionRef.current;
|
|
662
|
+
if (!current || refs.activeTurnIdRef.current)
|
|
663
|
+
return false;
|
|
664
|
+
try {
|
|
665
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(current.sessionId)}/fork${sessionOwnerQuery(current.workspaceId)}`, {
|
|
666
|
+
method: "POST",
|
|
667
|
+
credentials: "include",
|
|
668
|
+
headers: {
|
|
669
|
+
"content-type": "application/json",
|
|
670
|
+
"x-loop-console-confirmation": confirmationToken(),
|
|
671
|
+
},
|
|
672
|
+
body: JSON.stringify({ throughMessageId }),
|
|
673
|
+
});
|
|
674
|
+
const body = (await res.json().catch(() => ({})));
|
|
675
|
+
if (!res.ok || !body.sessionId) {
|
|
676
|
+
throw new Error(body.error?.message ?? "创建分叉会话失败");
|
|
677
|
+
}
|
|
678
|
+
await refreshSessionList();
|
|
679
|
+
return activateSession(body.sessionId, current.workspaceId);
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}, [activateSession, origin, refreshSessionList, refs, sessionOwnerQuery, setError]);
|
|
546
686
|
useEffect(() => {
|
|
547
687
|
void refreshSessionList();
|
|
548
688
|
}, [refreshSessionList]);
|
|
@@ -639,10 +779,12 @@ export function useChatSessions(params) {
|
|
|
639
779
|
applyActivatedSession({
|
|
640
780
|
messages: hydrated.messages,
|
|
641
781
|
toolCalls: hydrated.toolCalls,
|
|
782
|
+
turnStates: body.session.turnStates,
|
|
642
783
|
conversationEvents: body.conversationEvents,
|
|
643
784
|
sessionStats: body.sessionStats,
|
|
644
785
|
usage: body.usage,
|
|
645
786
|
turnTelemetry: body.turnTelemetry,
|
|
787
|
+
userTurnOutline: body.userTurnOutline,
|
|
646
788
|
sddSessionIdentity: body.sddDataAlignment ?? null,
|
|
647
789
|
streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
|
|
648
790
|
turnPhase: body.session.turnPhase ??
|
|
@@ -698,6 +840,7 @@ export function useChatSessions(params) {
|
|
|
698
840
|
optimisticState: "creating",
|
|
699
841
|
};
|
|
700
842
|
pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
|
|
843
|
+
invalidateSessionListBaseline();
|
|
701
844
|
const optimisticList = insertOptimisticSession(sessionListRef.current, optimisticRow);
|
|
702
845
|
sessionListRef.current = optimisticList;
|
|
703
846
|
setSessionList(optimisticList);
|
|
@@ -713,6 +856,14 @@ export function useChatSessions(params) {
|
|
|
713
856
|
creatingRef: creatingSessionRef,
|
|
714
857
|
});
|
|
715
858
|
pendingCreatedRowsRef.current.delete(optimisticSessionId);
|
|
859
|
+
const durableRow = sessionListRef.current.find((item) => item.sessionId === created.sessionId);
|
|
860
|
+
if (durableRow) {
|
|
861
|
+
pendingCreatedRowsRef.current.set(created.sessionId, durableRow);
|
|
862
|
+
invalidateSessionListBaseline();
|
|
863
|
+
void refreshSessionList().finally(() => {
|
|
864
|
+
pendingCreatedRowsRef.current.delete(created.sessionId);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
716
867
|
return created;
|
|
717
868
|
}
|
|
718
869
|
catch (error) {
|
|
@@ -722,7 +873,13 @@ export function useChatSessions(params) {
|
|
|
722
873
|
setSessionList(cleaned);
|
|
723
874
|
throw error;
|
|
724
875
|
}
|
|
725
|
-
}, [
|
|
876
|
+
}, [
|
|
877
|
+
origin,
|
|
878
|
+
refreshSessionList,
|
|
879
|
+
workspaceId,
|
|
880
|
+
setError,
|
|
881
|
+
invalidateSessionListBaseline,
|
|
882
|
+
]);
|
|
726
883
|
const createSession = useCallback(async (options) => {
|
|
727
884
|
// Suppress concurrent create from any entry (sidebar / Landing / chip).
|
|
728
885
|
if (creatingSessionRef.current)
|
|
@@ -904,11 +1061,13 @@ export function useChatSessions(params) {
|
|
|
904
1061
|
};
|
|
905
1062
|
return {
|
|
906
1063
|
sessionList,
|
|
1064
|
+
sessionListStatus,
|
|
907
1065
|
selectedSession,
|
|
908
1066
|
sessionSwitching: Boolean(selectedSession &&
|
|
909
1067
|
refs.sessionRef.current?.sessionId !== selectedSession.sessionId),
|
|
910
1068
|
creatingSession,
|
|
911
1069
|
activateSession,
|
|
1070
|
+
forkSessionAt,
|
|
912
1071
|
refreshSessionList,
|
|
913
1072
|
deleteSession,
|
|
914
1073
|
createSession,
|