@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.
Files changed (52) hide show
  1. package/dist/cli/dispatch.js +1 -1
  2. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  3. package/dist/features/owned-ui/settings-app.js +1 -1
  4. package/dist/features/prompt-history/service.d.ts +31 -1
  5. package/dist/features/prompt-history/service.js +294 -129
  6. package/dist/features/prompt-history/store.d.ts +2 -1
  7. package/dist/features/prompt-history/store.js +17 -4
  8. package/dist/features/prompt-history/worker.js +6 -3
  9. package/dist/foundation/launch-context/index.d.ts +36 -6
  10. package/dist/foundation/launch-context/index.js +64 -11
  11. package/dist/foundation/launch-guardian/main.js +2 -1
  12. package/dist/foundation/release/bootstrap.d.ts +5 -0
  13. package/dist/foundation/release/bootstrap.js +49 -9
  14. package/dist/foundation/release/cohort-state.js +0 -3
  15. package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
  16. package/dist/foundation/release/dependency-certification-retention.js +88 -0
  17. package/dist/foundation/release/dependency-certification.d.ts +24 -0
  18. package/dist/foundation/release/dependency-certification.js +208 -0
  19. package/dist/foundation/release/dependency-layer.d.ts +3 -4
  20. package/dist/foundation/release/dependency-layer.js +6 -37
  21. package/dist/foundation/release/release-gc.js +72 -44
  22. package/dist/foundation/release/release-store.d.ts +2 -0
  23. package/dist/foundation/release/release-store.js +4 -3
  24. package/dist/foundation/release/restart-certification.js +16 -5
  25. package/dist/foundation/release/update.js +1 -6
  26. package/dist/foundation/release/warmup.js +16 -5
  27. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  28. package/dist/integrations/pi/components/owned-editor-ux.js +51 -27
  29. package/dist/integrations/pi/engine/adapter.d.ts +20 -0
  30. package/dist/integrations/pi/engine/adapter.js +223 -87
  31. package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
  32. package/dist/integrations/pi/engine/pending-delivery.js +149 -0
  33. package/dist/integrations/pi/session-ui/prompt-chips.js +38 -25
  34. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
  35. package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
  36. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +1 -1
  37. package/dist/integrations/pi/session-ui/session-shell-root.js +5 -1
  38. package/dist/integrations/pi/session-ui/session-shell.js +9 -6
  39. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
  40. package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -5
  41. package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
  42. package/dist/integrations/pi/session-ui/text-paste.js +16 -0
  43. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +2 -2
  44. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +82 -39
  45. package/dist/native/darwin-arm64/manifest.json +1 -1
  46. package/dist/native/linux-x64/manifest.json +1 -1
  47. package/dist/native/win32-x64/manifest.json +2 -2
  48. package/dist/native/win32-x64/process-guardian.exe +0 -0
  49. package/dist/product-identity.json +1 -1
  50. package/docs/architecture/internal-naming.md +5 -3
  51. package/docs/manual-code-streaming-cleanup.md +88 -0
  52. package/package.json +1 -1
