@addai/node 0.30.16-msp.8 → 0.30.16-msp.9

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.
@@ -16,6 +16,9 @@ export interface ClaudePrintInput {
16
16
  allowedTools?: string[];
17
17
  /** Write the CLI's debug log here, to time each app's connection. */
18
18
  debugFile?: string;
19
+ /** Start now and take the prompt later through sendPrompt, so the CLI's
20
+ * start-up overlaps whatever the prompt is still waiting on. */
21
+ deferPrompt?: boolean;
19
22
  }
20
23
  /**
21
24
  * findClaudeBinary for the spawn path. The lookup runs `where` or a login shell
@@ -51,6 +54,8 @@ export type ClaudePrintInputBlock = {
51
54
  };
52
55
  };
53
56
  /** Stream JSON reaches stdin only; files never become command-line arguments. */
57
+ /** The one stdin line a run started with deferPrompt gets. */
58
+ export declare function claudePrintPromptMessage(prompt: string): string;
54
59
  export declare function claudePrintInputMessage(prompt: string, blocks: readonly ClaudePrintInputBlock[]): string;
55
60
  /**
56
61
  * The text to emit for one flushed block, given what came before.
@@ -73,6 +78,11 @@ export interface ClaudePrintHandle {
73
78
  onEvent(cb: (e: RuntimeEvent) => void): void;
74
79
  /** Resolves with exit code; rejects on spawn error. */
75
80
  done: Promise<number>;
81
+ /** deferPrompt only. Closes stdin behind the message, so from here the run
82
+ * ends the way any other run does: one turn, then the process exits. */
83
+ sendPrompt?(prompt: string): void;
84
+ /** True once the process has gone, so a deferred start can be passed over. */
85
+ hasExited(): boolean;
76
86
  }
77
87
  /**
78
88
  * Parse one stream-json line into 0+ RuntimeEvents.
@@ -40,6 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.cachedClaudeBinary = cachedClaudeBinary;
42
42
  exports.parseMcpTimings = parseMcpTimings;
43
+ exports.claudePrintPromptMessage = claudePrintPromptMessage;
43
44
  exports.claudePrintInputMessage = claudePrintInputMessage;
44
45
  exports.blockDelta = blockDelta;
45
46
  exports.lineToEvents = lineToEvents;
@@ -104,6 +105,10 @@ function parseMcpTimings(log) {
104
105
  };
105
106
  }
106
107
  /** Stream JSON reaches stdin only; files never become command-line arguments. */
