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

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 (124) 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/shell-host.js +6 -3
  7. package/dist/app/session-shell/transcript-document.js +1 -1
  8. package/dist/composition/owned-ui.d.ts +3 -11
  9. package/dist/composition/owned-ui.js +93 -42
  10. package/dist/composition/resident-tab-bridge.d.ts +10 -1
  11. package/dist/composition/resident-tab-bridge.js +21 -0
  12. package/dist/composition/settings-route-host.d.ts +12 -2
  13. package/dist/composition/settings-route-host.js +27 -9
  14. package/dist/composition/settings-search-input.d.ts +9 -0
  15. package/dist/composition/settings-search-input.js +58 -0
  16. package/dist/contracts/owned-ui/session-backend.d.ts +2 -0
  17. package/dist/contracts/owned-ui/session-workflows.d.ts +7 -3
  18. package/dist/contracts/presentation/index.d.ts +11 -0
  19. package/dist/contracts/presentation/index.js +12 -0
  20. package/dist/features/launch/development-launch.js +6 -0
  21. package/dist/features/launch/profile-paths.js +4 -0
  22. package/dist/features/owned-ui/plus-menu-dialog.d.ts +22 -0
  23. package/dist/features/owned-ui/plus-menu-dialog.js +76 -0
  24. package/dist/features/owned-ui/reference-screen-app.d.ts +2 -0
  25. package/dist/features/owned-ui/reference-screen-app.js +12 -2
  26. package/dist/features/owned-ui/settings-app.d.ts +10 -3
  27. package/dist/features/owned-ui/settings-app.js +98 -45
  28. package/dist/features/owned-ui/settings-search-input.d.ts +29 -0
  29. package/dist/features/owned-ui/settings-search-input.js +1 -0
  30. package/dist/features/prompt-history/index.d.ts +1 -1
  31. package/dist/features/prompt-history/paths.d.ts +2 -1
  32. package/dist/features/prompt-history/paths.js +10 -2
  33. package/dist/features/prompt-history/service.d.ts +5 -0
  34. package/dist/features/prompt-history/service.js +1 -1
  35. package/dist/features/resident-tab/shortcut-declarations.d.ts +1 -1
  36. package/dist/features/resident-tab/shortcut-declarations.js +2 -0
  37. package/dist/foundation/launch-context/index.d.ts +2 -0
  38. package/dist/foundation/launch-context/index.js +1 -0
  39. package/dist/foundation/release/automatic-update.d.ts +37 -0
  40. package/dist/foundation/release/automatic-update.js +265 -0
  41. package/dist/foundation/release/bootstrap.d.ts +1 -9
  42. package/dist/foundation/release/bootstrap.js +7 -3
  43. package/dist/foundation/release/index.d.ts +1 -1
  44. package/dist/foundation/release/index.js +1 -1
  45. package/dist/foundation/release/latest-release.d.ts +3 -2
  46. package/dist/foundation/release/latest-release.js +1 -1
  47. package/dist/foundation/release/update-execution-lease.d.ts +36 -0
  48. package/dist/foundation/release/update-execution-lease.js +145 -0
  49. package/dist/foundation/release/update.d.ts +8 -6
  50. package/dist/foundation/release/update.js +232 -167
  51. package/dist/foundation/resident-tabs/bridge-client.d.ts +8 -2
  52. package/dist/foundation/resident-tabs/bridge-client.js +13 -6
  53. package/dist/foundation/resident-tabs/progress-observation-schema.d.ts +34 -0
  54. package/dist/foundation/resident-tabs/progress-observation-schema.js +1 -0
  55. package/dist/foundation/resident-tabs/progress-observation.d.ts +3 -6
  56. package/dist/foundation/resident-tabs/progress-observation.js +42 -12
  57. package/dist/foundation/resident-tabs/routes.d.ts +5 -0
  58. package/dist/foundation/resident-tabs/routes.js +5 -0
  59. package/dist/integrations/pi/components/editor-interaction.d.ts +2 -0
  60. package/dist/integrations/pi/components/modal-frame.d.ts +17 -1
  61. package/dist/integrations/pi/components/modal-frame.js +48 -9
  62. package/dist/integrations/pi/components/models-dialog.js +4 -52
  63. package/dist/integrations/pi/components/owned-editor-ux.d.ts +3 -0
  64. package/dist/integrations/pi/components/owned-editor-ux.js +75 -4
  65. package/dist/integrations/pi/components/shell-editor-autocomplete.js +57 -37
  66. package/dist/integrations/pi/components/shell-footer-status.js +19 -3
  67. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -1
  68. package/dist/integrations/pi/components/shell-selectors-dialogs.js +4 -1
  69. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  70. package/dist/integrations/pi/components/skills-dialog.js +2 -2
  71. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -5
  72. package/dist/integrations/pi/components/upstream/components/owned-editor.js +15 -8
  73. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -3
  74. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +2 -2
  75. package/dist/integrations/pi/components/upstream/components/session-footer.js +26 -23
  76. package/dist/integrations/pi/components/upstream/components/session-selector.js +7 -6
  77. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  78. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +9 -8
  79. package/dist/integrations/pi/components/upstream/components/tool-execution.js +6 -4
  80. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +8 -8
  81. package/dist/integrations/pi/components/upstream/components/tree-selector.js +10 -10
  82. package/dist/integrations/pi/components/upstream/history/editor-core.js +2 -0
  83. package/dist/integrations/pi/engine/adapter.js +7 -2
  84. package/dist/integrations/pi/engine/ports.d.ts +1 -1
  85. package/dist/integrations/pi/engine/ports.js +4 -0
  86. package/dist/integrations/pi/engine/workflow-runner.js +9 -2
  87. package/dist/native/darwin-arm64/manifest.json +1 -1
  88. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  89. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  90. package/dist/native/darwin-x64/manifest.json +1 -1
  91. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  92. package/dist/native/darwin-x64/terminal-host.node +0 -0
  93. package/dist/native/linux-x64/manifest.json +1 -1
  94. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  95. package/dist/native/linux-x64/terminal-host.node +0 -0
  96. package/dist/native/win32-x64/manifest.json +2 -2
  97. package/dist/native/win32-x64/process-guardian.node +0 -0
  98. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  99. package/dist/native/win32-x64/terminal-host.node +0 -0
  100. package/dist/product-identity.d.ts +1 -1
  101. package/dist/product-identity.js +1 -1
  102. package/dist/product-identity.json +1 -0
  103. package/dist/ui/apps/host.d.ts +2 -0
  104. package/dist/ui/apps/host.js +7 -1
  105. package/dist/ui/components/dialog-panel.js +35 -30
  106. package/dist/ui/components/line-input.d.ts +3 -0
  107. package/dist/ui/components/line-input.js +3 -1
  108. package/dist/ui/components/prompt-input.d.ts +3 -2
  109. package/dist/ui/components/prompt-input.js +6 -4
  110. package/dist/ui/components/spans.js +2 -2
  111. package/dist/ui/components/surface.js +4 -2
  112. package/dist/ui/components/text.d.ts +3 -6
  113. package/dist/ui/components/text.js +6 -8
  114. package/dist/ui/settings/declarations.d.ts +54 -8
  115. package/dist/ui/settings/declarations.js +36 -7
  116. package/dist/ui/settings/manager.js +7 -3
  117. package/dist/ui/settings/migrations.js +15 -0
  118. package/dist/ui/settings/sections.d.ts +4 -0
  119. package/dist/ui/settings/sections.js +9 -3
  120. package/docs/architecture/resident-tabs.md +36 -8
  121. package/docs/architecture/tool-image-presentation.md +2 -1
  122. package/docs/features/prompt-history.md +41 -20
  123. package/docs/validation.md +25 -5
  124. package/package.json +1 -1
