@jini-ai/daemon 0.6.15-rc.1 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/agent-executor/codex/home-config.js +3 -2
  2. package/dist/agent-executor/codex/home-staging.js +2 -2
  3. package/dist/agent-executor/codex/isolation.js +6 -4
  4. package/dist/agent-executor/event-translation.js +2 -2
  5. package/dist/agent-executor/events.js +68 -49
  6. package/dist/agent-executor/launch.js +39 -50
  7. package/dist/agent-executor/live-session-state.js +6 -3
  8. package/dist/agent-executor/mcp-bridge.js +13 -5
  9. package/dist/agent-executor/message-relay.js +5 -6
  10. package/dist/agent-executor/run-output.js +7 -2
  11. package/dist/close-status.js +6 -2
  12. package/dist/core/omit-keys.d.ts +7 -0
  13. package/dist/core/omit-keys.js +12 -0
  14. package/dist/core/read-only-tools.js +5 -1
  15. package/dist/core/tool-gaps.js +1 -1
  16. package/dist/delegated-result-correlation.js +2 -2
  17. package/dist/http/agents.d.ts +4 -0
  18. package/dist/http/attachments.js +13 -12
  19. package/dist/http/component-catalog.js +1 -1
  20. package/dist/http/connectors.js +7 -5
  21. package/dist/http/db-ops.js +5 -0
  22. package/dist/http/delegated-tools.js +5 -0
  23. package/dist/http/frontend-sessions.js +7 -5
  24. package/dist/http/host-tools.js +7 -5
  25. package/dist/http/memory.js +3 -1
  26. package/dist/http/message-relay.js +4 -1
  27. package/dist/http/model-proxy.js +13 -8
  28. package/dist/http/research-tavily.adapter.js +2 -1
  29. package/dist/http/routines.js +7 -5
  30. package/dist/http/terminals.js +5 -0
  31. package/dist/http/tool-catalog.js +1 -1
  32. package/dist/http/xai.js +10 -7
  33. package/dist/legacy-data-migration.d.ts +0 -12
  34. package/dist/legacy-data-migration.js +21 -13
  35. package/dist/remote-tool-executor.js +2 -1
  36. package/dist/routines/schedule.js +2 -1
  37. package/dist/routines/scheduler.js +6 -7
  38. package/dist/run-lifecycle.js +2 -1
  39. package/dist/store/agent-sessions/sql.js +7 -3
  40. package/dist/store/event-log/sqlite-kernel.js +5 -1
  41. package/dist/store/event-log/sqlite.js +6 -2
  42. package/dist/surface-exchanges.js +3 -3
  43. package/dist/terminal-session.js +4 -5
  44. package/dist/tool-executor.js +47 -3
  45. package/dist/tool-recovery/recovery.js +5 -3
  46. package/dist/tool-recovery/redaction.js +1 -2
  47. package/dist/tool-recovery/stack.js +5 -1
  48. package/dist/tool-result-media.js +7 -1
  49. package/dist/tool-result-surfaces.js +10 -9
  50. package/package.json +20 -15
@@ -1,7 +1,12 @@
1
1
  import { redactSecrets } from '@jini-ai/core';
2
2
  // Only a terminal-prefixed failure overrides real output; ordinary assistant explanations
3
3
  // can mention empty output or headless permissions without being failed runs themselves.
4
- const DIAGNOSTIC_LINE = /^(?:jetski|agy|error|fatal):\s*(?:no (?:output|response) produced(?:\s*[—–:-]|[.!]\s*$)|(?:error:\s*)?permission denied\b.*headless|headless\b.*auto-denied)/i;
4
+ // One pattern per diagnostic; a line is a diagnostic when any of them matches.
5
+ const DIAGNOSTIC_LINES = [
6
+ /^(?:jetski|agy|error|fatal):\s*no (?:output|response) produced(?:\s*[—–:-]|[.!]\s*$)/i,
7
+ /^(?:jetski|agy|error|fatal):\s*(?:error:\s*)?permission denied\b.*headless/i,
8
+ /^(?:jetski|agy|error|fatal):\s*headless\b.*auto-denied/i,
9
+ ];
5
10
  /** Records whether the event is assistant text, tool activity or a run error. */
6
11
  function noteActivity(seen, event) {
7
12
  if (event.kind === 'error')
@@ -23,7 +28,7 @@ function noteDiagnostic({ seen, tails, event, plainStdout }) {
23
28
  return;
24
29
  const channel = event.kind === 'stderr' ? 'stderr' : 'stdout';
25
30
  tails[channel] = (tails[channel] + event.payload.chunk).slice(-8192);
26
- const line = tails[channel].split(/\r?\n/).find(value => DIAGNOSTIC_LINE.test(value.trim()));
31
+ const line = tails[channel].split(/\r?\n/).find(value => DIAGNOSTIC_LINES.some(pattern => pattern.test(value.trim())));
27
32
  if (line)
28
33
  seen.diagnostic = line;
29
34
  }
@@ -30,6 +30,11 @@ export function classifyRunCloseStatus(requiredArgs, optionalArgs = {}) {
30
30
  }
31
31
  return 'failed';
32
32
  }
