@timurproko/a1 0.1.8-dev.322 → 0.1.8-dev.335
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/README.md +22 -11
- package/dist/cli/dispatch.js +1 -1
- package/dist/features/owned-ui/project-trust-prompt.js +1 -1
- package/dist/features/owned-ui/settings-app.js +1 -1
- package/dist/features/prompt-history/service.d.ts +31 -1
- package/dist/features/prompt-history/service.js +294 -129
- package/dist/features/prompt-history/store.d.ts +2 -1
- package/dist/features/prompt-history/store.js +17 -4
- package/dist/features/prompt-history/worker.js +6 -3
- package/dist/foundation/launch-context/index.d.ts +36 -6
- package/dist/foundation/launch-context/index.js +64 -11
- package/dist/foundation/launch-guardian/main.js +2 -1
- package/dist/foundation/release/bootstrap.d.ts +7 -0
- package/dist/foundation/release/bootstrap.js +95 -19
- package/dist/foundation/release/cohort-state.js +0 -3
- package/dist/foundation/release/dependency-certification-retention.d.ts +7 -0
- package/dist/foundation/release/dependency-certification-retention.js +88 -0
- package/dist/foundation/release/dependency-certification.d.ts +24 -0
- package/dist/foundation/release/dependency-certification.js +208 -0
- package/dist/foundation/release/dependency-layer.d.ts +3 -4
- package/dist/foundation/release/dependency-layer.js +6 -37
- package/dist/foundation/release/index.d.ts +1 -0
- package/dist/foundation/release/index.js +1 -0
- package/dist/foundation/release/release-gc.js +72 -44
- package/dist/foundation/release/release-store.d.ts +2 -0
- package/dist/foundation/release/release-store.js +4 -3
- package/dist/foundation/release/restart-certification.js +16 -5
- package/dist/foundation/release/update-launch.d.ts +6 -0
- package/dist/foundation/release/update-launch.js +17 -0
- package/dist/foundation/release/update.d.ts +2 -0
- package/dist/foundation/release/update.js +21 -27
- package/dist/foundation/release/warmup.js +16 -5
- package/dist/foundation/supervision/main.js +5 -2
- package/dist/foundation/supervision/server.js +40 -13
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
- package/dist/integrations/pi/components/owned-editor-ux.js +51 -27
- package/dist/integrations/pi/engine/adapter.d.ts +20 -0
- package/dist/integrations/pi/engine/adapter.js +223 -87
- package/dist/integrations/pi/engine/pending-delivery.d.ts +26 -0
- package/dist/integrations/pi/engine/pending-delivery.js +149 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +38 -25
- package/dist/integrations/pi/session-ui/prompt-history-controller.d.ts +0 -2
- package/dist/integrations/pi/session-ui/prompt-history-controller.js +3 -8
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +1 -1
- package/dist/integrations/pi/session-ui/session-shell-root.js +5 -1
- package/dist/integrations/pi/session-ui/session-shell.js +9 -6
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -2
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +4 -5
- package/dist/integrations/pi/session-ui/text-paste.d.ts +5 -0
- package/dist/integrations/pi/session-ui/text-paste.js +16 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +2 -2
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +82 -39
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.json +1 -1
- package/docs/architecture/internal-naming.md +5 -3
- package/docs/architecture/toolchain.md +1 -1
- package/docs/ci-release-runbook.md +45 -7
- package/docs/manual-code-streaming-cleanup.md +88 -0
- 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
|
-
|
|
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() {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
220
|
-
const tag = match[0];
|
|
221
|
+
const resolveChip = (tag) => {
|
|
221
222
|
const chip = this.#chips.get(tag);
|
|
222
223
|
if (chip === undefined)
|
|
223
|
-
|
|
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
|
-
|
|
232
|
+
return omitImages ? "" : tag;
|
|
230
233
|
}
|
|
231
|
-
|
|
232
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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,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
|
|
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
|
|
42
|
-
requestHyperlinkCleanup() {
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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.#
|
|
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
|
|
121
|
-
//
|
|
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.#
|
|
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
|
-
#
|
|
271
|
+
#hasExplicitLinkRisk(region) {
|
|
253
272
|
for (const [row, state] of this.#links) {
|
|
254
|
-
if (
|
|
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
|
-
#
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
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 (
|
|
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
|
-
|
|
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.#
|
|
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
|
|
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
|
|
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
|
-
|
|
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-
|
|
8
|
+
"builtAt": "2026-09-12T16:36:02.575Z",
|
|
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-
|
|
8
|
+
"builtAt": "2026-09-12T16:36:05.106Z",
|
|
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-
|
|
8
|
+
"builtAt": "2026-09-12T16:36:30.210Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "0bc42bc571ebdcf244d60f42d03472a2dd57da4984b1d39931f9a2ad3787cde5",
|
|
12
12
|
"size": 177664
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -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
|
-
##
|
|
74
|
+
## Negotiated handoff and protected state
|
|
75
75
|
|
|
76
|
-
|
|
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
|
-
|
|
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
|
| --- | --- |
|