@timurproko/a1 0.2.7-dev.754 → 0.2.7-dev.767

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 (149) hide show
  1. package/bin/guardian.js +5 -0
  2. package/bin/tab-launch-context.d.ts +23 -0
  3. package/bin/tab-launch-context.js +45 -0
  4. package/bin/ui.js +20 -5
  5. package/dist/app/session-shell/helper-pool.d.ts +3 -5
  6. package/dist/app/session-shell/helper-pool.js +3 -5
  7. package/dist/app/session-shell/session-shell-root.d.ts +29 -1
  8. package/dist/app/session-shell/session-shell-root.js +3 -2
  9. package/dist/app/session-shell/session-shell.js +153 -30
  10. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -1
  11. package/dist/app/session-shell/session-viewport-controller.js +3 -3
  12. package/dist/composition/owned-ui.d.ts +20 -0
  13. package/dist/composition/owned-ui.js +76 -3
  14. package/dist/contracts/owned-ui/session-backend.d.ts +1 -0
  15. package/dist/features/owned-ui/settings-app.js +8 -35
  16. package/dist/foundation/launch-context/index.d.ts +6 -0
  17. package/dist/foundation/launch-context/index.js +15 -0
  18. package/dist/foundation/launch-guardian/main.d.ts +5 -0
  19. package/dist/foundation/launch-guardian/main.js +2 -1
  20. package/dist/foundation/release/bootstrap.d.ts +4 -0
  21. package/dist/foundation/release/bootstrap.js +44 -6
  22. package/dist/foundation/release/dependency-certification.js +54 -19
  23. package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
  24. package/dist/foundation/release/progress-palette.generated.js +2 -2
  25. package/dist/foundation/resident-tabs/bridge-client.d.ts +41 -0
  26. package/dist/foundation/resident-tabs/bridge-client.js +199 -0
  27. package/dist/foundation/resident-tabs/draft-checkpoint.d.ts +27 -0
  28. package/dist/foundation/resident-tabs/draft-checkpoint.js +145 -0
  29. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +30 -0
  30. package/dist/foundation/resident-tabs/ensure-terminal-host.js +121 -0
  31. package/dist/foundation/resident-tabs/fault-injection.d.ts +8 -0
  32. package/dist/foundation/resident-tabs/fault-injection.js +26 -0
  33. package/dist/foundation/resident-tabs/index.d.ts +6 -0
  34. package/dist/foundation/resident-tabs/index.js +3 -0
  35. package/dist/foundation/resident-tabs/prompt-journal.d.ts +65 -0
  36. package/dist/foundation/resident-tabs/prompt-journal.js +214 -0
  37. package/dist/foundation/resident-tabs/protocol.d.ts +12 -0
  38. package/dist/foundation/resident-tabs/protocol.js +195 -0
  39. package/dist/foundation/resident-tabs/recovery.d.ts +26 -0
  40. package/dist/foundation/resident-tabs/recovery.js +205 -0
  41. package/dist/foundation/resident-tabs/routing.d.ts +10 -0
  42. package/dist/foundation/resident-tabs/routing.js +8 -0
  43. package/dist/foundation/resident-tabs/session-sync.d.ts +14 -0
  44. package/dist/foundation/resident-tabs/session-sync.js +37 -0
  45. package/dist/foundation/resident-tabs/settings.d.ts +9 -0
  46. package/dist/foundation/resident-tabs/settings.js +65 -0
  47. package/dist/foundation/session-writer-lock/index.d.ts +27 -0
  48. package/dist/foundation/session-writer-lock/index.js +197 -0
  49. package/dist/foundation/startup/startup-descriptor.js +2 -2
  50. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  51. package/dist/foundation/terminal-cleanup/index.js +1 -1
  52. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +1 -0
  53. package/dist/foundation/terminal-cleanup/terminal-reset.js +1 -0
  54. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  55. package/dist/integrations/pi/components/owned-editor-ux.js +2 -2
  56. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +4 -4
  57. package/dist/integrations/pi/components/resources/builtin-themes.js +4 -4
  58. package/dist/integrations/pi/components/shell-shared-facade.d.ts +1 -1
  59. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  60. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +17 -4
  61. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  62. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  63. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  64. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  65. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  66. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  67. package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
  68. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  69. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
  70. package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
  71. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  72. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  73. package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
  74. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  75. package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
  76. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
  77. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
  78. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +2 -2
  79. package/dist/integrations/pi/components/upstream/components/status-indicator.js +3 -3
  80. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  81. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  82. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
  83. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
  84. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +0 -2
  85. package/dist/integrations/pi/components/upstream/components/tool-execution.js +41 -20
  86. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  87. package/dist/integrations/pi/components/upstream/components/tree-selector.js +1 -1
  88. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  89. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  90. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +2 -2
  91. package/dist/integrations/pi/components/upstream/history/editor-core.js +5 -5
  92. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  93. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  94. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  95. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  96. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  97. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  98. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  99. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  100. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  101. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  102. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +3 -0
  103. package/dist/integrations/pi/components/upstream/theme/theme.js +24 -1
  104. package/dist/integrations/pi/engine/adapter.js +1 -0
  105. package/dist/integrations/pi/engine/pi-settings-metadata.json +2 -2
  106. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  107. package/dist/integrations/pi/engine/runtime-integration.js +102 -29
  108. package/dist/integrations/pi/engine/session-runtime.d.ts +0 -6
  109. package/dist/integrations/pi/engine/session-runtime.js +8 -3
  110. package/dist/integrations/pi/engine/session-selection.d.ts +3 -0
  111. package/dist/integrations/pi/engine/session-selection.js +45 -9
  112. package/dist/integrations/pi/engine/workflow-contexts.js +10 -1
  113. package/dist/integrations/pi/startup-public.js +152 -222
  114. package/dist/integrations/pi/startup-public.manifest.json +266 -177
  115. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  116. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
  117. package/dist/native/darwin-arm64/manifest.json +1 -1
  118. package/dist/native/linux-x64/manifest.json +1 -1
  119. package/dist/native/win32-x64/manifest.json +2 -2
  120. package/dist/native/win32-x64/process-guardian.exe +0 -0
  121. package/dist/product-identity.d.ts +1 -1
  122. package/dist/product-identity.js +1 -1
  123. package/dist/product-identity.json +5 -0
  124. package/dist/runtime-payload-inventory.json +5349 -6035
  125. package/dist/ui/components/line-input.d.ts +1 -1
  126. package/dist/ui/components/prompt-input.d.ts +7 -5
  127. package/dist/ui/components/prompt-input.js +7 -7
  128. package/dist/ui/components/transcript-viewport.d.ts +6 -3
  129. package/dist/ui/components/transcript-viewport.js +13 -15
  130. package/dist/ui/settings/declarations.d.ts +49 -1
  131. package/dist/ui/settings/declarations.js +37 -1
  132. package/dist/ui/settings/index.d.ts +5 -3
  133. package/dist/ui/settings/index.js +3 -2
  134. package/dist/ui/settings/manager.d.ts +9 -3
  135. package/dist/ui/settings/manager.js +81 -29
  136. package/dist/ui/settings/migrations.d.ts +6 -0
  137. package/dist/ui/settings/migrations.js +32 -0
  138. package/dist/ui/settings/profile-document.d.ts +26 -0
  139. package/dist/ui/settings/profile-document.js +164 -0
  140. package/dist/ui/settings/resolution.d.ts +2 -0
  141. package/dist/ui/settings/resolution.js +13 -1
  142. package/dist/ui/settings/sections.js +4 -0
  143. package/docs/architecture/internal-naming.md +1 -1
  144. package/docs/architecture/resident-tabs-requirements.md +580 -0
  145. package/docs/architecture/resident-tabs.md +522 -0
  146. package/docs/architecture/terminal-host-proof-gate.md +2 -0
  147. package/docs/openspec-archive-automation.md +17 -16
  148. package/docs/validation.md +1 -1
  149. package/package.json +6 -4
