@addai/node 0.30.16-msp.0 → 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.
@@ -43,6 +43,8 @@ exports.notifyFlowsIfFlowRequest = notifyFlowsIfFlowRequest;
43
43
  exports.notifyWidgetstudioIfWidgetRequest = notifyWidgetstudioIfWidgetRequest;
44
44
  exports.notifySelfImproveIfEnabled = notifySelfImproveIfEnabled;
45
45
  exports.resumeSessionIsVoid = resumeSessionIsVoid;
46
+ exports.earlyStartWanted = earlyStartWanted;
47
+ exports.prepareFastSetup = prepareFastSetup;
46
48
  exports.runRequest = runRequest;
47
49
  const fs = __importStar(require("fs"));
48
50
  const os = __importStar(require("os"));
@@ -77,8 +79,10 @@ const projects_2 = require("./projects");
77
79
  const run_retry_1 = require("./run-retry");
78
80
  const hang_watchdog_1 = require("./hang-watchdog");
79
81
  const install_1 = require("./install");
82
+ const mcp_direct_1 = require("./mcp-direct");
80
83
  const memory_capture_1 = require("./memory-capture");
81
84
  const memory_pack_1 = require("./memory-pack");
85
+ const memory_recall_1 = require("./memory-recall");
82
86
  const context_files_1 = require("./context-files");
83
87
  const guardrails_1 = require("./guardrails");
84
88
  const time_context_1 = require("./time-context");
@@ -1577,22 +1581,23 @@ async function runClaudeTui(req, cwd, installed) {
1577
1581
  });
1578
1582
  });
1579
1583
  }
1580
- // ── Model Selector Protocol ────────────────────────────────────────────────
1581
- /** Ask the server whether MSP is on for this turn and, if it is, run the
1582
- * selector. Never rejects: anything that goes wrong resolves null (the old
1584
+ /** Ask the server whether MSP is on for this turn. Neither this nor
1585
+ * startModelSelection rejects: anything that goes wrong resolves null (the old
1583
1586
  * path) or a fallback decision (the old model, nothing said). */
