@addai/node 0.24.1 → 0.26.0
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 +23 -0
- package/dist/capabilities.js +48 -0
- package/dist/codex-spawn.js +19 -31
- package/dist/command-runner.js +35 -2
- package/dist/desktop/creds.d.ts +15 -0
- package/dist/desktop/creds.js +28 -0
- package/dist/desktop/manager.d.ts +19 -0
- package/dist/desktop/manager.js +87 -0
- package/dist/desktop/relay-client.js +29 -5
- package/dist/flows/engine-loader.d.ts +8 -0
- package/dist/flows/engine-loader.js +40 -0
- package/dist/flows/host.d.ts +40 -0
- package/dist/flows/host.js +125 -0
- package/dist/flows/pump.d.ts +31 -0
- package/dist/flows/pump.js +264 -0
- package/dist/flows/run.d.ts +7 -0
- package/dist/flows/run.js +219 -0
- package/dist/gemini-spawn.js +54 -2
- package/dist/grok-spawn.js +10 -39
- package/dist/heartbeat.js +24 -0
- package/dist/index.js +22 -4
- package/dist/mcp-headers.d.ts +12 -6
- package/dist/mcp-headers.js +12 -6
- package/dist/session-runner.js +32 -1
- package/dist/subagent-usage.d.ts +19 -0
- package/dist/subagent-usage.js +173 -0
- package/dist/types.d.ts +8 -0
- package/package.json +2 -1
package/dist/gemini-spawn.js
CHANGED
|
@@ -104,8 +104,8 @@ function writeGeminiWorkspaceSettings(cwd, servers) {
|
|
|
104
104
|
...(s.env && Object.keys(s.env).length > 0 ? { env: s.env } : {}),
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
-
// Start with the host's settings so auth method
|
|
108
|
-
//
|
|
107
|
+
// Start with the host's settings so auth method and theme carry through.
|
|
108
|
+
// Override just mcpServers — and the auth type, below.
|
|
109
109
|
const hostSettingsPath = path.join(os.homedir(), '.gemini', 'settings.json');
|
|
110
110
|
let baseSettings = {};
|
|
111
111
|
if (fs.existsSync(hostSettingsPath)) {
|
|
@@ -115,6 +115,24 @@ function writeGeminiWorkspaceSettings(cwd, servers) {
|
|
|
115
115
|
catch { /* corrupt — start fresh */ }
|
|
116
116
|
}
|
|
117
117
|
const merged = { ...baseSettings, mcpServers };
|
|
118
|
+
// Google retired individual Gemini Code Assist OAuth on 18 June 2026: a CLI
|
|
119
|
+
// set to 'oauth-personal' now answers IneligibleTierError and points at
|
|
120
|
+
// Antigravity, whatever else is configured. Most hosts still carry that
|
|
121
|
+
// retired value in ~/.gemini/settings.json, so inheriting it verbatim means
|
|
122
|
+
// the run cannot start.
|
|
123
|
+
//
|
|
124
|
+
// An API key is the supported path that survives (as is vertex-ai for
|
|
125
|
+
// enterprise), so when one is present the auth type is forced to match it
|
|
126
|
+
// rather than trusting whatever the host was left set to. childEnv already
|
|
127
|
+
// passes GEMINI_API_KEY through from the daemon's own environment.
|
|
128
|
+
if (process.env.GEMINI_API_KEY) {
|
|
129
|
+
const security = { ...(baseSettings.security ?? {}) };
|
|
130
|
+
security.auth = {
|
|
131
|
+
...(security.auth ?? {}),
|
|
132
|
+
selectedType: 'gemini-api-key',
|
|
133
|
+
};
|
|
134
|
+
merged.security = security;
|
|
135
|
+
}
|
|
118
136
|
const settingsPath = path.join(dir, 'settings.json');
|
|
119
137
|
fs.writeFileSync(settingsPath, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
120
138
|
return settingsPath;
|
|
@@ -274,6 +292,40 @@ function spawnGemini(input) {
|
|
|
274
292
|
if (input.resumeSessionId && isGeminiSessionId(input.resumeSessionId)) {
|
|
275
293
|
args.push('--resume', input.resumeSessionId);
|
|
276
294
|
}
|
|
295
|
+
// Fail early and legibly when there is nothing to authenticate with. Without
|
|
296
|
+
// this the run dies inside gemini with "IneligibleTierError: This client is no
|
|
297
|
+
// longer supported for Gemini Code Assist for individuals", which reads like
|
|
298
|
+
// an account problem rather than a missing key on this machine.
|
|
299
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
300
|
+
const host = path.join(os.homedir(), '.gemini', 'settings.json');
|
|
301
|
+
let selected = 'oauth-personal';
|
|
302
|
+
try {
|
|
303
|
+
const raw = JSON.parse(fs.readFileSync(host, 'utf8'));
|
|
304
|
+
selected = raw.security?.auth?.selectedType ?? selected;
|
|
305
|
+
}
|
|
306
|
+
catch { /* no host settings — the default is the retired one anyway */ }
|
|
307
|
+
if (selected === 'oauth-personal') {
|
|
308
|
+
const events = (0, events_1.bufferedEvents)([
|
|
309
|
+
{
|
|
310
|
+
type: 'error',
|
|
311
|
+
code: 'gemini_auth_retired',
|
|
312
|
+
message: 'gemini cannot start on this machine: Google retired individual ' +
|
|
313
|
+
'Gemini Code Assist OAuth on 18 June 2026, and this host is still set ' +
|
|
314
|
+
"to 'oauth-personal'. Set GEMINI_API_KEY on the node (or switch the " +
|
|
315
|
+
'host to vertex-ai for an enterprise licence), or point this entity at ' +
|
|
316
|
+
'another agent.',
|
|
317
|
+
},
|
|
318
|
+
{ type: 'turn_complete', stopReason: 'failed' },
|
|
319
|
+
]);
|
|
320
|
+
return {
|
|
321
|
+
pid: undefined,
|
|
322
|
+
sessionId: null,
|
|
323
|
+
kill: () => { },
|
|
324
|
+
onEvent: events.onEvent,
|
|
325
|
+
done: Promise.resolve(1),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
277
329
|
// No HOME override — gemini's auth + shell-out tools all need the
|
|
278
330
|
// real $HOME. Workspace-scope settings.json (written above) is enough
|
|
279
331
|
// to override the host's mcpServers without poisoning everything else.
|
package/dist/grok-spawn.js
CHANGED
|
@@ -74,7 +74,6 @@ const grok_binary_1 = require("./grok-binary");
|
|
|
74
74
|
const win_1 = require("./win");
|
|
75
75
|
const events_1 = require("./events");
|
|
76
76
|
const think_split_1 = require("./think-split");
|
|
77
|
-
const mcp_headers_1 = require("./mcp-headers");
|
|
78
77
|
/** Real config dir for the logged-in account. GROK_HOME points here so the
|
|
79
78
|
* entity uses the same account and token refresh works. */
|
|
80
79
|
function realGrokHome() {
|
|
@@ -161,44 +160,16 @@ function writeProjectMcpConfig(workingDirectory, servers) {
|
|
|
161
160
|
}
|
|
162
161
|
const lines = [];
|
|
163
162
|
for (const s of usable) {
|
|
164
|
-
// Remote MCP servers
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// registry moved from `npx` to `http`: the servers were simply not there,
|
|
175
|
-
// and grok reported it to the model as "(auth required)".
|
|
176
|
-
//
|
|
177
|
-
// mcp-remote is the standard stdio<->HTTP shim (grok's own README reaches
|
|
178
|
-
// for it in its Linear example) and it is verified working here. The cost
|
|
179
|
-
// is an npx start per remote server on grok runs only — the very cost the
|
|
180
|
-
// HTTP move removed for every other agent. Revisit when grok ships real
|
|
181
|
-
// remote-MCP support.
|
|
182
|
-
//
|
|
183
|
-
// Folder trust still gates repo-local config.toml servers, so the --trust
|
|
184
|
-
// launch flag remains load-bearing for these entries to start at all.
|
|
185
|
-
if ((0, mcp_headers_1.isRemoteMcp)(s.command)) {
|
|
186
|
-
const url = (0, mcp_headers_1.remoteMcpUrl)(s.args);
|
|
187
|
-
if (!url) {
|
|
188
|
-
console.error(`[grok] MCP ${s.slug}: ${s.command} server has no URL — skipped`);
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
// The bridge itself is shared — see AGENTS_WITHOUT_HTTP_MCP for which
|
|
192
|
-
// agents need it and the evidence for each.
|
|
193
|
-
const bridge = (0, mcp_headers_1.remoteMcpBridge)(url, s.env);
|
|
194
|
-
const bridged = (0, win_1.wrapMcpCommandForPlatform)(bridge.command, bridge.args);
|
|
195
|
-
lines.push(`[mcp_servers.${tomlStr(s.slug)}]`);
|
|
196
|
-
lines.push(`command = ${tomlStr(bridged.command)}`);
|
|
197
|
-
lines.push(`args = [${bridged.args.map(tomlStr).join(', ')}]`);
|
|
198
|
-
lines.push('enabled = true');
|
|
199
|
-
// The first run on a machine downloads mcp-remote, which 30s does not cover.
|
|
200
|
-
lines.push('startup_timeout_sec = 90');
|
|
201
|
-
lines.push('');
|
|
163
|
+
// Remote MCP servers (registry command='http'|'sse', args=[url]) have no
|
|
164
|
+
// verified grok config spelling, and writing the row verbatim is worse
|
|
165
|
+
// than skipping: grok would try to exec a binary called `http`, the
|
|
166
|
+
// server would never start, and its tools would vanish with no error —
|
|
167
|
+
// the exact silent failure that cost an entity its GitHub access after a
|
|
168
|
+
// provider-ladder hop. Skip it, and say so loudly enough to be findable.
|
|
169
|
+
if (s.command === 'http' || s.command === 'sse') {
|
|
170
|
+
console.error(`[grok] MCP ${s.slug}: remote ${s.command} servers are not supported by the grok ` +
|
|
171
|
+
`adapter — skipping. Its tools will be ABSENT from this run. Use claude (or codex, ` +
|
|
172
|
+
`which supports url + bearer_token_env_var) for entities that depend on it.`);
|
|
202
173
|
continue;
|
|
203
174
|
}
|
|
204
175
|
// `cmd /c` wrapper for npm-shim commands on native Windows (no-op on POSIX).
|
package/dist/heartbeat.js
CHANGED
|
@@ -13,6 +13,8 @@ const supabase_client_1 = require("./supabase-client");
|
|
|
13
13
|
const store_1 = require("./store");
|
|
14
14
|
const capabilities_1 = require("./capabilities");
|
|
15
15
|
const request_pump_1 = require("./request-pump");
|
|
16
|
+
const pump_1 = require("./flows/pump");
|
|
17
|
+
const manager_1 = require("./desktop/manager");
|
|
16
18
|
const config_1 = require("./config");
|
|
17
19
|
let timer = null;
|
|
18
20
|
let stopped = false;
|
|
@@ -59,6 +61,19 @@ async function tick() {
|
|
|
59
61
|
// whatever it already had (its own default of 4).
|
|
60
62
|
if (res && res.max_concurrent_runs != null)
|
|
61
63
|
(0, request_pump_1.setMaxConcurrent)(res.max_concurrent_runs);
|
|
64
|
+
// Flows and desktops ride the same heartbeat for the same reason: they are
|
|
65
|
+
// fields on this node's own row and we are already talking every 30s.
|
|
66
|
+
//
|
|
67
|
+
// Each is applied only when the server actually said something. An older
|
|
68
|
+
// server that has not had the migration says nothing, and nothing must
|
|
69
|
+
// read as "leave it alone" — never as "turn flows off" (which would
|
|
70
|
+
// silently strand a queue) or "cap desktops at zero".
|
|
71
|
+
if (res && typeof res.flows_enabled === 'boolean')
|
|
72
|
+
(0, pump_1.setFlowsEnabled)(res.flows_enabled);
|
|
73
|
+
if (res && res.max_concurrent_flows != null)
|
|
74
|
+
(0, pump_1.setMaxConcurrentFlows)(res.max_concurrent_flows);
|
|
75
|
+
if (res && res.max_concurrent_desktops != null)
|
|
76
|
+
(0, manager_1.setMaxConcurrentDesktops)(res.max_concurrent_desktops);
|
|
62
77
|
}
|
|
63
78
|
catch (err) {
|
|
64
79
|
if (err instanceof supabase_client_1.RpcError && err.status === 401) {
|
|
@@ -124,6 +139,15 @@ async function runTick() {
|
|
|
124
139
|
p_token: pairing.daemonToken,
|
|
125
140
|
p_request_ids: (0, request_pump_1.activeRequestIdList)(),
|
|
126
141
|
});
|
|
142
|
+
// Flow runs are claimed rows too, and the server's reclaim sweep is just
|
|
143
|
+
// as capable of requeueing a healthy one that happens to be quiet. Sent in
|
|
144
|
+
// the same tick so the two never drift apart under load; the empty call
|
|
145
|
+
// matters, because it is what tells the server this build reports flow
|
|
146
|
+
// runs at all.
|
|
147
|
+
await (0, supabase_client_1.rpc)('flow_run_heartbeat', {
|
|
148
|
+
p_token: pairing.daemonToken,
|
|
149
|
+
p_run_ids: (0, pump_1.activeFlowRunIds)(),
|
|
150
|
+
});
|
|
127
151
|
lastRunBeatError = '';
|
|
128
152
|
}
|
|
129
153
|
catch (err) {
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ const autostart_1 = require("./autostart");
|
|
|
56
56
|
const sleep_detector_1 = require("./sleep-detector");
|
|
57
57
|
const claude_config_1 = require("./claude-config");
|
|
58
58
|
const request_pump_1 = require("./request-pump");
|
|
59
|
+
const pump_1 = require("./flows/pump");
|
|
59
60
|
const projects_1 = require("./projects");
|
|
60
61
|
const manager_1 = require("./desktop/manager");
|
|
61
62
|
const relay_client_1 = require("./desktop/relay-client");
|
|
@@ -210,6 +211,12 @@ async function start(argv = []) {
|
|
|
210
211
|
(0, projects_1.start)();
|
|
211
212
|
// …and to keep desktops and the containers behind them in agreement.
|
|
212
213
|
(0, manager_1.startDesktopManager)();
|
|
214
|
+
// The flows pump starts idle and stays that way until the heartbeat reports
|
|
215
|
+
// this machine has flows switched on. Starting it unconditionally is right:
|
|
216
|
+
// the alternative is a daemon that only notices the switch on restart, and
|
|
217
|
+
// "turn it on, then restart the node" is not a setup step anyone should have
|
|
218
|
+
// to be told about.
|
|
219
|
+
(0, pump_1.start)();
|
|
213
220
|
// …and dial out to the relay. That socket is this node's live link: the
|
|
214
221
|
// server pushes work down it the instant it exists, so a command no longer
|
|
215
222
|
// waits up to 30s for the next heartbeat to notice it. The heartbeat and
|
|
@@ -231,6 +238,7 @@ async function start(argv = []) {
|
|
|
231
238
|
(0, manager_1.stopDesktopManager)();
|
|
232
239
|
(0, relay_client_1.stopRelayClient)();
|
|
233
240
|
(0, request_pump_1.stop)();
|
|
241
|
+
(0, pump_1.stop)();
|
|
234
242
|
(0, heartbeat_1.stop)();
|
|
235
243
|
(0, auto_update_1.stop)();
|
|
236
244
|
sleepMonitor.stop();
|
|
@@ -241,10 +249,20 @@ async function start(argv = []) {
|
|
|
241
249
|
// leave paused flow runs stuck `waiting` until server-side reclaim
|
|
242
250
|
// (5 min) requeued or failed them.
|
|
243
251
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
// Both pools drain against the SAME deadline, in parallel. Draining them
|
|
253
|
+
// one after the other would give a machine holding both up to twice the
|
|
254
|
+
// timeout to shut down, and the timeout is there because something
|
|
255
|
+
// upstream is waiting for this process to be gone.
|
|
256
|
+
const [requests, flows] = await Promise.all([
|
|
257
|
+
(0, request_pump_1.drain)(drainTimeoutMs),
|
|
258
|
+
(0, pump_1.drain)(drainTimeoutMs),
|
|
259
|
+
]);
|
|
260
|
+
lastDrain = { drained: requests.drained, timedOut: requests.timedOut };
|
|
261
|
+
if (requests.drained || requests.timedOut) {
|
|
262
|
+
console.log(`[shutdown] drained ${requests.drained} request(s); ${requests.timedOut} still in-flight (timed out)`);
|
|
263
|
+
}
|
|
264
|
+
if (flows.drained || flows.timedOut) {
|
|
265
|
+
console.log(`[shutdown] drained ${flows.drained} flow run(s); ${flows.timedOut} still in-flight (timed out)`);
|
|
248
266
|
}
|
|
249
267
|
}
|
|
250
268
|
catch (err) {
|
package/dist/mcp-headers.d.ts
CHANGED
|
@@ -20,12 +20,18 @@ export declare function remoteMcpHeaders(env?: Record<string, string> | null): R
|
|
|
20
20
|
* claude YES — proven in production; entities use these every day.
|
|
21
21
|
* codex YES — `codex mcp list` shows a `url` entry under Url, enabled.
|
|
22
22
|
* kimi YES — `kimi mcp list` prints "<slug> (http): <url>".
|
|
23
|
-
* gemini
|
|
24
|
-
* (
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
23
|
+
* gemini YES — its shipped bundle treats httpUrl as a network transport
|
|
24
|
+
* (hasNetworkTransport), carries StreamableHTTPClientTransport,
|
|
25
|
+
* and documents "httpUrl (for Streamable HTTP), url (for SSE),
|
|
26
|
+
* and command (for stdio)" with a headers map. Read out of the
|
|
27
|
+
* installed bundle rather than the docs, because docs are what
|
|
28
|
+
* lied about grok. It was never watched connecting, though:
|
|
29
|
+
* gemini-cli cannot authenticate at all on a personal account
|
|
30
|
+
* since Google retired individual Gemini Code Assist OAuth on
|
|
31
|
+
* 18 June 2026 (IneligibleTierError -> migrate to Antigravity).
|
|
32
|
+
* API-key and enterprise auth are unaffected. So gemini's
|
|
33
|
+
* blocker is AUTH, not MCP transport — bridging it would fix
|
|
34
|
+
* nothing. No entity runs gemini today.
|
|
29
35
|
* grok NO — grok 1.0.3 SILENTLY IGNORES url-based entries. In one run it
|
|
30
36
|
* loaded ten stdio servers and skipped every remote one, ours
|
|
31
37
|
* and a third party's, with no error, while its own bundled
|
package/dist/mcp-headers.js
CHANGED
|
@@ -57,12 +57,18 @@ function remoteMcpHeaders(env) {
|
|
|
57
57
|
* claude YES — proven in production; entities use these every day.
|
|
58
58
|
* codex YES — `codex mcp list` shows a `url` entry under Url, enabled.
|
|
59
59
|
* kimi YES — `kimi mcp list` prints "<slug> (http): <url>".
|
|
60
|
-
* gemini
|
|
61
|
-
* (
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
60
|
+
* gemini YES — its shipped bundle treats httpUrl as a network transport
|
|
61
|
+
* (hasNetworkTransport), carries StreamableHTTPClientTransport,
|
|
62
|
+
* and documents "httpUrl (for Streamable HTTP), url (for SSE),
|
|
63
|
+
* and command (for stdio)" with a headers map. Read out of the
|
|
64
|
+
* installed bundle rather than the docs, because docs are what
|
|
65
|
+
* lied about grok. It was never watched connecting, though:
|
|
66
|
+
* gemini-cli cannot authenticate at all on a personal account
|
|
67
|
+
* since Google retired individual Gemini Code Assist OAuth on
|
|
68
|
+
* 18 June 2026 (IneligibleTierError -> migrate to Antigravity).
|
|
69
|
+
* API-key and enterprise auth are unaffected. So gemini's
|
|
70
|
+
* blocker is AUTH, not MCP transport — bridging it would fix
|
|
71
|
+
* nothing. No entity runs gemini today.
|
|
66
72
|
* grok NO — grok 1.0.3 SILENTLY IGNORES url-based entries. In one run it
|
|
67
73
|
* loaded ten stdio servers and skipped every remote one, ours
|
|
68
74
|
* and a third party's, with no error, while its own bundled
|
package/dist/session-runner.js
CHANGED
|
@@ -59,6 +59,7 @@ const events_1 = require("./events");
|
|
|
59
59
|
const paths_1 = require("./paths");
|
|
60
60
|
const projects_1 = require("./projects");
|
|
61
61
|
const claude_print_1 = require("./claude-print");
|
|
62
|
+
const subagent_usage_1 = require("./subagent-usage");
|
|
62
63
|
const attachments_1 = require("./attachments");
|
|
63
64
|
const codex_spawn_1 = require("./codex-spawn");
|
|
64
65
|
const diskguard_1 = require("./diskguard");
|
|
@@ -808,6 +809,26 @@ function isOwnSessionDir(dir) {
|
|
|
808
809
|
const resolved = path.resolve(dir);
|
|
809
810
|
return resolved.startsWith(root + path.sep);
|
|
810
811
|
}
|
|
812
|
+
/**
|
|
813
|
+
* Report what this run's subagents cost, before the run goes terminal.
|
|
814
|
+
*
|
|
815
|
+
* Awaited rather than fired-and-forgotten: the server recomputes a run's
|
|
816
|
+
* rollup from its events the moment the status flips, so an event that lands
|
|
817
|
+
* a beat later would be counted only if something else came along to trigger
|
|
818
|
+
* a recompute. Failure here must never change the run's outcome — a missing
|
|
819
|
+
* figure is a worse report, not a worse run.
|
|
820
|
+
*/
|
|
821
|
+
async function emitSubagentUsage(requestId, cwd, startedAtMs) {
|
|
822
|
+
try {
|
|
823
|
+
const sub = await (0, subagent_usage_1.readSubagentUsage)(cwd, startedAtMs);
|
|
824
|
+
if (!sub)
|
|
825
|
+
return;
|
|
826
|
+
await emit(requestId, 'subagent_usage', { agents: sub.agents, usage: sub.usage });
|
|
827
|
+
}
|
|
828
|
+
catch (err) {
|
|
829
|
+
console.error(`[session-runner] subagent usage [reqId=${requestId}] failed: ${err.message}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
811
832
|
function eventPayload(e) {
|
|
812
833
|
const { type: _type, ...rest } = e;
|
|
813
834
|
return rest;
|
|
@@ -1110,6 +1131,9 @@ async function runClaudeTui(req, cwd, installed) {
|
|
|
1110
1131
|
await new Promise(r => setTimeout(r, 250));
|
|
1111
1132
|
}
|
|
1112
1133
|
}
|
|
1134
|
+
// Floor for "which subagent transcripts belong to this attempt" — see
|
|
1135
|
+
// subagent-usage.ts.
|
|
1136
|
+
const startedAtMs = Date.now();
|
|
1113
1137
|
let spawn;
|
|
1114
1138
|
try {
|
|
1115
1139
|
spawn = (0, claude_spawn_1.spawnClaudeForRuntime)({
|
|
@@ -1509,7 +1533,8 @@ async function runClaudeTui(req, cwd, installed) {
|
|
|
1509
1533
|
// is still non-terminal, reopening the server-side reclaim double-spawn
|
|
1510
1534
|
// window. Resolve only after the chain settles, like the print runners
|
|
1511
1535
|
// which `await finalizeTerminal`.
|
|
1512
|
-
const done = (
|
|
1536
|
+
const done = emitSubagentUsage(req.id, cwd, startedAtMs)
|
|
1537
|
+
.then(() => (0, supabase_client_1.rpc)('runtime_get_request_status', { p_token: token(), p_request_id: req.id }))
|
|
1513
1538
|
.then(currentStatus => {
|
|
1514
1539
|
if (currentStatus === 'canceled')
|
|
1515
1540
|
return;
|
|
@@ -1547,6 +1572,10 @@ async function runClaudePrint(req, cwd, installed) {
|
|
|
1547
1572
|
return;
|
|
1548
1573
|
}
|
|
1549
1574
|
const permissionMode = req.agent === 'claude-bypass' ? 'bypassPermissions' : (req.permission_mode ?? undefined);
|
|
1575
|
+
// Floor for "which subagent transcripts belong to this attempt" — see
|
|
1576
|
+
// subagent-usage.ts. Taken before the spawn so nothing this run writes
|
|
1577
|
+
// can fall outside the window.
|
|
1578
|
+
const startedAtMs = Date.now();
|
|
1550
1579
|
let handle;
|
|
1551
1580
|
try {
|
|
1552
1581
|
handle = (0, claude_print_1.spawnClaudePrint)({
|
|
@@ -1621,6 +1650,8 @@ async function runClaudePrint(req, cwd, installed) {
|
|
|
1621
1650
|
hang.stop();
|
|
1622
1651
|
if (handle.sessionId)
|
|
1623
1652
|
await setStatus(req.id, { spawnedSessionId: handle.sessionId });
|
|
1653
|
+
// The agent has exited, so every subagent transcript it wrote is complete.
|
|
1654
|
+
await emitSubagentUsage(req.id, cwd, startedAtMs);
|
|
1624
1655
|
void emit(req.id, 'session_end', { reason: 'claude_exit', exitCode });
|
|
1625
1656
|
const currentStatus = await (0, supabase_client_1.rpc)('runtime_get_request_status', { p_token: token(), p_request_id: req.id }).catch(() => null);
|
|
1626
1657
|
if (currentStatus !== 'canceled') {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SubagentUsage {
|
|
2
|
+
/** How many subagent transcripts contributed. */
|
|
3
|
+
agents: number;
|
|
4
|
+
/** Anthropic-shaped so the server normalizes it with every other usage
|
|
5
|
+
* block instead of needing a special case. */
|
|
6
|
+
usage: {
|
|
7
|
+
input_tokens: number;
|
|
8
|
+
output_tokens: number;
|
|
9
|
+
cache_read_input_tokens: number;
|
|
10
|
+
cache_creation_input_tokens: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Everything this run's subagents burned, or null if it spawned none.
|
|
15
|
+
*
|
|
16
|
+
* Null rather than zeros on purpose: "no subagents" and "subagents that cost
|
|
17
|
+
* nothing" are different claims, and only the first one is ever true.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readSubagentUsage(cwd: string | null | undefined, sinceMs: number): Promise<SubagentUsage | null>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// What the subagents cost.
|
|
3
|
+
//
|
|
4
|
+
// Claude Code streams the MAIN agent loop to stdout and reports that turn's
|
|
5
|
+
// usage on its `result` line — which is what the daemon forwards and what
|
|
6
|
+
// Entity Studio has always displayed. A Task/Agent subagent is invisible to
|
|
7
|
+
// that stream: it runs inside the parent process, and its transcript is
|
|
8
|
+
// written to a SEPARATE `agent-*.jsonl` beside the session file. Its tokens
|
|
9
|
+
// are never mentioned on stdout, and the tool_result the parent receives
|
|
10
|
+
// carries the subagent's text and nothing else.
|
|
11
|
+
//
|
|
12
|
+
// Measured on a real run before this existed: main loop 3,658,620 tokens,
|
|
13
|
+
// twenty subagents 5,479,638 more. The run was reported at 40% of what it
|
|
14
|
+
// actually cost. This reads the other 60% off the disk the agent just wrote.
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.readSubagentUsage = readSubagentUsage;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const readline = __importStar(require("readline"));
|
|
53
|
+
const paths_1 = require("./paths");
|
|
54
|
+
const claude_binary_1 = require("./claude-binary");
|
|
55
|
+
const isAgentTranscript = (name) => /^agent-.+\.jsonl$/i.test(name);
|
|
56
|
+
/**
|
|
57
|
+
* Which subagent transcripts belong to this run.
|
|
58
|
+
*
|
|
59
|
+
* +Ai gives most runs their own scratch directory (~/.ainode/sessions/<id>),
|
|
60
|
+
* and Claude Code names the transcript folder after the working directory —
|
|
61
|
+
* so for those the whole folder is this run's and nothing else's. A run pinned
|
|
62
|
+
* to a project checkout shares its folder with every other run against that
|
|
63
|
+
* project, so there we fall back to "written since this attempt started",
|
|
64
|
+
* which is exact unless two runs are working the same checkout concurrently.
|
|
65
|
+
*/
|
|
66
|
+
function transcriptsFor(cwd, sinceMs) {
|
|
67
|
+
const dir = path.join(paths_1.CLAUDE_PROJECTS_DIR, (0, claude_binary_1.encodeProjectPath)(cwd));
|
|
68
|
+
let names;
|
|
69
|
+
try {
|
|
70
|
+
names = fs.readdirSync(dir);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const exclusive = path.resolve(cwd).startsWith(path.resolve(paths_1.RUNTIME_SESSIONS_DIR) + path.sep);
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const name of names) {
|
|
78
|
+
if (!isAgentTranscript(name))
|
|
79
|
+
continue;
|
|
80
|
+
const file = path.join(dir, name);
|
|
81
|
+
if (exclusive) {
|
|
82
|
+
out.push(file);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
if (fs.statSync(file).mtimeMs >= sinceMs)
|
|
87
|
+
out.push(file);
|
|
88
|
+
}
|
|
89
|
+
catch { /* vanished */ }
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
94
|
+
/**
|
|
95
|
+
* Sum one transcript's usage.
|
|
96
|
+
*
|
|
97
|
+
* Claude Code writes the same message id more than once — the text first, the
|
|
98
|
+
* tool_use block after — and both copies repeat the identical usage. Summing
|
|
99
|
+
* blind therefore roughly doubles the answer, so this dedupes by message id.
|
|
100
|
+
*/
|
|
101
|
+
async function sumTranscript(file, into) {
|
|
102
|
+
let sawAny = false;
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
let stream;
|
|
105
|
+
try {
|
|
106
|
+
stream = fs.createReadStream(file);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
for await (const line of readline.createInterface({ input: stream, crlfDelay: Infinity })) {
|
|
113
|
+
if (!line.trim())
|
|
114
|
+
continue;
|
|
115
|
+
let o;
|
|
116
|
+
try {
|
|
117
|
+
o = JSON.parse(line);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (o.type !== 'assistant')
|
|
123
|
+
continue;
|
|
124
|
+
const message = o.message;
|
|
125
|
+
const u = message?.usage;
|
|
126
|
+
if (!u)
|
|
127
|
+
continue;
|
|
128
|
+
const id = typeof message?.id === 'string' ? message.id : '';
|
|
129
|
+
if (id) {
|
|
130
|
+
if (seen.has(id))
|
|
131
|
+
continue;
|
|
132
|
+
seen.add(id);
|
|
133
|
+
}
|
|
134
|
+
sawAny = true;
|
|
135
|
+
into.usage.input_tokens += num(u.input_tokens);
|
|
136
|
+
into.usage.output_tokens += num(u.output_tokens);
|
|
137
|
+
into.usage.cache_read_input_tokens += num(u.cache_read_input_tokens);
|
|
138
|
+
into.usage.cache_creation_input_tokens += num(u.cache_creation_input_tokens);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// A half-written or unreadable transcript costs us that subagent's
|
|
143
|
+
// tokens, not the run's terminal status.
|
|
144
|
+
}
|
|
145
|
+
return sawAny;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Everything this run's subagents burned, or null if it spawned none.
|
|
149
|
+
*
|
|
150
|
+
* Null rather than zeros on purpose: "no subagents" and "subagents that cost
|
|
151
|
+
* nothing" are different claims, and only the first one is ever true.
|
|
152
|
+
*/
|
|
153
|
+
async function readSubagentUsage(cwd, sinceMs) {
|
|
154
|
+
if (!cwd)
|
|
155
|
+
return null;
|
|
156
|
+
const files = transcriptsFor(cwd, sinceMs);
|
|
157
|
+
if (!files.length)
|
|
158
|
+
return null;
|
|
159
|
+
const total = {
|
|
160
|
+
agents: 0,
|
|
161
|
+
usage: {
|
|
162
|
+
input_tokens: 0, output_tokens: 0,
|
|
163
|
+
cache_read_input_tokens: 0, cache_creation_input_tokens: 0,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
for (const file of files) {
|
|
167
|
+
if (await sumTranscript(file, total))
|
|
168
|
+
total.agents++;
|
|
169
|
+
}
|
|
170
|
+
if (!total.agents)
|
|
171
|
+
return null;
|
|
172
|
+
return total;
|
|
173
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -25,6 +25,14 @@ export type RuntimeEvent = {
|
|
|
25
25
|
type: 'turn_complete';
|
|
26
26
|
usage?: Record<string, unknown>;
|
|
27
27
|
stopReason?: string;
|
|
28
|
+
}
|
|
29
|
+
/** What this run's subagents cost, read off their own transcripts once the
|
|
30
|
+
* agent has exited. Never reaches stdout, so it is never in turn_complete
|
|
31
|
+
* — see subagent-usage.ts. Emitted at most once per attempt. */
|
|
32
|
+
| {
|
|
33
|
+
type: 'subagent_usage';
|
|
34
|
+
agents: number;
|
|
35
|
+
usage: Record<string, unknown>;
|
|
28
36
|
} | {
|
|
29
37
|
type: 'error';
|
|
30
38
|
code: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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": [
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"node": ">=18"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
+
"@addai/node-flows": "^1.0.0",
|
|
52
53
|
"node-pty": "^1.1.0",
|
|
53
54
|
"ws": "^8.21.3"
|
|
54
55
|
},
|