@jini-ai/daemon 0.6.15-rc.2 → 0.6.16

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 (77) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +14 -4
  3. package/dist/agent-executor/codex/home-config.js +3 -2
  4. package/dist/agent-executor/codex/home-staging.js +2 -2
  5. package/dist/agent-executor/codex/isolation.js +4 -3
  6. package/dist/agent-executor/contracts.d.ts +1 -1
  7. package/dist/agent-executor/contracts.js +0 -1
  8. package/dist/agent-executor/event-translation.d.ts +3 -3
  9. package/dist/agent-executor/event-translation.js +6 -6
  10. package/dist/agent-executor/events.js +65 -51
  11. package/dist/agent-executor/index.d.ts +5 -5
  12. package/dist/agent-executor/index.js +5 -5
  13. package/dist/agent-executor/launch.js +30 -46
  14. package/dist/agent-executor/live-session-state.js +2 -2
  15. package/dist/agent-executor/mcp-bridge.js +5 -0
  16. package/dist/agent-executor/message-relay.js +5 -6
  17. package/dist/agent-executor/run-output.js +7 -2
  18. package/dist/ai-trace-lifecycle.d.ts +1 -1
  19. package/dist/ai-trace-lifecycle.js +1 -1
  20. package/dist/close-status.js +6 -2
  21. package/dist/core/omit-keys.d.ts +7 -0
  22. package/dist/core/omit-keys.js +12 -0
  23. package/dist/core/read-only-tools.js +5 -1
  24. package/dist/core/tool-audit.js +11 -7
  25. package/dist/core/tool-gaps.js +1 -1
  26. package/dist/delegated-result-correlation.js +2 -2
  27. package/dist/frontend-capability-tools.d.ts +2 -0
  28. package/dist/frontend-capability-tools.js +7 -1
  29. package/dist/frontend-session-registry.d.ts +42 -3
  30. package/dist/frontend-session-registry.js +61 -9
  31. package/dist/http/agents.d.ts +4 -0
  32. package/dist/http/attachments.js +13 -12
  33. package/dist/http/component-catalog.js +1 -1
  34. package/dist/http/connectors.js +7 -5
  35. package/dist/http/db-ops.js +5 -0
  36. package/dist/http/delegated-tools.js +5 -0
  37. package/dist/http/frontend-control.d.ts +13 -0
  38. package/dist/http/frontend-control.js +14 -3
  39. package/dist/http/frontend-guest-scope.d.ts +32 -0
  40. package/dist/http/frontend-guest-scope.js +63 -0
  41. package/dist/http/frontend-sessions.d.ts +29 -6
  42. package/dist/http/frontend-sessions.js +108 -28
  43. package/dist/http/host-tools.js +7 -5
  44. package/dist/http/index.d.ts +1 -0
  45. package/dist/http/index.js +1 -0
  46. package/dist/http/memory.js +3 -1
  47. package/dist/http/model-proxy.js +13 -8
  48. package/dist/http/research-tavily.adapter.js +2 -1
  49. package/dist/http/routines.js +7 -5
  50. package/dist/http/terminals.js +5 -0
  51. package/dist/http/tool-catalog.js +1 -1
  52. package/dist/http/xai.js +10 -7
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +2 -2
  55. package/dist/legacy-data-migration.d.ts +0 -12
  56. package/dist/legacy-data-migration.js +21 -13
  57. package/dist/remote-tool-executor.js +2 -1
  58. package/dist/routines/schedule.js +2 -1
  59. package/dist/routines/scheduler.js +6 -7
  60. package/dist/run-lifecycle.js +2 -1
  61. package/dist/store/agent-sessions/sql.js +7 -3
  62. package/dist/store/event-log/sqlite-kernel.js +7 -2
  63. package/dist/store/event-log/sqlite.js +6 -2
  64. package/dist/surface-exchanges.js +3 -3
  65. package/dist/terminal-session.d.ts +51 -20
  66. package/dist/terminal-session.js +10 -94
  67. package/dist/terminal.adapter.d.ts +22 -0
  68. package/dist/terminal.adapter.js +47 -0
  69. package/dist/tool-executor.d.ts +4 -2
  70. package/dist/tool-executor.js +95 -15
  71. package/dist/tool-recovery/recovery.js +5 -3
  72. package/dist/tool-recovery/redaction.js +1 -2
  73. package/dist/tool-recovery/stack.js +5 -1
  74. package/dist/tool-result-media.d.ts +4 -0
  75. package/dist/tool-result-media.js +10 -3
  76. package/dist/tool-result-surfaces.js +10 -9
  77. package/package.json +30 -34