@@ -4,7 +4,7 @@ import { PRODUCT_IDENTITY } from "../product-identity.js";
4
4
  import { PromptHistoryService } from "../features/prompt-history/service.js";
5
5
  import { PromptImageSidecar } from "../features/prompt-history/image-sidecar.js";
6
6
  import { resolvePromptHistoryPath } from "../features/prompt-history/paths.js";
7
- import { resolvePromptHistoryDataDir } from "../features/launch/profile-paths.js";
7
+ import { resolveLaunchProfilePaths, resolvePromptHistoryDataDir } from "../features/launch/profile-paths.js";
8
8
  import { resolveProductPaths } from "../foundation/lifecycle/paths.js";
9
9
  import { applyConfiguredPiTheme, getAvailablePiThemes, setPiAccentColor, setPiPackageBorderProjectionEnabled } from "../integrations/pi/components/upstream/theme/theme.js";
10
10
  import { createPiEngineHost } from "../integrations/pi/engine/host.js";
@@ -18,7 +18,13 @@ import { createPiTerminalBridge } from "../integrations/pi/tui-runtime/presentat
18
18
  import { createOwnedRouteHost } from "./settings-route-host.js";
19
19
  import { renderPiShellChangelogLines } from "../integrations/pi/components/shell-presenters-info.js";
20
20
  import { nativeHyperlinkStyle } from "../ui/components/spans.js";
21
+ import { ResidentTabRecovery } from "../foundation/resident-tabs/recovery.js";
21
22
  const SESSION_RUNTIME_REFRESH_MS = 30_000;
23
+ export function residentStartupPresentation(resident, fallbackNotice) {
24
+ if (resident)
25
+ return { startup: { quiet: true } };
26
+ return fallbackNotice === undefined ? {} : { startup: { notices: [{ kind: "warning", message: fallbackNotice }] } };
27
+ }
22
28
  export async function composeOwnedUiApplication(options = {}) {
23
29
  return (await composeOwnedUi(options)).application;
24
30
  }
@@ -82,13 +88,16 @@ export async function composeOwnedUi(options = {}) {
82
88
  sessionRuntimeRefreshTimer.unref();
83
89
  };
84
90
  const productPaths = resolveProductPaths();
91
+ const profilePaths = resolveLaunchProfilePaths();
85
92
  // Rationale: the manager reads the engine's settings port lazily, after the session exists.
86
93
  let backend;
87
94
  const settings = options.profileId === undefined
88
95
  ? null
