@addai/node 0.4.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/README.md +96 -0
- package/dist/agent-auth.d.ts +9 -0
- package/dist/agent-auth.js +96 -0
- package/dist/attachments.d.ts +20 -0
- package/dist/attachments.js +164 -0
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +406 -0
- package/dist/claude-binary.d.ts +16 -0
- package/dist/claude-binary.js +145 -0
- package/dist/claude-config.d.ts +11 -0
- package/dist/claude-config.js +144 -0
- package/dist/claude-print.d.ts +35 -0
- package/dist/claude-print.js +312 -0
- package/dist/claude-spawn.d.ts +40 -0
- package/dist/claude-spawn.js +220 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +137 -0
- package/dist/codex-binary.d.ts +4 -0
- package/dist/codex-binary.js +70 -0
- package/dist/codex-spawn.d.ts +47 -0
- package/dist/codex-spawn.js +320 -0
- package/dist/command-runner.d.ts +9 -0
- package/dist/command-runner.js +592 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +38 -0
- package/dist/control-server.d.ts +5 -0
- package/dist/control-server.js +292 -0
- package/dist/diskguard.d.ts +96 -0
- package/dist/diskguard.js +435 -0
- package/dist/error-classify.d.ts +20 -0
- package/dist/error-classify.js +159 -0
- package/dist/events.d.ts +26 -0
- package/dist/events.js +97 -0
- package/dist/gemini-binary.d.ts +4 -0
- package/dist/gemini-binary.js +64 -0
- package/dist/gemini-spawn.d.ts +52 -0
- package/dist/gemini-spawn.js +369 -0
- package/dist/git-identity.d.ts +18 -0
- package/dist/git-identity.js +104 -0
- package/dist/grok-binary.d.ts +4 -0
- package/dist/grok-binary.js +70 -0
- package/dist/grok-spawn.d.ts +41 -0
- package/dist/grok-spawn.js +495 -0
- package/dist/guardrails.d.ts +32 -0
- package/dist/guardrails.js +125 -0
- package/dist/hang-watchdog.d.ts +17 -0
- package/dist/hang-watchdog.js +48 -0
- package/dist/harness-registry.d.ts +55 -0
- package/dist/harness-registry.js +180 -0
- package/dist/heartbeat.d.ts +13 -0
- package/dist/heartbeat.js +95 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +440 -0
- package/dist/install.d.ts +63 -0
- package/dist/install.js +467 -0
- package/dist/jsonl-tail.d.ts +20 -0
- package/dist/jsonl-tail.js +192 -0
- package/dist/kimi-binary.d.ts +4 -0
- package/dist/kimi-binary.js +68 -0
- package/dist/kimi-spawn.d.ts +47 -0
- package/dist/kimi-spawn.js +319 -0
- package/dist/lockfile.d.ts +30 -0
- package/dist/lockfile.js +169 -0
- package/dist/mcp-config.d.ts +23 -0
- package/dist/mcp-config.js +133 -0
- package/dist/memory-capture.d.ts +20 -0
- package/dist/memory-capture.js +45 -0
- package/dist/memory-pack.d.ts +34 -0
- package/dist/memory-pack.js +171 -0
- package/dist/model-filter.d.ts +4 -0
- package/dist/model-filter.js +34 -0
- package/dist/node-health.d.ts +18 -0
- package/dist/node-health.js +74 -0
- package/dist/pairing.d.ts +20 -0
- package/dist/pairing.js +129 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +48 -0
- package/dist/precompact-hook.d.ts +27 -0
- package/dist/precompact-hook.js +136 -0
- package/dist/projects.d.ts +25 -0
- package/dist/projects.js +255 -0
- package/dist/prompt-keystrokes.d.ts +3 -0
- package/dist/prompt-keystrokes.js +56 -0
- package/dist/pty-helper.d.ts +26 -0
- package/dist/pty-helper.js +156 -0
- package/dist/request-pump.d.ts +12 -0
- package/dist/request-pump.js +249 -0
- package/dist/run-retry.d.ts +28 -0
- package/dist/run-retry.js +81 -0
- package/dist/self-update.d.ts +75 -0
- package/dist/self-update.js +192 -0
- package/dist/session-runner.d.ts +130 -0
- package/dist/session-runner.js +2271 -0
- package/dist/sleep-detector.d.ts +11 -0
- package/dist/sleep-detector.js +32 -0
- package/dist/state-migrate.d.ts +8 -0
- package/dist/state-migrate.js +91 -0
- package/dist/store.d.ts +5 -0
- package/dist/store.js +82 -0
- package/dist/supabase-client.d.ts +15 -0
- package/dist/supabase-client.js +119 -0
- package/dist/time-context.d.ts +10 -0
- package/dist/time-context.js +71 -0
- package/dist/tokens.d.ts +4 -0
- package/dist/tokens.js +33 -0
- package/dist/tui/app.d.ts +35 -0
- package/dist/tui/app.js +82 -0
- package/dist/tui/console-capture.d.ts +13 -0
- package/dist/tui/console-capture.js +62 -0
- package/dist/tui/dashboard.d.ts +42 -0
- package/dist/tui/dashboard.js +205 -0
- package/dist/tui/data.d.ts +57 -0
- package/dist/tui/data.js +62 -0
- package/dist/tui/harnesses.d.ts +16 -0
- package/dist/tui/harnesses.js +354 -0
- package/dist/tui/render.d.ts +21 -0
- package/dist/tui/render.js +116 -0
- package/dist/tui/requests.d.ts +18 -0
- package/dist/tui/requests.js +127 -0
- package/dist/tui/run.d.ts +13 -0
- package/dist/tui/run.js +178 -0
- package/dist/tui/transcript.d.ts +39 -0
- package/dist/tui/transcript.js +234 -0
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +314 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +3 -0
- package/dist/win.d.ts +67 -0
- package/dist/win.js +317 -0
- package/package.json +60 -0
- package/scripts/fix-pty-helper.js +28 -0
- package/scripts/precompact-capture.js +292 -0
- package/scripts/smoke-test.sh +74 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ainode — the +Ai Node background daemon that pairs with +Ai (Supabase)
|
|
3
|
+
// and runs Claude / Codex sessions on behalf of authenticated requests.
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.VERSION = void 0;
|
|
39
|
+
exports.start = start;
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const control_server_1 = require("./control-server");
|
|
42
|
+
const paths_1 = require("./paths");
|
|
43
|
+
const state_migrate_1 = require("./state-migrate");
|
|
44
|
+
const lockfile_1 = require("./lockfile");
|
|
45
|
+
const mcp_config_1 = require("./mcp-config");
|
|
46
|
+
const store_1 = require("./store");
|
|
47
|
+
const supabase_client_1 = require("./supabase-client");
|
|
48
|
+
const pairing_1 = require("./pairing");
|
|
49
|
+
const heartbeat_1 = require("./heartbeat");
|
|
50
|
+
const command_runner_1 = require("./command-runner");
|
|
51
|
+
const self_update_1 = require("./self-update");
|
|
52
|
+
const sleep_detector_1 = require("./sleep-detector");
|
|
53
|
+
const claude_config_1 = require("./claude-config");
|
|
54
|
+
const request_pump_1 = require("./request-pump");
|
|
55
|
+
const projects_1 = require("./projects");
|
|
56
|
+
const session_runner_1 = require("./session-runner");
|
|
57
|
+
const diskguard_1 = require("./diskguard");
|
|
58
|
+
const pty_helper_1 = require("./pty-helper");
|
|
59
|
+
// Read version from package.json at module-load. Previously hardcoded to
|
|
60
|
+
// '0.1.0' and never updated when package.json bumped — the CLI banner lied
|
|
61
|
+
// about which version was actually running, making upgrade verification
|
|
62
|
+
// hard ("addai-entity-runtime --version" always said 0.1.0).
|
|
63
|
+
const VERSION = (() => {
|
|
64
|
+
try {
|
|
65
|
+
// Resolves from compiled dist/ → ../package.json.
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
67
|
+
const pkg = require('../package.json');
|
|
68
|
+
if (typeof pkg.version === 'string' && pkg.version.length > 0)
|
|
69
|
+
return pkg.version;
|
|
70
|
+
}
|
|
71
|
+
catch { /* fall through */ }
|
|
72
|
+
return '0.0.0';
|
|
73
|
+
})();
|
|
74
|
+
exports.VERSION = VERSION;
|
|
75
|
+
// ── Daemon crash guard ──────────────────────────────────────────────────────
|
|
76
|
+
// A long-running daemon that spawns one child process per request must never
|
|
77
|
+
// die because a single request's child emitted an 'error' event with no
|
|
78
|
+
// listener, or a fire-and-forget promise rejected. Node's default is to crash
|
|
79
|
+
// the whole process — which kills every other session and wedges in-flight
|
|
80
|
+
// requests at 'running'. There is no OS supervisor on Windows to restart us,
|
|
81
|
+
// so we log and stay up; each request already has its own failure path
|
|
82
|
+
// (failRequest → 'spawn_failed'/etc.). This is the backstop behind the
|
|
83
|
+
// per-spawn guards in claude-print.ts / claude-spawn.ts / *-binary.ts.
|
|
84
|
+
process.on('uncaughtException', (err) => {
|
|
85
|
+
console.error('[runtime] uncaughtException — daemon staying up:', err?.stack ?? err);
|
|
86
|
+
});
|
|
87
|
+
process.on('unhandledRejection', (reason) => {
|
|
88
|
+
console.error('[runtime] unhandledRejection — daemon staying up:', reason instanceof Error ? reason.stack : reason);
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* Boot the daemon. If we don't have a pairing yet, run the user through
|
|
92
|
+
* the 5-letter code flow first; once paired, kick off the heartbeat.
|
|
93
|
+
*/
|
|
94
|
+
async function start() {
|
|
95
|
+
const migration = (0, state_migrate_1.migrateLegacyStateDir)();
|
|
96
|
+
if (migration.migrated) {
|
|
97
|
+
const parts = [
|
|
98
|
+
migration.copied.length ? `copied ${migration.copied.join(', ')}` : '',
|
|
99
|
+
migration.moved.length ? `moved ${migration.moved.join(', ')}` : '',
|
|
100
|
+
].filter(Boolean).join('; ');
|
|
101
|
+
console.log(`[ainode] state migrated to ~/.ainode (${parts})`);
|
|
102
|
+
}
|
|
103
|
+
(0, mcp_config_1.cleanupAllSessionMcpConfigs)();
|
|
104
|
+
// Pre-accept claude's bypass-permissions warning once so daemon-spawned
|
|
105
|
+
// TUIs don't block on the dialog. Idempotent + best-effort.
|
|
106
|
+
(0, claude_config_1.ensureBypassPermissionsAccepted)();
|
|
107
|
+
// node-pty's spawn-helper can arrive without its +x bit (npm extraction;
|
|
108
|
+
// reproducible under npx). Without it EVERY pty spawn — harness logins
|
|
109
|
+
// and claude TUI runs — dies with "posix_spawnp failed.". Repair here so
|
|
110
|
+
// a node that installed broken heals on restart, not on reinstall.
|
|
111
|
+
{
|
|
112
|
+
const line = (0, pty_helper_1.describeRepair)((0, pty_helper_1.ensurePtyHelperExecutable)());
|
|
113
|
+
if (line)
|
|
114
|
+
console.log(line);
|
|
115
|
+
}
|
|
116
|
+
const control = await (0, control_server_1.startControlServer)();
|
|
117
|
+
let lock;
|
|
118
|
+
try {
|
|
119
|
+
lock = (0, lockfile_1.acquireLockfile)({ port: control.port, version: VERSION, startedAt: Date.now() });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
await control.close();
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
if (!(0, store_1.isPaired)()) {
|
|
126
|
+
try {
|
|
127
|
+
const code = await (0, pairing_1.requestPairCode)();
|
|
128
|
+
(0, pairing_1.printPairingInstructions)(code);
|
|
129
|
+
const result = await (0, pairing_1.awaitPairing)(code);
|
|
130
|
+
console.log(` ✓ Paired (runtime_id: ${result.runtime_id})`);
|
|
131
|
+
console.log('');
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
console.error('Pairing failed:', err.message || err);
|
|
135
|
+
await control.close();
|
|
136
|
+
(0, lockfile_1.releaseLockfile)();
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Begin the heartbeat — this also pushes the first capabilities probe
|
|
141
|
+
// up to Supabase so the runtime appears "online" immediately. When the
|
|
142
|
+
// server reports queued install/login commands, the command runner
|
|
143
|
+
// drains them.
|
|
144
|
+
(0, heartbeat_1.setPendingCommandsHook)(command_runner_1.wake);
|
|
145
|
+
(0, heartbeat_1.start)();
|
|
146
|
+
// If we are the daemon a remote `update_runtime` restarted INTO, close out
|
|
147
|
+
// that command now — with the version we actually came up as. The process
|
|
148
|
+
// that issued the restart could not report this: it had to exit to make
|
|
149
|
+
// room for us. A restart that never came back leaves the command `running`
|
|
150
|
+
// and the node silent, which is exactly the signal we want.
|
|
151
|
+
void finalizeRestartHandoff();
|
|
152
|
+
// Crash recovery: if we restarted while requests were still
|
|
153
|
+
// 'starting'/'running', fail them so paused flow runs can resume.
|
|
154
|
+
// Best effort — pairing failures shouldn't block startup.
|
|
155
|
+
reclaimOrphanedRequests('startup').catch(err => console.error('[startup] reclaim error:', err.message));
|
|
156
|
+
// Sweep stale session dirs (>7 days old). Each request mints a fresh
|
|
157
|
+
// dir under ~/.ainode/sessions/<reqId>/ which can hold a
|
|
158
|
+
// 50-500 MB git clone. Without periodic cleanup, a chatty runtime
|
|
159
|
+
// fills the user's home disk silently over months. Run async at
|
|
160
|
+
// startup; if it fails, we just keep using the host disk.
|
|
161
|
+
sweepStaleSessionDirs().catch(err => console.error('[startup] session sweep error:', err.message));
|
|
162
|
+
// DiskGuard: CCTV-style watermark eviction of the per-run Codex scratch
|
|
163
|
+
// dirs under os.tmpdir(). codex-spawn cleans up its own dir on exit; this
|
|
164
|
+
// is the safety net for SIGKILL/crash/orphan cases that skip that path.
|
|
165
|
+
// On a dedicated runtime host these dirs once grew to ~158 GiB and filled
|
|
166
|
+
// the disk. Sweeps at startup and on its own interval; unref'd so it never
|
|
167
|
+
// keeps the process alive.
|
|
168
|
+
const diskGuard = (0, diskguard_1.getDiskGuard)();
|
|
169
|
+
diskGuard.start();
|
|
170
|
+
// Long-lived zombie sweeper: even when the daemon stays alive, a
|
|
171
|
+
// spawn can die silently (no onExit) or hang past the JSONL timeout
|
|
172
|
+
// and never reach a terminal status. The RPC's 5-minute stale-after
|
|
173
|
+
// window means it's safe to call periodically; in-flight legitimate
|
|
174
|
+
// requests are protected by recent activity timestamps.
|
|
175
|
+
const reclaimInterval = setInterval(() => {
|
|
176
|
+
reclaimOrphanedRequests('periodic').catch(err => console.error('[periodic] reclaim error:', err.message));
|
|
177
|
+
}, 5 * 60 * 1000);
|
|
178
|
+
// Don't keep the process alive just for this timer.
|
|
179
|
+
reclaimInterval.unref?.();
|
|
180
|
+
// Sleep/wake detector: a laptop runtime (e.g. a MacBook) suspends when the
|
|
181
|
+
// lid closes — timers freeze, in-flight runs stall, and the server watchdog
|
|
182
|
+
// eventually fails them. On resume, wall-clock has jumped far past the last
|
|
183
|
+
// probe. When we detect that, immediately push a heartbeat (so the server
|
|
184
|
+
// stops treating us as offline) and reclaim our own in-flight runs (they
|
|
185
|
+
// went stale while suspended) so they re-dispatch — to a backup node if
|
|
186
|
+
// we're still coming back to life.
|
|
187
|
+
const sleepMonitor = (0, sleep_detector_1.startSleepMonitor)({
|
|
188
|
+
intervalMs: 30_000,
|
|
189
|
+
thresholdMs: 90_000, // >90s late = we were suspended (matches server offline window)
|
|
190
|
+
onWake: (gapMs) => {
|
|
191
|
+
console.warn(`[wake] detected ~${Math.round(gapMs / 1000)}s suspend — re-heartbeating and reclaiming in-flight runs`);
|
|
192
|
+
void (0, heartbeat_1.beat)().catch(() => { });
|
|
193
|
+
reclaimOrphanedRequests('wake').catch(err => console.error('[wake] reclaim error:', err.message));
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
// Begin polling for pending requests.
|
|
197
|
+
(0, request_pump_1.start)();
|
|
198
|
+
// …and for pending projects (git clones, future MCP/skill installs).
|
|
199
|
+
(0, projects_1.start)();
|
|
200
|
+
let stopped = false;
|
|
201
|
+
// Outcome of the last drain, so a remote roll can report whether it left
|
|
202
|
+
// work unfinished instead of claiming a clean restart either way.
|
|
203
|
+
let lastDrain = { drained: 0, timedOut: 0 };
|
|
204
|
+
const stop = async (drainTimeoutMs = 15_000) => {
|
|
205
|
+
if (stopped)
|
|
206
|
+
return;
|
|
207
|
+
stopped = true;
|
|
208
|
+
// Stop accepting new work first — otherwise the pump would queue
|
|
209
|
+
// another request between drain start and process exit.
|
|
210
|
+
(0, projects_1.stop)();
|
|
211
|
+
(0, request_pump_1.stop)();
|
|
212
|
+
(0, heartbeat_1.stop)();
|
|
213
|
+
sleepMonitor.stop();
|
|
214
|
+
diskGuard.stop();
|
|
215
|
+
// Graceful drain: wait up to drainTimeoutMs for in-flight requests
|
|
216
|
+
// to settle so child spawns get a chance to finish their final
|
|
217
|
+
// setStatus → notifyFlows webhooks. Without this, a SIGTERM would
|
|
218
|
+
// leave paused flow runs stuck `waiting` until server-side reclaim
|
|
219
|
+
// (5 min) requeued or failed them.
|
|
220
|
+
try {
|
|
221
|
+
const { drained, timedOut } = await (0, request_pump_1.drain)(drainTimeoutMs);
|
|
222
|
+
lastDrain = { drained, timedOut };
|
|
223
|
+
if (drained || timedOut) {
|
|
224
|
+
console.log(`[shutdown] drained ${drained} request(s); ${timedOut} still in-flight (timed out)`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
console.error('[shutdown] drain error:', err.message);
|
|
229
|
+
}
|
|
230
|
+
// Announce offline AFTER draining, so in-flight runs finish on us first
|
|
231
|
+
// (marking offline earlier could let failover reassign a run we're about
|
|
232
|
+
// to complete — a needless double-run). Anything still unfinished past the
|
|
233
|
+
// drain timeout now fails over instantly instead of waiting the 90s window.
|
|
234
|
+
await (0, heartbeat_1.goOffline)();
|
|
235
|
+
await control.close();
|
|
236
|
+
(0, lockfile_1.releaseLockfile)();
|
|
237
|
+
};
|
|
238
|
+
// Remote roll: drain like a clean shutdown, install the target version if
|
|
239
|
+
// this launch mode can, then hand over to a detached replacement. The
|
|
240
|
+
// handoff file is written only AFTER the replacement is spawned, so a
|
|
241
|
+
// failure anywhere before that leaves this daemon running and the command
|
|
242
|
+
// is failed by the caller instead of silently taking the node down.
|
|
243
|
+
(0, command_runner_1.setRestartHook)(async ({ commandId, version }) => {
|
|
244
|
+
const mode = (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
|
|
245
|
+
const plan = (0, self_update_1.planRespawn)(mode, process.argv[1] ?? '', version, process.execPath, process.argv.slice(2));
|
|
246
|
+
if (plan.installFirst) {
|
|
247
|
+
const err = await (0, self_update_1.installGlobal)(version);
|
|
248
|
+
if (err)
|
|
249
|
+
throw new Error(err);
|
|
250
|
+
}
|
|
251
|
+
// Drain generously — a restart is elective, so it should never guillotine
|
|
252
|
+
// a live entity turn the way a 15s SIGTERM drain would.
|
|
253
|
+
await stop(RESTART_DRAIN_MS);
|
|
254
|
+
// Point of no return. Past this line a replacement daemon exists, so we
|
|
255
|
+
// MUST exit — two live daemons on one node is worse than a command left
|
|
256
|
+
// unreported. Hence the handoff write (which only affects reporting) can
|
|
257
|
+
// never abort the exit.
|
|
258
|
+
(0, self_update_1.spawnReplacement)(plan, path.join(paths_1.RUNTIME_HOME, 'daemon.log'), process.cwd());
|
|
259
|
+
try {
|
|
260
|
+
(0, self_update_1.writeHandoff)(paths_1.RUNTIME_HOME, {
|
|
261
|
+
command_id: commandId,
|
|
262
|
+
from_version: VERSION,
|
|
263
|
+
target_version: version,
|
|
264
|
+
mode,
|
|
265
|
+
at: new Date().toISOString(),
|
|
266
|
+
drained: lastDrain.drained,
|
|
267
|
+
timed_out: lastDrain.timedOut,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
console.error('[restart] handoff write failed — the roll still happened, it just cannot self-report:', err.message);
|
|
272
|
+
}
|
|
273
|
+
console.log(`[restart] handed over to ${plan.file} (${mode} → ${version}); exiting`);
|
|
274
|
+
setTimeout(() => process.exit(0), 250).unref?.();
|
|
275
|
+
});
|
|
276
|
+
process.once('SIGINT', () => { stop().finally(() => process.exit(0)); });
|
|
277
|
+
process.once('SIGTERM', () => { stop().finally(() => process.exit(0)); });
|
|
278
|
+
if (process.platform === 'win32') {
|
|
279
|
+
// SIGTERM never fires on Windows. The signals that DO exist there:
|
|
280
|
+
// SIGBREAK (Ctrl+Break) and SIGHUP (console window closed — Windows
|
|
281
|
+
// gives ~5s before hard-killing, enough to release the lockfile and
|
|
282
|
+
// start the drain). Without these, any non-Ctrl+C stop skips cleanup
|
|
283
|
+
// and leaves a stale lockfile that blocks the next daemon start until
|
|
284
|
+
// the pid-liveness check catches up.
|
|
285
|
+
process.once('SIGBREAK', () => { stop().finally(() => process.exit(0)); });
|
|
286
|
+
process.once('SIGHUP', () => { stop().finally(() => process.exit(0)); });
|
|
287
|
+
}
|
|
288
|
+
process.once('beforeExit', () => { (0, lockfile_1.releaseLockfile)(); });
|
|
289
|
+
return { port: lock.port, pid: lock.pid, stop };
|
|
290
|
+
}
|
|
291
|
+
async function sweepStaleSessionDirs() {
|
|
292
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
293
|
+
const fs = require('fs');
|
|
294
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
295
|
+
const path = require('path');
|
|
296
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
297
|
+
const { RUNTIME_SESSIONS_DIR } = require('./paths');
|
|
298
|
+
if (!fs.existsSync(RUNTIME_SESSIONS_DIR))
|
|
299
|
+
return;
|
|
300
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
301
|
+
const cutoff = Date.now() - SEVEN_DAYS_MS;
|
|
302
|
+
// A directory's own mtime only moves when an ENTRY is added or removed, and
|
|
303
|
+
// a continuation turn merely overwrites the files already in place
|
|
304
|
+
// (.entities-runtime.mcp.json, .grok/config.toml, skills). So a chat used
|
|
305
|
+
// every day still looks "last touched" on the day it was created, and a pure
|
|
306
|
+
// conversation — one that never creates or deletes a file — ages into the
|
|
307
|
+
// cutoff while fully live. Take the newest mtime among the dir and its
|
|
308
|
+
// immediate children instead: the per-run config rewrite lands there, which
|
|
309
|
+
// makes this a real liveness signal rather than a creation date.
|
|
310
|
+
const effectiveMtimeMs = (dir, dirMtimeMs) => {
|
|
311
|
+
let newest = dirMtimeMs;
|
|
312
|
+
try {
|
|
313
|
+
for (const child of fs.readdirSync(dir)) {
|
|
314
|
+
try {
|
|
315
|
+
const st = fs.statSync(path.join(dir, child));
|
|
316
|
+
if (st.mtimeMs > newest)
|
|
317
|
+
newest = st.mtimeMs;
|
|
318
|
+
}
|
|
319
|
+
catch { /* skip unreadable child */ }
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch { /* unreadable dir — fall back to its own mtime */ }
|
|
323
|
+
return newest;
|
|
324
|
+
};
|
|
325
|
+
let removed = 0;
|
|
326
|
+
let kept = 0;
|
|
327
|
+
for (const name of fs.readdirSync(RUNTIME_SESSIONS_DIR)) {
|
|
328
|
+
const p = path.join(RUNTIME_SESSIONS_DIR, name);
|
|
329
|
+
try {
|
|
330
|
+
const st = fs.statSync(p);
|
|
331
|
+
if (!st.isDirectory())
|
|
332
|
+
continue;
|
|
333
|
+
if (effectiveMtimeMs(p, st.mtimeMs) < cutoff) {
|
|
334
|
+
// Deleting a session dir un-homes any chat still resuming a session
|
|
335
|
+
// keyed to it, so say which ones went — the old count-only line made
|
|
336
|
+
// that class of breakage invisible.
|
|
337
|
+
console.log(`[startup] sweeping stale session dir: ${p}`);
|
|
338
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
339
|
+
removed++;
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
kept++;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch { /* skip unreadable entry */ }
|
|
346
|
+
}
|
|
347
|
+
if (removed > 0) {
|
|
348
|
+
console.log(`[startup] swept ${removed} session dir(s) unused for 7d; ${kept} kept`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/** How long a remote restart waits for live turns to finish before handing
|
|
352
|
+
* over. Elective, so far more generous than the 15s SIGTERM drain. */
|
|
353
|
+
const RESTART_DRAIN_MS = 10 * 60_000;
|
|
354
|
+
/**
|
|
355
|
+
* Complete the `update_runtime` command that restarted us, if any.
|
|
356
|
+
*
|
|
357
|
+
* Reports the version this process ACTUALLY came up as, which is the whole
|
|
358
|
+
* point: an update that installed but relaunched the old bits (the npx cache
|
|
359
|
+
* trap) reports the old version rather than a cheerful "completed".
|
|
360
|
+
*/
|
|
361
|
+
async function finalizeRestartHandoff() {
|
|
362
|
+
// Read the token BEFORE consuming the handoff: takeHandoff deletes the file,
|
|
363
|
+
// so bailing after it would strand the command as `running` forever with
|
|
364
|
+
// nothing left on disk to retry from.
|
|
365
|
+
const t = (0, store_1.readPairing)()?.daemonToken;
|
|
366
|
+
if (!t)
|
|
367
|
+
return;
|
|
368
|
+
const h = (0, self_update_1.takeHandoff)(paths_1.RUNTIME_HOME);
|
|
369
|
+
if (!h)
|
|
370
|
+
return;
|
|
371
|
+
const changed = h.from_version !== VERSION;
|
|
372
|
+
const wanted = h.target_version;
|
|
373
|
+
const missed = wanted !== 'latest' && wanted !== VERSION;
|
|
374
|
+
try {
|
|
375
|
+
await (0, supabase_client_1.rpc)('runtime_command_update', {
|
|
376
|
+
p_token: t,
|
|
377
|
+
p_id: h.command_id,
|
|
378
|
+
p_status: missed ? 'failed' : 'completed',
|
|
379
|
+
p_payload: {
|
|
380
|
+
step: 'done',
|
|
381
|
+
from_version: h.from_version,
|
|
382
|
+
version: VERSION,
|
|
383
|
+
mode: h.mode,
|
|
384
|
+
restarted: true,
|
|
385
|
+
version_changed: changed,
|
|
386
|
+
drained: h.drained ?? 0,
|
|
387
|
+
// >0 means the drain ceiling expired with work still running — the
|
|
388
|
+
// roll went ahead over the top of it. Visible rather than implied.
|
|
389
|
+
interrupted_runs: h.timed_out ?? 0,
|
|
390
|
+
},
|
|
391
|
+
p_error: missed
|
|
392
|
+
? `restarted, but came up as ${VERSION} (wanted ${wanted})`
|
|
393
|
+
: null,
|
|
394
|
+
});
|
|
395
|
+
console.log(`[restart] back up as ${VERSION} (was ${h.from_version}); command ${h.command_id} closed`);
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
console.error('[restart] could not close command:', err.message);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function reclaimOrphanedRequests(source) {
|
|
402
|
+
// Honour the shared circuit breaker — if Supabase is overloaded, the
|
|
403
|
+
// periodic reclaim isn't urgent (server-side reclaim runs anyway). Skip.
|
|
404
|
+
if ((0, supabase_client_1.rpcShouldSkip)())
|
|
405
|
+
return;
|
|
406
|
+
const pairing = (0, store_1.readPairing)();
|
|
407
|
+
if (!pairing)
|
|
408
|
+
return;
|
|
409
|
+
// v2 RPC: returns {request_id, prior_status, requeued}.
|
|
410
|
+
// requeued=true → row is back to 'pending', the request-pump will
|
|
411
|
+
// pick it up and try again. No webhook fires yet —
|
|
412
|
+
// the flow run is still waiting on a terminal state.
|
|
413
|
+
// requeued=false → retry budget exhausted, row is now 'failed'. We
|
|
414
|
+
// notify flows so the paused run resumes with the
|
|
415
|
+
// failure.
|
|
416
|
+
let rows = null;
|
|
417
|
+
try {
|
|
418
|
+
rows = await (0, supabase_client_1.rpc)('runtime_reclaim_inflight_requests', {
|
|
419
|
+
p_token: pairing.daemonToken,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
console.error(`[${source}] reclaim rpc failed:`, err.message);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (!rows || rows.length === 0)
|
|
427
|
+
return;
|
|
428
|
+
const requeued = rows.filter(r => r.requeued).length;
|
|
429
|
+
const failed = rows.length - requeued;
|
|
430
|
+
console.log(`[${source}] reclaimed ${rows.length} orphaned request(s)`
|
|
431
|
+
+ (requeued ? ` · ${requeued} requeued for retry` : '')
|
|
432
|
+
+ (failed ? ` · ${failed} failed (retry budget exhausted)` : ''));
|
|
433
|
+
// Only notify flows for the failure-final case. Requeued rows will
|
|
434
|
+
// notify normally via setStatus when they reach a terminal state.
|
|
435
|
+
for (const r of rows) {
|
|
436
|
+
if (r.requeued)
|
|
437
|
+
continue;
|
|
438
|
+
void (0, session_runner_1.notifyFlowsIfFlowRequest)(r.request_id).catch(err => console.error(`[${source}] reclaim notify [reqId=${r.request_id}] error: ${err.message}`));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Hidden per-session MCP config written next to the agent's working dir. */
|
|
2
|
+
export declare const MCP_CONFIG_FILENAME = ".ainode.mcp.json";
|
|
3
|
+
/** Pre-rename filename; removed when found so agents don't read a stale copy. */
|
|
4
|
+
export declare const LEGACY_MCP_CONFIG_FILENAME = ".entities-runtime.mcp.json";
|
|
5
|
+
export interface InstallSet {
|
|
6
|
+
/** Registry MCP ids to install (packages are expanded server-side). */
|
|
7
|
+
mcpIds: string[];
|
|
8
|
+
/** Registry skill ids to install. */
|
|
9
|
+
skillIds: string[];
|
|
10
|
+
/** Registry package ids to expand into their member MCPs + skills. */
|
|
11
|
+
packageIds: string[];
|
|
12
|
+
}
|
|
13
|
+
interface ResolvedMcp {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
slug: string;
|
|
17
|
+
command: string;
|
|
18
|
+
args: string[] | null;
|
|
19
|
+
/** Resolved env values from the user's vault credential for this MCP.
|
|
20
|
+
* Empty object when the MCP doesn't need credentials or the user
|
|
21
|
+
* hasn't picked one. */
|
|
22
|
+
env: Record<string, string> | null;
|
|
23
|
+
/** Spec from codeflows_mcps.env_vars — what credentials the MCP wants.
|
|
24
|
+
* Surface for future UI; not used by the daemon directly. */
|
|
25
|
+
env_spec: Array<{
|
|
26
|
+
key: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
required?: boolean;
|
|
29
|
+
help?: string;
|
|
30
|
+
}> | null;
|
|
31
|
+
npm_package: string | null;
|
|
32
|
+
version: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface InstallResult {
|
|
35
|
+
/** Path to a written .mcp.json file (suitable for --mcp-config). null = no MCPs installed. */
|
|
36
|
+
mcpConfigPath: string | null;
|
|
37
|
+
/** Slugs of skills written to <cwd>/.claude/skills/. */
|
|
38
|
+
skillSlugs: string[];
|
|
39
|
+
/** Resolved MCP rows (with env merged) so non-claude agents (codex)
|
|
40
|
+
* can build their own MCP config from the same source of truth. */
|
|
41
|
+
mcps: ResolvedMcp[];
|
|
42
|
+
/** Cleanup callback — removes everything this install wrote. */
|
|
43
|
+
dispose: () => Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Run install side-effects against `cwd` and return paths to clean up
|
|
47
|
+
* later. Safe to call with an empty set — returns a no-op result.
|
|
48
|
+
*/
|
|
49
|
+
export declare function install(cwd: string, set: InstallSet, credentials?: Record<string, unknown>,
|
|
50
|
+
/** Per-session env overrides keyed by MCP slug. Merged into each resolved
|
|
51
|
+
* MCP's env BEFORE writing .mcp.json, overriding credential-supplied
|
|
52
|
+
* values for the same env var. Used by widgetstudio for dynamic
|
|
53
|
+
* streaming auth tokens that can't live in a static vault credential. */
|
|
54
|
+
envOverrides?: Record<string, Record<string, string>>,
|
|
55
|
+
/** Per-skill variable values keyed by skill id → { varName: value }.
|
|
56
|
+
* Overlaid onto each skill's variable defaults and substituted into the
|
|
57
|
+
* skill content via {{var.<name>}} tokens before SKILL.md is written. */
|
|
58
|
+
skillVarValues?: Record<string, Record<string, string>>,
|
|
59
|
+
/** The entity_runtime_requests row being installed. Passed to the server
|
|
60
|
+
* so credential resolution is scoped to THIS request instead of the
|
|
61
|
+
* latest in-flight request on the runtime. */
|
|
62
|
+
requestId?: string): Promise<InstallResult>;
|
|
63
|
+
export {};
|