@jini-ai/chat 0.6.19 → 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.
- package/CHANGELOG.md +6 -0
- package/dist/core/accepted-input.d.ts +10 -2
- package/dist/core/accepted-input.js +87 -33
- package/dist/core/assistant-client/durable-run-subscription.js +6 -0
- package/dist/core/assistant-client/entry.d.ts +1 -1
- package/dist/core/assistant-client/entry.js +1 -1
- package/dist/core/assistant-client/messages.d.ts +21 -0
- package/dist/core/assistant-client/messages.js +31 -0
- package/dist/core/checkpoint-interval.d.ts +25 -0
- package/dist/core/checkpoint-interval.js +38 -0
- package/dist/core/durable-runs/ledger-contracts.d.ts +27 -1
- package/dist/core/durable-runs/ports.d.ts +25 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/tool-events.d.ts +5 -2
- package/dist/core/tool-events.js +1 -16
- package/dist/react/components/Composer.d.ts +3 -3
- package/dist/react/components/Composer.js +3 -3
- package/dist/react/components/MessageList.js +2 -1
- package/dist/react/components/MidRunUserMessage.js +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +1 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +5 -4
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +3 -9
- package/dist/react/hooks/useConversation.js +13 -2
- package/dist/server/durable-runs/recover.js +3 -3
- package/dist/store/ports.d.ts +17 -3
- package/dist/store/runtime/chat-history-store.js +6 -0
- package/dist/store/runtime/checkpoint-delta.d.ts +35 -0
- package/dist/store/runtime/checkpoint-delta.js +75 -0
- package/dist/store/runtime/durable-run-store.js +92 -25
- package/dist/store/runtime/events-json.d.ts +17 -0
- package/dist/store/runtime/events-json.js +19 -0
- package/dist/store/runtime/run-guarded-chat-store.js +8 -3
- package/dist/store/runtime/run-ledger.d.ts +2 -2
- package/dist/store/runtime/run-ledger.js +120 -45
- package/dist/store/sql/read-version.d.ts +15 -0
- package/dist/store/sql/read-version.js +21 -0
- package/dist/store/sql/store.d.ts +9 -0
- package/dist/store/sql/store.js +89 -4
- package/dist/store/sqlite/schema.d.ts +2 -2
- package/dist/store/sqlite/schema.js +4 -0
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
## 0.6.19
|
|
2
8
|
|
|
3
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.
|
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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?:
|
|
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: {
|
|
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;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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
|
package/dist/core/index.js
CHANGED
|
@@ -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
|
|
@@ -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(
|
|
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
|
package/dist/core/tool-events.js
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
import { canonicalJson } from '@jini-ai/core/records';
|
|
2
2
|
import { toolPresentation } from './tool-presentation.js';
|
|
3
|
-
|
|
4
|
-
* Drop repeated `tool_use` events that share an `id`. A retried/duplicated
|
|
5
|
-
* stream can replay the same tool-call id (e.g. reconnect-and-replay landing
|
|
6
|
-
* an event twice); rendering it twice would show a phantom second call.
|
|
7
|
-
* Non-`tool_use` events (and the first occurrence of each id) pass through
|
|
8
|
-
* unchanged and in original order.
|
|
9
|
-
*
|
|
10
|
-
* @param events - The event list to dedupe, or `undefined` for an empty conversation turn.
|
|
11
|
-
* @returns A new array with later duplicate `tool_use` events removed, or the
|
|
12
|
-
* original array reference when nothing needed to change (cheap no-op for
|
|
13
|
-
* the common case).
|
|
14
|
-
* @complexity O(n) time / O(k) extra space, where `n` is `events.length` and
|
|
15
|
-
* `k` is the number of distinct `tool_use` ids seen — one linear pass with
|
|
16
|
-
* a `Set` membership check.
|
|
17
|
-
*/
|
|
18
|
-
export function dedupeToolUsesById({ events }) {
|
|
3
|
+
export function dedupeToolUsesById({ events }, _optional) {
|
|
19
4
|
if (!events || events.length === 0)
|
|
20
5
|
return [];
|
|
21
6
|
const seen = new Set();
|
|
@@ -30,9 +30,9 @@ export interface ComposerProps {
|
|
|
30
30
|
/** Cancels the in-flight run. Required when `running` is true; ignored otherwise. */
|
|
31
31
|
onCancel?: () => void;
|
|
32
32
|
/**
|
|
33
|
-
* Cmd/Ctrl+Enter:
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* Cmd/Ctrl+Enter: optional host send shortcut. ChatPane shares the ordinary mid-run delivery
|
|
34
|
+
* path so sending keeps the current run going. Without this callback the modifier falls
|
|
35
|
+
* through to ordinary Enter, so a host that has not opted in keeps its previous behavior.
|
|
36
36
|
*/
|
|
37
37
|
onInterrupt?: () => void;
|
|
38
38
|
/**
|
|
@@ -273,9 +273,9 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
|
|
|
273
273
|
function submitFromEnter(event) {
|
|
274
274
|
if (disabled || !composer.canSubmit)
|
|
275
275
|
return;
|
|
276
|
-
// Cmd/Ctrl+Enter
|
|
277
|
-
// `sendDisabled` is deliberately NOT consulted
|
|
278
|
-
//
|
|
276
|
+
// Cmd/Ctrl+Enter uses the host shortcut; ChatPane routes it through the ordinary send guards.
|
|
277
|
+
// `sendDisabled` is deliberately NOT consulted here: hosts can offer a shortcut while a run
|
|
278
|
+
// itself blocks ordinary sends. ChatPane queues or delivers live without cancelling the run.
|
|
279
279
|
if ((event.metaKey || event.ctrlKey) && onInterrupt) {
|
|
280
280
|
onInterrupt();
|
|
281
281
|
return;
|
|
@@ -38,7 +38,8 @@ export const MessageList = memo(function MessageList({ messages: sourceMessages,
|
|
|
38
38
|
message,
|
|
39
39
|
isLast: message.id === messages[messages.length - 1]?.id,
|
|
40
40
|
// A reply's start is accepted together with its user turn (`inputPending` clears then).
|
|
41
|
-
|
|
41
|
+
// A run id also proves acceptance after reload, when a legacy pending flag can survive.
|
|
42
|
+
sending: message.runId === undefined && messages[index - 1]?.inputPending === true,
|
|
42
43
|
isStreaming,
|
|
43
44
|
usageSummary: usageByMessage.get(message.id),
|
|
44
45
|
}, rowProps)), (pendingPrompts ?? (pendingPrompt ? [pendingPrompt] : [])).map((prompt, index) => (_jsx(QueuedMessage, { prompt: prompt, renderAttachment: rowProps.renderAttachment }, prompt.id ?? index))), awayFromLatest && _jsx(JumpToLatest, { onJump: jumpToLatest })] }));
|
|
@@ -49,7 +49,7 @@ function MidRunInput({ input, queued, transcriptRow }) {
|
|
|
49
49
|
const t = useT();
|
|
50
50
|
const attachments = input.attachments ?? [];
|
|
51
51
|
const gallery = useAttachmentGallery({ attachments });
|
|
52
|
-
return (_jsxs("div", { className: `jini-message jini-message-user${transcriptRow ? '' : ' jini-message-user--mid-run'}`, "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [queued && _jsx("div", { className: "jini-message-user__queued", role: "status", children: t('Queued — read after the current step') }), input.folders?.length ? _jsx("div", { className: "jini-message-attachments", children: _jsx(FolderChips, { folders: input.folders }) }) : null, _jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: input.text, label: t('Copy message') }) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
|
|
52
|
+
return (_jsxs("div", { className: `jini-message jini-message-user${transcriptRow ? '' : ' jini-message-user--mid-run'}`, "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: [queued && _jsx("div", { className: "jini-chat-pane__status jini-message-user__queued", role: "status", children: t('Queued — read after the current step') }), input.folders?.length ? _jsx("div", { className: "jini-message-attachments", children: _jsx(FolderChips, { folders: input.folders }) }) : null, _jsx("div", { className: "jini-message-content", children: input.text }), attachments.length > 0 && _jsx("div", { className: "jini-message-attachments", children: attachments.map(attachment => _jsx(SentAttachmentButton, { attachment: attachment, onOpen: gallery.open }, attachment.path)) }), _jsx("div", { className: "jini-message-actions jini-message-actions--user", children: _jsx(CopyMessageButton, { text: input.text, label: t('Copy message') }) }), gallery.opened !== null && _jsx(AttachmentPreviewModal, { attachment: gallery.opened, onClose: gallery.close, navigation: gallery.navigation })] }));
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* Registers {@link MidRunUserMessage} against the ext-event registry.
|
|
@@ -225,7 +225,7 @@ function ChatPaneComposerArea({ fileDrop, uploadAttachments, pane, disabled, una
|
|
|
225
225
|
// rather than refusing it, so the operator can keep writing while the agent thinks.
|
|
226
226
|
disabled: disabled || unavailable,
|
|
227
227
|
// The one blocker `send()` handles itself. Every other refusal still greys out the button.
|
|
228
|
-
sendDisabled: !pane.canSend && pane.sendBlocker !== 'streaming', ...definedProps({ source: { sendDisabledReason, sendDisabledReasonId: sendDisabledReason ? sendDisabledReasonId : undefined } }), running: pane.conversation.isStreaming, onCancel: pane.conversation.cancel,
|
|
228
|
+
sendDisabled: !pane.canSend && pane.sendBlocker !== 'streaming', ...definedProps({ source: { sendDisabledReason, sendDisabledReasonId: sendDisabledReason ? sendDisabledReasonId : undefined } }), running: pane.conversation.isStreaming, onCancel: pane.conversation.cancel, ...definedProps({ source: { placeholder } }), slots: { ...slots, footerAccessories: _jsxs(_Fragment, { children: [sendDisabledReason ? _jsx("small", { id: sendDisabledReasonId, className: "jini-composer-send-blocker", children: sendDisabledReason }) : null, slots.footerAccessories] }) }, ...resolveComposerAttachmentPicker(uploadAttachments, pane, attachmentAccept), ...resolveComposerWorkingDirectory(workingDirectoryAccess, workingDirectoryControlPlacement, pane) }), _jsx(ChatPaneWorkingDirectoryBlock, { workingDirectoryAccess: workingDirectoryAccess, workingDirectoryControlPlacement: workingDirectoryControlPlacement, pane: pane, t: t })] }));
|
|
229
229
|
}
|
|
230
230
|
const EMPTY_SUGGESTIONS = [];
|
|
231
231
|
const CHAT_PANE_DEFAULTS = {
|
|
@@ -115,10 +115,11 @@ export interface UseChatPaneResult extends UseChatPaneWorkingDirectoryResult {
|
|
|
115
115
|
*/
|
|
116
116
|
sendAsNewMessage: () => Promise<void>;
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* queue-then-cancel rather than cancel-then-send
|
|
121
|
-
* that never
|
|
118
|
+
* @deprecated Use send and let Composer apply sendDisabled to keyboard shortcuts.
|
|
119
|
+
* Legacy name for the modifier-key send shortcut; uses {@link send} and keeps the run going.
|
|
120
|
+
* Previously queue-then-cancel rather than cancel-then-send kept both paths on one flush so a
|
|
121
|
+
* cancel that never landed could not strand the prompt. Sharing send now also preserves that
|
|
122
|
+
* queue ownership, while a capable run reads the input after its current step.
|
|
122
123
|
*/
|
|
123
124
|
interruptSend: () => void;
|
|
124
125
|
reset: () => void;
|
|
@@ -756,15 +756,9 @@ export function useChatPane(options) {
|
|
|
756
756
|
guard.clearNotice();
|
|
757
757
|
await sendOrdinaryTurn(prompt);
|
|
758
758
|
}, [composer, guard.clearNotice, sendOrdinaryTurn]);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
return;
|
|
763
|
-
guard.clearNotice();
|
|
764
|
-
enqueueTurn({ turn: guard.redact({ text: prompt }, {}) });
|
|
765
|
-
composer.setDraft('');
|
|
766
|
-
conversation.cancel();
|
|
767
|
-
}, [composer, conversation, enqueueTurn, guard.clearNotice, guard.redact]);
|
|
759
|
+
// A send shortcut must honor the same mid-run delivery contract as the Send button.
|
|
760
|
+
// Unsupported agents wait behind the current reply; only the explicit Stop action cancels it.
|
|
761
|
+
const interruptSend = useCallback(() => { void send(); }, [send]);
|
|
768
762
|
/** Abort only uploads owned by the removed inputs; late completions must not refill the composer. */
|
|
769
763
|
const discardQueuedUploads = useCallback(({ id }, _options = {}) => {
|
|
770
764
|
for (const [controller, owner] of queuedUploadOwnersRef.current) {
|