@@ -17,6 +17,7 @@ import { materializeRelease, readMaterializedRelease } from "./release-store.js"
17
17
  import { scheduleReleaseCleanup } from "./release-gc.js";
18
18
  import { UPDATE_ACTIVATION_CONTRACT, activateInstalledRelease, delegateActivation, readActivationContracts, } from "./update-activation.js";
19
19
  import { UpdateTransactionStore } from "./update-transaction.js";
20
+ import { acquireUpdateExecutionLease } from "./update-execution-lease.js";
20
21
  import { npmPrefixForGlobalRoot, removeUpdateRecoveryCapsule, resolveNpmCli, runProtectedPackageReplacement, updateLauncherPaths, updateNpmInstallArguments, } from "./update-recovery.js";
21
22
  export const PRODUCT_PACKAGE = PRODUCT_TEXT.packageName;
22
23
  const UPDATE_DIST_TAGS = { stable: "latest", next: "next" };
@@ -34,6 +35,7 @@ const defaultFileSystem = {
34
35
  * The update therefore keeps checking for this long before it calls the package locked.
35
36
  */
36
37
  export const PACKAGE_UNLOCK_PATIENCE_MS = 15_000;
38
+ export const UPDATE_ALREADY_RUNNING_EXIT_CODE = 75;
37
39
  const PACKAGE_UNLOCK_INITIAL_DELAY_MS = 100;
38
40
  const PACKAGE_UNLOCK_MAX_DELAY_MS = 1_000;
39
41
  // Platform: Windows reports a held directory as EPERM or EBUSY; EACCES and ENOTEMPTY are the other
@@ -85,6 +87,21 @@ export function createNpmProcessRunner(platform = process.platform) {
85
87
  * its own endpoint. A cohort running from the mutable installation is the one exception,
86
88
  * because that installation is exactly what is about to be replaced under it.
87
89
  */
90
+ async function verifyAutomaticUpdateSafety(paths, stateStore, environment) {
91
+ const state = await stateStore.read();
92
+ const owner = await readActiveEndpoint(paths, state.references.active, environment);
93
+ if (!owner)
94
+ return;
95
+ const ownership = await probeOwnership(owner.metadata);
96
+ if (ownership === "dead")
97
+ return;
98
+ if (ownership !== "live-verified") {
99
+ throw new UpdateAutomaticDeferred(PRODUCT_TEXT.diagnostic(`automatic update deferred because supervisor ownership is ${ownership}`));
100
+ }
101
+ if (!await canonicalImmutableRoot(paths.dataDir, owner.metadata.releaseRoot)) {
102
+ throw new UpdateAutomaticDeferred(PRODUCT_TEXT.diagnostic("automatic update deferred because the running session is not using a retained immutable release; run the update command manually"));
103
+ }
104
+ }
88
105
  export function planUpdateOwnership(ownership, runsFromRetainedRelease) {
89
106
  if (ownership === "dead")
90
107
  return "clean-dead-record";
@@ -111,13 +128,13 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
111
128
  const stateStore = new CohortStateStore(paths.dataDir);
112
129
  return {
113
130
  async targetIsActive(targetVersion) {
114
- const state = await stateStore.read();
115
- const activeId = state.references.active;
131
+ const state = await stateStore.read(), activeId = state.references.active;
116
132
  if (!activeId || state.releases[activeId]?.packageVersion !== targetVersion)
117
133
  return false;
118
134
  const endpoint = await readActiveEndpoint(paths, activeId, environment);
119
135
  return endpoint?.metadata.releaseId === activeId && await probeOwnership(endpoint.metadata) === "live-verified";
120
136
  },
137
+ async verifyAutomaticSafety() { await verifyAutomaticUpdateSafety(paths, stateStore, environment); },
121
138
  async shutdownVerifiedOwners(targetVersion) {
122
139
  const state = await stateStore.read();
123
140
  const activeId = state.references.active;
@@ -455,9 +472,58 @@ async function launchersOwnPackage(globalRoot, fileSystem) {
455
472
  return false;
456
473
  }
457
474
  }
475
+ async function resolveSelfUpdateTarget(input) {
476
+ if (input.exactTarget !== undefined && input.exactTarget.length > 0) {
477
+ return resolveExactAutomaticTarget(input.exactTarget, input.channel, input.output);
478
+ }
479
+ return input.requested === undefined || input.requested.length === 0
480
+ ? await resolveChannelHead(input.runner, input.distTag, input.output)
481
+ : await resolveRequestedPreview(input.runner, input.requested, input.output);
482
+ }
483
+ function resolveExactAutomaticTarget(target, channel, output) {
484
+ const version = validSemver(target);
485
+ const expected = channel === "stable" ? "stable" : "development";
486
+ if (version === null || releaseChannelOf(version) !== expected) {
487
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`refused invalid automatic ${expected} update target ${JSON.stringify(target)}`)}\n`);
488
+ return { version: null, exitCode: 1 };
489
+ }
490
+ return { version, exitCode: 0 };
491
+ }
458
492
  function reportUnmanagedInstallation(output, packageRoot, activeGlobalRoot) {
459
493
  output.stderr(`${PRODUCT_TEXT.diagnostic(`refused to update automatically because ${packageRoot} could not be verified as an npm-managed ${PRODUCT_PACKAGE} installation; npm's active global package root is ${activeGlobalRoot}. Run npm install --global ${PRODUCT_PACKAGE} to install it manually.`)}\n`);
460
494
  }
495
+ async function withUpdateExecutionLease(input, options, output, operation) {
496
+ const lease = await acquireUpdateExecutionLease(input);
497
+ if (lease.status === "busy") {
498
+ options.onLeaseBusy?.(lease.owner);
499
+ if ((options.mode ?? "manual") === "manual") {
500
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`update already in progress for ${lease.owner.targetVersion}`)}\n`);
501
+ }
502
+ return UPDATE_ALREADY_RUNNING_EXIT_CODE;
503
+ }
504
+ try {
505
+ options.onLeaseAcquired?.(lease.owner);
506
+ return await operation();
507
+ }
508
+ finally {
509
+ await lease.release();
510
+ }
511
+ }
512
+ async function readRunningPackageVersion(packageRoot, fileSystem, output) {
513
+ try {
514
+ const packageJson = JSON.parse(await fileSystem.readFile(resolve(packageRoot, "package.json")));
515
+ const version = typeof packageJson.version === "string" ? validSemver(packageJson.version) : null;
516
+ if (packageJson.name !== PRODUCT_PACKAGE)
517
+ throw new Error(`package.json does not identify ${PRODUCT_PACKAGE}`);
518
+ if (version === null)
519
+ throw new Error("package.json does not contain a valid semantic version");
520
+ return version;
521
+ }
522
+ catch (error) {
523
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
524
+ return null;
525
+ }
526
+ }
461
527
  export async function runSelfUpdate(options) {
462
528
  const fileSystem = options.fileSystem ?? defaultFileSystem;
463
529
  const output = options.output ?? defaultOutput;
@@ -474,24 +540,14 @@ export async function runSelfUpdate(options) {
474
540
  options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - startedAt) });
