@dmsdc-ai/aigentry-telepty 0.6.5 → 0.6.6
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 +34 -0
- package/cli.js +79 -2
- package/daemon-control.js +22 -0
- package/package.json +4 -4
- package/skills/telepty/SKILL.md +4 -3
- package/skills/telepty-allow/SKILL.md +16 -0
- package/skills/telepty-daemon/SKILL.md +21 -3
- package/src/transport/websocket.js +22 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.6.6] - 2026-06-14
|
|
6
|
+
|
|
7
|
+
### Fixed — duplicate same-id `allow` flap loop + `kill` doesn't stick (#56)
|
|
8
|
+
|
|
9
|
+
- **A second `telepty allow --id <X>` no longer makes the session undeliverable-to.** Previously a
|
|
10
|
+
duplicate wrap-owner caused the daemon to oscillate between owners (`Total: 1 ↔ 2`, repeated
|
|
11
|
+
"Replacing stale ownerWs"), so the session never stayed `ready for inject` and **injects were
|
|
12
|
+
silently dropped**. The daemon now does a **durable last-writer-wins Replace**: the displaced
|
|
13
|
+
owner is closed with a dedicated **`4001 'Owner replaced'`** code (reason-independent — a bare
|
|
14
|
+
terminate delivered an empty reason that the bridge mis-read as a reconnect), and the CLI treats
|
|
15
|
+
`4001` as a clean exit with **no reconnect**. The `1000 'Session destroyed'` path is untouched;
|
|
16
|
+
the `#536` owner-token guard still suppresses the displaced bridge's stale-token DELETE (no
|
|
17
|
+
shared-fate cascade). Total settles at 1.
|
|
18
|
+
- **`telepty kill --force` now sticks.** The owning wrap-owner PID is captured at `?owner=1` claim
|
|
19
|
+
time (previously only set on reconnect-register, so a first-connect owner had a null pid and could
|
|
20
|
+
re-register after a kill). Combined with the durable Replace, a killed session no longer respawns.
|
|
21
|
+
Cross-platform (`taskkill /T /F` on Windows).
|
|
22
|
+
|
|
23
|
+
### Added — daemon lifecycle: `daemon start` (detached) / `stop` / `restart` (#55)
|
|
24
|
+
|
|
25
|
+
- **`telepty daemon` is no longer foreground-only.** Previously the `daemon` command ignored its
|
|
26
|
+
subcommand argument and always started a foreground daemon (so `daemon stop` actually *started*
|
|
27
|
+
one, and there was no `restart`). Now:
|
|
28
|
+
- **`telepty daemon start`** — starts the daemon **detached/background** and returns control to the
|
|
29
|
+
shell immediately (prints pid + listen URL). Fixes one-command install/automation flows.
|
|
30
|
+
- **`telepty daemon stop`** — terminates the daemon process (SIGTERM → SIGKILL) and frees the port.
|
|
31
|
+
**Surgical**: it targets only the state-file pid / configured-port owner and force-disables the
|
|
32
|
+
system-wide process scan, so it can **never reap an unrelated telepty daemon**.
|
|
33
|
+
- **`telepty daemon restart`** — stop + detached start (a cross-platform restart; replaces the
|
|
34
|
+
mac-only `launchctl kickstart` and gives Windows a restart it never had).
|
|
35
|
+
- Bare `telepty daemon` keeps its foreground behavior (install/launchd flows depend on it); the
|
|
36
|
+
internal version-mismatch auto-restart (`ensureDaemonRunning`) is unchanged. `telepty allow`
|
|
37
|
+
(session bridges) stays foreground by design.
|
|
38
|
+
|
|
5
39
|
## [0.6.5] - 2026-06-13
|
|
6
40
|
|
|
7
41
|
### Fixed — orchestrator REPORT loss: hold-and-redeliver queued injects until idle (#617)
|
package/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
findPortOwnerPid,
|
|
19
19
|
readDaemonState,
|
|
20
20
|
readRestartFailureMarker,
|
|
21
|
+
stopDaemon,
|
|
21
22
|
writeRestartFailureMarker
|
|
22
23
|
} = require('./daemon-control');
|
|
23
24
|
const { attachInteractiveTerminal, getTerminalSize, restoreTerminalModes } = require('./interactive-terminal');
|
|
@@ -428,6 +429,7 @@ function startDetachedDaemon() {
|
|
|
428
429
|
stdio: 'ignore'
|
|
429
430
|
});
|
|
430
431
|
cp.unref();
|
|
432
|
+
return cp;
|
|
431
433
|
}
|
|
432
434
|
|
|
433
435
|
async function waitForDaemonHealth(maxMs = 5000) {
|
|
@@ -580,6 +582,15 @@ function isDaemonDestroyClose(code, reason) {
|
|
|
580
582
|
return code === 1000 && reasonText === 'Session destroyed';
|
|
581
583
|
}
|
|
582
584
|
|
|
585
|
+
// telepty#56: a dedicated 4001 close means the daemon deterministically replaced this wrap-owner
|
|
586
|
+
// with a newer ?owner=1 claim (durable last-writer-wins). The displaced bridge must EXIT, not
|
|
587
|
+
// reconnect — reconnecting would re-contend for the id and oscillate (Total flaps 1<->2). The code
|
|
588
|
+
// is the discriminator (not the reason): a half-open socket may never deliver the close reason.
|
|
589
|
+
// Pure predicate, exposed for unit-testing.
|
|
590
|
+
function isOwnerReplacedClose(code) {
|
|
591
|
+
return code === 4001;
|
|
592
|
+
}
|
|
593
|
+
|
|
583
594
|
function runUpdateInstall() {
|
|
584
595
|
if (process.env.TELEPTY_SKIP_PACKAGE_UPDATE === '1') {
|
|
585
596
|
return;
|
|
@@ -1231,6 +1242,58 @@ async function main() {
|
|
|
1231
1242
|
}
|
|
1232
1243
|
|
|
1233
1244
|
if (cmd === 'daemon') {
|
|
1245
|
+
// telepty#55: real daemon-lifecycle surface. Pre-0.6.6 this block ignored
|
|
1246
|
+
// args[1] entirely and ALWAYS started a foreground daemon — so `daemon start`
|
|
1247
|
+
// blocked the shell, `daemon stop` actually STARTED a daemon, and `restart`
|
|
1248
|
+
// didn't exist. We now parse the subcommand; bare `telepty daemon` keeps the
|
|
1249
|
+
// foreground behavior for back-compat (install/launchd flows depend on it).
|
|
1250
|
+
const sub = args[1];
|
|
1251
|
+
|
|
1252
|
+
if (sub === 'start') {
|
|
1253
|
+
// Detached/background start: return control to the shell immediately
|
|
1254
|
+
// (cross-platform spawn with detached + stdio:'ignore' + unref). The child
|
|
1255
|
+
// IS the daemon process, so cp.pid is the daemon's pid.
|
|
1256
|
+
const cp = startDetachedDaemon();
|
|
1257
|
+
console.log(`\x1b[32m✅ Telepty daemon started (pid ${cp.pid}) → ${DAEMON_URL}\x1b[0m`);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (sub === 'stop') {
|
|
1262
|
+
// Terminate the running daemon (state-file pid + configured-port owner),
|
|
1263
|
+
// graceful SIGTERM→SIGKILL. Surgical: never a system-wide process sweep
|
|
1264
|
+
// (that's `cleanup-daemons`). Internal auto-restart is untouched.
|
|
1265
|
+
const results = stopDaemon({ port: Number(PORT) });
|
|
1266
|
+
if (results.stopped.length === 0 && results.failed.length === 0) {
|
|
1267
|
+
console.log('No telepty daemon running.');
|
|
1268
|
+
} else {
|
|
1269
|
+
if (results.stopped.length > 0) {
|
|
1270
|
+
console.log(`\x1b[32m✅ Stopped telepty daemon (${results.stopped.map((d) => `pid ${d.pid}`).join(', ')}).\x1b[0m`);
|
|
1271
|
+
}
|
|
1272
|
+
if (results.failed.length > 0) {
|
|
1273
|
+
console.error(`\x1b[31m❌ Failed to stop ${results.failed.length} daemon process(es): ${results.failed.map((d) => `pid ${d.pid}`).join(', ')}.\x1b[0m`);
|
|
1274
|
+
process.exitCode = 1;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
if (sub === 'restart') {
|
|
1281
|
+
// Clean cross-platform restart = surgical stop + detached start. Replaces
|
|
1282
|
+
// the mac-only `launchctl kickstart` and gives Windows a restart it never
|
|
1283
|
+
// had. Internal auto-restart (ensureDaemonRunning) is NOT touched.
|
|
1284
|
+
stopDaemon({ port: Number(PORT) });
|
|
1285
|
+
const cp = startDetachedDaemon();
|
|
1286
|
+
console.log(`\x1b[32m✅ Telepty daemon restarted (pid ${cp.pid}) → ${DAEMON_URL}\x1b[0m`);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (sub) {
|
|
1291
|
+
console.error('❌ Usage: telepty daemon [start|stop|restart]');
|
|
1292
|
+
process.exit(1);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Bare `telepty daemon` — FOREGROUND (back-compat: install/launchd flows run
|
|
1296
|
+
// this and expect a blocking process). `daemon start` is the detached path.
|
|
1234
1297
|
console.log('Starting telepty daemon...');
|
|
1235
1298
|
// daemon.js binds the port only when launched as the daemon. The CLI reaches
|
|
1236
1299
|
// it via require() (not as require.main), so signal intent explicitly — tests
|
|
@@ -1769,7 +1832,9 @@ async function main() {
|
|
|
1769
1832
|
// Connect to daemon WebSocket with auto-reconnect
|
|
1770
1833
|
// owner=1 tells daemon this is the allow bridge (owner), not an attach viewer.
|
|
1771
1834
|
// Daemon uses this to reclaim ownership even if a stale ownerWs is still registered.
|
|
1772
|
-
|
|
1835
|
+
// telepty#56: owner_pid lets the daemon record this bridge's PID at claim time so
|
|
1836
|
+
// `kill --force` can SIGKILL the owning process (kill-stick), independent of register timing.
|
|
1837
|
+
const wsUrl = `${daemonWsUrl(REMOTE_HOST)}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(getAuthToken())}&owner=1&owner_pid=${process.pid}`;
|
|
1773
1838
|
let daemonWs = null;
|
|
1774
1839
|
let wsReady = false;
|
|
1775
1840
|
let reconnectAttempts = 0;
|
|
@@ -1889,6 +1954,16 @@ async function main() {
|
|
|
1889
1954
|
}
|
|
1890
1955
|
return;
|
|
1891
1956
|
}
|
|
1957
|
+
// #56: the daemon replaced this owner with a newer ?owner=1 claim (close 4001). Exit
|
|
1958
|
+
// cleanly without reconnecting — reconnecting re-contends and oscillates. The teardown
|
|
1959
|
+
// DELETE carries our now-stale ownerToken and is suppressed by the daemon's #536 guard,
|
|
1960
|
+
// so the live new owner is not torn down (no shared-fate cascade).
|
|
1961
|
+
if (isOwnerReplacedClose(code)) {
|
|
1962
|
+
if (closeAllowSession()) {
|
|
1963
|
+
exitAllowSession(0);
|
|
1964
|
+
}
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1892
1967
|
scheduleReconnect();
|
|
1893
1968
|
});
|
|
1894
1969
|
|
|
@@ -3958,7 +4033,8 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
3958
4033
|
\x1b[1mtelepty\x1b[0m — Connect any terminal to any terminal, any machine.
|
|
3959
4034
|
|
|
3960
4035
|
\x1b[1mSession Management:\x1b[0m
|
|
3961
|
-
telepty daemon Start the
|
|
4036
|
+
telepty daemon Start the daemon in the foreground (port 3848)
|
|
4037
|
+
telepty daemon start|stop|restart Start (detached) / stop / restart the background daemon
|
|
3962
4038
|
telepty spawn --id <id> <command> [args...] Spawn a new background session
|
|
3963
4039
|
telepty allow [--id <id>] [--idle-ttl 1h|off] [--auto-restart] <command> [args...] Wrap a CLI for remote control
|
|
3964
4040
|
telepty list [--json] List sessions (local + Tailnet)
|
|
@@ -4034,6 +4110,7 @@ if (require.main === module) {
|
|
|
4034
4110
|
module.exports = {
|
|
4035
4111
|
classifyBackend, // #29: TERM_PROGRAM/CMUX/kitty → backend string
|
|
4036
4112
|
isDaemonDestroyClose, // #17 OQ-2: 1000 'Session destroyed' → terminate-not-reconnect
|
|
4113
|
+
isOwnerReplacedClose, // #56: 4001 'Owner replaced' → exit-not-reconnect (durable Replace)
|
|
4037
4114
|
sanitizePathArg, // #26: path-arg validation/normalization
|
|
4038
4115
|
decideDaemonAction, // #567: pure restart-decision policy (meta-primary; no I/O)
|
|
4039
4116
|
ensureDaemonRunning, // #567: orchestrator (injectable probes for unit-testing)
|
package/daemon-control.js
CHANGED
|
@@ -417,10 +417,32 @@ function cleanupDaemonProcesses(opts) {
|
|
|
417
417
|
return { stopped, failed };
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
// telepty#55: user-facing `telepty daemon stop`. Unlike cleanupDaemonProcesses
|
|
421
|
+
// (which ALSO sweeps the whole process table for ANY telepty daemon — the right
|
|
422
|
+
// behavior for `cleanup-daemons` and internal repair), stop must be SURGICAL: it
|
|
423
|
+
// targets only the daemon THIS CLI is configured for — the state-file pid and the
|
|
424
|
+
// owner of the configured port (default 3848). It never blind-scans the process
|
|
425
|
+
// table, so it can never reap an unrelated telepty daemon (e.g. another node's
|
|
426
|
+
// daemon on a different port). This mirrors the #44/#15 survivor-detection surface
|
|
427
|
+
// (state-file pid + port owner) and reuses cleanupDaemonProcesses' graceful
|
|
428
|
+
// SIGTERM→SIGKILL kill path + stale-state cleanup with the global scan disabled.
|
|
429
|
+
function stopDaemon(opts) {
|
|
430
|
+
const o = opts || {};
|
|
431
|
+
return cleanupDaemonProcesses({
|
|
432
|
+
...o,
|
|
433
|
+
port: Number.isInteger(o.port) && o.port > 0 ? o.port : 3848,
|
|
434
|
+
// Force-disable the system-wide process scan unconditionally (never honor a
|
|
435
|
+
// caller-provided one) — this is the surgical guarantee: stop reaps only the
|
|
436
|
+
// state-file pid and the configured port's owner, never a table sweep.
|
|
437
|
+
listDaemonProcesses: () => []
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
420
441
|
module.exports = {
|
|
421
442
|
DAEMON_STATE_FILE,
|
|
422
443
|
claimDaemonState,
|
|
423
444
|
cleanupDaemonProcesses,
|
|
445
|
+
stopDaemon,
|
|
424
446
|
clearDaemonState,
|
|
425
447
|
clearRestartFailureMarker,
|
|
426
448
|
findParentProcessInfo,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmsdc-ai/aigentry-telepty",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"main": "daemon.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aigentry-telepty": "install.js",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"scripts": {
|
|
38
38
|
"postinstall": "node scripts/postinstall.js",
|
|
39
39
|
"preuninstall": "node scripts/preuninstall.js",
|
|
40
|
-
"test": "node --require ./test-support/setup-env.js --test test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.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/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.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/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.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 test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
41
|
-
"test:watch": "node --require ./test-support/setup-env.js --test --watch test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.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/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.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/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.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 test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js",
|
|
42
|
-
"test:ci": "node --require ./test-support/setup-env.js --test --test-reporter=spec test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.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/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.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/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.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 test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
40
|
+
"test": "node --require ./test-support/setup-env.js --test test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/dupid-flap-kill-stick-56.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/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.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/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/daemon-lifecycle-55.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 test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
41
|
+
"test:watch": "node --require ./test-support/setup-env.js --test --watch test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/dupid-flap-kill-stick-56.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/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.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/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/daemon-lifecycle-55.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 test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js",
|
|
42
|
+
"test:ci": "node --require ./test-support/setup-env.js --test --test-reporter=spec test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/dupid-flap-kill-stick-56.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/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.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/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/daemon-lifecycle-55.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 test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
43
43
|
"typecheck": "tsc --noEmit",
|
|
44
44
|
"regen-fixtures": "node scripts/regen-snippet-fixtures.js"
|
|
45
45
|
},
|
package/skills/telepty/SKILL.md
CHANGED
|
@@ -10,8 +10,9 @@ telepty is a PTY multiplexer and session orchestrator. It creates, connects, and
|
|
|
10
10
|
## Quick Start
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
# Start daemon
|
|
14
|
-
telepty daemon
|
|
13
|
+
# Start daemon in the background (detached; returns the shell immediately)
|
|
14
|
+
telepty daemon start
|
|
15
|
+
# (bare `telepty daemon` runs it in the FOREGROUND — used by install/launchd)
|
|
15
16
|
|
|
16
17
|
# Create a session wrapping Claude Code
|
|
17
18
|
telepty allow --id my-session claude
|
|
@@ -45,7 +46,7 @@ telepty tui
|
|
|
45
46
|
|
|
46
47
|
- For humans: prefer natural-language examples and TUI, then raw CLI commands
|
|
47
48
|
- For AI agents: use raw `telepty` commands directly for execution
|
|
48
|
-
- When daemon is broken: repair first with `telepty cleanup-daemons && telepty daemon`
|
|
49
|
+
- When daemon is broken: repair first with `telepty cleanup-daemons && telepty daemon start` (or `telepty daemon restart`)
|
|
49
50
|
|
|
50
51
|
## Related Skills
|
|
51
52
|
|
|
@@ -57,6 +57,22 @@ COLUMNS=120 LINES=40 telepty allow --id headless-session claude
|
|
|
57
57
|
| `TELEPTY_SESSION_ID` | The session ID you specified |
|
|
58
58
|
| `TELEPTY_AVAILABLE` | `true` |
|
|
59
59
|
|
|
60
|
+
### Duplicate `--id` is last-writer-wins (deterministic replace)
|
|
61
|
+
|
|
62
|
+
Running `telepty allow --id <X> …` again for an id that already has a live wrap-owner
|
|
63
|
+
**deterministically replaces** the old owner (last-writer-wins): the newer allow takes over the id,
|
|
64
|
+
and the **older bridge exits** (close code `4001 'Owner replaced'` — it does not reconnect). The
|
|
65
|
+
session stays continuously `ready for inject`; there is no owner flap and no dropped injects.
|
|
66
|
+
|
|
67
|
+
This is the intended path for a clean restart (e.g. `orchestrator-boot.sh` kill-9s a stale bridge
|
|
68
|
+
then re-`allow`s). You do not need to `kill` the old session first — the new `allow` reclaims it.
|
|
69
|
+
|
|
70
|
+
### kill stops the owning process (kill sticks)
|
|
71
|
+
|
|
72
|
+
`telepty kill <X> --force` terminates the **owning wrap-owner process**, not just the session
|
|
73
|
+
record. The owner's PID is captured when the bridge claims the id, so `--force` SIGKILLs it
|
|
74
|
+
(cross-platform: `taskkill /T /F` on Windows). A killed session does not silently re-register.
|
|
75
|
+
|
|
60
76
|
### Aliases
|
|
61
77
|
|
|
62
78
|
`telepty enable` and `telepty wrap` are aliases for `telepty allow`.
|
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: telepty-daemon
|
|
3
|
-
description: 'Manage the telepty daemon — start, stop, repair, update, and TUI dashboard. Use when daemon is broken or needs maintenance. 키워드: 데몬 시작, 데몬 재시작, 데몬 종료, TUI, 대시보드, 데몬 상태, daemon'
|
|
3
|
+
description: 'Manage the telepty daemon — start (detached), stop, restart, repair, update, and TUI dashboard. Use when daemon is broken or needs maintenance. 키워드: 데몬 시작, 데몬 재시작, 데몬 종료, 백그라운드, TUI, 대시보드, 데몬 상태, daemon'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# telepty-daemon — Daemon Management
|
|
7
7
|
|
|
8
|
-
## daemon — Start the daemon
|
|
8
|
+
## daemon — Start the daemon (foreground)
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
telepty daemon
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Starts the telepty daemon on port 3848. The daemon manages all sessions, handles inject delivery, and serves the HTTP/WS API.
|
|
14
|
+
Starts the telepty daemon on port 3848 **in the foreground** (blocks the shell). The daemon manages all sessions, handles inject delivery, and serves the HTTP/WS API. This is the form install/launchd flows use; for interactive/automation use, prefer `telepty daemon start` below.
|
|
15
15
|
|
|
16
16
|
The daemon auto-starts when needed (e.g., `telepty allow` starts it automatically). Manual start is rarely needed.
|
|
17
17
|
|
|
18
|
+
## daemon start | stop | restart — Background lifecycle
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
telepty daemon start # start DETACHED in the background, return the shell immediately
|
|
22
|
+
telepty daemon stop # gracefully stop the running daemon (SIGTERM → SIGKILL), free the port
|
|
23
|
+
telepty daemon restart # stop, then start detached (clean cross-platform restart)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Cross-platform (macOS / Linux / Windows):
|
|
27
|
+
|
|
28
|
+
- **`start`** spawns the daemon detached (`stdio` ignored, unref'd) and returns control to the shell at once, printing the pid and listen URL. Use this for one-command install/automation instead of the blocking foreground `telepty daemon`.
|
|
29
|
+
- **`stop`** is surgical — it terminates only the daemon this CLI is configured for (the state-file pid and/or the owner of the configured port, default 3848). It does **not** sweep the whole process table (that is `cleanup-daemons`), so it never reaps an unrelated telepty daemon.
|
|
30
|
+
- **`restart`** = `stop` then detached `start`. Replaces the macOS-only `launchctl kickstart` and gives Windows a restart it never had.
|
|
31
|
+
|
|
32
|
+
Natural-language: "데몬 백그라운드로 시작", "데몬 종료해줘", "데몬 재시작", "start the daemon in the background", "stop/restart the daemon".
|
|
33
|
+
|
|
34
|
+
> Internal auto-restart (version-mismatch recovery) is unaffected — these are user-facing controls layered on top.
|
|
35
|
+
|
|
18
36
|
## cleanup-daemons — Kill stale daemon processes
|
|
19
37
|
|
|
20
38
|
```bash
|
|
@@ -103,11 +103,31 @@ function installWebSocketTransport(deps) {
|
|
|
103
103
|
if (activeSession.type === 'wrapped' && (!activeSession.ownerWs || isOwnerConnect)) {
|
|
104
104
|
const hadDisconnectedOwner = !isOpenWebSocket(activeSession.ownerWs) && activeSession.lastDisconnectedAt;
|
|
105
105
|
if (isOwnerConnect && activeSession.ownerWs && activeSession.ownerWs !== ws) {
|
|
106
|
-
//
|
|
106
|
+
// telepty#56 (durable last-writer-wins Replace): close the displaced owner with the
|
|
107
|
+
// dedicated terminal code 4001 'Owner replaced' instead of a bare terminate(). A
|
|
108
|
+
// terminate() is an abnormal 1006 close, which a bridge reads as a transient drop and
|
|
109
|
+
// RECONNECTS — re-contending for the id and oscillating forever (Total flaps 1<->2,
|
|
110
|
+
// injects dropped). 4001 is reason-independent (WS 4000-4999 = app-reserved), so the
|
|
111
|
+
// displaced bridge exits without reconnecting even when the close reason is lost on a
|
|
112
|
+
// half-open TCP socket. The session RECORD survives under the new owner; no shared-fate
|
|
113
|
+
// cascade — the displaced bridge's now-stale ownerToken is suppressed by the #536 DELETE
|
|
114
|
+
// guard. A fallback terminate() reaps a half-open socket that never ACKs the close.
|
|
107
115
|
console.log(`[WS] Replacing stale ownerWs for session ${sessionId}`);
|
|
108
|
-
activeSession.ownerWs
|
|
116
|
+
const displaced = activeSession.ownerWs;
|
|
117
|
+
try { displaced.close(4001, 'Owner replaced'); } catch {}
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
if (displaced.readyState !== 3) { try { displaced.terminate(); } catch {} }
|
|
120
|
+
}, 1000);
|
|
109
121
|
}
|
|
110
122
|
activeSession.ownerWs = ws;
|
|
123
|
+
// telepty#56 (kill-stick): capture the owner PID at claim time. The reconnect-register POST
|
|
124
|
+
// only carries owner_pid on reconnect, so a first-connect owner would otherwise have a null
|
|
125
|
+
// ownerPid and `kill --force` could not SIGKILL the owning process. The bridge passes its pid
|
|
126
|
+
// on the ?owner=1 URL; record it so the kill path always has a process to signal.
|
|
127
|
+
const claimOwnerPid = Number(url.searchParams.get('owner_pid'));
|
|
128
|
+
if (Number.isInteger(claimOwnerPid) && claimOwnerPid > 0) {
|
|
129
|
+
activeSession.ownerPid = claimOwnerPid;
|
|
130
|
+
}
|
|
111
131
|
// BUG-C: mint a fresh per-owner token on every claim/reclaim and push it to this owner.
|
|
112
132
|
// The token is the exact "are-you-the-current-owner" discriminator the DELETE guard uses
|
|
113
133
|
// to suppress a stale/displaced owner's teardown (shared-fate fix). Reclaim refreshes it,
|