@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 +96 -96
- package/dist/autostart-mac.js +30 -30
- package/dist/autostart-win.js +50 -50
- package/dist/cli.js +32 -34
- package/dist/config.d.ts +1 -0
- package/dist/config.js +18 -3
- package/dist/heartbeat.js +46 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +7 -7
- package/dist/open-browser.d.ts +1 -0
- package/dist/open-browser.js +42 -0
- package/dist/pair-destination.d.ts +20 -0
- package/dist/pair-destination.js +59 -0
- package/dist/pairing.d.ts +4 -2
- package/dist/pairing.js +64 -16
- package/dist/request-pump.d.ts +1 -0
- package/dist/request-pump.js +9 -0
- package/dist/tui/dashboard.d.ts +2 -0
- package/dist/tui/dashboard.js +60 -2
- package/dist/tui/pair.d.ts +33 -0
- package/dist/tui/pair.js +178 -0
- package/dist/unpair.d.ts +20 -0
- package/dist/unpair.js +71 -0
- package/package.json +60 -60
- package/scripts/fix-pty-helper.js +28 -28
- package/scripts/precompact-capture.js +292 -292
- package/scripts/probe-tui.mjs +122 -122
- package/scripts/smoke-test.sh +74 -74
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
|
+
```
|
package/dist/autostart-mac.js
CHANGED
|
@@ -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`. */
|
package/dist/autostart-win.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
200
|
-
ainode
|
|
201
|
-
ainode startup
|
|
202
|
-
ainode startup
|
|
203
|
-
ainode
|
|
204
|
-
ainode
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
to
|
|
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
|
|
28
|
-
//
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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;
|