@gajae-code/coding-agent 0.11.10 → 0.11.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 +38 -0
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +57 -2
- package/dist/types/config/settings.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/modes/acp/acp-agent.d.ts +14 -9
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -1
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
- package/dist/types/sdk/host/control/operations.d.ts +1 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +9 -11
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
- package/dist/types/session/session-manager.d.ts +34 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/resource-gc.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli.ts +48 -2
- package/src/commands/sdk.ts +91 -7
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +24 -0
- package/src/config/settings-schema.ts +61 -2
- package/src/config/settings.ts +8 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +14 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
- package/src/gjc-runtime/deep-interview-state.ts +25 -3
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/ralplan-runtime.ts +252 -1
- package/src/gjc-runtime/state-runtime.ts +48 -11
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/modes/acp/acp-agent.ts +554 -60
- package/src/modes/acp/acp-event-mapper.ts +93 -6
- package/src/modes/components/model-selector.ts +15 -1
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +44 -3
- package/src/modes/interactive-mode.ts +65 -26
- package/src/modes/runtime-init.ts +1 -0
- package/src/prompts/agents/executor.md +1 -1
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/lifecycle.ts +82 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/index.ts +162 -22
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
- package/src/sdk/bus/telegram-daemon.ts +531 -53
- package/src/sdk/host/control/dispatch.ts +2 -0
- package/src/sdk/host/control/operations.ts +1 -0
- package/src/sdk/host/host.ts +2 -2
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/protocol/operation-inventory.generated.json +47 -0
- package/src/sdk/protocol/operation-registry.ts +1 -0
- package/src/sdk/session.ts +12 -1
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +256 -142
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +40 -10
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/session-manager.ts +608 -25
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +2 -0
- package/src/task/types.ts +22 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/resource-gc.ts +496 -120
- package/src/workflow/workflow-intent-diff.ts +4 -1
|
@@ -134,6 +134,36 @@ export interface NewSessionOptions {
|
|
|
134
134
|
drop?: boolean;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/** Internal successor prepared without changing the manager's visible identity. */
|
|
138
|
+
export interface PreparedNewSession {
|
|
139
|
+
readonly sessionId: string;
|
|
140
|
+
readonly sessionFile: string | undefined;
|
|
141
|
+
readonly artifactsDir: string | null;
|
|
142
|
+
readonly managedLegacyLocalMigrationSource: ManagedLegacyLocalMigrationSource | null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface PreparedSessionIndex {
|
|
146
|
+
readonly byId: Map<string, SessionEntry>;
|
|
147
|
+
readonly labelsById: Map<string, string>;
|
|
148
|
+
readonly leafId: string | null;
|
|
149
|
+
readonly usageStatistics: UsageStatistics;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface PreparedNewSessionState extends PreparedNewSession {
|
|
153
|
+
readonly header: SessionHeader;
|
|
154
|
+
readonly fileEntries: FileEntry[];
|
|
155
|
+
readonly sessionName: string | undefined;
|
|
156
|
+
readonly titleSource: "auto" | "user" | undefined;
|
|
157
|
+
residentFileEntries?: FileEntry[];
|
|
158
|
+
residentTextBlobStore?: BlobStore;
|
|
159
|
+
index?: PreparedSessionIndex;
|
|
160
|
+
flushed: boolean;
|
|
161
|
+
committed: boolean;
|
|
162
|
+
discarded: boolean;
|
|
163
|
+
persistenceWriter?: NdjsonFileWriter;
|
|
164
|
+
persistenceTempPath?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
137
167
|
export interface SessionEntryBase {
|
|
138
168
|
type: string;
|
|
139
169
|
id: string;
|
|
@@ -3977,6 +4007,7 @@ export class SessionManager {
|
|
|
3977
4007
|
#sessionId: string = "";
|
|
3978
4008
|
/** True once a lifecycle pre-allocated id has been adopted (consume-once). */
|
|
3979
4009
|
#lifecycleIdAdopted: boolean = false;
|
|
4010
|
+
#preparedNewSessions = new Set<PreparedNewSessionState>();
|
|
3980
4011
|
#sessionName: string | undefined;
|
|
3981
4012
|
#titleSource: "auto" | "user" | undefined;
|
|
3982
4013
|
#sessionFile: string | undefined;
|
|
@@ -4002,6 +4033,8 @@ export class SessionManager {
|
|
|
4002
4033
|
#persistChain: Promise<void> = Promise.resolve();
|
|
4003
4034
|
#persistError: Error | undefined;
|
|
4004
4035
|
#persistErrorReported = false;
|
|
4036
|
+
/** Failed staged persistence retains its exact writer and temporary pathname for retryable cleanup. */
|
|
4037
|
+
#preparedNewSessionCleanupInProgress = false;
|
|
4005
4038
|
|
|
4006
4039
|
#artifactManager: ArtifactManager | null = null;
|
|
4007
4040
|
#artifactManagerSessionFile: string | null = null;
|
|
@@ -4106,6 +4139,21 @@ export class SessionManager {
|
|
|
4106
4139
|
this.#buildIndex();
|
|
4107
4140
|
}
|
|
4108
4141
|
|
|
4142
|
+
#preparePreparedNewSessionForCommit(stage: PreparedNewSessionState): void {
|
|
4143
|
+
const residentTextBlobStore = new MemoryBlobStore();
|
|
4144
|
+
const residentFileEntries = stage.fileEntries.map(entry =>
|
|
4145
|
+
prepareEntryForResidentSync(entry, {
|
|
4146
|
+
textStore: residentTextBlobStore,
|
|
4147
|
+
imageStore: this.#residentImageBlobStore,
|
|
4148
|
+
sessionId: stage.sessionId,
|
|
4149
|
+
sessionFile: stage.sessionFile,
|
|
4150
|
+
}),
|
|
4151
|
+
);
|
|
4152
|
+
stage.residentTextBlobStore = residentTextBlobStore;
|
|
4153
|
+
stage.residentFileEntries = residentFileEntries;
|
|
4154
|
+
stage.index = this.#buildIndexForEntries(residentFileEntries, stage.sessionFile);
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4109
4157
|
#resetMaterializedCaches(): void {
|
|
4110
4158
|
this.#materializedEntriesRevision = -1;
|
|
4111
4159
|
this.#materializedEntriesCache = undefined;
|
|
@@ -4313,10 +4361,492 @@ export class SessionManager {
|
|
|
4313
4361
|
|
|
4314
4362
|
/** Start a new session. Closes any existing writer first. */
|
|
4315
4363
|
async newSession(options?: NewSessionOptions): Promise<string | undefined> {
|
|
4364
|
+
const prepared = await this.prepareNewSession(options);
|
|
4365
|
+
try {
|
|
4366
|
+
this.commitPreparedNewSession(prepared);
|
|
4367
|
+
} catch (error) {
|
|
4368
|
+
try {
|
|
4369
|
+
await this.discardPreparedNewSession(prepared);
|
|
4370
|
+
} catch (cleanupError) {
|
|
4371
|
+
throw new AggregateError(
|
|
4372
|
+
[error, cleanupError],
|
|
4373
|
+
"New session adoption and staged session cleanup both failed.",
|
|
4374
|
+
);
|
|
4375
|
+
}
|
|
4376
|
+
throw error;
|
|
4377
|
+
}
|
|
4378
|
+
return prepared.sessionFile;
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
/**
|
|
4382
|
+
* Allocate a fresh successor without publishing it through the manager's public
|
|
4383
|
+
* getters. The returned authority is deliberately immutable so readiness work
|
|
4384
|
+
* can resolve local:// against the successor while the predecessor stays live.
|
|
4385
|
+
* @internal
|
|
4386
|
+
*/
|
|
4387
|
+
async prepareNewSession(options?: NewSessionOptions): Promise<PreparedNewSession> {
|
|
4388
|
+
await this.#retryPreparedNewSessionCleanups();
|
|
4389
|
+
await this.#closePersistWriter();
|
|
4390
|
+
const preallocated = this.#lifecycleIdAdopted ? undefined : lifecyclePreallocatedSessionId();
|
|
4391
|
+
const sessionId = preallocated ?? createSessionId();
|
|
4392
|
+
const timestamp = new Date().toISOString();
|
|
4393
|
+
const sessionFile = this.persist
|
|
4394
|
+
? path.join(this.getSessionDir(), `${timestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`)
|
|
4395
|
+
: undefined;
|
|
4396
|
+
const header: SessionHeader = {
|
|
4397
|
+
type: "session",
|
|
4398
|
+
version: CURRENT_SESSION_VERSION,
|
|
4399
|
+
id: sessionId,
|
|
4400
|
+
timestamp,
|
|
4401
|
+
cwd: this.cwd,
|
|
4402
|
+
parentSession: options?.parentSession,
|
|
4403
|
+
};
|
|
4404
|
+
const stage: PreparedNewSessionState = {
|
|
4405
|
+
sessionId,
|
|
4406
|
+
sessionFile,
|
|
4407
|
+
artifactsDir: sessionFile ? sessionFile.slice(0, -6) : null,
|
|
4408
|
+
managedLegacyLocalMigrationSource: this.#managedLegacyLocalMigrationSourceFor(sessionFile),
|
|
4409
|
+
header,
|
|
4410
|
+
fileEntries: [header],
|
|
4411
|
+
sessionName: undefined,
|
|
4412
|
+
titleSource: undefined,
|
|
4413
|
+
flushed: false,
|
|
4414
|
+
committed: false,
|
|
4415
|
+
discarded: false,
|
|
4416
|
+
};
|
|
4417
|
+
this.#preparedNewSessions.add(stage);
|
|
4418
|
+
this.#freezePreparedNewSessionIdentity(stage);
|
|
4419
|
+
return stage;
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
/** Append a model selection to an unpublished successor. @internal */
|
|
4423
|
+
appendPreparedModelChange(prepared: PreparedNewSession, model: string): string {
|
|
4424
|
+
const stage = this.#getPreparedNewSessionStage(prepared);
|
|
4425
|
+
const entry: ModelChangeEntry = {
|
|
4426
|
+
type: "model_change",
|
|
4427
|
+
id: this.#nextPreparedNewSessionEntryId(stage),
|
|
4428
|
+
parentId: this.#preparedNewSessionLeafId(stage),
|
|
4429
|
+
timestamp: new Date().toISOString(),
|
|
4430
|
+
model,
|
|
4431
|
+
};
|
|
4432
|
+
stage.fileEntries.push(entry);
|
|
4433
|
+
return entry.id;
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
/** Append a thinking-level selection to an unpublished successor. @internal */
|
|
4437
|
+
appendPreparedThinkingLevelChange(prepared: PreparedNewSession, thinkingLevel?: string): string {
|
|
4438
|
+
const stage = this.#getPreparedNewSessionStage(prepared);
|
|
4439
|
+
const entry: ThinkingLevelChangeEntry = {
|
|
4440
|
+
type: "thinking_level_change",
|
|
4441
|
+
id: this.#nextPreparedNewSessionEntryId(stage),
|
|
4442
|
+
parentId: this.#preparedNewSessionLeafId(stage),
|
|
4443
|
+
timestamp: new Date().toISOString(),
|
|
4444
|
+
thinkingLevel: thinkingLevel ?? null,
|
|
4445
|
+
};
|
|
4446
|
+
stage.fileEntries.push(entry);
|
|
4447
|
+
return entry.id;
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
/** Append a service-tier selection to an unpublished successor. @internal */
|
|
4451
|
+
appendPreparedServiceTierChange(prepared: PreparedNewSession, serviceTier: ServiceTier | null): string {
|
|
4452
|
+
const stage = this.#getPreparedNewSessionStage(prepared);
|
|
4453
|
+
const entry: ServiceTierChangeEntry = {
|
|
4454
|
+
type: "service_tier_change",
|
|
4455
|
+
id: this.#nextPreparedNewSessionEntryId(stage),
|
|
4456
|
+
parentId: this.#preparedNewSessionLeafId(stage),
|
|
4457
|
+
timestamp: new Date().toISOString(),
|
|
4458
|
+
serviceTier,
|
|
4459
|
+
};
|
|
4460
|
+
stage.fileEntries.push(entry);
|
|
4461
|
+
return entry.id;
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
/** Append a displayable custom message to an unpublished successor. @internal */
|
|
4465
|
+
appendPreparedCustomMessageEntry<T = unknown>(
|
|
4466
|
+
prepared: PreparedNewSession,
|
|
4467
|
+
customType: string,
|
|
4468
|
+
content: string | (TextContent | ImageContent)[],
|
|
4469
|
+
display: boolean,
|
|
4470
|
+
details?: T,
|
|
4471
|
+
attribution: MessageAttribution = "agent",
|
|
4472
|
+
): string {
|
|
4473
|
+
const stage = this.#getPreparedNewSessionStage(prepared);
|
|
4474
|
+
const entry: CustomMessageEntry<T> = {
|
|
4475
|
+
type: "custom_message",
|
|
4476
|
+
customType,
|
|
4477
|
+
content,
|
|
4478
|
+
display,
|
|
4479
|
+
details: stripInternalDetailsFields(details),
|
|
4480
|
+
attribution,
|
|
4481
|
+
id: this.#nextPreparedNewSessionEntryId(stage),
|
|
4482
|
+
parentId: this.#preparedNewSessionLeafId(stage),
|
|
4483
|
+
timestamp: new Date().toISOString(),
|
|
4484
|
+
};
|
|
4485
|
+
stage.fileEntries.push(entry);
|
|
4486
|
+
return entry.id;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
/** Persist an unpublished successor without adopting it. @internal */
|
|
4490
|
+
async ensurePreparedNewSessionOnDisk(prepared: PreparedNewSession): Promise<void> {
|
|
4491
|
+
const stage = this.#getPreparedNewSessionStage(prepared);
|
|
4492
|
+
if (!this.persist || !stage.sessionFile || stage.flushed) return;
|
|
4493
|
+
const entries = await Promise.all(
|
|
4494
|
+
stage.fileEntries.map(entry => prepareEntryForPersistence(entry, this.#blobStore)),
|
|
4495
|
+
);
|
|
4496
|
+
if (this.destination.kind === "managed") {
|
|
4497
|
+
const bytes = Buffer.from(`${entries.map(entry => JSON.stringify(entry)).join("\n")}\n`, "utf8");
|
|
4498
|
+
await this.#managedTranscriptStore(stage.sessionFile).replace(path.basename(stage.sessionFile), bytes);
|
|
4499
|
+
} else {
|
|
4500
|
+
const staleCleanupError = await this.#cleanupPreparedNewSessionPersistence(stage);
|
|
4501
|
+
if (staleCleanupError) throw staleCleanupError;
|
|
4502
|
+
const dir = path.resolve(stage.sessionFile, "..");
|
|
4503
|
+
const tempPath = path.join(dir, `.${path.basename(stage.sessionFile)}.${Snowflake.next()}.tmp`);
|
|
4504
|
+
const writer = new NdjsonFileWriter(this.storage, tempPath, { flags: "w" });
|
|
4505
|
+
stage.persistenceTempPath = tempPath;
|
|
4506
|
+
stage.persistenceWriter = writer;
|
|
4507
|
+
try {
|
|
4508
|
+
for (const entry of entries) await writer.write(entry);
|
|
4509
|
+
await writer.flush();
|
|
4510
|
+
await writer.fsync();
|
|
4511
|
+
await writer.close();
|
|
4512
|
+
stage.persistenceWriter = undefined;
|
|
4513
|
+
await this.#replaceSessionFile(tempPath, stage.sessionFile);
|
|
4514
|
+
stage.persistenceTempPath = undefined;
|
|
4515
|
+
} catch (error) {
|
|
4516
|
+
const cleanupError = await this.#cleanupPreparedNewSessionPersistence(stage);
|
|
4517
|
+
if (cleanupError)
|
|
4518
|
+
throw new AggregateError(
|
|
4519
|
+
[toError(error), cleanupError],
|
|
4520
|
+
"Prepared session persistence and temporary-file cleanup both failed.",
|
|
4521
|
+
);
|
|
4522
|
+
throw toError(error);
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
stage.flushed = true;
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
/** Build context from an unpublished successor without reading active manager state. @internal */
|
|
4529
|
+
buildPreparedNewSessionContext(prepared: PreparedNewSession): SessionContext {
|
|
4530
|
+
const stage = this.#getPreparedNewSessionStage(prepared);
|
|
4531
|
+
return buildSessionContext(
|
|
4532
|
+
stage.fileEntries.filter((entry): entry is SessionEntry => entry.type !== "session"),
|
|
4533
|
+
undefined,
|
|
4534
|
+
undefined,
|
|
4535
|
+
stage.sessionId,
|
|
4536
|
+
);
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
#freezePreparedNewSessionIdentity(stage: PreparedNewSessionState): void {
|
|
4540
|
+
// Prevent identity ABA: callers must not rebind staged sessionId/file after prepare.
|
|
4541
|
+
Object.defineProperty(stage, "sessionId", {
|
|
4542
|
+
value: stage.sessionId,
|
|
4543
|
+
writable: false,
|
|
4544
|
+
enumerable: true,
|
|
4545
|
+
configurable: false,
|
|
4546
|
+
});
|
|
4547
|
+
Object.defineProperty(stage, "sessionFile", {
|
|
4548
|
+
value: stage.sessionFile,
|
|
4549
|
+
writable: false,
|
|
4550
|
+
enumerable: true,
|
|
4551
|
+
configurable: false,
|
|
4552
|
+
});
|
|
4553
|
+
Object.defineProperty(stage, "artifactsDir", {
|
|
4554
|
+
value: stage.artifactsDir,
|
|
4555
|
+
writable: false,
|
|
4556
|
+
enumerable: true,
|
|
4557
|
+
configurable: false,
|
|
4558
|
+
});
|
|
4559
|
+
Object.defineProperty(stage, "managedLegacyLocalMigrationSource", {
|
|
4560
|
+
value: stage.managedLegacyLocalMigrationSource,
|
|
4561
|
+
writable: false,
|
|
4562
|
+
enumerable: true,
|
|
4563
|
+
configurable: false,
|
|
4564
|
+
});
|
|
4565
|
+
}
|
|
4566
|
+
|
|
4567
|
+
#getPreparedNewSessionStage(prepared: PreparedNewSession): PreparedNewSessionState {
|
|
4568
|
+
const stage = prepared as PreparedNewSessionState;
|
|
4569
|
+
if (!this.#preparedNewSessions.has(stage) || stage.committed || stage.discarded)
|
|
4570
|
+
throw new Error("Prepared session is no longer available.");
|
|
4571
|
+
return stage;
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
#preparedNewSessionLeafId(stage: PreparedNewSessionState): string | null {
|
|
4575
|
+
for (let index = stage.fileEntries.length - 1; index >= 0; index--) {
|
|
4576
|
+
const entry = stage.fileEntries[index];
|
|
4577
|
+
if (entry.type !== "session") return entry.id;
|
|
4578
|
+
}
|
|
4579
|
+
return null;
|
|
4580
|
+
}
|
|
4581
|
+
|
|
4582
|
+
#nextPreparedNewSessionEntryId(stage: PreparedNewSessionState): string {
|
|
4583
|
+
const ids = new Map<string, SessionEntry>();
|
|
4584
|
+
for (const entry of stage.fileEntries) {
|
|
4585
|
+
if (entry.type !== "session") ids.set(entry.id, entry);
|
|
4586
|
+
}
|
|
4587
|
+
return generateId(ids);
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
/** Prepare a forked successor without publishing it through public manager state. @internal */
|
|
4591
|
+
async prepareFork(): Promise<PreparedNewSession | undefined> {
|
|
4592
|
+
await this.#retryPreparedNewSessionCleanups();
|
|
4593
|
+
if (!this.persist || !this.#sessionFile) return undefined;
|
|
4316
4594
|
await this.#closePersistWriter();
|
|
4317
|
-
const
|
|
4595
|
+
const sourceFile = this.#sessionFile;
|
|
4596
|
+
const sourceId = this.#sessionId;
|
|
4597
|
+
const timestamp = new Date().toISOString();
|
|
4598
|
+
const sessionId = createSessionId();
|
|
4599
|
+
const sessionFile = path.join(this.getSessionDir(), `${timestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`);
|
|
4600
|
+
const header: SessionHeader = {
|
|
4601
|
+
type: "session",
|
|
4602
|
+
version: CURRENT_SESSION_VERSION,
|
|
4603
|
+
id: sessionId,
|
|
4604
|
+
title: this.#sessionName,
|
|
4605
|
+
titleSource: this.#titleSource,
|
|
4606
|
+
timestamp,
|
|
4607
|
+
cwd: this.cwd,
|
|
4608
|
+
parentSession: sourceId,
|
|
4609
|
+
};
|
|
4610
|
+
const entries: FileEntry[] = [
|
|
4611
|
+
header,
|
|
4612
|
+
...materializeResidentEntriesForReadSync(this.#fileEntries, this.#residentBlobStores()).filter(
|
|
4613
|
+
(entry): entry is SessionEntry => entry.type !== "session",
|
|
4614
|
+
),
|
|
4615
|
+
];
|
|
4616
|
+
const stage: PreparedNewSessionState = {
|
|
4617
|
+
sessionId,
|
|
4618
|
+
sessionFile,
|
|
4619
|
+
artifactsDir: sessionFile.slice(0, -6),
|
|
4620
|
+
managedLegacyLocalMigrationSource: this.#managedLegacyLocalMigrationSourceFor(sessionFile),
|
|
4621
|
+
header,
|
|
4622
|
+
fileEntries: entries,
|
|
4623
|
+
sessionName: this.#sessionName,
|
|
4624
|
+
titleSource: this.#titleSource,
|
|
4625
|
+
flushed: true,
|
|
4626
|
+
committed: false,
|
|
4627
|
+
discarded: false,
|
|
4628
|
+
};
|
|
4629
|
+
this.#preparedNewSessions.add(stage);
|
|
4630
|
+
this.#freezePreparedNewSessionIdentity(stage);
|
|
4631
|
+
try {
|
|
4632
|
+
await this.copyArtifactsForFork(sourceFile, sessionFile);
|
|
4633
|
+
const content = `${entries.map(entry => JSON.stringify(prepareEntryForPersistenceSync(entry, this.#blobStore))).join("\n")}\n`;
|
|
4634
|
+
if (this.destination.kind === "managed") {
|
|
4635
|
+
this.#managedTranscriptStore(sessionFile).publishNoReplaceSync(
|
|
4636
|
+
path.basename(sessionFile),
|
|
4637
|
+
Buffer.from(content, "utf8"),
|
|
4638
|
+
);
|
|
4639
|
+
} else {
|
|
4640
|
+
this.storage.writeTextSync(sessionFile, content);
|
|
4641
|
+
}
|
|
4642
|
+
} catch (error) {
|
|
4643
|
+
try {
|
|
4644
|
+
await this.discardPreparedNewSession(stage);
|
|
4645
|
+
} catch (cleanupError) {
|
|
4646
|
+
throw new AggregateError(
|
|
4647
|
+
[toError(error), toError(cleanupError)],
|
|
4648
|
+
"Fork preparation and staged session cleanup both failed.",
|
|
4649
|
+
);
|
|
4650
|
+
}
|
|
4651
|
+
throw toError(error);
|
|
4652
|
+
}
|
|
4653
|
+
return stage;
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
/** Prepare a path-only branch successor without publishing it through public manager state. @internal */
|
|
4657
|
+
async prepareBranchedSession(leafId: string): Promise<PreparedNewSession> {
|
|
4658
|
+
await this.#retryPreparedNewSessionCleanups();
|
|
4659
|
+
const branchPath = this.#getCanonicalBranchClones(leafId);
|
|
4660
|
+
if (branchPath.length === 0) throw new Error(`Entry ${leafId} not found`);
|
|
4661
|
+
await this.#closePersistWriter();
|
|
4662
|
+
const timestamp = new Date().toISOString();
|
|
4663
|
+
const sessionId = createSessionId();
|
|
4664
|
+
const sessionFile = this.persist
|
|
4665
|
+
? path.join(this.getSessionDir(), `${timestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`)
|
|
4666
|
+
: undefined;
|
|
4667
|
+
const header: SessionHeader = {
|
|
4668
|
+
type: "session",
|
|
4669
|
+
version: CURRENT_SESSION_VERSION,
|
|
4670
|
+
id: sessionId,
|
|
4671
|
+
timestamp,
|
|
4672
|
+
cwd: this.cwd,
|
|
4673
|
+
parentSession: this.persist ? this.#sessionFile : undefined,
|
|
4674
|
+
};
|
|
4675
|
+
const pathWithoutLabels = branchPath.filter(entry => entry.type !== "label");
|
|
4676
|
+
const pathEntryIds = new Set(pathWithoutLabels.map(entry => entry.id));
|
|
4677
|
+
const labelEntries: LabelEntry[] = [];
|
|
4678
|
+
let labelParentId = pathWithoutLabels[pathWithoutLabels.length - 1]?.id ?? null;
|
|
4679
|
+
for (const [targetId, label] of this.#labelsById) {
|
|
4680
|
+
if (!pathEntryIds.has(targetId)) continue;
|
|
4681
|
+
const labelEntry: LabelEntry = {
|
|
4682
|
+
type: "label",
|
|
4683
|
+
id: generateId({ has: id => pathEntryIds.has(id) }),
|
|
4684
|
+
parentId: labelParentId,
|
|
4685
|
+
timestamp: new Date().toISOString(),
|
|
4686
|
+
targetId,
|
|
4687
|
+
label,
|
|
4688
|
+
};
|
|
4689
|
+
pathEntryIds.add(labelEntry.id);
|
|
4690
|
+
labelEntries.push(labelEntry);
|
|
4691
|
+
labelParentId = labelEntry.id;
|
|
4692
|
+
}
|
|
4693
|
+
const entries: FileEntry[] = [
|
|
4694
|
+
header,
|
|
4695
|
+
...materializeResidentEntriesForReadSync(pathWithoutLabels, this.#residentBlobStores()),
|
|
4696
|
+
...labelEntries,
|
|
4697
|
+
];
|
|
4698
|
+
const stage: PreparedNewSessionState = {
|
|
4699
|
+
sessionId,
|
|
4700
|
+
sessionFile,
|
|
4701
|
+
artifactsDir: sessionFile ? sessionFile.slice(0, -6) : null,
|
|
4702
|
+
managedLegacyLocalMigrationSource: this.#managedLegacyLocalMigrationSourceFor(sessionFile),
|
|
4703
|
+
header,
|
|
4704
|
+
fileEntries: entries,
|
|
4705
|
+
sessionName: undefined,
|
|
4706
|
+
titleSource: undefined,
|
|
4707
|
+
flushed: this.persist,
|
|
4708
|
+
committed: false,
|
|
4709
|
+
discarded: false,
|
|
4710
|
+
};
|
|
4711
|
+
this.#preparedNewSessions.add(stage);
|
|
4712
|
+
this.#freezePreparedNewSessionIdentity(stage);
|
|
4713
|
+
try {
|
|
4714
|
+
if (sessionFile) {
|
|
4715
|
+
const content = `${entries.map(entry => JSON.stringify(prepareEntryForPersistenceSync(entry, this.#blobStore))).join("\n")}\n`;
|
|
4716
|
+
if (this.destination.kind === "managed") {
|
|
4717
|
+
this.#managedTranscriptStore(sessionFile).publishNoReplaceSync(
|
|
4718
|
+
path.basename(sessionFile),
|
|
4719
|
+
Buffer.from(content, "utf8"),
|
|
4720
|
+
);
|
|
4721
|
+
} else {
|
|
4722
|
+
this.storage.writeTextSync(sessionFile, content);
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
return stage;
|
|
4726
|
+
} catch (error) {
|
|
4727
|
+
try {
|
|
4728
|
+
await this.discardPreparedNewSession(stage);
|
|
4729
|
+
} catch (cleanupError) {
|
|
4730
|
+
throw new AggregateError(
|
|
4731
|
+
[error, cleanupError],
|
|
4732
|
+
"Branch preparation and staged session cleanup both failed.",
|
|
4733
|
+
);
|
|
4734
|
+
}
|
|
4735
|
+
throw error;
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
/** Publish a prepared successor synchronously after all readiness awaits succeed. @internal */
|
|
4740
|
+
commitPreparedNewSession(prepared: PreparedNewSession): void {
|
|
4741
|
+
const stage = prepared as PreparedNewSessionState;
|
|
4742
|
+
if (!this.#preparedNewSessions.has(stage) || stage.committed || stage.discarded) {
|
|
4743
|
+
throw new Error("Prepared session is no longer available.");
|
|
4744
|
+
}
|
|
4745
|
+
this.#preparePreparedNewSessionForCommit(stage);
|
|
4746
|
+
const residentFileEntries = stage.residentFileEntries;
|
|
4747
|
+
const residentTextBlobStore = stage.residentTextBlobStore;
|
|
4748
|
+
const index = stage.index;
|
|
4749
|
+
if (!residentFileEntries || !residentTextBlobStore || !index)
|
|
4750
|
+
throw new Error("Prepared session adoption is incomplete.");
|
|
4751
|
+
if (stage.sessionFile) writeTerminalBreadcrumb(this.cwd, stage.sessionFile);
|
|
4752
|
+
|
|
4753
|
+
stage.committed = true;
|
|
4754
|
+
this.#preparedNewSessions.delete(stage);
|
|
4755
|
+
if (!this.#lifecycleIdAdopted && stage.sessionId === lifecyclePreallocatedSessionId())
|
|
4756
|
+
this.#lifecycleIdAdopted = true;
|
|
4757
|
+
this.#persistChain = Promise.resolve();
|
|
4758
|
+
this.#persistError = undefined;
|
|
4759
|
+
this.#persistErrorReported = false;
|
|
4760
|
+
this.#sessionId = stage.sessionId;
|
|
4761
|
+
this.#sessionName = stage.sessionName;
|
|
4762
|
+
this.#titleSource = stage.titleSource;
|
|
4763
|
+
this.#sessionFile = stage.sessionFile;
|
|
4764
|
+
this.#fileEntries = residentFileEntries;
|
|
4765
|
+
this.#residentTextBlobStore = residentTextBlobStore;
|
|
4766
|
+
this.#byId = index.byId;
|
|
4767
|
+
this.#labelsById = index.labelsById;
|
|
4768
|
+
this.#leafId = index.leafId;
|
|
4769
|
+
this.#usageStatistics = index.usageStatistics;
|
|
4770
|
+
this.#flushed = stage.flushed;
|
|
4771
|
+
this.#needsFullRewriteOnNextPersist = false;
|
|
4772
|
+
this.#ensuredOnDisk = stage.flushed;
|
|
4773
|
+
this.#inMemoryArtifacts = null;
|
|
4774
|
+
this.#inMemoryArtifactCounter = 0;
|
|
4775
|
+
this.#artifactManager = null;
|
|
4776
|
+
this.#artifactManagerSessionFile = null;
|
|
4777
|
+
this.#adoptedArtifactManager = null;
|
|
4318
4778
|
this.#bumpAllRevisions();
|
|
4319
|
-
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
/** Exact-discard only an uncommitted successor prepared by this manager. @internal */
|
|
4782
|
+
async discardPreparedNewSession(prepared: PreparedNewSession): Promise<void> {
|
|
4783
|
+
const stage = prepared as PreparedNewSessionState;
|
|
4784
|
+
if (!this.#preparedNewSessions.has(stage) || stage.committed || stage.discarded) return;
|
|
4785
|
+
const persistenceCleanupError = await this.#cleanupPreparedNewSessionPersistence(stage);
|
|
4786
|
+
let sessionCleanupError: Error | undefined;
|
|
4787
|
+
if (stage.sessionFile) {
|
|
4788
|
+
try {
|
|
4789
|
+
await this.discardUncommittedSession(stage.sessionFile);
|
|
4790
|
+
} catch (error) {
|
|
4791
|
+
sessionCleanupError = toError(error);
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
if (persistenceCleanupError || sessionCleanupError) {
|
|
4795
|
+
const errors = [persistenceCleanupError, sessionCleanupError].filter(
|
|
4796
|
+
(error): error is Error => error !== undefined,
|
|
4797
|
+
);
|
|
4798
|
+
throw errors.length === 1 ? errors[0] : new AggregateError(errors, "Prepared session cleanup failed.");
|
|
4799
|
+
}
|
|
4800
|
+
this.#preparedNewSessions.delete(stage);
|
|
4801
|
+
stage.discarded = true;
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4804
|
+
async #retryPreparedNewSessionCleanups(): Promise<void> {
|
|
4805
|
+
if (this.#preparedNewSessionCleanupInProgress) return;
|
|
4806
|
+
this.#preparedNewSessionCleanupInProgress = true;
|
|
4807
|
+
try {
|
|
4808
|
+
const errors: Error[] = [];
|
|
4809
|
+
for (const stage of [...this.#preparedNewSessions]) {
|
|
4810
|
+
if (stage.committed || stage.discarded) continue;
|
|
4811
|
+
try {
|
|
4812
|
+
await this.discardPreparedNewSession(stage);
|
|
4813
|
+
} catch (error) {
|
|
4814
|
+
errors.push(toError(error));
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
if (errors.length > 0)
|
|
4818
|
+
throw errors.length === 1
|
|
4819
|
+
? errors[0]
|
|
4820
|
+
: new AggregateError(errors, "Prepared session cleanup retry failed.");
|
|
4821
|
+
} finally {
|
|
4822
|
+
this.#preparedNewSessionCleanupInProgress = false;
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4826
|
+
async #cleanupPreparedNewSessionPersistence(stage: PreparedNewSessionState): Promise<Error | undefined> {
|
|
4827
|
+
const errors: Error[] = [];
|
|
4828
|
+
if (stage.persistenceWriter) {
|
|
4829
|
+
try {
|
|
4830
|
+
await stage.persistenceWriter.close();
|
|
4831
|
+
stage.persistenceWriter = undefined;
|
|
4832
|
+
} catch (error) {
|
|
4833
|
+
errors.push(toError(error));
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
if (stage.persistenceTempPath && !stage.persistenceWriter) {
|
|
4837
|
+
try {
|
|
4838
|
+
await this.storage.unlink(stage.persistenceTempPath);
|
|
4839
|
+
stage.persistenceTempPath = undefined;
|
|
4840
|
+
} catch (error) {
|
|
4841
|
+
if (!isEnoent(error)) errors.push(toError(error));
|
|
4842
|
+
else stage.persistenceTempPath = undefined;
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
return errors.length === 0
|
|
4846
|
+
? undefined
|
|
4847
|
+
: errors.length === 1
|
|
4848
|
+
? errors[0]
|
|
4849
|
+
: new AggregateError(errors, "Prepared session temporary-file cleanup failed.");
|
|
4320
4850
|
}
|
|
4321
4851
|
|
|
4322
4852
|
/** Tombstone and exact-delete managed transcripts, detaching the active transcript first. */
|
|
@@ -4962,41 +5492,73 @@ export class SessionManager {
|
|
|
4962
5492
|
}
|
|
4963
5493
|
|
|
4964
5494
|
#buildIndex(): void {
|
|
4965
|
-
this.#
|
|
4966
|
-
this.#
|
|
4967
|
-
this.#
|
|
4968
|
-
this.#
|
|
5495
|
+
const index = this.#buildIndexForEntries(this.#fileEntries, this.#sessionFile);
|
|
5496
|
+
this.#byId = index.byId;
|
|
5497
|
+
this.#labelsById = index.labelsById;
|
|
5498
|
+
this.#leafId = index.leafId;
|
|
5499
|
+
this.#usageStatistics = index.usageStatistics;
|
|
5500
|
+
}
|
|
5501
|
+
|
|
5502
|
+
#buildIndexForEntries(fileEntries: FileEntry[], sessionFile: string | undefined): PreparedSessionIndex {
|
|
5503
|
+
const byId = new Map<string, SessionEntry>();
|
|
5504
|
+
const labelsById = new Map<string, string>();
|
|
5505
|
+
let leafId: string | null = null;
|
|
5506
|
+
const usageStatistics: UsageStatistics = {
|
|
5507
|
+
input: 0,
|
|
5508
|
+
output: 0,
|
|
5509
|
+
cacheRead: 0,
|
|
5510
|
+
cacheWrite: 0,
|
|
5511
|
+
premiumRequests: 0,
|
|
5512
|
+
cost: 0,
|
|
5513
|
+
};
|
|
5514
|
+
const addUsage = (totals: ValidatedUsageTotals): boolean => {
|
|
5515
|
+
const next = {
|
|
5516
|
+
input: usageStatistics.input + totals.input,
|
|
5517
|
+
output: usageStatistics.output + totals.output,
|
|
5518
|
+
cacheRead: usageStatistics.cacheRead + totals.cacheRead,
|
|
5519
|
+
cacheWrite: usageStatistics.cacheWrite + totals.cacheWrite,
|
|
5520
|
+
premiumRequests: usageStatistics.premiumRequests + totals.premiumRequests,
|
|
5521
|
+
cost: usageStatistics.cost + totals.cost,
|
|
5522
|
+
};
|
|
5523
|
+
if (Object.values(next).some(value => !Number.isFinite(value))) return false;
|
|
5524
|
+
Object.assign(usageStatistics, next);
|
|
5525
|
+
return true;
|
|
5526
|
+
};
|
|
5527
|
+
const accumulateUsage = (entry: SessionEntry): boolean => {
|
|
5528
|
+
if (entry.type !== "message") return false;
|
|
5529
|
+
const usage =
|
|
5530
|
+
entry.message.role === "assistant"
|
|
5531
|
+
? entry.message.usage
|
|
5532
|
+
: entry.message.role === "toolResult" && entry.message.toolName === "task"
|
|
5533
|
+
? getTaskToolUsage(entry.message.details)
|
|
5534
|
+
: undefined;
|
|
5535
|
+
if (usage === undefined) return false;
|
|
5536
|
+
const totals = validatePersistedUsageTotals(usage);
|
|
5537
|
+
return !(totals && addUsage(totals));
|
|
5538
|
+
};
|
|
4969
5539
|
let malformedUsageRecords = 0;
|
|
4970
5540
|
const malformedUsageSample: string[] = [];
|
|
4971
|
-
for (const entry of
|
|
5541
|
+
for (const entry of fileEntries) {
|
|
4972
5542
|
if (entry.type === "session") continue;
|
|
4973
|
-
|
|
4974
|
-
|
|
5543
|
+
byId.set(entry.id, entry);
|
|
5544
|
+
leafId = entry.id;
|
|
4975
5545
|
if (entry.type === "label") {
|
|
4976
|
-
if (entry.label)
|
|
4977
|
-
|
|
4978
|
-
} else {
|
|
4979
|
-
this.#labelsById.delete(entry.targetId);
|
|
4980
|
-
}
|
|
5546
|
+
if (entry.label) labelsById.set(entry.targetId, entry.label);
|
|
5547
|
+
else labelsById.delete(entry.targetId);
|
|
4981
5548
|
}
|
|
4982
|
-
|
|
4983
|
-
// overflow-guarded path (see #accumulateEntryUsage). parseSessionEntries accepts
|
|
4984
|
-
// any parseable JSON, so a torn/corrupt record can carry a still-valid-JSON but
|
|
4985
|
-
// malformed `usage` (absent, {}, numeric strings, negatives, a non-record cost, or
|
|
4986
|
-
// a cumulative overflow); such a record is skipped and reported below instead of
|
|
4987
|
-
// poisoning every getUsageStatistics() consumer.
|
|
4988
|
-
if (this.#accumulateEntryUsage(entry)) {
|
|
5549
|
+
if (accumulateUsage(entry)) {
|
|
4989
5550
|
malformedUsageRecords++;
|
|
4990
5551
|
if (malformedUsageSample.length < MALFORMED_USAGE_SAMPLE_LIMIT) malformedUsageSample.push(entry.id);
|
|
4991
5552
|
}
|
|
4992
5553
|
}
|
|
4993
5554
|
if (malformedUsageRecords > 0) {
|
|
4994
5555
|
logger.warn("Skipped malformed or overflowing persisted usage records during resume aggregation", {
|
|
4995
|
-
sessionFile
|
|
5556
|
+
sessionFile,
|
|
4996
5557
|
count: malformedUsageRecords,
|
|
4997
5558
|
sampleEntryIds: malformedUsageSample,
|
|
4998
5559
|
});
|
|
4999
5560
|
}
|
|
5561
|
+
return { byId, labelsById, leafId, usageStatistics };
|
|
5000
5562
|
}
|
|
5001
5563
|
|
|
5002
5564
|
/**
|
|
@@ -5542,6 +6104,15 @@ export class SessionManager {
|
|
|
5542
6104
|
|
|
5543
6105
|
/** Close the persistent writer after flushing all pending data. */
|
|
5544
6106
|
async close(): Promise<void> {
|
|
6107
|
+
// Drain any uncommitted prepared successors before releasing resources so
|
|
6108
|
+
// dispose/shutdown retains exact cleanup authority (#3138).
|
|
6109
|
+
try {
|
|
6110
|
+
await this.#retryPreparedNewSessionCleanups();
|
|
6111
|
+
} catch (error) {
|
|
6112
|
+
logger.warn("Prepared session cleanup during close failed; retained for retry", {
|
|
6113
|
+
error: toError(error).message,
|
|
6114
|
+
});
|
|
6115
|
+
}
|
|
5545
6116
|
await this.#queuePersistTask(async () => {
|
|
5546
6117
|
if (this.#persistWriter) {
|
|
5547
6118
|
await this.#closePersistWriterInternal();
|
|
@@ -5566,6 +6137,14 @@ export class SessionManager {
|
|
|
5566
6137
|
* writer closure before any destructive operation.
|
|
5567
6138
|
*/
|
|
5568
6139
|
async closeStrict(): Promise<SessionManagerCloseOutcome> {
|
|
6140
|
+
// Drain staged successors on the strict ACP dispose path as well as best-effort close (#3138).
|
|
6141
|
+
try {
|
|
6142
|
+
await this.#retryPreparedNewSessionCleanups();
|
|
6143
|
+
} catch (error) {
|
|
6144
|
+
logger.warn("Prepared session cleanup during closeStrict failed; retained for retry", {
|
|
6145
|
+
error: toError(error).message,
|
|
6146
|
+
});
|
|
6147
|
+
}
|
|
5569
6148
|
let outcome: SessionManagerCloseOutcome = { kind: "closed" };
|
|
5570
6149
|
await this.#queuePersistTask(async () => {
|
|
5571
6150
|
const writer = this.#persistWriter;
|
|
@@ -5670,9 +6249,13 @@ export class SessionManager {
|
|
|
5670
6249
|
|
|
5671
6250
|
/** Supplies opaque retained authority for mandatory managed legacy local migration. */
|
|
5672
6251
|
getManagedLegacyLocalMigrationSource(): ManagedLegacyLocalMigrationSource | null {
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
6252
|
+
return this.#managedLegacyLocalMigrationSourceFor(this.#sessionFile);
|
|
6253
|
+
}
|
|
6254
|
+
|
|
6255
|
+
#managedLegacyLocalMigrationSourceFor(sessionFile: string | undefined): ManagedLegacyLocalMigrationSource | null {
|
|
6256
|
+
if (this.destination.kind !== "managed" || !sessionFile || this.#adoptedArtifactManager) return null;
|
|
6257
|
+
const store = this.#managedTranscriptStore(sessionFile);
|
|
6258
|
+
const legacyArtifactsRoot = path.basename(sessionFile.slice(0, -6));
|
|
5676
6259
|
const legacyLocalRoot = path.posix.join(legacyArtifactsRoot, "local");
|
|
5677
6260
|
return {
|
|
5678
6261
|
capture: async () => {
|