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

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 (105) hide show
  1. package/dist/app/session-shell/dialog-coordinator.d.ts +42 -0
  2. package/dist/app/session-shell/dialog-coordinator.js +133 -0
  3. package/dist/app/session-shell/dock-chrome.d.ts +34 -0
  4. package/dist/app/session-shell/dock-chrome.js +109 -0
  5. package/dist/app/session-shell/exit-notice.d.ts +1 -1
  6. package/dist/app/session-shell/exit-notice.js +1 -1
  7. package/dist/app/session-shell/extension-surface.d.ts +30 -0
  8. package/dist/app/session-shell/extension-surface.js +105 -0
  9. package/dist/app/session-shell/input-surface-switch.d.ts +36 -0
  10. package/dist/app/session-shell/input-surface-switch.js +85 -0
  11. package/dist/app/session-shell/login-interaction.d.ts +27 -0
  12. package/dist/app/session-shell/login-interaction.js +91 -0
  13. package/dist/app/session-shell/prompt-submission.d.ts +48 -0
  14. package/dist/app/session-shell/prompt-submission.js +276 -0
  15. package/dist/app/session-shell/resident-tab-coordinator.d.ts +41 -0
  16. package/dist/app/session-shell/resident-tab-coordinator.js +120 -0
  17. package/dist/app/session-shell/session-concern.d.ts +22 -0
  18. package/dist/app/session-shell/session-concern.js +29 -0
  19. package/dist/app/session-shell/session-lifecycle.d.ts +29 -0
  20. package/dist/app/session-shell/session-lifecycle.js +49 -0
  21. package/dist/app/session-shell/session-shell-root.d.ts +1 -1
  22. package/dist/app/session-shell/session-shell-root.js +182 -931
  23. package/dist/app/session-shell/session-shell.d.ts +2 -8
  24. package/dist/app/session-shell/session-shell.js +209 -897
  25. package/dist/app/session-shell/settings-bindings.d.ts +17 -0
  26. package/dist/app/session-shell/settings-bindings.js +91 -0
  27. package/dist/app/session-shell/shell-editor-factory.d.ts +27 -0
  28. package/dist/app/session-shell/shell-editor-factory.js +102 -0
  29. package/dist/app/session-shell/shell-host.d.ts +6 -0
  30. package/dist/app/session-shell/shell-host.js +26 -4
  31. package/dist/app/session-shell/shell-layout.d.ts +64 -0
  32. package/dist/app/session-shell/shell-layout.js +18 -0
  33. package/dist/app/session-shell/suggestion-surface.d.ts +25 -0
  34. package/dist/app/session-shell/suggestion-surface.js +63 -0
  35. package/dist/app/session-shell/transcript-document.d.ts +78 -0
  36. package/dist/app/session-shell/transcript-document.js +365 -0
  37. package/dist/app/session-shell/viewport-layout.d.ts +31 -0
  38. package/dist/app/session-shell/viewport-layout.js +160 -0
  39. package/dist/app/session-shell/workflow-output.d.ts +33 -0
  40. package/dist/app/session-shell/workflow-output.js +174 -0
  41. package/dist/app/session-shell/workflow-router.d.ts +25 -0
  42. package/dist/app/session-shell/workflow-router.js +39 -0
  43. package/dist/composition/owned-ui.d.ts +6 -25
  44. package/dist/composition/owned-ui.js +48 -242
  45. package/dist/composition/resident-tab-bridge.d.ts +16 -0
  46. package/dist/composition/resident-tab-bridge.js +159 -0
  47. package/dist/composition/session-backend.d.ts +19 -0
  48. package/dist/composition/session-backend.js +1 -0
  49. package/dist/composition/session-runtime-registry.d.ts +20 -0
  50. package/dist/composition/session-runtime-registry.js +85 -0
  51. package/dist/composition/settings-route-host.d.ts +6 -1
  52. package/dist/composition/settings-route-host.js +7 -6
  53. package/dist/composition/startup-route.d.ts +32 -0
  54. package/dist/composition/startup-route.js +72 -0
  55. package/dist/features/owned-ui/settings-app.d.ts +4 -1
  56. package/dist/features/owned-ui/settings-app.js +9 -3
  57. package/dist/features/resident-tab/shortcut-declarations.js +1 -1
  58. package/dist/features/resident-tab/status-deriver.d.ts +4 -3
  59. package/dist/features/resident-tab/status-deriver.js +2 -5
  60. package/dist/foundation/release/bootstrap.js +2 -2
  61. package/dist/foundation/resident-tabs/bridge-client.d.ts +1 -1
  62. package/dist/foundation/resident-tabs/bridge-client.js +2 -1
  63. package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
  64. package/dist/foundation/resident-tabs/fixture-control.js +35 -0
  65. package/dist/foundation/resident-tabs/progress-observation.d.ts +7 -0
  66. package/dist/foundation/resident-tabs/progress-observation.js +119 -0
  67. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  68. package/dist/foundation/terminal-cleanup/index.js +1 -1
  69. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +5 -0
  70. package/dist/foundation/terminal-cleanup/terminal-reset.js +5 -0
  71. package/dist/integrations/pi/engine/adapter.d.ts +4 -77
  72. package/dist/integrations/pi/engine/adapter.js +85 -393
  73. package/dist/integrations/pi/engine/command-dispatch.d.ts +13 -0
  74. package/dist/integrations/pi/engine/command-dispatch.js +57 -0
  75. package/dist/integrations/pi/engine/host.js +2 -2
  76. package/dist/integrations/pi/engine/ports.d.ts +56 -0
  77. package/dist/integrations/pi/engine/ports.js +89 -0
  78. package/dist/integrations/pi/engine/session-runtime.js +5 -2
  79. package/dist/integrations/pi/engine/view-state.d.ts +42 -0
  80. package/dist/integrations/pi/engine/view-state.js +126 -0
  81. package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
  82. package/dist/integrations/pi/tui-runtime/adapter.js +28 -1
  83. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +11 -0
  84. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +30 -1
  85. package/dist/native/darwin-arm64/manifest.json +1 -1
  86. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  87. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  88. package/dist/native/darwin-x64/manifest.json +1 -1
  89. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  90. package/dist/native/darwin-x64/terminal-host.node +0 -0
  91. package/dist/native/linux-x64/manifest.json +1 -1
  92. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  93. package/dist/native/linux-x64/terminal-host.node +0 -0
  94. package/dist/native/win32-x64/manifest.json +2 -2
  95. package/dist/native/win32-x64/process-guardian.node +0 -0
  96. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  97. package/dist/native/win32-x64/terminal-host.node +0 -0
  98. package/dist/product-identity.d.ts +2 -2
  99. package/dist/product-identity.js +2 -2
  100. package/dist/product-identity.json +6 -0
  101. package/dist/ui/settings/manager.d.ts +6 -0
  102. package/dist/ui/settings/manager.js +25 -0
  103. package/docs/architecture/boundaries.md +14 -0
  104. package/docs/architecture/resident-tabs.md +23 -18
  105. package/package.json +2 -1
