@dmsdc-ai/aigentry-telepty 0.5.5 → 0.5.7
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/daemon.js +144 -79
- package/package.json +4 -4
- package/session-state.js +11 -2
- package/terminal-backend.js +0 -19
package/daemon.js
CHANGED
|
@@ -88,8 +88,18 @@ sessionStateManager.onTransition((sessionId, from, to, detail) => {
|
|
|
88
88
|
// Mark as idle-notified (but keep the entry — REPORT is still pending).
|
|
89
89
|
// Entry is cleared when REPORT arrives (via inject endpoint) OR session dies.
|
|
90
90
|
if (pendingReport.idleNotified) return; // only fire once
|
|
91
|
+
// #545: only an OSC133-marked idle with the injected body consumed from the PTY outputRing
|
|
92
|
+
// is trustworthy enough to report TASK_COMPLETE. A weak prompt-glyph / silence flip (the
|
|
93
|
+
// residual WORKING case the THINKING-only state guard doesn't cover) stays
|
|
94
|
+
// TASK_IDLE_UNCONFIRMED — never a false complete.
|
|
95
|
+
const idleTrigger = detail && detail.detail ? detail.detail.trigger : null;
|
|
96
|
+
const bodyText = pendingReport.injectedBodyPreview;
|
|
97
|
+
const bodyVisible = bodyText
|
|
98
|
+
? submitGate.observeBodyVisibility(session, bodyText).visible === true
|
|
99
|
+
: false;
|
|
100
|
+
const idleEvidenceReliable = idleTrigger === 'osc_133_prompt' && !bodyVisible;
|
|
91
101
|
// real-idle: the state manager observed a genuine busy→idle transition.
|
|
92
|
-
fireAutoReport(sessionId, session, pendingReport, 'real-idle');
|
|
102
|
+
fireAutoReport(sessionId, session, pendingReport, 'real-idle', { idleEvidenceReliable });
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
// Fire TASK_DEAD_NO_REPORT when session dies with a pending report
|
|
@@ -129,6 +139,13 @@ app.use(express.json());
|
|
|
129
139
|
// Peer allowlist: comma-separated IPs/CIDRs in TELEPTY_PEER_ALLOWLIST env
|
|
130
140
|
const PEER_ALLOWLIST = (process.env.TELEPTY_PEER_ALLOWLIST || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
131
141
|
|
|
142
|
+
// #533 Phase 2 — peer-lane inject guardrail. The orchestrator sid(s) define the
|
|
143
|
+
// ORCH LANE (always allowed). Space-separated; default matches aigentry-orchestrator
|
|
144
|
+
// bin/ask.sh so both ends agree on "who is the orchestrator" from one config. If this
|
|
145
|
+
// resolves empty the guardrail fails OPEN (see classifyPeerLaneInject).
|
|
146
|
+
const ORCHESTRATOR_SIDS = (process.env.AIGENTRY_ORCHESTRATOR_SIDS || 'orchestrator aigentry-orchestrator-claude')
|
|
147
|
+
.split(/\s+/).map(s => s.trim()).filter(Boolean);
|
|
148
|
+
|
|
132
149
|
// Cross-machine bus relay: forward bus events to peer daemons
|
|
133
150
|
const relayToPeers = createPeerRelay({
|
|
134
151
|
relayPeers: relayPeersFromEnv(process.env),
|
|
@@ -307,11 +324,23 @@ function fireAutoReport(targetId, targetSession, pendingReport, trigger, deps =
|
|
|
307
324
|
pendingReport.submitConfirmedAt ||
|
|
308
325
|
(pendingReport.submitConfirm && pendingReport.submitConfirm.accepted === true)
|
|
309
326
|
);
|
|
327
|
+
// #545: a `real-idle` flip with weak evidence (no OSC133 REPL-done mark / injected body still
|
|
328
|
+
// visible in the PTY outputRing) must NOT be reported TASK_COMPLETE — a still-busy worker that
|
|
329
|
+
// merely paused output gets the honest TASK_IDLE_UNCONFIRMED. The caller (onTransition) sets
|
|
330
|
+
// deps.idleEvidenceReliable; === false forces the downgrade. Scoped to submitExpected (the #545
|
|
331
|
+
// symptom — a submit-confirmed worker still thinking), consistent with the BUG-B confirm gate;
|
|
332
|
+
// plain non-submit injects keep their existing floor-based completion. Absent flag / other
|
|
333
|
+
// triggers preserve prior behavior.
|
|
334
|
+
const idleEvidenceUnreliable = trigger === 'real-idle'
|
|
335
|
+
&& pendingReport.submitExpected
|
|
336
|
+
&& deps.idleEvidenceReliable === false;
|
|
310
337
|
const confirmed = trigger === 'ready-signal' && pendingReport.submitExpected
|
|
311
338
|
? false
|
|
312
|
-
:
|
|
313
|
-
?
|
|
314
|
-
:
|
|
339
|
+
: idleEvidenceUnreliable
|
|
340
|
+
? false
|
|
341
|
+
: pendingReport.submitExpected
|
|
342
|
+
? strongSubmitConfirmed
|
|
343
|
+
: (elapsedNum >= AUTO_REPORT_MIN_REAL_SECONDS || hasSubmitEvidence);
|
|
315
344
|
const injTag = pendingReport.injectId ? ` inject=${pendingReport.injectId}` : '';
|
|
316
345
|
const reportMsg = confirmed
|
|
317
346
|
? `TASK_COMPLETE: ${targetId} is now idle after processing inject (${elapsed}s, via ${trigger}${injTag})`
|
|
@@ -346,6 +375,68 @@ function respondWithError(res, httpStatus, code, error, extra = {}) {
|
|
|
346
375
|
return res.status(httpStatus).json(buildErrorBody(code, error, extra));
|
|
347
376
|
}
|
|
348
377
|
|
|
378
|
+
// #533 Phase 2 — pure peer-lane inject policy verdict (self-contained; no parser
|
|
379
|
+
// dependency). The PEER LANE is sender ≠ orchestrator AND target ≠ orchestrator.
|
|
380
|
+
// On that lane the body MUST be a sanctioned compact-JSON envelope (the shape
|
|
381
|
+
// produced by aigentry-orchestrator bin/ask.sh build_envelope); anything else is
|
|
382
|
+
// out-of-policy peer→peer traffic (e.g. work-delegation) and is blocked.
|
|
383
|
+
// Returns { lane, decision, reason, kind, envelopePresent }:
|
|
384
|
+
// lane ∈ 'orchestrator' | 'peer' | 'disabled'
|
|
385
|
+
// decision ∈ 'allow' | 'block'
|
|
386
|
+
// kind ∈ 'ask-request' | 'ask-reply' | null
|
|
387
|
+
const PEER_INJECT_KINDS = new Set(['ask-request', 'ask-reply']);
|
|
388
|
+
|
|
389
|
+
function classifyPeerLaneInject({ from, to, prompt, orchestratorSids } = {}) {
|
|
390
|
+
const orchSet = Array.isArray(orchestratorSids) ? orchestratorSids : [];
|
|
391
|
+
// Fail-OPEN: with no known orchestrator sid we cannot tell the orch lane apart
|
|
392
|
+
// from the peer lane (every inject would look peer-lane), which would over-block
|
|
393
|
+
// legitimate orchestrator traffic and brick the mesh. Degrade to allow + warn;
|
|
394
|
+
// the Phase-1 orchestrator-side auditor still detects raw bypass (defense in depth).
|
|
395
|
+
if (orchSet.length === 0) {
|
|
396
|
+
return { lane: 'disabled', decision: 'allow', reason: 'orch-sid-unconfigured-fail-open', kind: null, envelopePresent: false };
|
|
397
|
+
}
|
|
398
|
+
// No sender → operator/CLI/multicast/broadcast, never peer-lane.
|
|
399
|
+
if (!from) {
|
|
400
|
+
return { lane: 'orchestrator', decision: 'allow', reason: 'no-sender', kind: null, envelopePresent: false };
|
|
401
|
+
}
|
|
402
|
+
// Orchestrator lane (either end is the orchestrator) → always allowed, untouched.
|
|
403
|
+
if (orchSet.includes(from) || orchSet.includes(to)) {
|
|
404
|
+
return { lane: 'orchestrator', decision: 'allow', reason: 'orch-lane', kind: null, envelopePresent: false };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Peer lane: require a sanctioned envelope on the first non-empty line.
|
|
408
|
+
let env = null;
|
|
409
|
+
try {
|
|
410
|
+
const firstLine = String(prompt || '').split(/\r?\n/).map(l => l.trim()).find(l => l.length > 0);
|
|
411
|
+
if (firstLine) {
|
|
412
|
+
const parsed = JSON.parse(firstLine);
|
|
413
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) env = parsed;
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
env = null;
|
|
417
|
+
}
|
|
418
|
+
if (!env) {
|
|
419
|
+
return { lane: 'peer', decision: 'block', reason: 'malformed-envelope', kind: null, envelopePresent: false };
|
|
420
|
+
}
|
|
421
|
+
if (!PEER_INJECT_KINDS.has(env.kind)) {
|
|
422
|
+
return { lane: 'peer', decision: 'block', reason: 'wrong-kind', kind: null, envelopePresent: true };
|
|
423
|
+
}
|
|
424
|
+
// Common required fields + per-kind payload (matches ask.sh build_envelope).
|
|
425
|
+
const nonEmptyStr = (v) => typeof v === 'string' && v.length > 0;
|
|
426
|
+
const baseOk = nonEmptyStr(env.from) && nonEmptyStr(env.to) && nonEmptyStr(env.thread_id) && Number.isInteger(env.round);
|
|
427
|
+
const payloadOk = env.kind === 'ask-request' ? nonEmptyStr(env.question) : nonEmptyStr(env.answer);
|
|
428
|
+
if (!baseOk || !payloadOk) {
|
|
429
|
+
return { lane: 'peer', decision: 'block', reason: 'invalid-field', kind: null, envelopePresent: true };
|
|
430
|
+
}
|
|
431
|
+
// Sender-consistency: the envelope's declared sender must match the inject's
|
|
432
|
+
// from (cheap anti-spoof). `to` is NOT cross-checked — the route resolves aliases,
|
|
433
|
+
// which would false-block legitimate aliased targets.
|
|
434
|
+
if (env.from !== from) {
|
|
435
|
+
return { lane: 'peer', decision: 'block', reason: 'from-mismatch', kind: null, envelopePresent: true };
|
|
436
|
+
}
|
|
437
|
+
return { lane: 'peer', decision: 'allow', reason: 'sanctioned-envelope', kind: env.kind, envelopePresent: true };
|
|
438
|
+
}
|
|
439
|
+
|
|
349
440
|
function normalizeNullableText(value) {
|
|
350
441
|
if (value === undefined || value === null) {
|
|
351
442
|
return null;
|
|
@@ -1188,9 +1279,10 @@ async function writeDataToSession(id, session, data) {
|
|
|
1188
1279
|
* 0x0D into the CLI's innermost node-pty. The former kitty `send-text` (P1) and
|
|
1189
1280
|
* `cmux send-key` (P2) branches were SURFACE ops on a flaky side channel (75×
|
|
1190
1281
|
* "Failed to write to socket" vs 0× for pty_cr in a 222k-line run; live
|
|
1191
|
-
* 2026-06-07 confirmed pty-only works 3/3). `submitViaCmux`/`sendViaKitty`
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1282
|
+
* 2026-06-07 confirmed pty-only works 3/3). The `submitViaCmux`/`sendViaKitty`
|
|
1283
|
+
* defs were since removed (#544/#546 — submit-all also migrated to PTY, leaving
|
|
1284
|
+
* zero cmux send-key in the submit path). See
|
|
1285
|
+
* docs/adr/2026-06-07-submit-via-pty-context-layer.md.
|
|
1194
1286
|
*
|
|
1195
1287
|
* Returns the strategy name ('pty_cr') or null on failure.
|
|
1196
1288
|
*/
|
|
@@ -2018,45 +2110,6 @@ function findKittyWindowId(socket, sessionId) {
|
|
|
2018
2110
|
return null;
|
|
2019
2111
|
}
|
|
2020
2112
|
|
|
2021
|
-
function sendViaKitty(sessionId, text) {
|
|
2022
|
-
const { execSync } = require('child_process');
|
|
2023
|
-
const socket = findKittySocket();
|
|
2024
|
-
if (!socket) return false;
|
|
2025
|
-
|
|
2026
|
-
const windowId = findKittyWindowId(socket, sessionId);
|
|
2027
|
-
if (!windowId) {
|
|
2028
|
-
console.error(`[KITTY] No window found for ${sessionId}`);
|
|
2029
|
-
return false;
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
try {
|
|
2033
|
-
// Split text and CR — send-text for both (send-key corrupts keyboard protocol)
|
|
2034
|
-
const hasCr = text.endsWith('\r') || text.endsWith('\n');
|
|
2035
|
-
const textOnly = hasCr ? text.slice(0, -1) : text;
|
|
2036
|
-
if (textOnly.length > 0) {
|
|
2037
|
-
const escaped = textOnly.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
|
|
2038
|
-
execSync(`kitty @ --to unix:${socket} send-text --match id:${windowId} '${escaped}'`, {
|
|
2039
|
-
timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
|
|
2040
|
-
});
|
|
2041
|
-
}
|
|
2042
|
-
if (hasCr) {
|
|
2043
|
-
// Delay before sending Return — only when text was sent in the same call
|
|
2044
|
-
// (when CR-only, text was already delivered via a different path)
|
|
2045
|
-
if (textOnly.length > 0) {
|
|
2046
|
-
execSync('sleep 0.5', { timeout: 2000 });
|
|
2047
|
-
}
|
|
2048
|
-
execSync(`kitty @ --to unix:${socket} send-text --match id:${windowId} $'\\r'`, {
|
|
2049
|
-
timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
2050
|
-
});
|
|
2051
|
-
}
|
|
2052
|
-
console.log(`[KITTY] Sent ${textOnly.length} chars${hasCr ? ' + Return' : ''} to ${sessionId} (window ${windowId})`);
|
|
2053
|
-
return true;
|
|
2054
|
-
} catch (err) {
|
|
2055
|
-
console.error(`[KITTY] Failed for ${sessionId}:`, err.message);
|
|
2056
|
-
return false;
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
2113
|
function submitViaOsascript(sessionId, keyCombo) {
|
|
2061
2114
|
const { execSync } = require('child_process');
|
|
2062
2115
|
const session = sessions[sessionId];
|
|
@@ -2107,22 +2160,6 @@ function submitViaOsascript(sessionId, keyCombo) {
|
|
|
2107
2160
|
}
|
|
2108
2161
|
}
|
|
2109
2162
|
|
|
2110
|
-
function submitViaCmux(sessionId) {
|
|
2111
|
-
const { execSync } = require('child_process');
|
|
2112
|
-
const session = sessions[sessionId];
|
|
2113
|
-
if (!session || !session.cmuxWorkspaceId) return false;
|
|
2114
|
-
try {
|
|
2115
|
-
execSync(`cmux send-key --workspace ${session.cmuxWorkspaceId} return`, {
|
|
2116
|
-
timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
|
|
2117
|
-
});
|
|
2118
|
-
console.log(`[SUBMIT] cmux send-key return for ${sessionId} (workspace ${session.cmuxWorkspaceId})`);
|
|
2119
|
-
return true;
|
|
2120
|
-
} catch (err) {
|
|
2121
|
-
console.error(`[SUBMIT] cmux send-key failed for ${sessionId}:`, err.message);
|
|
2122
|
-
return false;
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
2163
|
// POST /api/sessions/:id/submit — render-gated CLI-aware submit
|
|
2127
2164
|
//
|
|
2128
2165
|
// Default behavior (0.3.0+): wait for the target REPL to be ready (sessionStateManager
|
|
@@ -2419,27 +2456,22 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
|
|
|
2419
2456
|
return res.json(responseBody);
|
|
2420
2457
|
});
|
|
2421
2458
|
|
|
2422
|
-
//
|
|
2423
|
-
|
|
2459
|
+
// Submit Enter to every active session. #546: submit is a PTY/context op (bare 0x0D) for every
|
|
2460
|
+
// wrapped + spawned backend INCLUDING cmux — the path validated 3/3 live for per-session submit
|
|
2461
|
+
// (#544). The cmux `send-key --surface return` surface op is removed (ZERO cmux send-key);
|
|
2462
|
+
// osascript Cmd+Enter remains only for app-window sessions with no PTY bridge. Exported (pure
|
|
2463
|
+
// over the passed sessions map) so the dispatch is unit-testable without starting the daemon.
|
|
2464
|
+
function runSubmitAll(sessionsMap) {
|
|
2424
2465
|
const results = { successful: [], failed: [] };
|
|
2425
2466
|
|
|
2426
|
-
for (const [id, session] of Object.entries(
|
|
2467
|
+
for (const [id, session] of Object.entries(sessionsMap)) {
|
|
2427
2468
|
const strategy = getSubmitStrategy(session.command);
|
|
2428
2469
|
let success = false;
|
|
2429
2470
|
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
if (!success && session.backend === 'cmux' && session.cmuxWorkspaceId) {
|
|
2435
|
-
success = submitViaCmux(id);
|
|
2436
|
-
}
|
|
2437
|
-
if (!success) {
|
|
2438
|
-
if (strategy === 'pty_cr') {
|
|
2439
|
-
success = submitViaPty(session);
|
|
2440
|
-
} else if (strategy === 'osascript_cmd_enter') {
|
|
2441
|
-
success = submitViaOsascript(id, 'cmd_enter');
|
|
2442
|
-
}
|
|
2471
|
+
if (strategy === 'pty_cr') {
|
|
2472
|
+
success = submitViaPty(session);
|
|
2473
|
+
} else if (strategy === 'osascript_cmd_enter') {
|
|
2474
|
+
success = submitViaOsascript(id, 'cmd_enter');
|
|
2443
2475
|
}
|
|
2444
2476
|
|
|
2445
2477
|
if (success) {
|
|
@@ -2449,7 +2481,12 @@ app.post('/api/sessions/submit-all', (req, res) => {
|
|
|
2449
2481
|
}
|
|
2450
2482
|
}
|
|
2451
2483
|
|
|
2452
|
-
|
|
2484
|
+
return results;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// POST /api/sessions/submit-all — Submit all active sessions
|
|
2488
|
+
app.post('/api/sessions/submit-all', (req, res) => {
|
|
2489
|
+
res.json({ success: true, results: runSubmitAll(sessions) });
|
|
2453
2490
|
});
|
|
2454
2491
|
|
|
2455
2492
|
app.post('/api/sessions/:id/inject', async (req, res) => {
|
|
@@ -2467,6 +2504,32 @@ app.post('/api/sessions/:id/inject', async (req, res) => {
|
|
|
2467
2504
|
// Routing metadata stays in session/bus state, not in the visible prompt text.
|
|
2468
2505
|
const finalPrompt = prompt;
|
|
2469
2506
|
const inject_id = crypto.randomUUID();
|
|
2507
|
+
|
|
2508
|
+
// #533 Phase 2 — peer-lane inject guardrail (in-band hard block, before delivery).
|
|
2509
|
+
// Out-of-policy peer→peer injects (no sanctioned ask-request/ask-reply envelope)
|
|
2510
|
+
// are blocked here so raw work-delegation bypass is prevented, not just detected.
|
|
2511
|
+
// Orchestrator↔peer, broadcast/multicast (no `from`), and existing kinds are untouched.
|
|
2512
|
+
const peerVerdict = classifyPeerLaneInject({ from, to: requestedId, prompt, orchestratorSids: ORCHESTRATOR_SIDS });
|
|
2513
|
+
if (peerVerdict.decision === 'block') {
|
|
2514
|
+
broadcastSessionEvent('peer_inject_blocked', id, session, {
|
|
2515
|
+
extra: {
|
|
2516
|
+
target_agent: id,
|
|
2517
|
+
from: from || null,
|
|
2518
|
+
reason: peerVerdict.reason,
|
|
2519
|
+
attempted_kind: peerVerdict.kind,
|
|
2520
|
+
envelope_present: peerVerdict.envelopePresent,
|
|
2521
|
+
inject_id
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
console.warn(`[PEER-GUARD] blocked peer inject ${from} → ${id} (${peerVerdict.reason})`);
|
|
2525
|
+
return respondWithError(res, 403, 'PEER_INJECT_BLOCKED',
|
|
2526
|
+
'Peer-lane inject blocked: not a sanctioned ask-request/ask-reply envelope. Use bin/ask.sh.',
|
|
2527
|
+
{ reason: peerVerdict.reason, sanctioned_channel: 'bin/ask.sh' });
|
|
2528
|
+
}
|
|
2529
|
+
if (peerVerdict.lane === 'disabled') {
|
|
2530
|
+
console.warn('[PEER-GUARD] orchestrator sid unconfigured (AIGENTRY_ORCHESTRATOR_SIDS empty) — peer guardrail disabled (fail-open)');
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2470
2533
|
try {
|
|
2471
2534
|
const delivery = await deliverInjectionToSession(id, session, finalPrompt, {
|
|
2472
2535
|
noEnter: !!no_enter,
|
|
@@ -3528,9 +3591,11 @@ module.exports = {
|
|
|
3528
3591
|
forceSubmitDeliveredToSurface, // #544/#537/Bug B: PTY-native force-confirm (pty_cr = delivered)
|
|
3529
3592
|
terminalLevelSubmit, // #544: PTY-only submit path (pty_cr | null)
|
|
3530
3593
|
submitViaPty, // #544: bare-0x0D submit into the innermost node-pty
|
|
3594
|
+
runSubmitAll, // #546: submit-all via PTY for every backend (no cmux send-key)
|
|
3531
3595
|
failBootstrapQueueOnTimeout, // #31: actionable bootstrap-timeout queue flush
|
|
3532
3596
|
shouldApplyOwnerAliveFloor, // #29: owner-alive optimistic-floor decision (deps DI: isProcessRunning/...)
|
|
3533
3597
|
scheduleBootstrapPromptPoll, // #29: arms the floor timer (deps DI: setTimeout/...)
|
|
3534
3598
|
decideSurfaceGc, // #17: surface-liveness verdict→action (incl. INV-17 unknown→skip)
|
|
3535
3599
|
applySurfaceMismatchProbe, // surface_mismatched debounce + payload helper (deps DI: emit/clock)
|
|
3600
|
+
classifyPeerLaneInject, // #533 Phase 2: pure peer-lane inject policy verdict
|
|
3536
3601
|
};
|
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.7",
|
|
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/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/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/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 --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/",
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
42
|
"regen-fixtures": "node scripts/regen-snippet-fixtures.js"
|
|
43
43
|
},
|
package/session-state.js
CHANGED
|
@@ -356,8 +356,7 @@ class SessionStateMachine {
|
|
|
356
356
|
});
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
_tick() {
|
|
360
|
-
const now = Date.now();
|
|
359
|
+
_tick(now = Date.now()) {
|
|
361
360
|
const silenceMs = now - this._lastOutputAt;
|
|
362
361
|
|
|
363
362
|
// Don't override lifecycle states
|
|
@@ -389,6 +388,16 @@ class SessionStateMachine {
|
|
|
389
388
|
: '';
|
|
390
389
|
|
|
391
390
|
const hasOsc133 = this._lastOsc133At && (now - this._lastOsc133At) < this.config.idle_timeout_ms * 2;
|
|
391
|
+
|
|
392
|
+
// #545: a THINKING session that merely went quiet is STILL thinking. The claude TUI
|
|
393
|
+
// input-box glyph (›/❯) false-matches PROMPT_PATTERNS and pure silence flips at 0.6, so
|
|
394
|
+
// a weak signal would wrongly end thinking. Only a reliable REPL-done mark (OSC 133) may.
|
|
395
|
+
// WORKING is intentionally NOT guarded — a real shell prompt `$ ` legitimately settles
|
|
396
|
+
// working→idle (test 255); the daemon real-idle gate covers the residual WORKING case.
|
|
397
|
+
if (this._state === STATES.THINKING && !hasOsc133) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
392
401
|
const hasPrompt = this._matchesAny(lastLine, PROMPT_PATTERNS);
|
|
393
402
|
const confidence = hasOsc133 ? 0.95 : (hasPrompt ? 0.9 : 0.6);
|
|
394
403
|
|
package/terminal-backend.js
CHANGED
|
@@ -107,24 +107,6 @@ function cmuxSendText(sessionId, text) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
// Send enter key to a cmux surface
|
|
111
|
-
function cmuxSendEnter(sessionId) {
|
|
112
|
-
const surface = findSurface(sessionId);
|
|
113
|
-
if (!surface) return false;
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
execSync(`cmux send-key --surface ${surface} return`, {
|
|
117
|
-
timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
|
|
118
|
-
});
|
|
119
|
-
console.log(`[BACKEND] cmux send-key return to ${sessionId} (${surface})`);
|
|
120
|
-
return true;
|
|
121
|
-
} catch (err) {
|
|
122
|
-
console.error(`[BACKEND] cmux send-key failed for ${sessionId}:`, err.message);
|
|
123
|
-
surfaceCache.delete(sessionId);
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
110
|
// Invalidate cache for a session (e.g., when surface changes)
|
|
129
111
|
function invalidateCache(sessionId) {
|
|
130
112
|
surfaceCache.delete(sessionId);
|
|
@@ -534,7 +516,6 @@ module.exports = {
|
|
|
534
516
|
detectTerminal,
|
|
535
517
|
findSurface,
|
|
536
518
|
cmuxSendText,
|
|
537
|
-
cmuxSendEnter,
|
|
538
519
|
refreshSurfaceCache,
|
|
539
520
|
invalidateCache,
|
|
540
521
|
clearCache,
|