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

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 (37) hide show
  1. package/bin/tab-launch-context.d.ts +1 -0
  2. package/bin/tab-launch-context.js +3 -3
  3. package/dist/app/session-shell/shell-host.js +6 -3
  4. package/dist/cli/tabs.d.ts +1 -1
  5. package/dist/cli/tabs.js +10 -14
  6. package/dist/foundation/release/release-gc.js +19 -4
  7. package/dist/foundation/resident-tabs/bridge-client.d.ts +1 -0
  8. package/dist/foundation/resident-tabs/bridge-client.js +6 -1
  9. package/dist/foundation/resident-tabs/maintenance.d.ts +2 -6
  10. package/dist/foundation/resident-tabs/maintenance.js +5 -23
  11. package/dist/foundation/resident-tabs/progress-observation-schema.d.ts +34 -0
  12. package/dist/foundation/resident-tabs/progress-observation-schema.js +1 -0
  13. package/dist/foundation/resident-tabs/progress-observation.d.ts +3 -6
  14. package/dist/foundation/resident-tabs/progress-observation.js +42 -12
  15. package/dist/foundation/resident-tabs/release-holds.js +3 -14
  16. package/dist/native/darwin-arm64/manifest.json +1 -1
  17. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  18. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  19. package/dist/native/darwin-x64/manifest.json +1 -1
  20. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  21. package/dist/native/darwin-x64/terminal-host.node +0 -0
  22. package/dist/native/linux-x64/manifest.json +1 -1
  23. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  24. package/dist/native/linux-x64/terminal-host.node +0 -0
  25. package/dist/native/win32-x64/manifest.json +2 -2
  26. package/dist/native/win32-x64/process-guardian.node +0 -0
  27. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  28. package/dist/native/win32-x64/terminal-host.node +0 -0
  29. package/dist/product-identity.d.ts +1 -1
  30. package/dist/product-identity.js +1 -1
  31. package/dist/product-identity.json +1 -0
  32. package/dist/ui/components/spans.js +2 -2
  33. package/dist/ui/components/text.d.ts +3 -6
  34. package/dist/ui/components/text.js +6 -8
  35. package/docs/architecture/resident-tabs.md +11 -11
  36. package/docs/validation.md +25 -5
  37. package/package.json +1 -1
@@ -2,6 +2,7 @@ export interface TabLaunchCredentials {
2
2
  readonly tabId: string;
3
3
  readonly token: string;
4
4
  readonly endpoint: string;
5
+ readonly releaseId?: string;
5
6
  }
6
7
 
7
8
  export function formatResidentFallbackNotice(reason: string): string;
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
 
3
- const TAB_ENVIRONMENT_NAMES = ["A1_TAB_ID", "A1_TAB_TOKEN", "A1_TAB_ENDPOINT"];
3
+ const TAB_ENVIRONMENT_NAMES = ["A1_TAB_ID", "A1_TAB_TOKEN", "A1_TAB_ENDPOINT", "A1_TAB_RELEASE_ID"];
4
4
 
5
5
  /** @param {string} reason */
6
6
  export function formatResidentFallbackNotice(reason) {
@@ -83,12 +83,12 @@ export function consumeTabLaunchContext(arguments_, environment) {
83
83
  const incarnationArgument = uiArguments.indexOf("--tab-incarnation");
84
84
  const tabIncarnation = incarnationArgument === -1 ? undefined : Number(uiArguments[incarnationArgument + 1]);
85
85
  if (incarnationArgument !== -1) uiArguments.splice(incarnationArgument, 2);
86
- const [tabId, token, endpoint] = TAB_ENVIRONMENT_NAMES.map(name => environment[name]);
86
+ const [tabId, token, endpoint, releaseId] = TAB_ENVIRONMENT_NAMES.map(name => environment[name]);
87
87
  for (const name of TAB_ENVIRONMENT_NAMES) delete environment[name];
88
88
  if (!tabMode) return { uiArguments, tabCredentials: undefined, tabStandby: false };
89
89
  if (!tabId || !token || !endpoint || !tabDataDir || tabDataDir.includes("\0")
90
90
  || typeof tabIncarnation !== "number" || !Number.isSafeInteger(tabIncarnation) || tabIncarnation < 1) {
91
91
  throw new Error("A1 tab mode requires bridge credentials, recovery directory, and incarnation");
92
92
  }
93
- return { uiArguments, tabCredentials: { tabId, token, endpoint }, tabDataDir, tabIncarnation, tabStandby };
93
+ return { uiArguments, tabCredentials: { tabId, token, endpoint, ...(releaseId === undefined ? {} : { releaseId }) }, tabDataDir, tabIncarnation, tabStandby };
94
94
  }
@@ -102,10 +102,13 @@ export class OwnedUiTerminalHost {
102
102
  },
103
103
  }),
