@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.
- package/dist/capabilities.d.ts +5 -1
- package/dist/capabilities.js +1 -1
- package/dist/claude-print.d.ts +34 -0
- package/dist/claude-print.js +134 -10
- package/dist/codex-models.d.ts +2 -0
- package/dist/codex-models.js +5 -0
- 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.d.ts +8 -0
- package/dist/memory-recall.js +163 -0
- package/dist/model-selector.d.ts +52 -1
- package/dist/model-selector.js +179 -14
- package/dist/session-runner.d.ts +25 -1
- package/dist/session-runner.js +373 -100
- package/dist/time-context.d.ts +10 -0
- package/dist/time-context.js +31 -0
- package/package.json +1 -1
package/dist/session-runner.js
CHANGED
|
@@ -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
|
-
|
|
1581
|
-
|
|
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
|
|
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
|
-
|
|
1593
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
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.
|