@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.
@@ -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,16 +1605,36 @@ 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), put the
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))
1627
+ return;
1628
+ void (0, supabase_client_1.rpc)('runtime_model_selector_announce', {
1629
+ p_token: token(),
1630
+ p_request_id: req.id,
1631
+ p_line: d.line,
1632
+ p_decision: { tier: d.tier, model: d.model, effort: d.effort },
1633
+ }).catch(err => console.error(`[msp] announce failed [reqId=${req.id}]: ${err.message}`));
1634
+ }
1635
+ /** Record the decision (the next turn reads it back to stay steady) and put the
1611
1636
  * model and effort that will really run on the request row so usage counts
1612
- * them, and post the line. All best-effort: the turn has already been decided. */
1637
+ * them. All best-effort: the turn has already been decided. */
1613
1638
  function publishModelSelection(req, d, changed) {
1614
1639
  void emit(req.id, 'msp_decision', d);
1615
1640
  if (changed && req.model && req.effort) {
@@ -1620,22 +1645,95 @@ function publishModelSelection(req, d, changed) {
1620
1645
  p_effort: req.effort,
1621
1646
  }).catch(err => console.error(`[msp] record failed [reqId=${req.id}]: ${err.message}`));
1622
1647
  }
1623
- if (!d.line)
1624
- return;
1625
- void (0, supabase_client_1.rpc)('runtime_model_selector_announce', {
1626
- p_token: token(),
1627
- p_request_id: req.id,
1628
- p_line: d.line,
1629
- p_decision: { tier: d.tier, model: d.model, effort: d.effort },
1630
- }).catch(err => console.error(`[msp] announce failed [reqId=${req.id}]: ${err.message}`));
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 */ }
1631
1686
  }
1632
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
+ }
1633
1732
  async function runClaudePrint(req, cwd, installed) {
1634
1733
  if (!req.prompt) {
1635
1734
  await setStatus(req.id, { status: 'failed', errorCode: 'no_prompt', errorMessage: 'print mode requires a prompt' });
1636
1735
  return;
1637
1736
  }
1638
- const permissionMode = req.agent === 'claude-bypass' ? 'bypassPermissions' : (req.permission_mode ?? undefined);
1639
1737
  // Floor for "which subagent transcripts belong to this attempt" — see
1640
1738
  // subagent-usage.ts. Taken before the spawn so nothing this run writes
1641
1739
  // can fall outside the window.
@@ -1660,31 +1758,25 @@ async function runClaudePrint(req, cwd, installed) {
1660
1758
  abort.abort();
1661
1759
  }
1662
1760
  }
1663
- // Model Selector Protocol: this turn's model and effort, when it is on.
1664
- // Written onto req so a guardrail retry turn stays on the same model.
1665
- const msp = req.mspDecision ? await req.mspDecision : null;
1666
- if (msp) {
1667
- const changed = (!!msp.model && msp.model !== req.model) || (!!msp.effort && msp.effort !== req.effort);
1668
- if (msp.model)
1669
- req.model = msp.model;
1670
- if (msp.effort)
1671
- req.effort = msp.effort;
1672
- publishModelSelection(req, msp, changed);
1673
- }
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;
1674
1767
  let handle;
1675
1768
  try {
1676
- handle = (0, claude_print_1.spawnClaudePrint)({
1677
- workingDirectory: cwd,
1678
- prompt: req.prompt,
1679
- ...(inputBlocks ? { inputBlocks } : {}),
1680
- permissionMode,
1681
- effortLevel: req.effort ?? undefined,
1682
- model: (0, model_filter_1.modelForAgent)('claude', req.model),
1683
- mcpConfigPath: installed.mcpConfigPath ?? undefined,
1684
- resumeSessionId: req.resume_session_id ?? undefined,
1685
- appendSystemPrompt: (0, time_context_1.appendTimeContext)(req.system_prompt),
1686
- allowedTools: req.allowed_tools ?? undefined,
1687
- });
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
+ }
1688
1780
  }
1689
1781
  catch (err) {
1690
1782
  await failRequest(req, 'spawn_failed', err.message);
@@ -1745,12 +1837,16 @@ async function runClaudePrint(req, cwd, installed) {
1745
1837
  nodeTimeout.clear();
1746
1838
  watcher.stop();
1747
1839
  hang.stop();
1840
+ if (mcpDebugFile)
1841
+ reportMcpTimings(req, mcpDebugFile);
1748
1842
  await failRequest(req, 'spawn_failed', err.message);
1749
1843
  return;
1750
1844
  }
1751
1845
  nodeTimeout.clear();
1752
1846
  watcher.stop();
1753
1847
  hang.stop();
1848
+ if (mcpDebugFile)
1849
+ reportMcpTimings(req, mcpDebugFile);
1754
1850
  // An MSP run that worked and then went quiet for max_run.silence_minutes
1755
1851
  // ends with what it has, and says so, instead of being retried from the
1756
1852
  // top. One that never produced anything still takes the old route (retry,
@@ -1810,6 +1906,7 @@ async function runCodex(req, cwd, installed) {
1810
1906
  void emit(req.id, 'session_dropped_missing_rollout', { sessionId: req.resume_session_id });
1811
1907
  req.resume_session_id = null;
1812
1908
  }
1909
+ const msp = await applyModelSelection(req);
1813
1910
  let handle;
1814
1911
  try {
1815
1912
  const codexModel = (0, model_filter_1.modelForAgent)('codex', req.model);
@@ -1848,11 +1945,15 @@ async function runCodex(req, cwd, installed) {
1848
1945
  // call looks identical to a hang. Give it grok's leash: standard window
1849
1946
  // before any output (a wedged spawn/resume trips fast), 3x after activity
1850
1947
  // has been seen (silent tool work is expected, not a hang). Raw stdout/
1851
- // 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).
1852
1951
  const sawActivity = { current: false };
1853
- 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(); }, {
1854
1955
  sawActivity: () => sawActivity.current,
1855
- 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),
1856
1957
  });
