@jini-ai/chat 0.6.17 → 0.6.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/core/accepted-input.d.ts +10 -2
  3. package/dist/core/accepted-input.js +87 -33
  4. package/dist/core/assistant-client/durable-run-subscription.js +6 -0
  5. package/dist/core/assistant-client/entry.d.ts +1 -1
  6. package/dist/core/assistant-client/entry.js +1 -1
  7. package/dist/core/assistant-client/messages.d.ts +21 -0
  8. package/dist/core/assistant-client/messages.js +31 -0
  9. package/dist/core/checkpoint-interval.d.ts +25 -0
  10. package/dist/core/checkpoint-interval.js +38 -0
  11. package/dist/core/durable-runs/ledger-contracts.d.ts +27 -1
  12. package/dist/core/durable-runs/ports.d.ts +25 -1
  13. package/dist/core/events.d.ts +6 -0
  14. package/dist/core/index.d.ts +1 -0
  15. package/dist/core/index.js +1 -0
  16. package/dist/core/run-events/entry.js +15 -2
  17. package/dist/core/surface-recovery.d.ts +19 -0
  18. package/dist/core/surface-recovery.js +26 -0
  19. package/dist/core/tool-events.d.ts +11 -3
  20. package/dist/core/tool-events.js +75 -43
  21. package/dist/core/turn-usage.d.ts +1 -1
  22. package/dist/core/turn-usage.js +5 -3
  23. package/dist/embed-widget/browser/frontend-session-bridge.d.ts +14 -1
  24. package/dist/embed-widget/browser/frontend-session-bridge.js +120 -47
  25. package/dist/react/components/Composer.d.ts +3 -3
  26. package/dist/react/components/Composer.js +5 -4
  27. package/dist/react/components/ConversationList.js +2 -1
  28. package/dist/react/components/ConversationTitleEditor.js +2 -1
  29. package/dist/react/components/ExtEventErrorBoundary.js +2 -1
  30. package/dist/react/components/McpUiSurfaceCard.js +12 -5
  31. package/dist/react/components/MessageList.js +2 -1
  32. package/dist/react/components/MidRunUserMessage.js +1 -1
  33. package/dist/react/components/ToolCard.js +3 -2
  34. package/dist/react/components/ToolCard.rules.js +2 -1
  35. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -3
  36. package/dist/react/features/chat-pane/components/ChatPane.js +1 -1
  37. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.d.ts +15 -2
  38. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +19 -3
  39. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +5 -4
  40. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +17 -9
  41. package/dist/react/features/chat-pane/hooks/useChatPaneFileDrop.hooks.js +3 -2
  42. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +20 -3
  43. package/dist/react/features/chat-pane/model-options.d.ts +10 -0
  44. package/dist/react/features/chat-pane/model-options.js +22 -3
  45. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  46. package/dist/react/features/chat-pane/styles.js +3 -1
  47. package/dist/react/features/chat-pane/surface-recovery-stream.d.ts +12 -0
  48. package/dist/react/features/chat-pane/surface-recovery-stream.js +47 -0
  49. package/dist/react/hooks/composer-draft-cache.d.ts +6 -6
  50. package/dist/react/hooks/composer-draft-cache.js +18 -17
  51. package/dist/react/hooks/useComposer.js +52 -4
  52. package/dist/react/hooks/useConversation.js +41 -22
  53. package/dist/react/hooks/useRunStream.js +2 -1
  54. package/dist/react/hooks/useSurfaceRecovery.d.ts +23 -0
  55. package/dist/react/hooks/useSurfaceRecovery.js +84 -0
  56. package/dist/react/styles/reference.css +4 -5
  57. package/dist/server/durable-runs/recover.js +3 -3
  58. package/dist/store/ports.d.ts +17 -3
  59. package/dist/store/runtime/chat-expiry-sweep.js +3 -1
  60. package/dist/store/runtime/chat-history-store.js +6 -0
  61. package/dist/store/runtime/checkpoint-delta.d.ts +35 -0
  62. package/dist/store/runtime/checkpoint-delta.js +75 -0
  63. package/dist/store/runtime/durable-run-store.js +92 -25
  64. package/dist/store/runtime/events-json.d.ts +17 -0
  65. package/dist/store/runtime/events-json.js +19 -0
  66. package/dist/store/runtime/run-guarded-chat-store.js +8 -3
  67. package/dist/store/runtime/run-ledger.d.ts +2 -2
  68. package/dist/store/runtime/run-ledger.js +120 -45
  69. package/dist/store/sql/read-version.d.ts +15 -0
  70. package/dist/store/sql/read-version.js +21 -0
  71. package/dist/store/sql/store.d.ts +9 -0
  72. package/dist/store/sql/store.js +89 -4
  73. package/dist/store/sqlite/schema.d.ts +2 -2
  74. package/dist/store/sqlite/schema.js +4 -0
  75. package/package.json +12 -12
