@timurproko/a1 0.2.7-dev.754 → 0.2.7-dev.772
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 +30 -0
- package/bin/cli.js +4 -0
- package/bin/guardian.js +5 -0
- package/bin/tab-launch-context.d.ts +26 -0
- package/bin/tab-launch-context.js +94 -0
- package/bin/ui.js +24 -6
- package/dist/app/session-shell/helper-pool.d.ts +3 -5
- package/dist/app/session-shell/helper-pool.js +3 -5
- package/dist/app/session-shell/session-shell-root.d.ts +28 -1
- package/dist/app/session-shell/session-shell-root.js +4 -3
- package/dist/app/session-shell/session-shell.js +149 -31
- package/dist/app/session-shell/session-viewport-controller.d.ts +1 -1
- package/dist/app/session-shell/session-viewport-controller.js +3 -3
- package/dist/app/session-shell/shell-host.d.ts +2 -0
- package/dist/app/session-shell/shell-host.js +20 -1
- package/dist/cli/dispatch.d.ts +11 -1
- package/dist/cli/dispatch.js +79 -5
- package/dist/cli/tabs-request.d.ts +7 -0
- package/dist/cli/tabs-request.js +1 -0
- package/dist/cli/tabs.d.ts +14 -0
- package/dist/cli/tabs.js +140 -0
- package/dist/composition/owned-ui.d.ts +26 -0
- package/dist/composition/owned-ui.js +176 -6
- package/dist/contracts/owned-ui/session-backend.d.ts +1 -0
- package/dist/features/launch/index.d.ts +1 -1
- package/dist/features/launch/intent.d.ts +3 -1
- package/dist/features/launch/intent.js +7 -4
- package/dist/features/owned-ui/settings-app.js +8 -35
- package/dist/features/resident-tab/auto-name.d.ts +11 -0
- package/dist/features/resident-tab/auto-name.js +42 -0
- package/dist/features/resident-tab/index.d.ts +3 -0
- package/dist/features/resident-tab/index.js +3 -0
- package/dist/features/resident-tab/shortcuts.d.ts +10 -0
- package/dist/features/resident-tab/shortcuts.js +35 -0
- package/dist/features/resident-tab/status-deriver.d.ts +9 -0
- package/dist/features/resident-tab/status-deriver.js +15 -0
- package/dist/foundation/launch-context/index.d.ts +10 -0
- package/dist/foundation/launch-context/index.js +25 -0
- package/dist/foundation/launch-guardian/main.d.ts +5 -0
- package/dist/foundation/launch-guardian/main.js +2 -1
- package/dist/foundation/release/bootstrap.d.ts +6 -0
- package/dist/foundation/release/bootstrap.js +51 -10
- package/dist/foundation/release/cohort-state.d.ts +1 -1
- package/dist/foundation/release/cohort-state.js +1 -1
- 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/release/release-gc.d.ts +3 -0
- package/dist/foundation/release/release-gc.js +44 -8
- package/dist/foundation/resident-tabs/bounded-host-command.d.ts +16 -0
- package/dist/foundation/resident-tabs/bounded-host-command.js +35 -0
- package/dist/foundation/resident-tabs/bridge-client.d.ts +56 -0
- package/dist/foundation/resident-tabs/bridge-client.js +279 -0
- package/dist/foundation/resident-tabs/doctor.d.ts +29 -0
- package/dist/foundation/resident-tabs/doctor.js +166 -0
- 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 +31 -0
- package/dist/foundation/resident-tabs/ensure-terminal-host.js +147 -0
- 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 +12 -0
- package/dist/foundation/resident-tabs/index.js +6 -0
- package/dist/foundation/resident-tabs/maintenance.d.ts +42 -0
- package/dist/foundation/resident-tabs/maintenance.js +78 -0
- 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/protocol.d.ts +12 -0
- package/dist/foundation/resident-tabs/protocol.js +195 -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/release-holds.d.ts +27 -0
- package/dist/foundation/resident-tabs/release-holds.js +77 -0
- package/dist/foundation/resident-tabs/routing.d.ts +11 -0
- package/dist/foundation/resident-tabs/routing.js +10 -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 +14 -0
- package/dist/foundation/resident-tabs/settings.js +66 -0
- package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +18 -0
- package/dist/foundation/resident-tabs/terminal-host-artifact.js +53 -0
- package/dist/foundation/resident-tabs/zip-archive.d.ts +9 -0
- package/dist/foundation/resident-tabs/zip-archive.js +56 -0
- package/dist/foundation/session-writer-lock/index.d.ts +32 -0
- package/dist/foundation/session-writer-lock/index.js +201 -0
- package/dist/foundation/startup/startup-descriptor.js +2 -2
- package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
- package/dist/foundation/terminal-cleanup/index.js +1 -1
- package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +1 -0
- package/dist/foundation/terminal-cleanup/terminal-reset.js +1 -0
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
- package/dist/integrations/pi/components/owned-editor-ux.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/shell-hotkey-sections.js +1 -1
- package/dist/integrations/pi/components/shell-presenters-info.d.ts +4 -2
- package/dist/integrations/pi/components/shell-presenters-info.js +38 -4
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +1 -1
- 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.d.ts +1 -0
- package/dist/integrations/pi/engine/adapter.js +4 -0
- package/dist/integrations/pi/engine/pi-settings-metadata.json +2 -2
- package/dist/integrations/pi/engine/prompt-suggestions.d.ts +1 -0
- package/dist/integrations/pi/engine/prompt-suggestions.js +40 -0
- 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/a1_session_lock.node +0 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host +0 -0
- package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +10500 -0
- package/dist/native/darwin-arm64/terminal-host.manifest.json +44 -0
- package/dist/native/darwin-x64/a1_session_lock.node +0 -0
- package/dist/native/darwin-x64/manifest.json +20 -0
- package/dist/native/darwin-x64/process-guardian +0 -0
- package/dist/native/darwin-x64/terminal-host +0 -0
- package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10737 -0
- package/dist/native/darwin-x64/terminal-host.manifest.json +44 -0
- package/dist/native/linux-x64/a1_session_lock.node +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host +0 -0
- package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +11226 -0
- package/dist/native/linux-x64/terminal-host.manifest.json +44 -0
- package/dist/native/win32-x64/a1_session_lock.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10757 -0
- package/dist/native/win32-x64/terminal-host.exe +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +44 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +1 -1
- package/dist/product-identity.json +5 -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/components/transcript-viewport.d.ts +6 -3
- package/dist/ui/components/transcript-viewport.js +13 -15
- package/dist/ui/settings/declarations.d.ts +25 -1
- package/dist/ui/settings/declarations.js +21 -1
- package/dist/ui/settings/index.d.ts +4 -2
- package/dist/ui/settings/index.js +2 -1
- package/dist/ui/settings/manager.d.ts +10 -3
- package/dist/ui/settings/manager.js +90 -30
- package/dist/ui/settings/migrations.d.ts +6 -0
- package/dist/ui/settings/migrations.js +44 -0
- package/dist/ui/settings/profile-document.d.ts +26 -0
- package/dist/ui/settings/profile-document.js +164 -0
- package/docs/architecture/evidence/resident-tabs/linux-x64.md +37 -0
- package/docs/architecture/evidence/resident-tabs/macos-arm64.md +36 -0
- package/docs/architecture/evidence/resident-tabs/macos-x64.md +35 -0
- package/docs/architecture/evidence/resident-tabs/windows-x64.md +36 -0
- package/docs/architecture/internal-naming.md +3 -1
- package/docs/architecture/process-guardian-provenance.md +2 -0
- package/docs/architecture/resident-tabs-requirements.md +18 -0
- package/docs/architecture/resident-tabs.md +594 -0
- package/docs/architecture/terminal-host-proof-gate.md +2 -0
- package/docs/architecture/terminal-host-provenance.md +13 -3
- package/docs/architecture/toolchain.md +1 -1
- package/docs/ci-release-runbook.md +2 -2
- package/docs/features/launch-profiles.md +11 -8
- package/docs/manual-resident-tabs-acceptance.md +51 -0
- package/docs/openspec-archive-automation.md +17 -16
- package/docs/validation.md +9 -1
- package/package.json +10 -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";
|
|
@@ -2,6 +2,7 @@ import { rename, rm } from "node:fs/promises";
|
|
|
2
2
|
import { CohortStateStore, type ExternalReleaseHold, type OrphanReleaseDisposition } from "./cohort-state.js";
|
|
3
3
|
import type { ProductPaths } from "../lifecycle/index.js";
|
|
4
4
|
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
5
|
+
import { type ResidentReleaseHoldReport } from "../resident-tabs/index.js";
|
|
5
6
|
export interface ReleaseCleanupLimits {
|
|
6
7
|
readonly maxItems: number;
|
|
7
8
|
readonly maxDurationMs: number;
|
|
@@ -21,6 +22,8 @@ export interface ReleaseCleanupOptions {
|
|
|
21
22
|
readonly operations?: ReleaseCleanupOperations;
|
|
22
23
|
/** Process-start seam for scheduler and continuation tests. */
|
|
23
24
|
readonly workerSpawner?: (entry: string, environment: NodeJS.ProcessEnv) => Promise<void>;
|
|
25
|
+
/** Resident-tab inspection seam; the default asks a verified installed terminal host. */
|
|
26
|
+
readonly residentHolds?: (dataDir: string, releaseRoots: readonly string[]) => Promise<ResidentReleaseHoldReport>;
|
|
24
27
|
}
|
|
25
28
|
export interface ReleaseCleanupWorkerOptions {
|
|
26
29
|
readonly cleanup?: ReleaseCleanupOptions;
|
|
@@ -14,6 +14,7 @@ import { UpdateTransactionStore } from "./update-transaction.js";
|
|
|
14
14
|
import { cleanupUpdateRecoveryCapsules } from "./update-recovery.js";
|
|
15
15
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
16
16
|
import { collectCompileCaches, startupCompileCachePath } from "../startup/index.js";
|
|
17
|
+
import { readResidentReleaseHolds } from "../resident-tabs/index.js";
|
|
17
18
|
const DEFAULT_CANDIDATE_AGE_MS = 60 * 60 * 1_000;
|
|
18
19
|
const DEFAULT_LIMITS = { maxItems: 8, maxDurationMs: 2_000, concurrency: 1 };
|
|
19
20
|
const DEFAULT_WORKER_DURATION_MS = 10 * 60 * 1_000;
|
|
@@ -30,7 +31,7 @@ export async function prepareReleaseCleanup(dataDir, paths, options = {}) {
|
|
|
30
31
|
const store = new CohortStateStore(dataDir);
|
|
31
32
|
const discovered = await discoverCleanupInputs(dataDir, store, options);
|
|
32
33
|
const transactionStore = options.transactionStore ?? new UpdateTransactionStore(dataDir);
|
|
33
|
-
await store.reconcileRetention(async () => await protectionInputs(paths, transactionStore, options.externalHolds ?? []), discovered.orphans);
|
|
34
|
+
await store.reconcileRetention(async () => await protectionInputs(dataDir, paths, transactionStore, options.externalHolds ?? [], options.residentHolds), discovered.orphans);
|
|
34
35
|
for (const diagnostic of discovered.diagnostics.slice(0, 16)) {
|
|
35
36
|
await store.recordCleanupFailure(diagnostic.releaseId, diagnostic.stage, diagnostic.error);
|
|
36
37
|
}
|
|
@@ -77,7 +78,7 @@ export async function runBoundedReleaseCleanup(dataDir, paths, options = {}) {
|
|
|
77
78
|
maximum -= 1;
|
|
78
79
|
const certificationId = certificationReleaseId(artifact);
|
|
79
80
|
if (certificationId !== null) {
|
|
80
|
-
const inputs = await protectionInputs(paths, transactionStore, options.externalHolds ?? []);
|
|
81
|
+
const inputs = await protectionInputs(dataDir, paths, transactionStore, options.externalHolds ?? [], options.residentHolds);
|
|
81
82
|
if (isProtectedDetachedRelease(await store.read(), inputs, certificationId))
|
|
82
83
|
continue;
|
|
83
84
|
}
|
|
@@ -232,7 +233,7 @@ export async function collectRelease(store, dataDir, releaseId, externalReferenc
|
|
|
232
233
|
const state = await store.read();
|
|
233
234
|
if (!state.releases[releaseId])
|
|
234
235
|
throw new Error(`unknown release ${releaseId}`);
|
|
235
|
-
await store.reconcileRetention(async () => ({ ...(await protectionInputs(paths, new UpdateTransactionStore(dataDir), holds)) }));
|
|
236
|
+
await store.reconcileRetention(async () => ({ ...(await protectionInputs(dataDir, paths, new UpdateTransactionStore(dataDir), holds)) }));
|
|
236
237
|
const disposition = (await store.read()).cleanup.pending[releaseId];
|
|
237
238
|
if (!disposition)
|
|
238
239
|
throw new Error(`release ${releaseId} is still referenced and cannot be collected`);
|
|
@@ -243,7 +244,7 @@ export async function collectRelease(store, dataDir, releaseId, externalReferenc
|
|
|
243
244
|
async function collectDisposition(store, dataDir, paths, disposition, options) {
|
|
244
245
|
const releaseId = disposition.release.releaseId;
|
|
245
246
|
try {
|
|
246
|
-
const inputs = await protectionInputs(paths, options.transactionStore ?? new UpdateTransactionStore(dataDir), options.externalHolds ?? []);
|
|
247
|
+
const inputs = await protectionInputs(dataDir, paths, options.transactionStore ?? new UpdateTransactionStore(dataDir), options.externalHolds ?? [], options.residentHolds);
|
|
247
248
|
const current = await store.read();
|
|
248
249
|
if (isProtectedDetachedRelease(current, inputs, releaseId))
|
|
249
250
|
return false;
|
|
@@ -289,12 +290,47 @@ function isProtectedDetachedRelease(state, inputs, releaseId) {
|
|
|
289
290
|
|| (inputs.externalHolds ?? []).some(hold => hold.releaseId === releaseId)
|
|
290
291
|
|| (inputs.transaction?.status === "active" && inputs.transaction.priorActiveReleaseId === releaseId);
|
|
291
292
|
}
|
|
292
|
-
async function protectionInputs(paths, transactionStore, externalHolds) {
|
|
293
|
-
const [liveIds, transaction] = await Promise.all([
|
|
293
|
+
async function protectionInputs(dataDir, paths, transactionStore, externalHolds, residentHolds) {
|
|
294
|
+
const [liveIds, transaction, resident] = await Promise.all([
|
|
294
295
|
paths ? liveReleaseIds(paths) : Promise.resolve([]),
|
|
295
296
|
transactionStore.read(),
|
|
297
|
+
residentReleaseHolds(dataDir, residentHolds),
|
|
296
298
|
]);
|
|
297
|
-
return { liveReleaseIds: liveIds, externalHolds, transaction: transactionReference(transaction) };
|
|
299
|
+
return { liveReleaseIds: liveIds, externalHolds: [...externalHolds, ...resident], transaction: transactionReference(transaction) };
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Holds every release a live resident server, holder, or tab child runs from. Retention fails
|
|
303
|
+
* closed: an incomplete report, or a recorded root inside the release store that names no known
|
|
304
|
+
* release, holds every known release.
|
|
305
|
+
*/
|
|
306
|
+
async function residentReleaseHolds(dataDir, reader) {
|
|
307
|
+
const state = await new CohortStateStore(dataDir).read();
|
|
308
|
+
const records = [
|
|
309
|
+
...Object.values(state.releases),
|
|
310
|
+
...Object.values(state.cleanup.pending).map(disposition => disposition.release),
|
|
311
|
+
];
|
|
312
|
+
const preferred = [state.references.active, state.references.rollback]
|
|
313
|
+
.flatMap(id => id && state.releases[id] ? [state.releases[id].releaseRoot] : []);
|
|
314
|
+
const roots = [...new Set([...preferred, ...Object.values(state.releases).map(record => record.releaseRoot)])];
|
|
315
|
+
const report = await (reader ?? ((directory, releaseRoots) => readResidentReleaseHolds({ dataDir: directory, releaseRoots })))(dataDir, roots);
|
|
316
|
+
const everything = () => [...new Set(records.map(record => record.releaseId))].map(releaseId => ({ authority: "resident", releaseId }));
|
|
317
|
+
if (!report.complete)
|
|
318
|
+
return everything();
|
|
319
|
+
const byRoot = new Map(records.map(record => [comparablePath(record.releaseRoot), record.releaseId]));
|
|
320
|
+
const store = comparablePath(resolve(dataDir, "releases"));
|
|
321
|
+
const holds = new Set();
|
|
322
|
+
for (const root of report.releaseRoots) {
|
|
323
|
+
const releaseId = byRoot.get(comparablePath(root));
|
|
324
|
+
if (releaseId)
|
|
325
|
+
holds.add(releaseId);
|
|
326
|
+
else if (comparablePath(root).startsWith(`${store}${sep}`))
|
|
327
|
+
return everything();
|
|
328
|
+
}
|
|
329
|
+
return [...holds].sort().map(releaseId => ({ authority: "resident", releaseId }));
|
|
330
|
+
}
|
|
331
|
+
function comparablePath(path) {
|
|
332
|
+
const absolute = resolve(path);
|
|
333
|
+
return process.platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
298
334
|
}
|
|
299
335
|
function transactionReference(transaction) {
|
|
300
336
|
return transaction === null ? null : { status: transaction.status, priorActiveReleaseId: transaction.priorActiveReleaseId };
|
|
@@ -745,7 +781,7 @@ function parseWorkerHolds(value) {
|
|
|
745
781
|
if (!Array.isArray(parsed))
|
|
746
782
|
return [];
|
|
747
783
|
return parsed.filter((hold) => Boolean(hold) && typeof hold === "object"
|
|
748
|
-
&&
|
|
784
|
+
&& ["agent", "migration", "resident"].includes(hold.authority)
|
|
749
785
|
&& typeof hold.releaseId === "string");
|
|
750
786
|
}
|
|
751
787
|
catch {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface BoundedHostCommandResult {
|
|
2
|
+
readonly code: number;
|
|
3
|
+
readonly stdout: string;
|
|
4
|
+
}
|
|
5
|
+
export interface BoundedHostCommandLimits {
|
|
6
|
+
/** Output beyond this many bytes fails the exchange instead of being parsed. */
|
|
7
|
+
readonly maxStdoutBytes: number;
|
|
8
|
+
readonly timeoutMs: number;
|
|
9
|
+
/** Names the exchange in the timeout error. */
|
|
10
|
+
readonly label: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Runs one terminal-host command whose report is small. An oversized report kills the command and
|
|
14
|
+
* reports a failed exchange; a command that outlives its deadline is killed and rejected.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runBoundedHostCommand(executable: string, arguments_: readonly string[], limits: BoundedHostCommandLimits): Promise<BoundedHostCommandResult>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Runs one terminal-host command whose report is small. An oversized report kills the command and
|
|
4
|
+
* reports a failed exchange; a command that outlives its deadline is killed and rejected.
|
|
5
|
+
*/
|
|
6
|
+
export async function runBoundedHostCommand(executable, arguments_, limits) {
|
|
7
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
8
|
+
const child = spawn(executable, [...arguments_], { windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
9
|
+
const stdout = [];
|
|
10
|
+
let size = 0;
|
|
11
|
+
let overflowed = false;
|
|
12
|
+
child.stdout.on("data", chunk => {
|
|
13
|
+
size += chunk.length;
|
|
14
|
+
// Security: an oversized answer is a failed exchange, never a truncated report to parse.
|
|
15
|
+
if (size > limits.maxStdoutBytes) {
|
|
16
|
+
overflowed = true;
|
|
17
|
+
child.kill("SIGKILL");
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
stdout.push(Buffer.from(chunk));
|
|
21
|
+
});
|
|
22
|
+
child.once("error", rejectPromise);
|
|
23
|
+
child.once("close", code => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
resolvePromise(overflowed
|
|
26
|
+
? { code: code === 0 || code === null ? 1 : code, stdout: "" }
|
|
27
|
+
: { code: code ?? 1, stdout: Buffer.concat(stdout).toString("utf8") });
|
|
28
|
+
});
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
child.kill("SIGKILL");
|
|
31
|
+
rejectPromise(new Error(`${limits.label} timed out`));
|
|
32
|
+
}, limits.timeoutMs);
|
|
33
|
+
timer.unref();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type ResidentTabStatus = "starting" | "working" | "needs-input" | "done-unseen" | "idle" | "unresponsive" | "stalled" | "crashed" | "restarting" | "failed";
|
|
2
|
+
export interface TabBridgeCredentials {
|
|
3
|
+
readonly tabId: string;
|
|
4
|
+
readonly token: string;
|
|
5
|
+
readonly endpoint: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TabAttachment {
|
|
8
|
+
readonly clientId: string;
|
|
9
|
+
readonly controllerRevision: number;
|
|
10
|
+
}
|
|
11
|
+
export type TabBridgeCommand = {
|
|
12
|
+
readonly type: "visibility";
|
|
13
|
+
readonly visible: boolean;
|
|
14
|
+
} | {
|
|
15
|
+
readonly type: "rename";
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly nameSource: "default" | "user" | "auto";
|
|
18
|
+
} | {
|
|
19
|
+
readonly type: "stop";
|
|
20
|
+
readonly deadlineMs: number;
|
|
21
|
+
};
|
|
22
|
+
interface BridgeTransport {
|
|
23
|
+
write(bytes: Uint8Array): boolean;
|
|
24
|
+
destroy(): void;
|
|
25
|
+
once(event: "connect" | "error" | "close", listener: (...args: unknown[]) => void): unknown;
|
|
26
|
+
on(event: "data", listener: (bytes: Buffer) => void): unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface TabBridgeOptions {
|
|
29
|
+
readonly credentials: TabBridgeCredentials;
|
|
30
|
+
readonly pid?: number;
|
|
31
|
+
readonly startIdentity?: string;
|
|
32
|
+
readonly build?: string;
|
|
33
|
+
readonly connect?: (endpoint: string) => BridgeTransport;
|
|
34
|
+
readonly heartbeatIntervalMs?: number;
|
|
35
|
+
readonly setInterval?: (callback: () => void, milliseconds: number) => unknown;
|
|
36
|
+
readonly clearInterval?: (timer: unknown) => void;
|
|
37
|
+
}
|
|
38
|
+
/** Metadata-only advisory bridge. Terminal input and transcript bytes have no API here. */
|
|
39
|
+
export declare class TabBridgeClient {
|
|
40
|
+
#private;
|
|
41
|
+
constructor(options: TabBridgeOptions);
|
|
42
|
+
get available(): boolean;
|
|
43
|
+
captureAttachment(): TabAttachment | null;
|
|
44
|
+
onCommand(listener: (command: TabBridgeCommand) => void): () => void;
|
|
45
|
+
start(timeoutMs?: number): Promise<boolean>;
|
|
46
|
+
status(state: ResidentTabStatus, degraded?: boolean, pendingRequests?: number, queuedInput?: number): void;
|
|
47
|
+
degraded(degraded: boolean): void;
|
|
48
|
+
accent(accent: string): void;
|
|
49
|
+
session(file: string | null, name: string | null, nameSource?: "default" | "user" | "auto"): void;
|
|
50
|
+
prompt(interrupted: boolean): void;
|
|
51
|
+
requestDetach(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
|
|
52
|
+
requestRestart(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
|
|
53
|
+
consumeControllerBoundaryInput(data: string): string;
|
|
54
|
+
close(): void;
|
|
55
|
+
}
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { connect } from "node:net";
|
|
2
|
+
import { encodeResidentFrame, ResidentFrameDecoder } from "./protocol.js";
|
|
3
|
+
/** Metadata-only advisory bridge. Terminal input and transcript bytes have no API here. */
|
|
4
|
+
export class TabBridgeClient {
|
|
5
|
+
#options;
|
|
6
|
+
#transport = null;
|
|
7
|
+
#connected = false;
|
|
8
|
+
#available = true;
|
|
9
|
+
#features = new Set();
|
|
10
|
+
#sequence = 0;
|
|
11
|
+
#request = 0;
|
|
12
|
+
#attachment = null;
|
|
13
|
+
#closed = false;
|
|
14
|
+
#retry = null;
|
|
15
|
+
#latestStatus = null;
|
|
16
|
+
#accent = null;
|
|
17
|
+
#latestSession = null;
|
|
18
|
+
#heartbeat = null;
|
|
19
|
+
#pending = new Map();
|
|
20
|
+
#startWaiter = null;
|
|
21
|
+
#commandListeners = new Set();
|
|
22
|
+
#boundaryInputRemainder = "";
|
|
23
|
+
constructor(options) { this.#options = options; }
|
|
24
|
+
get available() { return this.#available && this.#connected; }
|
|
25
|
+
captureAttachment() { return this.#attachment === null ? null : { ...this.#attachment }; }
|
|
26
|
+
onCommand(listener) {
|
|
27
|
+
this.#commandListeners.add(listener);
|
|
28
|
+
return () => this.#commandListeners.delete(listener);
|
|
29
|
+
}
|
|
30
|
+
async start(timeoutMs = 4_000) {
|
|
31
|
+
if (this.#closed || this.#transport !== null)
|
|
32
|
+
return this.available;
|
|
33
|
+
const transport = (this.#options.connect ?? (endpoint => connect(endpoint)))(this.#options.credentials.endpoint);
|
|
34
|
+
this.#transport = transport;
|
|
35
|
+
const decoder = new ResidentFrameDecoder();
|
|
36
|
+
transport.on("data", bytes => {
|
|
37
|
+
try {
|
|
38
|
+
for (const message of decoder.push(bytes))
|
|
39
|
+
this.#receive(message);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
if (this.#transport === transport)
|
|
43
|
+
this.#fail();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
transport.once("close", () => { if (this.#transport === transport)
|
|
47
|
+
this.#fail(); });
|
|
48
|
+
transport.once("error", () => { if (this.#transport === transport)
|
|
49
|
+
this.#fail(); });
|
|
50
|
+
return await new Promise(resolvePromise => {
|
|
51
|
+
let settled = false;
|
|
52
|
+
let timer;
|
|
53
|
+
const finish = (value) => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
if (timer !== undefined)
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
this.#startWaiter = null;
|
|
60
|
+
resolvePromise(value);
|
|
61
|
+
};
|
|
62
|
+
this.#startWaiter = finish;
|
|
63
|
+
transport.once("connect", () => {
|
|
64
|
+
this.#send("hello", {
|
|
65
|
+
role: "bridge", generation: 1, build: this.#options.build ?? "a1",
|
|
66
|
+
features: ["bridge-status", "bridge-detach", "bridge-heartbeat", "bridge-restart", "bridge-visibility", "bridge-rename", "bridge-stop", "controller-boundary", "recovery-journal"],
|
|
67
|
+
token: [...Buffer.from(this.#options.credentials.token, "base64")], tabId: this.#options.credentials.tabId,
|
|
68
|
+
pid: this.#options.pid ?? process.pid, startIdentity: this.#options.startIdentity ?? `${process.pid}:node`,
|
|
69
|
+
}, ++this.#request);
|
|
70
|
+
});
|
|
71
|
+
transport.once("error", () => finish(false));
|
|
72
|
+
timer = setTimeout(() => { this.#fail(); finish(false); }, timeoutMs);
|
|
73
|
+
timer.unref();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
status(state, degraded = false, pendingRequests = 0, queuedInput = 0) {
|
|
77
|
+
this.#latestStatus = { state, degraded, accent: this.#accent, pendingRequests, queuedInput };
|
|
78
|
+
const recoveryState = state === "unresponsive" || state === "stalled" || state === "crashed" || state === "restarting" || state === "failed";
|
|
79
|
+
if (recoveryState && !this.#supports("recovery-status"))
|
|
80
|
+
return;
|
|
81
|
+
this.#send("bridge.status", {
|
|
82
|
+
seq: ++this.#sequence,
|
|
83
|
+
state,
|
|
84
|
+
pendingRequests: Math.max(0, Math.trunc(pendingRequests)),
|
|
85
|
+
queuedInput: Math.max(0, Math.trunc(queuedInput)),
|
|
86
|
+
...(this.#accent === null ? {} : { accent: this.#accent }),
|
|
87
|
+
...(degraded && this.#supports("recovery-status") ? { degraded: true } : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
degraded(degraded) {
|
|
91
|
+
this.status(this.#latestStatus?.state ?? "idle", degraded);
|
|
92
|
+
}
|
|
93
|
+
accent(accent) {
|
|
94
|
+
this.#accent = accent;
|
|
95
|
+
if (this.#latestStatus !== null)
|
|
96
|
+
this.status(this.#latestStatus.state, this.#latestStatus.degraded, this.#latestStatus.pendingRequests, this.#latestStatus.queuedInput);
|
|
97
|
+
}
|
|
98
|
+
session(file, name, nameSource = "default") {
|
|
99
|
+
this.#latestSession = { file, name, nameSource };
|
|
100
|
+
this.#send("bridge.session", { file, name, nameSource });
|
|
101
|
+
}
|
|
102
|
+
prompt(interrupted) {
|
|
103
|
+
this.#send("bridge.prompt", { interrupted });
|
|
104
|
+
}
|
|
105
|
+
requestDetach(origin = this.captureAttachment(), timeoutMs = 1_000) {
|
|
106
|
+
return this.#requestAction("detach", origin, timeoutMs);
|
|
107
|
+
}
|
|
108
|
+
requestRestart(origin = this.captureAttachment(), timeoutMs = 1_000) {
|
|
109
|
+
return this.#requestAction("restart", origin, timeoutMs);
|
|
110
|
+
}
|
|
111
|
+
consumeControllerBoundaryInput(data) {
|
|
112
|
+
const prefix = "\x1bP+a1;controller=";
|
|
113
|
+
const suffix = "\x1b\\";
|
|
114
|
+
let pending = this.#boundaryInputRemainder + data;
|
|
115
|
+
this.#boundaryInputRemainder = "";
|
|
116
|
+
let output = "";
|
|
117
|
+
while (pending.length > 0) {
|
|
118
|
+
const start = pending.indexOf(prefix);
|
|
119
|
+
if (start === -1) {
|
|
120
|
+
let retained = 0;
|
|
121
|
+
const maximum = Math.min(prefix.length - 1, pending.length);
|
|
122
|
+
for (let length = maximum; length > 0; length--) {
|
|
123
|
+
if (pending.endsWith(prefix.slice(0, length))) {
|
|
124
|
+
retained = length;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const split = pending.length - retained;
|
|
129
|
+
output += pending.slice(0, split);
|
|
130
|
+
this.#boundaryInputRemainder = pending.slice(split);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
output += pending.slice(0, start);
|
|
134
|
+
const end = pending.indexOf(suffix, start + prefix.length);
|
|
135
|
+
if (end === -1) {
|
|
136
|
+
this.#boundaryInputRemainder = pending.slice(start);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
const body = pending.slice(start + prefix.length, end);
|
|
140
|
+
const separator = body.indexOf(";");
|
|
141
|
+
const generation = Number(body.slice(0, separator));
|
|
142
|
+
const nonceHex = body.slice(separator + 1);
|
|
143
|
+
if (separator > 0 && Number.isSafeInteger(generation) && generation > 0 && /^[0-9a-f]{32}$/u.test(nonceHex)) {
|
|
144
|
+
this.#send("bridge.boundary", {
|
|
145
|
+
generation,
|
|
146
|
+
nonce: [...Buffer.from(nonceHex, "hex")],
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
output += pending.slice(start, end + suffix.length);
|
|
151
|
+
}
|
|
152
|
+
pending = pending.slice(end + suffix.length);
|
|
153
|
+
}
|
|
154
|
+
return output;
|
|
155
|
+
}
|
|
156
|
+
close() {
|
|
157
|
+
this.#closed = true;
|
|
158
|
+
if (this.#retry !== null)
|
|
159
|
+
clearTimeout(this.#retry);
|
|
160
|
+
this.#retry = null;
|
|
161
|
+
this.#stopHeartbeat();
|
|
162
|
+
const transport = this.#transport;
|
|
163
|
+
this.#transport = null;
|
|
164
|
+
transport?.destroy();
|
|
165
|
+
this.#fail(false);
|
|
166
|
+
}
|
|
167
|
+
#emitCommand(command) {
|
|
168
|
+
for (const listener of this.#commandListeners)
|
|
169
|
+
listener(command);
|
|
170
|
+
}
|
|
171
|
+
#send(type, body, requestId) {
|
|
172
|
+
if (this.#transport === null || (!this.#connected && type !== "hello"))
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
this.#transport.write(encodeResidentFrame({ type, ...(requestId === undefined ? {} : { requestId }), body }));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
this.#fail();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
#receive(message) {
|
|
182
|
+
if (message.type === "welcome" && message.body["generation"] === 1) {
|
|
183
|
+
const advertised = message.body["features"];
|
|
184
|
+
this.#features = new Set(Array.isArray(advertised) ? advertised.filter((feature) => typeof feature === "string") : []);
|
|
185
|
+
this.#connected = true;
|
|
186
|
+
this.#available = true;
|
|
187
|
+
this.#startWaiter?.(true);
|
|
188
|
+
if (this.#supports("bridge-heartbeat"))
|
|
189
|
+
this.#startHeartbeat();
|
|
190
|
+
if (this.#latestStatus !== null)
|
|
191
|
+
this.status(this.#latestStatus.state, this.#latestStatus.degraded, this.#latestStatus.pendingRequests, this.#latestStatus.queuedInput);
|
|
192
|
+
if (this.#latestSession !== null)
|
|
193
|
+
this.#send("bridge.session", this.#latestSession);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (message.type === "incompatible-generation") {
|
|
197
|
+
this.#fail();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (message.type === "bridge.visibility" && typeof message.body["visible"] === "boolean") {
|
|
201
|
+
this.#emitCommand({ type: "visibility", visible: message.body["visible"] });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (message.type === "bridge.rename" && typeof message.body["name"] === "string") {
|
|
205
|
+
const source = message.body["nameSource"];
|
|
206
|
+
this.#emitCommand({ type: "rename", name: message.body["name"], nameSource: source === "auto" || source === "default" ? source : "user" });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (message.type === "bridge.stop" && typeof message.body["deadlineMs"] === "number") {
|
|
210
|
+
this.#emitCommand({ type: "stop", deadlineMs: message.body["deadlineMs"] });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (message.type === "attachment") {
|
|
214
|
+
const clientId = message.body["clientId"];
|
|
215
|
+
const revision = message.body["controllerRevision"];
|
|
216
|
+
if (typeof clientId === "string" && typeof revision === "number")
|
|
217
|
+
this.#attachment = { clientId, controllerRevision: revision };
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if ((message.type === "bridge.result" || message.type === "result") && typeof message.requestId === "number") {
|
|
221
|
+
const resolvePromise = this.#pending.get(message.requestId);
|
|
222
|
+
if (resolvePromise) {
|
|
223
|
+
this.#pending.delete(message.requestId);
|
|
224
|
+
resolvePromise(message.body["accepted"] === true);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async #requestAction(action, origin, timeoutMs) {
|
|
229
|
+
if (!this.available || origin === null || (action === "restart" && !this.#supports("bridge-restart")))
|
|
230
|
+
return false;
|
|
231
|
+
const requestId = ++this.#request;
|
|
232
|
+
const result = new Promise(resolvePromise => this.#pending.set(requestId, resolvePromise));
|
|
233
|
+
this.#send("bridge.request", { action, clientId: origin.clientId, controllerRevision: origin.controllerRevision }, requestId);
|
|
234
|
+
const timeout = new Promise(resolvePromise => {
|
|
235
|
+
const timer = setTimeout(() => { this.#pending.delete(requestId); resolvePromise(false); }, timeoutMs);
|
|
236
|
+
timer.unref();
|
|
237
|
+
});
|
|
238
|
+
return await Promise.race([result, timeout]);
|
|
239
|
+
}
|
|
240
|
+
#startHeartbeat() {
|
|
241
|
+
if (this.#heartbeat !== null)
|
|
242
|
+
return;
|
|
243
|
+
const schedule = this.#options.setInterval ?? ((callback, milliseconds) => setInterval(callback, milliseconds));
|
|
244
|
+
this.#heartbeat = schedule(() => this.#send("bridge.heartbeat", { seq: ++this.#sequence }), this.#options.heartbeatIntervalMs ?? 5_000);
|
|
245
|
+
this.#heartbeat.unref?.();
|
|
246
|
+
}
|
|
247
|
+
#stopHeartbeat() {
|
|
248
|
+
if (this.#heartbeat === null)
|
|
249
|
+
return;
|
|
250
|
+
(this.#options.clearInterval ?? (timer => clearInterval(timer)))(this.#heartbeat);
|
|
251
|
+
this.#heartbeat = null;
|
|
252
|
+
}
|
|
253
|
+
#supports(feature) {
|
|
254
|
+
return this.#features.has("crash-recovery") || this.#features.has(feature);
|
|
255
|
+
}
|
|
256
|
+
#fail(retry = true) {
|
|
257
|
+
const transport = this.#transport;
|
|
258
|
+
this.#stopHeartbeat();
|
|
259
|
+
this.#available = false;
|
|
260
|
+
this.#connected = false;
|
|
261
|
+
this.#features.clear();
|
|
262
|
+
this.#attachment = null;
|
|
263
|
+
this.#transport = null;
|
|
264
|
+
transport?.destroy();
|
|
265
|
+
this.#startWaiter?.(false);
|
|
266
|
+
this.#startWaiter = null;
|
|
267
|
+
for (const resolvePromise of this.#pending.values())
|
|
268
|
+
resolvePromise(false);
|
|
269
|
+
this.#pending.clear();
|
|
270
|
+
if (retry && !this.#closed && this.#retry === null) {
|
|
271
|
+
this.#retry = setTimeout(() => {
|
|
272
|
+
this.#retry = null;
|
|
273
|
+
void this.start().then(connected => { if (!connected)
|
|
274
|
+
this.#fail(); });
|
|
275
|
+
}, 250);
|
|
276
|
+
this.#retry.unref();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MaintenanceReport } from "./maintenance.js";
|
|
2
|
+
export declare const DOCTOR_MAX_BYTES: number;
|
|
3
|
+
export interface DoctorOptions {
|
|
4
|
+
/** The profile's resident state directory, as reported by the terminal host. */
|
|
5
|
+
readonly stateDir: string;
|
|
6
|
+
readonly hostStatus: MaintenanceReport;
|
|
7
|
+
readonly releaseRoots: readonly string[];
|
|
8
|
+
readonly settings: {
|
|
9
|
+
readonly tabsMax: number;
|
|
10
|
+
readonly tabsAutoName: boolean;
|
|
11
|
+
};
|
|
12
|
+
readonly home?: string;
|
|
13
|
+
readonly platform?: NodeJS.Platform;
|
|
14
|
+
readonly architecture?: string;
|
|
15
|
+
readonly now?: Date;
|
|
16
|
+
readonly maxBytes?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface DoctorBundle {
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly bytes: number;
|
|
21
|
+
readonly dropped: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Writes an owner-only diagnostic bundle by allowlist: structured logs, crash records, host
|
|
25
|
+
* status, a redacted registry, the owner marker without secrets, artifact verification, platform
|
|
26
|
+
* facts, and the Tabs settings. Secrets, tokens, environment values, prompt journals, drafts,
|
|
27
|
+
* last screens, the private child-stderr sink, and terminal content are never read.
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeDoctorBundle(options: DoctorOptions): Promise<DoctorBundle>;
|