89
96
  : new OwnedSettingsManager({
90
- configDir: resolveProductPaths().configDir,
97
+ profileDir: profilePaths.agentProfile,
91
98
  profileId: options.profileId,
99
+ namespace: PRODUCT_IDENTITY.commandName,
100
+ legacyConfigDir: productPaths.configDir,
92
101
  agentProvider: () => backend.settingsPort(),
93
102
  hiddenAgentSettingIds: ["fullscreenWheelScrollLines"],
94
103
  agentSettingLabelOverrides: { fullscreenCopyOnSelect: "Copy on select" },
@@ -120,6 +129,18 @@ export async function composeOwnedUi(options = {}) {
120
129
  await host.dispose();
121
130
  throw error;
122
131
  }
132
+ const tabBridge = options.tab?.bridge ?? null;
133
+ let recoveryDegraded = false;
134
+ let presentRecoveryDegraded = (_degraded) => { };
135
+ const tabRecovery = options.tab === undefined ? null : new ResidentTabRecovery(options.tab.dataDir, {
136
+ onDegraded: degraded => {
137
+ recoveryDegraded = degraded;
138
+ options.tab.bridge.degraded(degraded);
139
+ presentRecoveryDegraded(degraded);
140
+ },
141
+ });
142
+ const recoveredTabInput = await tabRecovery?.initialize(currentBackendSessionFile(backend), backend.identity.sessionId);
143
+ const unsubscribeTabBridge = tabBridge === null ? () => { } : await bindResidentTabBridge(tabBridge, backend, tabRecovery);
123
144
  let releaseNotes = null;
124
145
  let releaseNotesFailure;
125
146
  if (ownedSurfaces) {
@@ -228,13 +249,26 @@ export async function composeOwnedUi(options = {}) {
228
249
  },
229
250
  }),
230
251
  ...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
252
+ ...(tabBridge === null || tabRecovery === null ? {} : { residentTab: {
253
+ requestDetach: () => tabBridge.requestDetach(),
254
+ requestRestart: () => tabBridge.requestRestart(),
255
+ reportStatus: status => tabBridge.status(status),
256
+ stallNoticeSeconds: settings?.value("tabsStallNoticeSeconds") ?? 300,
257
+ admit: input => tabRecovery.admit({ ...input, incarnation: options.tab.incarnation }),
258
+ dispatched: () => tabRecovery.dispatched(),
259
+ correlated: (submissionId, entryId) => tabRecovery.correlated(submissionId, entryId),
260
+ checkpoint: text => tabRecovery.checkpoint(text),
261
+ gracefulStop: sessionFile => tabRecovery.gracefulStop(sessionFile),
262
+ ...(recoveredTabInput === undefined ? {} : { recovered: recoveredTabInput }),
263
+ } }),
231
264
  },
