@timurproko/a1 0.2.7-dev.746 → 0.2.7-dev.767
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/bin/tab-launch-context.d.ts +6 -1
- package/bin/tab-launch-context.js +11 -2
- package/bin/ui.js +7 -3
- package/dist/app/session-shell/session-shell-root.d.ts +23 -0
- package/dist/app/session-shell/session-shell.js +112 -6
- package/dist/composition/owned-ui.d.ts +4 -1
- package/dist/composition/owned-ui.js +54 -8
- package/dist/contracts/owned-ui/session-backend.d.ts +1 -0
- package/dist/features/owned-ui/settings-app.js +8 -35
- package/dist/foundation/release/bootstrap.d.ts +1 -1
- package/dist/foundation/release/bootstrap.js +15 -4
- package/dist/foundation/release/dependency-certification.js +54 -19
- package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
- package/dist/foundation/release/progress-palette.generated.js +2 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +7 -2
- package/dist/foundation/resident-tabs/bridge-client.js +58 -16
- package/dist/foundation/resident-tabs/draft-checkpoint.d.ts +27 -0
- package/dist/foundation/resident-tabs/draft-checkpoint.js +145 -0
- package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +2 -0
- package/dist/foundation/resident-tabs/ensure-terminal-host.js +7 -1
- package/dist/foundation/resident-tabs/fault-injection.d.ts +8 -0
- package/dist/foundation/resident-tabs/fault-injection.js +26 -0
- package/dist/foundation/resident-tabs/index.d.ts +2 -1
- package/dist/foundation/resident-tabs/index.js +1 -1
- package/dist/foundation/resident-tabs/prompt-journal.d.ts +65 -0
- package/dist/foundation/resident-tabs/prompt-journal.js +214 -0
- package/dist/foundation/resident-tabs/recovery.d.ts +26 -0
- package/dist/foundation/resident-tabs/recovery.js +205 -0
- package/dist/foundation/resident-tabs/session-sync.d.ts +14 -0
- package/dist/foundation/resident-tabs/session-sync.js +37 -0
- package/dist/foundation/resident-tabs/settings.d.ts +9 -2
- package/dist/foundation/resident-tabs/settings.js +55 -7
- package/dist/foundation/session-writer-lock/index.d.ts +27 -0
- package/dist/foundation/session-writer-lock/index.js +197 -0
- package/dist/foundation/startup/startup-descriptor.js +2 -2
- package/dist/integrations/pi/components/resources/builtin-themes.d.ts +4 -4
- package/dist/integrations/pi/components/resources/builtin-themes.js +4 -4
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +17 -4
- package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
- package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
- package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
- package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
- package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +2 -2
- package/dist/integrations/pi/components/upstream/components/status-indicator.js +3 -3
- package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +0 -2
- package/dist/integrations/pi/components/upstream/components/tool-execution.js +41 -20
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +2 -2
- package/dist/integrations/pi/components/upstream/history/editor-core.js +5 -5
- package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
- package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
- package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
- package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
- package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
- package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/theme/theme.d.ts +3 -0
- package/dist/integrations/pi/components/upstream/theme/theme.js +24 -1
- package/dist/integrations/pi/engine/adapter.js +1 -0
- package/dist/integrations/pi/engine/pi-settings-metadata.json +2 -2
- package/dist/integrations/pi/engine/resources/changelog.json +4 -4
- package/dist/integrations/pi/engine/runtime-integration.js +102 -29
- package/dist/integrations/pi/engine/session-runtime.d.ts +0 -6
- package/dist/integrations/pi/engine/session-runtime.js +8 -3
- package/dist/integrations/pi/engine/session-selection.d.ts +3 -0
- package/dist/integrations/pi/engine/session-selection.js +45 -9
- package/dist/integrations/pi/engine/workflow-contexts.js +10 -1
- package/dist/integrations/pi/startup-public.js +152 -222
- package/dist/integrations/pi/startup-public.manifest.json +266 -177
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
- 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/runtime-payload-inventory.json +5349 -6035
- package/dist/ui/components/line-input.d.ts +1 -1
- package/dist/ui/components/prompt-input.d.ts +7 -5
- package/dist/ui/components/prompt-input.js +7 -7
- package/dist/ui/settings/declarations.d.ts +38 -2
- package/dist/ui/settings/declarations.js +29 -2
- package/dist/ui/settings/index.d.ts +4 -2
- package/dist/ui/settings/index.js +2 -1
- package/dist/ui/settings/manager.d.ts +9 -3
- package/dist/ui/settings/manager.js +81 -29
- package/dist/ui/settings/migrations.d.ts +6 -0
- package/dist/ui/settings/migrations.js +25 -0
- package/dist/ui/settings/profile-document.d.ts +26 -0
- package/dist/ui/settings/profile-document.js +164 -0
- package/dist/ui/settings/sections.js +4 -0
- package/docs/architecture/resident-tabs-requirements.md +1 -234
- package/docs/architecture/resident-tabs.md +30 -10
- package/docs/openspec-archive-automation.md +7 -7
- package/package.json +6 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { chmod, lstat, mkdir, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
4
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
5
5
|
import { immutablePlatformPolicy } from "./immutable-platform.js";
|
|
6
6
|
export const DEPENDENCY_CERTIFICATIONS_DIRECTORY = "dependency-certifications";
|
|
@@ -194,7 +194,8 @@ async function releasePublicationLock(path, token) {
|
|
|
194
194
|
while (true) {
|
|
195
195
|
try {
|
|
196
196
|
if (!retired) {
|
|
197
|
-
await assertPublicationLockOwner(path, token)
|
|
197
|
+
if (!await assertPublicationLockOwner(path, token))
|
|
198
|
+
return;
|
|
198
199
|
await rename(path, released);
|
|
199
200
|
retired = true;
|
|
200
201
|
}
|
|
@@ -218,24 +219,58 @@ async function releasePublicationLock(path, token) {
|
|
|
218
219
|
}
|
|
219
220
|
/** Ownership uncertainty is never authority to retire a missing, linked, or replaced generation. */
|
|
220
221
|
async function assertPublicationLockOwner(path, token) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|| owner
|
|
232
|
-
|
|
222
|
+
for (let attempt = 1;; attempt += 1) {
|
|
223
|
+
try {
|
|
224
|
+
const metadata = await lstat(path);
|
|
225
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || await realpath(path) !== path)
|
|
226
|
+
throw new Error("invalid lock directory");
|
|
227
|
+
const ownerPath = resolve(path, "owner.json");
|
|
228
|
+
const ownerMetadata = await lstat(ownerPath);
|
|
229
|
+
if (!ownerMetadata.isFile() || ownerMetadata.isSymbolicLink() || await realpath(ownerPath) !== ownerPath)
|
|
230
|
+
throw new Error("invalid owner file");
|
|
231
|
+
const owner = JSON.parse(await readFile(ownerPath, "utf8"));
|
|
232
|
+
if (!owner || typeof owner !== "object" || !("pid" in owner) || !("token" in owner)
|
|
233
|
+
|| owner.pid !== process.pid || owner.token !== token)
|
|
234
|
+
throw new Error("owner generation differs");
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
// Platform: Windows can briefly hide a directory immediately after a successful rename
|
|
239
|
+
// when independent publishers are contending. Re-read the complete identity; never treat
|
|
240
|
+
// absence as ownership and never retire a generation whose token differs.
|
|
241
|
+
if (hasCode(error, "ENOENT") && attempt < 3) {
|
|
242
|
+
await new Promise(resolvePromise => setTimeout(resolvePromise, 20 * attempt));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// Concurrency: a delayed abandoned-owner reclaimer can move a successor generation after
|
|
246
|
+
// reading the predecessor. If our exact token is now in a managed tombstone, no active
|
|
247
|
+
// generation belongs to this release call and returning without mutation is fail-safe.
|
|
248
|
+
if (hasCode(error, "ENOENT") && await publicationLockWasRetired(path, token))
|
|
249
|
+
return false;
|
|
250
|
+
if (isReleaseContention(error))
|
|
251
|
+
throw error;
|
|
252
|
+
throw new Error("publication lease release ownership could not be verified", { cause: error });
|
|
253
|
+
}
|
|
233
254
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
255
|
+
}
|
|
256
|
+
async function publicationLockWasRetired(path, token) {
|
|
257
|
+
const directory = dirname(path);
|
|
258
|
+
const prefix = `${basename(path)}.`;
|
|
259
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
260
|
+
if (entries.length > 256)
|
|
261
|
+
return false;
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !entry.name.startsWith(prefix)
|
|
264
|
+
|| (!entry.name.includes(".abandoned-") && !entry.name.includes(".released-")))
|
|
265
|
+
continue;
|
|
266
|
+
const ownerPath = resolve(directory, entry.name, "owner.json");
|
|
267
|
+
const owner = await readFile(ownerPath, "utf8")
|
|
268
|
+
.then(source => JSON.parse(source))
|
|
269
|
+
.catch(() => null);
|
|
270
|
+
if (owner?.pid === process.pid && owner.token === token)
|
|
271
|
+
return true;
|
|
238
272
|
}
|
|
273
|
+
return false;
|
|
239
274
|
}
|
|
240
275
|
function isReleaseContention(error) {
|
|
241
276
|
return ["EPERM", "EACCES", "EBUSY"].some(code => hasCode(error, code));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generated by scripts/pi/sync-progress-palette.ts from @earendil-works/pi-coding-agent 1.
|
|
3
|
-
* (MIT), commit
|
|
2
|
+
* Generated by scripts/pi/sync-progress-palette.ts from @earendil-works/pi-coding-agent 1.2.0
|
|
3
|
+
* (MIT), commit 06a466935536f8c9bcacb2d5efc0fc021be1bdff. Do not edit directly.
|
|
4
4
|
*/
|
|
5
5
|
export declare const SHARED_PROGRESS_ACCENT_ANSI = "\u001B[38;2;167;152;215m";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generated by scripts/pi/sync-progress-palette.ts from @earendil-works/pi-coding-agent 1.
|
|
3
|
-
* (MIT), commit
|
|
2
|
+
* Generated by scripts/pi/sync-progress-palette.ts from @earendil-works/pi-coding-agent 1.2.0
|
|
3
|
+
* (MIT), commit 06a466935536f8c9bcacb2d5efc0fc021be1bdff. Do not edit directly.
|
|
4
4
|
*/
|
|
5
5
|
export const SHARED_PROGRESS_ACCENT_ANSI = "\u001b[38;2;167;152;215m";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ResidentTabStatus = "starting" | "working" | "idle";
|
|
1
|
+
export type ResidentTabStatus = "starting" | "working" | "idle" | "unresponsive" | "stalled" | "crashed" | "restarting" | "failed";
|
|
2
2
|
export interface TabBridgeCredentials {
|
|
3
3
|
readonly tabId: string;
|
|
4
4
|
readonly token: string;
|
|
@@ -20,6 +20,9 @@ export interface TabBridgeOptions {
|
|
|
20
20
|
readonly startIdentity?: string;
|
|
21
21
|
readonly build?: string;
|
|
22
22
|
readonly connect?: (endpoint: string) => BridgeTransport;
|
|
23
|
+
readonly heartbeatIntervalMs?: number;
|
|
24
|
+
readonly setInterval?: (callback: () => void, milliseconds: number) => unknown;
|
|
25
|
+
readonly clearInterval?: (timer: unknown) => void;
|
|
23
26
|
}
|
|
24
27
|
/** Metadata-only advisory bridge. Terminal input and transcript bytes have no API here. */
|
|
25
28
|
export declare class TabBridgeClient {
|
|
@@ -28,9 +31,11 @@ export declare class TabBridgeClient {
|
|
|
28
31
|
get available(): boolean;
|
|
29
32
|
captureAttachment(): TabAttachment | null;
|
|
30
33
|
start(timeoutMs?: number): Promise<boolean>;
|
|
31
|
-
status(state: ResidentTabStatus): void;
|
|
34
|
+
status(state: ResidentTabStatus, degraded?: boolean): void;
|
|
35
|
+
degraded(degraded: boolean): void;
|
|
32
36
|
session(file: string | null, name: string | null): void;
|
|
33
37
|
requestDetach(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
|
|
38
|
+
requestRestart(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
|
|
34
39
|
close(): void;
|
|
35
40
|
}
|
|
36
41
|
export {};
|
|
@@ -6,6 +6,7 @@ export class TabBridgeClient {
|
|
|
6
6
|
#transport = null;
|
|
7
7
|
#connected = false;
|
|
8
8
|
#available = true;
|
|
9
|
+
#features = new Set();
|
|
9
10
|
#sequence = 0;
|
|
10
11
|
#request = 0;
|
|
11
12
|
#attachment = null;
|
|
@@ -13,6 +14,7 @@ export class TabBridgeClient {
|
|
|
13
14
|
#retry = null;
|
|
14
15
|
#latestStatus = null;
|
|
15
16
|
#latestSession = null;
|
|
17
|
+
#heartbeat = null;
|
|
16
18
|
#pending = new Map();
|
|
17
19
|
#startWaiter = null;
|
|
18
20
|
constructor(options) { this.#options = options; }
|
|
@@ -53,7 +55,8 @@ export class TabBridgeClient {
|
|
|
53
55
|
this.#startWaiter = finish;
|
|
54
56
|
transport.once("connect", () => {
|
|
55
57
|
this.#send("hello", {
|
|
56
|
-
role: "bridge", generation: 1, build: this.#options.build ?? "a1",
|
|
58
|
+
role: "bridge", generation: 1, build: this.#options.build ?? "a1",
|
|
59
|
+
features: ["bridge-status", "bridge-detach", "bridge-heartbeat", "bridge-restart", "recovery-journal"],
|
|
57
60
|
token: [...Buffer.from(this.#options.credentials.token, "base64")], tabId: this.#options.credentials.tabId,
|
|
58
61
|
pid: this.#options.pid ?? process.pid, startIdentity: this.#options.startIdentity ?? `${process.pid}:node`,
|
|
59
62
|
}, ++this.#request);
|
|
@@ -63,31 +66,36 @@ export class TabBridgeClient {
|
|
|
63
66
|
timer.unref();
|
|
64
67
|
});
|
|
65
68
|
}
|
|
66
|
-
status(state) {
|
|
67
|
-
this.#latestStatus = state;
|
|
68
|
-
|
|
69
|
+
status(state, degraded = false) {
|
|
70
|
+
this.#latestStatus = { state, degraded };
|
|
71
|
+
const recoveryState = state === "unresponsive" || state === "stalled" || state === "crashed" || state === "restarting" || state === "failed";
|
|
72
|
+
if (recoveryState && !this.#supports("recovery-status"))
|
|
73
|
+
return;
|
|
74
|
+
this.#send("bridge.status", {
|
|
75
|
+
seq: ++this.#sequence,
|
|
76
|
+
state,
|
|
77
|
+
...(degraded && this.#supports("recovery-status") ? { degraded: true } : {}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
degraded(degraded) {
|
|
81
|
+
this.status(this.#latestStatus?.state ?? "idle", degraded);
|
|
69
82
|
}
|
|
70
83
|
session(file, name) {
|
|
71
84
|
this.#latestSession = { file, name };
|
|
72
85
|
this.#send("bridge.session", { file, name });
|
|
73
86
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.#send("bridge.request", { action: "detach", clientId: origin.clientId, controllerRevision: origin.controllerRevision }, requestId);
|
|
80
|
-
const timeout = new Promise(resolvePromise => {
|
|
81
|
-
const timer = setTimeout(() => { this.#pending.delete(requestId); resolvePromise(false); }, timeoutMs);
|
|
82
|
-
timer.unref();
|
|
83
|
-
});
|
|
84
|
-
return await Promise.race([result, timeout]);
|
|
87
|
+
requestDetach(origin = this.captureAttachment(), timeoutMs = 1_000) {
|
|
88
|
+
return this.#requestAction("detach", origin, timeoutMs);
|
|
89
|
+
}
|
|
90
|
+
requestRestart(origin = this.captureAttachment(), timeoutMs = 1_000) {
|
|
91
|
+
return this.#requestAction("restart", origin, timeoutMs);
|
|
85
92
|
}
|
|
86
93
|
close() {
|
|
87
94
|
this.#closed = true;
|
|
88
95
|
if (this.#retry !== null)
|
|
89
96
|
clearTimeout(this.#retry);
|
|
90
97
|
this.#retry = null;
|
|
98
|
+
this.#stopHeartbeat();
|
|
91
99
|
const transport = this.#transport;
|
|
92
100
|
this.#transport = null;
|
|
93
101
|
transport?.destroy();
|
|
@@ -105,11 +113,15 @@ export class TabBridgeClient {
|
|
|
105
113
|
}
|
|
106
114
|
#receive(message) {
|
|
107
115
|
if (message.type === "welcome" && message.body["generation"] === 1) {
|
|
116
|
+
const advertised = message.body["features"];
|
|
117
|
+
this.#features = new Set(Array.isArray(advertised) ? advertised.filter((feature) => typeof feature === "string") : []);
|
|
108
118
|
this.#connected = true;
|
|
109
119
|
this.#available = true;
|
|
110
120
|
this.#startWaiter?.(true);
|
|
121
|
+
if (this.#supports("bridge-heartbeat"))
|
|
122
|
+
this.#startHeartbeat();
|
|
111
123
|
if (this.#latestStatus !== null)
|
|
112
|
-
this
|
|
124
|
+
this.status(this.#latestStatus.state, this.#latestStatus.degraded);
|
|
113
125
|
if (this.#latestSession !== null)
|
|
114
126
|
this.#send("bridge.session", this.#latestSession);
|
|
115
127
|
return;
|
|
@@ -133,10 +145,40 @@ export class TabBridgeClient {
|
|
|
133
145
|
}
|
|
134
146
|
}
|
|
135
147
|
}
|
|
148
|
+
async #requestAction(action, origin, timeoutMs) {
|
|
149
|
+
if (!this.available || origin === null || (action === "restart" && !this.#supports("bridge-restart")))
|
|
150
|
+
return false;
|
|
151
|
+
const requestId = ++this.#request;
|
|
152
|
+
const result = new Promise(resolvePromise => this.#pending.set(requestId, resolvePromise));
|
|
153
|
+
this.#send("bridge.request", { action, clientId: origin.clientId, controllerRevision: origin.controllerRevision }, requestId);
|
|
154
|
+
const timeout = new Promise(resolvePromise => {
|
|
155
|
+
const timer = setTimeout(() => { this.#pending.delete(requestId); resolvePromise(false); }, timeoutMs);
|
|
156
|
+
timer.unref();
|
|
157
|
+
});
|
|
158
|
+
return await Promise.race([result, timeout]);
|
|
159
|
+
}
|
|
160
|
+
#startHeartbeat() {
|
|
161
|
+
if (this.#heartbeat !== null)
|
|
162
|
+
return;
|
|
163
|
+
const schedule = this.#options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
|
|
164
|
+
this.#heartbeat = schedule(() => this.#send("bridge.heartbeat", { seq: ++this.#sequence }), this.#options.heartbeatIntervalMs ?? 5_000);
|
|
165
|
+
this.#heartbeat.unref?.();
|
|
166
|
+
}
|
|
167
|
+
#stopHeartbeat() {
|
|
168
|
+
if (this.#heartbeat === null)
|
|
169
|
+
return;
|
|
170
|
+
(this.#options.clearInterval ?? (timer => clearInterval(timer)))(this.#heartbeat);
|
|
171
|
+
this.#heartbeat = null;
|
|
172
|
+
}
|
|
173
|
+
#supports(feature) {
|
|
174
|
+
return this.#features.has("crash-recovery") || this.#features.has(feature);
|
|
175
|
+
}
|
|
136
176
|
#fail(retry = true) {
|
|
137
177
|
const transport = this.#transport;
|
|
178
|
+
this.#stopHeartbeat();
|
|
138
179
|
this.#available = false;
|
|
139
180
|
this.#connected = false;
|
|
181
|
+
this.#features.clear();
|
|
140
182
|
this.#attachment = null;
|
|
141
183
|
this.#transport = null;
|
|
142
184
|
transport?.destroy();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ResidentTabFaultInjection } from "./fault-injection.js";
|
|
2
|
+
export interface DraftCheckpointState {
|
|
3
|
+
readonly text: string;
|
|
4
|
+
readonly savedAt: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DraftCheckpointOptions {
|
|
7
|
+
readonly intervalMs?: number;
|
|
8
|
+
readonly maximumDirtyMs?: number;
|
|
9
|
+
readonly faults?: ResidentTabFaultInjection;
|
|
10
|
+
readonly now?: () => number;
|
|
11
|
+
readonly onDegraded?: (degraded: boolean) => void;
|
|
12
|
+
readonly write?: (path: string, text: string) => Promise<void>;
|
|
13
|
+
readonly move?: (from: string, to: string) => Promise<void>;
|
|
14
|
+
readonly schedule?: (callback: () => void | Promise<void>, delayMs: number) => unknown;
|
|
15
|
+
readonly cancel?: (timer: unknown) => void;
|
|
16
|
+
}
|
|
17
|
+
/** Periodic rather than inactivity-debounced: continuous input still checkpoints every interval. */
|
|
18
|
+
export declare class DraftCheckpointer {
|
|
19
|
+
#private;
|
|
20
|
+
readonly path: string;
|
|
21
|
+
constructor(path: string, options?: DraftCheckpointOptions);
|
|
22
|
+
get degraded(): boolean;
|
|
23
|
+
change(text: string): void;
|
|
24
|
+
restore(): Promise<DraftCheckpointState | null>;
|
|
25
|
+
flush(): Promise<void>;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { ResidentTabFaultInjection } from "./fault-injection.js";
|
|
5
|
+
/** Periodic rather than inactivity-debounced: continuous input still checkpoints every interval. */
|
|
6
|
+
export class DraftCheckpointer {
|
|
7
|
+
path;
|
|
8
|
+
#intervalMs;
|
|
9
|
+
#maximumDirtyMs;
|
|
10
|
+
#faults;
|
|
11
|
+
#now;
|
|
12
|
+
#onDegraded;
|
|
13
|
+
#write;
|
|
14
|
+
#move;
|
|
15
|
+
#scheduleTimer;
|
|
16
|
+
#cancelTimer;
|
|
17
|
+
#text = "";
|
|
18
|
+
#dirtySince = null;
|
|
19
|
+
#timer = null;
|
|
20
|
+
#degradedTimer = null;
|
|
21
|
+
#writing = null;
|
|
22
|
+
#closed = false;
|
|
23
|
+
#degraded = false;
|
|
24
|
+
constructor(path, options = {}) {
|
|
25
|
+
this.path = path;
|
|
26
|
+
this.#intervalMs = options.intervalMs ?? 500;
|
|
27
|
+
this.#maximumDirtyMs = options.maximumDirtyMs ?? 1_000;
|
|
28
|
+
this.#faults = options.faults ?? new ResidentTabFaultInjection();
|
|
29
|
+
this.#now = options.now ?? Date.now;
|
|
30
|
+
this.#onDegraded = options.onDegraded ?? (() => { });
|
|
31
|
+
this.#write = options.write ?? (async (target, text) => writeFile(target, text, { encoding: "utf8", mode: 0o600, flag: "wx" }));
|
|
32
|
+
this.#move = options.move ?? rename;
|
|
33
|
+
this.#scheduleTimer = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
34
|
+
this.#cancelTimer = options.cancel ?? (timer => clearTimeout(timer));
|
|
35
|
+
}
|
|
36
|
+
get degraded() { return this.#degraded; }
|
|
37
|
+
change(text) {
|
|
38
|
+
if (this.#closed)
|
|
39
|
+
return;
|
|
40
|
+
this.#text = text;
|
|
41
|
+
this.#dirtySince ??= this.#now();
|
|
42
|
+
this.#schedule();
|
|
43
|
+
this.#armDegradedDeadline();
|
|
44
|
+
}
|
|
45
|
+
async restore() {
|
|
46
|
+
try {
|
|
47
|
+
const value = JSON.parse(await readFile(this.path, "utf8"));
|
|
48
|
+
if (typeof value !== "object" || value === null || typeof value.text !== "string"
|
|
49
|
+
|| typeof value.savedAt !== "string")
|
|
50
|
+
return null;
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError)
|
|
55
|
+
return null;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async flush() {
|
|
60
|
+
if (this.#timer !== null) {
|
|
61
|
+
this.#cancelTimer(this.#timer);
|
|
62
|
+
this.#timer = null;
|
|
63
|
+
}
|
|
64
|
+
await this.#writing;
|
|
65
|
+
if (this.#dirtySince !== null)
|
|
66
|
+
await this.#checkpoint();
|
|
67
|
+
}
|
|
68
|
+
async close() {
|
|
69
|
+
await this.flush();
|
|
70
|
+
this.#closed = true;
|
|
71
|
+
}
|
|
72
|
+
#armDegradedDeadline() {
|
|
73
|
+
if (this.#degradedTimer !== null || this.#dirtySince === null || this.#closed)
|
|
74
|
+
return;
|
|
75
|
+
const delay = Math.max(0, this.#maximumDirtyMs - (this.#now() - this.#dirtySince));
|
|
76
|
+
this.#degradedTimer = this.#scheduleTimer(() => {
|
|
77
|
+
this.#degradedTimer = null;
|
|
78
|
+
if (this.#dirtySince !== null && this.#now() - this.#dirtySince >= this.#maximumDirtyMs) {
|
|
79
|
+
this.#setDegraded(true);
|
|
80
|
+
}
|
|
81
|
+
else if (this.#dirtySince !== null) {
|
|
82
|
+
this.#armDegradedDeadline();
|
|
83
|
+
}
|
|
84
|
+
}, delay);
|
|
85
|
+
}
|
|
86
|
+
#clearDegradedDeadline() {
|
|
87
|
+
if (this.#degradedTimer !== null)
|
|
88
|
+
this.#cancelTimer(this.#degradedTimer);
|
|
89
|
+
this.#degradedTimer = null;
|
|
90
|
+
}
|
|
91
|
+
#schedule() {
|
|
92
|
+
if (this.#timer !== null || this.#writing !== null || this.#closed)
|
|
93
|
+
return;
|
|
94
|
+
const age = this.#dirtySince === null ? 0 : this.#now() - this.#dirtySince;
|
|
95
|
+
if (age > this.#maximumDirtyMs)
|
|
96
|
+
this.#setDegraded(true);
|
|
97
|
+
this.#timer = this.#scheduleTimer(async () => {
|
|
98
|
+
this.#timer = null;
|
|
99
|
+
this.#writing = this.#checkpoint().finally(() => {
|
|
100
|
+
this.#writing = null;
|
|
101
|
+
if (this.#dirtySince !== null)
|
|
102
|
+
this.#schedule();
|
|
103
|
+
});
|
|
104
|
+
await this.#writing.catch(() => undefined);
|
|
105
|
+
}, this.#intervalMs);
|
|
106
|
+
}
|
|
107
|
+
async #checkpoint() {
|
|
108
|
+
if (this.#dirtySince === null)
|
|
109
|
+
return;
|
|
110
|
+
const snapshot = this.#text;
|
|
111
|
+
const temporary = `${this.path}.tmp-${process.pid}-${randomUUID()}`;
|
|
112
|
+
try {
|
|
113
|
+
await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
114
|
+
if (process.platform !== "win32")
|
|
115
|
+
await chmod(dirname(this.path), 0o700);
|
|
116
|
+
this.#faults.at("draft-during-temp-write");
|
|
117
|
+
await this.#write(temporary, `${JSON.stringify({ text: snapshot, savedAt: new Date(this.#now()).toISOString() })}\n`);
|
|
118
|
+
this.#faults.at("draft-before-rename");
|
|
119
|
+
await this.#move(temporary, this.path);
|
|
120
|
+
this.#faults.at("draft-after-rename");
|
|
121
|
+
if (process.platform !== "win32")
|
|
122
|
+
await chmod(this.path, 0o600);
|
|
123
|
+
if (this.#text === snapshot)
|
|
124
|
+
this.#dirtySince = null;
|
|
125
|
+
else
|
|
126
|
+
this.#dirtySince = this.#now();
|
|
127
|
+
this.#clearDegradedDeadline();
|
|
128
|
+
if (this.#dirtySince !== null)
|
|
129
|
+
this.#armDegradedDeadline();
|
|
130
|
+
this.#setDegraded(false);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
await unlink(temporary).catch(() => undefined);
|
|
134
|
+
this.#clearDegradedDeadline();
|
|
135
|
+
this.#setDegraded(true);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
#setDegraded(value) {
|
|
140
|
+
if (this.#degraded === value)
|
|
141
|
+
return;
|
|
142
|
+
this.#degraded = value;
|
|
143
|
+
this.#onDegraded(value);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -21,6 +21,8 @@ export interface EnsureTerminalHostOptions {
|
|
|
21
21
|
readonly platform?: NodeJS.Platform;
|
|
22
22
|
readonly architecture?: string;
|
|
23
23
|
readonly timeoutMs?: number;
|
|
24
|
+
readonly maxConcurrentStarts?: number;
|
|
25
|
+
readonly unresponsiveRestartSeconds?: number;
|
|
24
26
|
readonly run?: (executable: string, arguments_: readonly string[], environment: NodeJS.ProcessEnv, timeoutMs: number) => Promise<HostCommandResult>;
|
|
25
27
|
}
|
|
26
28
|
export declare function resolveTerminalHostPath(releaseRoot: string, environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, architecture?: string): string;
|
|
@@ -42,7 +42,13 @@ export async function ensureTerminalHost(options) {
|
|
|
42
42
|
return { status: "fallback", reason: "incompatible generation" };
|
|
43
43
|
if (existing?.outcome !== "not-running" && probe.code !== 3)
|
|
44
44
|
return { status: "fallback", reason: classifiedReason(existing?.reason, "host probe failed") };
|
|
45
|
-
const
|
|
45
|
+
const maximum = Math.max(1, Math.min(8, Math.trunc(options.maxConcurrentStarts ?? 2)));
|
|
46
|
+
const unresponsiveRestartSeconds = Math.max(30, Math.min(600, Math.trunc(options.unresponsiveRestartSeconds ?? 120)));
|
|
47
|
+
const start = await run(executable, [
|
|
48
|
+
"server", "--detach", ...common,
|
|
49
|
+
"--max-concurrent-starts", String(maximum),
|
|
50
|
+
"--unresponsive-restart-seconds", String(unresponsiveRestartSeconds),
|
|
51
|
+
], environment, timeoutMs);
|
|
46
52
|
const started = parseResult(start.stdout);
|
|
47
53
|
if (start.code !== 0)
|
|
48
54
|
return { status: "fallback", reason: classifiedReason(started?.reason, "launch not verified") };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const RESIDENT_TAB_FAULT_ENVIRONMENT: readonly ["A1_TABS_FAULT_CRASH_POINT", "A1_TABS_HERMETIC_TEST"];
|
|
2
|
+
export declare function residentTabFaultEnvironment(point: string, hermetic?: boolean): NodeJS.ProcessEnv;
|
|
3
|
+
/** Test-only crash points are inert unless the caller explicitly marks a hermetic process. */
|
|
4
|
+
export declare class ResidentTabFaultInjection {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(environment?: NodeJS.ProcessEnv, crash?: (point: string) => never);
|
|
7
|
+
at(point: string): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const CRASH_POINT = "A1_TABS_FAULT_CRASH_POINT";
|
|
2
|
+
const HERMETIC = "A1_TABS_HERMETIC_TEST";
|
|
3
|
+
export const RESIDENT_TAB_FAULT_ENVIRONMENT = Object.freeze([CRASH_POINT, HERMETIC]);
|
|
4
|
+
export function residentTabFaultEnvironment(point, hermetic = true) {
|
|
5
|
+
return Object.fromEntries(hermetic ? [[CRASH_POINT, point], [HERMETIC, "1"]] : [[CRASH_POINT, point]]);
|
|
6
|
+
}
|
|
7
|
+
/** Test-only crash points are inert unless the caller explicitly marks a hermetic process. */
|
|
8
|
+
export class ResidentTabFaultInjection {
|
|
9
|
+
#point;
|
|
10
|
+
#crash;
|
|
11
|
+
constructor(environment = process.env, crash = point => {
|
|
12
|
+
process.stderr.write(`resident tab injected crash: ${point}\n`);
|
|
13
|
+
process.abort();
|
|
14
|
+
}) {
|
|
15
|
+
const point = environment[CRASH_POINT];
|
|
16
|
+
if (point !== undefined && environment[HERMETIC] !== "1") {
|
|
17
|
+
throw new Error(`${CRASH_POINT} is available only in a hermetic resident-tab test`);
|
|
18
|
+
}
|
|
19
|
+
this.#point = point;
|
|
20
|
+
this.#crash = crash;
|
|
21
|
+
}
|
|
22
|
+
at(point) {
|
|
23
|
+
if (this.#point === point)
|
|
24
|
+
this.#crash(point);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { ensureTerminalHost, resolveTerminalHostPath } from "./ensure-terminal-host.js";
|
|
2
2
|
export type { EnsureTerminalHostOptions, EnsureTerminalHostResult, TerminalHostFallbackReason } from "./ensure-terminal-host.js";
|
|
3
|
-
export { readResidentTabsSetting } from "./settings.js";
|
|
3
|
+
export { readResidentTabLaunchSettings, readResidentTabsSetting } from "./settings.js";
|
|
4
|
+
export type { ResidentTabLaunchSettings } from "./settings.js";
|
|
4
5
|
export { selectResidentHost } from "./routing.js";
|
|
5
6
|
export type { ResidentHostSelectionOptions } from "./routing.js";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { ensureTerminalHost, resolveTerminalHostPath } from "./ensure-terminal-host.js";
|
|
2
|
-
export { readResidentTabsSetting } from "./settings.js";
|
|
2
|
+
export { readResidentTabLaunchSettings, readResidentTabsSetting } from "./settings.js";
|
|
3
3
|
export { selectResidentHost } from "./routing.js";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ResidentTabFaultInjection } from "./fault-injection.js";
|
|
2
|
+
export interface SubmittedPromptRecord {
|
|
3
|
+
readonly type: "submitted";
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly sessionId: string;
|
|
6
|
+
readonly sessionFile: string | null;
|
|
7
|
+
readonly incarnation: number;
|
|
8
|
+
readonly fileExisted: boolean;
|
|
9
|
+
readonly kind: "prompt" | "steer" | "follow-up";
|
|
10
|
+
readonly text: string;
|
|
11
|
+
readonly images?: readonly unknown[];
|
|
12
|
+
readonly at: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CorrelatedPromptRecord {
|
|
15
|
+
readonly type: "correlated";
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly entryId: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SettledPromptRecord {
|
|
20
|
+
readonly type: "settled";
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly entryId: string;
|
|
23
|
+
}
|
|
24
|
+
export interface RetiredPromptRecord {
|
|
25
|
+
readonly type: "retired";
|
|
26
|
+
readonly id: string;
|
|
27
|
+
}
|
|
28
|
+
export type PromptJournalRecord = SubmittedPromptRecord | CorrelatedPromptRecord | SettledPromptRecord | RetiredPromptRecord;
|
|
29
|
+
export interface PromptSubmission {
|
|
30
|
+
readonly sessionId: string;
|
|
31
|
+
readonly sessionFile: string | null;
|
|
32
|
+
readonly incarnation: number;
|
|
33
|
+
readonly fileExisted: boolean;
|
|
34
|
+
readonly kind: SubmittedPromptRecord["kind"];
|
|
35
|
+
readonly text: string;
|
|
36
|
+
readonly images?: readonly unknown[];
|
|
37
|
+
}
|
|
38
|
+
export interface SessionJournalEntry {
|
|
39
|
+
readonly entryId: string;
|
|
40
|
+
readonly text: string;
|
|
41
|
+
readonly settled: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface PendingPrompt {
|
|
44
|
+
readonly submitted: SubmittedPromptRecord;
|
|
45
|
+
readonly correlatedEntryId?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare class PromptJournal {
|
|
48
|
+
#private;
|
|
49
|
+
readonly path: string;
|
|
50
|
+
constructor(path: string, options?: {
|
|
51
|
+
readonly faults?: ResidentTabFaultInjection;
|
|
52
|
+
readonly maximumBytes?: number;
|
|
53
|
+
});
|
|
54
|
+
initialize(): Promise<void>;
|
|
55
|
+
submit(submission: PromptSubmission): Promise<SubmittedPromptRecord>;
|
|
56
|
+
adopt(record: SubmittedPromptRecord): Promise<boolean>;
|
|
57
|
+
correlate(id: string, entryId: string): Promise<void>;
|
|
58
|
+
settle(id: string, entryId: string): Promise<void>;
|
|
59
|
+
retire(id: string): Promise<void>;
|
|
60
|
+
read(): Promise<readonly PromptJournalRecord[]>;
|
|
61
|
+
pending(sessionEntries?: readonly SessionJournalEntry[]): Promise<readonly PendingPrompt[]>;
|
|
62
|
+
compactIfRetired(): Promise<boolean>;
|
|
63
|
+
}
|
|
64
|
+
export declare function classifyMissingSession(record: SubmittedPromptRecord, sessionExists: boolean): "available" | "first-turn" | "missing-transcript";
|
|
65
|
+
export declare function removeJournalTemporary(path: string): Promise<void>;
|