@timurproko/a1 0.2.7-dev.820 → 0.2.7-dev.831

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 (115) hide show
  1. package/README.md +7 -1
  2. package/bin/automatic-update.js +7 -0
  3. package/bin/ui.js +1 -0
  4. package/dist/app/session-shell/session-shell-root.d.ts +8 -1
  5. package/dist/app/session-shell/session-shell.js +68 -18
  6. package/dist/app/session-shell/transcript-document.js +1 -1
  7. package/dist/composition/owned-ui.d.ts +3 -11
  8. package/dist/composition/owned-ui.js +93 -42
  9. package/dist/composition/resident-tab-bridge.d.ts +10 -1
  10. package/dist/composition/resident-tab-bridge.js +21 -0
  11. package/dist/composition/settings-route-host.d.ts +12 -2
  12. package/dist/composition/settings-route-host.js +27 -9
  13. package/dist/composition/settings-search-input.d.ts +9 -0
  14. package/dist/composition/settings-search-input.js +58 -0
  15. package/dist/contracts/owned-ui/session-backend.d.ts +2 -0
  16. package/dist/contracts/owned-ui/session-workflows.d.ts +7 -3
  17. package/dist/contracts/presentation/index.d.ts +11 -0
  18. package/dist/contracts/presentation/index.js +12 -0
  19. package/dist/features/launch/development-launch.js +6 -0
  20. package/dist/features/launch/profile-paths.js +4 -0
  21. package/dist/features/owned-ui/plus-menu-dialog.d.ts +22 -0
  22. package/dist/features/owned-ui/plus-menu-dialog.js +76 -0
  23. package/dist/features/owned-ui/reference-screen-app.d.ts +2 -0
  24. package/dist/features/owned-ui/reference-screen-app.js +12 -2
  25. package/dist/features/owned-ui/settings-app.d.ts +10 -3
  26. package/dist/features/owned-ui/settings-app.js +98 -45
  27. package/dist/features/owned-ui/settings-search-input.d.ts +29 -0
  28. package/dist/features/owned-ui/settings-search-input.js +1 -0
  29. package/dist/features/prompt-history/index.d.ts +1 -1
  30. package/dist/features/prompt-history/paths.d.ts +2 -1
  31. package/dist/features/prompt-history/paths.js +10 -2
  32. package/dist/features/prompt-history/service.d.ts +5 -0
  33. package/dist/features/prompt-history/service.js +1 -1
  34. package/dist/features/resident-tab/shortcut-declarations.d.ts +1 -1
  35. package/dist/features/resident-tab/shortcut-declarations.js +2 -0
  36. package/dist/foundation/launch-context/index.d.ts +2 -0
  37. package/dist/foundation/launch-context/index.js +1 -0
  38. package/dist/foundation/release/automatic-update.d.ts +37 -0
  39. package/dist/foundation/release/automatic-update.js +265 -0
  40. package/dist/foundation/release/bootstrap.d.ts +1 -9
  41. package/dist/foundation/release/bootstrap.js +7 -3
  42. package/dist/foundation/release/index.d.ts +1 -1
  43. package/dist/foundation/release/index.js +1 -1
  44. package/dist/foundation/release/latest-release.d.ts +3 -2
  45. package/dist/foundation/release/latest-release.js +1 -1
  46. package/dist/foundation/release/update-execution-lease.d.ts +36 -0
  47. package/dist/foundation/release/update-execution-lease.js +145 -0
  48. package/dist/foundation/release/update.d.ts +8 -6
  49. package/dist/foundation/release/update.js +232 -167
  50. package/dist/foundation/resident-tabs/bridge-client.d.ts +8 -2
  51. package/dist/foundation/resident-tabs/bridge-client.js +13 -6
  52. package/dist/foundation/resident-tabs/routes.d.ts +5 -0
  53. package/dist/foundation/resident-tabs/routes.js +5 -0
  54. package/dist/integrations/pi/components/editor-interaction.d.ts +2 -0
  55. package/dist/integrations/pi/components/modal-frame.d.ts +17 -1
  56. package/dist/integrations/pi/components/modal-frame.js +48 -9
  57. package/dist/integrations/pi/components/models-dialog.js +4 -52
  58. package/dist/integrations/pi/components/owned-editor-ux.d.ts +3 -0
  59. package/dist/integrations/pi/components/owned-editor-ux.js +75 -4
  60. package/dist/integrations/pi/components/shell-editor-autocomplete.js +57 -37
  61. package/dist/integrations/pi/components/shell-footer-status.js +19 -3
  62. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -1
  63. package/dist/integrations/pi/components/shell-selectors-dialogs.js +4 -1
  64. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  65. package/dist/integrations/pi/components/skills-dialog.js +2 -2
  66. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -5
  67. package/dist/integrations/pi/components/upstream/components/owned-editor.js +15 -8
  68. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -3
  69. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +2 -2
  70. package/dist/integrations/pi/components/upstream/components/session-footer.js +26 -23
  71. package/dist/integrations/pi/components/upstream/components/session-selector.js +7 -6
  72. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  73. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +9 -8
  74. package/dist/integrations/pi/components/upstream/components/tool-execution.js +6 -4
  75. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +8 -8
  76. package/dist/integrations/pi/components/upstream/components/tree-selector.js +10 -10
  77. package/dist/integrations/pi/components/upstream/history/editor-core.js +2 -0
  78. package/dist/integrations/pi/engine/adapter.js +7 -2
  79. package/dist/integrations/pi/engine/ports.d.ts +1 -1
  80. package/dist/integrations/pi/engine/ports.js +4 -0
  81. package/dist/integrations/pi/engine/workflow-runner.js +9 -2
  82. package/dist/native/darwin-arm64/manifest.json +1 -1
  83. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  84. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  85. package/dist/native/darwin-x64/manifest.json +1 -1
  86. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  87. package/dist/native/darwin-x64/terminal-host.node +0 -0
  88. package/dist/native/linux-x64/manifest.json +1 -1
  89. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  90. package/dist/native/linux-x64/terminal-host.node +0 -0
  91. package/dist/native/win32-x64/manifest.json +2 -2
  92. package/dist/native/win32-x64/process-guardian.node +0 -0
  93. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  94. package/dist/native/win32-x64/terminal-host.node +0 -0
  95. package/dist/product-identity.d.ts +1 -1
  96. package/dist/product-identity.js +1 -1
  97. package/dist/product-identity.json +1 -0
  98. package/dist/ui/apps/host.d.ts +2 -0
  99. package/dist/ui/apps/host.js +7 -1
  100. package/dist/ui/components/dialog-panel.js +35 -30
  101. package/dist/ui/components/line-input.d.ts +3 -0
  102. package/dist/ui/components/line-input.js +3 -1
  103. package/dist/ui/components/prompt-input.d.ts +3 -2
  104. package/dist/ui/components/prompt-input.js +6 -4
  105. package/dist/ui/components/surface.js +4 -2
  106. package/dist/ui/settings/declarations.d.ts +54 -8
  107. package/dist/ui/settings/declarations.js +36 -7
  108. package/dist/ui/settings/manager.js +7 -3
  109. package/dist/ui/settings/migrations.js +15 -0
  110. package/dist/ui/settings/sections.d.ts +4 -0
  111. package/dist/ui/settings/sections.js +9 -3
  112. package/docs/architecture/resident-tabs.md +35 -7
  113. package/docs/architecture/tool-image-presentation.md +2 -1
  114. package/docs/features/prompt-history.md +41 -20
  115. package/package.json +1 -1
