@addai/node 0.30.16-msp.7 → 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
  }
@@ -0,0 +1,22 @@
1
+ export interface InstalledCopy {
2
+ version: string;
3
+ bin: string;
4
+ }
5
+ interface McpEntry {
6
+ command: string;
7
+ args: string[] | null;
8
+ }
9
+ /** The package an `npx -y @addai/x[@latest]` entry runs. Null for any other
10
+ * entry, including one pinned to a version, which npx should keep resolving. */
11
+ export declare function npxPackage(entry: McpEntry): string | null;
12
+ /** True when a is a higher release than b. A prerelease counts as its release. */
13
+ export declare function isNewer(a: string, b: string): boolean;
14
+ /** The newest complete copy of a package in npx's cache. */
15
+ export declare function findInstalledCopy(pkg: string, roots?: string[]): InstalledCopy | null;
16
+ /** What the registry last said is latest, or null if it hasn't said yet. Asks
17
+ * again in the background once an hour, so it never holds a turn back. */
18
+ export declare function latestVersion(pkg: string): string | null;
19
+ /** The entries with our npx apps switched to their installed copies. The same
20
+ * array when there's nothing to switch, and the same object for every other entry. */
21
+ export declare function directMcps<T extends McpEntry>(mcps: T[], find?: (pkg: string) => InstalledCopy | null, latest?: (pkg: string) => string | null): T[];
22
+ export {};
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ // Starting our own apps without asking the npm registry first.
3
+ //
4
+ // An app registered as `npx -y @addai/x@latest` checks the registry before it
5
+ // starts, which makes it the slowest app on a turn: 1.7 s on PopadomTV, against
6
+ // 0.4-0.8 s for each remote app, and the agent waits for all of them. npx has
7
+ // already unpacked the package, so selector turns run that copy directly.
8
+ //
9
+ // `@latest` still means latest. The registry is asked in the background, and when
10
+ // it knows of a newer version than the copy, the app goes through npx for that
11
+ // turn, which updates the copy. Anything unexpected leaves the entry on npx, as it
12
+ // was. ADDAI_MCP_DIRECT=0 turns all of this off.
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.npxPackage = npxPackage;
48
+ exports.isNewer = isNewer;
49
+ exports.findInstalledCopy = findInstalledCopy;
50
+ exports.latestVersion = latestVersion;
51
+ exports.directMcps = directMcps;
52
+ const fs = __importStar(require("fs"));
53
+ const os = __importStar(require("os"));
54
+ const path = __importStar(require("path"));
55
+ const OUR_PACKAGE = /^(@addai\/[a-z0-9._-]+)(?:@latest)?$/;
56
+ const LATEST_TTL_MS = 60 * 60 * 1000;
57
+ const LATEST_TIMEOUT_MS = 5000;
58
+ /** The package an `npx -y @addai/x[@latest]` entry runs. Null for any other
59
+ * entry, including one pinned to a version, which npx should keep resolving. */
60
+ function npxPackage(entry) {
61
+ const { command, args } = entry;
62
+ if (command !== 'npx' || !Array.isArray(args) || args.length !== 2 || args[0] !== '-y')
63
+ return null;
64
+ return OUR_PACKAGE.exec(args[1])?.[1] ?? null;
65
+ }
66
+ /** True when a is a higher release than b. A prerelease counts as its release. */
67
+ function isNewer(a, b) {
68
+ const parts = (v) => v.split('-')[0].split('.').map(n => parseInt(n, 10) || 0);
69
+ const [x, y] = [parts(a), parts(b)];
70
+ for (let i = 0; i < 3; i++) {
71
+ if ((x[i] ?? 0) !== (y[i] ?? 0))
72
+ return (x[i] ?? 0) > (y[i] ?? 0);
73
+ }
74
+ return false;
75
+ }
76
+ function npxRoots() {
77
+ const roots = [];
78
+ if (process.env.npm_config_cache)
79
+ roots.push(path.join(process.env.npm_config_cache, '_npx'));
80
+ if (process.platform === 'win32' && process.env.LOCALAPPDATA) {
81
+ roots.push(path.join(process.env.LOCALAPPDATA, 'npm-cache', '_npx'));
82
+ }
83
+ roots.push(path.join(os.homedir(), '.npm', '_npx'));
84
+ return roots;
85
+ }
86
+ function readCopy(modules, pkg) {
87
+ try {
88
+ const dir = path.join(modules, ...pkg.split('/'));
89
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
90
+ const bins = typeof manifest.bin === 'string'
91
+ ? { [pkg.split('/')[1]]: manifest.bin }
92
+ : manifest.bin ?? {};
93
+ const names = Object.keys(bins);
94
+ const bin = bins[pkg.split('/')[1]] ?? (names.length === 1 ? bins[names[0]] : undefined);
95
+ if (typeof bin !== 'string' || typeof manifest.version !== 'string')
96
+ return null;
97
+ const entry = path.resolve(dir, bin);
98
+ if (!fs.existsSync(entry))
99
+ return null;
100
+ // A half-finished install can hold the package without what it needs.
101
+ for (const dep of Object.keys(manifest.dependencies ?? {})) {
102
+ if (!fs.existsSync(path.join(modules, ...dep.split('/'), 'package.json')))
103
+ return null;
104
+ }
105
+ return { version: manifest.version, bin: entry };
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ /** The newest complete copy of a package in npx's cache. */
112
+ function findInstalledCopy(pkg, roots = npxRoots()) {
113
+ let best = null;
114
+ for (const root of roots) {
115
+ let dirs = [];
116
+ try {
117
+ dirs = fs.readdirSync(root);
118
+ }
119
+ catch {
120
+ continue;
121
+ }
122
+ for (const dir of dirs) {
123
+ const copy = readCopy(path.join(root, dir, 'node_modules'), pkg);
124
+ if (copy && (!best || isNewer(copy.version, best.version)))
125
+ best = copy;
126
+ }
127
+ }
128
+ return best;
129
+ }
130
+ const latestKnown = new Map();
131
+ /** What the registry last said is latest, or null if it hasn't said yet. Asks
132
+ * again in the background once an hour, so it never holds a turn back. */
133
+ function latestVersion(pkg) {
134
+ const known = latestKnown.get(pkg);
135
+ if (known && Date.now() - known.at < LATEST_TTL_MS)
136
+ return known.version;
137
+ latestKnown.set(pkg, { version: known?.version ?? null, at: Date.now() });
138
+ void fetch(`https://registry.npmjs.org/${pkg.replace('/', '%2f')}/latest`, { signal: AbortSignal.timeout(LATEST_TIMEOUT_MS) })
139
+ .then(r => (r.ok ? r.json() : null))
140
+ .then(body => {
141
+ if (typeof body?.version === 'string')
142
+ latestKnown.set(pkg, { version: body.version, at: Date.now() });
143
+ })
144
+ .catch(() => { });
145
+ return known?.version ?? null;
146
+ }
147
+ /** The entries with our npx apps switched to their installed copies. The same
148
+ * array when there's nothing to switch, and the same object for every other entry. */
149
+ function directMcps(mcps, find = findInstalledCopy, latest = latestVersion) {
150
+ let changed = false;
151
+ const out = mcps.map(m => {
152
+ const pkg = npxPackage(m);
153
+ if (!pkg)
154
+ return m;
155
+ const copy = find(pkg);
156
+ const newest = latest(pkg);
157
+ if (!copy || (newest && isNewer(newest, copy.version)))
158
+ return m;
159
+ changed = true;
160
+ return { ...m, command: process.execPath, args: [copy.bin] };
161
+ });
162
+ return changed ? out : mcps;
163
+ }
@@ -11,11 +11,17 @@ export declare function fetchMemoryPack(baseUrl: string, serviceToken: string, a
11
11
  prompt: string;
12
12
  budgetTokens?: number;
13
13
  memberProfileId?: string | null;
14
- }, timeoutMs?: number): Promise<{
14
+ }, timeoutMs?: number, fn?: string): Promise<{
15
15
  block: string;
16
16
  degraded: boolean;
17
17
  flagged: boolean;
18
18
  }>;
