@timurproko/a1 0.1.8-dev.322 → 0.1.8-dev.332
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/dist/cli/dispatch.js +1 -1
- package/dist/features/owned-ui/project-trust-prompt.js +1 -1
- package/dist/features/owned-ui/settings-app.js +1 -1
- package/dist/features/prompt-history/service.d.ts +31 -1
- package/dist/features/prompt-history/service.js +294 -129
- package/dist/features/prompt-history/store.d.ts +2 -1
- package/dist/features/prompt-history/store.js +17 -4
- package/dist/features/prompt-history/worker.js +6 -3
- package/dist/foundation/launch-context/index.d.ts +36 -6
- package/dist/foundation/launch-context/index.js +64 -11
- package/dist/foundation/launch-guardian/main.js +2 -1
- package/dist/foundation/release/bootstrap.d.ts +5 -0
- package/dist/foundation/release/bootstrap.js +49 -9
- package/dist/foundation/release/cohort-state.js +0 -3
- package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
- package/dist/foundation/release/dependency-certification-retention.js +88 -0
- package/dist/foundation/release/dependency-certification.d.ts +24 -0
- package/dist/foundation/release/dependency-certification.js +208 -0
- package/dist/foundation/release/dependency-layer.d.ts +3 -4
- package/dist/foundation/release/dependency-layer.js +6 -37
- package/dist/foundation/release/release-gc.js +72 -44
- package/dist/foundation/release/release-store.d.ts +2 -0
- package/dist/foundation/release/release-store.js +4 -3
- package/dist/foundation/release/restart-certification.js +16 -5
- package/dist/foundation/release/update.js +1 -6
- package/dist/foundation/release/warmup.js +16 -5
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
- package/dist/integrations/pi/components/owned-editor-ux.js +51 -27
- package/dist/integrations/pi/engine/adapter.d.ts +20 -0
- package/dist/integrations/pi/engine/adapter.js +223 -87
- package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
- package/dist/integrations/pi/engine/pending-delivery.js +149 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +38 -25
- package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
- package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +1 -1
- package/dist/integrations/pi/session-ui/session-shell-root.js +5 -1
- package/dist/integrations/pi/session-ui/session-shell.js +9 -6
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -5
- package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
- package/dist/integrations/pi/session-ui/text-paste.js +16 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +2 -2
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +82 -39
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.json +1 -1
- package/docs/architecture/internal-naming.md +5 -3
- package/docs/manual-code-streaming-cleanup.md +88 -0
- package/package.json +1 -1
package/dist/cli/dispatch.js
CHANGED
|
@@ -212,7 +212,7 @@ function pinnedPiUpdateError() {
|
|
|
212
212
|
message: [
|
|
213
213
|
PRODUCT_TEXT.diagnostic("pins its certified Pi runtime and cannot update it independently."),
|
|
214
214
|
"Use:",
|
|
215
|
-
` ${PRODUCT_TEXT.commandName} update Update
|
|
215
|
+
` ${PRODUCT_TEXT.commandName} update Update a1`,
|
|
216
216
|
` ${PRODUCT_TEXT.commandName} pi update --extensions Update Pi-compatible packages`,
|
|
217
217
|
` ${PRODUCT_TEXT.commandName} pi update --models Refresh model catalogs`,
|
|
218
218
|
].join("\n"),
|
|
@@ -37,7 +37,7 @@ export function createConsoleProjectTrustPrompt(options = {}) {
|
|
|
37
37
|
`${BOLD}${ACCENT}Trust project folder?${RESET_FG}${RESET_BOLD}`,
|
|
38
38
|
cwd,
|
|
39
39
|
"",
|
|
40
|
-
"This allows
|
|
40
|
+
"This allows a1 to load project settings and resources, install missing project packages, and execute project extensions.",
|
|
41
41
|
"",
|
|
42
42
|
optionRow("Trust", selected === 0),
|
|
43
43
|
optionRow("Do not trust", selected === 1),
|
|
@@ -628,7 +628,7 @@ export class SettingsApp {
|
|
|
628
628
|
const open = this.#structured;
|
|
629
629
|
if (open !== null)
|
|
630
630
|
return this.#dialogLines(open, width, theme);
|
|
631
|
-
const hint = this.#interruptArmed ? "press ctrl+c again to exit
|
|
631
|
+
const hint = this.#interruptArmed ? "press ctrl+c again to exit a1" : SETTINGS_SHORTCUTS.hint(SCOPE);
|
|
632
632
|
const status = renderStatusLine({ hint, report: this.#notice }, width, theme);
|
|
633
633
|
const input = this.#filter;
|
|
634
634
|
if (input === null)
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { Worker } from "node:worker_threads";
|
|
2
2
|
import { type PromptHistoryFailure, type PromptHistoryPort, type PromptHistoryResult, type PromptHistorySnapshot, type PromptHistorySubmission } from "../../contracts/owned-ui/index.js";
|
|
3
|
-
|
|
3
|
+
export type HistoryRecoveryState = "starting" | "ready" | "recovering" | "blocked-storage" | "closing" | "closed";
|
|
4
|
+
export interface HistoryClock {
|
|
5
|
+
now(): number;
|
|
6
|
+
setTimeout(callback: () => void, milliseconds: number): ReturnType<typeof setTimeout>;
|
|
7
|
+
clearTimeout(timer: ReturnType<typeof setTimeout> | undefined): void;
|
|
8
|
+
}
|
|
9
|
+
/** One generation owns storage work; only proven non-commits may be retried. No default output sink. */
|
|
4
10
|
export declare class PromptHistoryService implements PromptHistoryPort {
|
|
5
11
|
#private;
|
|
6
12
|
readonly options: {
|
|
@@ -8,13 +14,37 @@ export declare class PromptHistoryService implements PromptHistoryPort {
|
|
|
8
14
|
profileRoot: string;
|
|
9
15
|
limit: number;
|
|
10
16
|
createWorker?: () => Worker;
|
|
17
|
+
clock?: HistoryClock;
|
|
18
|
+
random?: () => number;
|
|
11
19
|
};
|
|
12
20
|
constructor(options: {
|
|
13
21
|
dataDir: string;
|
|
14
22
|
profileRoot: string;
|
|
15
23
|
limit: number;
|
|
16
24
|
createWorker?: () => Worker;
|
|
25
|
+
clock?: HistoryClock;
|
|
26
|
+
random?: () => number;
|
|
17
27
|
});
|
|
28
|
+
/** Explicit developer inspection: bounded classified counters, never exception or prompt payloads. */
|
|
29
|
+
diagnostics(): {
|
|
30
|
+
state: HistoryRecoveryState;
|
|
31
|
+
generation: number;
|
|
32
|
+
attempts: number;
|
|
33
|
+
recoveries: number;
|
|
34
|
+
pending: number;
|
|
35
|
+
bytes: number;
|
|
36
|
+
active: boolean;
|
|
37
|
+
timers: number;
|
|
38
|
+
failures: {
|
|
39
|
+
schema?: number;
|
|
40
|
+
shutdown?: number;
|
|
41
|
+
unavailable?: number;
|
|
42
|
+
busy?: number;
|
|
43
|
+
capacity?: number;
|
|
44
|
+
oversized?: number;
|
|
45
|
+
corrupt?: number;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
18
48
|
start(): void;
|
|
19
49
|
record(submission: PromptHistorySubmission): Promise<PromptHistoryResult>;
|
|
20
50
|
refresh(): void;
|
|
@@ -1,38 +1,54 @@
|
|
|
1
1
|
import { assertPromptHistorySubmission, PROMPT_HISTORY_MAX_ENTRY_BYTES, PROMPT_HISTORY_MAX_PENDING, PROMPT_HISTORY_MAX_TEXT_BYTES } from "../../contracts/owned-ui/index.js";
|
|
2
2
|
import { resolvePromptHistoryPath } from "./paths.js";
|
|
3
|
-
|
|
3
|
+
const clock = { now: () => Date.now(), setTimeout: (fn, ms) => setTimeout(fn, ms), clearTimeout: timer => clearTimeout(timer) };
|
|
4
|
+
const RETENTION_MS = 30_000;
|
|
5
|
+
const LIVENESS_MS = 10_000;
|
|
6
|
+
/** One generation owns storage work; only proven non-commits may be retried. No default output sink. */
|
|
4
7
|
export class PromptHistoryService {
|
|
5
8
|
options;
|
|
6
9
|
#snapshots = new Set();
|
|
7
10
|
#failures = new Set();
|
|
8
|
-
#
|
|
11
|
+
#counts = {};
|
|
9
12
|
#queue = [];
|
|
13
|
+
#clock;
|
|
14
|
+
#state = "starting";
|
|
10
15
|
#worker;
|
|
16
|
+
#stopping;
|
|
11
17
|
#active;
|
|
12
18
|
#timer;
|
|
19
|
+
#expiry;
|
|
13
20
|
#poll;
|
|
21
|
+
#closeTimer;
|
|
14
22
|
#sequence = 0;
|
|
23
|
+
#generation = 0;
|
|
15
24
|
#bytes = 0;
|
|
16
|
-
#writes = 0;
|
|
17
25
|
#started = false;
|
|
18
26
|
#ready = false;
|
|
19
|
-
#failed = false;
|
|
20
|
-
#closing = false;
|
|
21
|
-
#reading = false;
|
|
22
27
|
#refreshPending = false;
|
|
23
28
|
#lastRevision = -1;
|
|
29
|
+
#backoff = 100;
|
|
30
|
+
#recoveries = 0;
|
|
24
31
|
#closePromise;
|
|
32
|
+
#resolveClose;
|
|
25
33
|
constructor(options) {
|
|
26
34
|
this.options = options;
|
|
35
|
+
this.#clock = options.clock ?? clock;
|
|
36
|
+
}
|
|
37
|
+
/** Explicit developer inspection: bounded classified counters, never exception or prompt payloads. */
|
|
38
|
+
diagnostics() {
|
|
39
|
+
return { state: this.#state, generation: this.#generation, attempts: this.#sequence, recoveries: this.#recoveries,
|
|
40
|
+
pending: this.#queue.length, bytes: this.#bytes, active: this.#active !== undefined,
|
|
41
|
+
timers: [this.#timer, this.#expiry, this.#poll, this.#closeTimer].filter(value => value !== undefined).length,
|
|
42
|
+
failures: { ...this.#counts } };
|
|
27
43
|
}
|
|
28
44
|
start() {
|
|
29
|
-
if (this.#started || this.#
|
|
45
|
+
if (this.#started || this.#isClosed())
|
|
30
46
|
return;
|
|
31
47
|
this.#started = true;
|
|
32
|
-
|
|
48
|
+
void this.#open();
|
|
33
49
|
}
|
|
34
50
|
record(submission) {
|
|
35
|
-
if (this.#
|
|
51
|
+
if (this.#isClosed() || this.#state === "blocked-storage")
|
|
36
52
|
return Promise.resolve("skipped");
|
|
37
53
|
try {
|
|
38
54
|
assertPromptHistorySubmission(submission);
|
|
@@ -46,7 +62,7 @@ export class PromptHistoryService {
|
|
|
46
62
|
this.#report("oversized");
|
|
47
63
|
return Promise.resolve("skipped");
|
|
48
64
|
}
|
|
49
|
-
if (this.#
|
|
65
|
+
if (this.#queue.length >= PROMPT_HISTORY_MAX_PENDING || this.#bytes + bytes > PROMPT_HISTORY_MAX_TEXT_BYTES) {
|
|
50
66
|
this.#report("capacity");
|
|
51
67
|
return Promise.resolve("skipped");
|
|
52
68
|
}
|
|
@@ -56,53 +72,18 @@ export class PromptHistoryService {
|
|
|
56
72
|
...(submission.sessionId !== undefined && Buffer.byteLength(submission.sessionId) <= 256 ? { sessionId: submission.sessionId } : {}),
|
|
57
73
|
};
|
|
58
74
|
this.start();
|
|
59
|
-
this.#writes++;
|
|
60
75
|
this.#bytes += bytes;
|
|
61
76
|
return new Promise(resolve => {
|
|
62
|
-
this.#queue.push({
|
|
63
|
-
|
|
64
|
-
this.#bytes -= bytes;
|
|
65
|
-
resolve(ok ? "committed" : "skipped");
|
|
66
|
-
if (ok)
|
|
67
|
-
this.refresh();
|
|
68
|
-
} });
|
|
77
|
+
this.#queue.push({ submission: candidate, expires: this.#clock.now() + RETENTION_MS, bytes, done: resolve });
|
|
78
|
+
this.#armExpiry();
|
|
69
79
|
this.#drain();
|
|
70
80
|
});
|
|
71
81
|
}
|
|
72
82
|
refresh() {
|
|
73
|
-
if (this.#
|
|
83
|
+
if (this.#isClosed() || this.#state === "blocked-storage")
|
|
74
84
|
return;
|
|
75
85
|
this.start();
|
|
76
|
-
|
|
77
|
-
this.#refreshPending = true;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
this.#reading = true;
|
|
81
|
-
const observers = [...this.#snapshots];
|
|
82
|
-
this.#queue.push({ request: { id: ++this.#sequence, kind: "read" }, done: (value, ok) => {
|
|
83
|
-
this.#reading = false;
|
|
84
|
-
if (ok) {
|
|
85
|
-
if (!isSnapshot(value)) {
|
|
86
|
-
this.#fail("corrupt");
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (value.revision !== this.#lastRevision && observers.some(listener => this.#snapshots.has(listener))) {
|
|
90
|
-
this.#lastRevision = value.revision;
|
|
91
|
-
for (const listener of observers) {
|
|
92
|
-
if (!this.#snapshots.has(listener))
|
|
93
|
-
continue;
|
|
94
|
-
try {
|
|
95
|
-
listener(value);
|
|
96
|
-
}
|
|
97
|
-
catch { /* Security: observer errors must not escape the worker event handler. */ }
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (this.#refreshPending) {
|
|
102
|
-
this.#refreshPending = false;
|
|
103
|
-
this.refresh();
|
|
104
|
-
}
|
|
105
|
-
} });
|
|
86
|
+
this.#refreshPending = true;
|
|
106
87
|
this.#drain();
|
|
107
88
|
}
|
|
108
89
|
onSnapshot(listener) {
|
|
@@ -110,6 +91,7 @@ export class PromptHistoryService {
|
|
|
110
91
|
this.#lastRevision = -1;
|
|
111
92
|
return () => this.#snapshots.delete(listener);
|
|
112
93
|
}
|
|
94
|
+
// Security: this classified observer is developer-only, never wired to shell notifications.
|
|
113
95
|
onFailure(listener) {
|
|
114
96
|
this.#failures.add(listener);
|
|
115
97
|
return () => this.#failures.delete(listener);
|
|
@@ -117,116 +99,299 @@ export class PromptHistoryService {
|
|
|
117
99
|
close() {
|
|
118
100
|
if (this.#closePromise !== undefined)
|
|
119
101
|
return this.#closePromise;
|
|
120
|
-
this.#
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this.#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.#
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
102
|
+
this.#state = "closing";
|
|
103
|
+
this.#clock.clearTimeout(this.#poll);
|
|
104
|
+
this.#poll = undefined;
|
|
105
|
+
this.#clock.clearTimeout(this.#expiry);
|
|
106
|
+
this.#expiry = undefined;
|
|
107
|
+
this.#refreshPending = false;
|
|
108
|
+
this.#closePromise = new Promise(resolve => { this.#resolveClose = resolve; });
|
|
109
|
+
this.#closeTimer = this.#clock.setTimeout(() => {
|
|
110
|
+
this.#report("shutdown");
|
|
111
|
+
this.#finishClose();
|
|
112
|
+
}, 2000);
|
|
113
|
+
if (!this.#started || !this.#ready && this.#worker === undefined)
|
|
114
|
+
this.#finishClose();
|
|
115
|
+
else if (this.#active === undefined && this.#ready) {
|
|
116
|
+
this.#clock.clearTimeout(this.#timer);
|
|
117
|
+
this.#timer = undefined;
|
|
135
118
|
this.#drain();
|
|
136
|
-
}
|
|
119
|
+
}
|
|
137
120
|
return this.#closePromise;
|
|
138
121
|
}
|
|
122
|
+
#isClosed() { return this.#state === "closing" || this.#state === "closed"; }
|
|
139
123
|
async #open() {
|
|
140
|
-
if (this.#
|
|
124
|
+
if (this.#isClosed() || this.#state === "blocked-storage" || this.#worker !== undefined || this.#stopping !== undefined)
|
|
141
125
|
return;
|
|
142
|
-
const
|
|
143
|
-
|
|
126
|
+
const generation = ++this.#generation;
|
|
127
|
+
this.#state = "starting";
|
|
128
|
+
try {
|
|
129
|
+
const { Worker } = await import("node:worker_threads");
|
|
130
|
+
if (this.#isClosed() || generation !== this.#generation)
|
|
131
|
+
return;
|
|
132
|
+
const location = resolvePromptHistoryPath(this.options.dataDir, this.options.profileRoot);
|
|
133
|
+
const source = import.meta.url.endsWith(".ts");
|
|
134
|
+
const entry = new URL(source ? "./worker.ts" : "./worker.js", import.meta.url);
|
|
135
|
+
const options = { workerData: { ...location, limit: this.options.limit }, stdout: true, stderr: true };
|
|
136
|
+
const worker = this.options.createWorker?.() ?? (source
|
|
137
|
+
? new Worker(`import('tsx/esm/api').then(({ tsImport }) => tsImport(${JSON.stringify(entry.href)}, ${JSON.stringify(import.meta.url)}))`, { ...options, eval: true })
|
|
138
|
+
: new Worker(entry, options));
|
|
139
|
+
this.#worker = worker;
|
|
140
|
+
worker.stdout?.resume();
|
|
141
|
+
worker.stderr?.resume();
|
|
142
|
+
this.#timer = this.#clock.setTimeout(() => this.#lost("unavailable"), LIVENESS_MS);
|
|
143
|
+
worker.on("message", (message) => {
|
|
144
|
+
if (generation !== this.#generation || this.#state === "closed")
|
|
145
|
+
return;
|
|
146
|
+
this.#response(message);
|
|
147
|
+
});
|
|
148
|
+
worker.on("error", () => { if (generation === this.#generation)
|
|
149
|
+
this.#lost("unavailable"); });
|
|
150
|
+
worker.on("exit", () => { if (generation === this.#generation)
|
|
151
|
+
this.#lost("unavailable"); });
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
if (generation === this.#generation)
|
|
155
|
+
this.#lost("unavailable");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
#response(value) {
|
|
159
|
+
if (value === null || typeof value !== "object") {
|
|
160
|
+
this.#lost("unavailable");
|
|
144
161
|
return;
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.#
|
|
153
|
-
|
|
154
|
-
this.#
|
|
155
|
-
this.#
|
|
156
|
-
|
|
162
|
+
}
|
|
163
|
+
const message = value;
|
|
164
|
+
if (!Number.isSafeInteger(message.id) || typeof message.ok !== "boolean") {
|
|
165
|
+
this.#lost("unavailable");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Concurrency: late/duplicate attempt replies cannot clear another attempt's liveness timer.
|
|
169
|
+
if (message.id !== (this.#ready ? this.#active?.id : 0))
|
|
170
|
+
return;
|
|
171
|
+
this.#clock.clearTimeout(this.#timer);
|
|
172
|
+
this.#timer = undefined;
|
|
173
|
+
const attempt = this.#active;
|
|
174
|
+
this.#active = undefined;
|
|
175
|
+
if (!message.ok) {
|
|
176
|
+
const code = failureCode(message.code);
|
|
177
|
+
this.#report(code);
|
|
178
|
+
if (code === "schema" || code === "corrupt") {
|
|
179
|
+
this.#state = this.#isClosed() ? "closing" : "blocked-storage";
|
|
180
|
+
this.#settleAll();
|
|
181
|
+
void this.#stopWorker();
|
|
182
|
+
if (this.#isClosed())
|
|
183
|
+
this.#finishClose();
|
|
157
184
|
return;
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
this.#
|
|
185
|
+
}
|
|
186
|
+
if (attempt?.work !== undefined && message.certainty !== "uncommitted") {
|
|
187
|
+
this.#settle(attempt.work, "skipped");
|
|
188
|
+
this.#lost(code);
|
|
161
189
|
return;
|
|
162
190
|
}
|
|
163
|
-
if (
|
|
164
|
-
this.#
|
|
191
|
+
if (attempt?.work !== undefined && code === "oversized")
|
|
192
|
+
this.#settle(attempt.work, "skipped");
|
|
193
|
+
if (this.#isClosed()) {
|
|
194
|
+
this.#report("shutdown");
|
|
195
|
+
this.#finishClose();
|
|
165
196
|
return;
|
|
166
197
|
}
|
|
167
|
-
if (
|
|
168
|
-
this.#
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
198
|
+
if (!this.#ready)
|
|
199
|
+
void this.#stopWorker().then(() => this.#recover());
|
|
200
|
+
else
|
|
201
|
+
this.#recover();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!this.#ready) {
|
|
205
|
+
this.#ready = true;
|
|
206
|
+
if (!this.#isClosed()) {
|
|
207
|
+
this.#state = "ready";
|
|
208
|
+
this.#refreshPending = true;
|
|
209
|
+
this.#schedulePoll();
|
|
174
210
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
211
|
+
}
|
|
212
|
+
else if (attempt?.work !== undefined) {
|
|
213
|
+
this.#settle(attempt.work, "committed");
|
|
214
|
+
this.#refreshPending = !this.#isClosed();
|
|
215
|
+
}
|
|
216
|
+
else if (attempt?.observers !== undefined) {
|
|
217
|
+
if (!isSnapshot(message.value)) {
|
|
218
|
+
this.#lost("corrupt");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const snapshot = message.value;
|
|
222
|
+
if (snapshot.revision !== this.#lastRevision && attempt.observers.some(listener => this.#snapshots.has(listener))) {
|
|
223
|
+
this.#lastRevision = snapshot.revision;
|
|
224
|
+
for (const listener of attempt.observers) {
|
|
225
|
+
if (!this.#snapshots.has(listener) || this.#isClosed())
|
|
226
|
+
continue;
|
|
227
|
+
try {
|
|
228
|
+
listener(snapshot);
|
|
229
|
+
}
|
|
230
|
+
catch { /* Security: observer exceptions are not storage failures. */ }
|
|
180
231
|
}
|
|
181
|
-
this.#active = undefined;
|
|
182
|
-
item.done(message.value, true);
|
|
183
232
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
233
|
+
}
|
|
234
|
+
else if (this.#isClosed()) {
|
|
235
|
+
this.#finishClose();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.#backoff = 100;
|
|
239
|
+
if (!this.#isClosed())
|
|
240
|
+
this.#state = "ready";
|
|
241
|
+
this.#drain();
|
|
189
242
|
}
|
|
190
243
|
#drain() {
|
|
191
|
-
if (!this.#ready || this.#
|
|
244
|
+
if (!this.#ready || this.#active !== undefined || this.#timer !== undefined || this.#state === "blocked-storage" || this.#state === "closed")
|
|
192
245
|
return;
|
|
193
|
-
const
|
|
194
|
-
|
|
246
|
+
const work = this.#queue[0];
|
|
247
|
+
const id = ++this.#sequence;
|
|
248
|
+
let request;
|
|
249
|
+
if (work !== undefined) {
|
|
250
|
+
this.#active = { id, work };
|
|
251
|
+
request = { id, kind: "record", submission: work.submission };
|
|
252
|
+
}
|
|
253
|
+
else if (this.#isClosed()) {
|
|
254
|
+
this.#active = { id };
|
|
255
|
+
request = { id, kind: "close" };
|
|
256
|
+
}
|
|
257
|
+
else if (this.#refreshPending) {
|
|
258
|
+
this.#refreshPending = false;
|
|
259
|
+
this.#active = { id, observers: [...this.#snapshots] };
|
|
260
|
+
request = { id, kind: "read" };
|
|
261
|
+
}
|
|
262
|
+
else
|
|
195
263
|
return;
|
|
196
|
-
this.#
|
|
197
|
-
this.#timer = setTimeout(() => this.#fail("busy"), 1500);
|
|
264
|
+
this.#timer = this.#clock.setTimeout(() => this.#lost("unavailable"), LIVENESS_MS);
|
|
198
265
|
try {
|
|
199
|
-
this.#worker.postMessage(
|
|
266
|
+
this.#worker.postMessage(request);
|
|
200
267
|
}
|
|
201
268
|
catch {
|
|
202
|
-
this.#
|
|
269
|
+
this.#lost("unavailable");
|
|
203
270
|
}
|
|
204
271
|
}
|
|
205
|
-
#
|
|
206
|
-
|
|
272
|
+
#settle(work, result) {
|
|
273
|
+
const index = this.#queue.indexOf(work);
|
|
274
|
+
if (index < 0)
|
|
207
275
|
return;
|
|
208
|
-
this.#
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
276
|
+
this.#queue.splice(index, 1);
|
|
277
|
+
this.#bytes -= work.bytes;
|
|
278
|
+
work.done(result);
|
|
279
|
+
this.#armExpiry();
|
|
280
|
+
}
|
|
281
|
+
#settleAll() { for (const work of [...this.#queue])
|
|
282
|
+
this.#settle(work, "skipped"); }
|
|
283
|
+
#armExpiry() {
|
|
284
|
+
this.#clock.clearTimeout(this.#expiry);
|
|
285
|
+
this.#expiry = undefined;
|
|
286
|
+
if (this.#isClosed() || this.#queue.length === 0)
|
|
287
|
+
return;
|
|
288
|
+
this.#expiry = this.#clock.setTimeout(() => {
|
|
289
|
+
this.#expiry = undefined;
|
|
290
|
+
for (const work of [...this.#queue]) {
|
|
291
|
+
if (work.expires > this.#clock.now())
|
|
292
|
+
break;
|
|
293
|
+
if (this.#active?.work === work)
|
|
294
|
+
this.#lost("unavailable");
|
|
295
|
+
this.#settle(work, "skipped");
|
|
212
296
|
}
|
|
213
|
-
|
|
214
|
-
}
|
|
297
|
+
this.#armExpiry();
|
|
298
|
+
}, Math.max(0, this.#queue[0].expires - this.#clock.now()));
|
|
299
|
+
}
|
|
300
|
+
#schedulePoll() {
|
|
301
|
+
this.#clock.clearTimeout(this.#poll);
|
|
302
|
+
this.#poll = this.#clock.setTimeout(() => {
|
|
303
|
+
this.#poll = undefined;
|
|
304
|
+
if (this.#isClosed() || this.#state === "blocked-storage")
|
|
305
|
+
return;
|
|
306
|
+
this.refresh();
|
|
307
|
+
this.#schedulePoll();
|
|
308
|
+
}, 1000);
|
|
309
|
+
this.#poll.unref?.();
|
|
310
|
+
}
|
|
311
|
+
#recover() {
|
|
312
|
+
if (this.#isClosed() || this.#state === "blocked-storage")
|
|
313
|
+
return;
|
|
314
|
+
this.#state = "recovering";
|
|
315
|
+
this.#recoveries = Math.min(Number.MAX_SAFE_INTEGER, this.#recoveries + 1);
|
|
316
|
+
this.#clock.clearTimeout(this.#timer);
|
|
317
|
+
const jitter = Math.max(0, Math.min(1, (this.options.random ?? Math.random)()));
|
|
318
|
+
const delay = Math.min(5000, this.#backoff * (1 + jitter * 0.2));
|
|
319
|
+
this.#backoff = Math.min(5000, this.#backoff * 2);
|
|
320
|
+
this.#timer = this.#clock.setTimeout(() => {
|
|
321
|
+
this.#timer = undefined;
|
|
322
|
+
if (this.#isClosed())
|
|
323
|
+
return;
|
|
324
|
+
if (this.#ready) {
|
|
325
|
+
this.#state = "ready";
|
|
326
|
+
this.#refreshPending = true;
|
|
327
|
+
this.#drain();
|
|
328
|
+
}
|
|
329
|
+
else
|
|
330
|
+
void this.#open();
|
|
331
|
+
}, delay);
|
|
215
332
|
}
|
|
216
|
-
#
|
|
217
|
-
if (this.#
|
|
333
|
+
#lost(code) {
|
|
334
|
+
if (this.#state === "closed")
|
|
218
335
|
return;
|
|
219
|
-
this.#failed = true;
|
|
220
|
-
clearTimeout(this.#timer);
|
|
221
|
-
clearInterval(this.#poll);
|
|
222
336
|
this.#report(code);
|
|
223
337
|
const active = this.#active;
|
|
224
338
|
this.#active = undefined;
|
|
225
|
-
active?.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
339
|
+
if (active?.work !== undefined)
|
|
340
|
+
this.#settle(active.work, "skipped");
|
|
341
|
+
if (code === "schema" || code === "corrupt") {
|
|
342
|
+
this.#state = this.#isClosed() ? "closing" : "blocked-storage";
|
|
343
|
+
this.#settleAll();
|
|
344
|
+
}
|
|
345
|
+
if (this.#isClosed()) {
|
|
346
|
+
this.#report("shutdown");
|
|
347
|
+
this.#finishClose();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
void this.#stopWorker().then(() => this.#recover());
|
|
351
|
+
}
|
|
352
|
+
#stopWorker() {
|
|
353
|
+
if (this.#stopping !== undefined)
|
|
354
|
+
return this.#stopping;
|
|
355
|
+
this.#clock.clearTimeout(this.#timer);
|
|
356
|
+
this.#timer = undefined;
|
|
357
|
+
this.#clock.clearTimeout(this.#poll);
|
|
358
|
+
this.#poll = undefined;
|
|
359
|
+
this.#ready = false;
|
|
360
|
+
++this.#generation;
|
|
361
|
+
const worker = this.#worker;
|
|
362
|
+
this.#worker = undefined;
|
|
363
|
+
if (worker === undefined)
|
|
364
|
+
return Promise.resolve();
|
|
365
|
+
worker.unref();
|
|
366
|
+
// Invariant: a rejected terminate is not proof of death. Wait for exit before opening a replacement.
|
|
367
|
+
const exited = new Promise(resolve => worker.once("exit", () => resolve()));
|
|
368
|
+
this.#stopping = worker.terminate().then(() => { }, () => exited).then(() => { this.#stopping = undefined; });
|
|
369
|
+
return this.#stopping;
|
|
370
|
+
}
|
|
371
|
+
#finishClose() {
|
|
372
|
+
if (this.#queue.length > 0)
|
|
373
|
+
this.#report("shutdown");
|
|
374
|
+
this.#state = "closed";
|
|
375
|
+
this.#active = undefined;
|
|
376
|
+
this.#settleAll();
|
|
377
|
+
for (const timer of [this.#timer, this.#poll, this.#expiry, this.#closeTimer])
|
|
378
|
+
this.#clock.clearTimeout(timer);
|
|
379
|
+
this.#timer = this.#poll = this.#expiry = this.#closeTimer = undefined;
|
|
380
|
+
void this.#stopWorker();
|
|
381
|
+
this.#resolveClose?.();
|
|
382
|
+
this.#resolveClose = undefined;
|
|
383
|
+
}
|
|
384
|
+
#report(code) {
|
|
385
|
+
const count = this.#counts[code] ?? 0;
|
|
386
|
+
this.#counts[code] = Math.min(Number.MAX_SAFE_INTEGER, count + 1);
|
|
387
|
+
if (count > 0)
|
|
388
|
+
return;
|
|
389
|
+
for (const listener of this.#failures) {
|
|
390
|
+
try {
|
|
391
|
+
listener(code);
|
|
392
|
+
}
|
|
393
|
+
catch { /* Security: never forward arbitrary observer errors. */ }
|
|
394
|
+
}
|
|
230
395
|
}
|
|
231
396
|
}
|
|
232
397
|
function failureCode(value) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type PromptHistoryFailure, type PromptHistorySnapshot, type PromptHistorySubmission } from "../../contracts/owned-ui/index.js";
|
|
2
2
|
export declare class HistoryStorageError extends Error {
|
|
3
3
|
readonly code: PromptHistoryFailure;
|
|
4
|
-
|
|
4
|
+
readonly certainty: "uncommitted" | "unknown";
|
|
5
|
+
constructor(code: PromptHistoryFailure, certainty?: "uncommitted" | "unknown");
|
|
5
6
|
}
|
|
6
7
|
export declare class PromptHistoryStore {
|
|
7
8
|
#private;
|
|
@@ -5,9 +5,11 @@ import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
|
5
5
|
import { assertPromptHistorySubmission, PROMPT_HISTORY_MAX_ENTRY_BYTES, PROMPT_HISTORY_MAX_TEXT_BYTES } from "../../contracts/owned-ui/index.js";
|
|
6
6
|
export class HistoryStorageError extends Error {
|
|
7
7
|
code;
|
|
8
|
-
|
|
8
|
+
certainty;
|
|
9
|
+
constructor(code, certainty = "uncommitted") {
|
|
9
10
|
super(`Prompt history ${code}`);
|
|
10
11
|
this.code = code;
|
|
12
|
+
this.certainty = certainty;
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
15
|
const MAX_STORAGE_BYTES = 64 * 1024 * 1024;
|
|
@@ -107,14 +109,25 @@ export class PromptHistoryStore {
|
|
|
107
109
|
}
|
|
108
110
|
close() { this.#database.close(); }
|
|
109
111
|
#transaction(work) {
|
|
110
|
-
|
|
112
|
+
try {
|
|
113
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new HistoryStorageError(classifyHistoryError(error), "uncommitted");
|
|
117
|
+
}
|
|
111
118
|
try {
|
|
112
119
|
work();
|
|
113
120
|
this.#database.exec("COMMIT");
|
|
114
121
|
}
|
|
115
122
|
catch (error) {
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
// Invariant: only a successful rollback proves that replay cannot advance recency twice.
|
|
124
|
+
try {
|
|
125
|
+
this.#database.exec("ROLLBACK");
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
throw new HistoryStorageError(classifyHistoryError(error), "unknown");
|
|
129
|
+
}
|
|
130
|
+
throw new HistoryStorageError(classifyHistoryError(error), "uncommitted");
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
133
|
#prune(limit) {
|