@timurproko/a1 0.1.8-dev.299 → 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 (95) hide show
  1. package/README.md +16 -4
  2. package/bin/guardian.js +4 -3
  3. package/bin/ui.js +4 -2
  4. package/bin/update-recovery.js +3 -1
  5. package/dist/cli/dispatch.js +1 -1
  6. package/dist/composition/owned-ui.js +2 -1
  7. package/dist/features/launch/profile-paths.d.ts +2 -0
  8. package/dist/features/launch/profile-paths.js +11 -0
  9. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  10. package/dist/features/owned-ui/settings-app.js +1 -1
  11. package/dist/features/prompt-history/service.d.ts +31 -1
  12. package/dist/features/prompt-history/service.js +294 -129
  13. package/dist/features/prompt-history/store.d.ts +2 -1
  14. package/dist/features/prompt-history/store.js +17 -4
  15. package/dist/features/prompt-history/worker.js +6 -3
  16. package/dist/foundation/launch-context/index.d.ts +59 -0
  17. package/dist/foundation/launch-context/index.js +136 -0
  18. package/dist/foundation/launch-guardian/main.js +6 -4
  19. package/dist/foundation/release/bootstrap.d.ts +6 -1
  20. package/dist/foundation/release/bootstrap.js +59 -13
  21. package/dist/foundation/release/cohort-state.d.ts +1 -0
  22. package/dist/foundation/release/cohort-state.js +1 -0
  23. package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
  24. package/dist/foundation/release/dependency-certification-retention.js +88 -0
  25. package/dist/foundation/release/dependency-certification.d.ts +24 -0
  26. package/dist/foundation/release/dependency-certification.js +208 -0
  27. package/dist/foundation/release/dependency-layer.d.ts +3 -4
  28. package/dist/foundation/release/dependency-layer.js +6 -37
  29. package/dist/foundation/release/release-gc.js +74 -46
  30. package/dist/foundation/release/release-store.d.ts +2 -0
  31. package/dist/foundation/release/release-store.js +11 -3
  32. package/dist/foundation/release/release.d.ts +3 -1
  33. package/dist/foundation/release/release.js +7 -3
  34. package/dist/foundation/release/restart-certification.d.ts +1 -0
  35. package/dist/foundation/release/restart-certification.js +17 -1
  36. package/dist/foundation/release/update-recovery.d.ts +2 -0
  37. package/dist/foundation/release/update-recovery.js +4 -0
  38. package/dist/foundation/release/update.js +1 -1
  39. package/dist/foundation/release/warmup.d.ts +1 -1
  40. package/dist/foundation/release/warmup.js +18 -6
  41. package/dist/foundation/startup/startup-runtime.js +13 -11
  42. package/dist/foundation/supervision/main.js +8 -7
  43. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -1
  44. package/dist/integrations/pi/components/owned-editor-ux.js +105 -30
  45. package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
  46. package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
  47. package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
  48. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
  49. package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  51. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
  52. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
  53. package/dist/integrations/pi/engine/adapter.d.ts +33 -1
  54. package/dist/integrations/pi/engine/adapter.js +309 -112
  55. package/dist/integrations/pi/engine/changelog.d.ts +2 -0
  56. package/dist/integrations/pi/engine/changelog.js +63 -0
  57. package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
  58. package/dist/integrations/pi/engine/pending-delivery.js +149 -0
  59. package/dist/integrations/pi/engine/resources/changelog.json +12 -0
  60. package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
  61. package/dist/integrations/pi/engine/workflows.d.ts +1 -1
  62. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
  63. package/dist/integrations/pi/session-ui/prompt-chips.js +74 -37
  64. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
  65. package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
  66. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +26 -1
  67. package/dist/integrations/pi/session-ui/session-shell-root.js +70 -75
  68. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  69. package/dist/integrations/pi/session-ui/session-shell.js +67 -13
  70. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -2
  71. package/dist/integrations/pi/session-ui/session-viewport-controller.js +25 -20
  72. package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
  73. package/dist/integrations/pi/session-ui/text-paste.js +16 -0
  74. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +9 -2
  75. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +87 -43
  76. package/dist/native/darwin-arm64/manifest.json +1 -1
  77. package/dist/native/linux-x64/manifest.json +1 -1
  78. package/dist/native/win32-x64/manifest.json +2 -2
  79. package/dist/native/win32-x64/process-guardian.exe +0 -0
  80. package/dist/product-identity.d.ts +1 -1
  81. package/dist/product-identity.js +2 -35
  82. package/dist/product-identity.json +1 -23
  83. package/dist/ui/components/transcript-viewport.d.ts +8 -0
  84. package/dist/ui/components/transcript-viewport.js +23 -2
  85. package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
  86. package/dist/ui/components/visible-hyperlinks.js +1 -0
  87. package/dist/ui/settings/declarations.js +1 -0
  88. package/dist/ui/settings/sections.js +11 -1
  89. package/docs/architecture/boundaries.md +2 -1
  90. package/docs/architecture/internal-naming.md +91 -0
  91. package/docs/architecture/resource-and-data-policy.md +9 -4
  92. package/docs/architecture/toolchain.md +15 -1
  93. package/docs/features/prompt-history.md +39 -11
  94. package/docs/manual-code-streaming-cleanup.md +88 -0
  95. package/package.json +8 -3