104
104
  } : { layoutRoot: pinnedLayoutRoot(this.#delegatingLayoutParts()) }),
105
- ...(input?.onEvent === undefined ? {} : {
105
+ ...(input?.onEvent === undefined && progressObserver === null ? {} : {
106
106
  inputDiagnostics: {
107
- onEvent: input.onEvent,
108
- ...(input.now === undefined ? {} : { now: input.now }),
107
+ onEvent: event => {
108
+ input?.onEvent?.(event);
109
+ progressObserver?.inputPhase?.(event.phase, event.revision);
110
+ },
111
+ ...(input?.now === undefined ? {} : { now: input.now }),
109
112
  },
110
113
  }),
111
114
  ...(options.terminal === undefined ? {} : { terminal: options.terminal }),
@@ -2,7 +2,7 @@ import { runTerminalHostMaintenance, writeDoctorBundle } from "../foundation/res
2
2
  import type { TabsRequest } from "./tabs-request.js";
3
3
  export type { TabsRequest } from "./tabs-request.js";
4
4
  export interface TabsCommandOptions {
5
- /** The running package root; its terminal host is a candidate after the active release. */
5
+ /** The invoking package, never a different active or retained release. */
6
6
  readonly packageRoot?: string;
7
7
  readonly environment?: NodeJS.ProcessEnv;
8
8
  readonly stdout?: (message: string) => void;
package/dist/cli/tabs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { resolveProductPaths } from "../foundation/lifecycle/index.js";
2
- import { CohortStateStore } from "../foundation/release/index.js";
2
+ import { materializeRelease } from "../foundation/release/index.js";
3
3
  import { readResidentTabLaunchSettings, runTerminalHostMaintenance, writeDoctorBundle, } from "../foundation/resident-tabs/index.js";
4
4
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../product-identity.js";
5
5
  /** `a1 tabs`: inspect or stop resident tabs without attaching, and never start a host to report state. */
@@ -9,7 +9,7 @@ export async function runTabsCommand(request, options = {}) {
9
9
  const stderr = options.stderr ?? (message => process.stderr.write(message));
10
10
  const paths = resolveProductPaths(environment);
11
11
  const profileRoot = environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir;
12
- const releaseRoots = await installedReleaseRoots(paths.dataDir, options.packageRoot);
12
+ const releaseRoots = await invokingReleaseRoots(paths.dataDir, options.packageRoot, environment);
13
13
  const maintenance = options.maintenance ?? runTerminalHostMaintenance;
14
14
  const report = await maintenance({
15
15
  action: request.action === "doctor" ? "host-status" : request.action,
@@ -51,22 +51,18 @@ export async function runTabsCommand(request, options = {}) {
51
51
  }
52
52
  return report.exitCode === 0 ? 0 : 1;
53
53
  }
54
- async function installedReleaseRoots(dataDir, packageRoot) {
55
- const roots = [];
54
+ async function invokingReleaseRoots(dataDir, packageRoot, environment) {
55
+ if (!packageRoot)
56
+ return [];
57
+ if (environment[PRODUCT_IDENTITY.environment.developmentInstanceId])
58
+ return [packageRoot];
56
59
  try {
57
- const state = await new CohortStateStore(dataDir).read();
58
- for (const id of [state.references.active, state.references.rollback]) {
59
- if (id && state.releases[id])
60
- roots.push(state.releases[id].releaseRoot);
61
- }
62
- roots.push(...Object.values(state.releases).map(record => record.releaseRoot));
60
+ // Invariant: immutable materialization selects exact payload bytes without activating or starting a host.
61
+ return [(await materializeRelease(packageRoot, dataDir)).releaseRoot];
63
62
  }
64
63
  catch {
65
- // Rationale: unreadable release state still leaves the running package's own host.
64
+ return [];
66
65
  }
67
- if (packageRoot)
68
- roots.push(packageRoot);
69
- return [...new Set(roots)];
70
66
  }
71
67
  const NO_TABS = "No resident tabs are running.\n";
72
68
  function describe(request, report) {
@@ -316,14 +316,29 @@ async function residentReleaseHolds(dataDir, reader) {
316
316
  const everything = () => [...new Set(records.map(record => record.releaseId))].map(releaseId => ({ authority: "resident", releaseId }));
317
317
  if (!report.complete)
318
318
  return everything();
319
- const byRoot = new Map(records.map(record => [comparablePath(record.releaseRoot), record.releaseId]));
320
- const store = comparablePath(resolve(dataDir, "releases"));
319
+ if (report.releaseRoots.length === 0)
320
+ return [];
321
+ // Rationale: native Windows extended paths and Unix aliases must match the catalog's spelling.
322
+ const byRoot = new Map();
323
+ for (const record of records) {
324
+ const canonical = await realpath(record.releaseRoot).catch(() => record.releaseRoot);
325
+ byRoot.set(comparablePath(canonical), record.releaseId);
326
+ }
327
+ const directory = await realpath(resolve(dataDir, "releases")).catch(() => null);
328
+ if (directory === null)
329
+ return everything();
330
+ const store = comparablePath(directory);
321
331
  const holds = new Set();
322
332
  for (const root of report.releaseRoots) {
323
- const releaseId = byRoot.get(comparablePath(root));
333
+ // Security: an unresolved live root may alias managed content; uncertainty must retain it.
334
+ const canonical = await realpath(root).catch(() => null);
335
+ if (canonical === null)
336
+ return everything();
337
+ const key = comparablePath(canonical);
338
+ const releaseId = byRoot.get(key);
324
339
  if (releaseId)
325
340
  holds.add(releaseId);
326
- else if (comparablePath(root).startsWith(`${store}${sep}`))
341
+ else if (key.startsWith(`${store}${sep}`))
327
342
  return everything();
328
343
  }
329
344
  return [...holds].sort().map(releaseId => ({ authority: "resident", releaseId }));
@@ -5,6 +5,7 @@ export interface TabBridgeCredentials {
5
5
  readonly tabId: string;
6
6
  readonly token: string;
7
7
  readonly endpoint: string;
8
+ readonly releaseId?: string;
8
9
  }
9
10
  export interface TabAttachment {
10
11
  readonly clientId: string;
@@ -70,7 +70,7 @@ export class TabBridgeClient {
70
70
  this.#startWaiter = finish;
71
71
  transport.once("connect", () => {
72
72
  this.#send("hello", {
73
- role: "bridge", generation: 1, build: this.#options.build ?? "a1",
73
+ role: "bridge", generation: 1, build: this.#options.build ?? "a1", releaseId: this.#options.credentials.releaseId,
74
74
  features: ["bridge-status", "bridge-detach", "bridge-heartbeat", "bridge-restart", "bridge-visibility", "bridge-rename", "bridge-route", "bridge-stop", "bridge-shortcuts", "controller-boundary", "recovery-journal"],
75
75
  token: [...Buffer.from(this.#options.credentials.token, "base64")], tabId: this.#options.credentials.tabId,
76
76
  pid: this.#options.pid ?? process.pid, startIdentity: this.#options.startIdentity ?? `${process.pid}:node`,
@@ -208,6 +208,11 @@ export class TabBridgeClient {
208
208
  }
209
209
  #receive(message) {
210
210
  if (message.type === "welcome" && message.body["generation"] === 1) {
211
+ const id = this.#options.credentials.releaseId;
212
+ if (id !== undefined && message.body["releaseId"] !== id) {
213
+ this.#fail(false);
214
+ return;
215
+ }
211
216
  const advertised = message.body["features"];
212
217
  this.#features = new Set(Array.isArray(advertised) ? advertised.filter((feature) => typeof feature === "string") : []);
213
218
  this.#connected = true;
@@ -21,7 +21,7 @@ export interface MaintenanceOptions {
21
21
  readonly profileRoot: string;
22
22
  readonly dataDir: string;
23
23
  readonly runtimeDir: string;
24
- /** Installed release roots whose verified terminal host may run maintenance, in preference order. */
24
+ /** The first root is the invoking release; other roots never substitute for it. */
25
25
  readonly releaseRoots: readonly string[];
26
26
  readonly environment: NodeJS.ProcessEnv;
27
27
  readonly platform?: NodeJS.Platform;
@@ -33,11 +33,7 @@ export interface MaintenanceOptions {
33
33
  readonly stdout: string;
34
34
  }>;
35
35
  }
36
- /**
37
- * Runs one maintenance form through a verified terminal host. When the running host speaks
38
- * another protocol generation, it retries once with the addon from the release that host
39
- * recorded, after verifying that release's manifest; it never loads an unverified addon.
40
- */
36
+ /** Runs maintenance only through the invoking release; another cohort is never a retry target. */
41
37
  export declare function runTerminalHostMaintenance(options: MaintenanceOptions): Promise<MaintenanceReport>;
42
38
  /** The largest maintenance report accepted from the terminal host. */
43
39
  export declare const MAX_MAINTENANCE_REPORT_BYTES: number;
@@ -4,26 +4,14 @@ import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
4
  import { runBoundedHostCommand } from "./bounded-host-command.js";
5
5
  import { resolveTerminalHostEntryPath, resolveTerminalHostPath } from "./ensure-terminal-host.js";
6
6
  import { verifyTerminalHostArtifact } from "./terminal-host-artifact.js";
7
- const INCOMPATIBLE_EXIT = 4;
8
- /**
9
- * Runs one maintenance form through a verified terminal host. When the running host speaks
10
- * another protocol generation, it retries once with the addon from the release that host
11
- * recorded, after verifying that release's manifest; it never loads an unverified addon.
12
- */
7
+ /** Runs maintenance only through the invoking release; another cohort is never a retry target. */
13
8
  export async function runTerminalHostMaintenance(options) {
14
9
  const platform = options.platform ?? process.platform;
15
10
  const architecture = options.architecture ?? process.arch;
16
- const command = await firstVerifiedHost(options, options.releaseRoots, platform, architecture);
11
+ const command = await firstVerifiedHost(options, options.releaseRoots.slice(0, 1), platform, architecture);
17
12
  if (command === undefined)
18
- return unavailable(options.action, "no verified terminal host is installed");
19
- const report = await invoke(options, command);
20
- if (report.exitCode !== INCOMPATIBLE_EXIT || typeof report.releaseRoot !== "string")
21
- return report;
22
- const cohort = await firstVerifiedHost({ ...options, environment: {} }, [report.releaseRoot], platform, architecture);
23
- if (cohort === undefined || cohort.entry === command.entry) {
24
- return { ...report, message: "the running tabs use another version whose terminal host cannot be verified" };
25
- }
26
- return await invoke({ ...options, environment: installedEnvironment(options.environment) }, cohort);
13
+ return unavailable(options.action, "the invoking release has no verified terminal host");
14
+ return await invoke(options, command);
27
15
  }
28
16
  function arguments_(options) {
29
17
  const command = options.action === "stop" ? ["stop", options.tabId ?? ""] : [options.action];
@@ -67,17 +55,11 @@ async function firstVerifiedHost(options, roots, platform, architecture) {
67
55
  return { executable: process.execPath, entry: resolveTerminalHostEntryPath(root) };
68
56
  }
69
57
  catch {
70
- // Rationale: an older retained release may predate the packaged host; try the next one.
58
+ // Invariant: a missing or altered invoking artifact fails closed.
71
59
  }
72
60
  }
73
61
  return undefined;
74
62
  }
75
- function installedEnvironment(environment) {
76
- const sanitized = { ...environment };
77
- delete sanitized[PRODUCT_IDENTITY.environment.developmentInstanceId];
78
- delete sanitized[PRODUCT_IDENTITY.environment.terminalHostPath];
79
- return sanitized;
80
- }
81
63
  function unavailable(action, message) {
82
64
  return { schema: "a1-tabs-maintenance-v1", command: action, host: "unavailable", message, exitCode: 3 };
83
65
  }
@@ -0,0 +1,34 @@
1
+ /** Payload-free fixture schema; type-only imports keep it outside the evaluated startup graph. */
2
+ export type ResidentInputPhase = "receipt" | "semantic-start" | "semantic-end" | "composition-start" | "composition-end" | "write-start" | "write-end";
3
+ export interface ProgressFields {
4
+ readonly eventSequence?: number;
5
+ readonly revision?: number;
6
+ readonly geometryGeneration?: number;
7
+ readonly bytes?: number;
8
+ readonly count?: number;
9
+ readonly eventKind?: string;
10
+ readonly frameId?: number;
11
+ readonly renderCause?: string;
12
+ readonly fragmentCount?: number;
13
+ readonly transactionKind?: "synchronized-output" | "ordinary";
14
+ }
15
+ export interface ProgressRecord extends ProgressFields {
16
+ readonly schema: "a1-resident-progress-observation-v1";
17
+ readonly runId: string;
18
+ readonly role: "child";
19
+ readonly stage: "engine-delivery" | "child-render-write" | "child-input-phase";
20
+ readonly pid: number;
21
+ readonly processInstance: string;
22
+ readonly timestampUnixUs: number;
23
+ readonly clockErrorUs: number;
24
+ readonly sequence: number;
25
+ readonly tabId: string;
26
+ readonly incarnation: number;
27
+ readonly lostBefore: number;
28
+ }
29
+ export interface ResidentProgressObserverPort {
30
+ readonly tabId: string;
31
+ engineDelivery(event: unknown): void;
32
+ inputPhase?(phase: ResidentInputPhase, revision: number): void;
33
+ childRenderWrite(bytes: number, fragments: number, transaction: "synchronized-output" | "ordinary", frameId?: number, renderCause?: string): void;
34
+ }
@@ -1,7 +1,4 @@
1
- export interface ResidentProgressObserverPort {
2
- readonly tabId: string;
3
- engineDelivery(event: unknown): void;
4
- childRenderWrite(bytes: number, fragments: number, transaction: "synchronized-output" | "ordinary", frameId?: number, renderCause?: string): void;
5
- }
6
- /** Returns the opt-in payload-free observer for a resident child, or null on ordinary launches. */
1
+ import type { ResidentProgressObserverPort } from "./progress-observation-schema.js";
2
+ export type { ResidentProgressObserverPort } from "./progress-observation-schema.js";
3
+ /** Opt-in payload-free child observer; ordinary launches return null. */
7
4
  export declare function residentProgressObserver(environment?: NodeJS.ProcessEnv): ResidentProgressObserverPort | null;
@@ -17,7 +17,10 @@ class ResidentProgressObserver {
17
17
  #engineSequence = 0;
18
18
  #pendingBytes = 0;
19
19
  #lost = 0;
20
- #writing = Promise.resolve();
20
+ #writing = false;
21
+ #pending = [];
22
+ #scheduled = false;
23
+ #directoryReady = false;
21
24
  #failed = false;
22
25
  get tabId() { return this.#tabId; }
23
26
  constructor(directory, runId, tabId, incarnation) {
@@ -36,6 +39,9 @@ class ResidentProgressObserver {
36
39
  count: 1,
37
40
  });
38
41
  }
42
+ inputPhase(phase, revision) {
43
+ this.#observe("child-input-phase", { eventKind: phase, revision, count: 1 });
44
+ }
39
45
  childRenderWrite(bytes, fragments, transaction, frameId, renderCause) {
40
46
  this.#observe("child-render-write", {
41
47
  eventSequence: this.#engineSequence,
@@ -59,7 +65,7 @@ class ResidentProgressObserver {
59
65
  pid: process.pid,
60
66
  processInstance: this.#processInstance,
61
67
  timestampUnixUs: Math.round((performance.timeOrigin + performance.now()) * 1_000),
62
- // Rationale: timeOrigin is calibrated to the Unix epoch; retain a conservative cross-process error.
68
+ // Rationale: epoch-calibrated timeOrigin retains conservative cross-process clock error.
63
69
  clockErrorUs: 1_000,
64
70
  sequence: ++this.#sequence,
65
71
  tabId: this.#tabId,
@@ -75,18 +81,43 @@ class ResidentProgressObserver {
75
81
  }
76
82
  this.#lost = 0;
77
83
  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(() => {
84
+ this.#pending.push(line);
85
+ this.#schedule();
86
+ }
87
+ #schedule() {
88
+ if (this.#scheduled || this.#writing || this.#failed)
89
+ return;
90
+ this.#scheduled = true;
91
+ // Performance: check-phase batches yield to input and amortize file opens and promises.
92
+ setImmediate(() => { this.#scheduled = false; void this.#flush(); });
93
+ }
94
+ async #flush() {
95
+ if (this.#failed || this.#pending.length === 0)
96
+ return;
97
+ this.#writing = true;
98
+ const batch = this.#pending.join("");
99
+ this.#pending = [];
100
+ try {
101
+ if (!this.#directoryReady) {
102
+ await mkdir(this.#directory, { recursive: true });
103
+ this.#directoryReady = true;
104
+ }
105
+ await appendFile(this.#path, batch, "utf8");
106
+ }
107
+ catch {
82
108
  this.#failed = true;
83
- }).finally(() => {
84
- this.#pendingBytes -= length;
85
- });
109
+ this.#pending = [];
110
+ }
111
+ finally {
112
+ this.#pendingBytes -= Buffer.byteLength(batch);
113
+ this.#writing = false;
114
+ if (this.#pending.length > 0)
115
+ this.#schedule();
116
+ }
86
117
  }
87
118
  }
88
119
  let observer;
89
- /** Returns the opt-in payload-free observer for a resident child, or null on ordinary launches. */
120
+ /** Opt-in payload-free child observer; ordinary launches return null. */
90
121
  export function residentProgressObserver(environment = process.env) {
91
122
  if (environment === process.env && observer !== undefined)
92
123
  return observer;
@@ -99,8 +130,7 @@ export function residentProgressObserver(environment = process.env) {
99
130
  : null;
100
131
  if (environment === process.env) {
101
132
  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.
133
+ // Security: keep evidence paths and identity private; erase them before extensions/tools inherit env.
104
134
  for (const name of [
105
135
  OBSERVATION_DIRECTORY_ENV,
106
136
  OBSERVATION_RUN_ENV,
@@ -1,4 +1,4 @@
1
- import { readdir, stat } from "node:fs/promises";
1
+ import { readdir } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
4
4
  import { runBoundedHostCommand } from "./bounded-host-command.js";
@@ -51,19 +51,8 @@ async function hasResidentRecords(dataDir) {
51
51
  catch (error) {
52
52
  return !(error instanceof Error && "code" in error && error.code === "ENOENT");
53
53
  }
54
- for (const profile of profiles) {
55
- for (const record of ["server.json", "registry.json"]) {
56
- try {
57
- await stat(resolve(dataDir, "tabs", profile, record));
58
- return true;
59
- }
60
- catch (error) {
61
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
62
- return true;
63
- }
64
- }
65
- }
66
- return false;
54
+ // Invariant: native inspection, not a following stat, judges unknown or linked namespace entries.
55
+ return profiles.length > 0;
67
56
  }
68
57
  function incomplete(reason) {
69
58
  return { complete: false, releaseRoots: [], uncertain: [reason] };
@@ -7,7 +7,7 @@
7
7
  "platform": "darwin",
8
8
  "architecture": "arm64",
9
9
  "capability": "supported",
10
- "builtAt": "2026-10-24T19:42:42.626Z",
10
+ "builtAt": "2026-10-26T12:38:01.432Z",
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-24T19:46:17.340Z",
12
+ "builtAt": "2026-10-26T12:40:53.110Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "e4de01b7bec86cbced1c52a6d81be5978a0de558a92bbfed9537c5dfc1af4605",
16
- "size": 5925216
15
+ "sha256": "c42ed0b065515794627cd0064a5da4de4c84396460315615356aab340e73eef0",
16
+ "size": 5962320
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-24T19:42:48.347Z",
10
+ "builtAt": "2026-10-26T12:38:07.232Z",
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-24T19:48:24.843Z",
12
+ "builtAt": "2026-10-26T12:43:26.281Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "a0eb75eb03c62a5bab59db0fd60e43a79b91767dfb0661af0b00a6a8c23cb3c4",
16
- "size": 6476744
15
+ "sha256": "65f9a9556babcf6f361f643699142a854e564ef0c487d6709a48615a98dc8d75",
16
+ "size": 6497320
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-24T19:42:38.190Z",
10
+ "builtAt": "2026-10-26T12:37:52.900Z",
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-24T19:46:11.090Z",
12
+ "builtAt": "2026-10-26T12:40:25.329Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "fd28a219164f83256eba84c61ba55b662a01a9af77292ff8f5106970a5cd0dc4",
16
- "size": 7210032
15
+ "sha256": "234f73020b8f8cb31fd9a172f13c437255bb575a63466a3b369065e8866acead",
16
+ "size": 7236184
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-24T19:43:33.634Z",
10
+ "builtAt": "2026-10-26T12:38:48.590Z",
11
11
  "artifact": {
12
12
  "filename": "process-guardian.node",
13
- "sha256": "eec176bb7bc151266c5097b6f78e3ea95d11780aa13d045c7940f0d397e3d8e6",
13
+ "sha256": "4136caabb0a807ff5b7c10e4b2cb1dd5e326c72bd3d7c61429e970879ac46f77",
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-24T19:46:43.778Z",
12
+ "builtAt": "2026-10-26T12:40:52.007Z",
13
13
  "artifact": {
14
14
  "filename": "terminal-host.node",
15
- "sha256": "7eb4d836b8d9a4c13593921f329065bd3b706c0593d8d4e992b56ee461006ac5",
16
- "size": 6288896
15
+ "sha256": "a48fa7c8bdb979c8ca2834b280b590174b1574b16fc263aa5ad12ba1361a44a5",
16
+ "size": 6316032
17
17
  },
18
18
  "entry": {
19
19
  "filename": "dist/foundation/resident-tabs/host-entry.js",
@@ -1,4 +1,4 @@
1
- declare const ENVIRONMENT_KEYS: readonly ["clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentHistoryDataDir", "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"];
1
+ declare const ENVIRONMENT_KEYS: readonly ["clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentHistoryDataDir", "developmentInstanceId", "developmentRoot", "endpoint", "exitNoticePath", "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "residentFixtureControlDir", "residentProgressObservationDir", "residentProgressRunId", "residentProgressTabId", "residentProgressTabIncarnation", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "tabEndpoint", "tabId", "tabReleaseId", "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"];
@@ -1,7 +1,7 @@
1
1
  import rawIdentity from "./product-identity.json" with { type: "json" };
2
2
  const ENVIRONMENT_KEYS = [
3
3
  "clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentHistoryDataDir", "developmentInstanceId", "developmentRoot", "endpoint", "exitNoticePath",
4
- "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "residentFixtureControlDir", "residentProgressObservationDir", "residentProgressRunId", "residentProgressTabId", "residentProgressTabIncarnation", "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", "tabReleaseId", "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"];
@@ -36,6 +36,7 @@
36
36
  "suggestionDiagnostics": "A1_SUGGESTION_DIAGNOSTICS",
37
37
  "tabEndpoint": "A1_TAB_ENDPOINT",
38
38
  "tabId": "A1_TAB_ID",
39
+ "tabReleaseId": "A1_TAB_RELEASE_ID",
39
40
  "tabToken": "A1_TAB_TOKEN",
40
41
  "tabsEndpoint": "A1_TABS_ENDPOINT",
41
42
  "terminalHostPath": "A1_TERMINAL_HOST_PATH",
@@ -1,4 +1,4 @@
1
- import { displayWidth } from "./text.js";
1
+ import { displayWidth, graphemeWidth } from "./text.js";
2
2
  /**
3
3
  * Ported from the A1 UI reference implementation (`core/presentation/spans.ts`).
4
4
  * Writing over part of a rendered row is not string slicing: the row carries
@@ -53,7 +53,7 @@ export function overlaySpan(line, from, to, span, options = {}) {
53
53
  continue;
54
54
  }
55
55
  for (const { segment } of GRAPHEMES.segment(token)) {
56
- const width = displayWidth(segment);
56
+ const width = graphemeWidth(segment);
57
57
  if (column + width <= from)
58
58
  head += segment;
59
59
  else if (column >= to)
@@ -1,11 +1,8 @@
1
- /**
2
- * Display-width text handling. Terminals lay out by column, not by code unit,
3
- * so measuring with `String.length` mis-sizes CJK, emoji, and combining marks,
4
- * and counting styling escapes as visible width breaks every layout that uses
5
- * colour.
6
- */
1
+ /** Terminal-column geometry: preserve graphemes, Unicode cell widths and ANSI styling. */
7
2
  /** Removes styling sequences so only visible characters remain. */
8
3
  export declare function stripAnsi(text: string): string;
4
+ /** Width of one grapheme cluster: two for emoji, else the widest code point it contains. */
5
+ export declare function graphemeWidth(grapheme: string): number;
9
6
  export interface DisplayColumnRange {
10
7
  readonly from: number;
11
8
  readonly to: number;
@@ -1,9 +1,4 @@
1
- /**
2
- * Display-width text handling. Terminals lay out by column, not by code unit,
3
- * so measuring with `String.length` mis-sizes CJK, emoji, and combining marks,
4
- * and counting styling escapes as visible width breaks every layout that uses
5
- * colour.
6
- */
1
+ /** Terminal-column geometry: preserve graphemes, Unicode cell widths and ANSI styling. */
7
2
  const ANSI_PATTERN = /\[[0-9;:?]*[ -/]*[@-~]|[\]_][^]*(?:|\\)|[@-Z\\-_]/g;
8
3
  const SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
9
4
  const WORD_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "word" });
@@ -76,7 +71,7 @@ function couldBeEmoji(grapheme) {
76
71
  grapheme.length > 1);
77
72
  }
78
73
  /** Width of one grapheme cluster: two for emoji, else the widest code point it contains. */
79
- function graphemeWidth(grapheme) {
74
+ export function graphemeWidth(grapheme) {
80
75
  if (couldBeEmoji(grapheme) && RGI_EMOJI_PATTERN.test(grapheme))
81
76
  return 2;
82
77
  let width = 0;
@@ -126,8 +121,11 @@ export function displayColumnSlice(text, requestedFrom, requestedTo) {
126
121
  }
127
122
  /** Columns this text occupies once styling is removed. */
128
123
  export function displayWidth(text) {
124
+ const plain = stripAnsi(text);
125
+ if (!/[^\x20-\x7e]/u.test(plain))
126
+ return plain.length;
129
127
  let width = 0;
130
- for (const { segment } of SEGMENTER.segment(stripAnsi(text)))
128
+ for (const { segment } of SEGMENTER.segment(plain))
131
129
  width += graphemeWidth(segment);
132
130
  return width;
133
131
  }
@@ -175,13 +175,13 @@ Transport correctness does not rely on `timeBeginPeriod`. The existing per-role
175
175
 
176
176
  The existing opt-in progress route (`A1_RESIDENT_PROGRESS_OBSERVATION_DIR` plus `A1_RESIDENT_PROGRESS_RUN_ID`) adds `pipe-frame-read`, `pipe-write-complete`, `outer-write-complete`, `wake-gap`, and `wake-repaint-requested` stages. Connection-level records may omit tab identity. `durationUs` is local monotonic completion/assembly time; `idleWaitUs` separately describes waiting for the next frame and is not delivery latency. Native records include `monotonicUs`, `clockEpoch`, and optional `clockDiscontinuity`. Consumers exclude negative, epoch-crossing, and clock-discontinuity-crossing derived samples, while retaining valid local completion durations. The queue remains bounded and asynchronous, with loss accounting and no terminal content or credentials. Outer-write completion is not proof of physical pixels.
177
177
 
178
- For recurring post-wake stalls, compare an existing resident client, a new client on the same resident host, and direct `a1 solo` in the same outer terminal. Use matched holder/attach revisions and local I/O durations to distinguish child progress, transport delay, and outer writes. If solo and other terminal applications also stall, do not assign the failure to resident pipes without further evidence. Simulated-gap fixtures do not certify physical sleep/wake recovery; do not automate workstation suspend. Updates retain old resident cohorts, so new endpoint code takes effect only as those roles reach their ordinary lifecycle or the maintainer explicitly chooses to stop them. Installing the update never recycles running tabs.
178
+ For recurring post-wake stalls, compare an existing resident client, a new client on the same resident host, and direct `a1 solo` in the same outer terminal. Use matched holder/attach revisions and local I/O durations to distinguish child progress, transport delay, and outer writes. If solo and other terminal applications also stall, do not assign the failure to resident pipes without further evidence. Simulated-gap fixtures do not certify physical sleep/wake recovery; do not automate workstation suspend. Updates retain old resident cohorts, but a new launch selects its own immutable release namespace rather than reusing their host. Existing roles keep their original code and lifecycle; installing an update never recycles running tabs.
179
179
 
180
180
  ### 3. Host scope, endpoints, writer authority, and credentials
181
181
 
182
- - There is **one server per OS user × canonical A1 profile root**, independent of the release cohort. Tabs must outlive release changes, and the supervisor is forbidden from retaining runtime processes.
183
- - Windows endpoint: `\\.\pipe\a1-tabs-<sha256(canonicalProfileHome)[0:20]>`.
184
- - macOS and Linux endpoint: a Unix domain socket `a1-tabs-<same profile hash>.sock` inside an owner-only (`0700`) per-user runtime directory (`$XDG_RUNTIME_DIR` when set, otherwise a private directory under the profile's runtime dir), kept short enough for the platform's socket path limit.
182
+ - There is **one server per OS user, canonical A1 profile root and immutable release identity**. Different releases coexist without migration; the supervisor remains forbidden from retaining runtime processes. A domain-separated hash of canonical profile and fixed originating release/addon identity selects the flat `<data>/tabs/<scope>` directory, credential files, registry, epochs and `server-start-<scope>.lock`.
183
+ - Windows endpoint: `\\.\pipe\a1-tabs-<scope[0:20]>`.
184
+ - macOS and Linux endpoint: a Unix domain socket `a1-tabs-<same scope>.sock` inside an owner-only (`0700`) per-user runtime directory (`$XDG_RUNTIME_DIR` when set, otherwise a private directory under the profile's runtime dir), kept short enough for the platform's socket path limit.
185
185
  - Generation 1 admits holders over the authenticated owner endpoint. Dedicated `…-h-<tabId>` endpoints remain an optional later optimization.
186
186
  - Hermetic override: `A1_TABS_ENDPOINT`, together with the `A1_*_DIR` overrides.
187
187
  - **The endpoint is discovery; the writer lease is authority.** A starting server probes with a real handshake. If a live server answers, it exits as `already-running` and joins the winner. A failed probe does not authorize blind endpoint reclaim: the starter reads the owner marker, verifies pid and native start identity, requests termination, and waits for the owner-only OS-exclusive registry-writer lease. If the owner cannot be verified or the lease cannot be acquired, startup fails closed instead of creating split brain.
@@ -189,7 +189,7 @@ For recurring post-wake stalls, compare an existing resident client, a new clien
189
189
  - **Access and authentication:**
190
190
  - The endpoint, token files, secret file, registry, and writer lease are owner-only: Windows ACLs on the named pipe and files, `0700` directories and `0600` files on macOS and Linux, plus a peer-credential check (`SO_PEERCRED` / `getpeereid`) on every Unix socket connection.
191
191
  - Clients present a random 32-byte client token from an owner-only file.
192
- - One random owner-only profile secret persists outside the registry. Per-tab/per-incarnation holder and bridge credentials are derived from that secret and the non-secret tab/incarnation identity, so a replacement server can authenticate surviving processes without storing tab credentials.
192
+ - One random owner-only profile/release secret persists outside its registry. Per-tab/per-incarnation holder and bridge credentials are derived from that secret and the non-secret tab/incarnation identity, so a replacement server can authenticate surviving processes without storing tab credentials.
193
193
  - Holders and bridges also present pid and native start identity. Nothing unverifiable is adopted, signalled, terminated, or allowed to mutate state.
194
194
 
195
195
  ### 4. Detached start
@@ -239,7 +239,7 @@ The channels carry bounded generation-1 frames: a u32 little-endian length follo
239
239
 
240
240
  ### 6. Rendering, input, and outer-terminal fidelity
241
241
 
242
- - **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. On Windows, ConPTY re-renders the child's cells on its own schedule and relays the DEC 2026 markers when it parses them, so a render pass can land between a frame's markers and the rest of its cells can arrive after the end marker; the holder therefore treats a relayed end marker as "frame parsed", keeps the state private until the following render pass has arrived and settled for 2 ms, and publishes anyway after a 40 ms bound. A begin marker that arrives while that pass is still pending does not publish the torn state. Unix PTYs relay bytes verbatim, so their markers still delimit frames exactly. 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.
242
+ - **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. On Windows, ConPTY re-renders the child's cells on its own schedule and relays the DEC 2026 markers when it parses them, so a render pass can land between a frame's markers and the rest of its cells can arrive after the end marker; the holder therefore treats a relayed end marker as "frame parsed", keeps the state private until the following render pass has arrived and settled for 2 ms, with a non-renewable 16 ms fallback for the oldest unpublished pass. New end markers and continuous settling fragments cannot postpone that bound; consuming a complete or recovery publication clears it for the next update. The fallback never authorizes ordinary publication inside an open transaction. A begin marker that arrives while that pass is still pending does not publish the torn state. Unix PTYs relay bytes verbatim, so their markers still delimit frames exactly. 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. Decoded child grids move into shared immutable attach/outer-offer/completed-baseline custody, so chrome-only work neither deep-clones nor revalidates an unchanged body. Overlay restoration and new grids still take the damage/validation path, and resize fading uses copy-on-write. Each attach round admits one queued input before its bounded server-render round; neither input nor rendering can starve the other. Attach and holder wait on coalesced wake notifications only after a round receives no work, so an already-ready backlog never waits for another producer pulse; all per-lane round limits and quiescent idle waits remain intact.
243
243
  - **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.
244
244
  - 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.
245
245
  - Mouse rows are offset by the strip. Row 0 belongs to the client.
@@ -260,7 +260,7 @@ Direct `a1 solo` is the behavioural oracle for every viewed resident child. Only
260
260
  - **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.
261
261
  - **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.
262
262
  - **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.
263
- - **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. The outer-paint control waits for a payload-free `fixture-stop-ack` from the sole writer before starting its scenario; that acknowledgement follows all earlier writes, and same-writer sequence ordering excludes only pre-acknowledgement paints from the zero-progress check. Any later paint still fails. 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.
263
+ - **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. The child observer batches check-phase appends with one directory initialization while retaining its pending-byte and loss limits. Auxiliary PTY-input completion and child input/semantic/composition/write phases narrow input-side delays without recording key or terminal content. Healthy viewed windows require at least forty matched live commit-to-first-paint samples per tab and enforce p95 at most 33 ms; activation/stale re-offers, later chrome paints of the same revision, and deliberately stopped or paused-sink windows cannot masquerade as live delivery samples. An additional real-A1 fixture compares forty synthetic typing, Models navigation, and transcript wheel responses per path at equal child geometry. Timing requires the shipping Rust optimization profile with all fixture controls retained; debug unit/role/probe coverage and feature-free shipping-artifact checks remain separate. Both paths retain matching complete 60-marker transcript baselines before sampling. Untimed provider setup acknowledges a complete literal editor echo before sending a separate Enter; the measured inputs keep their ordinary 30 ms cadence. Its incremental native Ghostty oracle matches completed semantic states, permits coalesced intermediate states, and rejects missing responses, one-second stalls, or resident p95 beyond direct solo plus the existing 16+33 ms input/presentation envelope. Direct target hashes come from the final semantic state in each ordinary inter-input interval, while timing uses its first observed occurrence, not a later settling delay or an early torn ConPTY hash. A fixed fixture role entry pins the exact addon for all detached roles, including Windows WMI launches that do not inherit the invoking shell's development override; production launch behavior is unchanged. 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. The outer-paint control waits for a payload-free `fixture-stop-ack` from the sole writer before starting its scenario; that acknowledgement follows all earlier writes, and same-writer sequence ordering excludes only pre-acknowledgement paints from the zero-progress check. Any later paint still fails. 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.
264
264
 
265
265
  ### 7. Status and attention come from the tab bridge
266
266
 
@@ -326,10 +326,10 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
326
326
  ### 10. Updates and version skew
327
327
 
328
328
  - **Artifacts and process images.** The server and holders are Node runtimes that load the addon and fixed entry from immutable release directories, never from the mutable npm prefix. Activation therefore never overwrites a loaded addon or entry, and release retention keeps every release used by a verified resident process.
329
- - **Same generation, newer client.** The client attaches normally, and features the server lacks are disabled per method.
330
- - **No automatic handoff in the first version.** A compatible resident cohort remains on its immutable release until all of its tabs stop. A newer same-generation client may attach with unsupported optional operations disabled.
331
- - **Retention.** Release collection keeps the server's, every holder's, and every tab child's release while native identities verify. Package activation does not terminate or overwrite them.
332
- - **Incompatibility.** A generation mismatch never kills resident processes. The client reports that the resident host must be stopped explicitly before the preview can restart on the newer generation. Automatic server handoff and idle tab recycling require a follow-up change.
329
+ - **Exact release admission.** Clients, holders and bridges present a release identity before capability negotiation. Matching protocol generation or display version does not permit cross-release attachment. Native clients also check the welcome identity; bridge identity is consumed from holder-private launch context before extensions load.
330
+ - **Independent activation.** A newer release starts or reuses its own host. Existing resident and solo processes remain untouched. Server recovery and child launches retain the originating fixed Node, entry and addon; the active-release pointer never retargets them. No automatic migration, handoff or idle recycling is performed.
331
+ - **Retention.** Read-only bounded collection scans every flat release namespace and legacy profile namespace. It keeps server, holder and child releases while identities verify and retains all candidates on unreadable, linked, oversized or unverifiable state. Package activation never overwrites loaded artifacts.
332
+ - **Maintenance and legacy state.** Maintenance resolves the invoking package's immutable materialization without activation or host startup, uses only that release's verified entry, and never retries another recorded cohort. List, stop, status and doctor remain release-local, including unresponsive-host termination. Legacy unversioned state is neither migrated nor adopted; it is read only for conservative retention. Shared configuration and session file locks remain profile-neutral.
333
333
 
334
334
  ### 11. Foreground UX
335
335
 
@@ -6,11 +6,13 @@ Development validation computes one exact base/head selection with:
6
6
  npm run select:validation-impact -- --base <full-base-sha> --head <full-head-sha> --output .artifacts/validation/impact.json
7
7
  ```
8
8
 
9
- The versioned `config/validation-ownership.json` registry maps stable product and test path groups to a mandatory PR core, affected unit tests, resource-sensitive tests, and integration owners. The initial coarse owners are UI/rendering, launch/startup, release/package/update, Pi, native containment, image/history, governance, and shared product inputs. Each integration owner in `config/integration-owners.json` declares `pull-request` or `exhaustive` cadence. A changed pull-request test selects its owner; a changed exhaustive test selects focused deterministic contracts and records the exhaustive owner as cadence-deferred. Shared support selects every declared PR consumer and records affected exhaustive consumers; copies, renames, and deletions inspect both identities. Reasons and changed paths are recorded in `impact.json`.
9
+ The versioned `config/validation-ownership.json` registry maps stable product and test path groups to a mandatory PR core, an explicit critical floor per owner, changed and bounded transitive consumers, resource-sensitive tests, and integration owners. The coarse owners are UI/rendering, launch/startup, release/package/update, Pi, native containment, image/history, governance, and shared product inputs. Each integration owner in `config/integration-owners.json` declares `pull-request`, `direct-impact`, or `exhaustive` cadence. A changed pull-request test selects its owner; a changed exhaustive test selects focused deterministic contracts and records the exhaustive owner as cadence-deferred. Shared support selects every declared PR consumer and records affected exhaustive consumers; copies, renames, and deletions inspect both identities. Reasons and changed paths are recorded in `impact.json`.
10
10
 
11
- Unknown operational inputs, unavailable comparison history, workflow/selector/suite/aggregate changes, and manual Development dispatch select every pull-request owner. Missing, malformed, or unknown cadence blocks instead of guessing. Documentation-only and version-only changes retain explicit exemptions. An implementation-bound association (an `openspec-implementation` link in the PR body) only disables those two exemptions: the PR core still runs, and owners are still selected by impact rather than forced to conservative coverage. Selection does not require whole-repository source parsing.
11
+ Reviewed `refinements` can narrow a coarse owner's tests and integration links for known subareas: scrollbar/settings, clipboard/editor/repository commands, documentation/cleanup/delivery governance, and development-environment tooling. Each rule keeps an explicit minimum test set; a bounded relative-import graph across `test/`, `src/`, `scripts/`, and `bin/` adds at most 24 sorted transitive consumers per changed path, including consumers outside the coarse owner, and integration owners of those retained consumers. The graph is supplemental, not sole coverage authority. Unreviewed or absent source identities and an unavailable graph retain the coarse owner. Changed tests, shared support, and mixed source/support changes retain their coarse contracts; rename and deletion classification still includes both identities. Some UI changes deliberately select more tests than before because the graph exposes formerly missed shell consumers.
12
12
 
13
- The `changes` job then derives the modular job matrix from that same selection through `scripts/release/validation-matrix.mjs`, which declares every Development modular job and returns only the entries the selection activates; GitHub never schedules an inactive entry, so an ordinary PR does not spend runner minutes on jobs that would resolve to nothing. Each scheduled job still resolves its own owners from the uploaded impact artifact, and the aggregate still requires evidence for every selected owner, so an entry that should have run and did not fails the run.
13
+ Unknown operational inputs, unavailable comparison history, workflow/selector/suite/aggregate changes, and manual Development dispatch select every coarse owner's critical floor and only integration owners with `pull-request` cadence. Direct-impact owners require a changed declared test, support, or production surface; exhaustive owners remain deferred. Missing, malformed, or unknown cadence blocks instead of guessing. Documentation-only and version-only changes retain explicit exemptions. An implementation-bound association (an `openspec-implementation` link in the PR body) only disables those two exemptions: the PR core still runs, and owners are still selected by impact rather than forced to conservative coverage. Selection does not require whole-repository source parsing.
14
+
15
+ The `changes` job then derives the modular job matrix from that same selection through `scripts/release/validation-matrix.mjs`, which declares every Development modular job and returns only the entries the selection activates; GitHub never schedules an inactive entry, so an ordinary PR does not spend runner minutes on jobs that would resolve to nothing. The matrix uses fail-fast cancellation, so a decisive failure stops remaining cells while missing or cancelled selected evidence still blocks the aggregate. Each scheduled job still resolves its own owners from the uploaded impact artifact, and the aggregate still requires evidence for every selected owner, so an entry that should have run and did not fails the run.
14
16
 
15
17
  ## Commands and coverage levels
16
18
 
@@ -42,7 +44,7 @@ Preflight selects the committed merge-base diff with `origin/develop` exactly as
42
44
 
43
45
  `test:pr-core` is not a replacement for `test:fast`: CI combines it with directly changed and coarse-owner tests from the exact impact selection. Selected resource-sensitive tests run independently on an isolated Windows runner. Selected package, startup, rendering, Pi, compatibility, and platform owners also run independently after their actual prerequisites.
44
46
 
45
- Conservative Development retains every pull-request owner but does not schedule exhaustive owners. The complete regression implementation runs inside a PR only when base policy verifies an `openspec-ci` App-created repair, its generated candidate identity, and a failed Full regression source in `regression-provenance.json`. Publishing/prerequisite/validation-authority impact, shared inputs, unknown paths, labels, Publish-only repairs, and successful-run persistent-overrun candidates retain bounded ordinary validation. Planning-only generated repair drafts stay exempt; eligible implementation drafts get feedback without integration authority. The selected exact-head evidence must include Windows Node 22/24, Linux Node 24, and macOS Node 24. Full regression and nightly/stable release retain both cadence classes and the same coverage through the shared implementation. The real three-release `update-predecessor` exact-package oracle is exhaustive; deterministic predecessor command, fixture, materialization, warmup, package, and update contracts remain PR-eligible. Preview and release continue to consume exact candidate bytes under their channel-specific contracts. A standalone manual Full regression remains useful for diagnosis or an unselected candidate, but never replaces automatically selected current-PR evidence. The generated ownership ledger command is:
47
+ Conservative Development retains every critical owner floor and the small set of `pull-request` integration owners, while scheduling only directly affected `direct-impact` owners and deferring all exhaustive owners. The complete regression implementation runs inside a PR only when base policy verifies an `openspec-ci` App-created repair, its generated candidate identity, and a failed Full regression source in `regression-provenance.json`. Publishing/prerequisite/validation-authority impact, shared inputs, unknown paths, labels, Publish-only repairs, and successful-run persistent-overrun candidates retain bounded ordinary validation. Planning-only generated repair drafts stay exempt; eligible implementation drafts get feedback without integration authority. The selected exact-head evidence must include Windows Node 22/24, Linux Node 24, and macOS Node 24. Full regression and nightly/stable release retain both cadence classes and the same coverage through the shared implementation. The real three-release `update-predecessor` exact-package oracle is exhaustive; deterministic predecessor command, fixture, materialization, warmup, package, and update contracts remain PR-eligible. Preview and release continue to consume exact candidate bytes under their channel-specific contracts. A standalone manual Full regression remains useful for diagnosis or an unselected candidate, but never replaces automatically selected current-PR evidence. The generated ownership ledger command is:
46
48
 
47
49
  ```bash
48
50
  node scripts/release/generate-validation-ownership-ledger.mjs --output .artifacts/validation/ownership-ledger.json
@@ -54,6 +56,24 @@ The `terminal-host` owner is CI-only. A change under `native/terminal-host/` or
54
56
 
55
57
  The protected aggregate requires every partition of every selected platform, keyed by `partition` in the outcome authority and job envelope. Because the role suite no longer warms the resident launch paths in the build-and-probe job, its certification step runs one unmeasured `measure-resident-tabs.mjs` pass before the measured pass whose budgets are unchanged. Fidelity, latency, chaos, and certification start roles as `process.execPath dist/foundation/resident-tabs/host-entry.js` with `A1_TERMINAL_HOST_PATH` naming the addon. Windows cleanup identifies those Node-hosted residents by command line; it never kills every `node.exe` by image name. Locally, `npm run test:terminal-host` without a partition is unchanged and runs everything in one process. Local Windows builds fail in `build.rs` unless `TERMINAL_HOST_LOCAL_BUILD=1` is set, because Zig's package fetch unpacks test data that workstation antivirus quarantines. Native lanes also publish the terminal library they linked; a local build can link it instead of running Zig (see Local native iteration in [`architecture/resident-tabs.md`](architecture/resident-tabs.md)), which is development evidence only and never replaces selected CI. `TERMINAL_HOST_CONTRACTS_ONLY=1` is a library-only diagnostic and cannot substitute for selected CI. See [`native/terminal-host/README.md`](../native/terminal-host/README.md).
56
58
 
59
+ ## Coverage-preserving efficiency
60
+
61
+ Complete regression selects the full rendering scope instead of also executing its superseded smoke files. Full captures call the same smoke assertions, including independent producer identity, readiness, semantic parity, synchronization, and bounded paint checks. Smoke-only corrupted-evidence controls live in an ordinary deterministic test. Scheduled content runs once in each gate through the same owned-versus-pinned helper. The retained full matrix has 11 streaming workloads across three producers and two modes, two scheduled-content workloads across two producers, and six input workloads batched across three producers: 73 matrix launches plus 10 protocol-failure launches. Standalone smoke retains 19 launches. Full regression avoids the additional 19 smoke launches; it does not remove a workload or platform lane.
62
+
63
+ The coverage ledger test reconstructs every full-plan invocation and requires each retained file exactly once, except the two smoke files whose executed replacement contracts it names explicitly. Unit, shell integration, source/emitted entries, independent pinned parity, security/race controls, and runtime-specific tests are not interchangeable merely because they touch the same module.
64
+
65
+ The package-command transcript fixture uses four file-owned producer processes (owned/pinned times color 0/1), rather than starting 22 equivalent loaders. Every request still runs the real command with its independent home/settings and fresh transcript; failed producers are never restarted. Fresh-home isolation and process disposal are tested. Cold-entry contracts elsewhere retain their cold process boundary. Git cleanup scenarios still initialize their own real repository/worktree, but write private Git configuration once instead of launching four config/remote commands. Release fixtures create baseline refs in one real atomic ref transaction. Setup remains inside the owning scenario and no mutable fixture repository is shared.
66
+
67
+ Exact-package identity walks hash each payload as it is read, retain only file digests, and parse the manifest from that same walk. They keep the original digest contract, all pre/post consumer verifications, fresh prefixes, and separate shard installations. This removes duplicate manifest reads and full-payload buffering; it does not cache verification results or promise a material improvement to npm installation time.
68
+
69
+ Generate a content-free per-file inventory and optional observed timing report from an existing Actions run and log:
70
+
71
+ ```bash
72
+ node scripts/release/validation-cost-report.mjs --run .artifacts/agent/completed-run.json --log .artifacts/agent/job.log --output .artifacts/validation/cost-report.json
73
+ ```
74
+
75
+ The run JSON is the output of `gh run view RUN --json jobs,headSha,createdAt,updatedAt`; the log is from that same run's chosen job. Omit both inputs for inventory only. Reports are observational, never validation receipts. They identify source drift, each retained file's coarse owner/execution class, job and step duration, total runner time, longest job, workflow elapsed time, and runner span. Queue and DAG critical-path timing remain unavailable rather than guessed. File timings preserve repeated observations; never add concurrent job/file durations and call the result elapsed savings. Compare comparable completed runs before claiming a hosted speedup. No passing outcomes, installed trees, or mutable fixture state may be cached by these optimizations.
76
+
57
77
  ## Windows Full regression shards
58
78
 
59
79
  Standalone and PR-attached Full regression run each Windows runtime (Node 22 and Node 24) as five hosted-runner shards derived from the one canonical `full-release` plan: `core` (command gates and the bounded-parallel ordinary remainder), `resource` (the serial resource-sensitive invocation with its unchanged explicit bound), `rendering` (the serial isolated rendering invocation), `package` (packing, its own exact-package installation, Defender-backed first-attempt startup before package contracts, package smoke, and update timing), and `predecessor` (packing, its own exact-package installation, and the exhaustive published-predecessor invocation, which took 11 to 12 minutes of the former package shard on Windows). Each shard checks out, installs, builds, and records its own build receipt; the package and predecessor shards each pack and install their own candidate, and no installed state crosses shards. Linux, macOS, local `npm run test:full`, and the sequential publication lanes keep one complete plan with one shared installation.
@@ -132,4 +152,4 @@ Draft pull requests run no Development test suites. A ready version-3 PR whose b
132
152
 
133
153
  To disable selective execution without reducing PR coverage, use manual Development dispatch or force conservative ownership selection. To restore the historical predecessor oracle to every applicable PR, change its cadence from `exhaustive` to `pull-request`; do not delete it from Full/nightly/release. To disable prior-attempt reuse, require all accepted attempts to equal the aggregate attempt; this must not remove attempt-qualified artifacts or failure visibility. To disable prerequisite reuse, unset readiness/tarball variables and receipt paths so tier orchestration rebuilds and repacks.
134
154
 
135
- Rollback must retain every test, startup budget, zero automatic retry policy, platform/runtime lane, exact-package identity, resource isolation, complete fast/full/release compositions, and the single stable protected-branch aggregate.
155
+ Rollback must retain complete Full/nightly/release test coverage, startup budgets, zero automatic retry policy, required release platform/runtime lanes, exact-package identity, resource isolation, and the single stable protected-branch aggregate. It must not restore owner-wide or unrelated exhaustive execution to ordinary pull requests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.2.7-dev.831",
3
+ "version": "0.2.7-dev.835",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",