@timurproko/a1 0.1.8-dev.279 → 0.1.8-dev.287

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 (29) hide show
  1. package/bin/ui.js +14 -2
  2. package/dist/contracts/owned-ui/image-attachments.d.ts +20 -0
  3. package/dist/contracts/owned-ui/image-attachments.js +42 -0
  4. package/dist/contracts/owned-ui/index.d.ts +1 -0
  5. package/dist/contracts/owned-ui/index.js +1 -0
  6. package/dist/contracts/owned-ui/validation.js +2 -9
  7. package/dist/features/owned-ui/run.js +16 -1
  8. package/dist/foundation/release/bootstrap.js +2 -1
  9. package/dist/foundation/terminal-cleanup/fatal-exit.d.ts +12 -0
  10. package/dist/foundation/terminal-cleanup/fatal-exit.js +85 -0
  11. package/dist/foundation/terminal-cleanup/index.d.ts +2 -0
  12. package/dist/foundation/terminal-cleanup/index.js +2 -0
  13. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +4 -0
  14. package/dist/foundation/terminal-cleanup/terminal-reset.js +40 -0
  15. package/dist/integrations/pi/session-ui/clipboard-image.js +5 -1
  16. package/dist/integrations/pi/session-ui/prompt-chips.d.ts +1 -1
  17. package/dist/integrations/pi/session-ui/prompt-chips.js +4 -1
  18. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +1 -0
  19. package/dist/integrations/pi/session-ui/session-shell-root.js +9 -1
  20. package/dist/integrations/pi/session-ui/session-shell.js +124 -42
  21. package/dist/integrations/pi/session-ui/session-viewport-controller.js +17 -7
  22. package/dist/integrations/pi/session-ui/system-clipboard.js +12 -3
  23. package/dist/integrations/pi/tui-runtime/adapter.js +13 -2
  24. package/dist/native/darwin-arm64/manifest.json +1 -1
  25. package/dist/native/linux-x64/manifest.json +1 -1
  26. package/dist/native/win32-x64/manifest.json +2 -2
  27. package/dist/native/win32-x64/process-guardian.exe +0 -0
  28. package/docs/architecture/boundaries.md +2 -1
  29. package/package.json +1 -1
package/bin/ui.js CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  const startup = await import("../dist/foundation/startup/index.js");
4
4
  startup.enableEnvironmentCompileCache(process.env);
5
+ const profile = process.env.A1_LAUNCH_PROFILE ?? "a1";
6
+ const { installFatalExit } = await import("../dist/foundation/terminal-cleanup/index.js");
7
+ const { resolveProductPaths } = await import("../dist/foundation/lifecycle/index.js");
8
+ const { join } = await import("node:path");
9
+ let runningApplication;
10
+ const fatal = profile === "a1" ? installFatalExit({
11
+ directory: join(resolveProductPaths().runtimeDir, "crashes"),
12
+ releaseId: process.env.A1_RELEASE_ID,
13
+ dispose: () => runningApplication?.dispose(),
14
+ }) : undefined;
5
15
  // Performance: begin the exact launch graph together while the trace write is pending.
6
16
  // Direct owned-module entries avoid evaluating unrelated barrel exports before first paint.