@@ -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) {
@@ -1,6 +1,6 @@
1
1
  import { parentPort, workerData } from "node:worker_threads";
2
2
  import { setTimeout as delay } from "node:timers/promises";
3
- import { PromptHistoryStore, classifyHistoryError } from "./store.js";
3
+ import { HistoryStorageError, PromptHistoryStore, classifyHistoryError } from "./store.js";
4
4
  const port = parentPort;
5
5
  if (port !== null) {
6
6
  let store;
@@ -22,7 +22,8 @@ if (port !== null) {
22
22
  port.postMessage({ id: request.id, ok: true, value });
23
23
  }
24
24
  catch (error) {
25
- port.postMessage({ id: request.id, ok: false, code: classifyHistoryError(error) });
25
+ port.postMessage({ id: request.id, ok: false, code: classifyHistoryError(error),
26
+ certainty: error instanceof HistoryStorageError ? error.certainty : "unknown" });
26
27
  }
27
28
  });
28
29
  });
@@ -34,7 +35,9 @@ async function retry(operation) {
34
35
  return operation();
35
36
  }
36
37
  catch (error) {
37
- if (classifyHistoryError(error) !== "busy" || performance.now() >= deadline)
38
+ if (classifyHistoryError(error) !== "busy"
39
+ || error instanceof HistoryStorageError && error.certainty === "unknown"
40
+ || performance.now() >= deadline)
38
41
  throw error;
39
42
  await delay(25);
40
43
  }
@@ -0,0 +1,59 @@
1
+ /** The contract this build emits; supersedes the pre-cutover key set it still accepts. */
2
+ export declare const PRIVATE_LAUNCH_CONTRACT = "neutral-launch-v1";
3
+ export declare const PRIVATE_ENVIRONMENT: Readonly<{
4
+ releaseRoot: "LAUNCH_CONTEXT_RELEASE_ROOT";
5
+ releaseId: "LAUNCH_CONTEXT_RELEASE_ID";
6
+ releaseDigest: "LAUNCH_CONTEXT_RELEASE_DIGEST";
7
+ releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS";
8
+ launchProfile: "LAUNCH_CONTEXT_PROFILE";
9
+ immutableWarmup: "LAUNCH_CONTEXT_WARMUP";
10
+ }>;
11
+ /**
12
+ * Keys published by releases that predate the neutral cutover. They are read when
13
+ * an older launcher hands this build a process, and written when this build
14
+ * starts a release that declares no contract of its own, so an upgrade never
15
+ * depends on both sides having been rebuilt at the same moment.
16
+ */
17
+ export declare const SUPERSEDED_ENVIRONMENT: Readonly<{
18
+ releaseRoot: "A1_RELEASE_ROOT";
19
+ releaseId: "A1_RELEASE_ID";
20
+ releaseDigest: "A1_RELEASE_DIGEST";
21
+ releaseLayers: "A1_RELEASE_LAYERS";
22
+ launchProfile: "A1_LAUNCH_PROFILE";
23
+ immutableWarmup: "A1_IMMUTABLE_WARMUP";
24
+ }>;
25
+ /** A release either declares the current contract or predates contract declaration. */
26
+ export type LaunchContractTarget = typeof PRIVATE_LAUNCH_CONTRACT | "superseded";
27
+ export interface LaunchContext {
28
+ readonly releaseRoot?: string;
29
+ readonly releaseId?: string;
30
+ readonly releaseDigest?: string;
31
+ readonly releaseLayers?: string;
32
+ readonly launchProfile?: "a1" | "pi";
33
+ readonly immutableWarmup?: "1";
34
+ }
35
+ export type LaunchContextRequirement = "optional" | "profile" | "release" | "warmup";
36
+ /** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
37
+ export declare function readLaunchContext(environment: NodeJS.ProcessEnv, requirement?: LaunchContextRequirement, platform?: NodeJS.Platform): LaunchContext;
38
+ /** The key set a process started for this release must receive. */
39
+ export declare function launchEnvironmentKeys(contract: LaunchContractTarget): Readonly<Record<string, string>>;
40
+ /** Classify metadata by the contract it declares rather than rejecting what it omits. */
41
+ export declare function launchContractTarget(value: {
42
+ readonly launchContract?: unknown;
43
+ }): LaunchContractTarget;
44
+ /** Whether metadata was produced by a build that declares this contract. */
45
+ export declare function isCurrentLaunchContract(value: {
46
+ readonly launchContract?: unknown;
47
+ }): boolean;
48
+ /**
49
+ * Rebuild owned context without inheriting a prior session's private selection. The
50
+ * contract selects which key set is written, so this build can start either its
51
+ * own releases or a retained pre-cutover release it must keep serving.
52
+ */
53
+ export declare function withLaunchContext(environment: NodeJS.ProcessEnv, context: LaunchContext, platform?: NodeJS.Platform, contract?: LaunchContractTarget): NodeJS.ProcessEnv;
54
+ /** Remove both key sets, leaving user and integration settings intact. */
55
+ export declare function withoutLaunchContext(environment: NodeJS.ProcessEnv, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
56
+ /** Require this build's own contract where nothing older can be accepted, such as recovery capsules. */
57
+ export declare function assertCurrentLaunchContract(value: {
58
+ readonly launchContract?: unknown;
59
+ }): void;