@addai/node 0.30.16-msp.4 → 0.30.16-msp.5

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.
@@ -84,10 +84,12 @@ interface CapabilitiesShape {
84
84
  /** Present when this build has the Model Selector Protocol, so Studio can
85
85
  * say which nodes would ignore an entity's selector setting instead of
86
86
  * guessing from a version number. enabled is false under ADDAI_MSP=0.
87
- * jev is true on builds that can use Jev as the selector. */
87
+ * jev is true on builds that can use Jev as the selector, codex on builds
88
+ * that pick for Codex turns too. */
88
89
  model_selector?: {
89
90
  enabled: boolean;
90
91
  jev?: boolean;
92
+ codex?: boolean;
91
93
  };
92
94
  }
93
95
  /** What the heartbeat says about claude's models. `models` keeps its old
@@ -510,7 +510,7 @@ async function probeCapabilities() {
510
510
  autostart: (0, autostart_1.status)(),
511
511
  container_engine: containerEngine,
512
512
  flows: await probeFlows(),
513
- model_selector: { enabled: process.env.ADDAI_MSP !== '0', jev: true },
513
+ model_selector: { enabled: process.env.ADDAI_MSP !== '0', jev: true, codex: true },
514
514
  auto_update: (0, auto_update_1.autoUpdateState)(),
515
515
  machine: (0, machine_metrics_1.sampleMachine)(),
516
516
  claude: deco('claude', claude),
@@ -79,3 +79,5 @@ export declare function codexModelsFor(bin: string, key: string): CodexModelRepo
79
79
  * model is not in its list.
80
80
  */
81
81
  export declare function codexModelEfforts(model?: string | null, report?: CodexModelReport | null): string[] | null;
82
+ /** False only when codex has listed its models and this isn't one of them. */
83
+ export declare function codexModelListed(model: string, report?: CodexModelReport | null): boolean;
@@ -71,6 +71,7 @@ exports.probeCodexModels = probeCodexModels;
71
71
  exports.createCodexModelCache = createCodexModelCache;
72
72
  exports.codexModelsFor = codexModelsFor;
73
73
  exports.codexModelEfforts = codexModelEfforts;
74
+ exports.codexModelListed = codexModelListed;
74
75
  const child_process_1 = require("child_process");
75
76
  const fs = __importStar(require("fs"));
76
77
  const os = __importStar(require("os"));
@@ -383,3 +384,7 @@ function codexModelEfforts(model, report = sharedCache.last()) {
383
384
  const entry = model ? report.catalog.find(e => e.id === model) : report.catalog.find(e => e.default);
384
385
  return entry?.efforts?.length ? entry.efforts : null;
385
386
  }
387
+ /** False only when codex has listed its models and this isn't one of them. */
388
+ function codexModelListed(model, report = sharedCache.last()) {
389
+ return !report || report.catalog.some(e => e.id === model);
390
+ }
@@ -1,5 +1,6 @@
1
1
  export declare const TIER_ORDER: readonly ["quick", "standard", "deep", "max"];
2
2
  export type TierName = typeof TIER_ORDER[number];
