@jini-ai/chat 0.6.16 → 0.6.19
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 +18 -0
- package/dist/core/accepted-input.d.ts +12 -0
- package/dist/core/accepted-input.js +29 -1
- package/dist/core/assistant-client/transport.js +2 -0
- package/dist/core/events.d.ts +6 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/run-events/entry.js +15 -2
- package/dist/core/surface-recovery.d.ts +19 -0
- package/dist/core/surface-recovery.js +26 -0
- package/dist/core/tool-events.d.ts +6 -1
- package/dist/core/tool-events.js +74 -27
- package/dist/core/transport.d.ts +2 -0
- package/dist/core/turn-usage.d.ts +1 -1
- package/dist/core/turn-usage.js +5 -3
- package/dist/core/user-text-redaction.js +30 -4
- package/dist/embed-widget/browser/frontend-session-bridge.d.ts +14 -1
- package/dist/embed-widget/browser/frontend-session-bridge.js +120 -47
- package/dist/react/components/Composer.js +4 -2
- package/dist/react/components/ConversationList.js +2 -1
- package/dist/react/components/ConversationTitleEditor.js +2 -1
- package/dist/react/components/CopyMessageButton.d.ts +4 -1
- package/dist/react/components/CopyMessageButton.js +3 -2
- package/dist/react/components/ExtEventErrorBoundary.js +2 -1
- package/dist/react/components/FolderChips.d.ts +13 -0
- package/dist/react/components/FolderChips.js +32 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +1 -0
- package/dist/react/components/McpUiSurfaceCard.js +12 -5
- package/dist/react/components/MessageList.d.ts +1 -0
- package/dist/react/components/MessageList.js +2 -1
- package/dist/react/components/MessageRow.js +2 -1
- package/dist/react/components/MidRunUserMessage.js +3 -2
- package/dist/react/components/ToolCard.js +3 -2
- package/dist/react/components/ToolCard.rules.js +2 -1
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -3
- package/dist/react/features/chat-pane/components/ChatPane.js +1 -0
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.d.ts +15 -2
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +19 -3
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +47 -16
- package/dist/react/features/chat-pane/hooks/useChatPaneFileDrop.hooks.js +3 -2
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +20 -3
- package/dist/react/features/chat-pane/model-options.d.ts +10 -0
- package/dist/react/features/chat-pane/model-options.js +22 -3
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +23 -1
- package/dist/react/features/chat-pane/surface-recovery-stream.d.ts +12 -0
- package/dist/react/features/chat-pane/surface-recovery-stream.js +47 -0
- package/dist/react/features/chat-pane/types.d.ts +4 -0
- package/dist/react/hooks/composer-draft-cache.d.ts +13 -10
- package/dist/react/hooks/composer-draft-cache.js +39 -14
- package/dist/react/hooks/useComposer.d.ts +7 -0
- package/dist/react/hooks/useComposer.js +83 -8
- package/dist/react/hooks/useConversation.d.ts +2 -0
- package/dist/react/hooks/useConversation.js +31 -20
- package/dist/react/hooks/useRunStream.js +2 -1
- package/dist/react/hooks/useSurfaceRecovery.d.ts +23 -0
- package/dist/react/hooks/useSurfaceRecovery.js +84 -0
- package/dist/react/styles/reference.css +10 -5
- package/dist/store/legacy/sqlite/messages.d.ts +12 -10
- package/dist/store/legacy/sqlite/messages.js +4 -1
- package/dist/store/runtime/chat-expiry-sweep.js +3 -1
- package/dist/store/runtime/durable-run-store.js +35 -3
- package/dist/store/sql/store.js +5 -1
- package/package.json +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 0.6.19
|
|
2
|
+
|
|
3
|
+
- 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.
|
|
4
|
+
- 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.
|
|
5
|
+
- 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.
|
|
6
|
+
- Keep failed tool receipts visible unless a matching canonical call also failed, including buffered CLI receipts.
|
|
7
|
+
- Remove an obsolete hook lint directive for an uninstalled rule.
|
|
8
|
+
- Document the two 0.6.x fetch fallback exceptions, inject fetch in repo hosts, and redact expiry/React callback exception logs.
|
|
9
|
+
|
|
10
|
+
## 0.6.17
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Carry picked folders as per-message folder references, with draft and sent-message chips, persistence and replay.
|
|
15
|
+
- Preserve ordinary typed paths while redacting glued-on secrets and slash-containing keys.
|
|
16
|
+
- Handle npm 12 pack JSON through the shared parser in isolated-consumer tests.
|
|
17
|
+
- Treat Node builtin dependencies as builtins when copying isolated runtime fixtures.
|
|
18
|
+
|
|
1
19
|
## 0.6.13
|
|
2
20
|
|
|
3
21
|
### Patch Changes
|
|
@@ -9,6 +9,8 @@ export interface AcceptedChatInput {
|
|
|
9
9
|
answerText?: string;
|
|
10
10
|
/** Absent for legacy/ordinary inputs; consumed only after a CLI UUID receipt. */
|
|
11
11
|
deliveryState?: 'queued' | 'consumed';
|
|
12
|
+
/** Picked directory paths, kept byte-exact and separate from upload references. */
|
|
13
|
+
folders?: string[];
|
|
12
14
|
attachments?: ChatAttachment[];
|
|
13
15
|
secretRedaction?: ChatMessage['secretRedaction'];
|
|
14
16
|
/** Wire ids replaced by this receipt remain seen by recall. */
|
|
@@ -71,5 +73,15 @@ export declare function projectAcceptedInputEvents({ events }: {
|
|
|
71
73
|
export declare function acceptedInputsFromMessages({ messages }: {
|
|
72
74
|
messages: readonly ChatMessage[];
|
|
73
75
|
}, _options?: {}): AcceptedChatInput[];
|
|
76
|
+
/** Preserve folder metadata through existing event JSON storage; no attachment lifecycle or schema.
|
|
77
|
+
* Existing per-input receipts already own queued variants and must retain their identities.
|
|
78
|
+
*/
|
|
79
|
+
export declare function withFolderReceipt({ message }: {
|
|
80
|
+
message: ChatMessage;
|
|
81
|
+
}, _options?: {}): ChatMessage;
|
|
82
|
+
/** Hydrate the message's folder list from its accepted receipts after a database read. */
|
|
83
|
+
export declare function foldersFromReceipts({ events }: {
|
|
84
|
+
events: unknown;
|
|
85
|
+
}, _options?: {}): string[];
|
|
74
86
|
export {};
|
|
75
87
|
//# sourceMappingURL=accepted-input.d.ts.map
|
|
@@ -17,9 +17,11 @@ export function projectAcceptedInputMessages({ messages }, _options = {}) {
|
|
|
17
17
|
return [message];
|
|
18
18
|
const turn = { ...message };
|
|
19
19
|
delete turn.attachments;
|
|
20
|
+
delete turn.folders;
|
|
20
21
|
delete turn.secretRedaction;
|
|
21
22
|
// MOVE: Older receipts have no per-input files; retain those legacy files on the final bubble.
|
|
22
23
|
return inputs.map((input, index) => ({ ...turn, id: input.id, content: input.text,
|
|
24
|
+
folders: input.folders ?? (index === inputs.length - 1 ? message.folders ?? [] : []),
|
|
23
25
|
attachments: input.attachments ?? (index === inputs.length - 1 ? message.attachments ?? [] : []), ...(input.secretRedaction ? { secretRedaction: input.secretRedaction } : {}) }));
|
|
24
26
|
});
|
|
25
27
|
}
|
|
@@ -246,7 +248,7 @@ export function acceptedInputsFromMessages({ messages }, _options = {}) {
|
|
|
246
248
|
const input = acceptedInputEvent({ event });
|
|
247
249
|
return input ? [input.data] : [];
|
|
248
250
|
});
|
|
249
|
-
return receipts.length > 0 ? receipts : [{ id: message.id, text: message.content }];
|
|
251
|
+
return receipts.length > 0 ? receipts : [{ id: message.id, text: message.content, ...(message.folders ? { folders: message.folders } : {}) }];
|
|
250
252
|
}
|
|
251
253
|
return projectAcceptedInputEvents({ events: message.events ?? [] }).flatMap(event => {
|
|
252
254
|
const input = acceptedInputEvent({ event });
|
|
@@ -256,4 +258,30 @@ export function acceptedInputsFromMessages({ messages }, _options = {}) {
|
|
|
256
258
|
});
|
|
257
259
|
});
|
|
258
260
|
}
|
|
261
|
+
/** Preserve folder metadata through existing event JSON storage; no attachment lifecycle or schema.
|
|
262
|
+
* Existing per-input receipts already own queued variants and must retain their identities.
|
|
263
|
+
*/
|
|
264
|
+
export function withFolderReceipt({ message }, _options = {}) {
|
|
265
|
+
if (message.role !== 'user' || !message.folders?.length)
|
|
266
|
+
return message;
|
|
267
|
+
const events = message.events ?? [];
|
|
268
|
+
if (events.some(event => acceptedInputEvent({ event })))
|
|
269
|
+
return message;
|
|
270
|
+
return { ...message, events: [...events, acceptedInputUserEvent({ input: {
|
|
271
|
+
id: message.id, text: message.content, folders: message.folders,
|
|
272
|
+
...(message.secretRedaction ? { secretRedaction: message.secretRedaction } : {}),
|
|
273
|
+
...(message.attachments ? { attachments: message.attachments } : {}),
|
|
274
|
+
} }, { receipt: 'accepted' })] };
|
|
275
|
+
}
|
|
276
|
+
/** Hydrate the message's folder list from its accepted receipts after a database read. */
|
|
277
|
+
export function foldersFromReceipts({ events }, _options = {}) {
|
|
278
|
+
if (!Array.isArray(events))
|
|
279
|
+
return [];
|
|
280
|
+
return events.flatMap(event => {
|
|
281
|
+
if (event === null || typeof event !== 'object')
|
|
282
|
+
return [];
|
|
283
|
+
const folders = acceptedInputEvent({ event })?.data.folders;
|
|
284
|
+
return Array.isArray(folders) ? folders.filter((path) => typeof path === 'string') : [];
|
|
285
|
+
});
|
|
286
|
+
}
|
|
259
287
|
//# sourceMappingURL=accepted-input.js.map
|
|
@@ -375,6 +375,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
375
375
|
return {
|
|
376
376
|
messages: byokMessages({ history: input.history }, {}),
|
|
377
377
|
...attachmentIdsField(input),
|
|
378
|
+
...(input.folders?.length ? { folders: input.folders } : {}),
|
|
378
379
|
...conversationIdField(input),
|
|
379
380
|
agentPluginState: input.context?.["agentPluginState"],
|
|
380
381
|
userPrompt: input.context?.["userPrompt"],
|
|
@@ -584,6 +585,7 @@ export function createAssistantTransportClient({ ports }, _options = {}) {
|
|
|
584
585
|
...modelField(input),
|
|
585
586
|
...reasoningField(input),
|
|
586
587
|
...attachmentIdsField(input),
|
|
588
|
+
...(input.folders?.length ? { folders: input.folders } : {}),
|
|
587
589
|
...pluginRefIdsField(input),
|
|
588
590
|
agentPluginState: input.context?.["agentPluginState"],
|
|
589
591
|
userPrompt: input.context?.["userPrompt"],
|
package/dist/core/events.d.ts
CHANGED
package/dist/core/messages.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export interface ChatMessage {
|
|
|
58
58
|
lastRunEventId?: string;
|
|
59
59
|
startedAt?: number;
|
|
60
60
|
endedAt?: number;
|
|
61
|
+
/** Picked directory paths, kept byte-exact and separate from upload references. */
|
|
62
|
+
folders?: string[];
|
|
61
63
|
attachments?: ChatAttachment[];
|
|
62
64
|
}
|
|
63
65
|
//# sourceMappingURL=messages.d.ts.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
|
|
@@ -45,9 +45,14 @@ export declare function isDelegatedWrapperToolName({ name }: {
|
|
|
45
45
|
* Antigravity buffers CLI receipts: its adapter-described wrappers match equal inner inputs
|
|
46
46
|
* anywhere in this assistant turn, in appearance order among equal keys. Pass only one turn's events;
|
|
47
47
|
* no state is retained across calls. Wrapper and bridge rows are each claimed at most once.
|
|
48
|
+
* Receipt and canonical failure states must match: a success cannot consume a failed row,
|
|
49
|
+
* and an identical successful or pending call cannot account for a refusal before the bridge,
|
|
50
|
+
* regardless of buffered receipt ordering.
|
|
48
51
|
* A legacy gateway folds onto the first LATER canonical `tool_use` whose name equals its `input.toolId`.
|
|
49
52
|
* 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
|
|
53
|
+
* before the CLI's own result for it; failing that, once that result has arrived, onto the first
|
|
54
|
+
* unclaimed same-name row between the two, since argument normalization can change the bridge's
|
|
55
|
+
* input (see {@link findCanonicalRow}). No canonical row is claimed twice, and a folded wrapper's
|
|
51
56
|
* `tool_result` events go with it. A wrapper with no such canonical row (refused before the
|
|
52
57
|
* bridge, e.g. a read-only 403) stays, so its error is not hidden.
|
|
53
58
|
*
|
package/dist/core/tool-events.js
CHANGED
|
@@ -61,15 +61,21 @@ function directMcpToolName({ name }) {
|
|
|
61
61
|
* Antigravity's adapter-described wrappers require equal inner inputs but may arrive buffered.
|
|
62
62
|
*/
|
|
63
63
|
function wrapperCall({ event }, _options = {}) {
|
|
64
|
-
const wrapper = toolPresentation(event).wrapper;
|
|
64
|
+
const wrapper = event.bridgeCall ?? toolPresentation(event).wrapper;
|
|
65
|
+
const bridgeOnly = event.bridgeCall !== undefined;
|
|
66
|
+
const buffered = bridgeOnly ? event.bridgeCall?.buffered === true : wrapper !== undefined;
|
|
65
67
|
const call = wrapper ?? event;
|
|
66
68
|
if (!isDelegatedWrapperToolName({ name: call.name })) {
|
|
67
69
|
const name = wrapper ? wrapper.name : directMcpToolName({ name: call.name });
|
|
68
|
-
return name ? { name, input: call.input, matchInput: true, buffered
|
|
70
|
+
return name ? { name, input: call.input, matchInput: true, buffered, bridgeOnly } : undefined;
|
|
69
71
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
const inner = delegatedCall({ input: call.input });
|
|
73
|
+
return inner ? { ...inner, matchInput: wrapper !== undefined, buffered, bridgeOnly } : undefined;
|
|
74
|
+
}
|
|
75
|
+
/** The shared delegated gateway contract, independent of the CLI's envelope. Pure, O(1). */
|
|
76
|
+
function delegatedCall({ input }, _options = {}) {
|
|
77
|
+
const args = input !== null && typeof input === 'object' ? input : {};
|
|
78
|
+
return typeof args.toolId === 'string' && args.toolId ? { name: args.toolId, input: args.input } : undefined;
|
|
73
79
|
}
|
|
74
80
|
/**
|
|
75
81
|
* Drop each delegated or direct MCP wrapper call that the daemon also reported as its own canonical row,
|
|
@@ -88,9 +94,14 @@ function wrapperCall({ event }, _options = {}) {
|
|
|
88
94
|
* Antigravity buffers CLI receipts: its adapter-described wrappers match equal inner inputs
|
|
89
95
|
* anywhere in this assistant turn, in appearance order among equal keys. Pass only one turn's events;
|
|
90
96
|
* no state is retained across calls. Wrapper and bridge rows are each claimed at most once.
|
|
97
|
+
* Receipt and canonical failure states must match: a success cannot consume a failed row,
|
|
98
|
+
* and an identical successful or pending call cannot account for a refusal before the bridge,
|
|
99
|
+
* regardless of buffered receipt ordering.
|
|
91
100
|
* A legacy gateway folds onto the first LATER canonical `tool_use` whose name equals its `input.toolId`.
|
|
92
101
|
* A direct call folds onto the first LATER `X` row with structurally equal JSON input that arrives
|
|
93
|
-
* before the CLI's own result for it
|
|
102
|
+
* before the CLI's own result for it; failing that, once that result has arrived, onto the first
|
|
103
|
+
* unclaimed same-name row between the two, since argument normalization can change the bridge's
|
|
104
|
+
* input (see {@link findCanonicalRow}). No canonical row is claimed twice, and a folded wrapper's
|
|
94
105
|
* `tool_result` events go with it. A wrapper with no such canonical row (refused before the
|
|
95
106
|
* bridge, e.g. a read-only 403) stays, so its error is not hidden.
|
|
96
107
|
*
|
|
@@ -106,19 +117,24 @@ export function foldDelegatedWrapperCalls({ events }, _options = {}) {
|
|
|
106
117
|
return [];
|
|
107
118
|
const claimed = new Set();
|
|
108
119
|
const folded = new Set();
|
|
109
|
-
events.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
const failedResults = new Set(events.flatMap(event => event.kind === 'tool_result' && event.isError ? [event.toolUseId] : []));
|
|
121
|
+
// Equal inputs pair first across the whole turn, so a by-name pairing never takes the row an
|
|
122
|
+
// exact match needed; only then may a direct call pair by name inside its closed window.
|
|
123
|
+
for (const byName of [false, true]) {
|
|
124
|
+
events.forEach((event, i) => {
|
|
125
|
+
if (event.kind !== 'tool_use')
|
|
126
|
+
return;
|
|
127
|
+
const call = wrapperCall({ event });
|
|
128
|
+
// A replayed wrapper (same id) must not claim a second canonical row.
|
|
129
|
+
if (call === undefined || folded.has(event.id))
|
|
130
|
+
return;
|
|
131
|
+
const match = findCanonicalRow({ events, wrapper: event, wrapperIndex: i, call, claimed, failedResults }, { byName });
|
|
132
|
+
if (match === -1)
|
|
133
|
+
return;
|
|
134
|
+
claimed.add(match);
|
|
135
|
+
folded.add(event.id);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
122
138
|
if (folded.size === 0)
|
|
123
139
|
return events;
|
|
124
140
|
return events.filter((event) => !((event.kind === 'tool_use' && folded.has(event.id)) ||
|
|
@@ -127,29 +143,60 @@ export function foldDelegatedWrapperCalls({ events }, _options = {}) {
|
|
|
127
143
|
/**
|
|
128
144
|
* Index of the first unclaimed canonical row in the wrapper's matching window, or -1. Legacy gateways match
|
|
129
145
|
* on name alone; direct calls and adapter-described wrappers also require equal inner input.
|
|
146
|
+
*
|
|
147
|
+
* `byName` (the second pass) lets a direct `mcp__<server>__X` call pair with a same-name row whose
|
|
148
|
+
* input differs, but only inside a CLOSED window: after the call and before the CLI's own result for
|
|
149
|
+
* it. The MCP server normalizes arguments before the bridge sees them (an integer string read as the
|
|
150
|
+
* integer, an alias renamed, see `@jini-ai/mcp`'s `tool-arguments.ts`), so the bridge row's input is
|
|
151
|
+
* not always the CLI's (demo practice run, 2026-11-02: every such call showed twice). The CLI's result
|
|
152
|
+
* only returns after the bridge row its call caused, so a same-name row in that window is that call.
|
|
153
|
+
* An open window (no CLI result yet) and buffered or legacy wrappers never pair by name.
|
|
130
154
|
* @throws CanonicalJsonError if a direct call's input contains non-JSON values.
|
|
131
155
|
* @complexity O(n·s): n events, s canonical input serialization cost.
|
|
132
156
|
*/
|
|
133
|
-
function findCanonicalRow({ events, wrapper, wrapperIndex, call, claimed }) {
|
|
134
|
-
const
|
|
135
|
-
const
|
|
157
|
+
function findCanonicalRow({ events, wrapper, wrapperIndex, call, claimed, failedResults }, { byName = false } = {}) {
|
|
158
|
+
const [start, end, closed] = canonicalSearchWindow({ events, wrapper, wrapperIndex, call });
|
|
159
|
+
const match = matchRule({ call, byName, closed });
|
|
160
|
+
if (match === undefined)
|
|
161
|
+
return -1;
|
|
162
|
+
const inputKey = match.compareInput ? canonicalJson({ value: { input: call.input } }) : undefined;
|
|
163
|
+
const wrapperFailed = failedResults.has(wrapper.id);
|
|
136
164
|
for (let j = start; j < end; j += 1) {
|
|
137
165
|
const other = events[j];
|
|
138
|
-
if (claimed.has(j)
|
|
139
|
-
continue;
|
|
140
|
-
if (inputKey === undefined || canonicalJson({ value: { input: other.input } }) === inputKey)
|
|
166
|
+
if (!claimed.has(j) && isCandidateRow({ other, call, wrapperFailed, failedResults, inputKey }))
|
|
141
167
|
return j;
|
|
142
168
|
}
|
|
143
169
|
return -1;
|
|
144
170
|
}
|
|
171
|
+
/** How one pass matches `call`, or undefined when this pass must skip it. Pass two (`byName`) only
|
|
172
|
+
* revisits direct calls whose window is closed; everything else already had its chance. Pure; O(1). */
|
|
173
|
+
function matchRule({ call, byName, closed }, _options = {}) {
|
|
174
|
+
if (!byName)
|
|
175
|
+
return { compareInput: call.matchInput };
|
|
176
|
+
return closed && call.matchInput && !call.buffered ? { compareInput: false } : undefined;
|
|
177
|
+
}
|
|
178
|
+
/** Whether `other` is an unclaimed row's possible canonical twin: a same-name `tool_use` with the same
|
|
179
|
+
* outcome and, when `inputKey` is given, structurally equal input. Pure; O(s) input serialization. */
|
|
180
|
+
function isCandidateRow({ other, call, wrapperFailed, failedResults, inputKey }, _options = {}) {
|
|
181
|
+
if (other.kind !== 'tool_use' || other.name !== call.name)
|
|
182
|
+
return false;
|
|
183
|
+
// Adapter-proposed bare/native matches must be bridge rows, never independent CLI executions.
|
|
184
|
+
if (call.bridgeOnly && other.toolOrigin !== 'bridge')
|
|
185
|
+
return false;
|
|
186
|
+
// Outcome parity keeps successes from consuming failed rows and duplicating later refusals.
|
|
187
|
+
if (wrapperFailed !== failedResults.has(other.id))
|
|
188
|
+
return false;
|
|
189
|
+
return inputKey === undefined || canonicalJson({ value: { input: other.input } }) === inputKey;
|
|
190
|
+
}
|
|
145
191
|
/** Pure matching bounds within one turn; O(n) time, O(1) space for n events.
|
|
146
192
|
* Antigravity's buffered receipts cannot establish arrival/result boundaries. Direct MCP calls
|
|
147
193
|
* retain their later-only window so a completed/refused call cannot consume a separate retry.
|
|
194
|
+
* `closed` is true only when the CLI's own result ends the window (the by-name pass needs it).
|
|
148
195
|
*/
|
|
149
196
|
function canonicalSearchWindow({ events, wrapper, wrapperIndex, call }, _options = {}) {
|
|
150
197
|
if (call.buffered)
|
|
151
|
-
return [0, events.length];
|
|
198
|
+
return [0, events.length, false];
|
|
152
199
|
const resultIndex = !call.matchInput ? -1 : events.findIndex(other => other.kind === 'tool_result' && other.toolUseId === wrapper.id);
|
|
153
|
-
return
|
|
200
|
+
return resultIndex === -1 ? [wrapperIndex + 1, events.length, false] : [wrapperIndex + 1, resultIndex, true];
|
|
154
201
|
}
|
|
155
202
|
//# sourceMappingURL=tool-events.js.map
|
package/dist/core/transport.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface StartRunInput {
|
|
|
57
57
|
history: ChatMessage[];
|
|
58
58
|
agentId?: string;
|
|
59
59
|
conversationId?: string | null;
|
|
60
|
+
/** Picked directory paths, kept byte-exact and separate from upload references. */
|
|
61
|
+
folders?: string[];
|
|
60
62
|
attachments?: ChatAttachment[];
|
|
61
63
|
context?: RunContext;
|
|
62
64
|
/** Stops the browser-side subscription; the run continues host-side. */
|
|
@@ -40,7 +40,7 @@ export declare function turnUsage({ events }: {
|
|
|
40
40
|
agentId?: string;
|
|
41
41
|
}): UsageEvent | undefined;
|
|
42
42
|
/** Derive per-turn charges and prefix totals from persisted events_json, for live and reloaded
|
|
43
|
-
* chats alike.
|
|
43
|
+
* chats alike. A single reply's duration is the message's elapsed run time, from send to finish.
|
|
44
44
|
* A message with mid-run replies also gets per-reply `segments`. O(e + m) for e events
|
|
45
45
|
* and m messages; no per-row transcript scans or I/O.
|
|
46
46
|
* @example conversationUsage({ messages: savedMessages }).get('assistant-2') */
|
package/dist/core/turn-usage.js
CHANGED
|
@@ -113,7 +113,7 @@ function addUsage({ total, usage }, _optional = {}) {
|
|
|
113
113
|
return next;
|
|
114
114
|
}
|
|
115
115
|
/** Derive per-turn charges and prefix totals from persisted events_json, for live and reloaded
|
|
116
|
-
* chats alike.
|
|
116
|
+
* chats alike. A single reply's duration is the message's elapsed run time, from send to finish.
|
|
117
117
|
* A message with mid-run replies also gets per-reply `segments`. O(e + m) for e events
|
|
118
118
|
* and m messages; no per-row transcript scans or I/O.
|
|
119
119
|
* @example conversationUsage({ messages: savedMessages }).get('assistant-2') */
|
|
@@ -157,9 +157,11 @@ function messageUsage({ message, sessions, totalCostUsd }, _optional = {}) {
|
|
|
157
157
|
}
|
|
158
158
|
return { usage, segments, totalCostUsd: prefixTotal };
|
|
159
159
|
}
|
|
160
|
-
/**
|
|
160
|
+
/** Time one reply from the person's send (`startedAt`) to the run's end, the same way for every CLI.
|
|
161
|
+
* A CLI's own reported duration leaves out its start-up wait (Claude Code said 42 s for a turn that took
|
|
162
|
+
* 46.5 s from send to finish), so it is only the fallback when the timestamps are missing or invalid. */
|
|
161
163
|
function fillRunDuration({ usage, message }, _optional = {}) {
|
|
162
|
-
if (
|
|
164
|
+
if (message.startedAt === undefined || message.endedAt === undefined)
|
|
163
165
|
return;
|
|
164
166
|
const durationMs = message.endedAt - message.startedAt;
|
|
165
167
|
if (Number.isFinite(durationMs) && durationMs >= 0)
|
|
@@ -25,6 +25,24 @@ function entropy(text) {
|
|
|
25
25
|
}
|
|
26
26
|
return bits;
|
|
27
27
|
}
|
|
28
|
+
/** Recognize identifier paths, including the fragment after a space in a typed path.
|
|
29
|
+
* Require multiple segments and readable words: arbitrary base64 and long digit-mixed runs
|
|
30
|
+
* must still reach the entropy guard. Specific credential rules have already run. */
|
|
31
|
+
function isIdentifierPath({ text }, _options = {}) {
|
|
32
|
+
const segments = text.replace(/^(?:[a-z]:|~)?[\\/]*/i, '').split(/[\\/]/).filter(Boolean);
|
|
33
|
+
return segments.length >= 2 && segments.every(segment => /^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/.test(segment)
|
|
34
|
+
|| segment.split(/[-_.]/).every(word => /^[A-Z]$/.test(word)
|
|
35
|
+
|| /^[A-Z]?[a-z]{2,}(?:[A-Z][a-z]{2,}|[0-9]{1,4}(?=[A-Z][a-z]{2,}|$))*$/.test(word)));
|
|
36
|
+
}
|
|
37
|
+
/** A piece of a random key: long, both letter cases, and a character-class change at most positions.
|
|
38
|
+
* Readable names (CamelCase, SCREAMING_CASE, dated file names) change class far less often, and
|
|
39
|
+
* every piece of a 40-character base64 key split by '/' is 12+ characters in most keys. */
|
|
40
|
+
function isOpaqueSegment(segment) {
|
|
41
|
+
if (segment.length < 12 || !/[A-Z]/.test(segment) || !/[a-z]/.test(segment))
|
|
42
|
+
return false;
|
|
43
|
+
const changes = segment.match(/[A-Z](?=[^A-Z])|[a-z](?=[^a-z])|\d(?=\D)/g)?.length ?? 0;
|
|
44
|
+
return changes / (segment.length - 1) >= 0.4;
|
|
45
|
+
}
|
|
28
46
|
/** Stricter than error redaction: also detects unlabelled long tokens, while retaining ordinary URLs,
|
|
29
47
|
* filesystem paths, hexadecimal hashes, identifiers and prose. Strong credential syntax inside
|
|
30
48
|
* URLs or paths is still caught. */
|
|
@@ -42,15 +60,23 @@ export function redactUserText({ text }, { secretField = false } = {}) {
|
|
|
42
60
|
const base = redactSecretShapes({ text: protectedText, policy: { additionalRules: CHAT_RULES } });
|
|
43
61
|
let count = base.redactions;
|
|
44
62
|
// Exempt locations only AFTER specific secret rules. Only quoted paths may contain whitespace;
|
|
45
|
-
// unquoted paths end there
|
|
63
|
+
// unquoted paths and URLs end there, and at , | ; and invisible separators, so an adjacent
|
|
64
|
+
// secret glued after them cannot inherit the exemption.
|
|
46
65
|
// A quoted path ends at its first matching quote, which must sit on a word boundary, so an
|
|
47
66
|
// apostrophe later in the line ("don't") cannot close an unterminated quote and exempt the gap.
|
|
48
67
|
// Require a root boundary so a slash inside an opaque token cannot exempt that token, and a
|
|
49
68
|
// folder after the root so a bare "/" + token is not mistaken for a path.
|
|
50
|
-
const locations = [
|
|
51
|
-
.
|
|
69
|
+
const locations = [
|
|
70
|
+
...[...base.text.matchAll(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`,|;\u200B-\u200D\uFEFF]+|(?<!\w)(["'`])(?:~\/|[a-z]:[\\/]|\\\\|\/)[^\r\n"'`\\/]+[\\/](?:(?!\1)[^\r\n])*\1(?!\w)|(?<![\w+\/=\\~.-])(?:~\/|[a-z]:[\\/]|\\\\|\/)[^\s<>"'`,|;\u200B-\u200D\uFEFF\\/]+[\\/][^\s<>"'`,|;\u200B-\u200D\uFEFF]*/gi)]
|
|
71
|
+
.map(match => ({ match, url: /^[a-z][a-z0-9+.-]*:\/\//i.test(match[0]) })),
|
|
72
|
+
// The part of a hand-typed unquoted path after a space (Work/CustomerPortal…) has no root.
|
|
73
|
+
...[...base.text.matchAll(/[^\s<>"'`,|;\u200B-\u200D\uFEFF]+/g)]
|
|
74
|
+
.filter(match => isIdentifierPath({ text: match[0] })).map(match => ({ match, url: false })),
|
|
75
|
+
].map(({ match, url }) => ({ start: match.index, end: match.index + match[0].length, url }));
|
|
52
76
|
let cleaned = base.text.replace(/(?<![A-Za-z0-9_+\/=-])[A-Za-z0-9_+\/=-]{40,}(?![A-Za-z0-9_+\/=-])/g, (candidate, offset) => {
|
|
53
|
-
|
|
77
|
+
const location = locations.find(span => offset >= span.start && offset + candidate.length <= span.end);
|
|
78
|
+
// A key glued straight onto a path (/proj/KEY) is still checked; URLs keep their opaque ids.
|
|
79
|
+
if (location && (location.url || !candidate.split(/[\\/]/).some(isOpaqueSegment)))
|
|
54
80
|
return candidate;
|
|
55
81
|
if (/^[a-f0-9]+$/i.test(candidate) || !/[A-Za-z]/.test(candidate))
|
|
56
82
|
return candidate;
|
|
@@ -72,9 +72,22 @@ export interface FrontendSessionBridge {
|
|
|
72
72
|
sessionId: string;
|
|
73
73
|
bindToken: string;
|
|
74
74
|
}>;
|
|
75
|
+
/**
|
|
76
|
+
* Ensure a connection and wait for its current attachment; an already live binding is reused.
|
|
77
|
+
* Call after successful host activation. Unlike `ready`, prior readiness cannot satisfy this wait.
|
|
78
|
+
* Cancellation rejects only this caller's wait; park/failure/close rejects all current waiters.
|
|
79
|
+
* @example await bridge.rebind({ signal }, {});
|
|
80
|
+
* @complexity O(1) per waiter; one shared stream, no automatic reconnect after park/failure.
|
|
81
|
+
*/
|
|
82
|
+
rebind(required?: {
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
}, optional?: Record<string, never>): Promise<{
|
|
85
|
+
sessionId: string;
|
|
86
|
+
bindToken: string;
|
|
87
|
+
}>;
|
|
75
88
|
/**
|
|
76
89
|
* The token for the attachment that is live *now*, or `undefined` before the first attach and
|
|
77
|
-
* after {@link FrontendSessionBridge.close}. Read it at run-start
|
|
90
|
+
* after park, suspension, failure or {@link FrontendSessionBridge.close}. Read it at run-start.
|
|
78
91
|
*/
|
|
79
92
|
bindToken(): string | undefined;
|
|
80
93
|
close(): void;
|