475
541
  }
476
542
  };
477
- let runningVersion;
478
- try {
479
- const packageJson = JSON.parse(await measure("package-version", async () => await fileSystem.readFile(resolve(options.packageRoot, "package.json"))));
480
- const parsedVersion = typeof packageJson.version === "string" ? validSemver(packageJson.version) : null;
481
- if (packageJson.name !== PRODUCT_PACKAGE)
482
- throw new Error(`package.json does not identify ${PRODUCT_PACKAGE}`);
483
- if (parsedVersion === null)
484
- throw new Error("package.json does not contain a valid semantic version");
485
- runningVersion = parsedVersion;
486
- }
487
- catch (error) {
488
- output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
543
+ const runningVersion = await measure("package-version", async () => await readRunningPackageVersion(options.packageRoot, fileSystem, output));
544
+ if (runningVersion === null)
489
545
  return 1;
490
- }
491
546
  const requested = options.target?.trim();
492
- const resolved = await measure("target-resolution", async () => requested === undefined || requested.length === 0
493
- ? await resolveChannelHead(runner, distTag, output)
494
- : await resolveRequestedPreview(runner, requested, output));
547
+ const exactTarget = options.resolvedTarget?.trim();
548
+ const resolved = await measure("target-resolution", async () => await resolveSelfUpdateTarget({
549
+ runner, output, channel, distTag, requested, exactTarget,
550
+ }));
495
551
  if (resolved.version === null)
