@timurproko/a1 0.2.7-dev.806 → 0.2.7-dev.808

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 (47) hide show
  1. package/dist/app/session-shell/session-shell.js +12 -1
  2. package/dist/app/session-shell/shell-host.js +18 -4
  3. package/dist/composition/owned-ui.d.ts +2 -0
  4. package/dist/composition/owned-ui.js +61 -11
  5. package/dist/composition/settings-route-host.d.ts +6 -1
  6. package/dist/composition/settings-route-host.js +7 -6
  7. package/dist/features/owned-ui/settings-app.d.ts +4 -1
  8. package/dist/features/owned-ui/settings-app.js +9 -3
  9. package/dist/features/resident-tab/shortcut-declarations.js +1 -1
  10. package/dist/features/resident-tab/status-deriver.d.ts +4 -3
  11. package/dist/features/resident-tab/status-deriver.js +2 -5
  12. package/dist/foundation/release/bootstrap.js +2 -2
  13. package/dist/foundation/resident-tabs/bridge-client.d.ts +1 -1
  14. package/dist/foundation/resident-tabs/bridge-client.js +2 -1
  15. package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
  16. package/dist/foundation/resident-tabs/fixture-control.js +35 -0
  17. package/dist/foundation/resident-tabs/progress-observation.d.ts +7 -0
  18. package/dist/foundation/resident-tabs/progress-observation.js +119 -0
  19. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  20. package/dist/foundation/terminal-cleanup/index.js +1 -1
  21. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +5 -0
  22. package/dist/foundation/terminal-cleanup/terminal-reset.js +5 -0
  23. package/dist/integrations/pi/engine/session-runtime.js +5 -2
  24. package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
  25. package/dist/integrations/pi/tui-runtime/adapter.js +28 -1
  26. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +11 -0
  27. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +30 -1
  28. package/dist/native/darwin-arm64/manifest.json +1 -1
  29. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  30. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  31. package/dist/native/darwin-x64/manifest.json +1 -1
  32. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  33. package/dist/native/darwin-x64/terminal-host.node +0 -0
  34. package/dist/native/linux-x64/manifest.json +1 -1
  35. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  36. package/dist/native/linux-x64/terminal-host.node +0 -0
  37. package/dist/native/win32-x64/manifest.json +2 -2
  38. package/dist/native/win32-x64/process-guardian.node +0 -0
  39. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  40. package/dist/native/win32-x64/terminal-host.node +0 -0
  41. package/dist/product-identity.d.ts +2 -2
  42. package/dist/product-identity.js +2 -2
  43. package/dist/product-identity.json +6 -0
  44. package/dist/ui/settings/manager.d.ts +6 -0
  45. package/dist/ui/settings/manager.js +25 -0
  46. package/docs/architecture/resident-tabs.md +17 -12
  47. package/package.json +2 -1
@@ -1762,7 +1762,18 @@ export class OwnedUiSessionPresenter {
1762
1762
  this.#dialogId = dialog.id;
1763
1763
  }
1764
1764
  openOwnedRoute(route) {
1765
- this.#openOwnedRoute(route);
1765
+ if (this.#disposed)
1766
+ return;
1767
+ // Invariant: a route requested before the runtime runs (the resident Settings bridge route arrives
1768
+ // before start()) is retained, latest wins, and presented through the startup-route path once started.
1769
+ if (!this.#terminal.running) {
1770
+ this.#startupRoute = { route };
1771
+ return;
1772
+ }
1773
+ const result = this.#openOwnedRoute(route);
1774
+ if (result.outcome !== "completed" && result.diagnostic !== null) {
1775
+ this.root.appendWorkflowMessage({ kind: "warning", message: result.diagnostic });
1776
+ }
1766
1777
  }
1767
1778
  hotkeysPresentation() {
1768
1779
  const presentation = this.root.hotkeysPresentation();
@@ -1,8 +1,10 @@
1
1
  import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../ui/components/mouse.js";
2
2
  import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
3
3
  import { piCanvasBackgroundAnsi } from "../../integrations/pi/components/theme.js";
4
+ import { residentFixtureControl } from "../../foundation/resident-tabs/fixture-control.js";
5
+ import { residentProgressObserver } from "../../foundation/resident-tabs/progress-observation.js";
4
6
  import { DamageAwareTerminalAdapter, } from "../../integrations/pi/tui-runtime/damage-aware-terminal.js";
5
- import { PiTuiRuntimeAdapter } from "../../integrations/pi/tui-runtime/adapter.js";
7
+ import { PiTuiRuntimeAdapter, legacyKeyInput } from "../../integrations/pi/tui-runtime/adapter.js";
6
8
  import { classifyPiTuiInput } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
7
9
  import { pinnedLayoutRoot, } from "./session-shell-root.js";
8
10
  /** The terminal a session's shell options call for: its layout, TUI mode, cursor, and presentation seams. */
@@ -47,6 +49,8 @@ export class OwnedUiTerminalHost {
47
49
  this.#customViewport = options.layout === "custom-viewport";
48
50
  this.#quitOutro = options.quitOutro;
49
51
  const input = options.input;
52
+ const fixtureControl = residentFixtureControl();
53
+ const progressObserver = residentProgressObserver();
50
54
  let damageTerminal;
51
55
  const surface = {
52
56
  render: width => this.#active?.render(width) ?? [],
@@ -72,6 +76,13 @@ export class OwnedUiTerminalHost {
72
76
  inspectHyperlinks: readVisibleHyperlinks,
73
77
  onResize: () => this.#active?.noteImmediatePresentation(),
74
78
  onHyperlinkCleanupRequired: () => this.runtime?.requestRender(true),
79
+ ...(fixtureControl === null ? {} : {
80
+ allowWrite: () => !fixtureControl.stopped("child-render-write"),
81
+ writeChunks: data => fixtureControl.fragmentChildWrite(data),
82
+ }),
83
+ ...(progressObserver === null ? {} : {
84
+ onWrite: ({ bytes, fragments, transaction, frameId, cause }) => progressObserver.childRenderWrite(bytes, fragments, transaction, frameId, cause),
85
+ }),
75
86
  });
76
87
  return damageTerminal;
77
88
  },
@@ -337,7 +348,8 @@ export class OwnedUiTerminalHost {
337
348
  callbacks.closed(rendered);
338
349
  };
339
350
  removeInterruptWatch = runtime.addInputListener(data => {
340
- if (!attached() || !data.includes(INTERRUPT))
351
+ // Compatibility: a kitty-keyboard child reports Ctrl+C as a CSI-u sequence rather than the byte.
352
+ if (!attached() || !data.includes(INTERRUPT) && legacyKeyInput(data) !== INTERRUPT)
341
353
  return undefined;
342
354
  // Protocol: route hosts decide whether one interrupt closes their app. Raw-input forwarding
343
355
  // is required because Pi handles Ctrl+C before the fullscreen overlay receives normal input.
@@ -395,8 +407,10 @@ export class OwnedUiTerminalHost {
395
407
  const { events, rest } = parseMouseInput(data);
396
408
  for (const event of events)
397
409
  surface.handleMouse(event);
398
- if (rest.length > 0)
399
- surface.handleInput(rest);
410
+ // Invariant: owned screens see legacy key bytes whether the terminal reports legacy or kitty keys.
411
+ const key = rest.length > 0 ? legacyKeyInput(rest) : "";
412
+ if (key.length > 0)
413
+ surface.handleInput(key);
400
414
  if (surface.isClosed()) {
401
415
  closeSurface(true);
402
416
  return;
@@ -90,3 +90,5 @@ export declare function bindResidentTabBridge(tabBridge: TabBridgeClient, backen
90
90
  readonly openRoute?: (route: "settings") => void;
91
91
  }): Promise<() => void>;
92
92
  export declare function correlateResidentTabSubmission(recovery: Pick<ResidentTabRecovery, "correlated" | "settled">, backend: Pick<ComposedSessionBackend, "identity" | "session">, submissionId: string, entryId: string): Promise<void>;
93
+ /** Rereads the shared settings document whenever its stat changes; returns the stop. */
94
+ export declare function watchSharedSettings(settings: Pick<OwnedSettingsManager, "file" | "refresh">): () => void;
@@ -1,3 +1,4 @@
1
+ import { unwatchFile, watchFile } from "node:fs";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { SuggestionDiagnosticCapture } from "../features/prompt-suggestions/diagnostics.js";
3
4
  import { PRODUCT_IDENTITY } from "../product-identity.js";
@@ -188,13 +189,19 @@ export async function composeOwnedUi(options = {}) {
188
189
  : await import("../features/owned-ui/release-note-state.js").then(module => module.claimReleaseNote({
189
190
  configDir: productPaths.configDir, profileId: options.profileId, version: currentReleaseNote.version,
190
191
  }));
191
- const unsubscribeAccent = settings === null || !ownedSurfaces
192
+ const unsubscribeAccentChange = settings === null || !ownedSurfaces
192
193
  ? () => { }
193
194
  : settings.onChange(current => {
194
195
  const accent = current.value("accentColor");
195
196
  host.setAccentColor(accent);
196
197
  tabBridge?.accent(accent);
197
198
  });
199
+ // Rationale: every resident tab is its own process over one shared settings document, so a
200
+ // change saved in any tab reaches the others only by rereading that document.
201
+ const unwatchSharedSettings = settings === null || !ownedSurfaces || tabBridge === null
202
+ ? () => { }
203
+ : watchSharedSettings(settings);
204
+ const unsubscribeAccent = () => { unwatchSharedSettings(); unsubscribeAccentChange(); };
198
205
  // Rationale: Routes open only after shell construction.
199
206
  const references = {
200
207
  changelog: async (input) => {
@@ -210,7 +217,9 @@ export async function composeOwnedUi(options = {}) {
210
217
  },
211
218
  session: () => import("./session-info-reference.js").then(m => m.loadSessionInfoReference(backend)),
212
219
  };
213
- const routeHost = settings === null || !ownedSurfaces ? null : createOwnedRouteHost(settings, references);
220
+ const routeHost = settings === null || !ownedSurfaces ? null : createOwnedRouteHost(settings, references, {
221
+ settingsPinned: tabBridge?.tabId.startsWith("settings-") === true,
222
+ });
214
223
  const viewportSettings = settings === null || !ownedSurfaces ? null : {
215
224
  snapshot: () => viewportSettingsSnapshot(settings),
216
225
  onChange: listener => settings.onChange(() => listener(viewportSettingsSnapshot(settings))),
@@ -436,15 +445,27 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
436
445
  tabBridge.status("starting");
437
446
  const currentSessionFile = () => currentBackendSessionFile(backend);
438
447
  let pendingAutoName = null;
448
+ // Invariant: while a generated name is being applied, the session name it produces is automatic
449
+ // even if the session normalizes it; reporting it as `user` would block the strip's title
450
+ // animation and every later automatic rename.
451
+ let applyingAutoName = false;
439
452
  const isDefaultName = (name) => name == null || /^agent(?: \d+)?$/i.test(name);
440
453
  const currentSessionName = () => backend.session.view().status.footer?.sessionName ?? null;
441
454
  const announceSession = () => {
442
455
  const name = currentSessionName();
443
- const nameSource = name === pendingAutoName ? "auto" : isDefaultName(name) ? "default" : "user";
456
+ // Invariant: the session may announce the applied name after the workflow resolves; the
457
+ // first non-default name announced for a pending auto-name is that automatic name.
458
+ if (applyingAutoName && !isDefaultName(name)) {
459
+ pendingAutoName = name;
460
+ applyingAutoName = false;
461
+ }
462
+ const automatic = name === pendingAutoName;
463
+ const nameSource = automatic ? "auto" : isDefaultName(name) ? "default" : "user";
444
464
  tabBridge.session(currentSessionFile(), name, nameSource);
445
465
  };
446
466
  let working = false;
447
- let lastTurnFailed = false;
467
+ // Invariant: the last turn's settlement stays reported until the next turn starts.
468
+ let lastTurn;
448
469
  let autoNameAttempted = false;
449
470
  let autoNameArmed = false;
450
471
  // Invariant: naming launches once per tab during an agent run, as soon as its prompt is projected,
@@ -461,12 +482,26 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
461
482
  if (!isDefaultName(currentSessionName()))
462
483
  return;
463
484
  pendingAutoName = name;
485
+ applyingAutoName = true;
464
486
  const result = await backend.workflows.executeWorkflow({ command: "name", argument: name });
465
- if (result.outcome === "completed")
487
+ if (result.outcome !== "completed") {
488
+ applyingAutoName = false;
489
+ if (pendingAutoName === name)
490
+ pendingAutoName = null;
491
+ return;
492
+ }
493
+ const current = currentSessionName();
494
+ if (current !== null && !isDefaultName(current)) {
495
+ // Invariant: The session already holds the applied name; report it as automatic now.
496
+ pendingAutoName = current;
497
+ applyingAutoName = false;
498
+ tabBridge.session(currentSessionFile(), current, "auto");
499
+ }
500
+ else {
501
+ // Invariant: The session announces later; show the generated name meanwhile, still automatic.
466
502
  tabBridge.session(currentSessionFile(), name, "auto");
467
- else if (pendingAutoName === name)
468
- pendingAutoName = null;
469
- }).catch(() => { pendingAutoName = null; });
503
+ }
504
+ }).catch(() => { pendingAutoName = null; applyingAutoName = false; });
470
505
  };
471
506
  const reportStatus = (lifecycle) => {
472
507
  const view = backend.session.view();
@@ -476,7 +511,7 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
476
511
  lifecycle: lifecycle ?? view.lifecycle,
477
512
  pendingRequests,
478
513
  queuedInput,
479
- ...(lastTurnFailed ? { stopReason: "error" } : {}),
514
+ ...(lastTurn === undefined ? {} : { lastTurn }),
480
515
  }), false, pendingRequests, queuedInput);
481
516
  };
