@timurproko/a1 0.1.8-dev.322 → 0.1.8-dev.332

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 (52) hide show
  1. package/dist/cli/dispatch.js +1 -1
  2. package/dist/features/owned-ui/project-trust-prompt.js +1 -1
  3. package/dist/features/owned-ui/settings-app.js +1 -1
  4. package/dist/features/prompt-history/service.d.ts +31 -1
  5. package/dist/features/prompt-history/service.js +294 -129
  6. package/dist/features/prompt-history/store.d.ts +2 -1
  7. package/dist/features/prompt-history/store.js +17 -4
  8. package/dist/features/prompt-history/worker.js +6 -3
  9. package/dist/foundation/launch-context/index.d.ts +36 -6
  10. package/dist/foundation/launch-context/index.js +64 -11
  11. package/dist/foundation/launch-guardian/main.js +2 -1
  12. package/dist/foundation/release/bootstrap.d.ts +5 -0
  13. package/dist/foundation/release/bootstrap.js +49 -9
  14. package/dist/foundation/release/cohort-state.js +0 -3
  15. package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
  16. package/dist/foundation/release/dependency-certification-retention.js +88 -0
  17. package/dist/foundation/release/dependency-certification.d.ts +24 -0
  18. package/dist/foundation/release/dependency-certification.js +208 -0
  19. package/dist/foundation/release/dependency-layer.d.ts +3 -4
  20. package/dist/foundation/release/dependency-layer.js +6 -37
  21. package/dist/foundation/release/release-gc.js +72 -44
  22. package/dist/foundation/release/release-store.d.ts +2 -0
  23. package/dist/foundation/release/release-store.js +4 -3
  24. package/dist/foundation/release/restart-certification.js +16 -5
  25. package/dist/foundation/release/update.js +1 -6
  26. package/dist/foundation/release/warmup.js +16 -5
  27. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  28. package/dist/integrations/pi/components/owned-editor-ux.js +51 -27
  29. package/dist/integrations/pi/engine/adapter.d.ts +20 -0
  30. package/dist/integrations/pi/engine/adapter.js +223 -87
  31. package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
  32. package/dist/integrations/pi/engine/pending-delivery.js +149 -0
  33. package/dist/integrations/pi/session-ui/prompt-chips.js +38 -25
  34. package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
  35. package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
  36. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +1 -1
  37. package/dist/integrations/pi/session-ui/session-shell-root.js +5 -1
  38. package/dist/integrations/pi/session-ui/session-shell.js +9 -6
  39. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
  40. package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -5
  41. package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
  42. package/dist/integrations/pi/session-ui/text-paste.js +16 -0
  43. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +2 -2
  44. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +82 -39
  45. package/dist/native/darwin-arm64/manifest.json +1 -1
  46. package/dist/native/linux-x64/manifest.json +1 -1
  47. package/dist/native/win32-x64/manifest.json +2 -2
  48. package/dist/native/win32-x64/process-guardian.exe +0 -0
  49. package/dist/product-identity.json +1 -1
  50. package/docs/architecture/internal-naming.md +5 -3
  51. package/docs/manual-code-streaming-cleanup.md +88 -0
  52. package/package.json +1 -1
@@ -5,13 +5,16 @@ import { fileURLToPath } from "node:url";
5
5
  import { canonicalizeClipboardImage } from "./clipboard-image.js";
6
6
  import { assertImageEncodedSize, assertPromptImages, ImageAttachmentError } from "../../../contracts/owned-ui/index.js";
7
7
  import { ImagePreparationClient } from "./image-preparation-client.js";
