@jini-ai/daemon 0.5.1 → 0.5.3
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 +11 -0
- package/dist/agent-executor/contracts.d.ts +14 -10
- package/dist/agent-executor/event-translation.js +1 -0
- package/dist/agent-executor/events.js +8 -2
- package/dist/agent-executor/launch.js +2 -0
- package/dist/agent-executor.d.ts +4 -1
- package/dist/agent-executor.js +4 -1
- package/dist/attachment-content.d.ts +45 -0
- package/dist/attachment-content.js +40 -0
- package/dist/delegated-tool-bridge.js +12 -0
- package/dist/http/attachments.d.ts +16 -7
- package/dist/http/attachments.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/run-lifecycle.d.ts +2 -1
- package/dist/run-lifecycle.js +17 -6
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 0.5.3 — 2026-10-06
|
|
2
|
+
|
|
3
|
+
- Message attachments reach every runtime: new `prepareMessageAttachments` / `messageContentWithImages` (+ `MessageAttachmentImage`, `MessageAttachmentSource`, `MessageAttachmentReader`) turn claimed refs into image pixels for providers and a file notice for non-images.
|
|
4
|
+
- `AttachmentStore.listPendingForOwner({ ownerId }, { runId })`: with the claiming `runId`, that run's own claimed attachments stay discoverable (owner match still required); without it, behavior is unchanged.
|
|
5
|
+
- The agent executor reports the CLI session id on an early `status` event so hosts can checkpoint a run before it ends; run lifecycle carries it through.
|
|
6
|
+
- Requires `@jini-ai/protocol` ^0.4.1 and `@jini-ai/agent-runtime` ^0.4.1.
|
|
7
|
+
|
|
8
|
+
## 0.5.2 — 2026-10-06
|
|
9
|
+
|
|
10
|
+
- No slow-run notice while a delegated tool waits on a surface: the first `emitSurface` of a call suspends the notice and the call's end resumes it. Suspensions are counted, so with two calls in flight the first to finish does not re-arm the notice over the other's open form; `resume()` drops suspensions left by an ended run.
|
|
11
|
+
|
|
1
12
|
## 0.5.1 — 2026-10-05
|
|
2
13
|
|
|
3
14
|
- The `@jini-ai/db` peer range is `^0.2.0 || ^0.3.0` (daemon uses only db's kernel and store entries, which 0.3.0 leaves unchanged), so a host on db 0.3.0 installs without a peer conflict.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MessageAttachmentImage } from '../attachment-content.js';
|
|
1
2
|
import { type spawn as nodeSpawn, type ChildProcess } from 'node:child_process';
|
|
2
3
|
import { type Principal } from '@jini-ai/core';
|
|
3
4
|
import { type RunAgentPayload, type RunErrorPayload } from '@jini-ai/protocol';
|
|
@@ -55,15 +56,13 @@ export type AgentExecutorCompatibility = {
|
|
|
55
56
|
* produce plus defensively malformed/non-record input.
|
|
56
57
|
*
|
|
57
58
|
* `'agent'`'s optional `sessionId` (gap 5, session resume — see
|
|
58
|
-
* `RunEndPayload.sessionRef`'s doc in `@jini-ai/protocol`)
|
|
59
|
-
* side channel
|
|
60
|
-
*
|
|
61
|
-
* `
|
|
62
|
-
*
|
|
63
|
-
* `
|
|
64
|
-
*
|
|
65
|
-
* thread it into its own terminal `finish()` call, without widening the
|
|
66
|
-
* public wire protocol just to carry a value that only this module reads.
|
|
59
|
+
* `RunEndPayload.sessionRef`'s doc in `@jini-ai/protocol`) remains a daemon-internal
|
|
60
|
+
* side channel for lifecycle wiring to capture the locator and thread it into
|
|
61
|
+
* its terminal `finish()` call. The `'status'` wire payload also carries
|
|
62
|
+
* `sessionId` so durable hosts can persist it immediately, before a process
|
|
63
|
+
* crash can prevent the terminal `end` event from arriving. OpenCode's
|
|
64
|
+
* `sessionID`/Codex's `thread_id`/Qoder's and Claude's `session_id` are normalized
|
|
65
|
+
* by their stream parsers into this shared locator.
|
|
67
66
|
*
|
|
68
67
|
* `'turn-end'`'s optional `stopReason` (gap 3, capability-routed
|
|
69
68
|
* continuation transport) is the same kind of internal side channel: the
|
|
@@ -129,6 +128,8 @@ export interface AgentExecutorRunInput {
|
|
|
129
128
|
* the top of `run()` in `launch.ts`.
|
|
130
129
|
*/
|
|
131
130
|
readonly imagePaths?: readonly string[];
|
|
131
|
+
/** Host-prepared pixels from the shared attachment reader, for structured message delivery. */
|
|
132
|
+
readonly imageContents?: readonly MessageAttachmentImage[];
|
|
132
133
|
/** Additional host-validated directories the runtime may read. */
|
|
133
134
|
readonly extraAllowedDirs?: readonly string[];
|
|
134
135
|
/** Trusted root that must contain pi-rpc image paths after realpath resolution. */
|
|
@@ -192,7 +193,7 @@ export interface AgentExecutor {
|
|
|
192
193
|
* underlying run is always already transitioned to `'failed'` via
|
|
193
194
|
* `lifecycle.finish()` before this rejects (see index.ts module doc's Invariant).
|
|
194
195
|
*/
|
|
195
|
-
run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "permissionMode" | "imagePaths" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings">): Promise<void>;
|
|
196
|
+
run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings">): Promise<void>;
|
|
196
197
|
}
|
|
197
198
|
/** Process discovery is a getter; process collection and stopping accept explicit host inputs. */
|
|
198
199
|
export type CollectProcessTreePidsPort = (args: {
|
|
@@ -230,6 +231,9 @@ export interface AgentCleanupFailureContext {
|
|
|
230
231
|
}
|
|
231
232
|
/** A small handle `writePromptToStdin` uses to close stdin exactly once, shared with the `turn_end`-triggered close inside {@link wireChildLifecycle}. */
|
|
232
233
|
export interface StdinCloseHandle {
|
|
234
|
+
/** Prepared pixels ride on the initial stdin message without reopening filesystem tools or
|
|
235
|
+
* serializing base64 into text. Native argv/ACP/pi transports keep their existing image path. */
|
|
236
|
+
imageContents?: readonly MessageAttachmentImage[];
|
|
233
237
|
closeStdinOnce(): void;
|
|
234
238
|
/** Journals a sent-to-stdin byte chunk, queued through the same FIFO {@link wireChildLifecycle} already uses for emitted events. No-op when no journal was configured (see `CreateAgentExecutorOptions.journal`). */
|
|
235
239
|
recordSentBytes(content: string): void;
|
|
@@ -62,6 +62,7 @@ export function translateStatusEvent({ rawEvent }) {
|
|
|
62
62
|
...(model !== undefined ? { model } : {}),
|
|
63
63
|
...(ttftMs !== undefined ? { ttftMs } : {}),
|
|
64
64
|
...(detail !== undefined ? { detail } : {}),
|
|
65
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
65
66
|
},
|
|
66
67
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
67
68
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { messageContentWithImages } from '../attachment-content.js';
|
|
1
2
|
import {} from 'node:child_process';
|
|
2
3
|
import { redactSecrets, } from '@jini-ai/core';
|
|
3
4
|
import {} from '@jini-ai/protocol';
|
|
@@ -13,6 +14,11 @@ import {} from './contracts.js';
|
|
|
13
14
|
import { translateAgentRuntimeEvent, translateAcpError, applyAgentTranslationSideEffects, } from './event-translation.js';
|
|
14
15
|
import { errorMessage, } from './values.js';
|
|
15
16
|
import { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, bridgeUnavailableMessage, } from './mcp-bridge.js';
|
|
17
|
+
/** Carry PID only on structured status so the host can record its OS start identity early.
|
|
18
|
+
* A PID alone is never proof of death; hosts verify PID reuse before native continuation. */
|
|
19
|
+
function statusWithChildIdentity({ payload, childPid }, _optional = {}) {
|
|
20
|
+
return { ...payload, ...(payload.type === 'status' && childPid !== undefined ? { childPid } : {}) };
|
|
21
|
+
}
|
|
16
22
|
/**
|
|
17
23
|
* Selects and constructs the real stream-parser handler for a supported
|
|
18
24
|
* `streamFormat`. `json-event-stream` additionally dispatches on
|
|
@@ -411,7 +417,7 @@ export function wireChildLifecycle(ctx) {
|
|
|
411
417
|
translation.payload.delta.length > 0) {
|
|
412
418
|
userVisibleOutputSeen = true;
|
|
413
419
|
}
|
|
414
|
-
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: translation.payload } }));
|
|
420
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: statusWithChildIdentity({ payload: translation.payload, childPid: ctx.child.pid }, {}) } }));
|
|
415
421
|
}
|
|
416
422
|
else if (translation.kind === 'error') {
|
|
417
423
|
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
|
|
@@ -769,7 +775,7 @@ export function writePromptToStdin(def, child, prompt, handle) {
|
|
|
769
775
|
if (!stdin)
|
|
770
776
|
return;
|
|
771
777
|
if (def.promptInputFormat === 'stream-json') {
|
|
772
|
-
const line = JSON.stringify({ type: 'user', message: { role: 'user', content:
|
|
778
|
+
const line = JSON.stringify({ type: 'user', message: { role: 'user', content: messageContentWithImages({ prompt, images: handle.imageContents ?? [] }, {}) } });
|
|
773
779
|
stdin.write(`${line}\n`, 'utf8');
|
|
774
780
|
handle.recordSentBytes(prompt);
|
|
775
781
|
return;
|
|
@@ -994,6 +994,8 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
994
994
|
// fallback strategy, so their prefix is genuinely non-empty; what makes stdin the wrong
|
|
995
995
|
// channel for them is the def's declared transport, not the strategy.
|
|
996
996
|
const stdinPrompt = def.promptViaStdin === true ? overlayDelivery.prompt : imageDelivery.prompt;
|
|
997
|
+
if (input.imageContents !== undefined)
|
|
998
|
+
stdinHandle.imageContents = input.imageContents;
|
|
997
999
|
writePromptToStdin(def, child, stdinPrompt, stdinHandle);
|
|
998
1000
|
}
|
|
999
1001
|
return { run };
|
package/dist/agent-executor.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MessageAttachmentImage } from './attachment-content.js';
|
|
1
2
|
/**
|
|
2
3
|
* `AgentExecutor` — the driver `RunLifecycle`'s own module doc names as the
|
|
3
4
|
* missing piece: *"It does not spawn or signal a subprocess... A driver...
|
|
@@ -312,6 +313,8 @@ export interface AgentExecutorRunInput {
|
|
|
312
313
|
* the top of `run()` below.
|
|
313
314
|
*/
|
|
314
315
|
readonly imagePaths?: readonly string[];
|
|
316
|
+
/** Host-prepared pixels from the shared attachment reader, for structured message delivery. */
|
|
317
|
+
readonly imageContents?: readonly MessageAttachmentImage[];
|
|
315
318
|
/** Additional host-validated directories the runtime may read. */
|
|
316
319
|
readonly extraAllowedDirs?: readonly string[];
|
|
317
320
|
/** Trusted root that must contain pi-rpc image paths after realpath resolution. */
|
|
@@ -375,7 +378,7 @@ export interface AgentExecutor {
|
|
|
375
378
|
* underlying run is always already transitioned to `'failed'` via
|
|
376
379
|
* `lifecycle.finish()` before this rejects (see module doc's Invariant).
|
|
377
380
|
*/
|
|
378
|
-
run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "permissionMode" | "imagePaths" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings">): Promise<void>;
|
|
381
|
+
run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings">): Promise<void>;
|
|
379
382
|
}
|
|
380
383
|
/** Process discovery is a getter; process collection and stopping accept explicit host inputs. */
|
|
381
384
|
export type CollectProcessTreePidsPort = (args: {
|
package/dist/agent-executor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { messageContentWithImages } from './attachment-content.js';
|
|
1
2
|
/**
|
|
2
3
|
* `AgentExecutor` — the driver `RunLifecycle`'s own module doc names as the
|
|
3
4
|
* missing piece: *"It does not spawn or signal a subprocess... A driver...
|
|
@@ -1985,7 +1986,7 @@ function writePromptToStdin(def, child, prompt, handle) {
|
|
|
1985
1986
|
if (!stdin)
|
|
1986
1987
|
return;
|
|
1987
1988
|
if (def.promptInputFormat === 'stream-json') {
|
|
1988
|
-
const line = JSON.stringify({ type: 'user', message: { role: 'user', content:
|
|
1989
|
+
const line = JSON.stringify({ type: 'user', message: { role: 'user', content: messageContentWithImages({ prompt, images: handle.imageContents ?? [] }, {}) } });
|
|
1989
1990
|
stdin.write(`${line}\n`, 'utf8');
|
|
1990
1991
|
handle.recordSentBytes(prompt);
|
|
1991
1992
|
return;
|
|
@@ -3000,6 +3001,8 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
3000
3001
|
// fallback strategy, so their prefix is genuinely non-empty; what makes stdin the wrong
|
|
3001
3002
|
// channel for them is the def's declared transport, not the strategy.
|
|
3002
3003
|
const stdinPrompt = def.promptViaStdin === true ? overlayDelivery.prompt : imageDelivery.prompt;
|
|
3004
|
+
if (input.imageContents !== undefined)
|
|
3005
|
+
stdinHandle.imageContents = input.imageContents;
|
|
3003
3006
|
writePromptToStdin(def, child, stdinPrompt, stdinHandle);
|
|
3004
3007
|
}
|
|
3005
3008
|
return { run };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface MessageAttachmentImage {
|
|
2
|
+
readonly mimeType: string;
|
|
3
|
+
readonly data: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MessageAttachmentSource {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly mimeType: string;
|
|
8
|
+
readonly bytes: Uint8Array;
|
|
9
|
+
readonly path?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface MessageAttachmentReader {
|
|
12
|
+
(required: {
|
|
13
|
+
ref: string;
|
|
14
|
+
}, optional: Record<string, never>): Promise<MessageAttachmentSource>;
|
|
15
|
+
}
|
|
16
|
+
/** One byte-bearing message path for CLI and API consumers. The host reader owns authorization,
|
|
17
|
+
* integrity and MIME sniffing; this module never opens a caller-selected path itself. Unsupported
|
|
18
|
+
* files are explicit so the model cannot mistake a filename for content it has received. */
|
|
19
|
+
export declare function prepareMessageAttachments({ refs, read }: {
|
|
20
|
+
refs: readonly string[];
|
|
21
|
+
read: MessageAttachmentReader;
|
|
22
|
+
}, { fileAccess, imageAccess }?: {
|
|
23
|
+
fileAccess?: boolean;
|
|
24
|
+
imageAccess?: boolean;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
images: MessageAttachmentImage[];
|
|
27
|
+
imagePaths: string[];
|
|
28
|
+
notice: string;
|
|
29
|
+
}>;
|
|
30
|
+
/** Stream-JSON runtimes use the same normalized pixels as provider APIs. */
|
|
31
|
+
export declare function messageContentWithImages({ prompt, images }: {
|
|
32
|
+
prompt: string;
|
|
33
|
+
images: readonly MessageAttachmentImage[];
|
|
34
|
+
}, _optional?: {}): ({
|
|
35
|
+
type: "image";
|
|
36
|
+
source: {
|
|
37
|
+
type: "base64";
|
|
38
|
+
media_type: string;
|
|
39
|
+
data: string;
|
|
40
|
+
};
|
|
41
|
+
} | {
|
|
42
|
+
type: "text";
|
|
43
|
+
text: string;
|
|
44
|
+
})[];
|
|
45
|
+
//# sourceMappingURL=attachment-content.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
/** One byte-bearing message path for CLI and API consumers. The host reader owns authorization,
|
|
3
|
+
* integrity and MIME sniffing; this module never opens a caller-selected path itself. Unsupported
|
|
4
|
+
* files are explicit so the model cannot mistake a filename for content it has received. */
|
|
5
|
+
export async function prepareMessageAttachments({ refs, read }, { fileAccess = false, imageAccess = true } = {}) {
|
|
6
|
+
const images = [];
|
|
7
|
+
const notices = [];
|
|
8
|
+
const imagePaths = [];
|
|
9
|
+
for (const ref of refs) {
|
|
10
|
+
const source = await read({ ref }, {});
|
|
11
|
+
if (['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(source.mimeType)) {
|
|
12
|
+
if (!imageAccess) {
|
|
13
|
+
notices.push(`${source.name}: image attachments are unsupported by this runtime.`);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (source.path)
|
|
17
|
+
imagePaths.push(source.path);
|
|
18
|
+
images.push({ mimeType: source.mimeType, data: Buffer.from(source.bytes).toString('base64') });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const video = source.mimeType.startsWith('video/');
|
|
22
|
+
if (fileAccess && source.path) {
|
|
23
|
+
notices.push(video
|
|
24
|
+
? `${source.name}: video attachments are unsupported by this runtime; the file is available at ${source.path}.`
|
|
25
|
+
: `${source.name}: read the attached file at ${source.path}.`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
notices.push(`${source.name}: ${video ? 'video' : 'file'} attachments are unsupported by this runtime.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { images, imagePaths, notice: notices.length ? `\n\nAttachments to this message:\n${notices.join('\n')}` : '' };
|
|
32
|
+
}
|
|
33
|
+
/** Stream-JSON runtimes use the same normalized pixels as provider APIs. */
|
|
34
|
+
export function messageContentWithImages({ prompt, images }, _optional = {}) {
|
|
35
|
+
return [
|
|
36
|
+
{ type: 'text', text: prompt },
|
|
37
|
+
...images.map(image => ({ type: 'image', source: { type: 'base64', media_type: image.mimeType, data: image.data } })),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=attachment-content.js.map
|
|
@@ -109,9 +109,19 @@ export function createDelegatedToolBridge(options) {
|
|
|
109
109
|
* screen, with no call left to answer it.
|
|
110
110
|
*/
|
|
111
111
|
let settled = false;
|
|
112
|
+
// A handler emits a surface mid-call because it is about to WAIT on the human (a form, a card).
|
|
113
|
+
// Nothing reaches the run's event stream while they answer, so the slow-run watchdog would read
|
|
114
|
+
// that silence as a stall and post "Still working…" over an open form — which then reappears
|
|
115
|
+
// once the form is answered. Suspended from the first surface until the call settles (the
|
|
116
|
+
// `finally` below); the watchdog re-arms then, so a genuine stall afterwards is still caught.
|
|
117
|
+
let slowRunSuspended = false;
|
|
112
118
|
const emitSurface = async (emission) => {
|
|
113
119
|
if (settled)
|
|
114
120
|
throw new Error(`emitSurface: tool call ${toolUseId} has already completed`);
|
|
121
|
+
if (!slowRunSuspended) {
|
|
122
|
+
slowRunSuspended = true;
|
|
123
|
+
lifecycle.suspendSlowRunNotice({ runId: runId });
|
|
124
|
+
}
|
|
115
125
|
// Cast because `RunAgentPayload` is a closed union and `channel` is deliberately an open
|
|
116
126
|
// string — a seam that only accepts channels this file already knows about is not a
|
|
117
127
|
// channel-neutral seam. This mirrors `@jini-ai/protocol`'s own posture of typing each
|
|
@@ -202,6 +212,8 @@ export function createDelegatedToolBridge(options) {
|
|
|
202
212
|
finally {
|
|
203
213
|
unsubscribeCancel();
|
|
204
214
|
invocation.signal?.removeEventListener('abort', abortFromTransport);
|
|
215
|
+
if (slowRunSuspended)
|
|
216
|
+
lifecycle.resumeSlowRunNotice({ runId: runId });
|
|
205
217
|
}
|
|
206
218
|
}
|
|
207
219
|
return { execute };
|
|
@@ -147,7 +147,8 @@ export interface AttachmentStore {
|
|
|
147
147
|
readonly runId: string;
|
|
148
148
|
}, _optional?: Record<string, never>) => Promise<StoredAttachment | undefined>;
|
|
149
149
|
/**
|
|
150
|
-
* Lists
|
|
150
|
+
* Lists unclaimed attachments registered with `ownerId`, plus attachments claimed by optional
|
|
151
|
+
* `runId` when supplied — the discovery counterpart to
|
|
151
152
|
* `resolveForRun`'s single-lookup: a caller that does not yet hold a specific ref at all (an
|
|
152
153
|
* attachment uploaded in an earlier turn, never named in this run's prompt) has no id to look up
|
|
153
154
|
* with `resolveForRun` in the first place. This is what makes that attachment findable.
|
|
@@ -164,12 +165,18 @@ export interface AttachmentStore {
|
|
|
164
165
|
* attachment from before this method existed) is excluded from every caller's results, never
|
|
165
166
|
* just "unscoped" — this method is not callable with `ownerId: undefined`, so there is no input
|
|
166
167
|
* that could accidentally match an ownerless record;
|
|
167
|
-
* - a claimed attachment is excluded
|
|
168
|
-
*
|
|
169
|
-
*
|
|
168
|
+
* - a claimed attachment is excluded unless the host supplies that exact claiming `runId` in
|
|
169
|
+
* the optional argument. Hosts must derive ownerId/runId from authenticated execution context,
|
|
170
|
+
* never tool input. Run startup claims ALL message attachments before tools execute; including
|
|
171
|
+
* that run's claims keeps image, video and generic file discovery available after delivery.
|
|
172
|
+
* Owner matching remains mandatory even for the matching run. Without runId, the original
|
|
173
|
+
* unclaimed-only behavior is unchanged. Listing never claims, releases, or reads a file; callers
|
|
174
|
+
* still use `resolveForRun` for integrity-checked bytes/promotion.
|
|
170
175
|
*
|
|
171
176
|
* What this does NOT scope by: batch, conversation, or run — none of those are recorded on an
|
|
172
|
-
* `AttachmentRecord` today
|
|
177
|
+
* `AttachmentRecord` today (except run ownership once claimed). Hosts restricting discovery to
|
|
178
|
+
* an accepted message must also intersect these summaries with that message's refs. Without that
|
|
179
|
+
* intersection, two different conversations run by the SAME `ownerId` will each see
|
|
173
180
|
* the other's pending attachments through this method. A host for whom that is too wide needs a
|
|
174
181
|
* finer-grained id than `ownerId` to pass into `register()` — this method does not itself assume
|
|
175
182
|
* `ownerId` means "one admin account" rather than "one conversation, one composer, one browser
|
|
@@ -184,7 +191,9 @@ export interface AttachmentStore {
|
|
|
184
191
|
*/
|
|
185
192
|
listPendingForOwner: ({ ownerId }: {
|
|
186
193
|
readonly ownerId: string;
|
|
187
|
-
},
|
|
194
|
+
}, optional?: {
|
|
195
|
+
readonly runId?: string;
|
|
196
|
+
}) => Promise<PendingAttachmentSummary[]>;
|
|
188
197
|
/** Deletes the named still-unclaimed uploads, then the batch directory if it is now empty. */
|
|
189
198
|
deleteUnclaimed: ({ batchId, paths }: {
|
|
190
199
|
readonly batchId: string;
|
|
@@ -265,7 +274,7 @@ export interface AttachmentRecord {
|
|
|
265
274
|
ownerId?: string;
|
|
266
275
|
}
|
|
267
276
|
/**
|
|
268
|
-
* What `listPendingForOwner` hands back for
|
|
277
|
+
* What `listPendingForOwner` hands back for an unclaimed or same-run claimed attachment: enough for a caller to
|
|
269
278
|
* show a person what is waiting, and to name it again (`ref`) to `claim()`/`resolveForRun`.
|
|
270
279
|
*/
|
|
271
280
|
export interface PendingAttachmentSummary {
|
package/dist/http/attachments.js
CHANGED
|
@@ -835,12 +835,13 @@ export async function createDiskAttachmentStore(requiredArgs, optionalArgs = {})
|
|
|
835
835
|
}
|
|
836
836
|
return { path: record.filePath, name: record.name, kind: record.kind, size: record.size };
|
|
837
837
|
},
|
|
838
|
-
async listPendingForOwner({ ownerId },
|
|
838
|
+
async listPendingForOwner({ ownerId }, { runId } = {}) {
|
|
839
839
|
return [...records.values()]
|
|
840
840
|
// `record.ownerId !== undefined` first, short-circuiting before the comparison: this is
|
|
841
841
|
// what makes an ownerless record excluded rather than accidentally matched by a falsy-ish
|
|
842
842
|
// `ownerId` argument — see this method's own doc on why that must never be a wildcard.
|
|
843
|
-
.filter((record) => record.
|
|
843
|
+
.filter((record) => record.ownerId !== undefined && record.ownerId === ownerId
|
|
844
|
+
&& (record.claimedRunId === undefined || (runId !== undefined && record.claimedRunId === runId)))
|
|
844
845
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
845
846
|
.map((record) => ({ ref: record.id, name: record.name, kind: record.kind, size: record.size, createdAt: record.createdAt }));
|
|
846
847
|
},
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/run-lifecycle.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface StartRunInput {
|
|
|
15
15
|
readonly idempotencyKey?: string;
|
|
16
16
|
/** Override the generated run id — test/fixture hook only. */
|
|
17
17
|
readonly runId?: string;
|
|
18
|
-
/** When set, arms an inactivity watchdog that fails the run
|
|
18
|
+
/** When set, arms an inactivity watchdog that fails the run permanently if no `emit()` occurs within this window. */
|
|
19
19
|
readonly inactivityTimeoutMs?: number;
|
|
20
20
|
}
|
|
21
21
|
export interface StartRunResult {
|
|
@@ -106,6 +106,7 @@ export interface RunLifecycle {
|
|
|
106
106
|
* on a run that never actually stalled. A driver that knows exactly why a run is quiet must say so
|
|
107
107
|
* rather than leaving the watchdog to guess from silence alone. No-op if `runId` is unknown,
|
|
108
108
|
* already terminal, or the slow-run notice is disabled kernel-wide (`slowRunThresholdMs: null`).
|
|
109
|
+
* Pairs nest: the notice stays suspended until every open suspension has been resumed.
|
|
109
110
|
*/
|
|
110
111
|
suspendSlowRunNotice(args: {
|
|
111
112
|
readonly runId: string;
|
package/dist/run-lifecycle.js
CHANGED
|
@@ -102,6 +102,7 @@ function rehydratedRunRecord(runId, entries) {
|
|
|
102
102
|
terminalEndEntry: endEntry,
|
|
103
103
|
watchdog: undefined,
|
|
104
104
|
slowRunWatchdog: undefined,
|
|
105
|
+
slowRunSuspensions: 0,
|
|
105
106
|
startPromise: undefined,
|
|
106
107
|
finishPromise: undefined,
|
|
107
108
|
retentionTimer: undefined,
|
|
@@ -290,6 +291,7 @@ function buildNewRunRecord(startInput, runId, now) {
|
|
|
290
291
|
terminalEndEntry: undefined,
|
|
291
292
|
watchdog: undefined,
|
|
292
293
|
slowRunWatchdog: undefined,
|
|
294
|
+
slowRunSuspensions: 0,
|
|
293
295
|
startPromise: undefined,
|
|
294
296
|
finishPromise: undefined,
|
|
295
297
|
retentionTimer: undefined,
|
|
@@ -448,10 +450,9 @@ export function createRunLifecycle(requiredArgs, optionalArgs = {}) {
|
|
|
448
450
|
}
|
|
449
451
|
/**
|
|
450
452
|
* Fires when a run's inactivity watchdog times out with no intervening
|
|
451
|
-
* `emit()`/`finish()`.
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
* treats timeout/inactivity as one of only two resumable categories).
|
|
453
|
+
* `emit()`/`finish()`. A deliberate watchdog kill is permanent: automatically restarting
|
|
454
|
+
* it would undo the operator's inactivity policy. Other unexpected process failures still
|
|
455
|
+
* retain their original runtime classification.
|
|
455
456
|
*
|
|
456
457
|
* The `!record || isTerminalRunState(...)` guard below is defensive only and is currently
|
|
457
458
|
* unreachable through the public API — deliberately kept, not fake-tested, and the reason is
|
|
@@ -477,7 +478,9 @@ export function createRunLifecycle(requiredArgs, optionalArgs = {}) {
|
|
|
477
478
|
return;
|
|
478
479
|
}
|
|
479
480
|
try {
|
|
480
|
-
|
|
481
|
+
// A deliberate inactivity kill is permanent for this attempt, not an auto-resume signal.
|
|
482
|
+
await lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'Stopped by inactivity watchdog', detail: 'inactivity-watchdog' } } });
|
|
483
|
+
await lifecycle.finish({ runId, status: 'failed', code: null, signal: null, resumable: false });
|
|
481
484
|
}
|
|
482
485
|
catch (error) {
|
|
483
486
|
const context = { source: 'inactivity-timeout', runId, error };
|
|
@@ -748,7 +751,9 @@ export function createRunLifecycle(requiredArgs, optionalArgs = {}) {
|
|
|
748
751
|
// freshly-started one does. Without this, `finish()`'s `record.slowRunWatchdog?.cancel()` (which
|
|
749
752
|
// never fires again on its own — the underlying timer is one-shot) left the notice permanently
|
|
750
753
|
// dark for the rest of a resumed run's life, silently disabling a feature the run is otherwise
|
|
751
|
-
// still eligible for.
|
|
754
|
+
// still eligible for. Suspensions left open when the run ended can never be resumed (resume is a
|
|
755
|
+
// no-op on a terminal run), so they are dropped here or they would keep the notice dark forever.
|
|
756
|
+
record.slowRunSuspensions = 0;
|
|
752
757
|
armSlowRunWatchdogIfConfigured({ record: record, timeoutMs: slowRunThresholdMs, onTimeout: () => {
|
|
753
758
|
void handleSlowRunNotice(runId);
|
|
754
759
|
} });
|
|
@@ -762,12 +767,18 @@ export function createRunLifecycle(requiredArgs, optionalArgs = {}) {
|
|
|
762
767
|
const record = runs.get(runId);
|
|
763
768
|
if (!record || isTerminalRunState({ state: record.status.state }))
|
|
764
769
|
return;
|
|
770
|
+
record.slowRunSuspensions += 1;
|
|
765
771
|
record.slowRunWatchdog?.cancel({});
|
|
766
772
|
},
|
|
767
773
|
resumeSlowRunNotice({ runId }) {
|
|
768
774
|
const record = runs.get(runId);
|
|
769
775
|
if (!record || isTerminalRunState({ state: record.status.state }))
|
|
770
776
|
return;
|
|
777
|
+
// Re-arm only when the LAST open suspension closes (see `RunRecord.slowRunSuspensions`).
|
|
778
|
+
if (record.slowRunSuspensions > 0)
|
|
779
|
+
record.slowRunSuspensions -= 1;
|
|
780
|
+
if (record.slowRunSuspensions > 0)
|
|
781
|
+
return;
|
|
771
782
|
// Suspension cancels permanently. Resuming must create a new window, never revive that handle.
|
|
772
783
|
record.slowRunWatchdog?.cancel({});
|
|
773
784
|
armSlowRunWatchdogIfConfigured({ record, timeoutMs: slowRunThresholdMs, onTimeout: () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/daemon",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "The stateful agent daemon runtime: run lifecycle, durable event-log port, the tool-execution boundary, agent executor, terminal sessions, and routines.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -116,10 +116,10 @@
|
|
|
116
116
|
"registry": "https://registry.npmjs.org"
|
|
117
117
|
},
|
|
118
118
|
"dependencies": {
|
|
119
|
-
"@jini-ai/
|
|
120
|
-
"@jini-ai/
|
|
121
|
-
"@jini-ai/
|
|
122
|
-
"@jini-ai/
|
|
119
|
+
"@jini-ai/protocol": "^0.4.1",
|
|
120
|
+
"@jini-ai/agent-runtime": "^0.4.1",
|
|
121
|
+
"@jini-ai/platform": "^0.5.1",
|
|
122
|
+
"@jini-ai/core": "^0.4.1"
|
|
123
123
|
},
|
|
124
124
|
"devDependencies": {
|
|
125
125
|
"@vitest/coverage-v8": "^2.1.9",
|
|
@@ -132,9 +132,9 @@
|
|
|
132
132
|
"@types/pg": "8.23.1",
|
|
133
133
|
"express": "^4.21.0",
|
|
134
134
|
"@types/express": "^4.17.21",
|
|
135
|
-
"@jini-ai/chat": "0.5.2",
|
|
136
135
|
"@jini-ai/http-kit": "0.4.0",
|
|
137
|
-
"@jini-ai/db": "0.3.0"
|
|
136
|
+
"@jini-ai/db": "0.3.0",
|
|
137
|
+
"@jini-ai/chat": "0.5.4"
|
|
138
138
|
},
|
|
139
139
|
"peerDependencies": {
|
|
140
140
|
"node-pty": "^1.1.0",
|