@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.
- package/README.md +3 -0
- package/README.zh-CN.md +3 -0
- package/lib/ask-user.d.ts +72 -0
- package/lib/ask-user.js +212 -0
- package/lib/ask-user.js.map +1 -1
- package/lib/clipboard.d.ts +114 -0
- package/lib/clipboard.js +236 -0
- package/lib/clipboard.js.map +1 -0
- package/lib/index.js +139 -41
- package/lib/index.js.map +1 -1
- package/lib/log-repair.d.ts +161 -0
- package/lib/log-repair.js +273 -0
- package/lib/log-repair.js.map +1 -0
- package/lib/remote-tail.d.ts +49 -0
- package/lib/remote-tail.js +114 -0
- package/lib/remote-tail.js.map +1 -0
- package/lib/repair-dialog.d.ts +69 -0
- package/lib/repair-dialog.js +153 -0
- package/lib/repair-dialog.js.map +1 -0
- package/lib/session.d.ts +96 -1
- package/lib/session.js +346 -39
- package/lib/session.js.map +1 -1
- package/lib/sessions.d.ts +21 -0
- package/lib/sessions.js +30 -8
- package/lib/sessions.js.map +1 -1
- package/lib/tui.js +17 -1
- package/lib/tui.js.map +1 -1
- package/lib/writer-lock.d.ts +70 -0
- package/lib/writer-lock.js +206 -0
- package/lib/writer-lock.js.map +1 -0
- package/package.json +1 -1
- package/scripts/repair-session-log.mjs +246 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corrupted-resume-log repair orchestration — the /resume safety net.
|
|
3
|
+
*
|
|
4
|
+
* When a persisted session's log was forked by a historical double-writer
|
|
5
|
+
* (two dsh processes appending to one jsonl), the loader refuses it with
|
|
6
|
+
* "corrupt session log: seq gap …" and /resume dead-ends. The offline
|
|
7
|
+
* surgery itself lives in scripts/repair-session-log.mjs (deterministic
|
|
8
|
+
* dedupe + dense renumbering; the original is NEVER modified there). This
|
|
9
|
+
* module is the TUI-free bridge around it: it runs the script, verifies the
|
|
10
|
+
* result, and — only after an explicit user confirmation upstream — swaps
|
|
11
|
+
* the repaired copy in beside a kept backup, under the single-writer lock.
|
|
12
|
+
*
|
|
13
|
+
* Contract of the script (its own header is authoritative):
|
|
14
|
+
* exit 0 → the log is CLEAN (nothing written), or — with --apply — a
|
|
15
|
+
* `<stem>.repaired.jsonl[.zstd]` artifact was written BESIDE the
|
|
16
|
+
* original (the CLEAN verdict line distinguishes the two);
|
|
17
|
+
* exit 3 → corrupt diagnosed, nothing written (dry-run only by contract);
|
|
18
|
+
* exit 2 → usage/environment error (missing zstd, torn lines without
|
|
19
|
+
* --skip-bad-lines, …).
|
|
20
|
+
*
|
|
21
|
+
* Safety invariants (all enforced here, testable without a terminal):
|
|
22
|
+
* - The original log is never edited in place — swapping moves it aside to
|
|
23
|
+
* `<name>.corrupt-bak` first and only then renames the verified repaired
|
|
24
|
+
* copy over the canonical name.
|
|
25
|
+
* - A repaired copy that does not itself verify CLEAN is never swapped in.
|
|
26
|
+
* - The writer lock beside the log (<sessionDir>/writer.lock, same file the
|
|
27
|
+
* cold arms and the feishu guard compete on) is claimed for the whole
|
|
28
|
+
* apply→verify→swap window and always released, success or not.
|
|
29
|
+
*/
|
|
30
|
+
import { spawnSync } from 'node:child_process';
|
|
31
|
+
import { chmodSync, existsSync, renameSync, statSync } from 'node:fs';
|
|
32
|
+
import { basename, dirname, join } from 'node:path';
|
|
33
|
+
import { SESSION_LOG_FILE_NAMES } from "./retention.js";
|
|
34
|
+
import { acquireWriterLock, projectKeyFor, releaseOwnedWriterLock, } from "./writer-lock.js";
|
|
35
|
+
/** The repair script rides the published package (`files` includes scripts/). */
|
|
36
|
+
export function repairScriptPath() {
|
|
37
|
+
return new URL('../scripts/repair-session-log.mjs', import.meta.url).pathname;
|
|
38
|
+
}
|
|
39
|
+
/** Hard wall-clock budget for one script invocation (big logs still finish; a hung zstd must not freeze /resume). */
|
|
40
|
+
export const REPAIR_TIMEOUT_MS = 120_000;
|
|
41
|
+
/**
|
|
42
|
+
* The path the repair script writes its artifact to (mirrors the script's
|
|
43
|
+
* own `<stem>.repaired.jsonl[.zstd]` naming, stem = name minus the jsonl[.zstd]
|
|
44
|
+
* suffix). Exported so tests and callers can predict the artifact location.
|
|
45
|
+
*/
|
|
46
|
+
export function repairedArtifactPath(logPath) {
|
|
47
|
+
const isZstd = /\.jsonl\.zstd$/.test(logPath);
|
|
48
|
+
const stem = basename(logPath).replace(/\.jsonl(\.zstd)?$/, '');
|
|
49
|
+
return join(dirname(logPath), `${stem}.repaired.jsonl${isZstd ? '.zstd' : ''}`);
|
|
50
|
+
}
|
|
51
|
+
function firstLine(text) {
|
|
52
|
+
const line = text.split('\n').map(l => l.trim()).find(l => l !== '');
|
|
53
|
+
return line ?? '';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Run scripts/repair-session-log.mjs against one log. Never touches the log
|
|
57
|
+
* itself (the script cannot); classifies the outcome per the exit contract
|
|
58
|
+
* above. A missing zstd binary surfaces as the script's exit 2 — or, should
|
|
59
|
+
* the spawn itself fail with ENOENT, as a failed result with an install hint.
|
|
60
|
+
*/
|
|
61
|
+
export function runRepair(logPath, options = {}) {
|
|
62
|
+
const apply = options.apply ?? false;
|
|
63
|
+
const spawn = options.spawnSyncFn ?? spawnSync;
|
|
64
|
+
let proc;
|
|
65
|
+
try {
|
|
66
|
+
proc = spawn(process.execPath, [repairScriptPath(), logPath, ...(apply ? ['--apply'] : [])], { encoding: 'utf8', timeout: REPAIR_TIMEOUT_MS, maxBuffer: 1 << 26 });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
return { status: 'failed', detail: `cannot run the repair script: ${message}` };
|
|
71
|
+
}
|
|
72
|
+
if (proc.error !== undefined) {
|
|
73
|
+
const code = proc.error.code;
|
|
74
|
+
const hint = code === 'ENOENT' ? ' — is zstd installed and on PATH?' : '';
|
|
75
|
+
return {
|
|
76
|
+
status: 'failed',
|
|
77
|
+
detail: `cannot run the repair script: ${proc.error.message}${hint}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const out = proc.stdout ?? '';
|
|
81
|
+
const err = proc.stderr ?? '';
|
|
82
|
+
if (proc.status === 0) {
|
|
83
|
+
// Exit 0 is ambiguous by contract (CLEAN vs. repaired+written); the
|
|
84
|
+
// CLEAN verdict line disambiguates without guessing at files.
|
|
85
|
+
if (out.includes('verdict: CLEAN'))
|
|
86
|
+
return { status: 'clean' };
|
|
87
|
+
const repairedPath = repairedArtifactPath(logPath);
|
|
88
|
+
if (existsSync(repairedPath)) {
|
|
89
|
+
return { status: 'repaired', repairedPath, detail: firstLine(out) };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
status: 'failed',
|
|
93
|
+
detail: 'repair script exited 0 but wrote no repaired artifact beside the log',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (proc.status === 3) {
|
|
97
|
+
// By the script's contract exit 3 means "corrupt, dry-run, nothing
|
|
98
|
+
// written" — with --apply this is a contract violation, but either way
|
|
99
|
+
// there is nothing to swap in, so it is a failure for our caller.
|
|
100
|
+
return {
|
|
101
|
+
status: 'failed',
|
|
102
|
+
detail: firstLine(out) || 'log is corrupt — the script diagnosed it without writing a repair',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
status: 'failed',
|
|
107
|
+
detail: firstLine(err) || `repair script exited ${proc.status ?? 'by signal'}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Verify one log loads clean (dry-run diagnosis, never writes). Used on the
|
|
112
|
+
* repaired artifact BEFORE it replaces the canonical log.
|
|
113
|
+
*/
|
|
114
|
+
export function verifyClean(logPath, options = {}) {
|
|
115
|
+
const run = runRepair(logPath, { ...options, apply: false });
|
|
116
|
+
return run.status === 'clean' ? { ok: true } : { ok: false, detail: run.detail };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Swap the verified repaired copy in over the canonical log name: the
|
|
120
|
+
* original moves aside to `<name>.corrupt-bak` (suffixed with the epoch
|
|
121
|
+
* millisecond when that name is taken — never overwritten), the repaired
|
|
122
|
+
* copy takes its place with owner-only permissions, matching the store's
|
|
123
|
+
* private-file hygiene. Synchronous on purpose: the swap window sits under
|
|
124
|
+
* the writer lock and must be a tight rename-rename pair. A failed swap-in
|
|
125
|
+
* restores the original best-effort (mirroring the feishu surface) before
|
|
126
|
+
* rethrowing, so the session is never stranded without its canonical log.
|
|
127
|
+
*/
|
|
128
|
+
export function swapRepaired(logPath, repairedPath, options = {}) {
|
|
129
|
+
let backupPath = `${logPath}.corrupt-bak`;
|
|
130
|
+
try {
|
|
131
|
+
statSync(backupPath);
|
|
132
|
+
backupPath = `${backupPath}.${Date.now()}`;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Free — the plain .corrupt-bak name is ours.
|
|
136
|
+
}
|
|
137
|
+
renameSync(logPath, backupPath);
|
|
138
|
+
try {
|
|
139
|
+
renameSync(repairedPath, logPath);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
// Best-effort restore first — the canonical name must not stay missing
|
|
143
|
+
// with the original parked in the backup; the rethrow is the signal.
|
|
144
|
+
try {
|
|
145
|
+
renameSync(backupPath, logPath);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Nothing further is recoverable here; the caller reports the swap
|
|
149
|
+
// failure and the .corrupt-bak copy still holds the original.
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
;
|
|
155
|
+
(options.chmodFn ?? chmodSync)(logPath, 0o600);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Permissions are store hygiene, not log integrity: the swap already
|
|
159
|
+
// succeeded, so a chmod failure never flips the outcome. A later
|
|
160
|
+
// successful repair (or any manual chmod) fixes the mode.
|
|
161
|
+
}
|
|
162
|
+
return { backupPath };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* End-to-end repair of one session log, under the single-writer lock beside
|
|
166
|
+
* it: claim → apply → verify the repaired copy → swap → release. ANY failure
|
|
167
|
+
* leaves the canonical log untouched, releases the lock, and reports
|
|
168
|
+
* {kind:'failed'}; only a verified artifact is ever swapped in. The lock dir
|
|
169
|
+
* is the log's own directory — the same `<sessionDir>/writer.lock` the cold
|
|
170
|
+
* arms and the feishu guard compete on.
|
|
171
|
+
*/
|
|
172
|
+
export async function repairSessionLog(logPath, options = {}) {
|
|
173
|
+
const dir = dirname(logPath);
|
|
174
|
+
let claim;
|
|
175
|
+
try {
|
|
176
|
+
claim = await acquireWriterLock(dir);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
return { kind: 'failed', detail: `cannot claim the writer lock: ${message}` };
|
|
181
|
+
}
|
|
182
|
+
if (!claim.ok)
|
|
183
|
+
return { kind: 'locked', holder: claim.holder };
|
|
184
|
+
try {
|
|
185
|
+
const run = runRepair(logPath, { apply: true, spawnSyncFn: options.spawnSyncFn });
|
|
186
|
+
if (run.status === 'failed')
|
|
187
|
+
return { kind: 'failed', detail: run.detail };
|
|
188
|
+
if (run.status === 'clean')
|
|
189
|
+
return { kind: 'clean' };
|
|
190
|
+
const verdict = verifyClean(run.repairedPath, options);
|
|
191
|
+
if (!verdict.ok) {
|
|
192
|
+
return {
|
|
193
|
+
kind: 'failed',
|
|
194
|
+
detail: `the repaired copy still fails verification: ${verdict.detail}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
kind: 'repaired',
|
|
199
|
+
backupPath: swapRepaired(logPath, run.repairedPath, options).backupPath,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
// swapRepaired restored the original best-effort before rethrowing; map
|
|
204
|
+
// the residue to a plain failure so no exception ever escapes toward the
|
|
205
|
+
// command dispatch — this module's contract is results, not throws.
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
return { kind: 'failed', detail: `swap failed: ${message}` };
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
await releaseOwnedWriterLock(dir);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The /resume corrupt-log fingerprint: the loader's "corrupt session log:
|
|
215
|
+
* seq gap …" refusal, plus the raw-decompression twin ("corrupt zstandard
|
|
216
|
+
* log"). Case-insensitive; anything else (locked session, missing
|
|
217
|
+
* persistence, network) must NOT route into the repair flow.
|
|
218
|
+
*/
|
|
219
|
+
const CORRUPT_LOG_PATTERN = /corrupt .*(session|zstandard) log/i;
|
|
220
|
+
export function isCorruptLogError(message) {
|
|
221
|
+
return CORRUPT_LOG_PATTERN.test(message);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* User-facing notice for a repair attempt that did NOT swap anything;
|
|
225
|
+
* undefined when the flow should proceed to resume ('repaired' | 'clean').
|
|
226
|
+
* Pure so the wording is testable without a terminal (English-only).
|
|
227
|
+
*/
|
|
228
|
+
export function repairFailureNotice(result) {
|
|
229
|
+
if (result.kind === 'locked') {
|
|
230
|
+
return `session is driven by pid ${result.holder.pid} — close it on the other side first`;
|
|
231
|
+
}
|
|
232
|
+
if (result.kind === 'failed') {
|
|
233
|
+
return `repair failed: ${result.detail} — log untouched`;
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Locate one session's canonical log file on disk the same way the
|
|
239
|
+
* single-writer guard derives it: `<root>/<projectKeyFor(cwd)>/<sessionId>/<name>`,
|
|
240
|
+
* with the cwd taken from the persisted header list (the log lives under
|
|
241
|
+
* THAT project key). Best-effort: no header, no cwd, or no log file under
|
|
242
|
+
* the derived dir yields undefined — the caller reports a failure instead
|
|
243
|
+
* of repairing a decoy. When both suffixes coexist (a kept raw copy beside
|
|
244
|
+
* the compressed log) the compressed name wins: it is what the writer
|
|
245
|
+
* appends to and what the loader reads.
|
|
246
|
+
*/
|
|
247
|
+
export async function locateSessionLog(persistence, sessionId, root) {
|
|
248
|
+
let cwd;
|
|
249
|
+
try {
|
|
250
|
+
const stored = (await persistence?.list?.().catch(() => [])) ?? [];
|
|
251
|
+
const header = stored.find(candidate => String(candidate.id) === sessionId);
|
|
252
|
+
if (typeof header?.cwd === 'string' && header.cwd !== '')
|
|
253
|
+
cwd = header.cwd;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
if (cwd === undefined)
|
|
259
|
+
return undefined;
|
|
260
|
+
const dir = join(root, projectKeyFor(cwd), sessionId);
|
|
261
|
+
for (const name of SESSION_LOG_FILE_NAMES.slice().reverse()) {
|
|
262
|
+
const file = join(dir, name);
|
|
263
|
+
try {
|
|
264
|
+
statSync(file);
|
|
265
|
+
return file;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// Not this suffix — try the next one.
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=log-repair.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-repair.js","sourceRoot":"","sources":["../src/log-repair.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RemoteSessionTail — a READ-ONLY view over a session log that another
|
|
3
|
+
* dsh PROCESS is driving. Firehose events do not cross the process
|
|
4
|
+
* boundary, so a surface refusing to fork the log (single-writer guard)
|
|
5
|
+
* can still satisfy "both ends eventually show the final reply" by
|
|
6
|
+
* watching persisted bytes: poll-decode the whole jsonl.zstd (frames are
|
|
7
|
+
* independent concatenations, so full decode is append-correct), and emit
|
|
8
|
+
* only the durable suffix rows that appeared since the previous tick.
|
|
9
|
+
*
|
|
10
|
+
* Display contract (deliberately lossy): streaming deltas are skipped;
|
|
11
|
+
* durable rows only — user/message, assistant/message (the FINAL LLM text),
|
|
12
|
+
* tool/call + tool/result, plus turn/start|end so the viewer can
|
|
13
|
+
* synthesize a working/idle indicator from the driver's lifecycle.
|
|
14
|
+
* Latency is the poll interval; detail may be coarser than the driving
|
|
15
|
+
* surface's live view — but every turn's final assistant message arrives.
|
|
16
|
+
*/
|
|
17
|
+
export interface RemoteTailEvent {
|
|
18
|
+
type?: string;
|
|
19
|
+
seq?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface RemoteTailCallbacks {
|
|
22
|
+
/** New durable events, in log-append order (possibly empty per tick). */
|
|
23
|
+
onEvents(events: RemoteTailEvent[]): void;
|
|
24
|
+
/** Decode failure tick — transient IO hiccups are non-fatal noise. */
|
|
25
|
+
onError?(error: unknown): void;
|
|
26
|
+
}
|
|
27
|
+
export interface RemoteTailOptions {
|
|
28
|
+
/** Poll cadence; defaults to 2s (cross-process latency is accepted). */
|
|
29
|
+
intervalMs?: number;
|
|
30
|
+
/** Decoder seam — tests substitute a pure function; default shells zstd. */
|
|
31
|
+
decode?(file: string): Promise<string>;
|
|
32
|
+
}
|
|
33
|
+
export declare class RemoteSessionTail {
|
|
34
|
+
private timer;
|
|
35
|
+
private ticking;
|
|
36
|
+
/** Rows already emitted — append-only watermark across ticks. */
|
|
37
|
+
private seenRows;
|
|
38
|
+
private readonly file;
|
|
39
|
+
private readonly callbacks;
|
|
40
|
+
private readonly options;
|
|
41
|
+
constructor(file: string, callbacks: RemoteTailCallbacks, options?: RemoteTailOptions);
|
|
42
|
+
start(): void;
|
|
43
|
+
stop(): void;
|
|
44
|
+
/** Force one poll immediately — the deterministic backfill entry point. */
|
|
45
|
+
tickOnce(): Promise<void>;
|
|
46
|
+
/** Where the watched log lives — tests assert on it. */
|
|
47
|
+
getWatchedFile(): string;
|
|
48
|
+
private tick;
|
|
49
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RemoteSessionTail — a READ-ONLY view over a session log that another
|
|
3
|
+
* dsh PROCESS is driving. Firehose events do not cross the process
|
|
4
|
+
* boundary, so a surface refusing to fork the log (single-writer guard)
|
|
5
|
+
* can still satisfy "both ends eventually show the final reply" by
|
|
6
|
+
* watching persisted bytes: poll-decode the whole jsonl.zstd (frames are
|
|
7
|
+
* independent concatenations, so full decode is append-correct), and emit
|
|
8
|
+
* only the durable suffix rows that appeared since the previous tick.
|
|
9
|
+
*
|
|
10
|
+
* Display contract (deliberately lossy): streaming deltas are skipped;
|
|
11
|
+
* durable rows only — user/message, assistant/message (the FINAL LLM text),
|
|
12
|
+
* tool/call + tool/result, plus turn/start|end so the viewer can
|
|
13
|
+
* synthesize a working/idle indicator from the driver's lifecycle.
|
|
14
|
+
* Latency is the poll interval; detail may be coarser than the driving
|
|
15
|
+
* surface's live view — but every turn's final assistant message arrives.
|
|
16
|
+
*/
|
|
17
|
+
import { execFile } from 'node:child_process';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
const TURN_START = 'turn/start';
|
|
20
|
+
const TURN_END = 'turn/end';
|
|
21
|
+
/** Durable row types rendered in a remote view, in log order — streaming
|
|
22
|
+
* deltas excluded (see module doc for the display contract). */
|
|
23
|
+
const DURABLE_TYPES = new Set([
|
|
24
|
+
'user/message',
|
|
25
|
+
'assistant/message',
|
|
26
|
+
'tool/call',
|
|
27
|
+
'tool/result',
|
|
28
|
+
TURN_START,
|
|
29
|
+
TURN_END,
|
|
30
|
+
]);
|
|
31
|
+
function defaultDecode(file) {
|
|
32
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
33
|
+
execFile('zstd', ['-dc', file], { maxBuffer: 1 << 30 }, (error, stdout) => {
|
|
34
|
+
if (error !== null && error !== undefined)
|
|
35
|
+
rejectPromise(error);
|
|
36
|
+
else
|
|
37
|
+
resolvePromise(Buffer.from(stdout, 'utf8').toString('utf8'));
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export class RemoteSessionTail {
|
|
42
|
+
timer;
|
|
43
|
+
ticking = false;
|
|
44
|
+
/** Rows already emitted — append-only watermark across ticks. */
|
|
45
|
+
seenRows = 0;
|
|
46
|
+
file;
|
|
47
|
+
callbacks;
|
|
48
|
+
options;
|
|
49
|
+
constructor(file, callbacks, options = {}) {
|
|
50
|
+
this.file = file;
|
|
51
|
+
this.callbacks = callbacks;
|
|
52
|
+
this.options = options;
|
|
53
|
+
}
|
|
54
|
+
start() {
|
|
55
|
+
if (this.timer !== undefined)
|
|
56
|
+
return;
|
|
57
|
+
this.timer = setInterval(() => void this.tick(), this.options.intervalMs ?? 2000);
|
|
58
|
+
}
|
|
59
|
+
stop() {
|
|
60
|
+
if (this.timer === undefined)
|
|
61
|
+
return;
|
|
62
|
+
clearInterval(this.timer);
|
|
63
|
+
this.timer = undefined;
|
|
64
|
+
}
|
|
65
|
+
/** Force one poll immediately — the deterministic backfill entry point. */
|
|
66
|
+
tickOnce() {
|
|
67
|
+
return this.tick(true);
|
|
68
|
+
}
|
|
69
|
+
/** Where the watched log lives — tests assert on it. */
|
|
70
|
+
getWatchedFile() {
|
|
71
|
+
return join(this.file);
|
|
72
|
+
}
|
|
73
|
+
async tick(force = false) {
|
|
74
|
+
if (this.ticking)
|
|
75
|
+
return;
|
|
76
|
+
if (!force && this.timer === undefined)
|
|
77
|
+
return;
|
|
78
|
+
this.ticking = true;
|
|
79
|
+
try {
|
|
80
|
+
const decode = this.options.decode ?? defaultDecode;
|
|
81
|
+
const text = await decode(this.file);
|
|
82
|
+
const lines = text.split('\n').filter(line => line.trim() !== '');
|
|
83
|
+
// Log shrank underneath us (repair/rename) — treat as a fresh stream
|
|
84
|
+
// rather than emitting a bogus negative diff.
|
|
85
|
+
if (lines.length < this.seenRows)
|
|
86
|
+
this.seenRows = 0;
|
|
87
|
+
const fresh = [];
|
|
88
|
+
for (let i = this.seenRows; i < lines.length; i++) {
|
|
89
|
+
let value;
|
|
90
|
+
try {
|
|
91
|
+
value = JSON.parse(lines[i]);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue; // torn trailing frame mid-decode — next tick re-reads it
|
|
95
|
+
}
|
|
96
|
+
if (typeof value !== 'object' || value === null)
|
|
97
|
+
continue;
|
|
98
|
+
const event = value;
|
|
99
|
+
if (event.type !== undefined && DURABLE_TYPES.has(event.type))
|
|
100
|
+
fresh.push(event);
|
|
101
|
+
}
|
|
102
|
+
this.seenRows = lines.length;
|
|
103
|
+
if (fresh.length > 0)
|
|
104
|
+
this.callbacks.onEvents(fresh);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
this.callbacks.onError?.(error);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
this.ticking = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=remote-tail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-tail.js","sourceRoot":"","sources":["../src/remote-tail.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { type Component, type TUI } from '@earendil-works/pi-tui';
|
|
19
|
+
import { type TuiTheme } from './theme/index.ts';
|
|
20
|
+
/** The two choices in display order; index 0 is preselected. */
|
|
21
|
+
export declare const REPAIR_CONFIRM_OPTIONS: ReadonlyArray<{
|
|
22
|
+
id: 'repair' | 'cancel';
|
|
23
|
+
title: string;
|
|
24
|
+
hint: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** The fixed body copy (spec wording): what happened, what repair does, what is kept. */
|
|
27
|
+
export declare const REPAIR_CONFIRM_MESSAGE: string;
|
|
28
|
+
/** Footer hint — hardcoded like every other panel footer (English-only). */
|
|
29
|
+
export declare const REPAIR_CONFIRM_FOOTER = "\u2191\u2193 select \u00B7 1/2 pick \u00B7 Enter confirm \u00B7 Esc cancel";
|
|
30
|
+
/** Pure dialog state: which row is highlighted, and the terminal outcome. */
|
|
31
|
+
export interface RepairConfirmState {
|
|
32
|
+
selected: number;
|
|
33
|
+
/**
|
|
34
|
+
* Set once by a terminal key: `'confirm'` (Enter on a selection) or
|
|
35
|
+
* `'cancel'` (Esc). Further input is ignored afterwards.
|
|
36
|
+
*/
|
|
37
|
+
settled?: 'confirm' | 'cancel';
|
|
38
|
+
}
|
|
39
|
+
export declare function initialRepairConfirmState(): RepairConfirmState;
|
|
40
|
+
/**
|
|
41
|
+
* Apply one raw key sequence to the dialog state. Unknown keys are no-ops;
|
|
42
|
+
* anything after a settle is ignored (single terminal outcome guard).
|
|
43
|
+
*/
|
|
44
|
+
export declare function updateRepairConfirm(state: RepairConfirmState, data: string): RepairConfirmState;
|
|
45
|
+
/** Resolved dialog outcome: the chosen action, or undefined on cancel. */
|
|
46
|
+
export declare function repairConfirmOutcome(state: RepairConfirmState): 'repair' | 'cancel' | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* The framed overlay component. Renders the fixed body copy plus the two
|
|
49
|
+
* option rows; every key goes through {@link updateRepairConfirm}, and the
|
|
50
|
+
* first terminal key fires `onFinish` exactly once.
|
|
51
|
+
*/
|
|
52
|
+
export declare class RepairConfirmPanel implements Component {
|
|
53
|
+
private readonly theme;
|
|
54
|
+
private readonly onFinish;
|
|
55
|
+
private readonly requestRenderFn;
|
|
56
|
+
private state;
|
|
57
|
+
constructor(theme: TuiTheme, onFinish: (outcome: 'repair' | 'cancel' | undefined) => void, requestRender: () => void);
|
|
58
|
+
invalidate(): void;
|
|
59
|
+
render(width: number): string[];
|
|
60
|
+
handleInput(data: string): void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Open the repair confirmation dialog for one corrupt resume target.
|
|
64
|
+
* Resolves `'repair'` when the user explicitly confirmed, `'cancelled'`
|
|
65
|
+
* otherwise (Esc, or an overlay that failed to mount — treated as cancel so
|
|
66
|
+
* a half-mounted dialog can never imply consent). Closing always hands
|
|
67
|
+
* focus back through `restoreFocus` before the promise settles.
|
|
68
|
+
*/
|
|
69
|
+
export declare function openRepairConfirmDialog(tui: TUI, theme: TuiTheme, restoreFocus: () => void): Promise<'repair' | 'cancelled'>;
|