@narumitw/pi-codex-compact 0.47.0
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/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +52 -0
- package/src/checkpoint.ts +257 -0
- package/src/codex-compact.ts +283 -0
- package/src/index.ts +1 -0
- package/src/protocol.ts +224 -0
- package/src/remote.ts +117 -0
- package/src/settings-menu.ts +232 -0
- package/src/settings.ts +227 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
3
|
+
import type {
|
|
4
|
+
CodexCompactSettings,
|
|
5
|
+
CodexCompactSettingsRuntime,
|
|
6
|
+
CodexCompactSettingsState,
|
|
7
|
+
} from "./settings.js";
|
|
8
|
+
|
|
9
|
+
type Screen = "main" | "settings" | "invalid";
|
|
10
|
+
type Action =
|
|
11
|
+
| "compact-now"
|
|
12
|
+
| "set-enabled"
|
|
13
|
+
| "set-timeout"
|
|
14
|
+
| "set-retries"
|
|
15
|
+
| "set-retention"
|
|
16
|
+
| "set-notify";
|
|
17
|
+
|
|
18
|
+
export interface SettingsMenuOwner {
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
isCurrent(): boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CompactMenuStatus {
|
|
24
|
+
model: string;
|
|
25
|
+
remoteCompatible: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function safeText(value: string): string {
|
|
29
|
+
return Array.from(value, (character) => {
|
|
30
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
31
|
+
return codePoint < 32 || (codePoint >= 127 && codePoint <= 159) ? " " : character;
|
|
32
|
+
}).join("");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function timeoutLabel(milliseconds: number): string {
|
|
36
|
+
return `${milliseconds / 60_000} min`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function retentionLabel(tokens: number): string {
|
|
40
|
+
return `${tokens / 1000}K tokens`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function update(
|
|
44
|
+
runtime: CodexCompactSettingsRuntime,
|
|
45
|
+
ctx: ExtensionCommandContext,
|
|
46
|
+
patch: Partial<CodexCompactSettings>,
|
|
47
|
+
signal: AbortSignal,
|
|
48
|
+
) {
|
|
49
|
+
try {
|
|
50
|
+
await runtime.update(patch, signal);
|
|
51
|
+
ctx.ui.notify("Codex compaction settings saved.", "info");
|
|
52
|
+
return { kind: "stay" as const };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (signal.aborted) return { kind: "rejected" as const };
|
|
55
|
+
ctx.ui.notify(
|
|
56
|
+
`Could not save pi-codex-compact.json: ${safeText(error instanceof Error ? error.message : String(error))}`,
|
|
57
|
+
"error",
|
|
58
|
+
);
|
|
59
|
+
return { kind: "rejected" as const };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createCodexCompactMenu(
|
|
64
|
+
runtime: CodexCompactSettingsRuntime,
|
|
65
|
+
options: { onCompactRequested?: () => void; status?: CompactMenuStatus } = {},
|
|
66
|
+
) {
|
|
67
|
+
return defineMenu<CodexCompactSettingsState, Screen, Action, ExtensionCommandContext>({
|
|
68
|
+
start: "main",
|
|
69
|
+
screens: {
|
|
70
|
+
main: ({ state }) => ({
|
|
71
|
+
kind: "actions",
|
|
72
|
+
title: "Codex Remote Compaction",
|
|
73
|
+
lines: [
|
|
74
|
+
`Remote V2: ${state.settings.enabled ? "On" : "Off"}`,
|
|
75
|
+
`Active model: ${safeText(options.status?.model ?? "none")}`,
|
|
76
|
+
`Compact route: ${safeText(compactRoute(state, options.status))}`,
|
|
77
|
+
],
|
|
78
|
+
items: [
|
|
79
|
+
{
|
|
80
|
+
id: "compact-now",
|
|
81
|
+
label: "Compact now",
|
|
82
|
+
description: "Close this menu and compact the active session immediately.",
|
|
83
|
+
action: "compact-now",
|
|
84
|
+
},
|
|
85
|
+
state.kind === "invalid"
|
|
86
|
+
? {
|
|
87
|
+
id: "settings",
|
|
88
|
+
label: "Settings",
|
|
89
|
+
description: "Read-only until the invalid settings file is repaired.",
|
|
90
|
+
to: "invalid" as const,
|
|
91
|
+
}
|
|
92
|
+
: { id: "settings", label: "Settings", to: "settings" as const },
|
|
93
|
+
{ id: "close", label: "Close", close: true },
|
|
94
|
+
],
|
|
95
|
+
hint: "close",
|
|
96
|
+
}),
|
|
97
|
+
settings: ({ state }) => ({
|
|
98
|
+
kind: "settings",
|
|
99
|
+
title: "Codex Remote Compaction Settings",
|
|
100
|
+
lines: [`User settings · ${safeText(state.path)}`],
|
|
101
|
+
items: [
|
|
102
|
+
{
|
|
103
|
+
id: "enabled",
|
|
104
|
+
label: "Remote compaction",
|
|
105
|
+
description: "Use Codex Remote V2 when the active model is compatible.",
|
|
106
|
+
currentValue: state.settings.enabled ? "On" : "Off",
|
|
107
|
+
values: ["On", "Off"],
|
|
108
|
+
action: "set-enabled",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "requestTimeoutMs",
|
|
112
|
+
label: "Request timeout",
|
|
113
|
+
description: "Maximum time for the extension-owned remote compaction request.",
|
|
114
|
+
currentValue: timeoutLabel(state.settings.requestTimeoutMs),
|
|
115
|
+
values: ["2 min", "5 min", "10 min"],
|
|
116
|
+
action: "set-timeout",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "maxRetries",
|
|
120
|
+
label: "Transport retries",
|
|
121
|
+
description: "Retry transient provider failures before falling back to Pi.",
|
|
122
|
+
currentValue: String(state.settings.maxRetries),
|
|
123
|
+
values: ["0", "1", "2"],
|
|
124
|
+
action: "set-retries",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "replacementTokenBudget",
|
|
128
|
+
label: "Retained user history",
|
|
129
|
+
description: "Approximate user-message budget kept beside the opaque checkpoint.",
|
|
130
|
+
currentValue: retentionLabel(state.settings.replacementTokenBudget),
|
|
131
|
+
values: ["32K tokens", "64K tokens", "96K tokens", "128K tokens"],
|
|
132
|
+
action: "set-retention",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "notifyOnFallback",
|
|
136
|
+
label: "Fallback notifications",
|
|
137
|
+
description: "Warn when Remote V2 fails and native Pi compaction takes over.",
|
|
138
|
+
currentValue: state.settings.notifyOnFallback ? "On" : "Off",
|
|
139
|
+
values: ["On", "Off"],
|
|
140
|
+
action: "set-notify",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
invalid: ({ state }) => ({
|
|
145
|
+
kind: "detail",
|
|
146
|
+
title: "Codex Compact Settings · Read only",
|
|
147
|
+
lines: [
|
|
148
|
+
`Invalid settings file: ${safeText(state.path)}`,
|
|
149
|
+
`Issue: ${safeText(state.issue ?? "unknown validation error")}`,
|
|
150
|
+
"Built-in defaults are active. Repair the file and run /reload; it will not be overwritten.",
|
|
151
|
+
],
|
|
152
|
+
hint: "back",
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
actions: {
|
|
156
|
+
"compact-now": async () => {
|
|
157
|
+
options.onCompactRequested?.();
|
|
158
|
+
return { kind: "close" };
|
|
159
|
+
},
|
|
160
|
+
"set-enabled": ({ ctx, value, signal }) =>
|
|
161
|
+
update(runtime, ctx, { enabled: value === "On" }, signal),
|
|
162
|
+
"set-timeout": ({ ctx, value, signal }) =>
|
|
163
|
+
update(
|
|
164
|
+
runtime,
|
|
165
|
+
ctx,
|
|
166
|
+
{ requestTimeoutMs: Number.parseInt(value ?? "5", 10) * 60_000 },
|
|
167
|
+
signal,
|
|
168
|
+
),
|
|
169
|
+
"set-retries": ({ ctx, value, signal }) =>
|
|
170
|
+
update(runtime, ctx, { maxRetries: Number.parseInt(value ?? "2", 10) }, signal),
|
|
171
|
+
"set-retention": ({ ctx, value, signal }) =>
|
|
172
|
+
update(
|
|
173
|
+
runtime,
|
|
174
|
+
ctx,
|
|
175
|
+
{ replacementTokenBudget: Number.parseInt(value ?? "64", 10) * 1000 },
|
|
176
|
+
signal,
|
|
177
|
+
),
|
|
178
|
+
"set-notify": ({ ctx, value, signal }) =>
|
|
179
|
+
update(runtime, ctx, { notifyOnFallback: value === "On" }, signal),
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function showCodexCompactMenu(
|
|
185
|
+
runtime: CodexCompactSettingsRuntime,
|
|
186
|
+
ctx: ExtensionCommandContext,
|
|
187
|
+
owner: SettingsMenuOwner,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
if (ctx.mode !== "tui") {
|
|
190
|
+
ctx.ui.notify(`Edit Codex compaction settings at ${safeText(runtime.get().path)}.`, "info");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let compactRequested = false;
|
|
194
|
+
await runMenu(
|
|
195
|
+
ctx,
|
|
196
|
+
createCodexCompactMenu(runtime, {
|
|
197
|
+
onCompactRequested: () => {
|
|
198
|
+
compactRequested = true;
|
|
199
|
+
},
|
|
200
|
+
status: compactMenuStatus(ctx),
|
|
201
|
+
}),
|
|
202
|
+
{
|
|
203
|
+
getState: () => runtime.get(),
|
|
204
|
+
signal: owner.signal,
|
|
205
|
+
isCurrent: owner.isCurrent,
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
if (!compactRequested || owner.signal.aborted || !owner.isCurrent()) return;
|
|
209
|
+
ctx.compact({
|
|
210
|
+
onError: (error) => {
|
|
211
|
+
if (!owner.signal.aborted && owner.isCurrent()) {
|
|
212
|
+
ctx.ui.notify(`Compaction failed: ${safeText(error.message)}`, "error");
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function compactMenuStatus(ctx: ExtensionCommandContext): CompactMenuStatus {
|
|
219
|
+
const model = ctx.model;
|
|
220
|
+
return {
|
|
221
|
+
model: model ? `${model.provider}/${model.id}` : "none",
|
|
222
|
+
remoteCompatible: model?.provider === "openai-codex" && model.api === "openai-codex-responses",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function compactRoute(
|
|
227
|
+
state: Readonly<CodexCompactSettingsState>,
|
|
228
|
+
status: CompactMenuStatus | undefined,
|
|
229
|
+
): string {
|
|
230
|
+
if (!state.settings.enabled) return "Pi native (Remote V2 off)";
|
|
231
|
+
return status?.remoteCompatible ? "Codex Remote V2" : "Pi native (model not compatible)";
|
|
232
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { mkdir, open, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
export const CODEX_COMPACT_SETTINGS_FILE = "pi-codex-compact.json";
|
|
8
|
+
export const MAX_SETTINGS_BYTES = 64 * 1024;
|
|
9
|
+
|
|
10
|
+
export interface CodexCompactSettings {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
requestTimeoutMs: number;
|
|
13
|
+
maxRetries: number;
|
|
14
|
+
replacementTokenBudget: number;
|
|
15
|
+
notifyOnFallback: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_CODEX_COMPACT_SETTINGS: Readonly<CodexCompactSettings> = Object.freeze({
|
|
19
|
+
enabled: true,
|
|
20
|
+
requestTimeoutMs: 300_000,
|
|
21
|
+
maxRetries: 2,
|
|
22
|
+
replacementTokenBudget: 64_000,
|
|
23
|
+
notifyOnFallback: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const LIMITS = Object.freeze({
|
|
27
|
+
requestTimeoutMs: { minimum: 30_000, maximum: 600_000 },
|
|
28
|
+
maxRetries: { minimum: 0, maximum: 2 },
|
|
29
|
+
replacementTokenBudget: { minimum: 8_000, maximum: 128_000 },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export interface CodexCompactSettingsState {
|
|
33
|
+
kind: "missing" | "loaded" | "invalid";
|
|
34
|
+
path: string;
|
|
35
|
+
settings: CodexCompactSettings;
|
|
36
|
+
document?: Record<string, unknown>;
|
|
37
|
+
issue?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CodexCompactSettingsRuntime {
|
|
41
|
+
get(): Readonly<CodexCompactSettingsState>;
|
|
42
|
+
reload(signal?: AbortSignal): Promise<Readonly<CodexCompactSettingsState>>;
|
|
43
|
+
update(
|
|
44
|
+
patch: Partial<CodexCompactSettings>,
|
|
45
|
+
signal?: AbortSignal,
|
|
46
|
+
): Promise<Readonly<CodexCompactSettingsState>>;
|
|
47
|
+
flush(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validInteger(value: unknown, minimum: number, maximum: number): value is number {
|
|
55
|
+
return (
|
|
56
|
+
typeof value === "number" && Number.isSafeInteger(value) && value >= minimum && value <= maximum
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeCodexCompactSettings(value: unknown): CodexCompactSettings | undefined {
|
|
61
|
+
if (!isRecord(value)) return undefined;
|
|
62
|
+
if (Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean") return undefined;
|
|
63
|
+
if (Object.hasOwn(value, "notifyOnFallback") && typeof value.notifyOnFallback !== "boolean") {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
for (const [field, limits] of Object.entries(LIMITS) as Array<
|
|
67
|
+
[keyof typeof LIMITS, { minimum: number; maximum: number }]
|
|
68
|
+
>) {
|
|
69
|
+
if (
|
|
70
|
+
Object.hasOwn(value, field) &&
|
|
71
|
+
!validInteger(value[field], limits.minimum, limits.maximum)
|
|
72
|
+
) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
enabled:
|
|
78
|
+
typeof value.enabled === "boolean" ? value.enabled : DEFAULT_CODEX_COMPACT_SETTINGS.enabled,
|
|
79
|
+
requestTimeoutMs:
|
|
80
|
+
typeof value.requestTimeoutMs === "number"
|
|
81
|
+
? value.requestTimeoutMs
|
|
82
|
+
: DEFAULT_CODEX_COMPACT_SETTINGS.requestTimeoutMs,
|
|
83
|
+
maxRetries:
|
|
84
|
+
typeof value.maxRetries === "number"
|
|
85
|
+
? value.maxRetries
|
|
86
|
+
: DEFAULT_CODEX_COMPACT_SETTINGS.maxRetries,
|
|
87
|
+
replacementTokenBudget:
|
|
88
|
+
typeof value.replacementTokenBudget === "number"
|
|
89
|
+
? value.replacementTokenBudget
|
|
90
|
+
: DEFAULT_CODEX_COMPACT_SETTINGS.replacementTokenBudget,
|
|
91
|
+
notifyOnFallback:
|
|
92
|
+
typeof value.notifyOnFallback === "boolean"
|
|
93
|
+
? value.notifyOnFallback
|
|
94
|
+
: DEFAULT_CODEX_COMPACT_SETTINGS.notifyOnFallback,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function codexCompactSettingsPath(): string {
|
|
99
|
+
return join(getAgentDir(), CODEX_COMPACT_SETTINGS_FILE);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function aborted(signal?: AbortSignal): void {
|
|
103
|
+
if (signal?.aborted) throw new DOMException("Settings operation aborted", "AbortError");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function loadCodexCompactSettings(
|
|
107
|
+
path = codexCompactSettingsPath(),
|
|
108
|
+
signal?: AbortSignal,
|
|
109
|
+
): Promise<CodexCompactSettingsState> {
|
|
110
|
+
aborted(signal);
|
|
111
|
+
try {
|
|
112
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
113
|
+
let text: string;
|
|
114
|
+
try {
|
|
115
|
+
const stats = await handle.stat();
|
|
116
|
+
aborted(signal);
|
|
117
|
+
if (!stats.isFile()) throw new Error("settings path is not a regular file");
|
|
118
|
+
if (stats.size > MAX_SETTINGS_BYTES) throw new Error("settings file exceeds 64 KiB");
|
|
119
|
+
text = await handle.readFile("utf8");
|
|
120
|
+
} finally {
|
|
121
|
+
await handle.close();
|
|
122
|
+
}
|
|
123
|
+
aborted(signal);
|
|
124
|
+
const document = JSON.parse(text) as unknown;
|
|
125
|
+
const settings = normalizeCodexCompactSettings(document);
|
|
126
|
+
if (!settings || !isRecord(document)) throw new Error("invalid settings shape or bounds");
|
|
127
|
+
return { kind: "loaded", path, settings, document };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (signal?.aborted) throw error;
|
|
130
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
131
|
+
return {
|
|
132
|
+
kind: "missing",
|
|
133
|
+
path,
|
|
134
|
+
settings: { ...DEFAULT_CODEX_COMPACT_SETTINGS },
|
|
135
|
+
document: {},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
kind: "invalid",
|
|
140
|
+
path,
|
|
141
|
+
settings: { ...DEFAULT_CODEX_COMPACT_SETTINGS },
|
|
142
|
+
issue:
|
|
143
|
+
isNodeError(error) && error.code === "ELOOP"
|
|
144
|
+
? "symbolic links are not accepted"
|
|
145
|
+
: error instanceof Error
|
|
146
|
+
? error.message
|
|
147
|
+
: String(error),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function savePatch(
|
|
153
|
+
path: string,
|
|
154
|
+
patch: Partial<CodexCompactSettings>,
|
|
155
|
+
signal?: AbortSignal,
|
|
156
|
+
): Promise<CodexCompactSettingsState> {
|
|
157
|
+
const latest = await loadCodexCompactSettings(path, signal);
|
|
158
|
+
if (latest.kind === "invalid") {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"Cannot overwrite an invalid pi-codex-compact.json; repair it and reload first",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const document = { ...latest.document, ...patch };
|
|
164
|
+
const settings = normalizeCodexCompactSettings(document);
|
|
165
|
+
if (!settings) throw new Error("Refusing to save invalid Codex compaction settings");
|
|
166
|
+
const temporaryPath = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
|
|
167
|
+
await mkdir(dirname(path), { recursive: true });
|
|
168
|
+
aborted(signal);
|
|
169
|
+
try {
|
|
170
|
+
await writeFile(temporaryPath, `${JSON.stringify(document, null, 2)}\n`, {
|
|
171
|
+
encoding: "utf8",
|
|
172
|
+
flag: "wx",
|
|
173
|
+
mode: 0o600,
|
|
174
|
+
});
|
|
175
|
+
aborted(signal);
|
|
176
|
+
const current = await loadCodexCompactSettings(path, signal);
|
|
177
|
+
if (
|
|
178
|
+
current.kind === "invalid" ||
|
|
179
|
+
current.kind !== latest.kind ||
|
|
180
|
+
JSON.stringify(current.document) !== JSON.stringify(latest.document)
|
|
181
|
+
) {
|
|
182
|
+
throw new Error("pi-codex-compact.json changed while saving; reopen settings and retry");
|
|
183
|
+
}
|
|
184
|
+
await rename(temporaryPath, path);
|
|
185
|
+
} finally {
|
|
186
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
187
|
+
}
|
|
188
|
+
return { kind: "loaded", path, settings, document };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createCodexCompactSettingsRuntime(
|
|
192
|
+
path = codexCompactSettingsPath(),
|
|
193
|
+
): CodexCompactSettingsRuntime {
|
|
194
|
+
let state: CodexCompactSettingsState = {
|
|
195
|
+
kind: "missing",
|
|
196
|
+
path,
|
|
197
|
+
settings: { ...DEFAULT_CODEX_COMPACT_SETTINGS },
|
|
198
|
+
document: {},
|
|
199
|
+
};
|
|
200
|
+
let queue = Promise.resolve();
|
|
201
|
+
const enqueue = <T>(operation: () => Promise<T>): Promise<T> => {
|
|
202
|
+
const result = queue.then(operation, operation);
|
|
203
|
+
queue = result.then(
|
|
204
|
+
() => undefined,
|
|
205
|
+
() => undefined,
|
|
206
|
+
);
|
|
207
|
+
return result;
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
get: () => structuredClone(state),
|
|
211
|
+
reload: (signal) =>
|
|
212
|
+
enqueue(async () => {
|
|
213
|
+
state = await loadCodexCompactSettings(path, signal);
|
|
214
|
+
return structuredClone(state);
|
|
215
|
+
}),
|
|
216
|
+
update: (patch, signal) =>
|
|
217
|
+
enqueue(async () => {
|
|
218
|
+
state = await savePatch(path, patch, signal);
|
|
219
|
+
return structuredClone(state);
|
|
220
|
+
}),
|
|
221
|
+
flush: () => queue,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
226
|
+
return error instanceof Error && "code" in error;
|
|
227
|
+
}
|