@timurproko/a1 0.1.1-dev.1 → 0.1.1-dev.11

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 (176) hide show
  1. package/README.md +8 -43
  2. package/bin/guardian.js +22 -0
  3. package/bin/{a1-ui.js → ui.js} +4 -8
  4. package/dist/native/darwin-arm64/manifest.json +20 -0
  5. package/dist/native/darwin-arm64/process-guardian +0 -0
  6. package/dist/native/linux-x64/manifest.json +20 -0
  7. package/dist/native/linux-x64/process-guardian +0 -0
  8. package/dist/native/win32-x64/manifest.json +20 -0
  9. package/dist/native/win32-x64/process-guardian.exe +0 -0
  10. package/dist/src/composition/index.d.ts +26 -0
  11. package/dist/src/composition/index.js +94 -2
  12. package/dist/src/features/launch/initialize-profile.js +23 -2
  13. package/dist/src/features/launch/profiles.d.ts +1 -1
  14. package/dist/src/features/launch/profiles.js +2 -2
  15. package/dist/src/features/launch/runtime-selection.d.ts +13 -8
  16. package/dist/src/features/launch/runtime-selection.js +6 -6
  17. package/dist/src/features/owned-ui/index.d.ts +1 -0
  18. package/dist/src/features/owned-ui/index.js +1 -0
  19. package/dist/src/features/owned-ui/run.d.ts +6 -0
  20. package/dist/src/features/owned-ui/run.js +3 -1
  21. package/dist/src/features/owned-ui/settings-app.d.ts +22 -0
  22. package/dist/src/features/owned-ui/settings-app.js +633 -0
  23. package/dist/src/foundation/agent-engine-contracts/domain.d.ts +26 -0
  24. package/dist/src/foundation/launch-guardian/index.d.ts +1 -0
  25. package/dist/src/foundation/launch-guardian/index.js +1 -0
  26. package/dist/src/foundation/launch-guardian/main.d.ts +31 -0
  27. package/dist/src/foundation/launch-guardian/main.js +165 -0
  28. package/dist/src/foundation/lifecycle/commands.d.ts +40 -0
  29. package/dist/src/foundation/lifecycle/commands.js +1 -0
  30. package/dist/src/foundation/lifecycle/index.d.ts +2 -0
  31. package/dist/src/foundation/lifecycle/index.js +2 -0
  32. package/dist/src/foundation/lifecycle/launch-instance.d.ts +46 -0
  33. package/dist/src/foundation/lifecycle/launch-instance.js +134 -0
  34. package/dist/src/foundation/lifecycle/model.d.ts +6 -42
  35. package/dist/src/foundation/owned-ui-settings/declarations.d.ts +13 -0
  36. package/dist/src/foundation/owned-ui-settings/declarations.js +39 -0
  37. package/dist/src/foundation/owned-ui-settings/index.d.ts +6 -0
  38. package/dist/src/foundation/owned-ui-settings/index.js +6 -0
  39. package/dist/src/foundation/owned-ui-settings/migrations.d.ts +13 -0
  40. package/dist/src/foundation/owned-ui-settings/migrations.js +28 -0
  41. package/dist/src/foundation/owned-ui-settings/resolution.d.ts +45 -0
  42. package/dist/src/foundation/owned-ui-settings/resolution.js +147 -0
  43. package/dist/src/foundation/owned-ui-settings/sections.d.ts +70 -0
  44. package/dist/src/foundation/owned-ui-settings/sections.js +93 -0
  45. package/dist/src/foundation/owned-ui-settings/session.d.ts +44 -0
  46. package/dist/src/foundation/owned-ui-settings/session.js +145 -0
  47. package/dist/src/foundation/owned-ui-settings/store.d.ts +28 -0
  48. package/dist/src/foundation/owned-ui-settings/store.js +90 -0
  49. package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -0
  50. package/dist/src/foundation/pi-component-adapter/index.js +1 -0
  51. package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.d.ts +9 -1
  52. package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.js +10 -8
  53. package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.d.ts +8 -0
  54. package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.js +12 -0
  55. package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +17 -3
  56. package/dist/src/foundation/pi-engine-adapter/adapter.js +80 -7
  57. package/dist/src/foundation/pi-engine-adapter/pi-settings-metadata.json +374 -0
  58. package/dist/src/foundation/pi-engine-adapter/public-main-entry.js +10 -1
  59. package/dist/src/foundation/pi-engine-adapter/settings-integration.d.ts +40 -2
  60. package/dist/src/foundation/pi-engine-adapter/settings-integration.js +172 -24
  61. package/dist/src/foundation/pi-engine-adapter/workflows.d.ts +9 -0
  62. package/dist/src/foundation/pi-owned-ui-integration/index.d.ts +1 -0
  63. package/dist/src/foundation/pi-owned-ui-integration/index.js +1 -0
  64. package/dist/src/foundation/pi-owned-ui-integration/route-host.d.ts +29 -0
  65. package/dist/src/foundation/pi-owned-ui-integration/route-host.js +1 -0
  66. package/dist/src/foundation/pi-owned-ui-integration/session-shell.d.ts +6 -0
  67. package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +71 -1
  68. package/dist/src/foundation/pi-tui-runtime-adapter/adapter.d.ts +6 -0
  69. package/dist/src/foundation/pi-tui-runtime-adapter/adapter.js +9 -0
  70. package/dist/src/foundation/process-containment/artifact.d.ts +1 -0
  71. package/dist/src/foundation/process-containment/artifact.js +34 -0
  72. package/dist/src/foundation/process-containment/contracts.d.ts +31 -0
  73. package/dist/src/foundation/process-containment/contracts.js +35 -0
  74. package/dist/src/foundation/process-containment/index.d.ts +5 -0
  75. package/dist/src/foundation/process-containment/index.js +5 -0
  76. package/dist/src/foundation/process-containment/linux-process-inspector.d.ts +6 -0
  77. package/dist/src/foundation/process-containment/linux-process-inspector.js +26 -0
  78. package/dist/src/foundation/process-containment/native-guardian-containment.d.ts +16 -0
  79. package/dist/src/foundation/process-containment/native-guardian-containment.js +139 -0
  80. package/dist/src/foundation/process-containment/windows-process-inspector.d.ts +17 -0
  81. package/dist/src/foundation/process-containment/windows-process-inspector.js +53 -0
  82. package/dist/src/foundation/protocol/client.d.ts +2 -1
  83. package/dist/src/foundation/protocol/client.js +3 -0
  84. package/dist/src/foundation/protocol/messages.d.ts +5 -5
  85. package/dist/src/foundation/protocol/messages.js +1 -1
  86. package/dist/src/foundation/release/bootstrap.d.ts +2 -2
  87. package/dist/src/foundation/release/bootstrap.js +69 -18
  88. package/dist/src/foundation/release/cohort-selection.js +3 -3
  89. package/dist/src/foundation/release/cohort-state.d.ts +3 -2
  90. package/dist/src/foundation/release/concurrency.d.ts +1 -0
  91. package/dist/src/foundation/release/concurrency.js +17 -0
  92. package/dist/src/foundation/release/process-cleanup.d.ts +1 -1
  93. package/dist/src/foundation/release/process-cleanup.js +2 -2
  94. package/dist/src/foundation/release/release-store.d.ts +35 -2
  95. package/dist/src/foundation/release/release-store.js +90 -31
  96. package/dist/src/foundation/release/release.d.ts +15 -0
  97. package/dist/src/foundation/release/release.js +65 -36
  98. package/dist/src/foundation/release/update.d.ts +19 -2
  99. package/dist/src/foundation/release/update.js +132 -16
  100. package/dist/src/foundation/storage/control-store.d.ts +8 -7
  101. package/dist/src/foundation/storage/control-store.js +105 -55
  102. package/dist/src/foundation/supervision/main.js +6 -5
  103. package/dist/src/foundation/supervision/server.d.ts +4 -2
  104. package/dist/src/foundation/supervision/server.js +254 -87
  105. package/dist/src/foundation/ui-apps/contracts.d.ts +42 -0
  106. package/dist/src/foundation/ui-apps/contracts.js +12 -0
  107. package/dist/src/foundation/ui-apps/host.d.ts +42 -0
  108. package/dist/src/foundation/ui-apps/host.js +152 -0
  109. package/dist/src/foundation/ui-apps/index.d.ts +3 -0
  110. package/dist/src/foundation/ui-apps/index.js +3 -0
  111. package/dist/src/foundation/ui-apps/registry.d.ts +11 -0
  112. package/dist/src/foundation/ui-apps/registry.js +32 -0
  113. package/dist/src/foundation/ui-components/dialog-panel.d.ts +36 -0
  114. package/dist/src/foundation/ui-components/dialog-panel.js +47 -0
  115. package/dist/src/foundation/ui-components/frame.d.ts +21 -0
  116. package/dist/src/foundation/ui-components/frame.js +54 -0
  117. package/dist/src/foundation/ui-components/index.d.ts +18 -0
  118. package/dist/src/foundation/ui-components/index.js +18 -0
  119. package/dist/src/foundation/ui-components/label.d.ts +8 -0
  120. package/dist/src/foundation/ui-components/label.js +36 -0
  121. package/dist/src/foundation/ui-components/line-input.d.ts +74 -0
  122. package/dist/src/foundation/ui-components/line-input.js +245 -0
  123. package/dist/src/foundation/ui-components/list-block.d.ts +66 -0
  124. package/dist/src/foundation/ui-components/list-block.js +176 -0
  125. package/dist/src/foundation/ui-components/list-view.d.ts +66 -0
  126. package/dist/src/foundation/ui-components/list-view.js +76 -0
  127. package/dist/src/foundation/ui-components/mouse.d.ts +15 -0
  128. package/dist/src/foundation/ui-components/mouse.js +46 -0
  129. package/dist/src/foundation/ui-components/pane.d.ts +33 -0
  130. package/dist/src/foundation/ui-components/pane.js +7 -0
  131. package/dist/src/foundation/ui-components/revision.d.ts +35 -0
  132. package/dist/src/foundation/ui-components/revision.js +79 -0
  133. package/dist/src/foundation/ui-components/scrollbar.d.ts +62 -0
  134. package/dist/src/foundation/ui-components/scrollbar.js +102 -0
  135. package/dist/src/foundation/ui-components/shortcuts.d.ts +57 -0
  136. package/dist/src/foundation/ui-components/shortcuts.js +81 -0
  137. package/dist/src/foundation/ui-components/spans.d.ts +7 -0
  138. package/dist/src/foundation/ui-components/spans.js +59 -0
  139. package/dist/src/foundation/ui-components/status-line.d.ts +16 -0
  140. package/dist/src/foundation/ui-components/status-line.js +14 -0
  141. package/dist/src/foundation/ui-components/stepper.d.ts +24 -0
  142. package/dist/src/foundation/ui-components/stepper.js +33 -0
  143. package/dist/src/foundation/ui-components/surface.d.ts +20 -0
  144. package/dist/src/foundation/ui-components/surface.js +38 -0
  145. package/dist/src/foundation/ui-components/text.d.ts +24 -0
  146. package/dist/src/foundation/ui-components/text.js +123 -0
  147. package/dist/src/foundation/ui-components/theme.d.ts +23 -0
  148. package/dist/src/foundation/ui-components/theme.js +13 -0
  149. package/dist/src/foundation/ui-components/value-menu.d.ts +41 -0
  150. package/dist/src/foundation/ui-components/value-menu.js +33 -0
  151. package/dist/src/product-identity.d.ts +2 -2
  152. package/dist/src/product-identity.js +2 -1
  153. package/dist/src/product-identity.json +8 -5
  154. package/docs/architecture/boundaries.md +31 -13
  155. package/docs/architecture/process-guardian-provenance.md +26 -0
  156. package/docs/architecture/toolchain.md +9 -3
  157. package/docs/architecture/ui-reference-provenance.md +50 -0
  158. package/docs/ci-release-runbook.md +67 -0
  159. package/docs/features/launch-profiles.md +7 -1
  160. package/docs/manual-launch-instance-acceptance.md +66 -0
  161. package/docs/manual-terminal-colour-check.md +55 -0
  162. package/package.json +20 -14
  163. package/dist/src/composition/transparent-runtime.d.ts +0 -6
  164. package/dist/src/composition/transparent-runtime.js +0 -19
  165. package/dist/src/foundation/transparent-terminal/command-resolution.d.ts +0 -13
  166. package/dist/src/foundation/transparent-terminal/command-resolution.js +0 -69
  167. package/dist/src/foundation/transparent-terminal/foreground-broker.d.ts +0 -32
  168. package/dist/src/foundation/transparent-terminal/foreground-broker.js +0 -87
  169. package/dist/src/foundation/transparent-terminal/index.d.ts +0 -4
  170. package/dist/src/foundation/transparent-terminal/index.js +0 -4
  171. package/dist/src/foundation/transparent-terminal/main.d.ts +0 -9
  172. package/dist/src/foundation/transparent-terminal/main.js +0 -94
  173. package/dist/src/foundation/transparent-terminal/native-launcher.d.ts +0 -25
  174. package/dist/src/foundation/transparent-terminal/native-launcher.js +0 -146
  175. /package/bin/{a1.js → cli.js} +0 -0
  176. /package/bin/{a1-supervisor.js → supervisor.js} +0 -0
