@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.
- package/dist/claude-print.d.ts +10 -0
- package/dist/claude-print.js +42 -9
- package/dist/mcp-direct.d.ts +22 -0
- package/dist/mcp-direct.js +163 -0
- package/dist/memory-pack.d.ts +7 -1
- package/dist/memory-pack.js +14 -2
- package/dist/memory-recall.js +49 -11
- package/dist/session-runner.d.ts +13 -1
- package/dist/session-runner.js +91 -20
- package/package.json +1 -1
package/dist/claude-print.d.ts
CHANGED
|
@@ -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.
|
package/dist/claude-print.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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) => {
|
|
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
|
+
}
|
package/dist/memory-pack.d.ts
CHANGED
|
@@ -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;
|
package/dist/memory-pack.js
CHANGED
|
@@ -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
|
|
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/
|
package/dist/memory-recall.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 (
|
|
73
|
-
|
|
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 {
|
package/dist/session-runner.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/session-runner.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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": [
|