19
+ export declare function fetchMemoryPackVia(copy: unknown, baseUrl: string, serviceToken: string, args: {
20
+ entityId: string;
21
+ prompt: string;
22
+ budgetTokens?: number;
23
+ memberProfileId?: string | null;
24
+ }): ReturnType<typeof fetchMemoryPack>;
19
25
  export declare function resolvePackEntityId(req: {
20
26
  entity_studio_entity_id?: string | null;
21
27
  entity_id?: string | null;
@@ -4,6 +4,7 @@ exports.MEMORY_FALLBACK_MARKER = void 0;
4
4
  exports.replaceMemoryBlock = replaceMemoryBlock;
5
5
  exports.buildPackBody = buildPackBody;
6
6
  exports.fetchMemoryPack = fetchMemoryPack;
7
+ exports.fetchMemoryPackVia = fetchMemoryPackVia;
7
8
  exports.resolvePackEntityId = resolvePackEntityId;
8
9
  exports.applyMemoryPack = applyMemoryPack;
9
10
  // +Ai Memory (Task 7) — semantic memory pack fetcher.
@@ -68,9 +69,9 @@ function buildPackBody(args) {
68
69
  async function fetchMemoryPack(baseUrl, serviceToken, args,
69
70
  // edge fn steady-state ~600ms, cold start ~950ms; 3s bounds worst-case
70
71
  // spawn latency
71
- timeoutMs = 3000) {
72
+ timeoutMs = 3000, fn = 'entity-memory-pack') {
72
73
  try {
73
- const r = await fetch(`${baseUrl}/functions/v1/entity-memory-pack`, {
74
+ const r = await fetch(`${baseUrl}/functions/v1/${fn}`, {
74
75
  method: 'POST',
75
76
  headers: {
76
77
  'content-type': 'application/json',
@@ -98,6 +99,17 @@ timeoutMs = 3000) {
98
99
  return { block: exports.MEMORY_FALLBACK_MARKER, degraded: true, flagged: false };
99
100
  }
100
101
  }
102
+ // A selector setting can name a test copy of the pack, for proving a change to
103
+ // recall on one entity. If the copy fails, the real pack still answers.
104
+ const PACK_COPY = /^entity-memory-pack-[a-z0-9-]{1,40}$/;
105
+ async function fetchMemoryPackVia(copy, baseUrl, serviceToken, args) {
106
+ if (typeof copy === 'string' && PACK_COPY.test(copy)) {
107
+ const pack = await fetchMemoryPack(baseUrl, serviceToken, args, 3000, copy);
108
+ if (!pack.degraded)
109
+ return pack;
110
+ }
111
+ return fetchMemoryPack(baseUrl, serviceToken, args);
112
+ }
101
113
  // +Ai Memory — runtime "entity_id alias" fix (final phase-3 review finding).
102
114
  //
103
115
  // runtime_pick_next_request (addai-entity-studio/supabase/migrations/
@@ -53,29 +53,67 @@ const path = __importStar(require("path"));
53
53
  /** Beside the session's other per-chat files. Holds hashes, no memory text. */
54
54
  const SEEN_FILENAME = '.ainode.memory-seen.json';
55
55
  const MAX_SEEN = 300;
56
- /** The most one turn can add, so recall never becomes the bulk of a message. */
57
- const MAX_LINES = 8;
56
+ /** The most one turn can add, so recall never becomes the bulk of a message. A
57
+ * single memory can run to several lines, so the characters are the real limit. */
58
+ const MAX_LINES = 16;
58
59
  const MAX_CHARS = 2400;
59
60
  // The pack sections that answer this message. The rest (working memory, recent,
60
61
  // the person) don't change with the message, and the chat already carries them.
61
- const RECALL_SECTIONS = new Set(['Corrections', 'Relevant memories', 'Related (from your memory graph)']);
62
+ // The graph's links are bare keywords, nothing that can answer anything.
63
+ const RECALL_SECTIONS = new Set(['Corrections', 'Relevant memories']);
64
+ // Every heading a pack writes. A memory's own text can hold a markdown heading,
65
+ // which must not read as the end of the section.
66
+ const PACK_SECTIONS = new Set([
67
+ ...RECALL_SECTIONS, 'Related (from your memory graph)', 'Memory index', 'Working memory', 'Recent', "Who you're talking to",
68
+ ]);
69
+ // Search can also return the dream's own digests: its "Right now" notes and memory
70
+ // index, which have sections of their own, and a day's journal, whose lines all
71
+ // live on elsewhere. None of them is a memory matched to this message.
72
+ const DIGEST_HIT = /^- (?:Right now|Memory Index|\d{4}-\d{2}-\d{2}): /;
73
+ // The pack keeps this much of each hit, which is how to tell where a multi-line
74
+ // digest ends and the next hit starts.
75
+ const HIT_TEXT_LIMIT = 300;
62
76
  /** The bullets of a memory pack worth showing for one message, in pack order. */
63
77
  function recallLines(block) {
64
78
  const lines = [];
65
- let inSection = false;
66
- for (const line of (block ?? '').split('\n')) {
67
- const heading = /^#{2,3} (.+)$/.exec(line.trim());
68
- if (heading) {
69
- inSection = RECALL_SECTIONS.has(heading[1].trim());
79
+ const working = new Set();
80
+ let section = '';
81
+ let digestLeft = 0;
82
+ for (const raw of (block ?? '').split('\n')) {
83
+ const heading = /^#{2,3} (.+)$/.exec(raw.trim())?.[1].trim();
84
+ if (heading && PACK_SECTIONS.has(heading)) {
85
+ section = heading;
86
+ digestLeft = 0;
70
87
  continue;
71
88
  }
72
- if (inSection && line.startsWith('- '))
73
- lines.push(line.trim());
89
+ if (section === 'Working memory') {
90
+ if (raw.startsWith('- '))
91
+ working.add(raw.trim());
92
+ continue;
93
+ }
94
+ if (!RECALL_SECTIONS.has(section))
95
+ continue;
96
+ if (digestLeft > 0) {
97
+ digestLeft -= raw.length + 1;
98
+ continue;
99
+ }
100
+ const digest = DIGEST_HIT.exec(raw);
101
+ if (digest) {
102
+ digestLeft = HIT_TEXT_LIMIT - (raw.length - digest[0].length) - 1;
103
+ continue;
104
+ }
105
+ if (!raw.startsWith('- '))
106
+ continue;
107
+ const line = raw.trim().replace(/^- (?:- )+/, '- ');
108
+ if (!working.has(line))
109
+ lines.push(line);
74
110
  }
75
111
  return lines;
76
112
  }
113
+ // The system prompt's pack and a message's pack render a memory with no heading
114
+ // differently, and it's still the one memory.
77
115
  function hashLine(line) {
78
- return (0, crypto_1.createHash)('sha1').update(line).digest('hex').slice(0, 16);
116
+ return (0, crypto_1.createHash)('sha1').update(line.replace(/^- memory: /, '- ')).digest('hex').slice(0, 16);
79
117
  }
80
118
  function readSeen(cwd) {
81
119
  try {
@@ -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"));
@@ -78,6 +79,7 @@ const projects_2 = require("./projects");
78
79
  const run_retry_1 = require("./run-retry");
79
80
  const hang_watchdog_1 = require("./hang-watchdog");
80
81
  const install_1 = require("./install");
82
+ const mcp_direct_1 = require("./mcp-direct");
81
83
  const memory_capture_1 = require("./memory-capture");
82
84
  const memory_pack_1 = require("./memory-pack");
83
85
  const memory_recall_1 = require("./memory-recall");
@@ -1646,7 +1648,10 @@ function publishModelSelection(req, d, changed) {
1646
1648
  }
1647
1649
  /** This turn's model and effort, when MSP is on. Written onto req so a guardrail
1648
1650
  * retry turn stays on the same model. Call it just before the spawn. */
1649
- async function applyModelSelection(req) {
1651
+ function applyModelSelection(req) {
1652
+ return req.mspApplied ??= selectModelOnce(req);
1653
+ }
1654
+ async function selectModelOnce(req) {
1650
1655
  const msp = req.mspDecision ? await req.mspDecision : null;
1651
1656
  if (!msp)
1652
1657
  return null;
@@ -1680,12 +1685,55 @@ function reportMcpTimings(req, file) {
1680
1685
  catch { /* best-effort */ }
1681
1686
  }
1682
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
+ }
1683
1732
  async function runClaudePrint(req, cwd, installed) {
1684
1733
  if (!req.prompt) {
1685
1734
  await setStatus(req.id, { status: 'failed', errorCode: 'no_prompt', errorMessage: 'print mode requires a prompt' });
1686
1735
  return;
1687
1736
  }
1688
- const permissionMode = req.agent === 'claude-bypass' ? 'bypassPermissions' : (req.permission_mode ?? undefined);
1689
1737
  // Floor for "which subagent transcripts belong to this attempt" — see
1690
1738
  // subagent-usage.ts. Taken before the spawn so nothing this run writes
1691
1739
  // can fall outside the window.
@@ -1711,22 +1759,24 @@ async function runClaudePrint(req, cwd, installed) {
1711
1759
  }
1712
1760
  }
1713
1761
  const msp = await applyModelSelection(req);
1714
- 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;
1715
1767
  let handle;
1716
1768
  try {
1717
- handle = (0, claude_print_1.spawnClaudePrint)({
1718
- workingDirectory: cwd,
1719
- prompt: req.prompt,
1720
- ...(inputBlocks ? { inputBlocks } : {}),
1721
- permissionMode,
1722
- effortLevel: req.effort ?? undefined,
1723
- model: (0, model_filter_1.modelForAgent)('claude', req.model),
1724
- mcpConfigPath: installed.mcpConfigPath ?? undefined,
1725
- resumeSessionId: req.resume_session_id ?? undefined,
1726
- appendSystemPrompt: msp ? (0, time_context_1.appendChatTimeContext)(req.system_prompt) : (0, time_context_1.appendTimeContext)(req.system_prompt),
1727
- allowedTools: req.allowed_tools ?? undefined,
1728
- ...(mcpDebugFile ? { debugFile: mcpDebugFile } : {}),
1729
- });
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
+ }
1730
1780
  }
1731
1781
  catch (err) {
1732
1782
  await failRequest(req, 'spawn_failed', err.message);
@@ -2375,6 +2425,16 @@ function livingPresenceBlock() {
2375
2425
  * question can still lean on memory, so it gets a moment, and no more: the pack
2376
2426
  * usually lands around when the selector does, but can take seconds. */
2377
2427
  const QUICK_RECALL_WAIT_MS = 500;
2428
+ /** Selector turns start our own apps from the copy npx already has. */
2429
+ function launchAppsDirectly(cwd, installed) {
2430
+ if (process.env.ADDAI_MCP_DIRECT === '0' || !installed.mcps?.length)
2431
+ return;
2432
+ const mcps = (0, mcp_direct_1.directMcps)(installed.mcps);
2433
+ if (mcps === installed.mcps)
2434
+ return;
2435
+ installed.mcps = mcps;
2436
+ installed.mcpConfigPath = (0, install_1.writeMcpConfig)(cwd, mcps) ?? installed.mcpConfigPath;
2437
+ }
2378
2438
  /**
2379
2439
  * Setup for a turn with the model selector on. It does what the block in
2380
2440
  * runRequest does, without the wait: the Drive listing and memory pack run side
@@ -2382,9 +2442,15 @@ const QUICK_RECALL_WAIT_MS = 500;
2382
2442
  * turn. Claude reuses a resumed session's first system prompt, so its memory
2383
2443
  * pack and pointer there are only read again after a compaction.
2384
2444
  */
2385
- async function prepareFastSetup(req, cwd, installed, pack, startedAt) {
2445
+ async function prepareFastSetup(req, cwd, installed, pack, startedAt, onSystemPromptReady) {
2386
2446
  const resumedClaude = req.agent !== 'codex' && !!req.resume_session_id;
2387
2447
  const timing = { pack_ms: null };
2448
+ try {
2449
+ launchAppsDirectly(cwd, installed);
2450
+ }
2451
+ catch (err) {
2452
+ console.error(`[setup] direct app launch failed [reqId=${req.id}]: ${err.message}`);
2453
+ }
2388
2454
  void pack.then(() => { timing.pack_ms = Date.now() - startedAt; });
2389
2455
  // A listing from an earlier turn is good enough to point at. This turn's
2390
2456
  // replaces it in the background, well before the agent's first look.
@@ -2435,6 +2501,8 @@ async function prepareFastSetup(req, cwd, installed, pack, startedAt) {
2435
2501
  let recall = 'system_prompt';
2436
2502
  if (resumedClaude) {
2437
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?.();
2438
2506
  const found = await (decision?.picked === 'quick'
2439
2507
  ? Promise.race([pack, new Promise(done => setTimeout(() => done(null), QUICK_RECALL_WAIT_MS))])
2440
2508
  : pack);
@@ -2514,7 +2582,7 @@ async function runRequest(req, entitybox) {
2514
2582
  if (entityId && process.env.ADDAI_FAST_SETUP !== '0') {
2515
2583
  const message = (0, model_selector_1.messageFromPrompt)(req.prompt ?? '');
2516
2584
  earlyPack = setting.then(s => (s
2517
- ? (0, memory_pack_1.fetchMemoryPack)(config_1.SUPABASE_URL, config_1.SUPABASE_ANON_KEY, { entityId, prompt: message, memberProfileId: req.member_profile_id ?? null })
2585
+ ? (0, memory_pack_1.fetchMemoryPackVia)(s.config?.memory_pack, config_1.SUPABASE_URL, config_1.SUPABASE_ANON_KEY, { entityId, prompt: message, memberProfileId: req.member_profile_id ?? null })
2518
2586
  : { block: '', degraded: false, flagged: false }));
2519
2587
  }
2520
2588
  }
@@ -2586,10 +2654,11 @@ async function runRequest(req, entitybox) {
2586
2654
  skills_installed: installed.skillSlugs,
2587
2655
  });
2588
2656
  }
2589
- const fast = !!earlyPack && !!(await mspSetting);
2657
+ const fastSetting = earlyPack ? await mspSetting : null;
2658
+ const fast = !!fastSetting;
2590
2659
  if (fast) {
2591
2660
  try {
2592
- await prepareFastSetup(req, cwd, installed, earlyPack, startedAt);
2661
+ await prepareFastSetup(req, cwd, installed, earlyPack, startedAt, earlyStartWanted(req, fastSetting?.config) ? () => startClaudeEarly(req, cwd, installed) : undefined);
2593
2662
  }
2594
2663
  catch (err) {
2595
2664
  console.error(`[setup] fast setup failed [reqId=${req.id}]: ${err.message}`);
@@ -2683,6 +2752,8 @@ async function runRequest(req, entitybox) {
2683
2752
  }
2684
2753
  }
2685
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();
2686
2757
  // Best-effort cleanup of installed files. If the request crashed
2687
2758
  // mid-flight we may leak files; next install run for the same cwd
2688
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.7",
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": [