@@ -0,0 +1,265 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
4
+ import { dirname, resolve } from "node:path";
5
+ import { valid as validSemver } from "semver";
6
+ import { readLaunchContext } from "../launch-context/index.js";
7
+ import { resolveProductPaths } from "../lifecycle/index.js";
8
+ import { CohortStateStore } from "./cohort-state.js";
9
+ import { processIsAlive } from "./process-cleanup.js";
10
+ import { isNewerRelease, releaseChannelOf } from "./latest-release.js";
11
+ import { readCertifiedReleaseManifest, resolveReleaseEntryPoint } from "./release-store.js";
12
+ import { readUpdateExecutionOwner } from "./update-execution-lease.js";
13
+ import { runSelfUpdate, UPDATE_ALREADY_RUNNING_EXIT_CODE } from "./update.js";
14
+ export const AUTOMATIC_UPDATE_STATE_SCHEMA = 1;
15
+ export const AUTOMATIC_UPDATE_RETRY_MS = 24 * 60 * 60_000;
16
+ const AUTOMATIC_UPDATE_STATE_FILENAME = "automatic-update.json";
17
+ const MAX_STATE_BYTES = 16_384;
18
+ const OBSERVER_POLL_MS = 250;
19
+ const WORKER_START_GRACE_MS = 5_000;
20
+ export class AutomaticUpdateStateStore {
21
+ path;
22
+ constructor(dataDir) { this.path = resolve(dataDir, AUTOMATIC_UPDATE_STATE_FILENAME); }
23
+ async read() {
24
+ let source;
25
+ try {
26
+ source = await readFile(this.path, "utf8");
27
+ }
28
+ catch (error) {
29
+ if (hasCode(error, "ENOENT"))
30
+ return null;
31
+ throw error;
32
+ }
33
+ if (Buffer.byteLength(source, "utf8") > MAX_STATE_BYTES)
34
+ throw new Error("automatic update state is oversized");
35
+ const value = JSON.parse(source);
36
+ validateState(value);
37
+ return value;
38
+ }
39
+ async write(value) {
40
+ validateState(value);
41
+ await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
42
+ const temporary = `${this.path}.${process.pid}.${randomUUID()}.tmp`;
43
+ const file = await open(temporary, "wx", 0o600);
44
+ try {
45
+ await file.writeFile(`${JSON.stringify(value)}\n`);
46
+ await file.sync();
47
+ }
48
+ finally {
49
+ await file.close();
50
+ }
51
+ try {
52
+ await rename(temporary, this.path);
53
+ }
54
+ catch (error) {
55
+ await rm(temporary, { force: true });
56
+ throw error;
57
+ }
58
+ }
59
+ }
60
+ /** Start or observe one silent update and resolve with the active newer version once restart is useful. */
61
+ export async function requestAutomaticUpdate(options) {
62
+ if (options.signal?.aborted)
63
+ return null;
64
+ const environment = options.environment ?? process.env;
65
+ const paths = resolveProductPaths(environment);
66
+ const now = options.now ?? Date.now;
67
+ const store = new AutomaticUpdateStateStore(paths.dataDir);
68
+ let existing;
69
+ try {
70
+ existing = await store.read();
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ const readActiveVersion = options.activeVersion ?? (async () => await activeNewerVersion(paths.dataDir, options.runningVersion));
76
+ const active = await readActiveVersion();
77
+ if (active !== null)
78
+ return active;
79
+ if (existing?.status === "failed" && existing.channel === options.release.channel && now() - existing.attemptedAt >= 0
80
+ && now() - existing.attemptedAt < AUTOMATIC_UPDATE_RETRY_MS
81
+ && !isNewerRelease(options.release.version, existing.targetVersion))
82
+ return null;
83
+ let worker = options.worker;
84
+ if (worker === undefined) {
85
+ const launch = readLaunchContext(environment, "release");
86
+ const cohort = await new CohortStateStore(paths.dataDir).read();
87
+ const record = launch.releaseId === undefined ? undefined : cohort.releases[launch.releaseId];
88
+ if (!record || record.approval !== "approved" || record.releaseRoot !== launch.releaseRoot
89
+ || record.contentDigest !== launch.releaseDigest)
90
+ return null;
91
+ const selected = await readCertifiedReleaseManifest(record, resolve(paths.dataDir, "releases"));
92
+ worker = {
93
+ entry: await resolveReleaseEntryPoint(selected, "bin/automatic-update.js"),
94
+ packageRoot: selected.packageRoot,
95
+ };
96
+ }
97
+ const arguments_ = [
98
+ "--package-root", worker.packageRoot,
99
+ "--channel", options.release.channel,
100
+ "--target-version", options.release.version,
101
+ "--data-dir", paths.dataDir,
102
+ ];
103
+ let owner;
104
+ try {
105
+ owner = await readUpdateExecutionOwner(paths.dataDir);
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ if (owner === null || !processIsAlive(owner.pid)) {
111
+ await (options.spawnWorker ?? spawnAutomaticUpdateWorker)(worker.entry, arguments_, environment).catch(() => undefined);
112
+ }
113
+ const startedAt = now();
114
+ const sleep = options.sleep ?? (async (durationMs) => await new Promise(resolvePromise => {
115
+ const timer = setTimeout(resolvePromise, durationMs);
116
+ timer.unref?.();
117
+ }));
118
+ while (!options.signal?.aborted) {
119
+ const installed = await readActiveVersion();
120
+ if (installed !== null)
121
+ return installed;
122
+ const state = await store.read().catch(() => null);
123
+ if (state?.targetVersion === options.release.version && state.status === "failed" && state.attemptedAt >= startedAt)
124
+ return null;
125
+ const owner = await readUpdateExecutionOwner(paths.dataDir).catch(() => null);
126
+ if ((owner === null || !processIsAlive(owner.pid)) && now() - startedAt >= WORKER_START_GRACE_MS)
127
+ return null;
128
+ await sleep(OBSERVER_POLL_MS);
129
+ }
130
+ return null;
131
+ }
132
+ export async function runAutomaticUpdateWorker(argv, environment = process.env) {
133
+ const parsed = parseWorkerArguments(argv);
134
+ const paths = resolveProductPaths(environment);
135
+ if (!samePath(parsed.dataDir, paths.dataDir))
136
+ return 1;
137
+ const launch = readLaunchContext(environment, "release");
138
+ const cohort = await new CohortStateStore(paths.dataDir).read();
139
+ const record = launch.releaseId === undefined ? undefined : cohort.releases[launch.releaseId];
140
+ if (!record || record.approval !== "approved" || !samePath(record.releaseRoot, launch.releaseRoot ?? "")
141
+ || record.contentDigest !== launch.releaseDigest)
142
+ return 1;
143
+ const selected = await readCertifiedReleaseManifest(record, resolve(paths.dataDir, "releases"));
144
+ if (!samePath(selected.packageRoot, parsed.packageRoot))
145
+ return 1;
146
+ const store = new AutomaticUpdateStateStore(paths.dataDir);
147
+ const attemptedAt = Date.now();
148
+ const output = { stdout() { }, stderr() { } };
149
+ let acquired = false;
150
+ let runningWrite = Promise.resolve();
151
+ try {
152
+ const code = await runSelfUpdate({
153
+ packageRoot: parsed.packageRoot,
154
+ channel: parsed.channel === "stable" ? "stable" : "next",
155
+ resolvedTarget: parsed.targetVersion,
156
+ mode: "automatic",
157
+ environment,
158
+ output,
159
+ progress: false,
160
+ onLeaseAcquired: () => {
161
+ acquired = true;
162
+ runningWrite = store.write(state(parsed, "running", attemptedAt, null, null));
163
+ },
164
+ });
165
+ if (code === UPDATE_ALREADY_RUNNING_EXIT_CODE)
166
+ return 0;
167
+ if (code !== 0) {
168
+ await runningWrite;
169
+ await store.write(state(parsed, "failed", attemptedAt, null, `exit-${code}`));
170
+ return code;
171
+ }
172
+ const active = await activeRelease(paths.dataDir);
173
+ if (active === null || releaseChannelOf(active.packageVersion) !== parsed.channel
174
+ || (active.packageVersion !== parsed.targetVersion && !isNewerRelease(active.packageVersion, parsed.targetVersion))) {
175
+ if (acquired) {
176
+ await runningWrite;
177
+ await store.write(state(parsed, "failed", attemptedAt, null, "target-not-active"));
178
+ }
179
+ return 1;
180
+ }
181
+ await runningWrite;
182
+ await store.write(state(parsed, "installed", attemptedAt, active.packageVersion, null));
183
+ return 0;
184
+ }
185
+ catch (error) {
186
+ await runningWrite.catch(() => undefined);
187
+ await store.write(state(parsed, "failed", attemptedAt, null, errorMessage(error).slice(0, 512))).catch(() => undefined);
188
+ return 1;
189
+ }
190
+ }
191
+ async function spawnAutomaticUpdateWorker(entry, arguments_, environment) {
192
+ const child = spawn(process.execPath, [entry, ...arguments_], {
193
+ detached: true,
194
+ env: environment,
195
+ stdio: "ignore",
196
+ windowsHide: true,
197
+ });
198
+ await new Promise((resolvePromise, rejectPromise) => {
199
+ child.once("spawn", resolvePromise);
200
+ child.once("error", rejectPromise);
201
+ });
202
+ child.unref();
203
+ }
204
+ async function activeNewerVersion(dataDir, runningVersion) {
205
+ const active = await activeRelease(dataDir);
206
+ return active !== null && isNewerRelease(active.packageVersion, runningVersion) ? active.packageVersion : null;
207
+ }
208
+ async function activeRelease(dataDir) {
209
+ const cohort = await new CohortStateStore(dataDir).read();
210
+ const activeId = cohort.references.active;
211
+ if (activeId === null)
212
+ return null;
213
+ const active = cohort.releases[activeId];
214
+ return active?.approval === "approved" ? active : null;
215
+ }
216
+ function state(parsed, status, attemptedAt, installedVersion, failure) {
217
+ return {
218
+ schema: AUTOMATIC_UPDATE_STATE_SCHEMA,
219
+ channel: parsed.channel,
220
+ targetVersion: parsed.targetVersion,
221
+ status,
222
+ attemptedAt,
223
+ updatedAt: Date.now(),
224
+ installedVersion,
225
+ failure,
226
+ };
227
+ }
228
+ function parseWorkerArguments(argv) {
229
+ const values = new Map();
230
+ for (let index = 0; index < argv.length; index += 2) {
231
+ const key = argv[index];
232
+ const value = argv[index + 1];
233
+ if (!key?.startsWith("--") || value === undefined || values.has(key))
234
+ throw new Error("invalid automatic update worker arguments");
235
+ values.set(key, value);
236
+ }
237
+ const packageRoot = values.get("--package-root");
238
+ const channel = values.get("--channel");
239
+ const targetVersion = values.get("--target-version");
240
+ const dataDir = values.get("--data-dir");
241
+ if (!packageRoot || (channel !== "stable" && channel !== "development") || !targetVersion || !dataDir
242
+ || validSemver(targetVersion) === null)
243
+ throw new Error("incomplete automatic update worker arguments");
244
+ return { packageRoot, channel, targetVersion, dataDir };
245
+ }
246
+ function validateState(value) {
247
+ if (value.schema !== AUTOMATIC_UPDATE_STATE_SCHEMA || (value.channel !== "stable" && value.channel !== "development")
248
+ || validSemver(value.targetVersion) === null || !["running", "installed", "failed"].includes(value.status)
249
+ || !Number.isFinite(value.attemptedAt) || !Number.isFinite(value.updatedAt)
250
+ || (value.installedVersion !== null && validSemver(value.installedVersion) === null)
251
+ || (value.failure !== null && (typeof value.failure !== "string" || value.failure.length > 512))) {
252
+ throw new Error("invalid automatic update state");
253
+ }
254
+ }
255
+ function samePath(left, right) {
256
+ const canonicalLeft = resolve(left);
257
+ const canonicalRight = resolve(right);
258
+ return process.platform === "win32"
259
+ ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase()
260
+ : canonicalLeft === canonicalRight;
261
+ }
262
+ function hasCode(error, code) {
263
+ return error instanceof Error && "code" in error && error.code === code;
264
+ }
265
+ function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
@@ -13,15 +13,7 @@ export interface BootstrapOptions {
13
13
  };
14
14
  readonly environment?: NodeJS.ProcessEnv;
15
15
  readonly output?: Pick<NodeJS.WriteStream, "write">;
16
- /**
17
- * Whether a materialized release copy is fit to launch. The active release
18
- * is normally reused while its version matches the installation, but a copy
19
- * can be broken in ways a version cannot see — materialized from a tree a
20
- * blocked postinstall never finished repairing. The probe is injected from
21
- * the bin entry because deciding it requires inspecting dependency
22
- * resolution, which stays out of production code. Absent means every
23
- * release is fit.
24
- */
16
+ /** Optional package-resolution probe that excludes a damaged retained release. */
25
17
  readonly releaseIsLaunchable?: (releaseRoot: string) => boolean;
26
18
  /** Test seam for bounded maintenance scheduled after stale endpoint sweeping. */
27
19
  readonly scheduleMaintenance?: (dataDir: string, paths: ReturnType<typeof resolveProductPaths>) => Promise<void>;
@@ -5,6 +5,7 @@ import { connect } from "node:net";
5
5
  import { resolve } from "node:path";
6
6
  import { selectCohortLaunch } from "./cohort-selection.js";
7
7
  import { selectOrdinaryLaunchReleaseId } from "./ordinary-launch-selection.js";
8
+ import { releaseChannelOf } from "./latest-release.js";
8
9
  import { CohortStateStore } from "./cohort-state.js";
9
10
  import { assertLaunchProfileId, createSupervisorStartupAttempt, readSupervisorStartupResult, resolveCohortEndpoint, resolveProductPaths, sessionSelectionArguments } from "../lifecycle/index.js";
10
11
  import { encodeFrame, LineFrameDecoder } from "../protocol/index.js";
@@ -17,7 +18,7 @@ import { scheduleReleaseCleanup } from "./release-gc.js";
17
18
  import { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument } from "./restart-certification.js";
18
19
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
19
20
  import { markStartupPhase } from "../startup/index.js";
20
- import { isCurrentLaunchContract, launchContractTarget, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
21
+ import { PRIVATE_ENVIRONMENT, isCurrentLaunchContract, launchContractTarget, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
21
22
  import { ensureTerminalHost, readResidentTabLaunchSettings, selectResidentHost } from "../resident-tabs/index.js";
22
23
  const RELEASE_RESELECTION_MESSAGE = "a1-release-reselection";
23
24
  export async function runBootstrap(options) {
@@ -124,6 +125,8 @@ async function runBootstrapAttempt(options, retriesRemaining) {
124
125
  }
125
126
  let probe = endpoint ? await probeOwnership(endpoint) : "dead";
126
127
  const installedVersion = await readInstalledVersion(options.packageRoot);
128
+ if (releaseChannelOf(installedVersion) !== null)
129
+ environment[PRIVATE_ENVIRONMENT.updateVersion] = installedVersion;
127
130
  const activeId = state.references.active;
128
131
  const active = activeId === null ? undefined : state.releases[activeId];
129
132
  let activeIsLaunchable = active === undefined || (options.releaseIsLaunchable?.(active.releaseRoot) ?? true);
@@ -434,14 +437,15 @@ async function launchUi(release, environment, sessionArgs, residentContext = {})
434
437
  * version instead of failing while an installation is being replaced.
435
438
  */
436
439
  export function releaseEnvironment(environment, release, profile) {
437
- const launchMode = readLaunchContext(environment).launchMode;
440
+ const context = readLaunchContext(environment);
438
441
  return withLaunchContext(environment, {
439
442
  releaseId: release.releaseId,
440
443
  releaseLayers: (release.dependencyLayers ?? []).map(layer => layer.layerId).join(","),
441
444
  releaseRoot: release.releaseRoot,
442
445
  releaseDigest: release.contentDigest,
443
446
  ...(profile === undefined ? {} : { launchProfile: profile }),
444
- ...(launchMode === undefined ? {} : { launchMode }),
447
+ ...(context.launchMode === undefined ? {} : { launchMode: context.launchMode }),
448
+ ...(context.updateVersion === undefined ? {} : { updateVersion: context.updateVersion }),
445
449
  }, process.platform, launchContractTarget(release));
446
450
  }
447
451
  export async function waitForVerifiedEndpoint(path, release, timeoutMs, startup) {
@@ -21,7 +21,7 @@ export { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDoc
21
21
  export type { RestartSeal, RestartValidationEvent } from "./restart-certification.js";
22
22
  export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistry } from "./stable-release.js";
23
23
  export type { StableRegistryState, StableRegistryVerificationOptions, StableReleaseEvidence, StableReleaseEvidenceInput } from "./stable-release.js";
24
- export { PACKAGE_UNLOCK_PATIENCE_MS, PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, isPackageLockError, lockedPackageDiagnostic, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
24
+ export { PACKAGE_UNLOCK_PATIENCE_MS, UPDATE_ALREADY_RUNNING_EXIT_CODE, PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, isPackageLockError, lockedPackageDiagnostic, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
25
25
  export type { ProcessRequest, ProcessResult, SelfUpdateOptions, UpdateActivationPhase, UpdateChannel, UpdateFileMetadata, UpdateFileSystem, UpdateLifecycleCoordinator, UpdateMaterializationProgress, UpdateMeasuredPhase, UpdateOutput, UpdateOwnershipAction, UpdatePackageReplacementInput, UpdatePerformanceEvidence, UpdatePhaseTimingEvent, UpdateProcessRunner, UpdateTransactionJournal, UpdateUnlockPatience, } from "./update.js";
26
26
  export { UPDATE_ACTIVATION_CONTRACT, UPDATE_ACTIVATION_ENTRY, UPDATE_ACTIVATION_MANIFEST_FIELD, activateInstalledRelease, delegateActivation, readActivationContracts, runActivationEntry, } from "./update-activation.js";
27
27
  export type { UpdateActivationCallbacks, UpdateActivationEvent, UpdateActivationRequest } from "./update-activation.js";
@@ -10,7 +10,7 @@ export { collectRelease, prepareReleaseCleanup, runBoundedReleaseCleanup, runRel
10
10
  export { RELEASE_MANIFEST_FILENAME, ReleaseRepairUnavailableError, assertCertifiedFilesPresent, assertImmutableExecutionRoot, consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, repairMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease, } from "./release-store.js";
11
11
  export { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument, restartSealDigest } from "./restart-certification.js";
12
12
  export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistry } from "./stable-release.js";
13
- export { PACKAGE_UNLOCK_PATIENCE_MS, PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, isPackageLockError, lockedPackageDiagnostic, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
13
+ export { PACKAGE_UNLOCK_PATIENCE_MS, UPDATE_ALREADY_RUNNING_EXIT_CODE, PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, isPackageLockError, lockedPackageDiagnostic, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
14
14
  export { UPDATE_ACTIVATION_CONTRACT, UPDATE_ACTIVATION_ENTRY, UPDATE_ACTIVATION_MANIFEST_FIELD, activateInstalledRelease, delegateActivation, readActivationContracts, runActivationEntry, } from "./update-activation.js";
15
15
  export { selectSupervisorLaunchReleaseId, selectUpdateLaunchRelease } from "./update-launch.js";
16
16
  export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, npmPrefixForGlobalRoot, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, updateNpmInstallArguments, } from "./update-recovery.js";
@@ -1,5 +1,6 @@
1
1
  /** Published release channels: stable `X.Y.Z` on dist-tag `latest`, development `X.Y.Z-dev.N` on `next`. */
2
2
  export type ReleaseChannel = "stable" | "development";
3
+ export type StartupUpdateMode = "auto" | "notify" | "no";
3
4
  export interface DistTagVersions {
4
5
  readonly release: string | null;
5
6
  readonly develop: string | null;
@@ -39,8 +40,8 @@ export interface StartupReleaseCheckOptions {
39
40
  readonly environment?: NodeJS.ProcessEnv;
40
41
  /** Whether standard output is an interactive terminal. */
41
42
  readonly interactive: boolean;
42
- /** The bare-A1 `updateCheck` setting; comparison profiles omit it. */
43
- readonly settingEnabled?: boolean;
43
+ /** The bare-A1 `updateCheck` setting; comparison profiles omit it and use auto. */
44
+ readonly settingMode?: StartupUpdateMode;
44
45
  readonly fetcher?: RegistryFetcher;
45
46
  readonly now?: () => number;
46
47
  }
@@ -74,7 +74,7 @@ export function startupReleaseCheckSkipReason(options) {
74
74
  return "ci";
75
75
  if (!options.interactive)
76
76
  return "not-interactive";
77
- if (options.settingEnabled === false)
77
+ if (options.settingMode === "no")
78
78
  return "disabled-by-setting";
79
79
  if (releaseChannelOf(options.runningVersion) === null)
80
80
  return "unpublished-version";
@@ -0,0 +1,36 @@
1
+ import type { UpdateChannel } from "./types.js";
2
+ export declare const UPDATE_EXECUTION_LEASE_FILENAME = "update-execution.json";
3
+ declare const LEASE_SCHEMA = 1;
4
+ export type UpdateExecutionMode = "manual" | "automatic";
5
+ export interface UpdateExecutionOwner {
6
+ readonly schema: typeof LEASE_SCHEMA;
7
+ readonly ownerId: string;
8
+ readonly pid: number;
9
+ readonly pidStartIdentity: string;
10
+ readonly packageRoot: string;
11
+ readonly channel: UpdateChannel;
12
+ readonly targetVersion: string;
13
+ readonly mode: UpdateExecutionMode;
14
+ readonly acquiredAt: string;
15
+ }
16
+ export type UpdateExecutionLeaseResult = {
17
+ readonly status: "acquired";
18
+ readonly owner: UpdateExecutionOwner;
19
+ release(): Promise<void>;
20
+ } | {
21
+ readonly status: "busy";
22
+ readonly owner: UpdateExecutionOwner;
23
+ };
24
+ /**
25
+ * Serializes every package replacement for one A1 data root. A dead owner is moved aside before a
26
+ * new exclusive create, so concurrent recovery attempts cannot both acquire the installation.
27
+ */
28
+ export declare function acquireUpdateExecutionLease(input: {
29
+ readonly dataDir: string;
30
+ readonly packageRoot: string;
31
+ readonly channel: UpdateChannel;
32
+ readonly targetVersion: string;
33
+ readonly mode: UpdateExecutionMode;
34
+ }): Promise<UpdateExecutionLeaseResult>;
35
+ export declare function readUpdateExecutionOwner(dataDir: string): Promise<UpdateExecutionOwner | null>;
36
+ export {};
@@ -0,0 +1,145 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ import lockfile from "proper-lockfile";
5
+ import { processIsAlive } from "./process-cleanup.js";
6
+ import { UpdateTransactionStore } from "./update-transaction.js";
7
+ export const UPDATE_EXECUTION_LEASE_FILENAME = "update-execution.json";
8
+ const LEASE_SCHEMA = 1;
9
+ const MAX_LEASE_BYTES = 8_192;
10
+ /**
11
+ * Serializes every package replacement for one A1 data root. A dead owner is moved aside before a
12
+ * new exclusive create, so concurrent recovery attempts cannot both acquire the installation.
13
+ */
14
+ export async function acquireUpdateExecutionLease(input) {
15
+ const path = resolve(input.dataDir, UPDATE_EXECUTION_LEASE_FILENAME);
16
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
17
+ const releaseAcquisition = await lockfile.lock(path, {
18
+ realpath: false,
19
+ stale: 60_000,
20
+ update: 20_000,
21
+ retries: { retries: 20, factor: 1.2, minTimeout: 20, maxTimeout: 100, randomize: true },
22
+ });
23
+ const owner = {
24
+ schema: LEASE_SCHEMA,
25
+ ownerId: randomUUID(),
26
+ pid: process.pid,
27
+ pidStartIdentity: currentProcessStartIdentity(),
28
+ packageRoot: resolve(input.packageRoot),
29
+ channel: input.channel,
30
+ targetVersion: input.targetVersion,
31
+ mode: input.mode,
32
+ acquiredAt: new Date().toISOString(),
33
+ };
34
+ try {
35
+ for (let attempt = 0; attempt < 4; attempt += 1) {
36
+ try {
37
+ const file = await open(path, "wx", 0o600);
38
+ try {
39
+ await file.writeFile(`${JSON.stringify(owner)}\n`);
40
+ await file.sync();
41
+ }
42
+ finally {
43
+ await file.close();
44
+ }
45
+ return {
46
+ status: "acquired",
47
+ owner,
48
+ release: async () => await releaseLease(path, owner),
49
+ };
50
+ }
51
+ catch (error) {
52
+ if (!hasCode(error, "EEXIST"))
53
+ throw error;
54
+ }
55
+ const existing = await readUpdateExecutionOwner(input.dataDir);
56
+ if (existing === null)
57
+ continue;
58
+ // Security: an uninspectable or reused pid stays conservative. A process known to be absent is
59
+ // reclaimed only when its durable transaction agrees with both owners.
60
+ if (processIsAlive(existing.pid))
61
+ return { status: "busy", owner: existing };
62
+ if (!await staleLeaseMatchesTransaction(input.dataDir, existing, input))
63
+ return { status: "busy", owner: existing };
64
+ const stale = `${path}.stale-${existing.ownerId}-${randomUUID()}`;
65
+ try {
66
+ await rename(path, stale);
67
+ await rm(stale, { force: true });
68
+ }
69
+ catch (error) {
70
+ if (!hasCode(error, "ENOENT"))
71
+ throw error;
72
+ }
73
+ }
74
+ const existing = await readUpdateExecutionOwner(input.dataDir);
75
+ if (existing !== null)
76
+ return { status: "busy", owner: existing };
77
+ throw new Error("could not acquire update execution lease");
78
+ }
79
+ finally {
80
+ await releaseAcquisition();
81
+ }
82
+ }
83
+ export async function readUpdateExecutionOwner(dataDir) {
84
+ const path = resolve(dataDir, UPDATE_EXECUTION_LEASE_FILENAME);
85
+ let source;
86
+ try {
87
+ source = await readFile(path, "utf8");
88
+ }
89
+ catch (error) {
90
+ if (hasCode(error, "ENOENT"))
91
+ return null;
92
+ throw error;
93
+ }
94
+ if (Buffer.byteLength(source, "utf8") > MAX_LEASE_BYTES)
95
+ throw new Error("update execution lease is oversized");
96
+ const value = JSON.parse(source);
97
+ validateOwner(value);
98
+ return value;
99
+ }
100
+ async function releaseLease(path, owner) {
101
+ const current = await readUpdateExecutionOwner(dirname(path));
102
+ if (current === null || current.ownerId !== owner.ownerId || current.pid !== owner.pid
103
+ || current.pidStartIdentity !== owner.pidStartIdentity)
104
+ return;
105
+ const released = `${path}.released-${owner.ownerId}-${randomUUID()}`;
106
+ try {
107
+ await rename(path, released);
108
+ await rm(released, { force: true });
109
+ }
110
+ catch (error) {
111
+ if (!hasCode(error, "ENOENT"))
112
+ throw error;
113
+ }
114
+ }
115
+ function validateOwner(value) {
116
+ if (value.schema !== LEASE_SCHEMA || typeof value.ownerId !== "string" || value.ownerId.length === 0
117
+ || !Number.isSafeInteger(value.pid) || value.pid <= 0 || typeof value.pidStartIdentity !== "string"
118
+ || typeof value.packageRoot !== "string" || !["stable", "next"].includes(value.channel)
119
+ || typeof value.targetVersion !== "string" || !["manual", "automatic"].includes(value.mode)
120
+ || typeof value.acquiredAt !== "string") {
121
+ throw new Error("invalid update execution lease");
122
+ }
123
+ }
124
+ async function staleLeaseMatchesTransaction(dataDir, owner, requested) {
125
+ const transaction = await new UpdateTransactionStore(dataDir).read();
126
+ if (transaction?.status !== "active")
127
+ return true;
128
+ return transaction.channel === owner.channel && transaction.targetVersion === owner.targetVersion
129
+ && samePath(transaction.packageRoot, owner.packageRoot)
130
+ && requested.channel === owner.channel && requested.targetVersion === owner.targetVersion
131
+ && samePath(requested.packageRoot, owner.packageRoot);
132
+ }
133
+ function samePath(left, right) {
134
+ const canonicalLeft = resolve(left);
135
+ const canonicalRight = resolve(right);
136
+ return process.platform === "win32"
137
+ ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase()
138
+ : canonicalLeft === canonicalRight;
139
+ }
140
+ function currentProcessStartIdentity() {
141
+ return `${process.pid}:${Math.floor(Date.now() - process.uptime() * 1_000)}`;
142
+ }
143
+ function hasCode(error, code) {
144
+ return error instanceof Error && "code" in error && error.code === code;
145
+ }
@@ -1,6 +1,7 @@
1
1
  import type { UpdateChannel } from "./types.js";
2
2
  import { type UpdateActivationPhase, type UpdateMaterializationProgress } from "./update-activation.js";
3
3
  import { type UpdateRecoveryState, type UpdateTransaction, type UpdateTransactionPhase } from "./update-transaction.js";
4
+ import { type UpdateExecutionOwner } from "./update-execution-lease.js";
4
5
  import { resolveNpmCli, type ProtectedPackageReplacementResult } from "./update-recovery.js";
5
6
  export declare const PRODUCT_PACKAGE: string;
6
7
  export type { UpdateChannel } from "./types.js";
@@ -61,6 +62,11 @@ export interface SelfUpdateOptions {
61
62
  channel?: UpdateChannel;
62
63
  /** A specific preview to install, named by its development number or full version. */
63
64
  target?: string;
65
+ /** Internal exact channel target selected by the automatic update probe. */
66
+ resolvedTarget?: string;
67
+ mode?: "manual" | "automatic";
68
+ onLeaseAcquired?: (owner: UpdateExecutionOwner) => void;
69
+ onLeaseBusy?: (owner: UpdateExecutionOwner) => void;
64
70
  environment?: NodeJS.ProcessEnv;
65
71
  fileSystem?: UpdateFileSystem;
66
72
  output?: UpdateOutput;
@@ -80,6 +86,7 @@ export interface SelfUpdateOptions {
80
86
  export type { UpdateActivationPhase, UpdateMaterializationProgress } from "./update-activation.js";
81
87
  export interface UpdateLifecycleCoordinator {
82
88
  targetIsActive(targetVersion: string): Promise<boolean>;
89
+ verifyAutomaticSafety?(): Promise<void>;
83
90
  shutdownVerifiedOwners(targetVersion: string): Promise<{
84
91
  priorActiveVersion: string | null;
85
92
  }>;
@@ -123,6 +130,7 @@ export interface UpdatePackageReplacementInput {
123
130
  * The update therefore keeps checking for this long before it calls the package locked.
124
131
  */
125
132
  export declare const PACKAGE_UNLOCK_PATIENCE_MS = 15000;
133
+ export declare const UPDATE_ALREADY_RUNNING_EXIT_CODE = 75;
126
134
  /** Whether a rename failure means something holds the tree, as opposed to the tree being wrong. */
127
135
  export declare function isPackageLockError(error: unknown): boolean;
128
136
  /** What the user reads when the package stayed held for the whole patience window. */
@@ -131,12 +139,6 @@ export declare function lockedPackageDiagnostic(packageRoot: string, waitedMs: n
131
139
  export declare const CHILD_DIAGNOSTIC_LIMIT = 8000;
132
140
  export declare function createNpmProcessRunner(platform?: NodeJS.Platform): UpdateProcessRunner;
133
141
  export type UpdateOwnershipAction = "clean-dead-record" | "leave-running" | "end-session";
134
- /**
135
- * What an update does about the owner it found. A cohort running from retained immutable
136
- * content keeps its work: the installation replaces files it does not read, and it listens on
137
- * its own endpoint. A cohort running from the mutable installation is the one exception,
138
- * because that installation is exactly what is about to be replaced under it.
139
- */
140
142
  export declare function planUpdateOwnership(ownership: "live-verified" | "dead", runsFromRetainedRelease: boolean): UpdateOwnershipAction;
141
143
  export declare function createUpdateLifecycleCoordinator(environment?: NodeJS.ProcessEnv, fileSystem?: UpdateFileSystem, output?: UpdateOutput, patience?: UpdateUnlockPatience): UpdateLifecycleCoordinator;
142
144
  /** Renders the terminal-only update meter; the visual preview shares this exact frame. */