@timurproko/a1 0.1.8-dev.332 → 0.1.8-dev.368

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 (110) hide show
  1. package/README.md +40 -11
  2. package/dist/composition/owned-ui.d.ts +2 -0
  3. package/dist/composition/owned-ui.js +20 -3
  4. package/dist/contracts/owned-ui/index.d.ts +2 -0
  5. package/dist/contracts/owned-ui/index.js +2 -0
  6. package/dist/contracts/owned-ui/model.d.ts +38 -3
  7. package/dist/contracts/owned-ui/prompt-history.d.ts +18 -0
  8. package/dist/contracts/owned-ui/prompt-suggestions.d.ts +1 -1
  9. package/dist/contracts/owned-ui/prompt-suggestions.js +4 -1
  10. package/dist/contracts/owned-ui/suggestion-diagnostics.d.ts +22 -0
  11. package/dist/contracts/owned-ui/suggestion-diagnostics.js +9 -0
  12. package/dist/contracts/owned-ui/transcript-lifecycle.d.ts +5 -0
  13. package/dist/contracts/owned-ui/transcript-lifecycle.js +33 -0
  14. package/dist/contracts/owned-ui/validation.js +45 -2
  15. package/dist/contracts/presentation/index.d.ts +8 -0
  16. package/dist/features/owned-ui/settings-app.js +1 -1
  17. package/dist/features/prompt-history/image-sidecar.d.ts +32 -0
  18. package/dist/features/prompt-history/image-sidecar.js +130 -0
  19. package/dist/features/prompt-history/index.d.ts +2 -0
  20. package/dist/features/prompt-history/index.js +1 -0
  21. package/dist/features/prompt-history/paths.d.ts +1 -0
  22. package/dist/features/prompt-history/paths.js +2 -1
  23. package/dist/features/prompt-history/store.d.ts +1 -1
  24. package/dist/features/prompt-history/store.js +46 -2
  25. package/dist/features/prompt-history/worker.js +1 -1
  26. package/dist/features/prompt-suggestions/diagnostics.d.ts +18 -0
  27. package/dist/features/prompt-suggestions/diagnostics.js +82 -0
  28. package/dist/features/prompt-suggestions/index.d.ts +1 -0
  29. package/dist/features/prompt-suggestions/index.js +1 -0
  30. package/dist/foundation/release/bootstrap.d.ts +2 -0
  31. package/dist/foundation/release/bootstrap.js +54 -18
  32. package/dist/foundation/release/index.d.ts +1 -0
  33. package/dist/foundation/release/index.js +1 -0
  34. package/dist/foundation/release/update-launch.d.ts +6 -0
  35. package/dist/foundation/release/update-launch.js +17 -0
  36. package/dist/foundation/release/update.d.ts +2 -0
  37. package/dist/foundation/release/update.js +20 -21
  38. package/dist/foundation/supervision/main.js +5 -2
  39. package/dist/foundation/supervision/server.js +40 -13
  40. package/dist/integrations/pi/components/owned-editor-ux.d.ts +9 -1
  41. package/dist/integrations/pi/components/owned-editor-ux.js +11 -8
  42. package/dist/integrations/pi/components/prompt-input-port.d.ts +18 -0
  43. package/dist/integrations/pi/components/prompt-input-port.js +1 -0
  44. package/dist/integrations/pi/components/shell-editor-autocomplete.js +66 -11
  45. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  46. package/dist/integrations/pi/components/shell-footer-status.js +9 -5
  47. package/dist/integrations/pi/components/shell-presenters-info.js +7 -2
  48. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +6 -2
  49. package/dist/integrations/pi/components/shell-presenters-transcript.js +81 -49
  50. package/dist/integrations/pi/components/shell-shared-facade.d.ts +17 -4
  51. package/dist/integrations/pi/components/shell-shared-facade.js +2 -1
  52. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +3 -1
  53. package/dist/integrations/pi/components/submitted-prompt-adapter.js +20 -4
  54. package/dist/integrations/pi/components/tool-result-adapter.d.ts +6 -0
  55. package/dist/integrations/pi/components/tool-result-adapter.js +27 -0
  56. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -0
  57. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +5 -0
  58. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -2
  59. package/dist/integrations/pi/components/upstream/components/owned-editor.js +41 -33
  60. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  61. package/dist/integrations/pi/components/upstream/components/session-footer.js +12 -7
  62. package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
  63. package/dist/integrations/pi/engine/adapter.d.ts +2 -1
  64. package/dist/integrations/pi/engine/adapter.js +181 -84
  65. package/dist/integrations/pi/engine/tool-rendering.d.ts +13 -0
  66. package/dist/integrations/pi/engine/tool-rendering.js +122 -0
  67. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +30 -0
  68. package/dist/integrations/pi/session-ui/prompt-chips.js +128 -1
  69. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +8 -0
  70. package/dist/integrations/pi/session-ui/prompt-history-controller.js +30 -2
  71. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.d.ts +11 -3
  72. package/dist/integrations/pi/session-ui/prompt-suggestion-controller.js +126 -51
  73. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +24 -2
  74. package/dist/integrations/pi/session-ui/session-shell-root.js +108 -38
  75. package/dist/integrations/pi/session-ui/session-shell.js +51 -31
  76. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +7 -1
  77. package/dist/integrations/pi/session-ui/session-viewport-controller.js +106 -16
  78. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  79. package/dist/integrations/pi/tui-runtime/adapter.js +35 -11
  80. package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -1
  81. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -0
  82. package/dist/integrations/pi/tui-runtime/index.js +1 -0
  83. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +13 -0
  84. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +59 -0
  85. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +32 -0
  86. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +159 -0
  87. package/dist/native/darwin-arm64/manifest.json +1 -1
  88. package/dist/native/linux-x64/manifest.json +1 -1
  89. package/dist/native/win32-x64/manifest.json +2 -2
  90. package/dist/native/win32-x64/process-guardian.exe +0 -0
  91. package/dist/product-identity.d.ts +1 -1
  92. package/dist/product-identity.js +1 -1
  93. package/dist/product-identity.json +1 -0
  94. package/dist/ui/components/index.d.ts +1 -0
  95. package/dist/ui/components/index.js +1 -0
  96. package/dist/ui/components/line-input.d.ts +2 -3
  97. package/dist/ui/components/line-input.js +4 -20
  98. package/dist/ui/components/mouse.d.ts +1 -1
  99. package/dist/ui/components/mouse.js +4 -3
  100. package/dist/ui/components/prompt-input.d.ts +32 -0
  101. package/dist/ui/components/prompt-input.js +54 -0
  102. package/docs/architecture/boundaries.md +1 -0
  103. package/docs/architecture/history-editor-provenance.md +5 -1
  104. package/docs/architecture/project-structure.md +1 -0
  105. package/docs/architecture/prompt-suggestions.md +68 -0
  106. package/docs/architecture/toolchain.md +1 -1
  107. package/docs/architecture/ui-reference-provenance.md +23 -0
  108. package/docs/ci-release-runbook.md +45 -7
  109. package/docs/features/modal-content-interaction.md +37 -0
  110. package/package.json +1 -1
