@timurproko/a1 0.2.7-dev.1231 → 0.2.7-dev.1233
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.
- package/dist/app/session-shell/session-shell.js +2 -1
- package/dist/composition/owned-ui.js +13 -79
- package/dist/composition/release-update-observation.d.ts +22 -0
- package/dist/composition/release-update-observation.js +91 -0
- package/dist/composition/resident-tab-bridge.d.ts +16 -0
- package/dist/composition/resident-tab-bridge.js +45 -16
- package/dist/composition/startup-route.d.ts +8 -0
- package/dist/composition/startup-route.js +20 -0
- package/dist/contracts/owned-ui/session-workflows.d.ts +3 -0
- package/dist/contracts/presentation/index.js +2 -1
- package/dist/foundation/release/bootstrap.js +46 -36
- package/dist/foundation/release/update-activation.js +8 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +3 -0
- package/dist/foundation/resident-tabs/bridge-client.js +7 -1
- package/dist/foundation/resident-tabs/continuity.d.ts +12 -0
- package/dist/foundation/resident-tabs/continuity.js +55 -0
- package/dist/foundation/resident-tabs/index.d.ts +3 -0
- package/dist/foundation/resident-tabs/index.js +4 -0
- package/dist/foundation/resident-tabs/maintenance.d.ts +1 -1
- package/dist/foundation/resident-tabs/release-holds.js +11 -8
- package/dist/foundation/resident-tabs/runtime-control.d.ts +38 -0
- package/dist/foundation/resident-tabs/runtime-control.js +73 -0
- package/dist/integrations/pi/engine/adapter.js +6 -1
- package/dist/integrations/pi/engine/diagnostics-state.d.ts +9 -0
- package/dist/integrations/pi/engine/diagnostics-state.js +34 -0
- package/dist/integrations/pi/engine/view-state.d.ts +2 -1
- package/dist/integrations/pi/engine/view-state.js +4 -16
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/docs/architecture/native-fixture-isolation.md +23 -0
- package/docs/architecture/resident-tabs.md +23 -4
- package/package.json +8 -3
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { RUNTIME_CONTROL_API } from "./runtime-control.js";
|
|
4
|
+
import { runBoundedHostCommand } from "./bounded-host-command.js";
|
|
5
|
+
import { resolveTerminalHostEntryPath, resolveTerminalHostPath } from "./ensure-terminal-host.js";
|
|
6
|
+
import { verifyTerminalHostArtifact } from "./terminal-host-artifact.js";
|
|
7
|
+
/** Activation authority requires a stable authenticated source-contract set, never an idle Agent. */
|
|
8
|
+
export async function assertResidentContinuity(options) {
|
|
9
|
+
const { inspect = inspectResidentContinuity, ...context } = options;
|
|
10
|
+
const first = await inspect(context);
|
|
11
|
+
const second = await inspect(context);
|
|
12
|
+
if (first !== second && second !== await inspect(context)) {
|
|
13
|
+
throw new Error("Resident workspace changed during update readiness; the prior client remains selected.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Native inspection/import is read-only toward execution and never starts or stops a source. */
|
|
17
|
+
export async function inspectResidentContinuity(options) {
|
|
18
|
+
let hasRecords = false;
|
|
19
|
+
for (const directory of ["tabs", "resident-workspaces"]) {
|
|
20
|
+
try {
|
|
21
|
+
hasRecords ||= (await readdir(resolve(options.dataDir, directory))).length > 0;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
25
|
+
throw new Error("Resident workspace discovery is unavailable; update activation was refused.");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!hasRecords)
|
|
30
|
+
return "empty";
|
|
31
|
+
const manifest = JSON.parse(await readFile(resolve(options.releaseRoot, "package.json"), "utf8"));
|
|
32
|
+
const contracts = manifest.residentControlContracts;
|
|
33
|
+
if (!Array.isArray(contracts)
|
|
34
|
+
|| ![RUNTIME_CONTROL_API, "resident-workspace-v1"].every(contract => contracts.includes(contract))) {
|
|
35
|
+
throw new Error("Candidate does not preserve the stable resident control and workspace contracts; activation was refused.");
|
|
36
|
+
}
|
|
37
|
+
const addon = resolveTerminalHostPath(options.releaseRoot, {}, process.platform, process.arch);
|
|
38
|
+
await verifyTerminalHostArtifact(addon, process.platform, process.arch);
|
|
39
|
+
const result = await runBoundedHostCommand({ executable: process.execPath, entry: resolveTerminalHostEntryPath(options.releaseRoot) }, [
|
|
40
|
+
"workspace-check", "--profile", options.profileRoot,
|
|
41
|
+
"--data-dir", options.dataDir, "--runtime-dir", options.runtimeDir,
|
|
42
|
+
], { label: "resident continuity check", maxStdoutBytes: 4096, timeoutMs: 8_000, environment: options.environment });
|
|
43
|
+
let report;
|
|
44
|
+
try {
|
|
45
|
+
report = JSON.parse(result.stdout.trim());
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error("Resident control compatibility could not be verified; the prior client remains selected.");
|
|
49
|
+
}
|
|
50
|
+
if (result.code !== 0 || report.schema !== "resident-workspace-check-v1" || report.compatible !== true
|
|
51
|
+
|| typeof report.fingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(report.fingerprint)) {
|
|
52
|
+
throw new Error("Resident control compatibility could not be verified; the prior client remains selected.");
|
|
53
|
+
}
|
|
54
|
+
return report.fingerprint;
|
|
55
|
+
}
|
|
@@ -10,3 +10,6 @@ export { runTerminalHostMaintenance } from "./maintenance.js";
|
|
|
10
10
|
export type { MaintenanceAction, MaintenanceHostState, MaintenanceOptions, MaintenanceReport } from "./maintenance.js";
|
|
11
11
|
export { DOCTOR_MAX_BYTES, writeDoctorBundle } from "./doctor.js";
|
|
12
12
|
export type { DoctorBundle, DoctorOptions } from "./doctor.js";
|
|
13
|
+
export type { ContinuityOptions } from "./continuity.js";
|
|
14
|
+
/** Load native continuity inspection only when a caller is preparing client activation. */
|
|
15
|
+
export declare function assertResidentContinuity(options: import("./continuity.js").ContinuityOptions): Promise<void>;
|
|
@@ -4,3 +4,7 @@ export { selectResidentHost } from "./routing.js";
|
|
|
4
4
|
export { readResidentReleaseHolds } from "./release-holds.js";
|
|
5
5
|
export { runTerminalHostMaintenance } from "./maintenance.js";
|
|
6
6
|
export { DOCTOR_MAX_BYTES, writeDoctorBundle } from "./doctor.js";
|
|
7
|
+
/** Load native continuity inspection only when a caller is preparing client activation. */
|
|
8
|
+
export async function assertResidentContinuity(options) {
|
|
9
|
+
await (await import("./continuity.js")).assertResidentContinuity(options);
|
|
10
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TerminalHostCommand } from "./ensure-terminal-host.js";
|
|
2
|
-
export type MaintenanceAction = "list" | "stop" | "stop-all" | "host-status" | "host-stop";
|
|
2
|
+
export type MaintenanceAction = "list" | "stop" | "stop-all" | "host-status" | "host-stop" | "runtime-control";
|
|
3
3
|
export type MaintenanceHostState = "none" | "running" | "stopped" | "unresponsive" | "incompatible" | "refused" | "unverifiable" | "unavailable";
|
|
4
4
|
/** The versioned `a1 tabs` report: `a1-tabs-maintenance-v1`, as printed by the terminal host. */
|
|
5
5
|
export interface MaintenanceReport {
|
|
@@ -44,15 +44,18 @@ export async function readResidentReleaseHolds(options) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
async function hasResidentRecords(dataDir) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
for (const directory of ["tabs", "resident-workspaces"]) {
|
|
48
|
+
try {
|
|
49
|
+
// Invariant: native inspection, not a following stat, judges unknown or linked entries.
|
|
50
|
+
if ((await readdir(resolve(dataDir, directory))).length > 0)
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
|
-
|
|
55
|
-
return profiles.length > 0;
|
|
58
|
+
return false;
|
|
56
59
|
}
|
|
57
60
|
function incomplete(reason) {
|
|
58
61
|
return { complete: false, releaseRoots: [], uncertain: [reason] };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type MaintenanceOptions, type MaintenanceReport } from "./maintenance.js";
|
|
2
|
+
/** Mandatory native control semantics; optional methods never establish source authority. */
|
|
3
|
+
export declare const RUNTIME_CONTROL_API = "resident-control-v1";
|
|
4
|
+
export declare const RUNTIME_CONTROL_CORE: readonly ["attach", "tab.list", "tab.view", "input", "resize", "detach", "controller-boundary"];
|
|
5
|
+
export interface RuntimeControlIdentity {
|
|
6
|
+
readonly releaseId: string;
|
|
7
|
+
readonly scope: string;
|
|
8
|
+
readonly epoch: number;
|
|
9
|
+
readonly bootId: string;
|
|
10
|
+
readonly osSessionId: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RuntimeControlDescription extends RuntimeControlIdentity {
|
|
13
|
+
readonly api: typeof RUNTIME_CONTROL_API;
|
|
14
|
+
readonly generation: 1;
|
|
15
|
+
readonly core: readonly string[];
|
|
16
|
+
readonly frameLimit: number;
|
|
17
|
+
readonly inputLimit: number;
|
|
18
|
+
readonly retryInput: false;
|
|
19
|
+
}
|
|
20
|
+
export type RuntimeControlVerdict = {
|
|
21
|
+
readonly status: "compatible";
|
|
22
|
+
readonly contract: RuntimeControlDescription;
|
|
23
|
+
} | {
|
|
24
|
+
readonly status: "blocked";
|
|
25
|
+
readonly reason: "unavailable" | "invalid-contract" | "unsupported-api" | "source-mismatch" | "stale-epoch" | "missing-core";
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Validates an authenticated source's descriptor against separately verified source custody.
|
|
29
|
+
* This is not an activation verdict: the caller must also reconcile every live tab and fence
|
|
30
|
+
* changes to that topology before committing a client selection.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assessRuntimeControl(value: unknown, expected: RuntimeControlIdentity): RuntimeControlVerdict;
|
|
33
|
+
export interface ReadRuntimeControlOptions extends Omit<MaintenanceOptions, "action" | "tabId"> {
|
|
34
|
+
readonly expected: RuntimeControlIdentity;
|
|
35
|
+
readonly maintain?: (options: MaintenanceOptions) => Promise<MaintenanceReport>;
|
|
36
|
+
}
|
|
37
|
+
/** A bounded read-only authenticated exchange; it never starts a host or substitutes a release. */
|
|
38
|
+
export declare function readRuntimeControl(options: ReadRuntimeControlOptions): Promise<RuntimeControlVerdict>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { runTerminalHostMaintenance } from "./maintenance.js";
|
|
2
|
+
/** Mandatory native control semantics; optional methods never establish source authority. */
|
|
3
|
+
export const RUNTIME_CONTROL_API = "resident-control-v1";
|
|
4
|
+
export const RUNTIME_CONTROL_CORE = Object.freeze([
|
|
5
|
+
"attach", "tab.list", "tab.view", "input", "resize", "detach", "controller-boundary",
|
|
6
|
+
]);
|
|
7
|
+
/**
|
|
8
|
+
* Validates an authenticated source's descriptor against separately verified source custody.
|
|
9
|
+
* This is not an activation verdict: the caller must also reconcile every live tab and fence
|
|
10
|
+
* changes to that topology before committing a client selection.
|
|
11
|
+
*/
|
|
12
|
+
export function assessRuntimeControl(value, expected) {
|
|
13
|
+
if (!isRecord(value))
|
|
14
|
+
return blocked("invalid-contract");
|
|
15
|
+
if (value.api !== RUNTIME_CONTROL_API || value.generation !== 1)
|
|
16
|
+
return blocked("unsupported-api");
|
|
17
|
+
if (!identityValid(value) || !identityValid(expected)
|
|
18
|
+
|| !boundedStrings(value.core) || typeof value.retryInput !== "boolean"
|
|
19
|
+
|| !positiveInteger(value.frameLimit, 2 * 1024 * 1024)
|
|
20
|
+
|| !positiveInteger(value.inputLimit, 1024 * 1024) || value.inputLimit > value.frameLimit) {
|
|
21
|
+
return blocked("invalid-contract");
|
|
22
|
+
}
|
|
23
|
+
if (value.releaseId !== expected.releaseId || value.scope !== expected.scope
|
|
24
|
+
|| value.bootId !== expected.bootId || value.osSessionId !== expected.osSessionId)
|
|
25
|
+
return blocked("source-mismatch");
|
|
26
|
+
if (value.epoch !== expected.epoch)
|
|
27
|
+
return blocked("stale-epoch");
|
|
28
|
+
const core = value.core;
|
|
29
|
+
if (value.retryInput || RUNTIME_CONTROL_CORE.some(method => !core.includes(method)))
|
|
30
|
+
return blocked("missing-core");
|
|
31
|
+
// Security: retain only allowlisted metadata, never arbitrary optional source data.
|
|
32
|
+
return { status: "compatible", contract: Object.freeze({
|
|
33
|
+
api: RUNTIME_CONTROL_API, generation: 1, releaseId: value.releaseId, scope: value.scope,
|
|
34
|
+
epoch: value.epoch, bootId: value.bootId, osSessionId: value.osSessionId,
|
|
35
|
+
core: Object.freeze([...value.core]), frameLimit: value.frameLimit, inputLimit: value.inputLimit,
|
|
36
|
+
retryInput: false,
|
|
37
|
+
}) };
|
|
38
|
+
}
|
|
39
|
+
/** A bounded read-only authenticated exchange; it never starts a host or substitutes a release. */
|
|
40
|
+
export async function readRuntimeControl(options) {
|
|
41
|
+
const { expected, maintain = runTerminalHostMaintenance, ...maintenance } = options;
|
|
42
|
+
try {
|
|
43
|
+
const report = await maintain({ ...maintenance, action: "runtime-control" });
|
|
44
|
+
if (report.schema !== "a1-tabs-maintenance-v1" || report.exitCode !== 0
|
|
45
|
+
|| report.host !== "running" || report.command !== "runtime-control")
|
|
46
|
+
return blocked("unavailable");
|
|
47
|
+
return assessRuntimeControl(report.result, expected);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return blocked("unavailable");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function blocked(reason) {
|
|
54
|
+
return { status: "blocked", reason };
|
|
55
|
+
}
|
|
56
|
+
function isRecord(value) {
|
|
57
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
function positiveInteger(value, maximum = Number.MAX_SAFE_INTEGER) {
|
|
60
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= maximum;
|
|
61
|
+
}
|
|
62
|
+
function boundedStrings(value) {
|
|
63
|
+
return Array.isArray(value) && value.length <= 64 && new Set(value).size === value.length
|
|
64
|
+
&& value.every(entry => typeof entry === "string" && entry.length > 0 && entry.length <= 64);
|
|
65
|
+
}
|
|
66
|
+
function identityValid(value) {
|
|
67
|
+
const record = value;
|
|
68
|
+
return typeof record.releaseId === "string" && /^[a-f0-9]{64}$/u.test(record.releaseId)
|
|
69
|
+
&& typeof record.scope === "string" && /^[a-f0-9]{20}$/u.test(record.scope)
|
|
70
|
+
&& positiveInteger(record.epoch)
|
|
71
|
+
&& [record.bootId, record.osSessionId].every(entry => typeof entry === "string" && entry.length > 0
|
|
72
|
+
&& entry.length <= 256 && !/[\x00-\x1f\x7f]/u.test(entry));
|
|
73
|
+
}
|
|
@@ -211,6 +211,11 @@ export class PiEngineAdapter {
|
|
|
211
211
|
#announceReleaseUpdate(release) {
|
|
212
212
|
if (this.#disposed)
|
|
213
213
|
return;
|
|
214
|
+
if (release.status === "client-connected") {
|
|
215
|
+
if (this.#state.diagnosticsState.acknowledgeClientVersion(release.version))
|
|
216
|
+
this.#emitView();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
214
219
|
if (release.status === "installed") {
|
|
215
220
|
this.#addDiagnostic("info", "release-update-installed", `Update ${release.version} installed; restart to update.`, true);
|
|
216
221
|
}
|
|
@@ -510,7 +515,7 @@ export class PiEngineAdapter {
|
|
|
510
515
|
this.#engine.resume();
|
|
511
516
|
}
|
|
512
517
|
#recordDiagnostic(severity, code, message, recoverable) {
|
|
513
|
-
return this.#state.
|
|
518
|
+
return this.#state.diagnosticsState.record(severity, code, message, recoverable);
|
|
514
519
|
}
|
|
515
520
|
}
|
|
516
521
|
export async function createPiEngineAdapter(options) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OwnedUiDiagnostics } from "../../../contracts/owned-ui/index.js";
|
|
2
|
+
/** Bounded structured notices; acknowledgements never inspect terminal cells or touch Agent work. */
|
|
3
|
+
export declare class EngineDiagnostics {
|
|
4
|
+
#private;
|
|
5
|
+
get values(): readonly OwnedUiDiagnostics[];
|
|
6
|
+
get messages(): readonly string[];
|
|
7
|
+
record(severity: OwnedUiDiagnostics["severity"], code: string, message: string, recoverable: boolean): OwnedUiDiagnostics;
|
|
8
|
+
acknowledgeClientVersion(version: string): boolean;
|
|
9
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { gte, valid as validSemver } from "semver";
|
|
2
|
+
/** Bounded structured notices; acknowledgements never inspect terminal cells or touch Agent work. */
|
|
3
|
+
export class EngineDiagnostics {
|
|
4
|
+
#values = [];
|
|
5
|
+
#messages = [];
|
|
6
|
+
#sequence = 0;
|
|
7
|
+
get values() { return this.#values; }
|
|
8
|
+
get messages() { return this.#messages; }
|
|
9
|
+
record(severity, code, message, recoverable) {
|
|
10
|
+
const diagnostic = { sequence: this.#sequence++, code, severity, message, recoverable };
|
|
11
|
+
this.#values.push(diagnostic);
|
|
12
|
+
if (this.#values.length > 100)
|
|
13
|
+
this.#values.shift();
|
|
14
|
+
this.#messages = [...this.#messages, message].slice(-8);
|
|
15
|
+
return diagnostic;
|
|
16
|
+
}
|
|
17
|
+
acknowledgeClientVersion(version) {
|
|
18
|
+
if (validSemver(version) === null)
|
|
19
|
+
return false;
|
|
20
|
+
const removed = new Set();
|
|
21
|
+
for (let index = this.#values.length - 1; index >= 0; index--) {
|
|
22
|
+
const diagnostic = this.#values[index];
|
|
23
|
+
const installed = diagnostic.code === "release-update-installed" ? /^Update (\S+) installed; restart to update\.$/u.exec(diagnostic.message)?.[1] : undefined;
|
|
24
|
+
if (installed !== undefined && validSemver(installed) !== null && gte(version, installed)) {
|
|
25
|
+
removed.add(diagnostic.message);
|
|
26
|
+
this.#values.splice(index, 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (removed.size === 0)
|
|
30
|
+
return false;
|
|
31
|
+
this.#messages = this.#messages.filter(message => !removed.has(message) || this.#values.some(diagnostic => diagnostic.message === message));
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EngineDiagnostics } from "./diagnostics-state.js";
|
|
1
2
|
import type { OwnedUiDiagnostics, OwnedUiEditorState, OwnedUiModelInfo, OwnedUiSessionViewModel, OwnedUiStatusView, OwnedUiTerminalSurface, OwnedUiThinkingLevel, OwnedUiUsageView } from "../../../contracts/owned-ui/index.js";
|
|
2
3
|
/**
|
|
3
4
|
* The engine adapter's own view-model state: lifecycle, editor, status, terminal surface, active
|
|
@@ -10,6 +11,7 @@ export declare class EngineViewState {
|
|
|
10
11
|
lifecycle: OwnedUiSessionViewModel["lifecycle"];
|
|
11
12
|
activeModel: OwnedUiModelInfo | null;
|
|
12
13
|
thinkingLevel: OwnedUiThinkingLevel;
|
|
14
|
+
readonly diagnosticsState: EngineDiagnostics;
|
|
13
15
|
get revision(): number;
|
|
14
16
|
get editor(): OwnedUiEditorState;
|
|
15
17
|
get status(): OwnedUiStatusView;
|
|
@@ -38,5 +40,4 @@ export declare class EngineViewState {
|
|
|
38
40
|
replaceSession(): void;
|
|
39
41
|
/** Delivery overload ended the run: ready when it can resume, stopped once disposed, otherwise failed. */
|
|
40
42
|
settleOverload(canResume: boolean, disposed: boolean): void;
|
|
41
|
-
recordDiagnostic(severity: OwnedUiDiagnostics["severity"], code: string, message: string, recoverable: boolean): OwnedUiDiagnostics;
|
|
42
43
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import { EngineDiagnostics } from "./diagnostics-state.js";
|
|
1
2
|
const DEFAULT_SURFACE = {
|
|
2
3
|
columns: 100,
|
|
3
4
|
rows: 32,
|
|
4
5
|
focusedRegion: "editor",
|
|
5
6
|
hardwareCursor: false,
|
|
6
7
|
};
|
|
7
|
-
const MAX_DIAGNOSTICS = 100;
|
|
8
|
-
const MAX_STATUS_DIAGNOSTICS = 8;
|
|
9
8
|
/**
|
|
10
9
|
* The engine adapter's own view-model state: lifecycle, editor, status, terminal surface, active
|
|
11
10
|
* model, thinking level, the usage cache, diagnostics, and the view revision. Every change that
|
|
@@ -33,12 +32,12 @@ export class EngineViewState {
|
|
|
33
32
|
activeModel = null;
|
|
34
33
|
thinkingLevel = "medium";
|
|
35
34
|
#usage;
|
|
36
|
-
|
|
35
|
+
diagnosticsState = new EngineDiagnostics();
|
|
37
36
|
get revision() { return this.#revision; }
|
|
38
37
|
get editor() { return this.#editor; }
|
|
39
|
-
get status() { return this.#status; }
|
|
38
|
+
get status() { return { ...this.#status, diagnostics: this.diagnosticsState.messages }; }
|
|
40
39
|
get terminal() { return this.#terminal; }
|
|
41
|
-
get diagnostics() { return this
|
|
40
|
+
get diagnostics() { return this.diagnosticsState.values; }
|
|
42
41
|
/** Any emitted event advances the revision. */
|
|
43
42
|
advance() {
|
|
44
43
|
this.#revision += 1;
|
|
@@ -112,15 +111,4 @@ export class EngineViewState {
|
|
|
112
111
|
this.#editor = { ...this.#editor, submitEnabled: canResume && !disposed };
|
|
113
112
|
this.#revision += 1;
|
|
114
113
|
}
|
|
115
|
-
recordDiagnostic(severity, code, message, recoverable) {
|
|
116
|
-
const diagnostic = { sequence: this.#diagnostics.length, code, severity, message, recoverable };
|
|
117
|
-
this.#diagnostics.push(diagnostic);
|
|
118
|
-
if (this.#diagnostics.length > MAX_DIAGNOSTICS)
|
|
119
|
-
this.#diagnostics.shift();
|
|
120
|
-
this.#status = {
|
|
121
|
-
...this.#status,
|
|
122
|
-
diagnostics: [...this.#status.diagnostics, message].slice(-MAX_STATUS_DIAGNOSTICS),
|
|
123
|
-
};
|
|
124
|
-
return diagnostic;
|
|
125
|
-
}
|
|
126
114
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"architecture": "arm64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-
|
|
10
|
+
"builtAt": "2026-11-03T07:34:59.833Z",
|
|
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-
|
|
12
|
+
"builtAt": "2026-11-03T07:38:20.584Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "61ed28a1cb8a7e65c8b963a672917864b607f12b418468700e214663b9506b61",
|
|
16
|
+
"size": 6566864
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-
|
|
10
|
+
"builtAt": "2026-11-03T07:35:06.034Z",
|
|
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-
|
|
12
|
+
"builtAt": "2026-11-03T07:41:31.478Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "4e257b645cda56dce14b3c442b673da98049ec91fa7d8c7cd7d265ddd61d9c5b",
|
|
16
|
+
"size": 7159480
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "linux",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-
|
|
10
|
+
"builtAt": "2026-11-03T07:34:54.153Z",
|
|
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-
|
|
12
|
+
"builtAt": "2026-11-03T07:37:37.426Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "913e106efd96c5d59b33cdeea2261873a1e4a1bb73b3e94fbb847c6af5f941ba",
|
|
16
|
+
"size": 7936048
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"platform": "win32",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-
|
|
10
|
+
"builtAt": "2026-11-03T07:35:37.945Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
|
-
"sha256": "
|
|
13
|
+
"sha256": "f1906525fc8654e10b859c6ec679548c48f6f136441afac37a13a596cfcbaf35",
|
|
14
14
|
"size": 402432
|
|
15
15
|
},
|
|
16
16
|
"provenance": {
|
|
Binary file
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"architecture": "x64",
|
|
10
10
|
"target": "x86_64-pc-windows-msvc",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-
|
|
12
|
+
"builtAt": "2026-11-03T07:38:31.534Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "516d1cf1cfd627ce7f2594c67766ee1db644cf7952bb9a6522ab48417f6a91d0",
|
|
16
|
+
"size": 7048192
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Native fixture validation
|
|
2
|
+
|
|
3
|
+
## Normal automatic CI
|
|
4
|
+
|
|
5
|
+
Development, Full regression and publication run native builds and fixtures directly on their normal GitHub-hosted runners. The agent-added sandbox, restricted OS accounts, network provisioning, fixture receipts, feature/runtime permission checks and additional security gates have been removed.
|
|
6
|
+
|
|
7
|
+
The existing required suites remain required: terminal-host partitions, release isolation, resident chaos, exact-package published-predecessor continuity, terminal fidelity/latency certificates and bounded decoder fuzzing. Their platform coverage and product assertions are unchanged. Production publication protection is unchanged.
|
|
8
|
+
|
|
9
|
+
When implementation outcomes are complete, trusted readiness/finalization publishes the exact candidate head and normal PR validation starts automatically. A repair push triggers the same flow for its new head. No fixture approval, deployment review, manual dispatch or workflow restart is required. Failed, missing or cancelled required results block integration; the maintainer chooses when to merge after required checks pass.
|
|
10
|
+
|
|
11
|
+
## Optional local checks
|
|
12
|
+
|
|
13
|
+
`npm run check:local` runs typechecking, architecture/identity/naming checks and non-native workflow regression tests. It does not build native artifacts or launch application/fixture processes. It is a convenience command, not an execution permission system or a prerequisite for CI advancement.
|
|
14
|
+
|
|
15
|
+
The previous Defender report is unresolved historical evidence, not a passing security verdict. This workflow rollback changes no workstation protection setting or quarantined file.
|
|
16
|
+
|
|
17
|
+
## macOS custody
|
|
18
|
+
|
|
19
|
+
The macOS predecessor custody repair remains independent of CI provisioning. `getaudit_addr` supplies kernel audit user/session IDs; `audit_session_self()` returns only a process-local Mach port name.
|
|
20
|
+
|
|
21
|
+
For immutable predecessors using the older metadata, the explicit source route still requires unchanged boot, exact native PID/start identity, source credentials/epoch and a connected kernel `LOCAL_PEERTOKEN` matching PID, real/effective UID and current audit user/session. Incomparable metadata is retained, not treated as proof of login equality or termination. Generic cross-release admission is unchanged.
|
|
22
|
+
|
|
23
|
+
Static/workflow tests are not native platform execution evidence. Required exact-head native results remain the CI authority.
|
|
@@ -175,7 +175,7 @@ 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
|
|
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. A new client aggregates verified source routes while new execution uses its selected immutable release. 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
|
|
|
@@ -326,11 +326,27 @@ 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
|
-
- **Exact release admission.**
|
|
330
|
-
- **Independent activation.** A newer
|
|
329
|
+
- **Exact release admission.** Ordinary attach clients, holders and bridges present their originating release identity before capability negotiation. Matching protocol generation or display version does not permit implicit cross-release attachment. The explicit `SourceRoute` adapter selects and verifies one source namespace, native owner and epoch before authenticating client control to that source; it never adopts foreign holders or rewrites execution custody. Native clients also check the welcome identity; bridge identity is consumed from holder-private launch context before extensions load.
|
|
330
|
+
- **Independent activation.** A newer client starts or reuses its own execution source for new tabs, then reconnects the same profile workspace through compatible explicit source routes. Readiness and active-reference publication require repeated verified source-contract inspection; incompatibility keeps an eligible verified prior client selected. Existing resident and solo processes remain untouched. Source recovery and child launches retain the originating fixed Node, entry and addon; the active-release pointer never retargets them. No automatic migration, execution handoff or idle recycling is performed.
|
|
331
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
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
|
+
#### Stable runtime control and profile workspace
|
|
335
|
+
|
|
336
|
+
The `resident-control-v1` descriptor is available through authenticated native `runtime.describe` maintenance. It binds the mandatory attach/list/view/input/resize/detach/controller-boundary semantics to an exact source release, profile scope, epoch and boot/login identity. It explicitly forbids automatic input retry after uncertain delivery. Native MessagePack fixtures and the TypeScript consumer pin the same bytes; unknown optional metadata is not retained. This descriptor alone is not proof that every live Agent can be controlled and is not an activation verdict.
|
|
337
|
+
|
|
338
|
+
`SourceRoute` verifies canonical profile/source scope, bounded owner-private records, native owner/start identity, boot/login, credentials and the authenticated epoch. Inspection starts no source and loads no source code. The explicit attach adapter permits client control through the source's existing controller barriers, without acquiring holder or session-writer custody. Generic same-release refusal and release-local maintenance remain unchanged. Supported published generation-1 predecessors use their proven mandatory features and neutral private launch contract; sources advertising `resident-control-v1` must satisfy its exact binding and non-replay semantics. Unsupported declared versions fail closed.
|
|
339
|
+
|
|
340
|
+
`WorkspaceStore` lives at `<data>/resident-workspaces/<canonical-profile-token>/`. Its kernel writer lease, independent monotonic epoch counter, atomic primary/previous generations and bounded corruption preservation prevent stale writers and empty reinitialization over known state. It stores source-qualified logical tab identities, names, order, selection and custody metadata, not credentials, environment, prompts, drafts or terminal bytes. Only a positively authenticated complete source snapshot may remove that source's entries; discovery absence cannot remove another source. A positively ended boot/login archives bounded topology history rather than restoring Agents.
|
|
341
|
+
|
|
342
|
+
Ordinary resident launch uses the native workspace proxy. Its connection, routing, view-admission and persistence modules aggregate source lists/statuses and retained surfaces; new Agent and singleton creation uses the selected release. Input, paste, pointer/focus, resize, dialogs and explicit lifecycle requests go to the authenticated live owner. Drafts, attachments, queued work, session configuration and interaction state stay in the original child, not a client-side reconstruction. Selection commits durably before the exact source/tab/view generation can release bounded queued input. Superseded or failed views reject that input. Reconnection restores observation/resize only and repeats view admission; it never retries input or lifecycle mutations. Source status inspection and durable index writes run in workers, not the live control loop.
|
|
343
|
+
|
|
344
|
+
Coordinator/client loss retains source execution and uses the originating source's existing recovery path. Current and previous workspace generations retain source artifacts until positive closure; missing artifacts are not closure. Release/dependency retention also covers recent private recovery custody using bounded metadata only, without reading, exporting or deleting draft/journal contents. Unknown custody retains candidates. Explicit host shutdown remains execution-only rather than being blocked by recovery files.
|
|
345
|
+
|
|
346
|
+
Optional authenticated client-build metadata is advisory, not execution identity or controller authority. Modern retained children acknowledge installed update notices already satisfied by that connected client while preserving copy feedback and Notify/No behavior. An immutable predecessor unable to acknowledge that metadata gets a client-owned notice explaining that retained Agent UI notices do not require restarting Agents. Existing engine code remains its original version.
|
|
347
|
+
|
|
348
|
+
Real published-predecessor tests are included in exact-package CI on Windows x64, Linux x64 and macOS arm64. The focused Windows run exercises live model/tool work, queued input, retained draft/selection, hard client death and originating-coordinator recovery with unchanged Agent/holder/tool identities and no replay or deferred idle restart. Local evidence is not certification of unrun platforms or of the exact packed CI candidate.
|
|
349
|
+
|
|
334
350
|
### 11. Foreground UX
|
|
335
351
|
|
|
336
352
|
- **Strip.** Row 0, drawn by the attach client, reproduces the approved v2 default appearance:
|
|
@@ -376,13 +392,16 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
376
392
|
- **Changelog.** The changelog opens as a singleton `changelog-` tab titled `Changelog`, from the palette or the unbound `tabs.changelog` action. The tab registry accepts the `agent`, `settings`, and `changelog` kinds. Its provider registration makes it non-renameable, Close-only, sessionless, prewarmed, and hidden: the attach client starts it with `tab.prewarm-changelog` once Settings is ready, and again after a Changelog tab closes, so opening it shows the running tab at once. Like a hidden Settings tab, a hidden Changelog never takes the view when the viewed tab closes, so closing the last Agent shows the landing. Its screen is the tab: it shows no `Esc close` hint, and neither `Esc` nor one `Ctrl+C` closes it.
|
|
377
393
|
- **Tab-bar provider.** Agent, Settings, and Changelog are registered once in the I/O-free `TabKind` provider shared by the attach client and server. Each registration declares its label, shortcut action, multiplicity, create-or-toggle activation, prewarm policy, renameability, Agent-session ownership, child route, and launcher marker. Shortcuts, the command palette, `+`, popup capabilities, registry validation, session handling, and route delivery consume that provider instead of rebuilding Agent or utility tables. Multi-instance Agent activation creates a tab; singleton-toggle activation creates or reveals its one tab, selects it in place when already shown, and hides it while viewed according to the common toggle rule. Singleton visibility and prewarm state are arrays in provider order, so adding another singleton does not add a concrete view field.
|
|
378
394
|
- The tab bar also owns one kind-neutral ordering API: every newly created tab and every hidden tab newly joining the strip appends after all live tabs; selecting an already shown tab retains its slot; drag order remains authoritative; and stopped registry records never affect the visible insertion destination. Cold holder creation, standby promotion, and singleton reveal use these same operations, so `Agent`, then `Settings`, then another `Agent` stays in that order.
|
|
395
|
+
- The workspace proxy reserves creation positions before source IDs arrive, restores caller request identities, and prevents old prewarm responses from taking an explicit open. It publishes final topology before requesting the first selected source surface, including when a prewarmed utility ID already exists in an older client position. Durable acknowledgements update custody without replacing newer user order. Hidden foreign singleton records never produce duplicate Settings/Changelog chips or consume published reorder indexes, and unchanged list replies do not invent new topology revisions.
|
|
396
|
+
- Normal lifecycle reconciliation is quiet: known-closed identities absorb duplicate Close, stale references refresh topology rather than become input errors, and superseded or closed views cancel without banners. Source-side repeated Close does not repeat stop or persistence. Storage uses one coalesced latest-intent slot plus its in-flight operation, retains every affected source, and waits for other clients' writer leases only off the UI loop. Passive source recovery retains observation/resize intent but never queued input. See the delivery's tab-notification audit for producer coverage.
|
|
397
|
+
- Utility toggles hide and reuse their process even when the final visible utility leaves the strip; the landing replaces it. Explicit Close still removes the instance. Source/tab/generation fencing and one complete synchronized presentation commit exclude stale bodies and intermediate selection jumps. The full-coverage host landing repaints in place rather than clearing the strip between utility transitions.
|
|
379
398
|
- `bin/ui.js` resolves the utility route from the consumed tab id before loading the declared Agent startup graph. A utility id imports the slim composition instead, so no Pi engine host, Agent session, extensions, prompt history, suggestions, or draft recovery are loaded or started; Agent tabs, the standby, `a1 solo`, and `a1 pi` keep the full graph.
|
|
380
399
|
- The slim process still owns one fullscreen `OwnedUiTerminalHost`, the background canvas, theme and accent singletons, Pi keybindings, controller-boundary input, and the tab bridge. A minimal presenter opens only the pinned route. It publishes effective tab shortcuts, requests the attach client's Plus Menu through `configure-plus`, requests tab close when the screen closes, detaches the controlling client on the interrupt exit, and ends on `bridge.stop`.
|
|
381
400
|
- Settings builds its Agent section from Pi's persisted `SettingsManager` after the ordinary project-trust decision, with persist-only bindings because each Agent process owns live effects. Its per-model thinking choices come from an offline `ModelRuntime` over the profile's `auth.json` and `models.json`. Changelog reads the packaged release-note catalog directly and never takes or acknowledges an Agent startup "What's New" claim.
|
|
382
401
|
- **Reveal.** Opening a tab draws it at once: a created Agent and a revealed Settings or Changelog tab both keep the current content, another tab or the landing, until their first nonblank drawn frame commits, a retained blank surface included. Blank child revisions remain parked for any duration rather than exposing an empty body or child cursor; explicit failure and recovery outcomes still replace the startup presentation. Every changed-size resize forwards geometry immediately and repaints the retained strip and child surface directly at the new dimensions; no resize curtain or fade remains. Inactive terminal-tab resizes also repaint without an overlay, and focus return refreshes the complete frame. A starting chip shows no spinner. One tab opens at a time: new-Agent input while a tab the client asked for is starting or not yet shown starts nothing.
|
|
383
402
|
- **Rename selection.** The inline rename draws a selection on the prompt editor's selection background (RGB `38,79,120`, palette `24`) and keeps the chip's text color, restoring the chip's background after it; only the caret is a reversed cell.
|
|
384
403
|
- **Close.** `Alt+W` and the right-click menu close a tab directly, including while it is working. Closing removes the chip without a confirmation, stopping/read-only frame, or close glyph, then gracefully stops the child under the existing bounded termination policy. Its session stays resumable. Closing the last tab leaves `+` and the clock beside the landing.
|
|
385
|
-
- **Landing.** While the attach client has no tab to show, the attach role renders the a1 landing as the frame's body. The launch cwd sits where an Agent tab's footer shows it, flush left on the second-to-last body row with the same `~` formatting and `dim` color, so the folder does not move when a tab opens or closes; `v<version>`, read once from the release root's `package.json`, sits below it and the three-row half-block `a1` wordmark in the profile's accent above it (omitted in Apple Terminal). The accent colors are a native table pinned by `landing_accents.json`, which a TypeScript test checks against `derivePiAccentProjection`. The body is painted with the canvas `backgroundStyle` gives Agent tabs. The landing holds no input: tab shortcuts, the launcher, strip mouse, and double-`Ctrl+C` behave as with tabs, and no cursor shows. A hidden Settings tab is prewarmed and kept running beside the landing, as beside Agents, so Settings opens without a start; it closes when the client detaches
|
|
404
|
+
- **Landing.** While the attach client has no tab to show, the attach role renders the a1 landing as the frame's body. The launch cwd sits where an Agent tab's footer shows it, flush left on the second-to-last body row with the same `~` formatting and `dim` color, so the folder does not move when a tab opens or closes; `v<version>`, read once from the release root's `package.json`, sits below it and the three-row half-block `a1` wordmark in the profile's accent above it (omitted in Apple Terminal). The accent colors are a native table pinned by `landing_accents.json`, which a TypeScript test checks against `derivePiAccentProjection`. The body is painted with the canvas `backgroundStyle` gives Agent tabs. The landing holds no input: tab shortcuts, the launcher, strip mouse, and double-`Ctrl+C` behave as with tabs, and no cursor shows. A hidden Settings tab is prewarmed and kept running beside the landing, as beside Agents, so Settings opens without a start; it closes when the client detaches. Plain workspace launch treats a roster holding only utilities as empty and shows the landing without creating an Agent; new execution requires an explicit Agent or session-open action. An Agent record whose child exited and that no holder owns is not a live Agent for plain launch: remain on the landing rather than replacing its execution. Explicit creation uses a fresh identity and never adopts a stopped tab's directory, draft or recovery screen. A retained live workspace is selected from the complete authenticated roster before admission; its selected source/tab/generation is explicitly observed before the first nonempty frame, so no landing is painted while those Agents reconnect. On the client, a first registry list with nothing to show counts as the strip shown empty, so the landing with `+` and the clock paints at once instead of an empty screen, while a listed starting tab keeps its first-draw gate. Adding a tab from the landing keeps it until the tab's first drawn frame replaces it at once; the landing has no animation of its own.
|
|
386
405
|
- **Detach.**
|
|
387
406
|
- `Ctrl+C` twice detaches from any tab and is handled by the attach client, so it works even when the bridge is down. Inside an A1 tab, `/quit` and empty-editor `Ctrl+D` send a detach request carrying the immutable command-origin controller identity and revision; stale, ambiguous or unavailable attribution keeps the tab running and directs the user to `Ctrl+C` twice.
|
|
388
407
|
- The client restores the terminal. When agents are still running, it prints `1 agent still running run a1 to return.` or `N agents still running run a1 to return.`; otherwise it prints the resume hint.
|