@gajae-code/coding-agent 0.16.0 → 0.16.3
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 +52 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/rows.d.ts +15 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +12 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +156 -32
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +34 -19
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +6 -1
- package/src/sdk/broker/broker.ts +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +118 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/rows.ts +75 -7
- package/src/sdk/cli/session-cli.ts +68 -35
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +26 -2
- package/src/sdk/host/host.ts +6 -1
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +497 -31
- package/src/sdk/host/session-runtime.ts +387 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +210 -94
- package/src/sdk/session.ts +59 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +158 -33
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/index.ts +4 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +86 -1
|
@@ -23,7 +23,19 @@ function nativeSessionManager(): typeof import("@gajae-code/natives") {
|
|
|
23
23
|
return require("@gajae-code/natives") as typeof import("@gajae-code/natives");
|
|
24
24
|
}
|
|
25
25
|
const cwdTransitionAls = new AsyncLocalStorage<symbol>();
|
|
26
|
-
type CwdReadLeaseContext = {
|
|
26
|
+
type CwdReadLeaseContext = {
|
|
27
|
+
active: boolean;
|
|
28
|
+
released: boolean;
|
|
29
|
+
owner: symbol;
|
|
30
|
+
suspendedMoveCount: number;
|
|
31
|
+
committedMoveGeneration?: number;
|
|
32
|
+
/** Execution-local lineage; sibling tool executions never share this context. */
|
|
33
|
+
parent?: CwdReadLeaseContext;
|
|
34
|
+
root?: CwdReadLeaseContext;
|
|
35
|
+
executionId: symbol;
|
|
36
|
+
activeBranches: number;
|
|
37
|
+
suspendedMoveOwner?: symbol;
|
|
38
|
+
};
|
|
27
39
|
const cwdReadLeaseAls = new AsyncLocalStorage<CwdReadLeaseContext>();
|
|
28
40
|
const CWD_NOFOLLOW_OPEN_FLAGS =
|
|
29
41
|
fs.constants.O_RDONLY |
|
|
@@ -246,6 +258,8 @@ export interface SessionHeader {
|
|
|
246
258
|
id: string;
|
|
247
259
|
title?: string; // Auto-generated title from first message
|
|
248
260
|
titleSource?: "auto" | "user";
|
|
261
|
+
starredPatchVersion?: 1;
|
|
262
|
+
starred?: boolean;
|
|
249
263
|
timestamp: string;
|
|
250
264
|
cwd: string;
|
|
251
265
|
parentSession?: string;
|
|
@@ -931,7 +945,7 @@ export type SessionEntry =
|
|
|
931
945
|
/** Append-only replacement for mutable fields on the session header. */
|
|
932
946
|
export interface HeaderPatchRecord {
|
|
933
947
|
type: "header_patch";
|
|
934
|
-
patch: Partial<Pick<SessionHeader, "title" | "titleSource" | "cwd">>;
|
|
948
|
+
patch: Partial<Pick<SessionHeader, "title" | "titleSource" | "cwd" | "starred">>;
|
|
935
949
|
}
|
|
936
950
|
|
|
937
951
|
/** Append-only replacement for replay metadata on one existing session entry. */
|
|
@@ -2390,6 +2404,7 @@ export interface SessionInfo {
|
|
|
2390
2404
|
/** Working directory where the session was started. Empty string for old sessions. */
|
|
2391
2405
|
cwd: string;
|
|
2392
2406
|
title?: string;
|
|
2407
|
+
starred: boolean;
|
|
2393
2408
|
/** Path to the parent session (if this session was forked). */
|
|
2394
2409
|
parentSessionPath?: string;
|
|
2395
2410
|
created: Date;
|
|
@@ -2699,12 +2714,13 @@ function isHeaderPatchRecord(record: SessionPatchRecord): record is HeaderPatchR
|
|
|
2699
2714
|
)
|
|
2700
2715
|
return false;
|
|
2701
2716
|
const keys = Object.keys(record.patch);
|
|
2702
|
-
if (!keys.every(key => key === "cwd" || key === "title" || key === "titleSource")) return false;
|
|
2703
|
-
const { cwd, title, titleSource } = record.patch;
|
|
2717
|
+
if (!keys.every(key => key === "cwd" || key === "title" || key === "titleSource" || key === "starred")) return false;
|
|
2718
|
+
const { cwd, title, titleSource, starred } = record.patch;
|
|
2704
2719
|
return (
|
|
2705
2720
|
(cwd === undefined || typeof cwd === "string") &&
|
|
2706
2721
|
(title === undefined || typeof title === "string") &&
|
|
2707
|
-
(titleSource === undefined || titleSource === "auto" || titleSource === "user")
|
|
2722
|
+
(titleSource === undefined || titleSource === "auto" || titleSource === "user") &&
|
|
2723
|
+
(starred === undefined || typeof starred === "boolean")
|
|
2708
2724
|
);
|
|
2709
2725
|
}
|
|
2710
2726
|
|
|
@@ -2712,6 +2728,7 @@ function applyHeaderPatch(header: SessionHeader, patch: HeaderPatchRecord["patch
|
|
|
2712
2728
|
if (patch.cwd !== undefined) header.cwd = patch.cwd;
|
|
2713
2729
|
if (patch.title !== undefined) header.title = patch.title;
|
|
2714
2730
|
if (patch.titleSource !== undefined) header.titleSource = patch.titleSource;
|
|
2731
|
+
if (header.starredPatchVersion === 1 && patch.starred !== undefined) header.starred = patch.starred;
|
|
2715
2732
|
}
|
|
2716
2733
|
|
|
2717
2734
|
function isEntryPatchRecord(record: SessionPatchRecord): record is EntryPatchRecord {
|
|
@@ -4654,7 +4671,7 @@ async function getSortedSessions(
|
|
|
4654
4671
|
)
|
|
4655
4672
|
return undefined;
|
|
4656
4673
|
if (typeof header.version === "number" && header.version >= 4) {
|
|
4657
|
-
for (const patch of await readSessionListTrailingPatches(candidate.path, storage)) {
|
|
4674
|
+
for (const patch of await readSessionListTrailingPatches(candidate.path, storage, false)) {
|
|
4658
4675
|
applySessionListHeaderPatch(header as unknown as SessionListHeader, patch);
|
|
4659
4676
|
}
|
|
4660
4677
|
}
|
|
@@ -6413,6 +6430,10 @@ async function collectProjectSessions(cwd: string, storage: FileSessionStorage):
|
|
|
6413
6430
|
return await collectSessionsFromFiles(listProjectSessionTranscriptFiles(cwd), storage);
|
|
6414
6431
|
}
|
|
6415
6432
|
|
|
6433
|
+
export function prioritizeStarredSessions(sessions: readonly SessionInfo[]): SessionInfo[] {
|
|
6434
|
+
return [...sessions.filter(session => session.starred), ...sessions.filter(session => !session.starred)];
|
|
6435
|
+
}
|
|
6436
|
+
|
|
6416
6437
|
function mergeSessionInventories(...inventories: SessionInfo[][]): SessionInfo[] {
|
|
6417
6438
|
const sessions = new Map<string, SessionInfo>();
|
|
6418
6439
|
for (const inventory of inventories) {
|
|
@@ -6574,36 +6595,41 @@ async function readSessionListPrefix(file: string, storage: SessionStorage, buff
|
|
|
6574
6595
|
async function readSessionListTrailingPatches(
|
|
6575
6596
|
file: string,
|
|
6576
6597
|
storage: SessionStorage,
|
|
6598
|
+
includeStarred: boolean,
|
|
6577
6599
|
): Promise<HeaderPatchRecord["patch"][]> {
|
|
6578
6600
|
if (!(storage instanceof FileSessionStorage)) {
|
|
6579
6601
|
const content = await storage.readText(file);
|
|
6580
6602
|
const entries = parseSessionEntries(content);
|
|
6581
6603
|
const header = entries[0] as SessionHeader | undefined;
|
|
6582
|
-
return header?.type === "session"
|
|
6604
|
+
return header?.type === "session"
|
|
6605
|
+
? [{ cwd: header.cwd, title: header.title, ...(includeStarred ? { starred: header.starred } : {}) }]
|
|
6606
|
+
: [];
|
|
6583
6607
|
}
|
|
6584
6608
|
|
|
6585
6609
|
const size = storage.statSync(file).size;
|
|
6586
6610
|
if (size <= SESSION_LIST_PREFIX_BYTES) return [];
|
|
6587
6611
|
const latest: HeaderPatchRecord["patch"] = {};
|
|
6588
6612
|
let position = size;
|
|
6589
|
-
// Reverse-scan from EOF in fixed chunks. Stop once
|
|
6590
|
-
//
|
|
6613
|
+
// Reverse-scan from EOF in fixed chunks. Stop once every requested mutable list field
|
|
6614
|
+
// resolves so recent patches stay cheap. Continue past the historical 16 KiB
|
|
6591
6615
|
// window when a field is still missing so a buried but canonically valid
|
|
6592
6616
|
// header_patch remains listable without a full sequential JSONL parse (#3633).
|
|
6593
6617
|
// Chunks that cannot contain a header_patch marker skip JSON parsing.
|
|
6594
6618
|
//
|
|
6595
|
-
// The scan owns its buffer instead of borrowing the caller's 4 KiB prefix
|
|
6596
|
-
//
|
|
6597
|
-
//
|
|
6598
|
-
//
|
|
6599
|
-
// per transcript byte.
|
|
6619
|
+
// The scan owns its buffer instead of borrowing the caller's 4 KiB prefix.
|
|
6620
|
+
// A transcript with no header_patch at all — the common case — cannot be
|
|
6621
|
+
// recognized as such without reaching BOF, so the chunk size sets how many
|
|
6622
|
+
// read syscalls a resume costs per transcript byte.
|
|
6600
6623
|
let trailingFragment = Buffer.alloc(0);
|
|
6601
6624
|
const chunkSize = Math.min(size, SESSION_LIST_TRAILING_PATCH_BYTES);
|
|
6602
6625
|
const buffer = Buffer.allocUnsafe(chunkSize);
|
|
6603
6626
|
const headerPatchMarker = Buffer.from("header_patch");
|
|
6604
6627
|
const handle = await fs.promises.open(file, "r");
|
|
6605
6628
|
try {
|
|
6606
|
-
while (
|
|
6629
|
+
while (
|
|
6630
|
+
position > 0 &&
|
|
6631
|
+
(latest.cwd === undefined || latest.title === undefined || (includeStarred && latest.starred === undefined))
|
|
6632
|
+
) {
|
|
6607
6633
|
const start = Math.max(0, position - chunkSize);
|
|
6608
6634
|
const length = position - start;
|
|
6609
6635
|
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
@@ -6636,6 +6662,8 @@ async function readSessionListTrailingPatches(
|
|
|
6636
6662
|
if (latest.cwd === undefined && typeof record.patch.cwd === "string") latest.cwd = record.patch.cwd;
|
|
6637
6663
|
if (latest.title === undefined && typeof record.patch.title === "string")
|
|
6638
6664
|
latest.title = record.patch.title;
|
|
6665
|
+
if (includeStarred && latest.starred === undefined && typeof record.patch.starred === "boolean")
|
|
6666
|
+
latest.starred = record.patch.starred;
|
|
6639
6667
|
} catch {
|
|
6640
6668
|
// Ignore malformed or partial records exactly as the canonical loader does.
|
|
6641
6669
|
}
|
|
@@ -6643,7 +6671,7 @@ async function readSessionListTrailingPatches(
|
|
|
6643
6671
|
}
|
|
6644
6672
|
position = start;
|
|
6645
6673
|
}
|
|
6646
|
-
return latest.cwd === undefined && latest.title === undefined ? [] : [latest];
|
|
6674
|
+
return latest.cwd === undefined && latest.title === undefined && latest.starred === undefined ? [] : [latest];
|
|
6647
6675
|
} finally {
|
|
6648
6676
|
await handle.close();
|
|
6649
6677
|
}
|
|
@@ -6652,6 +6680,7 @@ async function readSessionListTrailingPatches(
|
|
|
6652
6680
|
function applySessionListHeaderPatch(header: SessionListHeader, patch: HeaderPatchRecord["patch"]): void {
|
|
6653
6681
|
if (typeof patch.cwd === "string") header.cwd = patch.cwd;
|
|
6654
6682
|
if (typeof patch.title === "string") header.title = patch.title;
|
|
6683
|
+
if (typeof patch.starred === "boolean") header.starred = patch.starred;
|
|
6655
6684
|
}
|
|
6656
6685
|
function decodeJsonStringFragment(value: string): string {
|
|
6657
6686
|
const safeValue = value.endsWith("\\") ? value.slice(0, -1) : value;
|
|
@@ -6702,6 +6731,79 @@ function extractStringProperty(source: string, name: string, startIndex = 0): st
|
|
|
6702
6731
|
return decodeJsonStringFragment(source.slice(valueStart));
|
|
6703
6732
|
}
|
|
6704
6733
|
|
|
6734
|
+
function extractTopLevelPrimitiveProperty(source: string, name: string): string | undefined {
|
|
6735
|
+
let depth = 0;
|
|
6736
|
+
let latest: string | undefined;
|
|
6737
|
+
for (let index = 0; index < source.length; index++) {
|
|
6738
|
+
const char = source.charCodeAt(index);
|
|
6739
|
+
if (char === 0x7b || char === 0x5b) {
|
|
6740
|
+
depth++;
|
|
6741
|
+
continue;
|
|
6742
|
+
}
|
|
6743
|
+
if (char === 0x7d || char === 0x5d) {
|
|
6744
|
+
depth--;
|
|
6745
|
+
continue;
|
|
6746
|
+
}
|
|
6747
|
+
if (char !== 0x22) continue;
|
|
6748
|
+
|
|
6749
|
+
const keyStart = index;
|
|
6750
|
+
let escaped = false;
|
|
6751
|
+
for (index++; index < source.length; index++) {
|
|
6752
|
+
const stringChar = source.charCodeAt(index);
|
|
6753
|
+
if (escaped) {
|
|
6754
|
+
escaped = false;
|
|
6755
|
+
continue;
|
|
6756
|
+
}
|
|
6757
|
+
if (stringChar === 0x5c) {
|
|
6758
|
+
escaped = true;
|
|
6759
|
+
continue;
|
|
6760
|
+
}
|
|
6761
|
+
if (stringChar === 0x22) break;
|
|
6762
|
+
}
|
|
6763
|
+
if (index >= source.length || depth !== 1) continue;
|
|
6764
|
+
|
|
6765
|
+
let key: unknown;
|
|
6766
|
+
try {
|
|
6767
|
+
key = JSON.parse(source.slice(keyStart, index + 1));
|
|
6768
|
+
} catch {
|
|
6769
|
+
continue;
|
|
6770
|
+
}
|
|
6771
|
+
if (key !== name) continue;
|
|
6772
|
+
|
|
6773
|
+
let valueStart = index + 1;
|
|
6774
|
+
while (valueStart < source.length && /\s/.test(source[valueStart]!)) valueStart++;
|
|
6775
|
+
if (source.charCodeAt(valueStart) !== 0x3a) continue;
|
|
6776
|
+
valueStart++;
|
|
6777
|
+
while (valueStart < source.length && /\s/.test(source[valueStart]!)) valueStart++;
|
|
6778
|
+
|
|
6779
|
+
let valueEnd = valueStart;
|
|
6780
|
+
while (
|
|
6781
|
+
valueEnd < source.length &&
|
|
6782
|
+
source[valueEnd] !== "," &&
|
|
6783
|
+
source[valueEnd] !== "}" &&
|
|
6784
|
+
!/\s/.test(source[valueEnd]!)
|
|
6785
|
+
)
|
|
6786
|
+
valueEnd++;
|
|
6787
|
+
latest = source.slice(valueStart, valueEnd);
|
|
6788
|
+
index = valueEnd - 1;
|
|
6789
|
+
}
|
|
6790
|
+
return latest;
|
|
6791
|
+
}
|
|
6792
|
+
|
|
6793
|
+
function extractTopLevelIntegerProperty(source: string, name: string): number | undefined {
|
|
6794
|
+
const token = extractTopLevelPrimitiveProperty(source, name);
|
|
6795
|
+
if (token === undefined || !/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(token)) return undefined;
|
|
6796
|
+
const value = Number(token);
|
|
6797
|
+
return Number.isSafeInteger(value) ? value : undefined;
|
|
6798
|
+
}
|
|
6799
|
+
|
|
6800
|
+
function extractTopLevelBooleanProperty(source: string, name: string): boolean | undefined {
|
|
6801
|
+
const token = extractTopLevelPrimitiveProperty(source, name);
|
|
6802
|
+
if (token === "true") return true;
|
|
6803
|
+
if (token === "false") return false;
|
|
6804
|
+
return undefined;
|
|
6805
|
+
}
|
|
6806
|
+
|
|
6705
6807
|
function countMessageMarkers(content: string): number {
|
|
6706
6808
|
let count = 0;
|
|
6707
6809
|
let index = 0;
|
|
@@ -6740,6 +6842,8 @@ interface SessionListHeader {
|
|
|
6740
6842
|
|
|
6741
6843
|
cwd?: string;
|
|
6742
6844
|
title?: string;
|
|
6845
|
+
starredPatchVersion?: 1;
|
|
6846
|
+
starred?: boolean;
|
|
6743
6847
|
parentSession?: string;
|
|
6744
6848
|
timestamp?: string;
|
|
6745
6849
|
}
|
|
@@ -6748,6 +6852,10 @@ function parseSessionListHeader(
|
|
|
6748
6852
|
content: string,
|
|
6749
6853
|
entries: Array<Record<string, unknown>>,
|
|
6750
6854
|
): SessionListHeader | undefined {
|
|
6855
|
+
const firstLineEnd = content.indexOf("\n");
|
|
6856
|
+
const firstLine = firstLineEnd === -1 ? content : content.slice(0, firstLineEnd);
|
|
6857
|
+
const inlineStarred = extractTopLevelBooleanProperty(firstLine, "starred");
|
|
6858
|
+
const starredPatchVersion = extractTopLevelIntegerProperty(firstLine, "starredPatchVersion") === 1 ? 1 : undefined;
|
|
6751
6859
|
const parsedHeader = entries[0];
|
|
6752
6860
|
if (parsedHeader?.type === "session" && typeof parsedHeader.id === "string") {
|
|
6753
6861
|
return {
|
|
@@ -6756,13 +6864,13 @@ function parseSessionListHeader(
|
|
|
6756
6864
|
version: typeof parsedHeader.version === "number" ? parsedHeader.version : undefined,
|
|
6757
6865
|
cwd: typeof parsedHeader.cwd === "string" ? parsedHeader.cwd : undefined,
|
|
6758
6866
|
title: typeof parsedHeader.title === "string" ? parsedHeader.title : undefined,
|
|
6867
|
+
starredPatchVersion: parsedHeader.starredPatchVersion === 1 ? 1 : starredPatchVersion,
|
|
6868
|
+
starred: typeof parsedHeader.starred === "boolean" ? parsedHeader.starred : inlineStarred,
|
|
6759
6869
|
parentSession: typeof parsedHeader.parentSession === "string" ? parsedHeader.parentSession : undefined,
|
|
6760
6870
|
timestamp: typeof parsedHeader.timestamp === "string" ? parsedHeader.timestamp : undefined,
|
|
6761
6871
|
};
|
|
6762
6872
|
}
|
|
6763
6873
|
|
|
6764
|
-
const firstLineEnd = content.indexOf("\n");
|
|
6765
|
-
const firstLine = firstLineEnd === -1 ? content : content.slice(0, firstLineEnd);
|
|
6766
6874
|
if (extractStringProperty(firstLine, "type") !== "session") return undefined;
|
|
6767
6875
|
|
|
6768
6876
|
const id = extractStringProperty(firstLine, "id");
|
|
@@ -6771,9 +6879,11 @@ function parseSessionListHeader(
|
|
|
6771
6879
|
return {
|
|
6772
6880
|
type: "session",
|
|
6773
6881
|
id,
|
|
6774
|
-
version:
|
|
6882
|
+
version: extractTopLevelIntegerProperty(firstLine, "version"),
|
|
6775
6883
|
cwd: extractStringProperty(firstLine, "cwd"),
|
|
6776
6884
|
title: extractStringProperty(firstLine, "title"),
|
|
6885
|
+
starred: inlineStarred,
|
|
6886
|
+
starredPatchVersion,
|
|
6777
6887
|
parentSession: extractStringProperty(firstLine, "parentSession"),
|
|
6778
6888
|
timestamp: extractStringProperty(firstLine, "timestamp"),
|
|
6779
6889
|
};
|
|
@@ -6798,8 +6908,9 @@ async function collectSessionFromFile(
|
|
|
6798
6908
|
const entries = parseSessionEntries(content).map(entry => entry as unknown as Record<string, unknown>);
|
|
6799
6909
|
const header = parseSessionListHeader(content, entries);
|
|
6800
6910
|
if (!header) return undefined;
|
|
6911
|
+
if (header.starredPatchVersion !== 1) header.starred = undefined;
|
|
6801
6912
|
if (typeof header.version === "number" && header.version >= 4 && header.version <= CURRENT_SESSION_VERSION) {
|
|
6802
|
-
for (const patch of await readSessionListTrailingPatches(file, storage)) {
|
|
6913
|
+
for (const patch of await readSessionListTrailingPatches(file, storage, header.starredPatchVersion === 1)) {
|
|
6803
6914
|
applySessionListHeaderPatch(header, patch);
|
|
6804
6915
|
}
|
|
6805
6916
|
}
|
|
@@ -6841,6 +6952,7 @@ async function collectSessionFromFile(
|
|
|
6841
6952
|
id: header.id,
|
|
6842
6953
|
cwd: header.cwd ?? "",
|
|
6843
6954
|
title: header.title ?? shortSummary,
|
|
6955
|
+
starred: header.starred === true,
|
|
6844
6956
|
parentSessionPath: header.parentSession,
|
|
6845
6957
|
created: new Date(header.timestamp ?? ""),
|
|
6846
6958
|
modified: stats.mtime,
|
|
@@ -7268,6 +7380,7 @@ export class SessionManager {
|
|
|
7268
7380
|
#flushed: boolean = false;
|
|
7269
7381
|
#needsFullRewriteOnNextPersist: boolean = false;
|
|
7270
7382
|
#readOnlyResume = false;
|
|
7383
|
+
#strictResumeMutationPending = false;
|
|
7271
7384
|
#resumedDraftConsumed = false;
|
|
7272
7385
|
#ensuredOnDisk: boolean = false;
|
|
7273
7386
|
#recoveryHydrationContext: RecoveryHydrationContext | undefined;
|
|
@@ -7296,10 +7409,16 @@ export class SessionManager {
|
|
|
7296
7409
|
/** Defense-in-depth (#4443): one-shot warn for adjacent private thinking blocks in persisted assistant transcripts. */
|
|
7297
7410
|
#warnedAdjacentThinkingPersist = false;
|
|
7298
7411
|
#closeRetryPending = false;
|
|
7412
|
+
#strictClosePending = false;
|
|
7299
7413
|
/** Serializes model, SDK, and ACP cwd transitions; dispose joins this tail. */
|
|
7300
7414
|
#cwdTransitionTail: Promise<void> = Promise.resolve();
|
|
7301
7415
|
#cwdTransitionOwner: symbol | undefined;
|
|
7416
|
+
#cwdMoveAdmissionClosing = false;
|
|
7302
7417
|
#cwdMoveAdmissionClosed = false;
|
|
7418
|
+
#pendingCwdMoveAdmissions = new Set<AbortController>();
|
|
7419
|
+
#cwdMoveAdmittedAdmissions = new Set<AbortController>();
|
|
7420
|
+
#cwdMoveAdmittedOwners = new Set<symbol>();
|
|
7421
|
+
#cwdMoveAdmittedOwnerCounts = new Map<symbol, number>();
|
|
7303
7422
|
#cwdReadLeaseOwner = Symbol("cwd-read-lease-owner");
|
|
7304
7423
|
#cwdGeneration = 0;
|
|
7305
7424
|
/**
|
|
@@ -8165,6 +8284,7 @@ export class SessionManager {
|
|
|
8165
8284
|
header: {
|
|
8166
8285
|
type: "session",
|
|
8167
8286
|
version: CURRENT_SESSION_VERSION,
|
|
8287
|
+
starredPatchVersion: 1,
|
|
8168
8288
|
id: sessionId,
|
|
8169
8289
|
timestamp,
|
|
8170
8290
|
cwd: this.cwd,
|
|
@@ -9721,6 +9841,7 @@ export class SessionManager {
|
|
|
9721
9841
|
const header: SessionHeader = {
|
|
9722
9842
|
type: "session",
|
|
9723
9843
|
version: CURRENT_SESSION_VERSION,
|
|
9844
|
+
starredPatchVersion: 1,
|
|
9724
9845
|
id: sessionId,
|
|
9725
9846
|
timestamp,
|
|
9726
9847
|
cwd: this.cwd,
|
|
@@ -9929,6 +10050,7 @@ export class SessionManager {
|
|
|
9929
10050
|
const header: SessionHeader = {
|
|
9930
10051
|
type: "session",
|
|
9931
10052
|
version: CURRENT_SESSION_VERSION,
|
|
10053
|
+
starredPatchVersion: 1,
|
|
9932
10054
|
id: sessionId,
|
|
9933
10055
|
title: this.#sessionName,
|
|
9934
10056
|
titleSource: this.#titleSource,
|
|
@@ -9997,6 +10119,7 @@ export class SessionManager {
|
|
|
9997
10119
|
const header: SessionHeader = {
|
|
9998
10120
|
type: "session",
|
|
9999
10121
|
version: CURRENT_SESSION_VERSION,
|
|
10122
|
+
starredPatchVersion: 1,
|
|
10000
10123
|
id: sessionId,
|
|
10001
10124
|
timestamp,
|
|
10002
10125
|
cwd: this.cwd,
|
|
@@ -10319,6 +10442,7 @@ export class SessionManager {
|
|
|
10319
10442
|
const newHeader: SessionHeader = {
|
|
10320
10443
|
type: "session",
|
|
10321
10444
|
version: CURRENT_SESSION_VERSION,
|
|
10445
|
+
starredPatchVersion: 1,
|
|
10322
10446
|
id: newSessionId,
|
|
10323
10447
|
title: oldHeader?.title ?? this.#sessionName,
|
|
10324
10448
|
titleSource: oldHeader?.titleSource ?? this.#titleSource,
|
|
@@ -10691,11 +10815,32 @@ export class SessionManager {
|
|
|
10691
10815
|
* only for the async context that already owns the lock — unrelated callers
|
|
10692
10816
|
* queue on the tail instead of skipping it.
|
|
10693
10817
|
*/
|
|
10694
|
-
async runExclusiveCwdTransition<T>(fn: () => Promise<T
|
|
10818
|
+
async runExclusiveCwdTransition<T>(fn: () => Promise<T>, options?: { signal?: AbortSignal }): Promise<T> {
|
|
10695
10819
|
const owner = this.#cwdTransitionOwner;
|
|
10696
10820
|
if (owner !== undefined && cwdTransitionAls.getStore() === owner) {
|
|
10697
10821
|
return fn();
|
|
10698
10822
|
}
|
|
10823
|
+
const signal = options?.signal;
|
|
10824
|
+
const throwIfAborted = (): void => {
|
|
10825
|
+
if (!signal?.aborted) return;
|
|
10826
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("Session cwd move admission is closed.");
|
|
10827
|
+
};
|
|
10828
|
+
const waitAbortably = async (promise: Promise<void>): Promise<void> => {
|
|
10829
|
+
throwIfAborted();
|
|
10830
|
+
if (!signal) {
|
|
10831
|
+
await promise;
|
|
10832
|
+
return;
|
|
10833
|
+
}
|
|
10834
|
+
const aborted = Promise.withResolvers<void>();
|
|
10835
|
+
const onAbort = () => aborted.resolve();
|
|
10836
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
10837
|
+
try {
|
|
10838
|
+
await Promise.race([promise, aborted.promise]);
|
|
10839
|
+
} finally {
|
|
10840
|
+
signal.removeEventListener("abort", onAbort);
|
|
10841
|
+
}
|
|
10842
|
+
throwIfAborted();
|
|
10843
|
+
};
|
|
10699
10844
|
const previous = this.#cwdTransitionTail;
|
|
10700
10845
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
10701
10846
|
this.#cwdTransitionTail = previous.then(
|
|
@@ -10707,11 +10852,11 @@ export class SessionManager {
|
|
|
10707
10852
|
this.#cwdWriterPending += 1;
|
|
10708
10853
|
const token = Symbol("cwd-transition");
|
|
10709
10854
|
try {
|
|
10710
|
-
await previous.catch(() => {});
|
|
10855
|
+
await waitAbortably(previous.catch(() => {}));
|
|
10711
10856
|
// A reader that entered before this writer was announced still holds the
|
|
10712
10857
|
// old cwd; the transition may not commit until every such lease is
|
|
10713
10858
|
// released, otherwise an in-flight tool resolves paths across the move.
|
|
10714
|
-
while (this.#cwdReaderCount > 0) await this.#cwdReadersIdle;
|
|
10859
|
+
while (this.#cwdReaderCount > 0) await waitAbortably(this.#cwdReadersIdle);
|
|
10715
10860
|
this.#cwdTransitionOwner = token;
|
|
10716
10861
|
return await cwdTransitionAls.run(token, fn);
|
|
10717
10862
|
} finally {
|
|
@@ -10721,6 +10866,90 @@ export class SessionManager {
|
|
|
10721
10866
|
}
|
|
10722
10867
|
}
|
|
10723
10868
|
|
|
10869
|
+
async runExclusiveCwdMoveTransition<T>(fn: () => Promise<T>): Promise<T> {
|
|
10870
|
+
if (this.#cwdMoveAdmissionClosing || this.#cwdMoveAdmissionClosed)
|
|
10871
|
+
throw new Error("Session cwd move admission is closed.");
|
|
10872
|
+
const transitionOwner = this.#cwdTransitionOwner;
|
|
10873
|
+
if (
|
|
10874
|
+
this.#strictClosePending &&
|
|
10875
|
+
(transitionOwner === undefined || cwdTransitionAls.getStore() !== transitionOwner)
|
|
10876
|
+
)
|
|
10877
|
+
throw new Error("Session manager is closing.");
|
|
10878
|
+
const readLease = cwdReadLeaseAls.getStore();
|
|
10879
|
+
const rootLease =
|
|
10880
|
+
readLease?.owner === this.#cwdReadLeaseOwner && !readLease.released
|
|
10881
|
+
? (readLease.root ?? readLease)
|
|
10882
|
+
: undefined;
|
|
10883
|
+
const suspendedOwnReadLease = rootLease !== undefined;
|
|
10884
|
+
if (rootLease) {
|
|
10885
|
+
// A move may suspend the shared root lease only when this execution is
|
|
10886
|
+
// the sole active descendant. If a sibling tool is still fenced by the
|
|
10887
|
+
// lease, returning a retry error is safer than restoring the lease while
|
|
10888
|
+
// that sibling continues against the old cwd.
|
|
10889
|
+
const activeBranchLimit = readLease === rootLease ? 1 : 2;
|
|
10890
|
+
if (
|
|
10891
|
+
rootLease.activeBranches > activeBranchLimit ||
|
|
10892
|
+
(rootLease.suspendedMoveOwner !== undefined && rootLease.suspendedMoveOwner !== readLease?.executionId)
|
|
10893
|
+
)
|
|
10894
|
+
throw new Error("Session working directory changed while this tool executed; retry against the new cwd.");
|
|
10895
|
+
rootLease.suspendedMoveCount += 1;
|
|
10896
|
+
rootLease.suspendedMoveOwner ??= readLease?.executionId;
|
|
10897
|
+
if (rootLease.active) {
|
|
10898
|
+
rootLease.active = false;
|
|
10899
|
+
this.#cwdReaderCount -= 1;
|
|
10900
|
+
if (this.#cwdReaderCount === 0) {
|
|
10901
|
+
const drained = this.#cwdReadersDrained;
|
|
10902
|
+
this.#cwdReadersDrained = undefined;
|
|
10903
|
+
drained?.();
|
|
10904
|
+
}
|
|
10905
|
+
}
|
|
10906
|
+
}
|
|
10907
|
+
const admittedBeforeClose = this.#cwdWriterPending === 0 && this.#cwdReaderCount === 0;
|
|
10908
|
+
const admission = new AbortController();
|
|
10909
|
+
this.#pendingCwdMoveAdmissions.add(admission);
|
|
10910
|
+
if (admittedBeforeClose) this.#cwdMoveAdmittedAdmissions.add(admission);
|
|
10911
|
+
try {
|
|
10912
|
+
return await this.runExclusiveCwdTransition(
|
|
10913
|
+
async () => {
|
|
10914
|
+
const owner = this.#cwdTransitionOwner;
|
|
10915
|
+
if (owner === undefined) throw new Error("Session cwd move transition owner is unavailable.");
|
|
10916
|
+
this.#cwdMoveAdmittedAdmissions.add(admission);
|
|
10917
|
+
this.#cwdMoveAdmittedOwners.add(owner);
|
|
10918
|
+
this.#cwdMoveAdmittedOwnerCounts.set(owner, (this.#cwdMoveAdmittedOwnerCounts.get(owner) ?? 0) + 1);
|
|
10919
|
+
try {
|
|
10920
|
+
return await fn();
|
|
10921
|
+
} finally {
|
|
10922
|
+
this.#cwdMoveAdmittedAdmissions.delete(admission);
|
|
10923
|
+
const count = this.#cwdMoveAdmittedOwnerCounts.get(owner) ?? 0;
|
|
10924
|
+
if (count <= 1) {
|
|
10925
|
+
this.#cwdMoveAdmittedOwnerCounts.delete(owner);
|
|
10926
|
+
this.#cwdMoveAdmittedOwners.delete(owner);
|
|
10927
|
+
} else {
|
|
10928
|
+
this.#cwdMoveAdmittedOwnerCounts.set(owner, count - 1);
|
|
10929
|
+
}
|
|
10930
|
+
}
|
|
10931
|
+
},
|
|
10932
|
+
{ signal: admission.signal },
|
|
10933
|
+
);
|
|
10934
|
+
} finally {
|
|
10935
|
+
this.#pendingCwdMoveAdmissions.delete(admission);
|
|
10936
|
+
this.#cwdMoveAdmittedAdmissions.delete(admission);
|
|
10937
|
+
if (suspendedOwnReadLease && rootLease) {
|
|
10938
|
+
rootLease.suspendedMoveCount -= 1;
|
|
10939
|
+
if (rootLease.suspendedMoveCount === 0 && !rootLease.released) {
|
|
10940
|
+
if (this.#cwdReaderCount === 0) {
|
|
10941
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
10942
|
+
this.#cwdReadersIdle = promise;
|
|
10943
|
+
this.#cwdReadersDrained = resolve;
|
|
10944
|
+
}
|
|
10945
|
+
this.#cwdReaderCount += 1;
|
|
10946
|
+
rootLease.active = true;
|
|
10947
|
+
rootLease.suspendedMoveOwner = undefined;
|
|
10948
|
+
}
|
|
10949
|
+
}
|
|
10950
|
+
}
|
|
10951
|
+
}
|
|
10952
|
+
|
|
10724
10953
|
/**
|
|
10725
10954
|
* Run `fn` under a shared read lease on `cwd`.
|
|
10726
10955
|
*
|
|
@@ -10734,14 +10963,39 @@ export class SessionManager {
|
|
|
10734
10963
|
async runWithCwdReadLease<T>(fn: () => Promise<T>): Promise<T> {
|
|
10735
10964
|
const activeReadLease = cwdReadLeaseAls.getStore();
|
|
10736
10965
|
// Nested tool dispatch (for example eval -> tool bridge) inherits the
|
|
10737
|
-
// outer lease's
|
|
10738
|
-
//
|
|
10739
|
-
|
|
10740
|
-
|
|
10966
|
+
// outer lease's cwd authority, but receives an execution-local context so
|
|
10967
|
+
// sibling descendants cannot attribute one another's nested move.
|
|
10968
|
+
if (activeReadLease?.owner === this.#cwdReadLeaseOwner && !activeReadLease.released) {
|
|
10969
|
+
const rootLease = activeReadLease.root ?? activeReadLease;
|
|
10970
|
+
if (rootLease.suspendedMoveOwner !== undefined)
|
|
10971
|
+
throw new Error("Session working directory changed while this tool executed; retry against the new cwd.");
|
|
10972
|
+
rootLease.activeBranches += 1;
|
|
10973
|
+
const nestedReadLease: CwdReadLeaseContext = {
|
|
10974
|
+
active: true,
|
|
10975
|
+
released: false,
|
|
10976
|
+
owner: this.#cwdReadLeaseOwner,
|
|
10977
|
+
parent: activeReadLease,
|
|
10978
|
+
root: rootLease,
|
|
10979
|
+
executionId: Symbol("cwd-read-execution"),
|
|
10980
|
+
activeBranches: 0,
|
|
10981
|
+
suspendedMoveCount: 0,
|
|
10982
|
+
};
|
|
10983
|
+
return cwdReadLeaseAls.run(nestedReadLease, async () => {
|
|
10984
|
+
try {
|
|
10985
|
+
return await fn();
|
|
10986
|
+
} finally {
|
|
10987
|
+
if (nestedReadLease.committedMoveGeneration !== undefined)
|
|
10988
|
+
activeReadLease.committedMoveGeneration = nestedReadLease.committedMoveGeneration;
|
|
10989
|
+
nestedReadLease.released = true;
|
|
10990
|
+
rootLease.activeBranches -= 1;
|
|
10991
|
+
}
|
|
10992
|
+
});
|
|
10993
|
+
}
|
|
10741
10994
|
const owner = this.#cwdTransitionOwner;
|
|
10742
10995
|
// The writer's own async context already holds exclusive access; taking a
|
|
10743
10996
|
// read lease there would wait on itself.
|
|
10744
10997
|
if (owner !== undefined && cwdTransitionAls.getStore() === owner) return fn();
|
|
10998
|
+
if (this.#strictClosePending || this.#cwdMoveAdmissionClosed) throw new Error("Session manager is closing.");
|
|
10745
10999
|
while (this.#cwdWriterPending > 0) await this.#cwdTransitionTail.catch(() => {});
|
|
10746
11000
|
if (this.#cwdReaderCount === 0) {
|
|
10747
11001
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
@@ -10749,18 +11003,29 @@ export class SessionManager {
|
|
|
10749
11003
|
this.#cwdReadersDrained = resolve;
|
|
10750
11004
|
}
|
|
10751
11005
|
this.#cwdReaderCount += 1;
|
|
10752
|
-
const readLease: CwdReadLeaseContext = {
|
|
11006
|
+
const readLease: CwdReadLeaseContext = {
|
|
11007
|
+
active: true,
|
|
11008
|
+
released: false,
|
|
11009
|
+
owner: this.#cwdReadLeaseOwner,
|
|
11010
|
+
suspendedMoveCount: 0,
|
|
11011
|
+
executionId: Symbol("cwd-read-execution"),
|
|
11012
|
+
activeBranches: 1,
|
|
11013
|
+
};
|
|
11014
|
+
readLease.root = readLease;
|
|
10753
11015
|
return cwdReadLeaseAls.run(readLease, async () => {
|
|
10754
11016
|
try {
|
|
10755
11017
|
return await fn();
|
|
10756
11018
|
} finally {
|
|
10757
|
-
readLease.active
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
11019
|
+
if (readLease.active) {
|
|
11020
|
+
readLease.active = false;
|
|
11021
|
+
this.#cwdReaderCount -= 1;
|
|
11022
|
+
if (this.#cwdReaderCount === 0) {
|
|
11023
|
+
const drained = this.#cwdReadersDrained;
|
|
11024
|
+
this.#cwdReadersDrained = undefined;
|
|
11025
|
+
drained?.();
|
|
11026
|
+
}
|
|
10763
11027
|
}
|
|
11028
|
+
readLease.released = true;
|
|
10764
11029
|
}
|
|
10765
11030
|
});
|
|
10766
11031
|
}
|
|
@@ -10770,15 +11035,36 @@ export class SessionManager {
|
|
|
10770
11035
|
await this.#cwdTransitionTail;
|
|
10771
11036
|
}
|
|
10772
11037
|
|
|
11038
|
+
async joinCwdReaders(): Promise<void> {
|
|
11039
|
+
while (this.#cwdReaderCount > 0) await this.#cwdReadersIdle;
|
|
11040
|
+
}
|
|
11041
|
+
|
|
10773
11042
|
async closeCwdMoveAdmission(): Promise<void> {
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
11043
|
+
if (this.#cwdMoveAdmissionClosed) return;
|
|
11044
|
+
if (this.#cwdMoveAdmissionClosing) return;
|
|
11045
|
+
this.#cwdMoveAdmissionClosing = true;
|
|
11046
|
+
this.#cwdMoveAdmissionClosed = true;
|
|
11047
|
+
// Preserve moves that already acquired exclusive ownership, but cancel every
|
|
11048
|
+
// queued admission. A queued writer can otherwise pass the initial admission
|
|
11049
|
+
// check after an earlier writer finishes and mutate the closed session.
|
|
11050
|
+
for (const admission of this.#pendingCwdMoveAdmissions) {
|
|
11051
|
+
if (!this.#cwdMoveAdmittedAdmissions.has(admission)) {
|
|
11052
|
+
admission.abort(new Error("Session cwd move admission is closed."));
|
|
11053
|
+
}
|
|
11054
|
+
}
|
|
10777
11055
|
}
|
|
10778
11056
|
getCwdGeneration(): number {
|
|
10779
11057
|
return this.#cwdGeneration;
|
|
10780
11058
|
}
|
|
10781
11059
|
|
|
11060
|
+
/** Consume a move committed by a nested move_session call in this read lease. */
|
|
11061
|
+
consumeNestedCwdMove(admittedGeneration: number): boolean {
|
|
11062
|
+
const readLease = cwdReadLeaseAls.getStore();
|
|
11063
|
+
const committed = readLease?.committedMoveGeneration;
|
|
11064
|
+
if (readLease) readLease.committedMoveGeneration = undefined;
|
|
11065
|
+
return committed !== undefined && committed !== admittedGeneration && committed === this.#cwdGeneration;
|
|
11066
|
+
}
|
|
11067
|
+
|
|
10782
11068
|
/**
|
|
10783
11069
|
* Register a listener invoked after every committed `moveTo`. Returns an idempotent
|
|
10784
11070
|
* unregister handle. Every move surface funnels through `moveTo`, so a single
|
|
@@ -10976,10 +11262,13 @@ export class SessionManager {
|
|
|
10976
11262
|
sourceHandle?: { stat: (opts: { bigint: true }) => Promise<fs.BigIntStats> };
|
|
10977
11263
|
},
|
|
10978
11264
|
): Promise<void> {
|
|
11265
|
+
if (this.#strictClosePending && !this.#ownsCwdTransition()) throw new Error("Session manager is closing.");
|
|
10979
11266
|
if (!this.#ownsCwdTransition()) {
|
|
10980
|
-
return this.
|
|
11267
|
+
return this.runExclusiveCwdMoveTransition(() => this.moveTo(newCwd, options));
|
|
10981
11268
|
}
|
|
10982
|
-
|
|
11269
|
+
const owner = this.#cwdTransitionOwner;
|
|
11270
|
+
if (this.#cwdMoveAdmissionClosed && (owner === undefined || !this.#cwdMoveAdmittedOwners.has(owner)))
|
|
11271
|
+
throw new Error("Session cwd move admission is closed.");
|
|
10983
11272
|
if (!options?.sourceHandle) {
|
|
10984
11273
|
const sourceHandle = await SessionManager.openNoFollowDirectory(this.cwd);
|
|
10985
11274
|
try {
|
|
@@ -11366,6 +11655,12 @@ export class SessionManager {
|
|
|
11366
11655
|
}
|
|
11367
11656
|
}
|
|
11368
11657
|
this.#cwdGeneration += 1;
|
|
11658
|
+
const readLease = cwdReadLeaseAls.getStore();
|
|
11659
|
+
if (readLease?.owner === this.#cwdReadLeaseOwner) {
|
|
11660
|
+
readLease.committedMoveGeneration = this.#cwdGeneration;
|
|
11661
|
+
for (let parent = readLease.parent; parent; parent = parent.parent)
|
|
11662
|
+
parent.committedMoveGeneration = this.#cwdGeneration;
|
|
11663
|
+
}
|
|
11369
11664
|
this.cwd = resolvedCwd;
|
|
11370
11665
|
this.sessionDir = newSessionDir;
|
|
11371
11666
|
this.destination = nextDestination;
|
|
@@ -11384,13 +11679,13 @@ export class SessionManager {
|
|
|
11384
11679
|
const hasAssistant = this.#fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
|
|
11385
11680
|
try {
|
|
11386
11681
|
if (this.persist && this.#sessionFile && hadSessionFile) {
|
|
11387
|
-
await this.#appendHeaderPatch({ cwd: resolvedCwd });
|
|
11682
|
+
await this.#appendHeaderPatch({ cwd: resolvedCwd }, { allowStrictClose: true });
|
|
11388
11683
|
await this.#rewriteFile();
|
|
11389
11684
|
} else if (this.persist && this.#sessionFile && (hasAssistant || (!hadSessionFile && hadPersistedSession))) {
|
|
11390
|
-
await this.#appendHeaderPatch({ cwd: resolvedCwd });
|
|
11685
|
+
await this.#appendHeaderPatch({ cwd: resolvedCwd }, { allowStrictClose: true });
|
|
11391
11686
|
await this.#rewriteFile();
|
|
11392
11687
|
} else {
|
|
11393
|
-
await this.#appendHeaderPatch({ cwd: resolvedCwd });
|
|
11688
|
+
await this.#appendHeaderPatch({ cwd: resolvedCwd }, { allowStrictClose: true });
|
|
11394
11689
|
}
|
|
11395
11690
|
} catch (error) {
|
|
11396
11691
|
residentTransition?.dispose();
|
|
@@ -15797,6 +16092,7 @@ export class SessionManager {
|
|
|
15797
16092
|
* Used by ACP mode where session/new must create a discoverable session immediately.
|
|
15798
16093
|
*/
|
|
15799
16094
|
async ensureOnDisk(): Promise<void> {
|
|
16095
|
+
this.#assertRecoveryHydrationWritable();
|
|
15800
16096
|
if (!this.persist || !this.#sessionFile) return;
|
|
15801
16097
|
if (this.#readOnlyResume) return;
|
|
15802
16098
|
if (this.#flushed && !this.#needsFullRewriteOnNextPersist) return;
|
|
@@ -15906,7 +16202,9 @@ export class SessionManager {
|
|
|
15906
16202
|
|
|
15907
16203
|
/** Close the persistent writer after flushing all pending data. */
|
|
15908
16204
|
async close(): Promise<void> {
|
|
16205
|
+
await this.closeCwdMoveAdmission();
|
|
15909
16206
|
await this.joinCwdTransition();
|
|
16207
|
+
await this.joinCwdReaders();
|
|
15910
16208
|
SessionManager.releaseProcessCwdOwnership(this);
|
|
15911
16209
|
// Drain any uncommitted prepared successors before releasing resources so
|
|
15912
16210
|
// dispose/shutdown retains exact cleanup authority (#3138).
|
|
@@ -15948,10 +16246,16 @@ export class SessionManager {
|
|
|
15948
16246
|
}
|
|
15949
16247
|
/** Flush while open, then strictly close; retryable close skips the invalid second flush. */
|
|
15950
16248
|
async flushAndCloseStrict(): Promise<SessionManagerCloseOutcome> {
|
|
16249
|
+
let priorFlushError: Error | undefined;
|
|
15951
16250
|
if (this.#persistWriter?.getCloseState() !== "close_failed_retryable") {
|
|
15952
|
-
|
|
16251
|
+
try {
|
|
16252
|
+
await this.flush();
|
|
16253
|
+
} catch (error) {
|
|
16254
|
+
priorFlushError = toError(error);
|
|
16255
|
+
}
|
|
15953
16256
|
}
|
|
15954
|
-
|
|
16257
|
+
const outcome = await this.closeStrict();
|
|
16258
|
+
return priorFlushError && outcome.kind === "closed" ? { kind: "close_unknown", error: priorFlushError } : outcome;
|
|
15955
16259
|
}
|
|
15956
16260
|
|
|
15957
16261
|
/**
|
|
@@ -15961,45 +16265,114 @@ export class SessionManager {
|
|
|
15961
16265
|
* writer closure before any destructive operation.
|
|
15962
16266
|
*/
|
|
15963
16267
|
async closeStrict(): Promise<SessionManagerCloseOutcome> {
|
|
16268
|
+
this.#strictClosePending = true;
|
|
16269
|
+
await this.closeCwdMoveAdmission();
|
|
16270
|
+
await this.joinCwdTransition();
|
|
16271
|
+
await this.joinCwdReaders();
|
|
15964
16272
|
// Drain staged successors on the strict ACP dispose path as well as best-effort close (#3138).
|
|
16273
|
+
let preparedCleanupError: Error | undefined;
|
|
15965
16274
|
try {
|
|
15966
16275
|
await this.#retryPreparedNewSessionCleanups();
|
|
15967
16276
|
} catch (error) {
|
|
16277
|
+
preparedCleanupError = toError(error);
|
|
15968
16278
|
logger.warn("Prepared session cleanup during closeStrict failed; retained for retry", {
|
|
15969
|
-
error:
|
|
16279
|
+
error: preparedCleanupError.message,
|
|
15970
16280
|
});
|
|
15971
16281
|
}
|
|
15972
|
-
|
|
15973
|
-
|
|
15974
|
-
|
|
15975
|
-
|
|
15976
|
-
|
|
15977
|
-
|
|
16282
|
+
if (preparedCleanupError) return { kind: "close_failed_retryable", error: preparedCleanupError };
|
|
16283
|
+
let priorPersistError = this.#persistError;
|
|
16284
|
+
if (this.#needsFullRewriteOnNextPersist && (!this.#readOnlyResume || this.#strictResumeMutationPending)) {
|
|
16285
|
+
await this.#persistChain.catch(() => {});
|
|
16286
|
+
try {
|
|
16287
|
+
await this.#rewriteFileContents();
|
|
16288
|
+
if (this.#strictResumeMutationPending && this.#readOnlyResume && this.#sessionFile) {
|
|
16289
|
+
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
16290
|
+
this.#readOnlyResume = false;
|
|
16291
|
+
}
|
|
16292
|
+
this.#strictResumeMutationPending = false;
|
|
16293
|
+
} catch (error) {
|
|
16294
|
+
priorPersistError ??= toError(error);
|
|
16295
|
+
return { kind: "close_failed_retryable", error: priorPersistError };
|
|
15978
16296
|
}
|
|
16297
|
+
}
|
|
16298
|
+
let outcome: SessionManagerCloseOutcome = { kind: "closed" };
|
|
16299
|
+
const closePublicationRevision = {
|
|
16300
|
+
entry: this.#entryRevision,
|
|
16301
|
+
header: this.#headerExportRevision,
|
|
16302
|
+
residentBlob: this.#residentBlobRevision,
|
|
16303
|
+
};
|
|
16304
|
+
await this.#queuePersistTask(
|
|
16305
|
+
async () => {
|
|
16306
|
+
const writer = this.#persistWriter;
|
|
16307
|
+
if (!writer) {
|
|
16308
|
+
this.#flushed = true;
|
|
16309
|
+
return;
|
|
16310
|
+
}
|
|
16311
|
+
try {
|
|
16312
|
+
await writer.close();
|
|
16313
|
+
} catch {
|
|
16314
|
+
// Outcome is captured from the underlying writer's close state below.
|
|
16315
|
+
}
|
|
16316
|
+
outcome = this.#closeOutcomeFromWriter(writer);
|
|
16317
|
+
if (outcome.kind === "closed") {
|
|
16318
|
+
this.#flushed = true;
|
|
16319
|
+
// Confirmed closed: release writer ownership.
|
|
16320
|
+
this.#persistWriter = undefined;
|
|
16321
|
+
this.#persistWriterPath = undefined;
|
|
16322
|
+
} else if (outcome.kind === "close_unknown") {
|
|
16323
|
+
// Quarantined (terminal): release ownership so no retry/finalizer
|
|
16324
|
+
// touches the uncertain fd again.
|
|
16325
|
+
this.#persistWriter = undefined;
|
|
16326
|
+
this.#persistWriterPath = undefined;
|
|
16327
|
+
}
|
|
16328
|
+
// close_failed_retryable: RETAIN the writer so a later closeStrict() call
|
|
16329
|
+
// can actually re-dispatch the OS close (ownership stays proven). The
|
|
16330
|
+
// wrapper must not manufacture success or surrender a retryable fd.
|
|
16331
|
+
},
|
|
16332
|
+
{ ignoreError: true },
|
|
16333
|
+
);
|
|
16334
|
+
// A mutation that entered before strict-close admission can finish its
|
|
16335
|
+
// asynchronous preparation after the initial rewrite check. Re-check the
|
|
16336
|
+
// publication debt after writer closure and repair it before releasing any
|
|
16337
|
+
// resident state; strict admission prevents new callers from creating a
|
|
16338
|
+
// second race while this repair is in flight.
|
|
16339
|
+
let observedPersistError = priorPersistError ?? this.#persistError;
|
|
16340
|
+
const mutationDuringWriterClose =
|
|
16341
|
+
this.#entryRevision !== closePublicationRevision.entry ||
|
|
16342
|
+
this.#headerExportRevision !== closePublicationRevision.header ||
|
|
16343
|
+
this.#residentBlobRevision !== closePublicationRevision.residentBlob;
|
|
16344
|
+
if (mutationDuringWriterClose) {
|
|
16345
|
+
this.#needsFullRewriteOnNextPersist = true;
|
|
16346
|
+
if (this.#readOnlyResume) this.#strictResumeMutationPending = true;
|
|
16347
|
+
}
|
|
16348
|
+
const lateRewriteDebt =
|
|
16349
|
+
(this.#needsFullRewriteOnNextPersist || this.#strictResumeMutationPending) &&
|
|
16350
|
+
(!this.#readOnlyResume || this.#strictResumeMutationPending);
|
|
16351
|
+
if (!observedPersistError && outcome.kind === "closed" && !this.#persistWriter && lateRewriteDebt) {
|
|
15979
16352
|
try {
|
|
15980
|
-
await
|
|
15981
|
-
|
|
15982
|
-
|
|
16353
|
+
await this.#rewriteFileContents();
|
|
16354
|
+
if (this.#strictResumeMutationPending && this.#readOnlyResume && this.#sessionFile) {
|
|
16355
|
+
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
16356
|
+
this.#readOnlyResume = false;
|
|
16357
|
+
}
|
|
16358
|
+
this.#strictResumeMutationPending = false;
|
|
16359
|
+
} catch (error) {
|
|
16360
|
+
observedPersistError = toError(error);
|
|
16361
|
+
outcome = { kind: "close_failed_retryable", error: observedPersistError };
|
|
15983
16362
|
}
|
|
15984
|
-
|
|
15985
|
-
|
|
15986
|
-
|
|
15987
|
-
|
|
15988
|
-
this.#persistWriter = undefined;
|
|
15989
|
-
this.#persistWriterPath = undefined;
|
|
15990
|
-
} else if (outcome.kind === "close_unknown") {
|
|
15991
|
-
// Quarantined (terminal): release ownership so no retry/finalizer
|
|
15992
|
-
// touches the uncertain fd again.
|
|
15993
|
-
this.#persistWriter = undefined;
|
|
15994
|
-
this.#persistWriterPath = undefined;
|
|
15995
|
-
}
|
|
15996
|
-
// close_failed_retryable: RETAIN the writer so a later closeStrict() call
|
|
15997
|
-
// can actually re-dispatch the OS close (ownership stays proven). The
|
|
15998
|
-
// wrapper must not manufacture success or surrender a retryable fd.
|
|
15999
|
-
});
|
|
16363
|
+
}
|
|
16364
|
+
const unpublishedMutation =
|
|
16365
|
+
this.persist &&
|
|
16366
|
+
(this.#strictResumeMutationPending || (this.#needsFullRewriteOnNextPersist && !this.#readOnlyResume));
|
|
16000
16367
|
// Only tear down the resident blob store on a terminal outcome; a retryable
|
|
16001
16368
|
// close leaves the session live for a genuine retry.
|
|
16002
|
-
if (
|
|
16369
|
+
if (
|
|
16370
|
+
!observedPersistError &&
|
|
16371
|
+
!this.#persistWriter &&
|
|
16372
|
+
(!this.persist ||
|
|
16373
|
+
(this.#readOnlyResume && !this.#strictResumeMutationPending) ||
|
|
16374
|
+
(!this.#needsFullRewriteOnNextPersist && !this.#strictResumeMutationPending))
|
|
16375
|
+
) {
|
|
16003
16376
|
this.#releaseResidentTextStore();
|
|
16004
16377
|
this.#retireEphemeralArtifacts();
|
|
16005
16378
|
try {
|
|
@@ -16010,6 +16383,15 @@ export class SessionManager {
|
|
|
16010
16383
|
if (this.#preparedNewSessions.size === 0) this.#releaseOwnedManagedAuthority();
|
|
16011
16384
|
this.#releaseClosedSessionState();
|
|
16012
16385
|
}
|
|
16386
|
+
if (!observedPersistError && !this.#persistWriter && unpublishedMutation && outcome.kind === "closed") {
|
|
16387
|
+
outcome = {
|
|
16388
|
+
kind: "close_failed_retryable",
|
|
16389
|
+
error: new Error("Session manager has unpublished state."),
|
|
16390
|
+
};
|
|
16391
|
+
}
|
|
16392
|
+
if (observedPersistError && outcome.kind === "closed") {
|
|
16393
|
+
return { kind: "close_unknown", error: observedPersistError };
|
|
16394
|
+
}
|
|
16013
16395
|
return outcome;
|
|
16014
16396
|
}
|
|
16015
16397
|
|
|
@@ -16888,6 +17270,49 @@ export class SessionManager {
|
|
|
16888
17270
|
return this.#sessionName;
|
|
16889
17271
|
}
|
|
16890
17272
|
|
|
17273
|
+
isSessionStarred(): boolean {
|
|
17274
|
+
const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
17275
|
+
return header?.starredPatchVersion === 1 && header.starred === true;
|
|
17276
|
+
}
|
|
17277
|
+
|
|
17278
|
+
async setSessionStarred(starred: boolean): Promise<boolean> {
|
|
17279
|
+
this.#assertRecoveryHydrationWritable();
|
|
17280
|
+
const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
17281
|
+
if (!header || this.isSessionStarred() === starred) return false;
|
|
17282
|
+
await this.#ensureStarPatchCapability(header);
|
|
17283
|
+
const previous = header.starred;
|
|
17284
|
+
try {
|
|
17285
|
+
await this.#appendHeaderPatch({ starred });
|
|
17286
|
+
return true;
|
|
17287
|
+
} catch (error) {
|
|
17288
|
+
if (previous === undefined) delete header.starred;
|
|
17289
|
+
else header.starred = previous;
|
|
17290
|
+
this.#headerExportRevision++;
|
|
17291
|
+
throw error;
|
|
17292
|
+
}
|
|
17293
|
+
}
|
|
17294
|
+
|
|
17295
|
+
async #ensureStarPatchCapability(header: SessionHeader): Promise<void> {
|
|
17296
|
+
if (header.starredPatchVersion === 1) return;
|
|
17297
|
+
const previousStarred = header.starred;
|
|
17298
|
+
const previousRewriteRequired = this.#needsFullRewriteOnNextPersist;
|
|
17299
|
+
header.starredPatchVersion = 1;
|
|
17300
|
+
delete header.starred;
|
|
17301
|
+
this.#headerExportRevision++;
|
|
17302
|
+
if (!this.persist || !this.#sessionFile || !this.#storage.existsSync(this.#sessionFile)) return;
|
|
17303
|
+
this.#needsFullRewriteOnNextPersist = true;
|
|
17304
|
+
try {
|
|
17305
|
+
await this.#persistPatches([{ type: "header_patch", patch: {} }]);
|
|
17306
|
+
} catch (error) {
|
|
17307
|
+
delete header.starredPatchVersion;
|
|
17308
|
+
if (previousStarred === undefined) delete header.starred;
|
|
17309
|
+
else header.starred = previousStarred;
|
|
17310
|
+
this.#needsFullRewriteOnNextPersist = previousRewriteRequired;
|
|
17311
|
+
this.#headerExportRevision++;
|
|
17312
|
+
throw error;
|
|
17313
|
+
}
|
|
17314
|
+
}
|
|
17315
|
+
|
|
16891
17316
|
/** Strip C0/C1 control characters (includes ESC, so removes ANSI sequences) and collapse whitespace. */
|
|
16892
17317
|
static #sanitizeName(name: string): string {
|
|
16893
17318
|
return name
|
|
@@ -16904,6 +17329,7 @@ export class SessionManager {
|
|
|
16904
17329
|
*/
|
|
16905
17330
|
#assertRecoveryHydrationWritable(): void {
|
|
16906
17331
|
if (this.#recoveryHydrationContext) throw new Error("recovery_hydration_not_promoted");
|
|
17332
|
+
if (this.#strictClosePending) throw new Error("Session manager is closing.");
|
|
16907
17333
|
}
|
|
16908
17334
|
async setSessionName(name: string, source: "auto" | "user" = "auto"): Promise<boolean> {
|
|
16909
17335
|
this.#assertRecoveryHydrationWritable();
|
|
@@ -16919,12 +17345,25 @@ export class SessionManager {
|
|
|
16919
17345
|
return true;
|
|
16920
17346
|
}
|
|
16921
17347
|
|
|
16922
|
-
async #appendHeaderPatch(
|
|
17348
|
+
async #appendHeaderPatch(
|
|
17349
|
+
patch: HeaderPatchRecord["patch"],
|
|
17350
|
+
options?: { allowStrictClose?: boolean },
|
|
17351
|
+
): Promise<void> {
|
|
17352
|
+
if (options?.allowStrictClose) {
|
|
17353
|
+
if (this.#recoveryHydrationContext) throw new Error("recovery_hydration_not_promoted");
|
|
17354
|
+
} else this.#assertRecoveryHydrationWritable();
|
|
16923
17355
|
const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
16924
17356
|
if (!header) return;
|
|
16925
17357
|
applyHeaderPatch(header, patch);
|
|
16926
17358
|
this.#headerExportRevision++;
|
|
16927
|
-
|
|
17359
|
+
const records: HeaderPatchRecord[] =
|
|
17360
|
+
header.starredPatchVersion === 1 && patch.starred === undefined
|
|
17361
|
+
? [
|
|
17362
|
+
{ type: "header_patch", patch: { starred: header.starred === true } },
|
|
17363
|
+
{ type: "header_patch", patch },
|
|
17364
|
+
]
|
|
17365
|
+
: [{ type: "header_patch", patch }];
|
|
17366
|
+
await this.#persistPatches(records);
|
|
16928
17367
|
}
|
|
16929
17368
|
|
|
16930
17369
|
#appendManagedRecordsSync(records: readonly (FileEntry | SessionPatchRecord)[]): void {
|
|
@@ -16957,10 +17396,6 @@ export class SessionManager {
|
|
|
16957
17396
|
});
|
|
16958
17397
|
}
|
|
16959
17398
|
|
|
16960
|
-
async #persistPatch(record: SessionPatchRecord): Promise<void> {
|
|
16961
|
-
await this.#persistPatches([record]);
|
|
16962
|
-
}
|
|
16963
|
-
|
|
16964
17399
|
async #persistPatches(records: readonly SessionPatchRecord[]): Promise<void> {
|
|
16965
17400
|
if (records.length === 0) return;
|
|
16966
17401
|
if (this.#coldSidecarActive()) this.#deactivateColdForBranchMutation();
|
|
@@ -17027,6 +17462,7 @@ export class SessionManager {
|
|
|
17027
17462
|
}
|
|
17028
17463
|
_persist(entry: SessionEntry): void {
|
|
17029
17464
|
if (!this.persist || !this.#sessionFile) return;
|
|
17465
|
+
if (this.#strictClosePending) throw new Error("Session manager is closing.");
|
|
17030
17466
|
const publishResumeBreadcrumb = this.#readOnlyResume;
|
|
17031
17467
|
if (this.#persistError) throw this.#persistError;
|
|
17032
17468
|
|
|
@@ -17615,6 +18051,7 @@ export class SessionManager {
|
|
|
17615
18051
|
*/
|
|
17616
18052
|
applyEntryMessageUpdates(entries: readonly SessionMessageEntry[]): void {
|
|
17617
18053
|
this.#assertRecoveryHydrationWritable();
|
|
18054
|
+
if (this.#readOnlyResume) this.#strictResumeMutationPending = true;
|
|
17618
18055
|
this.#deactivateColdForBranchMutation();
|
|
17619
18056
|
for (const updated of entries) {
|
|
17620
18057
|
const canonical = this.#byId.get(updated.id);
|
|
@@ -17636,6 +18073,7 @@ export class SessionManager {
|
|
|
17636
18073
|
options: { preserveEvictedContent?: boolean } = {},
|
|
17637
18074
|
): void {
|
|
17638
18075
|
this.#assertRecoveryHydrationWritable();
|
|
18076
|
+
if (this.#readOnlyResume) this.#strictResumeMutationPending = true;
|
|
17639
18077
|
this.#deactivateColdForBranchMutation();
|
|
17640
18078
|
for (const updated of entries) {
|
|
17641
18079
|
const canonical = this.#byId.get(updated.id);
|
|
@@ -17663,6 +18101,7 @@ export class SessionManager {
|
|
|
17663
18101
|
* boundary for both managed and explicit persistent destinations.
|
|
17664
18102
|
*/
|
|
17665
18103
|
async recoverPersistenceFailure(): Promise<void> {
|
|
18104
|
+
this.#assertRecoveryHydrationWritable();
|
|
17666
18105
|
const persistenceError = this.#persistError;
|
|
17667
18106
|
if (!persistenceError) return;
|
|
17668
18107
|
if (!this.#sessionFile) throw persistenceError;
|
|
@@ -18814,6 +19253,7 @@ export class SessionManager {
|
|
|
18814
19253
|
const header: SessionHeader = {
|
|
18815
19254
|
type: "session",
|
|
18816
19255
|
version: CURRENT_SESSION_VERSION,
|
|
19256
|
+
starredPatchVersion: 1,
|
|
18817
19257
|
id: newSessionId,
|
|
18818
19258
|
timestamp,
|
|
18819
19259
|
cwd: this.cwd,
|