@opengeni/react 0.15.0 → 0.23.0
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/README.md +110 -10
- package/dist/{chunk-TOJR776I.js → chunk-6XUS5VFM.js} +45 -369
- package/dist/chunk-6XUS5VFM.js.map +1 -0
- package/dist/chunk-CATATY77.js +2393 -0
- package/dist/chunk-CATATY77.js.map +1 -0
- package/dist/chunk-I3BJZIG5.js +439 -0
- package/dist/chunk-I3BJZIG5.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/chunk-UAHPKLB6.js +2237 -0
- package/dist/chunk-UAHPKLB6.js.map +1 -0
- package/dist/chunk-UQS7OQDE.js +591 -0
- package/dist/chunk-UQS7OQDE.js.map +1 -0
- package/dist/chunk-YDNWMKXO.js +102 -0
- package/dist/chunk-YDNWMKXO.js.map +1 -0
- package/dist/composer-CVm5uoUI.d.ts +585 -0
- package/dist/composer.d.ts +6 -0
- package/dist/composer.js +56 -0
- package/dist/composer.js.map +1 -0
- package/dist/index.d.ts +1288 -1785
- package/dist/index.js +7140 -8817
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +3 -1
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
- package/dist/queue-surface-implementation-3UW3MIIR.js +890 -0
- package/dist/queue-surface-implementation-3UW3MIIR.js.map +1 -0
- package/dist/session-Db00TRwl.d.ts +446 -0
- package/dist/session-context-rgrfElMl.d.ts +58 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +54 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-By1wn3MM.d.ts +99 -0
- package/package.json +20 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +44 -0
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +94 -993
- package/src/components/code-editor.tsx +104 -26
- package/src/components/command-palette.tsx +8 -2
- package/src/components/composer-transcription-control.tsx +211 -0
- package/src/components/composer.tsx +1536 -0
- package/src/components/desktop-viewer.tsx +3 -3
- package/src/components/enrollment-consent.tsx +2 -2
- package/src/components/file-browser.tsx +367 -29
- package/src/components/human-input-form.tsx +417 -0
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machines-dashboard.tsx +3 -3
- package/src/components/message-timeline.tsx +205 -25
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-draft-policy.ts +18 -0
- package/src/components/queue-surface-implementation.tsx +1037 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +42 -559
- package/src/components/sandbox-files.tsx +138 -14
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +339 -119
- package/src/components/workbench-changes.tsx +84 -33
- package/src/components/workspace-dock.tsx +188 -18
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +385 -104
- package/src/hooks/use-human-input.ts +131 -0
- package/src/hooks/use-machine-chip.ts +2 -2
- package/src/hooks/use-machines.ts +27 -11
- package/src/hooks/use-sandbox-files.ts +310 -62
- package/src/hooks/use-sandbox-git.ts +154 -40
- package/src/hooks/use-sandbox-terminal.ts +28 -6
- package/src/hooks/use-session-capabilities.ts +38 -9
- package/src/hooks/use-session-control.ts +37 -8
- package/src/hooks/use-session-events.ts +522 -48
- package/src/hooks/use-session-mcp-approval-policy.ts +161 -0
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +134 -66
- package/src/hooks/use-windowed-sections.ts +2 -2
- package/src/hooks/use-workspace-capture.ts +146 -40
- package/src/hooks/use-workspace-edit.ts +50 -14
- package/src/hooks/use-workspace-sessions.ts +3 -0
- package/src/human-input.ts +72 -0
- package/src/index.ts +85 -6
- package/src/lib/noto-sans-arabic-loader.ts +21 -0
- package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
- package/src/lib/use-portal-token-style.ts +51 -0
- package/src/lib/use-unicode-fonts.ts +57 -0
- package/src/provider.tsx +4 -69
- package/src/session-context.ts +160 -0
- package/src/session.ts +89 -0
- package/src/timeline/parsers.ts +35 -0
- package/src/timeline/projection.ts +12 -4
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +10 -9
- package/styles/index.css +56 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-TOJR776I.js.map +0 -1
- package/dist/machines-BpdwuQcD.d.ts +0 -449
|
@@ -0,0 +1,2237 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CREDIT_EXHAUSTION_MESSAGE,
|
|
3
|
+
humanizeFailureReason,
|
|
4
|
+
isCreditExhaustion,
|
|
5
|
+
tryParseJson,
|
|
6
|
+
useDebouncedCallback,
|
|
7
|
+
useEmbeddedHumanInputSession,
|
|
8
|
+
useEmbeddedSession,
|
|
9
|
+
useEmbeddedSessionMcpApprovalPolicy,
|
|
10
|
+
useMutationRunner,
|
|
11
|
+
usePolledValue,
|
|
12
|
+
useSessionEventTrigger
|
|
13
|
+
} from "./chunk-I3BJZIG5.js";
|
|
14
|
+
|
|
15
|
+
// src/timeline/projection.ts
|
|
16
|
+
var WORKER_SPAWN_TOOL = "session_create";
|
|
17
|
+
var WORKER_MESSAGE_TOOL = "session_send_message";
|
|
18
|
+
function buildTimeline(events) {
|
|
19
|
+
const items = [];
|
|
20
|
+
const prescan = prescanTurnAnchors(events);
|
|
21
|
+
const ordered = orderTimelineEvents(events, prescan);
|
|
22
|
+
const last = () => items[items.length - 1];
|
|
23
|
+
const closeStreamingTail = () => {
|
|
24
|
+
const open = last();
|
|
25
|
+
if ((open?.kind === "agent-message" || open?.kind === "reasoning") && open.streaming) {
|
|
26
|
+
open.streaming = false;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const finalizeOpen = (turnId, disposition = "complete") => {
|
|
30
|
+
for (const item of items) {
|
|
31
|
+
if (turnId !== void 0 && "turnId" in item && item.turnId && turnId && item.turnId !== turnId) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if ((item.kind === "agent-message" || item.kind === "reasoning") && item.streaming) {
|
|
35
|
+
item.streaming = false;
|
|
36
|
+
}
|
|
37
|
+
if ((item.kind === "tool-call" || item.kind === "worker") && item.status === "running") {
|
|
38
|
+
item.status = disposition;
|
|
39
|
+
}
|
|
40
|
+
if (item.kind === "sandbox" && item.status === "running") {
|
|
41
|
+
item.status = disposition;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
for (const event of ordered) {
|
|
46
|
+
const payload = asRecord(event.payload);
|
|
47
|
+
const turnId = event.turnId ?? null;
|
|
48
|
+
switch (event.type) {
|
|
49
|
+
case "user.message": {
|
|
50
|
+
closeStreamingTail();
|
|
51
|
+
const childCompletion = workerCompletionPayload(payload.childCompletion);
|
|
52
|
+
if (childCompletion) {
|
|
53
|
+
items.push({
|
|
54
|
+
kind: "worker-completion",
|
|
55
|
+
id: event.id,
|
|
56
|
+
turnId,
|
|
57
|
+
occurredAt: event.occurredAt,
|
|
58
|
+
childSessionId: childCompletion.childSessionId,
|
|
59
|
+
childStatus: childCompletion.childStatus,
|
|
60
|
+
goalStatus: childCompletion.goalStatus,
|
|
61
|
+
goalText: childCompletion.goalText,
|
|
62
|
+
evidence: childCompletion.evidence,
|
|
63
|
+
pausedReason: childCompletion.pausedReason,
|
|
64
|
+
text: typeof payload.text === "string" ? payload.text : ""
|
|
65
|
+
});
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
items.push({
|
|
69
|
+
kind: "user-message",
|
|
70
|
+
id: event.id,
|
|
71
|
+
text: typeof payload.text === "string" ? payload.text : "",
|
|
72
|
+
resources: resourceRefs(payload.resources),
|
|
73
|
+
tools: toolRefs(payload.tools),
|
|
74
|
+
occurredAt: event.occurredAt
|
|
75
|
+
});
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "agent.message.delta": {
|
|
79
|
+
const text = typeof payload.text === "string" ? payload.text : "";
|
|
80
|
+
if (!text) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const open = last();
|
|
84
|
+
if (open?.kind === "agent-message" && open.streaming && open.turnId === turnId) {
|
|
85
|
+
open.text += text;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
closeStreamingTail();
|
|
89
|
+
items.push({
|
|
90
|
+
kind: "agent-message",
|
|
91
|
+
id: event.id,
|
|
92
|
+
turnId,
|
|
93
|
+
text,
|
|
94
|
+
streaming: true,
|
|
95
|
+
occurredAt: event.occurredAt
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "agent.message.completed": {
|
|
100
|
+
const text = typeof payload.text === "string" ? payload.text : "";
|
|
101
|
+
const open = [...items].reverse().find(
|
|
102
|
+
(item) => item.kind === "agent-message" && item.turnId === turnId
|
|
103
|
+
);
|
|
104
|
+
if (open && (open.streaming || !open.text || text === open.text || text.startsWith(open.text))) {
|
|
105
|
+
if (!open.text || text && text.startsWith(open.text)) {
|
|
106
|
+
open.text = text || open.text;
|
|
107
|
+
}
|
|
108
|
+
open.streaming = false;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (text) {
|
|
112
|
+
items.push({
|
|
113
|
+
kind: "agent-message",
|
|
114
|
+
id: event.id,
|
|
115
|
+
turnId,
|
|
116
|
+
text,
|
|
117
|
+
streaming: false,
|
|
118
|
+
occurredAt: event.occurredAt
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case "agent.reasoning.delta": {
|
|
124
|
+
const text = reasoningText(event.payload);
|
|
125
|
+
if (!text) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
const open = last();
|
|
129
|
+
if (open?.kind === "reasoning" && open.streaming && open.turnId === turnId) {
|
|
130
|
+
open.text += text;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
closeStreamingTail();
|
|
134
|
+
items.push({
|
|
135
|
+
kind: "reasoning",
|
|
136
|
+
id: event.id,
|
|
137
|
+
turnId,
|
|
138
|
+
text,
|
|
139
|
+
streaming: true,
|
|
140
|
+
occurredAt: event.occurredAt
|
|
141
|
+
});
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "agent.toolCall.created": {
|
|
145
|
+
const name = typeof payload.name === "string" ? payload.name : "tool";
|
|
146
|
+
const callId = typeof payload.id === "string" ? payload.id : null;
|
|
147
|
+
const args = payload.arguments ?? null;
|
|
148
|
+
closeStreamingTail();
|
|
149
|
+
if (name === WORKER_SPAWN_TOOL || name === WORKER_MESSAGE_TOOL) {
|
|
150
|
+
items.push({
|
|
151
|
+
kind: "worker",
|
|
152
|
+
id: event.id,
|
|
153
|
+
turnId,
|
|
154
|
+
callId,
|
|
155
|
+
action: name === WORKER_SPAWN_TOOL ? "spawn" : "message",
|
|
156
|
+
prompt: workerPrompt(args),
|
|
157
|
+
workerSessionId: extractSessionRef(args),
|
|
158
|
+
status: "running",
|
|
159
|
+
occurredAt: event.occurredAt
|
|
160
|
+
});
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
items.push({
|
|
164
|
+
kind: "tool-call",
|
|
165
|
+
id: event.id,
|
|
166
|
+
turnId,
|
|
167
|
+
callId,
|
|
168
|
+
name,
|
|
169
|
+
arguments: args,
|
|
170
|
+
output: void 0,
|
|
171
|
+
// The provider-native item drives the per-tool renderers (apply_patch
|
|
172
|
+
// operation, computer_call action, web_search providerData, …).
|
|
173
|
+
raw: payload.raw,
|
|
174
|
+
status: "running",
|
|
175
|
+
occurredAt: event.occurredAt
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case "agent.toolCall.output": {
|
|
180
|
+
const callId = typeof payload.id === "string" ? payload.id : null;
|
|
181
|
+
const target = findOpenCall(items, callId);
|
|
182
|
+
if (!target) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
if (target.kind === "worker") {
|
|
186
|
+
target.status = isErrorOutput(payload) ? "failed" : "complete";
|
|
187
|
+
target.workerSessionId = target.workerSessionId ?? extractSessionRef(payload.output);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
target.status = isErrorOutput(payload) ? "failed" : "complete";
|
|
191
|
+
target.output = payload.output;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "sandbox.operation.started":
|
|
195
|
+
case "sandbox.operation.completed":
|
|
196
|
+
case "sandbox.operation.failed": {
|
|
197
|
+
const name = typeof payload.name === "string" ? payload.name : "sandbox";
|
|
198
|
+
const status = event.type.endsWith(".failed") ? "failed" : event.type.endsWith(".completed") ? "complete" : "running";
|
|
199
|
+
if ((name === "repository-clone" || name === "file-resource-download") && status !== "failed") {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
const existing = findOpenSandbox(items, name);
|
|
203
|
+
if (existing && status !== "running") {
|
|
204
|
+
existing.status = status;
|
|
205
|
+
if (payload.origin === "created" || payload.origin === "restored" || payload.origin === "resumed") {
|
|
206
|
+
existing.origin = payload.origin;
|
|
207
|
+
}
|
|
208
|
+
const message = failureMessage(payload);
|
|
209
|
+
if (message) {
|
|
210
|
+
existing.output = existing.output ? `${existing.output}
|
|
211
|
+
${message}` : message;
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
if (!existing) {
|
|
216
|
+
closeStreamingTail();
|
|
217
|
+
items.push({
|
|
218
|
+
kind: "sandbox",
|
|
219
|
+
id: event.id,
|
|
220
|
+
turnId,
|
|
221
|
+
name,
|
|
222
|
+
command: typeof payload.command === "string" ? payload.command : null,
|
|
223
|
+
output: failureMessage(payload) ?? "",
|
|
224
|
+
origin: payload.origin === "created" || payload.origin === "restored" || payload.origin === "resumed" ? payload.origin : null,
|
|
225
|
+
status,
|
|
226
|
+
occurredAt: event.occurredAt
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case "sandbox.command.output.delta": {
|
|
232
|
+
const text = typeof payload.chunk === "string" ? payload.chunk : typeof payload.text === "string" ? payload.text : typeof payload.output === "string" ? payload.output : "";
|
|
233
|
+
if (!text) {
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
const open = (typeof payload.name === "string" ? findOpenSandbox(items, payload.name) : void 0) ?? [...items].reverse().find(
|
|
237
|
+
(item) => item.kind === "sandbox" && item.status === "running"
|
|
238
|
+
);
|
|
239
|
+
if (open) {
|
|
240
|
+
open.output += text;
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
case "session.status.changed": {
|
|
245
|
+
const status = payload.status;
|
|
246
|
+
if (!isSessionStatus(status)) {
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
if (!ATTENTION_STATUSES.has(status)) {
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
const previous = [...items].reverse().find((item) => item.kind === "session-status");
|
|
253
|
+
if (previous?.status === status) {
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
items.push({ kind: "session-status", id: event.id, status, occurredAt: event.occurredAt });
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case "session.requiresAction": {
|
|
260
|
+
finalizeOpen(turnId);
|
|
261
|
+
items.push({
|
|
262
|
+
kind: "notice",
|
|
263
|
+
id: event.id,
|
|
264
|
+
tone: "waiting",
|
|
265
|
+
text: "Approval needed \u2014 the turn is paused until someone decides.",
|
|
266
|
+
occurredAt: event.occurredAt
|
|
267
|
+
});
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case "session.context.compacted": {
|
|
271
|
+
closeStreamingTail();
|
|
272
|
+
const before = typeof payload.estimatedTokensBefore === "number" ? Math.round(payload.estimatedTokensBefore).toLocaleString("en-US") : null;
|
|
273
|
+
const after = typeof payload.estimatedTokensAfter === "number" ? Math.round(payload.estimatedTokensAfter).toLocaleString("en-US") : null;
|
|
274
|
+
items.push({
|
|
275
|
+
kind: "notice",
|
|
276
|
+
id: event.id,
|
|
277
|
+
tone: "waiting",
|
|
278
|
+
text: before && after ? `Active conversation history compacted from approximately ${before} to ${after} tokens.` : "Context compacted so the turn could continue.",
|
|
279
|
+
occurredAt: event.occurredAt
|
|
280
|
+
});
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "session.context.compaction.skipped": {
|
|
284
|
+
closeStreamingTail();
|
|
285
|
+
const reason = typeof payload.reason === "string" ? payload.reason : null;
|
|
286
|
+
items.push({
|
|
287
|
+
kind: "notice",
|
|
288
|
+
id: event.id,
|
|
289
|
+
tone: reason === "summarization_failed" ? "failed" : "waiting",
|
|
290
|
+
text: reason === "no_history" ? "Context compaction skipped because there is no active history to compact." : reason === "replacement_not_smaller" ? "Context compaction skipped because the generated checkpoint would not reduce the context." : reason === "replacement_unchanged" ? "Context compaction stopped because it reproduced the same checkpoint without making progress." : reason === "summarization_failed" ? "Context compaction failed without replacing the active conversation history. Request it again to retry." : "Context compaction was not needed.",
|
|
291
|
+
occurredAt: event.occurredAt
|
|
292
|
+
});
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
case "turn.recovery.requested": {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case "turn.event.rejected_late": {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case "tool.auth_needed":
|
|
302
|
+
case "credential.auth_needed": {
|
|
303
|
+
closeStreamingTail();
|
|
304
|
+
items.push({
|
|
305
|
+
kind: "auth-needed",
|
|
306
|
+
id: event.id,
|
|
307
|
+
turnId,
|
|
308
|
+
providerDomain: typeof payload.providerDomain === "string" ? payload.providerDomain : "",
|
|
309
|
+
connectionId: typeof payload.connectionId === "string" ? payload.connectionId : null,
|
|
310
|
+
reason: authNeededReason(payload.reason),
|
|
311
|
+
scopes: stringList(payload.scopes),
|
|
312
|
+
resource: typeof payload.resource === "string" ? payload.resource : null,
|
|
313
|
+
toolName: typeof payload.toolName === "string" ? payload.toolName : null,
|
|
314
|
+
authorizationUrl: typeof payload.authorizationUrl === "string" ? payload.authorizationUrl : null,
|
|
315
|
+
occurredAt: event.occurredAt
|
|
316
|
+
});
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case "turn.completed": {
|
|
320
|
+
if (payload.maintenance === "context_compaction") {
|
|
321
|
+
finalizeOpen(turnId);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
if (isCreditExhaustionPayload(payload)) {
|
|
325
|
+
finalizeOpen(turnId);
|
|
326
|
+
items.push(turnEndItem(event, "failed", CREDIT_EXHAUSTION_MESSAGE));
|
|
327
|
+
items.push({
|
|
328
|
+
kind: "notice",
|
|
329
|
+
id: event.id,
|
|
330
|
+
tone: "failed",
|
|
331
|
+
text: CREDIT_EXHAUSTION_MESSAGE,
|
|
332
|
+
occurredAt: event.occurredAt
|
|
333
|
+
});
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
finalizeOpen(turnId);
|
|
337
|
+
items.push(turnEndItem(event, "complete", null));
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
case "turn.failed": {
|
|
341
|
+
const hadActivity = hasTurnActivity(items, turnId);
|
|
342
|
+
const failureText = isCreditExhaustionPayload(payload) ? CREDIT_EXHAUSTION_MESSAGE : failureMessage(payload);
|
|
343
|
+
finalizeOpen(turnId, "cancelled");
|
|
344
|
+
items.push(turnEndItem(event, "failed", failureText));
|
|
345
|
+
if (!hadActivity) {
|
|
346
|
+
items.push({
|
|
347
|
+
kind: "notice",
|
|
348
|
+
id: event.id,
|
|
349
|
+
tone: "failed",
|
|
350
|
+
text: failureText ?? "The turn failed.",
|
|
351
|
+
occurredAt: event.occurredAt
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case "turn.cancelled": {
|
|
357
|
+
if (turnId && !prescan.startedTurnIds.has(turnId)) {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
const hadActivity = hasTurnActivity(items, turnId);
|
|
361
|
+
finalizeOpen(turnId, "cancelled");
|
|
362
|
+
items.push(turnEndItem(event, "cancelled", null));
|
|
363
|
+
if (!hadActivity) {
|
|
364
|
+
items.push({
|
|
365
|
+
kind: "notice",
|
|
366
|
+
id: event.id,
|
|
367
|
+
tone: "cancelled",
|
|
368
|
+
text: "Interrupted.",
|
|
369
|
+
occurredAt: event.occurredAt
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case "memory.saved":
|
|
375
|
+
case "memory.corrected": {
|
|
376
|
+
const memory = memoryItem(event.id, event.type, turnId, payload, event.occurredAt);
|
|
377
|
+
if (memory) {
|
|
378
|
+
closeStreamingTail();
|
|
379
|
+
items.push(memory);
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case "goal.set":
|
|
384
|
+
case "goal.updated":
|
|
385
|
+
case "goal.completed":
|
|
386
|
+
case "goal.paused":
|
|
387
|
+
case "goal.resumed":
|
|
388
|
+
case "goal.cleared":
|
|
389
|
+
case "goal.continuation": {
|
|
390
|
+
items.push({
|
|
391
|
+
kind: "goal",
|
|
392
|
+
id: event.id,
|
|
393
|
+
action: event.type.slice("goal.".length),
|
|
394
|
+
text: goalText(payload),
|
|
395
|
+
occurredAt: event.occurredAt
|
|
396
|
+
});
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
default:
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return items;
|
|
404
|
+
}
|
|
405
|
+
function isCreditExhaustionPayload(payload) {
|
|
406
|
+
return isCreditExhaustion({
|
|
407
|
+
error: typeof payload.error === "string" ? payload.error : null,
|
|
408
|
+
detail: typeof payload.detail === "string" ? payload.detail : null,
|
|
409
|
+
segmentLimit: typeof payload.segmentLimit === "string" ? payload.segmentLimit : null
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function creditExhaustedFromEvents(events) {
|
|
413
|
+
const ordered = [...events].sort((a, b) => a.sequence - b.sequence);
|
|
414
|
+
for (let index = ordered.length - 1; index >= 0; index -= 1) {
|
|
415
|
+
const event = ordered[index];
|
|
416
|
+
if (event?.type !== "turn.completed" && event?.type !== "turn.failed" && event?.type !== "turn.cancelled") {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
return isCreditExhaustionPayload(asRecord(event.payload));
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
function sessionStatusFromEvents(events) {
|
|
424
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
425
|
+
const event = events[index];
|
|
426
|
+
if (event?.type !== "session.status.changed") {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const status = asRecord(event.payload).status;
|
|
430
|
+
if (isSessionStatus(status)) {
|
|
431
|
+
return status;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
function isActivityItem(item) {
|
|
437
|
+
switch (item.kind) {
|
|
438
|
+
case "reasoning":
|
|
439
|
+
case "tool-call":
|
|
440
|
+
case "worker":
|
|
441
|
+
case "sandbox":
|
|
442
|
+
case "memory":
|
|
443
|
+
return true;
|
|
444
|
+
default:
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function groupTimeline(items) {
|
|
449
|
+
const groups = [];
|
|
450
|
+
for (const item of items) {
|
|
451
|
+
if (isActivityItem(item)) {
|
|
452
|
+
const open = groups[groups.length - 1];
|
|
453
|
+
if (open?.kind === "activity" && open.outcome === void 0) {
|
|
454
|
+
open.items.push(item);
|
|
455
|
+
} else {
|
|
456
|
+
groups.push({ kind: "activity", id: `activity-${item.id}`, items: [item] });
|
|
457
|
+
}
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (item.kind === "turn-end") {
|
|
461
|
+
stampTurnOutcome(groups, item);
|
|
462
|
+
foldSettledTurn(groups, item);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
groups.push({ kind: "item", item });
|
|
466
|
+
}
|
|
467
|
+
return groups;
|
|
468
|
+
}
|
|
469
|
+
function prescanTurnAnchors(events) {
|
|
470
|
+
const ordered = [...events].sort((a, b) => a.sequence - b.sequence);
|
|
471
|
+
const queuedTurnByTrigger = /* @__PURE__ */ new Map();
|
|
472
|
+
const startSeqByTrigger = /* @__PURE__ */ new Map();
|
|
473
|
+
const cancelledTurnIds = /* @__PURE__ */ new Set();
|
|
474
|
+
const fallbackSeqByTurn = /* @__PURE__ */ new Map();
|
|
475
|
+
const startedTurnIds = /* @__PURE__ */ new Set();
|
|
476
|
+
for (const event of ordered) {
|
|
477
|
+
const payload = asRecord(event.payload);
|
|
478
|
+
const turnId = event.turnId ?? null;
|
|
479
|
+
if (event.type === "turn.queued") {
|
|
480
|
+
const triggerEventId = typeof payload.triggerEventId === "string" ? payload.triggerEventId : null;
|
|
481
|
+
const queuedTurnId = typeof payload.turnId === "string" ? payload.turnId : turnId;
|
|
482
|
+
if (triggerEventId && queuedTurnId) {
|
|
483
|
+
queuedTurnByTrigger.set(triggerEventId, queuedTurnId);
|
|
484
|
+
}
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (event.type === "turn.started") {
|
|
488
|
+
const triggerEventId = typeof payload.triggerEventId === "string" ? payload.triggerEventId : null;
|
|
489
|
+
if (triggerEventId && !startSeqByTrigger.has(triggerEventId)) {
|
|
490
|
+
startSeqByTrigger.set(triggerEventId, event.sequence);
|
|
491
|
+
}
|
|
492
|
+
if (turnId) {
|
|
493
|
+
startedTurnIds.add(turnId);
|
|
494
|
+
}
|
|
495
|
+
} else if (event.type === "turn.cancelled" && turnId) {
|
|
496
|
+
cancelledTurnIds.add(turnId);
|
|
497
|
+
}
|
|
498
|
+
if (turnId && isTurnExecutionEvidence(event.type)) {
|
|
499
|
+
const previous = fallbackSeqByTurn.get(turnId);
|
|
500
|
+
if (previous === void 0 || event.sequence < previous) {
|
|
501
|
+
fallbackSeqByTurn.set(turnId, event.sequence);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (turnId && isTurnExecutionEvidence(event.type)) {
|
|
505
|
+
startedTurnIds.add(turnId);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
|
|
509
|
+
if (!startSeqByTrigger.has(triggerEventId)) {
|
|
510
|
+
const fallbackSeq = fallbackSeqByTurn.get(turnId);
|
|
511
|
+
if (fallbackSeq !== void 0) {
|
|
512
|
+
startSeqByTrigger.set(triggerEventId, fallbackSeq);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const cancelledBeforeStartTriggers = /* @__PURE__ */ new Set();
|
|
517
|
+
for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
|
|
518
|
+
if (cancelledTurnIds.has(turnId) && !startSeqByTrigger.has(triggerEventId) && !fallbackSeqByTurn.has(turnId)) {
|
|
519
|
+
cancelledBeforeStartTriggers.add(triggerEventId);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return { queuedTurnByTrigger, startSeqByTrigger, cancelledBeforeStartTriggers, startedTurnIds };
|
|
523
|
+
}
|
|
524
|
+
function orderTimelineEvents(events, prescan) {
|
|
525
|
+
const ordered = [...events].sort((a, b) => a.sequence - b.sequence);
|
|
526
|
+
const insertions = /* @__PURE__ */ new Map();
|
|
527
|
+
for (const event of ordered) {
|
|
528
|
+
if (event.type !== "user.message") {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const queuedTurnId = prescan.queuedTurnByTrigger.get(event.id);
|
|
532
|
+
if (!queuedTurnId) {
|
|
533
|
+
pushInsertion(insertions, event.sequence, event);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (prescan.cancelledBeforeStartTriggers.has(event.id)) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const startSeq = prescan.startSeqByTrigger.get(event.id);
|
|
540
|
+
if (startSeq !== void 0) {
|
|
541
|
+
pushInsertion(insertions, startSeq, event);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const projected = [];
|
|
546
|
+
for (const event of ordered) {
|
|
547
|
+
const before = insertions.get(event.sequence);
|
|
548
|
+
if (before) {
|
|
549
|
+
projected.push(...before);
|
|
550
|
+
}
|
|
551
|
+
if (event.type !== "user.message") {
|
|
552
|
+
projected.push(event);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return projected;
|
|
556
|
+
}
|
|
557
|
+
function pushInsertion(insertions, sequence, event) {
|
|
558
|
+
const bucket = insertions.get(sequence);
|
|
559
|
+
if (bucket) {
|
|
560
|
+
bucket.push(event);
|
|
561
|
+
} else {
|
|
562
|
+
insertions.set(sequence, [event]);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function isAgentActivityEvent(type) {
|
|
566
|
+
return type.startsWith("agent.") || type.startsWith("sandbox.");
|
|
567
|
+
}
|
|
568
|
+
function isTurnExecutionEvidence(type) {
|
|
569
|
+
return isAgentActivityEvent(type) || type === "turn.completed" || type === "turn.failed" || type === "turn.recovery.requested" || type === "turn.capacity_waiting" || type === "session.requiresAction" || type === "tool.auth_needed" || type === "credential.auth_needed" || type.startsWith("rig.setup.") || type === "codex.capacity.waiting" || type === "codex.capacity.resumed";
|
|
570
|
+
}
|
|
571
|
+
function turnEndItem(event, outcome, failureText) {
|
|
572
|
+
return {
|
|
573
|
+
kind: "turn-end",
|
|
574
|
+
id: `${event.id}-turn-end`,
|
|
575
|
+
turnId: event.turnId ?? null,
|
|
576
|
+
outcome,
|
|
577
|
+
failureText,
|
|
578
|
+
occurredAt: event.occurredAt
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function hasTurnActivity(items, turnId) {
|
|
582
|
+
if (turnId) {
|
|
583
|
+
return items.some((item) => isActivityItem(item) && item.turnId === turnId);
|
|
584
|
+
}
|
|
585
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
586
|
+
const item = items[index];
|
|
587
|
+
if (!item || item.kind === "turn-end" || item.kind === "user-message") {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
if (isActivityItem(item)) {
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
function stampTurnOutcome(groups, turnEnd) {
|
|
597
|
+
if (turnEnd.turnId === null) {
|
|
598
|
+
const trailing = groups[groups.length - 1];
|
|
599
|
+
if (trailing?.kind === "activity" && trailing.outcome === void 0) {
|
|
600
|
+
applyTurnOutcome(trailing, turnEnd);
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
for (const group of groups) {
|
|
605
|
+
if (group.kind !== "activity" || group.outcome !== void 0) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (group.items.some((activity) => activity.turnId === turnEnd.turnId)) {
|
|
609
|
+
applyTurnOutcome(group, turnEnd);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function applyTurnOutcome(group, turnEnd) {
|
|
614
|
+
if (turnEnd.outcome === "complete") {
|
|
615
|
+
group.outcome = "complete";
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const hasFailed = group.items.some((item) => "status" in item && item.status === "failed");
|
|
619
|
+
const hasInterrupted = group.items.some(
|
|
620
|
+
(item) => "status" in item && item.status === "cancelled"
|
|
621
|
+
);
|
|
622
|
+
group.outcome = hasFailed ? "failed" : hasInterrupted ? "cancelled" : "complete";
|
|
623
|
+
if (turnEnd.failureText && hasFailed) {
|
|
624
|
+
group.failureText = turnEnd.failureText;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function foldSettledTurn(groups, turnEnd) {
|
|
628
|
+
let startIndex = groups.length;
|
|
629
|
+
let stoppedAtForeignTurn = false;
|
|
630
|
+
while (startIndex > 0) {
|
|
631
|
+
const previous = groups[startIndex - 1];
|
|
632
|
+
if (isTurnBoundary(previous)) {
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
if (belongsToDifferentTurn(previous, turnEnd.turnId)) {
|
|
636
|
+
stoppedAtForeignTurn = true;
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
startIndex -= 1;
|
|
640
|
+
}
|
|
641
|
+
if (stoppedAtForeignTurn) {
|
|
642
|
+
while (startIndex < groups.length && isBetweenTurnDivider(groups[startIndex])) {
|
|
643
|
+
startIndex += 1;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const collected = groups.slice(startIndex);
|
|
647
|
+
if (collected.length === 0) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const finalMessage = extractFinalAgentMessage(collected, turnEnd);
|
|
651
|
+
const body = finalMessage ? collected.slice(0, -1) : collected;
|
|
652
|
+
if (body.length === 0) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const firstOccurredAt = groupStartedAt(body[0]) ?? turnEnd.occurredAt;
|
|
656
|
+
const turnGroup = {
|
|
657
|
+
kind: "turn",
|
|
658
|
+
id: `turn-${turnEnd.turnId ?? turnEnd.id}`,
|
|
659
|
+
outcome: turnEnd.outcome,
|
|
660
|
+
startedAt: firstOccurredAt,
|
|
661
|
+
endedAt: turnEnd.occurredAt,
|
|
662
|
+
groups: body
|
|
663
|
+
};
|
|
664
|
+
if (turnEnd.failureText) {
|
|
665
|
+
turnGroup.failureText = turnEnd.failureText;
|
|
666
|
+
}
|
|
667
|
+
groups.splice(
|
|
668
|
+
startIndex,
|
|
669
|
+
collected.length,
|
|
670
|
+
...finalMessage ? [turnGroup, finalMessage] : [turnGroup]
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
function isTurnBoundary(group) {
|
|
674
|
+
return group?.kind === "turn" || group?.kind === "item" && group.item.kind === "user-message";
|
|
675
|
+
}
|
|
676
|
+
function belongsToDifferentTurn(group, turnId) {
|
|
677
|
+
if (!group || !turnId) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
if (group.kind === "activity") {
|
|
681
|
+
return group.items.length > 0 && group.items.every((item) => item.turnId !== null && item.turnId !== turnId);
|
|
682
|
+
}
|
|
683
|
+
return group.kind === "item" && group.item.kind === "agent-message" && group.item.turnId !== null && group.item.turnId !== turnId;
|
|
684
|
+
}
|
|
685
|
+
function isBetweenTurnDivider(group) {
|
|
686
|
+
return group?.kind === "item" && group.item.kind === "session-status" && group.item.status !== "running";
|
|
687
|
+
}
|
|
688
|
+
function extractFinalAgentMessage(groups, turnEnd) {
|
|
689
|
+
const tail = groups[groups.length - 1];
|
|
690
|
+
if (tail?.kind !== "item" || tail.item.kind !== "agent-message" || tail.item.streaming) {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
if (tail.item.turnId && turnEnd.turnId && tail.item.turnId !== turnEnd.turnId) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
return tail;
|
|
697
|
+
}
|
|
698
|
+
function groupStartedAt(group) {
|
|
699
|
+
if (!group) {
|
|
700
|
+
return void 0;
|
|
701
|
+
}
|
|
702
|
+
switch (group.kind) {
|
|
703
|
+
case "item":
|
|
704
|
+
return group.item.occurredAt;
|
|
705
|
+
case "activity":
|
|
706
|
+
return group.items[0]?.occurredAt;
|
|
707
|
+
case "turn":
|
|
708
|
+
return group.startedAt;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function asRecord(value) {
|
|
712
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
713
|
+
}
|
|
714
|
+
var SESSION_STATUSES = [
|
|
715
|
+
"queued",
|
|
716
|
+
"running",
|
|
717
|
+
"idle",
|
|
718
|
+
"requires_action",
|
|
719
|
+
"failed",
|
|
720
|
+
"cancelled"
|
|
721
|
+
];
|
|
722
|
+
var ATTENTION_STATUSES = /* @__PURE__ */ new Set([
|
|
723
|
+
"requires_action",
|
|
724
|
+
"failed",
|
|
725
|
+
"cancelled"
|
|
726
|
+
]);
|
|
727
|
+
function resourceRefs(value) {
|
|
728
|
+
if (!Array.isArray(value)) {
|
|
729
|
+
return [];
|
|
730
|
+
}
|
|
731
|
+
return value.filter((entry) => {
|
|
732
|
+
const record = asRecord(entry);
|
|
733
|
+
if (record.kind === "repository") {
|
|
734
|
+
return typeof record.uri === "string" && typeof record.ref === "string";
|
|
735
|
+
}
|
|
736
|
+
return record.kind === "file" && typeof record.fileId === "string";
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function toolRefs(value) {
|
|
740
|
+
if (!Array.isArray(value)) {
|
|
741
|
+
return [];
|
|
742
|
+
}
|
|
743
|
+
return value.filter((entry) => {
|
|
744
|
+
const record = asRecord(entry);
|
|
745
|
+
return record.kind === "mcp" && typeof record.id === "string";
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function workerCompletionPayload(value) {
|
|
749
|
+
const payload = asRecord(value);
|
|
750
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
751
|
+
if (typeof payload.childSessionId !== "string" || !uuidPattern.test(payload.childSessionId) || typeof payload.status !== "string" || payload.status.trim() === "") {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
const goal = asRecord(payload.goal);
|
|
755
|
+
return {
|
|
756
|
+
childSessionId: payload.childSessionId,
|
|
757
|
+
childStatus: payload.status,
|
|
758
|
+
goalStatus: typeof goal.status === "string" ? goal.status : null,
|
|
759
|
+
goalText: typeof goal.text === "string" ? goal.text : null,
|
|
760
|
+
evidence: typeof goal.evidence === "string" ? goal.evidence : null,
|
|
761
|
+
// Console pauses and several server paths put the human explanation on
|
|
762
|
+
// `rationale`, not `pausedReason` — same fallback the goal pill uses, so a
|
|
763
|
+
// paused worker card never shows "Worker paused" with the reason missing.
|
|
764
|
+
pausedReason: typeof goal.pausedReason === "string" ? goal.pausedReason : typeof goal.rationale === "string" ? goal.rationale : null
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function isSessionStatus(value) {
|
|
768
|
+
return typeof value === "string" && SESSION_STATUSES.includes(value);
|
|
769
|
+
}
|
|
770
|
+
function isErrorOutput(payload) {
|
|
771
|
+
if (payload.error === true || payload.failed === true) {
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
const output = payload.output;
|
|
775
|
+
return !!output && typeof output === "object" && output.isError === true;
|
|
776
|
+
}
|
|
777
|
+
function findOpenCall(items, callId) {
|
|
778
|
+
const reversed = [...items].reverse();
|
|
779
|
+
const isCall = (item) => item.kind === "tool-call" || item.kind === "worker";
|
|
780
|
+
if (callId) {
|
|
781
|
+
const byId = reversed.find((item) => isCall(item) && item.callId === callId);
|
|
782
|
+
if (byId) {
|
|
783
|
+
return byId;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return reversed.find(
|
|
787
|
+
(item) => isCall(item) && item.status === "running"
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
function findOpenSandbox(items, name) {
|
|
791
|
+
return [...items].reverse().find(
|
|
792
|
+
(item) => item.kind === "sandbox" && item.name === name && item.status === "running"
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
function failureMessage(payload) {
|
|
796
|
+
for (const key of ["error", "message"]) {
|
|
797
|
+
const value = payload[key];
|
|
798
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
799
|
+
return humanizeFailureReason(value);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
function goalText(payload) {
|
|
805
|
+
if (typeof payload.text === "string" && payload.text) {
|
|
806
|
+
return payload.text;
|
|
807
|
+
}
|
|
808
|
+
const goal = asRecord(payload.goal);
|
|
809
|
+
if (typeof goal.text === "string" && goal.text) {
|
|
810
|
+
return goal.text;
|
|
811
|
+
}
|
|
812
|
+
if (typeof payload.prompt === "string" && payload.prompt) {
|
|
813
|
+
return payload.prompt;
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
function memoryItem(id, type, turnId, payload, occurredAt) {
|
|
818
|
+
const memoryId = typeof payload.memoryId === "string" && payload.memoryId ? payload.memoryId : null;
|
|
819
|
+
if (!memoryId) {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
const replacementPreview = typeof payload.replacementPreview === "string" ? payload.replacementPreview : void 0;
|
|
823
|
+
const replacementMemoryId = typeof payload.replacementMemoryId === "string" ? payload.replacementMemoryId : void 0;
|
|
824
|
+
const action = typeof payload.action === "string" ? payload.action : void 0;
|
|
825
|
+
return {
|
|
826
|
+
kind: "memory",
|
|
827
|
+
id,
|
|
828
|
+
turnId,
|
|
829
|
+
variant: type === "memory.corrected" ? "corrected" : "saved",
|
|
830
|
+
memoryKind: typeof payload.kind === "string" ? payload.kind : "",
|
|
831
|
+
preview: typeof payload.preview === "string" ? payload.preview : "",
|
|
832
|
+
...payload.deduped === true ? { deduped: true } : {},
|
|
833
|
+
...replacementPreview ? { replacementPreview } : {},
|
|
834
|
+
...action ? { action } : {},
|
|
835
|
+
memoryId,
|
|
836
|
+
...replacementMemoryId ? { replacementMemoryId } : {},
|
|
837
|
+
occurredAt
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
var AUTH_NEEDED_REASONS = /* @__PURE__ */ new Set([
|
|
841
|
+
"missing_connection",
|
|
842
|
+
"expired",
|
|
843
|
+
"insufficient_scope",
|
|
844
|
+
"refresh_failed",
|
|
845
|
+
"unsupported_auth",
|
|
846
|
+
"resource_scope_unavailable"
|
|
847
|
+
]);
|
|
848
|
+
function authNeededReason(value) {
|
|
849
|
+
return typeof value === "string" && AUTH_NEEDED_REASONS.has(value) ? value : null;
|
|
850
|
+
}
|
|
851
|
+
function stringList(value) {
|
|
852
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
|
|
853
|
+
}
|
|
854
|
+
function reasoningText(payload) {
|
|
855
|
+
const record = asRecord(payload);
|
|
856
|
+
if (typeof record.text === "string") {
|
|
857
|
+
return record.text;
|
|
858
|
+
}
|
|
859
|
+
const content = asRecord(asRecord(record.item).rawItem).content;
|
|
860
|
+
if (!Array.isArray(content)) {
|
|
861
|
+
return "";
|
|
862
|
+
}
|
|
863
|
+
return content.map((part) => {
|
|
864
|
+
const text = asRecord(part).text;
|
|
865
|
+
return typeof text === "string" ? text : "";
|
|
866
|
+
}).join("");
|
|
867
|
+
}
|
|
868
|
+
function workerPrompt(args) {
|
|
869
|
+
const record = asRecord(typeof args === "string" ? tryParseJson(args) : args);
|
|
870
|
+
for (const key of ["initialMessage", "message", "text", "prompt"]) {
|
|
871
|
+
const value = record[key];
|
|
872
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
873
|
+
return value;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
function extractSessionRef(value, depth = 0) {
|
|
879
|
+
if (depth > 6 || value === null || value === void 0) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
if (typeof value === "string") {
|
|
883
|
+
return extractSessionRef(tryParseJson(value), depth + 1);
|
|
884
|
+
}
|
|
885
|
+
if (Array.isArray(value)) {
|
|
886
|
+
for (const entry of value) {
|
|
887
|
+
const found = extractSessionRef(entry, depth + 1);
|
|
888
|
+
if (found) {
|
|
889
|
+
return found;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
if (typeof value !== "object") {
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
const record = value;
|
|
898
|
+
if (typeof record.sessionId === "string" && looksLikeId(record.sessionId)) {
|
|
899
|
+
return record.sessionId;
|
|
900
|
+
}
|
|
901
|
+
if (typeof record.id === "string" && looksLikeId(record.id) && ("status" in record || "workspaceId" in record || "initialMessage" in record)) {
|
|
902
|
+
return record.id;
|
|
903
|
+
}
|
|
904
|
+
for (const key of ["structuredContent", "session", "result", "content"]) {
|
|
905
|
+
if (key in record) {
|
|
906
|
+
const found = extractSessionRef(record[key], depth + 1);
|
|
907
|
+
if (found) {
|
|
908
|
+
return found;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (typeof record.text === "string") {
|
|
913
|
+
return extractSessionRef(tryParseJson(record.text), depth + 1);
|
|
914
|
+
}
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
function looksLikeId(value) {
|
|
918
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
919
|
+
}
|
|
920
|
+
function toolDisplayName(name) {
|
|
921
|
+
const boundary = name.indexOf("__");
|
|
922
|
+
const toolPart = boundary >= 0 ? name.slice(boundary + 2) : name;
|
|
923
|
+
return toolPart.replace(/[_-]+/g, " ").trim();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// src/hooks/use-session-events.ts
|
|
927
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
928
|
+
var INITIAL_TAIL_PAGE_SIZE = 1e3;
|
|
929
|
+
var OLDER_PAGE_SIZE = 5e3;
|
|
930
|
+
var INITIAL_FETCH_CAP = 1;
|
|
931
|
+
var OLDER_GROUP_TARGET = 32;
|
|
932
|
+
var OLDER_FETCH_CAP = 2;
|
|
933
|
+
var BOUNDARY_PAGE_CAP = 4;
|
|
934
|
+
var EMPTY_EVENTS = [];
|
|
935
|
+
var encoder = new TextEncoder();
|
|
936
|
+
var decoder = new TextDecoder();
|
|
937
|
+
var BROWSER_EVENT_TYPE_MAX_BYTES = 256;
|
|
938
|
+
var BROWSER_EVENT_ID_MAX_BYTES = 256;
|
|
939
|
+
var BROWSER_EVENT_CLIENT_ID_MAX_BYTES = 4 * 1024;
|
|
940
|
+
var BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES = 4 * 1024;
|
|
941
|
+
var BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES = 48 * 1024;
|
|
942
|
+
var BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS = [
|
|
943
|
+
"id",
|
|
944
|
+
"callId",
|
|
945
|
+
"call_id",
|
|
946
|
+
"name",
|
|
947
|
+
"toolName",
|
|
948
|
+
"status",
|
|
949
|
+
"code",
|
|
950
|
+
"isError",
|
|
951
|
+
"stream",
|
|
952
|
+
"commandId",
|
|
953
|
+
"sequence",
|
|
954
|
+
"coalescedUntil",
|
|
955
|
+
"coalescedCount",
|
|
956
|
+
"firstSequence",
|
|
957
|
+
"lastSequence"
|
|
958
|
+
];
|
|
959
|
+
var SESSION_EVENT_BROWSER_MAX_BYTES = 8 * 1024 * 1024;
|
|
960
|
+
var SESSION_EVENT_BROWSER_MAX_COUNT = 1e4;
|
|
961
|
+
var SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES = 96 * 1024;
|
|
962
|
+
var SESSION_EVENT_BROWSER_PENDING_MAX_BYTES = 1024 * 1024;
|
|
963
|
+
var SESSION_EVENT_BROWSER_PENDING_MAX_COUNT = 256;
|
|
964
|
+
var EMPTY_EVENT_WINDOW = {
|
|
965
|
+
events: EMPTY_EVENTS,
|
|
966
|
+
bytes: 2,
|
|
967
|
+
truncated: false
|
|
968
|
+
};
|
|
969
|
+
function useSessionEvents(sessionId, options = {}) {
|
|
970
|
+
const { client, workspaceId, reconcileSession } = useEmbeddedSession(options);
|
|
971
|
+
const enabled = options.enabled ?? true;
|
|
972
|
+
const after = options.after ?? 0;
|
|
973
|
+
const replay = options.replay ?? "windowed";
|
|
974
|
+
const fullReplay = replay === "full" || after !== 0;
|
|
975
|
+
const streamKey = `${workspaceId}\0${sessionId ?? ""}\0${after}\0${fullReplay ? "full" : "windowed"}`;
|
|
976
|
+
const [eventWindow, setEventWindow] = useState(EMPTY_EVENT_WINDOW);
|
|
977
|
+
const [connectionState, setConnectionState] = useState("idle");
|
|
978
|
+
const [error, setError] = useState(null);
|
|
979
|
+
const [hasOlder, setHasOlder] = useState(false);
|
|
980
|
+
const [sessionStatusProjection, setSessionStatusProjection] = useState(
|
|
981
|
+
null
|
|
982
|
+
);
|
|
983
|
+
const [initialLoading, setInitialLoading] = useState(true);
|
|
984
|
+
const [loadingOlder, setLoadingOlder] = useState(false);
|
|
985
|
+
const [streamEpoch, setStreamEpoch] = useState(0);
|
|
986
|
+
const lastSequenceRef = useRef(after);
|
|
987
|
+
const streamResumeSequenceRef = useRef(after);
|
|
988
|
+
const oldestSequenceRef = useRef(null);
|
|
989
|
+
const hasOlderRef = useRef(false);
|
|
990
|
+
const loadingOlderRef = useRef(false);
|
|
991
|
+
const initialWindowLoadedRef = useRef(false);
|
|
992
|
+
const streamAbortRef = useRef(null);
|
|
993
|
+
const streamKeyRef = useRef(null);
|
|
994
|
+
const generationRef = useRef(0);
|
|
995
|
+
const eventWindowRef = useRef(EMPTY_EVENT_WINDOW);
|
|
996
|
+
const sessionStatusRef = useRef({
|
|
997
|
+
sequence: after,
|
|
998
|
+
status: null
|
|
999
|
+
});
|
|
1000
|
+
const [stateStreamKey, setStateStreamKey] = useState(streamKey);
|
|
1001
|
+
useEffect(() => {
|
|
1002
|
+
if (streamKeyRef.current !== streamKey) {
|
|
1003
|
+
streamKeyRef.current = streamKey;
|
|
1004
|
+
setStateStreamKey(streamKey);
|
|
1005
|
+
eventWindowRef.current = EMPTY_EVENT_WINDOW;
|
|
1006
|
+
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
1007
|
+
setError(null);
|
|
1008
|
+
setHasOlder(false);
|
|
1009
|
+
sessionStatusRef.current = { sequence: after, status: null };
|
|
1010
|
+
setSessionStatusProjection(null);
|
|
1011
|
+
setLoadingOlder(false);
|
|
1012
|
+
setInitialLoading(true);
|
|
1013
|
+
lastSequenceRef.current = after;
|
|
1014
|
+
streamResumeSequenceRef.current = after;
|
|
1015
|
+
oldestSequenceRef.current = null;
|
|
1016
|
+
hasOlderRef.current = false;
|
|
1017
|
+
loadingOlderRef.current = false;
|
|
1018
|
+
initialWindowLoadedRef.current = false;
|
|
1019
|
+
}
|
|
1020
|
+
const generation = generationRef.current + 1;
|
|
1021
|
+
generationRef.current = generation;
|
|
1022
|
+
if (loadingOlderRef.current) {
|
|
1023
|
+
loadingOlderRef.current = false;
|
|
1024
|
+
setLoadingOlder(false);
|
|
1025
|
+
}
|
|
1026
|
+
if (!sessionId || !enabled) {
|
|
1027
|
+
setConnectionState("idle");
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const controller = new AbortController();
|
|
1031
|
+
const isCurrent = () => generationRef.current === generation && !controller.signal.aborted;
|
|
1032
|
+
streamAbortRef.current = controller;
|
|
1033
|
+
let pending = [];
|
|
1034
|
+
let pendingBytes = 2;
|
|
1035
|
+
let flushTimer = null;
|
|
1036
|
+
const flush = () => {
|
|
1037
|
+
if (flushTimer !== null) {
|
|
1038
|
+
clearTimeout(flushTimer);
|
|
1039
|
+
flushTimer = null;
|
|
1040
|
+
}
|
|
1041
|
+
if (!isCurrent()) {
|
|
1042
|
+
pending = [];
|
|
1043
|
+
pendingBytes = 2;
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (pending.length === 0) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const batch = pending;
|
|
1050
|
+
pending = [];
|
|
1051
|
+
pendingBytes = 2;
|
|
1052
|
+
const lastInBatch = batch[batch.length - 1];
|
|
1053
|
+
if (lastInBatch) {
|
|
1054
|
+
const batchResumeSequence = Math.max(...batch.map(eventResumeSequence));
|
|
1055
|
+
lastSequenceRef.current = Math.max(lastSequenceRef.current, batchResumeSequence);
|
|
1056
|
+
streamResumeSequenceRef.current = Math.max(
|
|
1057
|
+
streamResumeSequenceRef.current,
|
|
1058
|
+
batchResumeSequence
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
observeSessionStatus(batch, sessionStatusRef, setSessionStatusProjection);
|
|
1062
|
+
const current = eventWindowRef.current;
|
|
1063
|
+
const next = boundBrowserSessionEventWindow([...current.events, ...batch]);
|
|
1064
|
+
const retained = {
|
|
1065
|
+
...next,
|
|
1066
|
+
truncated: current.truncated || next.truncated
|
|
1067
|
+
};
|
|
1068
|
+
eventWindowRef.current = retained;
|
|
1069
|
+
setEventWindow(retained);
|
|
1070
|
+
oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
|
|
1071
|
+
if (retained.truncated) {
|
|
1072
|
+
hasOlderRef.current = true;
|
|
1073
|
+
setHasOlder(true);
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
const scheduleFlush = () => {
|
|
1077
|
+
if (!isCurrent()) return;
|
|
1078
|
+
flushTimer ??= setTimeout(flush, 16);
|
|
1079
|
+
};
|
|
1080
|
+
void (async () => {
|
|
1081
|
+
try {
|
|
1082
|
+
if (!fullReplay && !initialWindowLoadedRef.current) {
|
|
1083
|
+
setConnectionState("connecting");
|
|
1084
|
+
const window = await loadEventWindow(client, workspaceId, sessionId, {
|
|
1085
|
+
before: Number.MAX_SAFE_INTEGER,
|
|
1086
|
+
pageSize: INITIAL_TAIL_PAGE_SIZE,
|
|
1087
|
+
targetGroups: Number.POSITIVE_INFINITY,
|
|
1088
|
+
maxFetches: INITIAL_FETCH_CAP,
|
|
1089
|
+
signal: controller.signal
|
|
1090
|
+
});
|
|
1091
|
+
if (!isCurrent()) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
observeSessionStatus(window.events, sessionStatusRef, setSessionStatusProjection);
|
|
1095
|
+
const retained = boundBrowserSessionEventWindow(window.events);
|
|
1096
|
+
eventWindowRef.current = retained;
|
|
1097
|
+
setEventWindow(retained);
|
|
1098
|
+
oldestSequenceRef.current = retained.events[0]?.sequence ?? window.oldestSequence;
|
|
1099
|
+
hasOlderRef.current = window.hasOlder || retained.truncated;
|
|
1100
|
+
lastSequenceRef.current = window.newestSequence;
|
|
1101
|
+
streamResumeSequenceRef.current = window.newestSequence;
|
|
1102
|
+
initialWindowLoadedRef.current = true;
|
|
1103
|
+
setHasOlder(window.hasOlder || retained.truncated);
|
|
1104
|
+
setInitialLoading(false);
|
|
1105
|
+
}
|
|
1106
|
+
if (fullReplay) {
|
|
1107
|
+
setInitialLoading(false);
|
|
1108
|
+
}
|
|
1109
|
+
const stream = client.streamEvents(workspaceId, sessionId, {
|
|
1110
|
+
after: streamResumeSequenceRef.current,
|
|
1111
|
+
signal: controller.signal,
|
|
1112
|
+
beforeLive: async () => await reconcileSession(sessionId),
|
|
1113
|
+
onStateChange: (state) => {
|
|
1114
|
+
if (isCurrent()) {
|
|
1115
|
+
setConnectionState(state);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
for await (const event of stream) {
|
|
1120
|
+
if (!isCurrent()) break;
|
|
1121
|
+
const boundedEvent = boundBrowserLegacyEvent(event);
|
|
1122
|
+
const boundedEventBytes = browserJsonBytes(boundedEvent);
|
|
1123
|
+
const separatorBytes = pending.length === 0 ? 0 : 1;
|
|
1124
|
+
if (pending.length > 0 && (pending.length >= SESSION_EVENT_BROWSER_PENDING_MAX_COUNT || pendingBytes + separatorBytes + boundedEventBytes > SESSION_EVENT_BROWSER_PENDING_MAX_BYTES)) {
|
|
1125
|
+
flush();
|
|
1126
|
+
}
|
|
1127
|
+
pending.push(boundedEvent);
|
|
1128
|
+
pendingBytes += (pending.length === 1 ? 0 : 1) + boundedEventBytes;
|
|
1129
|
+
if (pending.length >= SESSION_EVENT_BROWSER_PENDING_MAX_COUNT || pendingBytes >= SESSION_EVENT_BROWSER_PENDING_MAX_BYTES) {
|
|
1130
|
+
flush();
|
|
1131
|
+
} else {
|
|
1132
|
+
scheduleFlush();
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (isCurrent()) {
|
|
1136
|
+
flush();
|
|
1137
|
+
setConnectionState("ended");
|
|
1138
|
+
}
|
|
1139
|
+
} catch (cause) {
|
|
1140
|
+
if (isCurrent()) {
|
|
1141
|
+
flush();
|
|
1142
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
1143
|
+
setConnectionState("error");
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
})();
|
|
1147
|
+
return () => {
|
|
1148
|
+
controller.abort();
|
|
1149
|
+
if (generationRef.current === generation) {
|
|
1150
|
+
generationRef.current = generation + 1;
|
|
1151
|
+
}
|
|
1152
|
+
if (streamAbortRef.current === controller) {
|
|
1153
|
+
streamAbortRef.current = null;
|
|
1154
|
+
}
|
|
1155
|
+
if (flushTimer !== null) {
|
|
1156
|
+
clearTimeout(flushTimer);
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
}, [
|
|
1160
|
+
client,
|
|
1161
|
+
workspaceId,
|
|
1162
|
+
sessionId,
|
|
1163
|
+
after,
|
|
1164
|
+
enabled,
|
|
1165
|
+
fullReplay,
|
|
1166
|
+
streamKey,
|
|
1167
|
+
streamEpoch,
|
|
1168
|
+
reconcileSession
|
|
1169
|
+
]);
|
|
1170
|
+
const loadOlder = useCallback(async () => {
|
|
1171
|
+
if (!sessionId || loadingOlderRef.current || !hasOlderRef.current) {
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
const before = oldestSequenceRef.current;
|
|
1175
|
+
if (before === null) {
|
|
1176
|
+
hasOlderRef.current = false;
|
|
1177
|
+
setHasOlder(false);
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
const generation = generationRef.current;
|
|
1181
|
+
loadingOlderRef.current = true;
|
|
1182
|
+
setLoadingOlder(true);
|
|
1183
|
+
try {
|
|
1184
|
+
const window = await loadEventWindow(client, workspaceId, sessionId, {
|
|
1185
|
+
before,
|
|
1186
|
+
pageSize: OLDER_PAGE_SIZE,
|
|
1187
|
+
targetGroups: OLDER_GROUP_TARGET,
|
|
1188
|
+
maxFetches: OLDER_FETCH_CAP
|
|
1189
|
+
});
|
|
1190
|
+
if (generationRef.current !== generation) {
|
|
1191
|
+
return false;
|
|
1192
|
+
}
|
|
1193
|
+
if (window.events.length === 0) {
|
|
1194
|
+
oldestSequenceRef.current = null;
|
|
1195
|
+
hasOlderRef.current = false;
|
|
1196
|
+
setHasOlder(false);
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
const current = eventWindowRef.current;
|
|
1200
|
+
assertPrependOrder(current.events, window.events);
|
|
1201
|
+
streamAbortRef.current?.abort();
|
|
1202
|
+
observeSessionStatus(window.events, sessionStatusRef, setSessionStatusProjection);
|
|
1203
|
+
const next = boundBrowserSessionEventWindow([...window.events, ...current.events], {
|
|
1204
|
+
direction: "oldest"
|
|
1205
|
+
});
|
|
1206
|
+
const retained = {
|
|
1207
|
+
...next,
|
|
1208
|
+
truncated: current.truncated || next.truncated
|
|
1209
|
+
};
|
|
1210
|
+
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
1211
|
+
if (retainedOldest === null || retainedOldest >= before) {
|
|
1212
|
+
throw new Error("@opengeni/react: loadOlder made no durable sequence progress");
|
|
1213
|
+
}
|
|
1214
|
+
eventWindowRef.current = retained;
|
|
1215
|
+
setEventWindow(retained);
|
|
1216
|
+
oldestSequenceRef.current = retainedOldest;
|
|
1217
|
+
streamResumeSequenceRef.current = maxResumeSequence(retained.events);
|
|
1218
|
+
const olderStillAvailable = window.hasOlder;
|
|
1219
|
+
hasOlderRef.current = olderStillAvailable;
|
|
1220
|
+
setHasOlder(olderStillAvailable);
|
|
1221
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
1222
|
+
return olderStillAvailable;
|
|
1223
|
+
} finally {
|
|
1224
|
+
if (generationRef.current === generation) {
|
|
1225
|
+
loadingOlderRef.current = false;
|
|
1226
|
+
setLoadingOlder(false);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}, [client, workspaceId, sessionId]);
|
|
1230
|
+
const identityMatches = stateStreamKey === streamKey;
|
|
1231
|
+
const visibleEvents = identityMatches ? eventWindow.events : EMPTY_EVENTS;
|
|
1232
|
+
const timeline = useMemo(() => buildTimeline(visibleEvents), [visibleEvents]);
|
|
1233
|
+
return {
|
|
1234
|
+
events: visibleEvents,
|
|
1235
|
+
timeline,
|
|
1236
|
+
sessionStatus: identityMatches ? sessionStatusProjection : null,
|
|
1237
|
+
connectionState: identityMatches ? connectionState : "idle",
|
|
1238
|
+
lastSequence: identityMatches ? lastSequenceRef.current : after,
|
|
1239
|
+
windowBytes: identityMatches ? eventWindow.bytes : 2,
|
|
1240
|
+
windowTruncated: identityMatches ? eventWindow.truncated : false,
|
|
1241
|
+
initialLoading: fullReplay ? false : identityMatches ? initialLoading : true,
|
|
1242
|
+
hasOlder: !identityMatches ? false : hasOlder,
|
|
1243
|
+
loadingOlder: !identityMatches ? false : loadingOlder,
|
|
1244
|
+
loadOlder,
|
|
1245
|
+
error: identityMatches ? error : null
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
function boundBrowserSessionEventWindow(events, options = {}) {
|
|
1249
|
+
const maxBytes = Math.max(1024, options.maxBytes ?? SESSION_EVENT_BROWSER_MAX_BYTES);
|
|
1250
|
+
const maxCount = Math.max(1, Math.floor(options.maxCount ?? SESSION_EVENT_BROWSER_MAX_COUNT));
|
|
1251
|
+
const safe = events.map(boundBrowserLegacyEvent);
|
|
1252
|
+
const selected = [];
|
|
1253
|
+
let bytes = 2;
|
|
1254
|
+
const direction = options.direction ?? "newest";
|
|
1255
|
+
const start = direction === "newest" ? safe.length - 1 : 0;
|
|
1256
|
+
const end = direction === "newest" ? Math.max(-1, safe.length - maxCount - 1) : safe.length;
|
|
1257
|
+
const step = direction === "newest" ? -1 : 1;
|
|
1258
|
+
for (let index = start; index !== end && selected.length < maxCount; index += step) {
|
|
1259
|
+
const event = safe[index];
|
|
1260
|
+
const eventBytes = browserJsonBytes(event);
|
|
1261
|
+
const separator = selected.length === 0 ? 0 : 1;
|
|
1262
|
+
if (bytes + separator + eventBytes > maxBytes) break;
|
|
1263
|
+
selected.push(event);
|
|
1264
|
+
bytes += separator + eventBytes;
|
|
1265
|
+
}
|
|
1266
|
+
if (direction === "newest") selected.reverse();
|
|
1267
|
+
return {
|
|
1268
|
+
events: selected,
|
|
1269
|
+
bytes,
|
|
1270
|
+
truncated: selected.length < safe.length
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function boundBrowserLegacyEvent(event) {
|
|
1274
|
+
const serialized = browserSerialize(event);
|
|
1275
|
+
const originalBytes = serialized.serializable ? encoder.encode(serialized.value).byteLength : null;
|
|
1276
|
+
const typeIsSafe = browserUtf8Bytes(event.type) <= BROWSER_EVENT_TYPE_MAX_BYTES && !event.type.includes("\n") && !event.type.includes("\r");
|
|
1277
|
+
const clientEventId = boundBrowserOptionalText(
|
|
1278
|
+
event.clientEventId,
|
|
1279
|
+
BROWSER_EVENT_CLIENT_ID_MAX_BYTES
|
|
1280
|
+
);
|
|
1281
|
+
const duplicateReason = boundBrowserOptionalText(
|
|
1282
|
+
event.duplicateReason,
|
|
1283
|
+
BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES
|
|
1284
|
+
);
|
|
1285
|
+
if (serialized.serializable && originalBytes !== null && originalBytes <= SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES && typeIsSafe && clientEventId === event.clientEventId && duplicateReason === event.duplicateReason) {
|
|
1286
|
+
return event;
|
|
1287
|
+
}
|
|
1288
|
+
const envelopeProjection = [
|
|
1289
|
+
!typeIsSafe ? browserEnvelopeFieldProjection("type", event.type, "session.event.envelope_omitted") : null,
|
|
1290
|
+
clientEventId !== event.clientEventId ? browserEnvelopeFieldProjection("clientEventId", event.clientEventId, clientEventId) : null,
|
|
1291
|
+
duplicateReason !== event.duplicateReason ? browserEnvelopeFieldProjection("duplicateReason", event.duplicateReason, duplicateReason) : null
|
|
1292
|
+
].filter((field) => field !== null);
|
|
1293
|
+
const payloadSerialization = browserSerialize(event.payload);
|
|
1294
|
+
const payloadBytes = payloadSerialization.serializable ? encoder.encode(payloadSerialization.value).byteLength : null;
|
|
1295
|
+
const preview = truncateBrowserUtf8Middle(
|
|
1296
|
+
payloadSerialization.value,
|
|
1297
|
+
BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES
|
|
1298
|
+
);
|
|
1299
|
+
const truncation = {
|
|
1300
|
+
truncated: true,
|
|
1301
|
+
surface: "browser_legacy_guard",
|
|
1302
|
+
reason: serialized.serializable ? "event_envelope_bytes_exceeded" : "event_not_serializable",
|
|
1303
|
+
originalBytes,
|
|
1304
|
+
deliveredBytes: 0,
|
|
1305
|
+
omittedBytes: originalBytes,
|
|
1306
|
+
estimatedOriginalTokens: originalBytes === null ? null : Math.ceil(originalBytes / 4),
|
|
1307
|
+
estimatedDeliveredTokens: 0,
|
|
1308
|
+
fullEvidence: { available: false, reason: "not_retained" },
|
|
1309
|
+
details: [
|
|
1310
|
+
{
|
|
1311
|
+
path: "$.payload",
|
|
1312
|
+
kind: "payload_preview",
|
|
1313
|
+
originalBytes: payloadBytes,
|
|
1314
|
+
deliveredBytes: browserUtf8Bytes(preview)
|
|
1315
|
+
}
|
|
1316
|
+
]
|
|
1317
|
+
};
|
|
1318
|
+
const payload = {
|
|
1319
|
+
...browserPayloadIdentity(event.payload),
|
|
1320
|
+
preview,
|
|
1321
|
+
...envelopeProjection.length > 0 ? {
|
|
1322
|
+
originalType: boundBrowserText(event.type, BROWSER_EVENT_TYPE_MAX_BYTES),
|
|
1323
|
+
envelopeProjection: {
|
|
1324
|
+
truncated: true,
|
|
1325
|
+
surface: "browser_legacy_guard",
|
|
1326
|
+
fields: envelopeProjection
|
|
1327
|
+
}
|
|
1328
|
+
} : {},
|
|
1329
|
+
truncation
|
|
1330
|
+
};
|
|
1331
|
+
const bounded = {
|
|
1332
|
+
id: boundBrowserText(event.id, BROWSER_EVENT_ID_MAX_BYTES),
|
|
1333
|
+
workspaceId: boundBrowserText(event.workspaceId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
1334
|
+
sessionId: boundBrowserText(event.sessionId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
1335
|
+
sequence: event.sequence,
|
|
1336
|
+
type: typeIsSafe ? event.type : "session.event.envelope_omitted",
|
|
1337
|
+
payload,
|
|
1338
|
+
occurredAt: boundBrowserText(event.occurredAt, BROWSER_EVENT_ID_MAX_BYTES),
|
|
1339
|
+
clientEventId,
|
|
1340
|
+
turnId: boundBrowserOptionalText(event.turnId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
1341
|
+
turnGeneration: event.turnGeneration,
|
|
1342
|
+
turnAttemptId: boundBrowserOptionalText(event.turnAttemptId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
1343
|
+
turnAssociation: event.turnAssociation,
|
|
1344
|
+
duplicateOfEventId: boundBrowserOptionalText(
|
|
1345
|
+
event.duplicateOfEventId,
|
|
1346
|
+
BROWSER_EVENT_ID_MAX_BYTES
|
|
1347
|
+
),
|
|
1348
|
+
duplicateReason
|
|
1349
|
+
};
|
|
1350
|
+
settleBrowserEventTruncation(bounded, truncation);
|
|
1351
|
+
if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
|
|
1352
|
+
payload.preview = truncateBrowserUtf8Middle(String(payload.preview), 4 * 1024);
|
|
1353
|
+
truncation.details = truncation.details.slice(0, 1);
|
|
1354
|
+
settleBrowserEventTruncation(bounded, truncation);
|
|
1355
|
+
}
|
|
1356
|
+
if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
|
|
1357
|
+
bounded.clientEventId = null;
|
|
1358
|
+
bounded.duplicateReason = null;
|
|
1359
|
+
payload.preview = "[legacy event omitted at the browser byte boundary]";
|
|
1360
|
+
settleBrowserEventTruncation(bounded, truncation);
|
|
1361
|
+
}
|
|
1362
|
+
return bounded;
|
|
1363
|
+
}
|
|
1364
|
+
function browserJsonBytes(value) {
|
|
1365
|
+
return encoder.encode(browserSerialize(value).value).byteLength;
|
|
1366
|
+
}
|
|
1367
|
+
function browserSerialize(value) {
|
|
1368
|
+
try {
|
|
1369
|
+
const serialized = JSON.stringify(value);
|
|
1370
|
+
return { value: serialized === void 0 ? "null" : serialized, serializable: true };
|
|
1371
|
+
} catch {
|
|
1372
|
+
return {
|
|
1373
|
+
value: '"[unserializable event payload omitted at browser boundary]"',
|
|
1374
|
+
serializable: false
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function browserPayloadIdentity(payload) {
|
|
1379
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return {};
|
|
1380
|
+
const record = payload;
|
|
1381
|
+
const identity = {};
|
|
1382
|
+
for (const field of BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS) {
|
|
1383
|
+
const value = record[field];
|
|
1384
|
+
if (typeof value === "string") {
|
|
1385
|
+
identity[field] = boundBrowserText(value, BROWSER_EVENT_ID_MAX_BYTES);
|
|
1386
|
+
} else if (typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
1387
|
+
identity[field] = value;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return identity;
|
|
1391
|
+
}
|
|
1392
|
+
function browserEnvelopeFieldProjection(field, original, delivered) {
|
|
1393
|
+
return {
|
|
1394
|
+
field,
|
|
1395
|
+
originalBytes: typeof original === "string" ? browserUtf8Bytes(original) : 0,
|
|
1396
|
+
deliveredBytes: typeof delivered === "string" ? browserUtf8Bytes(delivered) : 0
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function boundBrowserOptionalText(value, maxBytes) {
|
|
1400
|
+
return typeof value === "string" ? boundBrowserText(value, maxBytes) : value;
|
|
1401
|
+
}
|
|
1402
|
+
function boundBrowserText(value, maxBytes) {
|
|
1403
|
+
const bytes = encoder.encode(value);
|
|
1404
|
+
if (bytes.byteLength <= maxBytes) return value;
|
|
1405
|
+
const marker = "\u2026[truncated]";
|
|
1406
|
+
const prefixBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
|
|
1407
|
+
let prefixEnd = Math.min(prefixBudget, bytes.byteLength);
|
|
1408
|
+
while (prefixEnd > 0 && prefixEnd < bytes.byteLength && isBrowserUtf8Continuation(bytes[prefixEnd])) {
|
|
1409
|
+
prefixEnd -= 1;
|
|
1410
|
+
}
|
|
1411
|
+
return `${decoder.decode(bytes.subarray(0, prefixEnd))}${marker}`;
|
|
1412
|
+
}
|
|
1413
|
+
function truncateBrowserUtf8Middle(value, maxBytes) {
|
|
1414
|
+
const bytes = encoder.encode(value);
|
|
1415
|
+
if (bytes.byteLength <= maxBytes) return value;
|
|
1416
|
+
const marker = `\u2026[${bytes.byteLength - maxBytes} bytes omitted]\u2026`;
|
|
1417
|
+
const contentBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
|
|
1418
|
+
const leftBudget = Math.floor(contentBudget / 2);
|
|
1419
|
+
const rightBudget = contentBudget - leftBudget;
|
|
1420
|
+
let leftEnd = Math.min(leftBudget, bytes.byteLength);
|
|
1421
|
+
while (leftEnd > 0 && leftEnd < bytes.byteLength && isBrowserUtf8Continuation(bytes[leftEnd])) {
|
|
1422
|
+
leftEnd -= 1;
|
|
1423
|
+
}
|
|
1424
|
+
let rightStart = Math.max(0, bytes.byteLength - rightBudget);
|
|
1425
|
+
while (rightStart < bytes.byteLength && isBrowserUtf8Continuation(bytes[rightStart])) {
|
|
1426
|
+
rightStart += 1;
|
|
1427
|
+
}
|
|
1428
|
+
return `${decoder.decode(bytes.subarray(0, leftEnd))}${marker}${decoder.decode(bytes.subarray(rightStart))}`;
|
|
1429
|
+
}
|
|
1430
|
+
function settleBrowserEventTruncation(event, truncation) {
|
|
1431
|
+
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
1432
|
+
const deliveredBytes = browserJsonBytes(event);
|
|
1433
|
+
const omittedBytes = truncation.originalBytes === null ? null : Math.max(0, truncation.originalBytes - deliveredBytes);
|
|
1434
|
+
const estimatedDeliveredTokens = Math.ceil(deliveredBytes / 4);
|
|
1435
|
+
if (truncation.deliveredBytes === deliveredBytes && truncation.omittedBytes === omittedBytes && truncation.estimatedDeliveredTokens === estimatedDeliveredTokens) {
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
truncation.deliveredBytes = deliveredBytes;
|
|
1439
|
+
truncation.omittedBytes = omittedBytes;
|
|
1440
|
+
truncation.estimatedDeliveredTokens = estimatedDeliveredTokens;
|
|
1441
|
+
}
|
|
1442
|
+
throw new RangeError("Browser event byte accounting did not converge");
|
|
1443
|
+
}
|
|
1444
|
+
function browserUtf8Bytes(value) {
|
|
1445
|
+
return encoder.encode(value).byteLength;
|
|
1446
|
+
}
|
|
1447
|
+
function isBrowserUtf8Continuation(value) {
|
|
1448
|
+
return (value & 192) === 128;
|
|
1449
|
+
}
|
|
1450
|
+
function observeSessionStatus(events, ref, setStatus) {
|
|
1451
|
+
let latest = null;
|
|
1452
|
+
for (const event of events) {
|
|
1453
|
+
const status = sessionStatusFromEvents([event]);
|
|
1454
|
+
if (status && (!latest || event.sequence >= latest.sequence)) {
|
|
1455
|
+
latest = { sequence: event.sequence, status };
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
if (!latest || latest.sequence < ref.current.sequence) return;
|
|
1459
|
+
ref.current = latest;
|
|
1460
|
+
setStatus(latest.status);
|
|
1461
|
+
}
|
|
1462
|
+
async function loadEventWindow(client, workspaceId, sessionId, options) {
|
|
1463
|
+
let cursor = options.before;
|
|
1464
|
+
let buffer = [];
|
|
1465
|
+
let reachedStart = false;
|
|
1466
|
+
let fetches = 0;
|
|
1467
|
+
while (fetches < options.maxFetches) {
|
|
1468
|
+
if (buffer.length > 0 && groupCount(buffer) >= options.targetGroups) {
|
|
1469
|
+
break;
|
|
1470
|
+
}
|
|
1471
|
+
const page = await loadPreviousPage(client, workspaceId, sessionId, cursor, {
|
|
1472
|
+
pageSize: options.pageSize,
|
|
1473
|
+
...options.signal ? { signal: options.signal } : {}
|
|
1474
|
+
});
|
|
1475
|
+
fetches += 1;
|
|
1476
|
+
if (page.length === 0) {
|
|
1477
|
+
reachedStart = true;
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
assertAscending(page);
|
|
1481
|
+
buffer = [...page, ...buffer];
|
|
1482
|
+
cursor = page[0].sequence;
|
|
1483
|
+
if (isLogStart(page[0])) {
|
|
1484
|
+
reachedStart = true;
|
|
1485
|
+
break;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
let snapPages = 0;
|
|
1489
|
+
while (!reachedStart && findBoundaryIndex(buffer) === -1 && snapPages < BOUNDARY_PAGE_CAP && fetches < options.maxFetches) {
|
|
1490
|
+
const page = await loadPreviousPage(client, workspaceId, sessionId, cursor, {
|
|
1491
|
+
pageSize: options.pageSize,
|
|
1492
|
+
...options.signal ? { signal: options.signal } : {}
|
|
1493
|
+
});
|
|
1494
|
+
fetches += 1;
|
|
1495
|
+
snapPages += 1;
|
|
1496
|
+
if (page.length === 0) {
|
|
1497
|
+
reachedStart = true;
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
assertAscending(page);
|
|
1501
|
+
buffer = [...page, ...buffer];
|
|
1502
|
+
cursor = page[0].sequence;
|
|
1503
|
+
if (isLogStart(page[0])) {
|
|
1504
|
+
reachedStart = true;
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (!reachedStart) {
|
|
1509
|
+
const boundary = findBoundaryIndex(buffer);
|
|
1510
|
+
if (boundary > 0) {
|
|
1511
|
+
buffer = buffer.slice(boundary);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
const oldest = buffer[0] ?? null;
|
|
1515
|
+
const newest = buffer[buffer.length - 1] ?? null;
|
|
1516
|
+
return {
|
|
1517
|
+
events: buffer,
|
|
1518
|
+
oldestSequence: oldest?.sequence ?? null,
|
|
1519
|
+
newestSequence: newest ? maxResumeSequence(buffer) : 0,
|
|
1520
|
+
hasOlder: buffer.length > 0 && !reachedStart && oldest?.type !== "session.created"
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
function findBoundaryIndex(events) {
|
|
1524
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
1525
|
+
const type = events[index].type;
|
|
1526
|
+
if (type === "session.created" || type === "user.message") {
|
|
1527
|
+
return index;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
return -1;
|
|
1531
|
+
}
|
|
1532
|
+
async function loadPreviousPage(client, workspaceId, sessionId, before, options) {
|
|
1533
|
+
if (options.signal?.aborted) {
|
|
1534
|
+
throw abortError();
|
|
1535
|
+
}
|
|
1536
|
+
const requested = options.pageSize;
|
|
1537
|
+
if (requested === 0) {
|
|
1538
|
+
return Object.assign([], { requested });
|
|
1539
|
+
}
|
|
1540
|
+
const page = await client.listEvents(workspaceId, sessionId, {
|
|
1541
|
+
before,
|
|
1542
|
+
limit: requested,
|
|
1543
|
+
compact: true
|
|
1544
|
+
});
|
|
1545
|
+
if (options.signal?.aborted) {
|
|
1546
|
+
throw abortError();
|
|
1547
|
+
}
|
|
1548
|
+
return Object.assign(page, { requested });
|
|
1549
|
+
}
|
|
1550
|
+
function groupCount(events) {
|
|
1551
|
+
if (events.length === 0) {
|
|
1552
|
+
return 0;
|
|
1553
|
+
}
|
|
1554
|
+
return groupTimeline(buildTimeline(events)).length;
|
|
1555
|
+
}
|
|
1556
|
+
function isLogStart(event) {
|
|
1557
|
+
return event.type === "session.created" || event.sequence <= 1;
|
|
1558
|
+
}
|
|
1559
|
+
function maxResumeSequence(events) {
|
|
1560
|
+
return events.reduce((max, event) => Math.max(max, eventResumeSequence(event)), 0);
|
|
1561
|
+
}
|
|
1562
|
+
function eventResumeSequence(event) {
|
|
1563
|
+
const payload = asRecord2(event.payload);
|
|
1564
|
+
const coalescedUntil = Number(payload.coalescedUntil);
|
|
1565
|
+
return Math.max(event.sequence, Number.isFinite(coalescedUntil) ? Math.floor(coalescedUntil) : 0);
|
|
1566
|
+
}
|
|
1567
|
+
function asRecord2(value) {
|
|
1568
|
+
return value && typeof value === "object" ? value : {};
|
|
1569
|
+
}
|
|
1570
|
+
function assertAscending(events) {
|
|
1571
|
+
for (let index = 1; index < events.length; index += 1) {
|
|
1572
|
+
if (events[index - 1].sequence >= events[index].sequence) {
|
|
1573
|
+
throw new Error("@opengeni/react: session events must be ordered by ascending sequence");
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function assertPrependOrder(existing, older) {
|
|
1578
|
+
if (!shouldAssertDevelopment() || existing.length === 0 || older.length === 0) {
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
if (older[older.length - 1].sequence >= existing[0].sequence) {
|
|
1582
|
+
throw new Error("@opengeni/react: loadOlder returned overlapping session events");
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
function shouldAssertDevelopment() {
|
|
1586
|
+
const processEnv = globalThis.process?.env;
|
|
1587
|
+
return processEnv !== void 0 && processEnv.NODE_ENV !== "production";
|
|
1588
|
+
}
|
|
1589
|
+
function abortError() {
|
|
1590
|
+
const error = new Error("The operation was aborted.");
|
|
1591
|
+
error.name = "AbortError";
|
|
1592
|
+
return error;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/hooks/use-turn-queue.ts
|
|
1596
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useLayoutEffect, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
|
|
1597
|
+
function isTurnQueueEvent(event) {
|
|
1598
|
+
return event.type.startsWith("turn.") || event.type.startsWith("session.queue.") || event.type.startsWith("session.control.") || event.type.startsWith("workspace.inference.");
|
|
1599
|
+
}
|
|
1600
|
+
function queueSnapshotCovers(candidate, observed) {
|
|
1601
|
+
return Boolean(
|
|
1602
|
+
candidate && candidate.version >= observed.version && candidate.effectiveControl.controlVersion >= observed.effectiveControl.controlVersion
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
var EMPTY_PENDING_BY_TURN = {};
|
|
1606
|
+
function useTurnQueue(sessionId, options = {}) {
|
|
1607
|
+
const { client, workspaceId, workspaceControlEvent, registerSessionReconciler } = useEmbeddedSession(options);
|
|
1608
|
+
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
1609
|
+
const targetKey = `${workspaceId}\0${sessionId ?? ""}`;
|
|
1610
|
+
const [snapshot, setSnapshot] = useState2(null);
|
|
1611
|
+
const [stateTargetKey, setStateTargetKey] = useState2(targetKey);
|
|
1612
|
+
const [loading, setLoading] = useState2(enabled);
|
|
1613
|
+
const [error, setError] = useState2(null);
|
|
1614
|
+
const [mutationError, setMutationError] = useState2(null);
|
|
1615
|
+
const [pendingByTurn, setPendingByTurn] = useState2({});
|
|
1616
|
+
const pendingRef = useRef2({});
|
|
1617
|
+
const readGeneration = useRef2(0);
|
|
1618
|
+
const targetKeyRef = useRef2(targetKey);
|
|
1619
|
+
const snapshotRef = useRef2(null);
|
|
1620
|
+
useLayoutEffect(() => {
|
|
1621
|
+
if (targetKeyRef.current === targetKey) return;
|
|
1622
|
+
targetKeyRef.current = targetKey;
|
|
1623
|
+
readGeneration.current += 1;
|
|
1624
|
+
snapshotRef.current = null;
|
|
1625
|
+
pendingRef.current = {};
|
|
1626
|
+
setStateTargetKey(targetKey);
|
|
1627
|
+
setSnapshot(null);
|
|
1628
|
+
setLoading(enabled);
|
|
1629
|
+
setError(null);
|
|
1630
|
+
setMutationError(null);
|
|
1631
|
+
setPendingByTurn({});
|
|
1632
|
+
}, [enabled, targetKey]);
|
|
1633
|
+
const acceptSnapshot = useCallback2(
|
|
1634
|
+
(ownedTargetKey, next) => {
|
|
1635
|
+
if (targetKeyRef.current !== ownedTargetKey) return false;
|
|
1636
|
+
const current = snapshotRef.current;
|
|
1637
|
+
if (next && current && (next.version < current.version || next.effectiveControl.controlVersion < current.effectiveControl.controlVersion)) {
|
|
1638
|
+
return false;
|
|
1639
|
+
}
|
|
1640
|
+
snapshotRef.current = next;
|
|
1641
|
+
setStateTargetKey(ownedTargetKey);
|
|
1642
|
+
setSnapshot(next);
|
|
1643
|
+
return true;
|
|
1644
|
+
},
|
|
1645
|
+
[]
|
|
1646
|
+
);
|
|
1647
|
+
const load = useCallback2(
|
|
1648
|
+
async (rejectOnFailure = false) => {
|
|
1649
|
+
if (!sessionId) return;
|
|
1650
|
+
const ownedTargetKey = `${workspaceId}\0${sessionId}`;
|
|
1651
|
+
const ticket = ++readGeneration.current;
|
|
1652
|
+
try {
|
|
1653
|
+
const fetched = await client.getQueue(workspaceId, sessionId);
|
|
1654
|
+
if (targetKeyRef.current !== ownedTargetKey) return;
|
|
1655
|
+
const ownsLatestRead = ticket === readGeneration.current;
|
|
1656
|
+
if (rejectOnFailure) {
|
|
1657
|
+
const committed = acceptSnapshot(ownedTargetKey, fetched);
|
|
1658
|
+
if (!committed && !queueSnapshotCovers(snapshotRef.current, fetched)) {
|
|
1659
|
+
throw new TypeError("Queue reconciliation did not commit authoritative state");
|
|
1660
|
+
}
|
|
1661
|
+
setError(null);
|
|
1662
|
+
if (ownsLatestRead) setLoading(false);
|
|
1663
|
+
} else if (ownsLatestRead) {
|
|
1664
|
+
acceptSnapshot(ownedTargetKey, fetched);
|
|
1665
|
+
setError(null);
|
|
1666
|
+
setLoading(false);
|
|
1667
|
+
}
|
|
1668
|
+
} catch (cause) {
|
|
1669
|
+
if (targetKeyRef.current === ownedTargetKey && (ticket === readGeneration.current || rejectOnFailure)) {
|
|
1670
|
+
setError(asError(cause));
|
|
1671
|
+
if (ticket === readGeneration.current) setLoading(false);
|
|
1672
|
+
}
|
|
1673
|
+
if (rejectOnFailure) throw cause;
|
|
1674
|
+
}
|
|
1675
|
+
},
|
|
1676
|
+
[client, workspaceId, sessionId, acceptSnapshot]
|
|
1677
|
+
);
|
|
1678
|
+
useEffect2(() => {
|
|
1679
|
+
if (!enabled) {
|
|
1680
|
+
setLoading(false);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
setLoading(true);
|
|
1684
|
+
void load();
|
|
1685
|
+
const pollIntervalMs = options.pollIntervalMs;
|
|
1686
|
+
if (pollIntervalMs === void 0 || pollIntervalMs <= 0) {
|
|
1687
|
+
return () => {
|
|
1688
|
+
readGeneration.current += 1;
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
const timer = setInterval(() => void load(), pollIntervalMs);
|
|
1692
|
+
return () => {
|
|
1693
|
+
clearInterval(timer);
|
|
1694
|
+
readGeneration.current += 1;
|
|
1695
|
+
};
|
|
1696
|
+
}, [load, enabled, options.pollIntervalMs]);
|
|
1697
|
+
useEffect2(() => {
|
|
1698
|
+
if (enabled && workspaceControlEvent) void load();
|
|
1699
|
+
}, [enabled, load, workspaceControlEvent]);
|
|
1700
|
+
useEffect2(() => {
|
|
1701
|
+
if (!sessionId || !enabled) return;
|
|
1702
|
+
return registerSessionReconciler(sessionId, "queue", load);
|
|
1703
|
+
}, [enabled, load, registerSessionReconciler, sessionId]);
|
|
1704
|
+
const scheduleRefresh = useDebouncedCallback(() => void load());
|
|
1705
|
+
useSessionEventTrigger(
|
|
1706
|
+
client,
|
|
1707
|
+
workspaceId,
|
|
1708
|
+
sessionId,
|
|
1709
|
+
isTurnQueueEvent,
|
|
1710
|
+
scheduleRefresh,
|
|
1711
|
+
{
|
|
1712
|
+
enabled,
|
|
1713
|
+
...options.events !== void 0 ? { events: options.events } : {}
|
|
1714
|
+
},
|
|
1715
|
+
async () => await load(true)
|
|
1716
|
+
);
|
|
1717
|
+
const mutate = useCallback2(
|
|
1718
|
+
async (turnId, kind, command) => {
|
|
1719
|
+
const ownedTargetKey = targetKey;
|
|
1720
|
+
if (!sessionId || targetKeyRef.current !== ownedTargetKey || pendingRef.current[turnId]) {
|
|
1721
|
+
return null;
|
|
1722
|
+
}
|
|
1723
|
+
const current = snapshotRef.current;
|
|
1724
|
+
const turn = current?.items.find((candidate) => candidate.id === turnId);
|
|
1725
|
+
if (!current || !turn) return null;
|
|
1726
|
+
pendingRef.current = { ...pendingRef.current, [turnId]: kind };
|
|
1727
|
+
setStateTargetKey(ownedTargetKey);
|
|
1728
|
+
setPendingByTurn(pendingRef.current);
|
|
1729
|
+
setMutationError(null);
|
|
1730
|
+
try {
|
|
1731
|
+
const result = await command(current, turn);
|
|
1732
|
+
if (targetKeyRef.current !== ownedTargetKey) return null;
|
|
1733
|
+
acceptSnapshot(ownedTargetKey, result.snapshot);
|
|
1734
|
+
return result;
|
|
1735
|
+
} catch (cause) {
|
|
1736
|
+
if (targetKeyRef.current === ownedTargetKey) {
|
|
1737
|
+
setMutationError(asError(cause));
|
|
1738
|
+
await load();
|
|
1739
|
+
}
|
|
1740
|
+
return null;
|
|
1741
|
+
} finally {
|
|
1742
|
+
if (targetKeyRef.current === ownedTargetKey && turnId in pendingRef.current) {
|
|
1743
|
+
const next = { ...pendingRef.current };
|
|
1744
|
+
delete next[turnId];
|
|
1745
|
+
pendingRef.current = next;
|
|
1746
|
+
setPendingByTurn(next);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
},
|
|
1750
|
+
[acceptSnapshot, load, sessionId, targetKey]
|
|
1751
|
+
);
|
|
1752
|
+
const moveTurn = useCallback2(
|
|
1753
|
+
async (turnId, beforeTurnId) => {
|
|
1754
|
+
const result = await mutate(
|
|
1755
|
+
turnId,
|
|
1756
|
+
"move",
|
|
1757
|
+
(current) => client.moveQueueItem(workspaceId, sessionId, turnId, {
|
|
1758
|
+
clientEventId: operationKey(),
|
|
1759
|
+
expectedQueueVersion: current.version,
|
|
1760
|
+
beforeTurnId
|
|
1761
|
+
})
|
|
1762
|
+
);
|
|
1763
|
+
return result !== null;
|
|
1764
|
+
},
|
|
1765
|
+
[client, mutate, sessionId, workspaceId]
|
|
1766
|
+
);
|
|
1767
|
+
const editTurn = useCallback2(
|
|
1768
|
+
async (turnId, edit) => {
|
|
1769
|
+
const result = await mutate(
|
|
1770
|
+
turnId,
|
|
1771
|
+
"edit",
|
|
1772
|
+
(_current, turn) => client.editQueueItem(workspaceId, sessionId, turnId, {
|
|
1773
|
+
clientEventId: operationKey(),
|
|
1774
|
+
expectedTurnVersion: turn.version,
|
|
1775
|
+
expectedDraftRevision: edit.expectedDraftRevision,
|
|
1776
|
+
replaceDraft: edit.replaceDraft
|
|
1777
|
+
})
|
|
1778
|
+
);
|
|
1779
|
+
return result?.draft ?? null;
|
|
1780
|
+
},
|
|
1781
|
+
[client, mutate, sessionId, workspaceId]
|
|
1782
|
+
);
|
|
1783
|
+
const steerTurn = useCallback2(
|
|
1784
|
+
async (turnId) => {
|
|
1785
|
+
const result = await mutate(
|
|
1786
|
+
turnId,
|
|
1787
|
+
"steer",
|
|
1788
|
+
(current, turn) => client.steerQueueItem(workspaceId, sessionId, turnId, {
|
|
1789
|
+
clientEventId: operationKey(),
|
|
1790
|
+
expectedTurnVersion: turn.version,
|
|
1791
|
+
controlEtag: current.effectiveControl.controlEtag
|
|
1792
|
+
})
|
|
1793
|
+
);
|
|
1794
|
+
return result !== null;
|
|
1795
|
+
},
|
|
1796
|
+
[client, mutate, sessionId, workspaceId]
|
|
1797
|
+
);
|
|
1798
|
+
const removeTurn = useCallback2(
|
|
1799
|
+
async (turnId) => {
|
|
1800
|
+
const result = await mutate(
|
|
1801
|
+
turnId,
|
|
1802
|
+
"delete",
|
|
1803
|
+
(_current, turn) => client.deleteQueueItem(workspaceId, sessionId, turnId, {
|
|
1804
|
+
clientEventId: operationKey(),
|
|
1805
|
+
expectedTurnVersion: turn.version,
|
|
1806
|
+
reason: "Deleted from the prompt queue"
|
|
1807
|
+
})
|
|
1808
|
+
);
|
|
1809
|
+
return result !== null;
|
|
1810
|
+
},
|
|
1811
|
+
[client, mutate, sessionId, workspaceId]
|
|
1812
|
+
);
|
|
1813
|
+
const identityMatches = stateTargetKey === targetKey;
|
|
1814
|
+
const visibleSnapshot = identityMatches ? snapshot : null;
|
|
1815
|
+
const visiblePendingByTurn = identityMatches ? pendingByTurn : EMPTY_PENDING_BY_TURN;
|
|
1816
|
+
const mutating = useMemo2(
|
|
1817
|
+
() => Object.keys(visiblePendingByTurn).length > 0,
|
|
1818
|
+
[visiblePendingByTurn]
|
|
1819
|
+
);
|
|
1820
|
+
const mutationFor = useCallback2(
|
|
1821
|
+
(turnId) => visiblePendingByTurn[turnId] ?? null,
|
|
1822
|
+
[visiblePendingByTurn]
|
|
1823
|
+
);
|
|
1824
|
+
return {
|
|
1825
|
+
snapshot: visibleSnapshot,
|
|
1826
|
+
queue: visibleSnapshot?.items ?? [],
|
|
1827
|
+
effectiveControl: visibleSnapshot?.effectiveControl ?? null,
|
|
1828
|
+
stoppingPreviousAttempt: visibleSnapshot?.stoppingPreviousAttempt ?? false,
|
|
1829
|
+
loading: identityMatches ? loading : enabled,
|
|
1830
|
+
error: identityMatches ? error : null,
|
|
1831
|
+
refresh: load,
|
|
1832
|
+
moveTurn,
|
|
1833
|
+
editTurn,
|
|
1834
|
+
steerTurn,
|
|
1835
|
+
removeTurn,
|
|
1836
|
+
pendingByTurn: visiblePendingByTurn,
|
|
1837
|
+
mutationFor,
|
|
1838
|
+
mutating,
|
|
1839
|
+
mutationError: identityMatches ? mutationError : null,
|
|
1840
|
+
clearMutationError: useCallback2(() => {
|
|
1841
|
+
if (targetKeyRef.current === targetKey) setMutationError(null);
|
|
1842
|
+
}, [targetKey])
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
function operationKey() {
|
|
1846
|
+
return globalThis.crypto.randomUUID();
|
|
1847
|
+
}
|
|
1848
|
+
function asError(cause) {
|
|
1849
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// src/hooks/use-session-control.ts
|
|
1853
|
+
import { useCallback as useCallback3, useLayoutEffect as useLayoutEffect2, useRef as useRef3 } from "react";
|
|
1854
|
+
function useSessionControl(sessionId, options = {}) {
|
|
1855
|
+
const { client, workspaceId } = useEmbeddedSession(options);
|
|
1856
|
+
const approvalTargetKey = `${workspaceId}\0${sessionId ?? ""}`;
|
|
1857
|
+
const pendingApproval = useRef3(null);
|
|
1858
|
+
useLayoutEffect2(() => {
|
|
1859
|
+
if (pendingApproval.current?.targetKey !== approvalTargetKey) {
|
|
1860
|
+
pendingApproval.current = null;
|
|
1861
|
+
}
|
|
1862
|
+
}, [approvalTargetKey]);
|
|
1863
|
+
const {
|
|
1864
|
+
run: runControl,
|
|
1865
|
+
mutating: controlling,
|
|
1866
|
+
mutationError: controlError,
|
|
1867
|
+
clearMutationError: clearControlError
|
|
1868
|
+
} = useMutationRunner(approvalTargetKey);
|
|
1869
|
+
const {
|
|
1870
|
+
run: runApproval,
|
|
1871
|
+
mutating: responding,
|
|
1872
|
+
mutationError: approvalError,
|
|
1873
|
+
clearMutationError: clearApprovalError
|
|
1874
|
+
} = useMutationRunner(approvalTargetKey);
|
|
1875
|
+
const pause = useCallback3(
|
|
1876
|
+
async (reason) => {
|
|
1877
|
+
if (!sessionId) {
|
|
1878
|
+
return null;
|
|
1879
|
+
}
|
|
1880
|
+
return await runControl(
|
|
1881
|
+
() => client.pauseSession(workspaceId, sessionId, reason !== void 0 ? { reason } : {})
|
|
1882
|
+
);
|
|
1883
|
+
},
|
|
1884
|
+
[client, workspaceId, sessionId, runControl]
|
|
1885
|
+
);
|
|
1886
|
+
const resume = useCallback3(
|
|
1887
|
+
async (reason) => {
|
|
1888
|
+
if (!sessionId) return null;
|
|
1889
|
+
return await runControl(
|
|
1890
|
+
() => client.resumeSession(workspaceId, sessionId, reason !== void 0 ? { reason } : {})
|
|
1891
|
+
);
|
|
1892
|
+
},
|
|
1893
|
+
[client, workspaceId, sessionId, runControl]
|
|
1894
|
+
);
|
|
1895
|
+
const decide = useCallback3(
|
|
1896
|
+
async (approvalId, decision, message) => {
|
|
1897
|
+
if (!sessionId) {
|
|
1898
|
+
return null;
|
|
1899
|
+
}
|
|
1900
|
+
const decisionKey = JSON.stringify([approvalId, decision, message ?? null]);
|
|
1901
|
+
if (pendingApproval.current?.decisionKey !== decisionKey) {
|
|
1902
|
+
pendingApproval.current = {
|
|
1903
|
+
targetKey: approvalTargetKey,
|
|
1904
|
+
decisionKey,
|
|
1905
|
+
clientEventId: crypto.randomUUID()
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
const clientEventId = pendingApproval.current.clientEventId;
|
|
1909
|
+
const accepted = await runApproval(
|
|
1910
|
+
() => client.sendApprovalDecision(workspaceId, sessionId, {
|
|
1911
|
+
approvalId,
|
|
1912
|
+
decision,
|
|
1913
|
+
...message !== void 0 ? { message } : {},
|
|
1914
|
+
clientEventId
|
|
1915
|
+
})
|
|
1916
|
+
);
|
|
1917
|
+
if (accepted !== null && pendingApproval.current?.targetKey === approvalTargetKey && pendingApproval.current.clientEventId === clientEventId) {
|
|
1918
|
+
pendingApproval.current = null;
|
|
1919
|
+
}
|
|
1920
|
+
return accepted;
|
|
1921
|
+
},
|
|
1922
|
+
[approvalTargetKey, client, workspaceId, sessionId, runApproval]
|
|
1923
|
+
);
|
|
1924
|
+
const approve = useCallback3(
|
|
1925
|
+
async (approvalId, message) => await decide(approvalId, "approve", message),
|
|
1926
|
+
[decide]
|
|
1927
|
+
);
|
|
1928
|
+
const reject = useCallback3(
|
|
1929
|
+
async (approvalId, message) => await decide(approvalId, "reject", message),
|
|
1930
|
+
[decide]
|
|
1931
|
+
);
|
|
1932
|
+
const error = approvalError ?? controlError;
|
|
1933
|
+
const clearError = useCallback3(() => {
|
|
1934
|
+
clearControlError();
|
|
1935
|
+
clearApprovalError();
|
|
1936
|
+
}, [clearControlError, clearApprovalError]);
|
|
1937
|
+
return {
|
|
1938
|
+
pause,
|
|
1939
|
+
resume,
|
|
1940
|
+
controlling,
|
|
1941
|
+
approve,
|
|
1942
|
+
reject,
|
|
1943
|
+
responding,
|
|
1944
|
+
error,
|
|
1945
|
+
clearError
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// src/hooks/use-session-mcp-approval-policy.ts
|
|
1950
|
+
import { useCallback as useCallback4, useEffect as useEffect3, useLayoutEffect as useLayoutEffect3, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
|
|
1951
|
+
function isSessionMcpApprovalPolicyEvent(event, serverId) {
|
|
1952
|
+
if (event.type !== "session.mcp.approval_policy.updated") {
|
|
1953
|
+
return false;
|
|
1954
|
+
}
|
|
1955
|
+
if (serverId === void 0) {
|
|
1956
|
+
return true;
|
|
1957
|
+
}
|
|
1958
|
+
const payload = event.payload;
|
|
1959
|
+
return payload?.serverId === serverId;
|
|
1960
|
+
}
|
|
1961
|
+
function useSessionMcpApprovalPolicy(sessionId, serverId, options = {}) {
|
|
1962
|
+
const { client, workspaceId } = useEmbeddedSessionMcpApprovalPolicy(options);
|
|
1963
|
+
const enabled = (options.enabled ?? true) && Boolean(sessionId && serverId);
|
|
1964
|
+
const [override, setOverride] = useState3(null);
|
|
1965
|
+
const targetKey = `${workspaceId}\0${sessionId ?? ""}\0${serverId ?? ""}`;
|
|
1966
|
+
const authoritativeGeneration = useRef4(0);
|
|
1967
|
+
const mutationIdentity = useMemo3(() => ({ client, targetKey }), [client, targetKey]);
|
|
1968
|
+
const { run, mutating, mutationError, clearMutationError } = useMutationRunner(mutationIdentity);
|
|
1969
|
+
useLayoutEffect3(() => {
|
|
1970
|
+
authoritativeGeneration.current += 1;
|
|
1971
|
+
setOverride(null);
|
|
1972
|
+
}, [client, targetKey]);
|
|
1973
|
+
const load = useCallback4(async () => {
|
|
1974
|
+
const generation = authoritativeGeneration.current;
|
|
1975
|
+
if (!sessionId || !serverId) {
|
|
1976
|
+
return { targetKey, generation, server: null };
|
|
1977
|
+
}
|
|
1978
|
+
const session = await client.getSession(workspaceId, sessionId);
|
|
1979
|
+
return {
|
|
1980
|
+
targetKey,
|
|
1981
|
+
generation,
|
|
1982
|
+
server: session.mcpServers.find((server2) => server2.id === serverId) ?? null
|
|
1983
|
+
};
|
|
1984
|
+
}, [client, serverId, sessionId, targetKey, workspaceId]);
|
|
1985
|
+
const {
|
|
1986
|
+
data,
|
|
1987
|
+
loading,
|
|
1988
|
+
error: loadError,
|
|
1989
|
+
refresh
|
|
1990
|
+
} = usePolledValue(load, {
|
|
1991
|
+
enabled,
|
|
1992
|
+
pollIntervalMs: options.pollIntervalMs
|
|
1993
|
+
});
|
|
1994
|
+
useEffect3(() => {
|
|
1995
|
+
if (data?.targetKey === targetKey && data.generation === authoritativeGeneration.current) {
|
|
1996
|
+
setOverride(null);
|
|
1997
|
+
}
|
|
1998
|
+
}, [data, targetKey]);
|
|
1999
|
+
const scheduleRefresh = useDebouncedCallback(() => void refresh());
|
|
2000
|
+
const matchesPolicyEvent = useCallback4(
|
|
2001
|
+
(event) => isSessionMcpApprovalPolicyEvent(event, serverId ?? void 0),
|
|
2002
|
+
[serverId]
|
|
2003
|
+
);
|
|
2004
|
+
useSessionEventTrigger(
|
|
2005
|
+
client,
|
|
2006
|
+
workspaceId,
|
|
2007
|
+
sessionId,
|
|
2008
|
+
matchesPolicyEvent,
|
|
2009
|
+
scheduleRefresh,
|
|
2010
|
+
{
|
|
2011
|
+
enabled,
|
|
2012
|
+
...options.events !== void 0 ? { events: options.events } : {}
|
|
2013
|
+
},
|
|
2014
|
+
async () => await refresh()
|
|
2015
|
+
);
|
|
2016
|
+
const update = useCallback4(
|
|
2017
|
+
async (policy) => {
|
|
2018
|
+
if (!sessionId || !serverId) {
|
|
2019
|
+
return null;
|
|
2020
|
+
}
|
|
2021
|
+
const response = await run(
|
|
2022
|
+
() => client.updateSessionMcpApprovalPolicy(workspaceId, sessionId, serverId, {
|
|
2023
|
+
requireApproval: policy
|
|
2024
|
+
})
|
|
2025
|
+
);
|
|
2026
|
+
if (response) {
|
|
2027
|
+
authoritativeGeneration.current += 1;
|
|
2028
|
+
setOverride(response.server);
|
|
2029
|
+
void refresh();
|
|
2030
|
+
}
|
|
2031
|
+
return response;
|
|
2032
|
+
},
|
|
2033
|
+
[client, refresh, run, serverId, sessionId, workspaceId]
|
|
2034
|
+
);
|
|
2035
|
+
const server = override ?? data?.server ?? null;
|
|
2036
|
+
return {
|
|
2037
|
+
server,
|
|
2038
|
+
policy: server?.requireApproval ?? null,
|
|
2039
|
+
loading,
|
|
2040
|
+
error: mutationError ?? loadError,
|
|
2041
|
+
refresh: async () => {
|
|
2042
|
+
await refresh();
|
|
2043
|
+
},
|
|
2044
|
+
update,
|
|
2045
|
+
updating: mutating,
|
|
2046
|
+
clearError: clearMutationError
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// src/approvals.ts
|
|
2051
|
+
function approvalsFromRequiresAction(payload) {
|
|
2052
|
+
const approvals = payload && typeof payload === "object" ? payload.approvals : void 0;
|
|
2053
|
+
if (!Array.isArray(approvals)) {
|
|
2054
|
+
return [];
|
|
2055
|
+
}
|
|
2056
|
+
return approvals.map((approval, index) => {
|
|
2057
|
+
const raw = approval && typeof approval === "object" ? approval : {};
|
|
2058
|
+
const rawItem = raw.rawItem && typeof raw.rawItem === "object" ? raw.rawItem : {};
|
|
2059
|
+
return {
|
|
2060
|
+
// Mirror the runtime's canonical interruption identity before retaining
|
|
2061
|
+
// the legacy top-level callId fallback for already-normalized payloads.
|
|
2062
|
+
id: String(rawItem.callId ?? rawItem.id ?? raw.id ?? raw.callId ?? raw.name ?? index),
|
|
2063
|
+
name: String(raw.name ?? raw.toolName ?? rawItem.name ?? "approval"),
|
|
2064
|
+
arguments: raw.arguments ?? rawItem.arguments,
|
|
2065
|
+
raw: approval
|
|
2066
|
+
};
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
function projectPendingApprovals(events) {
|
|
2070
|
+
let pending = [];
|
|
2071
|
+
let owningTurnId = null;
|
|
2072
|
+
for (const event of events) {
|
|
2073
|
+
switch (event.type) {
|
|
2074
|
+
case "session.requiresAction": {
|
|
2075
|
+
pending = approvalsFromRequiresAction(event.payload);
|
|
2076
|
+
owningTurnId = event.turnId ?? null;
|
|
2077
|
+
break;
|
|
2078
|
+
}
|
|
2079
|
+
case "user.approvalDecision": {
|
|
2080
|
+
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
2081
|
+
if (typeof payload.approvalId === "string") {
|
|
2082
|
+
pending = pending.filter((approval) => approval.id !== payload.approvalId);
|
|
2083
|
+
}
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
case "turn.completed":
|
|
2087
|
+
case "turn.failed":
|
|
2088
|
+
case "turn.cancelled": {
|
|
2089
|
+
if (owningTurnId === null || event.turnId == null || event.turnId === owningTurnId) {
|
|
2090
|
+
pending = [];
|
|
2091
|
+
owningTurnId = null;
|
|
2092
|
+
}
|
|
2093
|
+
break;
|
|
2094
|
+
}
|
|
2095
|
+
default:
|
|
2096
|
+
break;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return pending;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// src/human-input.ts
|
|
2103
|
+
function humanInputRequestFromEvent(event) {
|
|
2104
|
+
if (event.type !== "session.humanInput.requested" || !isRecord(event.payload)) return null;
|
|
2105
|
+
const request = event.payload.request;
|
|
2106
|
+
if (!isRecord(request) || typeof request.id !== "string" || !Array.isArray(request.questions)) {
|
|
2107
|
+
return null;
|
|
2108
|
+
}
|
|
2109
|
+
return {
|
|
2110
|
+
id: request.id,
|
|
2111
|
+
turnId: event.turnId ?? null,
|
|
2112
|
+
questions: request.questions,
|
|
2113
|
+
allowSkip: request.allowSkip === true,
|
|
2114
|
+
expiresAt: typeof request.expiresAt === "string" ? request.expiresAt : null
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
function projectPendingHumanInputRequests(events) {
|
|
2118
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2119
|
+
for (const event of events) {
|
|
2120
|
+
const requested = humanInputRequestFromEvent(event);
|
|
2121
|
+
if (requested) {
|
|
2122
|
+
pending.set(requested.id, requested);
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
if (event.type === "user.humanInputResponse" && isRecord(event.payload)) {
|
|
2126
|
+
if (typeof event.payload.requestId === "string") pending.delete(event.payload.requestId);
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
if (event.type === "turn.completed" || event.type === "turn.failed" || event.type === "turn.cancelled") {
|
|
2130
|
+
for (const [id, request] of pending) {
|
|
2131
|
+
if (request.turnId === null || event.turnId === null || event.turnId === void 0 || request.turnId === event.turnId) {
|
|
2132
|
+
pending.delete(id);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return [...pending.values()];
|
|
2138
|
+
}
|
|
2139
|
+
function isRecord(value) {
|
|
2140
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// src/hooks/use-human-input.ts
|
|
2144
|
+
import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef5, useState as useState4 } from "react";
|
|
2145
|
+
function isHumanInputEvent(event) {
|
|
2146
|
+
return event.type === "session.humanInput.requested" || event.type === "user.humanInputResponse" || event.type === "turn.completed" || event.type === "turn.failed" || event.type === "turn.cancelled";
|
|
2147
|
+
}
|
|
2148
|
+
function useHumanInputRequests(sessionId, options = {}) {
|
|
2149
|
+
const { client, workspaceId, registerSessionReconciler } = useEmbeddedHumanInputSession(options);
|
|
2150
|
+
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
2151
|
+
const load = useCallback5(
|
|
2152
|
+
async () => sessionId ? await client.listHumanInputRequests(workspaceId, sessionId, { status: "pending" }) : [],
|
|
2153
|
+
[client, sessionId, workspaceId]
|
|
2154
|
+
);
|
|
2155
|
+
const state = usePolledValue(load, {
|
|
2156
|
+
enabled,
|
|
2157
|
+
pollIntervalMs: options.pollIntervalMs
|
|
2158
|
+
});
|
|
2159
|
+
const mutation = useMutationRunner(`${workspaceId}\0${sessionId ?? ""}`);
|
|
2160
|
+
const [respondingRequestId, setRespondingRequestId] = useState4(null);
|
|
2161
|
+
const respondingRequestRef = useRef5(null);
|
|
2162
|
+
useEffect4(() => {
|
|
2163
|
+
respondingRequestRef.current = null;
|
|
2164
|
+
setRespondingRequestId(null);
|
|
2165
|
+
}, [sessionId, workspaceId]);
|
|
2166
|
+
useEffect4(() => {
|
|
2167
|
+
if (!sessionId || !enabled) return;
|
|
2168
|
+
return registerSessionReconciler(sessionId, "human-input", state.refresh);
|
|
2169
|
+
}, [enabled, registerSessionReconciler, sessionId, state.refresh]);
|
|
2170
|
+
const scheduleRefresh = useDebouncedCallback(() => void state.refresh());
|
|
2171
|
+
useSessionEventTrigger(client, workspaceId, sessionId, isHumanInputEvent, scheduleRefresh, {
|
|
2172
|
+
enabled,
|
|
2173
|
+
...options.events !== void 0 ? { events: options.events } : {}
|
|
2174
|
+
});
|
|
2175
|
+
const respond = useCallback5(
|
|
2176
|
+
async (requestId, response) => {
|
|
2177
|
+
if (!sessionId || respondingRequestRef.current !== null) return null;
|
|
2178
|
+
respondingRequestRef.current = requestId;
|
|
2179
|
+
setRespondingRequestId(requestId);
|
|
2180
|
+
try {
|
|
2181
|
+
const event = await mutation.run(
|
|
2182
|
+
() => client.submitHumanInputResponse(workspaceId, sessionId, requestId, response, {
|
|
2183
|
+
clientEventId: globalThis.crypto.randomUUID()
|
|
2184
|
+
})
|
|
2185
|
+
);
|
|
2186
|
+
if (event) await state.refresh();
|
|
2187
|
+
return event;
|
|
2188
|
+
} finally {
|
|
2189
|
+
if (respondingRequestRef.current === requestId) {
|
|
2190
|
+
respondingRequestRef.current = null;
|
|
2191
|
+
setRespondingRequestId(null);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
},
|
|
2195
|
+
[client, mutation, sessionId, state, workspaceId]
|
|
2196
|
+
);
|
|
2197
|
+
return {
|
|
2198
|
+
requests: [...state.data ?? []].sort(
|
|
2199
|
+
(left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
|
|
2200
|
+
),
|
|
2201
|
+
loading: state.loading,
|
|
2202
|
+
error: state.error,
|
|
2203
|
+
refresh: state.refresh,
|
|
2204
|
+
respond,
|
|
2205
|
+
respondingRequestId,
|
|
2206
|
+
mutationError: mutation.mutationError,
|
|
2207
|
+
clearMutationError: mutation.clearMutationError
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
export {
|
|
2212
|
+
buildTimeline,
|
|
2213
|
+
creditExhaustedFromEvents,
|
|
2214
|
+
sessionStatusFromEvents,
|
|
2215
|
+
groupTimeline,
|
|
2216
|
+
extractSessionRef,
|
|
2217
|
+
toolDisplayName,
|
|
2218
|
+
SESSION_EVENT_BROWSER_MAX_BYTES,
|
|
2219
|
+
SESSION_EVENT_BROWSER_MAX_COUNT,
|
|
2220
|
+
SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES,
|
|
2221
|
+
SESSION_EVENT_BROWSER_PENDING_MAX_BYTES,
|
|
2222
|
+
SESSION_EVENT_BROWSER_PENDING_MAX_COUNT,
|
|
2223
|
+
useSessionEvents,
|
|
2224
|
+
boundBrowserSessionEventWindow,
|
|
2225
|
+
isTurnQueueEvent,
|
|
2226
|
+
useTurnQueue,
|
|
2227
|
+
useSessionControl,
|
|
2228
|
+
isSessionMcpApprovalPolicyEvent,
|
|
2229
|
+
useSessionMcpApprovalPolicy,
|
|
2230
|
+
approvalsFromRequiresAction,
|
|
2231
|
+
projectPendingApprovals,
|
|
2232
|
+
humanInputRequestFromEvent,
|
|
2233
|
+
projectPendingHumanInputRequests,
|
|
2234
|
+
isHumanInputEvent,
|
|
2235
|
+
useHumanInputRequests
|
|
2236
|
+
};
|
|
2237
|
+
//# sourceMappingURL=chunk-UAHPKLB6.js.map
|