@termfleet/core 0.1.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/dist/agent-launch.d.ts +78 -0
- package/dist/agent-launch.js +247 -0
- package/dist/agent-session-id.d.ts +10 -0
- package/dist/agent-session-id.js +36 -0
- package/dist/agent-session-index-client.d.ts +7 -0
- package/dist/agent-session-index-client.js +86 -0
- package/dist/agent-session-index-worker.d.ts +1 -0
- package/dist/agent-session-index-worker.js +20 -0
- package/dist/agent-session-index.d.ts +34 -0
- package/dist/agent-session-index.js +527 -0
- package/dist/agent-session-tail.d.ts +33 -0
- package/dist/agent-session-tail.js +184 -0
- package/dist/agent-session-watcher.d.ts +36 -0
- package/dist/agent-session-watcher.js +194 -0
- package/dist/agent-session.d.ts +380 -0
- package/dist/agent-session.js +1688 -0
- package/dist/background-runner.d.ts +3 -0
- package/dist/background-runner.js +55 -0
- package/dist/boot-queue.d.ts +35 -0
- package/dist/boot-queue.js +66 -0
- package/dist/build-info.d.ts +5 -0
- package/dist/build-info.js +38 -0
- package/dist/collab/canvas-doc.d.ts +47 -0
- package/dist/collab/canvas-doc.js +83 -0
- package/dist/contracts/auth.d.ts +77 -0
- package/dist/contracts/auth.js +1 -0
- package/dist/contracts/canvas.d.ts +34 -0
- package/dist/contracts/canvas.js +76 -0
- package/dist/contracts/console-layout.d.ts +39 -0
- package/dist/contracts/console-layout.js +135 -0
- package/dist/contracts/files.d.ts +38 -0
- package/dist/contracts/files.js +37 -0
- package/dist/contracts/provider-url.d.ts +3 -0
- package/dist/contracts/provider-url.js +49 -0
- package/dist/contracts/registry.d.ts +58 -0
- package/dist/contracts/registry.js +285 -0
- package/dist/launch-trace.d.ts +6 -0
- package/dist/launch-trace.js +33 -0
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +5 -0
- package/dist/lib/exec.d.ts +13 -0
- package/dist/lib/exec.js +134 -0
- package/dist/local-providers.d.ts +32 -0
- package/dist/local-providers.js +184 -0
- package/dist/local-tunnel.d.ts +6 -0
- package/dist/local-tunnel.js +258 -0
- package/dist/provider-access-token.d.ts +11 -0
- package/dist/provider-access-token.js +77 -0
- package/dist/provider-client.d.ts +152 -0
- package/dist/provider-client.js +666 -0
- package/dist/provider-url-resolver.d.ts +16 -0
- package/dist/provider-url-resolver.js +37 -0
- package/dist/registry-client.d.ts +93 -0
- package/dist/registry-client.js +170 -0
- package/dist/registry.d.ts +56 -0
- package/dist/registry.js +406 -0
- package/dist/session-attention.d.ts +24 -0
- package/dist/session-attention.js +54 -0
- package/dist/session-lifecycle.d.ts +83 -0
- package/dist/session-lifecycle.js +658 -0
- package/dist/session-window.d.ts +3 -0
- package/dist/session-window.js +20 -0
- package/dist/terminal-client.d.ts +49 -0
- package/dist/terminal-client.js +89 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.js +21 -0
- package/package.json +26 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { open, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { setImmediate as yieldToEventLoop } from "node:timers/promises";
|
|
3
|
+
import { codexTranscriptPath, createClaudeSessionParser, emptyClaudeSession, listLocalAgentSubagents, parseCodexSessionJsonl, parseSubagentSessionId, resolveClaudeSubagentTranscriptPath, resolveClaudeTranscriptPath } from "./agent-session.js";
|
|
4
|
+
// How many transcript lines are parsed between yields back to the event
|
|
5
|
+
// loop. Keeps a multi-megabyte initial catch-up from starving sockets.
|
|
6
|
+
const linesPerSlice = 1_000;
|
|
7
|
+
const newlineByte = 0x0a;
|
|
8
|
+
// How many trailing consumed bytes are remembered to detect in-place
|
|
9
|
+
// rewrites: if the bytes immediately before our offset no longer match, the
|
|
10
|
+
// file is not the one we parsed and the tail replays from the start. JSONL
|
|
11
|
+
// lines share structural suffixes, so the anchor must outspan them.
|
|
12
|
+
const anchorBytes = 64;
|
|
13
|
+
// Incrementally tails one Claude transcript: each read() consumes only the
|
|
14
|
+
// bytes appended since the previous read and feeds complete lines into a
|
|
15
|
+
// persistent parser. Truncation, rotation, or an in-place rewrite resets the
|
|
16
|
+
// parser and replays from the start. Reads must not run concurrently for the
|
|
17
|
+
// same tail; the session watcher's read queue serializes them.
|
|
18
|
+
export class ClaudeTranscriptTail {
|
|
19
|
+
options;
|
|
20
|
+
offset = 0;
|
|
21
|
+
pendingTail = Buffer.alloc(0);
|
|
22
|
+
anchor = Buffer.alloc(0);
|
|
23
|
+
parser;
|
|
24
|
+
totalLines = 0;
|
|
25
|
+
cached;
|
|
26
|
+
lastIno;
|
|
27
|
+
lastMtimeMs;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.parser = createClaudeSessionParser(options);
|
|
31
|
+
}
|
|
32
|
+
get transcriptPath() {
|
|
33
|
+
return this.options.transcriptPath;
|
|
34
|
+
}
|
|
35
|
+
reset() {
|
|
36
|
+
this.offset = 0;
|
|
37
|
+
this.pendingTail = Buffer.alloc(0);
|
|
38
|
+
this.anchor = Buffer.alloc(0);
|
|
39
|
+
this.parser = createClaudeSessionParser(this.options);
|
|
40
|
+
this.totalLines = 0;
|
|
41
|
+
this.cached = undefined;
|
|
42
|
+
}
|
|
43
|
+
async read() {
|
|
44
|
+
const fileStat = await stat(this.options.transcriptPath);
|
|
45
|
+
// Three rewrite/rotation signals, cheapest first: the file shrank, the
|
|
46
|
+
// inode changed (file replaced at the same path), or the mtime moved
|
|
47
|
+
// without the file growing (in-place rewrite to the same or smaller
|
|
48
|
+
// size). Appends grow the file, so they never trip the third check.
|
|
49
|
+
const rotated = this.lastIno !== undefined && fileStat.ino !== this.lastIno;
|
|
50
|
+
const rewrittenInPlace = this.lastMtimeMs !== undefined
|
|
51
|
+
&& fileStat.mtimeMs !== this.lastMtimeMs
|
|
52
|
+
&& fileStat.size <= this.offset;
|
|
53
|
+
if (fileStat.size < this.offset || rotated || rewrittenInPlace) {
|
|
54
|
+
this.reset();
|
|
55
|
+
}
|
|
56
|
+
let consumedLines = 0;
|
|
57
|
+
let handle;
|
|
58
|
+
try {
|
|
59
|
+
if (this.offset > 0 || fileStat.size > 0) {
|
|
60
|
+
handle = await open(this.options.transcriptPath, "r");
|
|
61
|
+
}
|
|
62
|
+
if (handle && this.offset > 0 && this.anchor.length > 0) {
|
|
63
|
+
const check = await readAt(handle, this.offset - this.anchor.length, this.anchor.length);
|
|
64
|
+
if (!check.equals(this.anchor)) {
|
|
65
|
+
// The file at this path no longer contains what we parsed
|
|
66
|
+
// (rotation or in-place rewrite that did not shrink it).
|
|
67
|
+
this.reset();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (handle && fileStat.size > this.offset) {
|
|
71
|
+
const appended = await readAt(handle, this.offset, fileStat.size - this.offset);
|
|
72
|
+
this.offset += appended.length;
|
|
73
|
+
const combined = this.pendingTail.length > 0 ? Buffer.concat([this.pendingTail, appended]) : appended;
|
|
74
|
+
const lastNewline = combined.lastIndexOf(newlineByte);
|
|
75
|
+
// Lines are only decoded once complete: a chunk may end mid-codepoint,
|
|
76
|
+
// so the bytes after the final newline stay buffered as bytes.
|
|
77
|
+
this.pendingTail = lastNewline === -1 ? combined : Buffer.from(combined.subarray(lastNewline + 1));
|
|
78
|
+
this.anchor = Buffer.from(combined.subarray(Math.max(0, combined.length - anchorBytes)));
|
|
79
|
+
if (lastNewline !== -1) {
|
|
80
|
+
const lines = combined.subarray(0, lastNewline).toString("utf8").split("\n");
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
this.parser.feedLine(line);
|
|
83
|
+
consumedLines += 1;
|
|
84
|
+
if (consumedLines % linesPerSlice === 0) {
|
|
85
|
+
await yieldToEventLoop();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
this.totalLines += consumedLines;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await handle?.close();
|
|
94
|
+
}
|
|
95
|
+
this.lastIno = fileStat.ino;
|
|
96
|
+
this.lastMtimeMs = fileStat.mtimeMs;
|
|
97
|
+
if (consumedLines === 0 && this.cached) {
|
|
98
|
+
return { consumedLines, ...this.cached };
|
|
99
|
+
}
|
|
100
|
+
const details = this.parser.finalize();
|
|
101
|
+
const signature = [
|
|
102
|
+
"tail",
|
|
103
|
+
this.totalLines,
|
|
104
|
+
details.messages.length,
|
|
105
|
+
details.timeline.length,
|
|
106
|
+
details.lastMessage?.timestamp ?? ""
|
|
107
|
+
].join(":");
|
|
108
|
+
this.cached = { details, signature };
|
|
109
|
+
return { consumedLines, details, signature };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function readAt(handle, offset, length) {
|
|
113
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
114
|
+
let filled = 0;
|
|
115
|
+
while (filled < length) {
|
|
116
|
+
const { bytesRead } = await handle.read(buffer, filled, length - filled, offset + filled);
|
|
117
|
+
if (bytesRead === 0) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
filled += bytesRead;
|
|
121
|
+
}
|
|
122
|
+
return filled === length ? buffer : buffer.subarray(0, filled);
|
|
123
|
+
}
|
|
124
|
+
// Tails are cached per session+transcript so repeated reads stay O(appended
|
|
125
|
+
// bytes). The cache is bounded: least-recently-used tails are dropped and
|
|
126
|
+
// simply re-parse from the start if read again — the deterministic signature
|
|
127
|
+
// keeps that replay from re-publishing identical content.
|
|
128
|
+
const maxCachedTails = 64;
|
|
129
|
+
const tailCache = new Map();
|
|
130
|
+
function cachedTail(options) {
|
|
131
|
+
const key = `${options.sessionId}\0${options.transcriptPath}`;
|
|
132
|
+
const existing = tailCache.get(key);
|
|
133
|
+
if (existing) {
|
|
134
|
+
tailCache.delete(key);
|
|
135
|
+
tailCache.set(key, existing);
|
|
136
|
+
return existing;
|
|
137
|
+
}
|
|
138
|
+
const tail = new ClaudeTranscriptTail(options);
|
|
139
|
+
tailCache.set(key, tail);
|
|
140
|
+
if (tailCache.size > maxCachedTails) {
|
|
141
|
+
const oldest = tailCache.keys().next().value;
|
|
142
|
+
if (oldest !== undefined) {
|
|
143
|
+
tailCache.delete(oldest);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return tail;
|
|
147
|
+
}
|
|
148
|
+
export function clearAgentSessionTailCache() {
|
|
149
|
+
tailCache.clear();
|
|
150
|
+
}
|
|
151
|
+
export async function readLocalAgentSessionTailed(options) {
|
|
152
|
+
if (options.sessionId.startsWith("codex:")) {
|
|
153
|
+
const transcriptPath = codexTranscriptPath(options);
|
|
154
|
+
if (!transcriptPath) {
|
|
155
|
+
throw new Error(`Codex session transcript was not found for ${options.sessionId} in ${options.cwd}`);
|
|
156
|
+
}
|
|
157
|
+
const jsonl = await readFile(transcriptPath, "utf8");
|
|
158
|
+
return { details: parseCodexSessionJsonl({ jsonl, sessionId: options.sessionId, transcriptPath }) };
|
|
159
|
+
}
|
|
160
|
+
// A subagent sidechain, addressed by its composite id: tail its file directly
|
|
161
|
+
// (no nested subagents to list).
|
|
162
|
+
const subagentRef = parseSubagentSessionId(options.sessionId);
|
|
163
|
+
if (subagentRef) {
|
|
164
|
+
const subagentPath = resolveClaudeSubagentTranscriptPath({ ...(options.home ? { home: options.home } : {}), ...subagentRef });
|
|
165
|
+
const subagentTail = cachedTail({ sessionId: options.sessionId, transcriptPath: subagentPath });
|
|
166
|
+
const { details, signature } = await subagentTail.read();
|
|
167
|
+
return { details, signature };
|
|
168
|
+
}
|
|
169
|
+
// Resolution runs on every read: if the transcript moved, the stale tail is
|
|
170
|
+
// abandoned and a fresh one replays the new file.
|
|
171
|
+
const transcriptPath = resolveClaudeTranscriptPath(options);
|
|
172
|
+
if (!transcriptPath) {
|
|
173
|
+
// A just-launched agent with no transcript yet is a live, empty chat — not a
|
|
174
|
+
// failure. Once it writes messages, the next tailed read resolves and replays.
|
|
175
|
+
return { details: emptyClaudeSession(options) };
|
|
176
|
+
}
|
|
177
|
+
const tail = cachedTail({ sessionId: options.sessionId, transcriptPath });
|
|
178
|
+
const { details, signature } = await tail.read();
|
|
179
|
+
// Re-list subagents on every read (metadata only) so a live chat surfaces
|
|
180
|
+
// subagents as they spawn — the tailed message signature wouldn't otherwise
|
|
181
|
+
// change when only a new sidechain file appears.
|
|
182
|
+
const subagents = listLocalAgentSubagents({ ...(options.home ? { home: options.home } : {}), sessionId: options.sessionId, transcriptPath });
|
|
183
|
+
return { details: subagents.length ? { ...details, subagents } : details, signature };
|
|
184
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AgentSessionReadResult } from "./agent-session-tail.js";
|
|
2
|
+
import type { AgentSessionDetails } from "./agent-session.js";
|
|
3
|
+
export type AgentSessionWatchOptions = {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
};
|
|
7
|
+
export type AgentSessionWatchEvent = {
|
|
8
|
+
details: AgentSessionDetails;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
type: "update";
|
|
11
|
+
} | {
|
|
12
|
+
error: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
type: "error";
|
|
15
|
+
};
|
|
16
|
+
export type AgentSessionSubscription = {
|
|
17
|
+
unsubscribe(): void;
|
|
18
|
+
};
|
|
19
|
+
type AgentSessionListener = (event: AgentSessionWatchEvent) => void;
|
|
20
|
+
type AgentSessionReader = (options: AgentSessionWatchOptions) => AgentSessionDetails | AgentSessionReadResult | Promise<AgentSessionDetails | AgentSessionReadResult>;
|
|
21
|
+
export declare class AgentSessionWatcher {
|
|
22
|
+
private readonly readSession;
|
|
23
|
+
private readonly entries;
|
|
24
|
+
constructor(readSession: AgentSessionReader);
|
|
25
|
+
subscribe(options: AgentSessionWatchOptions, listener: AgentSessionListener): AgentSessionSubscription;
|
|
26
|
+
close(): void;
|
|
27
|
+
private unsubscribe;
|
|
28
|
+
private scheduleRead;
|
|
29
|
+
private scheduleRetry;
|
|
30
|
+
private readAndPublish;
|
|
31
|
+
private watchTranscript;
|
|
32
|
+
private publish;
|
|
33
|
+
private closeEntry;
|
|
34
|
+
private resetWatcher;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { asError } from "@termfleet/core/lib/errors.js";
|
|
2
|
+
import { watch } from "node:fs";
|
|
3
|
+
const initialRetryDelayMs = 1_000;
|
|
4
|
+
const maxRetryDelayMs = 10_000;
|
|
5
|
+
// Transcript reads run one at a time: tailed reads are incremental and yield
|
|
6
|
+
// to the event loop internally, and serializing them keeps two reads from
|
|
7
|
+
// racing on the same transcript tail.
|
|
8
|
+
const pendingReads = [];
|
|
9
|
+
let drainScheduled = false;
|
|
10
|
+
function enqueueSessionRead(task) {
|
|
11
|
+
pendingReads.push(task);
|
|
12
|
+
if (!drainScheduled) {
|
|
13
|
+
drainScheduled = true;
|
|
14
|
+
setImmediate(() => void drainSessionReads());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function drainSessionReads() {
|
|
18
|
+
const task = pendingReads.shift();
|
|
19
|
+
try {
|
|
20
|
+
await task?.();
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
if (pendingReads.length > 0) {
|
|
24
|
+
setImmediate(() => void drainSessionReads());
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
drainScheduled = false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class AgentSessionWatcher {
|
|
32
|
+
readSession;
|
|
33
|
+
entries = new Map();
|
|
34
|
+
constructor(readSession) {
|
|
35
|
+
this.readSession = readSession;
|
|
36
|
+
}
|
|
37
|
+
subscribe(options, listener) {
|
|
38
|
+
const key = watchKey(options);
|
|
39
|
+
let entry = this.entries.get(key);
|
|
40
|
+
if (!entry) {
|
|
41
|
+
entry = {
|
|
42
|
+
key,
|
|
43
|
+
listeners: new Set(),
|
|
44
|
+
options,
|
|
45
|
+
retryDelayMs: initialRetryDelayMs
|
|
46
|
+
};
|
|
47
|
+
this.entries.set(key, entry);
|
|
48
|
+
}
|
|
49
|
+
entry.listeners.add(listener);
|
|
50
|
+
this.scheduleRead(entry, 0);
|
|
51
|
+
return {
|
|
52
|
+
unsubscribe: () => {
|
|
53
|
+
this.unsubscribe(key, listener);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
close() {
|
|
58
|
+
for (const entry of this.entries.values()) {
|
|
59
|
+
this.closeEntry(entry);
|
|
60
|
+
}
|
|
61
|
+
this.entries.clear();
|
|
62
|
+
}
|
|
63
|
+
unsubscribe(key, listener) {
|
|
64
|
+
const entry = this.entries.get(key);
|
|
65
|
+
if (!entry) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
entry.listeners.delete(listener);
|
|
69
|
+
if (entry.listeners.size === 0) {
|
|
70
|
+
this.closeEntry(entry);
|
|
71
|
+
this.entries.delete(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
scheduleRead(entry, delayMs) {
|
|
75
|
+
if (entry.listeners.size === 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (entry.debounceTimer) {
|
|
79
|
+
clearTimeout(entry.debounceTimer);
|
|
80
|
+
}
|
|
81
|
+
entry.debounceTimer = setTimeout(() => {
|
|
82
|
+
entry.debounceTimer = undefined;
|
|
83
|
+
enqueueSessionRead(() => {
|
|
84
|
+
if (entry.listeners.size > 0) {
|
|
85
|
+
return this.readAndPublish(entry);
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
});
|
|
89
|
+
}, delayMs);
|
|
90
|
+
}
|
|
91
|
+
scheduleRetry(entry) {
|
|
92
|
+
if (entry.listeners.size === 0 || entry.retryTimer) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const delayMs = entry.retryDelayMs;
|
|
96
|
+
entry.retryDelayMs = Math.min(entry.retryDelayMs * 2, maxRetryDelayMs);
|
|
97
|
+
entry.retryTimer = setTimeout(() => {
|
|
98
|
+
entry.retryTimer = undefined;
|
|
99
|
+
this.scheduleRead(entry, 0);
|
|
100
|
+
}, delayMs);
|
|
101
|
+
}
|
|
102
|
+
async readAndPublish(entry) {
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
const raw = await this.readSession(entry.options);
|
|
106
|
+
result = "details" in raw ? raw : { details: raw };
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const message = asError(error).message;
|
|
110
|
+
this.resetWatcher(entry);
|
|
111
|
+
if (entry.error !== message) {
|
|
112
|
+
entry.error = message;
|
|
113
|
+
this.publish(entry, { error: message, sessionId: entry.options.sessionId, type: "error" });
|
|
114
|
+
}
|
|
115
|
+
this.scheduleRetry(entry);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
entry.error = undefined;
|
|
119
|
+
entry.retryDelayMs = initialRetryDelayMs;
|
|
120
|
+
this.watchTranscript(entry, result.details.transcriptPath);
|
|
121
|
+
// Tailed reads supply a cheap deterministic content signature; full
|
|
122
|
+
// re-parse paths fall back to stringifying the details. Either way the
|
|
123
|
+
// signature is tracked per entry, so entries sharing one tail dedupe
|
|
124
|
+
// independently instead of starving each other.
|
|
125
|
+
const signature = result.signature ?? JSON.stringify(result.details);
|
|
126
|
+
if (entry.lastSignature === signature) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
entry.lastSignature = signature;
|
|
130
|
+
this.publish(entry, { details: result.details, sessionId: entry.options.sessionId, type: "update" });
|
|
131
|
+
}
|
|
132
|
+
watchTranscript(entry, transcriptPath) {
|
|
133
|
+
if (!transcriptPath || entry.transcriptPath === transcriptPath) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
entry.watcher?.close();
|
|
137
|
+
entry.watcher = undefined;
|
|
138
|
+
try {
|
|
139
|
+
entry.watcher = watch(transcriptPath, { persistent: false }, (eventType) => {
|
|
140
|
+
if (eventType === "rename") {
|
|
141
|
+
this.resetWatcher(entry);
|
|
142
|
+
}
|
|
143
|
+
this.scheduleRead(entry, 100);
|
|
144
|
+
});
|
|
145
|
+
entry.watcher.on("error", (error) => {
|
|
146
|
+
const message = asError(error).message;
|
|
147
|
+
this.resetWatcher(entry);
|
|
148
|
+
this.publish(entry, { error: message, sessionId: entry.options.sessionId, type: "error" });
|
|
149
|
+
this.scheduleRetry(entry);
|
|
150
|
+
});
|
|
151
|
+
entry.transcriptPath = transcriptPath;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (isFileMissing(error)) {
|
|
155
|
+
// A freshly launched, message-less chat has no transcript file yet, so
|
|
156
|
+
// watching it directly throws ENOENT — not a user-facing error. Leave
|
|
157
|
+
// transcriptPath unset and keep polling: the next read resolves the
|
|
158
|
+
// transcript by id once it is written (which may be under a different
|
|
159
|
+
// project dir than this caller's cwd implies) and installs the watcher.
|
|
160
|
+
this.scheduleRetry(entry);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const message = asError(error).message;
|
|
164
|
+
this.resetWatcher(entry);
|
|
165
|
+
this.publish(entry, { error: message, sessionId: entry.options.sessionId, type: "error" });
|
|
166
|
+
this.scheduleRetry(entry);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
publish(entry, event) {
|
|
170
|
+
for (const listener of entry.listeners) {
|
|
171
|
+
listener(event);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
closeEntry(entry) {
|
|
175
|
+
if (entry.debounceTimer) {
|
|
176
|
+
clearTimeout(entry.debounceTimer);
|
|
177
|
+
}
|
|
178
|
+
if (entry.retryTimer) {
|
|
179
|
+
clearTimeout(entry.retryTimer);
|
|
180
|
+
}
|
|
181
|
+
entry.watcher?.close();
|
|
182
|
+
}
|
|
183
|
+
resetWatcher(entry) {
|
|
184
|
+
entry.watcher?.close();
|
|
185
|
+
entry.watcher = undefined;
|
|
186
|
+
entry.transcriptPath = undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function isFileMissing(error) {
|
|
190
|
+
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
191
|
+
}
|
|
192
|
+
function watchKey(options) {
|
|
193
|
+
return `${options.sessionId}\0${options.cwd ?? ""}`;
|
|
194
|
+
}
|