@addai/node 0.8.2 → 0.10.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 CHANGED
@@ -1,96 +1,96 @@
1
- # entities-runtime
2
-
3
- A standalone Node.js daemon. Install it on any machine, pair with your
4
- +Ai account, and your account can then run Claude Code / Codex sessions
5
- on that machine on its behalf — from Vault, from Entity Studio, or from
6
- anywhere else in +Ai.
7
-
8
- ## Quick start
9
-
10
- ```bash
11
- npx entities-runtime
12
- ```
13
-
14
- The first run prints a 5-letter pairing code and a URL:
15
-
16
- ```
17
- Pair this runtime with your +Ai account.
18
-
19
- https://vault.add.ai/entity/connect/HDXYC
20
-
21
- Enter this code: HDXYC
22
- ```
23
-
24
- Open the URL while signed into Vault, enter the code, and the runtime
25
- starts polling for work. Capabilities (claude, codex, git, node) are
26
- reported every 30 s.
27
-
28
- ## Commands
29
-
30
- ```
31
- entities-runtime run the daemon (foreground)
32
- entities-runtime status pairing + capabilities + last-seen
33
- entities-runtime sessions recent requests handled (--limit N)
34
- entities-runtime unpair --yes disconnect from +Ai
35
- entities-runtime version print version
36
- ```
37
-
38
- `status` and `sessions` work without the daemon running — they hit
39
- Supabase directly using the daemon token in
40
- `~/.entities-runtime/state.json`.
41
-
42
- ## Architecture
43
-
44
- See [`docs/architecture.md`](docs/architecture.md) for the full picture:
45
- Supabase tables, RPC inventory, request lifecycle, and the cancel-watcher
46
- flow.
47
-
48
- ## DiskGuard — Codex scratch-dir disk protection
49
-
50
- Each Codex run gets a throwaway `CODEX_HOME` under the OS temp dir
51
- (`entities-codex-<rand>`). Codex writes its `state_*.sqlite`, logs, caches
52
- and shell snapshots there — typically 50–350 MB per run. codex-spawn now
53
- deletes that dir on exit, but a `SIGKILL`, a crash, or an orphan left by a
54
- previous runtime process can skip that path. On a dedicated host doing
55
- thousands of runs, those leftovers once grew to ~158 GiB and filled the
56
- disk (`ENOSPC`, repeated crashes).
57
-
58
- **DiskGuard** is the safety net. Like a CCTV DVR overwriting the oldest
59
- footage when the disk fills, it watches free space on the volume holding
60
- the temp dir and, when usage crosses a high-water mark, deletes the
61
- **oldest inactive** scratch dirs — oldest first — until usage drops back
62
- below a low-water mark, leaving a buffer for the OS. It runs at startup, on
63
- an interval, and opportunistically right before each new Codex run.
64
-
65
- Safety guarantees: it only touches directories directly under the temp root
66
- whose names match the configured prefix (realpath-validated); it never
67
- deletes a dir owned by a live session (in-process, or one whose
68
- `session.lock` PID is still alive) or one younger than the min-age guard;
69
- and it removes symlink *entries* without following them, so the durable
70
- `~/.codex` targets behind `sessions`/`auth.json` are never harmed.
71
-
72
- Note: DiskGuard can only reclaim space by deleting scratch dirs it owns. If
73
- a disk is full of *other* data it will evict what it can and log that it
74
- couldn't reach the target rather than touching anything it doesn't own.
75
-
76
- Configure via environment variables (defaults shown):
77
-
78
- | Variable | Default | Meaning |
79
- |---|---|---|
80
- | `DISKGUARD_ENABLED` | `true` | Master switch for the sweeper. Cleanup-on-exit still runs when `false`. |
81
- | `DISKGUARD_HIGH_WATER_PCT` | `90` | Start evicting when volume usage reaches this %. |
82
- | `DISKGUARD_LOW_WATER_PCT` | `80` | Stop evicting once usage drops below this %. Clamped below high-water. |
83
- | `DISKGUARD_MIN_AGE_MINUTES` | `30` | Never evict a scratch dir younger than this. |
84
- | `DISKGUARD_CHECK_INTERVAL_SECONDS` | `60` | Periodic sweep cadence. |
85
- | `DISKGUARD_DRY_RUN` | `false` | Log what would be evicted without deleting anything. |
86
- | `DISKGUARD_PREFIXES` | `entities-codex-` | Comma-separated dir-name prefixes eligible for eviction. |
87
- | `DISKGUARD_TEMP_ROOT` | `os.tmpdir()` | Override the watched temp root (mainly for testing). |
88
-
89
- ## Development
90
-
91
- ```bash
92
- npm install
93
- npm run build
94
- node dist/cli.js
95
- npm test # builds, then runs the node:test suite (see test/)
96
- ```
1
+ # entities-runtime
2
+
3
+ A standalone Node.js daemon. Install it on any machine, pair with your
4
+ +Ai account, and your account can then run Claude Code / Codex sessions
5
+ on that machine on its behalf — from Vault, from Entity Studio, or from
6
+ anywhere else in +Ai.
7
+
8
+ ## Quick start
9
+
10
+ ```bash
11
+ npx entities-runtime
12
+ ```
13
+
14
+ The first run prints a 5-letter pairing code and a URL:
15
+
16
+ ```
17
+ Pair this runtime with your +Ai account.
18
+
19
+ https://vault.add.ai/entity/connect/HDXYC
20
+
21
+ Enter this code: HDXYC
22
+ ```
23
+
24
+ Open the URL while signed into Vault, enter the code, and the runtime
25
+ starts polling for work. Capabilities (claude, codex, git, node) are
26
+ reported every 30 s.
27
+
28
+ ## Commands
29
+
30
+ ```
31
+ entities-runtime run the daemon (foreground)
32
+ entities-runtime status pairing + capabilities + last-seen
33
+ entities-runtime sessions recent requests handled (--limit N)
34
+ entities-runtime unpair --yes disconnect from +Ai
35
+ entities-runtime version print version
36
+ ```
37
+
38
+ `status` and `sessions` work without the daemon running — they hit
39
+ Supabase directly using the daemon token in
40
+ `~/.entities-runtime/state.json`.
41
+
42
+ ## Architecture
43
+
44
+ See [`docs/architecture.md`](docs/architecture.md) for the full picture:
45
+ Supabase tables, RPC inventory, request lifecycle, and the cancel-watcher
46
+ flow.
47
+
48
+ ## DiskGuard — Codex scratch-dir disk protection
49
+
50
+ Each Codex run gets a throwaway `CODEX_HOME` under the OS temp dir
51
+ (`entities-codex-<rand>`). Codex writes its `state_*.sqlite`, logs, caches
52
+ and shell snapshots there — typically 50–350 MB per run. codex-spawn now
53
+ deletes that dir on exit, but a `SIGKILL`, a crash, or an orphan left by a
54
+ previous runtime process can skip that path. On a dedicated host doing
55
+ thousands of runs, those leftovers once grew to ~158 GiB and filled the
56
+ disk (`ENOSPC`, repeated crashes).
57
+
58
+ **DiskGuard** is the safety net. Like a CCTV DVR overwriting the oldest
59
+ footage when the disk fills, it watches free space on the volume holding
60
+ the temp dir and, when usage crosses a high-water mark, deletes the
61
+ **oldest inactive** scratch dirs — oldest first — until usage drops back
62
+ below a low-water mark, leaving a buffer for the OS. It runs at startup, on
63
+ an interval, and opportunistically right before each new Codex run.
64
+
65
+ Safety guarantees: it only touches directories directly under the temp root
66
+ whose names match the configured prefix (realpath-validated); it never
67
+ deletes a dir owned by a live session (in-process, or one whose
68
+ `session.lock` PID is still alive) or one younger than the min-age guard;
69
+ and it removes symlink *entries* without following them, so the durable
70
+ `~/.codex` targets behind `sessions`/`auth.json` are never harmed.
71
+
72
+ Note: DiskGuard can only reclaim space by deleting scratch dirs it owns. If
73
+ a disk is full of *other* data it will evict what it can and log that it
74
+ couldn't reach the target rather than touching anything it doesn't own.
75
+
76
+ Configure via environment variables (defaults shown):
77
+
78
+ | Variable | Default | Meaning |
79
+ |---|---|---|
80
+ | `DISKGUARD_ENABLED` | `true` | Master switch for the sweeper. Cleanup-on-exit still runs when `false`. |
81
+ | `DISKGUARD_HIGH_WATER_PCT` | `90` | Start evicting when volume usage reaches this %. |
82
+ | `DISKGUARD_LOW_WATER_PCT` | `80` | Stop evicting once usage drops below this %. Clamped below high-water. |
83
+ | `DISKGUARD_MIN_AGE_MINUTES` | `30` | Never evict a scratch dir younger than this. |
84
+ | `DISKGUARD_CHECK_INTERVAL_SECONDS` | `60` | Periodic sweep cadence. |
85
+ | `DISKGUARD_DRY_RUN` | `false` | Log what would be evicted without deleting anything. |
86
+ | `DISKGUARD_PREFIXES` | `entities-codex-` | Comma-separated dir-name prefixes eligible for eviction. |
87
+ | `DISKGUARD_TEMP_ROOT` | `os.tmpdir()` | Override the watched temp root (mainly for testing). |
88
+
89
+ ## Development
90
+
91
+ ```bash
92
+ npm install
93
+ npm run build
94
+ node dist/cli.js
95
+ npm test # builds, then runs the node:test suite (see test/)
96
+ ```
@@ -79,36 +79,36 @@ function buildPlist(spec) {
79
79
  const env = Object.entries(spec.env)
80
80
  .map(([k, v]) => ` <key>${esc(k)}</key>\n <string>${esc(v)}</string>`)
81
81
  .join('\n');
82
- return `<?xml version="1.0" encoding="UTF-8"?>
83
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
84
- <plist version="1.0">
85
- <dict>
86
- <key>Label</key>
87
- <string>${esc(spec.label)}</string>
88
- <key>ProgramArguments</key>
89
- <array>
90
- ${args}
91
- </array>
92
- <key>RunAtLoad</key>
93
- <true/>
94
- <key>KeepAlive</key>
95
- <true/>
96
- <key>ThrottleInterval</key>
97
- <integer>10</integer>
98
- <key>ProcessType</key>
99
- <string>Interactive</string>
100
- <key>WorkingDirectory</key>
101
- <string>${esc(spec.workingDirectory)}</string>
102
- <key>StandardOutPath</key>
103
- <string>${esc(spec.logPath)}</string>
104
- <key>StandardErrorPath</key>
105
- <string>${esc(spec.logPath)}</string>
106
- <key>EnvironmentVariables</key>
107
- <dict>
108
- ${env}
109
- </dict>
110
- </dict>
111
- </plist>
82
+ return `<?xml version="1.0" encoding="UTF-8"?>
83
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
84
+ <plist version="1.0">
85
+ <dict>
86
+ <key>Label</key>
87
+ <string>${esc(spec.label)}</string>
88
+ <key>ProgramArguments</key>
89
+ <array>
90
+ ${args}
91
+ </array>
92
+ <key>RunAtLoad</key>
93
+ <true/>
94
+ <key>KeepAlive</key>
95
+ <true/>
96
+ <key>ThrottleInterval</key>
97
+ <integer>10</integer>
98
+ <key>ProcessType</key>
99
+ <string>Interactive</string>
100
+ <key>WorkingDirectory</key>
101
+ <string>${esc(spec.workingDirectory)}</string>
102
+ <key>StandardOutPath</key>
103
+ <string>${esc(spec.logPath)}</string>
104
+ <key>StandardErrorPath</key>
105
+ <string>${esc(spec.logPath)}</string>
106
+ <key>EnvironmentVariables</key>
107
+ <dict>
108
+ ${env}
109
+ </dict>
110
+ </dict>
111
+ </plist>
112
112
  `;
113
113
  }
114
114
  /** Pull ProgramArguments back out of a plist we wrote, for `status`. */
@@ -128,56 +128,56 @@ function buildVbs(cmdPath) {
128
128
  * a second daemon.
129
129
  */
130
130
  function buildTaskXml(spec) {
131
- return `<?xml version="1.0" encoding="UTF-16"?>
132
- <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
133
- <RegistrationInfo>
134
- <Date>${xmlEsc(spec.createdAt)}</Date>
135
- <Author>@addai/node</Author>
136
- <Description>Runs this +Ai Node so entities can reach it. Created by \`ainode startup enable\`.</Description>
137
- </RegistrationInfo>
138
- <Triggers>
139
- <LogonTrigger>
140
- <Enabled>true</Enabled>
141
- <UserId>${xmlEsc(spec.userId)}</UserId>
142
- <Repetition>
143
- <Interval>PT5M</Interval>
144
- <StopAtDurationEnd>false</StopAtDurationEnd>
145
- </Repetition>
146
- </LogonTrigger>
147
- </Triggers>
148
- <Principals>
149
- <Principal id="Author">
150
- <UserId>${xmlEsc(spec.userId)}</UserId>
151
- <LogonType>InteractiveToken</LogonType>
152
- <RunLevel>LeastPrivilege</RunLevel>
153
- </Principal>
154
- </Principals>
155
- <Settings>
156
- <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
157
- <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
158
- <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
159
- <AllowHardTerminate>true</AllowHardTerminate>
160
- <StartWhenAvailable>true</StartWhenAvailable>
161
- <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
162
- <IdleSettings>
163
- <StopOnIdleEnd>false</StopOnIdleEnd>
164
- <RestartOnIdle>false</RestartOnIdle>
165
- </IdleSettings>
166
- <AllowStartOnDemand>true</AllowStartOnDemand>
167
- <Enabled>true</Enabled>
168
- <Hidden>false</Hidden>
169
- <RunOnlyIfIdle>false</RunOnlyIfIdle>
170
- <WakeToRun>false</WakeToRun>
171
- <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
172
- <Priority>7</Priority>
173
- </Settings>
174
- <Actions Context="Author">
175
- <Exec>
176
- <Command>${xmlEsc(spec.wscriptPath)}</Command>
177
- <Arguments>"${xmlEsc(spec.vbsPath)}"</Arguments>
178
- </Exec>
179
- </Actions>
180
- </Task>
131
+ return `<?xml version="1.0" encoding="UTF-16"?>
132
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
133
+ <RegistrationInfo>
134
+ <Date>${xmlEsc(spec.createdAt)}</Date>
135
+ <Author>@addai/node</Author>
136
+ <Description>Runs this +Ai Node so entities can reach it. Created by \`ainode startup enable\`.</Description>
137
+ </RegistrationInfo>
138
+ <Triggers>
139
+ <LogonTrigger>
140
+ <Enabled>true</Enabled>
141
+ <UserId>${xmlEsc(spec.userId)}</UserId>
142
+ <Repetition>
143
+ <Interval>PT5M</Interval>
144
+ <StopAtDurationEnd>false</StopAtDurationEnd>
145
+ </Repetition>
146
+ </LogonTrigger>
147
+ </Triggers>
148
+ <Principals>
149
+ <Principal id="Author">
150
+ <UserId>${xmlEsc(spec.userId)}</UserId>
151
+ <LogonType>InteractiveToken</LogonType>
152
+ <RunLevel>LeastPrivilege</RunLevel>
153
+ </Principal>
154
+ </Principals>
155
+ <Settings>
156
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
157
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
158
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
159
+ <AllowHardTerminate>true</AllowHardTerminate>
160
+ <StartWhenAvailable>true</StartWhenAvailable>
161
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
162
+ <IdleSettings>
163
+ <StopOnIdleEnd>false</StopOnIdleEnd>
164
+ <RestartOnIdle>false</RestartOnIdle>
165
+ </IdleSettings>
166
+ <AllowStartOnDemand>true</AllowStartOnDemand>
167
+ <Enabled>true</Enabled>
168
+ <Hidden>false</Hidden>
169
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
170
+ <WakeToRun>false</WakeToRun>
171
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
172
+ <Priority>7</Priority>
173
+ </Settings>
174
+ <Actions Context="Author">
175
+ <Exec>
176
+ <Command>${xmlEsc(spec.wscriptPath)}</Command>
177
+ <Arguments>"${xmlEsc(spec.vbsPath)}"</Arguments>
178
+ </Exec>
179
+ </Actions>
180
+ </Task>
181
181
  `;
182
182
  }
