@gajae-code/coding-agent 0.12.19 → 0.12.21
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 +27 -9
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/sdk.d.ts +66 -0
- package/dist/types/config/settings-schema.d.ts +34 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +97 -1
- package/dist/types/modes/acp/acp-agent.d.ts +33 -1
- package/dist/types/modes/components/bash-execution.d.ts +7 -0
- package/dist/types/modes/components/eval-execution.d.ts +7 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +10 -1
- package/dist/types/sdk/acp/adapter.d.ts +9 -17
- package/dist/types/sdk/acp/index.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +93 -1
- package/dist/types/sdk/broker/ensure.d.ts +17 -0
- package/dist/types/sdk/broker/index.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +10 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +81 -2
- package/dist/types/sdk/broker/startup-budget.d.ts +44 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +11 -1
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +20 -9
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -6
- package/dist/types/sdk/bus/slack-daemon.d.ts +2 -2
- package/dist/types/sdk/host/query/revision-store.d.ts +5 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/lifecycle-session.d.ts +13 -2
- package/dist/types/sdk/prompt-failure.d.ts +13 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +82 -0
- package/dist/types/sdk/session-reconnect.d.ts +20 -0
- package/dist/types/sdk/startup-capability.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +32 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +40 -2
- package/dist/types/session/internal/session-memory-sidecar.d.ts +881 -0
- package/dist/types/session/session-manager.d.ts +302 -27
- package/dist/types/session/session-storage.d.ts +170 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/todo-contract.d.ts +10 -1
- package/dist/types/tui/output-block.d.ts +6 -0
- package/package.json +8 -8
- package/src/cli/args.ts +75 -34
- package/src/commands/acp.ts +7 -1
- package/src/commands/gc.ts +31 -2
- package/src/commands/sdk.ts +141 -7
- package/src/config/settings-schema.ts +43 -0
- package/src/coordinator-mcp/server.ts +6 -1
- package/src/export/html/index.ts +83 -39
- package/src/gjc-runtime/gc-runtime.ts +1283 -4
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/local-protocol.ts +14 -0
- package/src/main.ts +40 -9
- package/src/modes/acp/acp-agent.ts +564 -37
- package/src/modes/acp/terminal-auth.ts +2 -2
- package/src/modes/components/bash-execution.ts +14 -0
- package/src/modes/components/eval-execution.ts +14 -0
- package/src/modes/controllers/command-controller.ts +63 -8
- package/src/modes/utils/ui-helpers.ts +81 -11
- package/src/sdk/acp/adapter.ts +46 -45
- package/src/sdk/acp/index.ts +1 -1
- package/src/sdk/broker/broker.ts +392 -11
- package/src/sdk/broker/ensure.ts +131 -61
- package/src/sdk/broker/index.ts +1 -0
- package/src/sdk/broker/lifecycle-ledger.ts +59 -5
- package/src/sdk/broker/lifecycle.ts +278 -63
- package/src/sdk/broker/startup-budget.ts +103 -0
- package/src/sdk/bus/chat-daemon-control.ts +9 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +565 -29
- package/src/sdk/bus/conversation-store.ts +1 -1
- package/src/sdk/bus/discord-daemon.ts +2 -1
- package/src/sdk/bus/discord-live-provider.ts +34 -6
- package/src/sdk/bus/index.ts +118 -20
- package/src/sdk/bus/kind-aware-reconciliation.ts +2 -1
- package/src/sdk/bus/prompt-reconciliation.ts +4 -9
- package/src/sdk/bus/slack-daemon.ts +91 -18
- package/src/sdk/cli/session-cli.ts +8 -1
- package/src/sdk/host/query/handlers.ts +1 -1
- package/src/sdk/host/query/revision-store.ts +48 -12
- package/src/sdk/host/session-runtime.test.ts +50 -0
- package/src/sdk/host/session-runtime.ts +12 -2
- package/src/sdk/lifecycle-session.ts +26 -3
- package/src/sdk/mcp/server.ts +6 -1
- package/src/sdk/prompt-failure.ts +42 -0
- package/src/sdk/prompt-watchdog.ts +162 -0
- package/src/sdk/session-reconnect.ts +43 -0
- package/src/sdk/startup-capability.ts +13 -4
- package/src/session/agent-session.ts +541 -226
- package/src/session/blob-store.ts +89 -0
- package/src/session/internal/managed-session-scope.ts +36 -26
- package/src/session/internal/managed-session-storage.ts +303 -32
- package/src/session/internal/session-memory-sidecar.ts +2160 -0
- package/src/session/session-manager.ts +7057 -399
- package/src/session/session-storage.ts +1134 -11
- package/src/task/executor.ts +17 -4
- package/src/tools/bash.ts +109 -7
- package/src/tools/descriptors.test.ts +51 -0
- package/src/tools/find.ts +55 -26
- package/src/tools/todo-contract.ts +82 -13
- package/src/tools/todo-write.ts +2 -5
- package/src/tui/output-block.ts +11 -1
package/src/sdk/broker/ensure.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import type { FileHandle } from "node:fs/promises";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
2
6
|
import { type BrokerDiscovery, brokerProcessIncarnation, readBrokerDiscovery } from "./discovery";
|
|
3
7
|
import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
|
|
4
8
|
export interface EnsureBrokerSettings {
|
|
@@ -18,6 +22,57 @@ const FIXTURE_DISCOVERY_TIMEOUT_MS = 30_000;
|
|
|
18
22
|
// owned-process teardown convention (SIGTERM -> grace -> SIGKILL -> hard cap).
|
|
19
23
|
const REAP_GRACEFUL_MS = 2_000;
|
|
20
24
|
const REAP_SIGKILL_CAP_MS = 2_000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Tail of the detached broker's stderr folded into a discovery failure.
|
|
28
|
+
*
|
|
29
|
+
* The broker used to spawn with `stdio: "ignore"`, so a broker that exited
|
|
30
|
+
* cleanly told the caller nothing beyond `code=0` (#3963). Its stderr goes to a
|
|
31
|
+
* file instead of a pipe because the child is detached and outlives this
|
|
32
|
+
* process: a pipe would break under it the moment the parent exits.
|
|
33
|
+
*/
|
|
34
|
+
export const BROKER_SPAWN_LOG_TAIL_BYTES = 4_096;
|
|
35
|
+
|
|
36
|
+
export interface BrokerSpawnLog {
|
|
37
|
+
path: string;
|
|
38
|
+
handle: FileHandle;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function brokerSpawnLogPath(agentDir: string): string {
|
|
42
|
+
return path.join(agentDir, "sdk", `broker-spawn.${randomUUID()}.log`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Opens an isolated, bounded-lifetime diagnostic sink for one broker spawn. */
|
|
46
|
+
export async function openBrokerSpawnLog(agentDir: string): Promise<BrokerSpawnLog | undefined> {
|
|
47
|
+
try {
|
|
48
|
+
await fs.mkdir(path.join(agentDir, "sdk"), { recursive: true, mode: 0o700 });
|
|
49
|
+
const spawnLogPath = brokerSpawnLogPath(agentDir);
|
|
50
|
+
return { path: spawnLogPath, handle: await fs.open(spawnLogPath, "w", 0o600) };
|
|
51
|
+
} catch {
|
|
52
|
+
// Diagnostics are never allowed to block a broker spawn.
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function readBrokerSpawnLogTail(spawnLogPath: string): Promise<string> {
|
|
58
|
+
try {
|
|
59
|
+
const file = Bun.file(spawnLogPath);
|
|
60
|
+
const size = file.size;
|
|
61
|
+
if (!Number.isFinite(size) || size <= 0) return "";
|
|
62
|
+
const tail = size > BROKER_SPAWN_LOG_TAIL_BYTES ? file.slice(size - BROKER_SPAWN_LOG_TAIL_BYTES) : file;
|
|
63
|
+
return (await tail.text()).trim();
|
|
64
|
+
} catch {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function removeBrokerSpawnLog(spawnLogPath: string): Promise<void> {
|
|
70
|
+
try {
|
|
71
|
+
await fs.unlink(spawnLogPath);
|
|
72
|
+
} catch {
|
|
73
|
+
// Diagnostics are best-effort and must not affect broker ownership.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
21
76
|
export interface FixtureBrokerLease {
|
|
22
77
|
/** Backward-compatible fixture cleanup alias for exact child termination. */
|
|
23
78
|
close(): Promise<void>;
|
|
@@ -245,74 +300,89 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
245
300
|
}
|
|
246
301
|
|
|
247
302
|
const command = resolveSdkInternalSpawnCommand("broker-internal");
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
303
|
+
const spawnLog = await openBrokerSpawnLog(settings.agentDir);
|
|
304
|
+
try {
|
|
305
|
+
const child = spawn(command.file, [...command.args, "--agent-dir", settings.agentDir], {
|
|
306
|
+
detached: true,
|
|
307
|
+
stdio: ["ignore", "ignore", spawnLog ? spawnLog.handle.fd : "ignore"],
|
|
308
|
+
env: brokerSpawnEnvironment(command, settings.env),
|
|
309
|
+
...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
|
|
310
|
+
});
|
|
311
|
+
// The child holds its own duplicate of the descriptor; this one is done.
|
|
312
|
+
await spawnLog?.handle.close();
|
|
313
|
+
child.unref();
|
|
314
|
+
let spawnError: Error | undefined;
|
|
315
|
+
child.once("error", error => {
|
|
316
|
+
spawnError = error;
|
|
317
|
+
});
|
|
318
|
+
const owner = registerBrokerOwner(settings.agentDir, child);
|
|
319
|
+
const discoveryTimeoutMs = initiator === "fixture-lease" ? FIXTURE_DISCOVERY_TIMEOUT_MS : DISCOVERY_TIMEOUT_MS;
|
|
320
|
+
const deadline = Date.now() + discoveryTimeoutMs;
|
|
321
|
+
let discoveryError: unknown;
|
|
322
|
+
while (Date.now() < deadline) {
|
|
323
|
+
if (spawnError || child.exitCode !== null || child.signalCode !== null) break;
|
|
324
|
+
try {
|
|
325
|
+
const discovered = await readBrokerDiscovery(settings.agentDir, settings.heartbeatTtlMs);
|
|
326
|
+
if (discovered) {
|
|
327
|
+
if (owner.markReady(discovered)) {
|
|
328
|
+
return initiator === "fixture-lease"
|
|
329
|
+
? { kind: "local-started-fixture", discovery: discovered, owner, child }
|
|
330
|
+
: { kind: "local-started-discovery", discovery: discovered };
|
|
331
|
+
}
|
|
332
|
+
await owner.stop();
|
|
333
|
+
return { kind: "external-discovery", discovery: discovered };
|
|
272
334
|
}
|
|
273
|
-
|
|
274
|
-
|
|
335
|
+
} catch (error) {
|
|
336
|
+
discoveryError = error;
|
|
275
337
|
}
|
|
276
|
-
|
|
277
|
-
discoveryError = error;
|
|
338
|
+
await sleep(50);
|
|
278
339
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
340
|
+
const exitedBeforeDiscovery = child.exitCode !== null || child.signalCode !== null;
|
|
341
|
+
if (exitedBeforeDiscovery && child.exitCode === 0) {
|
|
342
|
+
// A clean exit means another broker won the ownership lock (two ACP
|
|
343
|
+
// processes racing a cold broker state, e.g. a provider probe and an
|
|
344
|
+
// agent launch). The winner may publish its discovery right after our
|
|
345
|
+
// last poll; reuse it instead of failing the caller. Transient discovery
|
|
346
|
+
// read failures fall through to the common cleanup + failure path below.
|
|
347
|
+
try {
|
|
348
|
+
for (let retry = 0; retry < 20; retry++) {
|
|
349
|
+
const winner = await readBrokerDiscovery(settings.agentDir, settings.heartbeatTtlMs);
|
|
350
|
+
if (winner) {
|
|
351
|
+
await owner.stop();
|
|
352
|
+
return { kind: "external-discovery", discovery: winner };
|
|
353
|
+
}
|
|
354
|
+
await sleep(50);
|
|
294
355
|
}
|
|
295
|
-
|
|
356
|
+
} catch {
|
|
357
|
+
// fall through to cleanup + failure
|
|
296
358
|
}
|
|
297
|
-
} catch {
|
|
298
|
-
// fall through to cleanup + failure
|
|
299
359
|
}
|
|
360
|
+
const spawnLogTail = exitedBeforeDiscovery && spawnLog ? await readBrokerSpawnLogTail(spawnLog.path) : "";
|
|
361
|
+
const failure = spawnError
|
|
362
|
+
? new Error(`Failed to spawn detached SDK broker: ${spawnError.message}`)
|
|
363
|
+
: exitedBeforeDiscovery
|
|
364
|
+
? new Error(
|
|
365
|
+
`Detached SDK broker exited before discovery (code=${child.exitCode}, signal=${child.signalCode}).${
|
|
366
|
+
spawnLogTail
|
|
367
|
+
? ` Broker stderr: ${spawnLogTail}`
|
|
368
|
+
: " The broker wrote no stderr; see the gjc log for its lock-contention diagnostic."
|
|
369
|
+
}`,
|
|
370
|
+
)
|
|
371
|
+
: discoveryError
|
|
372
|
+
? discoveryError
|
|
373
|
+
: new Error("Timed out waiting for detached SDK broker discovery.");
|
|
374
|
+
try {
|
|
375
|
+
await owner.stop();
|
|
376
|
+
} catch (cleanupError) {
|
|
377
|
+
throw new AggregateError(
|
|
378
|
+
[failure, cleanupError],
|
|
379
|
+
"SDK broker discovery and spawned broker cleanup both failed.",
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
throw failure;
|
|
383
|
+
} finally {
|
|
384
|
+
if (spawnLog) await removeBrokerSpawnLog(spawnLog.path);
|
|
300
385
|
}
|
|
301
|
-
const failure = spawnError
|
|
302
|
-
? new Error(`Failed to spawn detached SDK broker: ${spawnError.message}`)
|
|
303
|
-
: exitedBeforeDiscovery
|
|
304
|
-
? new Error(
|
|
305
|
-
`Detached SDK broker exited before discovery (code=${child.exitCode}, signal=${child.signalCode}).`,
|
|
306
|
-
)
|
|
307
|
-
: discoveryError
|
|
308
|
-
? discoveryError
|
|
309
|
-
: new Error("Timed out waiting for detached SDK broker discovery.");
|
|
310
|
-
try {
|
|
311
|
-
await owner.stop();
|
|
312
|
-
} catch (cleanupError) {
|
|
313
|
-
throw new AggregateError([failure, cleanupError], "SDK broker discovery and spawned broker cleanup both failed.");
|
|
314
|
-
}
|
|
315
|
-
throw failure;
|
|
316
386
|
}
|
|
317
387
|
|
|
318
388
|
function startEnsure(settings: EnsureBrokerSettings, initiator: EnsureInitiator): EnsureInFlight {
|
package/src/sdk/broker/index.ts
CHANGED
|
@@ -99,6 +99,11 @@ export interface LifecycleLedgerLimits {
|
|
|
99
99
|
maxBytes?: number;
|
|
100
100
|
maxLineBytes?: number;
|
|
101
101
|
maxRows?: number;
|
|
102
|
+
/**
|
|
103
|
+
* Cap for the `.corrupt` quarantine sidecar. Reaching it rotates one
|
|
104
|
+
* generation aside instead of appending forever.
|
|
105
|
+
*/
|
|
106
|
+
maxCorruptBytes?: number;
|
|
102
107
|
}
|
|
103
108
|
function canonicalCleanupSessionId(value: unknown): value is string {
|
|
104
109
|
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
|
|
@@ -108,7 +113,11 @@ const DEFAULT_LIFECYCLE_LEDGER_LIMITS: Required<LifecycleLedgerLimits> = {
|
|
|
108
113
|
maxBytes: 64 * 1024 * 1024,
|
|
109
114
|
maxLineBytes: 8 * 1024 * 1024,
|
|
110
115
|
maxRows: 10_000,
|
|
116
|
+
maxCorruptBytes: 8 * 1024 * 1024,
|
|
111
117
|
};
|
|
118
|
+
|
|
119
|
+
/** Appended when a quarantined row is clipped to the sidecar cap. */
|
|
120
|
+
const LIFECYCLE_QUARANTINE_TRUNCATION_MARKER = "\n[gjc: quarantined row truncated at the corrupt-ledger cap]";
|
|
112
121
|
const MAX_LIFECYCLE_LEDGER_JSON_DEPTH = 64;
|
|
113
122
|
const MAX_LIFECYCLE_LEDGER_JSON_FIELDS = 1024;
|
|
114
123
|
|
|
@@ -251,6 +260,7 @@ export class LifecycleLedger {
|
|
|
251
260
|
maxBytes: limits.maxBytes ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxBytes,
|
|
252
261
|
maxLineBytes: limits.maxLineBytes ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxLineBytes,
|
|
253
262
|
maxRows: limits.maxRows ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxRows,
|
|
263
|
+
maxCorruptBytes: limits.maxCorruptBytes ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxCorruptBytes,
|
|
254
264
|
};
|
|
255
265
|
}
|
|
256
266
|
async open(): Promise<this> {
|
|
@@ -353,12 +363,16 @@ export class LifecycleLedger {
|
|
|
353
363
|
return this;
|
|
354
364
|
}
|
|
355
365
|
/**
|
|
356
|
-
* Reads terminal
|
|
366
|
+
* Reads the durable terminal record for one request without recovering or changing the ledger.
|
|
357
367
|
*
|
|
358
368
|
* This intentionally accepts an unrelated unterminated final write: concurrent
|
|
359
369
|
* appenders may have started a different row after this request's synced terminal
|
|
360
370
|
* row. Complete rows are still decoded and validated strictly, and any incomplete
|
|
361
371
|
* tail that identifies this request withholds proof.
|
|
372
|
+
*
|
|
373
|
+
* `terminal_uncertain` is a durable terminal record too: an operation that concluded
|
|
374
|
+
* uncertainly persists that conclusion, and refusing to read it back would report a
|
|
375
|
+
* synced row as unpersisted and replace its true reason with a generic one.
|
|
362
376
|
*/
|
|
363
377
|
async readTerminal(identity: string, requestHash: string): Promise<LifecycleLedgerEntry | undefined> {
|
|
364
378
|
const source = await this.#readBoundedSourceReadOnly();
|
|
@@ -403,12 +417,20 @@ export class LifecycleLedger {
|
|
|
403
417
|
const identityMarker = Buffer.from(`"identity":${JSON.stringify(identity)}`);
|
|
404
418
|
if (tail.includes(identityMarker)) return undefined;
|
|
405
419
|
}
|
|
406
|
-
return latest &&
|
|
420
|
+
return latest && final(latest.state) ? latest : undefined;
|
|
407
421
|
}
|
|
408
422
|
|
|
423
|
+
/**
|
|
424
|
+
* A recovery pass appends `terminal_uncertain` for every row it finds mid-flight, so a
|
|
425
|
+
* long-running operation can have that marker interleaved before its own terminal row
|
|
426
|
+
* lands. The marker records unresolved work, not a proven outcome: the owner that ran
|
|
427
|
+
* the effects stays authoritative and may still resolve it. A proven terminal row is
|
|
428
|
+
* immutable and admits no successor.
|
|
429
|
+
*/
|
|
409
430
|
#isValidHistoryContinuation(previous: LifecycleLedgerEntry | undefined, next: LifecycleLedgerEntry): boolean {
|
|
410
431
|
if (!previous) return next.state === "accepted";
|
|
411
|
-
if (previous.requestHash !== next.requestHash ||
|
|
432
|
+
if (previous.requestHash !== next.requestHash || terminal(previous.state)) return false;
|
|
433
|
+
if (previous.state === "terminal_uncertain") return final(next.state);
|
|
412
434
|
if (previous.state === "accepted") return true;
|
|
413
435
|
return next.state === "effect_started" || next.state === "awaiting_ready" || final(next.state);
|
|
414
436
|
}
|
|
@@ -533,11 +555,43 @@ export class LifecycleLedger {
|
|
|
533
555
|
await h.close();
|
|
534
556
|
}
|
|
535
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Rotate the quarantine sidecar aside once it would exceed its cap.
|
|
560
|
+
*
|
|
561
|
+
* Quarantine is diagnostic, not durable state: an unreadable ledger
|
|
562
|
+
* re-quarantines its whole contents on every open, so a plain append grows
|
|
563
|
+
* the sidecar without bound (a 126 MB `.corrupt` next to a 6 MB live ledger,
|
|
564
|
+
* #3963). One retained generation keeps the newest evidence while bounding
|
|
565
|
+
* the pair at twice the cap.
|
|
566
|
+
*/
|
|
567
|
+
async #rotateCorruptWhenFull(incomingBytes: number): Promise<void> {
|
|
568
|
+
let size: number;
|
|
569
|
+
try {
|
|
570
|
+
const stat = await fs.lstat(this.#corruptFile);
|
|
571
|
+
// A non-regular quarantine path is never rotated; the append guard rejects it.
|
|
572
|
+
if (!stat.isFile()) return;
|
|
573
|
+
size = stat.size;
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
if (size + incomingBytes <= this.#limits.maxCorruptBytes) return;
|
|
579
|
+
await fs.rename(this.#corruptFile, `${this.#corruptFile}.1`);
|
|
580
|
+
}
|
|
581
|
+
|
|
536
582
|
async #quarantine(line: string | Uint8Array): Promise<void> {
|
|
583
|
+
const payload =
|
|
584
|
+
typeof line === "string" ? Buffer.from(line) : Buffer.from(line.buffer, line.byteOffset, line.byteLength);
|
|
585
|
+
// A single quarantined row can be as large as the whole ledger, so the
|
|
586
|
+
// write itself is capped too; otherwise one oversized row would blow past
|
|
587
|
+
// the rotation bound it just triggered.
|
|
588
|
+
const truncated = payload.length > this.#limits.maxCorruptBytes;
|
|
589
|
+
const retained = truncated ? payload.subarray(0, this.#limits.maxCorruptBytes) : payload;
|
|
590
|
+
await this.#rotateCorruptWhenFull(retained.length + 1);
|
|
537
591
|
const h = await this.#openAppendRegular(this.#corruptFile);
|
|
538
592
|
try {
|
|
539
|
-
await h.writeFile(
|
|
540
|
-
await h.writeFile("\n");
|
|
593
|
+
await h.writeFile(retained);
|
|
594
|
+
await h.writeFile(truncated ? `${LIFECYCLE_QUARANTINE_TRUNCATION_MARKER}\n` : "\n");
|
|
541
595
|
await h.sync();
|
|
542
596
|
} finally {
|
|
543
597
|
await h.close();
|