@addai/node 0.30.16-msp.1 → 0.30.16-msp.10

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.
@@ -83,9 +83,13 @@ interface CapabilitiesShape {
83
83
  auto_update?: AutoUpdateState;
84
84
  /** Present when this build has the Model Selector Protocol, so Studio can
85
85
  * say which nodes would ignore an entity's selector setting instead of
86
- * guessing from a version number. enabled is false under ADDAI_MSP=0. */
86
+ * guessing from a version number. enabled is false under ADDAI_MSP=0.
87
+ * jev is true on builds that can use Jev as the selector, codex on builds
88
+ * that pick for Codex turns too. */
87
89
  model_selector?: {
88
90
  enabled: boolean;
91
+ jev?: boolean;
92
+ codex?: boolean;
89
93
  };
90
94
  }
91
95
  /** What the heartbeat says about claude's models. `models` keeps its old
@@ -510,7 +510,7 @@ async function probeCapabilities() {
510
510
  autostart: (0, autostart_1.status)(),
511
511
  container_engine: containerEngine,
512
512
  flows: await probeFlows(),
513
- model_selector: { enabled: process.env.ADDAI_MSP !== '0' },
513
+ model_selector: { enabled: process.env.ADDAI_MSP !== '0', jev: true, codex: true },
514
514
  auto_update: (0, auto_update_1.autoUpdateState)(),
515
515
  machine: (0, machine_metrics_1.sampleMachine)(),
516
516
  claude: deco('claude', claude),
@@ -14,7 +14,34 @@ export interface ClaudePrintInput {
14
14
  appendSystemPrompt?: string;
15
15
  /** Restrict to a specific set of tools (claude only). null/undefined = inherit. */
16
16
  allowedTools?: string[];
17
+ /** Write the CLI's debug log here, to time each app's connection. */
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;
17
22
  }
23
+ /**
24
+ * findClaudeBinary for the spawn path. The lookup runs `where` or a login shell
25
+ * synchronously, stopping the whole daemon while it does, and the answer rarely
26
+ * changes between turns. A path that has gone missing is looked up again.
27
+ */
28
+ export declare function cachedClaudeBinary(): string | null;
29
+ export interface McpTimings {
30
+ /** Slowest first. */
31
+ servers: Array<{
32
+ name: string;
33
+ transport: string;
34
+ ms: number;
35
+ }>;
36
+ failed: string[];
37
+ /** Log start to the first app starting to connect: the CLI's own boot. */
38
+ first_start_ms: number | null;
39
+ /** Log start to the last app connected. */
40
+ last_connected_ms: number | null;
41
+ }
42
+ /** Each app's connection time from a CLI debug log. Takes names and numbers only:
43
+ * the log can hold anything the CLI saw, so none of its free text goes further. */
44
+ export declare function parseMcpTimings(log: string): McpTimings;
18
45
  export type ClaudePrintInputBlock = {
19
46
  type: 'text';
20
47
  text: string;
@@ -27,6 +54,8 @@ export type ClaudePrintInputBlock = {
27
54
  };
28
55
  };
29
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;
30
59
  export declare function claudePrintInputMessage(prompt: string, blocks: readonly ClaudePrintInputBlock[]): string;
31
60
  /**
32
61
  * The text to emit for one flushed block, given what came before.
@@ -49,6 +78,11 @@ export interface ClaudePrintHandle {
49
78
  onEvent(cb: (e: RuntimeEvent) => void): void;
50
79
  /** Resolves with exit code; rejects on spawn error. */
51
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;
52
86
  }
