@opengeni/react 3.7.0-canary.0 → 3.7.0-canary.1
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/dist/artifacts.js +24 -5
- package/dist/artifacts.js.map +1 -1
- package/dist/{browser-viewer-E5DDEFQ4.js → browser-viewer-NTD6UIPR.js} +3 -3
- package/dist/{chunk-W2RGYBCV.js → chunk-24M4YBCL.js} +4 -4
- package/dist/{chunk-DOK4KZF2.js → chunk-5IXTJTK3.js} +1322 -392
- package/dist/chunk-5IXTJTK3.js.map +1 -0
- package/dist/chunk-5PQJOCX5.js +902 -0
- package/dist/chunk-5PQJOCX5.js.map +1 -0
- package/dist/{chunk-R55HFTT3.js → chunk-INGB2N5R.js} +4305 -4019
- package/dist/chunk-INGB2N5R.js.map +1 -0
- package/dist/{chunk-FJBOU2TH.js → chunk-MOWYEBCQ.js} +1728 -12
- package/dist/chunk-MOWYEBCQ.js.map +1 -0
- package/dist/chunk-PAJU5KKL.js +331 -0
- package/dist/chunk-PAJU5KKL.js.map +1 -0
- package/dist/{chunk-VTOREEXF.js → chunk-PLYRL5ER.js} +24 -2
- package/dist/{chunk-VTOREEXF.js.map → chunk-PLYRL5ER.js.map} +1 -1
- package/dist/{chunk-YVCHAQZY.js → chunk-ZFOFY5ST.js} +27 -349
- package/dist/chunk-ZFOFY5ST.js.map +1 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +3 -2
- package/dist/components/session-chrome.d.ts +37 -4
- package/dist/components/session-commands-panel.d.ts +6 -0
- package/dist/components/session-conversation.d.ts +14 -0
- package/dist/components/timeline-anchor.d.ts +21 -0
- package/dist/composer.js +2 -3
- package/dist/conversation-timeline.d.ts +5 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +716 -914
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +2 -2
- package/dist/lib/format.d.ts +5 -0
- package/dist/machines.js +3 -2
- package/dist/session-ui.d.ts +3 -0
- package/dist/session-ui.js +14 -4
- package/dist/session.js +19 -19
- package/dist/timeline/types.d.ts +3 -1
- package/package.json +2 -2
- package/src/components/artifacts/published-html-artifact-frame.tsx +27 -4
- package/src/components/machine-input-display.ts +6 -0
- package/src/components/message-timeline.tsx +320 -197
- package/src/components/session-chrome.tsx +498 -125
- package/src/components/session-commands-panel.tsx +113 -0
- package/src/components/session-conversation.tsx +141 -0
- package/src/components/timeline-anchor.tsx +91 -0
- package/src/conversation-timeline.ts +83 -0
- package/src/hooks/use-session-background-commands.ts +21 -10
- package/src/hooks/use-session-events.ts +36 -14
- package/src/hooks/use-session.ts +23 -2
- package/src/index.ts +4 -0
- package/src/lib/format.ts +32 -0
- package/src/session-ui.ts +3 -0
- package/src/timeline/projection.ts +6 -12
- package/src/timeline/types.ts +4 -0
- package/styles/compiled.css +495 -411
- package/styles/index.css +23 -0
- package/dist/chunk-7EAFGICR.js +0 -302
- package/dist/chunk-7EAFGICR.js.map +0 -1
- package/dist/chunk-C6WDLHBA.js +0 -2590
- package/dist/chunk-C6WDLHBA.js.map +0 -1
- package/dist/chunk-DOK4KZF2.js.map +0 -1
- package/dist/chunk-FJBOU2TH.js.map +0 -1
- package/dist/chunk-R55HFTT3.js.map +0 -1
- package/dist/chunk-YVCHAQZY.js.map +0 -1
- /package/dist/{browser-viewer-E5DDEFQ4.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
- /package/dist/{chunk-W2RGYBCV.js.map → chunk-24M4YBCL.js.map} +0 -0
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CREDIT_EXHAUSTION_MESSAGE,
|
|
3
|
-
humanizeFailureReason,
|
|
4
3
|
isCreditExhaustion,
|
|
4
|
+
presentFailure,
|
|
5
5
|
tryParseJson
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-PLYRL5ER.js";
|
|
7
|
+
import {
|
|
8
|
+
useDebouncedCallback,
|
|
9
|
+
useMutationRunner,
|
|
10
|
+
usePageLiveActivity,
|
|
11
|
+
usePolledValue,
|
|
12
|
+
useSessionEventTrigger
|
|
13
|
+
} from "./chunk-KZ73RL76.js";
|
|
14
|
+
import {
|
|
15
|
+
useEmbeddedHumanInputSession,
|
|
16
|
+
useEmbeddedSession,
|
|
17
|
+
useEmbeddedSessionRead
|
|
18
|
+
} from "./chunk-NEXLLTP4.js";
|
|
7
19
|
|
|
8
20
|
// src/human-input.ts
|
|
9
21
|
function isActionableHumanInputRequest(request, nowMs = Date.now()) {
|
|
@@ -1059,6 +1071,7 @@ ${message}` : message;
|
|
|
1059
1071
|
id: `${pendingWaitOutcome.id}-visible-outcome`,
|
|
1060
1072
|
tone: "waiting",
|
|
1061
1073
|
text: waitingOutcomeText(pendingWaitOutcome.reason),
|
|
1074
|
+
recordedOutcome: true,
|
|
1062
1075
|
occurredAt: pendingWaitOutcome.occurredAt
|
|
1063
1076
|
});
|
|
1064
1077
|
}
|
|
@@ -1631,7 +1644,7 @@ function foldSettledTurn(groups, turnEnd) {
|
|
|
1631
1644
|
);
|
|
1632
1645
|
}
|
|
1633
1646
|
function isTurnBoundary(group) {
|
|
1634
|
-
return group?.kind === "turn" || group?.kind === "item" && (group.item.kind === "user-message" || group.item.kind === "human-input" || group.item.kind === "context-compaction" || group.item.kind === "machine-input-batch"
|
|
1647
|
+
return group?.kind === "turn" || group?.kind === "item" && (group.item.kind === "user-message" || group.item.kind === "human-input" || group.item.kind === "context-compaction" || group.item.kind === "machine-input-batch" || group.item.kind === "notice" && group.item.tone === "input");
|
|
1635
1648
|
}
|
|
1636
1649
|
function belongsToDifferentTurn(group, turnId) {
|
|
1637
1650
|
if (!group || !turnId) {
|
|
@@ -1741,6 +1754,8 @@ function machineInputMembers(value) {
|
|
|
1741
1754
|
const kinds = /* @__PURE__ */ new Set([
|
|
1742
1755
|
"scheduled_occurrence",
|
|
1743
1756
|
"goal_continuation",
|
|
1757
|
+
"background_command_result",
|
|
1758
|
+
"session_wait_timeout",
|
|
1744
1759
|
"agent_message",
|
|
1745
1760
|
"agent_steer_instruction",
|
|
1746
1761
|
"child_terminal_result",
|
|
@@ -1915,13 +1930,7 @@ function elapsedDurationMs(startedAt, completedAt) {
|
|
|
1915
1930
|
return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : null;
|
|
1916
1931
|
}
|
|
1917
1932
|
function failureMessage(payload) {
|
|
1918
|
-
|
|
1919
|
-
const value = payload[key];
|
|
1920
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
1921
|
-
return humanizeFailureReason(value);
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
return null;
|
|
1933
|
+
return presentFailure(payload).reason;
|
|
1925
1934
|
}
|
|
1926
1935
|
function goalText(payload) {
|
|
1927
1936
|
if (typeof payload.text === "string" && payload.text) {
|
|
@@ -2127,6 +2136,1700 @@ function looksLikeId(value) {
|
|
|
2127
2136
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
2128
2137
|
}
|
|
2129
2138
|
|
|
2139
|
+
// src/hooks/use-session-events.ts
|
|
2140
|
+
import {
|
|
2141
|
+
sessionEventStreamCoveredThrough
|
|
2142
|
+
} from "@opengeni/sdk";
|
|
2143
|
+
import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2144
|
+
var SESSION_HISTORY_PAGE_SIZE = 1e3;
|
|
2145
|
+
var INITIAL_FETCH_CAP = 1;
|
|
2146
|
+
var OLDER_GROUP_TARGET = 32;
|
|
2147
|
+
var OLDER_FETCH_CAP = 2;
|
|
2148
|
+
var NEWER_GROUP_TARGET = 32;
|
|
2149
|
+
var NEWER_FETCH_CAP = 2;
|
|
2150
|
+
var OLDEST_GROUP_TARGET = 32;
|
|
2151
|
+
var OLDEST_FETCH_CAP = 2;
|
|
2152
|
+
var BOUNDARY_PAGE_CAP = 1;
|
|
2153
|
+
var FOREGROUND_DIRECT_REPLAY_MAX_SEQUENCES = 16;
|
|
2154
|
+
var FOREGROUND_COMPACT_PROBE_MAX_SEQUENCES = SESSION_HISTORY_PAGE_SIZE;
|
|
2155
|
+
var FOREGROUND_COMPACT_CATCHUP_MAX_EVENTS = 128;
|
|
2156
|
+
var FOREGROUND_COMPACT_CATCHUP_MAX_GROUPS = 16;
|
|
2157
|
+
var FOREGROUND_COMPACT_CATCHUP_MAX_BYTES = 512 * 1024;
|
|
2158
|
+
var EMPTY_EVENTS = [];
|
|
2159
|
+
var encoder = new TextEncoder();
|
|
2160
|
+
var decoder = new TextDecoder();
|
|
2161
|
+
var BROWSER_EVENT_TYPE_MAX_BYTES = 256;
|
|
2162
|
+
var BROWSER_EVENT_ID_MAX_BYTES = 256;
|
|
2163
|
+
var BROWSER_EVENT_CLIENT_ID_MAX_BYTES = 4 * 1024;
|
|
2164
|
+
var BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES = 4 * 1024;
|
|
2165
|
+
var BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES = 48 * 1024;
|
|
2166
|
+
var BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS = [
|
|
2167
|
+
"id",
|
|
2168
|
+
"callId",
|
|
2169
|
+
"call_id",
|
|
2170
|
+
"name",
|
|
2171
|
+
"toolName",
|
|
2172
|
+
"status",
|
|
2173
|
+
"code",
|
|
2174
|
+
"isError",
|
|
2175
|
+
"stream",
|
|
2176
|
+
"commandId",
|
|
2177
|
+
"sequence",
|
|
2178
|
+
"coalescedUntil",
|
|
2179
|
+
"coalescedCount",
|
|
2180
|
+
"firstSequence",
|
|
2181
|
+
"lastSequence"
|
|
2182
|
+
];
|
|
2183
|
+
var SESSION_EVENT_BROWSER_MAX_BYTES = 8 * 1024 * 1024;
|
|
2184
|
+
var SESSION_EVENT_BROWSER_MAX_COUNT = 1e4;
|
|
2185
|
+
var SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES = 96 * 1024;
|
|
2186
|
+
var SESSION_EVENT_BROWSER_PENDING_MAX_BYTES = 1024 * 1024;
|
|
2187
|
+
var SESSION_EVENT_BROWSER_PENDING_MAX_COUNT = 256;
|
|
2188
|
+
var EMPTY_EVENT_WINDOW = {
|
|
2189
|
+
events: EMPTY_EVENTS,
|
|
2190
|
+
bytes: 2,
|
|
2191
|
+
truncated: false
|
|
2192
|
+
};
|
|
2193
|
+
function useSessionEvents(sessionId, options = {}) {
|
|
2194
|
+
const { client, workspaceId, reconcileSession } = useEmbeddedSession(options);
|
|
2195
|
+
const enabled = options.enabled ?? true;
|
|
2196
|
+
const pageLive = usePageLiveActivity();
|
|
2197
|
+
const streamEnabled = enabled && pageLive;
|
|
2198
|
+
const after = options.after ?? 0;
|
|
2199
|
+
const replay = options.replay ?? "windowed";
|
|
2200
|
+
const fullReplay = replay === "full" || after !== 0;
|
|
2201
|
+
const streamKey = `${workspaceId}\0${sessionId ?? ""}\0${after}\0${fullReplay ? "full" : "windowed"}`;
|
|
2202
|
+
const [eventWindow, setEventWindow] = useState(EMPTY_EVENT_WINDOW);
|
|
2203
|
+
const [connectionState, setConnectionState] = useState("idle");
|
|
2204
|
+
const [error, setError] = useState(null);
|
|
2205
|
+
const [hasOlder, setHasOlder] = useState(false);
|
|
2206
|
+
const [hasNewer, setHasNewer] = useState(false);
|
|
2207
|
+
const [sessionStatusProjection, setSessionStatusProjection] = useState(
|
|
2208
|
+
null
|
|
2209
|
+
);
|
|
2210
|
+
const [initialLoading, setInitialLoading] = useState(true);
|
|
2211
|
+
const [loadingOlder, setLoadingOlder] = useState(false);
|
|
2212
|
+
const [loadingNewer, setLoadingNewer] = useState(false);
|
|
2213
|
+
const [loadingOldest, setLoadingOldest] = useState(false);
|
|
2214
|
+
const [loadingLatest, setLoadingLatest] = useState(false);
|
|
2215
|
+
const [streamEpoch, setStreamEpoch] = useState(0);
|
|
2216
|
+
const [viewMode, setViewMode] = useState("live");
|
|
2217
|
+
const lastSequenceRef = useRef(after);
|
|
2218
|
+
const streamResumeSequenceRef = useRef(after);
|
|
2219
|
+
const oldestSequenceRef = useRef(null);
|
|
2220
|
+
const newestSequenceRef = useRef(null);
|
|
2221
|
+
const hasOlderRef = useRef(false);
|
|
2222
|
+
const hasNewerRef = useRef(false);
|
|
2223
|
+
const loadingOlderRef = useRef(false);
|
|
2224
|
+
const loadingNewerRef = useRef(false);
|
|
2225
|
+
const loadingOldestRef = useRef(false);
|
|
2226
|
+
const loadingLatestRef = useRef(false);
|
|
2227
|
+
const viewModeRef = useRef("live");
|
|
2228
|
+
const initialWindowLoadedRef = useRef(false);
|
|
2229
|
+
const reconcileAfterPageResumeRef = useRef(false);
|
|
2230
|
+
const streamAbortRef = useRef(null);
|
|
2231
|
+
const streamKeyRef = useRef(null);
|
|
2232
|
+
const generationRef = useRef(0);
|
|
2233
|
+
const navigationGenerationRef = useRef(0);
|
|
2234
|
+
const eventWindowRef = useRef(EMPTY_EVENT_WINDOW);
|
|
2235
|
+
const sessionStatusRef = useRef({
|
|
2236
|
+
sequence: after,
|
|
2237
|
+
status: null
|
|
2238
|
+
});
|
|
2239
|
+
const [stateStreamKey, setStateStreamKey] = useState(streamKey);
|
|
2240
|
+
useEffect(() => {
|
|
2241
|
+
navigationGenerationRef.current += 1;
|
|
2242
|
+
loadingOlderRef.current = false;
|
|
2243
|
+
loadingNewerRef.current = false;
|
|
2244
|
+
loadingOldestRef.current = false;
|
|
2245
|
+
setLoadingOlder(false);
|
|
2246
|
+
setLoadingNewer(false);
|
|
2247
|
+
setLoadingOldest(false);
|
|
2248
|
+
return () => {
|
|
2249
|
+
navigationGenerationRef.current += 1;
|
|
2250
|
+
};
|
|
2251
|
+
}, [client, workspaceId, sessionId, after, enabled, fullReplay]);
|
|
2252
|
+
useEffect(() => {
|
|
2253
|
+
if (streamKeyRef.current !== streamKey) {
|
|
2254
|
+
streamKeyRef.current = streamKey;
|
|
2255
|
+
setStateStreamKey(streamKey);
|
|
2256
|
+
eventWindowRef.current = EMPTY_EVENT_WINDOW;
|
|
2257
|
+
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
2258
|
+
setError(null);
|
|
2259
|
+
setHasOlder(false);
|
|
2260
|
+
setHasNewer(false);
|
|
2261
|
+
sessionStatusRef.current = { sequence: after, status: null };
|
|
2262
|
+
setSessionStatusProjection(null);
|
|
2263
|
+
setLoadingOlder(false);
|
|
2264
|
+
setLoadingNewer(false);
|
|
2265
|
+
setLoadingOldest(false);
|
|
2266
|
+
setLoadingLatest(false);
|
|
2267
|
+
setInitialLoading(true);
|
|
2268
|
+
lastSequenceRef.current = after;
|
|
2269
|
+
streamResumeSequenceRef.current = after;
|
|
2270
|
+
oldestSequenceRef.current = null;
|
|
2271
|
+
newestSequenceRef.current = null;
|
|
2272
|
+
hasOlderRef.current = false;
|
|
2273
|
+
hasNewerRef.current = false;
|
|
2274
|
+
loadingOlderRef.current = false;
|
|
2275
|
+
loadingNewerRef.current = false;
|
|
2276
|
+
loadingOldestRef.current = false;
|
|
2277
|
+
loadingLatestRef.current = false;
|
|
2278
|
+
viewModeRef.current = "live";
|
|
2279
|
+
setViewMode("live");
|
|
2280
|
+
initialWindowLoadedRef.current = false;
|
|
2281
|
+
reconcileAfterPageResumeRef.current = false;
|
|
2282
|
+
}
|
|
2283
|
+
const generation = generationRef.current + 1;
|
|
2284
|
+
generationRef.current = generation;
|
|
2285
|
+
if (!sessionId || !streamEnabled) {
|
|
2286
|
+
if (sessionId && enabled && !pageLive && !fullReplay && viewModeRef.current === "live" && initialWindowLoadedRef.current) {
|
|
2287
|
+
reconcileAfterPageResumeRef.current = true;
|
|
2288
|
+
}
|
|
2289
|
+
setConnectionState("idle");
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
if (viewMode === "history") {
|
|
2293
|
+
reconcileAfterPageResumeRef.current = false;
|
|
2294
|
+
setConnectionState("idle");
|
|
2295
|
+
setInitialLoading(false);
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
const reconcileForegroundResume = reconcileAfterPageResumeRef.current && !fullReplay;
|
|
2299
|
+
reconcileAfterPageResumeRef.current = false;
|
|
2300
|
+
const controller = new AbortController();
|
|
2301
|
+
const isCurrent = () => generationRef.current === generation && !controller.signal.aborted;
|
|
2302
|
+
streamAbortRef.current = controller;
|
|
2303
|
+
let pending = [];
|
|
2304
|
+
let pendingBytes = 2;
|
|
2305
|
+
let flushTimer = null;
|
|
2306
|
+
const flush = () => {
|
|
2307
|
+
if (flushTimer !== null) {
|
|
2308
|
+
clearTimeout(flushTimer);
|
|
2309
|
+
flushTimer = null;
|
|
2310
|
+
}
|
|
2311
|
+
if (!isCurrent()) {
|
|
2312
|
+
pending = [];
|
|
2313
|
+
pendingBytes = 2;
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
if (pending.length === 0) {
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
const batch = pending;
|
|
2320
|
+
pending = [];
|
|
2321
|
+
pendingBytes = 2;
|
|
2322
|
+
const lastInBatch = batch[batch.length - 1];
|
|
2323
|
+
if (lastInBatch) {
|
|
2324
|
+
const batchResumeSequence = Math.max(...batch.map(eventResumeSequence));
|
|
2325
|
+
lastSequenceRef.current = Math.max(lastSequenceRef.current, batchResumeSequence);
|
|
2326
|
+
streamResumeSequenceRef.current = Math.max(
|
|
2327
|
+
streamResumeSequenceRef.current,
|
|
2328
|
+
batchResumeSequence
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
const status = observeSessionStatus(batch, sessionStatusRef);
|
|
2332
|
+
if (status !== void 0) {
|
|
2333
|
+
setSessionStatusProjection(status);
|
|
2334
|
+
}
|
|
2335
|
+
const current = eventWindowRef.current;
|
|
2336
|
+
const next = boundBrowserSessionEventWindow([...current.events, ...batch]);
|
|
2337
|
+
const retained = {
|
|
2338
|
+
...next,
|
|
2339
|
+
truncated: current.truncated || next.truncated
|
|
2340
|
+
};
|
|
2341
|
+
eventWindowRef.current = retained;
|
|
2342
|
+
setEventWindow(retained);
|
|
2343
|
+
oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
|
|
2344
|
+
newestSequenceRef.current = maxResumeSequenceOrNull(retained.events);
|
|
2345
|
+
if (retained.truncated) {
|
|
2346
|
+
hasOlderRef.current = true;
|
|
2347
|
+
setHasOlder(true);
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
const scheduleFlush = () => {
|
|
2351
|
+
if (!isCurrent()) return;
|
|
2352
|
+
flushTimer ??= setTimeout(flush, 16);
|
|
2353
|
+
};
|
|
2354
|
+
void (async () => {
|
|
2355
|
+
try {
|
|
2356
|
+
if (reconcileForegroundResume) {
|
|
2357
|
+
setConnectionState("connecting");
|
|
2358
|
+
const cursor = streamResumeSequenceRef.current;
|
|
2359
|
+
let plan = { kind: "resume" };
|
|
2360
|
+
try {
|
|
2361
|
+
plan = await planForegroundCatchup(client, workspaceId, sessionId, cursor, {
|
|
2362
|
+
signal: controller.signal
|
|
2363
|
+
});
|
|
2364
|
+
} catch {
|
|
2365
|
+
if (controller.signal.aborted) return;
|
|
2366
|
+
plan = { kind: "resume" };
|
|
2367
|
+
}
|
|
2368
|
+
if (!isCurrent()) return;
|
|
2369
|
+
if (plan.kind === "append") {
|
|
2370
|
+
const current = eventWindowRef.current;
|
|
2371
|
+
assertAppendOrder(current.events, plan.events);
|
|
2372
|
+
const status = observeSessionStatus(plan.events, sessionStatusRef);
|
|
2373
|
+
const next = boundBrowserSessionEventWindow([...current.events, ...plan.events]);
|
|
2374
|
+
const retained = {
|
|
2375
|
+
...next,
|
|
2376
|
+
truncated: current.truncated || next.truncated
|
|
2377
|
+
};
|
|
2378
|
+
eventWindowRef.current = retained;
|
|
2379
|
+
oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
|
|
2380
|
+
newestSequenceRef.current = maxResumeSequenceOrNull(retained.events);
|
|
2381
|
+
lastSequenceRef.current = Math.max(lastSequenceRef.current, plan.resumeSequence);
|
|
2382
|
+
streamResumeSequenceRef.current = Math.max(
|
|
2383
|
+
streamResumeSequenceRef.current,
|
|
2384
|
+
plan.resumeSequence
|
|
2385
|
+
);
|
|
2386
|
+
if (status !== void 0) {
|
|
2387
|
+
setSessionStatusProjection(status);
|
|
2388
|
+
}
|
|
2389
|
+
setEventWindow(retained);
|
|
2390
|
+
if (retained.truncated) {
|
|
2391
|
+
hasOlderRef.current = true;
|
|
2392
|
+
setHasOlder(true);
|
|
2393
|
+
}
|
|
2394
|
+
} else if (plan.kind === "reload") {
|
|
2395
|
+
initialWindowLoadedRef.current = false;
|
|
2396
|
+
setError(null);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
if (!fullReplay && !initialWindowLoadedRef.current) {
|
|
2400
|
+
setConnectionState("connecting");
|
|
2401
|
+
const window = await loadEventWindow(client, workspaceId, sessionId, {
|
|
2402
|
+
before: Number.MAX_SAFE_INTEGER,
|
|
2403
|
+
pageSize: SESSION_HISTORY_PAGE_SIZE,
|
|
2404
|
+
targetGroups: Number.POSITIVE_INFINITY,
|
|
2405
|
+
maxFetches: INITIAL_FETCH_CAP,
|
|
2406
|
+
boundaryPageCap: BOUNDARY_PAGE_CAP,
|
|
2407
|
+
signal: controller.signal
|
|
2408
|
+
});
|
|
2409
|
+
if (!isCurrent()) {
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
2413
|
+
const retained = boundBrowserSessionEventWindow(window.events);
|
|
2414
|
+
navigationGenerationRef.current += 1;
|
|
2415
|
+
loadingOlderRef.current = false;
|
|
2416
|
+
loadingNewerRef.current = false;
|
|
2417
|
+
loadingOldestRef.current = false;
|
|
2418
|
+
setLoadingOlder(false);
|
|
2419
|
+
setLoadingNewer(false);
|
|
2420
|
+
setLoadingOldest(false);
|
|
2421
|
+
eventWindowRef.current = retained;
|
|
2422
|
+
oldestSequenceRef.current = retained.events[0]?.sequence ?? window.oldestSequence;
|
|
2423
|
+
newestSequenceRef.current = maxResumeSequenceOrNull(retained.events) ?? window.newestSequence;
|
|
2424
|
+
hasOlderRef.current = window.hasOlder || retained.truncated;
|
|
2425
|
+
hasNewerRef.current = false;
|
|
2426
|
+
lastSequenceRef.current = window.newestSequence;
|
|
2427
|
+
streamResumeSequenceRef.current = window.newestSequence;
|
|
2428
|
+
initialWindowLoadedRef.current = true;
|
|
2429
|
+
if (status !== void 0) {
|
|
2430
|
+
setSessionStatusProjection(status);
|
|
2431
|
+
}
|
|
2432
|
+
setEventWindow(retained);
|
|
2433
|
+
setHasOlder(window.hasOlder || retained.truncated);
|
|
2434
|
+
setHasNewer(false);
|
|
2435
|
+
setInitialLoading(false);
|
|
2436
|
+
}
|
|
2437
|
+
if (fullReplay) {
|
|
2438
|
+
setInitialLoading(false);
|
|
2439
|
+
}
|
|
2440
|
+
const stream = client.streamEvents(workspaceId, sessionId, {
|
|
2441
|
+
after: streamResumeSequenceRef.current,
|
|
2442
|
+
signal: controller.signal,
|
|
2443
|
+
onOpen: () => {
|
|
2444
|
+
void Promise.resolve().then(() => reconcileSession(sessionId)).catch((cause) => {
|
|
2445
|
+
if (isCurrent()) {
|
|
2446
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
2447
|
+
}
|
|
2448
|
+
});
|
|
2449
|
+
},
|
|
2450
|
+
onStateChange: (state) => {
|
|
2451
|
+
if (isCurrent()) {
|
|
2452
|
+
setConnectionState(state);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
});
|
|
2456
|
+
for await (const event of stream) {
|
|
2457
|
+
if (!isCurrent()) break;
|
|
2458
|
+
const boundedEvent = boundBrowserLegacyEvent(event);
|
|
2459
|
+
const boundedEventBytes = browserJsonBytes(boundedEvent);
|
|
2460
|
+
const separatorBytes = pending.length === 0 ? 0 : 1;
|
|
2461
|
+
if (pending.length > 0 && (pending.length >= SESSION_EVENT_BROWSER_PENDING_MAX_COUNT || pendingBytes + separatorBytes + boundedEventBytes > SESSION_EVENT_BROWSER_PENDING_MAX_BYTES)) {
|
|
2462
|
+
flush();
|
|
2463
|
+
}
|
|
2464
|
+
pending.push(boundedEvent);
|
|
2465
|
+
pendingBytes += (pending.length === 1 ? 0 : 1) + boundedEventBytes;
|
|
2466
|
+
if (pending.length >= SESSION_EVENT_BROWSER_PENDING_MAX_COUNT || pendingBytes >= SESSION_EVENT_BROWSER_PENDING_MAX_BYTES) {
|
|
2467
|
+
flush();
|
|
2468
|
+
} else {
|
|
2469
|
+
scheduleFlush();
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
if (isCurrent()) {
|
|
2473
|
+
flush();
|
|
2474
|
+
setConnectionState("ended");
|
|
2475
|
+
}
|
|
2476
|
+
} catch (cause) {
|
|
2477
|
+
if (isCurrent()) {
|
|
2478
|
+
flush();
|
|
2479
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
2480
|
+
setConnectionState("error");
|
|
2481
|
+
setInitialLoading(false);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
})();
|
|
2485
|
+
return () => {
|
|
2486
|
+
controller.abort();
|
|
2487
|
+
if (generationRef.current === generation) {
|
|
2488
|
+
generationRef.current = generation + 1;
|
|
2489
|
+
}
|
|
2490
|
+
if (streamAbortRef.current === controller) {
|
|
2491
|
+
streamAbortRef.current = null;
|
|
2492
|
+
}
|
|
2493
|
+
if (flushTimer !== null) {
|
|
2494
|
+
clearTimeout(flushTimer);
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
}, [
|
|
2498
|
+
client,
|
|
2499
|
+
workspaceId,
|
|
2500
|
+
sessionId,
|
|
2501
|
+
after,
|
|
2502
|
+
enabled,
|
|
2503
|
+
pageLive,
|
|
2504
|
+
streamEnabled,
|
|
2505
|
+
fullReplay,
|
|
2506
|
+
streamKey,
|
|
2507
|
+
streamEpoch,
|
|
2508
|
+
viewMode,
|
|
2509
|
+
reconcileSession
|
|
2510
|
+
]);
|
|
2511
|
+
const navigationBusy = () => loadingOlderRef.current || loadingNewerRef.current || loadingOldestRef.current || loadingLatestRef.current;
|
|
2512
|
+
const loadOlder = useCallback(
|
|
2513
|
+
() => createOlderHistoryLoadReceipt(async (markCommitted) => {
|
|
2514
|
+
if (!sessionId || navigationBusy() || !hasOlderRef.current) {
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
const before = oldestSequenceRef.current;
|
|
2518
|
+
if (before === null) {
|
|
2519
|
+
oldestSequenceRef.current = null;
|
|
2520
|
+
hasOlderRef.current = false;
|
|
2521
|
+
setHasOlder(false);
|
|
2522
|
+
return false;
|
|
2523
|
+
}
|
|
2524
|
+
const generation = navigationGenerationRef.current;
|
|
2525
|
+
loadingOlderRef.current = true;
|
|
2526
|
+
setLoadingOlder(true);
|
|
2527
|
+
let published = false;
|
|
2528
|
+
try {
|
|
2529
|
+
const window = await loadEventWindow(client, workspaceId, sessionId, {
|
|
2530
|
+
before,
|
|
2531
|
+
pageSize: SESSION_HISTORY_PAGE_SIZE,
|
|
2532
|
+
targetGroups: OLDER_GROUP_TARGET,
|
|
2533
|
+
maxFetches: OLDER_FETCH_CAP
|
|
2534
|
+
});
|
|
2535
|
+
if (navigationGenerationRef.current !== generation) {
|
|
2536
|
+
return false;
|
|
2537
|
+
}
|
|
2538
|
+
if (window.events.length === 0) {
|
|
2539
|
+
oldestSequenceRef.current = null;
|
|
2540
|
+
hasOlderRef.current = false;
|
|
2541
|
+
setHasOlder(false);
|
|
2542
|
+
return false;
|
|
2543
|
+
}
|
|
2544
|
+
const current = eventWindowRef.current;
|
|
2545
|
+
assertPrependOrder(current.events, window.events);
|
|
2546
|
+
streamAbortRef.current?.abort();
|
|
2547
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
2548
|
+
const next = boundBrowserSessionEventWindow([...window.events, ...current.events], {
|
|
2549
|
+
direction: "oldest"
|
|
2550
|
+
});
|
|
2551
|
+
const retained = {
|
|
2552
|
+
...next,
|
|
2553
|
+
truncated: current.truncated || next.truncated
|
|
2554
|
+
};
|
|
2555
|
+
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
2556
|
+
if (retainedOldest === null || retainedOldest >= before) {
|
|
2557
|
+
throw new Error("@opengeni/react: loadOlder made no durable sequence progress");
|
|
2558
|
+
}
|
|
2559
|
+
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
2560
|
+
const previousNewest = maxResumeSequenceOrNull(current.events);
|
|
2561
|
+
eventWindowRef.current = retained;
|
|
2562
|
+
markCommitted();
|
|
2563
|
+
oldestSequenceRef.current = retainedOldest;
|
|
2564
|
+
newestSequenceRef.current = retainedNewest;
|
|
2565
|
+
streamResumeSequenceRef.current = maxResumeSequence(retained.events);
|
|
2566
|
+
const olderStillAvailable = window.hasOlder;
|
|
2567
|
+
hasOlderRef.current = olderStillAvailable;
|
|
2568
|
+
const evictedLiveTail = previousNewest !== null && retainedNewest !== null && retainedNewest < previousNewest;
|
|
2569
|
+
let newer = null;
|
|
2570
|
+
let enterHistory = false;
|
|
2571
|
+
let reconnectLive = false;
|
|
2572
|
+
if (viewModeRef.current === "history" || evictedLiveTail) {
|
|
2573
|
+
const highWater = lastSequenceRef.current;
|
|
2574
|
+
newer = retainedNewest !== null && (retainedNewest < highWater || retained.truncated);
|
|
2575
|
+
hasNewerRef.current = newer;
|
|
2576
|
+
if (evictedLiveTail) {
|
|
2577
|
+
viewModeRef.current = "history";
|
|
2578
|
+
enterHistory = true;
|
|
2579
|
+
}
|
|
2580
|
+
} else {
|
|
2581
|
+
reconnectLive = true;
|
|
2582
|
+
}
|
|
2583
|
+
loadingOlderRef.current = false;
|
|
2584
|
+
startTransition(() => {
|
|
2585
|
+
if (status !== void 0) {
|
|
2586
|
+
setSessionStatusProjection(status);
|
|
2587
|
+
}
|
|
2588
|
+
setEventWindow(retained);
|
|
2589
|
+
setHasOlder(olderStillAvailable);
|
|
2590
|
+
if (newer !== null) {
|
|
2591
|
+
setHasNewer(newer);
|
|
2592
|
+
}
|
|
2593
|
+
if (enterHistory) {
|
|
2594
|
+
setViewMode("history");
|
|
2595
|
+
} else if (reconnectLive) {
|
|
2596
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
2597
|
+
}
|
|
2598
|
+
setLoadingOlder(false);
|
|
2599
|
+
});
|
|
2600
|
+
published = true;
|
|
2601
|
+
return olderStillAvailable;
|
|
2602
|
+
} finally {
|
|
2603
|
+
if (!published && navigationGenerationRef.current === generation) {
|
|
2604
|
+
loadingOlderRef.current = false;
|
|
2605
|
+
setLoadingOlder(false);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
}),
|
|
2609
|
+
[client, workspaceId, sessionId]
|
|
2610
|
+
);
|
|
2611
|
+
const loadOldest = useCallback(async () => {
|
|
2612
|
+
if (!sessionId || navigationBusy() || !hasOlderRef.current) {
|
|
2613
|
+
return false;
|
|
2614
|
+
}
|
|
2615
|
+
const generation = navigationGenerationRef.current;
|
|
2616
|
+
loadingOldestRef.current = true;
|
|
2617
|
+
setLoadingOldest(true);
|
|
2618
|
+
let published = false;
|
|
2619
|
+
try {
|
|
2620
|
+
const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
|
|
2621
|
+
after: 0,
|
|
2622
|
+
pageSize: SESSION_HISTORY_PAGE_SIZE,
|
|
2623
|
+
targetGroups: OLDEST_GROUP_TARGET,
|
|
2624
|
+
maxFetches: OLDEST_FETCH_CAP
|
|
2625
|
+
});
|
|
2626
|
+
if (navigationGenerationRef.current !== generation) {
|
|
2627
|
+
return false;
|
|
2628
|
+
}
|
|
2629
|
+
if (window.events.length === 0) {
|
|
2630
|
+
hasOlderRef.current = false;
|
|
2631
|
+
setHasOlder(false);
|
|
2632
|
+
return false;
|
|
2633
|
+
}
|
|
2634
|
+
streamAbortRef.current?.abort();
|
|
2635
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
2636
|
+
const retained = boundBrowserSessionEventWindow(window.events, { direction: "oldest" });
|
|
2637
|
+
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
2638
|
+
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
2639
|
+
eventWindowRef.current = retained;
|
|
2640
|
+
oldestSequenceRef.current = retainedOldest;
|
|
2641
|
+
newestSequenceRef.current = retainedNewest;
|
|
2642
|
+
hasOlderRef.current = false;
|
|
2643
|
+
const highWater = Math.max(lastSequenceRef.current, retainedNewest ?? 0);
|
|
2644
|
+
lastSequenceRef.current = highWater;
|
|
2645
|
+
const newer = window.hasNewer || retained.truncated || retainedNewest !== null && retainedNewest < highWater;
|
|
2646
|
+
hasNewerRef.current = newer;
|
|
2647
|
+
initialWindowLoadedRef.current = true;
|
|
2648
|
+
viewModeRef.current = "history";
|
|
2649
|
+
loadingOldestRef.current = false;
|
|
2650
|
+
startTransition(() => {
|
|
2651
|
+
if (status !== void 0) {
|
|
2652
|
+
setSessionStatusProjection(status);
|
|
2653
|
+
}
|
|
2654
|
+
setEventWindow(retained);
|
|
2655
|
+
setHasOlder(false);
|
|
2656
|
+
setHasNewer(newer);
|
|
2657
|
+
setViewMode("history");
|
|
2658
|
+
setLoadingOldest(false);
|
|
2659
|
+
});
|
|
2660
|
+
published = true;
|
|
2661
|
+
return newer;
|
|
2662
|
+
} finally {
|
|
2663
|
+
if (!published && navigationGenerationRef.current === generation) {
|
|
2664
|
+
loadingOldestRef.current = false;
|
|
2665
|
+
setLoadingOldest(false);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
}, [client, workspaceId, sessionId]);
|
|
2669
|
+
const loadNewer = useCallback(async () => {
|
|
2670
|
+
if (!sessionId || navigationBusy() || !hasNewerRef.current) {
|
|
2671
|
+
return false;
|
|
2672
|
+
}
|
|
2673
|
+
const afterSequence = newestSequenceRef.current;
|
|
2674
|
+
if (afterSequence === null) {
|
|
2675
|
+
hasNewerRef.current = false;
|
|
2676
|
+
setHasNewer(false);
|
|
2677
|
+
return false;
|
|
2678
|
+
}
|
|
2679
|
+
const generation = navigationGenerationRef.current;
|
|
2680
|
+
loadingNewerRef.current = true;
|
|
2681
|
+
setLoadingNewer(true);
|
|
2682
|
+
let published = false;
|
|
2683
|
+
try {
|
|
2684
|
+
const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
|
|
2685
|
+
after: afterSequence,
|
|
2686
|
+
pageSize: SESSION_HISTORY_PAGE_SIZE,
|
|
2687
|
+
targetGroups: NEWER_GROUP_TARGET,
|
|
2688
|
+
maxFetches: NEWER_FETCH_CAP
|
|
2689
|
+
});
|
|
2690
|
+
if (navigationGenerationRef.current !== generation) {
|
|
2691
|
+
return false;
|
|
2692
|
+
}
|
|
2693
|
+
if (window.events.length === 0) {
|
|
2694
|
+
hasNewerRef.current = false;
|
|
2695
|
+
setHasNewer(false);
|
|
2696
|
+
streamResumeSequenceRef.current = afterSequence;
|
|
2697
|
+
viewModeRef.current = "live";
|
|
2698
|
+
setViewMode("live");
|
|
2699
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
2700
|
+
return false;
|
|
2701
|
+
}
|
|
2702
|
+
const current = eventWindowRef.current;
|
|
2703
|
+
assertAppendOrder(current.events, window.events);
|
|
2704
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
2705
|
+
const previousOldest = current.events[0]?.sequence ?? null;
|
|
2706
|
+
const next = boundBrowserSessionEventWindow([...current.events, ...window.events], {
|
|
2707
|
+
direction: "newest"
|
|
2708
|
+
});
|
|
2709
|
+
const retained = {
|
|
2710
|
+
...next,
|
|
2711
|
+
truncated: current.truncated || next.truncated || window.hasNewer
|
|
2712
|
+
};
|
|
2713
|
+
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
2714
|
+
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
2715
|
+
if (retainedNewest === null || retainedNewest <= afterSequence) {
|
|
2716
|
+
throw new Error("@opengeni/react: loadNewer made no durable sequence progress");
|
|
2717
|
+
}
|
|
2718
|
+
eventWindowRef.current = retained;
|
|
2719
|
+
oldestSequenceRef.current = retainedOldest;
|
|
2720
|
+
newestSequenceRef.current = retainedNewest;
|
|
2721
|
+
if (retainedOldest !== null && (previousOldest === null || retainedOldest > previousOldest || retained.truncated && retained.events[0]?.type !== "session.created" && retainedOldest > 1)) {
|
|
2722
|
+
hasOlderRef.current = true;
|
|
2723
|
+
}
|
|
2724
|
+
const rearmedOlder = hasOlderRef.current;
|
|
2725
|
+
const highWater = Math.max(lastSequenceRef.current, retainedNewest);
|
|
2726
|
+
lastSequenceRef.current = highWater;
|
|
2727
|
+
const newer = window.hasNewer || retainedNewest < highWater;
|
|
2728
|
+
hasNewerRef.current = newer;
|
|
2729
|
+
let resumeLive = false;
|
|
2730
|
+
if (!newer) {
|
|
2731
|
+
streamResumeSequenceRef.current = retainedNewest;
|
|
2732
|
+
viewModeRef.current = "live";
|
|
2733
|
+
resumeLive = true;
|
|
2734
|
+
}
|
|
2735
|
+
loadingNewerRef.current = false;
|
|
2736
|
+
startTransition(() => {
|
|
2737
|
+
if (status !== void 0) {
|
|
2738
|
+
setSessionStatusProjection(status);
|
|
2739
|
+
}
|
|
2740
|
+
setEventWindow(retained);
|
|
2741
|
+
setHasOlder(rearmedOlder);
|
|
2742
|
+
setHasNewer(newer);
|
|
2743
|
+
if (resumeLive) {
|
|
2744
|
+
setViewMode("live");
|
|
2745
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
2746
|
+
}
|
|
2747
|
+
setLoadingNewer(false);
|
|
2748
|
+
});
|
|
2749
|
+
published = true;
|
|
2750
|
+
return newer;
|
|
2751
|
+
} finally {
|
|
2752
|
+
if (!published && navigationGenerationRef.current === generation) {
|
|
2753
|
+
loadingNewerRef.current = false;
|
|
2754
|
+
setLoadingNewer(false);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
}, [client, workspaceId, sessionId]);
|
|
2758
|
+
const jumpToLatest = useCallback(async () => {
|
|
2759
|
+
if (!sessionId || navigationBusy()) {
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
loadingLatestRef.current = true;
|
|
2763
|
+
setLoadingLatest(true);
|
|
2764
|
+
let published = false;
|
|
2765
|
+
try {
|
|
2766
|
+
streamAbortRef.current?.abort();
|
|
2767
|
+
hasNewerRef.current = false;
|
|
2768
|
+
hasOlderRef.current = false;
|
|
2769
|
+
newestSequenceRef.current = null;
|
|
2770
|
+
oldestSequenceRef.current = null;
|
|
2771
|
+
eventWindowRef.current = EMPTY_EVENT_WINDOW;
|
|
2772
|
+
initialWindowLoadedRef.current = false;
|
|
2773
|
+
streamResumeSequenceRef.current = after;
|
|
2774
|
+
viewModeRef.current = "live";
|
|
2775
|
+
loadingLatestRef.current = false;
|
|
2776
|
+
startTransition(() => {
|
|
2777
|
+
setHasNewer(false);
|
|
2778
|
+
setHasOlder(false);
|
|
2779
|
+
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
2780
|
+
setInitialLoading(true);
|
|
2781
|
+
setViewMode("live");
|
|
2782
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
2783
|
+
setLoadingLatest(false);
|
|
2784
|
+
});
|
|
2785
|
+
published = true;
|
|
2786
|
+
await Promise.resolve();
|
|
2787
|
+
} finally {
|
|
2788
|
+
if (!published) {
|
|
2789
|
+
loadingLatestRef.current = false;
|
|
2790
|
+
setLoadingLatest(false);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
}, [after, sessionId]);
|
|
2794
|
+
const identityMatches = stateStreamKey === streamKey;
|
|
2795
|
+
const visibleEvents = identityMatches ? eventWindow.events : EMPTY_EVENTS;
|
|
2796
|
+
const timeline = useMemo(() => buildTimeline(visibleEvents), [visibleEvents]);
|
|
2797
|
+
return {
|
|
2798
|
+
events: visibleEvents,
|
|
2799
|
+
timeline,
|
|
2800
|
+
sessionStatus: identityMatches ? sessionStatusProjection : null,
|
|
2801
|
+
connectionState: identityMatches ? connectionState : "idle",
|
|
2802
|
+
lastSequence: identityMatches ? lastSequenceRef.current : after,
|
|
2803
|
+
windowBytes: identityMatches ? eventWindow.bytes : 2,
|
|
2804
|
+
windowTruncated: identityMatches ? eventWindow.truncated : false,
|
|
2805
|
+
initialLoading: fullReplay ? false : identityMatches ? initialLoading : true,
|
|
2806
|
+
hasOlder: !identityMatches ? false : hasOlder,
|
|
2807
|
+
loadingOlder: !identityMatches ? false : loadingOlder,
|
|
2808
|
+
loadOlder,
|
|
2809
|
+
hasNewer: !identityMatches ? false : hasNewer,
|
|
2810
|
+
loadingNewer: !identityMatches ? false : loadingNewer,
|
|
2811
|
+
loadNewer,
|
|
2812
|
+
loadingOldest: !identityMatches ? false : loadingOldest,
|
|
2813
|
+
loadOldest,
|
|
2814
|
+
loadingLatest: !identityMatches ? false : loadingLatest,
|
|
2815
|
+
jumpToLatest,
|
|
2816
|
+
error: identityMatches ? error : null
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
function boundBrowserSessionEventWindow(events, options = {}) {
|
|
2820
|
+
const maxBytes = Math.max(1024, options.maxBytes ?? SESSION_EVENT_BROWSER_MAX_BYTES);
|
|
2821
|
+
const maxCount = Math.max(1, Math.floor(options.maxCount ?? SESSION_EVENT_BROWSER_MAX_COUNT));
|
|
2822
|
+
const safe = events.map(boundBrowserLegacyEvent);
|
|
2823
|
+
const selected = [];
|
|
2824
|
+
let bytes = 2;
|
|
2825
|
+
const direction = options.direction ?? "newest";
|
|
2826
|
+
const start = direction === "newest" ? safe.length - 1 : 0;
|
|
2827
|
+
const end = direction === "newest" ? Math.max(-1, safe.length - maxCount - 1) : safe.length;
|
|
2828
|
+
const step = direction === "newest" ? -1 : 1;
|
|
2829
|
+
for (let index = start; index !== end && selected.length < maxCount; index += step) {
|
|
2830
|
+
const event = safe[index];
|
|
2831
|
+
const eventBytes = browserJsonBytes(event);
|
|
2832
|
+
const separator = selected.length === 0 ? 0 : 1;
|
|
2833
|
+
if (bytes + separator + eventBytes > maxBytes) break;
|
|
2834
|
+
selected.push(event);
|
|
2835
|
+
bytes += separator + eventBytes;
|
|
2836
|
+
}
|
|
2837
|
+
if (direction === "newest") selected.reverse();
|
|
2838
|
+
return {
|
|
2839
|
+
events: selected,
|
|
2840
|
+
bytes,
|
|
2841
|
+
truncated: selected.length < safe.length
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
function boundBrowserLegacyEvent(event) {
|
|
2845
|
+
const serialized = browserSerialize(event);
|
|
2846
|
+
const originalBytes = serialized.serializable ? encoder.encode(serialized.value).byteLength : null;
|
|
2847
|
+
const typeIsSafe = browserUtf8Bytes(event.type) <= BROWSER_EVENT_TYPE_MAX_BYTES && !event.type.includes("\n") && !event.type.includes("\r");
|
|
2848
|
+
const clientEventId = boundBrowserOptionalText(
|
|
2849
|
+
event.clientEventId,
|
|
2850
|
+
BROWSER_EVENT_CLIENT_ID_MAX_BYTES
|
|
2851
|
+
);
|
|
2852
|
+
const duplicateReason = boundBrowserOptionalText(
|
|
2853
|
+
event.duplicateReason,
|
|
2854
|
+
BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES
|
|
2855
|
+
);
|
|
2856
|
+
if (serialized.serializable && originalBytes !== null && originalBytes <= SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES && typeIsSafe && clientEventId === event.clientEventId && duplicateReason === event.duplicateReason) {
|
|
2857
|
+
return event;
|
|
2858
|
+
}
|
|
2859
|
+
const envelopeProjection = [
|
|
2860
|
+
!typeIsSafe ? browserEnvelopeFieldProjection("type", event.type, "session.event.envelope_omitted") : null,
|
|
2861
|
+
clientEventId !== event.clientEventId ? browserEnvelopeFieldProjection("clientEventId", event.clientEventId, clientEventId) : null,
|
|
2862
|
+
duplicateReason !== event.duplicateReason ? browserEnvelopeFieldProjection("duplicateReason", event.duplicateReason, duplicateReason) : null
|
|
2863
|
+
].filter((field) => field !== null);
|
|
2864
|
+
const payloadSerialization = browserSerialize(event.payload);
|
|
2865
|
+
const payloadBytes = payloadSerialization.serializable ? encoder.encode(payloadSerialization.value).byteLength : null;
|
|
2866
|
+
const preview = truncateBrowserUtf8Middle(
|
|
2867
|
+
payloadSerialization.value,
|
|
2868
|
+
BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES
|
|
2869
|
+
);
|
|
2870
|
+
const truncation = {
|
|
2871
|
+
truncated: true,
|
|
2872
|
+
surface: "browser_legacy_guard",
|
|
2873
|
+
reason: serialized.serializable ? "event_envelope_bytes_exceeded" : "event_not_serializable",
|
|
2874
|
+
originalBytes,
|
|
2875
|
+
deliveredBytes: 0,
|
|
2876
|
+
omittedBytes: originalBytes,
|
|
2877
|
+
estimatedOriginalTokens: originalBytes === null ? null : Math.ceil(originalBytes / 4),
|
|
2878
|
+
estimatedDeliveredTokens: 0,
|
|
2879
|
+
fullEvidence: { available: false, reason: "not_retained" },
|
|
2880
|
+
details: [
|
|
2881
|
+
{
|
|
2882
|
+
path: "$.payload",
|
|
2883
|
+
kind: "payload_preview",
|
|
2884
|
+
originalBytes: payloadBytes,
|
|
2885
|
+
deliveredBytes: browserUtf8Bytes(preview)
|
|
2886
|
+
}
|
|
2887
|
+
]
|
|
2888
|
+
};
|
|
2889
|
+
const payload = {
|
|
2890
|
+
...browserPayloadIdentity(event.payload),
|
|
2891
|
+
preview,
|
|
2892
|
+
...envelopeProjection.length > 0 ? {
|
|
2893
|
+
originalType: boundBrowserText(event.type, BROWSER_EVENT_TYPE_MAX_BYTES),
|
|
2894
|
+
envelopeProjection: {
|
|
2895
|
+
truncated: true,
|
|
2896
|
+
surface: "browser_legacy_guard",
|
|
2897
|
+
fields: envelopeProjection
|
|
2898
|
+
}
|
|
2899
|
+
} : {},
|
|
2900
|
+
truncation
|
|
2901
|
+
};
|
|
2902
|
+
const bounded = {
|
|
2903
|
+
id: boundBrowserText(event.id, BROWSER_EVENT_ID_MAX_BYTES),
|
|
2904
|
+
workspaceId: boundBrowserText(event.workspaceId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
2905
|
+
sessionId: boundBrowserText(event.sessionId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
2906
|
+
sequence: event.sequence,
|
|
2907
|
+
type: typeIsSafe ? event.type : "session.event.envelope_omitted",
|
|
2908
|
+
payload,
|
|
2909
|
+
occurredAt: boundBrowserText(event.occurredAt, BROWSER_EVENT_ID_MAX_BYTES),
|
|
2910
|
+
clientEventId,
|
|
2911
|
+
turnId: boundBrowserOptionalText(event.turnId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
2912
|
+
turnGeneration: event.turnGeneration,
|
|
2913
|
+
turnAttemptId: boundBrowserOptionalText(event.turnAttemptId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
2914
|
+
turnAssociation: event.turnAssociation,
|
|
2915
|
+
duplicateOfEventId: boundBrowserOptionalText(
|
|
2916
|
+
event.duplicateOfEventId,
|
|
2917
|
+
BROWSER_EVENT_ID_MAX_BYTES
|
|
2918
|
+
),
|
|
2919
|
+
duplicateReason
|
|
2920
|
+
};
|
|
2921
|
+
settleBrowserEventTruncation(bounded, truncation);
|
|
2922
|
+
if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
|
|
2923
|
+
payload.preview = truncateBrowserUtf8Middle(String(payload.preview), 4 * 1024);
|
|
2924
|
+
truncation.details = truncation.details.slice(0, 1);
|
|
2925
|
+
settleBrowserEventTruncation(bounded, truncation);
|
|
2926
|
+
}
|
|
2927
|
+
if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
|
|
2928
|
+
bounded.clientEventId = null;
|
|
2929
|
+
bounded.duplicateReason = null;
|
|
2930
|
+
payload.preview = "[legacy event omitted at the browser byte boundary]";
|
|
2931
|
+
settleBrowserEventTruncation(bounded, truncation);
|
|
2932
|
+
}
|
|
2933
|
+
return bounded;
|
|
2934
|
+
}
|
|
2935
|
+
function browserJsonBytes(value) {
|
|
2936
|
+
return encoder.encode(browserSerialize(value).value).byteLength;
|
|
2937
|
+
}
|
|
2938
|
+
function browserSerialize(value) {
|
|
2939
|
+
try {
|
|
2940
|
+
const serialized = JSON.stringify(value);
|
|
2941
|
+
return { value: serialized === void 0 ? "null" : serialized, serializable: true };
|
|
2942
|
+
} catch {
|
|
2943
|
+
return {
|
|
2944
|
+
value: '"[unserializable event payload omitted at browser boundary]"',
|
|
2945
|
+
serializable: false
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
function browserPayloadIdentity(payload) {
|
|
2950
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return {};
|
|
2951
|
+
const record = payload;
|
|
2952
|
+
const identity = {};
|
|
2953
|
+
for (const field of BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS) {
|
|
2954
|
+
const value = record[field];
|
|
2955
|
+
if (typeof value === "string") {
|
|
2956
|
+
identity[field] = boundBrowserText(value, BROWSER_EVENT_ID_MAX_BYTES);
|
|
2957
|
+
} else if (typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
2958
|
+
identity[field] = value;
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
return identity;
|
|
2962
|
+
}
|
|
2963
|
+
function browserEnvelopeFieldProjection(field, original, delivered) {
|
|
2964
|
+
return {
|
|
2965
|
+
field,
|
|
2966
|
+
originalBytes: typeof original === "string" ? browserUtf8Bytes(original) : 0,
|
|
2967
|
+
deliveredBytes: typeof delivered === "string" ? browserUtf8Bytes(delivered) : 0
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
function boundBrowserOptionalText(value, maxBytes) {
|
|
2971
|
+
return typeof value === "string" ? boundBrowserText(value, maxBytes) : value;
|
|
2972
|
+
}
|
|
2973
|
+
function boundBrowserText(value, maxBytes) {
|
|
2974
|
+
const bytes = encoder.encode(value);
|
|
2975
|
+
if (bytes.byteLength <= maxBytes) return value;
|
|
2976
|
+
const marker = "\u2026[truncated]";
|
|
2977
|
+
const prefixBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
|
|
2978
|
+
let prefixEnd = Math.min(prefixBudget, bytes.byteLength);
|
|
2979
|
+
while (prefixEnd > 0 && prefixEnd < bytes.byteLength && isBrowserUtf8Continuation(bytes[prefixEnd])) {
|
|
2980
|
+
prefixEnd -= 1;
|
|
2981
|
+
}
|
|
2982
|
+
return `${decoder.decode(bytes.subarray(0, prefixEnd))}${marker}`;
|
|
2983
|
+
}
|
|
2984
|
+
function truncateBrowserUtf8Middle(value, maxBytes) {
|
|
2985
|
+
const bytes = encoder.encode(value);
|
|
2986
|
+
if (bytes.byteLength <= maxBytes) return value;
|
|
2987
|
+
const marker = `\u2026[${bytes.byteLength - maxBytes} bytes omitted]\u2026`;
|
|
2988
|
+
const contentBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
|
|
2989
|
+
const leftBudget = Math.floor(contentBudget / 2);
|
|
2990
|
+
const rightBudget = contentBudget - leftBudget;
|
|
2991
|
+
let leftEnd = Math.min(leftBudget, bytes.byteLength);
|
|
2992
|
+
while (leftEnd > 0 && leftEnd < bytes.byteLength && isBrowserUtf8Continuation(bytes[leftEnd])) {
|
|
2993
|
+
leftEnd -= 1;
|
|
2994
|
+
}
|
|
2995
|
+
let rightStart = Math.max(0, bytes.byteLength - rightBudget);
|
|
2996
|
+
while (rightStart < bytes.byteLength && isBrowserUtf8Continuation(bytes[rightStart])) {
|
|
2997
|
+
rightStart += 1;
|
|
2998
|
+
}
|
|
2999
|
+
return `${decoder.decode(bytes.subarray(0, leftEnd))}${marker}${decoder.decode(bytes.subarray(rightStart))}`;
|
|
3000
|
+
}
|
|
3001
|
+
function settleBrowserEventTruncation(event, truncation) {
|
|
3002
|
+
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
3003
|
+
const deliveredBytes = browserJsonBytes(event);
|
|
3004
|
+
const omittedBytes = truncation.originalBytes === null ? null : Math.max(0, truncation.originalBytes - deliveredBytes);
|
|
3005
|
+
const estimatedDeliveredTokens = Math.ceil(deliveredBytes / 4);
|
|
3006
|
+
if (truncation.deliveredBytes === deliveredBytes && truncation.omittedBytes === omittedBytes && truncation.estimatedDeliveredTokens === estimatedDeliveredTokens) {
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
truncation.deliveredBytes = deliveredBytes;
|
|
3010
|
+
truncation.omittedBytes = omittedBytes;
|
|
3011
|
+
truncation.estimatedDeliveredTokens = estimatedDeliveredTokens;
|
|
3012
|
+
}
|
|
3013
|
+
throw new RangeError("Browser event byte accounting did not converge");
|
|
3014
|
+
}
|
|
3015
|
+
function browserUtf8Bytes(value) {
|
|
3016
|
+
return encoder.encode(value).byteLength;
|
|
3017
|
+
}
|
|
3018
|
+
function isBrowserUtf8Continuation(value) {
|
|
3019
|
+
return (value & 192) === 128;
|
|
3020
|
+
}
|
|
3021
|
+
function observeSessionStatus(events, ref) {
|
|
3022
|
+
let latest = null;
|
|
3023
|
+
for (const event of events) {
|
|
3024
|
+
const status = sessionStatusFromEvents([event]);
|
|
3025
|
+
if (status && (!latest || event.sequence >= latest.sequence)) {
|
|
3026
|
+
latest = { sequence: event.sequence, status };
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
if (!latest || latest.sequence < ref.current.sequence) return void 0;
|
|
3030
|
+
ref.current = latest;
|
|
3031
|
+
return latest.status;
|
|
3032
|
+
}
|
|
3033
|
+
async function planForegroundCatchup(client, workspaceId, sessionId, cursor, options = {}) {
|
|
3034
|
+
if (options.signal?.aborted) throw abortError();
|
|
3035
|
+
const session = await client.getSession(workspaceId, sessionId, {
|
|
3036
|
+
fresh: true,
|
|
3037
|
+
...options.signal ? { signal: options.signal } : {}
|
|
3038
|
+
});
|
|
3039
|
+
if (options.signal?.aborted) throw abortError();
|
|
3040
|
+
const durableHead = Math.max(cursor, session.lastSequence);
|
|
3041
|
+
const rawGap = durableHead - cursor;
|
|
3042
|
+
if (rawGap <= FOREGROUND_DIRECT_REPLAY_MAX_SEQUENCES) {
|
|
3043
|
+
return { kind: "resume" };
|
|
3044
|
+
}
|
|
3045
|
+
if (rawGap > FOREGROUND_COMPACT_PROBE_MAX_SEQUENCES) {
|
|
3046
|
+
return { kind: "reload" };
|
|
3047
|
+
}
|
|
3048
|
+
const page = await loadNextPage(client, workspaceId, sessionId, cursor, {
|
|
3049
|
+
pageSize: rawGap,
|
|
3050
|
+
...options.signal ? { signal: options.signal } : {}
|
|
3051
|
+
});
|
|
3052
|
+
const events = page.filter((event) => eventResumeSequence(event) > cursor).map(boundBrowserLegacyEvent);
|
|
3053
|
+
if (events.length === 0) {
|
|
3054
|
+
return { kind: "reload" };
|
|
3055
|
+
}
|
|
3056
|
+
assertAscending(events);
|
|
3057
|
+
const resumeSequence = maxResumeSequence(events);
|
|
3058
|
+
if (resumeSequence < durableHead) {
|
|
3059
|
+
return { kind: "reload" };
|
|
3060
|
+
}
|
|
3061
|
+
if (events.length > FOREGROUND_COMPACT_CATCHUP_MAX_EVENTS || groupCount(events) > FOREGROUND_COMPACT_CATCHUP_MAX_GROUPS || browserJsonBytes(events) > FOREGROUND_COMPACT_CATCHUP_MAX_BYTES) {
|
|
3062
|
+
return { kind: "reload" };
|
|
3063
|
+
}
|
|
3064
|
+
return { kind: "append", events, resumeSequence };
|
|
3065
|
+
}
|
|
3066
|
+
async function loadEventWindow(client, workspaceId, sessionId, options) {
|
|
3067
|
+
let cursor = options.before;
|
|
3068
|
+
let buffer = [];
|
|
3069
|
+
let reachedStart = false;
|
|
3070
|
+
let fetches = 0;
|
|
3071
|
+
while (fetches < options.maxFetches) {
|
|
3072
|
+
if (buffer.length > 0 && groupCount(buffer) >= options.targetGroups) {
|
|
3073
|
+
break;
|
|
3074
|
+
}
|
|
3075
|
+
const page = await loadPreviousPage(client, workspaceId, sessionId, cursor, {
|
|
3076
|
+
pageSize: options.pageSize,
|
|
3077
|
+
...options.signal ? { signal: options.signal } : {}
|
|
3078
|
+
});
|
|
3079
|
+
fetches += 1;
|
|
3080
|
+
if (page.length === 0) {
|
|
3081
|
+
reachedStart = true;
|
|
3082
|
+
break;
|
|
3083
|
+
}
|
|
3084
|
+
assertAscending(page);
|
|
3085
|
+
buffer = [...page, ...buffer];
|
|
3086
|
+
cursor = page[0].sequence;
|
|
3087
|
+
if (isLogStart(page[0])) {
|
|
3088
|
+
reachedStart = true;
|
|
3089
|
+
break;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
let snapPages = 0;
|
|
3093
|
+
while (!reachedStart && findBoundaryIndex(buffer) === -1 && snapPages < (options.boundaryPageCap ?? 0)) {
|
|
3094
|
+
const page = await loadPreviousPage(client, workspaceId, sessionId, cursor, {
|
|
3095
|
+
pageSize: options.pageSize,
|
|
3096
|
+
...options.signal ? { signal: options.signal } : {}
|
|
3097
|
+
});
|
|
3098
|
+
fetches += 1;
|
|
3099
|
+
snapPages += 1;
|
|
3100
|
+
if (page.length === 0) {
|
|
3101
|
+
reachedStart = true;
|
|
3102
|
+
break;
|
|
3103
|
+
}
|
|
3104
|
+
assertAscending(page);
|
|
3105
|
+
buffer = [...page, ...buffer];
|
|
3106
|
+
cursor = page[0].sequence;
|
|
3107
|
+
if (isLogStart(page[0])) {
|
|
3108
|
+
reachedStart = true;
|
|
3109
|
+
break;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
if (!reachedStart) {
|
|
3113
|
+
const boundary = findBoundaryIndex(buffer);
|
|
3114
|
+
if (boundary > 0) {
|
|
3115
|
+
buffer = buffer.slice(boundary);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
const densityWindow = trimBackwardToGroupTarget(buffer, options.targetGroups);
|
|
3119
|
+
buffer = densityWindow.events;
|
|
3120
|
+
const oldest = buffer[0] ?? null;
|
|
3121
|
+
const newest = buffer[buffer.length - 1] ?? null;
|
|
3122
|
+
return {
|
|
3123
|
+
events: buffer,
|
|
3124
|
+
oldestSequence: oldest?.sequence ?? null,
|
|
3125
|
+
newestSequence: newest ? maxResumeSequence(buffer) : 0,
|
|
3126
|
+
hasOlder: buffer.length > 0 && (densityWindow.trimmed || !reachedStart && oldest?.type !== "session.created")
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
function findBoundaryIndex(events) {
|
|
3130
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
3131
|
+
if (isTurnBoundary2(events[index])) {
|
|
3132
|
+
return index;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
return -1;
|
|
3136
|
+
}
|
|
3137
|
+
function isTurnBoundary2(event) {
|
|
3138
|
+
return event.type === "session.created" || event.type === "user.message";
|
|
3139
|
+
}
|
|
3140
|
+
async function loadForwardEventWindow(client, workspaceId, sessionId, options) {
|
|
3141
|
+
let cursor = options.after;
|
|
3142
|
+
let buffer = [];
|
|
3143
|
+
let fetches = 0;
|
|
3144
|
+
let reachedEnd = false;
|
|
3145
|
+
while (fetches < options.maxFetches) {
|
|
3146
|
+
if (buffer.length > 0 && groupCount(buffer) >= options.targetGroups) {
|
|
3147
|
+
break;
|
|
3148
|
+
}
|
|
3149
|
+
const page = await loadNextPage(client, workspaceId, sessionId, cursor, {
|
|
3150
|
+
pageSize: options.pageSize,
|
|
3151
|
+
...options.signal ? { signal: options.signal } : {}
|
|
3152
|
+
});
|
|
3153
|
+
fetches += 1;
|
|
3154
|
+
if (page.length === 0) {
|
|
3155
|
+
reachedEnd = true;
|
|
3156
|
+
break;
|
|
3157
|
+
}
|
|
3158
|
+
assertAscending(page);
|
|
3159
|
+
buffer = [...buffer, ...page];
|
|
3160
|
+
cursor = maxResumeSequence(page);
|
|
3161
|
+
}
|
|
3162
|
+
const densityWindow = trimForwardToGroupTarget(buffer, options.targetGroups);
|
|
3163
|
+
buffer = densityWindow.events;
|
|
3164
|
+
const oldest = buffer[0] ?? null;
|
|
3165
|
+
const newest = buffer[buffer.length - 1] ?? null;
|
|
3166
|
+
return {
|
|
3167
|
+
events: buffer,
|
|
3168
|
+
oldestSequence: oldest?.sequence ?? null,
|
|
3169
|
+
newestSequence: newest ? maxResumeSequence(buffer) : 0,
|
|
3170
|
+
hasNewer: buffer.length > 0 && (densityWindow.trimmed || !reachedEnd)
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
function trimBackwardToGroupTarget(events, targetGroups) {
|
|
3174
|
+
if (!Number.isFinite(targetGroups) || targetGroups <= 0 || events.length === 0) {
|
|
3175
|
+
return { events, trimmed: false };
|
|
3176
|
+
}
|
|
3177
|
+
for (let index = events.length - 1; index > 0; index -= 1) {
|
|
3178
|
+
if (!isTurnBoundary2(events[index])) continue;
|
|
3179
|
+
const candidate = events.slice(index);
|
|
3180
|
+
if (groupCount(candidate) >= targetGroups) {
|
|
3181
|
+
return { events: candidate, trimmed: true };
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
return { events, trimmed: false };
|
|
3185
|
+
}
|
|
3186
|
+
function trimForwardToGroupTarget(events, targetGroups) {
|
|
3187
|
+
if (!Number.isFinite(targetGroups) || targetGroups <= 0 || events.length === 0) {
|
|
3188
|
+
return { events, trimmed: false };
|
|
3189
|
+
}
|
|
3190
|
+
for (let index = 1; index < events.length; index += 1) {
|
|
3191
|
+
if (!isTurnBoundary2(events[index])) continue;
|
|
3192
|
+
const candidate = events.slice(0, index);
|
|
3193
|
+
if (groupCount(candidate) >= targetGroups) {
|
|
3194
|
+
return { events: candidate, trimmed: true };
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
return { events, trimmed: false };
|
|
3198
|
+
}
|
|
3199
|
+
async function loadPreviousPage(client, workspaceId, sessionId, before, options) {
|
|
3200
|
+
if (options.signal?.aborted) {
|
|
3201
|
+
throw abortError();
|
|
3202
|
+
}
|
|
3203
|
+
const requested = options.pageSize;
|
|
3204
|
+
if (requested === 0) {
|
|
3205
|
+
return Object.assign([], { requested });
|
|
3206
|
+
}
|
|
3207
|
+
const page = await client.listEvents(workspaceId, sessionId, {
|
|
3208
|
+
before,
|
|
3209
|
+
limit: requested,
|
|
3210
|
+
compact: true,
|
|
3211
|
+
// Monitoring summaries omit correlation fields once a bounded durable
|
|
3212
|
+
// payload exceeds their preview threshold. The browser timeline needs the
|
|
3213
|
+
// exact stored payload so persisted tool outputs still match their
|
|
3214
|
+
// calls and expose truthful truncation telemetry after a reload.
|
|
3215
|
+
payloadMode: "full"
|
|
3216
|
+
});
|
|
3217
|
+
if (options.signal?.aborted) {
|
|
3218
|
+
throw abortError();
|
|
3219
|
+
}
|
|
3220
|
+
return Object.assign(page, { requested });
|
|
3221
|
+
}
|
|
3222
|
+
async function loadNextPage(client, workspaceId, sessionId, after, options) {
|
|
3223
|
+
if (options.signal?.aborted) {
|
|
3224
|
+
throw abortError();
|
|
3225
|
+
}
|
|
3226
|
+
const requested = options.pageSize;
|
|
3227
|
+
if (requested === 0) {
|
|
3228
|
+
return Object.assign([], { requested });
|
|
3229
|
+
}
|
|
3230
|
+
const page = await client.listEvents(workspaceId, sessionId, {
|
|
3231
|
+
after,
|
|
3232
|
+
limit: requested,
|
|
3233
|
+
compact: true,
|
|
3234
|
+
direction: "after",
|
|
3235
|
+
payloadMode: "full"
|
|
3236
|
+
});
|
|
3237
|
+
if (options.signal?.aborted) {
|
|
3238
|
+
throw abortError();
|
|
3239
|
+
}
|
|
3240
|
+
return Object.assign(page, { requested });
|
|
3241
|
+
}
|
|
3242
|
+
function groupCount(events) {
|
|
3243
|
+
if (events.length === 0) {
|
|
3244
|
+
return 0;
|
|
3245
|
+
}
|
|
3246
|
+
return groupTimeline(buildTimeline(events)).length;
|
|
3247
|
+
}
|
|
3248
|
+
function isLogStart(event) {
|
|
3249
|
+
return event.type === "session.created" || event.sequence <= 1;
|
|
3250
|
+
}
|
|
3251
|
+
function maxResumeSequence(events) {
|
|
3252
|
+
return events.reduce((max, event) => Math.max(max, eventResumeSequence(event)), 0);
|
|
3253
|
+
}
|
|
3254
|
+
function maxResumeSequenceOrNull(events) {
|
|
3255
|
+
return events.length > 0 ? maxResumeSequence(events) : null;
|
|
3256
|
+
}
|
|
3257
|
+
function eventResumeSequence(event) {
|
|
3258
|
+
const streamedCoverage = sessionEventStreamCoveredThrough(event);
|
|
3259
|
+
if (streamedCoverage !== null) return streamedCoverage;
|
|
3260
|
+
return typeof event.coveredThrough === "number" && Number.isSafeInteger(event.coveredThrough) && event.coveredThrough >= event.sequence ? event.coveredThrough : event.sequence;
|
|
3261
|
+
}
|
|
3262
|
+
function assertAscending(events) {
|
|
3263
|
+
for (let index = 1; index < events.length; index += 1) {
|
|
3264
|
+
if (events[index - 1].sequence >= events[index].sequence) {
|
|
3265
|
+
throw new Error("@opengeni/react: session events must be ordered by ascending sequence");
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
function assertPrependOrder(existing, older) {
|
|
3270
|
+
if (!shouldAssertDevelopment() || existing.length === 0 || older.length === 0) {
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
if (older[older.length - 1].sequence >= existing[0].sequence) {
|
|
3274
|
+
throw new Error("@opengeni/react: loadOlder returned overlapping session events");
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
function assertAppendOrder(existing, newer) {
|
|
3278
|
+
if (!shouldAssertDevelopment() || existing.length === 0 || newer.length === 0) {
|
|
3279
|
+
return;
|
|
3280
|
+
}
|
|
3281
|
+
if (newer[0].sequence <= existing[existing.length - 1].sequence) {
|
|
3282
|
+
throw new Error("@opengeni/react: loadNewer returned overlapping session events");
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
function shouldAssertDevelopment() {
|
|
3286
|
+
const processEnv = globalThis.process?.env;
|
|
3287
|
+
return processEnv !== void 0 && processEnv.NODE_ENV !== "production";
|
|
3288
|
+
}
|
|
3289
|
+
function abortError() {
|
|
3290
|
+
const error = new Error("The operation was aborted.");
|
|
3291
|
+
error.name = "AbortError";
|
|
3292
|
+
return error;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
// src/hooks/use-session.ts
|
|
3296
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
|
|
3297
|
+
function isTitleEvent(event) {
|
|
3298
|
+
return event.type === "session.title_set";
|
|
3299
|
+
}
|
|
3300
|
+
function isSessionDetailEvent(event) {
|
|
3301
|
+
return isTitleEvent(event) || event.type.startsWith("session.command.");
|
|
3302
|
+
}
|
|
3303
|
+
function useSession(sessionId, options = {}) {
|
|
3304
|
+
const { client, workspaceId, workspaceControlEvent, registerSessionReconciler } = useEmbeddedSessionRead(options);
|
|
3305
|
+
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
3306
|
+
const [override, setOverride] = useState2(null);
|
|
3307
|
+
const nextReadRevision = useRef2(0);
|
|
3308
|
+
const nextReadGeneration = useRef2(0);
|
|
3309
|
+
const beginRead = options.beginRead;
|
|
3310
|
+
const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
|
|
3311
|
+
const load = useCallback2(
|
|
3312
|
+
async (signal) => {
|
|
3313
|
+
if (!sessionId) {
|
|
3314
|
+
return null;
|
|
3315
|
+
}
|
|
3316
|
+
let readGeneration2 = 0;
|
|
3317
|
+
const fetched = await client.getSession(workspaceId, sessionId, {
|
|
3318
|
+
fresh: true,
|
|
3319
|
+
signal,
|
|
3320
|
+
onRequestStart: () => {
|
|
3321
|
+
readGeneration2 = beginRead?.() ?? ++nextReadGeneration.current;
|
|
3322
|
+
}
|
|
3323
|
+
});
|
|
3324
|
+
setOverride(null);
|
|
3325
|
+
return {
|
|
3326
|
+
session: fetched,
|
|
3327
|
+
revision: ++nextReadRevision.current,
|
|
3328
|
+
readGeneration: readGeneration2
|
|
3329
|
+
};
|
|
3330
|
+
},
|
|
3331
|
+
[beginRead, client, workspaceId, sessionId]
|
|
3332
|
+
);
|
|
3333
|
+
const { data, loading, error, refresh } = usePolledValue(load, {
|
|
3334
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
3335
|
+
enabled
|
|
3336
|
+
});
|
|
3337
|
+
useEffect2(() => {
|
|
3338
|
+
if (enabled && workspaceControlEvent) void refresh();
|
|
3339
|
+
}, [enabled, refresh, workspaceControlEvent]);
|
|
3340
|
+
useEffect2(() => {
|
|
3341
|
+
if (!sessionId || !enabled) return;
|
|
3342
|
+
return registerSessionReconciler(sessionId, "session", refresh);
|
|
3343
|
+
}, [enabled, refresh, registerSessionReconciler, sessionId]);
|
|
3344
|
+
const base = data?.session ?? null;
|
|
3345
|
+
const readRevision = data?.revision ?? 0;
|
|
3346
|
+
const readGeneration = data?.readGeneration ?? 0;
|
|
3347
|
+
const session = useMemo2(
|
|
3348
|
+
() => base && override && override.id === base.id ? { ...base, title: override.title, titleSource: override.titleSource } : base,
|
|
3349
|
+
[base, override]
|
|
3350
|
+
);
|
|
3351
|
+
const sharedEvents = options.events;
|
|
3352
|
+
const onTitleEvent = useCallback2(
|
|
3353
|
+
(event) => {
|
|
3354
|
+
if (sharedEvents?.some(
|
|
3355
|
+
(item) => item.sessionId === sessionId && item.type.startsWith("session.command.") && item.sequence > (base?.lastSequence ?? -1)
|
|
3356
|
+
)) {
|
|
3357
|
+
void refresh();
|
|
3358
|
+
} else if (event.type.startsWith("session.command.")) {
|
|
3359
|
+
if (!base || event.sequence > base.lastSequence) void refresh();
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
if (!base || event.sequence <= base.lastSequence) {
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
const payload = event.payload ?? {};
|
|
3366
|
+
const title = payload.title;
|
|
3367
|
+
if (typeof title !== "string") {
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
const source = payload.source === "user" || payload.source === "agent" ? payload.source : null;
|
|
3371
|
+
setOverride((current) => {
|
|
3372
|
+
const next = current ?? base;
|
|
3373
|
+
return { ...next, title, titleSource: source };
|
|
3374
|
+
});
|
|
3375
|
+
},
|
|
3376
|
+
[base, refresh, sharedEvents, sessionId]
|
|
3377
|
+
);
|
|
3378
|
+
useSessionEventTrigger(client, workspaceId, sessionId, isSessionDetailEvent, onTitleEvent, {
|
|
3379
|
+
enabled,
|
|
3380
|
+
...options.events !== void 0 ? { events: options.events } : {}
|
|
3381
|
+
});
|
|
3382
|
+
const updateTitle = useCallback2(
|
|
3383
|
+
async (title) => {
|
|
3384
|
+
if (!sessionId) {
|
|
3385
|
+
return null;
|
|
3386
|
+
}
|
|
3387
|
+
const result = await run(() => client.updateSession(workspaceId, sessionId, { title }));
|
|
3388
|
+
if (result) {
|
|
3389
|
+
setOverride(result);
|
|
3390
|
+
}
|
|
3391
|
+
return result;
|
|
3392
|
+
},
|
|
3393
|
+
[client, workspaceId, sessionId, run]
|
|
3394
|
+
);
|
|
3395
|
+
return {
|
|
3396
|
+
session,
|
|
3397
|
+
loading,
|
|
3398
|
+
error,
|
|
3399
|
+
readRevision,
|
|
3400
|
+
readGeneration,
|
|
3401
|
+
refresh,
|
|
3402
|
+
updateTitle,
|
|
3403
|
+
updating: mutating,
|
|
3404
|
+
mutationError,
|
|
3405
|
+
clearMutationError
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// src/hooks/use-turn-queue.ts
|
|
3410
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
|
|
3411
|
+
function isTurnQueueEvent(event) {
|
|
3412
|
+
return event.type.startsWith("turn.") || event.type.startsWith("session.queue.") || event.type.startsWith("session.control.") || event.type.startsWith("system.update.") || event.type.startsWith("workspace.inference.");
|
|
3413
|
+
}
|
|
3414
|
+
function queueSnapshotCovers(candidate, observed) {
|
|
3415
|
+
return Boolean(
|
|
3416
|
+
candidate && candidate.version >= observed.version && candidate.effectiveControl.controlVersion >= observed.effectiveControl.controlVersion
|
|
3417
|
+
);
|
|
3418
|
+
}
|
|
3419
|
+
var EMPTY_PENDING_BY_TURN = {};
|
|
3420
|
+
function useTurnQueue(sessionId, options = {}) {
|
|
3421
|
+
const { client, workspaceId, workspaceControlEvent, registerSessionReconciler } = useEmbeddedSession(options);
|
|
3422
|
+
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
3423
|
+
const targetKey = `${workspaceId}\0${sessionId ?? ""}`;
|
|
3424
|
+
const pageLive = usePageLiveActivity();
|
|
3425
|
+
const [snapshot, setSnapshot] = useState3(null);
|
|
3426
|
+
const [stateTargetKey, setStateTargetKey] = useState3(targetKey);
|
|
3427
|
+
const [loading, setLoading] = useState3(enabled);
|
|
3428
|
+
const [error, setError] = useState3(null);
|
|
3429
|
+
const [mutationError, setMutationError] = useState3(null);
|
|
3430
|
+
const [pendingByTurn, setPendingByTurn] = useState3({});
|
|
3431
|
+
const [acceptedSteers, setAcceptedSteers] = useState3([]);
|
|
3432
|
+
const pendingRef = useRef3({});
|
|
3433
|
+
const readGeneration = useRef3(0);
|
|
3434
|
+
const targetKeyRef = useRef3(targetKey);
|
|
3435
|
+
const snapshotRef = useRef3(null);
|
|
3436
|
+
useLayoutEffect(() => {
|
|
3437
|
+
if (targetKeyRef.current === targetKey) return;
|
|
3438
|
+
targetKeyRef.current = targetKey;
|
|
3439
|
+
readGeneration.current += 1;
|
|
3440
|
+
snapshotRef.current = null;
|
|
3441
|
+
pendingRef.current = {};
|
|
3442
|
+
setStateTargetKey(targetKey);
|
|
3443
|
+
setSnapshot(null);
|
|
3444
|
+
setLoading(enabled);
|
|
3445
|
+
setError(null);
|
|
3446
|
+
setMutationError(null);
|
|
3447
|
+
setPendingByTurn({});
|
|
3448
|
+
setAcceptedSteers([]);
|
|
3449
|
+
}, [enabled, targetKey]);
|
|
3450
|
+
useEffect3(() => {
|
|
3451
|
+
if (acceptedSteers.length === 0 || options.events === void 0) return;
|
|
3452
|
+
const durablyStartedTurnIds = new Set(
|
|
3453
|
+
options.events.flatMap(
|
|
3454
|
+
(event) => event.type === "turn.started" && event.turnId ? [event.turnId] : []
|
|
3455
|
+
)
|
|
3456
|
+
);
|
|
3457
|
+
if (durablyStartedTurnIds.size === 0) return;
|
|
3458
|
+
setAcceptedSteers(
|
|
3459
|
+
(current) => current.filter((steer) => !durablyStartedTurnIds.has(steer.turnId))
|
|
3460
|
+
);
|
|
3461
|
+
}, [acceptedSteers.length, options.events]);
|
|
3462
|
+
const acceptSnapshot = useCallback3(
|
|
3463
|
+
(ownedTargetKey, next) => {
|
|
3464
|
+
if (targetKeyRef.current !== ownedTargetKey) return false;
|
|
3465
|
+
const current = snapshotRef.current;
|
|
3466
|
+
if (next && current && (next.version < current.version || next.effectiveControl.controlVersion < current.effectiveControl.controlVersion)) {
|
|
3467
|
+
return false;
|
|
3468
|
+
}
|
|
3469
|
+
snapshotRef.current = next;
|
|
3470
|
+
setStateTargetKey(ownedTargetKey);
|
|
3471
|
+
setSnapshot(next);
|
|
3472
|
+
return true;
|
|
3473
|
+
},
|
|
3474
|
+
[]
|
|
3475
|
+
);
|
|
3476
|
+
const load = useCallback3(
|
|
3477
|
+
async (rejectOnFailure = false) => {
|
|
3478
|
+
if (!sessionId) return;
|
|
3479
|
+
const ownedTargetKey = `${workspaceId}\0${sessionId}`;
|
|
3480
|
+
const ticket = ++readGeneration.current;
|
|
3481
|
+
try {
|
|
3482
|
+
const fetched = await client.getQueue(workspaceId, sessionId);
|
|
3483
|
+
if (targetKeyRef.current !== ownedTargetKey) return;
|
|
3484
|
+
const ownsLatestRead = ticket === readGeneration.current;
|
|
3485
|
+
if (rejectOnFailure) {
|
|
3486
|
+
const committed = acceptSnapshot(ownedTargetKey, fetched);
|
|
3487
|
+
if (!committed && !queueSnapshotCovers(snapshotRef.current, fetched)) {
|
|
3488
|
+
throw new TypeError("Queue reconciliation did not commit authoritative state");
|
|
3489
|
+
}
|
|
3490
|
+
setError(null);
|
|
3491
|
+
if (ownsLatestRead) setLoading(false);
|
|
3492
|
+
} else if (ownsLatestRead) {
|
|
3493
|
+
acceptSnapshot(ownedTargetKey, fetched);
|
|
3494
|
+
setError(null);
|
|
3495
|
+
setLoading(false);
|
|
3496
|
+
}
|
|
3497
|
+
} catch (cause) {
|
|
3498
|
+
if (targetKeyRef.current === ownedTargetKey && (ticket === readGeneration.current || rejectOnFailure)) {
|
|
3499
|
+
setError(asError(cause));
|
|
3500
|
+
if (ticket === readGeneration.current) setLoading(false);
|
|
3501
|
+
}
|
|
3502
|
+
if (rejectOnFailure) throw cause;
|
|
3503
|
+
}
|
|
3504
|
+
},
|
|
3505
|
+
[client, workspaceId, sessionId, acceptSnapshot]
|
|
3506
|
+
);
|
|
3507
|
+
useEffect3(() => {
|
|
3508
|
+
if (!enabled || !pageLive) {
|
|
3509
|
+
setLoading(false);
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
3512
|
+
setLoading(true);
|
|
3513
|
+
const pollIntervalMs = options.pollIntervalMs;
|
|
3514
|
+
if (pollIntervalMs === void 0 || pollIntervalMs <= 0) {
|
|
3515
|
+
void load();
|
|
3516
|
+
return () => {
|
|
3517
|
+
readGeneration.current += 1;
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
let cancelled = false;
|
|
3521
|
+
let timer = null;
|
|
3522
|
+
const schedule = () => {
|
|
3523
|
+
if (cancelled) return;
|
|
3524
|
+
timer = setTimeout(() => {
|
|
3525
|
+
timer = null;
|
|
3526
|
+
void load().finally(schedule);
|
|
3527
|
+
}, pollIntervalMs);
|
|
3528
|
+
};
|
|
3529
|
+
void load().finally(schedule);
|
|
3530
|
+
return () => {
|
|
3531
|
+
cancelled = true;
|
|
3532
|
+
if (timer !== null) clearTimeout(timer);
|
|
3533
|
+
readGeneration.current += 1;
|
|
3534
|
+
};
|
|
3535
|
+
}, [load, enabled, pageLive, options.pollIntervalMs]);
|
|
3536
|
+
useEffect3(() => {
|
|
3537
|
+
if (enabled && workspaceControlEvent) void load();
|
|
3538
|
+
}, [enabled, load, workspaceControlEvent]);
|
|
3539
|
+
useEffect3(() => {
|
|
3540
|
+
if (!sessionId || !enabled) return;
|
|
3541
|
+
return registerSessionReconciler(sessionId, "queue", load);
|
|
3542
|
+
}, [enabled, load, registerSessionReconciler, sessionId]);
|
|
3543
|
+
const scheduleRefresh = useDebouncedCallback(() => void load());
|
|
3544
|
+
useSessionEventTrigger(
|
|
3545
|
+
client,
|
|
3546
|
+
workspaceId,
|
|
3547
|
+
sessionId,
|
|
3548
|
+
isTurnQueueEvent,
|
|
3549
|
+
scheduleRefresh,
|
|
3550
|
+
{
|
|
3551
|
+
enabled,
|
|
3552
|
+
...options.events !== void 0 ? { events: options.events } : {}
|
|
3553
|
+
},
|
|
3554
|
+
async () => await load(true)
|
|
3555
|
+
);
|
|
3556
|
+
const mutate = useCallback3(
|
|
3557
|
+
async (turnId, kind, command) => {
|
|
3558
|
+
const ownedTargetKey = targetKey;
|
|
3559
|
+
if (!sessionId || targetKeyRef.current !== ownedTargetKey || pendingRef.current[turnId]) {
|
|
3560
|
+
return null;
|
|
3561
|
+
}
|
|
3562
|
+
const current = snapshotRef.current;
|
|
3563
|
+
const turn = current?.items.find((candidate) => candidate.id === turnId);
|
|
3564
|
+
if (!current || !turn) return null;
|
|
3565
|
+
pendingRef.current = { ...pendingRef.current, [turnId]: kind };
|
|
3566
|
+
setStateTargetKey(ownedTargetKey);
|
|
3567
|
+
setPendingByTurn(pendingRef.current);
|
|
3568
|
+
setMutationError(null);
|
|
3569
|
+
const clientEventId = operationKey();
|
|
3570
|
+
try {
|
|
3571
|
+
let result;
|
|
3572
|
+
try {
|
|
3573
|
+
result = await command(current, turn, clientEventId);
|
|
3574
|
+
} catch (cause) {
|
|
3575
|
+
if (!isOutcomeUnknownError(cause)) throw cause;
|
|
3576
|
+
result = await command(current, turn, clientEventId);
|
|
3577
|
+
}
|
|
3578
|
+
if (targetKeyRef.current !== ownedTargetKey) return null;
|
|
3579
|
+
acceptSnapshot(ownedTargetKey, result.snapshot);
|
|
3580
|
+
return result;
|
|
3581
|
+
} catch (cause) {
|
|
3582
|
+
if (targetKeyRef.current === ownedTargetKey) {
|
|
3583
|
+
setMutationError(asError(cause));
|
|
3584
|
+
await load();
|
|
3585
|
+
}
|
|
3586
|
+
return null;
|
|
3587
|
+
} finally {
|
|
3588
|
+
if (targetKeyRef.current === ownedTargetKey && turnId in pendingRef.current) {
|
|
3589
|
+
const next = { ...pendingRef.current };
|
|
3590
|
+
delete next[turnId];
|
|
3591
|
+
pendingRef.current = next;
|
|
3592
|
+
setPendingByTurn(next);
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
},
|
|
3596
|
+
[acceptSnapshot, load, sessionId, targetKey]
|
|
3597
|
+
);
|
|
3598
|
+
const moveTurn = useCallback3(
|
|
3599
|
+
async (turnId, beforeTurnId) => {
|
|
3600
|
+
const result = await mutate(
|
|
3601
|
+
turnId,
|
|
3602
|
+
"move",
|
|
3603
|
+
(current, _turn, clientEventId) => client.moveQueueItem(workspaceId, sessionId, turnId, {
|
|
3604
|
+
clientEventId,
|
|
3605
|
+
expectedQueueVersion: current.version,
|
|
3606
|
+
beforeTurnId
|
|
3607
|
+
})
|
|
3608
|
+
);
|
|
3609
|
+
return result !== null;
|
|
3610
|
+
},
|
|
3611
|
+
[client, mutate, sessionId, workspaceId]
|
|
3612
|
+
);
|
|
3613
|
+
const editTurn = useCallback3(
|
|
3614
|
+
async (turnId, edit) => {
|
|
3615
|
+
const result = await mutate(
|
|
3616
|
+
turnId,
|
|
3617
|
+
"edit",
|
|
3618
|
+
(_current, turn, clientEventId) => client.editQueueItem(workspaceId, sessionId, turnId, {
|
|
3619
|
+
clientEventId,
|
|
3620
|
+
expectedTurnVersion: turn.version,
|
|
3621
|
+
expectedDraftRevision: edit.expectedDraftRevision,
|
|
3622
|
+
replaceDraft: edit.replaceDraft
|
|
3623
|
+
})
|
|
3624
|
+
);
|
|
3625
|
+
return result?.draft ?? null;
|
|
3626
|
+
},
|
|
3627
|
+
[client, mutate, sessionId, workspaceId]
|
|
3628
|
+
);
|
|
3629
|
+
const steerTurn = useCallback3(
|
|
3630
|
+
async (turnId) => {
|
|
3631
|
+
const ownedTargetKey = targetKey;
|
|
3632
|
+
const turn = snapshotRef.current?.items.find((candidate) => candidate.id === turnId);
|
|
3633
|
+
if (targetKeyRef.current !== ownedTargetKey) return false;
|
|
3634
|
+
if (!turn) {
|
|
3635
|
+
setMutationError(new Error("That queued prompt changed before Steer could be sent."));
|
|
3636
|
+
await load();
|
|
3637
|
+
return false;
|
|
3638
|
+
}
|
|
3639
|
+
const optimistic = {
|
|
3640
|
+
turnId: turn.id,
|
|
3641
|
+
triggerEventId: turn.triggerEventId,
|
|
3642
|
+
text: turn.prompt,
|
|
3643
|
+
annotations: turn.annotations ?? [],
|
|
3644
|
+
resources: turn.resources,
|
|
3645
|
+
tools: turn.tools,
|
|
3646
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3647
|
+
state: "sending"
|
|
3648
|
+
};
|
|
3649
|
+
setAcceptedSteers((current) => [
|
|
3650
|
+
...current.filter((steer) => steer.turnId !== turnId),
|
|
3651
|
+
optimistic
|
|
3652
|
+
]);
|
|
3653
|
+
const result = await mutate(
|
|
3654
|
+
turnId,
|
|
3655
|
+
"steer",
|
|
3656
|
+
(current, queuedTurn, clientEventId) => client.steerQueueItem(workspaceId, sessionId, turnId, {
|
|
3657
|
+
clientEventId,
|
|
3658
|
+
expectedTurnVersion: queuedTurn.version,
|
|
3659
|
+
controlEtag: current.effectiveControl.controlEtag
|
|
3660
|
+
})
|
|
3661
|
+
);
|
|
3662
|
+
if (targetKeyRef.current !== ownedTargetKey) return false;
|
|
3663
|
+
const advancedWithoutSteer = result === null && snapshotRef.current !== null && !snapshotRef.current.items.some((candidate) => candidate.id === turnId);
|
|
3664
|
+
if (advancedWithoutSteer) {
|
|
3665
|
+
setMutationError(null);
|
|
3666
|
+
setAcceptedSteers((current) => current.filter((steer) => steer.turnId !== turnId));
|
|
3667
|
+
return true;
|
|
3668
|
+
}
|
|
3669
|
+
const accepted = result !== null || snapshotRef.current?.items.some(
|
|
3670
|
+
(candidate) => candidate.id === turnId && candidate.metadata.delivery === "steer"
|
|
3671
|
+
) === true;
|
|
3672
|
+
setAcceptedSteers(
|
|
3673
|
+
(current) => accepted ? current.map(
|
|
3674
|
+
(steer) => steer.turnId === turnId ? { ...steer, state: "queued" } : steer
|
|
3675
|
+
) : current.filter((steer) => steer.turnId !== turnId)
|
|
3676
|
+
);
|
|
3677
|
+
return accepted;
|
|
3678
|
+
},
|
|
3679
|
+
[client, load, mutate, sessionId, targetKey, workspaceId]
|
|
3680
|
+
);
|
|
3681
|
+
const removeTurn = useCallback3(
|
|
3682
|
+
async (turnId) => {
|
|
3683
|
+
const result = await mutate(
|
|
3684
|
+
turnId,
|
|
3685
|
+
"delete",
|
|
3686
|
+
(_current, turn, clientEventId) => client.deleteQueueItem(workspaceId, sessionId, turnId, {
|
|
3687
|
+
clientEventId,
|
|
3688
|
+
expectedTurnVersion: turn.version,
|
|
3689
|
+
reason: "Deleted from the prompt queue"
|
|
3690
|
+
})
|
|
3691
|
+
);
|
|
3692
|
+
return result !== null;
|
|
3693
|
+
},
|
|
3694
|
+
[client, mutate, sessionId, workspaceId]
|
|
3695
|
+
);
|
|
3696
|
+
const identityMatches = stateTargetKey === targetKey;
|
|
3697
|
+
const visibleSnapshot = identityMatches ? snapshot : null;
|
|
3698
|
+
const visiblePendingByTurn = identityMatches ? pendingByTurn : EMPTY_PENDING_BY_TURN;
|
|
3699
|
+
const mutating = useMemo3(
|
|
3700
|
+
() => Object.keys(visiblePendingByTurn).length > 0,
|
|
3701
|
+
[visiblePendingByTurn]
|
|
3702
|
+
);
|
|
3703
|
+
const mutationFor = useCallback3(
|
|
3704
|
+
(turnId) => visiblePendingByTurn[turnId] ?? null,
|
|
3705
|
+
[visiblePendingByTurn]
|
|
3706
|
+
);
|
|
3707
|
+
return {
|
|
3708
|
+
snapshot: visibleSnapshot,
|
|
3709
|
+
queue: visibleSnapshot?.items ?? [],
|
|
3710
|
+
pendingInputs: visibleSnapshot?.pendingInputs ?? [],
|
|
3711
|
+
pendingInputAttachment: visibleSnapshot?.pendingInputAttachment ?? null,
|
|
3712
|
+
activePersonalConnections: visibleSnapshot?.activePersonalConnections ?? [],
|
|
3713
|
+
effectiveControl: visibleSnapshot?.effectiveControl ?? null,
|
|
3714
|
+
stoppingPreviousAttempt: visibleSnapshot?.stoppingPreviousAttempt ?? false,
|
|
3715
|
+
loading: identityMatches ? loading : enabled,
|
|
3716
|
+
error: identityMatches ? error : null,
|
|
3717
|
+
refresh: load,
|
|
3718
|
+
moveTurn,
|
|
3719
|
+
editTurn,
|
|
3720
|
+
steerTurn,
|
|
3721
|
+
removeTurn,
|
|
3722
|
+
pendingByTurn: visiblePendingByTurn,
|
|
3723
|
+
mutationFor,
|
|
3724
|
+
mutating,
|
|
3725
|
+
mutationError: identityMatches ? mutationError : null,
|
|
3726
|
+
clearMutationError: useCallback3(() => {
|
|
3727
|
+
if (targetKeyRef.current === targetKey) setMutationError(null);
|
|
3728
|
+
}, [targetKey]),
|
|
3729
|
+
acceptedSteers: identityMatches ? acceptedSteers : []
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
function operationKey() {
|
|
3733
|
+
return globalThis.crypto.randomUUID();
|
|
3734
|
+
}
|
|
3735
|
+
function asError(cause) {
|
|
3736
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
3737
|
+
}
|
|
3738
|
+
function isOutcomeUnknownError(cause) {
|
|
3739
|
+
return typeof cause === "object" && cause !== null && cause.outcomeUnknown === true;
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
// src/hooks/use-human-input.ts
|
|
3743
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
|
|
3744
|
+
function isHumanInputEvent(event) {
|
|
3745
|
+
return event.type === "session.humanInput.requested" || event.type === "user.humanInputResponse" || event.type === "turn.completed" || event.type === "turn.failed" || event.type === "turn.cancelled";
|
|
3746
|
+
}
|
|
3747
|
+
function useHumanInputRequests(sessionId, options = {}) {
|
|
3748
|
+
const { client, workspaceId, registerSessionReconciler } = useEmbeddedHumanInputSession(options);
|
|
3749
|
+
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
3750
|
+
const load = useCallback4(
|
|
3751
|
+
async () => sessionId ? await client.listHumanInputRequests(workspaceId, sessionId, { status: "pending" }) : [],
|
|
3752
|
+
[client, sessionId, workspaceId]
|
|
3753
|
+
);
|
|
3754
|
+
const state = usePolledValue(load, {
|
|
3755
|
+
enabled,
|
|
3756
|
+
pollIntervalMs: options.pollIntervalMs
|
|
3757
|
+
});
|
|
3758
|
+
const loadedRequests = state.data;
|
|
3759
|
+
const refreshRequests = state.refresh;
|
|
3760
|
+
const mutation = useMutationRunner(`${workspaceId}\0${sessionId ?? ""}`);
|
|
3761
|
+
const [respondingRequestId, setRespondingRequestId] = useState4(null);
|
|
3762
|
+
const [deadlineVersion, setDeadlineVersion] = useState4(0);
|
|
3763
|
+
const respondingRequestRef = useRef4(null);
|
|
3764
|
+
useEffect4(() => {
|
|
3765
|
+
respondingRequestRef.current = null;
|
|
3766
|
+
setRespondingRequestId(null);
|
|
3767
|
+
}, [sessionId, workspaceId]);
|
|
3768
|
+
useEffect4(() => {
|
|
3769
|
+
if (!sessionId || !enabled) return;
|
|
3770
|
+
return registerSessionReconciler(sessionId, "human-input", state.refresh);
|
|
3771
|
+
}, [enabled, registerSessionReconciler, sessionId, state.refresh]);
|
|
3772
|
+
const scheduleRefresh = useDebouncedCallback(() => void state.refresh());
|
|
3773
|
+
useSessionEventTrigger(client, workspaceId, sessionId, isHumanInputEvent, scheduleRefresh, {
|
|
3774
|
+
enabled,
|
|
3775
|
+
...options.events !== void 0 ? { events: options.events } : {}
|
|
3776
|
+
});
|
|
3777
|
+
useEffect4(() => {
|
|
3778
|
+
const now2 = Date.now();
|
|
3779
|
+
const nextDeadline = (loadedRequests ?? []).reduce((next, request) => {
|
|
3780
|
+
if (request.status !== "pending" || !request.expiresAt) return next;
|
|
3781
|
+
const deadline = Date.parse(request.expiresAt);
|
|
3782
|
+
if (!Number.isFinite(deadline) || deadline <= now2) return next;
|
|
3783
|
+
return next === null ? deadline : Math.min(next, deadline);
|
|
3784
|
+
}, null);
|
|
3785
|
+
if (nextDeadline === null) return;
|
|
3786
|
+
const timer = globalThis.setTimeout(
|
|
3787
|
+
() => {
|
|
3788
|
+
setDeadlineVersion((current) => current + 1);
|
|
3789
|
+
void refreshRequests();
|
|
3790
|
+
},
|
|
3791
|
+
Math.min(2147483647, Math.max(0, nextDeadline - now2 + 1))
|
|
3792
|
+
);
|
|
3793
|
+
return () => globalThis.clearTimeout(timer);
|
|
3794
|
+
}, [loadedRequests, refreshRequests]);
|
|
3795
|
+
const respond = useCallback4(
|
|
3796
|
+
async (requestId, response) => {
|
|
3797
|
+
if (!sessionId || respondingRequestRef.current !== null) return null;
|
|
3798
|
+
respondingRequestRef.current = requestId;
|
|
3799
|
+
setRespondingRequestId(requestId);
|
|
3800
|
+
try {
|
|
3801
|
+
const event = await mutation.run(
|
|
3802
|
+
() => client.submitHumanInputResponse(workspaceId, sessionId, requestId, response, {
|
|
3803
|
+
clientEventId: globalThis.crypto.randomUUID()
|
|
3804
|
+
})
|
|
3805
|
+
);
|
|
3806
|
+
if (event) await state.refresh();
|
|
3807
|
+
return event;
|
|
3808
|
+
} finally {
|
|
3809
|
+
if (respondingRequestRef.current === requestId) {
|
|
3810
|
+
respondingRequestRef.current = null;
|
|
3811
|
+
setRespondingRequestId(null);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
},
|
|
3815
|
+
[client, mutation, sessionId, state, workspaceId]
|
|
3816
|
+
);
|
|
3817
|
+
const now = Date.now();
|
|
3818
|
+
void deadlineVersion;
|
|
3819
|
+
return {
|
|
3820
|
+
requests: [...state.data ?? []].filter((request) => isActionableHumanInputRequest(request, now)).sort(
|
|
3821
|
+
(left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
|
|
3822
|
+
),
|
|
3823
|
+
loading: state.loading,
|
|
3824
|
+
error: state.error,
|
|
3825
|
+
refresh: state.refresh,
|
|
3826
|
+
respond,
|
|
3827
|
+
respondingRequestId,
|
|
3828
|
+
mutationError: mutation.mutationError,
|
|
3829
|
+
clearMutationError: mutation.clearMutationError
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
|
|
2130
3833
|
export {
|
|
2131
3834
|
isActionableHumanInputRequest,
|
|
2132
3835
|
humanInputRequestFromEvent,
|
|
@@ -2139,6 +3842,19 @@ export {
|
|
|
2139
3842
|
creditExhaustedFromEvents,
|
|
2140
3843
|
sessionStatusFromEvents,
|
|
2141
3844
|
groupTimeline,
|
|
2142
|
-
extractSessionRef
|
|
3845
|
+
extractSessionRef,
|
|
3846
|
+
SESSION_EVENT_BROWSER_MAX_BYTES,
|
|
3847
|
+
SESSION_EVENT_BROWSER_MAX_COUNT,
|
|
3848
|
+
SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES,
|
|
3849
|
+
SESSION_EVENT_BROWSER_PENDING_MAX_BYTES,
|
|
3850
|
+
SESSION_EVENT_BROWSER_PENDING_MAX_COUNT,
|
|
3851
|
+
useSessionEvents,
|
|
3852
|
+
boundBrowserSessionEventWindow,
|
|
3853
|
+
isTitleEvent,
|
|
3854
|
+
useSession,
|
|
3855
|
+
isTurnQueueEvent,
|
|
3856
|
+
useTurnQueue,
|
|
3857
|
+
isHumanInputEvent,
|
|
3858
|
+
useHumanInputRequests
|
|
2143
3859
|
};
|
|
2144
|
-
//# sourceMappingURL=chunk-
|
|
3860
|
+
//# sourceMappingURL=chunk-MOWYEBCQ.js.map
|