33
+ /** A set environment value parsed as a positive, finite millisecond count, else undefined. */
34
+ function positiveTimeout(raw) {
35
+ const value = raw !== undefined ? Number(raw) : undefined;
36
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : undefined;
37
+ }
33
38
  /**
34
39
  * Resolves a timeout in milliseconds from an env-var override, an
35
40
  * agent-supplied default, and a kernel default, in that precedence order,
@@ -48,8 +53,7 @@ export function resolveTimeoutMs(requiredArgs, optionalArgs = {}) {
48
53
  const input = { ...requiredArgs, ...optionalArgs };
49
54
  const env = input.env ?? (typeof process !== 'undefined' ? process.env : {});
50
55
  const rawFromEnv = input.envVar ? env[input.envVar] : undefined;
51
- const fromEnv = rawFromEnv !== undefined ? Number(rawFromEnv) : undefined;
52
- const candidate = fromEnv !== undefined && Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : (input.agentDefaultMs ?? input.defaultMs);
56
+ const candidate = positiveTimeout(rawFromEnv) ?? input.agentDefaultMs ?? input.defaultMs;
53
57
  return Math.min(candidate, input.maxMs);
54
58
  }
55
59
  /**
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @file Copy an object without some of its keys — what `const { a: _a, ...rest } = source` does,
3
+ * without binding a throwaway name for every dropped key.
4
+ */
5
+ /** A shallow copy of `source`'s own enumerable properties, minus `keys`. @complexity O(own keys). */
6
+ export declare function omitKeys<T extends object, K extends keyof T>(source: T, keys: readonly K[]): Omit<T, K>;
7
+ //# sourceMappingURL=omit-keys.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @file Copy an object without some of its keys — what `const { a: _a, ...rest } = source` does,
3
+ * without binding a throwaway name for every dropped key.
4
+ */
5
+ /** A shallow copy of `source`'s own enumerable properties, minus `keys`. @complexity O(own keys). */
6
+ export function omitKeys(source, keys) {
7
+ const copy = { ...source };
8
+ for (const key of keys)
9
+ delete copy[key];
10
+ return copy;
11
+ }
12
+ //# sourceMappingURL=omit-keys.js.map
@@ -1,5 +1,9 @@
1
1
  import { isReadOnlyTool, toolDiscoveryMetadata } from '@jini-ai/core';
2
2
  import { toolCandidates } from './tool-candidates.js';
3
+ /** ` — did you mean a, b?` for any close tool ids, else nothing. */
4
+ function didYouMean(candidates) {
5
+ return candidates.length > 0 ? ` — did you mean ${candidates.join(', ')}?` : '';
6
+ }
3
7
  /**
4
8
  * Default daemon refusal wording, replaceable by the host. Unknown registrations and writes both
5
9
  * refuse unsafe dispatch; unknown ids direct discovery because an unrestricted retry cannot run them.
@@ -11,7 +15,7 @@ export const defaultDaemonMessages = {
11
15
  toolRefusalMessage: ({ toolId }) => `tool "${toolId}" is not registered as read-only — this gateway executes only tools whose registration declares readOnly; call it through execute_delegated_tool instead`,
12
16
  unknownToolMessage: ({ toolId, candidates }) => toolId === 'check_messages'
13
17
  ? 'check_messages is an MCP-local tool; call check_messages directly, never as toolId in a delegated gateway'
14
- : `no tool "${toolId}" is registered; use search_tools${candidates.length > 0 ? ` — did you mean ${candidates.join(', ')}?` : ''}`,
18
+ : `no tool "${toolId}" is registered; use search_tools${didYouMean(candidates)}`,
15
19
  },
16
20
  };
17
21
  /** @returns A copied, attenuated principal; its roles are unchanged. */
@@ -189,7 +189,7 @@ export function createToolGapCollector(required, optional = {}) {
189
189
  const payload = event.payload;
190
190
  if (payload.type === 'text_delta')
191
191
  readDelta(payload.delta);
192
- else if (payload.type === 'tool_use')
192
+ if (payload.type === 'tool_use')
193
193
  recordToolUse(payload, event.ts);
194
194
  },
195
195
  /** Await outstanding audit writes during graceful shutdown/tests. */
@@ -1,3 +1,4 @@
1
+ import { omitKeys } from './core/omit-keys.js';
1
2
  /** Accept only the complete base64 source shape; extra fields must remain part of the digest. */
