@timurproko/a1 0.2.7-dev.754 → 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/guardian.js +5 -0
- package/bin/tab-launch-context.d.ts +23 -0
- package/bin/tab-launch-context.js +45 -0
- package/bin/ui.js +20 -5
- 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 +29 -1
- package/dist/app/session-shell/session-shell-root.js +3 -2
- package/dist/app/session-shell/session-shell.js +153 -30
- 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/composition/owned-ui.d.ts +20 -0
- package/dist/composition/owned-ui.js +76 -3
- package/dist/contracts/owned-ui/session-backend.d.ts +1 -0
- package/dist/features/owned-ui/settings-app.js +8 -35
- package/dist/foundation/launch-context/index.d.ts +6 -0
- package/dist/foundation/launch-context/index.js +15 -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 +4 -0
- package/dist/foundation/release/bootstrap.js +44 -6
- 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 +41 -0
- package/dist/foundation/resident-tabs/bridge-client.js +199 -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 +30 -0
- package/dist/foundation/resident-tabs/ensure-terminal-host.js +121 -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 +6 -0
- package/dist/foundation/resident-tabs/index.js +3 -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/routing.d.ts +10 -0
- package/dist/foundation/resident-tabs/routing.js +8 -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 -0
- package/dist/foundation/resident-tabs/settings.js +65 -0
- 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/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-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.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/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 +49 -1
- package/dist/ui/settings/declarations.js +37 -1
- package/dist/ui/settings/index.d.ts +5 -3
- package/dist/ui/settings/index.js +3 -2
- 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 +32 -0
- package/dist/ui/settings/profile-document.d.ts +26 -0
- package/dist/ui/settings/profile-document.js +164 -0
- package/dist/ui/settings/resolution.d.ts +2 -0
- package/dist/ui/settings/resolution.js +13 -1
- package/dist/ui/settings/sections.js +4 -0
- package/docs/architecture/internal-naming.md +1 -1
- package/docs/architecture/resident-tabs-requirements.md +580 -0
- package/docs/architecture/resident-tabs.md +522 -0
- package/docs/architecture/terminal-host-proof-gate.md +2 -0
- package/docs/openspec-archive-automation.md +17 -16
- package/docs/validation.md +1 -1
- package/package.json +6 -4
|
@@ -4,7 +4,7 @@ import { PRODUCT_IDENTITY } from "../product-identity.js";
|
|
|
4
4
|
import { PromptHistoryService } from "../features/prompt-history/service.js";
|
|
5
5
|
import { PromptImageSidecar } from "../features/prompt-history/image-sidecar.js";
|
|
6
6
|
import { resolvePromptHistoryPath } from "../features/prompt-history/paths.js";
|
|
7
|
-
import { resolvePromptHistoryDataDir } from "../features/launch/profile-paths.js";
|
|
7
|
+
import { resolveLaunchProfilePaths, resolvePromptHistoryDataDir } from "../features/launch/profile-paths.js";
|
|
8
8
|
import { resolveProductPaths } from "../foundation/lifecycle/paths.js";
|
|
9
9
|
import { applyConfiguredPiTheme, getAvailablePiThemes, setPiAccentColor, setPiPackageBorderProjectionEnabled } from "../integrations/pi/components/upstream/theme/theme.js";
|
|
10
10
|
import { createPiEngineHost } from "../integrations/pi/engine/host.js";
|
|
@@ -18,7 +18,13 @@ import { createPiTerminalBridge } from "../integrations/pi/tui-runtime/presentat
|
|
|
18
18
|
import { createOwnedRouteHost } from "./settings-route-host.js";
|
|
19
19
|
import { renderPiShellChangelogLines } from "../integrations/pi/components/shell-presenters-info.js";
|
|
20
20
|
import { nativeHyperlinkStyle } from "../ui/components/spans.js";
|
|
21
|
+
import { ResidentTabRecovery } from "../foundation/resident-tabs/recovery.js";
|
|
21
22
|
const SESSION_RUNTIME_REFRESH_MS = 30_000;
|
|
23
|
+
export function residentStartupPresentation(resident, fallbackNotice) {
|
|
24
|
+
if (resident)
|
|
25
|
+
return { startup: { quiet: true } };
|
|
26
|
+
return fallbackNotice === undefined ? {} : { startup: { notices: [{ kind: "warning", message: fallbackNotice }] } };
|
|
27
|
+
}
|
|
22
28
|
export async function composeOwnedUiApplication(options = {}) {
|
|
23
29
|
return (await composeOwnedUi(options)).application;
|
|
24
30
|
}
|
|
@@ -82,13 +88,16 @@ export async function composeOwnedUi(options = {}) {
|
|
|
82
88
|
sessionRuntimeRefreshTimer.unref();
|
|
83
89
|
};
|
|
84
90
|
const productPaths = resolveProductPaths();
|
|
91
|
+
const profilePaths = resolveLaunchProfilePaths();
|
|
85
92
|
// Rationale: the manager reads the engine's settings port lazily, after the session exists.
|
|
86
93
|
let backend;
|
|
87
94
|
const settings = options.profileId === undefined
|
|
88
95
|
? null
|
|
89
96
|
: new OwnedSettingsManager({
|
|
90
|
-
|
|
97
|
+
profileDir: profilePaths.agentProfile,
|
|
91
98
|
profileId: options.profileId,
|
|
99
|
+
namespace: PRODUCT_IDENTITY.commandName,
|
|
100
|
+
legacyConfigDir: productPaths.configDir,
|
|
92
101
|
agentProvider: () => backend.settingsPort(),
|
|
93
102
|
hiddenAgentSettingIds: ["fullscreenWheelScrollLines"],
|
|
94
103
|
agentSettingLabelOverrides: { fullscreenCopyOnSelect: "Copy on select" },
|
|
@@ -120,6 +129,18 @@ export async function composeOwnedUi(options = {}) {
|
|
|
120
129
|
await host.dispose();
|
|
121
130
|
throw error;
|
|
122
131
|
}
|
|
132
|
+
const tabBridge = options.tab?.bridge ?? null;
|
|
133
|
+
let recoveryDegraded = false;
|
|
134
|
+
let presentRecoveryDegraded = (_degraded) => { };
|
|
135
|
+
const tabRecovery = options.tab === undefined ? null : new ResidentTabRecovery(options.tab.dataDir, {
|
|
136
|
+
onDegraded: degraded => {
|
|
137
|
+
recoveryDegraded = degraded;
|
|
138
|
+
options.tab.bridge.degraded(degraded);
|
|
139
|
+
presentRecoveryDegraded(degraded);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const recoveredTabInput = await tabRecovery?.initialize(currentBackendSessionFile(backend), backend.identity.sessionId);
|
|
143
|
+
const unsubscribeTabBridge = tabBridge === null ? () => { } : await bindResidentTabBridge(tabBridge, backend, tabRecovery);
|
|
123
144
|
let releaseNotes = null;
|
|
124
145
|
let releaseNotesFailure;
|
|
125
146
|
if (ownedSurfaces) {
|
|
@@ -228,13 +249,26 @@ export async function composeOwnedUi(options = {}) {
|
|
|
228
249
|
},
|
|
229
250
|
}),
|
|
230
251
|
...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
|
|
252
|
+
...(tabBridge === null || tabRecovery === null ? {} : { residentTab: {
|
|
253
|
+
requestDetach: () => tabBridge.requestDetach(),
|
|
254
|
+
requestRestart: () => tabBridge.requestRestart(),
|
|
255
|
+
reportStatus: status => tabBridge.status(status),
|
|
256
|
+
stallNoticeSeconds: settings?.value("tabsStallNoticeSeconds") ?? 300,
|
|
257
|
+
admit: input => tabRecovery.admit({ ...input, incarnation: options.tab.incarnation }),
|
|
258
|
+
dispatched: () => tabRecovery.dispatched(),
|
|
259
|
+
correlated: (submissionId, entryId) => tabRecovery.correlated(submissionId, entryId),
|
|
260
|
+
checkpoint: text => tabRecovery.checkpoint(text),
|
|
261
|
+
gracefulStop: sessionFile => tabRecovery.gracefulStop(sessionFile),
|
|
262
|
+
...(recoveredTabInput === undefined ? {} : { recovered: recoveredTabInput }),
|
|
263
|
+
} }),
|
|
231
264
|
},
|
|
232
265
|
presentation: {
|
|
233
266
|
...(options.terminal === undefined ? {} : { terminal: createPiTerminalBridge(options.terminal) }),
|
|
267
|
+
...residentStartupPresentation(tabBridge !== null, options.residentFallbackNotice),
|
|
234
268
|
...(clipboardDiagnostics === null ? {} : { input: { onEvent: event => clipboardDiagnostics.runtime(event) } }),
|
|
235
269
|
...(viewportSettings === null ? {} : { viewportSettings }),
|
|
236
270
|
...(backgroundSettings === null ? {} : { backgroundSettings }),
|
|
237
|
-
...(settings === null || !ownedSurfaces ? {} : {
|
|
271
|
+
...(settings === null || !ownedSurfaces || tabBridge !== null ? {} : {
|
|
238
272
|
quitOutro: { snapshot: () => quitOutroSettingsSnapshot(settings), interactive: process.stdout.isTTY === true },
|
|
239
273
|
}),
|
|
240
274
|
},
|
|
@@ -255,6 +289,14 @@ export async function composeOwnedUi(options = {}) {
|
|
|
255
289
|
// the host and paints once attached.
|
|
256
290
|
terminalHost = createdTerminalHost = new OwnedUiTerminalHost(sessionTerminalHostOptions(presenterOptions));
|
|
257
291
|
presenter = new OwnedUiSessionPresenter(terminalHost, presenterOptions);
|
|
292
|
+
presentRecoveryDegraded = degraded => {
|
|
293
|
+
if (!degraded)
|
|
294
|
+
return;
|
|
295
|
+
presenter.root.appendWorkflowStatus("draft recovery degraded");
|
|
296
|
+
terminalHost.runtime.requestRender();
|
|
297
|
+
};
|
|
298
|
+
if (recoveryDegraded)
|
|
299
|
+
presentRecoveryDegraded(true);
|
|
258
300
|
terminalHost.attach(presenter);
|
|
259
301
|
}
|
|
260
302
|
catch (error) {
|
|
@@ -323,6 +365,8 @@ export async function composeOwnedUi(options = {}) {
|
|
|
323
365
|
await releaseNoteClaim?.release();
|
|
324
366
|
suggestionDiagnostics?.dispose();
|
|
325
367
|
clipboardDiagnostics?.dispose();
|
|
368
|
+
unsubscribeTabBridge();
|
|
369
|
+
tabBridge?.close();
|
|
326
370
|
// Invariant: the host outlives the application; disposing it cancels host-level probes last.
|
|
327
371
|
await host.dispose();
|
|
328
372
|
}
|
|
@@ -330,6 +374,35 @@ export async function composeOwnedUi(options = {}) {
|
|
|
330
374
|
};
|
|
331
375
|
return { application, settings };
|
|
332
376
|
}
|
|
377
|
+
export async function bindResidentTabBridge(tabBridge, backend, recovery = null) {
|
|
378
|
+
await tabBridge.start().catch(() => false);
|
|
379
|
+
tabBridge.status("starting");
|
|
380
|
+
const currentSessionFile = () => currentBackendSessionFile(backend);
|
|
381
|
+
const announceSession = () => {
|
|
382
|
+
tabBridge.session(currentSessionFile(), backend.session.view().status.footer?.sessionName ?? null);
|
|
383
|
+
};
|
|
384
|
+
let working = false;
|
|
385
|
+
announceSession();
|
|
386
|
+
return backend.session.onEvent(event => {
|
|
387
|
+
if (event.type === "session-lifecycle") {
|
|
388
|
+
const wasWorking = working;
|
|
389
|
+
working = event.lifecycle === "busy";
|
|
390
|
+
tabBridge.status(working ? "working" : event.lifecycle === "starting" ? "starting" : "idle");
|
|
391
|
+
if (wasWorking && !working && recovery !== null) {
|
|
392
|
+
void recovery.settled(currentSessionFile()).catch(() => tabBridge.degraded(true));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (event.type === "session-view")
|
|
396
|
+
announceSession();
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function currentBackendSessionFile(backend) {
|
|
400
|
+
const direct = backend.identity.currentSessionFile?.();
|
|
401
|
+
if (direct !== undefined && direct !== null)
|
|
402
|
+
return direct;
|
|
403
|
+
const resume = backend.identity.currentSessionResumeMetadata();
|
|
404
|
+
return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
|
|
405
|
+
}
|
|
333
406
|
async function readPackageVersion() {
|
|
334
407
|
const manifest = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
|
|
335
408
|
const version = manifest?.version;
|
|
@@ -36,6 +36,7 @@ export interface OwnedUiSessionIdentityPort {
|
|
|
36
36
|
/** Actual session replacements, excluding delivery-only invalidation of callbacks. */
|
|
37
37
|
readonly sessionBindingGeneration: number;
|
|
38
38
|
readonly disposed: boolean;
|
|
39
|
+
currentSessionFile(): string | null;
|
|
39
40
|
currentSessionResumeMetadata(): OwnedUiSessionResumeMetadata | null;
|
|
40
41
|
}
|
|
41
42
|
/** The session lifecycle, its view, its event stream, and command admission. */
|
|
@@ -94,12 +94,10 @@ export class SettingsApp {
|
|
|
94
94
|
// Invariant: keyboard navigation requests visibility once; pointer scrolling then stays free.
|
|
95
95
|
#selectionNeedsReveal = true;
|
|
96
96
|
#reveal;
|
|
97
|
-
#notice = null;
|
|
98
97
|
#filter = null;
|
|
99
98
|
#scrollBeforeFilter = null;
|
|
100
99
|
#menu = null;
|
|
101
100
|
#structured = null;
|
|
102
|
-
#interruptArmed = false;
|
|
103
101
|
// Invariant: pending values remain visible until the source reflects them.
|
|
104
102
|
#pending = new Map();
|
|
105
103
|
#undoHistory = [];
|
|
@@ -138,7 +136,6 @@ export class SettingsApp {
|
|
|
138
136
|
}
|
|
139
137
|
render(rect, host) {
|
|
140
138
|
const theme = host.theme ?? PLAIN_THEME;
|
|
141
|
-
this.#interruptArmed = host.interruptArmed;
|
|
142
139
|
const rows = this.#rows();
|
|
143
140
|
const selected = indexOfKey(rows, this.#selectedKey);
|
|
144
141
|
const footer = this.#footerLines(rect.width, theme);
|
|
@@ -259,7 +256,6 @@ export class SettingsApp {
|
|
|
259
256
|
case "open-filter":
|
|
260
257
|
this.#scrollBeforeFilter = this.#scroll;
|
|
261
258
|
this.#filter = new LineInput("");
|
|
262
|
-
this.#notice = null;
|
|
263
259
|
return { consumed: true };
|
|
264
260
|
case "activate": {
|
|
265
261
|
const row = rows[selected];
|
|
@@ -385,7 +381,6 @@ export class SettingsApp {
|
|
|
385
381
|
const index = rows.findIndex(candidate => rowKey(candidate) === row.key);
|
|
386
382
|
if (index >= 0) {
|
|
387
383
|
// Rationale: the pointer acts where it points; the arrow belongs to the keyboard.
|
|
388
|
-
this.#notice = null;
|
|
389
384
|
if (this.#hoverRegion === "minus")
|
|
390
385
|
this.#cycle(rows, index, -1);
|
|
391
386
|
else if (this.#hoverRegion === "plus")
|
|
@@ -458,10 +453,8 @@ export class SettingsApp {
|
|
|
458
453
|
// and pointing at it is not a request for anything else.
|
|
459
454
|
if (typeof shown === "number")
|
|
460
455
|
return;
|
|
461
|
-
if (!entry.editable || entry.choices === null || entry.choices.length === 0)
|
|
462
|
-
this.#notice = `${labelOf(entry)} cannot be changed here`;
|
|
456
|
+
if (!entry.editable || entry.choices === null || entry.choices.length === 0)
|
|
463
457
|
return;
|
|
464
|
-
}
|
|
465
458
|
const current = shown === null ? 0 : Math.max(0, entry.choices.indexOf(shown));
|
|
466
459
|
const anchorKey = `${entry.backend}:${entry.id}`;
|
|
467
460
|
// Compatibility: pointer opening already brightens the source value. Enter borrows that
|
|
@@ -492,10 +485,8 @@ export class SettingsApp {
|
|
|
492
485
|
#openStructured(entry) {
|
|
493
486
|
// Invariant: the flags come from the declaration, not from the stored value: an unset
|
|
494
487
|
// flag still has a row, showing the default the source would apply.
|
|
495
|
-
if (entry.flags.length === 0)
|
|
496
|
-
this.#notice = `${labelOf(entry)} has nothing to configure`;
|
|
488
|
+
if (entry.flags.length === 0)
|
|
497
489
|
return;
|
|
498
|
-
}
|
|
499
490
|
const stored = typeof entry.rawValue === "object" && entry.rawValue !== null && !Array.isArray(entry.rawValue)
|
|
500
491
|
? entry.rawValue
|
|
501
492
|
: {};
|
|
@@ -648,13 +639,11 @@ export class SettingsApp {
|
|
|
648
639
|
const failure = changeFailure(outcome);
|
|
649
640
|
if (failure === null) {
|
|
650
641
|
this.#recordUndo({ kind: "structured", sequence, entry: open.entry, previous, forward });
|
|
651
|
-
this.#notice = null;
|
|
652
642
|
return;
|
|
653
643
|
}
|
|
654
644
|
// Concurrency: only roll back the dialog if no later press has moved the record on.
|
|
655
645
|
if (this.#structured === open && sameRecord(open.record, forward))
|
|
656
646
|
replaceRecord(open.record, previous);
|
|
657
|
-
this.#notice = `Could not save ${labelOf(open.entry)}: ${failure}`;
|
|
658
647
|
});
|
|
659
648
|
}
|
|
660
649
|
#menuKey(data) {
|
|
@@ -744,10 +733,8 @@ export class SettingsApp {
|
|
|
744
733
|
if (row === undefined || row.kind !== "element")
|
|
745
734
|
return;
|
|
746
735
|
const entry = row.value;
|
|
747
|
-
if (!entry.editable)
|
|
748
|
-
this.#notice = `${labelOf(entry)} cannot be changed here`;
|
|
736
|
+
if (!entry.editable)
|
|
749
737
|
return;
|
|
750
|
-
}
|
|
751
738
|
const shown = this.#shownValue(entry);
|
|
752
739
|
if (typeof shown === "number") {
|
|
753
740
|
// Rationale: at the end of the range there is nothing to say: the arrow already reads
|
|
@@ -758,10 +745,8 @@ export class SettingsApp {
|
|
|
758
745
|
return;
|
|
759
746
|
}
|
|
760
747
|
const choices = entry.choices;
|
|
761
|
-
if (choices === null || choices.length === 0)
|
|
762
|
-
this.#notice = `${labelOf(entry)} cannot be changed here`;
|
|
748
|
+
if (choices === null || choices.length === 0)
|
|
763
749
|
return;
|
|
764
|
-
}
|
|
765
750
|
const current = shown === null ? -1 : choices.indexOf(shown);
|
|
766
751
|
const next = choices[current < 0
|
|
767
752
|
? (delta > 0 ? 0 : choices.length - 1)
|
|
@@ -781,7 +766,6 @@ export class SettingsApp {
|
|
|
781
766
|
if (failure !== null) {
|
|
782
767
|
if (this.#pending.get(key) === value)
|
|
783
768
|
this.#pending.delete(key);
|
|
784
|
-
this.#notice = `Could not save ${labelOf(entry)}: ${failure}`;
|
|
785
769
|
return;
|
|
786
770
|
}
|
|
787
771
|
if (previous !== null && previous !== value)
|
|
@@ -789,7 +773,6 @@ export class SettingsApp {
|
|
|
789
773
|
// Concurrency: a later press may have moved on; only the last request clears itself.
|
|
790
774
|
if (this.#pending.get(key) === value)
|
|
791
775
|
this.#pending.delete(key);
|
|
792
|
-
this.#notice = null;
|
|
793
776
|
});
|
|
794
777
|
}
|
|
795
778
|
#shownValue(entry) {
|
|
@@ -841,13 +824,8 @@ export class SettingsApp {
|
|
|
841
824
|
}
|
|
842
825
|
#finishUndo(record, outcome) {
|
|
843
826
|
const failure = changeFailure(outcome);
|
|
844
|
-
if (failure !== null)
|
|
827
|
+
if (failure !== null)
|
|
845
828
|
this.#recordUndo(record);
|
|
846
|
-
this.#notice = `Could not restore ${labelOf(record.entry)}: ${failure}`;
|
|
847
|
-
}
|
|
848
|
-
else {
|
|
849
|
-
this.#notice = null;
|
|
850
|
-
}
|
|
851
829
|
this.#undoActive = false;
|
|
852
830
|
this.#drainUndo();
|
|
853
831
|
}
|
|
@@ -878,7 +856,6 @@ export class SettingsApp {
|
|
|
878
856
|
return;
|
|
879
857
|
this.#selectedKey = rowKey(rows[index]);
|
|
880
858
|
this.#selectionNeedsReveal = true;
|
|
881
|
-
this.#notice = null;
|
|
882
859
|
}
|
|
883
860
|
#rows() {
|
|
884
861
|
const needle = this.#filter?.value.trim().toLowerCase() ?? "";
|
|
@@ -1037,13 +1014,9 @@ export class SettingsApp {
|
|
|
1037
1014
|
const open = this.#structured;
|
|
1038
1015
|
if (open !== null)
|
|
1039
1016
|
return this.#dialogLines(open, width, theme);
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
: this.#interruptArmed ? "press ctrl+c again to exit a1" : null;
|
|
1044
|
-
const status = plainStatus === null
|
|
1045
|
-
? renderShortcutHintsWithClose(SETTINGS_SHORTCUTS.hintEntries(SCOPE), theme, width, width > 0 ? 1 : 0)
|
|
1046
|
-
: truncateToWidth(`${width > 0 ? " " : ""}${theme.fg("dim", plainStatus)}`, width);
|
|
1017
|
+
// Rationale: standing guidance is more useful than transient feedback; failures still
|
|
1018
|
+
// roll back without displacing the Settings interaction contract.
|
|
1019
|
+
const status = renderShortcutHintsWithClose(SETTINGS_SHORTCUTS.hintEntries(SCOPE), theme, width, width > 0 ? 1 : 0);
|
|
1047
1020
|
const input = this.#filter;
|
|
1048
1021
|
if (input === null)
|
|
1049
1022
|
return [status];
|
|
@@ -7,6 +7,9 @@ export declare const PRIVATE_ENVIRONMENT: Readonly<{
|
|
|
7
7
|
releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS";
|
|
8
8
|
launchProfile: "LAUNCH_CONTEXT_PROFILE";
|
|
9
9
|
immutableWarmup: "LAUNCH_CONTEXT_WARMUP";
|
|
10
|
+
residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE";
|
|
11
|
+
residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS";
|
|
12
|
+
residentFallback: "LAUNCH_CONTEXT_RESIDENT_FALLBACK";
|
|
10
13
|
}>;
|
|
11
14
|
/**
|
|
12
15
|
* Keys published by releases that predate the neutral cutover. They are read when
|
|
@@ -31,6 +34,9 @@ export interface LaunchContext {
|
|
|
31
34
|
readonly releaseLayers?: string;
|
|
32
35
|
readonly launchProfile?: "a1" | "pi";
|
|
33
36
|
readonly immutableWarmup?: "1";
|
|
37
|
+
readonly residentRootExecutable?: string;
|
|
38
|
+
readonly residentRootArguments?: string;
|
|
39
|
+
readonly residentFallback?: string;
|
|
34
40
|
}
|
|
35
41
|
export type LaunchContextRequirement = "optional" | "profile" | "release" | "warmup";
|
|
36
42
|
/** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
|
|
@@ -7,6 +7,9 @@ export const PRIVATE_ENVIRONMENT = Object.freeze({
|
|
|
7
7
|
releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS",
|
|
8
8
|
launchProfile: "LAUNCH_CONTEXT_PROFILE",
|
|
9
9
|
immutableWarmup: "LAUNCH_CONTEXT_WARMUP",
|
|
10
|
+
residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE",
|
|
11
|
+
residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS",
|
|
12
|
+
residentFallback: "LAUNCH_CONTEXT_RESIDENT_FALLBACK",
|
|
10
13
|
});
|
|
11
14
|
/**
|
|
12
15
|
* Keys published by releases that predate the neutral cutover. They are read when
|
|
@@ -63,6 +66,18 @@ export function readLaunchContext(environment, requirement = "optional", platfor
|
|
|
63
66
|
}
|
|
64
67
|
if (values.immutableWarmup !== undefined && values.immutableWarmup !== "1")
|
|
65
68
|
throw invalidContext("immutableWarmup", "invalid permission flag");
|
|
69
|
+
if ((values.residentRootExecutable === undefined) !== (values.residentRootArguments === undefined))
|
|
70
|
+
throw invalidContext("residentRootExecutable", "resident root command is incomplete");
|
|
71
|
+
if (values.residentRootArguments !== undefined) {
|
|
72
|
+
try {
|
|
73
|
+
const argumentsValue = JSON.parse(values.residentRootArguments);
|
|
74
|
+
if (!Array.isArray(argumentsValue) || argumentsValue.some(value => typeof value !== "string"))
|
|
75
|
+
throw new Error("not string arguments");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw invalidContext("residentRootArguments", "invalid JSON argument list");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
66
81
|
if (values.releaseDigest !== undefined && !/^[a-f0-9]{64}$/.test(values.releaseDigest))
|
|
67
82
|
throw invalidContext("releaseDigest", "invalid digest");
|
|
68
83
|
if (requirement === "profile")
|
|
@@ -16,11 +16,16 @@ interface GuardianControl {
|
|
|
16
16
|
off(event: "disconnect", listener: () => void): unknown;
|
|
17
17
|
close(): void;
|
|
18
18
|
}
|
|
19
|
+
export interface LaunchGuardianRootCommand {
|
|
20
|
+
readonly executable: string;
|
|
21
|
+
readonly arguments: readonly string[];
|
|
22
|
+
}
|
|
19
23
|
export interface LaunchGuardianOptions {
|
|
20
24
|
readonly profileId: LaunchProfileId;
|
|
21
25
|
readonly releaseRoot: string;
|
|
22
26
|
readonly uiEntry: string;
|
|
23
27
|
readonly sessionSelection?: SessionSelection;
|
|
28
|
+
readonly rootCommand?: LaunchGuardianRootCommand;
|
|
24
29
|
readonly environment?: NodeJS.ProcessEnv;
|
|
25
30
|
readonly cwd?: string;
|
|
26
31
|
readonly helperPath?: string;
|
|
@@ -44,7 +44,8 @@ export async function runLaunchGuardian(options) {
|
|
|
44
44
|
});
|
|
45
45
|
let handle;
|
|
46
46
|
try {
|
|
47
|
-
|
|
47
|
+
const rootCommand = options.rootCommand ?? { executable: process.execPath, arguments: [options.uiEntry, ...sessionArgs] };
|
|
48
|
+
handle = await containment.spawn(rootCommand.executable, [...rootCommand.arguments], {
|
|
48
49
|
cwd: options.cwd ?? process.cwd(),
|
|
49
50
|
environment: environmentEntries(environment),
|
|
50
51
|
...(environment.TERM ? { terminalType: environment.TERM } : {}),
|
|
@@ -2,6 +2,7 @@ import { type SupervisorEndpointMetadata } from "./cohort-state.js";
|
|
|
2
2
|
import { resolveProductPaths, type SessionSelection, type LaunchProfileId, type SupervisorStartupAttemptIdentity } from "../lifecycle/index.js";
|
|
3
3
|
import { cleanupProvenIdleOwner } from "./process-cleanup.js";
|
|
4
4
|
import { type MaterializedRelease, type VerifyMaterializedReleaseOptions } from "./release-store.js";
|
|
5
|
+
import { type EnsureTerminalHostOptions, type EnsureTerminalHostResult } from "../resident-tabs/index.js";
|
|
5
6
|
export interface BootstrapOptions {
|
|
6
7
|
readonly packageRoot: string;
|
|
7
8
|
readonly launchIntent?: {
|
|
@@ -23,6 +24,9 @@ export interface BootstrapOptions {
|
|
|
23
24
|
readonly releaseIsLaunchable?: (releaseRoot: string) => boolean;
|
|
24
25
|
/** Test seam for bounded maintenance scheduled after stale endpoint sweeping. */
|
|
25
26
|
readonly scheduleMaintenance?: (dataDir: string, paths: ReturnType<typeof resolveProductPaths>) => Promise<void>;
|
|
27
|
+
/** Test seams for resident launch routing before the guardian starts. */
|
|
28
|
+
readonly readResidentTabs?: (profileDir: string, namespace: string, legacyConfigDir: string, profileId: string) => Promise<boolean>;
|
|
29
|
+
readonly ensureTerminalHost?: (options: EnsureTerminalHostOptions) => Promise<EnsureTerminalHostResult>;
|
|
26
30
|
}
|
|
27
31
|
export declare function runBootstrap(options: BootstrapOptions): Promise<number>;
|
|
28
32
|
export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string, verification?: VerifyMaterializedReleaseOptions): Promise<string>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { restoreAfterOwnedExit } from "../terminal-cleanup/index.js";
|
|
2
|
+
import { CLEAR_EXIT_INTERVAL_MS, restoreAfterOwnedExit } from "../terminal-cleanup/index.js";
|
|
3
3
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { connect } from "node:net";
|
|
5
5
|
import { resolve } from "node:path";
|
|
@@ -15,9 +15,10 @@ import { selectUpdateLaunchRelease } from "./update-launch.js";
|
|
|
15
15
|
import { consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
|
|
16
16
|
import { scheduleReleaseCleanup } from "./release-gc.js";
|
|
17
17
|
import { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument } from "./restart-certification.js";
|
|
18
|
-
import { PRODUCT_TEXT } from "../../product-identity.js";
|
|
18
|
+
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
19
19
|
import { markStartupPhase } from "../startup/index.js";
|
|
20
|
-
import { launchContractTarget, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
|
|
20
|
+
import { isCurrentLaunchContract, launchContractTarget, readLaunchContext, withLaunchContext } from "../launch-context/index.js";
|
|
21
|
+
import { ensureTerminalHost, readResidentTabLaunchSettings, selectResidentHost } from "../resident-tabs/index.js";
|
|
21
22
|
const RELEASE_RESELECTION_MESSAGE = "a1-release-reselection";
|
|
22
23
|
export async function runBootstrap(options) {
|
|
23
24
|
return await runBootstrapAttempt(options, 1);
|
|
@@ -34,7 +35,42 @@ async function runBootstrapAttempt(options, retriesRemaining) {
|
|
|
34
35
|
const paths = resolveProductPaths(environment);
|
|
35
36
|
await mkdir(paths.runtimeDir, { recursive: true, mode: 0o700 });
|
|
36
37
|
const launchSelected = async (release) => {
|
|
37
|
-
|
|
38
|
+
let residentContext = {};
|
|
39
|
+
// Compatibility: public launch preparation supplies PI_CODING_AGENT_DIR; the fallback keeps
|
|
40
|
+
// direct SDK/bootstrap callers hermetic while retaining their former config-root read.
|
|
41
|
+
const agentProfileDir = environment.PI_CODING_AGENT_DIR ?? paths.configDir;
|
|
42
|
+
const residentLaunchSettings = launchProfileId === "a1"
|
|
43
|
+
? readResidentTabLaunchSettings(agentProfileDir, PRODUCT_IDENTITY.commandName, paths.configDir, launchProfileId)
|
|
44
|
+
: null;
|
|
45
|
+
const resident = await selectResidentHost({
|
|
46
|
+
profileId: launchProfileId,
|
|
47
|
+
hasSessionSelection: sessionArgs.length > 0,
|
|
48
|
+
currentLaunchContract: isCurrentLaunchContract(release),
|
|
49
|
+
readSetting: options.readResidentTabs === undefined
|
|
50
|
+
? async () => (await residentLaunchSettings).residentTabs
|
|
51
|
+
: () => options.readResidentTabs(agentProfileDir, PRODUCT_IDENTITY.commandName, paths.configDir, launchProfileId),
|
|
52
|
+
ensureHost: async () => (options.ensureTerminalHost ?? ensureTerminalHost)({
|
|
53
|
+
releaseRoot: release.releaseRoot,
|
|
54
|
+
profileRoot: environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir,
|
|
55
|
+
dataDir: paths.dataDir,
|
|
56
|
+
runtimeDir: paths.runtimeDir,
|
|
57
|
+
environment,
|
|
58
|
+
maxConcurrentStarts: (await residentLaunchSettings).maxConcurrentStarts,
|
|
59
|
+
unresponsiveRestartSeconds: (await residentLaunchSettings).unresponsiveRestartSeconds,
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
if (resident !== null) {
|
|
63
|
+
residentContext = resident.status === "ready" ? {
|
|
64
|
+
residentRootExecutable: resident.executable,
|
|
65
|
+
residentRootArguments: JSON.stringify([
|
|
66
|
+
"attach", "--endpoint", resident.endpoint, "--cwd", process.cwd(), "--clear-exit-ms", String(CLEAR_EXIT_INTERVAL_MS),
|
|
67
|
+
"--profile", environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir,
|
|
68
|
+
"--data-dir", paths.dataDir, "--runtime-dir", paths.runtimeDir,
|
|
69
|
+
"--release-root", release.releaseRoot, "--node-executable", process.execPath,
|
|
70
|
+
]),
|
|
71
|
+
} : { residentFallback: resident.reason };
|
|
72
|
+
}
|
|
73
|
+
const result = await launchUi(release, environment, sessionArgs, residentContext);
|
|
38
74
|
if (!result.releaseSuperseded)
|
|
39
75
|
return result.code;
|
|
40
76
|
if (retriesRemaining > 0)
|
|
@@ -327,13 +363,15 @@ export async function startSupervisor(release, environment) {
|
|
|
327
363
|
child.unref();
|
|
328
364
|
return { ...attempt, childOutcome };
|
|
329
365
|
}
|
|
330
|
-
async function launchUi(release, environment, sessionArgs) {
|
|
366
|
+
async function launchUi(release, environment, sessionArgs, residentContext = {}) {
|
|
331
367
|
await markStartupPhase(environment, "bootstrap-selected");
|
|
332
368
|
const entry = await resolveReleaseEntryPoint(release, "bin/guardian.js");
|
|
333
369
|
return await new Promise((resolvePromise, rejectPromise) => {
|
|
334
370
|
let releaseSuperseded = false;
|
|
371
|
+
const releaseEnv = releaseEnvironment(environment, release, readLaunchContext(environment, "profile").launchProfile);
|
|
372
|
+
const launchEnvironment = Object.keys(residentContext).length === 0 ? releaseEnv : withLaunchContext(releaseEnv, { ...readLaunchContext(releaseEnv), ...residentContext }, process.platform, launchContractTarget(release));
|
|
335
373
|
const child = spawn(process.execPath, [entry, ...sessionArgs], {
|
|
336
|
-
env:
|
|
374
|
+
env: launchEnvironment,
|
|
337
375
|
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
338
376
|
windowsHide: false,
|
|
339
377
|
});
|
|
@@ -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";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type ResidentTabStatus = "starting" | "working" | "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
|
+
interface BridgeTransport {
|
|
12
|
+
write(bytes: Uint8Array): boolean;
|
|
13
|
+
destroy(): void;
|
|
14
|
+
once(event: "connect" | "error" | "close", listener: (...args: unknown[]) => void): unknown;
|
|
15
|
+
on(event: "data", listener: (bytes: Buffer) => void): unknown;
|
|
16
|
+
}
|
|
17
|
+
export interface TabBridgeOptions {
|
|
18
|
+
readonly credentials: TabBridgeCredentials;
|
|
19
|
+
readonly pid?: number;
|
|
20
|
+
readonly startIdentity?: string;
|
|
21
|
+
readonly build?: string;
|
|
22
|
+
readonly connect?: (endpoint: string) => BridgeTransport;
|
|
23
|
+
readonly heartbeatIntervalMs?: number;
|
|
24
|
+
readonly setInterval?: (callback: () => void, milliseconds: number) => unknown;
|
|
25
|
+
readonly clearInterval?: (timer: unknown) => void;
|
|
26
|
+
}
|
|
27
|
+
/** Metadata-only advisory bridge. Terminal input and transcript bytes have no API here. */
|
|
28
|
+
export declare class TabBridgeClient {
|
|
29
|
+
#private;
|
|
30
|
+
constructor(options: TabBridgeOptions);
|
|
31
|
+
get available(): boolean;
|
|
32
|
+
captureAttachment(): TabAttachment | null;
|
|
33
|
+
start(timeoutMs?: number): Promise<boolean>;
|
|
34
|
+
status(state: ResidentTabStatus, degraded?: boolean): void;
|
|
35
|
+
degraded(degraded: boolean): void;
|
|
36
|
+
session(file: string | null, name: string | null): void;
|
|
37
|
+
requestDetach(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
|
|
38
|
+
requestRestart(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
|
|
39
|
+
close(): void;
|
|
40
|
+
}
|
|
41
|
+
export {};
|