@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
@@ -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
  }
@@ -1,4 +1,4 @@
1
- /** The one supported private process contract; public settings remain product identity data. */
1
+ /** The contract this build emits; supersedes the pre-cutover key set it still accepts. */
2
2
  export declare const PRIVATE_LAUNCH_CONTRACT = "neutral-launch-v1";
3
3
  export declare const PRIVATE_ENVIRONMENT: Readonly<{
4
4
  releaseRoot: "LAUNCH_CONTEXT_RELEASE_ROOT";
@@ -8,6 +8,22 @@ export declare const PRIVATE_ENVIRONMENT: Readonly<{
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE";
9
9
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP";
10
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";
11
27
  export interface LaunchContext {
12
28
  readonly releaseRoot?: string;
13
29
  readonly releaseId?: string;
@@ -17,13 +33,27 @@ export interface LaunchContext {
17
33
  readonly immutableWarmup?: "1";
18
34
  }
19
35
  export type LaunchContextRequirement = "optional" | "profile" | "release" | "warmup";
20
- /** Read current private keys only, with entry-specific required fields and bounded diagnostics. */
36
+ /** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
21
37
  export declare function readLaunchContext(environment: NodeJS.ProcessEnv, requirement?: LaunchContextRequirement, platform?: NodeJS.Platform): LaunchContext;
22
- /** Rebuild owned context without inheriting a prior session's private selection. */
23
- export declare function withLaunchContext(environment: NodeJS.ProcessEnv, context: LaunchContext, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
24
- /** Remove only this implementation's private keys, leaving user and integration settings intact. */
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. */
25
55
  export declare function withoutLaunchContext(environment: NodeJS.ProcessEnv, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
26
- /** Reject unsupported target metadata instead of negotiating or rewriting an older contract. */
56
+ /** Require this build's own contract where nothing older can be accepted, such as recovery capsules. */
27
57
  export declare function assertCurrentLaunchContract(value: {
28
58
  readonly launchContract?: unknown;
29
59
  }): void;
@@ -1,4 +1,4 @@
1
- /** The one supported private process contract; public settings remain product identity data. */
1
+ /** The contract this build emits; supersedes the pre-cutover key set it still accepts. */
2
2
  export const PRIVATE_LAUNCH_CONTRACT = "neutral-launch-v1";
3
3
  export const PRIVATE_ENVIRONMENT = Object.freeze({
4
4
  releaseRoot: "LAUNCH_CONTEXT_RELEASE_ROOT",
@@ -8,18 +8,39 @@ export const PRIVATE_ENVIRONMENT = Object.freeze({
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE",
9
9
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP",
10
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 const SUPERSEDED_ENVIRONMENT = Object.freeze({
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
+ });
11
25
  const PRIVATE_ENTRIES = Object.entries(PRIVATE_ENVIRONMENT);
12
- const PRIVATE_KEYS = new Set(Object.values(PRIVATE_ENVIRONMENT));
26
+ const SUPERSEDED_ENTRIES = Object.entries(SUPERSEDED_ENVIRONMENT);
27
+ const PRIVATE_KEYS = new Set([...Object.values(PRIVATE_ENVIRONMENT), ...Object.values(SUPERSEDED_ENVIRONMENT)]);
13
28
  const LOGICAL_KEYS = new Map(PRIVATE_ENTRIES.map(([logical, key]) => [key, logical]));
14
- /** Read current private keys only, with entry-specific required fields and bounded diagnostics. */
29
+ const SUPERSEDED_LOGICAL_KEYS = new Map(SUPERSEDED_ENTRIES.map(([logical, key]) => [key, logical]));
30
+ /** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
15
31
  export function readLaunchContext(environment, requirement = "optional", platform = process.platform) {
16
32
  const values = {};
33
+ const superseded = {};
17
34
  // Performance: enumerate names only for Windows casing; never fetch unrelated environment values.
18
35
  if (platform === "win32") {
19
36
  for (const name of Object.keys(environment)) {
20
- const logical = LOGICAL_KEYS.get(name.toUpperCase());
37
+ const upper = name.toUpperCase();
38
+ const logical = LOGICAL_KEYS.get(upper);
21
39
  if (logical !== undefined)
22
40
  acceptValue(values, logical, environment[name]);
41
+ const supersededLogical = SUPERSEDED_LOGICAL_KEYS.get(upper);
42
+ if (supersededLogical !== undefined)
43
+ acceptValue(superseded, supersededLogical, environment[name]);
23
44
  }
24
45
  }
25
46
  else {
@@ -27,7 +48,16 @@ export function readLaunchContext(environment, requirement = "optional", platfor
27
48
  if (Object.prototype.propertyIsEnumerable.call(environment, key))
28
49
  acceptValue(values, logical, environment[key]);
29
50
  }
51
+ for (const [logical, key] of SUPERSEDED_ENTRIES) {
52
+ if (Object.prototype.propertyIsEnumerable.call(environment, key))
53
+ acceptValue(superseded, logical, environment[key]);
54
+ }
30
55
  }
56
+ // Compatibility: the current key wins wherever both are present, so a launcher that
57
+ // emits this contract is never overruled by an inherited pre-cutover value.
58
+ for (const [logical, value] of Object.entries(superseded))
59
+ if (values[logical] === undefined)
60
+ values[logical] = value;
31
61
  if (values.launchProfile !== undefined && values.launchProfile !== "a1" && values.launchProfile !== "pi") {
32
62
  throw invalidContext("launchProfile", "unsupported profile");
33
63
  }
@@ -37,16 +67,39 @@ export function readLaunchContext(environment, requirement = "optional", platfor
37
67
  throw invalidContext("releaseDigest", "invalid digest");
38
68
  if (requirement === "profile")
39
69
  requireFields(values, ["launchProfile"]);
40
- if (requirement === "release" || requirement === "warmup")
41
- requireFields(values, ["releaseRoot", "releaseId", "releaseDigest", "releaseLayers"]);
70
+ if (requirement === "release" || requirement === "warmup") {
71
+ requireFields(values, ["releaseRoot", "releaseId", "releaseDigest"]);
72
+ // Compatibility: Windows drops an empty value from a child environment block, so a
73
+ // release with no dependency layer arrives without the key it was given. The
74
+ // layer list is metadata of the release being started, never authority, and an
75
+ // absent list means the same thing the empty string does.
76
+ if (values.releaseLayers === undefined)
77
+ values.releaseLayers = "";
78
+ }
42
79
  if (requirement === "warmup")
43
80
  requireFields(values, ["immutableWarmup"]);
44
81
  return Object.freeze(values);
45
82
  }
46
- /** Rebuild owned context without inheriting a prior session's private selection. */
47
- export function withLaunchContext(environment, context, platform = process.platform) {
83
+ /** The key set a process started for this release must receive. */
84
+ export function launchEnvironmentKeys(contract) {
85
+ return contract === PRIVATE_LAUNCH_CONTRACT ? PRIVATE_ENVIRONMENT : SUPERSEDED_ENVIRONMENT;
86
+ }
87
+ /** Classify metadata by the contract it declares rather than rejecting what it omits. */
88
+ export function launchContractTarget(value) {
89
+ return value.launchContract === PRIVATE_LAUNCH_CONTRACT ? PRIVATE_LAUNCH_CONTRACT : "superseded";
90
+ }
91
+ /** Whether metadata was produced by a build that declares this contract. */
92
+ export function isCurrentLaunchContract(value) {
93
+ return value.launchContract === PRIVATE_LAUNCH_CONTRACT;
94
+ }
95
+ /**
96
+ * Rebuild owned context without inheriting a prior session's private selection. The
97
+ * contract selects which key set is written, so this build can start either its
98
+ * own releases or a retained pre-cutover release it must keep serving.
99
+ */
100
+ export function withLaunchContext(environment, context, platform = process.platform, contract = PRIVATE_LAUNCH_CONTRACT) {
48
101
  const result = withoutLaunchContext(environment, platform);
49
- for (const [logical, key] of PRIVATE_ENTRIES) {
102
+ for (const [logical, key] of Object.entries(launchEnvironmentKeys(contract))) {
50
103
  const value = context[logical];
51
104
  if (value !== undefined)
52
105
  result[key] = value;
@@ -54,11 +107,11 @@ export function withLaunchContext(environment, context, platform = process.platf
54
107
  readLaunchContext(result, "optional", platform);
55
108
  return result;
56
109
  }
57
- /** Remove only this implementation's private keys, leaving user and integration settings intact. */
110
+ /** Remove both key sets, leaving user and integration settings intact. */
58
111
  export function withoutLaunchContext(environment, platform = process.platform) {
59
112
  return Object.fromEntries(Object.entries(environment).filter(([key]) => !PRIVATE_KEYS.has(platform === "win32" ? key.toUpperCase() : key)));
60
113
  }
61
- /** Reject unsupported target metadata instead of negotiating or rewriting an older contract. */
114
+ /** Require this build's own contract where nothing older can be accepted, such as recovery capsules. */
62
115
  export function assertCurrentLaunchContract(value) {
63
116
  if (value.launchContract !== PRIVATE_LAUNCH_CONTRACT) {
64
117
  throw new Error("Unsupported private launch contract. Stop existing processes and install the current package directly with npm; review disposable runtime/release state before any manual reset. User settings, sessions, and history must be preserved.");
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { PRODUCT_TEXT } from "../../product-identity.js";
2
3
  import { readLaunchContext } from "../launch-context/index.js";
3
4
  import { resolve } from "node:path";
4
5
  import { assertLaunchProfileId, sessionSelectionArguments } from "../lifecycle/index.js";
@@ -175,5 +176,5 @@ function exitCode(outcome) {
175
176
  return 1;
176
177
  }
177
178
  function diagnosticError(message, code) {
178
- return Object.assign(new Error(`A1: ${message}`), { code });
179
+ return Object.assign(new Error(`${PRODUCT_TEXT.displayName}: ${message}`), { code });
179
180
  }
@@ -35,7 +35,14 @@ 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>;
41
+ /**
42
+ * Hand a release the key set its own build reads. A retained pre-cutover release is
43
+ * still startable this way, which is what lets a launch fall back to the previous
44
+ * version instead of failing while an installation is being replaced.
45
+ */
39
46
  export declare function releaseEnvironment(environment: NodeJS.ProcessEnv, release: MaterializedRelease, profile?: LaunchProfileId): NodeJS.ProcessEnv;
40
47
  export declare function waitForVerifiedEndpoint(path: string, release: MaterializedRelease, timeoutMs: number, startup?: SupervisorStartupAttempt): Promise<void>;
41
48
  export declare function readEndpointMetadata(path: string): Promise<SupervisorEndpointMetadata | null>;
@@ -10,12 +10,14 @@ 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";
16
18
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
17
19
  import { markStartupPhase } from "../startup/index.js";
18
- import { assertCurrentLaunchContract, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
20
+ import { launchContractTarget, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
19
21
  export async function runBootstrap(options) {
20
22
  const environment = withLaunchContext(options.environment ?? process.env, { launchProfile: options.launchIntent?.profileId ?? "a1" });
21
23
  await markStartupPhase(environment, "bootstrap-start");
@@ -29,9 +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();
32
- const activeRecord = state.references.active === null ? undefined : state.releases[state.references.active];
33
- if (activeRecord)
34
- assertCurrentLaunchContract(activeRecord);
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;
35
49
  // Invariant: records left by cohorts whose processes are gone say nothing about ownership, and there
36
50
  // can now be several of them. Clearing them first keeps the decision below about what is
37
51
  // actually running.
@@ -83,15 +97,30 @@ export async function runBootstrap(options) {
83
97
  return verified;
84
98
  });
85
99
  await markStartupPhase(environment, "durable-validation-complete");
86
- const retainedPaths = resolveCohortEndpoint(paths, retained.releaseId, environment);
87
100
  await markStartupPhase(environment, "replacement-supervisor-start");
88
- const startup = await startSupervisor(retained, environment);
89
- await waitForVerifiedEndpoint(retainedPaths.endpointMetadataPath, retained, 8_000, startup);
101
+ await ensureSupervisor(retained, environment);
90
102
  await markStartupPhase(environment, "replacement-supervisor-ready");
91
103
  return await launchUi(retained, environment, sessionArgs);
92
104
  }
93
105
  }
94
- const candidate = await materializeRelease(options.packageRoot, paths.dataDir);
106
+ // Concurrency: an installation being replaced is not a reason to refuse a launch. While
107
+ // npm is rewriting the package tree the candidate cannot be read consistently, so a
108
+ // launch that cannot take the installed payload starts the retained active release
109
+ // instead and leaves activation to the launch that follows the replacement.
110
+ let candidate;
111
+ try {
112
+ candidate = await materializeRelease(options.packageRoot, paths.dataDir);
113
+ }
114
+ catch (error) {
115
+ const fallback = await launchRetainedActive(state, paths, environment, sessionArgs, output);
116
+ if (fallback === null)
117
+ throw error;
118
+ return fallback;
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;
95
124
  await stateStore.recordCandidate(candidate);
96
125
  state = await stateStore.read();
97
126
  if (!state.references.active) {
@@ -182,10 +211,30 @@ export async function runBootstrap(options) {
182
211
  else {
183
212
  selected = await readMaterializedRelease(decision.releaseRoot);
184
213
  }
185
- const startup = await startSupervisor(selected, environment);
186
- await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, selected.releaseId, environment).endpointMetadataPath, selected, 8_000, startup);
214
+ await ensureSupervisor(selected, environment);
187
215
  return await launchUi(selected, environment, sessionArgs);
188
216
  }
217
+ /**
218
+ * Start the retained active cohort when the installed payload cannot be taken. The
219
+ * release already carries certified evidence, so this reuses it rather than
220
+ * certifying anything new, and reports no selection when there is nothing safe
221
+ * to fall back to.
222
+ */
223
+ async function launchRetainedActive(state, paths, environment, sessionArgs, output) {
224
+ const activeId = state.references.active;
225
+ const active = activeId === null ? undefined : state.releases[activeId];
226
+ if (!active || active.approval !== "approved")
227
+ return null;
228
+ try {
229
+ const retained = await readCertifiedReleaseManifest(active, resolve(paths.dataDir, "releases"));
230
+ await ensureSupervisor(retained, environment);
231
+ output.write(`${PRODUCT_TEXT.diagnostic(`installation is being replaced; starting the retained release ${retained.packageVersion}`)}\n`);
232
+ return await launchUi(retained, environment, sessionArgs);
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ }
189
238
  async function readInstalledVersion(packageRoot) {
190
239
  const manifest = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
191
240
  if (typeof manifest.version !== "string" || manifest.version.length === 0) {
@@ -194,7 +243,6 @@ async function readInstalledVersion(packageRoot) {
194
243
  return manifest.version;
195
244
  }
196
245
  export async function certifyMaterializedRelease(release, dataDir, verification = {}) {
197
- assertCurrentLaunchContract(release);
198
246
  if (!consumeMaterializationProof(release)) {
199
247
  await verifyMaterializedRelease(release.releaseRoot, release, resolve(dataDir, "releases"), verification);
200
248
  }
@@ -202,7 +250,6 @@ export async function certifyMaterializedRelease(release, dataDir, verification
202
250
  }
203
251
  /** Persist current-format evidence after an authenticated parent has certified the exact release. */
204
252
  export async function recordParentCertifiedRelease(release, dataDir) {
205
- assertCurrentLaunchContract(release);
206
253
  const path = resolve(dataDir, `certification-${release.releaseId}.json`);
207
254
  const restartSeal = await createRestartSeal(release, dataDir);
208
255
  await chmod(path, 0o600).catch(() => { });
@@ -210,6 +257,25 @@ export async function recordParentCertifiedRelease(release, dataDir) {
210
257
  await chmod(path, 0o400);
211
258
  return path;
212
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
+ }
213
279
  export async function startSupervisor(release, environment) {
214
280
  const entry = await resolveReleaseEntryPoint(release, "bin/supervisor.js");
215
281
  const paths = resolveProductPaths(environment);
@@ -243,23 +309,28 @@ async function launchUi(release, environment, sessionArgs) {
243
309
  child.once("close", (code, signal) => resolvePromise(restoreAfterOwnedExit(readLaunchContext(environment).launchProfile === "a1", code, signal)));
244
310
  });
245
311
  }
312
+ /**
313
+ * Hand a release the key set its own build reads. A retained pre-cutover release is
314
+ * still startable this way, which is what lets a launch fall back to the previous
315
+ * version instead of failing while an installation is being replaced.
316
+ */
246
317
  export function releaseEnvironment(environment, release, profile) {
247
- assertCurrentLaunchContract(release);
248
318
  return withLaunchContext(environment, {
249
319
  releaseId: release.releaseId,
250
320
  releaseLayers: (release.dependencyLayers ?? []).map(layer => layer.layerId).join(","),
251
321
  releaseRoot: release.releaseRoot,
252
322
  releaseDigest: release.contentDigest,
253
323
  ...(profile === undefined ? {} : { launchProfile: profile }),
254
- });
324
+ }, process.platform, launchContractTarget(release));
255
325
  }
256
326
  export async function waitForVerifiedEndpoint(path, release, timeoutMs, startup) {
257
327
  const deadline = Date.now() + timeoutMs;
258
328
  let childOutcome = null;
329
+ let collision = null;
259
330
  void startup?.childOutcome.then(outcome => { childOutcome = outcome; });
260
331
  while (Date.now() < deadline) {
261
332
  const metadata = await readEndpointMetadata(path);
262
- if (metadata && metadata.releaseId === release.releaseId && await probeOwnership(metadata) === "live-verified") {
333
+ if (metadata && endpointMatchesRelease(metadata, release) && await probeOwnership(metadata) === "live-verified") {
263
334
  if (startup)
264
335
  await rm(startup.resultPath, { force: true });
265
336
  return;
@@ -267,15 +338,21 @@ export async function waitForVerifiedEndpoint(path, release, timeoutMs, startup)
267
338
  if (startup) {
268
339
  const result = await readSupervisorStartupResult(startup.resultPath, startup.attemptId, startup.releaseId);
269
340
  if (result?.outcome === "failure") {
270
- 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}`)), {
271
342
  code: result.code ?? "SUPERVISOR_STARTUP_FAILED",
272
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;
273
348
  }
274
349
  }
275
- if (childOutcome)
350
+ if (childOutcome && !collision)
276
351
  throw new Error(PRODUCT_TEXT.diagnostic(`supervisor exited before readiness: ${JSON.stringify(childOutcome)}`));
277
352
  await new Promise(resolvePromise => setTimeout(resolvePromise, 40));
278
353
  }
354
+ if (collision)
355
+ throw collision;
279
356
  throw new Error(PRODUCT_TEXT.diagnostic(`supervisor did not publish verified endpoint metadata within ${timeoutMs}ms`));
280
357
  }
281
358
  export async function readEndpointMetadata(path) {
@@ -370,8 +447,7 @@ async function activatePendingAfterBlockerExit(candidate, stateStore, paths, env
370
447
  return;
371
448
  await removeEndpointArtifacts(paths.endpointMetadataPath, paths.endpoint);
372
449
  await stateStore.activate(candidate.releaseId);
373
- const startup = await startSupervisor(candidate, environment);
374
- await waitForVerifiedEndpoint(resolveCohortEndpoint(paths, candidate.releaseId, environment).endpointMetadataPath, candidate, 8_000, startup);
450
+ await ensureSupervisor(candidate, environment);
375
451
  }
376
452
  export async function releaseVerifiedIdleOwner(metadata, dataDir, operations = {}) {
377
453
  try {
@@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto";
2
2
  import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
- import { assertCurrentLaunchContract } from "../launch-context/index.js";
6
5
  export const RELEASE_COHORT_SCHEMA = PRODUCT_IDENTITY.protocol.releaseCohortSchema;
7
6
  const RELEASE_ID_PATTERN = /^[0-9A-Za-z.+_-]+-[a-f0-9]{20}$/;
8
7
  /**
@@ -122,7 +121,6 @@ export class CohortStateStore {
122
121
  const release = requiredRelease(current, releaseId);
123
122
  if (release.approval !== "approved")
124
123
  throw new Error(`cannot activate unverified release ${releaseId}`);
125
- assertCurrentLaunchContract(release);
126
124
  const prior = current.references.active;
127
125
  return {
128
126
  ...current,
@@ -150,7 +148,6 @@ export class CohortStateStore {
150
148
  const rollback = requiredRelease(current, rollbackId);
151
149
  if (rollback.approval !== "approved")
152
150
  throw new Error(`cannot roll back to unverified release ${rollbackId}`);
153
- assertCurrentLaunchContract(rollback);
154
151
  return {
155
152
  ...current,
156
153
  references: {
@@ -0,0 +1,7 @@
1
+ export interface DependencyCertificationProtection {
2
+ readonly referenced: Set<string>;
3
+ readonly legacyRequired: Set<string>;
4
+ readonly uncertain: boolean;
5
+ }
6
+ /** Inspect retained and not-yet-collected release metadata, never dependency payload bytes. */
7
+ export declare function dependencyCertificationProtection(dataDir: string, retainedReleaseIds?: readonly string[]): Promise<DependencyCertificationProtection>;
@@ -0,0 +1,88 @@
1
+ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
2
+ import { basename, resolve } from "node:path";
3
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
+ import { dependencyLayerCertificationPath } from "./dependency-certification.js";
5
+ import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
6
+ import { readRestartCertifiedRelease } from "./restart-certification.js";
7
+ /** Inspect retained and not-yet-collected release metadata, never dependency payload bytes. */
8
+ export async function dependencyCertificationProtection(dataDir, retainedReleaseIds = []) {
9
+ const referenced = new Set();
10
+ const legacyRequired = new Set();
11
+ const canonicalData = await realpath(dataDir);
12
+ const releasesRoot = resolve(canonicalData, "releases");
13
+ const roots = [];
14
+ let uncertain = false;
15
+ const rootMetadata = await lstat(releasesRoot);
16
+ if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink())
17
+ return { referenced, legacyRequired, uncertain: true };
18
+ const entries = await readdir(releasesRoot, { withFileTypes: true });
19
+ for (const entry of entries) {
20
+ if (entry.isSymbolicLink()) {
21
+ uncertain = true;
22
+ continue;
23
+ }
24
+ if (!entry.isDirectory() || entry.name.startsWith(".candidate-"))
25
+ continue;
26
+ if (entry.name === ".trash") {
27
+ for (const trash of await readdir(resolve(releasesRoot, entry.name), { withFileTypes: true })) {
28
+ if (trash.isSymbolicLink())
29
+ uncertain = true;
30
+ else if (trash.isDirectory() && !trash.name.startsWith(".candidate"))
31
+ roots.push(resolve(releasesRoot, entry.name, trash.name));
32
+ }
33
+ }
34
+ else
35
+ roots.push(resolve(releasesRoot, entry.name));
36
+ }
37
+ const discoveredIds = new Set(roots.map(root => basename(root).split("--", 1)[0]));
38
+ if (retainedReleaseIds.some(id => !discoveredIds.has(id)))
39
+ uncertain = true;
40
+ for (const root of roots) {
41
+ try {
42
+ const manifestPath = resolve(root, RELEASE_MANIFEST_FILENAME);
43
+ const metadata = await lstat(manifestPath);
44
+ if (!metadata.isFile() || metadata.isSymbolicLink())
45
+ throw new Error("release manifest is not a regular file");
46
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
47
+ if (!manifest || !/^[0-9A-Za-z.+_-]+-[a-f0-9]{20}$/.test(manifest.releaseId)
48
+ || (manifest.dependencyLayers !== undefined && !Array.isArray(manifest.dependencyLayers)))
49
+ throw new Error("invalid retained release manifest");
50
+ const seal = await canonicalConsumerSeal(canonicalData, manifest.releaseId, manifest.contentDigest);
51
+ for (const layer of manifest.dependencyLayers ?? []) {
52
+ if (!layer || !/^dependencies-[a-f0-9]{32}$/.test(layer.layerId))
53
+ throw new Error("invalid retained dependency reference");
54
+ referenced.add(layer.layerId);
55
+ const evidence = seal?.dependencyLayers.filter(item => item.layerId === layer.layerId);
56
+ if (evidence?.length !== 1 || evidence[0].contentDigest !== layer.contentDigest
57
+ || evidence[0].certification?.path !== dependencyLayerCertificationPath(canonicalData, layer.layerId)) {
58
+ // Compatibility: an absent, legacy, or uncertain seal cannot prove canonical-only use.
59
+ legacyRequired.add(layer.layerId);
60
+ }
61
+ }
62
+ }
63
+ catch {
64
+ // Security: an unreadable retained manifest might reference any layer; do not guess ownership.
65
+ uncertain = true;
66
+ }
67
+ }
68
+ return { referenced, legacyRequired, uncertain };
69
+ }
70
+ async function canonicalConsumerSeal(dataDir, releaseId, contentDigest) {
71
+ try {
72
+ const path = resolve(dataDir, `certification-${releaseId}.json`);
73
+ const metadata = await lstat(path);
74
+ if (!metadata.isFile() || metadata.isSymbolicLink() || (metadata.mode & 0o222) !== 0)
75
+ return null;
76
+ const document = JSON.parse(await readFile(path, "utf8"));
77
+ if (document.schema !== PRODUCT_IDENTITY.evidence.releaseCertificationSchema || document.releaseId !== releaseId
78
+ || document.contentDigest !== contentDigest || !document.restartSeal)
79
+ return null;
80
+ // Security: path claims alone do not prove a canonical-only consumer. Reuse the bounded
81
+ // restart validator so stale, unsupported, or tampered seals conservatively retain legacy files.
82
+ await readRestartCertifiedRelease({ releaseId, contentDigest, releaseRoot: resolve(dataDir, "releases", releaseId) }, dataDir);
83
+ return document.restartSeal;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }