@dmsdc-ai/aigentry-telepty 0.5.2 → 0.5.4
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/cli.js +14 -1
- package/daemon.js +49 -4
- package/package.json +4 -4
- package/src/transport/websocket.js +8 -0
package/cli.js
CHANGED
|
@@ -1273,6 +1273,10 @@ async function main() {
|
|
|
1273
1273
|
let wsReady = false;
|
|
1274
1274
|
let reconnectAttempts = 0;
|
|
1275
1275
|
let reconnectTimer = null;
|
|
1276
|
+
// BUG-C: the daemon mints a per-owner token on each owner claim/reclaim and pushes it here.
|
|
1277
|
+
// We echo it on the teardown DELETE so the daemon can tell our (current-owner) exit apart
|
|
1278
|
+
// from a stale/displaced owner's exit and avoid the shared-fate teardown.
|
|
1279
|
+
let currentOwnerToken = null;
|
|
1276
1280
|
let lastInjectTextTime = 0;
|
|
1277
1281
|
const MAX_RECONNECT_DELAY = 30000;
|
|
1278
1282
|
|
|
@@ -1318,6 +1322,10 @@ async function main() {
|
|
|
1318
1322
|
daemonWs.on('message', (message) => {
|
|
1319
1323
|
try {
|
|
1320
1324
|
const msg = JSON.parse(message);
|
|
1325
|
+
if (msg.type === 'owner_token') {
|
|
1326
|
+
currentOwnerToken = msg.token || null;
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1321
1329
|
if (msg.type === 'inject') {
|
|
1322
1330
|
const chunks = [];
|
|
1323
1331
|
const rawData = typeof msg.data === 'string' ? msg.data : String(msg.data ?? '');
|
|
@@ -1430,7 +1438,12 @@ async function main() {
|
|
|
1430
1438
|
// Purge bridge mailbox on clean exit (undelivered messages are stale)
|
|
1431
1439
|
try { bridgeMailbox.purge(bridgeTarget); } catch {}
|
|
1432
1440
|
process.stdout.write(`\x1b]0;\x07`);
|
|
1433
|
-
|
|
1441
|
+
// BUG-C: carry our owner token so the daemon destroys only on the CURRENT owner's exit;
|
|
1442
|
+
// a stale/displaced owner's DELETE (mismatched token) must not tear down the live owner.
|
|
1443
|
+
const deleteUrl = currentOwnerToken
|
|
1444
|
+
? `${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}?owner_token=${encodeURIComponent(currentOwnerToken)}`
|
|
1445
|
+
: `${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}`;
|
|
1446
|
+
fetchWithAuth(deleteUrl, { method: 'DELETE' }).catch(() => {});
|
|
1434
1447
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
1435
1448
|
try {
|
|
1436
1449
|
daemonWs.close();
|
package/daemon.js
CHANGED
|
@@ -298,9 +298,20 @@ function fireAutoReport(targetId, targetSession, pendingReport, trigger, deps =
|
|
|
298
298
|
const srcSession = _sessions[srcId];
|
|
299
299
|
if (!srcSession) return;
|
|
300
300
|
|
|
301
|
+
// #537 / Bug B: a never-started worker (transient submit failure → claude startup
|
|
302
|
+
// busy→idle settle at ~4.5s) must NOT be reported TASK_COMPLETE. When a submit was
|
|
303
|
+
// expected, the elapsed floor and startup-polluted sawWorkingAfterInject are NOT trusted
|
|
304
|
+
// as proof of processing — require positive submit confirmation (screen-poll verify /
|
|
305
|
+
// honest force / gate-off). Paths with no submit expected keep the legacy floor/work rule.
|
|
306
|
+
const strongSubmitConfirmed = !!(
|
|
307
|
+
pendingReport.submitConfirmedAt ||
|
|
308
|
+
(pendingReport.submitConfirm && pendingReport.submitConfirm.accepted === true)
|
|
309
|
+
);
|
|
301
310
|
const confirmed = trigger === 'ready-signal' && pendingReport.submitExpected
|
|
302
311
|
? false
|
|
303
|
-
:
|
|
312
|
+
: pendingReport.submitExpected
|
|
313
|
+
? strongSubmitConfirmed
|
|
314
|
+
: (elapsedNum >= AUTO_REPORT_MIN_REAL_SECONDS || hasSubmitEvidence);
|
|
304
315
|
const injTag = pendingReport.injectId ? ` inject=${pendingReport.injectId}` : '';
|
|
305
316
|
const reportMsg = confirmed
|
|
306
317
|
? `TASK_COMPLETE: ${targetId} is now idle after processing inject (${elapsed}s, via ${trigger}${injTag})`
|
|
@@ -1184,6 +1195,19 @@ function terminalLevelSubmit(id, session) {
|
|
|
1184
1195
|
return null;
|
|
1185
1196
|
}
|
|
1186
1197
|
|
|
1198
|
+
// #537 / Bug B: a forced submit is only HONESTLY confirmed when its strategy actually
|
|
1199
|
+
// reached the rendered surface. On a cmux-backed session the surface is cmux; a `pty_cr`
|
|
1200
|
+
// fallback means `cmux send-key` failed ("Failed to write to socket") and the live CLI
|
|
1201
|
+
// never received Enter. Terminal-level strategies (kitty/cmux) are real delivery; a pty_cr
|
|
1202
|
+
// fallback on a cmux surface is NOT. Pure + exported so the decision is unit-testable.
|
|
1203
|
+
function forceSubmitDeliveredToSurface(session, strategy) {
|
|
1204
|
+
if (!strategy) return false;
|
|
1205
|
+
if (session && session.backend === 'cmux' && session.cmuxWorkspaceId && strategy === 'pty_cr') {
|
|
1206
|
+
return false;
|
|
1207
|
+
}
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1187
1211
|
async function deliverInjectionToSession(id, session, prompt, options = {}) {
|
|
1188
1212
|
const now = Date.now();
|
|
1189
1213
|
if (!options.bypassBootstrapQueue && shouldQueueBootstrapOperation(session)) {
|
|
@@ -2190,11 +2214,19 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
|
|
|
2190
2214
|
}
|
|
2191
2215
|
const strategy = terminalLevelSubmit(id, session);
|
|
2192
2216
|
if (strategy) {
|
|
2217
|
+
// #537 / Bug B: force-confirm must reflect ACTUAL delivery. A pty_cr fallback on a
|
|
2218
|
+
// cmux surface means cmux send-key failed and Enter never reached the CLI — record
|
|
2219
|
+
// UNCONFIRMED so the ENFORCE-REPORT gate never labels a never-delivered inject DONE.
|
|
2220
|
+
const deliveredToSurface = forceSubmitDeliveredToSurface(session, strategy);
|
|
2193
2221
|
if (injectedBody) {
|
|
2194
|
-
|
|
2222
|
+
if (deliveredToSurface) {
|
|
2223
|
+
markPendingReportSubmitConfirmed(id, { reason: 'force', attempts: 1 });
|
|
2224
|
+
} else {
|
|
2225
|
+
markPendingReportSubmitUnconfirmed(id, { reason: 'cmux_send_failed', attempts: 1, retryable: true });
|
|
2226
|
+
}
|
|
2195
2227
|
}
|
|
2196
|
-
emitSubmitBus({ strategy, attempts: 1, gated: false, forced: true });
|
|
2197
|
-
return res.json({ success: true, strategy, attempts: 1, gated: false, forced: true });
|
|
2228
|
+
emitSubmitBus({ strategy, attempts: 1, gated: false, forced: true, submit_confirmed: deliveredToSurface });
|
|
2229
|
+
return res.json({ success: true, strategy, attempts: 1, gated: false, forced: true, submit_confirmed: deliveredToSurface });
|
|
2198
2230
|
}
|
|
2199
2231
|
if (injectedBody) {
|
|
2200
2232
|
markPendingReportSubmitUnconfirmed(id, { reason: 'strategy_failed', attempts: 0, retryable: false });
|
|
@@ -2757,6 +2789,18 @@ app.delete('/api/sessions/:id', (req, res) => {
|
|
|
2757
2789
|
const session = sessions[resolvedId];
|
|
2758
2790
|
const id = resolvedId;
|
|
2759
2791
|
if (session.isClosing) return res.json({ success: true, status: 'closing' });
|
|
2792
|
+
// BUG-C (shared-fate): a wrapped session can be co-bound by a stale/displaced owner bridge
|
|
2793
|
+
// (duplicate --id). A DELETE carrying a token that is NOT the current owner's, while a live
|
|
2794
|
+
// owner ws is still open, is that stale bridge exiting — it must NOT tear down the live owner.
|
|
2795
|
+
// Detach-only (no-op): leave the record and every client untouched. Tokenless callers
|
|
2796
|
+
// (operator `telepty delete`, ghost clean) and matching-token current-owner exits are
|
|
2797
|
+
// unaffected. Forceful kills go through POST /:id/kill (teardownSessionById), not here.
|
|
2798
|
+
const ownerToken = req.query.owner_token;
|
|
2799
|
+
if (session.type === 'wrapped'
|
|
2800
|
+
&& ownerToken && session.ownerToken && ownerToken !== session.ownerToken
|
|
2801
|
+
&& isOpenWebSocket(session.ownerWs)) {
|
|
2802
|
+
return res.json({ success: true, status: 'stale-detached' });
|
|
2803
|
+
}
|
|
2760
2804
|
try {
|
|
2761
2805
|
session.isClosing = true;
|
|
2762
2806
|
if (session.type === 'wrapped') {
|
|
@@ -3476,6 +3520,7 @@ if (require.main === module) {
|
|
|
3476
3520
|
// production call sites is unchanged. NOT a public API — internal/test use only.
|
|
3477
3521
|
module.exports = {
|
|
3478
3522
|
fireAutoReport, // #32: provenance-tagged auto-report (deps DI: now/deliver/...)
|
|
3523
|
+
forceSubmitDeliveredToSurface, // #537/Bug B: honest force-confirm (pty_cr on cmux = not delivered)
|
|
3479
3524
|
failBootstrapQueueOnTimeout, // #31: actionable bootstrap-timeout queue flush
|
|
3480
3525
|
shouldApplyOwnerAliveFloor, // #29: owner-alive optimistic-floor decision (deps DI: isProcessRunning/...)
|
|
3481
3526
|
scheduleBootstrapPromptPoll, // #29: arms the floor timer (deps DI: setTimeout/...)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmsdc-ai/aigentry-telepty",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
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/submit-gate.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/submit-gate.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/submit-gate.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 --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/enforce-submit-gate.test.js test/submit-gate.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/enforce-submit-gate.test.js test/submit-gate.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/enforce-submit-gate.test.js test/submit-gate.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/",
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
42
|
"regen-fixtures": "node scripts/regen-snippet-fixtures.js"
|
|
43
43
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('node:crypto');
|
|
3
4
|
const { WebSocketServer } = require('ws');
|
|
4
5
|
|
|
5
6
|
function isOpenWebSocket(ws) {
|
|
@@ -107,6 +108,13 @@ function installWebSocketTransport(deps) {
|
|
|
107
108
|
activeSession.ownerWs.terminate();
|
|
108
109
|
}
|
|
109
110
|
activeSession.ownerWs = ws;
|
|
111
|
+
// BUG-C: mint a fresh per-owner token on every claim/reclaim and push it to this owner.
|
|
112
|
+
// The token is the exact "are-you-the-current-owner" discriminator the DELETE guard uses
|
|
113
|
+
// to suppress a stale/displaced owner's teardown (shared-fate fix). Reclaim refreshes it,
|
|
114
|
+
// so the live current owner always holds the current token while a displaced owner keeps a
|
|
115
|
+
// stale one.
|
|
116
|
+
activeSession.ownerToken = crypto.randomUUID();
|
|
117
|
+
try { ws.send(JSON.stringify({ type: 'owner_token', token: activeSession.ownerToken })); } catch {}
|
|
110
118
|
markSessionConnected(activeSession);
|
|
111
119
|
initializeBootstrapState(activeSession);
|
|
112
120
|
console.log(`[WS] Wrap owner ${isOwnerConnect && activeSession.clients.size > 1 ? 're-' : ''}connected for session ${sessionId} (Total: ${activeSession.clients.size})`);
|