@gajae-code/coding-agent 0.12.10 → 0.12.11
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 +9 -2
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +12 -2
- package/dist/types/session/session-manager.d.ts +17 -0
- package/dist/types/session/session-storage.d.ts +1 -0
- package/dist/types/tools/computer.d.ts +35 -2
- package/dist/types/tools/todo-write.d.ts +2 -0
- package/package.json +8 -8
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/prompts/tools/computer.md +6 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
- package/src/session/agent-session.ts +111 -21
- package/src/session/internal/managed-session-storage.ts +244 -246
- package/src/session/session-manager.ts +140 -6
- package/src/session/session-storage.ts +3 -0
- package/src/tools/computer.ts +322 -45
- package/src/tools/todo-write.ts +29 -11
|
@@ -899,6 +899,7 @@ export interface ResumeSessionIdentity {
|
|
|
899
899
|
size: number;
|
|
900
900
|
mtimeMs: number;
|
|
901
901
|
mtimeNs: bigint;
|
|
902
|
+
ctimeNs?: bigint;
|
|
902
903
|
sha256: string;
|
|
903
904
|
}
|
|
904
905
|
|
|
@@ -918,6 +919,21 @@ export interface ResumeTailError {
|
|
|
918
919
|
}
|
|
919
920
|
|
|
920
921
|
export type SessionDirectoryMigrationPolicy = ManagedMigrationPolicy;
|
|
922
|
+
export type SessionAppendPersistenceFailurePhase = "current_append" | "prior_failure";
|
|
923
|
+
|
|
924
|
+
export class SessionAppendPersistenceError extends Error {
|
|
925
|
+
readonly phase: SessionAppendPersistenceFailurePhase;
|
|
926
|
+
readonly entryId: string;
|
|
927
|
+
readonly persistenceError: Error;
|
|
928
|
+
|
|
929
|
+
constructor(phase: SessionAppendPersistenceFailurePhase, entryId: string, persistenceError: Error) {
|
|
930
|
+
super(persistenceError.message, { cause: persistenceError });
|
|
931
|
+
this.name = "SessionAppendPersistenceError";
|
|
932
|
+
this.phase = phase;
|
|
933
|
+
this.entryId = entryId;
|
|
934
|
+
this.persistenceError = persistenceError;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
921
937
|
|
|
922
938
|
export class SessionManagedStorageError extends Error {
|
|
923
939
|
readonly code = "managed_storage_unsupported";
|
|
@@ -2161,6 +2177,7 @@ function sameResumeIdentity(left: ResumeSessionIdentity, right: ResumeSessionIde
|
|
|
2161
2177
|
left.size === right.size &&
|
|
2162
2178
|
left.mtimeMs === right.mtimeMs &&
|
|
2163
2179
|
left.mtimeNs === right.mtimeNs &&
|
|
2180
|
+
(left.ctimeNs === undefined || right.ctimeNs === undefined || left.ctimeNs === right.ctimeNs) &&
|
|
2164
2181
|
left.sha256 === right.sha256
|
|
2165
2182
|
);
|
|
2166
2183
|
}
|
|
@@ -2174,7 +2191,8 @@ function sameResumeStat(left: SessionStorageStat, right: SessionStorageStat): bo
|
|
|
2174
2191
|
left.nlink === right.nlink &&
|
|
2175
2192
|
left.size === right.size &&
|
|
2176
2193
|
left.mtimeMs === right.mtimeMs &&
|
|
2177
|
-
left.mtimeNs === right.mtimeNs
|
|
2194
|
+
left.mtimeNs === right.mtimeNs &&
|
|
2195
|
+
left.ctimeNs === right.ctimeNs
|
|
2178
2196
|
);
|
|
2179
2197
|
}
|
|
2180
2198
|
|
|
@@ -2372,6 +2390,7 @@ function inspectResumeSessionFile(
|
|
|
2372
2390
|
size: snapshot.size,
|
|
2373
2391
|
mtimeMs: snapshot.mtimeMs,
|
|
2374
2392
|
mtimeNs: snapshot.mtimeNs,
|
|
2393
|
+
ctimeNs: snapshot.ctimeNs,
|
|
2375
2394
|
sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
|
|
2376
2395
|
};
|
|
2377
2396
|
return { identity, content: bytes, entries, context, migrationApplied };
|
|
@@ -2380,6 +2399,46 @@ function inspectResumeSessionFile(
|
|
|
2380
2399
|
}
|
|
2381
2400
|
}
|
|
2382
2401
|
|
|
2402
|
+
function fsyncResumeSessionIdentity(expected: ResumeSessionIdentity): void {
|
|
2403
|
+
const fd = fs.openSync(expected.canonicalPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
2404
|
+
try {
|
|
2405
|
+
const before = fs.fstatSync(fd, { bigint: true });
|
|
2406
|
+
if (
|
|
2407
|
+
before.dev !== expected.dev ||
|
|
2408
|
+
before.ino !== expected.ino ||
|
|
2409
|
+
(expected.nlink !== undefined && before.nlink !== expected.nlink) ||
|
|
2410
|
+
Number(before.size) !== expected.size ||
|
|
2411
|
+
before.mtimeNs !== expected.mtimeNs ||
|
|
2412
|
+
(expected.ctimeNs !== undefined && before.ctimeNs !== expected.ctimeNs)
|
|
2413
|
+
) {
|
|
2414
|
+
throw new Error("session_persistence_recovery_identity_mismatch");
|
|
2415
|
+
}
|
|
2416
|
+
fs.fsyncSync(fd);
|
|
2417
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
2418
|
+
const named = fs.lstatSync(expected.canonicalPath, { bigint: true });
|
|
2419
|
+
if (
|
|
2420
|
+
after.dev !== before.dev ||
|
|
2421
|
+
after.ino !== before.ino ||
|
|
2422
|
+
after.nlink !== before.nlink ||
|
|
2423
|
+
after.size !== before.size ||
|
|
2424
|
+
after.mtimeNs !== before.mtimeNs ||
|
|
2425
|
+
after.ctimeNs !== before.ctimeNs ||
|
|
2426
|
+
named.dev !== after.dev ||
|
|
2427
|
+
named.ino !== after.ino ||
|
|
2428
|
+
named.nlink !== after.nlink
|
|
2429
|
+
) {
|
|
2430
|
+
throw new Error("session_persistence_recovery_identity_mismatch");
|
|
2431
|
+
}
|
|
2432
|
+
} finally {
|
|
2433
|
+
fs.closeSync(fd);
|
|
2434
|
+
}
|
|
2435
|
+
const parentFd = fs.openSync(path.dirname(expected.canonicalPath), fs.constants.O_RDONLY | fs.constants.O_DIRECTORY);
|
|
2436
|
+
try {
|
|
2437
|
+
fs.fsyncSync(parentFd);
|
|
2438
|
+
} finally {
|
|
2439
|
+
fs.closeSync(parentFd);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2383
2442
|
/** Revalidate previously parsed authority without another decode, parse, migration, or context build. */
|
|
2384
2443
|
function revalidateResumeSessionIdentity(
|
|
2385
2444
|
filePath: string,
|
|
@@ -2410,6 +2469,7 @@ function revalidateResumeSessionIdentity(
|
|
|
2410
2469
|
size: snapshot.stat.size,
|
|
2411
2470
|
mtimeMs: snapshot.stat.mtimeMs,
|
|
2412
2471
|
mtimeNs: snapshot.stat.mtimeNs,
|
|
2472
|
+
ctimeNs: snapshot.stat.ctimeNs,
|
|
2413
2473
|
sha256: crypto.createHash("sha256").update(snapshot.bytes).digest("hex"),
|
|
2414
2474
|
};
|
|
2415
2475
|
return sameResumeIdentity(expected, observed) ? { kind: "valid" } : { kind: "error", reason: "identity-mismatch" };
|
|
@@ -4751,7 +4811,9 @@ export class SessionManager {
|
|
|
4751
4811
|
#recoveryPromotionTranscriptPath: string | undefined;
|
|
4752
4812
|
#memoryGuardParticipantIngressToken: symbol | undefined;
|
|
4753
4813
|
#fileEntries: FileEntry[] = [];
|
|
4754
|
-
#pendingStrictAdoption:
|
|
4814
|
+
#pendingStrictAdoption:
|
|
4815
|
+
| { canonicalPath: string; identity: ResumeSessionIdentity; inspection?: ResumeInspectionSnapshot }
|
|
4816
|
+
| undefined;
|
|
4755
4817
|
#byId: Map<string, SessionEntry> = new Map();
|
|
4756
4818
|
#labelsById: Map<string, string> = new Map();
|
|
4757
4819
|
#leafId: string | null = null;
|
|
@@ -5501,11 +5563,14 @@ export class SessionManager {
|
|
|
5501
5563
|
if (strictAdoption && resolvedSessionFile === strictAdoption.canonicalPath) {
|
|
5502
5564
|
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
|
|
5503
5565
|
if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
|
|
5504
|
-
throw new Error("Prepared
|
|
5566
|
+
throw new Error("Prepared session changed before strict adoption.");
|
|
5505
5567
|
}
|
|
5506
5568
|
let entries: FileEntry[];
|
|
5507
5569
|
let candidateMigrationApplied = false;
|
|
5508
|
-
if (
|
|
5570
|
+
if (strictAdoption?.inspection && resolvedSessionFile === strictAdoption.canonicalPath) {
|
|
5571
|
+
entries = strictAdoption.inspection.entries;
|
|
5572
|
+
candidateMigrationApplied = strictAdoption.inspection.migrationApplied;
|
|
5573
|
+
} else if (this.storage.existsSync(resolvedSessionFile)) {
|
|
5509
5574
|
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
|
|
5510
5575
|
if ("kind" in inspected) throw new Error(`Could not switch session: ${inspected.reason}`);
|
|
5511
5576
|
entries = inspected.entries;
|
|
@@ -5516,7 +5581,7 @@ export class SessionManager {
|
|
|
5516
5581
|
if (strictAdoption) {
|
|
5517
5582
|
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
|
|
5518
5583
|
if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
|
|
5519
|
-
throw new Error("Prepared
|
|
5584
|
+
throw new Error("Prepared session changed during strict adoption.");
|
|
5520
5585
|
}
|
|
5521
5586
|
if (entries.length > 0) {
|
|
5522
5587
|
const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
@@ -5541,6 +5606,13 @@ export class SessionManager {
|
|
|
5541
5606
|
prepared.dispose();
|
|
5542
5607
|
throw error;
|
|
5543
5608
|
}
|
|
5609
|
+
if (strictAdoption) {
|
|
5610
|
+
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
|
|
5611
|
+
if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity)) {
|
|
5612
|
+
prepared.dispose();
|
|
5613
|
+
throw new Error("Prepared session changed before strict adoption commit.");
|
|
5614
|
+
}
|
|
5615
|
+
}
|
|
5544
5616
|
const previous = {
|
|
5545
5617
|
sessionId: this.#sessionId,
|
|
5546
5618
|
sessionName: this.#sessionName,
|
|
@@ -8265,13 +8337,31 @@ export class SessionManager {
|
|
|
8265
8337
|
this.#assertRecoveryHydrationWritable();
|
|
8266
8338
|
const normalizedEntry = normalizeSessionEntryForStorage(entry);
|
|
8267
8339
|
const residentEntry = this.#prepareEntryForCurrentResidentStore(normalizedEntry) as SessionEntry;
|
|
8340
|
+
const previousLeafId = this.#leafId;
|
|
8341
|
+
const priorPersistenceError = this.#persistError;
|
|
8268
8342
|
this.#fileEntries.push(residentEntry);
|
|
8269
8343
|
this.#byId.set(residentEntry.id, residentEntry);
|
|
8270
8344
|
this.#leafId = residentEntry.id;
|
|
8271
8345
|
this.#bumpEntryRevision();
|
|
8272
8346
|
this.#leafRevision++;
|
|
8273
8347
|
if (entry.type === "label") this.#labelRevision++;
|
|
8274
|
-
|
|
8348
|
+
try {
|
|
8349
|
+
this._persist(residentEntry);
|
|
8350
|
+
} catch (error) {
|
|
8351
|
+
const removed = this.#fileEntries.pop();
|
|
8352
|
+
if (removed !== residentEntry)
|
|
8353
|
+
throw new Error("Session append rollback lost resident ordering.", { cause: error });
|
|
8354
|
+
this.#byId.delete(residentEntry.id);
|
|
8355
|
+
this.#leafId = previousLeafId;
|
|
8356
|
+
this.#bumpEntryRevision();
|
|
8357
|
+
this.#leafRevision++;
|
|
8358
|
+
if (entry.type === "label") this.#labelRevision++;
|
|
8359
|
+
throw new SessionAppendPersistenceError(
|
|
8360
|
+
priorPersistenceError ? "prior_failure" : "current_append",
|
|
8361
|
+
residentEntry.id,
|
|
8362
|
+
this.#persistError ?? toError(error),
|
|
8363
|
+
);
|
|
8364
|
+
}
|
|
8275
8365
|
// Same validated, overflow-guarded aggregation as the resume path (#buildIndex): a
|
|
8276
8366
|
// malformed task-result or assistant usage shape reaching the append path must not
|
|
8277
8367
|
// crash or poison getUsageStatistics() either.
|
|
@@ -8513,6 +8603,50 @@ export class SessionManager {
|
|
|
8513
8603
|
this.#replayMetadataRevision++;
|
|
8514
8604
|
}
|
|
8515
8605
|
|
|
8606
|
+
/**
|
|
8607
|
+
* Rehydrate the canonical transcript after a synchronous persistence failure.
|
|
8608
|
+
*
|
|
8609
|
+
* The failed append may have committed before reporting an uncertain outcome,
|
|
8610
|
+
* so callers must not clear the sticky error or retry against the resident
|
|
8611
|
+
* branch. Reloading the exact session file is the only supported recovery
|
|
8612
|
+
* boundary for both managed and explicit persistent destinations.
|
|
8613
|
+
*/
|
|
8614
|
+
async recoverPersistenceFailure(): Promise<void> {
|
|
8615
|
+
const persistenceError = this.#persistError;
|
|
8616
|
+
if (!persistenceError) return;
|
|
8617
|
+
if (!this.#sessionFile) throw persistenceError;
|
|
8618
|
+
const sessionFile = this.#sessionFile;
|
|
8619
|
+
try {
|
|
8620
|
+
await this.#closePersistWriter();
|
|
8621
|
+
} catch {
|
|
8622
|
+
// A writer reports its prior write failure after a confirmed close. A
|
|
8623
|
+
// second close clears that terminal writer; retryable close failures are
|
|
8624
|
+
// genuinely re-dispatched and still fail if the descriptor cannot settle.
|
|
8625
|
+
}
|
|
8626
|
+
await this.#closePersistWriter();
|
|
8627
|
+
const inspected = inspectResumeSessionFile(sessionFile, this.storage);
|
|
8628
|
+
if ("kind" in inspected) throw persistenceError;
|
|
8629
|
+
try {
|
|
8630
|
+
fsyncResumeSessionIdentity(inspected.identity);
|
|
8631
|
+
} catch {
|
|
8632
|
+
throw persistenceError;
|
|
8633
|
+
}
|
|
8634
|
+
const durable = inspectResumeSessionFile(sessionFile, this.storage);
|
|
8635
|
+
if ("kind" in durable || !sameResumeIdentity(inspected.identity, durable.identity)) {
|
|
8636
|
+
throw persistenceError;
|
|
8637
|
+
}
|
|
8638
|
+
const adoption = {
|
|
8639
|
+
canonicalPath: durable.identity.canonicalPath,
|
|
8640
|
+
identity: durable.identity,
|
|
8641
|
+
inspection: durable,
|
|
8642
|
+
};
|
|
8643
|
+
this.#pendingStrictAdoption = adoption;
|
|
8644
|
+
try {
|
|
8645
|
+
await this.setSessionFile(durable.identity.canonicalPath);
|
|
8646
|
+
} finally {
|
|
8647
|
+
if (this.#pendingStrictAdoption === adoption) this.#pendingStrictAdoption = undefined;
|
|
8648
|
+
}
|
|
8649
|
+
}
|
|
8516
8650
|
/**
|
|
8517
8651
|
* Rewrite the session file after in-place entry updates.
|
|
8518
8652
|
* Use sparingly (e.g., pruning old tool outputs).
|
|
@@ -28,6 +28,7 @@ export interface SessionStorageStat {
|
|
|
28
28
|
size: number;
|
|
29
29
|
mtimeMs: number;
|
|
30
30
|
mtimeNs: bigint;
|
|
31
|
+
ctimeNs: bigint;
|
|
31
32
|
mtime: Date;
|
|
32
33
|
isFile: boolean;
|
|
33
34
|
}
|
|
@@ -47,6 +48,7 @@ function statFromNode(stats: fs.BigIntStats): SessionStorageStat {
|
|
|
47
48
|
size: Number(stats.size),
|
|
48
49
|
mtimeMs: Number(stats.mtimeMs),
|
|
49
50
|
mtimeNs: stats.mtimeNs,
|
|
51
|
+
ctimeNs: stats.ctimeNs,
|
|
50
52
|
mtime: stats.mtime,
|
|
51
53
|
isFile: stats.isFile(),
|
|
52
54
|
};
|
|
@@ -1624,6 +1626,7 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
1624
1626
|
size: entry.content.byteLength,
|
|
1625
1627
|
mtimeMs: entry.mtimeMs,
|
|
1626
1628
|
mtimeNs: BigInt(entry.mtimeMs) * 1_000_000n,
|
|
1629
|
+
ctimeNs: BigInt(entry.mtimeMs) * 1_000_000n,
|
|
1627
1630
|
mtime: new Date(entry.mtimeMs),
|
|
1628
1631
|
isFile: true,
|
|
1629
1632
|
};
|