@aiwayds/dsh-tui-pi 0.28.0 → 1.0.0

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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The /resume repair confirmation dialog — shown when a picked session's
3
+ * log fails to load with a corrupt-log error (src/log-repair.ts
4
+ * `isCorruptLogError`). Repairing rewrites user data on disk, so it is
5
+ * NEVER done implicitly: this dialog is the explicit confirmation gate.
6
+ *
7
+ * Two options, in the spec's button order:
8
+ * 1. Repair & resume — run the guarded in-place repair, then re-enter the
9
+ * selected row's resume path.
10
+ * 2. Cancel — leave the log untouched; /resume reports the original error.
11
+ * ↑↓ moves the selection, `1`/`2` select directly, Enter confirms, Esc
12
+ * cancels — the exact keymap of the submit routing dialog (route-dialog.ts),
13
+ * whose pure-reducer split this file mirrors so the decision matrix stays
14
+ * unit-testable without a terminal. Framing/focus follow the shared overlay
15
+ * contract: PanelHost framing, close re-focuses the CURRENT editor instance
16
+ * through `restoreFocus`.
17
+ */
18
+ import { getKeybindings } from '@earendil-works/pi-tui';
19
+ import { PanelHost, panelThemeFns } from "./panels.js";
20
+ import { BOLD, RESET, ansiFg } from "./theme/index.js";
21
+ import { clipToWidth, wrapText } from "./text.js";
22
+ /** The two choices in display order; index 0 is preselected. */
23
+ export const REPAIR_CONFIRM_OPTIONS = [
24
+ { id: 'repair', title: 'Repair & resume', hint: 'rebuild the log in place, then enter the session' },
25
+ { id: 'cancel', title: 'Cancel', hint: 'leave the log untouched' },
26
+ ];
27
+ /** The fixed body copy (spec wording): what happened, what repair does, what is kept. */
28
+ export const REPAIR_CONFIRM_MESSAGE = 'Resume log is corrupted (likely from a historical double-writer). '
29
+ + 'Repair it in place? The original is kept as .corrupt-bak.';
30
+ /** Footer hint — hardcoded like every other panel footer (English-only). */
31
+ export const REPAIR_CONFIRM_FOOTER = '↑↓ select · 1/2 pick · Enter confirm · Esc cancel';
32
+ export function initialRepairConfirmState() {
33
+ return { selected: 0 };
34
+ }
35
+ /**
36
+ * Apply one raw key sequence to the dialog state. Unknown keys are no-ops;
37
+ * anything after a settle is ignored (single terminal outcome guard).
38
+ */
39
+ export function updateRepairConfirm(state, data) {
40
+ if (state.settled !== undefined)
41
+ return state;
42
+ const kb = getKeybindings();
43
+ if (kb.matches(data, 'tui.select.cancel'))
44
+ return { ...state, settled: 'cancel' };
45
+ if (kb.matches(data, 'tui.input.submit'))
46
+ return { ...state, settled: 'confirm' };
47
+ if (kb.matches(data, 'tui.select.up')) {
48
+ return { ...state, selected: Math.max(0, state.selected - 1) };
49
+ }
50
+ if (kb.matches(data, 'tui.select.down')) {
51
+ return { ...state, selected: Math.min(REPAIR_CONFIRM_OPTIONS.length - 1, state.selected + 1) };
52
+ }
53
+ // Digit direct-select (1-based): selects the row, Enter still confirms —
54
+ // same select-then-confirm split as the routing dialog.
55
+ const digit = /^([1-9])$/.exec(data);
56
+ if (digit !== null) {
57
+ const index = Number(digit[1]) - 1;
58
+ if (index < REPAIR_CONFIRM_OPTIONS.length)
59
+ return { ...state, selected: index };
60
+ }
61
+ return state;
62
+ }
63
+ /** Resolved dialog outcome: the chosen action, or undefined on cancel. */
64
+ export function repairConfirmOutcome(state) {
65
+ if (state.settled !== 'confirm')
66
+ return undefined;
67
+ return REPAIR_CONFIRM_OPTIONS[state.selected]?.id;
68
+ }
69
+ /**
70
+ * The framed overlay component. Renders the fixed body copy plus the two
71
+ * option rows; every key goes through {@link updateRepairConfirm}, and the
72
+ * first terminal key fires `onFinish` exactly once.
73
+ */
74
+ export class RepairConfirmPanel {
75
+ theme;
76
+ onFinish;
77
+ requestRenderFn;
78
+ state = initialRepairConfirmState();
79
+ constructor(theme, onFinish, requestRender) {
80
+ this.theme = theme;
81
+ this.onFinish = onFinish;
82
+ this.requestRenderFn = requestRender;
83
+ }
84
+ invalidate() { }
85
+ render(width) {
86
+ const fns = panelThemeFns(this.theme);
87
+ const wrap = Math.max(2, width - 2);
88
+ // Word-wrap the body FIRST, then paint (iron rule: width math runs on
89
+ // plain text, ANSI goes on after clipping).
90
+ const lines = [
91
+ fns.accent(BOLD + clipToWidth('● Corrupted resume log', wrap) + RESET),
92
+ ...wrapText(REPAIR_CONFIRM_MESSAGE, wrap).map(segment => fns.muted(clipToWidth(segment, wrap))),
93
+ '',
94
+ ];
95
+ for (let i = 0; i < REPAIR_CONFIRM_OPTIONS.length; i++) {
96
+ const option = REPAIR_CONFIRM_OPTIONS[i];
97
+ const marker = i === this.state.selected ? '▸' : ' ';
98
+ const row = clipToWidth(`${marker} ${i + 1}. ${option.title} — ${option.hint}`, wrap);
99
+ lines.push(i === this.state.selected
100
+ ? ansiFg(this.theme.palette.accent) + BOLD + row + RESET
101
+ : fns.muted(row));
102
+ }
103
+ lines.push('');
104
+ lines.push(fns.subtle(clipToWidth(REPAIR_CONFIRM_FOOTER, wrap)));
105
+ return lines;
106
+ }
107
+ handleInput(data) {
108
+ const previous = this.state;
109
+ this.state = updateRepairConfirm(this.state, data);
110
+ if (this.state === previous)
111
+ return;
112
+ if (this.state.settled === undefined) {
113
+ this.requestRenderFn();
114
+ return;
115
+ }
116
+ this.onFinish(repairConfirmOutcome(this.state));
117
+ }
118
+ }
119
+ /**
120
+ * Open the repair confirmation dialog for one corrupt resume target.
121
+ * Resolves `'repair'` when the user explicitly confirmed, `'cancelled'`
122
+ * otherwise (Esc, or an overlay that failed to mount — treated as cancel so
123
+ * a half-mounted dialog can never imply consent). Closing always hands
124
+ * focus back through `restoreFocus` before the promise settles.
125
+ */
126
+ export function openRepairConfirmDialog(tui, theme, restoreFocus) {
127
+ return new Promise(resolve => {
128
+ let settled = false;
129
+ const settle = (outcome) => {
130
+ if (settled)
131
+ return;
132
+ settled = true;
133
+ resolve(outcome);
134
+ };
135
+ // A half-mounted overlay must not strand the keyboard: PanelHost's
136
+ // onError closes + calls restoreFocus, then we settle as cancelled.
137
+ const host = new PanelHost(tui, theme, () => {
138
+ restoreFocus();
139
+ settle('cancelled');
140
+ });
141
+ const finish = (outcome) => {
142
+ host.close();
143
+ restoreFocus();
144
+ settle(outcome === 'repair' ? 'repair' : 'cancelled');
145
+ };
146
+ const panel = new RepairConfirmPanel(theme, outcome => finish(outcome), () => tui.requestRender());
147
+ // maxHeight is a hard slice in pi-tui: ~9 content rows + 5 frame rows
148
+ // must fit or the bottom border (and footer) is cut. 75% of the 24-row
149
+ // e2e floor is 18 — the same headroom the /resume picker reserves.
150
+ host.open(panel, '70%', '75%');
151
+ });
152
+ }
153
+ //# sourceMappingURL=repair-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repair-dialog.js","sourceRoot":"","sources":["../src/repair-dialog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,cAAc,EAA4B,MAAM,wBAAwB,CAAA;AACjF,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAiB,MAAM,kBAAkB,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEjD,gEAAgE;AAChE,MAAM,CAAC,MAAM,sBAAsB,GAA4E;IAC7G,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,iBAAiB,EAAE,IAAI,EAAE,kDAAkD,EAAE;IACpG,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,yBAAyB,EAAE;CACnE,CAAA;AAED,yFAAyF;AACzF,MAAM,CAAC,MAAM,sBAAsB,GACjC,oEAAoE;MAClE,2DAA2D,CAAA;AAE/D,4EAA4E;AAC5E,MAAM,CAAC,MAAM,qBAAqB,GAAG,mDAAmD,CAAA;AAYxF,MAAM,UAAU,yBAAyB;IACvC,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAA;AACxB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAyB,EAAE,IAAY;IACzE,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IAC7C,MAAM,EAAE,GAAG,cAAc,EAAE,CAAA;IAC3B,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,mBAAmB,CAAC;QAAE,OAAO,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAA;IACjF,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,kBAAkB,CAAC;QAAE,OAAO,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;IACjF,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,eAAe,CAAC,EAAE,CAAC;QACtC,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAA;IAChE,CAAC;IACD,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,iBAAiB,CAAC,EAAE,CAAC;QACxC,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,sBAAsB,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAA;IAChG,CAAC;IACD,yEAAyE;IACzE,wDAAwD;IACxD,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACpC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QAClC,IAAI,KAAK,GAAG,sBAAsB,CAAC,MAAM;YAAE,OAAO,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;IACjF,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,oBAAoB,CAAC,KAAyB;IAC5D,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACjD,OAAO,sBAAsB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAA;AACnD,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,kBAAkB;IACZ,KAAK,CAAU;IACf,QAAQ,CAAoD;IAC5D,eAAe,CAAY;IACpC,KAAK,GAAuB,yBAAyB,EAAE,CAAA;IAE/D,YACE,KAAe,EACf,QAA4D,EAC5D,aAAyB;QAEzB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,eAAe,GAAG,aAAa,CAAA;IACtC,CAAC;IAED,UAAU,KAAU,CAAC;IAErB,MAAM,CAAC,KAAa;QAClB,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAA;QACnC,sEAAsE;QACtE,4CAA4C;QAC5C,MAAM,KAAK,GAAa;YACtB,GAAG,CAAC,MAAM,CAAC,IAAI,GAAG,WAAW,CAAC,wBAAwB,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC;YACtE,GAAG,QAAQ,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;YAC/F,EAAE;SACH,CAAA;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,sBAAsB,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvD,MAAM,MAAM,GAAG,sBAAsB,CAAC,CAAC,CAAE,CAAA;YACzC,MAAM,MAAM,GAAG,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;YACpD,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,MAAM,CAAC,KAAK,MAAM,MAAM,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,CAAA;YACrF,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ;gBAClC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,GAAG,GAAG,KAAK;gBACxD,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;QACrB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACd,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,qBAAqB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;QAChE,OAAO,KAAK,CAAA;IACd,CAAC;IAED,WAAW,CAAC,IAAY;QACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAA;QAC3B,IAAI,CAAC,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;QAClD,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAM;QACnC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,eAAe,EAAE,CAAA;YACtB,OAAM;QACR,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;IACjD,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CACrC,GAAQ,EACR,KAAe,EACf,YAAwB;IAExB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;QAC3B,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,MAAM,MAAM,GAAG,CAAC,OAA+B,EAAQ,EAAE;YACvD,IAAI,OAAO;gBAAE,OAAM;YACnB,OAAO,GAAG,IAAI,CAAA;YACd,OAAO,CAAC,OAAO,CAAC,CAAA;QAClB,CAAC,CAAA;QACD,mEAAmE;QACnE,oEAAoE;QACpE,MAAM,IAAI,GAAG,IAAI,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE;YAC1C,YAAY,EAAE,CAAA;YACd,MAAM,CAAC,WAAW,CAAC,CAAA;QACrB,CAAC,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,CAAC,OAAwC,EAAQ,EAAE;YAChE,IAAI,CAAC,KAAK,EAAE,CAAA;YACZ,YAAY,EAAE,CAAA;YACd,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;QACvD,CAAC,CAAA;QACD,MAAM,KAAK,GAAG,IAAI,kBAAkB,CAClC,KAAK,EACL,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,EAC1B,GAAG,EAAE,CAAC,GAAG,CAAC,aAAa,EAAE,CAC1B,CAAA;QACD,sEAAsE;QACtE,uEAAuE;QACvE,mEAAmE;QACnE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IAChC,CAAC,CAAC,CAAA;AACJ,CAAC"}
package/lib/session.d.ts CHANGED
@@ -19,6 +19,13 @@ export interface BridgeCallbacks {
19
19
  onEvent(event: SessionEvent): void;
20
20
  /** Whole-agent lifecycle transition. */
21
21
  onStatus(status: 'idle' | 'running'): void;
22
+ /**
23
+ * Watched remote session is idle and queued follow-ups await: the bridge
24
+ * already took the writer lock; rebind locally (like a manual /resume)
25
+ * then drain takePendingRemoteFollowups(). Throw ⇒ back to watching;
26
+ * the poll retries. Absent ⇒ queuing still works, waits for manual resume.
27
+ */
28
+ onRemotePromotable?(sessionId: string): Promise<void>;
22
29
  /** Live snapshot of tracked subagent (child session) rows, on any change. */
23
30
  onLive(agents: readonly AgentView[]): void;
24
31
  /**
@@ -58,6 +65,30 @@ export declare class DshSessionBridge {
58
65
  private resuming;
59
66
  /** Target id of the in-flight resume — see `resume()`/`getResumingSessionId()`. */
60
67
  private resumeTargetId;
68
+ /**
69
+ * Session dirs whose cross-process writer lock THIS bridge established via
70
+ * a cold arm. Release is bound to the owning handle's disposal
71
+ * (`ownWithLock`), so the set itself only tracks double-release safety.
72
+ */
73
+ private readonly heldLockDirs;
74
+ /** Active cross-process read-only view (see watchRemote). */
75
+ private remoteTail;
76
+ /** Session watched read-only right now. */
77
+ private remoteWatchId;
78
+ /** Follow-ups typed while watching — sent once promotion wins the lock. */
79
+ private readonly remoteFollowups;
80
+ private promotingRemote;
81
+ private promoteTimer;
82
+ private idleReleaseTimer;
83
+ /** Writer lock taken for the CURRENTLY bound local agent (see drive arms). */
84
+ private driveLock;
85
+ /** Test seam + tuning for the watcher (interval/decoder injection). */
86
+ private readonly remoteTailOptions;
87
+ private readonly idleReleaseDelayMs;
88
+ private readonly promoteIntervalMs;
89
+ /** Serializes watch-lifecycle ops against the promotion poll. */
90
+ private opChain;
91
+ private runExclusive;
61
92
  private readonly disposers;
62
93
  private sessionId;
63
94
  /** Agent preset id to pass to `meta.agentPreset` on the next `createSession`. */
@@ -153,7 +184,14 @@ export declare class DshSessionBridge {
153
184
  private readonly trackedSessions;
154
185
  /** `${runId}:${seq}` → childId, for `tool-workflow/agent-end` pairing. */
155
186
  private readonly runSeqToChild;
156
- constructor(ctx: Context, callbacks: BridgeCallbacks);
187
+ constructor(ctx: Context, callbacks: BridgeCallbacks, options?: {
188
+ remoteTailOptions?: {
189
+ intervalMs?: number;
190
+ decode?(file: string): Promise<string>;
191
+ };
192
+ idleReleaseDelayMs?: number;
193
+ promoteIntervalMs?: number;
194
+ });
157
195
  /**
158
196
  * Re-derive each tracked child's round count from its own session log
159
197
  * (`ctx.sessions`), the authoritative append-only facts. The firehose
@@ -198,6 +236,38 @@ export declare class DshSessionBridge {
198
236
  isChildLogTruncated(childId: string): boolean;
199
237
  /** Queue one user prompt, creating the session lazily on first use. */
200
238
  prompt(text: string): Promise<void>;
239
+ /** While a remote watch is active input QUEUES instead of driving. */
240
+ isReadOnlyView(): boolean;
241
+ /** Follow-ups typed while watching, oldest first; takes them all. */
242
+ takePendingRemoteFollowups(): string[];
243
+ /**
244
+ * READ-ONLY cross-process view: another dsh process drives this session
245
+ * (the single-writer guard refused our cold resume), so sync DISPLAY from
246
+ * its persisted log instead — poll-decode, emit durable events through
247
+ * the same render pipeline a live session uses. Contract: latency ≈ poll
248
+ * interval, streaming detail omitted, every turn's final assistant
249
+ * message arrives. Replaces any prior binding; ends via stopRemoteWatch
250
+ * on dispose or the next resume.
251
+ */
252
+ watchRemote(sessionId: string): Promise<void>;
253
+ private watchRemoteInner;
254
+ private stopRemoteWatch;
255
+ /**
256
+ * Contract "idle releases the write; next input takes it": when OUR agent
257
+ * settles with nothing pending locally (own re-wake included), drop the
258
+ * writer lock so another surface can take over. Delayed once and
259
+ * re-checked because upstream may latch idle only to immediately replay
260
+ * a queued wake as a new turn.
261
+ */
262
+ private scheduleIdleRelease;
263
+ /**
264
+ * Promote poll tick: viewer + queued follow-ups + watched driver idle →
265
+ * race for the writer lock; winning hands to onRemotePromotable (UI
266
+ * rebuild via resume + queue drain), losing retries next tick. All async
267
+ * sequencing stays in THIS loop — never inside event callbacks.
268
+ */
269
+ private maybePromoteRemote;
270
+ private maybePromoteRemoteInner;
201
271
  /** The live agent, creating the session lazily when needed. */
202
272
  ensureAgent(): Promise<Agent>;
203
273
  /** The live agent when a session already exists (no creation side effect). */
@@ -239,6 +309,31 @@ export declare class DshSessionBridge {
239
309
  * an empty `onLive` is emitted so the renderer drops the agents block.
240
310
  */
241
311
  detachCurrent(): Promise<void>;
312
+ /**
313
+ * Historical cwd for a session, from the persistence header list — the
314
+ * log lives under THAT project key. All access best-effort: an absent
315
+ * service or odd host yields undefined and the arm stays UNGUARDED
316
+ * (fail-open, never locks a decoy), never breaks the resume.
317
+ */
318
+ private headerCwdOf;
319
+ /**
320
+ * Single-writer guard for COLD arms (create / cold resume) only — the
321
+ * adopt arm above shares the live instance in-process and must stay
322
+ * lock-free. Locks sit beside the jsonl log so every process that could
323
+ * append to it competes on one file; a live foreign holder throws
324
+ * WriterLockedError (surfaces as "Cannot resume <id>: session is locked
325
+ * by a live process …"). `knownCwd` skips the header scan for creations,
326
+ * which always target process.cwd().
327
+ */
328
+ private acquireSessionLock;
329
+ /** Undo a guard whose arm then failed — transient errors must not pin the session. */
330
+ private releaseColdLock;
331
+ /**
332
+ * Bind lock release to an owned handle's disposal — every teardown path
333
+ * already goes through handle.dispose(), so this is the single choke
334
+ * point; switching away or exiting releases what we established.
335
+ */
336
+ private ownWithLock;
242
337
  /**
243
338
  * Resume a persisted session: tear down the live agent (disposers are kept —
244
339
  * the session-id filter re-binds to the resumed id) and load the persisted