183
183
  function schtasks(args) {
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // ainode CLI — the +Ai Node console.
4
4
  //
5
5
  // ainode run this node: daemon + live dashboard
6
+ // ainode entities the same, pairing into +Ai Entities rather than Vault
6
7
  // ainode harnesses interactive harness manager (install / login)
7
8
  // ainode startup … start this node when the machine starts
8
9
  // ainode unpair --yes disconnect from +Ai, cancel in-flight work
@@ -53,7 +54,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
53
54
  const lockfile_1 = require("./lockfile");
54
55
  const index_1 = require("./index");
55
56
  const store_1 = require("./store");
56
- const supabase_client_1 = require("./supabase-client");
57
+ const unpair_1 = require("./unpair");
57
58
  const run_1 = require("./tui/run");
58
59
  const autostart = __importStar(require("./autostart"));
59
60
  const args = process.argv.slice(2);
@@ -72,21 +73,16 @@ async function cmdUnpair() {
72
73
  }
73
74
  const confirm = args.includes('--yes') || args.includes('-y');
74
75
  if (!confirm) {
75
- console.log('This will disconnect the runtime from +Ai and cancel any in-flight requests.');
76
+ console.log('This disconnects the node from +Ai and cancels any in-flight requests.');
77
+ console.log((0, unpair_1.describeDisconnect)(0));
76
78
  console.log('Re-run with --yes to confirm.');
77
79
  return;
78
80
  }
79
- try {
80
- const r = await (0, supabase_client_1.rpc)('runtime_self_unpair', { p_token: token() });
81
- if (r?.cancelled_requests) {
82
- console.log(`Cancelled ${r.cancelled_requests} in-flight request(s).`);
83
- }
84
- }
85
- catch (err) {
86
- console.error(`server unpair failed (continuing with local cleanup): ${err.message}`);
87
- }
88
- (0, store_1.clearPairing)();
89
- console.log('Unpaired. Run `ainode` to pair again.');
81
+ const r = await (0, unpair_1.disconnectNode)();
82
+ console.log((0, unpair_1.summariseDisconnect)(r));
83
+ if (!r.ok)
84
+ process.exit(1);
85
+ console.log('Run `ainode` to pair again.');
90
86
  }