482
517
  announceSession();
@@ -501,7 +536,7 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
501
536
  const wasWorking = working;
502
537
  working = event.lifecycle === "busy";
503
538
  if (working)
504
- lastTurnFailed = false;
539
+ lastTurn = undefined;
505
540
  reportStatus(event.lifecycle);
506
541
  if (wasWorking && !working && recovery !== null) {
507
542
  void recovery.settled(currentSessionFile()).catch(() => tabBridge.degraded(true));
@@ -517,7 +552,7 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
517
552
  if (event.type === "agent-run-settled") {
518
553
  launchAutoName();
519
554
  autoNameArmed = false;
520
- lastTurnFailed = !event.successful && !event.aborted;
555
+ lastTurn = event.aborted ? "aborted" : event.successful ? "done" : "error";
521
556
  tabBridge.prompt(event.aborted);
522
557
  reportStatus();
523
558
  }
@@ -546,6 +581,21 @@ function currentBackendSessionFile(backend) {
546
581
  const resume = backend.identity.currentSessionResumeMetadata();
547
582
  return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
548
583
  }
584
+ const SHARED_SETTINGS_POLL_MS = 250;
585
+ /** Rereads the shared settings document whenever its stat changes; returns the stop. */
586
+ export function watchSharedSettings(settings) {
587
+ const file = settings.file;
588
+ const listener = () => {
589
+ try {
590
+ settings.refresh();
591
+ }
592
+ catch {
593
+ // Compatibility: an unreadable peer write resolves on the next stat change.
594
+ }
595
+ };
596
+ watchFile(file, { interval: SHARED_SETTINGS_POLL_MS, persistent: false }, listener);
597
+ return () => unwatchFile(file, listener);
598
+ }
549
599
  async function readPackageVersion() {
550
600
  const manifest = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
551
601
  const version = manifest?.version;
@@ -9,5 +9,10 @@ export interface OwnedReferenceProviders {
9
9
  hotkeys(): Promise<ReferenceDocumentProvider>;
10
10
  session(): Promise<ReferenceDocumentProvider>;
11
11
  }
12
+ /** How the settings route is presented. */
13
+ export interface OwnedRouteHostOptions {
14
+ /** The resident Settings tab: the screen is the tab, so it offers no screen-level close. */
15
+ readonly settingsPinned?: boolean;
16
+ }
12
17
  /** Declares settings and lazy document routes. */
13
- export declare function createOwnedRouteHost(settings: OwnedSettingsManager, references?: OwnedReferenceProviders): UiRouteHost;
18
+ export declare function createOwnedRouteHost(settings: OwnedSettingsManager, references?: OwnedReferenceProviders, options?: OwnedRouteHostOptions): UiRouteHost;
@@ -3,7 +3,7 @@ import { SETTINGS_APP_ID, SETTINGS_ROUTE } from "../features/owned-ui/settings-r
3
3
  import { piTheme, renderPiAccentPreview } from "../integrations/pi/components/upstream/theme/theme.js";
4
4
  import { faint } from "../ui/components/text.js";
5
5
  /** Declares settings and lazy document routes. */
6
- export function createOwnedRouteHost(settings, references) {
6
+ export function createOwnedRouteHost(settings, references, options = {}) {
7
7
  const routes = new Set([SETTINGS_ROUTE, ...(references === undefined ? [] : [CHANGELOG_ROUTE, HOTKEYS_ROUTE, "session"])]);
8
8
  return {
9
9
  claims: route => routes.has(route),
@@ -11,7 +11,7 @@ export function createOwnedRouteHost(settings, references) {
11
11
  if (!routes.has(route))
12
12
  return null;
13
13
  if (route === SETTINGS_ROUTE) {
14
- return deferredSurface(SETTINGS_APP_ID, "settings", () => loadSettingsSurface(settings));
14
+ return deferredSurface(SETTINGS_APP_ID, "settings", () => loadSettingsSurface(settings, options.settingsPinned === true));
15
15
  }
16
16
  const changelog = route === CHANGELOG_ROUTE;
17
17
  const session = route === "session";
@@ -61,9 +61,10 @@ function deferredSurface(id, label, load) {
61
61
  onExitRequested: listener => { onExit = listener; },
62
62
  };
63
63
  }
64
- async function loadSettingsSurface(settings) {
64
+ async function loadSettingsSurface(settings, pinned) {
65
65
  const { SettingsApp } = await import("../features/owned-ui/settings-app.js");
66
- return hostApp(SETTINGS_APP_ID, SETTINGS_ROUTE, () => new SettingsApp(settings));
66
+ // Invariant: a pinned Settings tab closes as a tab; neither Esc nor one Ctrl+C dismisses its screen.
67
+ return hostApp(SETTINGS_APP_ID, SETTINGS_ROUTE, () => new SettingsApp(settings, { pinned }), !pinned);
67
68
  }
68
69
  async function loadReferenceSurface(settings, id, route, title, document) {
69
70
  // Rationale: the app module and the document load together, so a document that fails
@@ -80,7 +81,7 @@ async function loadReferenceSurface(settings, id, route, title, document) {
80
81
  }),
81
82
  }));
82
83
  }
83
- async function hostApp(id, route, create) {
84
+ async function hostApp(id, route, create, closeOnInterrupt = true) {
84
85
  const [{ UiAppHost }, { UiAppRegistry }] = await Promise.all([
85
86
  import("../ui/apps/host.js"),
86
87
  import("../ui/apps/registry.js"),
@@ -94,7 +95,7 @@ async function hostApp(id, route, create) {
94
95
  let onExit = () => undefined;
95
96
  const host = new UiAppHost({
96
97
  registry,
97
- closeOnInterrupt: true,
98
+ closeOnInterrupt,
98
99
  theme: pinnedTheme(),
99
100
  surface: {
100
101
  size: () => size,
@@ -12,7 +12,10 @@ export declare const SETTINGS_SHORTCUTS: ShortcutRegistry<Action>;
12
12
  export declare class SettingsApp implements UiApp {
13
13
  #private;
14
14
  readonly id = "settings";
15
- constructor(session: OwnedSettingsManager);
15
+ /** `pinned` presents the screen as a resident Settings tab, which offers no screen-level close. */
16
+ constructor(session: OwnedSettingsManager, options?: {
17
+ readonly pinned?: boolean;
18
+ });
16
19
  onActivate(host: AppHostServices): void;
17
20
  onClose(_host: AppHostServices): void;
18
21
  render(rect: PaneRect, host: AppHostServices): readonly string[];
@@ -120,8 +120,11 @@ export class SettingsApp {
120
120
  #activeUntil = 0;
121
121
  #activityTimer;
122
122
  #renderedScroll;
123
- constructor(session) {
123
+ #pinned;
124
+ /** `pinned` presents the screen as a resident Settings tab, which offers no screen-level close. */
125
+ constructor(session, options = {}) {
124
126
  this.#session = session;
127
+ this.#pinned = options.pinned === true;
125
128
  }
126
129
  onActivate(host) {
127
130
  this.#closed = false;
@@ -251,7 +254,8 @@ export class SettingsApp {
251
254
  const selected = indexOfKey(rows, this.#selectedKey);
252
255
  switch (action) {
253
256
  case "close":
254
- host.close();
257
+ if (!this.#pinned)
258
+ host.close();
255
259
  return { consumed: true };
256
260
  case "open-filter":
257
261
  this.#scrollBeforeFilter = this.#scroll;
@@ -1016,7 +1020,9 @@ export class SettingsApp {
1016
1020
  return this.#dialogLines(open, width, theme);
1017
1021
  // Rationale: standing guidance is more useful than transient feedback; failures still
1018
1022
  // roll back without displacing the Settings interaction contract.
1019
- const status = renderShortcutHintsWithClose(SETTINGS_SHORTCUTS.hintEntries(SCOPE), theme, width, width > 0 ? 1 : 0);
1023
+ const entries = SETTINGS_SHORTCUTS.hintEntries(SCOPE).filter(entry => !this.#pinned
1024
+ || entry.key !== DIALOG_CLOSE_SHORTCUT_HINT.key || entry.action !== DIALOG_CLOSE_SHORTCUT_HINT.action);
1025
+ const status = renderShortcutHintsWithClose(entries, theme, width, width > 0 ? 1 : 0);
1020
1026
  const input = this.#filter;
1021
1027
  if (input === null)
1022
1028
  return [status];
@@ -13,7 +13,7 @@ export const RESIDENT_TAB_SHORTCUTS = Object.freeze([
13
13
  { id: "tabs.new", defaultKey: "alt+a", scope: "global", description: "New A1 tab" },
14
14
  { id: "tabs.close", defaultKey: "alt+w", scope: "global", description: "Close A1 tab" },
15
15
  { id: "tabs.rename", defaultKey: "f2", scope: "global", description: "Rename A1 tab" },
16
- { id: "tabs.settings", defaultKey: "alt+.", scope: "global", description: "Open Settings tab" },
16
+ { id: "tabs.settings", defaultKey: "alt+,", scope: "global", description: "Open Settings tab" },
17
17
  ...jumps,
18
18
  { id: "tabs.next", defaultKey: null, scope: "global", description: "View next A1 tab" },
19
19
  { id: "tabs.previous", defaultKey: null, scope: "global", description: "View previous A1 tab" },
@@ -1,9 +1,10 @@
1
- export type ResidentTabDerivedStatus = "starting" | "working" | "needs-input" | "done-unseen" | "idle" | "failed";
1
+ /** How the tab's last agent turn settled; it persists until the next turn starts. */
2
+ export type ResidentTabTurnSettlement = "done" | "aborted" | "error";
3
+ export type ResidentTabDerivedStatus = "starting" | "working" | "needs-input" | "idle" | "failed" | ResidentTabTurnSettlement;
2
4
  export interface ResidentTabFacts {
3
5
  readonly lifecycle: string;
4
6
  readonly pendingRequests: number;
5
7
  readonly queuedInput: number;
6
- readonly stopReason?: string;
7
- readonly settledUnseen?: boolean;
8
+ readonly lastTurn?: ResidentTabTurnSettlement;
8
9
  }
9
10
  export declare function deriveResidentTabStatus(facts: ResidentTabFacts): ResidentTabDerivedStatus;
@@ -1,3 +1,4 @@
1
+ // Invariant: `failed` is reserved for a failed tab lifecycle; a turn that errored reports `error`.
1
2
  export function deriveResidentTabStatus(facts) {
2
3
  if (facts.lifecycle === "failed")
3
4
  return "failed";
@@ -7,9 +8,5 @@ export function deriveResidentTabStatus(facts) {
7
8
  return "working";
8
9
  if (facts.lifecycle === "starting")
9
10
  return "starting";
10
- if (facts.stopReason === "error")
11
- return "failed";
12
- if (facts.settledUnseen === true)
13
- return "done-unseen";
14
- return "idle";
11
+ return facts.lastTurn ?? "idle";
15
12
  }
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { CLEAR_EXIT_INTERVAL_MS, restoreAfterOwnedExit } from "../terminal-cleanup/index.js";
2
+ import { RESIDENT_DETACH_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";
@@ -64,7 +64,7 @@ async function runBootstrapAttempt(options, retriesRemaining) {
64
64
  residentRootExecutable: resident.executable.executable,
65
65
  residentRootArguments: JSON.stringify([
66
66
  resident.executable.entry,
67
- "attach", "--endpoint", resident.endpoint, "--cwd", process.cwd(), "--clear-exit-ms", String(CLEAR_EXIT_INTERVAL_MS),
67
+ "attach", "--endpoint", resident.endpoint, "--cwd", process.cwd(), "--clear-exit-ms", String(RESIDENT_DETACH_INTERVAL_MS),
68
68
  "--profile", environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir,
69
69
  "--settings-file", resolve(agentProfileDir, "settings.json"),
70
70
  "--data-dir", paths.dataDir, "--runtime-dir", paths.runtimeDir,
@@ -1,4 +1,4 @@
1
- export type ResidentTabStatus = "starting" | "working" | "needs-input" | "done-unseen" | "idle" | "unresponsive" | "stalled" | "crashed" | "restarting" | "failed";
1
+ export type ResidentTabStatus = "starting" | "working" | "needs-input" | "done-unseen" | "done" | "aborted" | "error" | "idle" | "unresponsive" | "stalled" | "crashed" | "restarting" | "failed";
2
2
  export interface TabBridgeCredentials {
3
3
  readonly tabId: string;
4
4
  readonly token: string;
@@ -137,7 +137,8 @@ export class TabBridgeClient {
137
137
  if (start === -1) {
138
138
  let retained = 0;
139
139
  const maximum = Math.min(prefix.length - 1, pending.length);
140
- for (let length = maximum; length > 0; length--) {
140
+ // Invariant: a lone trailing Esc is a user key and passes at once; only `\x1bP` or more is withheld.
141
+ for (let length = maximum; length > 1; length--) {
141
142
  if (pending.endsWith(prefix.slice(0, length))) {
142
143
  retained = length;
143
144
  break;
@@ -0,0 +1,7 @@
1
+ export type ResidentFixtureStoppedStage = "child-render-write";
2
+ export interface ResidentFixtureControlPort {
3
+ stopped(stage: ResidentFixtureStoppedStage): boolean;
4
+ fragmentChildWrite(data: string): readonly string[];
5
+ }
6
+ /** Returns the exact-artifact fixture control, or null for every ordinary resident child. */
7
+ export declare function residentFixtureControl(environment?: NodeJS.ProcessEnv): ResidentFixtureControlPort | null;
@@ -0,0 +1,35 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
+ import { residentProgressObserver } from "./progress-observation.js";
5
+ /** Returns the exact-artifact fixture control, or null for every ordinary resident child. */
6
+ export function residentFixtureControl(environment = process.env) {
7
+ const directory = environment[PRODUCT_IDENTITY.environment.residentFixtureControlDir];
8
+ if (environment === process.env)
9
+ delete process.env[PRODUCT_IDENTITY.environment.residentFixtureControlDir];
10
+ // Invariant: launch erases the bridge tab identity; the observer retains the observed one.
11
+ const tabId = residentProgressObserver(environment)?.tabId;
12
+ if (!directory || !tabId || !/^[a-z0-9-]{1,128}$/iu.test(tabId))
13
+ return null;
14
+ return {
15
+ stopped: stage => existsSync(join(directory, `${tabId}.${stage}.stop`)),
16
+ fragmentChildWrite: data => existsSync(join(directory, `${tabId}.child-render-fragments.enable`))
17
+ ? boundedFragments(data)
18
+ : [data],
19
+ };
20
+ }
21
+ function boundedFragments(data) {
22
+ const points = [...data];
23
+ if (points.length <= 1)
24
+ return [data];
25
+ if (points.length <= 16)
26
+ return points;
27
+ const fragments = points.slice(0, 8);
28
+ const middle = points.slice(8, -8);
29
+ const chunkSize = Math.max(1, Math.ceil(middle.length / 48));
30
+ for (let index = 0; index < middle.length; index += chunkSize) {
31
+ fragments.push(middle.slice(index, index + chunkSize).join(""));
32
+ }
33
+ fragments.push(...points.slice(-8));
34
+ return fragments;
35
+ }
@@ -0,0 +1,7 @@
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. */
7
+ export declare function residentProgressObserver(environment?: NodeJS.ProcessEnv): ResidentProgressObserverPort | null;
@@ -0,0 +1,119 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { appendFile, mkdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { performance } from "node:perf_hooks";
5
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
6
+ const OBSERVATION_DIRECTORY_ENV = PRODUCT_IDENTITY.environment.residentProgressObservationDir;
7
+ const OBSERVATION_RUN_ENV = PRODUCT_IDENTITY.environment.residentProgressRunId;
8
+ const MAX_PENDING_BYTES = 256 * 1024;
9
+ class ResidentProgressObserver {
10
+ #directory;
11
+ #runId;
12
+ #tabId;
13
+ #incarnation;
14
+ #processInstance;
15
+ #path;
16
+ #sequence = 0;
17
+ #engineSequence = 0;
18
+ #pendingBytes = 0;
19
+ #lost = 0;
20
+ #writing = Promise.resolve();
21
+ #failed = false;
22
+ get tabId() { return this.#tabId; }
23
+ constructor(directory, runId, tabId, incarnation) {
24
+ this.#directory = directory;
25
+ this.#runId = runId;
26
+ this.#tabId = tabId;
27
+ this.#incarnation = incarnation;
28
+ this.#processInstance = `${process.pid}-${randomUUID()}`;
29
+ this.#path = join(directory, `child-${this.#processInstance}.jsonl`);
30
+ }
31
+ engineDelivery(event) {
32
+ this.#engineSequence += 1;
33
+ this.#observe("engine-delivery", {
34
+ eventSequence: this.#engineSequence,
35
+ eventKind: eventKind(event),
36
+ count: 1,
37
+ });
38
+ }
39
+ childRenderWrite(bytes, fragments, transaction, frameId, renderCause) {
40
+ this.#observe("child-render-write", {
41
+ eventSequence: this.#engineSequence,
42
+ bytes,
43
+ count: 1,
44
+ fragmentCount: fragments,
45
+ transactionKind: transaction,
46
+ ...(frameId === undefined ? {} : { frameId }),
47
+ ...(renderCause === undefined ? {} : { renderCause }),
48
+ });
49
+ }
50
+ #observe(stage, fields) {
51
+ if (this.#failed)
52
+ return;
53
+ const lostBefore = this.#lost;
54
+ const record = {
55
+ schema: "a1-resident-progress-observation-v1",
56
+ runId: this.#runId,
57
+ role: "child",
58
+ stage,
59
+ pid: process.pid,
60
+ processInstance: this.#processInstance,
61
+ timestampUnixUs: Math.round((performance.timeOrigin + performance.now()) * 1_000),
62
+ // Rationale: timeOrigin is calibrated to the Unix epoch; retain a conservative cross-process error.
63
+ clockErrorUs: 1_000,
64
+ sequence: ++this.#sequence,
65
+ tabId: this.#tabId,
66
+ incarnation: this.#incarnation,
67
+ lostBefore,
68
+ ...fields,
69
+ };
70
+ const line = `${JSON.stringify(record)}\n`;
71
+ const length = Buffer.byteLength(line);
72
+ if (this.#pendingBytes + length > MAX_PENDING_BYTES) {
73
+ this.#lost = lostBefore + 1;
74
+ return;
75
+ }
76
+ this.#lost = 0;
77
+ this.#pendingBytes += length;
78
+ this.#writing = this.#writing.then(async () => {
79
+ await mkdir(this.#directory, { recursive: true });
80
+ await appendFile(this.#path, line, "utf8");
81
+ }).catch(() => {
82
+ this.#failed = true;
83
+ }).finally(() => {
84
+ this.#pendingBytes -= length;
85
+ });
86
+ }
87
+ }
88
+ let observer;
89
+ /** Returns the opt-in payload-free observer for a resident child, or null on ordinary launches. */
90
+ export function residentProgressObserver(environment = process.env) {
91
+ if (environment === process.env && observer !== undefined)
92
+ return observer;
93
+ const directory = environment[OBSERVATION_DIRECTORY_ENV];
94
+ const runId = environment[OBSERVATION_RUN_ENV];
95
+ const tabId = environment[PRODUCT_IDENTITY.environment.residentProgressTabId];
96
+ const incarnation = Number(environment[PRODUCT_IDENTITY.environment.residentProgressTabIncarnation]);
97
+ const value = directory && runId && runId.length <= 128 && tabId && Number.isSafeInteger(incarnation) && incarnation >= 0
98
+ ? new ResidentProgressObserver(directory, runId, tabId, incarnation)
99
+ : null;
100
+ if (environment === process.env) {
101
+ observer = value;
102
+ // Security: test evidence paths and tab identity are not child/tool configuration. Retain them only in
103
+ // this observer instance so extensions and spawned tools cannot append to the fixture trace.
104
+ for (const name of [
105
+ OBSERVATION_DIRECTORY_ENV,
106
+ OBSERVATION_RUN_ENV,
107
+ PRODUCT_IDENTITY.environment.residentProgressTabId,
108
+ PRODUCT_IDENTITY.environment.residentProgressTabIncarnation,
109
+ ])
110
+ delete process.env[name];
111
+ }
112
+ return value;
113
+ }
114
+ function eventKind(event) {
115
+ if (typeof event !== "object" || event === null)
116
+ return "unknown";
117
+ const value = event.type;
118
+ return typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/iu.test(value) ? value : "unknown";
119
+ }
@@ -1,3 +1,3 @@
1
- export { CLEAR_EXIT_INTERVAL_MS, EMERGENCY_TERMINAL_RESET, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
1
+ export { CLEAR_EXIT_INTERVAL_MS, EMERGENCY_TERMINAL_RESET, RESIDENT_DETACH_INTERVAL_MS, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
2
2
  export { installFatalExit, writeFatalDiagnostic } from "./fatal-exit.js";
3
3
  export type { FatalExitOptions } from "./fatal-exit.js";
@@ -1,2 +1,2 @@
1
- export { CLEAR_EXIT_INTERVAL_MS, EMERGENCY_TERMINAL_RESET, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
1
+ export { CLEAR_EXIT_INTERVAL_MS, EMERGENCY_TERMINAL_RESET, RESIDENT_DETACH_INTERVAL_MS, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
2
2
  export { installFatalExit, writeFatalDiagnostic } from "./fatal-exit.js";
@@ -1,4 +1,9 @@
1
1
  export declare const CLEAR_EXIT_INTERVAL_MS = 500;
2
+ /**
3
+ * How long a first `Ctrl+C` keeps the resident attach client's detach chord armed. It matches the
4
+ * owned dialogs' interrupt chord so an unhurried double press leaves resident tabs from any tab.
5
+ */
6
+ export declare const RESIDENT_DETACH_INTERVAL_MS = 1500;
2
7
  export declare const EMERGENCY_TERMINAL_RESET: string;
3
8
  export declare function restoreProcessTerminal(): void;
4
9
  export declare function restoreAfterOwnedExit(ownsTerminal: boolean, code: number | null, signal: string | null, restore?: () => void): number;
@@ -1,5 +1,10 @@
1
1
  import { writeSync } from "node:fs";
2
2
  export const CLEAR_EXIT_INTERVAL_MS = 500;
3
+ /**
4
+ * How long a first `Ctrl+C` keeps the resident attach client's detach chord armed. It matches the
5
+ * owned dialogs' interrupt chord so an unhurried double press leaves resident tabs from any tab.
6
+ */
7
+ export const RESIDENT_DETACH_INTERVAL_MS = 1_500;
3
8
  // Protocol: reset modes on both screens; setting keyboard flags to zero is idempotent,
4
9
  // unlike repeatedly popping a keyboard-protocol stack owned by a parent application.
5
10
  // Resetting parent-screen margins homes the cursor, so preserve its restored position.
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { promisify } from "node:util";
4
4
  import { PRODUCT_IDENTITY } from "../../../product-identity.js";
5
+ import { residentProgressObserver } from "../../../foundation/resident-tabs/progress-observation.js";
5
6
  import { observeCompactionProgress } from "./compaction-progress.js";
6
7
  import { createPiRuntimeIntegration } from "./runtime-integration.js";
7
8
  import { PiSessionCommandIntegration } from "./session-integration.js";
@@ -369,8 +370,10 @@ export class PiEngineRuntime {
369
370
  return;
370
371
  const generation = this.#sessionGeneration;
371
372
  this.#unsubscribe = session.subscribe(event => {
372
- if (generation === this.#sessionGeneration && !this.#ports.disposed())
373
- this.#ports.event(event);
373
+ if (generation !== this.#sessionGeneration || this.#ports.disposed())
374
+ return;
375
+ residentProgressObserver()?.engineDelivery(event);
376
+ this.#ports.event(event);
374
377
  });
375
378
  }
376
379
  /**
@@ -1,5 +1,11 @@
1
1
  import { type ProgramStatus, type WheelScrollLines } from "@earendil-works/pi-tui";
2
2
  import type { PiTuiComponentPort, PiTuiInputListener, PiTuiOverlayHandle, PiTuiOverlayOptions, PiTuiPreInputListener, PiTuiRuntimeAdapterOptions, PiTuiRuntimeState, PiTuiScrollState, PiTuiStopOptions, PiTuiViewport } from "./contracts.js";
3
+ /**
4
+ * Re-encodes one key report decoded by pi-tui in the legacy form, so a kitty-keyboard child
5
+ * (resident tabs) and a legacy terminal produce the same owned-screen action. Releases decode
6
+ * to nothing; mouse reports, paste, and unknown sequences pass through unchanged.
7
+ */
8
+ export declare function legacyKeyInput(data: string): string;
3
9
  export type PiTuiRuntimeErrorStage = "construction" | "start" | "input-drain" | "restoration";
4
10
  /** Identifies the Pi TUI lifecycle stage that failed while preserving the original cause. */
5
11
  export declare class PiTuiRuntimeError extends Error {
@@ -1,8 +1,35 @@
1
- import { HStack, ProcessTerminal, ScrollView, TuiAltScreen, TuiMainScreen, VStack, visibleWidth, } from "@earendil-works/pi-tui";
1
+ import { HStack, ProcessTerminal, ScrollView, TuiAltScreen, TuiMainScreen, VStack, decodeKittyPrintable, isKeyRelease, parseKey, visibleWidth, } from "@earendil-works/pi-tui";
2
2
  import { MouseReportInput, stripSgrMouseReports } from "./mouse-report-input.js";
3
3
  import { GeometryObservedAltScreen, OverlayGeometryTracker } from "./overlay-geometry.js";
4
4
  import { boundedCleanup, EMERGENCY_TERMINAL_RESET } from "../../../foundation/terminal-cleanup/terminal-reset.js";
5
5
  import { InputPresentationCoordinator, } from "./input-presentation-coordinator.js";
6
+ // Protocol: the legacy bytes owned screens and their key tables compare against.
7
+ const LEGACY_KEY_BYTES = {
8
+ escape: "\u001b", up: "\u001b[A", down: "\u001b[B", right: "\u001b[C", left: "\u001b[D",
9
+ "shift+up": "\u001b[1;2A", "shift+down": "\u001b[1;2B", pageUp: "\u001b[5~", pageDown: "\u001b[6~",
10
+ home: "\u001b[H", end: "\u001b[F", "ctrl+home": "\u001b[1;5H", "ctrl+end": "\u001b[1;5F",
11
+ enter: "\r", tab: "\t", "shift+tab": "\u001b[Z", backspace: "\u007f", delete: "\u001b[3~",
12
+ space: " ", "ctrl+c": "\u0003", "ctrl+z": "\u001a",
13
+ };
14
+ /**
15
+ * Re-encodes one key report decoded by pi-tui in the legacy form, so a kitty-keyboard child
16
+ * (resident tabs) and a legacy terminal produce the same owned-screen action. Releases decode
17
+ * to nothing; mouse reports, paste, and unknown sequences pass through unchanged.
18
+ */
19
+ export function legacyKeyInput(data) {
20
+ if (data.length <= 1 || !data.startsWith("\u001b"))
21
+ return data;
22
+ const key = parseKey(data);
23
+ if (key === undefined)
24
+ return data;
25
+ if (isKeyRelease(data))
26
+ return "";
27
+ const legacy = LEGACY_KEY_BYTES[key];
28
+ if (legacy !== undefined)
29
+ return legacy;
30
+ const printable = decodeKittyPrintable(data);
31
+ return printable !== undefined && printable >= " " && printable !== "\u007f" ? printable : data;
32
+ }
6
33
  /** Identifies the Pi TUI lifecycle stage that failed while preserving the original cause. */
7
34
  export class PiTuiRuntimeError extends Error {
8
35
  stage;
@@ -52,6 +52,17 @@ export interface DamageAwareTerminalOptions {
52
52
  };
53
53
  readonly onResize?: () => void;
54
54
  readonly onHyperlinkCleanupRequired?: () => void;
55
+ /** Exact-artifact fault controls evaluated before physical writes; absent in ordinary sessions. */
56
+ readonly allowWrite?: () => boolean;
57
+ readonly writeChunks?: (data: string) => readonly string[];
58
+ /** Optional payload-free observation after a terminal write returns. */
59
+ readonly onWrite?: (metadata: {
60
+ readonly bytes: number;
61
+ readonly fragments: number;
62
+ readonly transaction: "synchronized-output" | "ordinary";
63
+ readonly frameId?: number;
64
+ readonly cause?: string;
65
+ }) => void;
55
66
  }
56
67
  /**
57
68
  * A fail-closed adapter for one pinned Pi fullscreen write grammar. Semantic
@@ -166,7 +166,36 @@ export class DamageAwareTerminalAdapter {
166
166
  const forwarded = this.#canvasBackgroundAnsi === null
167
167
  ? output
168
168
  : paintTerminalCanvasFrame(output, this.#canvasBackgroundAnsi);
169
- this.inner.write(forwarded);
169
+ let writeAllowed = true;
170
+ try {
171
+ writeAllowed = this.options.allowWrite?.() ?? true;
172
+ }
173
+ catch { /* Invariant: fixture controls fail open. */ }
174
+ if (!writeAllowed)
175
+ return;
176
+ let chunks = [forwarded];
177
+ try {
178
+ const controlled = this.options.writeChunks?.(forwarded);
179
+ if (controlled !== undefined && controlled.length > 0 && controlled.length <= 4_096
180
+ && controlled.join("") === forwarded)
181
+ chunks = controlled;
182
+ }
183
+ catch { /* Invariant: fixture controls fail open without corrupting the renderer write. */ }
184
+ for (const chunk of chunks)
185
+ this.inner.write(chunk);
186
+ try {
187
+ this.options.onWrite?.({
188
+ bytes: Buffer.byteLength(forwarded),
189
+ fragments: chunks.length,
190
+ transaction: forwarded.startsWith(BEGIN_SYNCHRONIZED_OUTPUT) && forwarded.endsWith(END_SYNCHRONIZED_OUTPUT)
191
+ ? "synchronized-output" : "ordinary",
192
+ ...(armed === undefined ? {} : {
193
+ frameId: armed.descriptor.frameId,
194
+ cause: armed.descriptor.cause,
195
+ }),
196
+ });
197
+ }
198
+ catch { /* Invariant: observation never changes rendering or write completion. */ }
170
199
  if (epoch !== this.#epoch
171
200
  || (armed !== undefined && this.#lastConsumedFrameId !== armed.descriptor.frameId))
172
201
  return;
@@ -7,7 +7,7 @@
7
7
  "platform": "darwin",
8
8
  "architecture": "arm64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-19T04:50:42.711Z",
10
+ "builtAt": "2026-10-19T14:46:40.019Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "a87eaaffcd986b624c8b044b756446cc1bdaf4334a1775b97820542fe34f9fb7",
@@ -9,11 +9,11 @@
9
9
  "architecture": "arm64",
10
10
  "target": "aarch64-apple-darwin",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-19T04:52:31.538Z",
12
+ "builtAt": "2026-10-19T14:50:21.966Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "af7b9fee2efa8478c728465031e73fb316b6b7012d11de0364130b0acd1b6e62",
16
- "size": 5173904
15
+ "sha256": "12b73c5b9c2023badcdcbea9f2c3cca018cbb1704c0cb452303f59a551b0c407",
16
+ "size": 5813520
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,7 +7,7 @@
7
7
  "platform": "darwin",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-19T04:50:48.870Z",
10
+ "builtAt": "2026-10-19T14:46:46.372Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "783a65f0de287a1c02cd5429dbe97bc8633f4cc8a7b92e3e961180b9bdcff814",
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-apple-darwin",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-19T04:54:11.822Z",
12
+ "builtAt": "2026-10-19T14:53:04.323Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "1a5465ea12ecfe76d5f0c8fd5c11dee99597b4206a7e9ece01b37fc5c477c211",
16
- "size": 5656912
15
+ "sha256": "aea613cb66de9d7f87018ee32aba7d30d695a86752301e1dde771bdeb7dad724",
16
+ "size": 6349264
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,7 +7,7 @@
7
7
  "platform": "linux",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-19T04:50:20.956Z",
10
+ "builtAt": "2026-10-19T14:46:32.168Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
13
  "sha256": "118e5954e3864672bd8482ab7edf419cb27dcdf2533720316979b7f5cc5a53a5",
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-unknown-linux-gnu",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-19T04:53:24.471Z",
12
+ "builtAt": "2026-10-19T14:49:27.044Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "843b10ab29eddf859a74a97875ba646a90917d9ec78445474895bb64f6e0772a",
16
- "size": 6353496
15
+ "sha256": "890d87b2fccecb4956243c40da97c461e180a40ace6b340d5c9f808ab9f4b586",
16
+ "size": 7099552
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -7,10 +7,10 @@
7
7
  "platform": "win32",
8
8
  "architecture": "x64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-19T04:51:53.049Z",
10
+ "builtAt": "2026-10-19T14:46:55.773Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
- "sha256": "19eb07160f90033855f43c38008aaf90665e40bc68a87fac0c44585b362977cd",
13
+ "sha256": "e8b39eb8c589c028024164c021a08183021574d06b48fbefc34fb3fb55636c62",
14
14
  "size": 402432
15
15
  },
16
16
  "provenance": {
@@ -9,11 +9,11 @@
9
9
  "architecture": "x64",
10
10
  "target": "x86_64-pc-windows-msvc",
11
11
  "capability": "supported",
12
- "builtAt": "2026-10-19T04:54:01.087Z",
12
+ "builtAt": "2026-10-19T14:49:14.794Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "2e11d6f772aac6780a0d4d55a6a79169483b14b2b1e055f0f694702d38cab825",
16
- "size": 5617152
15
+ "sha256": "7661bc29672ef40853a94bcfe367fd6ed5c03b9e6e3e3ac4ac4bbea4c2aad891",
16
+ "size": 6323712
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -1,10 +1,10 @@
1
- declare const ENVIRONMENT_KEYS: readonly ["clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "exitNoticePath", "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "tabEndpoint", "tabId", "tabToken", "tabsEndpoint", "terminalHostPath", "terminalSessionId"];
1
+ declare const ENVIRONMENT_KEYS: readonly ["clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "exitNoticePath", "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "residentFixtureControlDir", "residentProgressObservationDir", "residentProgressRunId", "residentProgressTabId", "residentProgressTabIncarnation", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "tabEndpoint", "tabId", "tabToken", "tabsEndpoint", "terminalHostPath", "terminalSessionId"];
2
2
  declare const FILESYSTEM_KEYS: readonly ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
3
3
  declare const STATE_KEYS: readonly ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile", "sessionRepositoryDirectory", "sessionWorktreeClaimDirectory"];
4
4
  declare const ENDPOINT_KEYS: readonly ["windowsPipeStem", "unixSocketFilename", "metadataFilename", "supervisorLogFilename", "databaseFilename"];
5
5
  declare const MANIFEST_KEYS: readonly ["releaseFilename", "packageFilename"];
6
6
  declare const PROTOCOL_KEYS: readonly ["namespace", "controlEnvelope", "supervisorSchema", "nativeHostSchema", "structuredAgentSchema", "controlStoreSchema", "promptHistorySchema", "releaseCohortSchema", "updateJournalSchema"];
7
- declare const EVIDENCE_KEYS: readonly ["nativeSpikeSchema", "terminalProvenanceSchema", "terminalProofSchema", "stableReleaseSchema", "previewReleaseSchema", "releaseCertificationSchema", "previewPlatformVerdictSchema", "piSourceLedgerSchema", "piComponentParitySchema", "piEventFrameParitySchema", "startupTraceSchema", "dependencyLayerSchema", "dependencyLayerCertificationSchema", "runtimePayloadSchema"];
7
+ declare const EVIDENCE_KEYS: readonly ["nativeSpikeSchema", "terminalProvenanceSchema", "terminalProofSchema", "terminalTemporalSnapshotSchema", "stableReleaseSchema", "previewReleaseSchema", "releaseCertificationSchema", "previewPlatformVerdictSchema", "piSourceLedgerSchema", "piComponentParitySchema", "piEventFrameParitySchema", "startupTraceSchema", "dependencyLayerSchema", "dependencyLayerCertificationSchema", "runtimePayloadSchema"];
8
8
  declare const ARTIFACT_KEYS: readonly ["cliEntry", "supervisorEntry", "guardianEntry", "uiEntry", "terminalHostAddon", "terminalHostEntry", "nativeCrate", "processGuardianAddon", "releaseTarballStem", "diagnosticStem"];
9
9
  type StringRecord<Keys extends readonly string[]> = {
10
10
  readonly [Key in Keys[number]]: string;
@@ -1,14 +1,14 @@
1
1
  import rawIdentity from "./product-identity.json" with { type: "json" };
2
2
  const ENVIRONMENT_KEYS = [
3
3
  "clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "exitNoticePath",
4
- "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "tabEndpoint", "tabId", "tabToken", "tabsEndpoint", "terminalHostPath", "terminalSessionId",
4
+ "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "residentFixtureControlDir", "residentProgressObservationDir", "residentProgressRunId", "residentProgressTabId", "residentProgressTabIncarnation", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "tabEndpoint", "tabId", "tabToken", "tabsEndpoint", "terminalHostPath", "terminalSessionId",
5
5
  ];
6
6
  const FILESYSTEM_KEYS = ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
7
7
  const STATE_KEYS = ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile", "sessionRepositoryDirectory", "sessionWorktreeClaimDirectory"];
8
8
  const ENDPOINT_KEYS = ["windowsPipeStem", "unixSocketFilename", "metadataFilename", "supervisorLogFilename", "databaseFilename"];
9
9
  const MANIFEST_KEYS = ["releaseFilename", "packageFilename"];
10
10
  const PROTOCOL_KEYS = ["namespace", "controlEnvelope", "supervisorSchema", "nativeHostSchema", "structuredAgentSchema", "controlStoreSchema", "promptHistorySchema", "releaseCohortSchema", "updateJournalSchema"];
11
- const EVIDENCE_KEYS = ["nativeSpikeSchema", "terminalProvenanceSchema", "terminalProofSchema", "stableReleaseSchema", "previewReleaseSchema", "releaseCertificationSchema", "previewPlatformVerdictSchema", "piSourceLedgerSchema", "piComponentParitySchema", "piEventFrameParitySchema", "startupTraceSchema", "dependencyLayerSchema", "dependencyLayerCertificationSchema", "runtimePayloadSchema"];
11
+ const EVIDENCE_KEYS = ["nativeSpikeSchema", "terminalProvenanceSchema", "terminalProofSchema", "terminalTemporalSnapshotSchema", "stableReleaseSchema", "previewReleaseSchema", "releaseCertificationSchema", "previewPlatformVerdictSchema", "piSourceLedgerSchema", "piComponentParitySchema", "piEventFrameParitySchema", "startupTraceSchema", "dependencyLayerSchema", "dependencyLayerCertificationSchema", "runtimePayloadSchema"];
12
12
  const ARTIFACT_KEYS = ["cliEntry", "supervisorEntry", "guardianEntry", "uiEntry", "terminalHostAddon", "terminalHostEntry", "nativeCrate", "processGuardianAddon", "releaseTarballStem", "diagnosticStem"];
13
13
  const ROOT_KEYS = ["schema", "displayName", "commandName", "packageName", "filesystem", "environment", "state", "endpoint", "manifest", "protocol", "evidence", "artifacts"];
14
14
  export function validateProductIdentity(value) {
@@ -23,6 +23,11 @@
23
23
  "processGuardianPath": "A1_PROCESS_GUARDIAN_PATH",
24
24
  "prFooterPreview": "A1_PR_FOOTER_PREVIEW",
25
25
  "profileHome": "A1_PROFILE_HOME",
26
+ "residentFixtureControlDir": "A1_RESIDENT_FIXTURE_CONTROL_DIR",
27
+ "residentProgressObservationDir": "A1_RESIDENT_PROGRESS_OBSERVATION_DIR",
28
+ "residentProgressRunId": "A1_RESIDENT_PROGRESS_RUN_ID",
29
+ "residentProgressTabId": "A1_RESIDENT_PROGRESS_TAB_ID",
30
+ "residentProgressTabIncarnation": "A1_RESIDENT_PROGRESS_TAB_INCARNATION",
26
31
  "runtimeDir": "A1_RUNTIME_DIR",
27
32
  "sessionRuntimeId": "A1_SESSION_RUNTIME_ID",
28
33
  "skipVersionCheck": "A1_SKIP_VERSION_CHECK",
@@ -70,6 +75,7 @@
70
75
  "nativeSpikeSchema": "a1-native-host-spike-evidence-v1",
71
76
  "terminalProvenanceSchema": "a1-terminal-host-provenance-v1",
72
77
  "terminalProofSchema": "a1-terminal-host-proof-v1",
78
+ "terminalTemporalSnapshotSchema": "a1-terminal-temporal-snapshot-v1",
73
79
  "stableReleaseSchema": "a1-stable-release-v1",
74
80
  "previewReleaseSchema": "a1-preview-release-v1",
75
81
  "releaseCertificationSchema": "a1-release-certification-v1",
@@ -47,6 +47,12 @@ export declare class OwnedSettingsManager {
47
47
  /** Reads and resolves the product namespace. Never throws: every failure becomes a notice. */
48
48
  read(): OwnedUiSettingsResolution;
49
49
  load(): Promise<void>;
50
+ /**
51
+ * Adopts values another process stored in the shared document. Notifies only when a
52
+ * resolved value differs, so a peer's own write echo stays silent. Restart-bound values
53
+ * keep their session-start effect and surface as pending instead.
54
+ */
55
+ refresh(): boolean;
50
56
  sections(): readonly OwnedUiSettingsSection[];
51
57
  /**
52
58
  * The value in effect for a declared setting, typed by its declaration. A restart-bound
@@ -129,6 +129,28 @@ export class OwnedSettingsManager {
129
129
  this.#agentSnapshot = agent === null ? null : await snapshotOf(agent, this.#hiddenAgentSettingIds);
130
130
  this.#notify();
131
131
  }
132
+ /**
133
+ * Adopts values another process stored in the shared document. Notifies only when a
134
+ * resolved value differs, so a peer's own write echo stays silent. Restart-bound values
135
+ * keep their session-start effect and surface as pending instead.
136
+ */
137
+ refresh() {
138
+ const next = this.read();
139
+ if (resolvedValuesKey(next) === resolvedValuesKey(this.#resolution))
140
+ return false;
141
+ this.#resolution = next;
142
+ for (const setting of next.settings) {
143
+ const id = setting.declaration.id;
144
+ if (!this.#restartEffective.has(id))
145
+ continue;
146
+ if (setting.value === this.#restartEffective.get(id))
147
+ this.#pending.delete(id);
148
+ else
149
+ this.#pending.set(id, setting.value);
150
+ }
151
+ this.#notify();
152
+ return true;
153
+ }
132
154
  sections() {
133
155
  const resolution = this.#hiddenOwnedSettingIds.size === 0
134
156
  ? this.#resolution
@@ -270,6 +292,9 @@ async function snapshotOf(agent, hidden) {
270
292
  return { descriptors: [], writeAdvertised: false, failure: describe(error) };
271
293
  }
272
294
  }
295
+ function resolvedValuesKey(resolution) {
296
+ return JSON.stringify(resolution.settings.map(setting => [setting.declaration.id, setting.value]));
297
+ }
273
298
  function failed(failure) {
274
299
  return {
275
300
  status: "failed",
@@ -223,13 +223,13 @@ The channels carry bounded generation-1 frames: a u32 little-endian length follo
223
223
  - The server sends `bridge.visibility{visible}`, so a hidden A1 throttles animation, and `bridge.rename`, so a strip rename sets the Pi session name.
224
224
  - The bridge is advisory. A missing or broken bridge degrades that tab to process-level status and never breaks the tab.
225
225
  - **Backpressure:**
226
- - Each client connection has a reliable, bounded control lane and a **single-slot render lane**. A newer full current surface replaces an unsent one, so a client that drains never depends on a dropped intermediate revision.
227
- - The server never blocks a holder, another client, or registry writes on a slow client.
226
+ - Each client connection has a reliable, bounded control lane and a **single-slot render lane**. A newer full current surface replaces an unsent one, so a client that drains never depends on a dropped intermediate revision. Structured surfaces are immutable `Arc` values while routed; encoding happens outside the routing mutex, decoded structured state is capped at 8 MiB, and a superseded holder socket cannot publish into a newer incarnation.
227
+ - The server never blocks a holder, another client, or registry writes on a slow client. Attach and holder hot loops yield after bounded frame, byte, and two-millisecond work quanta; completed outer writes, rather than offers, advance the presentation baseline.
228
228
  - Holders parse their PTY continuously, whether or not the tab is viewed, so a PTY is never throttled and a child never blocks on output.
229
229
 
230
230
  ### 6. Rendering, input, and outer-terminal fidelity
231
231
 
232
- - **Composition.** The attach client composes row 0 (the strip) and rows `1..N` (the viewed tab's surface). The tab PTY is sized `cols × (rows − 1)`. Composition reuses the proof's damage-aware composer and synchronized-output support, and falls back to a full repaint only when the layout changes.
232
+ - **Composition.** The attach client composes row 0 (the strip) and rows `1..N` (the viewed tab's surface). The tab PTY is sized `cols × (rows − 1)`. Ghostty remains the sole child-terminal parser. Its stream parser reports synchronized-output begin/end/reset boundaries to the holder, which keeps mutable parser state separate from an immutable committed surface and never publishes an open transaction. A non-renewable 250 ms deadline publishes one typed recovery commit for a malformed transaction. The outer composer owns the only synchronized envelope and falls back to a full repaint only when identity, geometry generation, dimensions, or the structured base changes.
233
233
  - **Outer modes.** The client negotiates the outer terminal's modes: raw mode, alternate screen, bracketed paste, focus events, SGR mouse, kitty keyboard protocol where available, and synchronized output.
234
234
  - Child-requested modes are tracked in the holder's model. Input is re-encoded per child mode by libghostty-vt, which the proof already does.
235
235
  - Mouse rows are offset by the strip. Row 0 belongs to the client.
@@ -249,8 +249,8 @@ Direct `a1 solo` is the behavioural oracle for every viewed resident child. Only
249
249
  - **Typed effects.** The holder registers libghostty's clipboard-write callback, which normalizes OSC 52 and OSC 1337. A write from the viewed tab crosses as one bounded `terminal.effect`; attach writes one OSC 52 for it. A hidden tab's write is dropped, never replayed into a later view. Payloads are never logged.
250
250
  - **Modes and cursor.** Each surface carries the child's cursor cell and modes. Attach mirrors a cursor shape the child chose and resets it on detach or failure; a default block cursor is left to the outer terminal, as direct A1 leaves it.
251
251
  - **Shortcut projection.** The owned child projects the effective A1 keybindings plus `keybindings.json` into one assignment per tab action: a key an A1 action holds never becomes a tab shortcut, and the earlier declaration keeps a duplicated key. It publishes the projection as `bridge.shortcuts`; the server relays it as `tabs.shortcuts`, and attach dispatches and labels exactly that projection. `/hotkeys` renders the same projection. A shared case file is replayed by both resolvers.
252
- - **Committed presentation.** A requested view commits only with a topology that lists it and a complete target surface. Surfaces of the committed view apply in revision order, and revisions rise across holder incarnations. A retained re-offer is marked stale and parks for at most 120 ms while the fresh frame arrives. One compositor writes strip, body, overlay, and final cursor inside a single synchronized update, and refuses clear-only, blank, and cursor-less strip-only frames. Clock, hover, and title-animation damage repaints only the strip and restores the child's cursor.
253
- - **Evidence.** One interaction matrix drives three layers: an owned-shell oracle that records content-free observations, role fixtures that drive real server, holder, and attach processes with a certified parity child, and an exact-artifact harness, `verify-interaction-parity.mjs`, that compares the bytes the same child observes directly and through the built host on each platform. Observations carry digests and lengths, never content. Each platform's verdict stands alone.
252
+ - **Committed presentation.** A requested view commits only with a topology that lists it and a complete target surface. Retained rows, palette/default colors, modes, and cursor are captured atomically; equal captures do not invent a revision. Surfaces of the committed view apply in revision order, and revisions rise across holder incarnations. A retained re-offer is marked stale from the request start and parks for at most 120 ms while the fresh frame arrives. One compositor writes strip, body, overlay, modes, and the viewport-clipped final cursor inside a single synchronized update. It erases suffix cells when a complete replacement row becomes shorter and restores the newest underlying surface when an overlay leaves. Clock, hover, and title-animation damage repaints only the strip and restores the child's cursor.
253
+ - **Evidence.** One interaction matrix drives three layers: an owned-shell oracle that records content-free observations, role fixtures that drive real server, holder, and attach processes with a certified parity child, and exact-artifact harnesses. `verify-interaction-parity.mjs` compares the bytes the same child observes directly and through the built host. `verify-resident-parallel-progress.mjs` configures the real engine against a bounded loopback streaming provider and crosses all six first-response orders with all three initial views while every turn remains unfinished behind a completion barrier. At matching 100×29 child geometry, the same real-A1 prompt also runs through isolated direct-solo and resident profiles; the native Ghostty oracle compares a content-free digest of the intermediate fixture cells and their resolved styles together with normalized cursor and mode state. It advances only at parser-reported synchronized-output boundaries, rejects unbalanced transactions or disappearing intermediate cells, and excludes only the resident strip row rather than normalizing terminal semantics. Opt-in per-process JSONL observations identify engine delivery, completed child writes, PTY ingestion, distinct holder commits, attach receipt, and completed outer paint by run/tab/incarnation/process-instance/sequence without recording terminal or conversation content; queue loss, clock faults, duplicates, stale order, and missing stages fail the report. Named pressure variants keep all turns unfinished while exercising hidden progress, repeated switching, navigation input, rename overlays, tab-local resize, a bounded 35 ms attach-receipt delay, and a 900 ms paused outer sink; every tab must retain partial animation/progress before settlement and then present its final output. A detached/reconnect variant releases all three unfinished turns after terminating the authenticated attach and exact self-reported server identity; child writes, PTY ingestion, and holder commits must continue before a replacement server and attach present every retained final output. Separate isolated exact-artifact control markers stop one target tab at engine delivery, physical child rendering, holder commit, or outer paint; each control requires its upstream stages and the other tabs to advance, then removes the stop and requires final output from all three tabs. A fifth control fragments a real owned-shell synchronized write into at most 64 physical writes without changing its bytes, records the DEC private mode 2026 transaction classification, and requires ordinary complete holder commits without deadline recovery before final output. This characterizes the built PTY/ConPTY coherence path; a passing fragmented run does not by itself assign the historical stall to that path without a failing live reproduction. The marker path is absent in ordinary sessions and is scrubbed before extensions or tools inherit the child environment, while observation callbacks remain passive and cannot select a control. CI runs the vendored Ghostty VT tests and both exact-artifact harnesses on Windows x64, Linux x64, and macOS arm64. Each platform's verdict stands alone.
254
254
 
255
255
  ### 7. Status and attention come from the tab bridge
256
256
 
@@ -262,11 +262,12 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
262
262
  | `idle` | settled idle | none |
263
263
  | `working` | agent or turn start, tool execution, compaction | shared progress frames |
264
264
  | `needs-input` | an extension UI, trust, or permission request is pending | yellow `?` (warning) |
265
- | `done-unseen` | the turn settled while no client viewed the tab | `✓` success |
266
- | `error` | the last turn's `stopReason` was error | `✗` error |
267
- | `crashed` / `restarting` / `failed` | holder or child exit | `✗` error plus a banner |
265
+ | `done` | the last turn finished successfully | green `✓` success |
266
+ | `aborted` | the last turn was aborted | red `✗` error |
267
+ | `error` | the last turn ended with an error | red `✗` error |
268
+ | `crashed` / `restarting` / `failed` | holder or child exit, or a failed tab lifecycle (never a failed turn) | `✗` error plus a banner |
268
269
 
269
- - `seen` is tracked server-side, so viewing a tab in any client clears `✓` everywhere.
270
+ - A `done`, `aborted`, or `error` marker stays on the chip, including after any client views the tab, until that tab's next turn starts.
270
271
  - A tab with a broken bridge shows only process-level states.
271
272
  - Future CLI tabs will derive status from process state, and may use BEL or OSC 9/777 from the program as an attention input.
272
273
  - The glyph set is the same in every client. A working spinner uses the resolved A1 accent reported by that specific tab.
@@ -335,7 +336,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
335
336
  | `Alt+A` | new tab |
336
337
  | `Alt+W` | close |
337
338
  | `F2` | rename |
338
- | `Alt+.` | create or activate the global Settings tab |
339
+ | `Alt+,` | open the global Settings tab, or close it while it is viewed |
339
340
  | `Alt+1`…`Alt+9`, `Alt+0` | jump to a tab |
340
341
  | unbound (`tabs.next` / `tabs.previous`) | next / previous, wrapping |
341
342
  | unbound (`tabs.moveRight` / `tabs.moveLeft`) | move the tab right / left |
@@ -349,7 +350,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
349
350
  - right-click opens `Rename`/`Close` (`Close` only for Settings);
350
351
  - dragging reorders, with a `│` drop marker, after 250 ms or once the pointer moves;
351
352
  - `+` opens a persistent dark v2 menu with `+ Agent`, a separator, and `Settings`; hover and click remain active through the opening release.
352
- - **Settings.** The strip owns one profile-global `Settings` tab. The grouped launcher and `Alt+.` create or activate it; closing the screen or tab removes it. Resident Agent autocomplete omits `/settings`, and manually entering it does not open a per-Agent screen. `a1 solo` and `a1 pi` retain their existing Settings routes.
353
+ - **Settings.** The strip owns one profile-global `Settings` tab. The grouped launcher creates or activates it; `Alt+,` does the same from another tab and closes it while it is viewed. Esc does not close the Settings tab; closing the tab removes it. Resident Agent autocomplete omits `/settings`, and manually entering it does not open a per-Agent screen. `a1 solo` and `a1 pi` retain their existing Settings routes.
353
354
  - **Commands inside A1 tabs:** existing `/name <name>` also renames the tab. Milestone 5 adds no tab-management slash commands: `/new-tab`, `/close`, `/tabs`, `/quit-all`, and `/restart-tab` are not exposed.
354
355
  - **Naming:**
355
356
  - Default names are `agent` or `agent N`.
@@ -391,7 +392,9 @@ Every queue, buffer, file, and collection the resident roles keep has a declared
391
392
  | Bound | Cap | Enforced in | Test |
392
393
  |---|---|---|---|
393
394
  | Protocol frame | 2 MiB, nesting 64, 65,536 collection items | `native/terminal-host/src/protocol.rs`, `src/foundation/resident-tabs/protocol.ts` | `refuses_oversized_header_before_payload`; `refuses a frame header past 2 MiB before buffering its payload`, `refuses nesting past 64 levels`, `refuses a collection past 65,536 items` |
395
+ | Structured complete surface | 512 × 256 geometry, 65,536 cells, 4,096 styles, 4,096 links, and 8 MiB decoded state | `protocol.rs` `StructuredSurface::validate` | `structured_surfaces_fail_closed_on_dimensions_indexes_and_control_injection` |
394
396
  | Input per message and holder input queue | 1 MiB | `protocol.rs`, `core/holder.rs`, `roles/holder.rs` | `over_capacity_paste_delivers_no_prefix` |
397
+ | Holder PTY ingress and work round | 64 × 16 KiB lossless chunks; at most 256 KiB or 2 ms before yielding | `roles/holder.rs` | `verify-resident-parallel-progress.mjs` concurrent and stopped-stage cases |
395
398
  | Per-client control lane | 1,024 frames and 4 MiB | `roles/server.rs` | `control_lane_is_bounded_by_frames_and_bytes` |
396
399
  | Per-client render slot | one pending full surface; a newer one replaces it | `roles/server.rs` `Peer::offer_surface` | `stalled_client_gets_one_full_current_surface` |
397
400
  | Concurrent endpoint connections | 128 | `roles/server.rs` `admit_connection` | `connections_beyond_the_cap_are_refused_and_released_slots_are_reused` |
@@ -407,7 +410,9 @@ Every queue, buffer, file, and collection the resident roles keep has a declared
407
410
  | Session reconciliation read | 64 MiB | `src/foundation/resident-tabs/recovery.ts` | `refuses to scan a session past the 64 MiB reconciliation cap` |
408
411
  | Release-hold report | 32 uncertainty reasons; 1 MiB read | `roles/release_holds.rs`, `release-holds.ts`, `bounded-host-command.ts` | `uncertainty_reasons_stay_within_their_cap`; `fails a release-hold report that pushes past its byte cap without returning a prefix` |
409
412
  | Maintenance exchange | 5 s reads, 60 s stops, 4 MiB report | `roles/maintain.rs`, `maintenance.ts`, `bounded-host-command.ts` | `fails a maintenance report that pushes past its byte cap without returning a prefix`, `kills and rejects a command that outlives its deadline` |
410
- | Attach client | no outbound queue: input is written synchronously in chunks of at most 1 MiB; inbound frames at most 2 MiB; one coalesced wake | `roles/attach.rs` | `refuses_oversized_header_before_payload`, `over_capacity_paste_delivers_no_prefix` |
413
+ | Attach server ingress | reader queue 64 frames/4 MiB; execution queue 8 frames/4 MiB; at most 64 KiB or 2 ms before yielding | `roles/attach.rs` | `verify-resident-parallel-progress.mjs` slow-receipt and switching cases |
414
+ | Attach input | 1,024 parsed events; semantic input is emitted in at most 1 MiB chunks and target-bound while a view/create is pending | `roles/attach.rs` | `deferred_input_is_atomic_bounded_and_bound_to_the_requested_tab` |
415
+ | Outer presentation writer | one replaceable complete desired state, 8 control/effect frames, and 4 MiB effect bytes | `roles/attach.rs` `OuterWriter` | `presentation_is_one_ordered_synchronized_write`; `verify-resident-parallel-progress.mjs` paused-sink case |
411
416
  | Doctor bundle | 25 MiB, oldest log generations dropped first | `src/foundation/resident-tabs/doctor.ts` | `drops the oldest log generations first to stay within its cap` |
412
417
 
413
418
  ### 13. Packaging and certification
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.2.7-dev.806",
3
+ "version": "0.2.7-dev.808",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",
@@ -53,6 +53,7 @@
53
53
  "test:full": "node scripts/release/run-validation-tier.mjs full-release",
54
54
  "test:release": "node scripts/release/run-release-gates.mjs",
55
55
  "test:terminal-host": "node scripts/development/run-terminal-host-probe.mjs",
56
+ "test:resident-parallel-progress": "node scripts/development/verify-resident-parallel-progress.mjs",
56
57
  "test:resident-chaos": "node scripts/development/run-resident-chaos.mjs --build",
57
58
  "test:pi-engine-conformance": "vitest run test/integrations/pi/engine/conformance.test.ts test/integrations/pi/engine/runtime-integration.test.ts test/integrations/pi/engine/session-integration.test.ts",
58
59
  "report:pi-engine-conformance": "npm run build --silent && node scripts/pi/run-pi-engine-conformance.mjs",