8
- const CHIP_PATTERN = /\[(?:📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
8
+ import { prepareTextPaste } from "./text-paste.js";
9
+ const CHIP_PATTERN = /\[(?:paste #\d+ (?:\+\d+ lines|\d+ chars)|📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
9
10
  const IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|bmp|tiff?)$/iu;
10
11
  const URL_PATTERN = /^https?:\/\/[^\s\u0000-\u001f\u007f]+$/iu;
11
12
  const URL_DISPLAY_LENGTH = 40;
12
13
  /** Owns semantic chips and bounded pending paste references; cancels background work on reset or disposal. */
13
14
  export class PromptChipStore {
14
15
  #chips = new Map();
16
+ // Invariant: clearing the editor never recycles a recoverable text chip's identity.
17
+ #textCounter = 0;
15
18
  #pending = new Map();
16
19
  #preparation = new ImagePreparationClient();
17
20
  #stopping = new Set();
@@ -98,7 +101,11 @@ export class PromptChipStore {
98
101
  entry.job.cancel();
99
102
  return remaining;
100
103
  }
101
- async dispose() { await Promise.all([this.#preparation.dispose(), ...this.#stopping]); }
104
+ async dispose() {
105
+ await Promise.all([this.#preparation.dispose(), ...this.#stopping]);
106
+ this.#chips.clear();
107
+ this.#pending.clear();
108
+ }
102
109
  #imageCount(text) {
103
110
  const tags = new Set([...this.#chips.values()].filter(chip => chip.kind === "image" && text.includes(chip.tag)).map(chip => chip.tag));
104
111
  for (const entry of this.#pending.values()) {
@@ -141,8 +148,14 @@ export class PromptChipStore {
141
148
  return this.#recordUnique({ kind: "url", tag: `[🔗 ${label}]`, label, url });
142
149
  }
143
150
  const paths = pathsFromClipboard(text);
144
- if (paths.length === 0)
145
- return text;
151
+ if (paths.length === 0) {
152
+ const paste = prepareTextPaste(text);
153
+ if (paste.label === undefined)
154
+ return paste.text;
155
+ const tag = `[paste #${++this.#textCounter} ${paste.label}]`;
156
+ this.#chips.set(tag, Object.freeze({ kind: "text", tag, text: paste.text }));
157
+ return tag;
158
+ }
146
159
  return paths.map(item => {
147
160
  const label = pathLabel(item.fullPath);
148
161
  if (item.kind === "folder") {
@@ -167,7 +180,7 @@ export class PromptChipStore {
167
180
  return ranges.sort((left, right) => left.start - right.start);
168
181
  }
169
182
  atomicRanges(line) {
170
- return [...line.matchAll(CHIP_PATTERN)].map(match => ({
183
+ return [...line.matchAll(CHIP_PATTERN)].filter(match => !match[0].startsWith("[paste #") || this.#chips.has(match[0])).map(match => ({
171
184
  start: match.index,
172
185
  end: match.index + match[0].length,
173
186
  }));
@@ -196,41 +209,41 @@ export class PromptChipStore {
196
209
  return this.#replaceResolvable(text, false).text;
197
210
  }
198
211
  prepareHistoryText(text) {
199
- return this.#replaceResolvable(text, false).text.replace(CHIP_PATTERN, tag => this.#chips.get(tag)?.kind === "image" ? "" : tag).trim();
212
+ return this.#replaceResolvable(text, false, true).text.trim();
200
213
  }
201
214
  prepareSubmission(text) {
202
215
  const expanded = this.#replaceResolvable(text, true);
203
216
  return { text: expanded.text, images: expanded.images };
204
217
  }
205
- #replaceResolvable(text, includeImages) {
206
- let expanded = text;
207
- for (const entry of this.#pending.values()) {
208
- if (!expanded.includes(entry.marker) && !expanded.includes(entry.marker.replace("screenshot-", "failed-")))
209
- continue;
210
- if (includeImages && entry.error !== undefined)
211
- throw entry.error;
212
- if (includeImages && entry.replacement === undefined)
213
- throw new ImageAttachmentError("image-pending");
214
- if (entry.replacement !== undefined)
215
- expanded = expanded.replaceAll(entry.marker, entry.replacement);
216
- }
218
+ #replaceResolvable(text, includeImages, omitImages = false) {
217
219
  const images = [];
218
220
  const seenImages = new Set();
219
- for (const match of expanded.matchAll(CHIP_PATTERN)) {
220
- const tag = match[0];
221
+ const resolveChip = (tag) => {
221
222
  const chip = this.#chips.get(tag);
222
223
  if (chip === undefined)
223
- continue;
224
+ return tag;
225
+ if (chip.kind === "text")
226
+ return chip.text;
224
227
  if (chip.kind === "image") {
225
228
  if (includeImages && !seenImages.has(tag)) {
226
229
  images.push(chip.image);
227
230
  seenImages.add(tag);
228
231
  }
229
- continue;
232
+ return omitImages ? "" : tag;
230
233
  }
231
- const value = chip.kind === "url" ? chip.url : chip.path;
232
- expanded = expanded.replaceAll(tag, value);
233
- }
234
+ return chip.kind === "url" ? chip.url : chip.path;
235
+ };
236
+ // Invariant: scan only draft tokens (and resolved reservation tokens), never emitted payloads.
237
+ const expanded = text.replace(CHIP_PATTERN, tag => {
238
+ const entry = this.#pending.get(tag) ?? this.#pending.get(tag.replace("failed-", "screenshot-"));
239
+ if (entry === undefined)
240
+ return resolveChip(tag);
241
+ if (includeImages && entry.error !== undefined)
242
+ throw entry.error;
243
+ if (includeImages && entry.replacement === undefined)
244
+ throw new ImageAttachmentError("image-pending");
245
+ return entry.replacement === undefined ? tag : entry.replacement.replace(CHIP_PATTERN, resolveChip);
246
+ });
234
247
  return { text: expanded, images };
235
248
  }
236
249
  #recordUnique(chip) {
@@ -10,7 +10,6 @@ export declare class PromptHistoryController {
10
10
  fallback: readonly string[];
11
11
  active(): boolean;
12
12
  render(): void;
13
- failure(message: string): void;
14
13
  };
15
14
  constructor(options: {
16
15
  editor: PiShellEditorPort;
@@ -19,7 +18,6 @@ export declare class PromptHistoryController {
19
18
  fallback: readonly string[];
20
19
  active(): boolean;
21
20
  render(): void;
22
- failure(message: string): void;
23
21
  });
24
22
  start(): void;
25
23
  capture(text: string, kind: PromptHistoryKind, cwd: string, sessionId: string): void;
@@ -20,11 +20,9 @@ export class PromptHistoryController {
20
20
  this.#snapshot = { revision: 0, limit: options.limit, entries: [] };
21
21
  this.#fallback = boundedTexts([...options.fallback].reverse(), options.limit);
22
22
  this.#unsubscribeSnapshot = this.#subscribeSnapshot();
23
- this.#unsubscribeFailure = options.store.onFailure(code => {
23
+ this.#unsubscribeFailure = options.store.onFailure(() => {
24
24
  if (this.#disposed)
25
25
  this.#closeFailed = true;
26
- else
27
- options.failure(`Prompt history ${code}; current-session recall remains available.`);
28
26
  });
29
27
  this.#unsubscribe = [
30
28
  options.editor.recall.observe(() => {
@@ -57,10 +55,7 @@ export class PromptHistoryController {
57
55
  if (this.#snapshot.entries.some(entry => entry.submissionId === id))
58
56
  this.#local.delete(id);
59
57
  this.synchronize();
60
- }).catch(() => {
61
- if (!this.#disposed)
62
- this.options.failure("Prompt history unavailable; current-session recall remains available.");
63
- });
58
+ }).catch(() => { });
64
59
  }
65
60
  rememberRecovery(text) {
66
61
  if (this.#disposed || !text.trim())
@@ -111,7 +106,7 @@ export class PromptHistoryController {
111
106
  return;
112
107
  this.#snapshot = snapshot;
113
108
  for (const [id, entry] of this.#local) {
114
- if (entry.committed)
109
+ if (entry.committed || snapshot.entries.some(saved => saved.submissionId === id))
115
110
  this.#local.delete(id);
116
111
  }
117
112
  this.synchronize();
@@ -70,7 +70,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
70
70
  readonly getRows: () => number;
71
71
  readonly requestRender: (force?: boolean) => void;
72
72
  readonly onViewportFrame?: (frame: TranscriptViewportFrame) => void;
73
- readonly requestHyperlinkCleanup?: () => void;
73
+ readonly requestHyperlinkCleanup?: (rows?: readonly number[]) => void;
74
74
  readonly enableDockInputReuse?: boolean;
75
75
  readonly persistentHistory?: boolean;
76
76
  readonly historyEditor?: import("../components/index.js").HistoryEditorConstructor;
@@ -138,7 +138,7 @@ export class OwnedUiSessionShellRoot {
138
138
  enabled: this.#customViewport,
139
139
  editor: this.editor,
140
140
  requestRender: force => this.#componentRuntime.requestRender(force),
141
- requestHyperlinkCleanup: () => handlers.requestHyperlinkCleanup?.(),
141
+ requestHyperlinkCleanup: rows => handlers.requestHyperlinkCleanup?.(rows),
142
142
  hasEditorLinks: () => this.#promptChips.hyperlinkRanges(this.editor.getText()).length > 0,
143
143
  });
144
144
  // Performance: stable painter identities let the neutral viewport retain row-level
@@ -258,6 +258,10 @@ export class OwnedUiSessionShellRoot {
258
258
  * walk of the whole transcript.
259
259
  */
260
260
  applyTranscriptBlock(block) {
261
+ const current = this.#blocksById.get(block.id);
262
+ // Invariant: a full-view/final presentation may preempt queued partials. Never revive an older revision.
263
+ if (current !== undefined && (current.revision > block.revision || current.status === "finalized" && block.status === "live"))
264
+ return;
261
265
  this.#blocksById.set(block.id, block);
262
266
  const component = this.#transcript.get(block.id);
263
267
  if (component === undefined) {
@@ -53,10 +53,12 @@ export class OwnedUiSessionShell {
53
53
  #lastEscapeTime = 0;
54
54
  #activeLoginDialog;
55
55
  #sessionGeneration;
56
+ #sessionBindingGeneration;
56
57
  #suggestionModelKey;
57
58
  constructor(options) {
58
59
  this.backend = options.backend;
59
60
  this.#sessionGeneration = this.backend.sessionGeneration;
61
+ this.#sessionBindingGeneration = this.backend.sessionBindingGeneration;
60
62
  this.#suggestionModelKey = modelKey(this.backend.view());
61
63
  this.#cwd = options.cwd;
62
64
  this.#routeHost = options.routeHost ?? null;
@@ -73,7 +75,7 @@ export class OwnedUiSessionShell {
73
75
  getColumns: () => runtime?.viewport().columns ?? options.terminal?.columns ?? 80,
74
76
  getRows: () => runtime?.viewport().rows ?? options.terminal?.rows ?? 24,
75
77
  requestRender: force => runtime?.requestRender(force),
76
- requestHyperlinkCleanup: () => damageTerminal?.requestHyperlinkCleanup(),
78
+ requestHyperlinkCleanup: rows => damageTerminal?.requestHyperlinkCleanup(rows),
77
79
  onViewportFrame: frame => damageTerminal?.arm(frame.descriptor, {
78
80
  overlayActive: runtime?.hasOverlay() ?? false,
79
81
  selectionActive: this.root.hasActiveSelection(),
@@ -334,7 +336,6 @@ export class OwnedUiSessionShell {
334
336
  fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
335
337
  active: () => this.root.usesDefaultInputSurface(),
336
338
  render: () => this.runtime.requestRender(),
337
- failure: message => this.root.addExtensionNotification(message, "warning"),
338
339
  });
339
340
  }
340
341
  this.#extensionBridge = createPiExtensionUiBridge({
@@ -1262,9 +1263,7 @@ export class OwnedUiSessionShell {
1262
1263
  attempt(() => this.#extensionBridge.dispose());
1263
1264
  // Invariant: terminal restoration precedes any potentially stalled backend teardown.
1264
1265
  await this.runtime.dispose().catch(error => failures.push(error));
1265
- const historySaved = await historyCleanup.catch(() => false);
1266
- if (!historySaved)
1267
- this.runtime.writeAfterStop("Prompt history could not finish saving before exit.\n");
1266
+ await historyCleanup.catch(() => false); // Security: background durability outcomes never enter terminal output.
1268
1267
  await boundedCleanup(() => pasteCleanup).catch(error => failures.push(error));
1269
1268
  await boundedCleanup(() => this.backend.unbindExtensionUi()).catch(error => failures.push(error));
1270
1269
  if (failures.length > 0)
@@ -1302,7 +1301,11 @@ export class OwnedUiSessionShell {
1302
1301
  this.root.resetPendingPastes();
1303
1302
  this.#promptSuggestions?.invalidate();
1304
1303
  this.#sessionGeneration = this.backend.sessionGeneration;
1305
- this.#promptHistory?.reset(view.transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
1304
+ // Invariant: delivery recovery invalidates callbacks, not same-session local recall or its draft.
1305
+ if (this.#sessionBindingGeneration !== this.backend.sessionBindingGeneration) {
1306
+ this.#sessionBindingGeneration = this.backend.sessionBindingGeneration;
1307
+ this.#promptHistory?.reset(view.transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
1308
+ }
1306
1309
  this.#activeLoginDialog = undefined;
1307
1310
  this.#extensionBridge.reset();
1308
1311
  // Invariant: a replaced session takes its transient viewport and owned-route state with it.
@@ -5,8 +5,8 @@ export interface SessionViewportControllerOptions {
5
5
  readonly enabled: boolean;
6
6
  readonly editor: PiShellEditorPort;
7
7
  readonly requestRender: (force?: boolean) => void;
8
- readonly requestHyperlinkCleanup?: () => void;
9
- /** Whether mutable editor URL chips need an immediate forced repaint on deletion. */
8
+ readonly requestHyperlinkCleanup?: (rows?: readonly number[]) => void;
9
+ /** Whether mutable editor URL chips need targeted link-cell cleanup on deletion. */
10
10
  readonly hasEditorLinks?: () => boolean;
11
11
  }
12
12
  export interface SessionViewportInputResult {
@@ -89,8 +89,8 @@ export class SessionViewportController {
89
89
  if (this.#hoveredHyperlinkKey !== undefined && next !== this.#hoveredHyperlinkKey) {
90
90
  // Platform: Windows Terminal can leave its solid native-hover underline behind
91
91
  // when a following viewport moves the link away from a stationary pointer.
92
- this.#requestHyperlinkCleanup();
93
- this.#requestRender(true);
92
+ this.#requestHyperlinkCleanup([this.#pointerPosition.row]);
93
+ this.#requestRender();
94
94
  }
95
95
  this.#hoveredHyperlinkKey = next;
96
96
  }
@@ -161,7 +161,7 @@ export class SessionViewportController {
161
161
  // Platform: URL chip deletion must overwrite terminal link cells in the same frame.
162
162
  if (EDITOR_LINK_DELETION_INPUTS.has(data) && this.#hasEditorLinks()) {
163
163
  this.#requestHyperlinkCleanup();
164
- this.#requestRender(true);
164
+ this.#requestRender();
165
165
  }
166
166
  // Invariant: content shortcuts never reach the prompt, even before the first frame
167
167
  // or when scrolling is a no-op. Plain Home/End remain editor line navigation.
@@ -242,8 +242,7 @@ export class SessionViewportController {
242
242
  const nextHyperlink = this.#hyperlinkKeyAt(frame, event.column, event.row);
243
243
  if (this.#hoveredHyperlinkKey !== undefined && nextHyperlink !== this.#hoveredHyperlinkKey) {
244
244
  // Platform: overwrite Windows Terminal's cached native-hover underline exactly once.
245
- this.#requestHyperlinkCleanup();
246
- forceRepaint = true;
245
+ this.#requestHyperlinkCleanup(previousPointer === undefined ? undefined : [previousPointer.row]);
247
246
  repaint = true;
248
247
  }
249
248
  this.#hoveredHyperlinkKey = nextHyperlink;
@@ -0,0 +1,5 @@
1
+ /** Normalize clipboard text and classify its compact label using the pinned Pi 0.84.2 paste policy. */
2
+ export declare function prepareTextPaste(source: string): {
3
+ readonly text: string;
4
+ readonly label?: string;
5
+ };
@@ -0,0 +1,16 @@
1
+ /** Normalize clipboard text and classify its compact label using the pinned Pi 0.84.2 paste policy. */
2
+ export function prepareTextPaste(source) {
3
+ // Compatibility: extended-key terminals can encode literal controls inside bracketed paste.
4
+ const decoded = source.replace(/\x1b\[(\d+);5u/g, (sequence, value) => {
5
+ const code = Number(value);
6
+ if (code >= 65 && code <= 90)
7
+ return String.fromCharCode(code - 64);
8
+ if (code >= 97 && code <= 122)
9
+ return String.fromCharCode(code - 96);
10
+ return sequence;
11
+ });
12
+ const text = decoded.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x09\x0b-\x1f]/g, "");
13
+ const lines = text.split("\n").length;
14
+ const label = lines > 10 ? `+${lines} lines` : text.length > 1000 ? `${text.length} chars` : undefined;
15
+ return { text, ...(label === undefined ? {} : { label }) };
16
+ }
@@ -63,8 +63,8 @@ export declare class DamageAwareTerminalAdapter implements PiTuiTerminalPort {
63
63
  get kittyProtocolActive(): boolean;
64
64
  get lastDecision(): PiTuiDamageDecision;
65
65
  get hyperlinkCleanupPending(): boolean;
66
- /** Latches cleanup through composition/coalescing until a complete write covers it. */
67
- requestHyperlinkCleanup(): void;
66
+ /** Latches the former link rows, including rows whose replacement contains no link. */
67
+ requestHyperlinkCleanup(rows?: readonly number[]): void;
68
68
  arm(descriptor: PiTuiDamageFrameDescriptor, safety: PiTuiDamageFrameSafety): void;
69
69
  start(onInput: (data: string) => void, onResize: () => void): void;
70
70
  stop(): void;
@@ -15,6 +15,8 @@ export class DamageAwareTerminalAdapter {
15
15
  #rows = new Map();
16
16
  #links = new Map();
17
17
  #cleanupRevision = 0;
18
+ #cleanupRows = new Set();
19
+ #cleanupNeedsCompleteFrame = false;
18
20
  #cleanedRevision = 0;
19
21
  #recoveryRevision = 0;
20
22
  #epoch = 0;
@@ -38,8 +40,23 @@ export class DamageAwareTerminalAdapter {
38
40
  get kittyProtocolActive() { return this.inner.kittyProtocolActive; }
39
41
  get lastDecision() { return this.#decision; }
40
42
  get hyperlinkCleanupPending() { return this.#cleanupRevision > this.#cleanedRevision; }
41
- /** Latches cleanup through composition/coalescing until a complete write covers it. */
42
- requestHyperlinkCleanup() { this.#cleanupRevision += 1; }
43
+ /** Latches the former link rows, including rows whose replacement contains no link. */
44
+ requestHyperlinkCleanup(rows) {
45
+ this.#cleanupRevision += 1;
46
+ if (rows !== undefined) {
47
+ for (const row of rows) {
48
+ if (Number.isSafeInteger(row) && row >= 1 && row <= this.rows)
49
+ this.#cleanupRows.add(row);
50
+ }
51
+ }
52
+ else {
53
+ // Invariant: no known pointer is not proof of no host-hover decoration. Unscoped
54
+ // removal/invalidation repairs every previously linked row, not every screen row.
55
+ for (const [row, state] of this.#links)
56
+ if (state.ranges.length > 0)
57
+ this.#cleanupRows.add(row);
58
+ }
59
+ }
43
60
  arm(descriptor, safety) {
44
61
  this.#armed = { descriptor, safety };
45
62
  }
@@ -56,6 +73,8 @@ export class DamageAwareTerminalAdapter {
56
73
  this.#epoch += 1;
57
74
  this.#armed = undefined;
58
75
  this.#cleanupRevision = this.#cleanedRevision = this.#recoveryRevision = 0;
76
+ this.#cleanupRows.clear();
77
+ this.#cleanupNeedsCompleteFrame = false;
59
78
  this.#lastConsumedFrameId = 0;
60
79
  this.#invalidateRows();
61
80
  this.inner.stop();
@@ -77,25 +96,19 @@ export class DamageAwareTerminalAdapter {
77
96
  this.#invalidatePresentation();
78
97
  if (unknownPaint && !this.hyperlinkCleanupPending && this.options.inspectHyperlinks(data).ranges.length > 0) {
79
98
  this.requestHyperlinkCleanup();
99
+ this.#cleanupNeedsCompleteFrame = true;
80
100
  }
81
- if (parsed !== null && !this.hyperlinkCleanupPending) {
82
- for (const row of parsed.rows) {
83
- const previous = this.#links.get(row.row);
84
- if (previous === undefined || this.#rows.get(row.row) === row.content
85
- || (previous.replaySafe && previous.ranges.length === 0))
86
- continue;
87
- const next = this.options.inspectHyperlinks(row.content);
88
- if (previous.ranges.length > 0 && previous.signature !== next.signature) {
89
- this.requestHyperlinkCleanup();
90
- break;
91
- }
92
- }
93
- }
101
+ // Invariant: a deliberate same-geometry reset discards presented link state. Ordinary streamed
102
+ // row changes are not cleanup requests: candidate-looking text is not terminal state.
103
+ if (sameGeometry && parsed?.structuralPrefix === "\u001b[2J"
104
+ && hasCompleteScreenRows(parsed.rows, this.rows) && !this.hyperlinkCleanupPending
105
+ && !this.#hasSameHyperlinkState(parsed.rows))
106
+ this.requestHyperlinkCleanup();
94
107
  const decision = this.#decide(armed, parsed);
108
+ const coveredRevision = this.#cleanupRevision;
95
109
  const cleanup = this.hyperlinkCleanupPending && parsed !== null
96
110
  && (armed === undefined || (armed.descriptor.width === this.columns && armed.descriptor.height === this.rows))
97
- ? this.#completeCleanupFrame(parsed) : null;
98
- const coveredRevision = this.#cleanupRevision;
111
+ ? this.#boundedCleanupFrame(parsed) : null;
99
112
  const epoch = this.#epoch;
100
113
  let output = data;
101
114
  if (cleanup !== null) {
@@ -107,8 +120,9 @@ export class DamageAwareTerminalAdapter {
107
120
  }
108
121
  else {
109
122
  this.#decision = decision;
110
- if (decision.reason === "suppressed-redundant-clear" && parsed !== null)
111
- output = buildClearlessWrite(parsed);
123
+ if (decision.reason === "suppressed-redundant-clear" && parsed !== null) {
124
+ output = buildClearlessWrite(parsed, this.#rows);
125
+ }
112
126
  else if (decision.transformed && armed !== undefined && parsed !== null) {
113
127
  output = buildDamageWrite(parsed, armed.descriptor, new Set(decision.paintedRows), this.#rows);
114
128
  }
@@ -117,11 +131,16 @@ export class DamageAwareTerminalAdapter {
117
131
  if (epoch !== this.#epoch
118
132
  || (armed !== undefined && this.#lastConsumedFrameId !== armed.descriptor.frameId))
119
133
  return;
120
- // Invariant: cache invalidation follows forwarded bytes, not a clear which
121
- // was stripped from the input. A synthesized cleanup publishes every row.
134
+ // Invariant: cache invalidation follows forwarded bytes, not a suppressed clear.
135
+ // A bounded cleanup preserves prior rows and publishes all changed or invalidated rows.
122
136
  this.#rememberFrame(armed?.descriptor, cleanup ?? parsed, output);
123
- if (cleanup !== null)
137
+ if (cleanup !== null) {
124
138
  this.#cleanedRevision = Math.max(this.#cleanedRevision, coveredRevision);
139
+ if (this.#cleanupRevision === coveredRevision) {
140
+ this.#cleanupRows.clear();
141
+ this.#cleanupNeedsCompleteFrame = false;
142
+ }
143
+ }
125
144
  this.#scheduleCleanupRecovery();
126
145
  }
127
146
  moveBy(lines) { this.inner.moveBy(lines); }
@@ -175,7 +194,7 @@ export class DamageAwareTerminalAdapter {
175
194
  return { ...base, reason: "grammar-mismatch" };
176
195
  if (parsed.structuralPrefix.length > 0
177
196
  || hasUnsafeTerminalContent(parsed, this.options.inspectHyperlinks, descriptor.transcript)
178
- || this.#hasLinkRisk(descriptor.transcript) || this.#hasUnsafeRows(descriptor.transcript)) {
197
+ || this.#hasExplicitLinkRisk(descriptor.transcript) || this.#hasUnsafeRows(descriptor.transcript)) {
179
198
  return { ...base, reason: "unsafe-terminal-content" };
180
199
  }
181
200
  const { rowStart, rowEnd } = descriptor.transcript;
@@ -249,11 +268,9 @@ export class DamageAwareTerminalAdapter {
249
268
  return next?.replaySafe === true && (previous?.signature ?? "") === next.signature;
250
269
  });
251
270
  }
252
- #hasLinkRisk(region) {
271
+ #hasExplicitLinkRisk(region) {
253
272
  for (const [row, state] of this.#links) {
254
- if (region !== undefined && (row < region.rowStart || row > region.rowEnd))
255
- continue;
256
- if (state.hasExplicitLink || state.ranges.length > 0)
273
+ if (row >= region.rowStart && row <= region.rowEnd && state.hasExplicitLink)
257
274
  return true;
258
275
  }
259
276
  return false;
@@ -265,14 +282,33 @@ export class DamageAwareTerminalAdapter {
265
282
  }
266
283
  return false;
267
284
  }
268
- #completeCleanupFrame(parsed) {
269
- const desired = parsed.structuralPrefix.length === 0 ? new Map(this.#rows) : new Map();
270
- for (const row of parsed.rows)
271
- desired.set(row.row, row.content);
272
- if (desired.size !== this.rows)
285
+ #boundedCleanupFrame(parsed) {
286
+ const sameGeometry = this.#cacheWidth === this.columns && this.#cacheHeight === this.rows;
287
+ const complete = hasCompleteScreenRows(parsed.rows, this.rows);
288
+ if (this.#cleanupNeedsCompleteFrame && !complete)
273
289
  return null;
290
+ const suppressReset = sameGeometry && !this.#cleanupNeedsCompleteFrame
291
+ && parsed.structuralPrefix === "\u001b[2J" && complete;
292
+ const structuralPrefix = suppressReset ? "" : parsed.structuralPrefix;
293
+ const desired = structuralPrefix.length === 0 ? new Map(this.#rows) : new Map();
294
+ const dirty = new Set([...this.#cleanupRows].filter(row => row <= this.rows));
295
+ for (const row of parsed.rows) {
296
+ if (row.row > this.rows)
297
+ return null;
298
+ desired.set(row.row, row.content);
299
+ if (structuralPrefix.length > 0 || this.#cleanupNeedsCompleteFrame
300
+ || this.#rows.get(row.row) !== row.content)
301
+ dirty.add(row.row);
302
+ }
303
+ // Invariant: validate all incoming rows before dropping an apparent redundant reset. Unknown
304
+ // content must pass through unchanged, never become a partially transformed frame.
305
+ for (const row of parsed.rows) {
306
+ const state = row.content === this.#rows.get(row.row) ? this.#links.get(row.row) : this.options.inspectHyperlinks(row.content);
307
+ if (state?.replaySafe !== true || state.width > this.columns)
308
+ return null;
309
+ }
274
310
  const rows = [];
275
- for (let row = 1; row <= this.rows; row += 1) {
311
+ for (const row of [...dirty].sort((left, right) => left - right)) {
276
312
  const content = desired.get(row);
277
313
  if (content === undefined)
278
314
  return null;
@@ -281,7 +317,8 @@ export class DamageAwareTerminalAdapter {
281
317
  return null;
282
318
  rows.push({ row, content, segment: `\u001b[${row};1H\u001b[2K${content}` });
283
319
  }
284
- return { structuralPrefix: "\u001b[2J", rows, cursorSuffix: parsed.cursorSuffix };
320
+ // Invariant: only preserve a structural clear supplied by Pi; cleanup never manufactures one.
321
+ return { structuralPrefix, rows, cursorSuffix: parsed.cursorSuffix };
285
322
  }
286
323
  #scheduleCleanupRecovery() {
287
324
  if (!this.hyperlinkCleanupPending || this.#recoveryRevision === this.#cleanupRevision
@@ -297,8 +334,13 @@ export class DamageAwareTerminalAdapter {
297
334
  });
298
335
  }
299
336
  #invalidatePresentation() {
300
- if (this.#hasLinkRisk() && !this.hyperlinkCleanupPending)
337
+ if ([...this.#links.values()].some(state => state.ranges.length > 0) && !this.hyperlinkCleanupPending) {
301
338
  this.requestHyperlinkCleanup();
339
+ }
340
+ // Invariant: after unknown paint or geometry loss, exact former row locations are no longer
341
+ // authoritative. Wait for a complete safe frame instead of guessing a partial repair.
342
+ if (this.hyperlinkCleanupPending)
343
+ this.#cleanupNeedsCompleteFrame = true;
302
344
  this.#invalidateRows();
303
345
  }
304
346
  #invalidateRows() {
@@ -346,7 +388,7 @@ function hasUnsafeTerminalContent(parsed, inspect, linkRegion) {
346
388
  if (linkRegion !== undefined && (row.row < linkRegion.rowStart || row.row > linkRegion.rowEnd))
347
389
  return false;
348
390
  const state = inspect(row.content);
349
- return !state.replaySafe || state.hasExplicitLink || state.ranges.length > 0;
391
+ return !state.replaySafe || state.hasExplicitLink;
350
392
  });
351
393
  }
352
394
  function buildDamageWrite(parsed, descriptor, paintedRows, previousRows) {
@@ -364,12 +406,13 @@ function buildDamageWrite(parsed, descriptor, paintedRows, previousRows) {
364
406
  }).join("");
365
407
  return `${BEGIN_SYNCHRONIZED_OUTPUT}${regionShift}${rowPaint}${parsed.cursorSuffix}`;
366
408
  }
367
- /** Keeps the clear and all current rows in the same synchronized transaction. */
409
+ /** Keeps required cleanup rows and current content in one synchronized transaction. */
368
410
  function buildCompleteWrite(parsed) {
369
411
  return `${BEGIN_SYNCHRONIZED_OUTPUT}${parsed.structuralPrefix}${parsed.rows.map(row => row.segment).join("")}${parsed.cursorSuffix}`;
370
412
  }
371
- function buildClearlessWrite(parsed) {
372
- return `${BEGIN_SYNCHRONIZED_OUTPUT}${parsed.rows.map(row => row.segment).join("")}${parsed.cursorSuffix}`;
413
+ function buildClearlessWrite(parsed, previousRows) {
414
+ const changed = parsed.rows.filter(row => previousRows.get(row.row) !== row.content);
415
+ return `${BEGIN_SYNCHRONIZED_OUTPUT}${changed.map(row => row.segment).join("")}${parsed.cursorSuffix}`;
373
416
  }
374
417
  function hasCompleteScreenRows(rows, height) {
375
418
  return rows.length === height && rows.every((row, index) => row.row === index + 1);
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-12T11:45:08.507Z",
8
+ "builtAt": "2026-09-12T13:59:03.140Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-12T11:45:14.951Z",
8
+ "builtAt": "2026-09-12T13:58:54.058Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-12T11:45:38.237Z",
8
+ "builtAt": "2026-09-12T13:59:34.009Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "ae2bf5d92411273a936b89376077bc13db6f73d16707a2bace8a8f048523891e",
11
+ "sha256": "15e00144629345d7f00b110494775b80c65e6e575e6758d88b533c25b57d35d5",
12
12
  "size": 177664
13
13
  },
14
14
  "provenance": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "a1-product-identity-v1",
3
- "displayName": "A1",
3
+ "displayName": "a1",
4
4
  "commandName": "a1",
5
5
  "packageName": "@timurproko/a1",
6
6
  "filesystem": {
@@ -71,11 +71,13 @@ The old environment registry included `fixture` and `inputAcknowledgement`, but
71
71
 
72
72
  `certificationTarball`, `internalPackaging`, `nativePi`, `piParityIntentionalMutation`, `protocolVersion`, `structuredFlowLimits`, `terminalArgumentsJson`, and `terminalExecutable` had no active runtime consumer in the reviewed tree. They are removed from the current environment authority, not renamed into new settings. Rejection patterns and immutable historical evidence remain factual.
73
73
 
74
- ## Clean cutover and protected state
74
+ ## Negotiated handoff and protected state
75
75
 
76
- The first new-contract installation is intentionally not an old-to-new `a1 update` path. Stop existing A1 sessions, supervisors, and workers first. Install the **accepted published version** explicitly with `npm install --global @timurproko/a1@<accepted-version>`; the implementation/release handoff must replace the placeholder with the actual accepted version. Do not execute an old recovery launcher to bridge the cutover.
76
+ An update is performed by the release that is already installed, so the contract is negotiated rather than cut over. `neutral-launch-v1` is the only contract this build **writes** for its own releases, and the superseded `A1_RELEASE_*`/`A1_IMMUTABLE_WARMUP` keys are still **read** when an older launcher starts this build, and still **written** when this build starts a retained release that declares no contract. An installation therefore stays launchable across the cutover in both directions, and no user is asked to stop processes and reinstall by hand.
77
77
 
78
- Old-contract targets are rejected with a cutover diagnostic. No reset or migration runs automatically. If disposable state blocks installation, inspect its resolved paths and obtain separate approval before removal. A supported rollback must also use `neutral-launch-v1`; no eligible target means an explicit unavailable rollback, not a fallback to older code.
78
+ Metadata that predates contract declaration is stale, never fatal. A pre-cutover active record does not stop a launch: it is ignored, the installed payload is materialized, and ordinary cohort selection activates it. A launch that cannot take the installed payload at all — an installation being replaced right now — starts the retained active release instead of failing, and leaves activation to the next launch. What still requires `neutral-launch-v1` outright is narrow and never blocks a launch: the installed package's own manifest must declare it, and an update recovery capsule is never translated.
79
+
80
+ `test/foundation/release/update-predecessor.integration.test.ts` is the gate for this. It installs each of the most recent published releases and drives the candidate through **that release's own** materialization and warmup, because a fixture built from the candidate would only prove the candidate agrees with itself. No reset or migration runs automatically. If disposable state blocks installation, inspect its resolved paths and obtain separate approval before removal.
79
81
 
80
82
  | State | Treatment |
81
83
  | --- | --- |