@gajae-code/coding-agent 0.11.3 → 0.11.4
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/CHANGELOG.md +24 -0
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/tool-transcript-format.d.ts +19 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/tools/ask.d.ts +39 -12
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +24 -8
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +3 -2
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +42 -210
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/session-observer-overlay.ts +10 -23
- package/src/modes/components/tool-transcript-format.ts +72 -0
- package/src/modes/components/transcript-viewer-overlay.ts +42 -2
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +228 -15
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/telegram-daemon.ts +50 -10
- package/src/session/internal/managed-session-scope.ts +15 -1
- package/src/tools/ask.ts +216 -41
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/read.ts +7 -1
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/providers/openai-compatible.ts +76 -23
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { withFileLock } from "../../config/file-lock";
|
|
@@ -48,6 +48,27 @@ export interface SessionList {
|
|
|
48
48
|
sessions: IndexedSession[];
|
|
49
49
|
warnings: string[];
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
export interface SessionIndexDiagnosis {
|
|
53
|
+
status: "healthy" | "corrupt" | "unsupported";
|
|
54
|
+
validPrefixSeq: number;
|
|
55
|
+
snapshotSeq: number;
|
|
56
|
+
reason?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SessionIndexRepairResult extends SessionIndexDiagnosis {
|
|
60
|
+
repaired: boolean;
|
|
61
|
+
quarantinePath?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface SessionIndexScan {
|
|
65
|
+
diagnosis: SessionIndexDiagnosis;
|
|
66
|
+
snapshotEvents: SessionIndexEvent[];
|
|
67
|
+
validLogEvents: SessionIndexEvent[];
|
|
68
|
+
snapshotContents: Buffer | undefined;
|
|
69
|
+
logContents: Buffer | undefined;
|
|
70
|
+
unsupportedError?: UnsupportedStateVersionError;
|
|
71
|
+
}
|
|
51
72
|
const canonical = (event: Omit<SessionIndexEvent, "checksum">) => JSON.stringify(event);
|
|
52
73
|
export const sessionIndexChecksum = (event: Omit<SessionIndexEvent, "checksum">) =>
|
|
53
74
|
createHash("sha256").update(canonical(event)).digest("hex");
|
|
@@ -142,6 +163,27 @@ async function syncDirectory(file: string): Promise<void> {
|
|
|
142
163
|
}
|
|
143
164
|
}
|
|
144
165
|
|
|
166
|
+
async function writeAndSync(file: string, contents: Buffer | string): Promise<void> {
|
|
167
|
+
const handle = await fs.open(file, "w", 0o600);
|
|
168
|
+
try {
|
|
169
|
+
await handle.writeFile(contents);
|
|
170
|
+
await handle.sync();
|
|
171
|
+
} finally {
|
|
172
|
+
await handle.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function replaceAtomically(file: string, contents: Buffer | string): Promise<void> {
|
|
177
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
178
|
+
try {
|
|
179
|
+
await writeAndSync(temporary, contents);
|
|
180
|
+
await fs.rename(temporary, file);
|
|
181
|
+
await syncDirectory(file);
|
|
182
|
+
} finally {
|
|
183
|
+
await fs.rm(temporary, { force: true });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
145
187
|
function alive(pid: number): boolean {
|
|
146
188
|
try {
|
|
147
189
|
process.kill(pid, 0);
|
|
@@ -166,26 +208,194 @@ export class SessionIndex {
|
|
|
166
208
|
return this;
|
|
167
209
|
}
|
|
168
210
|
async replay(): Promise<void> {
|
|
169
|
-
|
|
211
|
+
const scan = await this.#scan();
|
|
212
|
+
if (scan.diagnosis.status === "unsupported") throw scan.unsupportedError!;
|
|
213
|
+
this.#events = [...scan.snapshotEvents, ...scan.validLogEvents];
|
|
170
214
|
this.#warnings = [];
|
|
171
|
-
this.#logOffset = 0;
|
|
172
|
-
this.#corruptSuffix =
|
|
215
|
+
this.#logOffset = scan.logContents?.length ?? 0;
|
|
216
|
+
this.#corruptSuffix = scan.diagnosis.status === "corrupt";
|
|
217
|
+
if (scan.diagnosis.reason === "invalid snapshot") this.#warnings.push("Invalid session index snapshot");
|
|
218
|
+
if (this.#corruptSuffix) this.#warnings.push("Corrupt session index entry; replay truncated");
|
|
219
|
+
}
|
|
220
|
+
async #scan(): Promise<SessionIndexScan> {
|
|
221
|
+
let snapshotContents: Buffer | undefined;
|
|
222
|
+
let logContents: Buffer | undefined;
|
|
223
|
+
let snapshotEvents: SessionIndexEvent[] = [];
|
|
173
224
|
let snapshotSeq = 0;
|
|
225
|
+
let trustedSnapshotSeq = 0;
|
|
226
|
+
let invalidSnapshot = false;
|
|
227
|
+
let unsupportedError: UnsupportedStateVersionError | undefined;
|
|
174
228
|
try {
|
|
175
|
-
|
|
229
|
+
snapshotContents = await fs.readFile(snapshotFor(this.#agentDir));
|
|
230
|
+
const snapshot = JSON.parse(snapshotContents.toString("utf8")) as {
|
|
176
231
|
version?: number;
|
|
177
|
-
|
|
178
|
-
|
|
232
|
+
indexSeq?: unknown;
|
|
233
|
+
events?: unknown;
|
|
179
234
|
};
|
|
235
|
+
if (typeof snapshot.indexSeq === "number" && Number.isSafeInteger(snapshot.indexSeq) && snapshot.indexSeq >= 0)
|
|
236
|
+
snapshotSeq = snapshot.indexSeq;
|
|
180
237
|
assertSupportedSnapshotVersion(snapshotFor(this.#agentDir), snapshot);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
238
|
+
const supportedEvents: SessionIndexEvent[] = [];
|
|
239
|
+
if (Array.isArray(snapshot.events)) {
|
|
240
|
+
try {
|
|
241
|
+
for (const event of snapshot.events) {
|
|
242
|
+
assertSupportedStateVersion(snapshotFor(this.#agentDir), event);
|
|
243
|
+
supportedEvents.push(event as SessionIndexEvent);
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (!(error instanceof UnsupportedStateVersionError)) throw error;
|
|
247
|
+
unsupportedError = error;
|
|
248
|
+
snapshotEvents = supportedEvents;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!unsupportedError) {
|
|
252
|
+
if (!isValidSnapshot(snapshot)) invalidSnapshot = true;
|
|
253
|
+
else {
|
|
254
|
+
snapshotEvents = snapshot.events;
|
|
255
|
+
trustedSnapshotSeq = snapshot.indexSeq;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (error instanceof UnsupportedStateVersionError) unsupportedError = error;
|
|
260
|
+
else if ((error as NodeJS.ErrnoException).code !== "ENOENT") invalidSnapshot = true;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
logContents = await fs.readFile(logFor(this.#agentDir));
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
266
|
+
}
|
|
267
|
+
const validLogEvents: SessionIndexEvent[] = [];
|
|
268
|
+
let corrupt = invalidSnapshot;
|
|
269
|
+
let logCorrupt = false;
|
|
270
|
+
let tailStarted = false;
|
|
271
|
+
let historicalLast: number | undefined;
|
|
272
|
+
let expected = trustedSnapshotSeq + 1;
|
|
273
|
+
if (logContents) {
|
|
274
|
+
const text = logContents.toString("utf8");
|
|
275
|
+
const lines = text.split("\n");
|
|
276
|
+
const terminal = lines.pop();
|
|
277
|
+
for (const line of lines) {
|
|
278
|
+
if (!line) continue;
|
|
279
|
+
try {
|
|
280
|
+
const event = JSON.parse(line) as SessionIndexEvent;
|
|
281
|
+
assertSupportedStateVersion(logFor(this.#agentDir), event);
|
|
282
|
+
const { checksum, ...unsigned } = event;
|
|
283
|
+
if (checksum !== sessionIndexChecksum(unsigned)) {
|
|
284
|
+
corrupt = true;
|
|
285
|
+
logCorrupt = true;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (!tailStarted && !invalidSnapshot && event.indexSeq <= trustedSnapshotSeq) {
|
|
289
|
+
if (
|
|
290
|
+
!Number.isSafeInteger(event.indexSeq) ||
|
|
291
|
+
event.indexSeq <= 0 ||
|
|
292
|
+
(historicalLast !== undefined && event.indexSeq !== historicalLast + 1)
|
|
293
|
+
) {
|
|
294
|
+
corrupt = true;
|
|
295
|
+
logCorrupt = true;
|
|
296
|
+
} else {
|
|
297
|
+
historicalLast = event.indexSeq;
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
tailStarted = true;
|
|
302
|
+
if (historicalLast !== undefined && historicalLast !== trustedSnapshotSeq) {
|
|
303
|
+
corrupt = true;
|
|
304
|
+
logCorrupt = true;
|
|
305
|
+
}
|
|
306
|
+
if (event.indexSeq !== expected) {
|
|
307
|
+
corrupt = true;
|
|
308
|
+
logCorrupt = true;
|
|
309
|
+
} else if (!logCorrupt) {
|
|
310
|
+
validLogEvents.push(event);
|
|
311
|
+
expected++;
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (error instanceof UnsupportedStateVersionError) {
|
|
315
|
+
const verifiedSnapshotPrefix = snapshotEvents.at(-1)?.indexSeq ?? trustedSnapshotSeq;
|
|
316
|
+
const validPrefixSeq = validLogEvents.at(-1)?.indexSeq ?? verifiedSnapshotPrefix;
|
|
317
|
+
return {
|
|
318
|
+
diagnosis: { status: "unsupported", validPrefixSeq, snapshotSeq, reason: error.message },
|
|
319
|
+
snapshotEvents,
|
|
320
|
+
validLogEvents,
|
|
321
|
+
snapshotContents,
|
|
322
|
+
logContents,
|
|
323
|
+
unsupportedError: error,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
corrupt = true;
|
|
327
|
+
logCorrupt = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (historicalLast !== undefined && !tailStarted && historicalLast !== trustedSnapshotSeq) {
|
|
331
|
+
corrupt = true;
|
|
332
|
+
logCorrupt = true;
|
|
333
|
+
}
|
|
334
|
+
if (terminal !== "") {
|
|
335
|
+
corrupt = true;
|
|
336
|
+
logCorrupt = true;
|
|
337
|
+
}
|
|
187
338
|
}
|
|
188
|
-
|
|
339
|
+
const verifiedSnapshotPrefix = snapshotEvents.at(-1)?.indexSeq ?? trustedSnapshotSeq;
|
|
340
|
+
const validPrefixSeq = validLogEvents.at(-1)?.indexSeq ?? verifiedSnapshotPrefix;
|
|
341
|
+
return {
|
|
342
|
+
diagnosis: {
|
|
343
|
+
status: unsupportedError ? "unsupported" : corrupt ? "corrupt" : "healthy",
|
|
344
|
+
validPrefixSeq,
|
|
345
|
+
snapshotSeq,
|
|
346
|
+
reason:
|
|
347
|
+
unsupportedError?.message ??
|
|
348
|
+
(invalidSnapshot ? "invalid snapshot" : corrupt ? "invalid log sequence" : undefined),
|
|
349
|
+
},
|
|
350
|
+
snapshotEvents,
|
|
351
|
+
validLogEvents,
|
|
352
|
+
snapshotContents,
|
|
353
|
+
logContents,
|
|
354
|
+
unsupportedError,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
async diagnose(): Promise<SessionIndexDiagnosis> {
|
|
358
|
+
const exists = await Promise.all(
|
|
359
|
+
[snapshotFor(this.#agentDir), logFor(this.#agentDir)].map(async file => {
|
|
360
|
+
try {
|
|
361
|
+
await fs.stat(file);
|
|
362
|
+
return true;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
369
|
+
if (!exists.some(Boolean)) return { status: "healthy", validPrefixSeq: 0, snapshotSeq: 0 };
|
|
370
|
+
return await withFileLock(logFor(this.#agentDir), async () => (await this.#scan()).diagnosis);
|
|
371
|
+
}
|
|
372
|
+
async repair(): Promise<SessionIndexRepairResult> {
|
|
373
|
+
await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
|
|
374
|
+
return await withFileLock(logFor(this.#agentDir), async () => {
|
|
375
|
+
const scan = await this.#scan();
|
|
376
|
+
if (scan.diagnosis.status === "unsupported") return { ...scan.diagnosis, repaired: false };
|
|
377
|
+
if (scan.diagnosis.status === "healthy") return { ...scan.diagnosis, repaired: false };
|
|
378
|
+
const quarantineBase = path.join(dirFor(this.#agentDir), "quarantine");
|
|
379
|
+
await fs.mkdir(quarantineBase, { recursive: true, mode: 0o700 });
|
|
380
|
+
await syncDirectory(quarantineBase);
|
|
381
|
+
const quarantinePath = path.join(quarantineBase, `repair-${Date.now()}-${process.pid}-${randomUUID()}`);
|
|
382
|
+
await fs.mkdir(quarantinePath, { mode: 0o700 });
|
|
383
|
+
await syncDirectory(quarantinePath);
|
|
384
|
+
if (scan.snapshotContents)
|
|
385
|
+
await writeAndSync(path.join(quarantinePath, "index.snapshot.json"), scan.snapshotContents);
|
|
386
|
+
if (scan.logContents) await writeAndSync(path.join(quarantinePath, "index.jsonl"), scan.logContents);
|
|
387
|
+
await syncDirectory(path.join(quarantinePath, "index.jsonl"));
|
|
388
|
+
const snapshot = JSON.stringify({
|
|
389
|
+
version: SESSION_INDEX_SNAPSHOT_VERSION,
|
|
390
|
+
indexSeq: scan.snapshotEvents.at(-1)?.indexSeq ?? 0,
|
|
391
|
+
events: scan.snapshotEvents,
|
|
392
|
+
});
|
|
393
|
+
const log = scan.validLogEvents.map(event => JSON.stringify(event)).join("\n");
|
|
394
|
+
await replaceAtomically(snapshotFor(this.#agentDir), snapshot);
|
|
395
|
+
await replaceAtomically(logFor(this.#agentDir), log ? `${log}\n` : "");
|
|
396
|
+
await this.replay();
|
|
397
|
+
return { ...scan.diagnosis, repaired: true, quarantinePath };
|
|
398
|
+
});
|
|
189
399
|
}
|
|
190
400
|
async #tail(snapshotSeq = this.indexSeq, allowResync = true): Promise<void> {
|
|
191
401
|
let data: Buffer;
|
|
@@ -252,7 +462,10 @@ export class SessionIndex {
|
|
|
252
462
|
await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
|
|
253
463
|
return withFileLock(logFor(this.#agentDir), async () => {
|
|
254
464
|
await this.replay();
|
|
255
|
-
if (this.#corruptSuffix)
|
|
465
|
+
if (this.#corruptSuffix)
|
|
466
|
+
throw new Error(
|
|
467
|
+
"Cannot append to corrupt session index log; run `gjc gc --repair-session-index` to quarantine evidence and retain the valid prefix",
|
|
468
|
+
);
|
|
256
469
|
const unsigned: Omit<SessionIndexEvent, "checksum"> = {
|
|
257
470
|
...input,
|
|
258
471
|
version: SDK_STATE_VERSION,
|
|
@@ -13,8 +13,11 @@ export class UnsupportedStateVersionError extends Error {
|
|
|
13
13
|
constructor(
|
|
14
14
|
readonly file: string,
|
|
15
15
|
readonly version: number,
|
|
16
|
+
readonly maximumSupportedVersion = SDK_STATE_VERSION,
|
|
16
17
|
) {
|
|
17
|
-
super(
|
|
18
|
+
super(
|
|
19
|
+
`Unsupported SDK state version ${version} in ${file}; maximum supported version is ${maximumSupportedVersion}.`,
|
|
20
|
+
);
|
|
18
21
|
this.name = "UnsupportedStateVersionError";
|
|
19
22
|
}
|
|
20
23
|
}
|
|
@@ -38,7 +41,7 @@ export function assertSupportedSnapshotVersion(file: string, value: unknown): vo
|
|
|
38
41
|
const record = value as { version?: unknown; stateVersion?: unknown };
|
|
39
42
|
for (const version of [record.version, record.stateVersion]) {
|
|
40
43
|
if (typeof version === "number" && Number.isFinite(version) && version > SESSION_INDEX_SNAPSHOT_VERSION) {
|
|
41
|
-
throw new UnsupportedStateVersionError(file, version);
|
|
44
|
+
throw new UnsupportedStateVersionError(file, version, SESSION_INDEX_SNAPSHOT_VERSION);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
}
|
|
@@ -1293,6 +1293,20 @@ export interface DaemonControlHooks {
|
|
|
1293
1293
|
clear?(ownerId: string): Promise<void>;
|
|
1294
1294
|
}
|
|
1295
1295
|
|
|
1296
|
+
type BtwTerminalDeliveryOutcome = "accepted" | "not_delivered" | "uncertain" | "partial_accepted" | "stale";
|
|
1297
|
+
|
|
1298
|
+
interface BtwTerminalDeliveryReceipt {
|
|
1299
|
+
requestId: string;
|
|
1300
|
+
logicalSessionId: string;
|
|
1301
|
+
transportSessionId: string;
|
|
1302
|
+
threadId: string;
|
|
1303
|
+
updateId: number;
|
|
1304
|
+
messageId: number;
|
|
1305
|
+
outcome: BtwTerminalDeliveryOutcome;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const BTW_TERMINAL_DELIVERY_TEST_OBSERVER = Symbol.for("gjc.test.btw-terminal-delivery-observer");
|
|
1309
|
+
|
|
1296
1310
|
export interface TelegramDaemonOptions {
|
|
1297
1311
|
settings: Settings;
|
|
1298
1312
|
ownerId: string;
|
|
@@ -3958,6 +3972,7 @@ export class TelegramNotificationDaemon {
|
|
|
3958
3972
|
};
|
|
3959
3973
|
this.#btwTerminalDeliveries.set(requestId, terminalDelivery);
|
|
3960
3974
|
let deliveryOutcome: "accepted" | "not_delivered" | "uncertain" | "partial_accepted" = "not_delivered";
|
|
3975
|
+
let observerOutcome: BtwTerminalDeliveryOutcome = deliveryOutcome;
|
|
3961
3976
|
try {
|
|
3962
3977
|
if (msg.status !== "ok") {
|
|
3963
3978
|
const text =
|
|
@@ -4023,7 +4038,7 @@ export class TelegramNotificationDaemon {
|
|
|
4023
4038
|
}
|
|
4024
4039
|
};
|
|
4025
4040
|
const html = markdownToTelegramHtml(markdown);
|
|
4026
|
-
const fallback = async (): Promise<
|
|
4041
|
+
const fallback = async (): Promise<BtwTerminalDeliveryOutcome> => {
|
|
4027
4042
|
let acceptedChunks = 0;
|
|
4028
4043
|
for (const [index, text] of splitTelegramHtml(html).entries()) {
|
|
4029
4044
|
const outcome = await this.#queueBtwFallbackChunk({
|
|
@@ -4045,6 +4060,7 @@ export class TelegramNotificationDaemon {
|
|
|
4045
4060
|
continue;
|
|
4046
4061
|
}
|
|
4047
4062
|
if (outcome === "partial_accepted" || acceptedChunks > 0) return "partial_accepted";
|
|
4063
|
+
if (outcome === "stale") return "stale";
|
|
4048
4064
|
return outcome === "uncertain" ? "uncertain" : "not_delivered";
|
|
4049
4065
|
}
|
|
4050
4066
|
return "accepted";
|
|
@@ -4056,16 +4072,21 @@ export class TelegramNotificationDaemon {
|
|
|
4056
4072
|
reply_parameters: { message_id: pending.messageId },
|
|
4057
4073
|
rich_message: { markdown, skip_entity_detection: true },
|
|
4058
4074
|
});
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4075
|
+
if (outcome === "accepted") {
|
|
4076
|
+
deliveryOutcome = "accepted";
|
|
4077
|
+
observerOutcome = "accepted";
|
|
4078
|
+
} else if (outcome === "rejected") {
|
|
4079
|
+
const fallbackOutcome = await fallback();
|
|
4080
|
+
observerOutcome = fallbackOutcome;
|
|
4081
|
+
deliveryOutcome = fallbackOutcome === "stale" ? "not_delivered" : fallbackOutcome;
|
|
4082
|
+
} else {
|
|
4083
|
+
deliveryOutcome = outcome === "uncertain" ? "uncertain" : "not_delivered";
|
|
4084
|
+
observerOutcome = outcome === "stale" ? "stale" : deliveryOutcome;
|
|
4085
|
+
}
|
|
4067
4086
|
} else {
|
|
4068
|
-
|
|
4087
|
+
const fallbackOutcome = await fallback();
|
|
4088
|
+
observerOutcome = fallbackOutcome;
|
|
4089
|
+
deliveryOutcome = fallbackOutcome === "stale" ? "not_delivered" : fallbackOutcome;
|
|
4069
4090
|
}
|
|
4070
4091
|
} finally {
|
|
4071
4092
|
if (this.#btwTerminalDeliveries.get(requestId) === terminalDelivery) {
|
|
@@ -4084,6 +4105,25 @@ export class TelegramNotificationDaemon {
|
|
|
4084
4105
|
}
|
|
4085
4106
|
} finally {
|
|
4086
4107
|
terminalDelivery.finish();
|
|
4108
|
+
observerOutcome = observerOutcome === "stale" ? "stale" : deliveryOutcome;
|
|
4109
|
+
if (this.#pendingBtwTurns.get(requestId) !== pending) {
|
|
4110
|
+
try {
|
|
4111
|
+
const observer = (
|
|
4112
|
+
this as unknown as Record<symbol, ((receipt: BtwTerminalDeliveryReceipt) => void) | undefined>
|
|
4113
|
+
)[BTW_TERMINAL_DELIVERY_TEST_OBSERVER];
|
|
4114
|
+
observer?.({
|
|
4115
|
+
requestId,
|
|
4116
|
+
logicalSessionId: pending.logicalSessionId,
|
|
4117
|
+
transportSessionId: pending.transportSessionId,
|
|
4118
|
+
threadId: pending.threadId,
|
|
4119
|
+
updateId: pending.updateId,
|
|
4120
|
+
messageId: pending.messageId,
|
|
4121
|
+
outcome: observerOutcome,
|
|
4122
|
+
});
|
|
4123
|
+
} catch {
|
|
4124
|
+
logger.warn("notifications: /btw terminal delivery observer failed");
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4087
4127
|
}
|
|
4088
4128
|
}
|
|
4089
4129
|
return;
|
|
@@ -99,6 +99,7 @@ export type ManagedDeleteCandidateResult =
|
|
|
99
99
|
type NativeIdentity =
|
|
100
100
|
| { ok: true; platform: "posix" | "win32"; canonicalPath: string }
|
|
101
101
|
| { ok: false; code: NativeIdentityFailureCode };
|
|
102
|
+
type CanonicalNativeIdentity = Extract<NativeIdentity, { ok: true }>;
|
|
102
103
|
|
|
103
104
|
type NativeIdentityFailureCode =
|
|
104
105
|
| "not_found"
|
|
@@ -149,6 +150,18 @@ function identityFor(cwd: string): NativeIdentity {
|
|
|
149
150
|
return canonicalExistingDirectoryIdentity(cwd) as NativeIdentity;
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
function canonicalExistingPathForIo(base: string, identity: CanonicalNativeIdentity): string {
|
|
154
|
+
if (identity.platform !== "win32") return identity.canonicalPath;
|
|
155
|
+
try {
|
|
156
|
+
// Native identity uses a stable Volume GUID path on Windows. Bun 1.3.14
|
|
157
|
+
// cannot reliably create/read files through that path, so retain the
|
|
158
|
+
// symlink-resolved DOS path for JavaScript filesystem I/O.
|
|
159
|
+
return fs.realpathSync.native(base);
|
|
160
|
+
} catch {
|
|
161
|
+
return path.resolve(base);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
152
165
|
/**
|
|
153
166
|
* Resolve benign symlinks in the deepest existing ancestor of a trusted storage
|
|
154
167
|
* root (e.g. macOS `/var -> /private/var`, or a symlinked `$HOME`) while keeping
|
|
@@ -164,7 +177,8 @@ export function canonicalizeTrustedPath(target: string): string {
|
|
|
164
177
|
for (;;) {
|
|
165
178
|
const identity = canonicalExistingDirectoryIdentity(base) as NativeIdentity;
|
|
166
179
|
if (identity.ok) {
|
|
167
|
-
|
|
180
|
+
const canonicalBase = canonicalExistingPathForIo(base, identity);
|
|
181
|
+
return suffix.length === 0 ? canonicalBase : path.join(canonicalBase, ...suffix);
|
|
168
182
|
}
|
|
169
183
|
if (identity.code !== "not_found" && identity.code !== "not_directory") return path.resolve(target);
|
|
170
184
|
const parent = path.dirname(base);
|