@@ -40,7 +40,7 @@ export function createNpmProcessRunner(platform = process.platform) {
40
40
  } });
41
41
  });
42
42
  }
43
- export function createUpdateLifecycleCoordinator(environment = process.env, fileSystem = defaultFileSystem) {
43
+ export function createUpdateLifecycleCoordinator(environment = process.env, fileSystem = defaultFileSystem, output = defaultOutput) {
44
44
  const paths = resolveProductPaths(environment);
45
45
  const stateStore = new CohortStateStore(paths.dataDir);
46
46
  return {
@@ -71,7 +71,7 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
71
71
  // Explicit update consent permits bounded cleanup of an authenticated
72
72
  // older owner that predates the update-shutdown message.
73
73
  const cleanup = await cleanupVerifiedOwner(endpoint, {
74
- allowLiveGenerations: true,
74
+ allowLiveInstances: true,
75
75
  reason: legacyMutableInstall ? "legacy-mutable-install" : "explicit-update",
76
76
  });
77
77
  if (!cleanup.terminated)
@@ -80,7 +80,7 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
80
80
  else if (identity.accepted) {
81
81
  await waitForProcessExit(endpoint.pid, 3_000).catch(async () => {
82
82
  const cleanup = await cleanupVerifiedOwner(endpoint, {
83
- allowLiveGenerations: true,
83
+ allowLiveInstances: true,
84
84
  reason: legacyMutableInstall ? "legacy-mutable-install" : "explicit-update",
85
85
  });
86
86
  if (!cleanup.terminated)
@@ -121,15 +121,91 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
121
121
  },
122
122
  };
123
123
  }
124
+ const PROGRESS_BAR_WIDTH = 39;
125
+ const PROGRESS_TICK_MS = 200;
126
+ const ACTIVATION_PROGRESS = {
127
+ materialized: { at: 85, creepTo: 90 },
128
+ certified: { at: 90, creepTo: 95 },
129
+ "active-reference-committed": { at: 95, creepTo: 99 },
130
+ };
131
+ function renderProgressBar(percent) {
132
+ const bounded = Math.min(100, Math.max(0, Math.round(percent)));
133
+ const filled = Math.round((bounded / 100) * PROGRESS_BAR_WIDTH);
134
+ return `${"█".repeat(filled)}${"░".repeat(PROGRESS_BAR_WIDTH - filled)} ${bounded}%`;
135
+ }
136
+ function createUpdateProgress(output, enabled) {
137
+ let visible = false;
138
+ let current = 0;
139
+ let shown = -1;
140
+ let timer = null;
141
+ const draw = () => {
142
+ const rounded = Math.round(current);
143
+ if (rounded === shown)
144
+ return;
145
+ shown = rounded;
146
+ visible = true;
147
+ output.stdout(`\r${renderProgressBar(rounded)}`);
148
+ };
149
+ const stopCreep = () => {
150
+ if (timer !== null) {
151
+ clearInterval(timer);
152
+ timer = null;
153
+ }
154
+ };
155
+ return {
156
+ set(percent, creepTo = percent) {
157
+ if (!enabled)
158
+ return;
159
+ stopCreep();
160
+ current = Math.max(current, percent);
161
+ shown = -1;
162
+ draw();
163
+ if (creepTo <= current)
164
+ return;
165
+ // Creep asymptotically toward (but never reach) the next milestone so
166
+ // long opaque phases such as npm install still show visible motion.
167
+ timer = setInterval(() => {
168
+ current = Math.min(creepTo - 0.5, current + (creepTo - current) * 0.04);
169
+ draw();
170
+ }, PROGRESS_TICK_MS);
171
+ timer.unref?.();
172
+ },
173
+ finish() {
174
+ stopCreep();
175
+ if (!visible)
176
+ return;
177
+ output.stdout(`\r${renderProgressBar(100)}\n`);
178
+ visible = false;
179
+ },
180
+ clear() {
181
+ stopCreep();
182
+ if (!visible)
183
+ return;
184
+ output.stdout(`\r${" ".repeat(PROGRESS_BAR_WIDTH + 6)}\r`);
185
+ visible = false;
186
+ shown = -1;
187
+ },
188
+ };
189
+ }
124
190
  export async function runSelfUpdate(options) {
125
191
  const fileSystem = options.fileSystem ?? defaultFileSystem;
126
192
  const output = options.output ?? defaultOutput;
127
193
  const runner = options.runner ?? createNpmProcessRunner();
128
194
  const channel = options.channel ?? "stable";
129
195
  const distTag = UPDATE_DIST_TAGS[channel];
196
+ const now = options.now ?? (() => performance.now());
197
+ const measure = async (phase, operation) => {
198
+ const startedAt = now();
199
+ try {
200
+ return await operation();
201
+ }
202
+ finally {
203
+ options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - startedAt) });
204
+ }
205
+ };
130
206
  let runningVersion;
131
207
  try {
132
- const packageJson = JSON.parse(await fileSystem.readFile(resolve(options.packageRoot, "package.json")));
208
+ const packageJson = JSON.parse(await measure("package-version", async () => await fileSystem.readFile(resolve(options.packageRoot, "package.json"))));
133
209
  const parsedVersion = typeof packageJson.version === "string" ? validSemver(packageJson.version) : null;
134
210
  if (parsedVersion === null)
135
211
  throw new Error("package.json does not contain a valid semantic version");
@@ -139,7 +215,7 @@ export async function runSelfUpdate(options) {
139
215
  output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
140
216
  return 1;
141
217
  }
142
- const targetLookup = await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${distTag} channel`);
218
+ const targetLookup = await measure("target-resolution", async () => await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${distTag} channel`));
143
219
  if (targetLookup.result === null)
