@jini-ai/daemon 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 +12 -0
- package/dist/agent-executor/claude/mcp-config.d.ts +17 -0
- package/dist/agent-executor/claude/mcp-config.js +29 -0
- package/dist/agent-executor/codex/home-config.d.ts +7 -6
- package/dist/agent-executor/codex/home-config.js +15 -3
- package/dist/agent-executor/codex/home-staging.d.ts +4 -2
- package/dist/agent-executor/codex/home-staging.js +5 -3
- package/dist/agent-executor/contracts.d.ts +15 -0
- package/dist/agent-executor/event-translation.js +3 -0
- package/dist/agent-executor/events.js +2 -1
- package/dist/agent-executor/gemini/tool-calls.d.ts +7 -0
- package/dist/agent-executor/gemini/tool-calls.js +9 -0
- package/dist/agent-executor/http-mcp.d.ts +23 -0
- package/dist/agent-executor/http-mcp.js +74 -0
- package/dist/agent-executor/launch.js +43 -11
- package/dist/agent-executor/mcp-bridge.d.ts +10 -2
- package/dist/agent-executor/mcp-bridge.js +2 -2
- package/dist/agent-executor/opencode/mcp-config.d.ts +10 -0
- package/dist/agent-executor/opencode/mcp-config.js +11 -0
- package/dist/agent-executor/opencode/tool-calls.d.ts +7 -0
- package/dist/agent-executor/opencode/tool-calls.js +12 -0
- package/dist/agent-executor/tool-call-receipts.d.ts +7 -0
- package/dist/agent-executor/tool-call-receipts.js +9 -0
- package/dist/core/read-only-tools.d.ts +2 -0
- package/dist/core/read-only-tools.js +19 -3
- package/dist/delegated-tool-bridge.js +6 -1
- package/dist/http/attachments.d.ts +2 -101
- package/dist/http/attachments.js +13 -1
- package/dist/http/connectors.js +3 -1
- package/dist/http/db-ops.js +3 -1
- package/dist/http/delegated-tools.d.ts +1 -1
- package/dist/http/delegated-tools.js +5 -3
- package/dist/http/frontend-control.d.ts +0 -36
- package/dist/http/frontend-control.js +39 -1
- package/dist/http/frontend-sessions.js +4 -2
- package/dist/http/index.d.ts +2 -1
- package/dist/http/index.js +1 -0
- package/dist/http/media.js +3 -1
- package/dist/http/model-proxy.js +4 -2
- package/dist/http/remote-run-events.js +3 -1
- package/dist/http/research.js +3 -2
- package/dist/http/runs.js +3 -1
- package/dist/http/surface-restoration.d.ts +17 -0
- package/dist/http/surface-restoration.js +50 -0
- package/dist/http/terminals.js +3 -1
- package/dist/http/tool-catalog.d.ts +15 -1
- package/dist/http/tool-execution-routes.d.ts +3 -1
- package/dist/http/tool-execution-routes.js +4 -3
- package/dist/http/xai.js +3 -2
- package/dist/idle-monitor.d.ts +29 -0
- package/dist/idle-monitor.js +64 -0
- package/dist/image-prompt-delivery.d.ts +1 -1
- package/dist/image-prompt-delivery.js +4 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/legacy-data-migration.js +3 -1
- package/dist/remote-attachment-lookup.d.ts +3 -1
- package/dist/remote-attachment-lookup.js +2 -2
- package/dist/routines/scheduler.js +6 -4
- package/dist/routing-tool-executor.d.ts +6 -1
- package/dist/routing-tool-executor.js +15 -2
- package/dist/run-lifecycle.d.ts +0 -16
- package/dist/run-lifecycle.js +24 -6
- package/dist/surface-exchanges.d.ts +5 -1
- package/dist/surface-exchanges.js +41 -7
- package/dist/surface-restoration.d.ts +38 -0
- package/dist/surface-restoration.js +152 -0
- package/dist/tool-recovery/index.d.ts +1 -0
- package/dist/tool-recovery/index.js +1 -0
- package/dist/tool-recovery/stack.js +2 -1
- package/dist/tool-recovery/surface-lifetime.d.ts +11 -0
- package/dist/tool-recovery/surface-lifetime.js +41 -0
- package/package.json +14 -14
package/dist/http/terminals.js
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* - Adapts `deps.manager.attach`'s push/end sink to `sse.ts`'s generic SSE
|
|
26
26
|
* channel, the same way `runs.ts` adapts `RunLifecycle.stream`.
|
|
27
27
|
*/
|
|
28
|
+
import { inspect } from 'node:util';
|
|
29
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
28
30
|
import { randomUUID } from 'node:crypto';
|
|
29
31
|
import { TERMINAL_CREATE_TOOL_ID } from '../index.js';
|
|
30
32
|
import { createApiError } from '@jini-ai/protocol';
|
|
@@ -38,7 +40,7 @@ import { isRecord, readOptionalNonEmptyString, readOptionalNumber, resolveResour
|
|
|
38
40
|
import { err, ok } from '@jini-ai/http-kit';
|
|
39
41
|
function defaultTerminalsInternalErrorSink(context) {
|
|
40
42
|
// eslint-disable-next-line no-console
|
|
41
|
-
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
43
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
42
44
|
}
|
|
43
45
|
/** Every one-off `terminal.create` tool call gets its own opaque `RunRef` — matching `db-ops.ts`'s `freshRun()`: these are single requests, not steps of a longer-lived agent run. */
|
|
44
46
|
function freshRun() {
|
|
@@ -36,6 +36,20 @@ export interface ToolCatalogEntry {
|
|
|
36
36
|
readonly description: string;
|
|
37
37
|
readonly inputSchema?: unknown;
|
|
38
38
|
readonly source: string;
|
|
39
|
+
/**
|
|
40
|
+
* Set only on guidance entries that are not tools themselves (a retired-alias pointer, a
|
|
41
|
+
* namespace overview): every tool id the guidance points at. A read-only view shows guidance
|
|
42
|
+
* only when each target is a registered read-only tool, and hides guidance without targets.
|
|
43
|
+
*/
|
|
44
|
+
readonly targetToolIds?: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
/** Optional describe inputs. */
|
|
47
|
+
export interface ToolCatalogDescribeOptions {
|
|
48
|
+
/**
|
|
49
|
+
* When set, the caller may use only these tool ids; guidance that lists tools (a namespace
|
|
50
|
+
* overview) should list only these. Never consulted for a registered tool's own entry.
|
|
51
|
+
*/
|
|
52
|
+
readonly visibleToolIds?: ReadonlySet<string>;
|
|
39
53
|
}
|
|
40
54
|
/** Structurally identical to `@jini-ai/sqlite-chat`'s `searchToolCatalog`/`getToolCatalogEntry` — defined locally so this package incurs no `better-sqlite3` dependency. */
|
|
41
55
|
export interface ToolCatalogQuery {
|
|
@@ -46,7 +60,7 @@ export interface ToolCatalogQuery {
|
|
|
46
60
|
}) => readonly ToolCatalogSearchHit[];
|
|
47
61
|
readonly describe: ({ id }: {
|
|
48
62
|
readonly id: string;
|
|
49
|
-
},
|
|
63
|
+
}, optional?: ToolCatalogDescribeOptions) => ToolCatalogEntry | null;
|
|
50
64
|
}
|
|
51
65
|
export interface ToolCatalogHttpDeps {
|
|
52
66
|
readonly catalog: ToolCatalogQuery;
|
|
@@ -57,6 +57,8 @@ export interface ToolExecutionRoutesOptional {
|
|
|
57
57
|
/** Defaults to {@link DEFAULT_TOOL_EXEC_MAX_BODY_BYTES}. */
|
|
58
58
|
readonly maxBodyBytes?: number;
|
|
59
59
|
readonly runWithContext?: RunWithToolContext;
|
|
60
|
+
/** Host-owned live-run/identity gate, after parsing and before either route dispatches. */
|
|
61
|
+
readonly authorizeRun?: RequestHandler;
|
|
60
62
|
}
|
|
61
63
|
/** The token + loopback gate; mounted before any body parser. Exported for hosts that need it on another path. */
|
|
62
64
|
export declare function requireToolExecutionCaller(_required: Record<string, never>, { tokenEnvVar, env }?: Pick<ToolExecutionRoutesOptional, 'tokenEnvVar' | 'env'>): RequestHandler;
|
|
@@ -65,5 +67,5 @@ export declare function requireToolExecutionCaller(_required: Record<string, nev
|
|
|
65
67
|
* `express.json()` with a smaller limit: this route parses its own body after its gate, and an
|
|
66
68
|
* earlier parser would both buffer unauthenticated bodies and cap tool inputs at its own limit.
|
|
67
69
|
*/
|
|
68
|
-
export declare function registerToolExecutionRoutes({ app, executor, listToolsFor }: ToolExecutionRoutesRequired, { tokenEnvVar, env, maxBodyBytes, runWithContext }?: ToolExecutionRoutesOptional): void;
|
|
70
|
+
export declare function registerToolExecutionRoutes({ app, executor, listToolsFor }: ToolExecutionRoutesRequired, { tokenEnvVar, env, maxBodyBytes, runWithContext, authorizeRun }?: ToolExecutionRoutesOptional): void;
|
|
69
71
|
//# sourceMappingURL=tool-execution-routes.d.ts.map
|
|
@@ -172,10 +172,11 @@ function catalogHandler({ listToolsFor }) {
|
|
|
172
172
|
* `express.json()` with a smaller limit: this route parses its own body after its gate, and an
|
|
173
173
|
* earlier parser would both buffer unauthenticated bodies and cap tool inputs at its own limit.
|
|
174
174
|
*/
|
|
175
|
-
export function registerToolExecutionRoutes({ app, executor, listToolsFor }, { tokenEnvVar, env, maxBodyBytes = DEFAULT_TOOL_EXEC_MAX_BODY_BYTES, runWithContext } = {}) {
|
|
175
|
+
export function registerToolExecutionRoutes({ app, executor, listToolsFor }, { tokenEnvVar, env, maxBodyBytes = DEFAULT_TOOL_EXEC_MAX_BODY_BYTES, runWithContext, authorizeRun } = {}) {
|
|
176
176
|
const gate = requireToolExecutionCaller({}, { ...(tokenEnvVar === undefined ? {} : { tokenEnvVar }), ...(env === undefined ? {} : { env }) });
|
|
177
177
|
const parser = express.json({ limit: maxBodyBytes });
|
|
178
|
-
|
|
179
|
-
app.
|
|
178
|
+
const runGate = authorizeRun ? [authorizeRun] : [];
|
|
179
|
+
app.post(TOOL_EXECUTIONS_ROUTE_PATH, gate, parser, ...runGate, executionHandler({ executor }, runWithContext === undefined ? {} : { runWithContext }), bodyParserFailure);
|
|
180
|
+
app.get(TOOL_CATALOG_ROUTE_PATH, gate, ...runGate, catalogHandler({ listToolsFor }));
|
|
180
181
|
}
|
|
181
182
|
//# sourceMappingURL=tool-execution-routes.js.map
|
package/dist/http/xai.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { redactSecrets } from '@jini-ai/core';
|
|
2
1
|
/**
|
|
3
2
|
* @module xai
|
|
4
3
|
*
|
|
@@ -117,6 +116,8 @@ import { redactSecrets } from '@jini-ai/core';
|
|
|
117
116
|
* `@jini-ai/agent-runtime`'s already-shipped generic module, outside this file's own scope to alter
|
|
118
117
|
* without special-casing one provider's capitalization inside otherwise-generic code.
|
|
119
118
|
*/
|
|
119
|
+
import { inspect } from 'node:util';
|
|
120
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
120
121
|
import { randomUUID } from 'node:crypto';
|
|
121
122
|
import { beginOAuthPkce, clearStoredOAuthToken, completeOAuthPkce, getStoredOAuthToken, PendingAuthCache, resolveOAuthBearer, setStoredOAuthToken, startOAuthCallbackListener, validateBaseUrl, XAI_OAUTH_PROVIDER_CONFIG, XAI_OAUTH_REDIRECT_HOST, XAI_OAUTH_REDIRECT_PATH, XAI_OAUTH_REDIRECT_PORT, } from '@jini-ai/agent-runtime';
|
|
122
123
|
import { createApiError } from '@jini-ai/protocol';
|
|
@@ -137,7 +138,7 @@ const DEFAULT_SEARCH_MODEL = 'grok-4.20-reasoning';
|
|
|
137
138
|
const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
|
|
138
139
|
function defaultInternalErrorSink(context) {
|
|
139
140
|
// eslint-disable-next-line no-console
|
|
140
|
-
console.error(`[@jini-ai/http-kit] internal error (xai/${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
141
|
+
console.error(`[@jini-ai/http-kit] internal error (xai/${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
141
142
|
}
|
|
142
143
|
/** Resolves every optional `XaiHttpDeps` field to a concrete value. Idempotent — calling this again on an already-resolved object (e.g. `registerXaiRoutes`'s own single resolution, passed through to each route's `handle`) is a harmless pass-through. See module doc on why stateful fields (`pending`/`listenerRef`) must be resolved once per registration, not once per request. */
|
|
143
144
|
function resolveXaiHttpDeps(deps) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Clock } from '@jini-ai/core/primitives';
|
|
2
|
+
import type { SchedulerPort } from './scheduler.js';
|
|
3
|
+
/** A one-shot observation lifecycle; stopping permanently cancels further observations. */
|
|
4
|
+
export interface IdleMonitor {
|
|
5
|
+
/** Idempotent; callbacks queued before cancellation become harmless. */
|
|
6
|
+
stop(): void;
|
|
7
|
+
/** Hold busy state through asynchronous work. Null means intake is fenced; releases are idempotent. */
|
|
8
|
+
acquire(required: Record<string, never>, optional?: Record<string, never>): (() => void) | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Observe host busy state immediately, scheduling polls at most 1s ahead; fire once after a full
|
|
12
|
+
* observed idle interval. Busy observations discard idle credit; the next idle
|
|
13
|
+
* observation starts a new interval. acquire() also records activity between observations.
|
|
14
|
+
* @param input Injected synchronous busy predicate, finite positive idleMs, clock and scheduler.
|
|
15
|
+
* @param options onIdle runs synchronously after the monitor has stopped; its errors propagate.
|
|
16
|
+
* @returns A permanent, idempotent stop handle. Owns at most one scheduled callback.
|
|
17
|
+
* @throws RangeError for a non-positive or non-finite idleMs, before scheduling.
|
|
18
|
+
* @complexity O(1) time per observation and O(1) retained space.
|
|
19
|
+
* @example createIdleMonitor({ isBusy, idleMs: 900_000, clock, scheduler }, { onIdle });
|
|
20
|
+
*/
|
|
21
|
+
export declare function createIdleMonitor({ isBusy, idleMs, clock, scheduler }: {
|
|
22
|
+
readonly isBusy: () => boolean;
|
|
23
|
+
readonly idleMs: number;
|
|
24
|
+
readonly clock: Clock;
|
|
25
|
+
readonly scheduler: SchedulerPort;
|
|
26
|
+
}, { onIdle }: {
|
|
27
|
+
readonly onIdle: () => void;
|
|
28
|
+
}): IdleMonitor;
|
|
29
|
+
//# sourceMappingURL=idle-monitor.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observe host busy state immediately, scheduling polls at most 1s ahead; fire once after a full
|
|
3
|
+
* observed idle interval. Busy observations discard idle credit; the next idle
|
|
4
|
+
* observation starts a new interval. acquire() also records activity between observations.
|
|
5
|
+
* @param input Injected synchronous busy predicate, finite positive idleMs, clock and scheduler.
|
|
6
|
+
* @param options onIdle runs synchronously after the monitor has stopped; its errors propagate.
|
|
7
|
+
* @returns A permanent, idempotent stop handle. Owns at most one scheduled callback.
|
|
8
|
+
* @throws RangeError for a non-positive or non-finite idleMs, before scheduling.
|
|
9
|
+
* @complexity O(1) time per observation and O(1) retained space.
|
|
10
|
+
* @example createIdleMonitor({ isBusy, idleMs: 900_000, clock, scheduler }, { onIdle });
|
|
11
|
+
*/
|
|
12
|
+
export function createIdleMonitor({ isBusy, idleMs, clock, scheduler }, { onIdle }) {
|
|
13
|
+
if (!Number.isFinite(idleMs) || idleMs <= 0) {
|
|
14
|
+
throw new RangeError('idleMs must be finite and positive');
|
|
15
|
+
}
|
|
16
|
+
let stopped = false;
|
|
17
|
+
let active = 0;
|
|
18
|
+
let idleSince;
|
|
19
|
+
let cancelPending;
|
|
20
|
+
const stop = () => {
|
|
21
|
+
stopped = true;
|
|
22
|
+
cancelPending?.();
|
|
23
|
+
cancelPending = undefined;
|
|
24
|
+
};
|
|
25
|
+
// The host owns parking; this callback only observes state and reports idle.
|
|
26
|
+
const observe = () => {
|
|
27
|
+
if (stopped)
|
|
28
|
+
return;
|
|
29
|
+
const now = clock.nowMs();
|
|
30
|
+
if (active > 0 || isBusy())
|
|
31
|
+
idleSince = undefined;
|
|
32
|
+
else
|
|
33
|
+
idleSince ??= now;
|
|
34
|
+
const remainingMs = idleSince === undefined ? idleMs : idleMs - (now - idleSince);
|
|
35
|
+
if (remainingMs <= 0) {
|
|
36
|
+
stop();
|
|
37
|
+
onIdle();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
cancelPending = scheduler.schedule({ delayMs: Math.min(1_000, remainingMs), callback: observe });
|
|
41
|
+
};
|
|
42
|
+
observe();
|
|
43
|
+
return {
|
|
44
|
+
stop,
|
|
45
|
+
acquire(_required, _optional = {}) {
|
|
46
|
+
if (stopped)
|
|
47
|
+
return null;
|
|
48
|
+
active++;
|
|
49
|
+
idleSince = undefined;
|
|
50
|
+
let released = false;
|
|
51
|
+
return () => {
|
|
52
|
+
if (released)
|
|
53
|
+
return;
|
|
54
|
+
released = true;
|
|
55
|
+
active--;
|
|
56
|
+
// A sub-poll request is still activity. Start its idle credit at completion, not at
|
|
57
|
+
// the previous observation, so frequent short calls cannot trigger a premature park.
|
|
58
|
+
if (active === 0)
|
|
59
|
+
idleSince = clock.nowMs();
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=idle-monitor.js.map
|
|
@@ -22,7 +22,7 @@ export interface ImagePromptDelivery {
|
|
|
22
22
|
export declare function augmentPromptWithImageAttachments({ prompt, imagePaths }: {
|
|
23
23
|
readonly prompt: string;
|
|
24
24
|
readonly imagePaths: readonly string[];
|
|
25
|
-
}): string;
|
|
25
|
+
}, _optional?: Record<string, never>): string;
|
|
26
26
|
/**
|
|
27
27
|
* Directories a 'prompt-path' def's CLI must be allowed to read from so it
|
|
28
28
|
* can actually open the files the augmented prompt just told it about — the
|
|
@@ -45,13 +45,14 @@ import path from 'node:path';
|
|
|
45
45
|
* @complexity O(n) in the number of images — one line rendered per path, no I/O.
|
|
46
46
|
* @overallScore 100
|
|
47
47
|
*/
|
|
48
|
-
export function augmentPromptWithImageAttachments({ prompt, imagePaths }) {
|
|
48
|
+
export function augmentPromptWithImageAttachments({ prompt, imagePaths }, _optional = {}) {
|
|
49
49
|
const list = imagePaths.map((imagePath, index) => `${index + 1}. ${imagePath}`).join('\n');
|
|
50
50
|
const plural = imagePaths.length === 1;
|
|
51
51
|
return (`${prompt}\n\n---\n` +
|
|
52
52
|
`The user attached the following image ${plural ? 'file' : 'files'} to this message. ` +
|
|
53
|
-
`${plural ? 'It is a' : 'These are'} real file${plural ? '' : 's'} on disk — read ${plural ? 'it' : 'each one'}
|
|
54
|
-
`
|
|
53
|
+
`${plural ? 'It is a' : 'These are'} real file${plural ? '' : 's'} on disk — read ${plural ? 'it' : 'each one'} ` +
|
|
54
|
+
`with the host attachment/image tool first, or your native Read/image tool for this supplied path. ` +
|
|
55
|
+
`Never use Bash, a shell command or a script to inspect attachments:\n${list}`);
|
|
55
56
|
}
|
|
56
57
|
/**
|
|
57
58
|
* Directories a 'prompt-path' def's CLI must be allowed to read from so it
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* connection opens, so it can't race a concurrent open of the destination.
|
|
17
17
|
*/
|
|
18
18
|
import * as nodeFilesystem from 'node:fs';
|
|
19
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
19
20
|
import * as path from 'node:path';
|
|
20
21
|
const DEFAULT_MARKER_FILE = '.migrated-from';
|
|
21
22
|
/**
|
|
@@ -254,7 +255,8 @@ function stagePromoteAndMark({ plan, markerFile, payloadEntries, writeMarker },
|
|
|
254
255
|
export function migrateLegacyDataDirSync(requiredArgs, optionalArgs = {}) {
|
|
255
256
|
const options = { ...requiredArgs, ...optionalArgs };
|
|
256
257
|
const fs = options.filesystem ?? nodeFilesystem;
|
|
257
|
-
|
|
258
|
+
// Paths and payload names are host input, so the default sink redacts the complete diagnostic.
|
|
259
|
+
const log = options.logger ?? { info: ({ message }) => console.log(redactSecrets({ input: `[jini-migrate] ${message}` }, { redactPii: false })) };
|
|
258
260
|
const markerFile = options.markerFile ?? DEFAULT_MARKER_FILE;
|
|
259
261
|
const plan = planMigration({ legacyDir: options.legacyDir, dataDir: options.dataDir, markerFile }, fs);
|
|
260
262
|
if ('status' in plan)
|
|
@@ -18,6 +18,8 @@ export interface RemoteAttachmentLookupRequired {
|
|
|
18
18
|
readonly token: string;
|
|
19
19
|
}
|
|
20
20
|
export interface RemoteAttachmentLookupOptional {
|
|
21
|
+
/** Explicit authenticated mount prefix. Omitted preserves the historical origin-root route. */
|
|
22
|
+
readonly pathPrefix?: string;
|
|
21
23
|
/** Injectable `node:http.request`. */
|
|
22
24
|
readonly request?: typeof http.request;
|
|
23
25
|
}
|
|
@@ -25,5 +27,5 @@ export interface RemoteAttachmentLookupOptional {
|
|
|
25
27
|
* Creates the remote lookup.
|
|
26
28
|
* @complexity One loopback round trip per call; O(response bytes).
|
|
27
29
|
*/
|
|
28
|
-
export declare function createRemoteAttachmentLookup({ baseUrl, token }: RemoteAttachmentLookupRequired, { request }?: RemoteAttachmentLookupOptional): AttachmentLookup;
|
|
30
|
+
export declare function createRemoteAttachmentLookup({ baseUrl, token }: RemoteAttachmentLookupRequired, { request, pathPrefix }?: RemoteAttachmentLookupOptional): AttachmentLookup;
|
|
29
31
|
//# sourceMappingURL=remote-attachment-lookup.d.ts.map
|
|
@@ -56,8 +56,8 @@ function unwrap({ status, body }) {
|
|
|
56
56
|
* Creates the remote lookup.
|
|
57
57
|
* @complexity One loopback round trip per call; O(response bytes).
|
|
58
58
|
*/
|
|
59
|
-
export function createRemoteAttachmentLookup({ baseUrl, token }, { request = http.request } = {}) {
|
|
60
|
-
const url = new URL(ATTACHMENT_LOOKUP_ROUTE_PATH
|
|
59
|
+
export function createRemoteAttachmentLookup({ baseUrl, token }, { request = http.request, pathPrefix = '' } = {}) {
|
|
60
|
+
const url = new URL(`${pathPrefix.replace(/\/$/, '')}${ATTACHMENT_LOOKUP_ROUTE_PATH}`, baseUrl);
|
|
61
61
|
const call = async (payload) => unwrap(await postJson({ url, token, payload }, { request }));
|
|
62
62
|
return {
|
|
63
63
|
resolveForRun: async ({ ref, runId }) => ((await call({ op: 'resolveForRun', ref, runId })) ?? undefined),
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* faithful port, not a redesign, per the proposal's own warning against "casually reinventing"
|
|
21
21
|
* the race-safe scheduled-slot claim below.
|
|
22
22
|
*/
|
|
23
|
+
import { inspect } from 'node:util';
|
|
24
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
23
25
|
import { randomUUID } from 'node:crypto';
|
|
24
26
|
import { nextRunAtForSchedule } from './schedule.js';
|
|
25
27
|
function clearRoutinePlaceholderId(value) {
|
|
@@ -43,10 +45,10 @@ export class ScheduledRunPersistenceError extends Error {
|
|
|
43
45
|
this.name = 'ScheduledRunPersistenceError';
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
|
-
/**
|
|
48
|
+
/** Preserve the underlying failure (including its code) for the redacted log diagnostic. */
|
|
47
49
|
function scheduledRunFailureDetail(error) {
|
|
48
50
|
const cause = error instanceof ScheduledRunPersistenceError ? error.originalError : error;
|
|
49
|
-
return cause
|
|
51
|
+
return cause;
|
|
50
52
|
}
|
|
51
53
|
function isScheduledRunPersistenceError(error) {
|
|
52
54
|
return error instanceof ScheduledRunPersistenceError;
|
|
@@ -141,7 +143,7 @@ export class RoutineService {
|
|
|
141
143
|
this.rescheduleOne({ routineId: routine.id });
|
|
142
144
|
})
|
|
143
145
|
.catch((error) => {
|
|
144
|
-
console.error(`[@jini-ai/daemon] routine ${routine.id} scheduled run failed:`, scheduledRunFailureDetail(error));
|
|
146
|
+
console.error(`[@jini-ai/daemon] routine ${routine.id} scheduled run failed:`, redactSecrets({ input: inspect(scheduledRunFailureDetail(error)) }, { redactPii: false }));
|
|
145
147
|
if (isScheduledRunPersistenceError(error)) {
|
|
146
148
|
this.retryScheduledSlot(routine.id, fireAt);
|
|
147
149
|
}
|
|
@@ -249,7 +251,7 @@ export class RoutineService {
|
|
|
249
251
|
discardError = err;
|
|
250
252
|
}
|
|
251
253
|
if (discardError != null) {
|
|
252
|
-
console.error(`[@jini-ai/daemon] routine ${attempt.routine.id} prepare cleanup failed:`,
|
|
254
|
+
console.error(`[@jini-ai/daemon] routine ${attempt.routine.id} prepare cleanup failed:`, redactSecrets({ input: inspect(discardError) }, { redactPii: false }));
|
|
253
255
|
}
|
|
254
256
|
run.projectId = clearRoutinePlaceholderId(run.projectId);
|
|
255
257
|
run.conversationId = clearRoutinePlaceholderId(run.conversationId);
|
|
@@ -23,7 +23,12 @@ export interface RoutingToolExecutorRequired {
|
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Creates the routing executor.
|
|
26
|
+
* @param required Local/remote executors and the owning registry's routing predicate.
|
|
27
|
+
* @param optional.acquireActivity Host busy lease, released on success or failure after real work.
|
|
28
|
+
* @returns One executor preserving the selected route's result/error and cancellation policy.
|
|
26
29
|
* @complexity O(1) plus `isLocal`'s own cost per call.
|
|
27
30
|
*/
|
|
28
|
-
export declare function createRoutingToolExecutor({ local, remote, isLocal }: RoutingToolExecutorRequired,
|
|
31
|
+
export declare function createRoutingToolExecutor({ local, remote, isLocal }: RoutingToolExecutorRequired, { acquireActivity }?: {
|
|
32
|
+
readonly acquireActivity?: () => () => void;
|
|
33
|
+
}): ToolExecutor;
|
|
29
34
|
//# sourceMappingURL=routing-tool-executor.d.ts.map
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Creates the routing executor.
|
|
3
|
+
* @param required Local/remote executors and the owning registry's routing predicate.
|
|
4
|
+
* @param optional.acquireActivity Host busy lease, released on success or failure after real work.
|
|
5
|
+
* @returns One executor preserving the selected route's result/error and cancellation policy.
|
|
3
6
|
* @complexity O(1) plus `isLocal`'s own cost per call.
|
|
4
7
|
*/
|
|
5
|
-
export function createRoutingToolExecutor({ local, remote, isLocal },
|
|
8
|
+
export function createRoutingToolExecutor({ local, remote, isLocal }, { acquireActivity } = {}) {
|
|
6
9
|
return {
|
|
7
|
-
execute
|
|
10
|
+
async execute(args, optional) {
|
|
11
|
+
// HTTP disconnects/cancel requests may precede actual completion. The embedding's busy
|
|
12
|
+
// lease follows the real executor promise, including its approval wait and cleanup.
|
|
13
|
+
const release = acquireActivity?.();
|
|
14
|
+
try {
|
|
15
|
+
return await (isLocal({ toolId: args.toolId }) ? local : remote).execute(args, optional);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
release?.();
|
|
19
|
+
}
|
|
20
|
+
},
|
|
8
21
|
resumeConfirmation: (args) => local.resumeConfirmation(args),
|
|
9
22
|
cancel: (args) => {
|
|
10
23
|
local.cancel(args);
|
package/dist/run-lifecycle.d.ts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `RunLifecycle` — start/stream/cancel/resume a run keyed on an opaque
|
|
3
|
-
* `contextRef` (extraction-plan §2.1: runs key on an opaque `contextRef`,
|
|
4
|
-
* never a product-record identifier), generalized from OD's `design.runs` service
|
|
5
|
-
* (`apps/daemon/src/runtimes/runs.ts` + the terminal-decision half of
|
|
6
|
-
* `apps/daemon/src/runtimes/start-chat-run.ts`, `arch/server-startserver-endgame`
|
|
7
|
-
* branch — see `archived provenance ledger` for exact line citations).
|
|
8
|
-
*
|
|
9
|
-
* Scope boundary: this module owns the run *state machine* and its event
|
|
10
|
-
* log — create, transition, emit, replay, cancellation-intent propagation,
|
|
11
|
-
* idempotent start/finish, resume. It does not spawn or signal a subprocess;
|
|
12
|
-
* that is `@jini-ai/agent-runtime`'s job (extraction-plan task 7). A driver
|
|
13
|
-
* (today: this package's tests; later: agent-runtime) calls `emit()` for
|
|
14
|
-
* agent/stdout/stderr/error events, observes cancellation via
|
|
15
|
-
* `onCancelRequested`, and calls `finish()` once it knows the real outcome.
|
|
16
|
-
*/
|
|
17
1
|
import type { DelegatedResultCorrelation } from './delegated-result-correlation.js';
|
|
18
2
|
import type { RunAgentPayload, RunCancelRequest, RunChunkPayload, RunErrorPayload, RunProtocolEvent, RunStartPayload, RunStatus } from '@jini-ai/protocol';
|
|
19
3
|
import type { EventLog, EventLogEntry, EventLogReplayResult } from './event-log.js';
|
package/dist/run-lifecycle.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RunLifecycle` — start/stream/cancel/resume a run keyed on an opaque
|
|
3
|
+
* `contextRef` (extraction-plan §2.1: runs key on an opaque `contextRef`,
|
|
4
|
+
* never a product-record identifier), generalized from OD's `design.runs` service
|
|
5
|
+
* (`apps/daemon/src/runtimes/runs.ts` + the terminal-decision half of
|
|
6
|
+
* `apps/daemon/src/runtimes/start-chat-run.ts`, `arch/server-startserver-endgame`
|
|
7
|
+
* branch — see `archived provenance ledger` for exact line citations).
|
|
8
|
+
*
|
|
9
|
+
* Scope boundary: this module owns the run *state machine* and its event
|
|
10
|
+
* log — create, transition, emit, replay, cancellation-intent propagation,
|
|
11
|
+
* idempotent start/finish, resume. It does not spawn or signal a subprocess;
|
|
12
|
+
* that is `@jini-ai/agent-runtime`'s job (extraction-plan task 7). A driver
|
|
13
|
+
* (today: this package's tests; later: agent-runtime) calls `emit()` for
|
|
14
|
+
* agent/stdout/stderr/error events, observes cancellation via
|
|
15
|
+
* `onCancelRequested`, and calls `finish()` once it knows the real outcome.
|
|
16
|
+
*/
|
|
17
|
+
import { inspect } from 'node:util';
|
|
18
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
1
19
|
import { randomUUID } from 'node:crypto';
|
|
2
20
|
import { RUN_PROTOCOL_VERSION, isTerminalRunState } from '@jini-ai/protocol';
|
|
3
21
|
import { createInactivityWatchdog } from './close-status.js';
|
|
@@ -455,11 +473,11 @@ export function createRunLifecycle(requiredArgs, optionalArgs = {}) {
|
|
|
455
473
|
input.onInternalError(context);
|
|
456
474
|
// eslint-disable-next-line no-console
|
|
457
475
|
else
|
|
458
|
-
console.error(`[@jini-ai/daemon] internal error (${context.source}, runId=${context.runId})`, context.error);
|
|
476
|
+
console.error(`[@jini-ai/daemon] internal error (${context.source}, runId=${context.runId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
459
477
|
}
|
|
460
478
|
catch (sinkError) {
|
|
461
479
|
// eslint-disable-next-line no-console
|
|
462
|
-
console.error(`[@jini-ai/daemon] internal error sink failed (${context.source}, runId=${context.runId})`, sinkError);
|
|
480
|
+
console.error(`[@jini-ai/daemon] internal error sink failed (${context.source}, runId=${context.runId})`, redactSecrets({ input: inspect(sinkError) }, { redactPii: false }));
|
|
463
481
|
}
|
|
464
482
|
}
|
|
465
483
|
/** Trims the oldest terminal records until `terminalRunOrder` is back at or under `maxTerminalRuns`. */
|
|
@@ -531,14 +549,14 @@ export function createRunLifecycle(requiredArgs, optionalArgs = {}) {
|
|
|
531
549
|
}
|
|
532
550
|
else {
|
|
533
551
|
// eslint-disable-next-line no-console
|
|
534
|
-
console.error(`[@jini-ai/daemon] internal error (inactivity-timeout, runId=${runId})`, error);
|
|
552
|
+
console.error(`[@jini-ai/daemon] internal error (inactivity-timeout, runId=${runId})`, redactSecrets({ input: inspect(error) }, { redactPii: false }));
|
|
535
553
|
}
|
|
536
554
|
}
|
|
537
555
|
catch (sinkError) {
|
|
538
556
|
// A diagnostic sink must not turn a contained timer failure into another unhandled
|
|
539
557
|
// rejection.
|
|
540
558
|
// eslint-disable-next-line no-console
|
|
541
|
-
console.error(`[@jini-ai/daemon] internal error sink failed (inactivity-timeout, runId=${runId})`, sinkError);
|
|
559
|
+
console.error(`[@jini-ai/daemon] internal error sink failed (inactivity-timeout, runId=${runId})`, redactSecrets({ input: inspect(sinkError) }, { redactPii: false }));
|
|
542
560
|
}
|
|
543
561
|
}
|
|
544
562
|
}
|
|
@@ -579,13 +597,13 @@ export function createRunLifecycle(requiredArgs, optionalArgs = {}) {
|
|
|
579
597
|
}
|
|
580
598
|
else {
|
|
581
599
|
// eslint-disable-next-line no-console
|
|
582
|
-
console.error(`[@jini-ai/daemon] internal error (slow-run-notice, runId=${runId})`, error);
|
|
600
|
+
console.error(`[@jini-ai/daemon] internal error (slow-run-notice, runId=${runId})`, redactSecrets({ input: inspect(error) }, { redactPii: false }));
|
|
583
601
|
}
|
|
584
602
|
}
|
|
585
603
|
catch (sinkError) {
|
|
586
604
|
// Same containment reasoning as `handleInactivityTimeout`'s own catch below it.
|
|
587
605
|
// eslint-disable-next-line no-console
|
|
588
|
-
console.error(`[@jini-ai/daemon] internal error sink failed (slow-run-notice, runId=${runId})`, sinkError);
|
|
606
|
+
console.error(`[@jini-ai/daemon] internal error sink failed (slow-run-notice, runId=${runId})`, redactSecrets({ input: inspect(sinkError) }, { redactPii: false }));
|
|
589
607
|
}
|
|
590
608
|
}
|
|
591
609
|
}
|
|
@@ -68,6 +68,8 @@ export type { SchedulerPort } from './scheduler.js';
|
|
|
68
68
|
export declare const DEFAULT_SURFACE_IDLE_TTL_MS: number;
|
|
69
69
|
/**
|
|
70
70
|
* Hard ceiling on one exchange's total life, regardless of activity.
|
|
71
|
+
* Applies only without an executor lifetime. The historical paired-transport rationale below
|
|
72
|
+
* describes unleased callers; run-bound cards ignore both deadlines and end with their call.
|
|
71
73
|
*
|
|
72
74
|
* Exists because the idle deadline alone is unbounded in aggregate — a human answering every four
|
|
73
75
|
* minutes could hold a call open indefinitely, and the call is not ours to hold: it is an HTTP
|
|
@@ -92,7 +94,9 @@ export declare const DEFAULT_SURFACE_MAX_LIFETIME_MS: number;
|
|
|
92
94
|
* @param deps.idGenerator - Id source, injected so a test can assert a known value.
|
|
93
95
|
* @param deps.clock - Wall clock for {@link SurfaceExchange.expiresAtMs}. The injected scheduler owns
|
|
94
96
|
* the timers; the clock dates and checks their deadlines.
|
|
95
|
-
* @complexity O(1) per
|
|
97
|
+
* @complexity O(1) per receive, O(n) per accepted answer in active/retained exchanges, and O(t)
|
|
98
|
+
* per send to t observing tabs. Closed recovery entries are capped at 128. Unleased exchanges own
|
|
99
|
+
* two timers; run-bound exchanges own one cancellation subscription and no deadline timers.
|
|
96
100
|
*/
|
|
97
101
|
export declare function createSurfaceExchangeStore(deps: {
|
|
98
102
|
scheduler: SchedulerPort;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { SURFACE_EXCHANGE_ID_PARAM, SURFACE_DISMISSED_PARAM, SURFACE_TYPED_ANSWER_PARAM, } from '@jini-ai/core';
|
|
2
|
+
import { createSurfaceRestoration } from './surface-restoration.js';
|
|
2
3
|
/**
|
|
3
4
|
* How long an exchange may sit with nothing happening before it gives up.
|
|
4
5
|
*
|
|
@@ -10,6 +11,8 @@ export { SURFACE_EXCHANGE_ID_PARAM, SURFACE_DISMISSED_PARAM, SURFACE_TYPED_ANSWE
|
|
|
10
11
|
export const DEFAULT_SURFACE_IDLE_TTL_MS = 5 * 60 * 1000;
|
|
11
12
|
/**
|
|
12
13
|
* Hard ceiling on one exchange's total life, regardless of activity.
|
|
14
|
+
* Applies only without an executor lifetime. The historical paired-transport rationale below
|
|
15
|
+
* describes unleased callers; run-bound cards ignore both deadlines and end with their call.
|
|
13
16
|
*
|
|
14
17
|
* Exists because the idle deadline alone is unbounded in aggregate — a human answering every four
|
|
15
18
|
* minutes could hold a call open indefinitely, and the call is not ours to hold: it is an HTTP
|
|
@@ -29,6 +32,12 @@ class SurfaceExchangeEndedError extends Error {
|
|
|
29
32
|
}
|
|
30
33
|
terminal;
|
|
31
34
|
}
|
|
35
|
+
/** Run-bound cards use cancellation instead of a date; other callers retain both timed budgets. */
|
|
36
|
+
function initialSurfaceDeadlines({ leased, nowMs, idleTtlMs, maxLifetimeMs }) {
|
|
37
|
+
if (leased)
|
|
38
|
+
return { idle: Number.POSITIVE_INFINITY, lifetime: Number.POSITIVE_INFINITY };
|
|
39
|
+
return { idle: nowMs + idleTtlMs, lifetime: nowMs + maxLifetimeMs };
|
|
40
|
+
}
|
|
32
41
|
/**
|
|
33
42
|
* Creates an in-process store of open surface exchanges.
|
|
34
43
|
*
|
|
@@ -42,13 +51,18 @@ class SurfaceExchangeEndedError extends Error {
|
|
|
42
51
|
* @param deps.idGenerator - Id source, injected so a test can assert a known value.
|
|
43
52
|
* @param deps.clock - Wall clock for {@link SurfaceExchange.expiresAtMs}. The injected scheduler owns
|
|
44
53
|
* the timers; the clock dates and checks their deadlines.
|
|
45
|
-
* @complexity O(1) per
|
|
54
|
+
* @complexity O(1) per receive, O(n) per accepted answer in active/retained exchanges, and O(t)
|
|
55
|
+
* per send to t observing tabs. Closed recovery entries are capped at 128. Unleased exchanges own
|
|
56
|
+
* two timers; run-bound exchanges own one cancellation subscription and no deadline timers.
|
|
46
57
|
*/
|
|
47
58
|
export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_IDLE_TTL_MS, maxLifetimeMs = DEFAULT_SURFACE_MAX_LIFETIME_MS } = {}) {
|
|
48
59
|
if (!Number.isFinite(idleTtlMs) || idleTtlMs <= 0 || !Number.isFinite(maxLifetimeMs) || maxLifetimeMs <= 0)
|
|
49
60
|
throw new Error('surface deadlines must be finite and positive');
|
|
50
61
|
const openExchanges = new Map();
|
|
51
|
-
|
|
62
|
+
const restoration = createSurfaceRestoration({ clock: deps.clock, defaultChannel: deps.defaultChannel,
|
|
63
|
+
deliver: (required, optional) => store.deliver(required, optional) });
|
|
64
|
+
const store = {
|
|
65
|
+
restore: restoration.restore,
|
|
52
66
|
open({ binding, emit }, _optional = {}) {
|
|
53
67
|
// Waiting without a live emitter is a deadlock; reject before allocating any timers or ID.
|
|
54
68
|
if (typeof emit !== 'function')
|
|
@@ -56,13 +70,20 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
56
70
|
const id = deps.idGenerator.newId();
|
|
57
71
|
if (!id || openExchanges.has(id))
|
|
58
72
|
throw new Error('surface exchange ID must be unique and nonempty');
|
|
73
|
+
const lifetime = emit.exchangeLifetime;
|
|
74
|
+
// Only the executor can supply this lease. Plain emitters (including bounded visitor
|
|
75
|
+
// confirmations) retain their existing deadlines. No card/tool needs its own TTL override.
|
|
76
|
+
const leased = lifetime !== undefined && lifetime.principalId === binding.principalId;
|
|
77
|
+
const recovery = leased ? restoration.track({ id, binding, lifetime }) : undefined;
|
|
59
78
|
const inbox = [];
|
|
60
79
|
const waiters = [];
|
|
61
80
|
let terminal;
|
|
62
81
|
let cancelIdle;
|
|
63
82
|
let cancelLifetime;
|
|
64
|
-
const
|
|
65
|
-
|
|
83
|
+
const deadlines = initialSurfaceDeadlines({ leased, nowMs: deps.clock.nowMs(), idleTtlMs, maxLifetimeMs });
|
|
84
|
+
const lifetimeDeadline = deadlines.lifetime;
|
|
85
|
+
let idleDeadline = deadlines.idle;
|
|
86
|
+
const closeOnAbort = () => end({ status: 'abandoned' });
|
|
66
87
|
function end(reason) {
|
|
67
88
|
if (terminal)
|
|
68
89
|
return;
|
|
@@ -70,6 +91,8 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
70
91
|
cancelIdle?.();
|
|
71
92
|
cancelLifetime?.();
|
|
72
93
|
openExchanges.delete(id);
|
|
94
|
+
lifetime?.signal.removeEventListener('abort', closeOnAbort);
|
|
95
|
+
recovery?.end();
|
|
73
96
|
while (waiters.length > 0)
|
|
74
97
|
waiters.shift()(reason);
|
|
75
98
|
}
|
|
@@ -79,7 +102,7 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
79
102
|
end({ status: 'expired' });
|
|
80
103
|
}
|
|
81
104
|
function armIdle() {
|
|
82
|
-
if (terminal)
|
|
105
|
+
if (terminal || leased)
|
|
83
106
|
return;
|
|
84
107
|
cancelIdle?.();
|
|
85
108
|
idleDeadline = deps.clock.nowMs() + idleTtlMs;
|
|
@@ -87,7 +110,8 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
87
110
|
}
|
|
88
111
|
// Register only after both timers are installed, so scheduler failure leaves no discoverable exchange.
|
|
89
112
|
try {
|
|
90
|
-
|
|
113
|
+
if (!leased)
|
|
114
|
+
cancelLifetime = deps.scheduler.schedule({ delayMs: maxLifetimeMs, callback: () => end({ status: 'expired' }) });
|
|
91
115
|
armIdle();
|
|
92
116
|
}
|
|
93
117
|
catch (error) {
|
|
@@ -102,6 +126,7 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
102
126
|
expireIfDue();
|
|
103
127
|
if (terminal)
|
|
104
128
|
return;
|
|
129
|
+
recovery?.answered();
|
|
105
130
|
armIdle();
|
|
106
131
|
const waiter = waiters.shift();
|
|
107
132
|
if (waiter)
|
|
@@ -112,6 +137,11 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
112
137
|
inbox.push(params);
|
|
113
138
|
},
|
|
114
139
|
});
|
|
140
|
+
if (leased) {
|
|
141
|
+
lifetime.signal.addEventListener('abort', closeOnAbort, { once: true });
|
|
142
|
+
if (lifetime.signal.aborted)
|
|
143
|
+
closeOnAbort();
|
|
144
|
+
}
|
|
115
145
|
return {
|
|
116
146
|
id,
|
|
117
147
|
async send({ emission }, _optional = {}) {
|
|
@@ -119,7 +149,10 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
119
149
|
if (terminal)
|
|
120
150
|
throw new SurfaceExchangeEndedError({ id, terminal });
|
|
121
151
|
armIdle();
|
|
122
|
-
|
|
152
|
+
const sent = recovery === undefined ? emission : { ...emission, payload: { ...emission.payload,
|
|
153
|
+
surfaceExchange: { exchangeId: id, toolId: binding.toolId, runId: lifetime.runId } } };
|
|
154
|
+
await recovery?.send({ emission: sent });
|
|
155
|
+
await emit(sent);
|
|
123
156
|
},
|
|
124
157
|
async receive(_args, _optional = {}) {
|
|
125
158
|
expireIfDue();
|
|
@@ -166,6 +199,7 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
|
|
|
166
199
|
},
|
|
167
200
|
size: () => openExchanges.size,
|
|
168
201
|
};
|
|
202
|
+
return store;
|
|
169
203
|
}
|
|
170
204
|
/**
|
|
171
205
|
* The one-shot case: send once, wait once, close.
|