@dmsdc-ai/aigentry-telepty 0.5.7 → 0.5.9

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/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.9] - 2026-06-08
8
+
9
+ ### Fixed — managed service install never started the daemon (#41)
10
+
11
+ - **The launchd/systemd/Windows service install generated an `env: node`
12
+ invocation that exited 127** under a minimal service-manager PATH (the daemon
13
+ never started when managed by launchd/systemd). **Fix:** `install.js` now uses
14
+ the absolute `process.execPath` + `cli.js` path for launchd/systemd/Windows
15
+ service generation, sets the daemon `PATH` via EnvironmentVariables, and adds
16
+ managed-instance live assertions so a managed daemon actually starts. Landed on
17
+ `main` at commit `7b2ab92`.
18
+
19
+ ### Changed — CI test wiring
20
+
21
+ - Wired `test/install-service-generation.test.js` (the #41 regression test) into
22
+ the `test`, `test:ci`, and `test:watch` script file lists so CI's
23
+ `npm run test:ci` actually exercises the service-install generation.
24
+
25
+ ### Docs
26
+
27
+ - Landed the #42 cross-machine relay/broker (hub) mode ADR and MVP
28
+ implementation spec.
29
+
7
30
  ## [0.5.2] - 2026-06-06
8
31
 
9
32
  ### Fixed — submit handshake confirmation (#507-B / #508)
package/daemon.js CHANGED
@@ -1617,6 +1617,24 @@ function resolveSessionAlias(requestedId) {
1617
1617
  return candidates[0];
1618
1618
  }
1619
1619
 
1620
+ // #548 (alias-cascade shared-fate): resolveSessionAlias' most-recent-wins fuzzy match is correct for
1621
+ // READ/inject ("talk to the current `coder`"), but DESTRUCTIVE ops (DELETE / kill) must NEVER cascade
1622
+ // across distinct sids that merely share an alias. The incident: cleaning an already-gone `coder-532`
1623
+ // fuzzy-fell-through to its live sibling `coder-533` (same `coder` track) and KILLED it. Distinct sids
1624
+ // = distinct lifecycles. This resolver enforces "destroy exactly the session you named":
1625
+ // - exact sid match → that sid;
1626
+ // - a fully-qualified sid (ends in `-<digits>`) with no exact match → null (a stale/duplicate DELETE
1627
+ // of a gone sid must NOT fall through to a sibling — this is the exact #548 cascade);
1628
+ // - a bare alias → resolve ONLY when a single session carries it (unambiguous); multiple siblings → null
1629
+ // (refuse rather than silently pick most-recent and kill the wrong one).
1630
+ function resolveSessionForDestroy(requestedId) {
1631
+ if (sessions[requestedId]) return requestedId;
1632
+ if (/-\d+$/.test(requestedId)) return null;
1633
+ const baseAlias = requestedId.replace(/-\d+$/, '');
1634
+ const candidates = Object.keys(sessions).filter(id => id.replace(/-\d+$/, '') === baseAlias);
1635
+ return candidates.length === 1 ? candidates[0] : null;
1636
+ }
1637
+
1620
1638
  app.post('/api/sessions/spawn', (req, res) => {
1621
1639
  const { session_id, command, args = [], cwd = process.cwd(), cols = 80, rows = 30, type = 'AGENT' } = req.body;
1622
1640
  if (!session_id) return res.status(400).json({ error: 'session_id is strictly required.' });
@@ -2823,7 +2841,8 @@ app.patch('/api/sessions/:id', (req, res) => {
2823
2841
 
2824
2842
  app.post('/api/sessions/:id/kill', async (req, res) => {
2825
2843
  const requestedId = req.params.id;
2826
- const resolvedId = resolveSessionAlias(requestedId);
2844
+ // #548: destructive op — must not cascade across alias-sharing siblings.
2845
+ const resolvedId = resolveSessionForDestroy(requestedId);
2827
2846
  if (!resolvedId) return res.status(404).json({ error: 'Session not found', requested: requestedId });
2828
2847
 
2829
2848
  try {
@@ -2852,7 +2871,8 @@ app.post('/api/sessions/:id/kill', async (req, res) => {
2852
2871
 
2853
2872
  app.delete('/api/sessions/:id', (req, res) => {
2854
2873
  const requestedId = req.params.id;
2855
- const resolvedId = resolveSessionAlias(requestedId);
2874
+ // #548: destructive op — must not cascade across alias-sharing siblings.
2875
+ const resolvedId = resolveSessionForDestroy(requestedId);
2856
2876
  if (!resolvedId) return res.status(404).json({ error: 'Session not found', requested: requestedId });
2857
2877
  const session = sessions[resolvedId];
2858
2878
  const id = resolvedId;
package/install.js CHANGED
@@ -5,9 +5,6 @@ const os = require('os');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { cleanupDaemonProcesses } = require('./daemon-control');
8
- const { runInteractiveSkillInstaller } = require('./skill-installer');
9
-
10
- console.log("🚀 Installing @dmsdc-ai/aigentry-telepty...");
11
8
 
12
9
  function run(cmd) {
13
10
  try {
@@ -18,6 +15,10 @@ function run(cmd) {
18
15
  }
19
16
  }
20
17
 
18
+ function shellQuote(value) {
19
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
20
+ }
21
+
21
22
  function resolveInstalledPackageRoot() {
22
23
  try {
23
24
  const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
@@ -27,6 +28,15 @@ function resolveInstalledPackageRoot() {
27
28
  }
28
29
  }
29
30
 
31
+ function resolveDaemonLaunchOptions(options = {}) {
32
+ const packageRoot = options.packageRoot || __dirname;
33
+ const nodeBin = options.nodeBin || process.execPath;
34
+ const cliJs = options.cliJs || path.join(packageRoot, 'cli.js');
35
+ const logDir = options.logDir || path.join(os.homedir(), '.telepty', 'logs');
36
+
37
+ return { nodeBin, cliJs, logDir };
38
+ }
39
+
30
40
  function cleanupLocalDaemons() {
31
41
  console.log('🧹 Cleaning up existing telepty daemons...');
32
42
  const results = cleanupDaemonProcesses();
@@ -36,6 +46,161 @@ function cleanupLocalDaemons() {
36
46
  }
37
47
  }
38
48
 
49
+ function escapeXml(value) {
50
+ return String(value)
51
+ .replace(/&/g, '&amp;')
52
+ .replace(/</g, '&lt;')
53
+ .replace(/>/g, '&gt;')
54
+ .replace(/"/g, '&quot;')
55
+ .replace(/'/g, '&apos;');
56
+ }
57
+
58
+ function uniquePathEntries(entries) {
59
+ const seen = new Set();
60
+ const result = [];
61
+
62
+ for (const entry of entries) {
63
+ if (!entry || seen.has(entry)) {
64
+ continue;
65
+ }
66
+ seen.add(entry);
67
+ result.push(entry);
68
+ }
69
+
70
+ return result;
71
+ }
72
+
73
+ function buildDaemonPath(nodeBin, baseEntries) {
74
+ return uniquePathEntries([path.dirname(nodeBin), ...baseEntries]).join(':');
75
+ }
76
+
77
+ function systemdExecArg(value) {
78
+ const text = String(value);
79
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(text)) {
80
+ return text;
81
+ }
82
+ return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
83
+ }
84
+
85
+ function quoteWindowsArg(value) {
86
+ return `"${String(value).replace(/"/g, '\\"')}"`;
87
+ }
88
+
89
+ function buildLaunchdPlist(options = {}) {
90
+ const label = options.label || 'com.aigentry.telepty';
91
+ const nodeBin = options.nodeBin || process.execPath;
92
+ const cliJs = options.cliJs || path.join(__dirname, 'cli.js');
93
+ const logDir = options.logDir || path.join(os.homedir(), '.telepty', 'logs');
94
+ const daemonPath = buildDaemonPath(nodeBin, ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin']);
95
+ const stdoutPath = path.join(logDir, 'launchd.out.log');
96
+ const stderrPath = path.join(logDir, 'launchd.err.log');
97
+
98
+ return `<?xml version="1.0" encoding="UTF-8"?>
99
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
100
+ <plist version="1.0">
101
+ <dict>
102
+ <key>Label</key>
103
+ <string>${escapeXml(label)}</string>
104
+ <key>ProgramArguments</key>
105
+ <array>
106
+ <string>${escapeXml(nodeBin)}</string>
107
+ <string>${escapeXml(cliJs)}</string>
108
+ <string>daemon</string>
109
+ </array>
110
+ <key>EnvironmentVariables</key>
111
+ <dict>
112
+ <key>PATH</key>
113
+ <string>${escapeXml(daemonPath)}</string>
114
+ </dict>
115
+ <key>StandardOutPath</key>
116
+ <string>${escapeXml(stdoutPath)}</string>
117
+ <key>StandardErrorPath</key>
118
+ <string>${escapeXml(stderrPath)}</string>
119
+ <key>RunAtLoad</key>
120
+ <true/>
121
+ <key>KeepAlive</key>
122
+ <true/>
123
+ </dict>
124
+ </plist>`;
125
+ }
126
+
127
+ function buildSystemdService(options = {}) {
128
+ const nodeBin = options.nodeBin || process.execPath;
129
+ const cliJs = options.cliJs || path.join(__dirname, 'cli.js');
130
+ const user = options.user;
131
+ const userLine = user ? `User=${user}\n` : '';
132
+ const daemonPath = buildDaemonPath(nodeBin, ['/usr/local/bin', '/usr/bin', '/bin']);
133
+ const wantedBy = options.wantedBy || 'multi-user.target';
134
+
135
+ return `[Unit]
136
+ Description=Telepty Daemon
137
+ After=network.target
138
+
139
+ [Service]
140
+ ExecStart=${systemdExecArg(nodeBin)} ${systemdExecArg(cliJs)} daemon
141
+ Restart=always
142
+ ${userLine}Environment=PATH=${daemonPath}
143
+ Environment=NODE_ENV=production
144
+
145
+ [Install]
146
+ WantedBy=${wantedBy}`;
147
+ }
148
+
149
+ function buildWindowsAutostartCommand(options = {}) {
150
+ const nodeBin = options.nodeBin || process.execPath;
151
+ const cliJs = options.cliJs || path.join(__dirname, 'cli.js');
152
+ const taskName = options.taskName || 'telepty-daemon';
153
+ const taskCommand = `${quoteWindowsArg(nodeBin)} ${quoteWindowsArg(cliJs)} daemon`;
154
+
155
+ return `schtasks /create /tn ${quoteWindowsArg(taskName)} /sc onlogon /rl LIMITED /f /tr ${quoteWindowsArg(taskCommand)}`;
156
+ }
157
+
158
+ function buildWindowsRunTaskCommand(options = {}) {
159
+ const taskName = options.taskName || 'telepty-daemon';
160
+ return `schtasks /run /tn ${quoteWindowsArg(taskName)}`;
161
+ }
162
+
163
+ function buildWindowsQueryTaskCommand(options = {}) {
164
+ const taskName = options.taskName || 'telepty-daemon';
165
+ return `schtasks /query /tn ${quoteWindowsArg(taskName)} /fo LIST`;
166
+ }
167
+
168
+ function assertLaunchdServiceLive(label = 'com.aigentry.telepty') {
169
+ let output = '';
170
+ try {
171
+ output = execSync(`launchctl list ${shellQuote(label)}`, { encoding: 'utf8' });
172
+ } catch (e) {
173
+ throw new Error(`launchd service ${label} was not found after load`);
174
+ }
175
+
176
+ const pidMatch = output.match(/"PID"\s*=\s*([0-9]+)/);
177
+ if (!pidMatch || Number(pidMatch[1]) <= 0) {
178
+ throw new Error(`launchd service ${label} loaded but has no live PID. launchctl output:\n${output}`);
179
+ }
180
+ }
181
+
182
+ function assertSystemdServiceLive(serviceName = 'telepty', options = {}) {
183
+ const scope = options.user ? '--user ' : '';
184
+ try {
185
+ execSync(`systemctl ${scope}is-active --quiet ${serviceName}`, { stdio: 'ignore' });
186
+ } catch (e) {
187
+ throw new Error(`systemd service ${serviceName} is not active after start`);
188
+ }
189
+ }
190
+
191
+ function assertWindowsTaskRunning(taskName = 'telepty-daemon') {
192
+ let output = '';
193
+ try {
194
+ output = execSync(buildWindowsQueryTaskCommand({ taskName }), { encoding: 'utf8' });
195
+ } catch (e) {
196
+ throw new Error(`Windows scheduled task ${taskName} was not found after creation`);
197
+ }
198
+
199
+ if (!/^Status:\s*Running$/im.test(output)) {
200
+ throw new Error(`Windows scheduled task ${taskName} started but is not running. schtasks output:\n${output}`);
201
+ }
202
+ }
203
+
39
204
  async function installSkills() {
40
205
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
41
206
  console.log('⏭️ Skipping interactive skill installation (no TTY).');
@@ -46,6 +211,7 @@ async function installSkills() {
46
211
  console.log('\n📋 Telepty skill installation');
47
212
 
48
213
  try {
214
+ const { runInteractiveSkillInstaller } = require('./skill-installer');
49
215
  await runInteractiveSkillInstaller({
50
216
  packageRoot: resolveInstalledPackageRoot(),
51
217
  cwd: process.cwd()
@@ -55,7 +221,9 @@ async function installSkills() {
55
221
  }
56
222
  }
57
223
 
58
- (async () => {
224
+ async function main() {
225
+ console.log("🚀 Installing @dmsdc-ai/aigentry-telepty...");
226
+
59
227
  // 1. Install globally via npm
60
228
  console.log("📦 Installing package globally...");
61
229
  run("npm install -g @dmsdc-ai/aigentry-telepty");
@@ -63,12 +231,10 @@ async function installSkills() {
63
231
  // 2. Install telepty skills for supported clients
64
232
  await installSkills();
65
233
 
66
- // 3. Find executable
67
- let teleptyPath = '';
68
- try {
69
- teleptyPath = execSync(os.platform() === 'win32' ? 'where telepty' : 'which telepty', { encoding: 'utf8' }).split('\n')[0].trim();
70
- } catch (e) {
71
- teleptyPath = 'telepty'; // fallback
234
+ // 3. Resolve daemon entrypoint without relying on service-manager PATH.
235
+ const launchOptions = resolveDaemonLaunchOptions({ packageRoot: resolveInstalledPackageRoot() });
236
+ if (!fs.existsSync(launchOptions.cliJs)) {
237
+ throw new Error(`Cannot find daemon entrypoint: ${launchOptions.cliJs}`);
72
238
  }
73
239
 
74
240
  // 4. Setup OS-specific autostart or background daemon
@@ -76,86 +242,95 @@ async function installSkills() {
76
242
 
77
243
  if (platform === 'win32') {
78
244
  cleanupLocalDaemons();
79
- console.log("⚙️ Setting up Windows background process...");
80
- const subprocess = spawn(teleptyPath, ['daemon'], {
81
- detached: true,
82
- stdio: 'ignore',
83
- windowsHide: true
84
- });
85
- subprocess.unref();
86
- console.log("✅ Windows daemon started in background.");
245
+ console.log("⚙️ Setting up Windows scheduled task...");
246
+ run(buildWindowsAutostartCommand(launchOptions));
247
+ run(buildWindowsRunTaskCommand());
248
+ assertWindowsTaskRunning();
249
+ console.log("✅ Windows scheduled task installed and started.");
87
250
 
88
251
  } else if (platform === 'darwin') {
89
252
  console.log("⚙️ Setting up macOS launchd service...");
90
253
  const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.aigentry.telepty.plist');
91
254
  fs.mkdirSync(path.dirname(plistPath), { recursive: true });
92
- try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`); } catch(e){}
255
+ fs.mkdirSync(launchOptions.logDir, { recursive: true });
256
+ try { execSync(`launchctl unload ${shellQuote(plistPath)} 2>/dev/null`); } catch(e){}
93
257
  cleanupLocalDaemons();
94
258
 
95
- const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
96
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
97
- <plist version="1.0">
98
- <dict>
99
- <key>Label</key>
100
- <string>com.aigentry.telepty</string>
101
- <key>ProgramArguments</key>
102
- <array>
103
- <string>${teleptyPath}</string>
104
- <string>daemon</string>
105
- </array>
106
- <key>RunAtLoad</key>
107
- <true/>
108
- <key>KeepAlive</key>
109
- <true/>
110
- </dict>
111
- </plist>`;
259
+ const plistContent = buildLaunchdPlist(launchOptions);
112
260
 
113
261
  fs.writeFileSync(plistPath, plistContent);
114
- run(`launchctl load "${plistPath}"`);
262
+ run(`launchctl load ${shellQuote(plistPath)}`);
263
+ assertLaunchdServiceLive();
115
264
  console.log("✅ macOS LaunchAgent installed and started.");
116
265
 
117
266
  } else {
118
267
  // Linux
268
+ let hasSystemd = false;
119
269
  try {
120
270
  execSync('systemctl --version', { stdio: 'ignore' });
271
+ hasSystemd = true;
272
+ } catch(e) {}
273
+
274
+ if (hasSystemd) {
121
275
  if (process.getuid && process.getuid() === 0) {
122
276
  console.log("⚙️ Setting up systemd service for Linux...");
123
277
  try { execSync('systemctl stop telepty', { stdio: 'ignore' }); } catch(e) {}
124
278
  cleanupLocalDaemons();
125
- const serviceContent = `[Unit]
126
- Description=Telepty Daemon
127
- After=network.target
128
-
129
- [Service]
130
- ExecStart=${teleptyPath} daemon
131
- Restart=always
132
- User=${process.env.SUDO_USER || process.env.USER || 'root'}
133
- Environment=PATH=/usr/bin:/usr/local/bin:$PATH
134
- Environment=NODE_ENV=production
135
-
136
- [Install]
137
- WantedBy=multi-user.target`;
279
+ const serviceContent = buildSystemdService({
280
+ ...launchOptions,
281
+ user: process.env.SUDO_USER || process.env.USER || 'root'
282
+ });
138
283
 
139
284
  fs.writeFileSync('/etc/systemd/system/telepty.service', serviceContent);
140
285
  run('systemctl daemon-reload');
141
286
  run('systemctl enable telepty');
142
287
  run('systemctl start telepty');
288
+ assertSystemdServiceLive();
143
289
  console.log("✅ Systemd service installed and started.");
144
290
  process.exit(0);
145
291
  }
146
- } catch(e) {}
147
292
 
148
- // Fallback for Linux without systemd or non-root
149
- console.log("⚠️ Skipping systemd (no root or no systemd). Starting in background...");
293
+ console.log("⚙️ Setting up user systemd service for Linux...");
294
+ const userServicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'telepty.service');
295
+ fs.mkdirSync(path.dirname(userServicePath), { recursive: true });
296
+ cleanupLocalDaemons();
297
+ fs.writeFileSync(userServicePath, buildSystemdService({
298
+ ...launchOptions,
299
+ wantedBy: 'default.target'
300
+ }));
301
+ run('systemctl --user daemon-reload');
302
+ run('systemctl --user enable telepty');
303
+ run('systemctl --user start telepty');
304
+ assertSystemdServiceLive('telepty', { user: true });
305
+ console.log("✅ User systemd service installed and started.");
306
+ process.exit(0);
307
+ }
308
+
309
+ // Fallback for Linux without systemd
310
+ console.log("⚠️ Skipping persistent systemd setup. Starting daemon for this session only...");
150
311
  cleanupLocalDaemons();
151
- const subprocess = spawn(teleptyPath, ['daemon'], {
312
+ const subprocess = spawn(launchOptions.nodeBin, [launchOptions.cliJs, 'daemon'], {
152
313
  detached: true,
153
314
  stdio: 'ignore'
154
315
  });
155
316
  subprocess.unref();
156
- console.log("✅ Linux daemon started in background using nohup equivalent.");
317
+ console.log("✅ Linux daemon started in background for the current session.");
157
318
  }
158
319
 
159
320
  console.log("\n🎉 Installation complete! Telepty daemon is running.");
160
321
  console.log("👉 Try running: telepty attach\n");
161
- })();
322
+ }
323
+
324
+ if (require.main === module) {
325
+ main().catch((e) => {
326
+ console.error('❌ Installation failed:', e.message);
327
+ process.exit(1);
328
+ });
329
+ }
330
+
331
+ module.exports = {
332
+ buildLaunchdPlist,
333
+ buildSystemdService,
334
+ buildWindowsAutostartCommand,
335
+ resolveDaemonLaunchOptions,
336
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
@@ -35,9 +35,9 @@
35
35
  ],
36
36
  "scripts": {
37
37
  "postinstall": "node scripts/postinstall.js",
38
- "test": "node --test test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js && git diff --exit-code tests/snippet-protocol/v1/",
39
- "test:watch": "node --test --watch test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js",
40
- "test:ci": "node --test --test-reporter=spec test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js && git diff --exit-code tests/snippet-protocol/v1/",
38
+ "test": "node --require ./test-support/setup-env.js --test test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js && git diff --exit-code tests/snippet-protocol/v1/",
39
+ "test:watch": "node --require ./test-support/setup-env.js --test --watch test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js",
40
+ "test:ci": "node --require ./test-support/setup-env.js --test --test-reporter=spec test/auth.test.js test/http-auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/integration/daemon-launch.test.js test/cli.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js && git diff --exit-code tests/snippet-protocol/v1/",
41
41
  "typecheck": "tsc --noEmit",
42
42
  "regen-fixtures": "node scripts/regen-snippet-fixtures.js"
43
43
  },
package/session-state.js CHANGED
@@ -96,6 +96,14 @@ const THINKING_PATTERNS = [
96
96
  /\breading\b/i, // Claude Code "Reading..."
97
97
  /\bsearching\b/i, // Claude Code "Searching..."
98
98
  /\bplanning\b/i, // Claude Code "Planning..."
99
+ // codex CLI active-work markers (#558): codex emits NO braille spinner and NO OSC 133, so its
100
+ // "busy" state went unrecognized → blank sidebar pill. These are high-signal codex/claude markers
101
+ // shown ONLY while actively generating/running tools. ("Working" is intentionally NOT matched —
102
+ // it false-positives on common dev output like "working tree" / "working directory".)
103
+ /\besc to interrupt\b/i, // codex + claude: shown only during active generation / tool run
104
+ /\bstarting mcp servers?\b/i,// codex: MCP bootstrap on launch
105
+ /\bbooting mcp server\b/i, // codex: MCP server boot
106
+ /\bexploring\b/i, // codex activity verb (parallels Claude Code "Searching")
99
107
  /\.{3,}\s*$/, // trailing dots "..."
100
108
  ];
101
109