2
3
  function base64ImageSource({ source }, _options = {}) {
3
4
  if (typeof source !== 'object' || source === null)
@@ -14,8 +15,7 @@ function normalizeSourceImage({ record }, _options = {}) {
14
15
  const image = base64ImageSource({ source: record.source });
15
16
  if (!image)
16
17
  return record;
17
- const { source: _source, ...rest } = record;
18
- return { ...rest, data: image.data, mimeType: image.media_type };
18
+ return { ...omitKeys(record, ['source']), data: image.data, mimeType: image.media_type };
19
19
  }
20
20
  /** Normalize unambiguous CLI images and JSON object key order.
21
21
  * @complexity O(n + k log k) for n bytes and k keys per object; inputs are wire JSON. */
@@ -51,6 +51,10 @@ export interface AgentSummary {
51
51
  * as `true` (no known reason to warn) rather than assuming the worst.
52
52
  */
53
53
  readonly supportsTools?: boolean;
54
+ /** Project the runtime definition so picker preflight agrees with launch enforcement. */
55
+ readonly supportsNativeToolAccess?: boolean;
56
+ readonly fullCliAccessOnly?: boolean;
57
+ readonly fullCliAccessNote?: string;
54
58
  }
55
59
  export interface AgentsHttpDeps {
56
60
  /** Returns the host's cached or freshly resolved client-safe agent inventory. */
@@ -284,13 +284,12 @@ const SIGNATURE_BYTES = FTYP_COMPATIBLE_BRANDS_OFFSET + MAX_SCANNED_COMPATIBLE_B
284
284
  * production is a check nobody knows works — a fake identity states each case directly.
285
285
  */
286
286
  export function isUnchangedAttachment({ recorded, observed }, _optional = {}) {
287
- return observed.isRegularFile
288
- && observed.canonicalPath === recorded.filePath
289
- && observed.dev === recorded.dev
290
- && observed.ino === recorded.ino
291
- && observed.size === recorded.size
292
- && (recorded.mtimeMs === undefined || observed.mtimeMs === recorded.mtimeMs)
287
+ const sameFile = observed.isRegularFile && observed.canonicalPath === recorded.filePath
288
+ && observed.dev === recorded.dev && observed.ino === recorded.ino;
289
+ // A recorded timestamp must still hold; one never recorded cannot disagree.
290
+ const sameTimes = (recorded.mtimeMs === undefined || observed.mtimeMs === recorded.mtimeMs)
293
291
  && (recorded.ctimeMs === undefined || observed.ctimeMs === recorded.ctimeMs);
292
+ return sameFile && observed.size === recorded.size && sameTimes;
294
293
  }
295
294
  /**
296
295
  * Phase 1 of `claim()`: synchronously reserves `attachments` for `runId`, or throws (releasing
@@ -384,13 +383,15 @@ export function sanitizeAttachmentName({ requestedName }, _optional = {}) {
384
383
  return 'attachment';
385
384
  return basename(requestedName).replaceAll(/[^a-zA-Z0-9._ -]/gu, '_') || 'attachment';
386
385
  }
386
+ /** An attachment no run has claimed, or one claimed by `runId` (never by an absent run id). */
387
+ function isUnclaimedOrClaimedBy(claimedRunId, runId) {
388
+ return claimedRunId === undefined || (runId !== undefined && claimedRunId === runId);
389
+ }
390
+ /** The complete PNG signature includes the CR/LF and control-byte corruption markers. */
391
+ const PNG_SIGNATURE_PREFIX = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
387
392
  /** `true` when `body`'s first 8 bytes are the PNG signature. */
388
393
  export function hasPngSignature({ body }, _optional = {}) {
389
- return body.length >= 8
390
- && body[0] === 0x89
391
- && body[1] === 0x50
392
- && body[2] === 0x4e
393
- && body[3] === 0x47;
394
+ return body.length >= 8 && PNG_SIGNATURE_PREFIX.every((byte, index) => body[index] === byte);
394
395
  }
395
396
  /** `true` when `body`'s first 3 bytes are the JPEG start-of-image marker. */
396
397
  export function hasJpegSignature({ body }, _optional = {}) {
@@ -934,7 +935,7 @@ export async function createDiskAttachmentStore(requiredArgs, optionalArgs = {})
934
935
  // what makes an ownerless record excluded rather than accidentally matched by a falsy-ish
935
936
  // `ownerId` argument — see this method's own doc on why that must never be a wildcard.
936
937
  .filter((record) => record.ownerId !== undefined && record.ownerId === ownerId
937
- && (record.claimedRunId === undefined || (runId !== undefined && record.claimedRunId === runId)))
938
+ && isUnclaimedOrClaimedBy(record.claimedRunId, runId))
938
939
  .sort((a, b) => a.createdAt - b.createdAt)
939
940
  .map((record) => ({ ref: record.id, name: record.name, kind: record.kind, size: record.size, createdAt: record.createdAt }));
940
941
  },
@@ -12,7 +12,7 @@ function parseSearchInput(input) {
12
12
  const rawLimit = input.query.limit;
13
13
  if (rawLimit === undefined)
14
14
  return ok({ value: { query: rawQuery, limit: DEFAULT_SEARCH_LIMIT } });
15
- if (typeof rawLimit !== 'string' || !/^[0-9]+$/.test(rawLimit)) {
15
+ if (typeof rawLimit !== 'string' || !/^\d+$/.test(rawLimit)) {
16
16
  return err({ error: validationError({ message: 'limit must be a single positive-integer query-string value when provided' }) });
17
17
  }
18
18
  const parsed = Number(rawLimit);
@@ -49,6 +49,8 @@ import { createApiError } from '@jini-ai/protocol';
49
49
  import { ClientFacingError, defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
50
50
  import { validationError } from '@jini-ai/http-kit';
51
51
  import { err, ok } from '@jini-ai/http-kit';
52
+ /** Validation message for a request body that is not a JSON object. */
53
+ const BODY_NOT_OBJECT_MESSAGE = 'body must be a JSON object';
52
54
  function defaultInternalErrorSink(context) {
53
55
  // eslint-disable-next-line no-console
54
56
  console.error(`[@jini-ai/http-kit] internal error (connectors/${context.source}.${context.operation}, correlationId=${context.correlationId})`, context.error);
@@ -86,7 +88,7 @@ function firstQueryValue(raw) {
86
88
  // ---------------------------------------------------------------------------
87
89
  function parseAuthCredentials(input) {
88
90
  if (!isRecord(input.body))
89
- return err({ error: validationError({ message: 'body must be a JSON object' }) });
91
+ return err({ error: validationError({ message: BODY_NOT_OBJECT_MESSAGE }) });
90
92
  const email = nonEmptyString(input.body.email);
91
93
  if (!email)
92
94
  return err({ error: validationError({ message: 'email must be a non-empty string' }, { issues: [{ path: 'email', message: 'required non-empty string' }] }) });
@@ -97,7 +99,7 @@ function parseAuthCredentials(input) {
97
99
  }
98
100
  function parseAuthToken(input) {
99
101
  if (!isRecord(input.body))
100
- return err({ error: validationError({ message: 'body must be a JSON object' }) });
102
+ return err({ error: validationError({ message: BODY_NOT_OBJECT_MESSAGE }) });
101
103
  const token = nonEmptyString(input.body.token);
102
104
  if (!token)
103
105
  return err({ error: validationError({ message: 'token must be a non-empty string' }, { issues: [{ path: 'token', message: 'required non-empty string' }] }) });
@@ -164,7 +166,7 @@ function parseStoragePut(input) {
164
166
  if (!keyResult.ok)
165
167
  return keyResult;
166
168
  if (!isRecord(input.body))
167
- return err({ error: validationError({ message: 'body must be a JSON object' }) });
169
+ return err({ error: validationError({ message: BODY_NOT_OBJECT_MESSAGE }) });
168
170
  const dataBase64 = input.body.dataBase64;
169
171
  if (typeof dataBase64 !== 'string' || !isWellFormedBase64(dataBase64)) {
170
172
  return err({ error: validationError({ message: 'dataBase64 must be a non-empty base64-encoded string' }, { issues: [{ path: 'dataBase64', message: 'required base64 string' }] }) });
@@ -231,7 +233,7 @@ function parseDbInsert(input) {
231
233
  if (!collectionResult.ok)
232
234
  return collectionResult;
233
235
  if (!isRecord(input.body))
234
- return err({ error: validationError({ message: 'body must be a JSON object' }) });
236
+ return err({ error: validationError({ message: BODY_NOT_OBJECT_MESSAGE }) });
235
237
  const id = input.body.id;
236
238
  if (typeof id !== 'string' || id.length === 0) {
237
239
  return err({ error: validationError({ message: 'id must be a non-empty string field' }, { issues: [{ path: 'id', message: 'required non-empty string' }] }) });
@@ -252,7 +254,7 @@ function parseDbUpdate(input) {
252
254
  if (!parsed.ok)
253
255
  return parsed;
254
256
  if (!isRecord(input.body))
255
- return err({ error: validationError({ message: 'body must be a JSON object' }) });
257
+ return err({ error: validationError({ message: BODY_NOT_OBJECT_MESSAGE }) });
256
258
  return ok({ value: { ...parsed.value, patch: input.body } });
257
259
  }
258
260
  function parseDbQueryRequest(input) {
@@ -138,6 +138,11 @@ function toolResultToApiResult(deps, source, result) {
138
138
  sink({ source, correlationId, error: result.status === 'failed' ? result.error : result.status });
139
139
  return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
140
140
  }
141
+ default: {
142
+ // Unreachable: every tool result status has a case above, and a new one fails typecheck here.
143
+ const unhandled = result.status;
144
+ throw new TypeError(`Unhandled tool result status: ${String(unhandled)}`);
145
+ }
141
146
  }
142
147
  }
143
148
  /** `GET /api/daemon/db` — the database inventory (schema version, tables, row counts, size). */
@@ -144,6 +144,11 @@ function toolExecutionResultToApiResult(deps, { runId, toolId, result }) {
144
144
  return err({ error: reportInternalError(deps, { source: 'delegated-tool-execute', error: result.status, runId, toolId, status: result.status }) });
145
145
  case 'failed':
146
146
  return err({ error: failedResultToApiError(deps, { runId, toolId, result }) });
147
+ default: {
148
+ // Unreachable: every tool result status has a case above, and a new one fails typecheck here.
149
+ const unhandled = result.status;
150
+ throw new TypeError(`Unhandled tool result status: ${String(unhandled)}`);
151
+ }
147
152
  }
148
153
  }
149
154
  /** The `failed` arm of {@link toolExecutionResultToApiResult} — see its doc for the three-way split. */
@@ -59,7 +59,8 @@ function reportInternalError(deps, source, error) {
59
59
  * from a bracketed query string) is rejected rather than coerced.
60
60
  */
61
61
  export function parseCapabilityQuery({ raw }, _optional = {}) {
62
- const values = raw === undefined ? [] : (Array.isArray(raw) ? raw : [raw]);
62
+ const listed = Array.isArray(raw) ? raw : [raw];
63
+ const values = raw === undefined ? [] : listed;
63
64
  const capabilities = [];
64
65
  for (const value of values) {
65
66
  if (typeof value !== 'string' || value.trim().length === 0)
@@ -167,11 +168,12 @@ export function registerFrontendSessionRoutes({ app, deps, adapter }, _optional
167
168
  }
168
169
  catch (error) {
169
170
  reportInternalError(deps, 'stream', error);
170
- if (!res.headersSent) {
171
- res.status(500).json({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }) });
171
+ if (res.headersSent) {
172
+ if (!res.writableEnded)
173
+ res.end();
172
174
  }
173
- else if (!res.writableEnded) {
174
- res.end();
175
+ else {
176
+ res.status(500).json({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }) });
175
177
  }
176
178
  }
177
179
  });
@@ -71,6 +71,12 @@ export function applicableForPlatform({ entry, platform }, _optional = {}) {
71
71
  export function defaultProbeEnv(_required, _optional = {}) {
72
72
  return { access: ({ path, mode }) => fsAccess(path, mode), env: process.env, platform: currentPlatform({}) };
73
73
  }
74
+ /** The login-shell bin directories a GUI-launched daemon's `PATH` tends to lack, per platform. */
75
+ function extraProbeDirs(platform, home) {
76
+ if (platform === 'darwin')
77
+ return ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', `${home}/.local/bin`];
78
+ return platform === 'linux' ? ['/usr/local/bin', '/usr/bin', '/bin', `${home}/.local/bin`] : [];
79
+ }
74
80
  /**
75
81
  * `$PATH` directories to probe, plus the common macOS/Linux locations a login shell would have
76
82
  * that a GUI-launched daemon's thin inherited `PATH` often lacks (no `/usr/local/bin`, no
@@ -81,11 +87,7 @@ export function pathDirs(probeEnv) {
81
87
  const raw = probeEnv.env.PATH ?? '';
82
88
  const sep = probeEnv.platform === 'win32' ? ';' : ':';
83
89
  const home = probeEnv.env.HOME ?? '';
84
- const extras = probeEnv.platform === 'darwin'
85
- ? ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', `${home}/.local/bin`]
86
- : probeEnv.platform === 'linux'
87
- ? ['/usr/local/bin', '/usr/bin', '/bin', `${home}/.local/bin`]
88
- : [];
90
+ const extras = extraProbeDirs(probeEnv.platform, home);
89
91
  return [...raw.split(sep), ...extras].filter((dir) => dir.length > 0);
90
92
  }
91
93
  export async function probeCommandOnPath({ command, probeEnv }, _optional = {}) {
@@ -290,7 +290,9 @@ export function registerMemoryEventStream({ app, deps, adapter }, _optional = {}
290
290
  let seq = 0;
291
291
  const channel = createSseChannel({ res });
292
292
  const emit = (kind, data) => {
293
- channel.enqueue({ event: { opaqueCursor: String(seq++), kind, data } });
293
+ const cursor = seq;
294
+ seq += 1;
295
+ channel.enqueue({ event: { opaqueCursor: String(cursor), kind, data } });
294
296
  };
295
297
  const onChange = (event) => emit('change', event);
296
298
  const onExtraction = (event) => emit('extraction', event);
@@ -8,7 +8,7 @@ export function createMessageRelayClaimHandler({ agentExecutor, runScopedCallers
8
8
  return (req, res, next) => {
9
9
  try {
10
10
  const token = bearerTokenFromHeader({ header: req.get('authorization') });
11
- const caller = token ? runScopedCallers?.resolveCaller({ token }) : undefined;
11
+ const caller = resolveRelayCaller({ token, runScopedCallers });
12
12
  if (runScopedCallers && !caller) {
13
13
  res.status(401).json({ error: { code: 'UNAUTHENTICATED', message: 'a live run credential is required' } });
14
14
  return;
@@ -30,6 +30,9 @@ export function createMessageRelayClaimHandler({ agentExecutor, runScopedCallers
30
30
  }
31
31
  };
32
32
  }
33
+ function resolveRelayCaller({ token, runScopedCallers }) {
34
+ return token ? runScopedCallers?.resolveCaller({ token }) : undefined;
35
+ }
33
36
  /** Strict receipt-channel validation; no caller-selected run or principal is accepted. */
34
37
  function validClaim(body) {
35
38
  if (body === null || typeof body !== 'object' || Array.isArray(body))
@@ -218,12 +218,12 @@ function toStreamEvent(seq, event) {
218
218
  }
219
219
  /** Answers a failure that happened before any SSE stream existed: a redacted JSON 500 if nothing was sent yet, otherwise just end the response (matching `mountJsonRoute`'s own SEC-005 catch). */
220
220
  function respondInternalError(res, correlationId) {
221
- if (!res.headersSent) {
222
- sendApiError({ res, status: 500, error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
223
- }
224
- else if (!res.writableEnded) {
225
- res.end();
221
+ if (res.headersSent) {
222
+ if (!res.writableEnded)
223
+ res.end();
224
+ return;
226
225
  }
226
+ sendApiError({ res, status: 500, error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
227
227
  }
228
228
  /**
229
229
  * The actual request handler body, shared by every fixed-path route and the `:provider`
@@ -238,6 +238,11 @@ async function runProxyStream({ req, res, adapter, provider, onInternalError, en
238
238
  // reach them however far execution got — including a throw from `guardSameOrigin`/`parse`,
239
239
  // before either exists yet.
240
240
  let seq = 0;
241
+ const nextSeq = () => {
242
+ const current = seq;
243
+ seq += 1;
244
+ return current;
245
+ };
241
246
  let channel;
242
247
  try {
243
248
  const origin = guardSameOrigin({ req, origin: adapter });
@@ -253,7 +258,7 @@ async function runProxyStream({ req, res, adapter, provider, onInternalError, en
253
258
  const openChannel = createSseChannel({ res }, { isEndEvent: ({ event }) => event.kind === 'end' });
254
259
  channel = openChannel;
255
260
  openChannel.open();
256
- await entry.run(parsed.value, (event) => openChannel.enqueue({ event: toStreamEvent(seq++, event) }));
261
+ await entry.run(parsed.value, (event) => openChannel.enqueue({ event: toStreamEvent(nextSeq(), event) }));
257
262
  // No `channel.end()` call here: every reachable exit path inside every turn-runner emits
258
263
  // exactly one `'end'`-kind event before its promise resolves (the duplicate-end-event fix —
259
264
  // see each provider module's own `turn-end-guard.ts` usage, and each function's own
@@ -283,8 +288,8 @@ async function runProxyStream({ req, res, adapter, provider, onInternalError, en
283
288
  // here (an exception escaping `run` — e.g. a caller-supplied `executeTool` throwing — means
284
289
  // no `'end'` event was ever emitted for this connection), and the synthetic `'end'`-kind
285
290
  // event's own `isEndEvent` match closes the channel via the same path as a real one.
286
- channel.enqueue({ event: toStreamEvent(seq++, { type: 'error', message: 'an internal error occurred', code: correlationId }) });
287
- channel.enqueue({ event: toStreamEvent(seq++, { type: 'end', reason: 'error' }) });
291
+ channel.enqueue({ event: toStreamEvent(nextSeq(), { type: 'error', message: 'an internal error occurred', code: correlationId }) });
292
+ channel.enqueue({ event: toStreamEvent(nextSeq(), { type: 'end', reason: 'error' }) });
288
293
  }
289
294
  }
290
295
  /** Erases one provider's concrete parse/run types into the registry's shared entry shape — the single cast every entry needs. */
@@ -1,6 +1,7 @@
1
1
  /** Tavily default adapter. Request limits, timeout and secret-redaction policy remain here. */
2
2
  import { validateBaseUrl } from '@jini-ai/agent-runtime';
3
3
  import { redactSecrets } from '@jini-ai/core';
4
+ import { trimCharsEnd } from '@jini-ai/core/text';
4
5
  const DEFAULT_TAVILY_BASE_URL = 'https://api.tavily.com';
5
6
  /** Tavily's own documented cap on `max_results` (see OD's `tavily.ts#TAVILY_MAX_RESULTS_LIMIT`). */
6
7
  const TAVILY_MAX_RESULTS_LIMIT = 20;
@@ -20,7 +21,7 @@ function resolveTavilyBase(credentials) {
20
21
  if (baseUrlCheck.error) {
21
22
  throw new Error(baseUrlCheck.error);
22
23
  }
23
- return baseUrl.replace(/\/+$/, '');
24
+ return trimCharsEnd({ text: baseUrl, chars: '/' });
24
25
  }
25
26
  function trimmedOrUndefined(value) {
26
27
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
@@ -3,6 +3,8 @@ import { createApiError } from '@jini-ai/protocol';
3
3
  import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
4
4
  import { validationError } from '@jini-ai/http-kit';
5
5
  import { err, ok } from '@jini-ai/http-kit';
6
+ /** The 404 message for an unknown routine id. */
7
+ const ROUTINE_NOT_FOUND_MESSAGE = 'routine not found';
6
8
  function isRecord(value) {
7
9
  return typeof value === 'object' && value !== null && !Array.isArray(value);
8
10
  }
@@ -192,7 +194,7 @@ export const routineGetRoute = defineJsonRoute({
192
194
  method: 'get', path: '/api/routines/:id', parse: parseRoutineId, handle: async ({ input: id, deps }) => {
193
195
  const routine = await deps.store.get({ id });
194
196
  return routine === null
195
- ? err({ error: createApiError({ code: 'NOT_FOUND', message: 'routine not found' }) })
197
+ ? err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) })
196
198
  : ok({ value: { routine: attachNextRunAt(routine, deps.scheduler) } });
197
199
  }
198
200
  });
@@ -205,7 +207,7 @@ export const routineUpdateRoute = defineJsonRoute({
205
207
  }
206
208
  const routine = await deps.store.update({ id, patch });
207
209
  if (routine === null)
208
- return err({ error: createApiError({ code: 'NOT_FOUND', message: 'routine not found' }) });
210
+ return err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
209
211
  deps.scheduler.rescheduleOne({ routineId: id });
210
212
  return ok({ value: { routine: attachNextRunAt(routine, deps.scheduler) } });
211
213
  }
@@ -214,14 +216,14 @@ export const routineDeleteRoute = defineJsonRoute({
214
216
  method: 'delete', path: '/api/routines/:id', parse: parseRoutineId, handle: async ({ input: id, deps }) => {
215
217
  deps.scheduler.unschedule({ routineId: id });
216
218
  const removed = await deps.store.delete({ id });
217
- return removed ? ok({ value: { ok: true } }) : err({ error: createApiError({ code: 'NOT_FOUND', message: 'routine not found' }) });
219
+ return removed ? ok({ value: { ok: true } }) : err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
218
220
  }
219
221
  }, { requireSameOrigin: true });
220
222
  export const routineRunNowRoute = defineJsonRoute({
221
223
  method: 'post', path: '/api/routines/:id/run', parse: parseRoutineId, handle: async ({ input: id, deps }) => {
222
224
  const existing = await deps.store.get({ id });
223
225
  if (existing === null)
224
- return err({ error: createApiError({ code: 'NOT_FOUND', message: 'routine not found' }) });
226
+ return err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
225
227
  const started = await deps.scheduler.runNow({ routineId: id });
226
228
  const [routine, run] = await Promise.all([deps.store.get({ id }), deps.store.getLatestRun({ routineId: id })]);
227
229
  return ok({
@@ -239,7 +241,7 @@ export const routineRunsListRoute = defineJsonRoute({
239
241
  method: 'get', path: '/api/routines/:id/runs', parse: parseRoutineRunsQuery, handle: async ({ input: { id, limit }, deps }) => {
240
242
  const existing = await deps.store.get({ id });
241
243
  if (existing === null)
242
- return err({ error: createApiError({ code: 'NOT_FOUND', message: 'routine not found' }) });
244
+ return err({ error: createApiError({ code: 'NOT_FOUND', message: ROUTINE_NOT_FOUND_MESSAGE }) });
243
245
  const runs = await deps.store.listRuns({ routineId: id, limit });
244
246
  return ok({ value: { runs } });
245
247
  }
@@ -60,6 +60,11 @@ function toolResultToApiResult(deps, result) {
60
60
  sink({ source: 'terminal-create', correlationId, error: result.status === 'failed' ? result.error : result.status });
61
61
  return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
62
62
  }
63
+ default: {
64
+ // Unreachable: every tool result status has a case above, and a new one fails typecheck here.
65
+ const unhandled = result.status;
66
+ throw new TypeError(`Unhandled tool result status: ${String(unhandled)}`);
67
+ }
63
68
  }
64
69
  }
65
70
  function parseTerminalCreate(input) {
@@ -12,7 +12,7 @@ function parseSearchInput(input) {
12
12
  const rawLimit = input.query.limit;
13
13
  if (rawLimit === undefined)
14
14
  return ok({ value: { query: rawQuery, limit: DEFAULT_SEARCH_LIMIT } });
15
- if (typeof rawLimit !== 'string' || !/^[0-9]+$/.test(rawLimit)) {
15
+ if (typeof rawLimit !== 'string' || !/^\d+$/.test(rawLimit)) {
16
16
  return err({ error: validationError({ message: 'limit must be a single positive-integer query-string value when provided' }) });
17
17
  }
18
18
  const parsed = Number(rawLimit);
package/dist/http/xai.js CHANGED
@@ -123,6 +123,9 @@ import { createApiError } from '@jini-ai/protocol';
123
123
  import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
124
124
  import { validationError } from '@jini-ai/http-kit';
125
125
  import { err, ok } from '@jini-ai/http-kit';
126
+ import { trimCharsEnd } from '@jini-ai/core/text';
127
+ /** The redacted message every internal failure answers with; details go to the error sink. */
128
+ const INTERNAL_ERROR_MESSAGE = 'an internal error occurred';
126
129
  const DEFAULT_TOKEN_FILE_NAME = 'xai-oauth-token.json';
127
130
  /** Matches the loopback listener's own 30-minute self-close timeout (OD's real comment: keeps the
128
131
  * PKCE state, the open socket, and any paste-back UI expiring together). */
@@ -361,7 +364,7 @@ export const xaiOauthStartRoute = defineJsonRoute({
361
364
  await stopListener(resolved);
362
365
  const correlationId = randomUUID();
363
366
  resolved.onInternalError({ source: 'oauth-start', correlationId, error });
364
- return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
367
+ return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
365
368
  }
366
369
  });
367
370
  }
@@ -410,7 +413,7 @@ export const xaiOauthCompleteRoute = defineJsonRoute({
410
413
  }
411
414
  const correlationId = randomUUID();
412
415
  resolved.onInternalError({ source: 'oauth-complete', correlationId, error: redactError(error) });
413
- return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
416
+ return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
414
417
  }
415
418
  }
416
419
  }, { requireSameOrigin: true });
@@ -448,7 +451,7 @@ export const xaiAuthStatusRoute = defineJsonRoute({
448
451
  catch (error) {
449
452
  const correlationId = randomUUID();
450
453
  resolved.onInternalError({ source: 'auth-status', correlationId, error });
451
- return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
454
+ return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
452
455
  }
453
456
  }
454
457
  }, { requireSameOrigin: true });
@@ -466,7 +469,7 @@ export const xaiOauthDisconnectRoute = defineJsonRoute({
466
469
  catch (error) {
467
470
  const correlationId = randomUUID();
468
471
  resolved.onInternalError({ source: 'oauth-disconnect', correlationId, error });
469
- return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
472
+ return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
470
473
  }
471
474
  }
472
475
  }, { requireSameOrigin: true });
@@ -594,7 +597,7 @@ async function callXaiSearch(resolved, accessToken, request) {
594
597
  const baseUrlCheck = validateBaseUrl({ baseUrl: resolved.searchBaseUrl });
595
598
  if (baseUrlCheck.error)
596
599
  throw new Error(baseUrlCheck.error);
597
- const base = resolved.searchBaseUrl.replace(/\/+$/, '');
600
+ const base = trimCharsEnd({ text: resolved.searchBaseUrl, chars: '/' });
598
601
  const model = request.model ?? resolved.searchDefaultModel;
599
602
  const requestBody = {
600
603
  model,
@@ -633,7 +636,7 @@ export const xaiSearchRoute = defineJsonRoute({
633
636
  catch (error) {
634
637
  const correlationId = randomUUID();
635
638
  resolved.onInternalError({ source: 'search', correlationId, error });
636
- return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
639
+ return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
637
640
  }
638
641
  if (!credential) {
639
642
  // No entitlement gate here — see module doc's "Dropped: OD's SuperGrok subscription gate"
@@ -649,7 +652,7 @@ export const xaiSearchRoute = defineJsonRoute({
649
652
  catch (error) {
650
653
  const correlationId = randomUUID();
651
654
  resolved.onInternalError({ source: 'search', correlationId, error });
652
- return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
655
+ return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE }, { requestId: correlationId }) });
653
656
  }
654
657
  }
655
658
  }, { requireSameOrigin: true });
@@ -122,18 +122,6 @@ export declare function dataDirHasExistingPayload({ dataDir, config }: {
122
122
  }, { filesystem: fs }?: {
123
123
  filesystem?: LegacyFilesystemPort;
124
124
  }): string[];
125
- /**
126
- * Move staged payload entries into the final `dataDir`. Each entry is moved
127
- * with `renameSync` (atomic on the same filesystem); a cross-device rename
128
- * falls back to copy + remove. Rollback-safe in two layers: the copy
129
- * fallback removes a partial destination on its own failure, and the outer
130
- * loop removes every entry it already promoted before rethrowing.
131
- *
132
- * @param stagingDir - The sibling staging directory holding the copied payload.
133
- * @param dataDir - The destination data root.
134
- * @param entries - The payload entries to promote.
135
- * @returns The entries that were placed into `dataDir` before any failure.
136
- */
137
125
  export declare function promoteStaged({ stagingDir, dataDir, entries }: {
138
126
  readonly stagingDir: string;
139
127
  readonly dataDir: string;