1584
- async function startModelSelection(req) {
1585
- let setting;
1587
+ async function fetchModelSelectorSetting(req) {
1586
1588
  try {
1587
- setting = await (0, supabase_client_1.rpc)('runtime_model_selector_for_request', { p_token: token(), p_request_id: req.id });
1589
+ const setting = await (0, supabase_client_1.rpc)('runtime_model_selector_for_request', { p_token: token(), p_request_id: req.id });
1590
+ return setting && typeof setting === 'object' ? setting : null;
1588
1591
  }
1589
1592
  catch {
1590
1593
  return null; // not deployed, or not ours: the old path
1591
1594
  }
1592
- if (!setting || typeof setting !== 'object')
1593
- return null;
1595
+ }
1596
+ /** Run the selector. Its line goes out the moment it answers, not when the turn
1597
+ * spawns: it's the first thing the person sees, and setup has seconds to run. */
1598
+ async function startModelSelection(req, setting) {
1594
1599
  try {
1595
- return await (0, model_selector_1.decide)({
1600
+ const decision = await (0, model_selector_1.decide)({
1596
1601
  config: setting.config,
1597
1602
  last: setting.last ?? null,
1598
1603
  prompt: req.prompt ?? '',
@@ -1600,18 +1605,25 @@ async function startModelSelection(req) {
1600
1605
  entityName: setting.entity_name || 'the assistant',
1601
1606
  entityModel: req.model,
1602
1607
  entityEffort: req.effort,
1608
+ agent: req.agent === 'codex' ? 'codex' : 'claude',
1609
+ runJev: (0, model_selector_1.askJev)(token(), req.id),
1603
1610
  });
1611
+ if (decision)
1612
+ postModelLine(req, decision);
1613
+ return decision;
1604
1614
  }
1605
1615
  catch (err) {
1606
1616
  console.error(`[msp] selection failed [reqId=${req.id}]: ${err.message}`);
1607
1617
  return null;
1608
1618
  }
1609
1619
  }
1610
- /** Record the decision (the next turn reads it back to stay steady) and post
1611
- * the line. Both best-effort: the turn has already been decided. */
1612
- function publishModelSelection(req, d) {
1613
- void emit(req.id, 'msp_decision', d);
1614
- if (!d.line)
1620
+ /** A ChatGPT login refuses a model codex doesn't list, which would fail the
1621
+ * turn. The node's list can differ from the node the tiers were chosen on. */
1622
+ function modelMissingOnNode(req, d) {
1623
+ return req.agent === 'codex' && !!d.model && d.model !== req.model && !(0, codex_models_1.codexModelListed)(d.model);
1624
+ }
1625
+ function postModelLine(req, d) {
1626
+ if (!d.line || modelMissingOnNode(req, d))
1615
1627
  return;
1616
1628
  void (0, supabase_client_1.rpc)('runtime_model_selector_announce', {
1617
1629
  p_token: token(),
@@ -1620,13 +1632,108 @@ function publishModelSelection(req, d) {
1620
1632
  p_decision: { tier: d.tier, model: d.model, effort: d.effort },
1621
1633
  }).catch(err => console.error(`[msp] announce failed [reqId=${req.id}]: ${err.message}`));
1622
1634
  }
1635
+ /** Record the decision (the next turn reads it back to stay steady) and put the
1636
+ * model and effort that will really run on the request row so usage counts
1637
+ * them. All best-effort: the turn has already been decided. */
1638
+ function publishModelSelection(req, d, changed) {
1639
+ void emit(req.id, 'msp_decision', d);
1640
+ if (changed && req.model && req.effort) {
1641
+ void (0, supabase_client_1.rpc)('runtime_model_selector_record', {
1642
+ p_token: token(),
1643
+ p_request_id: req.id,
1644
+ p_model: req.model,
1645
+ p_effort: req.effort,
1646
+ }).catch(err => console.error(`[msp] record failed [reqId=${req.id}]: ${err.message}`));
1647
+ }
1648
+ }
1649
+ /** This turn's model and effort, when MSP is on. Written onto req so a guardrail
1650
+ * retry turn stays on the same model. Call it just before the spawn. */
1651
+ function applyModelSelection(req) {
1652
+ return req.mspApplied ??= selectModelOnce(req);
1653
+ }
1654
+ async function selectModelOnce(req) {
1655
+ const msp = req.mspDecision ? await req.mspDecision : null;
1656
+ if (!msp)
1657
+ return null;
1658
+ if (modelMissingOnNode(req, msp)) {
1659
+ console.log(`[msp] ${msp.model} is not on this node's codex; running the entity's own model [reqId=${req.id}]`);
1660
+ msp.fallback_reason = msp.selector_error = `model_not_on_node: ${msp.model}`;
1661
+ msp.fell_back = true;
1662
+ msp.line = null;
1663
+ msp.model = req.model;
1664
+ msp.effort = req.effort;
1665
+ }
1666
+ const changed = (!!msp.model && msp.model !== req.model) || (!!msp.effort && msp.effort !== req.effort);
1667
+ if (msp.model)
1668
+ req.model = msp.model;
1669
+ if (msp.effort)
1670
+ req.effort = msp.effort;
1671
+ publishModelSelection(req, msp, changed);
1672
+ return msp;
1673
+ }
1674
+ /** The selector's mcp_timing test setting: each app's connection time for this
1675
+ * turn, from the CLI's debug log. The log itself never leaves the node and goes
1676
+ * as soon as the turn ends, as it can hold anything the CLI saw. */
1677
+ function reportMcpTimings(req, file) {
1678
+ try {
1679
+ void emit(req.id, 'mcp_timing', (0, claude_print_1.parseMcpTimings)(fs.readFileSync(file, 'utf8')));
1680
+ }
1681
+ catch { /* no log, nothing to report */ }
1682
+ try {
1683
+ fs.rmSync(file, { force: true });
1684
+ }
1685
+ catch { /* best-effort */ }
1686
+ }
1623
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
+ }
1624
1732
  async function runClaudePrint(req, cwd, installed) {
1625
1733
  if (!req.prompt) {
1626
1734
  await setStatus(req.id, { status: 'failed', errorCode: 'no_prompt', errorMessage: 'print mode requires a prompt' });
1627
1735
  return;
1628
1736
  }
1629
- const permissionMode = req.agent === 'claude-bypass' ? 'bypassPermissions' : (req.permission_mode ?? undefined);
1630
1737
  // Floor for "which subagent transcripts belong to this attempt" — see
1631
1738
  // subagent-usage.ts. Taken before the spawn so nothing this run writes
1632
1739
  // can fall outside the window.
@@ -1651,30 +1758,25 @@ async function runClaudePrint(req, cwd, installed) {
1651
1758
  abort.abort();
1652
1759
  }
1653
1760
  }
1654
- // Model Selector Protocol: this turn's model and effort, when it is on.
1655
- // Written onto req so a guardrail retry turn stays on the same model.
1656
- const msp = req.mspDecision ? await req.mspDecision : null;
1657
- if (msp) {
1658
- if (msp.model)
1659
- req.model = msp.model;
1660
- if (msp.effort)
1661
- req.effort = msp.effort;
1662
- publishModelSelection(req, msp);
1663
- }
1761
+ const msp = await applyModelSelection(req);
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;
1664
1767
  let handle;
1665
1768
  try {
1666
- handle = (0, claude_print_1.spawnClaudePrint)({
1667
- workingDirectory: cwd,
1668
- prompt: req.prompt,
1669
- ...(inputBlocks ? { inputBlocks } : {}),
1670
- permissionMode,
1671
- effortLevel: req.effort ?? undefined,
1672
- model: (0, model_filter_1.modelForAgent)('claude', req.model),
1673
- mcpConfigPath: installed.mcpConfigPath ?? undefined,
1674
- resumeSessionId: req.resume_session_id ?? undefined,
1675
- appendSystemPrompt: (0, time_context_1.appendTimeContext)(req.system_prompt),
1676
- allowedTools: req.allowed_tools ?? undefined,
1677
- });
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
+ }
1678
1780
  }
1679
1781
  catch (err) {
1680
1782
  await failRequest(req, 'spawn_failed', err.message);
@@ -1735,12 +1837,16 @@ async function runClaudePrint(req, cwd, installed) {
1735
1837
  nodeTimeout.clear();
1736
1838
  watcher.stop();
1737
1839
  hang.stop();
1840
+ if (mcpDebugFile)
1841
+ reportMcpTimings(req, mcpDebugFile);
1738
1842
  await failRequest(req, 'spawn_failed', err.message);
1739
1843
  return;
1740
1844
  }
1741
1845
  nodeTimeout.clear();
1742
1846
  watcher.stop();
1743
1847
  hang.stop();
1848
+ if (mcpDebugFile)
1849
+ reportMcpTimings(req, mcpDebugFile);
1744
1850
  // An MSP run that worked and then went quiet for max_run.silence_minutes
1745
1851
  // ends with what it has, and says so, instead of being retried from the
1746
1852
  // top. One that never produced anything still takes the old route (retry,
@@ -1800,6 +1906,7 @@ async function runCodex(req, cwd, installed) {
1800
1906
  void emit(req.id, 'session_dropped_missing_rollout', { sessionId: req.resume_session_id });
1801
1907
  req.resume_session_id = null;
1802
1908
  }
1909
+ const msp = await applyModelSelection(req);
1803
1910
  let handle;
1804
1911
  try {
1805
1912
  const codexModel = (0, model_filter_1.modelForAgent)('codex', req.model);
@@ -1838,11 +1945,15 @@ async function runCodex(req, cwd, installed) {
1838
1945
  // call looks identical to a hang. Give it grok's leash: standard window
1839
1946
  // before any output (a wedged spawn/resume trips fast), 3x after activity
1840
1947
  // has been seen (silent tool work is expected, not a hang). Raw stdout/
1841
- // stderr bytes count as liveness via onActivity.
1948
+ // stderr bytes count as liveness via onActivity. An MSP run can lengthen
1949
+ // that leash with max_run.silence_minutes but not shorten it, because silence
1950
+ // here can be a tool working. It is stopped rather than retried (below).
1842
1951
  const sawActivity = { current: false };
1843
- const hang = armHangWatchdog(req, lastError, () => handle.kill(), {
1952
+ const silenced = { current: false };
1953
+ const quietMs = () => Math.max(config_1.HANG_WATCHDOG_IDLE_MS * 3, (msp?.max_run.silence_minutes ?? 0) * 60_000);
1954
+ const hang = armHangWatchdog(req, lastError, () => { silenced.current = true; handle.kill(); }, {
1844
1955
  sawActivity: () => sawActivity.current,
1845
- idleMsFn: () => (sawActivity.current ? config_1.HANG_WATCHDOG_IDLE_MS * 3 : config_1.HANG_WATCHDOG_IDLE_MS),
1956
+ idleMsFn: () => (sawActivity.current ? quietMs() : config_1.HANG_WATCHDOG_IDLE_MS),
1846
1957
  });
1847
1958
  handle.onActivity(() => { sawActivity.current = true; hang.touch(); });
1848
1959
  handle.onEvent(ev => {
@@ -1858,7 +1969,7 @@ async function runCodex(req, cwd, installed) {
1858
1969
  });
1859
1970
  // Node-configured execution timeout (null/0 = unlimited). On timeout we
1860
1971
  // kill the agent; the completion below treats it as complete-with-partial.
1861
- const nodeTimeout = armNodeTimeout(req, () => handle.kill());
1972
+ const nodeTimeout = armNodeTimeout(req, () => handle.kill(), msp ? msp.max_run.backstop_hours * 3600 : undefined);
1862
1973
  let exitCode;
1863
1974
  try {
1864
1975
  exitCode = await handle.done;
@@ -1878,6 +1989,14 @@ async function runCodex(req, cwd, installed) {
1878
1989
  nodeTimeout.clear();
1879
1990
  watcher.stop();
1880
1991
  hang.stop();
1992
+ // An MSP run that worked and then went quiet ends with what it has, and says
1993
+ // so, instead of being retried from the top. As in runClaudePrint.
1994
+ const mspSilenced = !!msp && silenced.current && sawActivity.current;
1995
+ if (mspSilenced) {
1996
+ await emit(req.id, 'assistant_text', {
1997
+ delta: `\n\n_I went quiet for ${Math.round(quietMs() / 60_000)} minutes, so I was stopped here._`,
1998
+ });
1999
+ }
1881
2000
  if (handle.threadId)
1882
2001
  await setStatus(req.id, { spawnedSessionId: handle.threadId });
1883
2002
  void emit(req.id, 'session_end', { reason: 'codex_exit', exitCode });
@@ -1885,7 +2004,7 @@ async function runCodex(req, cwd, installed) {
1885
2004
  if (currentStatus !== 'canceled') {
1886
2005
  // A timed-out run completes-with-partial: the node asked for a cap, not
1887
2006
  // a failure, so surface whatever the agent produced.
1888
- const cleanExit = exitCode === 0 || nodeTimeout.timedOut();
2007
+ const cleanExit = exitCode === 0 || nodeTimeout.timedOut() || mspSilenced;
1889
2008
  await finalizeTerminal(req, {
1890
2009
  clean: cleanExit,
1891
2010
  errorCode: cleanExit ? undefined : (lastError.current?.code ?? `exit_${exitCode}`),
@@ -2175,7 +2294,7 @@ async function fetchDriveList(apiKey, workspaceId, folderId, signal) {
2175
2294
  return null;
2176
2295
  return (await res.json());
2177
2296
  }
2178
- async function buildDriveManifest(installed) {
2297
+ async function buildDriveManifest(installed, concurrent = false) {
2179
2298
  const drive = installed.mcps.find((m) => m.slug === 'addai-drive');
2180
2299
  const env = drive?.env ?? null;
2181
2300
  const apiKey = env?.ADDAI_DRIVE_API_KEY;
@@ -2195,9 +2314,12 @@ async function buildDriveManifest(installed) {
2195
2314
  lines.push(fmt(f));
2196
2315
  // One level of subfolders so the entity sees its structure, capped to keep
2197
2316
  // the injection small and the listing fast.
2198
- for (const sub of (root.folders ?? []).slice(0, 12)) {
2317
+ const subs = (root.folders ?? []).slice(0, 12);
2318
+ const listSub = (id) => fetchDriveList(apiKey, workspaceId, id, controller.signal).catch(() => null);
2319
+ const listed = concurrent ? await Promise.all(subs.map(sub => listSub(sub.id))) : null;
2320
+ for (const [i, sub] of subs.entries()) {
2199
2321
  lines.push(` - ${sub.name}/`);
2200
- const child = await fetchDriveList(apiKey, workspaceId, sub.id, controller.signal).catch(() => null);
2322
+ const child = listed ? listed[i] : await listSub(sub.id);
2201
2323
  for (const f of (child?.files ?? []).slice(0, 30))
2202
2324
  lines.push(` - ${sub.name}/${f.name}`);
2203
2325
  }
@@ -2264,8 +2386,156 @@ async function buildMemoryManifest(installed) {
2264
2386
  clearTimeout(timer);
2265
2387
  }
2266
2388
  }
2389
+ // ── Turn setup ─────────────────────────────────────────────────────────────
2390
+ function appendToSystemPrompt(req, block) {
2391
+ req.system_prompt =
2392
+ req.system_prompt && req.system_prompt.trim().length > 0
2393
+ ? `${req.system_prompt}\n\n${block}`
2394
+ : block;
2395
+ }
2396
+ /** With no inline listing it points at ./context/drive.md instead. */
2397
+ function driveManifestBlock(count, inlineListing) {
2398
+ const intro = "You have a private +Ai Drive — file storage for working files and artifacts (documents, exports, data you produce) that survives across runs. This is for FILES, not memory: to remember facts, preferences, decisions, or what you learned, use your DocFlows memory (see below), never the Drive and never a local file.";
2399
+ const tools = 'Use your +Ai Drive tools (list_files, read_file, write_file, make_folder, …) to read, add, and organise files.';
2400
+ return inlineListing === null
2401
+ ? ['## Your +Ai Drive', intro, `Your current file list (${count} entries) is in ./context/drive.md — read it when you need a file.`, tools].join('\n')
2402
+ : ['## Your +Ai Drive', `${intro} Files currently in your Drive:`, inlineListing, tools].join('\n');
2403
+ }
2404
+ function writeDriveFile(cwd, listing) {
2405
+ return (0, context_files_1.writeContextFiles)(cwd, { 'context/drive.md': `# Your +Ai Drive\n\nFiles currently in your Drive:\n${listing}\n` }).written.length > 0;
2406
+ }
2407
+ /** The size of the listing an earlier turn left in this folder, if there is one. */
2408
+ function driveCountOnDisk(cwd) {
2409
+ try {
2410
+ const text = fs.readFileSync(path.join(cwd, 'context', 'drive.md'), 'utf8');
2411
+ return text.split('\n').filter(line => /^\s+- /.test(line)).length;
2412
+ }
2413
+ catch {
2414
+ return null;
2415
+ }
2416
+ }
2417
+ function livingPresenceBlock() {
2418
+ return [
2419
+ 'LIVING PRESENCE — narrate your work so people can see what you are doing in real time.',
2420
+ 'Whenever you are working on a specific table record (a card / task / row), call the `report_status` tool at your real milestones: when you claim/start it (status "working", a low progress, a short note), at each checkpoint (bump progress + update the note), if you get stuck and need a human (status "blocked"), and when you finish (status "done"). ALWAYS pass the record_id you are working on; if you are working on several records at once, report each separately with its own record_id so they never overwrite each other. This is lightweight — a handful of milestone updates per task; progress is a rough milestone percent (0-100), not a precise measurement.',
2421
+ 'THE `note` MUST BE ULTRA-SHORT — 1 to 3 plain, friendly words a teammate could read at a glance, e.g. "Reviewing", "Playing widget", "Writing summary", "Checking design", "Almost done". It renders in a tiny bubble, so NEVER write a sentence and NEVER use technical or internal jargon (not "waiting for publish propagation" — just "Publishing"). Think status label, not explanation.',
2422
+ ].join('\n');
2423
+ }
2424
+ /** How long a turn the selector calls quick waits on the memory pack. A quick
2425
+ * question can still lean on memory, so it gets a moment, and no more: the pack
2426
+ * usually lands around when the selector does, but can take seconds. */
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
+ }
2438
+ /**
2439
+ * Setup for a turn with the model selector on. It does what the block in
2440
+ * runRequest does, without the wait: the Drive listing and memory pack run side
2441
+ * by side, and neither holds the spawn back when its answer can't change the
2442
+ * turn. Claude reuses a resumed session's first system prompt, so its memory
2443
+ * pack and pointer there are only read again after a compaction.
2444
+ */
2445
+ async function prepareFastSetup(req, cwd, installed, pack, startedAt, onSystemPromptReady) {
2446
+ const resumedClaude = req.agent !== 'codex' && !!req.resume_session_id;
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
+ }
2454
+ void pack.then(() => { timing.pack_ms = Date.now() - startedAt; });
2455
+ // A listing from an earlier turn is good enough to point at. This turn's
2456
+ // replaces it in the background, well before the agent's first look.
2457
+ const hasDrive = installed.mcps?.some(m => m.slug === 'addai-drive') ?? false;
2458
+ const listing = hasDrive ? buildDriveManifest(installed, true).catch(() => null) : Promise.resolve(null);
2459
+ const knownCount = hasDrive ? driveCountOnDisk(cwd) : null;
2460
+ if (knownCount !== null) {
2461
+ void listing.then(drive => { if (drive)
2462
+ writeDriveFile(cwd, drive.body); }).catch(() => { });
2463
+ }
2464
+ let fallbackSent = false;
2465
+ const onFallback = () => {
2466
+ fallbackSent = true;
2467
+ void emit(req.id, 'memory_fallback', { at: 'pack' });
2468
+ };
2469
+ // The dispatcher's keyword block is already in place for a compaction to pick
2470
+ // up, so a resumed turn has no reason to wait for a better one here.
2471
+ if (!(resumedClaude && (req.system_prompt ?? '').includes('\n\n## Memory\n'))) {
2472
+ try {
2473
+ req.system_prompt = await (0, memory_pack_1.applyMemoryPack)(req, req.system_prompt ?? null, config_1.SUPABASE_URL, config_1.SUPABASE_ANON_KEY, {
2474
+ fetchPack: () => pack,
2475
+ buildLegacyManifest: () => buildMemoryManifest(installed),
2476
+ onFallback,
2477
+ onFlagged: (flagged) => { req.memoryV2Flagged = flagged; },
2478
+ });
2479
+ }
2480
+ catch (err) {
2481
+ console.error('[memory-pack] wrapper failed:', err?.message);
2482
+ }
2483
+ }
2484
+ // Memory goes in first: replaceMemoryBlock cuts from its heading to the end, so
2485
+ // anything appended before it would be lost.
2486
+ try {
2487
+ if (knownCount !== null) {
2488
+ appendToSystemPrompt(req, driveManifestBlock(knownCount, null));
2489
+ }
2490
+ else {
2491
+ const drive = await listing;
2492
+ if (drive)
2493
+ appendToSystemPrompt(req, driveManifestBlock(drive.count, writeDriveFile(cwd, drive.body) ? null : drive.body));
2494
+ }
2495
+ }
2496
+ catch {
2497
+ /* drive manifest is best-effort — never block a run */
2498
+ }
2499
+ if (installed.mcps?.some((m) => m.slug === 'entity-self-mcp'))
2500
+ appendToSystemPrompt(req, livingPresenceBlock());
2501
+ let recall = 'system_prompt';
2502
+ if (resumedClaude) {
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?.();
2506
+ const found = await (decision?.picked === 'quick'
2507
+ ? Promise.race([pack, new Promise(done => setTimeout(() => done(null), QUICK_RECALL_WAIT_MS))])
2508
+ : pack);
2509
+ if (!found) {
2510
+ recall = 'missed';
2511
+ }
2512
+ else if (found.degraded) {
2513
+ recall = 'unavailable';
2514
+ if (!fallbackSent)
2515
+ onFallback();
2516
+ }
2517
+ else {
2518
+ const withRecall = (0, memory_recall_1.withFreshRecall)(req.prompt ?? '', found.block, cwd);
2519
+ recall = withRecall === (req.prompt ?? '') ? 'nothing_new' : 'added';
2520
+ if (req.prompt)
2521
+ req.prompt = withRecall;
2522
+ }
2523
+ }
2524
+ else if (req.agent !== 'codex') {
2525
+ const found = await pack;
2526
+ (0, memory_recall_1.rememberRecall)(cwd, found.degraded ? '' : found.block);
2527
+ }
2528
+ void emit(req.id, 'setup_timing', {
2529
+ setup_ms: Date.now() - startedAt,
2530
+ pack_ms: timing.pack_ms,
2531
+ drive_waited: hasDrive && knownCount === null,
2532
+ resumed: !!req.resume_session_id,
2533
+ recall,
2534
+ });
2535
+ }
2267
2536
  // ── Dispatcher ─────────────────────────────────────────────────────────────
2268
2537
  async function runRequest(req, entitybox) {
2538
+ const startedAt = Date.now();
2269
2539
  if (await (0, entitybox_queue_request_1.runEntityboxQueueRequest)(req, { rpc: supabase_client_1.rpc, pairing: store_1.readPairing }, entitybox))
2270
2540
  return;
2271
2541
  if (req.entitybox_file_request === true) {
@@ -2294,12 +2564,28 @@ async function runRequest(req, entitybox) {
2294
2564
  return;
2295
2565
  }
2296
2566
  // Model Selector Protocol: start now, so the selector runs alongside the
2297
- // cwd/install/memory setup below instead of after it. Claude print chat
2298
- // turns only; whether it is on for this entity is the server's answer.
2567
+ // cwd/install/memory setup below instead of after it. Claude print and Codex
2568
+ // chat turns only; whether it is on for this entity is the server's answer.
2299
2569
  // Nothing in here may stop the run, so even the gate is guarded.
2570
+ //
2571
+ // The memory pack starts now too, on the person's own words rather than the
2572
+ // whole briefing, when the selector is on. ADDAI_FAST_SETUP=0 turns that part
2573
+ // back to the old order.
2574
+ let mspSetting = null;
2575
+ let earlyPack = null;
2300
2576
  try {
2301
- if ((0, model_selector_1.mspApplies)(req))
2302
- req.mspDecision = startModelSelection(req);
2577
+ if ((0, model_selector_1.mspApplies)(req)) {
2578
+ const setting = fetchModelSelectorSetting(req);
2579
+ mspSetting = setting;
2580
+ req.mspDecision = setting.then(s => (s ? startModelSelection(req, s) : null));
2581
+ const entityId = (0, memory_pack_1.resolvePackEntityId)(req);
2582
+ if (entityId && process.env.ADDAI_FAST_SETUP !== '0') {
2583
+ const message = (0, model_selector_1.messageFromPrompt)(req.prompt ?? '');
2584
+ earlyPack = setting.then(s => (s
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 })
2586
+ : { block: '', degraded: false, flagged: false }));
2587
+ }
2588
+ }
2303
2589
  }
2304
2590
  catch (err) {
2305
2591
  console.error(`[msp] not started [reqId=${req.id}]: ${err.message}`);
@@ -2368,46 +2654,40 @@ async function runRequest(req, entitybox) {
2368
2654
  skills_installed: installed.skillSlugs,
2369
2655
  });
2370
2656
  }
2657
+ const fastSetting = earlyPack ? await mspSetting : null;
2658
+ const fast = !!fastSetting;
2659
+ if (fast) {
2660
+ try {
2661
+ await prepareFastSetup(req, cwd, installed, earlyPack, startedAt, earlyStartWanted(req, fastSetting?.config) ? () => startClaudeEarly(req, cwd, installed) : undefined);
2662
+ }
2663
+ catch (err) {
2664
+ console.error(`[setup] fast setup failed [reqId=${req.id}]: ${err.message}`);
2665
+ }
2666
+ }
2371
2667
  // Fold the entity's +Ai Drive contents into its system prompt (best-effort) so
2372
2668
  // it begins the run already aware of the files it has. All spawn paths read
2373
2669
  // req.system_prompt, so mutating it here reaches every agent variant.
2374
- try {
2375
- const drive = await buildDriveManifest(installed);
2376
- if (drive) {
2377
- const intro = "You have a private +Ai Drive — file storage for working files and artifacts (documents, exports, data you produce) that survives across runs. This is for FILES, not memory: to remember facts, preferences, decisions, or what you learned, use your DocFlows memory (see below), never the Drive and never a local file.";
2378
- const tools = 'Use your +Ai Drive tools (list_files, read_file, write_file, make_folder, …) to read, add, and organise files.';
2379
- // The listing can run to hundreds of lines and changes whenever a file
2380
- // does, so it lives in a file the entity reads when it needs one, and the
2381
- // system prompt keeps a two-line pointer that stays stable turn to turn.
2382
- // If the file cannot be written, the full listing goes inline as before.
2383
- const wrote = (0, context_files_1.writeContextFiles)(cwd, { 'context/drive.md': `# Your +Ai Drive\n\nFiles currently in your Drive:\n${drive.body}\n` }).written.length > 0;
2384
- const driveManifest = wrote
2385
- ? ['## Your +Ai Drive', intro, `Your current file list (${drive.count} entries) is in ./context/drive.md — read it when you need a file.`, tools].join('\n')
2386
- : ['## Your +Ai Drive', `${intro} Files currently in your Drive:`, drive.body, tools].join('\n');
2387
- req.system_prompt =
2388
- req.system_prompt && req.system_prompt.trim().length > 0
2389
- ? `${req.system_prompt}\n\n${driveManifest}`
2390
- : driveManifest;
2670
+ if (!fast)
2671
+ try {
2672
+ const drive = await buildDriveManifest(installed);
2673
+ if (drive) {
2674
+ // The listing can run to hundreds of lines and changes whenever a file
2675
+ // does, so it lives in a file the entity reads when it needs one, and the
2676
+ // system prompt keeps a two-line pointer that stays stable turn to turn.
2677
+ // If the file cannot be written, the full listing goes inline as before.
2678
+ appendToSystemPrompt(req, driveManifestBlock(drive.count, writeDriveFile(cwd, drive.body) ? null : drive.body));
2679
+ }
2680
+ }
2681
+ catch {
2682
+ /* drive manifest is best-effort — never block a run */
2391
2683
  }
2392
- }
2393
- catch {
2394
- /* drive manifest is best-effort — never block a run */
2395
- }
2396
2684
  // Living Presence: entities that carry the entity-self MCP narrate their work on
2397
2685
  // table records via its `report_status` tool. Injected HERE — the one place every
2398
2686
  // spawn path reads (req.system_prompt) — so it reaches EVERY session, board and
2399
2687
  // channel from a single source, never per-board. Gated on the tool actually being
2400
2688
  // installed, so it can't confuse entities that don't have it.
2401
- if (installed.mcps?.some((m) => m.slug === 'entity-self-mcp')) {
2402
- const livingPresence = [
2403
- 'LIVING PRESENCE — narrate your work so people can see what you are doing in real time.',
2404
- 'Whenever you are working on a specific table record (a card / task / row), call the `report_status` tool at your real milestones: when you claim/start it (status "working", a low progress, a short note), at each checkpoint (bump progress + update the note), if you get stuck and need a human (status "blocked"), and when you finish (status "done"). ALWAYS pass the record_id you are working on; if you are working on several records at once, report each separately with its own record_id so they never overwrite each other. This is lightweight — a handful of milestone updates per task; progress is a rough milestone percent (0-100), not a precise measurement.',
2405
- 'THE `note` MUST BE ULTRA-SHORT — 1 to 3 plain, friendly words a teammate could read at a glance, e.g. "Reviewing", "Playing widget", "Writing summary", "Checking design", "Almost done". It renders in a tiny bubble, so NEVER write a sentence and NEVER use technical or internal jargon (not "waiting for publish propagation" — just "Publishing"). Think status label, not explanation.',
2406
- ].join('\n');
2407
- req.system_prompt =
2408
- req.system_prompt && req.system_prompt.trim().length > 0
2409
- ? `${req.system_prompt}\n\n${livingPresence}`
2410
- : livingPresence;
2689
+ if (!fast && installed.mcps?.some((m) => m.slug === 'entity-self-mcp')) {
2690
+ appendToSystemPrompt(req, livingPresenceBlock());
2411
2691
  }
2412
2692
  // Same for the entity's long-term memory: try the +Ai Memory semantic pack
2413
2693
  // (T6's entity-memory-pack edge function) first — it replaces the flat
@@ -2427,20 +2707,21 @@ async function runRequest(req, entitybox) {
2427
2707
  // runtime_pick_next_request returns the bare entity_runtime_requests
2428
2708
  // rowtype (no jsonb aliasing), so that is the real field name; req.entity_id
2429
2709
  // is kept only as a fallback (see resolvePackEntityId in memory-pack.ts).
2430
- try {
2431
- req.system_prompt = await (0, memory_pack_1.applyMemoryPack)(req, req.system_prompt ?? null, config_1.SUPABASE_URL, config_1.SUPABASE_ANON_KEY, {
2432
- buildLegacyManifest: () => buildMemoryManifest(installed),
2433
- onFallback: () => void emit(req.id, 'memory_fallback', { at: 'pack' }),
2434
- // Phase 4 T7: PreCompact hook gate (see precompact-hook.ts). Reuses
2435
- // this same pack fetch's `skipped:"flag"` signal instead of asking
2436
- // the flag question a second time.
2437
- onFlagged: (flagged) => { req.memoryV2Flagged = flagged; },
2438
- });
2439
- }
2440
- catch (err) {
2441
- /* memory manifest is best-effort — never block a run */
2442
- console.error('[memory-pack] wrapper failed:', err?.message);
2443
- }
2710
+ if (!fast)
2711
+ try {
2712
+ req.system_prompt = await (0, memory_pack_1.applyMemoryPack)(req, req.system_prompt ?? null, config_1.SUPABASE_URL, config_1.SUPABASE_ANON_KEY, {
2713
+ buildLegacyManifest: () => buildMemoryManifest(installed),
2714
+ onFallback: () => void emit(req.id, 'memory_fallback', { at: 'pack' }),
2715
+ // Phase 4 T7: PreCompact hook gate (see precompact-hook.ts). Reuses
2716
+ // this same pack fetch's `skipped:"flag"` signal instead of asking
2717
+ // the flag question a second time.
2718
+ onFlagged: (flagged) => { req.memoryV2Flagged = flagged; },
2719
+ });
2720
+ }
2721
+ catch (err) {
2722
+ /* memory manifest is best-effort — never block a run */
2723
+ console.error('[memory-pack] wrapper failed:', err?.message);
2724
+ }
2444
2725
  try {
2445
2726
  // Agent value now encodes the mode for claude variants:
2446
2727
  // 'claude' → claude --print (one-shot stream)
@@ -2471,6 +2752,8 @@ async function runRequest(req, entitybox) {
2471
2752
  }
2472
2753
  }
2473
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();
2474
2757
  // Best-effort cleanup of installed files. If the request crashed
2475
2758
  // mid-flight we may leak files; next install run for the same cwd
2476
2759
  // will overwrite them.