1857
1958
  handle.onActivity(() => { sawActivity.current = true; hang.touch(); });
1858
1959
  handle.onEvent(ev => {
@@ -1868,7 +1969,7 @@ async function runCodex(req, cwd, installed) {
1868
1969
  });
1869
1970
  // Node-configured execution timeout (null/0 = unlimited). On timeout we
1870
1971
  // kill the agent; the completion below treats it as complete-with-partial.
1871
- const nodeTimeout = armNodeTimeout(req, () => handle.kill());
1972
+ const nodeTimeout = armNodeTimeout(req, () => handle.kill(), msp ? msp.max_run.backstop_hours * 3600 : undefined);
1872
1973
  let exitCode;
1873
1974
  try {
1874
1975
  exitCode = await handle.done;
@@ -1888,6 +1989,14 @@ async function runCodex(req, cwd, installed) {
1888
1989
  nodeTimeout.clear();
1889
1990
  watcher.stop();
1890
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
+ }
1891
2000
  if (handle.threadId)
1892
2001
  await setStatus(req.id, { spawnedSessionId: handle.threadId });
1893
2002
  void emit(req.id, 'session_end', { reason: 'codex_exit', exitCode });
@@ -1895,7 +2004,7 @@ async function runCodex(req, cwd, installed) {
1895
2004
  if (currentStatus !== 'canceled') {
1896
2005
  // A timed-out run completes-with-partial: the node asked for a cap, not
1897
2006
  // a failure, so surface whatever the agent produced.
1898
- const cleanExit = exitCode === 0 || nodeTimeout.timedOut();
2007
+ const cleanExit = exitCode === 0 || nodeTimeout.timedOut() || mspSilenced;
1899
2008
  await finalizeTerminal(req, {
1900
2009
  clean: cleanExit,
1901
2010
  errorCode: cleanExit ? undefined : (lastError.current?.code ?? `exit_${exitCode}`),
@@ -2185,7 +2294,7 @@ async function fetchDriveList(apiKey, workspaceId, folderId, signal) {
2185
2294
  return null;
2186
2295
  return (await res.json());
2187
2296
  }
2188
- async function buildDriveManifest(installed) {
2297
+ async function buildDriveManifest(installed, concurrent = false) {
2189
2298
  const drive = installed.mcps.find((m) => m.slug === 'addai-drive');
2190
2299
  const env = drive?.env ?? null;
2191
2300
  const apiKey = env?.ADDAI_DRIVE_API_KEY;
@@ -2205,9 +2314,12 @@ async function buildDriveManifest(installed) {
2205
2314
  lines.push(fmt(f));
2206
2315
  // One level of subfolders so the entity sees its structure, capped to keep
2207
2316
  // the injection small and the listing fast.
2208
- 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()) {
2209
2321
  lines.push(` - ${sub.name}/`);
2210
- const child = await fetchDriveList(apiKey, workspaceId, sub.id, controller.signal).catch(() => null);
2322
+ const child = listed ? listed[i] : await listSub(sub.id);
2211
2323
  for (const f of (child?.files ?? []).slice(0, 30))
2212
2324
  lines.push(` - ${sub.name}/${f.name}`);
2213
2325
  }
@@ -2274,8 +2386,156 @@ async function buildMemoryManifest(installed) {
2274
2386
  clearTimeout(timer);
2275
2387
  }
2276
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
+ }
2277
2536
  // ── Dispatcher ─────────────────────────────────────────────────────────────
2278
2537
  async function runRequest(req, entitybox) {
2538
+ const startedAt = Date.now();
2279
2539
  if (await (0, entitybox_queue_request_1.runEntityboxQueueRequest)(req, { rpc: supabase_client_1.rpc, pairing: store_1.readPairing }, entitybox))
2280
2540
  return;
2281
2541
  if (req.entitybox_file_request === true) {
@@ -2304,12 +2564,28 @@ async function runRequest(req, entitybox) {
2304
2564
  return;
2305
2565
  }
2306
2566
  // Model Selector Protocol: start now, so the selector runs alongside the
2307
- // cwd/install/memory setup below instead of after it. Claude print chat
2308
- // 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.
2309
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;
2310
2576
  try {
2311
- if ((0, model_selector_1.mspApplies)(req))
2312
- 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
+ }
2313
2589
  }
2314
2590
  catch (err) {
2315
2591
  console.error(`[msp] not started [reqId=${req.id}]: ${err.message}`);
@@ -2378,46 +2654,40 @@ async function runRequest(req, entitybox) {
2378
2654
  skills_installed: installed.skillSlugs,
2379
2655
  });
2380
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
+ }
2381
2667
  // Fold the entity's +Ai Drive contents into its system prompt (best-effort) so
2382
2668
  // it begins the run already aware of the files it has. All spawn paths read
2383
2669
  // req.system_prompt, so mutating it here reaches every agent variant.
2384
- try {
2385
- const drive = await buildDriveManifest(installed);
2386
- if (drive) {
2387
- 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.";
2388
- const tools = 'Use your +Ai Drive tools (list_files, read_file, write_file, make_folder, …) to read, add, and organise files.';
2389
- // The listing can run to hundreds of lines and changes whenever a file
2390
- // does, so it lives in a file the entity reads when it needs one, and the
2391
- // system prompt keeps a two-line pointer that stays stable turn to turn.
2392
- // If the file cannot be written, the full listing goes inline as before.
2393
- 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;
2394
- const driveManifest = wrote
2395
- ? ['## 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')
2396
- : ['## Your +Ai Drive', `${intro} Files currently in your Drive:`, drive.body, tools].join('\n');
2397
- req.system_prompt =
2398
- req.system_prompt && req.system_prompt.trim().length > 0
2399
- ? `${req.system_prompt}\n\n${driveManifest}`
2400
- : 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 */
2401
2683
  }
2402
- }
2403
- catch {
2404
- /* drive manifest is best-effort — never block a run */
2405
- }
2406
2684
  // Living Presence: entities that carry the entity-self MCP narrate their work on
2407
2685
  // table records via its `report_status` tool. Injected HERE — the one place every
2408
2686
  // spawn path reads (req.system_prompt) — so it reaches EVERY session, board and
2409
2687
  // channel from a single source, never per-board. Gated on the tool actually being
2410
2688
  // installed, so it can't confuse entities that don't have it.
2411
- if (installed.mcps?.some((m) => m.slug === 'entity-self-mcp')) {
2412
- const livingPresence = [
2413
- 'LIVING PRESENCE — narrate your work so people can see what you are doing in real time.',
2414
- '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.',
2415
- '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.',
2416
- ].join('\n');
2417
- req.system_prompt =
2418
- req.system_prompt && req.system_prompt.trim().length > 0
2419
- ? `${req.system_prompt}\n\n${livingPresence}`
2420
- : livingPresence;
2689
+ if (!fast && installed.mcps?.some((m) => m.slug === 'entity-self-mcp')) {
2690
+ appendToSystemPrompt(req, livingPresenceBlock());
2421
2691
  }
2422
2692
  // Same for the entity's long-term memory: try the +Ai Memory semantic pack
2423
2693
  // (T6's entity-memory-pack edge function) first — it replaces the flat
@@ -2437,20 +2707,21 @@ async function runRequest(req, entitybox) {
2437
2707
  // runtime_pick_next_request returns the bare entity_runtime_requests
2438
2708
  // rowtype (no jsonb aliasing), so that is the real field name; req.entity_id
2439
2709
  // is kept only as a fallback (see resolvePackEntityId in memory-pack.ts).
2440
- try {
2441
- req.system_prompt = await (0, memory_pack_1.applyMemoryPack)(req, req.system_prompt ?? null, config_1.SUPABASE_URL, config_1.SUPABASE_ANON_KEY, {
2442
- buildLegacyManifest: () => buildMemoryManifest(installed),
2443
- onFallback: () => void emit(req.id, 'memory_fallback', { at: 'pack' }),
2444
- // Phase 4 T7: PreCompact hook gate (see precompact-hook.ts). Reuses
2445
- // this same pack fetch's `skipped:"flag"` signal instead of asking
2446
- // the flag question a second time.
2447
- onFlagged: (flagged) => { req.memoryV2Flagged = flagged; },
2448
- });
2449
- }
2450
- catch (err) {
2451
- /* memory manifest is best-effort — never block a run */
2452
- console.error('[memory-pack] wrapper failed:', err?.message);
2453
- }
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
+ }
2454
2725
  try {
2455
2726
  // Agent value now encodes the mode for claude variants:
2456
2727
  // 'claude' → claude --print (one-shot stream)
@@ -2481,6 +2752,8 @@ async function runRequest(req, entitybox) {
2481
2752
  }
2482
2753
  }
2483
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();
2484
2757
  // Best-effort cleanup of installed files. If the request crashed
2485
2758
  // mid-flight we may leak files; next install run for the same cwd
2486
2759
  // will overwrite them.