53
87
  /**
54
88
  * Parse one stream-json line into 0+ RuntimeEvents.
@@ -4,17 +4,111 @@
4
4
  // Spawns Claude non-interactively, parses each JSONL line on stdout into
5
5
  // our normalised RuntimeEvent shape, and resolves when the process exits.
6
6
  // No PTY needed — Claude detects non-TTY and runs headless.
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
7
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.cachedClaudeBinary = cachedClaudeBinary;
42
+ exports.parseMcpTimings = parseMcpTimings;
43
+ exports.claudePrintPromptMessage = claudePrintPromptMessage;
8
44
  exports.claudePrintInputMessage = claudePrintInputMessage;
9
45
  exports.blockDelta = blockDelta;
10
46
  exports.lineToEvents = lineToEvents;
11
47
  exports.spawnClaudePrint = spawnClaudePrint;
12
48
  const child_process_1 = require("child_process");
49
+ const fs = __importStar(require("fs"));
13
50
  const claude_binary_1 = require("./claude-binary");
14
51
  const win_1 = require("./win");
15
52
  const claude_system_prompt_1 = require("./claude-system-prompt");
16
53
  const prompt_transport_1 = require("./prompt-transport");
54
+ let cachedBinary = null;
55
+ const CACHED_BINARY_MS = 10 * 60_000;
56
+ /**
57
+ * findClaudeBinary for the spawn path. The lookup runs `where` or a login shell
58
+ * synchronously, stopping the whole daemon while it does, and the answer rarely
59
+ * changes between turns. A path that has gone missing is looked up again.
60
+ */
61
+ function cachedClaudeBinary() {
62
+ const c = cachedBinary;
63
+ if (c && c.pathEnv === process.env.PATH && Date.now() - c.at < CACHED_BINARY_MS && fs.existsSync(c.path)) {
64
+ return c.path;
65
+ }
66
+ const found = (0, claude_binary_1.findClaudeBinary)();
67
+ cachedBinary = found ? { path: found, pathEnv: process.env.PATH, at: Date.now() } : null;
68
+ return found;
69
+ }
70
+ /** Each app's connection time from a CLI debug log. Takes names and numbers only:
71
+ * the log can hold anything the CLI saw, so none of its free text goes further. */
72
+ function parseMcpTimings(log) {
73
+ const servers = [];
74
+ const failed = [];
75
+ const stamp = (line) => {
76
+ const t = Date.parse(line.slice(0, 24));
77
+ return Number.isNaN(t) ? null : t;
78
+ };
79
+ let logStart = null;
80
+ let firstStart = null;
81
+ let lastConnected = null;
82
+ for (const line of log.split('\n')) {
83
+ const at = stamp(line);
84
+ if (logStart === null && at !== null)
85
+ logStart = at;
86
+ if (firstStart === null && /MCP server "[^"]+": Starting connection/.test(line))
87
+ firstStart = at;
88
+ const ok = /MCP server "([^"]+)": Successfully connected \(transport: ([^)]+)\) in (\d+)ms/.exec(line);
89
+ if (ok) {
90
+ servers.push({ name: ok[1], transport: ok[2], ms: Number(ok[3]) });
91
+ if (at !== null)
92
+ lastConnected = at;
93
+ continue;
94
+ }
95
+ const bad = /MCP server "([^"]+)": Connection (?:failed|timed out)/.exec(line);
96
+ if (bad)
97
+ failed.push(bad[1]);
98
+ }
99
+ const since = (t) => (t !== null && logStart !== null ? t - logStart : null);
100
+ return {
101
+ servers: servers.sort((a, b) => b.ms - a.ms),
102
+ failed,
103
+ first_start_ms: since(firstStart),
104
+ last_connected_ms: since(lastConnected),
105
+ };
106
+ }
17
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
+ }
18
112
  function claudePrintInputMessage(prompt, blocks) {
19
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)
20
114
  && (b.type === 'document' ? b.source.media_type === 'application/pdf' : ['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(b.source.media_type)))))
@@ -125,7 +219,7 @@ function lineToEvents(line, suppressText = false) {
125
219
  }
126
220
  function spawnClaudePrint(input) {
127
221
  const inputMessage = input.inputBlocks ? claudePrintInputMessage(input.prompt, input.inputBlocks) : undefined;
128
- const bin = (0, claude_binary_1.findClaudeBinary)();
222
+ const bin = cachedClaudeBinary();
129
223
  // No claude on this runtime — fail THIS request cleanly rather than
130
224
  // spawning a bare 'claude' that ENOENTs and crashes the daemon. Both
131
225
  // callers (runClaudePrint, runGuardrailRetryTurn) wrap this in try/catch
@@ -150,7 +244,9 @@ function spawnClaudePrint(input) {
150
244
  ];
151
245
  if (partialStream)
152
246
  args.push('--include-partial-messages');
153
- if (input.inputBlocks)
247
+ if (input.debugFile)
248
+ args.push('--debug-file', input.debugFile);
249
+ if (input.inputBlocks || input.deferPrompt)
154
250
  args.push('--input-format', 'stream-json');
155
251
  if (input.permissionMode === 'bypassPermissions') {
156
252
  args.push('--dangerously-skip-permissions');
@@ -217,15 +313,28 @@ function spawnClaudePrint(input) {
217
313
  // Mark handled now — the caller awaits `done` a few ticks later, and an
218
314
  // already-rejected promise with no handler would fire unhandledRejection.
219
315
  done.catch(() => { });
220
- return { pid: undefined, sessionId: null, kill: () => { }, onEvent: () => { }, done };
316
+ return { pid: undefined, sessionId: null, kill: () => { }, onEvent: () => { }, done, hasExited: () => true };
221
317
  }
222
- (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);
223
324
  const listeners = [];
224
- const emit = (e) => { for (const l of listeners)
225
- try {
226
- 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;
227
331
  }
228
- catch { } };
332
+ for (const l of listeners)
333
+ try {
334
+ l(e);
335
+ }
336
+ catch { }
337
+ };
229
338
  let sessionId = null;
230
339
  let buffer = '';
231
340
  // Text-delta coalescer (partial-stream mode). Token deltas arrive many times
@@ -356,9 +465,11 @@ function spawnClaudePrint(input) {
356
465
  proc.stderr?.on('data', (chunk) => {
357
466
  stderrBuf += chunk.toString('utf8');
358
467
  });
468
+ let exited = false;
359
469
  const done = new Promise((resolve, reject) => {
360
- proc.once('error', reject);
470
+ proc.once('error', (err) => { exited = true; reject(err); });
361
471
  proc.once('exit', (code) => {
472
+ exited = true;
362
473
  sysPrompt.cleanup();
363
474
  flushThinking();
364
475
  flushText();
@@ -369,11 +480,24 @@ function spawnClaudePrint(input) {
369
480
  resolve(code ?? -1);
370
481
  });
371
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(() => { });
372
487
  return {
373
488
  pid: proc.pid,
374
489
  get sessionId() { return sessionId; },
375
490
  kill: () => { (0, win_1.killProcessTree)(proc); },
376
- 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
+ },
377
499
  done,
500
+ hasExited: () => exited,
501
+ ...(input.deferPrompt ? { sendPrompt: (prompt) => { proc.stdin?.end(claudePrintPromptMessage(prompt), 'utf8'); } } : {}),
378
502
  };
379
503
  }
@@ -79,3 +79,5 @@ export declare function codexModelsFor(bin: string, key: string): CodexModelRepo
79
79
  * model is not in its list.
80
80
  */
81
81
  export declare function codexModelEfforts(model?: string | null, report?: CodexModelReport | null): string[] | null;
82
+ /** False only when codex has listed its models and this isn't one of them. */
83
+ export declare function codexModelListed(model: string, report?: CodexModelReport | null): boolean;
@@ -71,6 +71,7 @@ exports.probeCodexModels = probeCodexModels;
71
71
  exports.createCodexModelCache = createCodexModelCache;
72
72
  exports.codexModelsFor = codexModelsFor;
73
73
  exports.codexModelEfforts = codexModelEfforts;
74
+ exports.codexModelListed = codexModelListed;
74
75
  const child_process_1 = require("child_process");
75
76
  const fs = __importStar(require("fs"));
76
77
  const os = __importStar(require("os"));
@@ -383,3 +384,7 @@ function codexModelEfforts(model, report = sharedCache.last()) {
383
384
  const entry = model ? report.catalog.find(e => e.id === model) : report.catalog.find(e => e.default);
384
385
  return entry?.efforts?.length ? entry.efforts : null;
385
386
  }
387
+ /** False only when codex has listed its models and this isn't one of them. */
388
+ function codexModelListed(model, report = sharedCache.last()) {
389
+ return !report || report.catalog.some(e => e.id === model);
390
+ }
@@ -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/
@@ -0,0 +1,8 @@
1
+ /** The bullets of a memory pack worth showing for one message, in pack order. */
2
+ export declare function recallLines(block: string): string[];
3
+ /** A new session's first turn shows the whole pack in its system prompt: start
4
+ * the seen list from it, so the second turn doesn't repeat those lines. */
5
+ export declare function rememberRecall(cwd: string, block: string): void;
6
+ /** The prompt with this message's new memories at the top, or unchanged when
7
+ * there are none. Only what actually went in counts as seen. */
8
+ export declare function withFreshRecall(prompt: string, block: string, cwd: string): string;