496
552
  return resolved.exitCode;
497
553
  const targetVersion = resolved.version;
@@ -515,169 +571,176 @@ export async function runSelfUpdate(options) {
515
571
  const paths = resolveProductPaths(environment);
516
572
  const lifecycle = options.lifecycle ?? createUpdateLifecycleCoordinator(environment, fileSystem, output);
517
573
  const transactionStore = options.transactionStore ?? new UpdateTransactionStore(paths.dataDir);
574
+ const mode = options.mode ?? "manual";
518
575
  const maintenance = options.maintenance ?? (options.lifecycle
519
576
  ? async () => { }
520
577
  : async () => await scheduleReleaseCleanup(paths.dataDir, paths));
521
- let transaction = await transactionStore.read();
522
- let npmCli;
523
- try {
524
- if ((!transaction || transaction.status === "completed") && await lifecycle.targetIsActive(targetVersion)) {
525
- await maintenance();
526
- await transactionStore.clearCompleted();
527
- output.stdout(`${PRODUCT_TEXT.commandName} is up to date — no update needed.\n`);
528
- return 0;
529
- }
530
- const preflightRequired = options.npmCliResolver !== undefined
531
- || options.packageReplacement === undefined && options.runner === undefined;
532
- if (preflightRequired) {
533
- try {
534
- npmCli = await (options.npmCliResolver ?? resolveNpmCli)(npmCliRoot, environment);
578
+ return await withUpdateExecutionLease({ dataDir: paths.dataDir, packageRoot, channel, targetVersion, mode }, options, output, async () => {
579
+ let transaction = await transactionStore.read();
580
+ let npmCli;
581
+ try {
582
+ if (mode === "automatic")
583
+ await lifecycle.verifyAutomaticSafety?.();
584
+ if ((!transaction || transaction.status === "completed") && await lifecycle.targetIsActive(targetVersion)) {
585
+ await maintenance();
586
+ await transactionStore.clearCompleted();
587
+ output.stdout(`${PRODUCT_TEXT.commandName} is up to date — no update needed.\n`);
588
+ return 0;
535
589
  }
536
- catch (error) {
537
- throw new UpdatePreflightFailure(errorMessage(error));
590
+ const preflightRequired = options.npmCliResolver !== undefined
591
+ || options.packageReplacement === undefined && options.runner === undefined;
592
+ if (preflightRequired) {
593
+ try {
594
+ npmCli = await (options.npmCliResolver ?? resolveNpmCli)(npmCliRoot, environment);
595
+ }
596
+ catch (error) {
597
+ throw new UpdatePreflightFailure(errorMessage(error));
598
+ }
538
599
  }
539
- }
540
- // Rationale: the bar first appears only after every non-mutating replacement prerequisite passes.
541
- progress.set(3, 15);
542
- const cohortStore = new CohortStateStore(paths.dataDir);
543
- let cohortState = await cohortStore.read();
544
- let priorActiveReleaseId = cohortState.references.active;
545
- if (options.runner === undefined && (priorActiveReleaseId === null || cohortState.releases[priorActiveReleaseId]?.approval !== "approved")) {
546
- // Invariant: protected replacement needs an immutable launch authority that npm cannot rename.
547
- const recoveryRelease = await materializeRelease(packageRoot, paths.dataDir);
548
- await cohortStore.recordCandidate(recoveryRelease);
549
- const diagnostics = await certifyMaterializedRelease(recoveryRelease, paths.dataDir);
550
- await cohortStore.approve(recoveryRelease.releaseId, diagnostics);
551
- await cohortStore.activate(recoveryRelease.releaseId);
552
- cohortState = await cohortStore.read();
553
- priorActiveReleaseId = recoveryRelease.releaseId;
554
- }
555
- transaction = await transactionStore.begin({
556
- channel,
557
- targetVersion,
558
- packageRoot,
559
- priorActiveReleaseId,
560
- });
561
- if (phaseBefore(transaction.phase, "ownership-released")) {
562
- await measure("ownership-release", async () => {
563
- await lifecycle.shutdownVerifiedOwners(targetVersion);
564
- await lifecycle.verifyPackageUnlocked(packageRoot);
600
+ // Rationale: the bar first appears only after every non-mutating replacement prerequisite passes.
601
+ progress.set(3, 15);
602
+ const cohortStore = new CohortStateStore(paths.dataDir);
603
+ let cohortState = await cohortStore.read();
604
+ let priorActiveReleaseId = cohortState.references.active;
605
+ if (options.runner === undefined && (priorActiveReleaseId === null || cohortState.releases[priorActiveReleaseId]?.approval !== "approved")) {
606
+ // Invariant: protected replacement needs an immutable launch authority that npm cannot rename.
607
+ const recoveryRelease = await materializeRelease(packageRoot, paths.dataDir);
608
+ await cohortStore.recordCandidate(recoveryRelease);
609
+ const diagnostics = await certifyMaterializedRelease(recoveryRelease, paths.dataDir);
610
+ await cohortStore.approve(recoveryRelease.releaseId, diagnostics);
611
+ await cohortStore.activate(recoveryRelease.releaseId);
612
+ cohortState = await cohortStore.read();
613
+ priorActiveReleaseId = recoveryRelease.releaseId;
614
+ }
615
+ transaction = await transactionStore.begin({
616
+ channel,
617
+ targetVersion,
618
+ packageRoot,
619
+ priorActiveReleaseId,
565
620
  });
566
- transaction = await transactionStore.advance("ownership-released");
567
- }
568
- progress.set(15, 70);
569
- if (phaseBefore(transaction.phase, "package-installed")) {
570
- if (options.packageReplacement || options.runner === undefined) {
571
- const priorReleaseId = transaction.priorActiveReleaseId ?? priorActiveReleaseId;
572
- const recordedPrior = priorReleaseId === null ? undefined : cohortState.releases[priorReleaseId];
573
- if ((!recordedPrior || recordedPrior.approval !== "approved") && !options.packageReplacement) {
574
- throw new Error(PRODUCT_TEXT.diagnostic("cannot protect package replacement because no approved prior release is available"));
621
+ if (phaseBefore(transaction.phase, "ownership-released")) {
622
+ await measure("ownership-release", async () => {
623
+ await lifecycle.shutdownVerifiedOwners(targetVersion);
624
+ await lifecycle.verifyPackageUnlocked(packageRoot);
625
+ });
626
+ transaction = await transactionStore.advance("ownership-released");
627
+ }
628
+ progress.set(15, 70);
629
+ if (phaseBefore(transaction.phase, "package-installed")) {
630
+ if (options.packageReplacement || options.runner === undefined) {
631
+ const priorReleaseId = transaction.priorActiveReleaseId ?? priorActiveReleaseId;
632
+ const recordedPrior = priorReleaseId === null ? undefined : cohortState.releases[priorReleaseId];
633
+ if ((!recordedPrior || recordedPrior.approval !== "approved") && !options.packageReplacement) {
634
+ throw new Error(PRODUCT_TEXT.diagnostic("cannot protect package replacement because no approved prior release is available"));
635
+ }
636
+ const priorRelease = recordedPrior ?? {
637
+ releaseId: priorReleaseId ?? "injected-prior-release",
638
+ releaseRoot: packageRoot,
639
+ contentDigest: "0".repeat(64),
640
+ };
641
+ const replacementTransaction = transaction;
642
+ const replacement = await measure("npm-install", async () => await (options.packageReplacement ?? runProtectedPackageReplacement)({
643
+ dataDir: paths.dataDir,
644
+ globalRoot,
645
+ npmCliRoot,
646
+ ...(npmCli === undefined ? {} : { npmCli }),
647
+ packageRoot,
648
+ transaction: replacementTransaction,
649
+ priorRelease,
650
+ output,
651
+ environment,
652
+ onRecoveryState: async (recovery) => {
653
+ if (transactionStore.setRecovery)
654
+ transaction = await transactionStore.setRecovery(recovery) ?? transaction;
655
+ },
656
+ }));
657
+ // Rationale: npm's own text explains a failed replacement and says nothing about a
658
+ // successful one, so it reaches the terminal only when the installation did not happen.
659
+ if (replacement.outcome !== "installed")
660
+ reportChildDiagnostics(output, replacement);
661
+ if (replacement.outcome === "installed")
662
+ transaction = await transactionStore.advance("package-installed");
663
+ if (replacement.cancelled) {
664
+ progress.clear();
665
+ output.stderr(`${PRODUCT_TEXT.diagnostic("update cancelled safely; the launcher is available and the transaction can be resumed.")}\n`);
666
+ return 130;
667
+ }
668
+ if (replacement.outcome !== "installed") {
669
+ throw new UpdateFailure(unsuccessfulCode(replacement.npmExitCode), `npm exited with status ${formatExitCode(replacement.npmExitCode)}`);
670
+ }
575
671
  }
576
- const priorRelease = recordedPrior ?? {
577
- releaseId: priorReleaseId ?? "injected-prior-release",
578
- releaseRoot: packageRoot,
579
- contentDigest: "0".repeat(64),
580
- };
581
- const replacementTransaction = transaction;
582
- const replacement = await measure("npm-install", async () => await (options.packageReplacement ?? runProtectedPackageReplacement)({
583
- dataDir: paths.dataDir,
584
- globalRoot,
585
- npmCliRoot,
586
- ...(npmCli === undefined ? {} : { npmCli }),
587
- packageRoot,
588
- transaction: replacementTransaction,
589
- priorRelease,
590
- output,
591
- environment,
592
- onRecoveryState: async (recovery) => {
593
- if (transactionStore.setRecovery)
594
- transaction = await transactionStore.setRecovery(recovery) ?? transaction;
595
- },
596
- }));
597
- // Rationale: npm's own text explains a failed replacement and says nothing about a
598
- // successful one, so it reaches the terminal only when the installation did not happen.
599
- if (replacement.outcome !== "installed")
600
- reportChildDiagnostics(output, replacement);
601
- if (replacement.outcome === "installed")
672
+ else {
673
+ const installation = await measure("npm-install", async () => await runNpm(runner, updateNpmInstallArguments(globalRoot, targetVersion), true, output, "start the global npm installation", false));
674
+ if (installation.result === null)
675
+ throw new UpdateFailure(installation.exitCode, "npm process failed");
676
+ if (installation.result.code !== 0) {
677
+ reportChildDiagnostics(output, installation.result);
678
+ throw new UpdateFailure(unsuccessfulCode(installation.result.code), `npm exited with status ${formatExitCode(installation.result.code)}`);
679
+ }
602
680
  transaction = await transactionStore.advance("package-installed");
603
- if (replacement.cancelled) {
604
- progress.clear();
605
- output.stderr(`${PRODUCT_TEXT.diagnostic("update cancelled safely; the launcher is available and the transaction can be resumed.")}\n`);
606
- return 130;
607
- }
608
- if (replacement.outcome !== "installed") {
609
- throw new UpdateFailure(unsuccessfulCode(replacement.npmExitCode), `npm exited with status ${formatExitCode(replacement.npmExitCode)}`);
610
681
  }
611
682
  }
612
- else {
613
- const installation = await measure("npm-install", async () => await runNpm(runner, updateNpmInstallArguments(globalRoot, targetVersion), true, output, "start the global npm installation", false));
614
- if (installation.result === null)
615
- throw new UpdateFailure(installation.exitCode, "npm process failed");
616
- if (installation.result.code !== 0) {
617
- reportChildDiagnostics(output, installation.result);
618
- throw new UpdateFailure(unsuccessfulCode(installation.result.code), `npm exited with status ${formatExitCode(installation.result.code)}`);
683
+ progress.set(70, 75);
684
+ // Concurrency: ownership can be reacquired after an interrupted installation (for
685
+ // example, if bare A1 is launched before the update is resumed). Recheck
686
+ // immediately before activation so recovery cannot start a second cohort.
687
+ await measure("ownership-release", async () => { await lifecycle.shutdownVerifiedOwners(targetVersion); });
688
+ progress.set(75, MATERIALIZE_PROGRESS.from);
689
+ let activationPhaseStartedAt = now();
690
+ await lifecycle.activateInstalled(packageRoot, targetVersion, async (phase) => {
691
+ options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - activationPhaseStartedAt) });
692
+ transaction = await transactionStore.advance(phase);
693
+ progress.set(ACTIVATION_PROGRESS[phase].at, ACTIVATION_PROGRESS[phase].creepTo);
694
+ activationPhaseStartedAt = now();
695
+ }, ({ completed, total }) => {
696
+ const span = MATERIALIZE_PROGRESS.to - MATERIALIZE_PROGRESS.from;
697
+ const done = total > 0 ? Math.min(1, completed / total) : 0;
698
+ progress.set(MATERIALIZE_PROGRESS.from + span * done);
699
+ }, state => {
700
+ if (state === "started") {
701
+ activationPhaseStartedAt = now();
702
+ progress.set(98, 99);
619
703
  }
620
- transaction = await transactionStore.advance("package-installed");
621
- }
704
+ else {
705
+ options.onPhaseTiming?.({ phase: "warmup", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
706
+ activationPhaseStartedAt = now();
707
+ progress.set(99);
708
+ }
709
+ });
710
+ options.onPhaseTiming?.({ phase: "supervisor-verified", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
711
+ const transactionStartedAt = now();
712
+ await transactionStore.advance("supervisor-verified");
713
+ if (transaction.recovery?.capsulePath)
714
+ await removeUpdateRecoveryCapsule(paths.dataDir, transaction.transactionId);
715
+ // Invariant: successful output follows the durable cleanup disposition. Slow recursive
716
+ // removal belongs to the detached worker started by this maintenance coordinator.
717
+ await maintenance();
718
+ // Concurrency: this is the launch cutover. No failure-prone update work follows success.
719
+ await transactionStore.finish("completed");
720
+ await transactionStore.clearCompleted().catch(() => undefined);
721
+ options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
722
+ progress.finish();
723
+ output.stdout(`${PRODUCT_TEXT.commandName} updated successfully to ${targetVersion}\n`);
724
+ return 0;
622
725
  }
623
- progress.set(70, 75);
624
- // Concurrency: ownership can be reacquired after an interrupted installation (for
625
- // example, if bare A1 is launched before the update is resumed). Recheck
626
- // immediately before activation so recovery cannot start a second cohort.
627
- await measure("ownership-release", async () => { await lifecycle.shutdownVerifiedOwners(targetVersion); });
628
- progress.set(75, MATERIALIZE_PROGRESS.from);
629
- let activationPhaseStartedAt = now();
630
- await lifecycle.activateInstalled(packageRoot, targetVersion, async (phase) => {
631
- options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - activationPhaseStartedAt) });
632
- transaction = await transactionStore.advance(phase);
633
- progress.set(ACTIVATION_PROGRESS[phase].at, ACTIVATION_PROGRESS[phase].creepTo);
634
- activationPhaseStartedAt = now();
635
- }, ({ completed, total }) => {
636
- const span = MATERIALIZE_PROGRESS.to - MATERIALIZE_PROGRESS.from;
637
- const done = total > 0 ? Math.min(1, completed / total) : 0;
638
- progress.set(MATERIALIZE_PROGRESS.from + span * done);
639
- }, state => {
640
- if (state === "started") {
641
- activationPhaseStartedAt = now();
642
- progress.set(98, 99);
643
- }
644
- else {
645
- options.onPhaseTiming?.({ phase: "warmup", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
646
- activationPhaseStartedAt = now();
647
- progress.set(99);
726
+ catch (error) {
727
+ progress.clear();
728
+ const message = errorMessage(error);
729
+ if (error instanceof UpdateAutomaticDeferred)
730
+ return 1;
731
+ if (error instanceof UpdatePreflightFailure) {
732
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`update could not start safely: ${message}. Nothing was changed. Retry with npm x -y -- ${PRODUCT_PACKAGE}-install`)}\n`);
733
+ return 1;
648
734
  }
649
- });
650
- options.onPhaseTiming?.({ phase: "supervisor-verified", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
651
- const transactionStartedAt = now();
652
- await transactionStore.advance("supervisor-verified");
653
- if (transaction.recovery?.capsulePath)
654
- await removeUpdateRecoveryCapsule(paths.dataDir, transaction.transactionId);
655
- // Invariant: successful output follows the durable cleanup disposition. Slow recursive
656
- // removal belongs to the detached worker started by this maintenance coordinator.
657
- await maintenance();
658
- // Concurrency: this is the launch cutover. No failure-prone update work follows success.
659
- await transactionStore.finish("completed");
660
- await transactionStore.clearCompleted().catch(() => undefined);
661
- options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
662
- progress.finish();
663
- output.stdout(`${PRODUCT_TEXT.commandName} updated successfully to ${targetVersion}\n`);
664
- return 0;
665
- }
666
- catch (error) {
667
- progress.clear();
668
- const message = errorMessage(error);
669
- if (error instanceof UpdatePreflightFailure) {
670
- output.stderr(`${PRODUCT_TEXT.diagnostic(`update could not start safely: ${message}. Nothing was changed. Retry with npm x -y -- ${PRODUCT_PACKAGE}-install`)}\n`);
671
- return 1;
735
+ const rollback = options.lifecycle
736
+ ? "previous test lifecycle retained"
737
+ : await rollbackPriorCohort(paths.dataDir, environment, transaction?.priorActiveReleaseId ?? null).catch(rollbackError => `rollback failed: ${errorMessage(rollbackError)}`);
738
+ if (transaction)
739
+ await transactionStore.finish(rollback === "rolled back" ? "rolled-back" : "failed", `${message}; ${rollback}`);
740
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`update failed: ${message}. ${rollback}. Diagnostics: ${transactionStore.path}`)}\n`);
741
+ return error instanceof UpdateFailure ? error.exitCode : 1;
672
742
  }
673
- const rollback = options.lifecycle
674
- ? "previous test lifecycle retained"
675
- : await rollbackPriorCohort(paths.dataDir, environment, transaction?.priorActiveReleaseId ?? null).catch(rollbackError => `rollback failed: ${errorMessage(rollbackError)}`);
676
- if (transaction)
677
- await transactionStore.finish(rollback === "rolled back" ? "rolled-back" : "failed", `${message}; ${rollback}`);
678
- output.stderr(`${PRODUCT_TEXT.diagnostic(`update failed: ${message}. ${rollback}. Diagnostics: ${transactionStore.path}`)}\n`);
679
- return error instanceof UpdateFailure ? error.exitCode : 1;
680
- }
743
+ });
681
744
  }
682
745
  export function assertUpdatePerformanceBudget(evidence, maximumPostNpmDurationMs = 30_000) {
683
746
  const failures = [];
@@ -777,6 +840,8 @@ class UpdateFailure extends Error {
777
840
  }
778
841
  class UpdatePreflightFailure extends Error {
779
842
  }
843
+ class UpdateAutomaticDeferred extends Error {
844
+ }
780
845
  async function canonicalImmutableRoot(dataDir, releaseRoot) {
781
846
  try {
782
847
  const [store, selected] = await Promise.all([realpath(resolve(dataDir, "releases")), realpath(releaseRoot)]);
@@ -1,4 +1,6 @@
1
+ import { type ResidentTabRoute } from "./routes.js";
1
2
  export type ResidentTabStatus = "starting" | "working" | "needs-input" | "done-unseen" | "done" | "aborted" | "error" | "idle" | "unresponsive" | "crashed" | "restarting" | "failed";
3
+ export { residentUtilityRoute, type ResidentTabRoute } from "./routes.js";
2
4
  export interface TabBridgeCredentials {
3
5
  readonly tabId: string;
4
6
  readonly token: string;
@@ -17,7 +19,7 @@ export type TabBridgeCommand = {
17
19
  readonly nameSource: "default" | "user" | "auto";
18
20
  } | {
19
21
  readonly type: "route";
20
- readonly route: "settings";
22
+ readonly route: ResidentTabRoute;
21
23
  } | {
22
24
  readonly type: "stop";
23
25
  readonly deadlineMs: number;
@@ -59,5 +61,9 @@ export declare class TabBridgeClient {
59
61
  requestClose(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
60
62
  consumeControllerBoundaryInput(data: string): string;
61
63
  close(): void;
64
+ /**
65
+ * Asks the server to act for the client that controls this tab, as `origin`: leave it, restart or close the tab,
66
+ * or open the tab bar's Plus Menu dialog (`configure-plus`).
67
+ */
68
+ requestAction(action: "detach" | "restart" | "close" | "configure-plus", origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
62
69
  }
63
- export {};
@@ -1,5 +1,8 @@
1
1
  import { connect } from "node:net";
2
2
  import { encodeResidentFrame, ResidentFrameDecoder } from "./protocol.js";
3
+ import { RESIDENT_TAB_ROUTES } from "./routes.js";
4
+ export { residentUtilityRoute } from "./routes.js";
5
+ const RESIDENT_TAB_ROUTE_SET = new Set(RESIDENT_TAB_ROUTES);
3
6
  /** Metadata-only advisory bridge. Terminal input and transcript bytes have no API here. */
4
7
  export class TabBridgeClient {
5
8
  #options;
@@ -118,13 +121,13 @@ export class TabBridgeClient {
118
121
  this.#send("bridge.shortcuts", { assignments: this.#latestShortcuts.map(([action, key]) => [action, key]) });
119
122
  }
120
123
  requestDetach(origin = this.captureAttachment(), timeoutMs = 1_000) {
121
- return this.#requestAction("detach", origin, timeoutMs);
124
+ return this.requestAction("detach", origin, timeoutMs);
122
125
  }
123
126
  requestRestart(origin = this.captureAttachment(), timeoutMs = 1_000) {
124
- return this.#requestAction("restart", origin, timeoutMs);
127
+ return this.requestAction("restart", origin, timeoutMs);
125
128
  }
126
129
  requestClose(origin = this.captureAttachment(), timeoutMs = 1_000) {
127
- return this.#requestAction("close", origin, timeoutMs);
130
+ return this.requestAction("close", origin, timeoutMs);
128
131
  }
129
132
  consumeControllerBoundaryInput(data) {
130
133
  const prefix = "\x1bP+a1;controller=";
@@ -232,8 +235,8 @@ export class TabBridgeClient {
232
235
  this.#emitCommand({ type: "rename", name: message.body["name"], nameSource: source === "auto" || source === "default" ? source : "user" });
233
236
  return;
234
237
  }
235
- if (message.type === "bridge.route" && message.body["route"] === "settings") {
236
- this.#emitCommand({ type: "route", route: "settings" });
238
+ if (message.type === "bridge.route" && RESIDENT_TAB_ROUTE_SET.has(message.body["route"])) {
239
+ this.#emitCommand({ type: "route", route: message.body["route"] });
237
240
  return;
238
241
  }
239
242
  if (message.type === "bridge.stop" && typeof message.body["deadlineMs"] === "number") {
@@ -255,7 +258,11 @@ export class TabBridgeClient {
255
258
  }
256
259
  }
257
260
  }
258
- async #requestAction(action, origin, timeoutMs) {
261
+ /**
262
+ * Asks the server to act for the client that controls this tab, as `origin`: leave it, restart or close the tab,
263
+ * or open the tab bar's Plus Menu dialog (`configure-plus`).
264
+ */
265
+ async requestAction(action, origin = this.captureAttachment(), timeoutMs = 1_000) {
259
266
  if (!this.available || origin === null || (action === "restart" && !this.#supports("bridge-restart")))
260
267
  return false;
261
268
  const requestId = ++this.#request;
@@ -0,0 +1,34 @@
1
+ /** Payload-free fixture schema; type-only imports keep it outside the evaluated startup graph. */
2
+ export type ResidentInputPhase = "receipt" | "semantic-start" | "semantic-end" | "composition-start" | "composition-end" | "write-start" | "write-end";
3
+ export interface ProgressFields {
4
+ readonly eventSequence?: number;
5
+ readonly revision?: number;
6
+ readonly geometryGeneration?: number;
7
+ readonly bytes?: number;
8
+ readonly count?: number;
9
+ readonly eventKind?: string;
10
+ readonly frameId?: number;
11
+ readonly renderCause?: string;
12
+ readonly fragmentCount?: number;
13
+ readonly transactionKind?: "synchronized-output" | "ordinary";
14
+ }
15
+ export interface ProgressRecord extends ProgressFields {
16
+ readonly schema: "a1-resident-progress-observation-v1";
17
+ readonly runId: string;
18
+ readonly role: "child";
19
+ readonly stage: "engine-delivery" | "child-render-write" | "child-input-phase";
20
+ readonly pid: number;
21
+ readonly processInstance: string;
22
+ readonly timestampUnixUs: number;
23
+ readonly clockErrorUs: number;
24
+ readonly sequence: number;
25
+ readonly tabId: string;
26
+ readonly incarnation: number;
27
+ readonly lostBefore: number;
28
+ }
29
+ export interface ResidentProgressObserverPort {
30
+ readonly tabId: string;
31
+ engineDelivery(event: unknown): void;
32
+ inputPhase?(phase: ResidentInputPhase, revision: number): void;
33
+ childRenderWrite(bytes: number, fragments: number, transaction: "synchronized-output" | "ordinary", frameId?: number, renderCause?: string): void;
34
+ }
@@ -1,7 +1,4 @@
1
- export interface ResidentProgressObserverPort {
2
- readonly tabId: string;
3
- engineDelivery(event: unknown): void;
4
- childRenderWrite(bytes: number, fragments: number, transaction: "synchronized-output" | "ordinary", frameId?: number, renderCause?: string): void;
5
- }
6
- /** Returns the opt-in payload-free observer for a resident child, or null on ordinary launches. */
1
+ import type { ResidentProgressObserverPort } from "./progress-observation-schema.js";
2
+ export type { ResidentProgressObserverPort } from "./progress-observation-schema.js";
3
+ /** Opt-in payload-free child observer; ordinary launches return null. */
7
4
  export declare function residentProgressObserver(environment?: NodeJS.ProcessEnv): ResidentProgressObserverPort | null;