@kortix/sdk 0.13.3 → 0.13.5
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/browser/cache/idb-sync-cache.d.ts +19 -1
- package/dist/browser/cache/idb-sync-cache.d.ts.map +1 -1
- package/dist/browser/cache/idb-sync-cache.js +39 -4
- package/dist/browser/cache/idb-sync-cache.js.map +1 -1
- package/dist/browser/session-sync/session-sync-registry.d.ts +20 -0
- package/dist/browser/session-sync/session-sync-registry.d.ts.map +1 -1
- package/dist/browser/session-sync/session-sync-registry.js +69 -6
- package/dist/browser/session-sync/session-sync-registry.js.map +1 -1
- package/dist/browser/session-sync/visibility.d.ts +19 -0
- package/dist/browser/session-sync/visibility.d.ts.map +1 -0
- package/dist/browser/session-sync/visibility.js +16 -0
- package/dist/browser/session-sync/visibility.js.map +1 -0
- package/dist/browser/stores/sandbox-connection-store.js +1 -1
- package/dist/browser/stores/sandbox-connection-store.js.map +1 -1
- package/dist/browser/stores/sync-store.d.ts +27 -1
- package/dist/browser/stores/sync-store.d.ts.map +1 -1
- package/dist/browser/stores/sync-store.js +198 -41
- package/dist/browser/stores/sync-store.js.map +1 -1
- package/dist/core/client/kortix.d.ts +0 -11
- package/dist/core/client/kortix.d.ts.map +1 -1
- package/dist/core/client/kortix.js +14 -8
- package/dist/core/client/kortix.js.map +1 -1
- package/dist/core/rest/projects-client/channels.d.ts +0 -3
- package/dist/core/rest/projects-client/channels.d.ts.map +1 -1
- package/dist/core/rest/projects-client/channels.js +0 -6
- package/dist/core/rest/projects-client/channels.js.map +1 -1
- package/dist/core/rest/projects-client/index.d.ts +0 -2
- package/dist/core/rest/projects-client/index.d.ts.map +1 -1
- package/dist/core/rest/projects-client/index.js +0 -2
- package/dist/core/rest/projects-client/index.js.map +1 -1
- package/dist/core/rest/projects-client/projects.d.ts +1 -1
- package/dist/core/rest/projects-client/projects.d.ts.map +1 -1
- package/dist/core/rest/projects-client/projects.js +5 -3
- package/dist/core/rest/projects-client/projects.js.map +1 -1
- package/dist/core/rest/projects-client/sessions.d.ts +8 -33
- package/dist/core/rest/projects-client/sessions.d.ts.map +1 -1
- package/dist/core/rest/projects-client/sessions.js +1 -18
- package/dist/core/rest/projects-client/sessions.js.map +1 -1
- package/dist/core/runtime/attachment-part.d.ts +26 -0
- package/dist/core/runtime/attachment-part.d.ts.map +1 -0
- package/dist/core/runtime/attachment-part.js +41 -0
- package/dist/core/runtime/attachment-part.js.map +1 -0
- package/dist/core/runtime/client.d.ts +4 -2
- package/dist/core/runtime/client.d.ts.map +1 -1
- package/dist/core/runtime/client.js +3 -2
- package/dist/core/runtime/client.js.map +1 -1
- package/dist/core/runtime/opencode-v2-event-aliases.d.ts +184 -0
- package/dist/core/runtime/opencode-v2-event-aliases.d.ts.map +1 -0
- package/dist/core/runtime/opencode-v2-event-aliases.js +2 -0
- package/dist/core/runtime/opencode-v2-event-aliases.js.map +1 -0
- package/dist/core/runtime/pty.d.ts +11 -1
- package/dist/core/runtime/pty.d.ts.map +1 -1
- package/dist/core/runtime/pty.js +19 -6
- package/dist/core/runtime/pty.js.map +1 -1
- package/dist/core/session/connection.d.ts +64 -0
- package/dist/core/session/connection.d.ts.map +1 -0
- package/dist/core/session/connection.js +36 -0
- package/dist/core/session/connection.js.map +1 -0
- package/dist/core/session/index.d.ts +1 -0
- package/dist/core/session/index.d.ts.map +1 -1
- package/dist/core/session/index.js +1 -0
- package/dist/core/session/index.js.map +1 -1
- package/dist/core/session/preview-auth.d.ts +25 -0
- package/dist/core/session/preview-auth.d.ts.map +1 -1
- package/dist/core/session/preview-auth.js +31 -1
- package/dist/core/session/preview-auth.js.map +1 -1
- package/dist/core/session/preview-config.d.ts +26 -0
- package/dist/core/session/preview-config.d.ts.map +1 -0
- package/dist/core/session/preview-config.js +95 -0
- package/dist/core/session/preview-config.js.map +1 -0
- package/dist/core/session/preview-options.d.ts +25 -0
- package/dist/core/session/preview-options.d.ts.map +1 -0
- package/dist/core/session/preview-options.js +45 -0
- package/dist/core/session/preview-options.js.map +1 -0
- package/dist/core/session/preview.d.ts +8 -3
- package/dist/core/session/preview.d.ts.map +1 -1
- package/dist/core/session/preview.js +11 -4
- package/dist/core/session/preview.js.map +1 -1
- package/dist/core/session/rewind.d.ts +67 -24
- package/dist/core/session/rewind.d.ts.map +1 -1
- package/dist/core/session/rewind.js +74 -26
- package/dist/core/session/rewind.js.map +1 -1
- package/dist/core/session/server-store/active.d.ts +10 -12
- package/dist/core/session/server-store/active.d.ts.map +1 -1
- package/dist/core/session/server-store/active.js +11 -9
- package/dist/core/session/server-store/active.js.map +1 -1
- package/dist/core/session/server-store/url-helpers.d.ts +11 -2
- package/dist/core/session/server-store/url-helpers.d.ts.map +1 -1
- package/dist/core/session/server-store/url-helpers.js +11 -2
- package/dist/core/session/server-store/url-helpers.js.map +1 -1
- package/dist/core/session/url.d.ts +25 -2
- package/dist/core/session/url.d.ts.map +1 -1
- package/dist/core/session/url.js +87 -11
- package/dist/core/session/url.js.map +1 -1
- package/dist/core/session/working.d.ts +44 -0
- package/dist/core/session/working.d.ts.map +1 -1
- package/dist/core/session/working.js +146 -4
- package/dist/core/session/working.js.map +1 -1
- package/dist/core/session-sync/durable-log.d.ts +96 -0
- package/dist/core/session-sync/durable-log.d.ts.map +1 -0
- package/dist/core/session-sync/durable-log.js +94 -0
- package/dist/core/session-sync/durable-log.js.map +1 -0
- package/dist/core/session-sync/fragment.d.ts +27 -0
- package/dist/core/session-sync/fragment.d.ts.map +1 -0
- package/dist/core/session-sync/fragment.js +24 -0
- package/dist/core/session-sync/fragment.js.map +1 -0
- package/dist/core/session-sync/session-sync-controller.d.ts +83 -9
- package/dist/core/session-sync/session-sync-controller.d.ts.map +1 -1
- package/dist/core/session-sync/session-sync-controller.js +183 -17
- package/dist/core/session-sync/session-sync-controller.js.map +1 -1
- package/dist/core/turns/grouping.d.ts.map +1 -1
- package/dist/core/turns/grouping.js +60 -23
- package/dist/core/turns/grouping.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/kortix.esm.min.js +24 -24
- package/dist/kortix.global.js +24 -24
- package/dist/platform/auth-core.d.ts +13 -1
- package/dist/platform/auth-core.d.ts.map +1 -1
- package/dist/platform/auth-core.js +13 -1
- package/dist/platform/auth-core.js.map +1 -1
- package/dist/react/opencode.d.ts +1 -0
- package/dist/react/opencode.d.ts.map +1 -1
- package/dist/react/opencode.js +3 -0
- package/dist/react/opencode.js.map +1 -1
- package/dist/react/use-admin-accounts.d.ts +35 -0
- package/dist/react/use-admin-accounts.d.ts.map +1 -1
- package/dist/react/use-admin-accounts.js +18 -0
- package/dist/react/use-admin-accounts.js.map +1 -1
- package/dist/react/use-opencode-events/handle-event.d.ts.map +1 -1
- package/dist/react/use-opencode-events/handle-event.js +26 -0
- package/dist/react/use-opencode-events/handle-event.js.map +1 -1
- package/dist/react/use-opencode-events/index.d.ts.map +1 -1
- package/dist/react/use-opencode-events/index.js +59 -6
- package/dist/react/use-opencode-events/index.js.map +1 -1
- package/dist/react/use-opencode-events/rehydrate-targets.d.ts +18 -0
- package/dist/react/use-opencode-events/rehydrate-targets.d.ts.map +1 -0
- package/dist/react/use-opencode-events/rehydrate-targets.js +20 -0
- package/dist/react/use-opencode-events/rehydrate-targets.js.map +1 -0
- package/dist/react/use-opencode-events/stream-revival.d.ts +37 -0
- package/dist/react/use-opencode-events/stream-revival.d.ts.map +1 -0
- package/dist/react/use-opencode-events/stream-revival.js +53 -0
- package/dist/react/use-opencode-events/stream-revival.js.map +1 -0
- package/dist/react/use-opencode-pty.d.ts +7 -1
- package/dist/react/use-opencode-pty.d.ts.map +1 -1
- package/dist/react/use-opencode-pty.js +6 -2
- package/dist/react/use-opencode-pty.js.map +1 -1
- package/dist/react/use-opencode-sessions/agents.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/agents.js +13 -2
- package/dist/react/use-opencode-sessions/agents.js.map +1 -1
- package/dist/react/use-opencode-sessions/commands.d.ts +12 -0
- package/dist/react/use-opencode-sessions/commands.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/commands.js +15 -3
- package/dist/react/use-opencode-sessions/commands.js.map +1 -1
- package/dist/react/use-opencode-sessions/index.d.ts +1 -0
- package/dist/react/use-opencode-sessions/index.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/index.js +1 -0
- package/dist/react/use-opencode-sessions/index.js.map +1 -1
- package/dist/react/use-opencode-sessions/keys.d.ts +8 -0
- package/dist/react/use-opencode-sessions/keys.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/keys.js +8 -0
- package/dist/react/use-opencode-sessions/keys.js.map +1 -1
- package/dist/react/use-opencode-sessions/providers.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/providers.js +12 -2
- package/dist/react/use-opencode-sessions/providers.js.map +1 -1
- package/dist/react/use-opencode-sessions/shared.d.ts +23 -0
- package/dist/react/use-opencode-sessions/shared.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/shared.js +31 -0
- package/dist/react/use-opencode-sessions/shared.js.map +1 -1
- package/dist/react/use-opencode-sessions/vcs.d.ts +28 -0
- package/dist/react/use-opencode-sessions/vcs.d.ts.map +1 -0
- package/dist/react/use-opencode-sessions/vcs.js +42 -0
- package/dist/react/use-opencode-sessions/vcs.js.map +1 -0
- package/dist/react/use-session-prompts.d.ts +37 -2
- package/dist/react/use-session-prompts.d.ts.map +1 -1
- package/dist/react/use-session-prompts.js +62 -15
- package/dist/react/use-session-prompts.js.map +1 -1
- package/dist/react/use-session-send.d.ts +17 -0
- package/dist/react/use-session-send.d.ts.map +1 -1
- package/dist/react/use-session-send.js +40 -4
- package/dist/react/use-session-send.js.map +1 -1
- package/dist/react/use-session-sync.d.ts +13 -1
- package/dist/react/use-session-sync.d.ts.map +1 -1
- package/dist/react/use-session-sync.js +81 -55
- package/dist/react/use-session-sync.js.map +1 -1
- package/dist/react/use-session-working.d.ts +17 -0
- package/dist/react/use-session-working.d.ts.map +1 -1
- package/dist/react/use-session-working.js +33 -3
- package/dist/react/use-session-working.js.map +1 -1
- package/package.json +3 -3
- package/dist/browser/session-sync/session-transcript-cache.d.ts +0 -79
- package/dist/browser/session-sync/session-transcript-cache.d.ts.map +0 -1
- package/dist/browser/session-sync/session-transcript-cache.js +0 -91
- package/dist/browser/session-sync/session-transcript-cache.js.map +0 -1
- package/dist/core/rest/projects-client/public-voice-join.d.ts +0 -59
- package/dist/core/rest/projects-client/public-voice-join.d.ts.map +0 -1
- package/dist/core/rest/projects-client/public-voice-join.js +0 -64
- package/dist/core/rest/projects-client/public-voice-join.js.map +0 -1
- package/dist/core/rest/projects-client/transcription.d.ts +0 -5
- package/dist/core/rest/projects-client/transcription.d.ts.map +0 -1
- package/dist/core/rest/projects-client/transcription.js +0 -14
- package/dist/core/rest/projects-client/transcription.js.map +0 -1
|
@@ -36,6 +36,68 @@ function isTextLikePart(part) {
|
|
|
36
36
|
return part.type === "text" || part.type === "reasoning";
|
|
37
37
|
}
|
|
38
38
|
// ============================================================================
|
|
39
|
+
// Locating things in a list that is NOT id-sorted.
|
|
40
|
+
//
|
|
41
|
+
// `Binary.search` requires ascending ids. None of this store's lists are
|
|
42
|
+
// reliably ascending, for two independent reasons:
|
|
43
|
+
//
|
|
44
|
+
// 1. optimistic messages are appended out of order on purpose (see
|
|
45
|
+
// `optimisticRemove`, which says so), and
|
|
46
|
+
// 2. OpenCode 1.18.15 retired the invariant that ids ascend with time
|
|
47
|
+
// altogether — `MessageV2.latest()` orders by `time.created`, with the id
|
|
48
|
+
// only as a tie-break, and `MessageV2.page()` (every transcript read this
|
|
49
|
+
// store performs) has ALWAYS ordered by `time_created`.
|
|
50
|
+
//
|
|
51
|
+
// A binary HIT is still trustworthy: `Binary.search` reports `found` only on an
|
|
52
|
+
// exact id match. A binary MISS is not — on an unsorted list it is a coin
|
|
53
|
+
// flip, and treating it as proof of absence is how `upsertPart` spliced a
|
|
54
|
+
// duplicate part (restarting the visible stream), `removePart` no-opped (a
|
|
55
|
+
// cancelled tool call stayed on screen), and `applyPartDelta` dropped deltas
|
|
56
|
+
// (the assistant appeared to hang mid-sentence).
|
|
57
|
+
// ============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* How coarsely `sessionActivityAt` is stamped.
|
|
60
|
+
*
|
|
61
|
+
* One second: far finer than any consumer needs (the projection compares it
|
|
62
|
+
* against observations bounded in tens of seconds) and coarse enough that a
|
|
63
|
+
* runtime streaming at ~140ms per frame updates it once, not seven times.
|
|
64
|
+
*/
|
|
65
|
+
const ACTIVITY_STAMP_RESOLUTION_MS = 1_000;
|
|
66
|
+
/** The index of `id` in `list`, or `-1`. Binary first, linear on a miss. */
|
|
67
|
+
function indexOfId(list, id, idOf) {
|
|
68
|
+
const result = Binary.search(list, id, idOf);
|
|
69
|
+
if (result.found && list[result.index] !== undefined && idOf(list[result.index]) === id) {
|
|
70
|
+
return result.index;
|
|
71
|
+
}
|
|
72
|
+
return list.findIndex((item) => idOf(item) === id);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Where a message the current snapshot did not contain belongs: the first
|
|
76
|
+
* position holding a strictly newer message, or the end.
|
|
77
|
+
*
|
|
78
|
+
* Ordered by `time.created` with the id as the ONLY tie-break — the same order
|
|
79
|
+
* the server's own `MessageV2.latest()` uses, and the key `MessageV2.page()`
|
|
80
|
+
* pages by. That keeps a locally-known message (an SSE arrival, a re-keyed
|
|
81
|
+
* stub) in the sequence the next page read will produce. A message with no
|
|
82
|
+
* readable `time` — every `session.error` stub this store mints — cannot be
|
|
83
|
+
* dated and goes last, which is where the newest thing we know about belongs.
|
|
84
|
+
*/
|
|
85
|
+
function insertIndexByTime(list, message) {
|
|
86
|
+
const created = message.time?.created;
|
|
87
|
+
if (created === undefined)
|
|
88
|
+
return list.length;
|
|
89
|
+
for (let index = 0; index < list.length; index++) {
|
|
90
|
+
const other = list[index].time?.created;
|
|
91
|
+
if (other === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
if (other > created)
|
|
94
|
+
return index;
|
|
95
|
+
if (other === created && list[index].id > message.id)
|
|
96
|
+
return index;
|
|
97
|
+
}
|
|
98
|
+
return list.length;
|
|
99
|
+
}
|
|
100
|
+
// ============================================================================
|
|
39
101
|
// Store Implementation
|
|
40
102
|
// Track optimistic message IDs so we can remove them when the server sends
|
|
41
103
|
// the real user message (which has a different, server-generated ID).
|
|
@@ -271,26 +333,52 @@ function untrackStub(sessionID, stubId) {
|
|
|
271
333
|
}
|
|
272
334
|
/** The stub id for the turn `userId` opened.
|
|
273
335
|
*
|
|
274
|
-
* Derived from the user message's own id
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* message
|
|
278
|
-
*
|
|
279
|
-
* follows `userId` itself. Position and `parentID` therefore agree, whichever
|
|
280
|
-
* of the two a reader uses. */
|
|
336
|
+
* Derived from the user message's own id so it is IDEMPOTENT — a second
|
|
337
|
+
* `session.error` for the same turn finds the stub it already made instead of
|
|
338
|
+
* appending another. The stub is always PLACED directly after its user
|
|
339
|
+
* message (see `rekeyStubParent` and `hydrate`), and carries `parentID`;
|
|
340
|
+
* neither its position nor its linkage is inferred from the id. */
|
|
281
341
|
const stubIdFor = (userId) => `${userId}_error`;
|
|
282
342
|
/** A stub id as seen from a list that has no tracking map to hand (`hydrate`'s
|
|
283
343
|
* incoming server snapshot, which can never legitimately contain one). */
|
|
284
344
|
const isStubShaped = (id) => id.endsWith("_error");
|
|
345
|
+
/** The index in `messages` of the assistant message answering the turn
|
|
346
|
+
* `parentId` opened, or `-1`.
|
|
347
|
+
*
|
|
348
|
+
* `parentID` is the linkage `groupMessagesIntoTurns` reads and the only
|
|
349
|
+
* authoritative one. A wire assistant message that carries none is matched by
|
|
350
|
+
* TIME instead — `time.created` at or after the prompt's. It used to be
|
|
351
|
+
* matched by `m.id > parentId`, on the rationale "ids ascend, so a greater id
|
|
352
|
+
* is a later message". OpenCode 1.18.15 retired that invariant (turn exit now
|
|
353
|
+
* reads `lastAssistant.parentID === lastUser.id`, and `MessageV2.latest()`
|
|
354
|
+
* orders by `time.created`), and the comparison failed both ways: a real
|
|
355
|
+
* reply with a lower id left a stale `session.error` stub beside it, and an
|
|
356
|
+
* EARLIER turn's parentless reply with a higher id deleted the only record
|
|
357
|
+
* that this turn failed (the 2026-08-19 `ModelNotFound` report).
|
|
358
|
+
*
|
|
359
|
+
* A parentless candidate that cannot be dated — because it carries no `time`,
|
|
360
|
+
* or because the prompt itself is not in `messages` — is not a match: keeping
|
|
361
|
+
* a stub that turns out to be redundant is recoverable on the next snapshot;
|
|
362
|
+
* deleting the only evidence of a failed turn is not.
|
|
363
|
+
*
|
|
364
|
+
* With no parent turn to speak of, any assistant message answers. */
|
|
365
|
+
function findAssistantForTurn(messages, parentId) {
|
|
366
|
+
const parentCreated = parentId
|
|
367
|
+
? messages.find((m) => m.id === parentId)?.time?.created
|
|
368
|
+
: undefined;
|
|
369
|
+
return messages.findIndex((m) => m.role === "assistant" &&
|
|
370
|
+
!isStubShaped(m.id) &&
|
|
371
|
+
(!parentId ||
|
|
372
|
+
m.parentID === parentId ||
|
|
373
|
+
(!m.parentID &&
|
|
374
|
+
parentCreated !== undefined &&
|
|
375
|
+
m.time?.created !== undefined &&
|
|
376
|
+
m.time.created >= parentCreated)));
|
|
377
|
+
}
|
|
285
378
|
/** Does `messages` hold an assistant message answering the turn `parentId`
|
|
286
|
-
* opened?
|
|
287
|
-
* comparison covers a wire assistant message that carries none (ids ascend,
|
|
288
|
-
* so a greater id is a later message). With no parent turn to speak of, any
|
|
289
|
-
* assistant message answers. */
|
|
379
|
+
* opened? See {@link findAssistantForTurn}. */
|
|
290
380
|
function hasAssistantForTurn(messages, parentId) {
|
|
291
|
-
return messages
|
|
292
|
-
!isStubShaped(m.id) &&
|
|
293
|
-
(!parentId || m.parentID === parentId || (!m.parentID && m.id > parentId)));
|
|
381
|
+
return findAssistantForTurn(messages, parentId) !== -1;
|
|
294
382
|
}
|
|
295
383
|
/** Move this session's `session.error` stubs from the optimistic user message
|
|
296
384
|
* `fromId` onto the server's own copy of that prompt, `toId`.
|
|
@@ -543,6 +631,7 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
543
631
|
messages: {},
|
|
544
632
|
parts: {},
|
|
545
633
|
sessionStatus: {},
|
|
634
|
+
sessionActivityAt: {},
|
|
546
635
|
diffs: {},
|
|
547
636
|
todos: {},
|
|
548
637
|
sessionRevert: {},
|
|
@@ -571,9 +660,15 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
571
660
|
next[linearIdx] = message;
|
|
572
661
|
return { messages: { ...s.messages, [sessionID]: next } };
|
|
573
662
|
}
|
|
574
|
-
// New message —
|
|
663
|
+
// New message — placed by `time.created`, the key the server's own
|
|
664
|
+
// `MessageV2.page()` orders by. It used to go in at the binary index,
|
|
665
|
+
// which was only ever meaningful while `hydrate` re-sorted the
|
|
666
|
+
// transcript by id. It no longer does (the page order IS the order),
|
|
667
|
+
// and ids stopped ascending with time in OpenCode 1.18.15 — so an
|
|
668
|
+
// id-sorted insert dropped a brand-new SSE message into the MIDDLE of
|
|
669
|
+
// the transcript. See `insertIndexByTime`.
|
|
575
670
|
const next = [...list];
|
|
576
|
-
next.splice(
|
|
671
|
+
next.splice(insertIndexByTime(next, message), 0, message);
|
|
577
672
|
return { messages: { ...s.messages, [sessionID]: next } };
|
|
578
673
|
}),
|
|
579
674
|
removeMessage: (sessionID, messageID) => set((s) => {
|
|
@@ -779,12 +874,20 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
779
874
|
return s;
|
|
780
875
|
return { sessionRevert: { ...s.sessionRevert, [sessionId]: null } };
|
|
781
876
|
}),
|
|
782
|
-
applyCommittedRevert: (sessionId, boundaryId, watermark) => set((s) => {
|
|
877
|
+
applyCommittedRevert: (sessionId, boundaryId, watermark, hiddenIds) => set((s) => {
|
|
783
878
|
const list = s.messages[sessionId];
|
|
784
879
|
if (!list || list.length === 0) {
|
|
785
880
|
return { sessionRevert: { ...s.sessionRevert, [sessionId]: null } };
|
|
786
881
|
}
|
|
787
|
-
|
|
882
|
+
// `hiddenIds` is the set captured when the rewind was staged, and it
|
|
883
|
+
// is what decides. `watermark` only still answers for a caller that
|
|
884
|
+
// has no captured set — see `isWithinRewindWindow`.
|
|
885
|
+
const rewind = {
|
|
886
|
+
messageId: boundaryId,
|
|
887
|
+
watermark,
|
|
888
|
+
...(hiddenIds ? { hiddenIds } : {}),
|
|
889
|
+
staged: false,
|
|
890
|
+
};
|
|
788
891
|
const removedIds = [];
|
|
789
892
|
const kept = list.filter((m) => {
|
|
790
893
|
if (isWithinRewindWindow(m.id, rewind)) {
|
|
@@ -933,6 +1036,18 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
933
1036
|
};
|
|
934
1037
|
});
|
|
935
1038
|
},
|
|
1039
|
+
noteSessionActivity: (sessionID, atMs) => {
|
|
1040
|
+
if (!sessionID)
|
|
1041
|
+
return;
|
|
1042
|
+
const now = atMs ?? Date.now();
|
|
1043
|
+
const previous = get().sessionActivityAt[sessionID];
|
|
1044
|
+
// Quantized: a busy runtime emits parts every few tens of milliseconds and
|
|
1045
|
+
// nothing reads this to the millisecond. Without the floor, every
|
|
1046
|
+
// subscriber re-renders at the stream's own rate for a timestamp.
|
|
1047
|
+
if (previous !== undefined && now - previous < ACTIVITY_STAMP_RESOLUTION_MS)
|
|
1048
|
+
return;
|
|
1049
|
+
set((s) => ({ sessionActivityAt: { ...s.sessionActivityAt, [sessionID]: now } }));
|
|
1050
|
+
},
|
|
936
1051
|
clearOptimisticMessages: (sessionID) => {
|
|
937
1052
|
set((s) => {
|
|
938
1053
|
const list = s.messages[sessionID];
|
|
@@ -970,9 +1085,12 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
970
1085
|
for (const message of existingMessages)
|
|
971
1086
|
delete nextParts[message.id];
|
|
972
1087
|
deleteOrphanPartBuckets(nextParts, [sessionID]);
|
|
1088
|
+
const nextActivity = { ...s.sessionActivityAt };
|
|
1089
|
+
delete nextActivity[sessionID];
|
|
973
1090
|
return {
|
|
974
1091
|
messages: { ...s.messages, [sessionID]: [] },
|
|
975
1092
|
parts: nextParts,
|
|
1093
|
+
sessionActivityAt: nextActivity,
|
|
976
1094
|
sessionStatus: { ...s.sessionStatus, [sessionID]: { type: "idle" } },
|
|
977
1095
|
diffs: { ...s.diffs, [sessionID]: [] },
|
|
978
1096
|
todos: { ...s.todos, [sessionID]: [] },
|
|
@@ -1059,12 +1177,16 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
1059
1177
|
// An authoritative load — the disk repaint itself, or a reconcile —
|
|
1060
1178
|
// re-establishes the session, so its entry is no longer a fragment.
|
|
1061
1179
|
evictedSessions.delete(sessionID);
|
|
1062
|
-
const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
1063
1180
|
const tombstones = cancelledMessageIds.get(sessionID);
|
|
1181
|
+
// NOT re-sorted. `MessageV2.page()` orders by `time_created`
|
|
1182
|
+
// server-side and always has, so the snapshot arrives in the only
|
|
1183
|
+
// order that is true. Re-deriving one from id strings was wrong the
|
|
1184
|
+
// moment OpenCode 1.18.15 retired "ids ascend with time" — and the
|
|
1185
|
+
// mobile store did the same re-sort with `localeCompare`, which is
|
|
1186
|
+
// not even byte order, so the two hosts disagreed on identical data.
|
|
1064
1187
|
const incoming = msgs
|
|
1065
1188
|
.filter((m) => !!m?.info?.id && !tombstones?.has(m.info.id))
|
|
1066
|
-
.map((m) => m.info)
|
|
1067
|
-
.sort((a, b) => cmp(a.id, b.id));
|
|
1189
|
+
.map((m) => m.info);
|
|
1068
1190
|
const fromCache = opts?.source === "cache";
|
|
1069
1191
|
if (fromCache) {
|
|
1070
1192
|
for (const m of incoming)
|
|
@@ -1075,17 +1197,26 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
1075
1197
|
// COVERS (at or above the oldest message it returned) never existed
|
|
1076
1198
|
// there and are dropped. Older ones are history the bounded tail did
|
|
1077
1199
|
// not reach — kept, still provisional.
|
|
1200
|
+
//
|
|
1201
|
+
// The bound is derived by VALUE, not by position: `incoming` is now the
|
|
1202
|
+
// server's own page order, which is not an id order, so `incoming[0].id`
|
|
1203
|
+
// is no longer the smallest id in the page. (That this comparison is an
|
|
1204
|
+
// id comparison at all is a separate open question — see B-note in
|
|
1205
|
+
// PROGRESS.md — deliberately left as-is here.)
|
|
1078
1206
|
let droppedPhantoms = null;
|
|
1079
1207
|
const provisional = fromCache ? undefined : cacheSourcedIds.get(sessionID);
|
|
1080
1208
|
if (provisional && provisional.size > 0 && incoming.length > 0) {
|
|
1081
|
-
|
|
1209
|
+
let oldestIncoming = incoming[0].id;
|
|
1210
|
+
for (const m of incoming)
|
|
1211
|
+
if (m.id < oldestIncoming)
|
|
1212
|
+
oldestIncoming = m.id;
|
|
1082
1213
|
const incomingIds = new Set(incoming.map((m) => m.id));
|
|
1083
1214
|
for (const id of [...provisional]) {
|
|
1084
1215
|
if (incomingIds.has(id)) {
|
|
1085
1216
|
untrackId(cacheSourcedIds, sessionID, id);
|
|
1086
1217
|
continue;
|
|
1087
1218
|
}
|
|
1088
|
-
if (
|
|
1219
|
+
if (id >= oldestIncoming && !isOptimistic(sessionID, id)) {
|
|
1089
1220
|
untrackId(cacheSourcedIds, sessionID, id);
|
|
1090
1221
|
(droppedPhantoms ??= new Set()).add(id);
|
|
1091
1222
|
}
|
|
@@ -1218,8 +1349,9 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
1218
1349
|
}
|
|
1219
1350
|
}
|
|
1220
1351
|
else {
|
|
1221
|
-
|
|
1222
|
-
|
|
1352
|
+
// By TIME, not by a binary index over ids the snapshot never
|
|
1353
|
+
// ordered that way — see `insertIndexByTime`.
|
|
1354
|
+
merged.splice(insertIndexByTime(merged, m), 0, m);
|
|
1223
1355
|
}
|
|
1224
1356
|
}
|
|
1225
1357
|
}
|
|
@@ -1300,9 +1432,7 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
1300
1432
|
// stub against an error-free server copy would erase the only
|
|
1301
1433
|
// evidence the turn failed.
|
|
1302
1434
|
if (hasAssistantForTurn(incoming, parentId)) {
|
|
1303
|
-
const replyIdx = merged
|
|
1304
|
-
!isStubShaped(m.id) &&
|
|
1305
|
-
(!parentId || m.parentID === parentId || (!m.parentID && m.id > parentId)));
|
|
1435
|
+
const replyIdx = findAssistantForTurn(merged, parentId);
|
|
1306
1436
|
const reply = replyIdx === -1 ? undefined : merged[replyIdx];
|
|
1307
1437
|
const stubError = stub.error;
|
|
1308
1438
|
if (reply && !reply.error && stubError) {
|
|
@@ -1320,9 +1450,13 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
1320
1450
|
merged.splice(parentIdx + 1, 0, placed);
|
|
1321
1451
|
continue;
|
|
1322
1452
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1453
|
+
// Its user message is not in the transcript at all. Linear on a
|
|
1454
|
+
// miss so a re-keyed stub never lands twice (`stubIdFor(parentId)`
|
|
1455
|
+
// can name a stub already merged), and by time otherwise — a stub
|
|
1456
|
+
// carries no `time`, so it goes last.
|
|
1457
|
+
if (indexOfId(merged, placed.id, (x) => x.id) === -1) {
|
|
1458
|
+
merged.splice(insertIndexByTime(merged, placed), 0, placed);
|
|
1459
|
+
}
|
|
1326
1460
|
}
|
|
1327
1461
|
// Merge parts: for each message, reconcile by part ID.
|
|
1328
1462
|
// If a message is optimistic (still in optimisticIds), keep existing
|
|
@@ -1362,9 +1496,11 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
1362
1496
|
const mid = m.info.id;
|
|
1363
1497
|
if (isOptimistic(sessionID, mid))
|
|
1364
1498
|
continue; // Don't touch optimistic parts
|
|
1499
|
+
// Parts, not messages: this sort is untouched by the message-order
|
|
1500
|
+
// work and keeps its own byte-order comparison.
|
|
1365
1501
|
const inParts = m.parts
|
|
1366
1502
|
.filter((p) => !!p?.id)
|
|
1367
|
-
.sort((a, b) =>
|
|
1503
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1368
1504
|
// If this message still carries bridged optimistic parts, a hydrate
|
|
1369
1505
|
// snapshot with real parts should replace them immediately. Otherwise
|
|
1370
1506
|
// reconcile-by-extras can keep both copies and duplicate user text.
|
|
@@ -1491,6 +1627,12 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
1491
1627
|
const store = get();
|
|
1492
1628
|
switch (event.type) {
|
|
1493
1629
|
case "message.updated": {
|
|
1630
|
+
{
|
|
1631
|
+
const info = event.properties?.info;
|
|
1632
|
+
const sid = info?.sessionID ?? event.properties?.sessionID;
|
|
1633
|
+
if (sid)
|
|
1634
|
+
get().noteSessionActivity(sid);
|
|
1635
|
+
}
|
|
1494
1636
|
const info = event.properties.info;
|
|
1495
1637
|
if (!info?.sessionID)
|
|
1496
1638
|
return;
|
|
@@ -1589,14 +1731,17 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
1589
1731
|
const list = s.messages[info.sessionID] ?? [];
|
|
1590
1732
|
// Remove all optimistic user messages
|
|
1591
1733
|
const without = list.filter((m) => !optIds.includes(m.id));
|
|
1592
|
-
//
|
|
1593
|
-
|
|
1734
|
+
// Place the real message. Linear on a miss, then by TIME: a
|
|
1735
|
+
// false miss spliced a SECOND copy of a message already in
|
|
1736
|
+
// the transcript, at a binary index that means nothing on a
|
|
1737
|
+
// list the server never ordered by id.
|
|
1738
|
+
const at = indexOfId(without, info.id, (m) => m.id);
|
|
1594
1739
|
let next = [...without];
|
|
1595
|
-
if (
|
|
1596
|
-
next[
|
|
1740
|
+
if (at !== -1) {
|
|
1741
|
+
next[at] = info;
|
|
1597
1742
|
}
|
|
1598
1743
|
else {
|
|
1599
|
-
next.splice(
|
|
1744
|
+
next.splice(insertIndexByTime(next, info), 0, info);
|
|
1600
1745
|
}
|
|
1601
1746
|
// A turn that failed before this confirmation arrived
|
|
1602
1747
|
// has a `session.error` stub keyed to the optimistic id
|
|
@@ -1645,6 +1790,14 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
1645
1790
|
const part = event.properties.part;
|
|
1646
1791
|
if (!part?.messageID)
|
|
1647
1792
|
return;
|
|
1793
|
+
// The runtime just produced output. This is the evidence
|
|
1794
|
+
// `projectWorking` trusts above every observer — see
|
|
1795
|
+
// `sessionActivityAt`.
|
|
1796
|
+
{
|
|
1797
|
+
const sid = part.sessionID ?? event.properties?.sessionID;
|
|
1798
|
+
if (sid)
|
|
1799
|
+
get().noteSessionActivity(sid);
|
|
1800
|
+
}
|
|
1648
1801
|
const eventSessionID = event.properties?.sessionID;
|
|
1649
1802
|
let resolvedSessionID = part.sessionID ?? eventSessionID;
|
|
1650
1803
|
if (!resolvedSessionID) {
|
|
@@ -1659,9 +1812,10 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
1659
1812
|
const existingMsgs = resolvedSessionID
|
|
1660
1813
|
? get().messages[resolvedSessionID]
|
|
1661
1814
|
: undefined;
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1815
|
+
// The `.some()` alone is authoritative; the binary search that used
|
|
1816
|
+
// to sit beside it as a first disjunct could only agree with it or
|
|
1817
|
+
// miss on a list that is not id-sorted.
|
|
1818
|
+
const exists = existingMsgs?.some((m) => m.id === part.messageID);
|
|
1665
1819
|
if (!exists && resolvedSessionID) {
|
|
1666
1820
|
store.upsertMessage(resolvedSessionID, {
|
|
1667
1821
|
id: part.messageID,
|
|
@@ -1871,7 +2025,10 @@ export const useSyncStore = create()((set, get) => ({
|
|
|
1871
2025
|
if (props.sessionID && props.messageID) {
|
|
1872
2026
|
const tracked = get().sessionRevert[props.sessionID];
|
|
1873
2027
|
if (tracked) {
|
|
1874
|
-
|
|
2028
|
+
// The captured set only describes the boundary it was staged for.
|
|
2029
|
+
// A `.committed` naming a different one falls back to the legacy
|
|
2030
|
+
// range rather than deleting the wrong trajectory.
|
|
2031
|
+
store.applyCommittedRevert(props.sessionID, props.messageID, tracked.watermark, tracked.messageId === props.messageID ? tracked.hiddenIds : undefined);
|
|
1875
2032
|
}
|
|
1876
2033
|
else {
|
|
1877
2034
|
// F2 — no tracked local record (fresh mount / second tab that
|