package/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## 0.6.21
2
+
3
+ - Per-chat memory churn stopped: run checkpoints are size-scaled (`checkpointIntervalMs({ bytes })`: 1 s, 3 s from 1 MB, 10 s from 5 MB) and written as guarded SQL deltas (append or text-tail splice under a length+tail receipt), falling back to a full checkpoint on mismatch. A completed checkpoint is never retried unchanged.
4
+ - Leaner reads: message-scoped run heads, lean transcripts for acceptance and recovery, a conditional history read keyed on the connection-wide version, and `hasActiveRuns()` as a single-row probe.
5
+ - Mid-run sends are deduplicated by accepted-input id (two sends with equal text are both kept); Cmd/Ctrl+Enter sends instead of cancelling; "Sending…" clears once the send is accepted; the queued label uses the status pill.
6
+
7
+ ## 0.6.19
8
+
9
+ - A direct MCP call (`mcp__<server>__X`, any CLI) shows as one row even when the MCP server normalized its arguments before the bridge saw them: once the CLI's own result has arrived, the call pairs by name with the bridge's `X` row between the two. Equal-input pairing still runs first across the turn; refusals and open calls never pair by name.
10
+ - The chat model picker names raw catalog ids friendly ("Claude Opus 5.5", context and date suffixes kept), shows "Offline fallback" only once the live model list has actually failed rather than while the host is still probing, and closes on Escape from the menu or its trigger even when an ancestor stops the keydown.
11
+ - A reply's "Done · time" runs from the person's send to the run's end for every CLI; the CLI's own reported duration, which leaves out its start-up wait, is only the fallback.
12
+ - Keep failed tool receipts visible unless a matching canonical call also failed, including buffered CLI receipts.
13
+ - Remove an obsolete hook lint directive for an uninstalled rule.
14
+ - Document the two 0.6.x fetch fallback exceptions, inject fetch in repo hosts, and redact expiry/React callback exception logs.
15
+
1
16
  ## 0.6.17
2
17
 
3
18
  ### Patch Changes
@@ -18,7 +18,11 @@ export interface AcceptedChatInput {
18
18
  }
19
19
  /** Display queue constituents using their accepted identities; storage and delivery retain one turn.
20
20
  * MOVE: Receipts travel in events_json, so separate bubbles survive a history reload without another store.
21
- * @complexity O(m + e + i) time/space for m messages, e receipt events and i accepted queue inputs.
21
+ * The earlier in-run receipt owns the bubble when an unconsumed fallback also became a user row.
22
+ * @param messages The already loaded transcript; never fetched or mutated here.
23
+ * @returns Display rows with queued fallback copies removed by accepted id and aliases, never text.
24
+ * @complexity O(m + e + i + a) time/space for messages, receipt events, inputs and aliases; no I/O.
25
+ * @example projectAcceptedInputMessages({ messages });
22
26
  */
