@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.
@@ -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
- startAutoRefresh(onRefresh: (token: string) => void): void;
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
- /** One refresh attempt. On success persist + broadcast the new token; on
12
- * failure warn and keep the current one so the daemon never crashes. */
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;AAoBxD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAS;IAC9B,OAAO,CAAC,KAAK,CAA+C;gBAEhD,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,MAAM;IAKhD,QAAQ,IAAI,MAAM;IAIlB,QAAQ,IAAI,MAAM;IAUlB,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAO1D,eAAe,IAAI,IAAI;IAOvB;6EACyE;YAC3D,OAAO;CActB"}
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 periodically refreshes it against
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. A refresh failure is
7
- * non-fatal: the current token is kept and the next tick retries.
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
- // Refresh well under the backend's ~24h access-token TTL so a transient refresh
14
- // failure has many retries before the token could actually expire. Overridable
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
- startAutoRefresh(onRefresh) {
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.timer = setInterval(() => { void this.refresh(onRefresh); }, REFRESH_INTERVAL_MS);
39
- // Don't let the interval keep the event loop alive on its own.
40
- if (this.timer.unref)
41
- this.timer.unref();
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
- clearInterval(this.timer);
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. On success persist + broadcast the new token; on
50
- * failure warn and keep the current one so the daemon never crashes. */
51
- async refresh(onRefresh) {
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
- onRefresh(token);
123
+ this.consecutiveFailures = 0;
124
+ this.onRefresh?.(token);
125
+ this.schedule(this.delayUntilRefresh());
61
126
  }
62
127
  catch (err) {
63
- console.warn(`JWT refresh failed, keeping current token: ${err.message}`);
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;;;;;;;;;;GAUG;AAEH,gFAAgF;AAChF,+EAA+E;AAC/E,kDAAkD;AAClD,MAAM,mBAAmB,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE/F,MAAM,OAAO,YAAY;IACf,MAAM,CAAiB;IACd,GAAG,CAAU;IACtB,KAAK,GAA0C,IAAI,CAAC;IAE5D,YAAY,MAAsB,EAAE,GAAY;QAC9C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC;IACzB,CAAC;IAED,QAAQ;QACN,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;QAC9B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CACb,uEAAuE,CACxE,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,gBAAgB,CAAC,SAAkC;QACjD,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAC;QACvF,+DAA+D;QAC/D,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IAC3C,CAAC;IAED,eAAe;QACb,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;IACH,CAAC;IAED;6EACyE;IACjE,KAAK,CAAC,OAAO,CAAC,SAAkC;QACtD,IAAI,CAAC;YACH,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,YAAY,CAAC;gBACnC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;gBACtB,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;gBAChC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG;aACrB,CAAC,CAAC;YACH,IAAI,CAAC,MAAM,CAAC,GAAG,GAAG,KAAK,CAAC;YACxB,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;YAClC,SAAS,CAAC,KAAK,CAAC,CAAC;QACnB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,8CAA+C,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACvF,CAAC;IACH,CAAC;CACF"}
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":"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"}
1
+ {"version":3,"file":"base-tmux-launcher.d.ts","sourceRoot":"","sources":["../../src/core/base-tmux-launcher.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AACxF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAE3E,UAAU,eAAe;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,IAAI,CAAC;IAIhB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,8BAAsB,gBAAiB,YAAW,aAAa;IAC7D,SAAS,CAAC,MAAM,EAAE,cAAc,CAAC;IACjC,SAAS,CAAC,cAAc,EAAE,cAAc,CAAC;IACzC,SAAS,CAAC,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9C,SAAS,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACpD,SAAS,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5C,SAAS,CAAC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAa;IACjE,SAAS,CAAC,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAa;IACjE,SAAS,CAAC,gBAAgB,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,GAAG,IAAI,CAAQ;IACzE,SAAS,CAAC,cAAc,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,GAAG,IAAI,CAAQ;IAEvE,SAAS,CAAC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAa;IAC7D,SAAS,CAAC,QAAQ,CAAC,6BAA6B,UAAW;IAC3D,SAAS,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAI1C,SAAS,CAAC,mBAAmB,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAa;IACjE,SAAS,CAAC,sBAAsB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAa;IAC1D,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,WAAW,SAAK;IAC1B,SAAS,CAAC,UAAU,SAAK;IACzB,SAAS,CAAC,eAAe,SAAK;IAC9B,SAAS,CAAC,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAa;IAMhE,SAAS,CAAC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAC7F;IAGZ,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,QAAQ;IAKzD,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,SAAkB;IAU/D,SAAS,CAAC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAa;IAG5G,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,SAAiB;IAEpE,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,SAAkB;IACpE,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAQ;IAK9C,SAAS,CAAC,yBAAyB,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC1D,SAAS,CAAC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAQ;IACtD,SAAS,CAAC,sBAAsB,EAAE,qBAAqB,GAAG,IAAI,CAAQ;IACtE,SAAS,CAAC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAQ;IACpD,SAAS,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAQ;IAClD,SAAS,CAAC,uBAAuB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAQ;gBAGxD,MAAM,EAAE,cAAc,EACtB,cAAc,EAAE,cAAc,EAC9B,aAAa,CAAC,EAAE,aAAa,EAC7B,YAAY,CAAC,EAAE,YAAY,EAC3B,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,WAAW,CAAC,EAAE,WAAW;IAW3B;;;;;;;;OAQG;IACH,SAAS,CAAC,uBAAuB,CAC/B,OAAO,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,EACpC,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,WAAW,GAAG,QAAsB,GAC1C,IAAI;IAYP,+DAA+D;IAC/D,SAAS,CAAC,QAAQ,CAAC,UAAU,IAAI,MAAM;IAEvC,uCAAuC;IACvC,SAAS,CAAC,QAAQ,CAAC,OAAO,IAAI,MAAM,EAAE;IAEtC,uFAAuF;IACvF,SAAS,CAAC,QAAQ,CAAC,iBAAiB,IAAI,MAAM,EAAE;IAEhD,sGAAsG;IACtG,SAAS,CAAC,iBAAiB,IAAI,MAAM;IAErC,0GAA0G;IAC1G,SAAS,CAAC,mBAAmB,IAAI,MAAM;IAEvC,qDAAqD;IACrD,SAAS,CAAC,QAAQ,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAEvD,sEAAsE;IACtE,SAAS,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAE9F,yDAAyD;IACzD,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,OAAO;IAE1C,+CAA+C;IAC/C,SAAS,CAAC,QAAQ,CAAC,mBAAmB,IAAI,MAAM;IAEhD,0CAA0C;IAC1C,SAAS,CAAC,QAAQ,CAAC,uBAAuB,IAAI,OAAO;IAErD,0CAA0C;IAC1C,SAAS,CAAC,QAAQ,CAAC,gBAAgB,IAAI,MAAM;IAE7C,mEAAmE;IACnE,SAAS,CAAC,kBAAkB,IAAI,MAAM;IAEtC;;;;;;;;uDAQmD;IACnD,SAAS,CAAC,kBAAkB,IAAI,OAAO;IAEvC,4CAA4C;IAC5C,SAAS,CAAC,QAAQ,CAAC,gBAAgB,IAAI,MAAM,GAAG,SAAS;IAEzD,uEAAuE;IACvE,SAAS,CAAC,QAAQ,CAAC,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAExD,wFAAwF;IACxF,SAAS,CAAC,QAAQ,CAAC,aAAa,IAAI,MAAM;IAE1C,iHAAiH;IACjH,SAAS,CAAC,QAAQ,CAAC,UAAU,IAAI,MAAM;IAEvC,sEAAsE;IACtE,SAAS,CAAC,QAAQ,CAAC,SAAS,IAAI,MAAM;IAEtC,oCAAoC;IACpC,SAAS,CAAC,QAAQ,CAAC,kBAAkB,IAAI,MAAM;IAE/C,kEAAkE;IAClE,SAAS,CAAC,QAAQ,CAAC,iBAAiB,CAClC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,GACvB;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE;IAIpC,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAkC1D,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAK5E,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;IAQtD,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAItC,YAAY,IAAI,IAAI;IAIpB,YAAY,IAAI,YAAY,EAAE;IAU9B,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAenE,KAAK,IAAI,OAAO;IAgBhB;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,SAAkB;IACjE,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,0BAA0B,SAAkB;IACtE,SAAS,CAAC,cAAc,IAAI,OAAO;IAoBnC,SAAS,IAAI,OAAO;IACpB,oBAAoB,IAAI,OAAO;IAC/B,cAAc,IAAI,OAAO;IAEzB,gBAAgB,IAAI,IAAI;IAyBxB;;;;;;OAMG;cACa,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IA6CvD,SAAS,CAAC,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IA2CpD,OAAO,IAAI,IAAI;IAQf,kBAAkB,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,GAAG,IAAI;IAC9D,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAClC,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAC3C,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,IAAI;IACjD,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI;IAMhE;;;;;;OAMG;IACH,SAAS,CAAC,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI;IAS5E;;;;;;;OAOG;IACH,aAAa,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAmBrC,eAAe,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkB9G;;;;;;OAMG;IACH,SAAS,CAAC,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAOxE,kBAAkB,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,GAAE,MAA6B,GAAG,IAAI;IAiCvF,IAAI,IAAI,IAAI;IAiBZ;;;;;;;;OAQG;IACH,SAAS,CAAC,eAAe,CACvB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GACpC,IAAI;IAiCP;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IA0CtD;;;;;OAKG;IACH,SAAS,CAAC,UAAU,CAClB,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,MAAM,GACnB;QAAE,WAAW,EAAE,OAAO,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAIrE;;;;;;;;OAQG;IACH,SAAS,CAAC,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAmBjD,SAAS,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoB3E,6BAA6B,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IASpE;;;;;;;;OAQG;IACH,SAAS,CAAC,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAsBxD;;;OAGG;IACH,SAAS,CAAC,mBAAmB,IAAI,MAAM,EAAE;IAIzC;;;;OAIG;IACH,SAAS,CAAC,qBAAqB,IAAI,MAAM,GAAG,IAAI;IAIhD;;;OAGG;IACH,oBAAoB,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IAKlE,cAAc,CAAC,MAAM,GAAE,MAAiB,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkV1F,SAAS,CAAC,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;IAMxD,SAAS,CAAC,iBAAiB,IAAI,IAAI;IA4CnC,OAAO,CAAC,kBAAkB;IAyD1B,SAAS,CAAC,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI;IAmChF;;;;;;;;;;OAUG;IACH,SAAS,CAAC,sBAAsB,CAC9B,IAAI,EAAE,MAAM,EACZ,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,GAC5B;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,KAAK,EAAE,UAAU,EAAE,CAAA;KAAE,GAAG,IAAI;IAInF;;;;;;;;OAQG;IACH,SAAS,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAIrD;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAqDvD,SAAS,CAAC,0BAA0B,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,UAAQ,GAAG,OAAO;IAoDpF;;;;OAIG;IACH,2BAA2B,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;IAI5D;;;;;;;;;;OAUG;IACG,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;IAuB/F;;;;;OAKG;IACG,iBAAiB,CAAC,SAAS,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAW1D,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI;IAO1D;;;;OAIG;IACH,SAAS,CAAC,iBAAiB,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI;IAkB5F,SAAS,CAAC,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;cAS5C,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IA+D1C;;;;;OAKG;IACH,SAAS,CAAC,mBAAmB,IAAI,MAAM,EAAE;IAIzC;;;;OAIG;IACH,SAAS,CAAC,iBAAiB,IAAI,MAAM,EAAE;IAIvC;;;;;;;;;;;;OAYG;cACa,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAsCrE;;;;;;OAMG;cACa,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCzH;;;;;;;;;;;;;;;;;;;OAmBG;IACH,SAAS,CAAC,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IA8B1D;;;;OAIG;IACH,SAAS,CAAC,0BAA0B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAWxE;;;;;;OAMG;IACH,SAAS,CAAC,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAcnD;;;;;OAKG;IACH,SAAS,CAAC,wBAAwB,IAAI,MAAM,EAAE;IAS9C,SAAS,CAAC,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;CAYnE"}
@@ -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
- if (this.isActivelyThinking(s.sessionName)) {
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