@@ -30,6 +30,7 @@
30
30
  */
31
31
  import { randomUUID } from 'node:crypto';
32
32
  import { createApiError } from '@jini-ai/protocol';
33
+ import { FrontendSessionCapacityError } from '../frontend-session-registry.js';
33
34
  import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
34
35
  import { guardSameOrigin } from '@jini-ai/http-kit';
35
36
  import { createSseResponse } from '@jini-ai/http-kit';
@@ -59,7 +60,8 @@ function reportInternalError(deps, source, error) {
59
60
  * from a bracketed query string) is rejected rather than coerced.
60
61
  */
61
62
  export function parseCapabilityQuery({ raw }, _optional = {}) {
62
- const values = raw === undefined ? [] : (Array.isArray(raw) ? raw : [raw]);
63
+ const listed = Array.isArray(raw) ? raw : [raw];
64
+ const values = raw === undefined ? [] : listed;
63
65
  const capabilities = [];
64
66
  for (const value of values) {
65
67
  if (typeof value !== 'string' || value.trim().length === 0)
@@ -71,20 +73,34 @@ export function parseCapabilityQuery({ raw }, _optional = {}) {
71
73
  /**
72
74
  * Handles one surface's stream from open to close.
73
75
  *
74
- * SSE commits `text/event-stream` headers before anything else can be known, so a bad request is
75
- * reported as one `{type:'error'}` data event and then closed — there is no status-code channel
76
- * left. Same precedent as `run-stream.ts`.
76
+ * Invalid capability queries use a final SSE error event. Capacity admission happens before
77
+ * opening the stream so resource refusals retain a real HTTP status and create no connection.
77
78
  */
78
- export function handleFrontendSessionStream({ req, res, deps }, _optional = {}) {
79
- const capabilities = parseCapabilityQuery({ raw: req.query['capability'] });
79
+ export function handleFrontendSessionStream({ req, res, deps, owner }, _optional = {}) {
80
+ const requested = parseCapabilityQuery({ raw: req.query['capability'] });
81
+ const capabilities = requested?.filter(id => !deps.allowedCapabilities || deps.allowedCapabilities.includes(id)) ?? null;
80
82
  // Assigned below, after the connection exists — `attach` needs something to deliver through, and
81
83
  // `onClose` has to be armed before then so a connection that dies during setup still detaches.
82
84
  let handle;
83
- const connection = createSseResponse({ req, res }, {
84
- // Detach on close rather than on an explicit teardown call: this fires for a closed tab and a
85
- // dropped connection too, which an unregister route would not.
86
- onClose: () => handle?.detach({}),
87
- });
85
+ let expiry;
86
+ // Reserve capacity before committing SSE headers, so a full scope gets a real 429.
87
+ // Delivery cannot occur until the attached token has been sent to the client.
88
+ let connection;
89
+ if (capabilities !== null) {
90
+ handle = attachFrontendSession({ req, deps, capabilities, owner,
91
+ deliver: invocation => connection.send({ data: { type: 'invocation', ...invocation } }),
92
+ cancel: ({ invocationId }) => connection.send({ data: { type: 'cancelled', invocationId } }) });
93
+ }
94
+ try {
95
+ connection = createSseResponse({ req, res }, {
96
+ // Close and expiry release the same quota reservation, without a separate counter lifecycle.
97
+ onClose: () => { clearTimeout(expiry); handle?.detach({}); },
98
+ });
99
+ }
100
+ catch (error) {
101
+ handle?.detach({});
102
+ throw error;
103
+ }
88
104
  if (capabilities === null) {
89
105
  connection.send({
90
106
  data: {
@@ -95,16 +111,22 @@ export function handleFrontendSessionStream({ req, res, deps }, _optional = {})
95
111
  connection.close();
96
112
  return;
97
113
  }
98
- const sessionId = (deps.newSessionId ?? randomUUID)();
99
- handle = deps.registry.attach({ descriptor: { sessionId, capabilities }, deliver: (invocation) => connection.send({ data: { type: 'invocation', ...invocation } }) });
114
+ expiry = expireScopedSession({ owner, handle: handle, close: () => connection.close(), ttl: deps.sessionTtlMs });
100
115
  connection.send({
101
116
  data: {
102
117
  type: 'attached',
103
- sessionId,
118
+ sessionId: handle.sessionId,
104
119
  bindToken: handle.bindToken,
105
120
  }
106
121
  });
107
122
  }
123
+ /** Reserve the registry's capacity before SSE commits headers. Host keys are never client input. */
124
+ function attachFrontendSession({ req, deps, capabilities, owner, deliver, cancel }) {
125
+ const sessionId = (deps.newSessionId ?? randomUUID)();
126
+ const capacityKeys = deps.resolveCapacityKeys?.({ req, ...(owner ? { owner } : {}) });
127
+ return deps.registry.attach({ descriptor: { sessionId, capabilities, ...(owner ? { owner } : {}), ...(capacityKeys ? { capacityKeys } : {}) },
128
+ deliver, cancel });
129
+ }
108
130
  function isRecord(value) {
109
131
  return typeof value === 'object' && value !== null && !Array.isArray(value);
110
132
  }
@@ -119,12 +141,28 @@ function parseFrontendSessionResponse(input) {
119
141
  if (typeof invocationId !== 'string' || invocationId.trim().length === 0) {
120
142
  return err({ error: validationError({ message: 'invocationId must be a non-empty string' }, { issues: [{ path: 'invocationId', message: 'required non-empty string' }] }) });
121
143
  }
122
- const okFlag = input.body['ok'];
144
+ return parseResponseOutcome({ sessionId, invocationId, body: input.body });
145
+ }
146
+ /** Progress is scoped to a pending invocation; only fixed, value-free decisions enter the audit. */
147
+ function parseApprovalProgress({ sessionId, invocationId, phase, decision }, _options = {}) {
148
+ if (decision === undefined)
149
+ return ok({ value: { sessionId, invocationId, phase } });
150
+ if (phase !== 'execution-started' || typeof decision !== 'string' || !['approved', 'declined', 'expired', 'abandoned'].includes(decision)) {
151
+ return err({ error: validationError({ message: 'invalid approval decision' }) });
152
+ }
153
+ return ok({ value: { sessionId, invocationId, phase, decision: decision } });
154
+ }
155
+ /** Identity parsing precedes outcome parsing so progress and results share exactly one scope. */
156
+ function parseResponseOutcome({ sessionId, invocationId, body }, _options = {}) {
157
+ const phase = body['phase'];
158
+ if (phase === 'approval-pending' || phase === 'execution-started')
159
+ return parseApprovalProgress({ sessionId, invocationId, phase, decision: body['decision'] });
160
+ const okFlag = body['ok'];
123
161
  if (typeof okFlag !== 'boolean') {
124
162
  return err({ error: validationError({ message: 'ok must be a boolean' }, { issues: [{ path: 'ok', message: 'required boolean' }] }) });
125
163
  }
126
164
  if (!okFlag) {
127
- const message = input.body['message'];
165
+ const message = body['message'];
128
166
  if (typeof message !== 'string' || message.trim().length === 0) {
129
167
  // A failure with no reason gives the agent nothing to act on, which is how a caller ends up
130
168
  // retrying the same refusal forever.
@@ -132,7 +170,7 @@ function parseFrontendSessionResponse(input) {
132
170
  }
133
171
  return ok({ value: { sessionId, invocationId, ok: false, message } });
134
172
  }
135
- return ok({ value: { sessionId, invocationId, ok: true, output: input.body['output'] } });
173
+ return ok({ value: { sessionId, invocationId, ok: true, output: body['output'] } });
136
174
  }
137
175
  /**
138
176
  * `POST /api/frontend-sessions/:sessionId/responses` — delivers a surface's answer to the tool
@@ -141,6 +179,8 @@ function parseFrontendSessionResponse(input) {
141
179
  */
142
180
  export const frontendSessionResponseRoute = defineJsonRoute({
143
181
  method: 'post', path: FRONTEND_SESSION_RESPONSE_ROUTE_PATH, parse: parseFrontendSessionResponse, handle: ({ input, deps }) => {
182
+ if ('phase' in input)
183
+ return ok({ value: { settled: deps.registry.progress({ sessionId: input.sessionId, invocationId: input.invocationId, pending: input.phase === 'approval-pending', ...(input.decision ? { decision: input.decision } : {}) }) } });
144
184
  const settled = deps.registry.settle({ sessionId: input.sessionId, invocationId: input.invocationId, outcome: input.ok
145
185
  ? { ok: true, output: input.output }
146
186
  : { ok: false, message: input.message } });
@@ -156,25 +196,65 @@ export const frontendSessionResponseRoute = defineJsonRoute({
156
196
  * never gets a stream (and, notably, never mints a session id or `bindToken` either).
157
197
  */
158
198
  export function registerFrontendSessionRoutes({ app, deps, adapter }, _optional = {}) {
159
- app.get(FRONTEND_SESSION_STREAM_ROUTE_PATH, (req, res) => {
160
- const origin = guardSameOrigin({ req, origin: adapter });
199
+ const prefix = deps.routePrefix ?? '/api/frontend-sessions';
200
+ app.get(`${prefix}/stream`, async (req, res) => {
201
+ // Scoped hosts authenticate origin together with guest/tab ownership in resolveOwner.
202
+ const origin = deps.resolveOwner ? ok({ value: undefined }) : guardSameOrigin({ req, origin: adapter });
161
203
  if (!origin.ok) {
162
204
  sendApiError({ res, status: statusForError({ error: origin.error }), error: origin.error });
163
205
  return;
164
206
  }
165
207
  try {
166
- handleFrontendSessionStream({ req, res, deps });
208
+ const owner = deps.resolveOwner ? await deps.resolveOwner({ req, res }) : undefined;
209
+ if (owner === null) {
210
+ res.sendStatus(403);
211
+ return;
212
+ }
213
+ handleFrontendSessionStream({ req, res, deps, ...(owner ? { owner } : {}) });
167
214
  }
168
215
  catch (error) {
169
- reportInternalError(deps, 'stream', error);
170
- if (!res.headersSent) {
171
- res.status(500).json({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }) });
172
- }
173
- else if (!res.writableEnded) {
174
- res.end();
175
- }
216
+ endStreamOnError({ error, res, deps });
176
217
  }
177
218
  });
178
- mountJsonRoute({ app, spec: frontendSessionResponseRoute, deps, adapter });
219
+ const responsePath = `${prefix}/:sessionId/responses`;
220
+ if (deps.resolveOwner)
221
+ app.post(responsePath, async (req, res, next) => {
222
+ try {
223
+ const owner = await deps.resolveOwner({ req, res });
224
+ const bindToken = req.get('x-frontend-bind-token') ?? '';
225
+ if (!owner || !deps.registry.ownsSession({ sessionId: String(req.params['sessionId']), owner, bindToken })) {
226
+ res.sendStatus(403);
227
+ return;
228
+ }
229
+ next();
230
+ }
231
+ catch {
232
+ res.sendStatus(403);
233
+ }
234
+ });
235
+ mountJsonRoute({ app, spec: { ...frontendSessionResponseRoute, path: responsePath, requireSameOrigin: !deps.resolveOwner }, deps, adapter });
236
+ }
237
+ /** Capacity is ordinary admission refusal, not a loggable internal failure. */
238
+ function endStreamOnError({ error, res, deps }) {
239
+ if (error instanceof FrontendSessionCapacityError && !res.headersSent) {
240
+ res.sendStatus(429);
241
+ return;
242
+ }
243
+ reportInternalError(deps, 'stream', error);
244
+ if (res.headersSent) {
245
+ if (!res.writableEnded)
246
+ res.end();
247
+ }
248
+ else {
249
+ res.status(500).json({ error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }) });
250
+ }
251
+ }
252
+ /** Expiry tears down addressing and the stream together, including clients which never disconnect. */
253
+ function expireScopedSession({ owner, handle, close, ttl = 30 * 60_000 }) {
254
+ if (!owner)
255
+ return undefined;
256
+ const timer = setTimeout(() => { handle.detach({}); close(); }, ttl);
257
+ timer.unref();
258
+ return timer;
179
259
  }
180
260
  //# sourceMappingURL=frontend-sessions.js.map
@@ -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 = {}) {
@@ -57,4 +57,5 @@ export { ATTACHMENT_LOOKUP_ROUTE_PATH, registerAttachmentLookupRoutes } from './
57
57
  export type { RemoteAttachmentLookupOptional, RemoteAttachmentLookupRequired } from '../remote-attachment-lookup.js';
58
58
  export { createRemoteAttachmentLookup } from '../remote-attachment-lookup.js';
59
59
  export { createTavilyResearchProvider } from './research-tavily.adapter.js';
60
+ export { createFrontendGuestScope } from './frontend-guest-scope.js';
60
61
  //# sourceMappingURL=index.d.ts.map
@@ -30,4 +30,5 @@ export { DEFAULT_TOOL_EXEC_MAX_BODY_BYTES, DEFAULT_TOOL_EXEC_TOKEN_ENV_VAR, regi
30
30
  export { ATTACHMENT_LOOKUP_ROUTE_PATH, registerAttachmentLookupRoutes } from './attachment-lookup-routes.js';
31
31
  export { createRemoteAttachmentLookup } from '../remote-attachment-lookup.js';
32
32
  export { createTavilyResearchProvider } from './research-tavily.adapter.js';
33
+ export { createFrontendGuestScope } from './frontend-guest-scope.js';
33
34
  //# sourceMappingURL=index.js.map
@@ -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);
@@ -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 });
package/dist/index.d.ts CHANGED
@@ -72,8 +72,8 @@
72
72
  * solution belongs at the lifecycle or transport boundary, rather than a
73
73
  * restart-unsafe recorder cache that makes the two paths disagree.
74
74
  *
75
- * node-pty uses native addons. Linux deployment requires python3, make and a
76
- * C/C++ compiler during installation; source compilation is the fallback.
75
+ * The opt-in @jini-ai/daemon/terminal adapter uses node-pty's native addon.
76
+ * Linux hosts selecting it need python3, make and a C/C++ compiler for fallback compilation.
77
77
  * Vendoring foreign prebuilt binaries would create an unowned maintenance burden.
78
78
  * See the owning modules and packages/daemon/README.md for detailed contracts.
79
79
  */
package/dist/index.js CHANGED
@@ -72,8 +72,8 @@
72
72
  * solution belongs at the lifecycle or transport boundary, rather than a
73
73
  * restart-unsafe recorder cache that makes the two paths disagree.
74
74
  *
75
- * node-pty uses native addons. Linux deployment requires python3, make and a
76
- * C/C++ compiler during installation; source compilation is the fallback.
75
+ * The opt-in @jini-ai/daemon/terminal adapter uses node-pty's native addon.
76
+ * Linux hosts selecting it need python3, make and a C/C++ compiler for fallback compilation.
77
77
  * Vendoring foreign prebuilt binaries would create an unowned maintenance burden.
78
78
  * See the owning modules and packages/daemon/README.md for detailed contracts.
79
79
  */
@@ -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;
@@ -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
  // ---------------------------------------------------------------------------