232
265
  presentation: {
233
266
  ...(options.terminal === undefined ? {} : { terminal: createPiTerminalBridge(options.terminal) }),
267
+ ...residentStartupPresentation(tabBridge !== null, options.residentFallbackNotice),
234
268
  ...(clipboardDiagnostics === null ? {} : { input: { onEvent: event => clipboardDiagnostics.runtime(event) } }),
235
269
  ...(viewportSettings === null ? {} : { viewportSettings }),
236
270
  ...(backgroundSettings === null ? {} : { backgroundSettings }),
237
- ...(settings === null || !ownedSurfaces ? {} : {
271
+ ...(settings === null || !ownedSurfaces || tabBridge !== null ? {} : {
238
272
  quitOutro: { snapshot: () => quitOutroSettingsSnapshot(settings), interactive: process.stdout.isTTY === true },
239
273
  }),
240
274
  },
@@ -255,6 +289,14 @@ export async function composeOwnedUi(options = {}) {
255
289
  // the host and paints once attached.
256
290
  terminalHost = createdTerminalHost = new OwnedUiTerminalHost(sessionTerminalHostOptions(presenterOptions));
257
291
  presenter = new OwnedUiSessionPresenter(terminalHost, presenterOptions);
292
+ presentRecoveryDegraded = degraded => {
293
+ if (!degraded)
294
+ return;
295
+ presenter.root.appendWorkflowStatus("draft recovery degraded");
296
+ terminalHost.runtime.requestRender();
297
+ };
298
+ if (recoveryDegraded)
299
+ presentRecoveryDegraded(true);
258
300
  terminalHost.attach(presenter);
259
301
  }
260
302
  catch (error) {
@@ -323,6 +365,8 @@ export async function composeOwnedUi(options = {}) {
323
365
  await releaseNoteClaim?.release();
324
366
  suggestionDiagnostics?.dispose();
325
367
  clipboardDiagnostics?.dispose();
368
+ unsubscribeTabBridge();
369
+ tabBridge?.close();
326
370
  // Invariant: the host outlives the application; disposing it cancels host-level probes last.
327
371
  await host.dispose();
328
372
  }
@@ -330,6 +374,35 @@ export async function composeOwnedUi(options = {}) {
330
374
  };
331
375
  return { application, settings };
332
376
  }
377
+ export async function bindResidentTabBridge(tabBridge, backend, recovery = null) {
378
+ await tabBridge.start().catch(() => false);
379
+ tabBridge.status("starting");
380
+ const currentSessionFile = () => currentBackendSessionFile(backend);
381
+ const announceSession = () => {
382
+ tabBridge.session(currentSessionFile(), backend.session.view().status.footer?.sessionName ?? null);
383
+ };
384
+ let working = false;
385
+ announceSession();
386
+ return backend.session.onEvent(event => {
387
+ if (event.type === "session-lifecycle") {
388
+ const wasWorking = working;
389
+ working = event.lifecycle === "busy";
390
+ tabBridge.status(working ? "working" : event.lifecycle === "starting" ? "starting" : "idle");
391
+ if (wasWorking && !working && recovery !== null) {
392
+ void recovery.settled(currentSessionFile()).catch(() => tabBridge.degraded(true));
393
+ }
394
+ }
395
+ if (event.type === "session-view")
396
+ announceSession();
397
+ });
398
+ }
399
+ function currentBackendSessionFile(backend) {
400
+ const direct = backend.identity.currentSessionFile?.();
401
+ if (direct !== undefined && direct !== null)
402
+ return direct;
403
+ const resume = backend.identity.currentSessionResumeMetadata();
404
+ return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
405
+ }
333
406
  async function readPackageVersion() {
334
407
  const manifest = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
335
408
  const version = manifest?.version;
@@ -36,6 +36,7 @@ export interface OwnedUiSessionIdentityPort {
36
36
  /** Actual session replacements, excluding delivery-only invalidation of callbacks. */
37
37
  readonly sessionBindingGeneration: number;
38
38
  readonly disposed: boolean;
39
+ currentSessionFile(): string | null;
39
40
  currentSessionResumeMetadata(): OwnedUiSessionResumeMetadata | null;
40
41
  }
41
42
  /** The session lifecycle, its view, its event stream, and command admission. */
@@ -94,12 +94,10 @@ export class SettingsApp {
94
94
  // Invariant: keyboard navigation requests visibility once; pointer scrolling then stays free.
95
95
  #selectionNeedsReveal = true;
96
96
  #reveal;
97
- #notice = null;
98
97
  #filter = null;
99
98
  #scrollBeforeFilter = null;
100
99
  #menu = null;
101
100
  #structured = null;
102
- #interruptArmed = false;
103
101
  // Invariant: pending values remain visible until the source reflects them.
104
102
  #pending = new Map();
105
103
  #undoHistory = [];
@@ -138,7 +136,6 @@ export class SettingsApp {
138
136
  }
139
137
  render(rect, host) {
140
138
  const theme = host.theme ?? PLAIN_THEME;
141
- this.#interruptArmed = host.interruptArmed;
142
139
  const rows = this.#rows();
143
140
  const selected = indexOfKey(rows, this.#selectedKey);
144
141
  const footer = this.#footerLines(rect.width, theme);
@@ -259,7 +256,6 @@ export class SettingsApp {
259
256
  case "open-filter":
260
257
  this.#scrollBeforeFilter = this.#scroll;
261
258
  this.#filter = new LineInput("");
262
- this.#notice = null;
263
259
  return { consumed: true };
264
260
  case "activate": {
265
261
  const row = rows[selected];
@@ -385,7 +381,6 @@ export class SettingsApp {
385
381
  const index = rows.findIndex(candidate => rowKey(candidate) === row.key);
386
382
  if (index >= 0) {
387
383
  // Rationale: the pointer acts where it points; the arrow belongs to the keyboard.
388
- this.#notice = null;
389
384
  if (this.#hoverRegion === "minus")
390
385
  this.#cycle(rows, index, -1);
391
386
  else if (this.#hoverRegion === "plus")
@@ -458,10 +453,8 @@ export class SettingsApp {
458
453
  // and pointing at it is not a request for anything else.
459
454
  if (typeof shown === "number")
460
455
  return;
461
- if (!entry.editable || entry.choices === null || entry.choices.length === 0) {
462
- this.#notice = `${labelOf(entry)} cannot be changed here`;
456
+ if (!entry.editable || entry.choices === null || entry.choices.length === 0)
463
457
  return;
464
- }
465
458
  const current = shown === null ? 0 : Math.max(0, entry.choices.indexOf(shown));
466
459
  const anchorKey = `${entry.backend}:${entry.id}`;
467
460
  // Compatibility: pointer opening already brightens the source value. Enter borrows that
@@ -492,10 +485,8 @@ export class SettingsApp {
492
485
  #openStructured(entry) {
493
486
  // Invariant: the flags come from the declaration, not from the stored value: an unset
494
487
  // flag still has a row, showing the default the source would apply.
495
- if (entry.flags.length === 0) {
496
- this.#notice = `${labelOf(entry)} has nothing to configure`;
488
+ if (entry.flags.length === 0)
497
489
  return;
498
- }
499
490
  const stored = typeof entry.rawValue === "object" && entry.rawValue !== null && !Array.isArray(entry.rawValue)
500
491
  ? entry.rawValue
501
492
  : {};
@@ -648,13 +639,11 @@ export class SettingsApp {
648
639
  const failure = changeFailure(outcome);
649
640
  if (failure === null) {
650
641
  this.#recordUndo({ kind: "structured", sequence, entry: open.entry, previous, forward });
651
- this.#notice = null;
652
642
  return;
653
643
  }
654
644
  // Concurrency: only roll back the dialog if no later press has moved the record on.
655
645
  if (this.#structured === open && sameRecord(open.record, forward))
656
646
  replaceRecord(open.record, previous);
657
- this.#notice = `Could not save ${labelOf(open.entry)}: ${failure}`;
658
647
  });
659
648
  }
660
649
  #menuKey(data) {
@@ -744,10 +733,8 @@ export class SettingsApp {
744
733
  if (row === undefined || row.kind !== "element")
745
734
  return;
746
735
  const entry = row.value;
747
- if (!entry.editable) {
748
- this.#notice = `${labelOf(entry)} cannot be changed here`;
736
+ if (!entry.editable)
749
737
  return;
750
- }
751
738
  const shown = this.#shownValue(entry);
752
739
  if (typeof shown === "number") {
753
740
  // Rationale: at the end of the range there is nothing to say: the arrow already reads
@@ -758,10 +745,8 @@ export class SettingsApp {
758
745
  return;
759
746
  }
760
747
  const choices = entry.choices;
761
- if (choices === null || choices.length === 0) {
762
- this.#notice = `${labelOf(entry)} cannot be changed here`;
748
+ if (choices === null || choices.length === 0)
763
749
  return;
764
- }
765
750
  const current = shown === null ? -1 : choices.indexOf(shown);
766
751
  const next = choices[current < 0
767
752
  ? (delta > 0 ? 0 : choices.length - 1)
@@ -781,7 +766,6 @@ export class SettingsApp {
781
766
  if (failure !== null) {
782
767
  if (this.#pending.get(key) === value)
783
768
  this.#pending.delete(key);
784
- this.#notice = `Could not save ${labelOf(entry)}: ${failure}`;
785
769
  return;
786
770
  }
787
771
  if (previous !== null && previous !== value)
@@ -789,7 +773,6 @@ export class SettingsApp {
789
773
  // Concurrency: a later press may have moved on; only the last request clears itself.
790
774
  if (this.#pending.get(key) === value)
791
775
  this.#pending.delete(key);
792
- this.#notice = null;
793
776
  });
794
777
  }
795
778
  #shownValue(entry) {
@@ -841,13 +824,8 @@ export class SettingsApp {
841
824
  }
842
825
  #finishUndo(record, outcome) {
843
826
  const failure = changeFailure(outcome);
844
- if (failure !== null) {
827
+ if (failure !== null)
845
828
  this.#recordUndo(record);
846
- this.#notice = `Could not restore ${labelOf(record.entry)}: ${failure}`;
847
- }
848
- else {
849
- this.#notice = null;
850
- }
851
829
  this.#undoActive = false;
852
830
  this.#drainUndo();
853
831
  }
@@ -878,7 +856,6 @@ export class SettingsApp {
878
856
  return;
879
857
  this.#selectedKey = rowKey(rows[index]);
880
858
  this.#selectionNeedsReveal = true;
881
- this.#notice = null;
882
859
  }
883
860
  #rows() {
884
861
  const needle = this.#filter?.value.trim().toLowerCase() ?? "";
@@ -1037,13 +1014,9 @@ export class SettingsApp {
1037
1014
  const open = this.#structured;
1038
1015
  if (open !== null)
1039
1016
  return this.#dialogLines(open, width, theme);
1040
- const notice = this.#notice;
1041
- const plainStatus = notice !== null && notice.length > 0
1042
- ? notice
1043
- : this.#interruptArmed ? "press ctrl+c again to exit a1" : null;
1044
- const status = plainStatus === null
1045
- ? renderShortcutHintsWithClose(SETTINGS_SHORTCUTS.hintEntries(SCOPE), theme, width, width > 0 ? 1 : 0)
1046
- : truncateToWidth(`${width > 0 ? " " : ""}${theme.fg("dim", plainStatus)}`, width);
1017
+ // Rationale: standing guidance is more useful than transient feedback; failures still
1018
+ // roll back without displacing the Settings interaction contract.
1019
+ const status = renderShortcutHintsWithClose(SETTINGS_SHORTCUTS.hintEntries(SCOPE), theme, width, width > 0 ? 1 : 0);
1047
1020
  const input = this.#filter;
1048
1021
  if (input === null)
1049
1022
  return [status];
@@ -7,6 +7,9 @@ export declare const PRIVATE_ENVIRONMENT: Readonly<{
7
7
  releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS";
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE";
9
9
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP";
10
+ residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE";
11
+ residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS";
12
+ residentFallback: "LAUNCH_CONTEXT_RESIDENT_FALLBACK";
10
13
  }>;
11
14
  /**
12
15
  * Keys published by releases that predate the neutral cutover. They are read when
@@ -31,6 +34,9 @@ export interface LaunchContext {
31
34
  readonly releaseLayers?: string;
32
35
  readonly launchProfile?: "a1" | "pi";
33
36
  readonly immutableWarmup?: "1";
37
+ readonly residentRootExecutable?: string;
38
+ readonly residentRootArguments?: string;
39
+ readonly residentFallback?: string;
34
40
  }
35
41
  export type LaunchContextRequirement = "optional" | "profile" | "release" | "warmup";
36
42
  /** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
@@ -7,6 +7,9 @@ export const PRIVATE_ENVIRONMENT = Object.freeze({
7
7
  releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS",
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE",
9
9
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP",
10
+ residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE",
11
+ residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS",
12
+ residentFallback: "LAUNCH_CONTEXT_RESIDENT_FALLBACK",
10
13
  });
11
14
  /**
12
15
  * Keys published by releases that predate the neutral cutover. They are read when
@@ -63,6 +66,18 @@ export function readLaunchContext(environment, requirement = "optional", platfor
63
66
  }
64
67
  if (values.immutableWarmup !== undefined && values.immutableWarmup !== "1")
65
68
  throw invalidContext("immutableWarmup", "invalid permission flag");
69
+ if ((values.residentRootExecutable === undefined) !== (values.residentRootArguments === undefined))
70
+ throw invalidContext("residentRootExecutable", "resident root command is incomplete");
71
+ if (values.residentRootArguments !== undefined) {
72
+ try {
73
+ const argumentsValue = JSON.parse(values.residentRootArguments);
74
+ if (!Array.isArray(argumentsValue) || argumentsValue.some(value => typeof value !== "string"))
75
+ throw new Error("not string arguments");
76
+ }
77
+ catch {
78
+ throw invalidContext("residentRootArguments", "invalid JSON argument list");
79
+ }
80
+ }
66
81
  if (values.releaseDigest !== undefined && !/^[a-f0-9]{64}$/.test(values.releaseDigest))
67
82
  throw invalidContext("releaseDigest", "invalid digest");
68
83
  if (requirement === "profile")
@@ -16,11 +16,16 @@ interface GuardianControl {
16
16
  off(event: "disconnect", listener: () => void): unknown;
17
17
  close(): void;
18
18
  }
19
+ export interface LaunchGuardianRootCommand {
20
+ readonly executable: string;
21
+ readonly arguments: readonly string[];
22
+ }
19
23
  export interface LaunchGuardianOptions {
20
24
  readonly profileId: LaunchProfileId;
21
25
  readonly releaseRoot: string;
22
26
  readonly uiEntry: string;
23
27
  readonly sessionSelection?: SessionSelection;
28
+ readonly rootCommand?: LaunchGuardianRootCommand;
24
29
  readonly environment?: NodeJS.ProcessEnv;
25
30
  readonly cwd?: string;
26
31
  readonly helperPath?: string;
@@ -44,7 +44,8 @@ export async function runLaunchGuardian(options) {
44
44
  });
45
45
  let handle;
46
46
  try {
47
- handle = await containment.spawn(process.execPath, [options.uiEntry, ...sessionArgs], {
47
+ const rootCommand = options.rootCommand ?? { executable: process.execPath, arguments: [options.uiEntry, ...sessionArgs] };
48
+ handle = await containment.spawn(rootCommand.executable, [...rootCommand.arguments], {
48
49
  cwd: options.cwd ?? process.cwd(),
49
50
  environment: environmentEntries(environment),
50
51
  ...(environment.TERM ? { terminalType: environment.TERM } : {}),
@@ -2,6 +2,7 @@ import { type SupervisorEndpointMetadata } from "./cohort-state.js";
2
2
  import { resolveProductPaths, type SessionSelection, type LaunchProfileId, type SupervisorStartupAttemptIdentity } from "../lifecycle/index.js";
3
3
  import { cleanupProvenIdleOwner } from "./process-cleanup.js";
4
4
  import { type MaterializedRelease, type VerifyMaterializedReleaseOptions } from "./release-store.js";
5
+ import { type EnsureTerminalHostOptions, type EnsureTerminalHostResult } from "../resident-tabs/index.js";
5
6
  export interface BootstrapOptions {
6
7
  readonly packageRoot: string;
7
8
  readonly launchIntent?: {
@@ -23,6 +24,9 @@ export interface BootstrapOptions {
23
24
  readonly releaseIsLaunchable?: (releaseRoot: string) => boolean;
24
25
  /** Test seam for bounded maintenance scheduled after stale endpoint sweeping. */
25
26
  readonly scheduleMaintenance?: (dataDir: string, paths: ReturnType<typeof resolveProductPaths>) => Promise<void>;
27
+ /** Test seams for resident launch routing before the guardian starts. */
28
+ readonly readResidentTabs?: (profileDir: string, namespace: string, legacyConfigDir: string, profileId: string) => Promise<boolean>;
29
+ readonly ensureTerminalHost?: (options: EnsureTerminalHostOptions) => Promise<EnsureTerminalHostResult>;
26
30
  }
27
31
  export declare function runBootstrap(options: BootstrapOptions): Promise<number>;
28
32
  export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string, verification?: VerifyMaterializedReleaseOptions): Promise<string>;
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { restoreAfterOwnedExit } from "../terminal-cleanup/index.js";
2
+ import { CLEAR_EXIT_INTERVAL_MS, restoreAfterOwnedExit } from "../terminal-cleanup/index.js";
3
3
  import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { connect } from "node:net";
5
5
  import { resolve } from "node:path";
@@ -15,9 +15,10 @@ import { selectUpdateLaunchRelease } from "./update-launch.js";
15
15
  import { consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
16
16
  import { scheduleReleaseCleanup } from "./release-gc.js";
17
17
  import { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument } from "./restart-certification.js";
18
- import { PRODUCT_TEXT } from "../../product-identity.js";
18
+ import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
19
19
  import { markStartupPhase } from "../startup/index.js";
20
- import { launchContractTarget, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
20
+ import { isCurrentLaunchContract, launchContractTarget, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
21
+ import { ensureTerminalHost, readResidentTabLaunchSettings, selectResidentHost } from "../resident-tabs/index.js";
21
22
  const RELEASE_RESELECTION_MESSAGE = "a1-release-reselection";
22
23
  export async function runBootstrap(options) {
23
24
  return await runBootstrapAttempt(options, 1);
@@ -34,7 +35,42 @@ async function runBootstrapAttempt(options, retriesRemaining) {
34
35
  const paths = resolveProductPaths(environment);
35
36
  await mkdir(paths.runtimeDir, { recursive: true, mode: 0o700 });
36
37
  const launchSelected = async (release) => {
37
- const result = await launchUi(release, environment, sessionArgs);
38
+ let residentContext = {};
39
+ // Compatibility: public launch preparation supplies PI_CODING_AGENT_DIR; the fallback keeps
40
+ // direct SDK/bootstrap callers hermetic while retaining their former config-root read.
41
+ const agentProfileDir = environment.PI_CODING_AGENT_DIR ?? paths.configDir;
42
+ const residentLaunchSettings = launchProfileId === "a1"
43
+ ? readResidentTabLaunchSettings(agentProfileDir, PRODUCT_IDENTITY.commandName, paths.configDir, launchProfileId)
44
+ : null;
45
+ const resident = await selectResidentHost({
46
+ profileId: launchProfileId,
47
+ hasSessionSelection: sessionArgs.length > 0,
48
+ currentLaunchContract: isCurrentLaunchContract(release),
49
+ readSetting: options.readResidentTabs === undefined
50
+ ? async () => (await residentLaunchSettings).residentTabs
51
+ : () => options.readResidentTabs(agentProfileDir, PRODUCT_IDENTITY.commandName, paths.configDir, launchProfileId),
52
+ ensureHost: async () => (options.ensureTerminalHost ?? ensureTerminalHost)({
53
+ releaseRoot: release.releaseRoot,
54
+ profileRoot: environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir,
55
+ dataDir: paths.dataDir,
56
+ runtimeDir: paths.runtimeDir,
57
+ environment,
58
+ maxConcurrentStarts: (await residentLaunchSettings).maxConcurrentStarts,
59
+ unresponsiveRestartSeconds: (await residentLaunchSettings).unresponsiveRestartSeconds,
60
+ }),
61
+ });
62
+ if (resident !== null) {
63
+ residentContext = resident.status === "ready" ? {
64
+ residentRootExecutable: resident.executable,
65
+ residentRootArguments: JSON.stringify([
66
+ "attach", "--endpoint", resident.endpoint, "--cwd", process.cwd(), "--clear-exit-ms", String(CLEAR_EXIT_INTERVAL_MS),
67
+ "--profile", environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir,
68
+ "--data-dir", paths.dataDir, "--runtime-dir", paths.runtimeDir,
69
+ "--release-root", release.releaseRoot, "--node-executable", process.execPath,
70
+ ]),
71
+ } : { residentFallback: resident.reason };
72
+ }
73
+ const result = await launchUi(release, environment, sessionArgs, residentContext);
38
74
  if (!result.releaseSuperseded)
39
75
  return result.code;
40
76
  if (retriesRemaining > 0)
@@ -327,13 +363,15 @@ export async function startSupervisor(release, environment) {
327
363
  child.unref();
328
364
  return { ...attempt, childOutcome };
329
365
  }
330
- async function launchUi(release, environment, sessionArgs) {
366
+ async function launchUi(release, environment, sessionArgs, residentContext = {}) {
331
367
  await markStartupPhase(environment, "bootstrap-selected");
332
368
  const entry = await resolveReleaseEntryPoint(release, "bin/guardian.js");
333
369
  return await new Promise((resolvePromise, rejectPromise) => {
334
370
  let releaseSuperseded = false;
371
+ const releaseEnv = releaseEnvironment(environment, release, readLaunchContext(environment, "profile").launchProfile);
372
+ const launchEnvironment = Object.keys(residentContext).length === 0 ? releaseEnv : withLaunchContext(releaseEnv, { ...readLaunchContext(releaseEnv), ...residentContext }, process.platform, launchContractTarget(release));
335
373
  const child = spawn(process.execPath, [entry, ...sessionArgs], {
336
- env: releaseEnvironment(environment, release, readLaunchContext(environment, "profile").launchProfile),
374
+ env: launchEnvironment,
337
375
  stdio: ["inherit", "inherit", "inherit", "ipc"],
338
376
  windowsHide: false,
339
377
  });
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
3
- import { dirname, resolve } from "node:path";
2
+ import { chmod, lstat, mkdir, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, resolve } from "node:path";
4
4
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
5
  import { immutablePlatformPolicy } from "./immutable-platform.js";
6
6
  export const DEPENDENCY_CERTIFICATIONS_DIRECTORY = "dependency-certifications";
@@ -194,7 +194,8 @@ async function releasePublicationLock(path, token) {
194
194
  while (true) {
195
195
  try {
196
196
  if (!retired) {
197
- await assertPublicationLockOwner(path, token);
197
+ if (!await assertPublicationLockOwner(path, token))
198
+ return;
198
199
  await rename(path, released);
199
200
  retired = true;
200
201
  }
@@ -218,24 +219,58 @@ async function releasePublicationLock(path, token) {
218
219
  }
219
220
  /** Ownership uncertainty is never authority to retire a missing, linked, or replaced generation. */
220
221
  async function assertPublicationLockOwner(path, token) {
221
- try {
222
- const metadata = await lstat(path);
223
- if (!metadata.isDirectory() || metadata.isSymbolicLink() || await realpath(path) !== path)
224
- throw new Error("invalid lock directory");
225
- const ownerPath = resolve(path, "owner.json");
226
- const ownerMetadata = await lstat(ownerPath);
227
- if (!ownerMetadata.isFile() || ownerMetadata.isSymbolicLink() || await realpath(ownerPath) !== ownerPath)
228
- throw new Error("invalid owner file");
229
- const owner = JSON.parse(await readFile(ownerPath, "utf8"));
230
- if (!owner || typeof owner !== "object" || !("pid" in owner) || !("token" in owner)
231
- || owner.pid !== process.pid || owner.token !== token)
232
- throw new Error("owner generation differs");
222
+ for (let attempt = 1;; attempt += 1) {
223
+ try {
224
+ const metadata = await lstat(path);
225
+ if (!metadata.isDirectory() || metadata.isSymbolicLink() || await realpath(path) !== path)
226
+ throw new Error("invalid lock directory");
227
+ const ownerPath = resolve(path, "owner.json");
228
+ const ownerMetadata = await lstat(ownerPath);
229
+ if (!ownerMetadata.isFile() || ownerMetadata.isSymbolicLink() || await realpath(ownerPath) !== ownerPath)
230
+ throw new Error("invalid owner file");
231
+ const owner = JSON.parse(await readFile(ownerPath, "utf8"));
232
+ if (!owner || typeof owner !== "object" || !("pid" in owner) || !("token" in owner)
233
+ || owner.pid !== process.pid || owner.token !== token)
234
+ throw new Error("owner generation differs");
235
+ return true;
236
+ }
237
+ catch (error) {
238
+ // Platform: Windows can briefly hide a directory immediately after a successful rename
239
+ // when independent publishers are contending. Re-read the complete identity; never treat
240
+ // absence as ownership and never retire a generation whose token differs.
241
+ if (hasCode(error, "ENOENT") && attempt < 3) {
242
+ await new Promise(resolvePromise => setTimeout(resolvePromise, 20 * attempt));
243
+ continue;
244
+ }
245
+ // Concurrency: a delayed abandoned-owner reclaimer can move a successor generation after
246
+ // reading the predecessor. If our exact token is now in a managed tombstone, no active
247
+ // generation belongs to this release call and returning without mutation is fail-safe.
248
+ if (hasCode(error, "ENOENT") && await publicationLockWasRetired(path, token))
249
+ return false;
250
+ if (isReleaseContention(error))
251
+ throw error;
252
+ throw new Error("publication lease release ownership could not be verified", { cause: error });
253
+ }
233
254
  }
234
- catch (error) {
235
- if (isReleaseContention(error))
236
- throw error;
237
- throw new Error("publication lease release ownership could not be verified", { cause: error });
255
+ }
256
+ async function publicationLockWasRetired(path, token) {
257
+ const directory = dirname(path);
258
+ const prefix = `${basename(path)}.`;
259
+ const entries = await readdir(directory, { withFileTypes: true });
260
+ if (entries.length > 256)
261
+ return false;
262
+ for (const entry of entries) {
263
+ if (!entry.isDirectory() || entry.isSymbolicLink() || !entry.name.startsWith(prefix)
264
+ || (!entry.name.includes(".abandoned-") && !entry.name.includes(".released-")))
265
+ continue;
266
+ const ownerPath = resolve(directory, entry.name, "owner.json");
267
+ const owner = await readFile(ownerPath, "utf8")
268
+ .then(source => JSON.parse(source))
269
+ .catch(() => null);
270
+ if (owner?.pid === process.pid && owner.token === token)
271
+ return true;
238
272
  }
273
+ return false;
239
274
  }
240
275
  function isReleaseContention(error) {
241
276
  return ["EPERM", "EACCES", "EBUSY"].some(code => hasCode(error, code));
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Generated by scripts/pi/sync-progress-palette.ts from @earendil-works/pi-coding-agent 1.1.0
3
- * (MIT), commit abe508e1b89912adde45528136c3221eb69acdd7. Do not edit directly.
2
+ * Generated by scripts/pi/sync-progress-palette.ts from @earendil-works/pi-coding-agent 1.2.0
3
+ * (MIT), commit 06a466935536f8c9bcacb2d5efc0fc021be1bdff. Do not edit directly.
4
4
  */
5
5
  export declare const SHARED_PROGRESS_ACCENT_ANSI = "\u001B[38;2;167;152;215m";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Generated by scripts/pi/sync-progress-palette.ts from @earendil-works/pi-coding-agent 1.1.0
3
- * (MIT), commit abe508e1b89912adde45528136c3221eb69acdd7. Do not edit directly.
2
+ * Generated by scripts/pi/sync-progress-palette.ts from @earendil-works/pi-coding-agent 1.2.0
3
+ * (MIT), commit 06a466935536f8c9bcacb2d5efc0fc021be1bdff. Do not edit directly.
4
4
  */
5
5
  export const SHARED_PROGRESS_ACCENT_ANSI = "\u001b[38;2;167;152;215m";
@@ -0,0 +1,41 @@
1
+ export type ResidentTabStatus = "starting" | "working" | "idle" | "unresponsive" | "stalled" | "crashed" | "restarting" | "failed";
2
+ export interface TabBridgeCredentials {
3
+ readonly tabId: string;
4
+ readonly token: string;
5
+ readonly endpoint: string;
6
+ }
7
+ export interface TabAttachment {
8
+ readonly clientId: string;
9
+ readonly controllerRevision: number;
10
+ }
11
+ interface BridgeTransport {
12
+ write(bytes: Uint8Array): boolean;
13
+ destroy(): void;
14
+ once(event: "connect" | "error" | "close", listener: (...args: unknown[]) => void): unknown;
15
+ on(event: "data", listener: (bytes: Buffer) => void): unknown;
16
+ }
17
+ export interface TabBridgeOptions {
18
+ readonly credentials: TabBridgeCredentials;
19
+ readonly pid?: number;
20
+ readonly startIdentity?: string;
21
+ readonly build?: string;
22
+ readonly connect?: (endpoint: string) => BridgeTransport;
23
+ readonly heartbeatIntervalMs?: number;
24
+ readonly setInterval?: (callback: () => void, milliseconds: number) => unknown;
25
+ readonly clearInterval?: (timer: unknown) => void;
26
+ }
27
+ /** Metadata-only advisory bridge. Terminal input and transcript bytes have no API here. */
28
+ export declare class TabBridgeClient {
29
+ #private;
30
+ constructor(options: TabBridgeOptions);
31
+ get available(): boolean;
32
+ captureAttachment(): TabAttachment | null;
33
+ start(timeoutMs?: number): Promise<boolean>;
34
+ status(state: ResidentTabStatus, degraded?: boolean): void;
35
+ degraded(degraded: boolean): void;
36
+ session(file: string | null, name: string | null): void;
37
+ requestDetach(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
38
+ requestRestart(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
39
+ close(): void;
40
+ }
41
+ export {};