3
+ export type MspAgent = 'claude' | 'codex';
3
4
  export interface TierSpec {
4
5
  model: string;
5
6
  effort: string;
@@ -77,7 +78,7 @@ export interface MspDecision {
77
78
  * the shape, but the node must not trust it: a typo in a tier must cost that
78
79
  * tier its override, not the turn.
79
80
  */
80
- export declare function parseConfig(raw: unknown): MspConfig | null;
81
+ export declare function parseConfig(raw: unknown, agent?: MspAgent): MspConfig | null;
81
82
  /**
82
83
  * The person's own words for this turn. The chat dispatcher wraps the message
83
84
  * in a bracketed briefing ("[Direct message · … ]\n\n<message>"); the selector
@@ -186,6 +187,8 @@ export declare function decide(opts: {
186
187
  entityName: string;
187
188
  entityModel: string | null;
188
189
  entityEffort: string | null;
190
+ /** Which tiers this turn runs on. Claude's when not said. */
191
+ agent?: MspAgent;
189
192
  run?: SelectorRunner;
190
193
  /** Required for an entity on Jev; without it the turn falls back. */
191
194
  runJev?: JevRunner;
@@ -29,6 +29,12 @@
29
29
  // the server: the node asks the entity-msp-select edge function, which reads
30
30
  // the entity's chosen Vault key. Only the new message goes, never the chat, and
31
31
  // a message with override words isn't sent at all.
32
+ //
33
+ // Claude and Codex turns both come through here. The picking is the same; each
34
+ // has its own models for the tiers (tiers and codex_tiers in the setting), so an
35
+ // entity with both in its chain stays right on either. Claude as the selector
36
+ // needs a Claude login on the node. On a Codex-only node Jev is what can pick,
37
+ // and otherwise the turn falls back and runs as it would have.
32
38
  // Plan: addai-entity-studio docs/model-selector-protocol-plan.md.
33
39
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
34
40
  if (k2 === undefined) k2 = k;
@@ -87,6 +93,7 @@ const os = __importStar(require("os"));
87
93
  const path = __importStar(require("path"));
88
94
  const claude_binary_1 = require("./claude-binary");
89
95
  const config_1 = require("./config");
96
+ const model_filter_1 = require("./model-filter");
90
97
  const win_1 = require("./win");
91
98
  exports.TIER_ORDER = ['quick', 'standard', 'deep', 'max'];
92
99
  const DEFAULT_CONFIG = {
@@ -107,6 +114,15 @@ const DEFAULT_CONFIG = {
107
114
  max_run: { silence_minutes: 15, backstop_hours: 6 },
108
115
  voice: '',
109
116
  };
117
+ // Luna is codex's fast one, Terra its balanced one, Sol its coding one and Astra
118
+ // its most capable. A tier naming a model the node's codex doesn't list runs on
119
+ // the entity's own model instead (session-runner's applyModelSelection).
120
+ const DEFAULT_CODEX_TIERS = {
121
+ quick: { model: 'gpt-6-luna', effort: 'low' },
122
+ standard: { model: 'gpt-6.1-terra', effort: 'medium' },
123
+ deep: { model: 'gpt-6.1-sol', effort: 'high' },
124
+ max: { model: 'gpt-6-astra', effort: 'max', fan_out: true },
125
+ };
110
126
  const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
111
127
  const isTier = (t) => typeof t === 'string' && exports.TIER_ORDER.includes(t);
112
128
  const rank = (t) => exports.TIER_ORDER.indexOf(t);
@@ -117,18 +133,21 @@ const num = (v, lo, hi, dflt) => (typeof v === 'number' && Number.isFinite(v) ?
117
133
  * the shape, but the node must not trust it: a typo in a tier must cost that
118
134
  * tier its override, not the turn.
119
135
  */
120
- function parseConfig(raw) {
136
+ function parseConfig(raw, agent = 'claude') {
121
137
  if (!raw || typeof raw !== 'object' || Array.isArray(raw))
122
138
  return null;
123
139
  const r = raw;
124
140
  if (r.enabled !== true)
125
141
  return null;
126
- const tiers = { ...DEFAULT_CONFIG.tiers };
142
+ // The tiers this turn runs on. A Claude model in codex_tiers, or the other way
143
+ // round, costs that tier its override like any other typo.
144
+ const tiers = { ...(agent === 'codex' ? DEFAULT_CODEX_TIERS : DEFAULT_CONFIG.tiers) };
145
+ const rawTiers = agent === 'codex' ? r.codex_tiers : r.tiers;
127
146
  for (const t of exports.TIER_ORDER) {
128
- const spec = r.tiers?.[t];
147
+ const spec = rawTiers?.[t];
129
148
  const model = str(spec?.model, 80);
130
149
  const effort = str(spec?.effort, 10);
131
- if (model && /^(claude|sonnet|opus|haiku|fable)/i.test(model) && EFFORTS.has(effort)) {
150
+ if (model && (0, model_filter_1.modelForAgent)(agent, model) && EFFORTS.has(effort)) {
132
151
  tiers[t] = { model, effort, ...(spec?.fan_out === true ? { fan_out: true } : {}) };
133
152
  }
134
153
  }
@@ -280,6 +299,9 @@ const label = (model) => {
280
299
  for (const name of ['fable', 'opus', 'sonnet', 'haiku'])
281
300
  if (m.includes(name))
282
301
  return name[0].toUpperCase() + name.slice(1);
302
+ // gpt-6.1-sol reads GPT-6.1-Sol, as codex names it.
303
+ if (m.startsWith('gpt-'))
304
+ return model.split('-').map((p, i) => (i === 0 ? 'GPT' : p.charAt(0).toUpperCase() + p.slice(1))).join('-');
283
305
  return model;
284
306
  };
285
307
  const PLAIN_OPENER = {
@@ -493,7 +515,7 @@ function freshLast(last, now) {
493
515
  * a fallback decision, and the turn runs.
494
516
  */
495
517
  async function decide(opts) {
496
- const cfg = parseConfig(opts.config);
518
+ const cfg = parseConfig(opts.config, opts.agent);
497
519
  if (!cfg)
498
520
  return null;
499
521
  const last = freshLast(opts.last, opts.now ?? Date.now());
@@ -631,7 +653,8 @@ async function decide(opts) {
631
653
  function mspApplies(req) {
632
654
  if (process.env.ADDAI_MSP === '0')
633
655
  return false;
634
- if (req.agent !== 'claude' || req.mode !== 'print')
656
+ // Codex only has the one way to run. Claude's TUI runs can't change model here.
657
+ if (req.agent !== 'codex' && (req.agent !== 'claude' || req.mode !== 'print'))
635
658
  return false;
636
659
  return req.issued_via === 'chatflows' || req.issued_via === 'chat';
637
660
  }
@@ -124,7 +124,7 @@ export interface RuntimeRequestRow {
124
124
  memoryV2Flagged?: boolean;
125
125
  /** Model Selector Protocol — NOT a DB column. Started by runRequest() as
126
126
  * soon as the run is accepted, so the selector's second or so overlaps the
127
- * cwd/install/memory-pack setup; awaited by runClaudePrint() just before
127
+ * cwd/install/memory-pack setup; awaited by applyModelSelection() just before
128
128
  * the spawn. Resolves null for every entity without MSP switched on, and
129
129
  * is undefined for runs it can't apply to. See model-selector.ts. */
130
130
  mspDecision?: Promise<MspDecision | null>;
@@ -1600,6 +1600,7 @@ async function startModelSelection(req) {
1600
1600
  entityName: setting.entity_name || 'the assistant',
1601
1601
  entityModel: req.model,
1602
1602
  entityEffort: req.effort,
1603
+ agent: req.agent === 'codex' ? 'codex' : 'claude',
1603
1604
  runJev: (0, model_selector_1.askJev)(token(), req.id),
1604
1605
  });
1605
1606
  }
@@ -1630,6 +1631,30 @@ function publishModelSelection(req, d, changed) {
1630
1631
  p_decision: { tier: d.tier, model: d.model, effort: d.effort },
1631
1632
  }).catch(err => console.error(`[msp] announce failed [reqId=${req.id}]: ${err.message}`));
1632
1633
  }
1634
+ /** This turn's model and effort, when MSP is on. Written onto req so a guardrail
1635
+ * retry turn stays on the same model. Call it just before the spawn. */
1636
+ async function applyModelSelection(req) {
1637
+ const msp = req.mspDecision ? await req.mspDecision : null;
1638
+ if (!msp)
1639
+ return null;
1640
+ // A ChatGPT login refuses a model codex doesn't list, which would fail the
1641
+ // turn. The node's list can differ from the node the tiers were chosen on.
1642
+ if (req.agent === 'codex' && msp.model && msp.model !== req.model && !(0, codex_models_1.codexModelListed)(msp.model)) {
1643
+ console.log(`[msp] ${msp.model} is not on this node's codex; running the entity's own model [reqId=${req.id}]`);
1644
+ msp.fallback_reason = msp.selector_error = `model_not_on_node: ${msp.model}`;
1645
+ msp.fell_back = true;
1646
+ msp.line = null;
1647
+ msp.model = req.model;
1648
+ msp.effort = req.effort;
1649
+ }
1650
+ const changed = (!!msp.model && msp.model !== req.model) || (!!msp.effort && msp.effort !== req.effort);
1651
+ if (msp.model)
1652
+ req.model = msp.model;
1653
+ if (msp.effort)
1654
+ req.effort = msp.effort;
1655
+ publishModelSelection(req, msp, changed);
1656
+ return msp;
1657
+ }
1633
1658
  // ── claude --print ─────────────────────────────────────────────────────────
1634
1659
  async function runClaudePrint(req, cwd, installed) {
1635
1660
  if (!req.prompt) {
@@ -1661,17 +1686,7 @@ async function runClaudePrint(req, cwd, installed) {
1661
1686
  abort.abort();
1662
1687
  }
1663
1688
  }
1664
- // Model Selector Protocol: this turn's model and effort, when it is on.
1665
- // Written onto req so a guardrail retry turn stays on the same model.
1666
- const msp = req.mspDecision ? await req.mspDecision : null;
1667
- if (msp) {
1668
- const changed = (!!msp.model && msp.model !== req.model) || (!!msp.effort && msp.effort !== req.effort);
1669
- if (msp.model)
1670
- req.model = msp.model;
1671
- if (msp.effort)
1672
- req.effort = msp.effort;
1673
- publishModelSelection(req, msp, changed);
1674
- }
1689
+ const msp = await applyModelSelection(req);
1675
1690
  let handle;
1676
1691
  try {
1677
1692
  handle = (0, claude_print_1.spawnClaudePrint)({
@@ -1811,6 +1826,7 @@ async function runCodex(req, cwd, installed) {
1811
1826
  void emit(req.id, 'session_dropped_missing_rollout', { sessionId: req.resume_session_id });
1812
1827
  req.resume_session_id = null;
1813
1828
  }
1829
+ const msp = await applyModelSelection(req);
1814
1830
  let handle;
1815
1831
  try {
1816
1832
  const codexModel = (0, model_filter_1.modelForAgent)('codex', req.model);
@@ -1849,11 +1865,15 @@ async function runCodex(req, cwd, installed) {
1849
1865
  // call looks identical to a hang. Give it grok's leash: standard window
1850
1866
  // before any output (a wedged spawn/resume trips fast), 3x after activity
1851
1867
  // has been seen (silent tool work is expected, not a hang). Raw stdout/
1852
- // stderr bytes count as liveness via onActivity.
1868
+ // stderr bytes count as liveness via onActivity. An MSP run can lengthen
1869
+ // that leash with max_run.silence_minutes but not shorten it, because silence
1870
+ // here can be a tool working. It is stopped rather than retried (below).
1853
1871
  const sawActivity = { current: false };
1854
- const hang = armHangWatchdog(req, lastError, () => handle.kill(), {
1872
+ const silenced = { current: false };
1873
+ const quietMs = () => Math.max(config_1.HANG_WATCHDOG_IDLE_MS * 3, (msp?.max_run.silence_minutes ?? 0) * 60_000);
1874
+ const hang = armHangWatchdog(req, lastError, () => { silenced.current = true; handle.kill(); }, {
1855
1875
  sawActivity: () => sawActivity.current,
1856
- idleMsFn: () => (sawActivity.current ? config_1.HANG_WATCHDOG_IDLE_MS * 3 : config_1.HANG_WATCHDOG_IDLE_MS),
1876
+ idleMsFn: () => (sawActivity.current ? quietMs() : config_1.HANG_WATCHDOG_IDLE_MS),
1857
1877
  });
1858
1878
  handle.onActivity(() => { sawActivity.current = true; hang.touch(); });
1859
1879
  handle.onEvent(ev => {
@@ -1869,7 +1889,7 @@ async function runCodex(req, cwd, installed) {
1869
1889
  });
1870
1890
  // Node-configured execution timeout (null/0 = unlimited). On timeout we
1871
1891
  // kill the agent; the completion below treats it as complete-with-partial.
1872
- const nodeTimeout = armNodeTimeout(req, () => handle.kill());
1892
+ const nodeTimeout = armNodeTimeout(req, () => handle.kill(), msp ? msp.max_run.backstop_hours * 3600 : undefined);
1873
1893
  let exitCode;
1874
1894
  try {
1875
1895
  exitCode = await handle.done;
@@ -1889,6 +1909,14 @@ async function runCodex(req, cwd, installed) {
1889
1909
  nodeTimeout.clear();
1890
1910
  watcher.stop();
1891
1911
  hang.stop();
1912
+ // An MSP run that worked and then went quiet ends with what it has, and says
1913
+ // so, instead of being retried from the top. As in runClaudePrint.
1914
+ const mspSilenced = !!msp && silenced.current && sawActivity.current;
1915
+ if (mspSilenced) {
1916
+ await emit(req.id, 'assistant_text', {
1917
+ delta: `\n\n_I went quiet for ${Math.round(quietMs() / 60_000)} minutes, so I was stopped here._`,
1918
+ });
1919
+ }
1892
1920
  if (handle.threadId)
1893
1921
  await setStatus(req.id, { spawnedSessionId: handle.threadId });
1894
1922
  void emit(req.id, 'session_end', { reason: 'codex_exit', exitCode });
@@ -1896,7 +1924,7 @@ async function runCodex(req, cwd, installed) {
1896
1924
  if (currentStatus !== 'canceled') {
1897
1925
  // A timed-out run completes-with-partial: the node asked for a cap, not
1898
1926
  // a failure, so surface whatever the agent produced.
1899
- const cleanExit = exitCode === 0 || nodeTimeout.timedOut();
1927
+ const cleanExit = exitCode === 0 || nodeTimeout.timedOut() || mspSilenced;
1900
1928
  await finalizeTerminal(req, {
1901
1929
  clean: cleanExit,
1902
1930
  errorCode: cleanExit ? undefined : (lastError.current?.code ?? `exit_${exitCode}`),
@@ -2305,8 +2333,8 @@ async function runRequest(req, entitybox) {
2305
2333
  return;
2306
2334
  }
2307
2335
  // Model Selector Protocol: start now, so the selector runs alongside the
2308
- // cwd/install/memory setup below instead of after it. Claude print chat
2309
- // turns only; whether it is on for this entity is the server's answer.
2336
+ // cwd/install/memory setup below instead of after it. Claude print and Codex
2337
+ // chat turns only; whether it is on for this entity is the server's answer.
2310
2338
  // Nothing in here may stop the run, so even the gate is guarded.
2311
2339
  try {
2312
2340
  if ((0, model_selector_1.mspApplies)(req))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.30.16-msp.4",
3
+ "version": "0.30.16-msp.5",
4
4
  "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
5
  "license": "MIT",
6
6
  "keywords": [