108
+ /** The one stdin line a run started with deferPrompt gets. */
109
+ function claudePrintPromptMessage(prompt) {
110
+ return JSON.stringify({ type: 'user', session_id: '', parent_tool_use_id: null, message: { role: 'user', content: [{ type: 'text', text: prompt }] } }) + '\n';
111
+ }
107
112
  function claudePrintInputMessage(prompt, blocks) {
108
113
  if (!Array.isArray(blocks) || blocks.length < 1 || blocks.length > 12 || !blocks.every(b => b && (b.type === 'text' && typeof b.text === 'string' || ['image', 'document'].includes(b.type) && 'source' in b && b.source?.type === 'base64' && typeof b.source.data === 'string' && /^[A-Za-z0-9+/]*={0,2}$/.test(b.source.data)
109
114
  && (b.type === 'document' ? b.source.media_type === 'application/pdf' : ['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(b.source.media_type)))))
@@ -241,7 +246,7 @@ function spawnClaudePrint(input) {
241
246
  args.push('--include-partial-messages');
242
247
  if (input.debugFile)
243
248
  args.push('--debug-file', input.debugFile);
244
- if (input.inputBlocks)
249
+ if (input.inputBlocks || input.deferPrompt)
245
250
  args.push('--input-format', 'stream-json');
246
251
  if (input.permissionMode === 'bypassPermissions') {
247
252
  args.push('--dangerously-skip-permissions');
@@ -308,15 +313,28 @@ function spawnClaudePrint(input) {
308
313
  // Mark handled now — the caller awaits `done` a few ticks later, and an
309
314
  // already-rejected promise with no handler would fire unhandledRejection.
310
315
  done.catch(() => { });
311
- return { pid: undefined, sessionId: null, kill: () => { }, onEvent: () => { }, done };
316
+ return { pid: undefined, sessionId: null, kill: () => { }, onEvent: () => { }, done, hasExited: () => true };
312
317
  }
313
- (0, prompt_transport_1.feedStdin)(proc, transport.stdin);
318
+ // A child that dies before its deferred prompt turns the later write into
319
+ // EPIPE; its exit reports that, same as feedStdin.
320
+ if (input.deferPrompt)
321
+ proc.stdin?.on('error', () => { });
322
+ else
323
+ (0, prompt_transport_1.feedStdin)(proc, transport.stdin);
314
324
  const listeners = [];
315
- const emit = (e) => { for (const l of listeners)
316
- try {
317
- l(e);
325
+ // A deferred start can say something before the run has attached to it.
326
+ const unheard = [];
327
+ const emit = (e) => {
328
+ if (input.deferPrompt && listeners.length === 0) {
329
+ unheard.push(e);
330
+ return;
318
331
  }
319
- catch { } };
332
+ for (const l of listeners)
333
+ try {
334
+ l(e);
335
+ }
336
+ catch { }
337
+ };
320
338
  let sessionId = null;
321
339
  let buffer = '';
322
340
  // Text-delta coalescer (partial-stream mode). Token deltas arrive many times
@@ -447,9 +465,11 @@ function spawnClaudePrint(input) {
447
465
  proc.stderr?.on('data', (chunk) => {
448
466
  stderrBuf += chunk.toString('utf8');
449
467
  });
468
+ let exited = false;
450
469
  const done = new Promise((resolve, reject) => {
451
- proc.once('error', reject);
470
+ proc.once('error', (err) => { exited = true; reject(err); });
452
471
  proc.once('exit', (code) => {
472
+ exited = true;
453
473
  sysPrompt.cleanup();
454
474
  flushThinking();
455
475
  flushText();
@@ -460,11 +480,24 @@ function spawnClaudePrint(input) {
460
480
  resolve(code ?? -1);
461
481
  });
462
482
  });
483
+ // Nothing awaits a deferred start's done until its prompt is ready, and a
484
+ // rejection nobody has handled yet would take the whole daemon down.
485
+ if (input.deferPrompt)
486
+ done.catch(() => { });
463
487
  return {
464
488
  pid: proc.pid,
465
489
  get sessionId() { return sessionId; },
466
490
  kill: () => { (0, win_1.killProcessTree)(proc); },
467
- onEvent: (cb) => { listeners.push(cb); },
491
+ onEvent: (cb) => {
492
+ listeners.push(cb);
493
+ for (const e of unheard.splice(0))
494
+ try {
495
+ cb(e);
496
+ }
497
+ catch { }
498
+ },
468
499
  done,
500
+ hasExited: () => exited,
501
+ ...(input.deferPrompt ? { sendPrompt: (prompt) => { proc.stdin?.end(claudePrintPromptMessage(prompt), 'utf8'); } } : {}),
469
502
  };
470
503
  }
@@ -1,3 +1,4 @@
1
+ import { type ClaudePrintHandle } from './claude-print';
1
2
  import { type EntityboxQueueServices } from './entitybox-queue-request';
2
3
  import { type InstallResult } from './install';
3
4
  import { fetchMemoryPack } from './memory-pack';
@@ -130,6 +131,14 @@ export interface RuntimeRequestRow {
130
131
  * the spawn. Resolves null for every entity without MSP switched on, and
131
132
  * is undefined for runs it can't apply to. See model-selector.ts. */
132
133
  mspDecision?: Promise<MspDecision | null>;
134
+ /** NOT a DB column. What applyModelSelection decided: it publishes, so it
135
+ * runs once however many steps ask. */
136
+ mspApplied?: Promise<MspDecision | null>;
137
+ /** NOT a DB column. A CLI startClaudeEarly has running, waiting for its prompt. */
138
+ earlyStart?: {
139
+ handle: ClaudePrintHandle;
140
+ at: number;
141
+ };
133
142
  }
134
143
  export declare function notifyFlowsIfFlowRequest(requestId: string): Promise<void>;
135
144
  export declare function notifyWidgetstudioIfWidgetRequest(requestId: string): Promise<void>;
@@ -145,6 +154,9 @@ export declare function notifySelfImproveIfEnabled(requestId: string): Promise<v
145
154
  *
146
155
  * Exported for tests. */
147
156
  export declare function resumeSessionIsVoid(workingDirectory: string | null | undefined, resumeSessionId: string | null | undefined, cwdSubstituted: boolean): boolean;
157
+ /** The selector's early_start setting, on a turn it can work for. A new session
158
+ * carries its memories in the system prompt, so that can't start before they land. */
159
+ export declare function earlyStartWanted(req: Pick<RuntimeRequestRow, 'agent' | 'mode' | 'prompt' | 'resume_session_id' | 'entitybox_file_request'>, config: unknown): boolean;
148
160
  type MemoryPack = Awaited<ReturnType<typeof fetchMemoryPack>>;
149
161
  /**
150
162
  * Setup for a turn with the model selector on. It does what the block in
@@ -153,6 +165,6 @@ type MemoryPack = Awaited<ReturnType<typeof fetchMemoryPack>>;
153
165
  * turn. Claude reuses a resumed session's first system prompt, so its memory
154
166
  * pack and pointer there are only read again after a compaction.
155
167
  */
156
- export declare function prepareFastSetup(req: RuntimeRequestRow, cwd: string, installed: InstallResult, pack: Promise<MemoryPack>, startedAt: number): Promise<void>;
168
+ export declare function prepareFastSetup(req: RuntimeRequestRow, cwd: string, installed: InstallResult, pack: Promise<MemoryPack>, startedAt: number, onSystemPromptReady?: () => Promise<void>): Promise<void>;
157
169
  export declare function runRequest(req: RuntimeRequestRow, entitybox?: EntityboxQueueServices): Promise<void>;
158
170
  export {};
@@ -43,6 +43,7 @@ exports.notifyFlowsIfFlowRequest = notifyFlowsIfFlowRequest;
43
43
  exports.notifyWidgetstudioIfWidgetRequest = notifyWidgetstudioIfWidgetRequest;
44
44
  exports.notifySelfImproveIfEnabled = notifySelfImproveIfEnabled;
45
45
  exports.resumeSessionIsVoid = resumeSessionIsVoid;
46
+ exports.earlyStartWanted = earlyStartWanted;
46
47
  exports.prepareFastSetup = prepareFastSetup;
47
48
  exports.runRequest = runRequest;
48
49
  const fs = __importStar(require("fs"));
@@ -1647,7 +1648,10 @@ function publishModelSelection(req, d, changed) {
1647
1648
  }
1648
1649
  /** This turn's model and effort, when MSP is on. Written onto req so a guardrail
1649
1650
  * retry turn stays on the same model. Call it just before the spawn. */
1650
- async function applyModelSelection(req) {
1651
+ function applyModelSelection(req) {
1652
+ return req.mspApplied ??= selectModelOnce(req);
1653
+ }
1654
+ async function selectModelOnce(req) {
1651
1655
  const msp = req.mspDecision ? await req.mspDecision : null;
1652
1656
  if (!msp)
1653
1657
  return null;
@@ -1681,12 +1685,55 @@ function reportMcpTimings(req, file) {
1681
1685
  catch { /* best-effort */ }
1682
1686
  }
1683
1687
  // ── claude --print ─────────────────────────────────────────────────────────
1688
+ function mcpDebugFileFor(req, msp) {
1689
+ return msp?.mcp_timing ? path.join(os.tmpdir(), `ainode-mcp-${req.id}.log`) : null;
1690
+ }
1691
+ function claudePrintInputFor(req, cwd, installed, msp) {
1692
+ const debugFile = mcpDebugFileFor(req, msp);
1693
+ return {
1694
+ workingDirectory: cwd,
1695
+ prompt: req.prompt ?? '',
1696
+ permissionMode: req.agent === 'claude-bypass' ? 'bypassPermissions' : (req.permission_mode ?? undefined),
1697
+ effortLevel: req.effort ?? undefined,
1698
+ model: (0, model_filter_1.modelForAgent)('claude', req.model),
1699
+ mcpConfigPath: installed.mcpConfigPath ?? undefined,
1700
+ resumeSessionId: req.resume_session_id ?? undefined,
1701
+ appendSystemPrompt: msp ? (0, time_context_1.appendChatTimeContext)(req.system_prompt) : (0, time_context_1.appendTimeContext)(req.system_prompt),
1702
+ allowedTools: req.allowed_tools ?? undefined,
1703
+ ...(debugFile ? { debugFile } : {}),
1704
+ };
1705
+ }
1706
+ /** The selector's early_start setting, on a turn it can work for. A new session
1707
+ * carries its memories in the system prompt, so that can't start before they land. */
1708
+ function earlyStartWanted(req, config) {
1709
+ if (process.env.ADDAI_EARLY_START === '0')
1710
+ return false;
1711
+ if (config?.early_start !== true)
1712
+ return false;
1713
+ return req.agent === 'claude' && req.mode === 'print' && !!req.prompt && !!req.resume_session_id
1714
+ && req.entitybox_file_request !== true;
1715
+ }
1716
+ /**
1717
+ * Start the CLI while the memory lookup is still running. Its start-up doesn't
1718
+ * need the message, so the two can overlap instead of one waiting for the other.
1719
+ * It's still one process for one turn, gone when the turn ends, so nothing stays
1720
+ * running on the node. If this fails, runClaudePrint spawns the normal way.
1721
+ */
1722
+ async function startClaudeEarly(req, cwd, installed) {
1723
+ try {
1724
+ const msp = await applyModelSelection(req);
1725
+ const handle = (0, claude_print_1.spawnClaudePrint)({ ...claudePrintInputFor(req, cwd, installed, msp), prompt: '', deferPrompt: true });
1726
+ req.earlyStart = { handle, at: Date.now() };
1727
+ }
1728
+ catch (err) {
1729
+ console.error(`[setup] early start failed [reqId=${req.id}]: ${err.message}`);
1730
+ }
1731
+ }
1684
1732
  async function runClaudePrint(req, cwd, installed) {
1685
1733
  if (!req.prompt) {
1686
1734
  await setStatus(req.id, { status: 'failed', errorCode: 'no_prompt', errorMessage: 'print mode requires a prompt' });
1687
1735
  return;
1688
1736
  }
1689
- const permissionMode = req.agent === 'claude-bypass' ? 'bypassPermissions' : (req.permission_mode ?? undefined);
1690
1737
  // Floor for "which subagent transcripts belong to this attempt" — see
1691
1738
  // subagent-usage.ts. Taken before the spawn so nothing this run writes
1692
1739
  // can fall outside the window.
@@ -1712,22 +1759,24 @@ async function runClaudePrint(req, cwd, installed) {
1712
1759
  }
1713
1760
  }
1714
1761
  const msp = await applyModelSelection(req);
1715
- const mcpDebugFile = msp?.mcp_timing ? path.join(os.tmpdir(), `ainode-mcp-${req.id}.log`) : null;
1762
+ const mcpDebugFile = mcpDebugFileFor(req, msp);
1763
+ // A CLI that started early and has already gone (a session it can't resume, a
1764
+ // bad flag) is passed over, so the normal spawn gives this turn its usual chance.
1765
+ const early = req.earlyStart && !req.earlyStart.handle.hasExited() ? req.earlyStart : null;
1766
+ req.earlyStart = undefined;
1716
1767
  let handle;
1717
1768
  try {
1718
- handle = (0, claude_print_1.spawnClaudePrint)({
1719
- workingDirectory: cwd,
1720
- prompt: req.prompt,
1721
- ...(inputBlocks ? { inputBlocks } : {}),
1722
- permissionMode,
1723
- effortLevel: req.effort ?? undefined,
1724
- model: (0, model_filter_1.modelForAgent)('claude', req.model),
1725
- mcpConfigPath: installed.mcpConfigPath ?? undefined,
1726
- resumeSessionId: req.resume_session_id ?? undefined,
1727
- appendSystemPrompt: msp ? (0, time_context_1.appendChatTimeContext)(req.system_prompt) : (0, time_context_1.appendTimeContext)(req.system_prompt),
1728
- allowedTools: req.allowed_tools ?? undefined,
1729
- ...(mcpDebugFile ? { debugFile: mcpDebugFile } : {}),
1730
- });
1769
+ if (early) {
1770
+ handle = early.handle;
1771
+ handle.sendPrompt?.(req.prompt);
1772
+ void emit(req.id, 'early_start', { lead_ms: Date.now() - early.at });
1773
+ }
1774
+ else {
1775
+ handle = (0, claude_print_1.spawnClaudePrint)({
1776
+ ...claudePrintInputFor(req, cwd, installed, msp),
1777
+ ...(inputBlocks ? { inputBlocks } : {}),
1778
+ });
1779
+ }
1731
1780
  }
1732
1781
  catch (err) {
1733
1782
  await failRequest(req, 'spawn_failed', err.message);
@@ -2393,7 +2442,7 @@ function launchAppsDirectly(cwd, installed) {
2393
2442
  * turn. Claude reuses a resumed session's first system prompt, so its memory
2394
2443
  * pack and pointer there are only read again after a compaction.
2395
2444
  */
2396
- async function prepareFastSetup(req, cwd, installed, pack, startedAt) {
2445
+ async function prepareFastSetup(req, cwd, installed, pack, startedAt, onSystemPromptReady) {
2397
2446
  const resumedClaude = req.agent !== 'codex' && !!req.resume_session_id;
2398
2447
  const timing = { pack_ms: null };
2399
2448
  try {
@@ -2452,6 +2501,8 @@ async function prepareFastSetup(req, cwd, installed, pack, startedAt) {
2452
2501
  let recall = 'system_prompt';
2453
2502
  if (resumedClaude) {
2454
2503
  const decision = await req.mspDecision;
2504
+ // Only the message changes from here on, and that's what the pack is for.
2505
+ await onSystemPromptReady?.();
2455
2506
  const found = await (decision?.picked === 'quick'
2456
2507
  ? Promise.race([pack, new Promise(done => setTimeout(() => done(null), QUICK_RECALL_WAIT_MS))])
2457
2508
  : pack);
@@ -2603,10 +2654,11 @@ async function runRequest(req, entitybox) {
2603
2654
  skills_installed: installed.skillSlugs,
2604
2655
  });
2605
2656
  }
2606
- const fast = !!earlyPack && !!(await mspSetting);
2657
+ const fastSetting = earlyPack ? await mspSetting : null;
2658
+ const fast = !!fastSetting;
2607
2659
  if (fast) {
2608
2660
  try {
2609
- await prepareFastSetup(req, cwd, installed, earlyPack, startedAt);
2661
+ await prepareFastSetup(req, cwd, installed, earlyPack, startedAt, earlyStartWanted(req, fastSetting?.config) ? () => startClaudeEarly(req, cwd, installed) : undefined);
2610
2662
  }
2611
2663
  catch (err) {
2612
2664
  console.error(`[setup] fast setup failed [reqId=${req.id}]: ${err.message}`);
@@ -2700,6 +2752,8 @@ async function runRequest(req, entitybox) {
2700
2752
  }
2701
2753
  }
2702
2754
  finally {
2755
+ // An early start nothing went on to use. The turn is over, so it must not outlive it.
2756
+ req.earlyStart?.handle.kill();
2703
2757
  // Best-effort cleanup of installed files. If the request crashed
2704
2758
  // mid-flight we may leak files; next install run for the same cwd
2705
2759
  // will overwrite them.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.30.16-msp.8",
3
+ "version": "0.30.16-msp.9",
4
4
  "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
5
  "license": "MIT",
6
6
  "keywords": [