@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
@@ -133,6 +133,26 @@ function stagePayload(legacyDir, stagingDir, payloadEntries, fs) {
133
133
  * @param entries - The payload entries to promote.
134
134
  * @returns The entries that were placed into `dataDir` before any failure.
135
135
  */
136
+ /** Renames `src` to `dst`, falling back to copy-then-delete (e.g. across devices); a failed copy
137
+ * removes its partial `dst` before rethrowing. */
138
+ function moveEntry(src, dst, fs) {
139
+ try {
140
+ fs.renameSync(src, dst);
141
+ }
142
+ catch {
143
+ copyThenRemove(src, dst, fs);
144
+ }
145
+ }
146
+ function copyThenRemove(src, dst, fs) {
147
+ try {
148
+ fs.cpSync(src, dst, { recursive: true, force: true, errorOnExist: false });
149
+ fs.rmSync(src, { recursive: true, force: true });
150
+ }
151
+ catch (cpErr) {
152
+ fs.rmSync(dst, { recursive: true, force: true });
153
+ throw cpErr;
154
+ }
155
+ }
136
156
  export function promoteStaged({ stagingDir, dataDir, entries }, { filesystem: fs = nodeFilesystem } = {}) {
137
157
  fs.mkdirSync(dataDir, { recursive: true });
138
158
  const promoted = [];
@@ -140,19 +160,7 @@ export function promoteStaged({ stagingDir, dataDir, entries }, { filesystem: fs
140
160
  for (const entry of entries) {
141
161
  const src = path.join(stagingDir, entry);
142
162
  const dst = path.join(dataDir, entry);
143
- try {
144
- fs.renameSync(src, dst);
145
- }
146
- catch {
147
- try {
148
- fs.cpSync(src, dst, { recursive: true, force: true, errorOnExist: false });
149
- fs.rmSync(src, { recursive: true, force: true });
150
- }
151
- catch (cpErr) {
152
- fs.rmSync(dst, { recursive: true, force: true });
153
- throw cpErr;
154
- }
155
- }
163
+ moveEntry(src, dst, fs);
156
164
  promoted.push(entry);
157
165
  }
158
166
  }
@@ -146,7 +146,8 @@ function readToolStream({ res, settle, fail }, { emitSurface }) {
146
146
  res.on('data', (chunk) => {
147
147
  buffer += chunk;
148
148
  const lines = buffer.split('\n');
149
- buffer = lines.pop() ?? '';
149
+ // String.split always produces at least one member, including for an empty chunk.
150
+ buffer = lines.pop();
150
151
  lines.forEach(onLine);
151
152
  });
152
153
  // Once the terminal line is in hand the outcome is known, so a later reset cannot unsettle it.
@@ -110,7 +110,8 @@ export function tzWallToUtcGapFallback({ timezone, year, month, day, hour, minut
110
110
  }
111
111
  function matchesWallClock(timezone, at, year, month, day, hour, minute) {
112
112
  const p = partsInTimezone({ timezone: timezone, atUtc: at });
113
- return p.year === year && p.month === month && p.day === day && p.hour === hour && p.minute === minute;
113
+ const sameDate = p.year === year && p.month === month && p.day === day;
114
+ return sameDate && p.hour === hour && p.minute === minute;
114
115
  }
115
116
  /** Minutes east of UTC for `timezone` at instant `at`. e.g. Asia/Shanghai returns 480. */
116
117
  function tzOffsetMinutes(timezone, at) {
@@ -43,6 +43,11 @@ export class ScheduledRunPersistenceError extends Error {
43
43
  this.name = 'ScheduledRunPersistenceError';
44
44
  }
45
45
  }
46
+ /** What a failed scheduled run logs: the underlying error's message when there is one. */
47
+ function scheduledRunFailureDetail(error) {
48
+ const cause = error instanceof ScheduledRunPersistenceError ? error.originalError : error;
49
+ return cause instanceof Error ? cause.message : cause;
50
+ }
46
51
  function isScheduledRunPersistenceError(error) {
47
52
  return error instanceof ScheduledRunPersistenceError;
48
53
  }
@@ -136,13 +141,7 @@ export class RoutineService {
136
141
  this.rescheduleOne({ routineId: routine.id });
137
142
  })
138
143
  .catch((error) => {
139
- console.error(`[@jini-ai/daemon] routine ${routine.id} scheduled run failed:`, error instanceof ScheduledRunPersistenceError
140
- ? error.originalError instanceof Error
141
- ? error.originalError.message
142
- : error.originalError
143
- : error instanceof Error
144
- ? error.message
145
- : error);
144
+ console.error(`[@jini-ai/daemon] routine ${routine.id} scheduled run failed:`, scheduledRunFailureDetail(error));
146
145
  if (isScheduledRunPersistenceError(error)) {
147
146
  this.retryScheduledSlot(routine.id, fireAt);
148
147
  }
@@ -101,7 +101,8 @@ function terminalStateFromEndEntry(entry) {
101
101
  if (!isRecord(entry.data))
102
102
  return { state: 'failed', resumable: false };
103
103
  const status = entry.data.status;
104
- const state = status === 'succeeded' ? 'succeeded' : status === 'canceled' ? 'cancelled' : 'failed';
104
+ const unsuccessful = status === 'canceled' ? 'cancelled' : 'failed';
105
+ const state = status === 'succeeded' ? 'succeeded' : unsuccessful;
105
106
  return { state, resumable: entry.data.resumable === true };
106
107
  }
107
108
  // ---------------------------------------------------------------------------
@@ -13,6 +13,12 @@ export function validateSessionKernel({ kernel, dialect, transports }) {
13
13
  function runSessionQuery(kernel, body) {
14
14
  return kernel.run(db => body(db));
15
15
  }
16
+ /** The one atomic upsert behind `setSessionId`: insert the pair, or replace its session id and time. */
17
+ function upsertSessionQuery({ conversationId, agentId, sessionId, updatedAt }) {
18
+ return (db) => db.insertInto('assistant_agent_sessions')
19
+ .values({ conversation_id: conversationId, agent_id: agentId, session_id: sessionId, updated_at: updatedAt })
20
+ .onConflict(oc => oc.columns(['conversation_id', 'agent_id']).doUpdateSet({ session_id: sessionId, updated_at: updatedAt })).execute();
21
+ }
16
22
  /** Translate opaque driver failures without losing their cause or mutating host state. */
17
23
  async function atSessionBoundary(operation) {
18
24
  try {
@@ -43,9 +49,7 @@ export function createSqlAgentSessionStore({ kernel }, { clock = createSystemClo
43
49
  setSessionId({ conversationId, agentId, sessionId }) {
44
50
  return atSessionBoundary(async () => {
45
51
  const updatedAt = clock.nowMs();
46
- await runSessionQuery(kernel, db => db.insertInto('assistant_agent_sessions')
47
- .values({ conversation_id: conversationId, agent_id: agentId, session_id: sessionId, updated_at: updatedAt })
48
- .onConflict(oc => oc.columns(['conversation_id', 'agent_id']).doUpdateSet({ session_id: sessionId, updated_at: updatedAt })).execute());
52
+ await runSessionQuery(kernel, upsertSessionQuery({ conversationId, agentId, sessionId, updatedAt }));
49
53
  });
50
54
  },
51
55
  clearSessionId({ conversationId, agentId }) {
@@ -129,7 +129,11 @@ async function replayInKernel({ db, runId, afterCursor }) {
129
129
  const result = replayResult({ run: { run_id: runId, next_cursor: first.next_cursor }, oldest, afterCursor });
130
130
  if (result.kind !== 'ok')
131
131
  return result;
132
- const entries = rows.flatMap(({ cursor, event, data, recorded_at }) => cursor === null || event === null || data === null || recorded_at === null ? [] : [rowToEntry({ cursor, event, data, recorded_at })]);
132
+ // A row missing any event column carries no entry.
133
+ const entries = rows.flatMap(({ cursor, event, data, recorded_at }) => {
134
+ const missing = cursor === null || event === null;
135
+ return missing || data === null || recorded_at === null ? [] : [rowToEntry({ cursor, event, data, recorded_at })];
136
+ });
133
137
  return { ...result, entries };
134
138
  }
135
139
  //# sourceMappingURL=sqlite-kernel.js.map
@@ -58,8 +58,12 @@ export function openSqliteEventLog(input, options = {}) {
58
58
  } }, { pragmas: ['journal_mode = WAL'] });
59
59
  const log = bindSqliteEventLog(db, options);
60
60
  let closed = false;
61
- return { ...log, async close(_args) { if (closed)
62
- return; closed = true; db.close(); } };
61
+ return { ...log, async close(_args) {
62
+ if (closed)
63
+ return;
64
+ closed = true;
65
+ db.close();
66
+ } };
63
67
  }
64
68
  catch (error) {
65
69
  try {
@@ -143,9 +143,9 @@ export function createSurfaceExchangeStore(deps, { idleTtlMs = DEFAULT_SURFACE_I
143
143
  // Unknown and closed are the same situation to a human whose dialog is no longer waiting.
144
144
  if (!entry)
145
145
  return { ok: false, reason: 'unknown-or-closed' };
146
- if ((spec.toolId !== undefined && entry.binding.toolId !== spec.toolId)
147
- || (entry.binding.channel ?? deps.defaultChannel) !== (spec.channel ?? deps.defaultChannel)
148
- || entry.binding.principalId !== spec.principalId) {
146
+ const toolMismatch = spec.toolId !== undefined && entry.binding.toolId !== spec.toolId;
147
+ const channelMismatch = (entry.binding.channel ?? deps.defaultChannel) !== (spec.channel ?? deps.defaultChannel);
148
+ if (toolMismatch || channelMismatch || entry.binding.principalId !== spec.principalId) {
149
149
  // A wrong-binding delivery must not consume a conversation the right human is still having.
150
150
  return { ok: false, reason: 'binding-mismatch' };
151
151
  }
@@ -200,11 +200,10 @@ export function createTerminalSessionManager(_requiredArgs, options = {}) {
200
200
  const result = [];
201
201
  for (const session of terminalService.list()) {
202
202
  const meta = metadata.get(session.id);
203
- if (!meta || meta.principalId !== principal.id)
204
- continue;
205
- if (filter.resourceRef !== undefined && meta.resourceRef !== filter.resourceRef)
206
- continue;
207
- result.push(toSessionInfo(session, meta.resourceRef));
203
+ const visible = meta !== undefined && meta.principalId === principal.id
204
+ && (filter.resourceRef === undefined || meta.resourceRef === filter.resourceRef);
205
+ if (visible)
206
+ result.push(toSessionInfo(session, meta.resourceRef));
208
207
  }
209
208
  return result;
210
209
  }
@@ -46,7 +46,7 @@
46
46
  * answer gets back in.
47
47
  */
48
48
  import { randomUUID } from 'node:crypto';
49
- import { ToolInputError, withNormalizationNotes, ToolInputNormalizationError, } from '@jini-ai/core';
49
+ import { ToolInputError, withNormalizationNotes, ToolInputNormalizationError, isReadOnlyTool, } from '@jini-ai/core';
50
50
  import { authorizeToolInvocation } from '@jini-ai/core/composition';
51
51
  import { toolCandidates } from './core/tool-candidates.js';
52
52
  /**
@@ -104,6 +104,8 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
104
104
  // Finished executionIds, oldest first (Set insertion order): the only records `retireAudit` evicts.
105
105
  const finishedAudits = new Set();
106
106
  const pendingConfirmations = new Map();
107
+ // Owned by the executor, not a bridge: concurrent HTTP/MCP calls create separate bridges.
108
+ const approvalWaits = new Map();
107
109
  const activeControllers = new Map();
108
110
  /**
109
111
  * Marks a pending confirmation `cancel()` just resolved (as opposed to a genuine human `deny`),
@@ -114,6 +116,34 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
114
116
  * `requestConfirmation()` settles, so it can never leak onto a later execution.
115
117
  */
116
118
  const cancelledConfirmations = new Set();
119
+ /** Records only actual human waits; terminal decisions and aborts release the run's write block.
120
+ * @complexity O(1) time/space per pending execution; empty run entries are removed. */
121
+ function approvalStateChange({ run, executionId, toolId, pending }, _optional = {}) {
122
+ if (pending) {
123
+ const waits = approvalWaits.get(run.id) ?? new Map();
124
+ waits.set(executionId, toolId);
125
+ approvalWaits.set(run.id, waits);
126
+ return;
127
+ }
128
+ const waits = approvalWaits.get(run.id);
129
+ waits?.delete(executionId);
130
+ if (waits?.size === 0)
131
+ approvalWaits.delete(run.id);
132
+ }
133
+ /** Refuses writes using the registered read-only contract, never a model's claim.
134
+ * The validation result reaches every delegated transport as an actionable error, without
135
+ * resolving or approving the original call. @complexity O(1) time/space. */
136
+ function approvalRefusal({ executionId, run, descriptor }, _optional = {}) {
137
+ const pendingToolId = approvalWaits.get(run.id)?.values().next().value;
138
+ if (pendingToolId === undefined || isReadOnlyTool({ descriptor }))
139
+ return undefined;
140
+ const error = JSON.stringify({
141
+ status: 'awaiting_approval', started: false, toolId: descriptor.id, pendingToolId,
142
+ message: 'Another action is waiting for human confirmation and has not started. This write was refused. Reads may continue. Never say an action has started until its tool result explicitly says it started.',
143
+ });
144
+ appendEvent(executionId, 'failed', error);
145
+ return { executionId, status: 'failed', errorKind: 'validation', error };
146
+ }
117
147
  function appendEvent(executionId, phase, detail) {
118
148
  // Every call site passes an `executionId` this same `execute()` call
119
149
  // already registered in `audits` (right before its first `appendEvent`
@@ -242,7 +272,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
242
272
  * Acts on the authorization outcome synchronously: `{ refusal }` if the call ends here
243
273
  * (cancelled meanwhile, or denied), otherwise records `authorized` and returns `{ allowed }`.
244
274
  */
245
- function admitAuthorized({ executionId, resolved, signal }) {
275
+ function admitAuthorized({ executionId, resolved, signal, run }) {
246
276
  // `authorize()` isn't itself signal-aware — it's typically a fast policy check, not a
247
277
  // human-in-the-loop wait — so this is a post-await check rather than a mid-flight abort, the
248
278
  // same idiom the post-handler check below already uses: a signal that fired while `authorize()`
@@ -253,6 +283,10 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
253
283
  return { refusal: terminal({ executionId, status: 'denied' }) };
254
284
  const { normalizationNotes } = resolved;
255
285
  appendEvent(executionId, 'authorized', normalizationNotes.length ? `normalized: ${normalizationNotes.join('; ')}` : undefined);
286
+ // Check after authorization awaits: an approval may have opened while this policy was pending.
287
+ const refusal = approvalRefusal({ executionId, run, descriptor: resolved.descriptor });
288
+ if (refusal)
289
+ return { refusal };
256
290
  return { allowed: resolved };
257
291
  }
258
292
  /**
@@ -268,13 +302,18 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
268
302
  async function confirmGate(request) {
269
303
  const { executionId } = request;
270
304
  let confirmation;
305
+ const state = { run: request.run, executionId, toolId: request.tool.id };
271
306
  try {
307
+ approvalStateChange({ ...state, pending: true });
272
308
  confirmation = await requestConfirmation(request);
273
309
  }
274
310
  catch {
275
311
  // Same reasoning as the authorization gate: terminal, recorded, and generic.
276
312
  return operationalFailure({ executionId, error: 'confirmation failed' });
277
313
  }
314
+ finally {
315
+ approvalStateChange({ ...state, pending: false });
316
+ }
278
317
  if (confirmation === 'confirm') {
279
318
  appendEvent(executionId, 'confirmed');
280
319
  return undefined;
@@ -353,6 +392,10 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
353
392
  if (controller.signal.aborted) {
354
393
  return terminal({ executionId, status: 'cancelled' });
355
394
  }
395
+ // Guidance may await I/O after admission; another call can open a confirmation in that gap.
396
+ const refusal = approvalRefusal({ executionId, run, descriptor });
397
+ if (refusal)
398
+ return refusal;
356
399
  const timeout = startTimeout(controller, descriptor.timeoutMs);
357
400
  // Cleanup (clear the timeout) is repeated at
358
401
  // each return below rather than centralized in a `finally` — a
@@ -379,6 +422,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
379
422
  signal: controller.signal,
380
423
  }, {
381
424
  ...surfaceOption(emitSurface),
425
+ onApprovalStateChange: ({ pending }) => approvalStateChange({ run, executionId, toolId: call.toolId, pending }),
382
426
  invokeTool: nestedInvoker({ principal, run, toolId: call.toolId, invocationRegistry: call.invocationRegistry, signal: controller.signal }, { emitSurface }),
383
427
  });
384
428
  cleanup();
@@ -443,7 +487,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
443
487
  // `invocationRegistry.has({ toolId })` above already confirmed the tool exists, and `ToolRegistry` is
444
488
  // append-only (no unregister — see `createToolRegistry`'s doc), so `authorizeToolInvocation`
445
489
  // resolving the same `toolId` against the same `registry` cannot come back `undefined` here.
446
- const gate = admitAuthorized({ executionId, resolved: resolved, signal: controller.signal });
490
+ const gate = admitAuthorized({ executionId, resolved: resolved, signal: controller.signal, run });
447
491
  if ('refusal' in gate) {
448
492
  cleanupCancellation();
449
493
  return gate.refusal;
@@ -1,5 +1,5 @@
1
1
  import { isIssuedToolFailureDiagnostic } from "./diagnostics.js";
2
- import { askOnce, SURFACE_DISMISSED_PARAM, SURFACE_EXCHANGE_ID_PARAM, } from "../surface-exchanges.js";
2
+ import { askOnce, SURFACE_DISMISSED_PARAM, } from "../surface-exchanges.js";
3
3
  import { refuseNonReadOnlyDispatch } from "../core/read-only-tools.js";
4
4
  /**
5
5
  * @file The CONSUMER of `contracts/core/tool-failure-diagnostics.ts`'s `ToolFailureDiagnostic`
@@ -119,7 +119,9 @@ function isActionableDiagnostic(value) {
119
119
  * @complexity O(1).
120
120
  */
121
121
  function remedyReportedFailure(output) {
122
- return isPlainObject(output) && (output["saved"] === false || output["created"] === false || output["cancelled"] === true || output["connected"] === false);
122
+ if (!isPlainObject(output))
123
+ return false;
124
+ return output["saved"] === false || output["created"] === false || output["cancelled"] === true || output["connected"] === false;
123
125
  }
124
126
  /** `true` once the bounded walk should stop descending — depth past {@link MAX_DIAGNOSTIC_SCAN_DEPTH}
125
127
  * or the node budget exhausted. Pulled out of {@link walkForDiagnostic} so that function's own guard
@@ -400,7 +402,7 @@ function remedyApplied(remedyResult) {
400
402
  export function withToolFailureRecovery({ inner, ...deps }, _optional = {}) {
401
403
  return {
402
404
  execute: async (required, optional = {}) => {
403
- const { principal, toolId, input } = required;
405
+ const { principal, input } = required;
404
406
  const { signal, emitSurface } = optional;
405
407
  const result = await inner.execute(required, optional);
406
408
  // SILENCE STAYS FREE: everything but a completed call whose output carries an actionable
@@ -148,8 +148,7 @@ function redactFailedResult(result, context, deps) {
148
148
  return result;
149
149
  return { ...result, error: deps.redactSecretShapes({ text: result.error }).text };
150
150
  }
151
- const mintErrorId = deps.mintErrorId;
152
- const onFailure = deps.onFailure;
151
+ const { mintErrorId, onFailure } = deps;
153
152
  const { text, redactions } = deps.redactSecretShapes({ text: result.error ?? `tool "${context.toolId}" failed` });
154
153
  const errorId = mintErrorId();
155
154
  safeOnFailure(onFailure, {
@@ -46,6 +46,10 @@ import { withReadOnlyToolConstraint } from "../core/read-only-tools.js";
46
46
  import { withToolAttemptAudit } from "../core/tool-audit.js";
47
47
  import { withToolFailureRecovery } from "./recovery.js";
48
48
  import { withRedactedToolFailures, readToolErrorId } from "./redaction.js";
49
+ /** The audit wrapper's options: only an error sink the caller actually supplied. */
50
+ function auditOptions(audit) {
51
+ return audit.onSinkError ? { onSinkError: audit.onSinkError } : {};
52
+ }
49
53
  /** One registry and innermost read-only gate protect original, remedy and retry alike.
50
54
  * Redaction precedes observation; recovery is outermost so each actual attempt is audited.
51
55
  * @complexity O(1) composition; each decorator owns its runtime cost.
@@ -55,7 +59,7 @@ export function createAssistantToolExecutor(deps, _optional = {}) {
55
59
  registry: deps.registry, idGenerator: { newId: deps.newExecutionId }, messages: deps.readOnlyMessages }, {});
56
60
  const redacted = withRedactedToolFailures({ inner: guarded, ...deps.toolFailures }, {});
57
61
  const audited = deps.toolAttemptAudit
58
- ? withToolAttemptAudit({ inner: redacted, ...deps.toolAttemptAudit, readErrorId: readToolErrorId }, deps.toolAttemptAudit.onSinkError ? { onSinkError: deps.toolAttemptAudit.onSinkError } : {})
62
+ ? withToolAttemptAudit({ inner: redacted, ...deps.toolAttemptAudit, readErrorId: readToolErrorId }, auditOptions(deps.toolAttemptAudit))
59
63
  : redacted;
60
64
  return withToolFailureRecovery({ ...deps, inner: audited }, {});
61
65
  }
@@ -74,6 +74,11 @@ export function resultContent({ result }) {
74
74
  return 'Tool execution cancelled.';
75
75
  case 'failed':
76
76
  return result.error ?? 'Tool execution failed.';
77
+ default: {
78
+ // Unreachable: every tool result status has a case above, and a new one fails typecheck here.
79
+ const unhandled = result.status;
80
+ throw new TypeError(`Unhandled tool result status: ${String(unhandled)}`);
81
+ }
77
82
  }
78
83
  }
79
84
  /** Projects a model-safe result into the saved event: pixels occur only in `media`.
@@ -81,9 +86,10 @@ export function resultContent({ result }) {
81
86
  * @complexity O(n) output bytes, with O(n) transient serialization space. */
82
87
  export function projectToolResultEvent({ result }, _options = {}) {
83
88
  const { remainder, media } = extractResultMedia({ output: result.output });
89
+ const outputFlagsError = isRecord(result.output) && result.output.isError === true;
84
90
  const isError = result.status !== 'completed'
85
91
  || (batchOutcomeFor({ output: result.output }) ?? 'completed') !== 'completed'
86
- || (isRecord(result.output) && result.output.isError === true);
92
+ || outputFlagsError;
87
93
  return {
88
94
  content: resultContent({ result: { ...result, output: remainder } }),
89
95
  ...(isError ? { isError: true } : {}),
@@ -52,6 +52,13 @@
52
52
  * break the feature. Anything else absent from this list is still treated as human-only.
53
53
  */
54
54
  export const MODEL_VISIBLE_BLOCK_TYPES = ['text', 'image'];
55
+ /** A record block whose string `type` is one the model may see. */
56
+ function isModelVisibleBlock(block) {
57
+ if (!isRecord(block))
58
+ return false;
59
+ const type = block['type'];
60
+ return typeof type === 'string' && MODEL_VISIBLE_BLOCK_TYPES.includes(type);
61
+ }
55
62
  function isRecord(value) {
56
63
  return typeof value === 'object' && value !== null && !Array.isArray(value);
57
64
  }
@@ -74,16 +81,10 @@ export function splitToolResultSurfaces({ output }) {
74
81
  for (const block of content) {
75
82
  // A non-record block carries no `type` to check, so it cannot be shown to be model-safe.
76
83
  // Withheld rather than passed through, per this module's fail-closed rule.
77
- if (!isRecord(block)) {
78
- surfaces.push({ type: 'unknown', value: block });
79
- continue;
80
- }
81
- const type = block['type'];
82
- if (typeof type === 'string' && MODEL_VISIBLE_BLOCK_TYPES.includes(type)) {
84
+ if (isModelVisibleBlock(block))
83
85
  modelBlocks.push(block);
84
- continue;
85
- }
86
- surfaces.push(block);
86
+ else
87
+ surfaces.push(isRecord(block) ? block : { type: 'unknown', value: block });
87
88
  }
88
89
  if (surfaces.length === 0)
89
90
  return { modelOutput: output, surfaces: [] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/daemon",
3
- "version": "0.6.15-rc.1",
3
+ "version": "0.6.15",
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": {
@@ -129,6 +129,7 @@
129
129
  "registry": "https://registry.npmjs.org"
130
130
  },
131
131
  "devDependencies": {
132
+ "smol-toml": "1.9.1",
132
133
  "@vitest/coverage-v8": "^2.1.9",
133
134
  "node-pty": "^1.1.0",
134
135
  "better-sqlite3": "^13.0.0",
@@ -139,32 +140,35 @@
139
140
  "@types/pg": "8.23.1",
140
141
  "express": "^4.21.0",
141
142
  "@types/express": "^4.17.21",
142
- "@jini-ai/db": "0.6.15-rc.1",
143
- "@jini-ai/chat": "0.6.15-rc.1",
144
- "@jini-ai/http-kit": "0.6.15-rc.1",
145
- "@jini-ai/protocol": "^0.6.15-rc.1",
146
- "@jini-ai/agent-runtime": "^0.6.15-rc.1",
147
- "@jini-ai/platform": "^0.6.15-rc.1",
148
- "@jini-ai/core": "^0.6.15-rc.1"
143
+ "@jini-ai/db": "0.6.15",
144
+ "@jini-ai/http-kit": "0.6.15",
145
+ "@jini-ai/chat": "0.6.15",
146
+ "@jini-ai/core": "^0.6.15",
147
+ "@jini-ai/platform": "^0.6.15",
148
+ "@jini-ai/protocol": "^0.6.15",
149
+ "@jini-ai/agent-runtime": "^0.6.15"
149
150
  },
150
151
  "dependencies": {
151
- "smol-toml": "1.9.1",
152
- "@jini-ai/core": "^0.6.15-rc.1",
153
- "@jini-ai/protocol": "^0.6.15-rc.1"
152
+ "@jini-ai/core": "^0.6.15",
153
+ "@jini-ai/protocol": "^0.6.15"
154
154
  },
155
155
  "peerDependencies": {
156
+ "smol-toml": "1.9.1",
156
157
  "node-pty": "^1.1.0",
157
- "@jini-ai/db": "^0.6.15-rc.1",
158
+ "@jini-ai/db": "^0.6.10",
158
159
  "kysely": "^0.29.6",
159
160
  "better-sqlite3": "^13.0.0",
160
161
  "@electric-sql/pglite": "0.5.8",
161
162
  "pg": "^8.23.0",
162
- "@jini-ai/http-kit": "^0.6.15-rc.1",
163
+ "@jini-ai/http-kit": "^0.6.10",
163
164
  "express": "^4.21.0 || ^5.0.0",
164
- "@jini-ai/agent-runtime": "^0.6.15-rc.1",
165
- "@jini-ai/platform": "^0.6.15-rc.1"
165
+ "@jini-ai/agent-runtime": "^0.6.10",
166
+ "@jini-ai/platform": "^0.6.10"
166
167
  },
167
168
  "peerDependenciesMeta": {
169
+ "smol-toml": {
170
+ "optional": true
171
+ },
168
172
  "node-pty": {
169
173
  "optional": true
170
174
  },
@@ -200,6 +204,7 @@
200
204
  "build": "tsc -p tsconfig.json",
201
205
  "typecheck": "tsc -p tsconfig.json --noEmit",
202
206
  "test": "vitest run",
207
+ "test:live:claude-bash": "vitest run --config vitest.claude-bash.live.config.ts",
203
208
  "test:coverage": "vitest run --coverage",
204
209
  "test:postgres": "vitest run --config vitest.postgres.config.ts src/store/agent-sessions/__tests__/postgres.postgres.test.ts"
205
210
  }