@@ -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 A1`,
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 A1 to load project settings and resources, install missing project packages, and execute project extensions.",
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 A1" : SETTINGS_SHORTCUTS.hint(SCOPE);
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
- /** Owns a finite asynchronous history queue; storage faults never throw into prompt dispatch. */
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
- /** Owns a finite asynchronous history queue; storage faults never throw into prompt dispatch. */
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
- #reported = new Set();
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.#closing)
45
+ if (this.#started || this.#isClosed())
30
46
  return;
31
47
  this.#started = true;
32
- setImmediate(() => { void this.#open().catch(() => this.#fail("unavailable")); });
48
+ void this.#open();
33
49
  }
34
50
  record(submission) {
35
- if (this.#failed || this.#closing)
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.#writes >= PROMPT_HISTORY_MAX_PENDING || this.#bytes + bytes > PROMPT_HISTORY_MAX_TEXT_BYTES) {
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({ request: { id: ++this.#sequence, kind: "record", submission: candidate }, done: (_value, ok) => {
63
- this.#writes--;
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.#closing || this.#failed)
83
+ if (this.#isClosed() || this.#state === "blocked-storage")
74
84
  return;
75
85
  this.start();
76
- if (this.#reading) {
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.#closing = true;
121
- if (this.#poll !== undefined)
122
- clearInterval(this.#poll);
123
- this.#closePromise = new Promise(resolve => {
124
- if (!this.#started || this.#failed) {
125
- resolve();
126
- return;
127
- }
128
- const deadline = setTimeout(() => { this.#report("shutdown"); this.#fail("shutdown"); resolve(); }, 2000);
129
- this.#queue.push({ request: { id: ++this.#sequence, kind: "close" }, done: (_value, ok) => {
130
- if (!ok)
131
- this.#report("shutdown");
132
- const stopped = this.#worker?.terminate() ?? Promise.resolve();
133
- void stopped.catch(() => { }).finally(() => { clearTimeout(deadline); resolve(); });
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.#failed)
124
+ if (this.#isClosed() || this.#state === "blocked-storage" || this.#worker !== undefined || this.#stopping !== undefined)
141
125
  return;
142
- const { Worker } = await import("node:worker_threads");
143
- if (this.#failed)
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
- const location = resolvePromptHistoryPath(this.options.dataDir, this.options.profileRoot);
146
- const source = import.meta.url.endsWith(".ts");
147
- const entry = new URL(source ? "./worker.ts" : "./worker.js", import.meta.url);
148
- const options = { workerData: { ...location, limit: this.options.limit }, stdout: true, stderr: true };
149
- this.#worker = this.options.createWorker?.() ?? (source
150
- ? new Worker(`import('tsx/esm/api').then(({ tsImport }) => tsImport(${JSON.stringify(entry.href)}, ${JSON.stringify(import.meta.url)}))`, { ...options, eval: true })
151
- : new Worker(entry, options));
152
- this.#worker.stdout.resume();
153
- this.#worker.stderr.resume();
154
- this.#timer = setTimeout(() => this.#fail("unavailable"), 5000);
155
- this.#worker.on("message", (message) => {
156
- if (this.#failed)
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
- clearTimeout(this.#timer);
159
- if (message === null || typeof message !== "object" || !Number.isSafeInteger(message.id) || typeof message.ok !== "boolean") {
160
- this.#fail("unavailable");
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 (!message.ok) {
164
- this.#fail(failureCode(message.code));
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 (message.id === 0) {
168
- this.#ready = true;
169
- if (!this.#closing) {
170
- this.refresh();
171
- this.#poll = setInterval(() => this.refresh(), 1000);
172
- this.#poll.unref();
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
- else {
176
- const item = this.#active;
177
- if (item?.request.id !== message.id) {
178
- this.#fail("unavailable");
179
- return;
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
- this.#drain();
185
- });
186
- this.#worker.on("error", () => this.#fail("unavailable"));
187
- this.#worker.on("exit", () => { if (!this.#closing || this.#active !== undefined || this.#queue.length > 0)
188
- this.#fail("unavailable"); });
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.#failed || this.#active !== undefined)
244
+ if (!this.#ready || this.#active !== undefined || this.#timer !== undefined || this.#state === "blocked-storage" || this.#state === "closed")
192
245
  return;
193
- const next = this.#queue.shift();
194
- if (next === undefined)
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.#active = next;
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(next.request);
266
+ this.#worker.postMessage(request);
200
267
  }
201
268
  catch {
202
- this.#fail("unavailable");
269
+ this.#lost("unavailable");
203
270
  }
204
271
  }
205
- #report(code) {
206
- if (this.#reported.has(code))
272
+ #settle(work, result) {
273
+ const index = this.#queue.indexOf(work);
274
+ if (index < 0)
207
275
  return;
208
- this.#reported.add(code);
209
- for (const listener of this.#failures) {
210
- try {
211
- listener(code);
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
- catch { /* Security: caller exceptions are not persistence diagnostics. */ }
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
- #fail(code) {
217
- if (this.#failed)
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?.done(undefined, false);
226
- for (const item of this.#queue.splice(0))
227
- item.done(undefined, false);
228
- this.#worker?.unref();
229
- void this.#worker?.terminate().catch(() => { });
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
- constructor(code: PromptHistoryFailure);
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
- constructor(code) {
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
- this.#database.exec("BEGIN IMMEDIATE");
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
- this.#database.exec("ROLLBACK");
117
- throw error;
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) {