91
87
  function cmdStartup() {
92
88
  const sub = args[1] ?? 'status';
@@ -128,7 +124,10 @@ function cmdStartup() {
128
124
  }
129
125
  async function main() {
130
126
  const cmd = args[0];
131
- if (!cmd || cmd === 'run' || cmd === 'start' || cmd === '--run') {
127
+ // `entities` is the same run command with a destination attached: the page
128
+ // that printed it knows which product this machine is joining, so the user
129
+ // never gets asked. An already-paired node ignores it and just runs.
130
+ if (!cmd || cmd === 'run' || cmd === 'start' || cmd === '--run' || cmd === 'entities') {
132
131
  // `--ensure` means "make sure a node is running here, then get out of the
133
132
  // way". Windows' startup task repeats every five minutes to catch a
134
133
  // crashed daemon; without this it would stack a second one every time.
@@ -158,11 +157,9 @@ async function main() {
158
157
  });
159
158
  return;
160
159
  }
161
- const runtime = await (0, index_1.start)();
160
+ const runtime = await (0, index_1.start)(args);
162
161
  if (!tui) {
163
162
  console.log(`ainode v${index_1.VERSION} running (pid ${runtime.pid}, port ${runtime.port})`);
164
- if (!(0, store_1.isPaired)())
165
- console.log('Not paired yet — pairing flow comes online in the next build.');
166
163
  await new Promise(() => { });
167
164
  return;
168
165
  }
@@ -192,23 +189,24 @@ async function main() {
192
189
  case 'help':
193
190
  case '--help':
194
191
  case '-h':
195
- console.log(`ainode v${index_1.VERSION}
196
-
197
- usage:
198
- ainode run this +Ai Node — daemon + live dashboard
199
- ainode harnesses install / log in agent harnesses (interactive)
200
- ainode startup enable start this node whenever you log in
201
- ainode startup disable stop starting at login
202
- ainode startup status where the startup entry is and what it runs
203
- ainode unpair --yes disconnect from +Ai
204
- ainode version print version
205
-
206
- Piped or headless output falls back to plain log lines. Set AINODE_NO_TUI=1
207
- to force that on a terminal. \`ainode run --ensure\` starts a node only if one
208
- isn't already running what the Windows startup task uses.
209
-
210
- Pair the node by running it with no args and following the prompt
211
- to vault.add.ai/entity/connect/<CODE>.
192
+ console.log(`ainode v${index_1.VERSION}
193
+
194
+ usage:
195
+ ainode run this +Ai Node — daemon + live dashboard
196
+ ainode entities the same, but pair into +Ai Entities
197
+ ainode harnesses install / log in agent harnesses (interactive)
198
+ ainode startup enable start this node whenever you log in
199
+ ainode startup disable stop starting at login
200
+ ainode startup status where the startup entry is and what it runs
201
+ ainode unpair --yes disconnect from +Ai
202
+ ainode version print version
203
+
204
+ Piped or headless output falls back to plain log lines. Set AINODE_NO_TUI=1
205
+ to force that on a terminal. \`ainode run --ensure\` starts a node only if one
206
+ isn't already running — what the Windows startup task uses.
207
+
208
+ Pairing opens your browser to confirm the machine; nothing to type. Over SSH
209
+ or on a headless box it prints a code and a link instead.
212
210
  `);
213
211
  return;
214
212
  }
package/dist/config.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export declare const SUPABASE_URL = "https://syhzpqqvrplaqdipcymw.supabase.co";
2
2
  export declare const SUPABASE_ANON_KEY: string;
3
3
  export declare const VAULT_PAIR_URL = "https://vault.add.ai/add/entity";
4
+ export declare const ENTITIES_PAIR_URL = "https://entities.add.ai/connect";
4
5
  export declare const HEARTBEAT_INTERVAL_MS = 30000;
5
6
  export declare const HANG_WATCHDOG_IDLE_MS: number;
6
7
  export declare const PAIR_POLL_INTERVAL_MS = 2000;
package/dist/config.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // public (PostgREST anon role only sees what RLS permits), so it's fine
4
4
  // to ship in the npm package.
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.PAIR_TIMEOUT_MS = exports.PAIR_POLL_INTERVAL_MS = exports.HANG_WATCHDOG_IDLE_MS = exports.HEARTBEAT_INTERVAL_MS = exports.VAULT_PAIR_URL = exports.SUPABASE_ANON_KEY = exports.SUPABASE_URL = void 0;
6
+ exports.PAIR_TIMEOUT_MS = exports.PAIR_POLL_INTERVAL_MS = exports.HANG_WATCHDOG_IDLE_MS = exports.HEARTBEAT_INTERVAL_MS = exports.ENTITIES_PAIR_URL = exports.VAULT_PAIR_URL = exports.SUPABASE_ANON_KEY = exports.SUPABASE_URL = void 0;
7
7
  exports.SUPABASE_URL = 'https://syhzpqqvrplaqdipcymw.supabase.co';
8
8
  // Legacy anon JWT for the +Ai Project. Used by the daemon for every RPC
9
9
  // call; per-runtime authentication happens inside each RPC via the
@@ -16,6 +16,11 @@ exports.SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' +
16
16
  // Entity provider pre-selected and the code pre-filled. Vault's
17
17
  // LegacyRedirect prepends the user's default workspace slug.
18
18
  exports.VAULT_PAIR_URL = 'https://vault.add.ai/add/entity';
19
+ // Where `ainode entities` sends you instead: Entity Studio's own confirm
20
+ // page. It takes the code on the end of the path, is workspace-less (this
21
+ // machine has no idea which workspace its human is in), and shows the
22
+ // hostname before it attaches anything.
23
+ exports.ENTITIES_PAIR_URL = 'https://entities.add.ai/connect';
19
24
  // Heartbeat interval. Picked so the desktop can mark a runtime offline
20
25
  // after ~2x missed heartbeats (~60s) without too much chatty traffic.
21
26
  exports.HEARTBEAT_INTERVAL_MS = 30_000;
@@ -24,8 +29,18 @@ exports.HEARTBEAT_INTERVAL_MS = 30_000;
24
29
  // Deliberately generous: a legit slow tool call (e.g. grok kicking off an
25
30
  // image/video flow, which emits no intermediate events) must complete inside
26
31
  // this window or a false kill would waste the run. 8 min clears typical flows
27
- // while still firing well before the 1h node-timeout ceiling and the 10-min
28
- // server watchdog's hard fail. Override with HANG_WATCHDOG_IDLE_MS (0 = off).
32
+ // while still firing well before the 1h node-timeout ceiling.
33
+ //
34
+ // The ordering that matters: this leash must fire BEFORE the server gives up on
35
+ // the run, or the server's guess wins and the user gets a failure for a run that
36
+ // is still working. A run that has shown activity gets 3x this window (see the
37
+ // idleMsFn call sites in session-runner), so the real node budget is 24 min —
38
+ // which for a long time was more than DOUBLE the server's 10-minute verdict, and
39
+ // is exactly why chats showed "No response from the entity in time" while the
40
+ // agent was mid-turn. The server now measures liveness from the per-run
41
+ // heartbeat instead of guessing, with a 30-min silence ceiling as its backstop:
42
+ // 24 < 30, so this leash gets to decide. Keep it that way if either changes.
43
+ // Override with HANG_WATCHDOG_IDLE_MS (0 = off).
29
44
  exports.HANG_WATCHDOG_IDLE_MS = (() => {
30
45
  const raw = process.env.HANG_WATCHDOG_IDLE_MS;
31
46
  if (raw == null || raw === '')
package/dist/heartbeat.js CHANGED
@@ -11,6 +11,7 @@ exports.stop = stop;
11
11
  const supabase_client_1 = require("./supabase-client");
12
12
  const store_1 = require("./store");
13
13
  const capabilities_1 = require("./capabilities");
14
+ const request_pump_1 = require("./request-pump");
14
15
  const config_1 = require("./config");
15
16
  let timer = null;
16
17
  let stopped = false;
@@ -78,6 +79,45 @@ async function goOffline() {
78
79
  async function beat() {
79
80
  await tick();
80
81
  }
82
+ // ── Per-run liveness ────────────────────────────────────────────────────────
83
+ // Tells the server which runs this daemon is holding a child process for. The
84
+ // server's watchdog used to infer that from rows in entity_runtime_request_events
85
+ // — i.e. from how CHATTY the agent was — and so failed runs that were alive but
86
+ // quiet: a grok turn routinely goes ten minutes between its last streamed token
87
+ // and turn_complete, and users were told "No response from the entity in time —
88
+ // it may be offline" under an answer that had already been delivered.
89
+ //
90
+ // Deliberately its OWN timer rather than a line inside tick(): tick() runs a
91
+ // capabilities probe that spawns agent CLIs and can be slow or throw, and
92
+ // liveness reporting must not be starved by it. Sent even when nothing is
93
+ // in-flight — the empty call is what tells the server this build claims its
94
+ // runs at all, so a node rolled back to an older build ages out of the strict
95
+ // rule by itself.
96
+ let runTimer = null;
97
+ let lastRunBeatError = '';
98
+ async function runTick() {
99
+ if (stopped || (0, supabase_client_1.rpcShouldSkip)())
100
+ return;
101
+ const pairing = (0, store_1.readPairing)();
102
+ if (!pairing)
103
+ return;
104
+ try {
105
+ await (0, supabase_client_1.rpc)('runtime_request_heartbeat', {
106
+ p_token: pairing.daemonToken,
107
+ p_request_ids: (0, request_pump_1.activeRequestIdList)(),
108
+ });
109
+ lastRunBeatError = '';
110
+ }
111
+ catch (err) {
112
+ // Soft fail, deduped: a server without this RPC yet (or a blip) must not
113
+ // produce a log line every 30 seconds forever.
114
+ const msg = err.message || String(err);
115
+ if (msg !== lastRunBeatError) {
116
+ lastRunBeatError = msg;
117
+ console.error('[heartbeat] run heartbeat failed:', msg);
118
+ }
119
+ }
120
+ }
81
121
  function start() {
82
122
  if (timer || stopped)
83
123
  return;
@@ -85,6 +125,8 @@ function start() {
85
125
  // 30s for the first interval.
86
126
  void tick();
87
127
  timer = setInterval(() => { void tick(); }, config_1.HEARTBEAT_INTERVAL_MS);
128
+ void runTick();
129
+ runTimer = setInterval(() => { void runTick(); }, config_1.HEARTBEAT_INTERVAL_MS);
88
130
  }
89
131
  function stop() {
90
132
  stopped = true;
@@ -92,4 +134,8 @@ function stop() {
92
134
  clearInterval(timer);
93
135
  timer = null;
94
136
  }
137
+ if (runTimer) {
138
+ clearInterval(runTimer);
139
+ runTimer = null;
140
+ }
95
141
  }
package/dist/index.d.ts CHANGED
@@ -6,7 +6,11 @@ export interface RunningRuntime {
6
6
  }
7
7
  /**
8
8
  * Boot the daemon. If we don't have a pairing yet, run the user through
9
- * the 5-letter code flow first; once paired, kick off the heartbeat.
9
+ * pairing first; once paired, kick off the heartbeat.
10
+ *
11
+ * `argv` is passed through only so pairing knows which product this machine
12
+ * is joining — `ainode entities` lands on Entity Studio, everything else on
13
+ * Vault. An already-paired daemon never looks at it.
10
14
  */
11
- export declare function start(): Promise<RunningRuntime>;
15
+ export declare function start(argv?: string[]): Promise<RunningRuntime>;
12
16
  export { VERSION };
package/dist/index.js CHANGED
@@ -90,9 +90,13 @@ process.on('unhandledRejection', (reason) => {
90
90
  });
91
91
  /**
92
92
  * Boot the daemon. If we don't have a pairing yet, run the user through
93
- * the 5-letter code flow first; once paired, kick off the heartbeat.
93
+ * pairing first; once paired, kick off the heartbeat.
94
+ *
95
+ * `argv` is passed through only so pairing knows which product this machine
96
+ * is joining — `ainode entities` lands on Entity Studio, everything else on
97
+ * Vault. An already-paired daemon never looks at it.
94
98
  */
95
- async function start() {
99
+ async function start(argv = []) {
96
100
  const migration = (0, state_migrate_1.migrateLegacyStateDir)();
97
101
  if (migration.migrated) {
98
102
  const parts = [
@@ -125,11 +129,7 @@ async function start() {
125
129
  }
126
130
  if (!(0, store_1.isPaired)()) {
127
131
  try {
128
- const code = await (0, pairing_1.requestPairCode)();
129
- (0, pairing_1.printPairingInstructions)(code);
130
- const result = await (0, pairing_1.awaitPairing)(code);
131
- console.log(` ✓ Paired (runtime_id: ${result.runtime_id})`);
132
- console.log('');
132
+ await (0, pairing_1.runPairing)(argv);
133
133
  }
134
134
  catch (err) {
135
135
  console.error('Pairing failed:', err.message || err);
@@ -0,0 +1 @@
1
+ export declare function openBrowser(url: string, platform?: NodeJS.Platform): boolean;