@@ -8,7 +8,7 @@ export declare class PromptHistoryStore {
8
8
  #private;
9
9
  readonly path: string;
10
10
  readonly profileId: string;
11
- constructor(path: string, profileId: string, limit: number);
11
+ constructor(path: string, profileId: string, limit: number, imagesDir?: string);
12
12
  record(input: PromptHistorySubmission): void;
13
13
  snapshot(): PromptHistorySnapshot;
14
14
  close(): void;
@@ -3,6 +3,7 @@ import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, statSync } from "
3
3
  import { dirname } from "node:path";
4
4
  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
+ import { collectImageChipIdentifiers, PromptImageSidecar } from "./image-sidecar.js";
6
7
  export class HistoryStorageError extends Error {
7
8
  code;
8
9
  certainty;
@@ -17,11 +18,13 @@ export class PromptHistoryStore {
17
18
  path;
18
19
  profileId;
19
20
  #database;
20
- constructor(path, profileId, limit) {
21
+ #sidecar;
22
+ constructor(path, profileId, limit, imagesDir) {
21
23
  this.path = path;
22
24
  this.profileId = profileId;
23
25
  if (!Number.isInteger(limit) || limit < 10 || limit > 100 || limit % 10 !== 0)
24
26
  throw new HistoryStorageError("schema");
27
+ this.#sidecar = imagesDir === undefined ? undefined : new PromptImageSidecar(imagesDir);
25
28
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
26
29
  if (lstatSync(dirname(path)).isSymbolicLink())
27
30
  throw new HistoryStorageError("unavailable");
@@ -67,6 +70,10 @@ export class PromptHistoryStore {
67
70
  this.#database.close();
68
71
  throw error;
69
72
  }
73
+ // Rationale: opportunistically reclaim any sidecar whose row was pruned in a prior process
74
+ // crash or by another concurrent opener. Bounded by the number of files on disk minus the
75
+ // surviving reference set; failures are swallowed to preserve honest history behavior.
76
+ this.#sweepOrphanedSidecars();
70
77
  }
71
78
  record(input) {
72
79
  assertPromptHistorySubmission(input);
@@ -133,11 +140,48 @@ export class PromptHistoryStore {
133
140
  #prune(limit) {
134
141
  const rows = this.#database.prepare("SELECT sequence,bytes FROM prompts ORDER BY sequence DESC").all();
135
142
  let bytes = 0;
143
+ const dropped = [];
136
144
  for (const [index, row] of rows.entries()) {
137
145
  bytes += Number(row.bytes);
138
146
  if (index >= limit || bytes > PROMPT_HISTORY_MAX_TEXT_BYTES)
139
- this.#database.prepare("DELETE FROM prompts WHERE sequence=?").run(row.sequence);
147
+ dropped.push(row.sequence);
140
148
  }
149
+ if (dropped.length === 0)
150
+ return;
151
+ // Invariant: sidecar reap uses text still referenced by SURVIVING rows so a row still
152
+ // referencing an id shared with a pruned row keeps the file. Collect surviving ids before
153
+ // deleting so a concurrent snapshot cannot see a live row whose sidecar is already gone.
154
+ const droppedTexts = dropped.map(sequence => this.#database.prepare("SELECT text FROM prompts WHERE sequence=?").get(sequence)?.text).filter((value) => typeof value === "string");
155
+ for (const sequence of dropped)
156
+ this.#database.prepare("DELETE FROM prompts WHERE sequence=?").run(sequence);
157
+ if (this.#sidecar === undefined)
158
+ return;
159
+ const survivingIds = this.#collectReferencedIdentifiers();
160
+ for (const text of droppedTexts) {
161
+ for (const id of collectImageChipIdentifiers(text)) {
162
+ if (!survivingIds.has(id))
163
+ this.#sidecar.unlink(id);
164
+ }
165
+ }
166
+ }
167
+ #collectReferencedIdentifiers() {
168
+ const identifiers = new Set();
169
+ const rows = this.#database.prepare("SELECT text FROM prompts").all();
170
+ for (const row of rows) {
171
+ if (typeof row.text !== "string")
172
+ continue;
173
+ for (const id of collectImageChipIdentifiers(row.text))
174
+ identifiers.add(id);
175
+ }
176
+ return identifiers;
177
+ }
178
+ #sweepOrphanedSidecars() {
179
+ if (this.#sidecar === undefined)
180
+ return;
181
+ try {
182
+ this.#sidecar.sweep(this.#collectReferencedIdentifiers());
183
+ }
184
+ catch { /* Rationale: sidecar sweep never blocks history readiness. */ }
141
185
  }
142
186
  #checkSize(maintain = false) {
143
187
  let size = 0;
@@ -6,7 +6,7 @@ if (port !== null) {
6
6
  let store;
7
7
  let chain = Promise.resolve();
8
8
  const options = workerData;
9
- const open = retry(() => { store = new PromptHistoryStore(options.path, options.profileId, options.limit); });
9
+ const open = retry(() => { store = new PromptHistoryStore(options.path, options.profileId, options.limit, options.imagesDir); });
10
10
  void open.then(() => port.postMessage({ id: 0, ok: true }), error => port.postMessage({ id: 0, ok: false, code: classifyHistoryError(error) }));
11
11
  port.on("message", (request) => {
12
12
  chain = chain.then(async () => {
@@ -0,0 +1,18 @@
1
+ import { type SuggestionDiagnosticObserver, type SuggestionDiagnosticRecord } from "../../contracts/owned-ui/index.js";
2
+ export interface SuggestionDiagnosticCaptureOptions {
3
+ readonly enabled?: boolean;
4
+ /** Explicit local destination, or an injected sink for deterministic I/O tests. */
5
+ readonly destination?: string;
6
+ readonly writeSnapshot?: (snapshot: string) => Promise<void>;
7
+ }
8
+ /** Bounded metadata snapshots; one in-flight write and one replaceable latest snapshot. */
9
+ export declare class SuggestionDiagnosticCapture implements SuggestionDiagnosticObserver {
10
+ #private;
11
+ readonly options: SuggestionDiagnosticCaptureOptions;
12
+ constructor(options?: SuggestionDiagnosticCaptureOptions);
13
+ record(record: SuggestionDiagnosticRecord): void;
14
+ snapshot(): readonly SuggestionDiagnosticRecord[];
15
+ /** Explicit evidence boundary, never awaited by input or rendering. */
16
+ flush(): Promise<void>;
17
+ dispose(): void;
18
+ }
@@ -0,0 +1,82 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { SUGGESTION_DECISION_REASONS, SUGGESTION_DIAGNOSTIC_EVENTS, } from "../../contracts/owned-ui/index.js";
3
+ /** Bounded metadata snapshots; one in-flight write and one replaceable latest snapshot. */
4
+ export class SuggestionDiagnosticCapture {
5
+ options;
6
+ #records = [];
7
+ #pending;
8
+ #writing;
9
+ #disposed = false;
10
+ #write;
11
+ constructor(options = {}) {
12
+ this.options = options;
13
+ this.#write = options.writeSnapshot ?? (options.destination === undefined ? undefined
14
+ : snapshot => writeFile(options.destination, snapshot, { encoding: "utf8", mode: 0o600 }));
15
+ }
16
+ record(record) {
17
+ if (!this.options.enabled || this.#disposed)
18
+ return;
19
+ const safe = sanitize(record);
20
+ if (safe === null)
21
+ return;
22
+ this.#records.push(safe);
23
+ if (this.#records.length > 128)
24
+ this.#records.shift();
25
+ let snapshot = this.#serialize();
26
+ while (Buffer.byteLength(snapshot, "utf8") > 65_536) {
27
+ this.#records.shift();
28
+ snapshot = this.#serialize();
29
+ }
30
+ if (this.#write) {
31
+ this.#pending = snapshot;
32
+ this.#pump();
33
+ }
34
+ }
35
+ snapshot() { return this.#records.map(record => ({ ...record })); }
36
+ /** Explicit evidence boundary, never awaited by input or rendering. */
37
+ async flush() {
38
+ while (this.#writing)
39
+ await this.#writing;
40
+ }
41
+ dispose() {
42
+ this.#disposed = true;
43
+ this.#records = [];
44
+ this.#pending = undefined;
45
+ }
46
+ #serialize() { return JSON.stringify({ schema: "prompt-suggestion-diagnostics-v1", records: this.#records }); }
47
+ #pump() {
48
+ if (this.#writing || this.#pending === undefined || !this.#write || this.#disposed)
49
+ return;
50
+ const snapshot = this.#pending;
51
+ this.#pending = undefined;
52
+ this.#writing = Promise.resolve().then(() => this.#write(snapshot)).catch(() => {
53
+ // Security: do not retain or display raw filesystem/provider errors.
54
+ }).finally(() => {
55
+ this.#writing = undefined;
56
+ this.#pump();
57
+ });
58
+ }
59
+ }
60
+ function sanitize(record) {
61
+ if (!SUGGESTION_DIAGNOSTIC_EVENTS.includes(record.event))
62
+ return null;
63
+ const reasoning = ["ordinary", "off", "minimal", "low", "medium", "high", "xhigh", "max", "unavailable"].includes(record.reasoning)
64
+ ? record.reasoning : "unavailable";
65
+ const reason = record.reason !== undefined && SUGGESTION_DECISION_REASONS.includes(record.reason) ? record.reason : undefined;
66
+ // Security: explicit projection prevents arbitrary extra fields, including raw error payloads, from escaping.
67
+ return {
68
+ event: record.event, ...(reason === undefined ? {} : { reason }),
69
+ session: boundedNumber(record.session), run: boundedNumber(record.run), response: boundedNumber(record.response),
70
+ request: boundedNumber(record.request), provider: identifier(record.provider), model: identifier(record.model),
71
+ reasoning, elapsedMs: boundedNumber(record.elapsedMs),
72
+ };
73
+ }
74
+ function boundedNumber(value) {
75
+ return Number.isFinite(value) ? Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.floor(value))) : 0;
76
+ }
77
+ function identifier(value) {
78
+ // Security: identifiers are metadata, not display names or arbitrary JSON. Reject controls/path-shaped values.
79
+ if (typeof value !== "string" || /[\p{C}\\\s]/u.test(value) || value.includes(":") || value.startsWith("/"))
80
+ return "redacted";
81
+ return value.slice(0, 64);
82
+ }
@@ -0,0 +1 @@
1
+ export * from "./diagnostics.js";
@@ -0,0 +1 @@
1
+ export * from "./diagnostics.js";
@@ -35,6 +35,8 @@ export interface SupervisorStartupAttempt extends SupervisorStartupAttemptIdenti
35
35
  readonly signal: NodeJS.Signals | null;
36
36
  }>;
37
37
  }
38
+ /** Reuse a verified owner, or join the winner if another launch starts the same cohort first. */
39
+ export declare function ensureSupervisor(release: MaterializedRelease, environment: NodeJS.ProcessEnv): Promise<void>;
38
40
  export declare function startSupervisor(release: MaterializedRelease, environment: NodeJS.ProcessEnv): Promise<SupervisorStartupAttempt>;
39
41
  /**
40
42
  * Hand a release the key set its own build reads. A retained pre-cutover release is
@@ -10,6 +10,8 @@ import { assertLaunchProfileId, createSupervisorStartupAttempt, readSupervisorSt
10
10
  import { encodeFrame, LineFrameDecoder } from "../protocol/index.js";
11
11
  import { cleanupProvenIdleOwner, processIsAlive } from "./process-cleanup.js";
12
12
  import { sweepDeadEndpoints } from "./endpoints.js";
13
+ import { UpdateTransactionStore } from "./update-transaction.js";
14
+ import { selectUpdateLaunchRelease } from "./update-launch.js";
13
15
  import { consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
14
16
  import { scheduleReleaseCleanup } from "./release-gc.js";
15
17
  import { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument } from "./restart-certification.js";
@@ -29,6 +31,21 @@ export async function runBootstrap(options) {
29
31
  await mkdir(paths.runtimeDir, { recursive: true, mode: 0o700 });
30
32
  const stateStore = new CohortStateStore(paths.dataDir);
31
33
  let state = await stateStore.read();
34
+ const launchDuringUpdate = async () => {
35
+ // Concurrency: read the journal before touching npm's mutable tree, including package.json.
36
+ const transaction = await new UpdateTransactionStore(paths.dataDir).read();
37
+ if (!transaction || transaction.status === "completed")
38
+ return null;
39
+ const prior = selectUpdateLaunchRelease(await stateStore.read(), transaction);
40
+ if (!prior)
41
+ return null;
42
+ const retained = await readCertifiedReleaseManifest(prior, resolve(paths.dataDir, "releases"));
43
+ await ensureSupervisor(retained, environment);
44
+ return await launchUi(retained, environment, sessionArgs);
45
+ };
46
+ const duringUpdate = await launchDuringUpdate();
47
+ if (duringUpdate !== null)
48
+ return duringUpdate;
32
49
  // Invariant: records left by cohorts whose processes are gone say nothing about ownership, and there
33
50
  // can now be several of them. Clearing them first keeps the decision below about what is
34
51
  // actually running.
@@ -80,10 +97,8 @@ export async function runBootstrap(options) {
80
97
  return verified;
81
98
  });
82
99
  await markStartupPhase(environment, "durable-validation-complete");
83
- const retainedPaths = resolveCohortEndpoint(paths, retained.releaseId, environment);
84
100
  await markStartupPhase(environment, "replacement-supervisor-start");
85
- const startup = await startSupervisor(retained, environment);
86
- await waitForVerifiedEndpoint(retainedPaths.endpointMetadataPath, retained, 8_000, startup);
101
+ await ensureSupervisor(retained, environment);
87
102
  await markStartupPhase(environment, "replacement-supervisor-ready");
88
103
  return await launchUi(retained, environment, sessionArgs);
89
104
  }
@@ -102,6 +117,10 @@ export async function runBootstrap(options) {
102
117
  throw error;
103
118
  return fallback;
104
119
  }
120
+ // Concurrency: an update may have begun while this launch was reading the installed payload.
121
+ const afterMaterialization = await launchDuringUpdate();
122
+ if (afterMaterialization !== null)
123
+ return afterMaterialization;
105
124
  await stateStore.recordCandidate(candidate);
106
125
  state = await stateStore.read();
107
126
  if (!state.references.active) {
@@ -192,8 +211,7 @@ export async function runBootstrap(options) {
192
211
  else {
193
212
  selected = await readMaterializedRelease(decision.releaseRoot);
194
213
  }
195
- const startup = await startSupervisor(selected, environment);
196
- await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, selected.releaseId, environment).endpointMetadataPath, selected, 8_000, startup);
214
+ await ensureSupervisor(selected, environment);
197
215
  return await launchUi(selected, environment, sessionArgs);
198
216
  }
199
217
  /**
@@ -209,14 +227,7 @@ async function launchRetainedActive(state, paths, environment, sessionArgs, outp
209
227
  return null;
210
228
  try {
211
229
  const retained = await readCertifiedReleaseManifest(active, resolve(paths.dataDir, "releases"));
212
- const retainedPaths = resolveCohortEndpoint(paths, retained.releaseId, environment);
213
- const endpoint = await readEndpointMetadata(retainedPaths.endpointMetadataPath);
214
- if (!endpoint || await probeOwnership(endpoint) !== "live-verified") {
215
- if (endpoint)
216
- await removeEndpointArtifacts(retainedPaths.endpointMetadataPath, retainedPaths.endpoint);
217
- const startup = await startSupervisor(retained, environment);
218
- await waitForVerifiedEndpoint(retainedPaths.endpointMetadataPath, retained, 8_000, startup);
219
- }
230
+ await ensureSupervisor(retained, environment);
220
231
  output.write(`${PRODUCT_TEXT.diagnostic(`installation is being replaced; starting the retained release ${retained.packageVersion}`)}\n`);
221
232
  return await launchUi(retained, environment, sessionArgs);
222
233
  }
@@ -246,6 +257,25 @@ export async function recordParentCertifiedRelease(release, dataDir) {
246
257
  await chmod(path, 0o400);
247
258
  return path;
248
259
  }
260
+ /** Reuse a verified owner, or join the winner if another launch starts the same cohort first. */
261
+ export async function ensureSupervisor(release, environment) {
262
+ const paths = resolveCohortEndpoint(resolveProductPaths(environment), release.releaseId, environment);
263
+ const metadata = await readEndpointMetadata(paths.endpointMetadataPath);
264
+ if (metadata) {
265
+ const probe = await probeOwnership(metadata);
266
+ if (probe !== "dead") {
267
+ if (probe === "live-verified" && endpointMatchesRelease(metadata, release))
268
+ return;
269
+ throw new Error(PRODUCT_TEXT.diagnostic(`refused duplicate supervisor startup: existing ownership is ${probe}`));
270
+ }
271
+ }
272
+ const startup = await startSupervisor(release, environment);
273
+ await waitForVerifiedEndpoint(paths.endpointMetadataPath, release, 8_000, startup);
274
+ }
275
+ function endpointMatchesRelease(metadata, release) {
276
+ return metadata.releaseId === release.releaseId && metadata.releaseRoot === release.releaseRoot
277
+ && metadata.contentDigest === release.contentDigest;
278
+ }
249
279
  export async function startSupervisor(release, environment) {
250
280
  const entry = await resolveReleaseEntryPoint(release, "bin/supervisor.js");
251
281
  const paths = resolveProductPaths(environment);
@@ -296,10 +326,11 @@ export function releaseEnvironment(environment, release, profile) {
296
326
  export async function waitForVerifiedEndpoint(path, release, timeoutMs, startup) {
297
327
  const deadline = Date.now() + timeoutMs;
298
328
  let childOutcome = null;
329
+ let collision = null;
299
330
  void startup?.childOutcome.then(outcome => { childOutcome = outcome; });
300
331
  while (Date.now() < deadline) {
301
332
  const metadata = await readEndpointMetadata(path);
302
- if (metadata && metadata.releaseId === release.releaseId && await probeOwnership(metadata) === "live-verified") {
333
+ if (metadata && endpointMatchesRelease(metadata, release) && await probeOwnership(metadata) === "live-verified") {
303
334
  if (startup)
304
335
  await rm(startup.resultPath, { force: true });
305
336
  return;
@@ -307,15 +338,21 @@ export async function waitForVerifiedEndpoint(path, release, timeoutMs, startup)
307
338
  if (startup) {
308
339
  const result = await readSupervisorStartupResult(startup.resultPath, startup.attemptId, startup.releaseId);
309
340
  if (result?.outcome === "failure") {
310
- throw Object.assign(new Error(PRODUCT_TEXT.diagnostic(`supervisor startup failed at ${result.stage}: ${result.message}`)), {
341
+ const error = Object.assign(new Error(PRODUCT_TEXT.diagnostic(`supervisor startup failed at ${result.stage}: ${result.message}`)), {
311
342
  code: result.code ?? "SUPERVISOR_STARTUP_FAILED",
312
343
  });
344
+ if (result.code !== "EADDRINUSE")
345
+ throw error;
346
+ // Concurrency: the winning process may not have published its authenticated metadata yet.
347
+ collision = error;
313
348
  }
314
349
  }
315
- if (childOutcome)
350
+ if (childOutcome && !collision)
316
351
  throw new Error(PRODUCT_TEXT.diagnostic(`supervisor exited before readiness: ${JSON.stringify(childOutcome)}`));
317
352
  await new Promise(resolvePromise => setTimeout(resolvePromise, 40));
318
353
  }
354
+ if (collision)
355
+ throw collision;
319
356
  throw new Error(PRODUCT_TEXT.diagnostic(`supervisor did not publish verified endpoint metadata within ${timeoutMs}ms`));
320
357
  }
321
358
  export async function readEndpointMetadata(path) {
@@ -410,8 +447,7 @@ async function activatePendingAfterBlockerExit(candidate, stateStore, paths, env
410
447
  return;
411
448
  await removeEndpointArtifacts(paths.endpointMetadataPath, paths.endpoint);
412
449
  await stateStore.activate(candidate.releaseId);
413
- const startup = await startSupervisor(candidate, environment);
414
- await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, candidate.releaseId, environment).endpointMetadataPath, candidate, 8_000, startup);
450
+ await ensureSupervisor(candidate, environment);
415
451
  }
416
452
  export async function releaseVerifiedIdleOwner(metadata, dataDir, operations = {}) {
417
453
  try {
@@ -10,6 +10,7 @@ export * from "./release-store.js";
10
10
  export * from "./restart-certification.js";
11
11
  export * from "./stable-release.js";
12
12
  export * from "./update.js";
13
+ export * from "./update-launch.js";
13
14
  export * from "./update-recovery.js";
14
15
  export * from "./update-transaction.js";
15
16
  export * from "./warmup.js";
@@ -10,6 +10,7 @@ export * from "./release-store.js";
10
10
  export * from "./restart-certification.js";
11
11
  export * from "./stable-release.js";
12
12
  export * from "./update.js";
13
+ export * from "./update-launch.js";
13
14
  export * from "./update-recovery.js";
14
15
  export * from "./update-transaction.js";
15
16
  export * from "./warmup.js";
@@ -0,0 +1,6 @@
1
+ import type { CohortState, ReleaseRecord } from "./cohort-state.js";
2
+ import type { UpdateTransaction } from "./update-transaction.js";
3
+ /** Keep interactive launches on the last successful release until the update commits success. */
4
+ export declare function selectUpdateLaunchRelease(state: CohortState, transaction: UpdateTransaction | null): ReleaseRecord | null;
5
+ /** Pause cohort retirement during preparation; afterward only the successful launch release admits new sessions. */
6
+ export declare function selectSupervisorLaunchReleaseId(state: CohortState, transaction: UpdateTransaction | null): string | null;
@@ -0,0 +1,17 @@
1
+ /** Keep interactive launches on the last successful release until the update commits success. */
2
+ export function selectUpdateLaunchRelease(state, transaction) {
3
+ if (transaction === null || transaction.status === "completed")
4
+ return null;
5
+ const prior = transaction.priorActiveReleaseId === null ? undefined : state.releases[transaction.priorActiveReleaseId];
6
+ if (!prior || prior.approval !== "approved") {
7
+ throw new Error("unfinished update has no verified previous release available for launch; resume the update");
8
+ }
9
+ return prior;
10
+ }
11
+ /** Pause cohort retirement during preparation; afterward only the successful launch release admits new sessions. */
12
+ export function selectSupervisorLaunchReleaseId(state, transaction) {
13
+ // Concurrency: both the previous cohort and the warming candidate must remain available until cutover.
14
+ if (transaction?.status === "active")
15
+ return null;
16
+ return selectUpdateLaunchRelease(state, transaction)?.releaseId ?? state.references.active;
17
+ }
@@ -112,5 +112,7 @@ export type UpdateOwnershipAction = "clean-dead-record" | "leave-running" | "end
112
112
  */
113
113
  export declare function planUpdateOwnership(ownership: "live-verified" | "dead", runsFromRetainedRelease: boolean): UpdateOwnershipAction;
114
114
  export declare function createUpdateLifecycleCoordinator(environment?: NodeJS.ProcessEnv, fileSystem?: UpdateFileSystem, output?: UpdateOutput): UpdateLifecycleCoordinator;
115
+ /** Renders the terminal-only update meter; the visual preview shares this exact frame. */
116
+ export declare function renderUpdateProgressBar(percent: number): string;
115
117
  export declare function runSelfUpdate(options: SelfUpdateOptions): Promise<number>;
116
118
  export declare function assertUpdatePerformanceBudget(evidence: UpdatePerformanceEvidence, maximumPostNpmDurationMs?: number): void;
@@ -5,7 +5,7 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
5
5
  import crossSpawn from "cross-spawn";
6
6
  import { valid as validSemver } from "semver";
7
7
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
8
- import { certifyMaterializedRelease, probeOwnership, readEndpointMetadata, removeEndpointArtifacts, startSupervisor, waitForProcessExit, waitForVerifiedEndpoint, } from "./bootstrap.js";
8
+ import { certifyMaterializedRelease, probeOwnership, readEndpointMetadata, removeEndpointArtifacts, ensureSupervisor, waitForProcessExit, } from "./bootstrap.js";
9
9
  import { resolveCohortEndpoint, resolveProductPaths } from "../lifecycle/index.js";
10
10
  import { encodeFrame, LineFrameDecoder } from "../protocol/index.js";
11
11
  import { CohortStateStore } from "./cohort-state.js";
@@ -163,17 +163,17 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
163
163
  const diagnostics = await certifyMaterializedRelease(candidate, paths.dataDir);
164
164
  await stateStore.approve(candidate.releaseId, diagnostics);
165
165
  await phase("certified");
166
- await stateStore.activate(candidate.releaseId);
167
- await phase("active-reference-committed");
168
166
  onWarmup?.("started");
169
167
  await warmMaterializedRelease(candidate, environment);
170
168
  onWarmup?.("completed");
171
- const startup = await startSupervisor(candidate, environment);
172
- await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, candidate.releaseId, environment).endpointMetadataPath, candidate, 8_000, startup);
169
+ await ensureSupervisor(candidate, environment);
170
+ // Invariant: warmup and authenticated readiness precede changing the active reference.
171
+ await stateStore.activate(candidate.releaseId);
172
+ await phase("active-reference-committed");
173
173
  },
174
174
  };
175
175
  }
176
- const PROGRESS_BAR_WIDTH = 39;
176
+ const PROGRESS_BAR_WIDTH = 40;
177
177
  const PROGRESS_TICK_MS = 200;
178
178
  /**
179
179
  * Copying the release owns 78–92 and is reported file by file, so the bar crosses
@@ -186,10 +186,15 @@ const ACTIVATION_PROGRESS = {
186
186
  certified: { at: 94, creepTo: 96 },
187
187
  "active-reference-committed": { at: 96, creepTo: 99 },
188
188
  };
189
- function renderProgressBar(percent) {
189
+ /** Renders the terminal-only update meter; the visual preview shares this exact frame. */
190
+ export function renderUpdateProgressBar(percent) {
190
191
  const bounded = Math.min(100, Math.max(0, Math.round(percent)));
191
192
  const filled = Math.round((bounded / 100) * PROGRESS_BAR_WIDTH);
192
- return `${"█".repeat(filled)}${"░".repeat(PROGRESS_BAR_WIDTH - filled)} ${bounded}%`;
193
+ // Rationale: a gray line, a darker gray track, and one space before the percentage.
194
+ // Explicit RGB keeps both grays neutral even when the terminal remaps its ANSI palette.
195
+ const gray = "\u001b[38;2;128;128;128m";
196
+ const track = "\u001b[38;2;102;102;102m";
197
+ return `${gray}${"━".repeat(filled)}${track}${"─".repeat(PROGRESS_BAR_WIDTH - filled)}${gray} ${bounded}%\u001b[39m`;
193
198
  }
194
199
  function createUpdateProgress(output, enabled) {
195
200
  let visible = false;
@@ -202,7 +207,7 @@ function createUpdateProgress(output, enabled) {
202
207
  return;
203
208
  shown = rounded;
204
209
  visible = true;
205
- output.stdout(`\r${renderProgressBar(rounded)}`);
210
+ output.stdout(`\r${renderUpdateProgressBar(rounded)}`);
206
211
  };
207
212
  const stopCreep = () => {
208
213
  if (timer !== null) {
@@ -375,11 +380,7 @@ export async function runSelfUpdate(options) {
375
380
  : async () => await scheduleReleaseCleanup(paths.dataDir, paths));
376
381
  let transaction = await transactionStore.read();
377
382
  try {
378
- if (await lifecycle.targetIsActive(targetVersion)) {
379
- if (transaction?.status === "active") {
380
- await transactionStore.advance("supervisor-verified");
381
- await transactionStore.finish("completed");
382
- }
383
+ if ((!transaction || transaction.status === "completed") && await lifecycle.targetIsActive(targetVersion)) {
383
384
  await maintenance();
384
385
  await transactionStore.clearCompleted();
385
386
  output.stdout(`${PRODUCT_TEXT.commandName} is up to date — no update needed.\n`);
@@ -510,13 +511,14 @@ export async function runSelfUpdate(options) {
510
511
  options.onPhaseTiming?.({ phase: "supervisor-verified", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
511
512
  const transactionStartedAt = now();
512
513
  await transactionStore.advance("supervisor-verified");
513
- await transactionStore.finish("completed");
514
514
  if (transaction.recovery?.capsulePath)
515
515
  await removeUpdateRecoveryCapsule(paths.dataDir, transaction.transactionId);
516
516
  // Invariant: successful output follows the durable cleanup disposition. Slow recursive
517
517
  // removal belongs to the detached worker started by this maintenance coordinator.
518
518
  await maintenance();
519
- await transactionStore.clearCompleted();
519
+ // Concurrency: this is the launch cutover. No failure-prone update work follows success.
520
+ await transactionStore.finish("completed");
521
+ await transactionStore.clearCompleted().catch(() => undefined);
520
522
  options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
521
523
  progress.finish();
522
524
  output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}\n`);
@@ -609,11 +611,8 @@ async function rollbackPriorCohort(dataDir, environment, priorReleaseId) {
609
611
  throw new Error(`prior release ${priorReleaseId} is not the recorded rollback cohort`);
610
612
  }
611
613
  const release = await readMaterializedRelease(prior.releaseRoot);
612
- const paths = resolveProductPaths(environment);
613
- // Invariant: rollback re-points the active reference and starts the prior cohort on its own endpoint;
614
- // a cohort that survived the update keeps serving the work it already had.
615
- const startup = await startSupervisor(release, environment);
616
- await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, release.releaseId, environment).endpointMetadataPath, release, 8_000, startup);
614
+ // Invariant: rollback reuses a surviving prior cohort rather than racing its occupied endpoint.
615
+ await ensureSupervisor(release, environment);
617
616
  return "rolled back";
618
617
  }
619
618
  function phaseBefore(current, target) {
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { appendFileSync, mkdirSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
- import { assertImmutableExecutionRoot, CohortStateStore, readCertifiedReleaseManifest, recordParentCertifiedRelease } from "../release/index.js";
4
+ import { assertImmutableExecutionRoot, CohortStateStore, readCertifiedReleaseManifest, recordParentCertifiedRelease, selectSupervisorLaunchReleaseId, UpdateTransactionStore } from "../release/index.js";
5
5
  import { publishSupervisorStartupResult, supervisorStartupFailure, supervisorStartupReady, supervisorStartupResultPath } from "../lifecycle/index.js";
6
6
  import { ControlStore } from "../storage/index.js";
7
7
  import { resolveCohortEndpoint, resolveProductPaths } from "./paths.js";
@@ -38,7 +38,10 @@ export async function runSupervisor(arguments_ = []) {
38
38
  const bootNonce = randomUUID();
39
39
  const store = new ControlStore(paths.databasePath, bootNonce);
40
40
  const cohortState = new CohortStateStore(paths.dataDir);
41
- server = new SupervisorServer(store, paths, release, bootNonce, undefined, undefined, undefined, async () => (await cohortState.read()).references.active);
41
+ server = new SupervisorServer(store, paths, release, bootNonce, undefined, undefined, undefined, async () => {
42
+ const transaction = await new UpdateTransactionStore(paths.dataDir).read();
43
+ return selectSupervisorLaunchReleaseId(await cohortState.read(), transaction);
44
+ });
42
45
  stage = "endpoint-listen";
43
46
  await server.listen();
44
47
  if (resultPath && attemptId)