@dsiloed/silo-link 1.16.3 → 1.17.2
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/dist/config/jwt-generator.d.ts +19 -3
- package/dist/config/jwt-generator.d.ts.map +1 -1
- package/dist/config/jwt-generator.js +94 -17
- package/dist/config/jwt-generator.js.map +1 -1
- package/dist/core/base-tmux-launcher.d.ts +51 -1
- package/dist/core/base-tmux-launcher.d.ts.map +1 -1
- package/dist/core/base-tmux-launcher.js +168 -1
- package/dist/core/base-tmux-launcher.js.map +1 -1
- package/dist/core/bridge.d.ts.map +1 -1
- package/dist/core/bridge.js +6 -1
- package/dist/core/bridge.js.map +1 -1
- package/dist/core/claude-launcher.d.ts +10 -0
- package/dist/core/claude-launcher.d.ts.map +1 -1
- package/dist/core/claude-launcher.js +40 -3
- package/dist/core/claude-launcher.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/core/baseline-verify.d.ts +0 -54
- package/dist/core/baseline-verify.d.ts.map +0 -1
- package/dist/core/baseline-verify.js +0 -164
- package/dist/core/baseline-verify.js.map +0 -1
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import type { SiloLinkConfig } from '../types/index.js';
|
|
2
|
+
/** Decode the `exp` claim (as epoch ms) from a JWT, or null if absent/unparseable. */
|
|
3
|
+
export declare function decodeJwtExpMs(token: string | undefined): number | null;
|
|
2
4
|
export declare class JwtGenerator {
|
|
3
5
|
private config;
|
|
4
6
|
private readonly env?;
|
|
5
7
|
private timer;
|
|
8
|
+
private onRefresh;
|
|
9
|
+
private onFatal;
|
|
10
|
+
private consecutiveFailures;
|
|
6
11
|
constructor(config: SiloLinkConfig, env?: string);
|
|
7
12
|
generate(): string;
|
|
8
13
|
getToken(): string;
|
|
9
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Start the refresh loop. `onFatal` is invoked when the token is expired and
|
|
16
|
+
* therefore unrefreshable — the caller should surface the error and exit
|
|
17
|
+
* rather than keep running disconnected. Defaults to console.error +
|
|
18
|
+
* process.exit(1) so a daemon can never silently zombie.
|
|
19
|
+
*/
|
|
20
|
+
startAutoRefresh(onRefresh: (token: string) => void, onFatal?: (reason: string) => void): void;
|
|
10
21
|
stopAutoRefresh(): void;
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
22
|
+
/** ms until the token should be refreshed: exp - margin, clamped to
|
|
23
|
+
* [1s, REFRESH_INTERVAL_MS]. Tokens without exp use the max interval. */
|
|
24
|
+
private delayUntilRefresh;
|
|
25
|
+
private schedule;
|
|
26
|
+
/** One refresh attempt. Success → persist + broadcast + schedule the next
|
|
27
|
+
* exp-aware refresh. Transient failure → backoff retry. Expired token →
|
|
28
|
+
* fatal (refresh can never succeed once exp has passed). */
|
|
13
29
|
private refresh;
|
|
14
30
|
}
|
|
15
31
|
//# sourceMappingURL=jwt-generator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt-generator.d.ts","sourceRoot":"","sources":["../../src/config/jwt-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"jwt-generator.d.ts","sourceRoot":"","sources":["../../src/config/jwt-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAqCxD,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAYvE;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAS;IAC9B,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,SAAS,CAA0C;IAC3D,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,mBAAmB,CAAK;gBAEpB,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,MAAM;IAKhD,QAAQ,IAAI,MAAM;IAIlB,QAAQ,IAAI,MAAM;IAUlB;;;;;OAKG;IACH,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAU9F,eAAe,IAAI,IAAI;IASvB;8EAC0E;IAC1E,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,QAAQ;IAOhB;;iEAE6D;YAC/C,OAAO;CAgCtB"}
|
|
@@ -1,23 +1,58 @@
|
|
|
1
1
|
import { refreshToken, saveConfig } from './config-manager.js';
|
|
2
2
|
/**
|
|
3
3
|
* Token store for the JWT obtained at `silolink config` time. The server issues
|
|
4
|
-
* a short-lived access token, so the daemon
|
|
4
|
+
* a short-lived access token, so the daemon refreshes it against
|
|
5
5
|
* POST /api/v1/auth/refresh — persisting the new token to the config file and
|
|
6
|
-
* notifying the live bridge — well before it can expire.
|
|
7
|
-
*
|
|
6
|
+
* notifying the live bridge — well before it can expire.
|
|
7
|
+
*
|
|
8
|
+
* Scheduling is exp-aware (2026-07-12, v1.16.4): the next refresh fires at
|
|
9
|
+
* `exp - REFRESH_MARGIN_MS`, capped at REFRESH_INTERVAL_MS. A blind interval
|
|
10
|
+
* zombied the personal daemon for 2 days: it was restarted with a token that
|
|
11
|
+
* had ~10h of life left, the first 12h tick landed after expiry, and
|
|
12
|
+
* /auth/refresh rejects expired tokens — so every subsequent attempt failed
|
|
13
|
+
* forever while the process looked alive.
|
|
14
|
+
*
|
|
15
|
+
* Failure handling follows from the same incident:
|
|
16
|
+
* - transient failure (network, 5xx) → retry on a 30s→15m backoff, not the
|
|
17
|
+
* next 12h tick;
|
|
18
|
+
* - token already past exp → unrecoverable (refresh can never succeed), so
|
|
19
|
+
* report fatal instead of idling disconnected. The daemon exits loudly and
|
|
20
|
+
* the operator re-runs `silolink config`.
|
|
8
21
|
*
|
|
9
22
|
* The class keeps the original `JwtGenerator` shape (generate / getToken /
|
|
10
23
|
* startAutoRefresh / stopAutoRefresh) so the rest of the bridge can
|
|
11
24
|
* continue to call into it unchanged.
|
|
12
25
|
*/
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// via SILOLINK_JWT_REFRESH_MS (mainly for tests).
|
|
26
|
+
// Longest we ever go between refreshes, even for long-lived tokens.
|
|
27
|
+
// Overridable via SILOLINK_JWT_REFRESH_MS (mainly for tests).
|
|
16
28
|
const REFRESH_INTERVAL_MS = Number(process.env.SILOLINK_JWT_REFRESH_MS) || 12 * 60 * 60 * 1000;
|
|
29
|
+
// Refresh no later than this long before the token expires.
|
|
30
|
+
const REFRESH_MARGIN_MS = Number(process.env.SILOLINK_JWT_REFRESH_MARGIN_MS) || 60 * 60 * 1000;
|
|
31
|
+
// Transient-failure retry backoff bounds.
|
|
32
|
+
const MIN_RETRY_MS = 30 * 1000;
|
|
33
|
+
const MAX_RETRY_MS = 15 * 60 * 1000;
|
|
34
|
+
/** Decode the `exp` claim (as epoch ms) from a JWT, or null if absent/unparseable. */
|
|
35
|
+
export function decodeJwtExpMs(token) {
|
|
36
|
+
if (!token)
|
|
37
|
+
return null;
|
|
38
|
+
const payload = token.split('.')[1];
|
|
39
|
+
if (!payload)
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
43
|
+
return typeof claims.exp === 'number' ? claims.exp * 1000 : null;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
17
49
|
export class JwtGenerator {
|
|
18
50
|
config;
|
|
19
51
|
env;
|
|
20
52
|
timer = null;
|
|
53
|
+
onRefresh = null;
|
|
54
|
+
onFatal = null;
|
|
55
|
+
consecutiveFailures = 0;
|
|
21
56
|
constructor(config, env) {
|
|
22
57
|
this.config = config;
|
|
23
58
|
this.env = env;
|
|
@@ -32,23 +67,51 @@ export class JwtGenerator {
|
|
|
32
67
|
}
|
|
33
68
|
return token;
|
|
34
69
|
}
|
|
35
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Start the refresh loop. `onFatal` is invoked when the token is expired and
|
|
72
|
+
* therefore unrefreshable — the caller should surface the error and exit
|
|
73
|
+
* rather than keep running disconnected. Defaults to console.error +
|
|
74
|
+
* process.exit(1) so a daemon can never silently zombie.
|
|
75
|
+
*/
|
|
76
|
+
startAutoRefresh(onRefresh, onFatal) {
|
|
36
77
|
if (this.timer)
|
|
37
78
|
return;
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
79
|
+
this.onRefresh = onRefresh;
|
|
80
|
+
this.onFatal = onFatal ?? ((reason) => {
|
|
81
|
+
console.error(`FATAL: ${reason}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|
|
84
|
+
this.schedule(this.delayUntilRefresh());
|
|
42
85
|
}
|
|
43
86
|
stopAutoRefresh() {
|
|
44
87
|
if (this.timer) {
|
|
45
|
-
|
|
88
|
+
clearTimeout(this.timer);
|
|
46
89
|
this.timer = null;
|
|
47
90
|
}
|
|
91
|
+
this.onRefresh = null;
|
|
92
|
+
this.onFatal = null;
|
|
93
|
+
}
|
|
94
|
+
/** ms until the token should be refreshed: exp - margin, clamped to
|
|
95
|
+
* [1s, REFRESH_INTERVAL_MS]. Tokens without exp use the max interval. */
|
|
96
|
+
delayUntilRefresh() {
|
|
97
|
+
const expMs = decodeJwtExpMs(this.config.jwt);
|
|
98
|
+
if (expMs === null)
|
|
99
|
+
return REFRESH_INTERVAL_MS;
|
|
100
|
+
const untilMargin = expMs - Date.now() - REFRESH_MARGIN_MS;
|
|
101
|
+
return Math.min(Math.max(untilMargin, 1000), REFRESH_INTERVAL_MS);
|
|
102
|
+
}
|
|
103
|
+
schedule(delayMs) {
|
|
104
|
+
if (this.timer)
|
|
105
|
+
clearTimeout(this.timer);
|
|
106
|
+
this.timer = setTimeout(() => { void this.refresh(); }, delayMs);
|
|
107
|
+
// Don't let the timer keep the event loop alive on its own.
|
|
108
|
+
if (this.timer.unref)
|
|
109
|
+
this.timer.unref();
|
|
48
110
|
}
|
|
49
|
-
/** One refresh attempt.
|
|
50
|
-
*
|
|
51
|
-
|
|
111
|
+
/** One refresh attempt. Success → persist + broadcast + schedule the next
|
|
112
|
+
* exp-aware refresh. Transient failure → backoff retry. Expired token →
|
|
113
|
+
* fatal (refresh can never succeed once exp has passed). */
|
|
114
|
+
async refresh() {
|
|
52
115
|
try {
|
|
53
116
|
const { token } = await refreshToken({
|
|
54
117
|
host: this.config.host,
|
|
@@ -57,10 +120,24 @@ export class JwtGenerator {
|
|
|
57
120
|
});
|
|
58
121
|
this.config.jwt = token;
|
|
59
122
|
saveConfig(this.config, this.env);
|
|
60
|
-
|
|
123
|
+
this.consecutiveFailures = 0;
|
|
124
|
+
this.onRefresh?.(token);
|
|
125
|
+
this.schedule(this.delayUntilRefresh());
|
|
61
126
|
}
|
|
62
127
|
catch (err) {
|
|
63
|
-
|
|
128
|
+
const expMs = decodeJwtExpMs(this.config.jwt);
|
|
129
|
+
if (expMs !== null && Date.now() > expMs) {
|
|
130
|
+
const fatal = this.onFatal;
|
|
131
|
+
this.stopAutoRefresh();
|
|
132
|
+
fatal?.(`JWT expired at ${new Date(expMs).toISOString()} and cannot be refreshed ` +
|
|
133
|
+
`(${err.message}). Run "silolink config${this.env ? ` --env ${this.env}` : ''}" ` +
|
|
134
|
+
'to obtain a fresh token, then restart the daemon.');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.consecutiveFailures += 1;
|
|
138
|
+
const backoff = Math.min(MIN_RETRY_MS * 2 ** (this.consecutiveFailures - 1), MAX_RETRY_MS);
|
|
139
|
+
console.warn(`JWT refresh failed (attempt ${this.consecutiveFailures}), retrying in ${Math.round(backoff / 1000)}s: ${err.message}`);
|
|
140
|
+
this.schedule(backoff);
|
|
64
141
|
}
|
|
65
142
|
}
|
|
66
143
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt-generator.js","sourceRoot":"","sources":["../../src/config/jwt-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAE/D
|
|
1
|
+
{"version":3,"file":"jwt-generator.js","sourceRoot":"","sources":["../../src/config/jwt-generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SiloLinkConfig, WorkspaceLaunchConfig } from '../types/index.js';
|
|
1
|
+
import type { Session, SiloLinkConfig, WorkspaceLaunchConfig } from '../types/index.js';
|
|
2
2
|
import type { SessionManager } from './session-manager.js';
|
|
3
3
|
import type { MessageQueue } from './message-queue.js';
|
|
4
4
|
import type { DSiloedClient } from '../api/dsiloed-client.js';
|
|
@@ -44,6 +44,13 @@ export declare abstract class BaseTmuxLauncher implements AgentLauncher {
|
|
|
44
44
|
}>;
|
|
45
45
|
protected static readonly PANE_SAMPLE_INTERVAL_MS = 4000;
|
|
46
46
|
protected static readonly FROZEN_THRESHOLD_MS: number;
|
|
47
|
+
protected promptStalls: Map<string, {
|
|
48
|
+
hash: string;
|
|
49
|
+
since: number;
|
|
50
|
+
notifiedAt: number | null;
|
|
51
|
+
}>;
|
|
52
|
+
protected static readonly PROMPT_STALL_THRESHOLD_MS: number;
|
|
53
|
+
protected static readonly PROMPT_STALL_RENOTIFY_MS: number;
|
|
47
54
|
protected lastSessionId: string | null;
|
|
48
55
|
protected lastSessionConversationId: number | null;
|
|
49
56
|
protected pendingTriggerMessage: string | null;
|
|
@@ -178,6 +185,14 @@ export declare abstract class BaseTmuxLauncher implements AgentLauncher {
|
|
|
178
185
|
*/
|
|
179
186
|
clearResumeId(conversationId: number): void;
|
|
180
187
|
launchOnMessage(conversationId?: number, triggerMessage?: string, llmAgentId?: number): Promise<boolean>;
|
|
188
|
+
/**
|
|
189
|
+
* Resolve the tmux session name for a conversation. Session.sessionName is
|
|
190
|
+
* the CONVERSATION name (see register-tools), NOT the tmux name — capturing
|
|
191
|
+
* a pane by it silently returns null, which had disabled the exec-20
|
|
192
|
+
* "actively working" kill suppressions in production and blinded
|
|
193
|
+
* prompt-stall detection (livefire conv 1904, 2026-07-13).
|
|
194
|
+
*/
|
|
195
|
+
protected tmuxNameForConversation(conversationId: number): string | null;
|
|
181
196
|
killByConversation(conversationId: number, reason?: string): void;
|
|
182
197
|
kill(): void;
|
|
183
198
|
/**
|
|
@@ -337,6 +352,41 @@ export declare abstract class BaseTmuxLauncher implements AgentLauncher {
|
|
|
337
352
|
protected updateUsageLedger(conversationId: number, tmuxSession: string, cwd?: string): void;
|
|
338
353
|
protected maybeReportLiveUsage(conversationId: number): void;
|
|
339
354
|
protected checkIdle(): Promise<void>;
|
|
355
|
+
/**
|
|
356
|
+
* Regexes matching an interactive dialog that only a HUMAN can answer
|
|
357
|
+
* (permission prompts, trust prompts, plan approvals). Base defaults cover
|
|
358
|
+
* the generic "Do you want to proceed?" shape; launchers override with their
|
|
359
|
+
* CLI's specific dialogs. Return [] to disable prompt-stall detection.
|
|
360
|
+
*/
|
|
361
|
+
protected humanPromptPatterns(): RegExp[];
|
|
362
|
+
/**
|
|
363
|
+
* Env vars carrying this CLI's credentials that must reach tmux panes even
|
|
364
|
+
* when the tmux server predates (or outlived) a properly-configured shell.
|
|
365
|
+
* Pushed into the server's global environment at session-create time.
|
|
366
|
+
*/
|
|
367
|
+
protected credentialEnvVars(): string[];
|
|
368
|
+
/**
|
|
369
|
+
* Detect sessions stuck on an interactive CLI prompt and notify a human.
|
|
370
|
+
*
|
|
371
|
+
* A stall is: the pane tail matches a humanPromptPatterns() dialog AND the
|
|
372
|
+
* substantive pane content stays byte-for-byte unchanged for
|
|
373
|
+
* PROMPT_STALL_THRESHOLD_MS. Neither signal alone is enough — dialog text
|
|
374
|
+
* scrolls past in history (match without freeze), and long tool runs freeze
|
|
375
|
+
* the pane without a dialog. Notified once per distinct stall (keyed on the
|
|
376
|
+
* frozen pane hash), re-notified every PROMPT_STALL_RENOTIFY_MS while the
|
|
377
|
+
* same stall persists. Regression target: Doug's video session 1643
|
|
378
|
+
* (2026-07-13) sat ~20 min on a dangerous-rm confirmation — which prompts
|
|
379
|
+
* even in bypass-permissions mode — and nothing upstream could see it.
|
|
380
|
+
*/
|
|
381
|
+
protected checkPromptStalls(sessions: Session[]): Promise<void>;
|
|
382
|
+
/**
|
|
383
|
+
* Tell a human about a prompt-stalled session: post to the session's
|
|
384
|
+
* DISPATCH conversation (where the originating human/agent actually reads)
|
|
385
|
+
* when the backend knows one, and always to the session conversation as the
|
|
386
|
+
* durable record. Best-effort — a notification failure must never break the
|
|
387
|
+
* idle loop.
|
|
388
|
+
*/
|
|
389
|
+
protected notifyPromptStall(session: Session, tmuxName: string, paneTail: string, stalledMs: number): Promise<void>;
|
|
340
390
|
/**
|
|
341
391
|
* Check whether a tmux pane shows the agent CLI genuinely PROGRESSING — used to
|
|
342
392
|
* suppress the idle/hung/zombie kill paths while it is legitimately mid-turn
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-tmux-launcher.d.ts","sourceRoot":"","sources":["../../src/core/base-tmux-launcher.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"base-tmux-launcher.d.ts","sourceRoot":"","sources":["../../src/core/base-tmux-launcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -46,6 +46,21 @@ export class BaseTmuxLauncher {
|
|
|
46
46
|
// turn / long-running tool emits some substantive output well inside this window,
|
|
47
47
|
// while a CLI hung on a dead MCP call stays frozen indefinitely.
|
|
48
48
|
static FROZEN_THRESHOLD_MS = 15 * 60 * 1000;
|
|
49
|
+
// Per-session prompt-stall tracking: tmux session name → { hash of the pane
|
|
50
|
+
// when a human-input prompt was first seen, when that hash was first seen,
|
|
51
|
+
// when we last notified }. A session whose pane BOTH matches a
|
|
52
|
+
// humanPromptPatterns() dialog AND stays byte-for-byte frozen past
|
|
53
|
+
// PROMPT_STALL_THRESHOLD_MS is stuck waiting on a human (permission dialog,
|
|
54
|
+
// trust prompt, plan approval) — the CLI will sit there forever and nothing
|
|
55
|
+
// upstream can see it (lastActivity only tracks MCP calls, and
|
|
56
|
+
// waiting_for_human status also fires on ordinary polling). Notify the
|
|
57
|
+
// dispatch + session conversations once per distinct stall.
|
|
58
|
+
promptStalls = new Map();
|
|
59
|
+
// How long a prompt dialog must stay frozen before we notify — long enough
|
|
60
|
+
// that a human actively working the session never triggers it.
|
|
61
|
+
static PROMPT_STALL_THRESHOLD_MS = 3 * 60 * 1000;
|
|
62
|
+
// Re-notify cadence while the SAME stall persists unanswered.
|
|
63
|
+
static PROMPT_STALL_RENOTIFY_MS = 30 * 60 * 1000;
|
|
49
64
|
lastSessionId = null;
|
|
50
65
|
// Which conversation lastSessionId belongs to. A pinned resume id replays ONE
|
|
51
66
|
// conversation's CLI context; multi-conversation daemons (personal) must never
|
|
@@ -445,6 +460,20 @@ export class BaseTmuxLauncher {
|
|
|
445
460
|
}
|
|
446
461
|
return this.launchInternal('inbound-message', conversationId);
|
|
447
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Resolve the tmux session name for a conversation. Session.sessionName is
|
|
465
|
+
* the CONVERSATION name (see register-tools), NOT the tmux name — capturing
|
|
466
|
+
* a pane by it silently returns null, which had disabled the exec-20
|
|
467
|
+
* "actively working" kill suppressions in production and blinded
|
|
468
|
+
* prompt-stall detection (livefire conv 1904, 2026-07-13).
|
|
469
|
+
*/
|
|
470
|
+
tmuxNameForConversation(conversationId) {
|
|
471
|
+
for (const [name, info] of this.tmuxSessions) {
|
|
472
|
+
if (info.conversationId === conversationId)
|
|
473
|
+
return name;
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
448
477
|
killByConversation(conversationId, reason = 'killByConversation') {
|
|
449
478
|
for (const [name, info] of this.tmuxSessions) {
|
|
450
479
|
if (info.conversationId === conversationId) {
|
|
@@ -826,6 +855,25 @@ export class BaseTmuxLauncher {
|
|
|
826
855
|
});
|
|
827
856
|
}
|
|
828
857
|
catch { /* best-effort cleanup */ }
|
|
858
|
+
// tmux panes inherit the SERVER's environment, not this daemon's — a tmux
|
|
859
|
+
// server (re)started from a credential-less shell strands every CC pane
|
|
860
|
+
// at a login screen (2026-07-13: a fleet restart left the server without
|
|
861
|
+
// CLAUDE_CODE_OAUTH_TOKEN and spawned sessions sat at "Not logged in").
|
|
862
|
+
// Push the daemon's own credential vars into the server's global
|
|
863
|
+
// environment before creating the session. set-environment keeps the
|
|
864
|
+
// token off argv (`ps` is world-readable; tmux command strings are not
|
|
865
|
+
// private) — that's why this doesn't ride the `env` prefix below. If no
|
|
866
|
+
// server exists yet, this fails harmlessly and the new-session spawn
|
|
867
|
+
// below starts one that inherits this daemon's full env anyway.
|
|
868
|
+
for (const key of this.credentialEnvVars()) {
|
|
869
|
+
const val = process.env[key];
|
|
870
|
+
if (!val)
|
|
871
|
+
continue;
|
|
872
|
+
try {
|
|
873
|
+
execFileSync('tmux', ['set-environment', '-g', key, val], { stdio: 'ignore' });
|
|
874
|
+
}
|
|
875
|
+
catch { /* no server yet — new-session inherits our env */ }
|
|
876
|
+
}
|
|
829
877
|
// Use tmux new-session -c to set working directory safely instead of
|
|
830
878
|
// interpolating into a shell string. This prevents shell injection via cwd/command.
|
|
831
879
|
//
|
|
@@ -1392,6 +1440,15 @@ export class BaseTmuxLauncher {
|
|
|
1392
1440
|
const now = Date.now();
|
|
1393
1441
|
if (sessions.length === 0)
|
|
1394
1442
|
return;
|
|
1443
|
+
// Prompt-stall detection runs BEFORE the startup-grace and all-idle gates:
|
|
1444
|
+
// a launch-time trust dialog stalls a brand-new session, and one active
|
|
1445
|
+
// session must not mask another one stuck on a permission prompt.
|
|
1446
|
+
try {
|
|
1447
|
+
await this.checkPromptStalls(sessions);
|
|
1448
|
+
}
|
|
1449
|
+
catch (err) {
|
|
1450
|
+
console.warn(`${this.getLogTag()} prompt-stall check failed: ${err}`);
|
|
1451
|
+
}
|
|
1395
1452
|
const startupGracePeriod = 90000;
|
|
1396
1453
|
if (sessions.some(s => now - s.createdAt.getTime() <= startupGracePeriod))
|
|
1397
1454
|
return;
|
|
@@ -1428,7 +1485,8 @@ export class BaseTmuxLauncher {
|
|
|
1428
1485
|
// pane is the tiebreaker: suppress the kill only while it shows genuine
|
|
1429
1486
|
// NEW output. A pane frozen past FROZEN_THRESHOLD_MS is wedged and gets
|
|
1430
1487
|
// killed despite any spinner animation.
|
|
1431
|
-
|
|
1488
|
+
const tmuxName = this.tmuxNameForConversation(s.conversationId);
|
|
1489
|
+
if (tmuxName && this.isActivelyThinking(tmuxName)) {
|
|
1432
1490
|
console.log(`${this.getLogTag()} conv ${s.conversationId} past idle-kill threshold but pane shows active work — suppressing idle-kill`);
|
|
1433
1491
|
continue;
|
|
1434
1492
|
}
|
|
@@ -1438,6 +1496,115 @@ export class BaseTmuxLauncher {
|
|
|
1438
1496
|
}
|
|
1439
1497
|
}
|
|
1440
1498
|
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Regexes matching an interactive dialog that only a HUMAN can answer
|
|
1501
|
+
* (permission prompts, trust prompts, plan approvals). Base defaults cover
|
|
1502
|
+
* the generic "Do you want to proceed?" shape; launchers override with their
|
|
1503
|
+
* CLI's specific dialogs. Return [] to disable prompt-stall detection.
|
|
1504
|
+
*/
|
|
1505
|
+
humanPromptPatterns() {
|
|
1506
|
+
return [/Do you want to proceed\?/i];
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Env vars carrying this CLI's credentials that must reach tmux panes even
|
|
1510
|
+
* when the tmux server predates (or outlived) a properly-configured shell.
|
|
1511
|
+
* Pushed into the server's global environment at session-create time.
|
|
1512
|
+
*/
|
|
1513
|
+
credentialEnvVars() {
|
|
1514
|
+
return [];
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Detect sessions stuck on an interactive CLI prompt and notify a human.
|
|
1518
|
+
*
|
|
1519
|
+
* A stall is: the pane tail matches a humanPromptPatterns() dialog AND the
|
|
1520
|
+
* substantive pane content stays byte-for-byte unchanged for
|
|
1521
|
+
* PROMPT_STALL_THRESHOLD_MS. Neither signal alone is enough — dialog text
|
|
1522
|
+
* scrolls past in history (match without freeze), and long tool runs freeze
|
|
1523
|
+
* the pane without a dialog. Notified once per distinct stall (keyed on the
|
|
1524
|
+
* frozen pane hash), re-notified every PROMPT_STALL_RENOTIFY_MS while the
|
|
1525
|
+
* same stall persists. Regression target: Doug's video session 1643
|
|
1526
|
+
* (2026-07-13) sat ~20 min on a dangerous-rm confirmation — which prompts
|
|
1527
|
+
* even in bypass-permissions mode — and nothing upstream could see it.
|
|
1528
|
+
*/
|
|
1529
|
+
async checkPromptStalls(sessions) {
|
|
1530
|
+
const patterns = this.humanPromptPatterns();
|
|
1531
|
+
if (patterns.length === 0)
|
|
1532
|
+
return;
|
|
1533
|
+
const now = Date.now();
|
|
1534
|
+
for (const s of sessions) {
|
|
1535
|
+
const tmuxName = this.tmuxNameForConversation(s.conversationId);
|
|
1536
|
+
if (!tmuxName)
|
|
1537
|
+
continue; // no pane under this daemon (e.g. externally-run CC)
|
|
1538
|
+
const pane = this.capturePaneTailForProgress(tmuxName);
|
|
1539
|
+
if (pane === null) {
|
|
1540
|
+
this.promptStalls.delete(tmuxName);
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
// Only the visible tail — a dialog scrolled into history is answered/gone.
|
|
1544
|
+
const tail = pane.split('\n').filter(l => l.trim() !== '').slice(-25).join('\n');
|
|
1545
|
+
if (!patterns.some(re => re.test(tail))) {
|
|
1546
|
+
this.promptStalls.delete(tmuxName);
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
const hash = this.hashSubstantivePane(tail);
|
|
1550
|
+
const prev = this.promptStalls.get(tmuxName);
|
|
1551
|
+
if (!prev || prev.hash !== hash) {
|
|
1552
|
+
// New dialog (or the pane moved) — start/restart the freeze clock.
|
|
1553
|
+
this.promptStalls.set(tmuxName, { hash, since: now, notifiedAt: null });
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
const stalledMs = now - prev.since;
|
|
1557
|
+
if (stalledMs < BaseTmuxLauncher.PROMPT_STALL_THRESHOLD_MS)
|
|
1558
|
+
continue;
|
|
1559
|
+
if (prev.notifiedAt !== null && now - prev.notifiedAt < BaseTmuxLauncher.PROMPT_STALL_RENOTIFY_MS)
|
|
1560
|
+
continue;
|
|
1561
|
+
prev.notifiedAt = now;
|
|
1562
|
+
await this.notifyPromptStall(s, tmuxName, tail, stalledMs);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Tell a human about a prompt-stalled session: post to the session's
|
|
1567
|
+
* DISPATCH conversation (where the originating human/agent actually reads)
|
|
1568
|
+
* when the backend knows one, and always to the session conversation as the
|
|
1569
|
+
* durable record. Best-effort — a notification failure must never break the
|
|
1570
|
+
* idle loop.
|
|
1571
|
+
*/
|
|
1572
|
+
async notifyPromptStall(session, tmuxName, paneTail, stalledMs) {
|
|
1573
|
+
if (!this.dsiloedClient)
|
|
1574
|
+
return;
|
|
1575
|
+
const mins = Math.round(stalledMs / 60000);
|
|
1576
|
+
const excerpt = paneTail.split('\n').slice(-12).join('\n');
|
|
1577
|
+
const body = `**[SiloLink]** ⚠️ Session \`${session.sessionName}\` (conv ${session.conversationId}, daemon \`${this.config.user_sub}\`) ` +
|
|
1578
|
+
`is STUCK on an interactive CLI prompt — pane frozen ~${mins} min. A human must answer it on the daemon host: ` +
|
|
1579
|
+
`\`tmux attach -t ${tmuxName}\` (or \`tmux send-keys -t ${tmuxName} <choice>\`).\n` +
|
|
1580
|
+
`\`\`\`\n${excerpt}\n\`\`\``;
|
|
1581
|
+
let dispatchConv = null;
|
|
1582
|
+
if (session.agentSessionId) {
|
|
1583
|
+
try {
|
|
1584
|
+
const agentSession = await this.dsiloedClient.getAgentSession(session.agentSessionId);
|
|
1585
|
+
dispatchConv = agentSession?.dispatch_conversation_id;
|
|
1586
|
+
}
|
|
1587
|
+
catch (err) {
|
|
1588
|
+
console.warn(`${this.getLogTag()} prompt-stall: could not resolve dispatch conversation: ${err}`);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
if (dispatchConv && dispatchConv !== session.conversationId) {
|
|
1592
|
+
try {
|
|
1593
|
+
await this.dsiloedClient.postMessage(dispatchConv, body);
|
|
1594
|
+
}
|
|
1595
|
+
catch (err) {
|
|
1596
|
+
console.warn(`${this.getLogTag()} prompt-stall: dispatch-conv notification failed: ${err}`);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
try {
|
|
1600
|
+
await this.dsiloedClient.postMessage(session.conversationId, body);
|
|
1601
|
+
}
|
|
1602
|
+
catch (err) {
|
|
1603
|
+
console.warn(`${this.getLogTag()} prompt-stall: session-conv notification failed: ${err}`);
|
|
1604
|
+
}
|
|
1605
|
+
console.log(`${this.getLogTag()} Prompt-stall notification for "${session.sessionName}" ` +
|
|
1606
|
+
`(frozen ${mins}min, dispatch conv: ${dispatchConv ?? 'n/a'})`);
|
|
1607
|
+
}
|
|
1441
1608
|
/**
|
|
1442
1609
|
* Check whether a tmux pane shows the agent CLI genuinely PROGRESSING — used to
|
|
1443
1610
|
* suppress the idle/hung/zombie kill paths while it is legitimately mid-turn
|