144
220
  return targetLookup.exitCode;
145
221
  const targetVersion = validSemver(targetLookup.result.stdout.trim());
@@ -147,8 +223,9 @@ export async function runSelfUpdate(options) {
147
223
  output.stderr(`${PRODUCT_TEXT.diagnostic(`received a malformed ${distTag} version from npm: ${JSON.stringify(targetLookup.result.stdout.trim())}.`)}\n`);
148
224
  return 1;
149
225
  }
150
- output.stdout(`${PRODUCT_TEXT.diagnostic(`update (${channel}): ${runningVersion} → ${targetVersion}.`)}\n`);
151
- const rootLookup = await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root");
226
+ output.stdout(`${PRODUCT_TEXT.commandName} update (${channel}): ${runningVersion} → ${targetVersion}.\n`);
227
+ const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
228
+ const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
152
229
  if (rootLookup.result === null)
153
230
  return rootLookup.exitCode;
154
231
  if (rootLookup.result.stdout.trim().length === 0) {
@@ -170,7 +247,7 @@ export async function runSelfUpdate(options) {
170
247
  }
171
248
  const environment = options.environment ?? process.env;
172
249
  const paths = resolveProductPaths(environment);
173
- const lifecycle = options.lifecycle ?? createUpdateLifecycleCoordinator(environment, fileSystem);
250
+ const lifecycle = options.lifecycle ?? createUpdateLifecycleCoordinator(environment, fileSystem, output);
174
251
  const transactionStore = options.transactionStore ?? new UpdateTransactionStore(paths.dataDir);
175
252
  let transaction = await transactionStore.read();
176
253
  try {
@@ -180,9 +257,11 @@ export async function runSelfUpdate(options) {
180
257
  await transactionStore.finish("completed");
181
258
  }
182
259
  await transactionStore.clearCompleted();
183
- output.stdout(`${PRODUCT_TEXT.diagnostic("is already current and active for this channel; no installation was changed.")}\n`);
260
+ output.stdout(`${PRODUCT_TEXT.commandName} is up to date no update needed.\n`);
184
261
  return 0;
185
262
  }
263
+ // The bar first appears here so a no-change run never flashes it.
264
+ progress.set(3, 15);
186
265
  const cohortState = await new CohortStateStore(paths.dataDir).read();
187
266
  transaction = await transactionStore.begin({
188
267
  channel,
@@ -191,27 +270,49 @@ export async function runSelfUpdate(options) {
191
270
  priorActiveReleaseId: cohortState.references.active,
192
271
  });
193
272
  if (phaseBefore(transaction.phase, "ownership-released")) {
194
- await lifecycle.shutdownVerifiedOwners(targetVersion);
195
- await lifecycle.verifyPackageUnlocked(packageRoot);
273
+ await measure("ownership-release", async () => {
274
+ await lifecycle.shutdownVerifiedOwners(targetVersion);
275
+ await lifecycle.verifyPackageUnlocked(packageRoot);
276
+ });
196
277
  transaction = await transactionStore.advance("ownership-released");
197
278
  }
279
+ progress.set(15, 70);
198
280
  if (phaseBefore(transaction.phase, "package-installed")) {
199
- output.stdout(`${PRODUCT_TEXT.diagnostic(`is installing ${PRODUCT_TEXT.packageName}@${targetVersion}.`)}\n`);
200
- const installation = await runNpm(runner, ["install", "--global", `${PRODUCT_PACKAGE}@${targetVersion}`], false, output, "start the global npm installation", false);
281
+ const installation = await measure("npm-install", async () => await runNpm(runner, ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${PRODUCT_PACKAGE}@${targetVersion}`], true, output, "start the global npm installation", false));
201
282
  if (installation.result === null)
202
283
  throw new UpdateFailure(installation.exitCode, "npm process failed");
203
- if (installation.result.code !== 0)
284
+ if (installation.result.code !== 0) {
285
+ if (installation.result.stdout.trim().length > 0)
286
+ output.stderr(`${installation.result.stdout.trimEnd()}\n`);
204
287
  throw new UpdateFailure(unsuccessfulCode(installation.result.code), `npm exited with status ${formatExitCode(installation.result.code)}`);
288
+ }
205
289
  transaction = await transactionStore.advance("package-installed");
206
290
  }
207
- await lifecycle.activateInstalled(packageRoot, targetVersion, async (phase) => { transaction = await transactionStore.advance(phase); });
291
+ progress.set(70, 75);
292
+ // Ownership can be reacquired after an interrupted installation (for
293
+ // example, if bare A1 is launched before the update is resumed). Recheck
294
+ // immediately before activation so recovery cannot start a second cohort.
295
+ await measure("ownership-release", async () => { await lifecycle.shutdownVerifiedOwners(targetVersion); });
296
+ progress.set(75, 85);
297
+ let activationPhaseStartedAt = now();
298
+ await lifecycle.activateInstalled(packageRoot, targetVersion, async (phase) => {
299
+ options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - activationPhaseStartedAt) });
300
+ transaction = await transactionStore.advance(phase);
301
+ progress.set(ACTIVATION_PROGRESS[phase].at, ACTIVATION_PROGRESS[phase].creepTo);
302
+ activationPhaseStartedAt = now();
303
+ });
304
+ options.onPhaseTiming?.({ phase: "supervisor-verified", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
305
+ const transactionStartedAt = now();
208
306
  await transactionStore.advance("supervisor-verified");
209
307
  await transactionStore.finish("completed");
210
308
  await transactionStore.clearCompleted();
211
- output.stdout(`${PRODUCT_TEXT.diagnostic(`updated successfully: ${targetVersion} (${channel}).`)}\n`);
309
+ options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
310
+ progress.finish();
311
+ output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion} (${channel}).\n`);
212
312
  return 0;
213
313
  }
214
314
  catch (error) {
315
+ progress.clear();
215
316
  const message = errorMessage(error);
216
317
  const rollback = options.lifecycle
217
318
  ? "previous test lifecycle retained"
@@ -222,6 +323,21 @@ export async function runSelfUpdate(options) {
222
323
  return error instanceof UpdateFailure ? error.exitCode : 1;
223
324
  }
224
325
  }
326
+ export function assertUpdatePerformanceBudget(evidence, maximumPostNpmDurationMs = 30_000) {
327
+ const failures = [];
328
+ if (evidence.fileCount < 1)
329
+ failures.push("fixture contains no payload files");
330
+ if (evidence.sourceReads !== evidence.fileCount)
331
+ failures.push(`source payload read count is ${evidence.sourceReads} for ${evidence.fileCount} files`);
332
+ if (evidence.candidateWrites !== evidence.fileCount)
333
+ failures.push(`candidate payload write count is ${evidence.candidateWrites} for ${evidence.fileCount} files`);
334
+ if (evidence.verificationReads > 0)
335
+ failures.push(`fresh certification reread ${evidence.verificationReads} candidate files`);
336
+ if (evidence.postNpmDurationMs > maximumPostNpmDurationMs)
337
+ failures.push(`post-npm activation took ${Math.round(evidence.postNpmDurationMs)}ms; budget is ${maximumPostNpmDurationMs}ms`);
338
+ if (failures.length > 0)
339
+ throw new Error(`update performance budget failed: ${failures.join("; ")}`);
340
+ }
225
341
  async function requestUpdateShutdown(metadata, targetVersion, timeoutMs) {
226
342
  if (!processIsAlive(metadata.pid))
227
343
  return { accepted: false, reason: "recorded owner is dead" };
@@ -1,6 +1,6 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
2
  import type { ManagedAgentDescriptor, PaneId, AgentRecoveryAuthority, RecoveryReferenceId, TerminalSessionLaunch, TerminalTopologySnapshot } from "../workspace-contracts/index.js";
3
- import type { ForegroundTerminalLease, ForegroundTerminalLeaseId, GenerationId, NativeProcessIdentity, TransparentTerminalLifecycleOutcome } from "../lifecycle/index.js";
3
+ import type { LaunchInstance, LaunchInstanceId, LaunchInstanceOutcome, NativeProcessIdentity, ProcessContainmentIdentity } from "../lifecycle/index.js";
4
4
  export declare const DEFAULT_WORKSPACE_ID = "workspace-default";
5
5
  export interface WorkspaceAgentPresentation {
6
6
  readonly unreadActivity: number;
@@ -21,12 +21,13 @@ export declare class ControlStore {
21
21
  constructor(path: string, bootNonce?: string | null);
22
22
  migrate(): void;
23
23
  reconcilePriorBootGenerations(bootNonce: string): number;
24
- acquireForegroundTerminalLease(lease: ForegroundTerminalLease): void;
25
- activateForegroundTerminalLease(leaseId: ForegroundTerminalLeaseId, generationId: GenerationId, processIdentity: NativeProcessIdentity, heartbeatAt: string): boolean;
26
- heartbeatForegroundTerminalLease(leaseId: ForegroundTerminalLeaseId, processIdentity: NativeProcessIdentity, heartbeatAt: string): boolean;
27
- releaseForegroundTerminalLease(leaseId: ForegroundTerminalLeaseId, processIdentity: NativeProcessIdentity | null, outcome: TransparentTerminalLifecycleOutcome, releasedAt: string): boolean;
28
- loadLiveForegroundTerminalLease(): ForegroundTerminalLease | null;
29
- reconcilePriorBootForegroundTerminalLeases(bootNonce: string, reconciledAt?: string): number;
24
+ createLaunchInstance(instance: LaunchInstance): void;
25
+ activateLaunchInstance(instanceId: LaunchInstanceId, ownerClientId: string, rootIdentity: NativeProcessIdentity, containmentIdentity: ProcessContainmentIdentity, activatedAt: string): boolean;
26
+ beginLaunchInstanceStop(instanceId: LaunchInstanceId, ownerClientId: string, stoppingAt: string): boolean;
27
+ completeLaunchInstance(instanceId: LaunchInstanceId, ownerClientId: string, terminalState: "completed" | "interrupted", outcome: LaunchInstanceOutcome, completedAt: string): boolean;
28
+ loadActiveLaunchInstances(): LaunchInstance[];
29
+ loadLaunchInstance(instanceId: LaunchInstanceId): LaunchInstance | null;
30
+ reconcilePriorBootLaunchInstances(bootNonce: string, reconciledAt?: string): number;
30
31
  persistWorkspaceAgent(agent: ManagedAgentDescriptor, updatedAt?: string): void;
31
32
  persistWorkspaceAgentRecord(record: StoredWorkspaceAgent, updatedAt?: string): void;
32
33
  replaceWorkspaceAgentRecords(records: readonly StoredWorkspaceAgent[], selectedAgentId: string | null, revision: number, updatedAt?: string): void;
@@ -3,6 +3,7 @@ import { mkdirSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
4
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
5
  import { assertManagedAgentDescriptor, assertRecoveryAuthority, assertTerminalSessionLaunch, assertTerminalTopologySnapshot, } from "../workspace-contracts/index.js";
6
+ import { assertLaunchInstance, assertLaunchInstanceOutcome, assertNativeProcessIdentity, assertProcessContainmentIdentity, } from "../lifecycle/index.js";
6
7
  export const DEFAULT_WORKSPACE_ID = "workspace-default";
7
8
  const INITIAL_WORKSPACE_ID = DEFAULT_WORKSPACE_ID;
8
9
  export class ControlStore {
@@ -18,7 +19,7 @@ export class ControlStore {
18
19
  this.migrate();
19
20
  if (bootNonce !== null) {
20
21
  this.reconcilePriorBootGenerations(bootNonce);
21
- this.reconcilePriorBootForegroundTerminalLeases(bootNonce);
22
+ this.reconcilePriorBootLaunchInstances(bootNonce);
22
23
  }
23
24
  }
24
25
  catch (error) {
@@ -29,8 +30,8 @@ export class ControlStore {
29
30
  migrate() {
30
31
  const versionRow = this.database.prepare("PRAGMA user_version").get();
31
32
  const version = versionRow.user_version;
32
- if (version > 4)
33
- throw new Error(`control database version ${version} is newer than supported version 4`);
33
+ if (version > 5)
34
+ throw new Error(`control database version ${version} is newer than supported version 5`);
34
35
  if (version !== 0)
35
36
  this.#assertCurrentControlSchema();
36
37
  if (version === 0) {
@@ -172,6 +173,41 @@ export class ControlStore {
172
173
  ALTER TABLE workspaces ADD COLUMN revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0);
173
174
  PRAGMA user_version = 4;
174
175
  COMMIT;
176
+ `);
177
+ }
178
+ if (version <= 4) {
179
+ this.database.exec(`
180
+ BEGIN IMMEDIATE;
181
+ UPDATE foreground_terminal_leases
182
+ SET state = 'interrupted',
183
+ released_at = COALESCE(released_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
184
+ outcome_json = '{"kind":"interrupted","reason":"legacy-migration","message":"legacy foreground ownership cannot prove current launch-instance liveness"}'
185
+ WHERE state IN ('requested', 'active');
186
+ DROP INDEX IF EXISTS idx_one_live_foreground_lease;
187
+ CREATE TABLE launch_instances (
188
+ id TEXT PRIMARY KEY,
189
+ owner_client_id TEXT NOT NULL,
190
+ profile_id TEXT NOT NULL CHECK (profile_id IN ('a1', 'pi', 'sandbox')),
191
+ state TEXT NOT NULL CHECK (state IN ('requested', 'active', 'stopping', 'completed', 'interrupted')),
192
+ shutdown_policy TEXT NOT NULL CHECK (shutdown_policy = 'terminate-tree-on-close'),
193
+ guardian_identity_json TEXT NOT NULL CHECK (json_valid(guardian_identity_json)),
194
+ root_identity_json TEXT CHECK (root_identity_json IS NULL OR json_valid(root_identity_json)),
195
+ containment_identity_json TEXT CHECK (containment_identity_json IS NULL OR json_valid(containment_identity_json)),
196
+ created_at TEXT NOT NULL,
197
+ activated_at TEXT,
198
+ stopping_at TEXT,
199
+ completed_at TEXT,
200
+ outcome_json TEXT CHECK (outcome_json IS NULL OR json_valid(outcome_json)),
201
+ owner_boot_nonce TEXT NOT NULL,
202
+ CHECK ((state = 'requested' AND root_identity_json IS NULL AND containment_identity_json IS NULL AND activated_at IS NULL AND stopping_at IS NULL AND completed_at IS NULL AND outcome_json IS NULL)
203
+ OR (state = 'active' AND root_identity_json IS NOT NULL AND containment_identity_json IS NOT NULL AND activated_at IS NOT NULL AND stopping_at IS NULL AND completed_at IS NULL AND outcome_json IS NULL)
204
+ OR (state = 'stopping' AND root_identity_json IS NOT NULL AND containment_identity_json IS NOT NULL AND activated_at IS NOT NULL AND stopping_at IS NOT NULL AND completed_at IS NULL AND outcome_json IS NULL)
205
+ OR (state IN ('completed', 'interrupted') AND completed_at IS NOT NULL AND outcome_json IS NOT NULL))
206
+ );
207
+ CREATE INDEX idx_launch_instances_owner ON launch_instances(owner_client_id, state);
208
+ CREATE INDEX idx_launch_instances_boot ON launch_instances(owner_boot_nonce, state);
209
+ PRAGMA user_version = 5;
210
+ COMMIT;
175
211
  `);
176
212
  }
177
213
  if (version === 0) {
@@ -199,61 +235,70 @@ export class ControlStore {
199
235
  return Number(result.changes);
200
236
  });
201
237
  }
202
- acquireForegroundTerminalLease(lease) {
238
+ createLaunchInstance(instance) {
203
239
  if (this.bootNonce === null)
204
- throw new Error("foreground lease storage requires a supervisor boot identity");
205
- if (lease.state !== "requested" || lease.generationId !== null || lease.processIdentity !== null || lease.releasedAt !== null || lease.outcome !== null) {
206
- throw new Error("new foreground lease must be requested and unactivated");
207
- }
208
- try {
209
- this.database.prepare(`INSERT INTO foreground_terminal_leases
210
- (id, owner_id, profile_json, state, generation_id, process_identity_json, acquired_at, heartbeat_at, released_at, outcome_json, owner_boot_nonce)
211
- VALUES (?, ?, ?, 'requested', NULL, NULL, ?, NULL, NULL, NULL, ?)`)
212
- .run(lease.id, lease.ownerId, JSON.stringify(lease.profile), lease.acquiredAt, this.bootNonce);
213
- }
214
- catch (error) {
215
- if (error instanceof Error && /UNIQUE constraint failed/i.test(error.message))
216
- throw new Error("an exclusive foreground terminal lease is already live");
217
- throw error;
218
- }
240
+ throw new Error("launch instance storage requires a supervisor boot identity");
241
+ assertLaunchInstance(instance);
242
+ if (instance.state !== "requested")
243
+ throw new Error("new launch instance must be requested");
244
+ this.database.prepare(`INSERT INTO launch_instances
245
+ (id, owner_client_id, profile_id, state, shutdown_policy, guardian_identity_json, root_identity_json, containment_identity_json,
246
+ created_at, activated_at, stopping_at, completed_at, outcome_json, owner_boot_nonce)
247
+ VALUES (?, ?, ?, 'requested', ?, ?, NULL, NULL, ?, NULL, NULL, NULL, NULL, ?)`)
248
+ .run(instance.id, instance.ownerClientId, instance.profileId, instance.shutdownPolicy, JSON.stringify(instance.guardianIdentity), instance.createdAt, this.bootNonce);
219
249
  }
220
- activateForegroundTerminalLease(leaseId, generationId, processIdentity, heartbeatAt) {
250
+ activateLaunchInstance(instanceId, ownerClientId, rootIdentity, containmentIdentity, activatedAt) {
221
251
  if (this.bootNonce === null)
222
- throw new Error("foreground lease storage requires a supervisor boot identity");
223
- const result = this.database.prepare(`UPDATE foreground_terminal_leases
224
- SET state = 'active', generation_id = ?, process_identity_json = ?, heartbeat_at = ?
225
- WHERE id = ? AND state = 'requested' AND owner_boot_nonce = ?`)
226
- .run(generationId, JSON.stringify(processIdentity), heartbeatAt, leaseId, this.bootNonce);
252
+ throw new Error("launch instance storage requires a supervisor boot identity");
253
+ assertNativeProcessIdentity(rootIdentity);
254
+ assertProcessContainmentIdentity(containmentIdentity);
255
+ const result = this.database.prepare(`UPDATE launch_instances
256
+ SET state = 'active', root_identity_json = ?, containment_identity_json = ?, activated_at = ?
257
+ WHERE id = ? AND owner_client_id = ? AND owner_boot_nonce = ? AND state = 'requested'`)
258
+ .run(JSON.stringify(rootIdentity), JSON.stringify(containmentIdentity), activatedAt, instanceId, ownerClientId, this.bootNonce);
227
259
  return Number(result.changes) === 1;
228
260
  }
229
- heartbeatForegroundTerminalLease(leaseId, processIdentity, heartbeatAt) {
261
+ beginLaunchInstanceStop(instanceId, ownerClientId, stoppingAt) {
230
262
  if (this.bootNonce === null)
231
- throw new Error("foreground lease storage requires a supervisor boot identity");
232
- const result = this.database.prepare(`UPDATE foreground_terminal_leases SET heartbeat_at = ?
233
- WHERE id = ? AND state = 'active' AND owner_boot_nonce = ? AND process_identity_json = ?`)
234
- .run(heartbeatAt, leaseId, this.bootNonce, JSON.stringify(processIdentity));
263
+ throw new Error("launch instance storage requires a supervisor boot identity");
264
+ const result = this.database.prepare(`UPDATE launch_instances SET state = 'stopping', stopping_at = ?
265
+ WHERE id = ? AND owner_client_id = ? AND owner_boot_nonce = ? AND state = 'active'`)
266
+ .run(stoppingAt, instanceId, ownerClientId, this.bootNonce);
235
267
  return Number(result.changes) === 1;
236
268
  }
237
- releaseForegroundTerminalLease(leaseId, processIdentity, outcome, releasedAt) {
269
+ completeLaunchInstance(instanceId, ownerClientId, terminalState, outcome, completedAt) {
238
270
  if (this.bootNonce === null)
239
- throw new Error("foreground lease storage requires a supervisor boot identity");
240
- const expectedIdentity = processIdentity === null ? null : JSON.stringify(processIdentity);
241
- const result = this.database.prepare(`UPDATE foreground_terminal_leases
242
- SET state = 'released', released_at = ?, outcome_json = ?
243
- WHERE id = ? AND state IN ('requested', 'active') AND owner_boot_nonce = ?
244
- AND ((process_identity_json IS NULL AND ? IS NULL) OR process_identity_json = ?)`)
245
- .run(releasedAt, JSON.stringify(outcome), leaseId, this.bootNonce, expectedIdentity, expectedIdentity);
271
+ throw new Error("launch instance storage requires a supervisor boot identity");
272
+ assertLaunchInstanceOutcome(outcome);
273
+ if (terminalState === "completed" && outcome.kind === "interrupted")
274
+ throw new Error("completed launch instance cannot carry an interrupted outcome");
275
+ if (terminalState === "interrupted" && outcome.kind !== "interrupted" && outcome.kind !== "cleanup-error") {
276
+ throw new Error("interrupted launch instance requires an interrupted or cleanup-error outcome");
277
+ }
278
+ const result = this.database.prepare(`UPDATE launch_instances
279
+ SET state = ?, completed_at = ?, outcome_json = ?
280
+ WHERE id = ? AND owner_client_id = ? AND owner_boot_nonce = ? AND state IN ('requested', 'active', 'stopping')`)
281
+ .run(terminalState, completedAt, JSON.stringify(outcome), instanceId, ownerClientId, this.bootNonce);
246
282
  return Number(result.changes) === 1;
247
283
  }
248
- loadLiveForegroundTerminalLease() {
249
- const row = this.database.prepare("SELECT * FROM foreground_terminal_leases WHERE state IN ('requested', 'active') LIMIT 1").get();
250
- return row ? foregroundLeaseFromRow(row) : null;
284
+ loadActiveLaunchInstances() {
285
+ return this.database.prepare(`SELECT * FROM launch_instances
286
+ WHERE state IN ('requested', 'active', 'stopping') ORDER BY created_at, id`).all()
287
+ .map(launchInstanceFromRow);
251
288
  }
252
- reconcilePriorBootForegroundTerminalLeases(bootNonce, reconciledAt = new Date().toISOString()) {
253
- const outcome = { kind: "broker-error", message: "foreground lease owner ended before a terminal outcome", code: "stale-owner-boot" };
254
- const result = this.database.prepare(`UPDATE foreground_terminal_leases
255
- SET state = 'released', released_at = ?, outcome_json = ?
256
- WHERE state IN ('requested', 'active') AND owner_boot_nonce <> ?`)
289
+ loadLaunchInstance(instanceId) {
290
+ const row = this.database.prepare("SELECT * FROM launch_instances WHERE id = ?").get(instanceId);
291
+ return row ? launchInstanceFromRow(row) : null;
292
+ }
293
+ reconcilePriorBootLaunchInstances(bootNonce, reconciledAt = new Date().toISOString()) {
294
+ const outcome = {
295
+ kind: "interrupted",
296
+ reason: "supervisor-disconnect",
297
+ message: "launch instance ownership ended with a prior supervisor boot",
298
+ };
299
+ const result = this.database.prepare(`UPDATE launch_instances
300
+ SET state = 'interrupted', completed_at = ?, outcome_json = ?
301
+ WHERE state IN ('requested', 'active', 'stopping') AND owner_boot_nonce <> ?`)
257
302
  .run(reconciledAt, JSON.stringify(outcome), bootNonce);
258
303
  return Number(result.changes);
259
304
  }
@@ -464,17 +509,22 @@ function assertWorkspaceAgentPresentation(presentation) {
464
509
  }
465
510
  }
466
511
  }
467
- function foregroundLeaseFromRow(row) {
468
- return {
512
+ function launchInstanceFromRow(row) {
513
+ const instance = {
469
514
  id: row.id,
470
- ownerId: row.owner_id,
471
- profile: JSON.parse(row.profile_json),
515
+ ownerClientId: row.owner_client_id,
516
+ profileId: row.profile_id,
472
517
  state: row.state,
473
- generationId: row.generation_id,
474
- processIdentity: row.process_identity_json ? JSON.parse(row.process_identity_json) : null,
475
- acquiredAt: row.acquired_at,
476
- heartbeatAt: row.heartbeat_at,
477
- releasedAt: row.released_at,
518
+ shutdownPolicy: row.shutdown_policy,
519
+ guardianIdentity: JSON.parse(row.guardian_identity_json),
520
+ rootIdentity: row.root_identity_json ? JSON.parse(row.root_identity_json) : null,
521
+ containmentIdentity: row.containment_identity_json ? JSON.parse(row.containment_identity_json) : null,
522
+ createdAt: row.created_at,
523
+ activatedAt: row.activated_at,
524
+ stoppingAt: row.stopping_at,
525
+ completedAt: row.completed_at,
478
526
  outcome: row.outcome_json ? JSON.parse(row.outcome_json) : null,
479
527
  };
528
+ assertLaunchInstance(instance);
529
+ return instance;
480
530
  }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { appendFileSync, mkdirSync } from "node:fs";
3
- import { assertImmutableExecutionRoot, readMaterializedRelease } from "../release/index.js";
3
+ import { resolve } from "node:path";
4
+ import { assertImmutableExecutionRoot, readCertifiedReleaseManifest } from "../release/index.js";
4
5
  import { ControlStore } from "../storage/index.js";
5
6
  import { resolveProductPaths } from "./paths.js";
6
7
  import { SupervisorServer } from "./server.js";
@@ -10,11 +11,11 @@ export async function runSupervisor() {
10
11
  mkdirSync(paths.runtimeDir, { recursive: true, mode: 0o700 });
11
12
  const log = (message) => appendFileSync(paths.supervisorLogPath, `${new Date().toISOString()} ${message}\n`);
12
13
  const releaseRoot = process.env[PRODUCT_IDENTITY.environment.releaseRoot];
13
- if (!releaseRoot)
14
+ const releaseId = process.env[PRODUCT_IDENTITY.environment.releaseId];
15
+ const contentDigest = process.env[PRODUCT_IDENTITY.environment.releaseDigest];
16
+ if (!releaseRoot || !releaseId || !contentDigest)
14
17
  throw new Error(PRODUCT_TEXT.diagnostic("supervisor must be launched from a verified immutable release"));
15
- const release = await readMaterializedRelease(releaseRoot);
16
- if (process.env[PRODUCT_IDENTITY.environment.releaseId] !== release.releaseId)
17
- throw new Error(PRODUCT_TEXT.diagnostic("selected release identity does not match its verified manifest"));
18
+ const release = await readCertifiedReleaseManifest({ releaseRoot, releaseId, contentDigest }, resolve(paths.dataDir, "releases"));
18
19
  await assertImmutableExecutionRoot(release, paths.dataDir);
19
20
  const bootNonce = randomUUID();
20
21
  const store = new ControlStore(paths.databasePath, bootNonce);
@@ -1,5 +1,5 @@
1
1
  import { type SupervisorSnapshot } from "../lifecycle/index.js";
2
- import type { MaterializedRelease } from "../release/index.js";
2
+ import { type MaterializedRelease } from "../release/index.js";
3
3
  import { ControlStore } from "../storage/index.js";
4
4
  import { type ProductPaths } from "./paths.js";
5
5
  export declare class SupervisorServer {
@@ -7,12 +7,14 @@ export declare class SupervisorServer {
7
7
  readonly store: ControlStore;
8
8
  readonly release: MaterializedRelease;
9
9
  readonly terminateProcess: (code: number) => void;
10
+ readonly reconciliationDeadlineMs: number;
11
+ readonly shutdownDeadlineMs: number;
10
12
  readonly id: `${string}-${string}-${string}-${string}-${string}`;
11
13
  readonly bootNonce: string;
12
14
  readonly pidStartIdentity: string;
13
15
  readonly startedAt: string;
14
16
  readonly paths: ProductPaths;
15
- constructor(store: ControlStore, paths: ProductPaths | undefined, release: MaterializedRelease, bootNonce?: string, terminateProcess?: (code: number) => void);
17
+ constructor(store: ControlStore, paths: ProductPaths | undefined, release: MaterializedRelease, bootNonce?: string, terminateProcess?: (code: number) => void, reconciliationDeadlineMs?: number, shutdownDeadlineMs?: number);
16
18
  snapshot(): SupervisorSnapshot;
17
19
  listen(): Promise<void>;
18
20
  close(stopAgents?: boolean): Promise<void>;