7
17
  const modules = Promise.all([
@@ -32,7 +42,6 @@ assertSinglePiTuiModuleAtLaunch(fileURLToPath(new URL("..", import.meta.url)), m
32
42
  await startup.markStartupPhase(process.env, "ui-modules-loaded");
33
43
 
34
44
  const sessionSelection = parseSessionSelection(process.argv.slice(2));
35
- const profile = process.env.A1_LAUNCH_PROFILE ?? "a1";
36
45
  Promise.resolve().then(() => {
37
46
  if (sessionSelection && profile !== "a1") throw new Error("session selection requires the normal A1 profile");
38
47
  return runSelectedInteractiveRuntime(profile, {
@@ -45,12 +54,15 @@ Promise.resolve().then(() => {
45
54
  sessionForkPrompt: createConsoleSessionForkPrompt(),
46
55
  ...(sessionSelection === undefined ? {} : { sessionSelection }),
47
56
  });
57
+ runningApplication = application;
48
58
  return await runOwnedUi({ application, ...(settings === null ? {} : { settings }) });
49
59
  },
50
60
  });
51
61
  }).then(
52
- code => { process.exitCode = code; },
62
+ code => { fatal?.remove(); process.exitCode = code; },
53
63
  error => {
64
+ if (fatal && error?.name !== "PiSessionSelectionError") { fatal.fail(error); return; }
65
+ fatal?.remove();
54
66
  console.error(error instanceof Error ? error.message : String(error));
55
67
  process.exitCode = error?.name === "PiSessionSelectionError" ? error.exitCode : 1;
56
68
  },
@@ -0,0 +1,20 @@
1
+ import type { OwnedUiImageAttachment } from "./model.js";
2
+ export declare const MAX_PROMPT_IMAGES = 8;
3
+ /** Limit applies to canonical ASCII base64, not decoded image bytes. */
4
+ export declare const MAX_IMAGE_DATA_BYTES: number;
5
+ declare const MESSAGES: {
6
+ readonly "image-size": "Image exceeds the 8 MiB encoded base64 limit. Reduce the image size and paste it again.";
7
+ readonly "image-count": "A prompt supports at most 8 images. Remove an attachment before adding another.";
8
+ readonly "image-data": "Image data is invalid. Paste a valid image again.";
9
+ readonly "image-mime": "Image MIME type is invalid. Paste a supported image again.";
10
+ };
11
+ /** Trusted, payload-free diagnostics for user-correctable attachment failures. */
12
+ export declare class ImageAttachmentError extends TypeError {
13
+ readonly code: keyof typeof MESSAGES;
14
+ constructor(code: keyof typeof MESSAGES);
15
+ }
16
+ /** Checks encoded length before decoding or copying a clipboard payload. */
17
+ export declare function assertImageEncodedSize(data: string): void;
18
+ /** Final command admission also covers restored and deferred non-clipboard inputs. */
19
+ export declare function assertPromptImages(images: readonly OwnedUiImageAttachment[]): void;
20
+ export {};
@@ -0,0 +1,42 @@
1
+ export const MAX_PROMPT_IMAGES = 8;
2
+ /** Limit applies to canonical ASCII base64, not decoded image bytes. */
3
+ export const MAX_IMAGE_DATA_BYTES = 8 * 1024 * 1024;
4
+ const MESSAGES = {
5
+ "image-size": "Image exceeds the 8 MiB encoded base64 limit. Reduce the image size and paste it again.",
6
+ "image-count": "A prompt supports at most 8 images. Remove an attachment before adding another.",
7
+ "image-data": "Image data is invalid. Paste a valid image again.",
8
+ "image-mime": "Image MIME type is invalid. Paste a supported image again.",
9
+ };
10
+ /** Trusted, payload-free diagnostics for user-correctable attachment failures. */
11
+ export class ImageAttachmentError extends TypeError {
12
+ code;
13
+ constructor(code) {
14
+ super(MESSAGES[code]);
15
+ this.code = code;
16
+ this.name = "ImageAttachmentError";
17
+ }
18
+ }
19
+ /** Checks encoded length before decoding or copying a clipboard payload. */
20
+ export function assertImageEncodedSize(data) {
21
+ if (typeof data !== "string" || data.length === 0)
22
+ throw new ImageAttachmentError("image-data");
23
+ if (Math.ceil(data.length / 4) * 4 > MAX_IMAGE_DATA_BYTES)
24
+ throw new ImageAttachmentError("image-size");
25
+ }
26
+ /** Final command admission also covers restored and deferred non-clipboard inputs. */
27
+ export function assertPromptImages(images) {
28
+ if (!Array.isArray(images) || images.length > MAX_PROMPT_IMAGES)
29
+ throw new ImageAttachmentError("image-count");
30
+ for (const image of images) {
31
+ if (!image || image.type !== "image")
32
+ throw new ImageAttachmentError("image-data");
33
+ assertImageEncodedSize(image.data);
34
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(image.data) || image.data.length % 4 !== 0
35
+ || Buffer.from(image.data, "base64").toString("base64") !== image.data) {
36
+ throw new ImageAttachmentError("image-data");
37
+ }
38
+ if (typeof image.mimeType !== "string" || image.mimeType.length > 256 || !/^image\/[a-z0-9.+-]+$/i.test(image.mimeType)) {
39
+ throw new ImageAttachmentError("image-mime");
40
+ }
41
+ }
42
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./extension-ui.js";
2
2
  export * from "./model.js";
3
+ export * from "./image-attachments.js";
3
4
  export * from "./prompt-suggestions.js";
4
5
  export * from "./validation.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./extension-ui.js";
2
2
  export * from "./model.js";
3
+ export * from "./image-attachments.js";
3
4
  export * from "./prompt-suggestions.js";
4
5
  export * from "./validation.js";
@@ -1,12 +1,11 @@
1
1
  import { OWNED_UI_CONTRACT_VERSION, } from "./model.js";
2
+ import { assertPromptImages } from "./image-attachments.js";
2
3
  const MAX_ID_LENGTH = 128;
3
4
  const MAX_LABEL_LENGTH = 256;
4
5
  const MAX_MESSAGE_LENGTH = 4_096;
5
6
  const MAX_TEXT_BYTES = 256 * 1024;
6
7
  const MAX_PAYLOAD_BYTES = 64 * 1024;
7
8
  const MAX_SESSION_VIEW_BYTES = 1024 * 1024;
8
- const MAX_IMAGE_DATA_BYTES = 8 * 1024 * 1024;
9
- const MAX_PROMPT_IMAGES = 8;
10
9
  const MAX_BLOCKS = 10_000;
11
10
  const MAX_CUSTOMIZATIONS = 1_000;
12
11
  const MAX_DIAGNOSTICS = 1_000;
@@ -57,13 +56,7 @@ export function assertOwnedUiCommand(command) {
57
56
  case "follow-up":
58
57
  assertBoundedText(command.text, "owned-UI prompt text", MAX_TEXT_BYTES);
59
58
  if (command.images !== undefined) {
60
- assertCollection(command.images, "owned-UI prompt images", MAX_PROMPT_IMAGES);
61
- for (const image of command.images) {
62
- if (image.type !== "image")
63
- throw new TypeError("owned-UI prompt image type is invalid");
64
- assertBoundedText(image.data, "owned-UI prompt image data", MAX_IMAGE_DATA_BYTES);
65
- assertBoundedText(image.mimeType, "owned-UI prompt image MIME type", MAX_LABEL_LENGTH);
66
- }
59
+ assertPromptImages(command.images);
67
60
  }
68
61
  return;
69
62
  case "abort":
@@ -1,6 +1,9 @@
1
1
  import { markStartupPhase } from "../../foundation/startup/index.js";
2
+ import { boundedCleanup } from "../../foundation/terminal-cleanup/index.js";
2
3
  export async function runOwnedUi(options) {
3
4
  const { application, settings } = options;
5
+ let failed = false;
6
+ let originalFailure;
4
7
  try {
5
8
  if (settings)
6
9
  await settings.load();
@@ -11,7 +14,19 @@ export async function runOwnedUi(options) {
11
14
  await application.waitUntilStopped();
12
15
  return 0;
13
16
  }
17
+ catch (error) {
18
+ failed = true;
19
+ originalFailure = error;
20
+ throw error;
21
+ }
14
22
  finally {
15
- await application.dispose();
23
+ try {
24
+ await boundedCleanup(() => application.dispose(), 2500);
25
+ }
26
+ catch (error) {
27
+ if (failed)
28
+ throw new AggregateError([originalFailure, error], "Owned UI run and cleanup failed", { cause: originalFailure });
29
+ throw error;
30
+ }
16
31
  }
17
32
  }
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { restoreAfterOwnedExit } from "../terminal-cleanup/index.js";
2
3
  import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
4
  import { connect } from "node:net";
4
5
  import { platform } from "node:os";
@@ -234,7 +235,7 @@ async function launchUi(release, environment, sessionArgs) {
234
235
  windowsHide: false,
235
236
  });
236
237
  child.once("error", rejectPromise);
237
- child.once("close", (code, signal) => resolvePromise(code ?? (signal ? 1 : 0)));
238
+ child.once("close", (code, signal) => resolvePromise(restoreAfterOwnedExit(environment[PRODUCT_IDENTITY.environment.launchProfile] === "a1", code, signal)));
238
239
  });
239
240
  }
240
241
  export function releaseEnvironment(environment, release) {
@@ -0,0 +1,12 @@
1
+ export interface FatalExitOptions {
2
+ readonly directory: string;
3
+ readonly releaseId?: string;
4
+ readonly restore?: () => void;
5
+ readonly dispose?: () => Promise<unknown> | void;
6
+ readonly timeoutMs?: number;
7
+ }
8
+ export declare function writeFatalDiagnostic(directory: string, origin: string, error: unknown, releaseId?: string): Promise<string | null>;
9
+ export declare function installFatalExit(options: FatalExitOptions): {
10
+ fail(error: unknown): void;
11
+ remove(): void;
12
+ };
@@ -0,0 +1,85 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
3
+ import { writeSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { boundedCleanup, restoreProcessTerminal } from "./terminal-reset.js";
6
+ export async function writeFatalDiagnostic(directory, origin, error, releaseId) {
7
+ try {
8
+ // Security: only known classifications and code locations are retained. Error messages,
9
+ // function names, arbitrary paths, and request objects are never persisted.
10
+ const cleanupFailed = error instanceof AggregateError && error.cause instanceof Error;
11
+ const primary = cleanupFailed ? error.cause : error;
12
+ const classification = primary instanceof TypeError ? "TypeError" : primary instanceof RangeError ? "RangeError" : "Error";
13
+ const stack = primary instanceof Error && typeof primary.stack === "string" ? primary.stack.slice(0, 32768) : "";
14
+ const locations = stack.split("\n").slice(1).flatMap(line => {
15
+ const match = /\/(?:dist|src)\/([a-zA-Z0-9_./-]+\.[cm]?[jt]s):(\d+):(\d+)\)?$/.exec(line.replaceAll("\\", "/"));
16
+ return match ? [{ file: match[1].slice(0, 160), line: Number(match[2]), column: Number(match[3]) }] : [];
17
+ }).slice(0, 16);
18
+ const record = {
19
+ timestamp: new Date().toISOString(),
20
+ origin: ["uncaughtException", "unhandledRejection", "entry"].includes(origin) ? origin : "entry",
21
+ classification, cleanupFailed, locations, node: process.version, platform: process.platform,
22
+ releaseId: releaseId && /^[a-zA-Z0-9._-]{1,128}$/.test(releaseId) ? releaseId : "unknown",
23
+ };
24
+ await mkdir(directory, { recursive: true, mode: 0o700 });
25
+ const name = `fatal-${Date.now()}-${randomUUID()}.json`;
26
+ const path = join(directory, name);
27
+ const text = JSON.stringify(record, null, 2);
28
+ if (Buffer.byteLength(text) > 16 * 1024)
29
+ return null;
30
+ await writeFile(path, text, { flag: "wx", mode: 0o600 });
31
+ const names = (await readdir(directory)).filter(file => /^fatal-\d+-[a-f0-9-]+\.json$/.test(file)).sort().reverse();
32
+ await Promise.all(names.slice(10).map(file => unlink(join(directory, file)).catch(() => undefined)));
33
+ return path;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export function installFatalExit(options) {
40
+ let failing = false;
41
+ const restore = () => { try {
42
+ (options.restore ?? restoreProcessTerminal)();
43
+ }
44
+ catch { } };
45
+ const terminate = (origin, error) => {
46
+ if (failing)
47
+ return;
48
+ failing = true;
49
+ process.exitCode = 1;
50
+ // Security: stop accepting input immediately; fatal errors never resume normal work.
51
+ try {
52
+ process.stdin.pause();
53
+ process.stdin.removeAllListeners("data");
54
+ }
55
+ catch { }
56
+ restore();
57
+ let record = null;
58
+ let finished = false;
59
+ const finish = () => {
60
+ if (finished)
61
+ return;
62
+ finished = true;
63
+ restore();
64
+ try {
65
+ const location = record?.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 512);
66
+ writeSync(2, `A1 stopped after an unexpected error.${location ? ` Diagnostic: ${location}` : " Diagnostic storage unavailable."}\n`);
67
+ }
68
+ catch { }
69
+ process.exit(1);
70
+ };
71
+ const deadline = setTimeout(finish, options.timeoutMs ?? 1000);
72
+ void Promise.all([
73
+ writeFatalDiagnostic(options.directory, origin, error, options.releaseId).then(path => { record = path; }),
74
+ boundedCleanup(() => options.dispose?.(), options.timeoutMs ?? 1000).catch(() => undefined),
75
+ ]).then(() => { clearTimeout(deadline); finish(); }, finish);
76
+ };
77
+ const exception = (error) => terminate("uncaughtException", error);
78
+ const rejection = (error) => terminate("unhandledRejection", error);
79
+ process.on("uncaughtException", exception);
80
+ process.on("unhandledRejection", rejection);
81
+ return {
82
+ fail: error => terminate("entry", error),
83
+ remove() { process.off("uncaughtException", exception); process.off("unhandledRejection", rejection); },
84
+ };
85
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./terminal-reset.js";
2
+ export * from "./fatal-exit.js";
@@ -0,0 +1,2 @@
1
+ export * from "./terminal-reset.js";
2
+ export * from "./fatal-exit.js";
@@ -0,0 +1,4 @@
1
+ export declare const EMERGENCY_TERMINAL_RESET: string;
2
+ export declare function restoreProcessTerminal(): void;
3
+ export declare function restoreAfterOwnedExit(ownsTerminal: boolean, code: number | null, signal: string | null, restore?: () => void): number;
4
+ export declare function boundedCleanup(cleanup: () => Promise<unknown> | void, timeoutMs?: number): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import { writeSync } from "node:fs";
2
+ // Protocol: reset modes on both screens; setting keyboard flags to zero is idempotent,
3
+ // unlike repeatedly popping a keyboard-protocol stack owned by a parent application.
4
+ export const EMERGENCY_TERMINAL_RESET = "\x1b[?2026l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1004l\x1b[?1006l"
5
+ + "\x1b[?2004l\x1b[=0u\x1b[>4;0m\x1b]8;;\x1b\\\x1b[0m\x1b[r\x1b[?7h\x1b[?25h"
6
+ + "\x1b[?1049l\x1b[r\x1b[?7h\x1b[?25h\x1b]9;4;0\x07";
7
+ export function restoreProcessTerminal() {
8
+ try {
9
+ if (process.stdout.isTTY)
10
+ writeSync(process.stdout.fd, EMERGENCY_TERMINAL_RESET);
11
+ }
12
+ catch { }
13
+ try {
14
+ if (process.stdin.isTTY)
15
+ process.stdin.setRawMode(false);
16
+ }
17
+ catch { }
18
+ }
19
+ export function restoreAfterOwnedExit(ownsTerminal, code, signal, restore = restoreProcessTerminal) {
20
+ const outcome = code ?? (signal ? 1 : 0);
21
+ if (ownsTerminal && outcome !== 0) {
22
+ try {
23
+ restore();
24
+ }
25
+ catch { }
26
+ }
27
+ return outcome;
28
+ }
29
+ export async function boundedCleanup(cleanup, timeoutMs = 1000) {
30
+ let timer;
31
+ try {
32
+ await Promise.race([
33
+ Promise.resolve().then(cleanup),
34
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Cleanup deadline exceeded")), timeoutMs); }),
35
+ ]);
36
+ }
37
+ finally {
38
+ clearTimeout(timer);
39
+ }
40
+ }
@@ -1,3 +1,4 @@
1
+ import { assertImageEncodedSize } from "../../../contracts/owned-ui/index.js";
1
2
  const STANDARD_BASE64 = /^[A-Za-z0-9+/]*={0,2}$/u;
2
3
  /**
3
4
  * Converts valid padded or unpadded standard base64 into its unique padded form.
@@ -9,7 +10,10 @@ export function canonicalizeClipboardImage(image) {
9
10
  return data === null ? null : { data, mimeType: image.mimeType };
10
11
  }
11
12
  export function canonicalizeStandardBase64(value) {
12
- if (value.length === 0 || !STANDARD_BASE64.test(value))
13
+ if (typeof value !== "string" || value.length === 0)
14
+ return null;
15
+ assertImageEncodedSize(value);
16
+ if (!STANDARD_BASE64.test(value))
13
17
  return null;
14
18
  const paddingStart = value.indexOf("=");
15
19
  const core = paddingStart < 0 ? value : value.slice(0, paddingStart);
@@ -11,7 +11,7 @@ export interface PreparedPrompt {
11
11
  /** Owns semantic clipboard records while the prompt displays compact chips. */
12
12
  export declare class PromptChipStore {
13
13
  #private;
14
- transformPastedContent(content: PiShellClipboardContent): string;
14
+ transformPastedContent(content: PiShellClipboardContent, currentText?: string): string;
15
15
  atomicRanges(line: string): readonly PiShellEditorTextRange[];
16
16
  hyperlinkRanges(text: string): readonly {
17
17
  start: number;
@@ -3,6 +3,7 @@ import { existsSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { canonicalizeClipboardImage } from "./clipboard-image.js";
6
+ import { assertImageEncodedSize, assertPromptImages } from "../../../contracts/owned-ui/index.js";
6
7
  const CHIP_PATTERN = /\[(?:📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
7
8
  const IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|bmp|tiff?)$/iu;
8
9
  const URL_PATTERN = /^https?:\/\/[^\s\u0000-\u001f\u007f]+$/iu;
@@ -10,11 +11,13 @@ const URL_DISPLAY_LENGTH = 40;
10
11
  /** Owns semantic clipboard records while the prompt displays compact chips. */
11
12
  export class PromptChipStore {
12
13
  #chips = new Map();
13
- transformPastedContent(content) {
14
+ transformPastedContent(content, currentText = "") {
14
15
  if (content.kind === "image") {
16
+ assertImageEncodedSize(content.data);
15
17
  const image = canonicalizeClipboardImage(content);
16
18
  if (image === null)
17
19
  return "";
20
+ assertPromptImages([...this.prepareSubmission(currentText).images, { type: "image", ...image }]);
18
21
  const id = randomBytes(5).toString("hex");
19
22
  const tag = `[📷 screenshot-${id}.png]`;
20
23
  this.#chips.set(tag, {
@@ -68,6 +68,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
68
68
  readonly requestHyperlinkCleanup?: () => void;
69
69
  readonly enableDockInputReuse?: boolean;
70
70
  readonly onSubmit: (text: string) => void;
71
+ readonly onPasteRejected?: (error: unknown) => void;
71
72
  readonly onInterrupt: () => void;
72
73
  readonly onClear?: () => void;
73
74
  readonly onExit: () => void;
@@ -85,7 +85,15 @@ export class OwnedUiSessionShellRoot {
85
85
  ...handlers,
86
86
  keybindingProfile: this.#customViewport ? "a1" : "pi",
87
87
  paintEditorSelection: (line, from, to, atomic) => backgroundSgrSpan(line, from, to, atomic ? "\u001b[7m" : "\u001b[27m\u001b[48;2;38;79;120m", atomic ? "\u001b[27m" : "\u001b[49m", piShellVisibleWidth),
88
- transformPastedContent: content => this.#promptChips.transformPastedContent(content),
88
+ transformPastedContent: content => {
89
+ try {
90
+ return this.#promptChips.transformPastedContent(content, this.editor.getText());
91
+ }
92
+ catch (error) {
93
+ handlers.onPasteRejected?.(error);
94
+ return "";
95
+ }
96
+ },
89
97
  editorAtomicRanges: line => this.#promptChips.atomicRanges(line),
90
98
  decorateEditorRow: (row, width) => {
91
99
  const plain = stripAnsi(row);
@@ -1,4 +1,6 @@
1
1
  import { PRODUCT_TEXT } from "../../../product-identity.js";
2
+ import { boundedCleanup } from "../../../foundation/terminal-cleanup/index.js";
3
+ import { assertOwnedUiCommand, assertPromptImages, ImageAttachmentError } from "../../../contracts/owned-ui/index.js";
2
4
  import { ContextualPromptSuggestionController } from "./prompt-suggestion-controller.js";
3
5
  import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput, readVisibleHyperlinks } from "../../../ui/components/index.js";
4
6
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
@@ -27,6 +29,7 @@ export class OwnedUiSessionShell {
27
29
  #routeHost;
28
30
  #dialogHandle;
29
31
  #sequence = 0;
32
+ #editorRevision = 0;
30
33
  #started = false;
31
34
  #disposed = false;
32
35
  #pointerReporting = false;
@@ -76,7 +79,8 @@ export class OwnedUiSessionShell {
76
79
  replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
77
80
  }),
78
81
  enableDockInputReuse: options.inputPresentation?.viewportReuse !== false,
79
- onSubmit: text => { void this.submit(text); },
82
+ onSubmit: text => { void this.submit(text).catch(() => this.#reportSubmissionError()); },
83
+ onPasteRejected: error => this.#reportSubmissionError(error),
80
84
  onInterrupt: () => { void this.interrupt(); },
81
85
  onClear: () => { void this.clearOrExit(); },
82
86
  onExit: () => { void this.shutdown(); },
@@ -88,11 +92,14 @@ export class OwnedUiSessionShell {
88
92
  this.runtime.requestRender();
89
93
  },
90
94
  onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
91
- onFollowUp: () => { void this.queueFollowUp(); },
95
+ onFollowUp: () => { void this.queueFollowUp().catch(() => this.#reportSubmissionError()); },
92
96
  onDequeue: () => this.restoreQueuedInput(),
93
- onEditorChange: () => promptSuggestionController?.invalidate(),
97
+ onEditorChange: () => { this.#editorRevision++; promptSuggestionController?.invalidate(); },
94
98
  onPromptSuggestionAccepted: () => promptSuggestionController?.accept(),
95
- onInputSurfaceChanged: () => promptSuggestionController?.invalidate(),
99
+ onInputSurfaceChanged: () => {
100
+ this.root.clearViewportPointerState();
101
+ promptSuggestionController?.invalidate();
102
+ },
96
103
  onCopyText: text => {
97
104
  runtime?.writeControl(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
98
105
  const write = options.clipboard === undefined
@@ -103,7 +110,11 @@ export class OwnedUiSessionShell {
103
110
  readClipboardContent: async () => {
104
111
  await pendingClipboardWrite;
105
112
  if (options.clipboard === undefined)
106
- return readSystemClipboardContent();
113
+ return readSystemClipboardContent().catch(error => {
114
+ if (error instanceof ImageAttachmentError)
115
+ this.#reportSubmissionError(error);
116
+ return null;
117
+ });
107
118
  try {
108
119
  const image = await options.clipboard.readImage?.();
109
120
  if (image !== null && image !== undefined) {
@@ -112,7 +123,11 @@ export class OwnedUiSessionShell {
112
123
  return { kind: "image", ...canonical };
113
124
  }
114
125
  }
115
- catch {
126
+ catch (error) {
127
+ if (error instanceof ImageAttachmentError) {
128
+ this.#reportSubmissionError(error);
129
+ return null;
130
+ }
116
131
  // Compatibility: treat an unavailable or malformed image as text-capable clipboard input.
117
132
  }
118
133
  const text = await options.clipboard.readText();
@@ -226,8 +241,11 @@ export class OwnedUiSessionShell {
226
241
  // surface. Letting the transcript pre-router inspect those reports
227
242
  // steals settings value menus and numeric +/- controls before the
228
243
  // settings app can receive them.
229
- if (this.runtime.hasOverlay() || !this.root.usesDefaultInputSurface())
244
+ if (this.runtime.hasOverlay() || !this.root.usesDefaultInputSurface()) {
245
+ if (data.includes("\u001b[<"))
246
+ this.root.clearViewportPointerState();
230
247
  return undefined;
248
+ }
231
249
  const routed = this.root.handleViewportPreInput(data, true);
232
250
  if (routed.copyText !== undefined) {
233
251
  this.runtime.writeControl(`\u001b]52;c;${Buffer.from(routed.copyText, "utf8").toString("base64")}\u0007`);
@@ -421,6 +439,9 @@ export class OwnedUiSessionShell {
421
439
  return this.#stopped;
422
440
  }
423
441
  async submit(text) {
442
+ return this.#guardSubmission(text, () => this.#submit(text));
443
+ }
444
+ async #submit(text) {
424
445
  this.#promptSuggestions?.invalidate();
425
446
  const displayInput = text.trim();
426
447
  if (!displayInput)
@@ -428,6 +449,7 @@ export class OwnedUiSessionShell {
428
449
  if (displayInput.startsWith("/"))
429
450
  return this.#slashCommand(displayInput);
430
451
  const prepared = this.root.preparePromptSubmission(displayInput);
452
+ assertPromptImages(prepared.images);
431
453
  const input = prepared.text.trim();
432
454
  if (input.startsWith("!")) {
433
455
  const excludeFromContext = input.startsWith("!!");
@@ -458,6 +480,7 @@ export class OwnedUiSessionShell {
458
480
  this.root.editor.addToHistory(displayInput);
459
481
  this.#compactionQueue.push({
460
482
  text: input,
483
+ draft: displayInput,
461
484
  type: "steer",
462
485
  ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
463
486
  });
@@ -474,7 +497,7 @@ export class OwnedUiSessionShell {
474
497
  sessionId: this.backend.sessionId,
475
498
  text: input,
476
499
  ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
477
- });
500
+ }, displayInput);
478
501
  }
479
502
  async clearOrExit(now = Date.now()) {
480
503
  this.#promptSuggestions?.invalidate();
@@ -565,10 +588,15 @@ export class OwnedUiSessionShell {
565
588
  return workflowAdapterResult(result);
566
589
  }
567
590
  async queueFollowUp() {
568
- const displayInput = this.root.editor.getText().trim();
591
+ const draft = this.root.editor.getText();
592
+ return this.#guardSubmission(draft, () => this.#queueFollowUp(draft));
593
+ }
594
+ async #queueFollowUp(draft) {
595
+ const displayInput = draft.trim();
569
596
  if (!displayInput)
570
597
  return rejected("nothing to queue");
571
598
  const prepared = this.root.preparePromptSubmission(displayInput);
599
+ assertPromptImages(prepared.images);
572
600
  const text = prepared.text.trim();
573
601
  this.root.editor.addToHistory(displayInput);
574
602
  this.root.editor.setText("");
@@ -576,6 +604,7 @@ export class OwnedUiSessionShell {
576
604
  if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
577
605
  this.#compactionQueue.push({
578
606
  text,
607
+ draft: displayInput,
579
608
  type: "follow-up",
580
609
  ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
581
610
  });
@@ -587,10 +616,10 @@ export class OwnedUiSessionShell {
587
616
  sessionId: this.backend.sessionId,
588
617
  text,
589
618
  ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
590
- });
619
+ }, displayInput);
591
620
  }
592
621
  restoreQueuedInput() {
593
- const queued = [...this.backend.clearQueuedWorkflows(), ...this.#compactionQueue.map(item => item.text)];
622
+ const queued = [...this.backend.clearQueuedWorkflows(), ...this.#compactionQueue.map(item => item.draft)];
594
623
  this.#compactionQueue = [];
595
624
  if (queued.length === 0)
596
625
  return;
@@ -1082,34 +1111,40 @@ export class OwnedUiSessionShell {
1082
1111
  if (this.#disposed)
1083
1112
  return;
1084
1113
  this.#disposed = true;
1085
- this.root.clearViewportPointerState();
1086
- this.#setPointerReporting(false, true);
1087
- this.#removeViewportPreInput();
1088
- this.#streamPresentation.dispose();
1089
- this.#promptSuggestions?.dispose();
1090
- this.#unsubscribePromptSuggestions();
1091
- this.#unsubscribeSettings();
1092
- const exitMode = this.backend.disposed
1093
- ? this.#fullscreenExitOutput
1094
- : this.backend.pinnedSettingsSnapshot().fullscreenExitOutput;
1095
- const exitTranscript = this.root.exitTranscript(this.runtime.viewport().columns);
1096
- const resume = this.backend.currentSessionResumeMetadata();
1097
- const resumeHint = resume === null
1098
- ? ""
1099
- : `${dim("To resume this session:")} ${formatSessionResumeCommand(resume)}`;
1100
- const fullscreenExitText = this.runtime.mode !== "fullscreen"
1101
- ? ""
1102
- : exitMode === "resume-hint"
1103
- ? resumeHint
1104
- : [exitTranscript, resumeHint].filter(Boolean).join("\n\n");
1105
- this.#unbindPiSettings();
1106
- this.#unbindTerminalSettings();
1107
- this.#unbindShutdownSettings();
1108
- this.#unsubscribe();
1109
- this.#dialogHandle?.hide();
1110
- await this.backend.unbindExtensionUi();
1111
- this.#extensionBridge.dispose();
1112
- await this.runtime.dispose();
1114
+ const failures = [];
1115
+ const attempt = (action) => { try {
1116
+ action();
1117
+ }
1118
+ catch (error) {
1119
+ failures.push(error);
1120
+ } };
1121
+ attempt(() => this.root.clearViewportPointerState());
1122
+ attempt(() => this.#setPointerReporting(false, true));
1123
+ attempt(() => this.#removeViewportPreInput());
1124
+ attempt(() => this.#streamPresentation.dispose());
1125
+ attempt(() => this.#promptSuggestions?.dispose());
1126
+ attempt(() => this.#unsubscribePromptSuggestions());
1127
+ attempt(() => this.#unsubscribeSettings());
1128
+ let fullscreenExitText = "";
1129
+ attempt(() => {
1130
+ const exitMode = this.backend.disposed ? this.#fullscreenExitOutput : this.backend.pinnedSettingsSnapshot().fullscreenExitOutput;
1131
+ const exitTranscript = this.root.exitTranscript(this.runtime.viewport().columns);
1132
+ const resume = this.backend.currentSessionResumeMetadata();
1133
+ const resumeHint = resume === null ? "" : `${dim("To resume this session:")} ${formatSessionResumeCommand(resume)}`;
1134
+ fullscreenExitText = this.runtime.mode !== "fullscreen" ? ""
1135
+ : exitMode === "resume-hint" ? resumeHint : [exitTranscript, resumeHint].filter(Boolean).join("\n\n");
1136
+ });
1137
+ attempt(() => this.#unbindPiSettings());
1138
+ attempt(() => this.#unbindTerminalSettings());
1139
+ attempt(() => this.#unbindShutdownSettings());
1140
+ attempt(() => this.#unsubscribe());
1141
+ attempt(() => this.#dialogHandle?.hide());
1142
+ attempt(() => this.#extensionBridge.dispose());
1143
+ // Invariant: terminal restoration precedes any potentially stalled backend teardown.
1144
+ await this.runtime.dispose().catch(error => failures.push(error));
1145
+ await boundedCleanup(() => this.backend.unbindExtensionUi()).catch(error => failures.push(error));
1146
+ if (failures.length > 0)
1147
+ throw new AggregateError(failures, "Owned UI disposal failed");
1113
1148
  if (fullscreenExitText.length > 0)
1114
1149
  this.runtime.writeAfterStop(`${fullscreenExitText}\n`);
1115
1150
  }
@@ -1423,11 +1458,58 @@ export class OwnedUiSessionShell {
1423
1458
  sessionId: this.backend.sessionId,
1424
1459
  text: item.text,
1425
1460
  ...(item.images === undefined ? {} : { images: item.images }),
1426
- });
1461
+ }, item.draft);
1427
1462
  }
1428
1463
  }
1429
- async #execute(command) {
1430
- return this.backend.execute(command);
1464
+ async #execute(command, draft) {
1465
+ if (draft === undefined)
1466
+ return this.backend.execute(command);
1467
+ const revision = this.#editorRevision;
1468
+ try {
1469
+ assertOwnedUiCommand(command);
1470
+ }
1471
+ catch (error) {
1472
+ return this.#recoverSubmission(draft, revision, error);
1473
+ }
1474
+ try {
1475
+ const result = await this.backend.execute(command);
1476
+ if (result.outcome === "rejected")
1477
+ return this.#recoverSubmission(draft, revision);
1478
+ return result;
1479
+ }
1480
+ catch {
1481
+ // Security: dispatch might already have reached the provider. Never retry automatically.
1482
+ this.root.editor.addToHistory(draft);
1483
+ this.#reportSubmissionError(undefined, "Submission failed; delivery is uncertain. Check the conversation before retrying. Press Up to recover the draft.");
1484
+ return { outcome: "failed", diagnostic: "submission delivery is uncertain" };
1485
+ }
1486
+ }
1487
+ async #guardSubmission(draft, action) {
1488
+ const revision = this.#editorRevision;
1489
+ try {
1490
+ return await action();
1491
+ }
1492
+ catch (error) {
1493
+ return this.#recoverSubmission(draft, revision, error);
1494
+ }
1495
+ }
1496
+ #recoverSubmission(draft, revision, error) {
1497
+ this.root.editor.addToHistory(draft);
1498
+ // Concurrency: never overwrite input typed (even typed and cleared) after this submission.
1499
+ if (revision === this.#editorRevision && this.root.editor.getText().length === 0)
1500
+ this.root.editor.setText(draft);
1501
+ const message = error instanceof ImageAttachmentError ? error.message : "Submission rejected. Check the prompt and attachments.";
1502
+ this.#reportSubmissionError(error, `${message} Press Up to recover the draft.`);
1503
+ return rejected(message);
1504
+ }
1505
+ #reportSubmissionError(error, message) {
1506
+ // Security: arbitrary provider/extension error messages can contain the entire request.
1507
+ try {
1508
+ this.root.appendWorkflowResult({ command: "debug", outcome: "failed", message: message
1509
+ ?? (error instanceof ImageAttachmentError ? error.message : "Submission failed. Check the prompt and try again.") });
1510
+ this.runtime.requestRender();
1511
+ }
1512
+ catch { /* Security: error presentation cannot create another rejected submission callback. */ }
1431
1513
  }
1432
1514
  #simple(type) {
1433
1515
  return { type, correlationId: this.#correlation(type), sessionId: this.backend.sessionId };
@@ -159,8 +159,16 @@ export class SessionViewportController {
159
159
  this.#requestHyperlinkCleanup();
160
160
  this.#requestRender(true);
161
161
  }
162
- if (this.#viewport.frame === null)
163
- return { data, consumed: false };
162
+ if (this.#viewport.frame === null) {
163
+ // Invariant: a not-yet-painted A1 screen is not Pi's selection surface.
164
+ return routeMouseInput(data, event => {
165
+ if (event.kind === "press" && event.button === 0)
166
+ this.#tailPointerSuppressed = true;
167
+ if (event.kind === "release")
168
+ this.#tailPointerSuppressed = false;
169
+ return event.kind === "motion" || event.kind === "release" || (event.kind === "press" && event.button === 0);
170
+ });
171
+ }
164
172
  if (data === "\u0003") {
165
173
  if (this.#editor.hasSelection()) {
166
174
  if (this.#viewport.clearSelection())
@@ -287,6 +295,8 @@ export class SessionViewportController {
287
295
  }
288
296
  if (event.button !== 0)
289
297
  return false;
298
+ this.#dockPointerSuppressed = false;
299
+ this.#tailPointerSuppressed = false;
290
300
  this.#stopSelectionAutoScroll();
291
301
  if (this.#viewport.clearSelection())
292
302
  repaint = true;
@@ -335,16 +345,16 @@ export class SessionViewportController {
335
345
  }
336
346
  if (event.row >= 1 && event.row <= hits.viewportHeight && event.column <= frame.contentWidth) {
337
347
  if (!this.#viewport.pressSelection(event.column, event.row, now)) {
338
- // Invariant: a full drag begun on transient tail rows cannot become transcript selection.
339
- if (hits.transientTail.includes(event.row))
340
- this.#tailPointerSuppressed = true;
348
+ // Invariant: empty rows and transient tail rows must never start Pi selection.
349
+ this.#tailPointerSuppressed = true;
341
350
  repaint = true;
342
- return this.#tailPointerSuppressed;
351
+ return true;
343
352
  }
344
353
  repaint = true;
345
354
  return true;
346
355
  }
347
- return false;
356
+ this.#tailPointerSuppressed = true;
357
+ return true;
348
358
  }
349
359
  if (event.kind === "release") {
350
360
  if (this.#editor.ownsPointer() && this.#editorPointerFrame !== undefined) {
@@ -1,5 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { canonicalizeClipboardImage } from "./clipboard-image.js";
3
+ import { ImageAttachmentError, MAX_IMAGE_DATA_BYTES } from "../../../contracts/owned-ui/index.js";
3
4
  const MAX_CLIPBOARD_BYTES = 16 * 1024 * 1024;
4
5
  let nativeClipboardPromise;
5
6
  let pendingClipboardWrite = Promise.resolve();
@@ -27,7 +28,9 @@ export async function readSystemClipboardContent() {
27
28
  if (image !== null)
28
29
  return { kind: "image", ...image };
29
30
  }
30
- catch {
31
+ catch (error) {
32
+ if (error instanceof ImageAttachmentError)
33
+ throw error;
31
34
  // Compatibility: fall through to text and platform readers.
32
35
  }
33
36
  }
@@ -40,6 +43,8 @@ export async function readSystemClipboardImage(reader) {
40
43
  if (reader.getImageBinary !== undefined) {
41
44
  try {
42
45
  const bytes = await reader.getImageBinary();
46
+ if (Math.ceil(bytes.length / 3) * 4 > MAX_IMAGE_DATA_BYTES)
47
+ throw new ImageAttachmentError("image-size");
43
48
  if (bytes.length > 0 && bytes.every(byte => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
44
49
  return canonicalizeClipboardImage({
45
50
  data: Buffer.from(bytes).toString("base64"),
@@ -47,7 +52,9 @@ export async function readSystemClipboardImage(reader) {
47
52
  });
48
53
  }
49
54
  }
50
- catch {
55
+ catch (error) {
56
+ if (error instanceof ImageAttachmentError)
57
+ throw error;
51
58
  // Compatibility: older or partially available native bindings may still expose base64.
52
59
  }
53
60
  }
@@ -58,7 +65,9 @@ export async function readSystemClipboardImage(reader) {
58
65
  mimeType: "image/png",
59
66
  });
60
67
  }
61
- catch {
68
+ catch (error) {
69
+ if (error instanceof ImageAttachmentError)
70
+ throw error;
62
71
  // Compatibility: fall through to the caller's text path.
63
72
  }
64
73
  }
@@ -1,4 +1,5 @@
1
1
  import { HStack, ProcessTerminal, ScrollView, TuiAltScreen, TuiMainScreen, VStack, visibleWidth, } from "#pi-tui";
2
+ import { boundedCleanup, EMERGENCY_TERMINAL_RESET } from "../../../foundation/terminal-cleanup/index.js";
2
3
  import { InputPresentationCoordinator, } from "./input-presentation-coordinator.js";
3
4
  /** Identifies the Pi TUI lifecycle stage that failed while preserving the original cause. */
4
5
  export class PiTuiRuntimeError extends Error {
@@ -402,11 +403,16 @@ export class PiTuiRuntimeAdapter {
402
403
  }
403
404
  async #stop(options) {
404
405
  this.#state = "stopping";
405
- this.#inputCoordinator?.flush();
406
406
  let failure;
407
+ try {
408
+ this.#inputCoordinator?.dispose(false);
409
+ }
410
+ catch (error) {
411
+ failure = new PiTuiRuntimeError("restoration", error);
412
+ }
407
413
  if (options.drainInput !== false) {
408
414
  try {
409
- await this.#terminal.drainInput(options.drainMaxMs, options.drainIdleMs);
415
+ await boundedCleanup(() => this.#terminal.drainInput(options.drainMaxMs, options.drainIdleMs));
410
416
  }
411
417
  catch (error) {
412
418
  failure = new PiTuiRuntimeError("input-drain", error);
@@ -559,6 +565,11 @@ export class PiTuiRuntimeAdapter {
559
565
  }
560
566
  #bestEffortTerminalRestore() {
561
567
  this.#clearTerminalProgress();
568
+ try {
569
+ if (this.mode === "fullscreen")
570
+ this.#terminal.write(EMERGENCY_TERMINAL_RESET);
571
+ }
572
+ catch { }
562
573
  try {
563
574
  this.#terminal.showCursor();
564
575
  }
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-07T08:48:09.287Z",
8
+ "builtAt": "2026-09-09T13:32:45.025Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "dc03605e5780e2aeebb4ecafd62868dc673e22ddd38b024a369aff704ff136dc",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-07T08:48:08.596Z",
8
+ "builtAt": "2026-09-09T13:32:36.719Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "ee8a00eaaf79c707459bbbfb52518e9739314967049fbe5fa625f36ce33db9ee",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-07T08:48:53.798Z",
8
+ "builtAt": "2026-09-09T13:34:58.223Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "ec965cf23ffb3dd4f8db9b265c620c3bbc078418a6f7e330bdcbf4d0055f6393",
11
+ "sha256": "ed500da6662711b12a26dd40d4b2d659a201a0d9e43fd8d521d8a16e04889f4b",
12
12
  "size": 177664
13
13
  },
14
14
  "provenance": {
@@ -31,7 +31,8 @@ The owned Pi-backed surface is not an arbitrary-CLI terminal multiplexer. A feat
31
31
  - `storage`: SQLite migrations, prior-boot reconciliation, and plural launch-instance persistence. Legacy foreground rows are historical migration input and never authorize current ownership.
32
32
  - `supervisor`: endpoint identity, plural cohort ownership, per-instance reconciliation, and aggregate release shutdown coordination. It owns no terminal surface.
33
33
  - `pi-engine-adapter`, `pi-component-adapter`, `pi-tui-runtime-adapter`, and `pi-session-ui-integration`: isolate pinned Pi engine and presentation knowledge behind neutral contracts; product features do not import them directly. The session UI render root assembles semantic document and dock rows, while its focused viewport controller owns follow state, pointer routing, selection, and interaction timers. Owned-app route lifecycle belongs to the neutral `ui-apps` owner and is only hosted by the Pi session UI.
34
- - release/update/bootstrap: package-derived immutable release identity, process-guardian integrity, cohort selection, durable update transactions, rollback, and dependency-light command entry.
34
+ - release/update/bootstrap: package-derived immutable release identity, process-guardian integrity, cohort selection, durable update transactions, rollback, and dependency-light command entry. After an unsuccessful bare-A1 child exit, the bootstrap restores owned terminal modes; neither guardian emits terminal controls.
35
+ - `terminal-cleanup`: bounded, idempotent emergency restoration and fatal-exit diagnostics for an explicitly owned terminal. It does not inspect commands, relay terminal input/output, or emulate terminal state. The owned UI installs its fatal boundary before terminal activation; packaged bootstrap and checkout launch provide the surviving-owner fallback. Fatal records retain at most ten 16 KiB records with trusted classifications and code locations, never prompts, attachments, raw input, credentials, or arbitrary exception messages.
35
36
 
36
37
  ## Dependency direction
37
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.1.8-dev.279",
3
+ "version": "0.1.8-dev.287",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.13.0",