23
27
  export declare function projectAcceptedInputMessages({ messages }: {
24
28
  messages: readonly ChatMessage[];
@@ -69,7 +73,11 @@ export declare function acceptedInputUserEvent({ input, inputs, toolUseId, after
69
73
  export declare function projectAcceptedInputEvents({ events }: {
70
74
  events: readonly AgentEvent[];
71
75
  }, options?: AcceptedInputProjectionOptions): AgentEvent[];
72
- /** The sole transcript-to-recall projection. A merged queued turn retains its original inputs. */
76
+ /** The sole transcript-to-recall projection. A merged queued turn retains its original inputs.
77
+ * @returns Accepted inputs once by id/alias, in send order; pending input is excluded.
78
+ * @complexity O(m + e + i + a) time/space for messages, events, inputs and aliases; no I/O.
79
+ * @example acceptedInputsFromMessages({ messages });
80
+ */
73
81
  export declare function acceptedInputsFromMessages({ messages }: {
74
82
  messages: readonly ChatMessage[];
75
83
  }, _options?: {}): AcceptedChatInput[];
@@ -3,28 +3,65 @@ import { questionAnswerFromResult, questionAnswerLabel } from './question-answer
3
3
  import { MID_RUN_USER_MESSAGE_EVENT_NAME } from './turn-usage.js';
4
4
  /** Display queue constituents using their accepted identities; storage and delivery retain one turn.
5
5
  * MOVE: Receipts travel in events_json, so separate bubbles survive a history reload without another store.
6
- * @complexity O(m + e + i) time/space for m messages, e receipt events and i accepted queue inputs.
6
+ * The earlier in-run receipt owns the bubble when an unconsumed fallback also became a user row.
7
+ * @param messages The already loaded transcript; never fetched or mutated here.
8
+ * @returns Display rows with queued fallback copies removed by accepted id and aliases, never text.
9
+ * @complexity O(m + e + i + a) time/space for messages, receipt events, inputs and aliases; no I/O.
10
+ * @example projectAcceptedInputMessages({ messages });
7
11
  */
8
12
  export function projectAcceptedInputMessages({ messages }, _options = {}) {
13
+ const seen = new Set();
9
14
  return messages.flatMap(message => {
10
- if (message.role !== 'user')
11
- return [message];
12
- const inputs = (message.events ?? []).flatMap(event => {
13
- const input = acceptedInputEvent({ event });
14
- return input ? [input.data] : [];
15
- });
16
- if (inputs.length === 0)
15
+ if (message.role !== 'user') {
16
+ for (const input of inputsFromMessage({ message }))
17
+ claimAcceptedInput({ input, seen });
17
18
  return [message];
18
- const turn = { ...message };
19
- delete turn.attachments;
20
- delete turn.folders;
21
- delete turn.secretRedaction;
19
+ }
20
+ return expandUserInputMessage({ message, seen });
21
+ });
22
+ }
23
+ /** Keep the original row fields and per-input files when splitting a batched fallback.
24
+ * @complexity O(e + i) time/space for the row's events and accepted inputs; no effects.
25
+ */
26
+ function expandUserInputMessage({ message, seen }, _options = {}) {
27
+ const inputs = inputsFromMessage({ message: { ...message, inputPending: false } });
28
+ // Ordinary user rows have no receipt metadata to split.
29
+ if (!(message.events ?? []).some(event => acceptedInputEvent({ event }))) {
30
+ return claimAcceptedInput({ input: inputs[0], seen }) ? [message] : [];
31
+ }
32
+ return inputs.flatMap((input, index) => {
33
+ if (!claimAcceptedInput({ input, seen }))
34
+ return [];
22
35
  // MOVE: Older receipts have no per-input files; retain those legacy files on the final bubble.
23
- return inputs.map((input, index) => ({ ...turn, id: input.id, content: input.text,
24
- folders: input.folders ?? (index === inputs.length - 1 ? message.folders ?? [] : []),
25
- attachments: input.attachments ?? (index === inputs.length - 1 ? message.attachments ?? [] : []), ...(input.secretRedaction ? { secretRedaction: input.secretRedaction } : {}) }));
36
+ const legacyFiles = index === inputs.length - 1 ? message : {};
37
+ return [userInputRow({ message, input, legacyFiles })];
26
38
  });
27
39
  }
40
+ /** Expand one accepted input without copying another input's files or redaction metadata.
41
+ * @returns A display-only row; stored messages and their receipt events remain unchanged.
42
+ * @complexity O(1) time/space, retaining attachment and folder arrays by reference.
43
+ */
44
+ function userInputRow({ message, input, legacyFiles }, _options = {}) {
45
+ const turn = { ...message };
46
+ delete turn.attachments;
47
+ delete turn.folders;
48
+ delete turn.secretRedaction;
49
+ return { ...turn, id: input.id, content: input.text,
50
+ folders: input.folders ?? legacyFiles.folders ?? [],
51
+ attachments: input.attachments ?? legacyFiles.attachments ?? [],
52
+ ...(input.secretRedaction ? { secretRedaction: input.secretRedaction } : {}) };
53
+ }
54
+ /** Claim the stable identity and all aliases even on a duplicate, so later aliases stay deduplicated.
55
+ * @returns Whether this is the first occurrence; mutates only the projection's local identity set.
56
+ * @complexity O(a) time/space for one input's aliases.
57
+ */
58
+ function claimAcceptedInput({ input, seen }, _options = {}) {
59
+ const ids = [input.id, ...(input.aliases ?? [])];
60
+ const duplicate = ids.some(id => seen.has(id));
61
+ for (const id of ids)
62
+ seen.add(id);
63
+ return !duplicate;
64
+ }
28
65
  /** Reads the existing event-backed user bubble without depending on a host renderer. */
29
66
  export function acceptedInputEvent({ event }, _options = {}) {
30
67
  if (event.kind !== 'ext' || event.name !== MID_RUN_USER_MESSAGE_EVENT_NAME)
@@ -237,26 +274,43 @@ function projectEvents({ events }, options) {
237
274
  }
238
275
  return projected;
239
276
  }
240
- /** The sole transcript-to-recall projection. A merged queued turn retains its original inputs. */
277
+ /** The sole transcript-to-recall projection. A merged queued turn retains its original inputs.
278
+ * @returns Accepted inputs once by id/alias, in send order; pending input is excluded.
279
+ * @complexity O(m + e + i + a) time/space for messages, events, inputs and aliases; no I/O.
280
+ * @example acceptedInputsFromMessages({ messages });
281
+ */
241
282
  export function acceptedInputsFromMessages({ messages }, _options = {}) {
242
- return messages.flatMap(message => {
243
- if (message.role === 'user') {
244
- if (message.inputPending)
245
- return [];
246
- // Queue constituents travel in events_json, already preserved by the existing chat store.
247
- const receipts = (message.events ?? []).flatMap(event => {
248
- const input = acceptedInputEvent({ event });
249
- return input ? [input.data] : [];
250
- });
251
- return receipts.length > 0 ? receipts : [{ id: message.id, text: message.content, ...(message.folders ? { folders: message.folders } : {}) }];
252
- }
253
- return projectAcceptedInputEvents({ events: message.events ?? [] }).flatMap(event => {
254
- const input = acceptedInputEvent({ event });
255
- if (!input)
256
- return [];
257
- return input.data.inputs?.map((receipt, index) => index === 0 ? { ...receipt, aliases: input.data.aliases ?? [] } : receipt) ?? [input.data];
258
- });
283
+ const seen = new Set();
284
+ return messages.flatMap(message => inputsFromMessage({ message }))
285
+ .filter(input => claimAcceptedInput({ input, seen }));
286
+ }
287
+ /** Read one message's accepted identities, retaining queue constituents and wire aliases.
288
+ * @complexity O(e + i) time/space for the message's events and expanded inputs; no effects.
289
+ */
290
+ function inputsFromMessage({ message }, _options = {}) {
291
+ if (message.inputPending)
292
+ return [];
293
+ // Queue constituents travel in events_json, already preserved by the existing chat store.
294
+ const receipts = projectAcceptedInputEvents({ events: message.events ?? [] }).flatMap(event => {
295
+ const input = acceptedInputEvent({ event });
296
+ if (!input)
297
+ return [];
298
+ return receiptInputs({ receipt: input });
259
299
  });
300
+ if (message.role !== 'user' || receipts.length > 0)
301
+ return receipts;
302
+ return [{ id: message.id, text: message.content, ...(message.folders ? { folders: message.folders } : {}) }];
303
+ }
304
+ /** Expand a batched receipt while retaining the first input's local and transport aliases.
305
+ * @returns The accepted constituents, or the single receipt; does not mutate either.
306
+ * @complexity O(i + a) time/space for constituents and first-input aliases.
307
+ */
308
+ function receiptInputs({ receipt }, _options = {}) {
309
+ if (!receipt.data.inputs)
310
+ return [receipt.data];
311
+ return receipt.data.inputs.map((input, index) => index === 0 ? {
312
+ ...input, aliases: [...(input.aliases ?? []), ...(receipt.data.aliases ?? [])],
313
+ } : input);
260
314
  }
261
315
  /** Preserve folder metadata through existing event JSON storage; no attachment lifecycle or schema.
262
316
  * Existing per-input receipts already own queued variants and must retain their identities.
@@ -87,12 +87,14 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
87
87
  stopTimer?.();
88
88
  stopTimer = undefined;
89
89
  const recover = recoveryPending;
90
+ let retryRecovery = recover;
90
91
  // Consume only when a read starts: faults during its await latch one follow-up recovery.
91
92
  recoveryPending = false;
92
93
  try {
93
94
  const message = await ports.read({ runId, ...(messageId ? { messageId } : {}) }, { recover });
94
95
  if (stopped || !message)
95
96
  return;
97
+ retryRecovery = false;
96
98
  if (message === "gone") {
97
99
  stop();
98
100
  handlers.onDone([]);
@@ -101,6 +103,10 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
101
103
  applySavedMessage(message);
102
104
  }
103
105
  finally {
106
+ // Recovery must return a body before conditional polls resume: a failed recovery followed
107
+ // by unchanged (304) polls would otherwise leave a quiet run's dropped stream closed.
108
+ if (retryRecovery)
109
+ recoveryPending = true;
104
110
  reading = false;
105
111
  if (!stopped) {
106
112
  // Always schedule: an error arriving during read must not create a tight recovery loop.
@@ -4,7 +4,7 @@ export type { AssistantTransportPorts, AssistantTransportOptions, AssistantTrans
4
4
  export type { AgUiClient, AgUiClientPorts, AgUiRunClient, AgUiEventContext } from "./ag-ui-transport.js";
5
5
  export { followDurableRun } from "./durable-run-subscription.js";
6
6
  export type { DurableRunBinding, DurableSubscriptionPorts } from "./durable-run-subscription.js";
7
- export { activeRunStub, messageWriteKey, persistableMessages } from "./messages.js";
7
+ export { activeRunStub, messageWriteKey, persistableMessages, createConditionalHistoryRead } from "./messages.js";
8
8
  export type { AssistantConversation } from "./messages.js";
9
9
  export { createChatAttachmentValidator } from "./attachment-liveness.js";
10
10
  export type { ChatAttachmentValidatorInput, ChatAttachmentValidatorOptions } from "./attachment-liveness.js";
@@ -1,7 +1,7 @@
1
1
  /** Daemon/BYOK/AG-UI chat client lifecycles. Host routes, identity and authenticated I/O are ports. */
2
2
  export { createAssistantTransportClient } from "./transport.js";
3
3
  export { followDurableRun } from "./durable-run-subscription.js";
4
- export { activeRunStub, messageWriteKey, persistableMessages } from "./messages.js";
4
+ export { activeRunStub, messageWriteKey, persistableMessages, createConditionalHistoryRead } from "./messages.js";
5
5
  export { createChatAttachmentValidator } from "./attachment-liveness.js";
6
6
  export { createA2uiActionPoster } from "./a2ui-action-poster.js";
7
7
  //# sourceMappingURL=entry.js.map
@@ -70,4 +70,25 @@ export declare function messageWriteKey({ message }: {
70
70
  export declare function activeRunStub({ messages }: {
71
71
  messages: ChatMessage[];
72
72
  }, _options?: Record<string, never>): ChatMessage | null;
73
+ /** Retain only the last history response by reference, while reauthorizing every refresh at the
74
+ * server. An unchanged reply reuses it without reading/parsing the whole conversation again.
75
+ * @param required.read Host HTTP adapter; version is opaque and scoped to one conversation.
76
+ * @returns An async history reader. Port failures clear its snapshot and propagate.
77
+ * @throws If an adapter returns unchanged without an earlier snapshot (a port contract violation).
78
+ * @complexity O(1) local work; one retained snapshot reference, with no message/event copies.
79
+ * @example const load = createConditionalHistoryRead({ read }); await load({ conversationId });
80
+ */
81
+ export declare function createConditionalHistoryRead({ read }: {
82
+ read: (required: {
83
+ conversationId: string;
84
+ }, optional: {
85
+ version?: string | undefined;
86
+ }) => Promise<{
87
+ messages?: ChatMessage[];
88
+ version?: string | undefined;
89
+ unchanged?: boolean;
90
+ }>;
91
+ }, _options?: {}): ({ conversationId }: {
92
+ conversationId: string;
93
+ }, _optional?: {}) => Promise<ChatMessage[]>;
73
94
  //# sourceMappingURL=messages.d.ts.map
@@ -65,4 +65,35 @@ export function activeRunStub({ messages }, _options = {}) {
65
65
  return null;
66
66
  return isTerminalRunStatus({ status: last.runStatus }) ? null : last;
67
67
  }
68
+ /** Retain only the last history response by reference, while reauthorizing every refresh at the
69
+ * server. An unchanged reply reuses it without reading/parsing the whole conversation again.
70
+ * @param required.read Host HTTP adapter; version is opaque and scoped to one conversation.
71
+ * @returns An async history reader. Port failures clear its snapshot and propagate.
72
+ * @throws If an adapter returns unchanged without an earlier snapshot (a port contract violation).
73
+ * @complexity O(1) local work; one retained snapshot reference, with no message/event copies.
74
+ * @example const load = createConditionalHistoryRead({ read }); await load({ conversationId });
75
+ */
76
+ export function createConditionalHistoryRead({ read }, _options = {}) {
77
+ let saved;
78
+ return async ({ conversationId }, _optional = {}) => {
79
+ const previous = saved?.conversationId === conversationId ? saved : undefined;
80
+ try {
81
+ const result = await read({ conversationId }, { version: previous?.version });
82
+ if (result.unchanged) {
83
+ if (!previous)
84
+ throw new Error('Unchanged history requires an earlier snapshot');
85
+ return previous.messages;
86
+ }
87
+ // Same coercion/reason as a conversation list: initialMessages must never be undefined.
88
+ // A successful transport guarantees JSON syntax, not the response object's field shape.
89
+ const messages = Array.isArray(result.messages) ? result.messages : [];
90
+ saved = { conversationId, messages, version: result.version };
91
+ return messages;
92
+ }
93
+ catch (error) {
94
+ saved = undefined;
95
+ throw error;
96
+ }
97
+ };
98
+ }
68
99
  //# sourceMappingURL=messages.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Select the checkpoint delay for an approximate saved-projection size.
3
+ * @param required.bytes Approximate payload size in bytes; thresholds use decimal MB.
4
+ * @returns 1 s below 1 MB, 3 s from 1 MB, and 10 s from 5 MB. Pure; no side effects.
5
+ * @complexity O(1) time and space.
6
+ */
7
+ export declare function checkpointIntervalMs({ bytes }: {
8
+ bytes: number;
9
+ }, _options?: {}): number;
10
+ /** Count durable progress without materializing the saved projection. Thinking and extension
11
+ * receipts must survive a crash even before the next answer/tool. Status chatter and duplicate
12
+ * tool echoes must not trigger another multi-MB compaction.
13
+ * @param required.events Current renderable projection; never mutated.
14
+ * @returns Text/thinking characters, extension count and unique tool identities.
15
+ * @complexity O(events) time and O(unique tool identities) space.
16
+ * @example checkpointProgress({ events: [{ kind: 'thinking', text: 'Working' }] });
17
+ */
18
+ export declare function checkpointProgress({ events }: {
19
+ events: readonly import('./events.js').AgentEvent[];
20
+ }, _options?: {}): {
21
+ textCharacters: number;
22
+ extensionEvents: number;
23
+ tools: Set<string>;
24
+ };
25
+ //# sourceMappingURL=checkpoint-interval.d.ts.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Select the checkpoint delay for an approximate saved-projection size.
3
+ * @param required.bytes Approximate payload size in bytes; thresholds use decimal MB.
4
+ * @returns 1 s below 1 MB, 3 s from 1 MB, and 10 s from 5 MB. Pure; no side effects.
5
+ * @complexity O(1) time and space.
6
+ */
7
+ export function checkpointIntervalMs({ bytes }, _options = {}) {
8
+ if (bytes >= 5_000_000)
9
+ return 10_000;
10
+ if (bytes >= 1_000_000)
11
+ return 3_000;
12
+ return 1_000;
13
+ }
14
+ /** Count durable progress without materializing the saved projection. Thinking and extension
15
+ * receipts must survive a crash even before the next answer/tool. Status chatter and duplicate
16
+ * tool echoes must not trigger another multi-MB compaction.
17
+ * @param required.events Current renderable projection; never mutated.
18
+ * @returns Text/thinking characters, extension count and unique tool identities.
19
+ * @complexity O(events) time and O(unique tool identities) space.
20
+ * @example checkpointProgress({ events: [{ kind: 'thinking', text: 'Working' }] });
21
+ */
22
+ export function checkpointProgress({ events }, _options = {}) {
23
+ let textCharacters = 0;
24
+ let extensionEvents = 0;
25
+ const tools = new Set();
26
+ for (const event of events) {
27
+ if (event.kind === 'text' || event.kind === 'thinking')
28
+ textCharacters += event.text.length;
29
+ if (event.kind === 'ext')
30
+ extensionEvents++;
31
+ if (event.kind === 'tool_use')
32
+ tools.add(`tool_use:${event.id}`);
33
+ if (event.kind === 'tool_result')
34
+ tools.add(`tool_result:${event.toolUseId}`);
35
+ }
36
+ return { textCharacters, extensionEvents, tools };
37
+ }
38
+ //# sourceMappingURL=checkpoint-interval.js.map
@@ -20,8 +20,34 @@ export interface ChatRunRecoveryOptions {
20
20
  * Called outside storage transactions so a daemon request cannot hold the database lock. */
21
21
  readonly recover?: (input: InterruptedChatTurn) => Promise<boolean>;
22
22
  }
23
+ /** Watch-owned receipt for the last exact incoming projection. SQL lengths count characters,
24
+ * not UTF-16 code units. A saved-only tool barrier invalidates this receipt. */
25
+ export interface CheckpointState {
26
+ /** Native row/connection version guards equal-length changes outside the bounded suffix. */
27
+ readonly version: string;
28
+ readonly length: number;
29
+ /** SQL character count, avoiding a second full content parameter in CAS. */
30
+ readonly contentLength: number;
31
+ /** SQL characters in the complete last serialized event plus closing array bracket. */
32
+ readonly tailLength: number;
33
+ readonly content: string;
34
+ readonly events: readonly import("../events.js").AgentEvent[];
35
+ /** At most 256 UTF-16 units of the saved suffix; never the whole last tool result. */
36
+ readonly tail: string;
37
+ }
38
+ export interface CheckpointOptions {
39
+ readonly expected?: number | undefined;
40
+ readonly previous?: CheckpointState | undefined;
41
+ /** Undefined means the authoritative merge retained saved-only events: use the full path next. */
42
+ readonly onPersisted?: (state: CheckpointState | undefined) => void;
43
+ }
23
44
  export interface ChatRunLedger {
24
45
  readonly durable?: DurableRunStore;
46
+ /** Read-only existence probe across all owners. Only queued/running assistant rows block idle
47
+ * shutdown; no transcript payloads, recovery or writes. Rejects on storage failure (host stays busy).
48
+ * @example if (await ledger.hasActiveRuns({}, {})) return;
49
+ */
50
+ hasActiveRuns(required?: {}, optional?: {}): Promise<boolean>;
25
51
  /**
26
52
  * Runs `write` only while `run`'s row holds no terminal status (an unknown row, another run id, or
27
53
  * a row still `queued`/`running` all count as unsettled), with no settle able to land in between.
@@ -43,7 +69,7 @@ export interface ChatRunLedger {
43
69
  * A no-op once the row is terminal or belongs to another run. Resolves `true` when it wrote, or
44
70
  * when the row already held exactly this checkpoint (accepted without rewriting the row).
45
71
  */
46
- checkpoint(progress: RunProgress, optional?: {}): Promise<boolean>;
72
+ checkpoint(progress: RunProgress, optional?: CheckpointOptions): Promise<boolean>;
47
73
  /** Serving-only discovery of unfinished rows. The injected recovery coordinator owns adopted
48
74
  * runs; rows it cannot adopt are canceled, preserving saved work. Returns handled rows.
49
75
  * Export apps must not invoke discovery while a serving daemon is still alive. */
@@ -27,10 +27,34 @@ export interface DurableRun extends RunRef {
27
27
  /** A run without its conversation transcript: what a fence, lock or tool guard reads. `message`
28
28
  * still carries the run's own saved events; only `load`/`accept`/`cancelPending` read every message. */
29
29
  export type DurableRunHead = Omit<DurableRun, "transcript">;
30
+ /** Raw current-message projection. An absent messageJson means the cursor is unchanged. */
31
+ export interface DurableRunPoll {
32
+ readonly cursor: string;
33
+ readonly conversationId?: string;
34
+ readonly messageJson?: string;
35
+ }
30
36
  export interface DurableRunStore {
37
+ /** Owner-scoped, unlocked poll. No transcript read or event parse; unchanged omits payloads. */
38
+ poll?(required: {
39
+ messageId: string;
40
+ principalId: string;
41
+ workspaceId: string;
42
+ }, optional: {
43
+ cursor?: string;
44
+ }): Promise<DurableRunPoll | null>;
45
+ /** `includeTranscriptEvents:false` omits prior assistant payloads, keeping current-run events
46
+ * and user receipts that preserve queued inputs and folder references. Defaults to a full read. */
31
47
  load(required: {
32
48
  messageId: string;
33
- }, optional: {}): Promise<DurableRun | null>;
49
+ }, optional: {
50
+ includeTranscriptEvents?: boolean;
51
+ }): Promise<DurableRun | null>;
52
+ /** Follow the current attempt by message id without loading its conversation. */
53
+ head?(required: {
54
+ messageId: string;
55
+ }, optional: {
56
+ events?: boolean;
57
+ }): Promise<DurableRunHead | null>;
34
58
  /** Reads only this run's message and attempt rows; `load` when the transcript is needed. */
35
59
  find(required: {
36
60
  runId: string;
@@ -56,6 +56,12 @@ export type AgentEvent = {
56
56
  id: string;
57
57
  name: string;
58
58
  input: unknown;
59
+ bridgeCall?: {
60
+ name: string;
61
+ input: unknown;
62
+ buffered?: boolean;
63
+ };
64
+ toolOrigin?: 'bridge';
59
65
  } | {
60
66
  kind: 'tool_result';
61
67
  toolUseId: string;
@@ -39,4 +39,5 @@ export { turnUsage, conversationUsage, replySegments, isReplyBoundary, MID_RUN_U
39
39
  export { formatUsageCost } from './usage-format.js';
40
40
  export * from './accepted-input.js';
41
41
  export * from './question-answer.js';
42
+ export { checkpointIntervalMs, checkpointProgress } from './checkpoint-interval.js';
42
43
  //# sourceMappingURL=index.d.ts.map
@@ -38,4 +38,5 @@ export { turnUsage, conversationUsage, replySegments, isReplyBoundary, MID_RUN_U
38
38
  export { formatUsageCost } from './usage-format.js';
39
39
  export * from './accepted-input.js';
40
40
  export * from './question-answer.js';
41
+ export { checkpointIntervalMs, checkpointProgress } from './checkpoint-interval.js';
41
42
  //# sourceMappingURL=index.js.map
@@ -5,8 +5,19 @@
5
5
  * Authenticated routing, ownership, boot repair and storage transactions remain host responsibilities. */
6
6
  import { decodeSseFrames, parseSseRecord } from "@jini-ai/core/primitives";
7
7
  import { assistantContentFromEvents } from "../assistant-content.js";
8
+ import { recoverableSurfaceResource } from '../surface-recovery.js';
8
9
  import { mergeAdjacentTextEvents } from "../compact-events.js";
9
10
  import { dedupeToolUsesById } from "../tool-events.js";
11
+ /** Retain executor-decoded display correlation through both live and saved chat reduction.
12
+ * Only the delegated bridge marks canonical rows; malformed optional metadata is ignored. O(1). */
13
+ function toolCallCorrelation({ payload }) {
14
+ const call = payload.bridgeCall;
15
+ return {
16
+ ...(payload.toolOrigin === 'bridge' ? { toolOrigin: 'bridge' } : {}),
17
+ ...(isRecord(call) && typeof call.name === 'string' ? { bridgeCall: { name: call.name, input: call.input,
18
+ ...(call.buffered === true ? { buffered: true } : {}) } } : {}),
19
+ };
20
+ }
10
21
  function wireString(v) {
11
22
  if (typeof v === "string")
12
23
  return v;
@@ -84,7 +95,8 @@ export function translateRunAgentPayload({ payload }, _options = {}) {
84
95
  case "thinking_delta":
85
96
  return { kind: "thinking", text: wireString(payload.delta) };
86
97
  case "tool_use":
87
- return { kind: "tool_use", id: wireString(payload.id), name: wireString(payload.name), input: payload.input };
98
+ return { kind: "tool_use", id: wireString(payload.id), name: wireString(payload.name), input: payload.input,
99
+ ...toolCallCorrelation({ payload }) };
88
100
  case "tool_result":
89
101
  return toolResultEvent(payload);
90
102
  case "usage":
@@ -126,7 +138,8 @@ function translateEnvelopePayload(payload) {
126
138
  // worst possible failure for a confirmation dialog. Unwrapping here is what makes the two ends
127
139
  // meet. `name` must stay `"mcp-ui"` to match `MCP_UI_EXT_EVENT_NAME`.
128
140
  case "mcp-ui":
129
- return { kind: "ext", name: "mcp-ui", data: payload.resource };
141
+ return { kind: "ext", name: "mcp-ui", data: recoverableSurfaceResource({ resource: payload.resource,
142
+ exchange: 'surfaceExchange' in payload ? payload.surfaceExchange : undefined }) };
130
143
  // A2UI's own agent->renderer envelope (`@jini-ai/core`'s `SurfaceEmission` with
131
144
  // `channel: "a2ui"`, injected by `@jini-ai/daemon`'s `delegated-tool-bridge.ts` as
132
145
  // `{type: "a2ui", message: <AgentToRendererMessage>}`). Unwrapped to the bare `.message` here,
@@ -0,0 +1,19 @@
1
+ /** Wire metadata belongs to the channel adapter, never to rendered HTML or model context. */
2
+ export declare const SURFACE_EXCHANGE_META = "jini/surface-exchange";
3
+ export interface SurfaceRecoveryInfo {
4
+ readonly exchangeId: string;
5
+ readonly toolId: string;
6
+ readonly runId: string;
7
+ }
8
+ /** Read only server-issued exchange identity; original tool arguments never cross this boundary.
9
+ * @complexity O(1). */
10
+ export declare function surfaceRecoveryInfo({ value }: {
11
+ value: unknown;
12
+ }, _options?: {}): SurfaceRecoveryInfo | undefined;
13
+ /** Preserve the existing bare-resource renderer contract while attaching recovery identity.
14
+ * @complexity O(1), shallow copies only. Malformed resources remain the renderer's responsibility. */
15
+ export declare function recoverableSurfaceResource({ resource, exchange }: {
16
+ resource: unknown;
17
+ exchange: unknown;
18
+ }, _options?: {}): unknown;
19
+ //# sourceMappingURL=surface-recovery.d.ts.map
@@ -0,0 +1,26 @@
1
+ /** Wire metadata belongs to the channel adapter, never to rendered HTML or model context. */
2
+ export const SURFACE_EXCHANGE_META = 'jini/surface-exchange';
3
+ function record(value) {
4
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
5
+ }
6
+ /** Read only server-issued exchange identity; original tool arguments never cross this boundary.
7
+ * @complexity O(1). */
8
+ export function surfaceRecoveryInfo({ value }, _options = {}) {
9
+ if (!record(value))
10
+ return undefined;
11
+ if (typeof value.exchangeId !== 'string' || typeof value.toolId !== 'string' || typeof value.runId !== 'string')
12
+ return undefined;
13
+ if (!value.exchangeId || !value.toolId || !value.runId)
14
+ return undefined;
15
+ return { exchangeId: value.exchangeId, toolId: value.toolId, runId: value.runId };
16
+ }
17
+ /** Preserve the existing bare-resource renderer contract while attaching recovery identity.
18
+ * @complexity O(1), shallow copies only. Malformed resources remain the renderer's responsibility. */
19
+ export function recoverableSurfaceResource({ resource, exchange }, _options = {}) {
20
+ const info = surfaceRecoveryInfo({ value: exchange });
21
+ if (!info || !record(resource) || !record(resource.resource))
22
+ return resource;
23
+ const meta = record(resource.resource._meta) ? resource.resource._meta : {};
24
+ return { ...resource, resource: { ...resource.resource, _meta: { ...meta, [SURFACE_EXCHANGE_META]: info } } };
25
+ }
26
+ //# sourceMappingURL=surface-recovery.js.map
@@ -14,9 +14,12 @@ import type { AgentEvent } from './events.js';
14
14
  * `k` is the number of distinct `tool_use` ids seen — one linear pass with
15
15
  * a `Set` membership check.
16
16
  */
17
- export declare function dedupeToolUsesById({ events }: {
17
+ export declare function dedupeToolUsesById(required: {
18
18
  events: AgentEvent[] | undefined;
19
- }): AgentEvent[];
19
+ }, optional?: {}): AgentEvent[];
20
+ export declare function dedupeToolUsesById(required: {
21
+ events: readonly AgentEvent[] | undefined;
22
+ }, optional?: {}): readonly AgentEvent[];
20
23
  /**
21
24
  * Whether `name` is a delegated-tool transport wrapper — `execute_delegated_tool` or
22
25
  * `execute_readonly_delegated_tool`, bare or as an MCP client names it (`mcp__<server>__…`). Its real
@@ -45,9 +48,14 @@ export declare function isDelegatedWrapperToolName({ name }: {
45
48
  * Antigravity buffers CLI receipts: its adapter-described wrappers match equal inner inputs
46
49
  * anywhere in this assistant turn, in appearance order among equal keys. Pass only one turn's events;
47
50
  * no state is retained across calls. Wrapper and bridge rows are each claimed at most once.
51
+ * Receipt and canonical failure states must match: a success cannot consume a failed row,
52
+ * and an identical successful or pending call cannot account for a refusal before the bridge,
53
+ * regardless of buffered receipt ordering.
48
54
  * A legacy gateway folds onto the first LATER canonical `tool_use` whose name equals its `input.toolId`.
49
55
  * A direct call folds onto the first LATER `X` row with structurally equal JSON input that arrives
50
- * before the CLI's own result for it. No canonical row is claimed twice, and a folded wrapper's
56
+ * before the CLI's own result for it; failing that, once that result has arrived, onto the first
57
+ * unclaimed same-name row between the two, since argument normalization can change the bridge's
58
+ * input (see {@link findCanonicalRow}). No canonical row is claimed twice, and a folded wrapper's
51
59
  * `tool_result` events go with it. A wrapper with no such canonical row (refused before the
52
60
  * bridge, e.g. a read-only 403) stays, so its error is not hidden.
53
61
  *