@@ -0,0 +1,85 @@
1
+ const SESSION_RUNTIME_REFRESH_MS = 30_000;
2
+ /**
3
+ * The live worktree claims this runtime's agents hold: each agent's session identity, the heartbeat
4
+ * that keeps the claims fresh, and their release when the runtime is disposed.
5
+ */
6
+ export class SessionRuntimeRegistry {
7
+ #options;
8
+ // Invariant: one entry per agent of this runtime; each agent holds its own claim, and the runtime's exit releases all.
9
+ #identities = new Map();
10
+ #owner = null;
11
+ #timer = null;
12
+ #refresh = Promise.resolve();
13
+ #disposed = false;
14
+ constructor(options) {
15
+ this.#options = options;
16
+ }
17
+ /** The repository context reader for one agent; undefined names the primary agent. */
18
+ readerFor(agentId) {
19
+ return async (sessionId, sessionFile, signal) => {
20
+ const { runtimeId, inspectProcess } = this.#options;
21
+ const owner = await this.#ownerIdentity();
22
+ if (owner === null || runtimeId === undefined || inspectProcess === undefined)
23
+ return null;
24
+ const identity = { sessionId, sessionFile };
25
+ this.#identities.set(agentId, identity);
26
+ const { activateSessionRepositoryContext } = await import("../foundation/lifecycle/session-repository-context.js");
27
+ const context = await activateSessionRepositoryContext(identity, runtimeId, owner, {
28
+ signal,
29
+ inspectProcess,
30
+ ...(agentId === undefined ? {} : { agentId }),
31
+ });
32
+ this.#arm();
33
+ return context;
34
+ };
35
+ }
36
+ /** Stops the heartbeat, waits for an in-flight refresh, and releases every claim this runtime holds. */
37
+ async dispose() {
38
+ this.#disposed = true;
39
+ if (this.#timer !== null)
40
+ clearInterval(this.#timer);
41
+ this.#timer = null;
42
+ await this.#refresh;
43
+ const { runtimeId } = this.#options;
44
+ const owner = await this.#ownerIdentity();
45
+ if (runtimeId === undefined || owner === null)
46
+ return;
47
+ await import("../foundation/lifecycle/session-repository-context.js")
48
+ .then(module => module.releaseSessionRepositoryRuntime(runtimeId, owner))
49
+ .catch(() => undefined);
50
+ }
51
+ async #ownerIdentity() {
52
+ const { runtimeId, inspectProcess } = this.#options;
53
+ if (runtimeId === undefined || inspectProcess === undefined)
54
+ return null;
55
+ this.#owner ??= inspectProcess(process.pid).catch(() => null);
56
+ return await this.#owner;
57
+ }
58
+ #arm() {
59
+ if (this.#timer !== null)
60
+ return;
61
+ this.#timer = setInterval(() => this.#queueRefresh(), this.#options.refreshMs ?? SESSION_RUNTIME_REFRESH_MS);
62
+ this.#timer.unref();
63
+ }
64
+ #queueRefresh() {
65
+ const { runtimeId, inspectProcess } = this.#options;
66
+ if (this.#disposed || this.#identities.size === 0 || runtimeId === undefined || inspectProcess === undefined)
67
+ return;
68
+ this.#refresh = this.#refresh.then(async () => {
69
+ if (this.#disposed)
70
+ return;
71
+ const owner = await this.#ownerIdentity();
72
+ if (owner === null)
73
+ return;
74
+ const { registerSessionRepositoryRuntime } = await import("../foundation/lifecycle/session-repository-context.js");
75
+ for (const [agentId, identity] of this.#identities) {
76
+ if (this.#disposed)
77
+ return;
78
+ await registerSessionRepositoryRuntime(identity, runtimeId, owner, {
79
+ inspectProcess,
80
+ ...(agentId === undefined ? {} : { agentId }),
81
+ }).catch(() => undefined);
82
+ }
83
+ }).catch(() => undefined);
84
+ }
85
+ }
@@ -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,
@@ -0,0 +1,32 @@
1
+ import type { OwnedUiShellEngineOptions } from "../app/session-shell/index.js";
2
+ import type { ReleaseNoteCatalog } from "../features/owned-ui/release-notes.js";
3
+ type StartupRoute = NonNullable<OwnedUiShellEngineOptions["startupRoute"]>;
4
+ export interface StartupReleaseNoteOptions {
5
+ readonly ownedSurfaces: boolean;
6
+ /** Deterministic catalog and version seams for composition tests. */
7
+ readonly releaseNotes?: ReleaseNoteCatalog;
8
+ readonly packageVersion?: string;
9
+ /** Without a profile there is nowhere to remember the note as seen, so it is never shown on startup. */
10
+ readonly profileId?: string;
11
+ readonly configDir: string;
12
+ }
13
+ export declare function readPackageVersion(): Promise<string>;
14
+ /**
15
+ * The release notes bare A1 ships and the one note this version shows once per profile on startup:
16
+ * the packaged catalog (or why it could not be read), the running version, and the claim that
17
+ * marks the note seen after its route closes.
18
+ */
19
+ export declare class StartupReleaseNote {
20
+ #private;
21
+ readonly catalog: ReleaseNoteCatalog | null;
22
+ readonly failure: unknown;
23
+ readonly packageVersion: string | null;
24
+ static load(options: StartupReleaseNoteOptions): Promise<StartupReleaseNote>;
25
+ private constructor();
26
+ /** The changelog route to present after the first frame, or undefined when there is no unseen note to show. */
27
+ get startupRoute(): StartupRoute | undefined;
28
+ /** Waits for a pending acknowledgement; a failed one stays unseen and is never reported. */
29
+ settleAcknowledgement(): Promise<void>;
30
+ releaseClaim(): Promise<void>;
31
+ }
32
+ export {};
@@ -0,0 +1,72 @@
1
+ import { readFile } from "node:fs/promises";
2
+ export async function readPackageVersion() {
3
+ const manifest = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
4
+ const version = manifest?.version;
5
+ if (typeof version !== "string")
6
+ throw new Error("A1 package version is unavailable");
7
+ return version;
8
+ }
9
+ /**
10
+ * The release notes bare A1 ships and the one note this version shows once per profile on startup:
11
+ * the packaged catalog (or why it could not be read), the running version, and the claim that
12
+ * marks the note seen after its route closes.
13
+ */
14
+ export class StartupReleaseNote {
15
+ catalog;
16
+ failure;
17
+ packageVersion;
18
+ #current;
19
+ #claim;
20
+ #acknowledgement = null;
21
+ static async load(options) {
22
+ let catalog = null;
23
+ let failure;
24
+ if (options.ownedSurfaces) {
25
+ try {
26
+ catalog = options.releaseNotes
27
+ ?? await import("../features/owned-ui/release-notes.js").then(module => module.readPackagedReleaseNotes());
28
+ }
29
+ catch (error) {
30
+ failure = error;
31
+ }
32
+ }
33
+ const packageVersion = options.ownedSurfaces ? options.packageVersion ?? await readPackageVersion() : null;
34
+ const current = packageVersion === null ? null : catalog?.current(packageVersion) ?? null;
35
+ const profileId = options.profileId;
36
+ const claim = current === null || profileId === undefined
37
+ ? null
38
+ : await import("../features/owned-ui/release-note-state.js").then(module => module.claimReleaseNote({
39
+ configDir: options.configDir, profileId, version: current.version,
40
+ }));
41
+ return new StartupReleaseNote(catalog, failure, packageVersion, current, claim);
42
+ }
43
+ constructor(catalog, failure, packageVersion, current, claim) {
44
+ this.catalog = catalog;
45
+ this.failure = failure;
46
+ this.packageVersion = packageVersion;
47
+ this.#current = current;
48
+ this.#claim = claim;
49
+ }
50
+ /** The changelog route to present after the first frame, or undefined when there is no unseen note to show. */
51
+ get startupRoute() {
52
+ const current = this.#current;
53
+ const claim = this.#claim;
54
+ if (current === null || claim === null)
55
+ return undefined;
56
+ return {
57
+ route: "changelog",
58
+ input: { document: current.markdown },
59
+ onClosed: () => {
60
+ this.#acknowledgement ??= claim.acknowledge();
61
+ return this.#acknowledgement;
62
+ },
63
+ };
64
+ }
65
+ /** Waits for a pending acknowledgement; a failed one stays unseen and is never reported. */
66
+ async settleAcknowledgement() {
67
+ await this.#acknowledgement?.catch(() => undefined);
68
+ }
69
+ async releaseClaim() {
70
+ await this.#claim?.release();
71
+ }
72
+ }
@@ -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.