@timurproko/a1 0.1.8-dev.322 → 0.1.8-dev.335

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 (62) hide show
  1. package/README.md +22 -11
  2. package/dist/cli/dispatch.js +1 -1
  3. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  4. package/dist/features/owned-ui/settings-app.js +1 -1
  5. package/dist/features/prompt-history/service.d.ts +31 -1
  6. package/dist/features/prompt-history/service.js +294 -129
  7. package/dist/features/prompt-history/store.d.ts +2 -1
  8. package/dist/features/prompt-history/store.js +17 -4
  9. package/dist/features/prompt-history/worker.js +6 -3
  10. package/dist/foundation/launch-context/index.d.ts +36 -6
  11. package/dist/foundation/launch-context/index.js +64 -11
  12. package/dist/foundation/launch-guardian/main.js +2 -1
  13. package/dist/foundation/release/bootstrap.d.ts +7 -0
  14. package/dist/foundation/release/bootstrap.js +95 -19
  15. package/dist/foundation/release/cohort-state.js +0 -3
  16. package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
  17. package/dist/foundation/release/dependency-certification-retention.js +88 -0
  18. package/dist/foundation/release/dependency-certification.d.ts +24 -0
  19. package/dist/foundation/release/dependency-certification.js +208 -0
  20. package/dist/foundation/release/dependency-layer.d.ts +3 -4
  21. package/dist/foundation/release/dependency-layer.js +6 -37
  22. package/dist/foundation/release/index.d.ts +1 -0
  23. package/dist/foundation/release/index.js +1 -0
  24. package/dist/foundation/release/release-gc.js +72 -44
  25. package/dist/foundation/release/release-store.d.ts +2 -0
  26. package/dist/foundation/release/release-store.js +4 -3
  27. package/dist/foundation/release/restart-certification.js +16 -5
  28. package/dist/foundation/release/update-launch.d.ts +6 -0
  29. package/dist/foundation/release/update-launch.js +17 -0
  30. package/dist/foundation/release/update.d.ts +2 -0
  31. package/dist/foundation/release/update.js +21 -27
  32. package/dist/foundation/release/warmup.js +16 -5
  33. package/dist/foundation/supervision/main.js +5 -2
  34. package/dist/foundation/supervision/server.js +40 -13
  35. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  36. package/dist/integrations/pi/components/owned-editor-ux.js +51 -27
  37. package/dist/integrations/pi/engine/adapter.d.ts +20 -0
  38. package/dist/integrations/pi/engine/adapter.js +223 -87
  39. package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
  40. package/dist/integrations/pi/engine/pending-delivery.js +149 -0
  41. package/dist/integrations/pi/session-ui/prompt-chips.js +38 -25
  42. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
  43. package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
  44. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +1 -1
  45. package/dist/integrations/pi/session-ui/session-shell-root.js +5 -1
  46. package/dist/integrations/pi/session-ui/session-shell.js +9 -6
  47. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
  48. package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -5
  49. package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
  50. package/dist/integrations/pi/session-ui/text-paste.js +16 -0
  51. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +2 -2
  52. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +82 -39
  53. package/dist/native/darwin-arm64/manifest.json +1 -1
  54. package/dist/native/linux-x64/manifest.json +1 -1
  55. package/dist/native/win32-x64/manifest.json +2 -2
  56. package/dist/native/win32-x64/process-guardian.exe +0 -0
  57. package/dist/product-identity.json +1 -1
  58. package/docs/architecture/internal-naming.md +5 -3
  59. package/docs/architecture/toolchain.md +1 -1
  60. package/docs/ci-release-runbook.md +45 -7
  61. package/docs/manual-code-streaming-cleanup.md +88 -0
  62. package/package.json +1 -1
package/README.md CHANGED
@@ -160,18 +160,29 @@ a1 update --develop 0.1.8-dev.107 # install that exact full preview versio
160
160
  ### Stable
161
161
 
162
162
  ```sh
163
- npm run release -- patch # 0.1.1 -> 0.1.2
164
- npm run release -- minor # 0.1.1 -> 0.2.0
165
- npm run release -- major # 0.1.1 -> 1.0.0
166
- npm run release -- 0.4.0 # an exact version
163
+ npm run release -- patch # 0.1.8-dev -> 0.1.8; already-stable 0.1.8 -> 0.1.9
164
+ npm run release -- minor # 0.1.8-dev -> 0.2.0
165
+ npm run release -- major # 0.1.8-dev -> 1.0.0
166
+ npm run release -- 0.4.0 # an exact stable version
167
167
  ```
168
168
 
169
- The command lands the version on `develop` through a self-merging pull request,
170
- dispatches publication for that exact commit, and waits for success. CI validates
171
- the packed release on Windows, Linux, and macOS, publishes to npm `latest` with
172
- provenance, then writes the `v<version>` tag and the GitHub Release. `master`
173
- fast-forwards to the released commit, so it always points at what npm `latest`
174
- serves. A failed release leaves nothing behind: no tag, no GitHub Release, no
175
- moved branch.
169
+ Run from the repository root on a clean `develop` matching `origin/develop`.
170
+ A target is required; bare `npm run release` displays usage and releases nothing.
171
+ `patch` promotes the current prerelease rather than skipping its stable version.
172
+
173
+ The command prepares a version-only PR in an isolated detached worktree, prints
174
+ its URL, and waits for you to validate and **merge it manually**. Neither this PR
175
+ nor the next-development PR is auto-merged, and green CI alone does not advance
176
+ the release. After the stable PR merges, the command dispatches publication for
177
+ that exact authoritative commit and waits for success. CI validates the packed
178
+ release on Windows, Linux, and macOS, publishes to npm `latest` with provenance,
179
+ then writes the `v<version>` tag and GitHub Release and fast-forwards `master`.
180
+
181
+ Only after confirmed publication of `0.1.8` does the command prepare the separate
182
+ `0.1.9-dev` PR. It reports development reopened only after you manually merge that
183
+ PR too. Work added to your checkout during either wait is preserved, not reset.
184
+ If publication fails or is uncertain, no reopening PR is prepared. If publication
185
+ succeeded but reopening failed, inspect the reported phase and PR; do not republish
186
+ the immutable stable version.
176
187
 
177
188
  `docs/ci-release-runbook.md` has the full picture.
@@ -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;