@timurproko/a1 0.2.6-dev.727 → 0.2.6-dev.736
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/dist/app/session-shell/clipboard-services.d.ts +19 -0
- package/dist/app/session-shell/clipboard-services.js +7 -0
- package/dist/app/session-shell/exit-notice.d.ts +9 -5
- package/dist/app/session-shell/exit-notice.js +5 -5
- package/dist/app/session-shell/index.d.ts +4 -2
- package/dist/app/session-shell/index.js +2 -1
- package/dist/app/session-shell/paste-preparation-client.d.ts +3 -0
- package/dist/app/session-shell/paste-preparation-client.js +5 -2
- package/dist/app/session-shell/path-chip-presentation.d.ts +10 -3
- package/dist/app/session-shell/path-chip-presentation.js +48 -6
- package/dist/app/session-shell/prompt-chips.js +30 -4
- package/dist/app/session-shell/quit-outro.d.ts +3 -1
- package/dist/app/session-shell/quit-outro.js +23 -8
- package/dist/app/session-shell/response-copy-transport.d.ts +6 -1
- package/dist/app/session-shell/response-copy-transport.js +8 -2
- package/dist/app/session-shell/session-shell-root.d.ts +29 -11
- package/dist/app/session-shell/session-shell-root.js +62 -53
- package/dist/app/session-shell/session-shell.d.ts +55 -29
- package/dist/app/session-shell/session-shell.js +748 -1132
- package/dist/app/session-shell/shell-host.d.ts +105 -0
- package/dist/app/session-shell/shell-host.js +523 -0
- package/dist/cli/session-context.js +6 -3
- package/dist/composition/owned-ui.d.ts +20 -2
- package/dist/composition/owned-ui.js +107 -56
- package/dist/composition/session-info-reference.d.ts +2 -2
- package/dist/composition/session-info-reference.js +2 -2
- package/dist/contracts/agent-engine/capability-ports.d.ts +1 -85
- package/dist/contracts/agent-engine/capability-ports.js +1 -35
- package/dist/contracts/agent-engine/index.d.ts +1 -7
- package/dist/contracts/agent-engine/index.js +0 -4
- package/dist/contracts/owned-ui/index.d.ts +5 -1
- package/dist/contracts/owned-ui/index.js +1 -0
- package/dist/contracts/owned-ui/model.d.ts +6 -0
- package/dist/contracts/owned-ui/session-backend.d.ts +119 -0
- package/dist/contracts/owned-ui/session-presenters.d.ts +82 -0
- package/dist/contracts/owned-ui/session-presenters.js +1 -0
- package/dist/contracts/owned-ui/session-workflows.d.ts +342 -0
- package/dist/contracts/owned-ui/session-workflows.js +30 -0
- package/dist/features/launch/index.d.ts +1 -1
- package/dist/features/launch/index.js +1 -1
- package/dist/features/launch/session-context.d.ts +1 -0
- package/dist/features/launch/session-context.js +1 -0
- package/dist/features/owned-ui/settings-app.js +35 -15
- package/dist/foundation/lifecycle/index.d.ts +1 -1
- package/dist/foundation/lifecycle/index.js +1 -1
- package/dist/foundation/lifecycle/session-repository-context.d.ts +8 -2
- package/dist/foundation/lifecycle/session-repository-context.js +200 -65
- package/dist/foundation/release/bootstrap.js +1 -2
- package/dist/foundation/release/release-gc.js +1 -1
- package/dist/foundation/release/update.js +0 -4
- package/dist/foundation/storage/control-store.js +0 -12
- package/dist/integrations/pi/components/components.js +0 -16
- package/dist/integrations/pi/components/index.d.ts +3 -3
- package/dist/integrations/pi/components/index.js +2 -2
- package/dist/integrations/pi/components/models-dialog.js +1 -30
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +11 -9
- package/dist/integrations/pi/components/shell-extension-ui.d.ts +3 -3
- package/dist/integrations/pi/components/shell-extension-ui.js +6 -2
- package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -4
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +21 -1
- package/dist/integrations/pi/components/shell-shared-facade.js +18 -2
- package/dist/integrations/pi/components/upstream/theme/theme.d.ts +5 -1
- package/dist/integrations/pi/components/upstream/theme/theme.js +20 -2
- package/dist/integrations/pi/engine/adapter.d.ts +44 -24
- package/dist/integrations/pi/engine/adapter.js +130 -22
- package/dist/integrations/pi/engine/command-dispatch.d.ts +2 -5
- package/dist/integrations/pi/engine/extension-ui-binding.d.ts +2 -11
- package/dist/integrations/pi/engine/host-ports.d.ts +20 -0
- package/dist/integrations/pi/engine/host-ports.js +13 -0
- package/dist/integrations/pi/engine/host.d.ts +70 -0
- package/dist/integrations/pi/engine/host.js +147 -0
- package/dist/integrations/pi/engine/index.d.ts +8 -4
- package/dist/integrations/pi/engine/index.js +3 -1
- package/dist/integrations/pi/engine/resource-catalog.d.ts +4 -23
- package/dist/integrations/pi/engine/runtime-integration.d.ts +8 -2
- package/dist/integrations/pi/engine/runtime-integration.js +12 -7
- package/dist/integrations/pi/engine/session-integration.d.ts +3 -8
- package/dist/integrations/pi/engine/session-integration.js +0 -49
- package/dist/integrations/pi/engine/session-runtime.d.ts +18 -3
- package/dist/integrations/pi/engine/session-runtime.js +18 -7
- package/dist/integrations/pi/engine/settings-bridge.d.ts +2 -2
- package/dist/integrations/pi/engine/settings-bridge.js +0 -2
- package/dist/integrations/pi/engine/settings-port.d.ts +2 -0
- package/dist/integrations/pi/engine/settings-port.js +3 -5
- package/dist/integrations/pi/engine/workflows.d.ts +33 -260
- package/dist/integrations/pi/engine/workflows.js +6 -28
- package/dist/integrations/pi/session-presenters/index.d.ts +2 -0
- package/dist/integrations/pi/session-presenters/index.js +1 -0
- package/dist/integrations/pi/session-presenters/presenters.d.ts +16 -0
- package/dist/integrations/pi/session-presenters/presenters.js +341 -0
- package/dist/integrations/pi/tui-runtime/adapter.js +0 -2
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +18 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +84 -3
- package/dist/integrations/pi/tui-runtime/index.d.ts +1 -1
- package/dist/integrations/pi/tui-runtime/index.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/ui/settings/declarations.d.ts +13 -1
- package/dist/ui/settings/declarations.js +12 -1
- package/dist/ui/settings/migrations.js +7 -0
- package/docs/architecture/boundaries.md +9 -1
- package/docs/architecture/project-structure.md +3 -2
- package/docs/architecture/ui-reference-provenance.md +1 -1
- package/package.json +1 -1
- package/dist/contracts/agent-engine/model.d.ts +0 -78
- package/dist/contracts/agent-engine/model.js +0 -1
- package/dist/contracts/agent-engine/ports.d.ts +0 -18
- package/dist/contracts/agent-engine/serialization.d.ts +0 -7
- package/dist/contracts/agent-engine/serialization.js +0 -58
- package/dist/contracts/agent-engine/validation.d.ts +0 -5
- package/dist/contracts/agent-engine/validation.js +0 -128
- package/dist/integrations/pi/components/upstream/theme/system-theme.d.ts +0 -61
- package/dist/integrations/pi/components/upstream/theme/system-theme.js +0 -709
- /package/dist/contracts/{agent-engine/ports.js → owned-ui/session-backend.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DIALOG_BACK_SHORTCUT_HINT, DIALOG_CLOSE_SHORTCUT_HINT } from "../../contracts/presentation/index.js";
|
|
2
|
-
import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, numericValues, RAIL_COLUMNS, renderDialogPanel, renderSteppedDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, renderShortcutHintsWithClose, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey,
|
|
3
|
-
import { SETTINGS_APP_ID
|
|
2
|
+
import { GLOBAL_SCOPE, LineInput, PLAIN_THEME, ShortcutRegistry, assertNoShortcutConflicts, blockJumpTarget, numericValues, RAIL_COLUMNS, renderDialogPanel, renderSteppedDialogPanel, renderEmptyState, renderGroupHeader, renderInputRow, renderListRow, renderNote, renderShortcutHintsWithClose, menuRowAt, regionAt, renderValueMenu, withScrollbarRail, stepperEnds, steppedValue, truncateToWidth, valueColumnFor, valueMenuFrame, blockRowSpan, displayWidth, handleLineInputKey, humanizeLabel, humanizeTitle, indexOfKey, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
|
|
3
|
+
import { SETTINGS_APP_ID } from "./settings-route.js";
|
|
4
4
|
export { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.js";
|
|
5
5
|
const SCOPE = SETTINGS_APP_ID;
|
|
6
6
|
const SETTINGS_TOP_RULE_ROWS = 1;
|
|
@@ -265,8 +265,10 @@ export class SettingsApp {
|
|
|
265
265
|
const row = rows[selected];
|
|
266
266
|
if (row?.kind === "element" && row.value.structured)
|
|
267
267
|
this.#openStructured(row.value);
|
|
268
|
-
else
|
|
268
|
+
else if (row?.kind === "element" && typeof this.#shownValue(row.value) === "number")
|
|
269
269
|
this.#cycle(rows, selected, 1);
|
|
270
|
+
else
|
|
271
|
+
this.#openMenu(rows, selected, true);
|
|
270
272
|
return { consumed: true };
|
|
271
273
|
}
|
|
272
274
|
case "move-up":
|
|
@@ -334,11 +336,11 @@ export class SettingsApp {
|
|
|
334
336
|
return { consumed: true, render: false };
|
|
335
337
|
if (overRow === null) {
|
|
336
338
|
// Invariant: a press anywhere else dismisses the menu rather than acting through it.
|
|
337
|
-
this.#menu
|
|
339
|
+
this.#closeMenu(menu);
|
|
338
340
|
return { consumed: true };
|
|
339
341
|
}
|
|
340
342
|
const value = menu.choices[overRow];
|
|
341
|
-
this.#menu
|
|
343
|
+
this.#closeMenu(menu);
|
|
342
344
|
if (value !== undefined)
|
|
343
345
|
this.#apply(menu.entry, value);
|
|
344
346
|
return { consumed: true };
|
|
@@ -442,7 +444,7 @@ export class SettingsApp {
|
|
|
442
444
|
clearTimeout(this.#activityTimer);
|
|
443
445
|
this.#activityTimer = undefined;
|
|
444
446
|
}
|
|
445
|
-
#openMenu(rows, selected) {
|
|
447
|
+
#openMenu(rows, selected, selectCurrent = false) {
|
|
446
448
|
const row = rows[selected];
|
|
447
449
|
if (row === undefined || row.kind !== "element")
|
|
448
450
|
return;
|
|
@@ -461,9 +463,30 @@ export class SettingsApp {
|
|
|
461
463
|
return;
|
|
462
464
|
}
|
|
463
465
|
const current = shown === null ? 0 : Math.max(0, entry.choices.indexOf(shown));
|
|
464
|
-
|
|
465
|
-
//
|
|
466
|
-
|
|
466
|
+
const anchorKey = `${entry.backend}:${entry.id}`;
|
|
467
|
+
// Compatibility: pointer opening already brightens the source value. Enter borrows that
|
|
468
|
+
// same value-region state for the life of its menu rather than inventing a keyboard color.
|
|
469
|
+
if (selectCurrent) {
|
|
470
|
+
this.#hoverKey = anchorKey;
|
|
471
|
+
this.#hoverRegion = "value";
|
|
472
|
+
}
|
|
473
|
+
// Invariant: pointer opening stays inactive because it targeted the setting value, while
|
|
474
|
+
// Enter explicitly starts keyboard choice navigation at the value already in effect.
|
|
475
|
+
this.#menu = {
|
|
476
|
+
entry,
|
|
477
|
+
current,
|
|
478
|
+
anchorKey,
|
|
479
|
+
choices: entry.choices,
|
|
480
|
+
keyboardOpened: selectCurrent,
|
|
481
|
+
index: selectCurrent ? current : -1,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
#closeMenu(menu) {
|
|
485
|
+
this.#menu = null;
|
|
486
|
+
if (!menu.keyboardOpened || this.#hoverKey !== menu.anchorKey)
|
|
487
|
+
return;
|
|
488
|
+
this.#hoverKey = null;
|
|
489
|
+
this.#hoverRegion = "label";
|
|
467
490
|
}
|
|
468
491
|
// Rationale: a structured setting opens as its own flag list rather than a value menu.
|
|
469
492
|
#openStructured(entry) {
|
|
@@ -640,12 +663,12 @@ export class SettingsApp {
|
|
|
640
663
|
return { consumed: false };
|
|
641
664
|
const key = KEYS[data] ?? data;
|
|
642
665
|
if (SETTINGS_SHORTCUTS.resolve(key, SCOPE) === "undo") {
|
|
643
|
-
this.#menu
|
|
666
|
+
this.#closeMenu(menu);
|
|
644
667
|
this.#undo();
|
|
645
668
|
return { consumed: true };
|
|
646
669
|
}
|
|
647
670
|
if (key === "escape") {
|
|
648
|
-
this.#menu
|
|
671
|
+
this.#closeMenu(menu);
|
|
649
672
|
return { consumed: true };
|
|
650
673
|
}
|
|
651
674
|
if (key === "up" || key === "down") {
|
|
@@ -656,7 +679,7 @@ export class SettingsApp {
|
|
|
656
679
|
}
|
|
657
680
|
else if (key === "enter") {
|
|
658
681
|
const value = menu.choices[menu.index];
|
|
659
|
-
this.#menu
|
|
682
|
+
this.#closeMenu(menu);
|
|
660
683
|
if (value !== undefined)
|
|
661
684
|
this.#apply(menu.entry, value);
|
|
662
685
|
}
|
|
@@ -1084,9 +1107,6 @@ function changeFailure(outcome) {
|
|
|
1084
1107
|
function labelOf(entry) {
|
|
1085
1108
|
return entry.label ?? humanizeLabel(entry.id);
|
|
1086
1109
|
}
|
|
1087
|
-
function isStepper(entry, shown = entry.value) {
|
|
1088
|
-
return typeof shown === "number" && entry.editable;
|
|
1089
|
-
}
|
|
1090
1110
|
/** Where a setting's number may go: what the engine states, or what it offers. */
|
|
1091
1111
|
function rangeOf(entry) {
|
|
1092
1112
|
return { minimum: entry.minimum, maximum: entry.maximum, values: numericValues(entry.choices) };
|
|
@@ -7,7 +7,7 @@ export { resolveCohortEndpoint, resolveProductPaths } from "./paths.js";
|
|
|
7
7
|
export type { CohortEndpointPaths, ProductPaths } from "./paths.js";
|
|
8
8
|
export { parseSessionSelection, sessionSelectionArguments } from "./session-selection.js";
|
|
9
9
|
export type { SessionSelection } from "./session-selection.js";
|
|
10
|
-
export { activateSessionRepositoryContext, clearSessionRepositoryContext, listSessionRepositoryWorktrees, readSessionRepositoryContext, registerSessionRepositoryRuntime, releaseSessionRepositoryRuntime, setSessionRepositoryContext, } from "./session-repository-context.js";
|
|
10
|
+
export { PRIMARY_SESSION_AGENT_ID, activateSessionRepositoryContext, clearSessionRepositoryContext, listSessionRepositoryWorktrees, readSessionRepositoryContext, registerSessionRepositoryRuntime, releaseSessionRepositoryRuntime, setSessionRepositoryContext, } from "./session-repository-context.js";
|
|
11
11
|
export type { SessionRepositoryContext, SessionRepositoryContextOptions, SessionRepositoryIdentity, SessionWorktreeInventoryEntry, SessionWorktreeStatus, } from "./session-repository-context.js";
|
|
12
12
|
export { createSupervisorStartupAttempt, publishSupervisorStartupResult, readSupervisorStartupResult, supervisorStartupFailure, supervisorStartupReady, supervisorStartupResultPath, } from "./supervisor-startup.js";
|
|
13
13
|
export type { SupervisorStartupAttemptIdentity, SupervisorStartupResult } from "./supervisor-startup.js";
|
|
@@ -2,5 +2,5 @@ export { assertLaunchInstance, assertLaunchInstanceOutcome, assertLaunchInstance
|
|
|
2
2
|
export { assertDimensions, assertLaunchProfileId, assertNativeProcessIdentity, assertTransparentTerminalLaunchProfile } from "./model.js";
|
|
3
3
|
export { resolveCohortEndpoint, resolveProductPaths } from "./paths.js";
|
|
4
4
|
export { parseSessionSelection, sessionSelectionArguments } from "./session-selection.js";
|
|
5
|
-
export { activateSessionRepositoryContext, clearSessionRepositoryContext, listSessionRepositoryWorktrees, readSessionRepositoryContext, registerSessionRepositoryRuntime, releaseSessionRepositoryRuntime, setSessionRepositoryContext, } from "./session-repository-context.js";
|
|
5
|
+
export { PRIMARY_SESSION_AGENT_ID, activateSessionRepositoryContext, clearSessionRepositoryContext, listSessionRepositoryWorktrees, readSessionRepositoryContext, registerSessionRepositoryRuntime, releaseSessionRepositoryRuntime, setSessionRepositoryContext, } from "./session-repository-context.js";
|
|
6
6
|
export { createSupervisorStartupAttempt, publishSupervisorStartupResult, readSupervisorStartupResult, supervisorStartupFailure, supervisorStartupReady, supervisorStartupResultPath, } from "./supervisor-startup.js";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { NativeProcessIdentity } from "./model.js";
|
|
2
|
+
/** Records without an agent id were written by a single-agent release and belong to its one agent. */
|
|
3
|
+
export declare const PRIMARY_SESSION_AGENT_ID = "primary";
|
|
2
4
|
export interface SessionRepositoryIdentity {
|
|
3
5
|
readonly sessionId: string;
|
|
4
6
|
readonly sessionFile: string;
|
|
@@ -10,20 +12,24 @@ export interface SessionRepositoryContext {
|
|
|
10
12
|
export type SessionWorktreeStatus = "current" | "busy" | "available" | "unverifiable";
|
|
11
13
|
export interface SessionWorktreeInventoryEntry extends SessionRepositoryContext {
|
|
12
14
|
readonly status: SessionWorktreeStatus;
|
|
15
|
+
/** Agent holding the claim within its runtime; null when the worktree is unclaimed or unverifiable. */
|
|
16
|
+
readonly agentId: string | null;
|
|
13
17
|
}
|
|
14
18
|
export interface SessionRepositoryContextOptions {
|
|
15
19
|
readonly dataDir?: string;
|
|
16
20
|
readonly signal?: AbortSignal;
|
|
17
21
|
readonly runtimeId?: string;
|
|
22
|
+
/** Agent within the runtime that owns the claim; defaults to the primary agent. */
|
|
23
|
+
readonly agentId?: string;
|
|
18
24
|
readonly runtimeIdentity?: NativeProcessIdentity;
|
|
19
25
|
readonly inspectProcess?: (pid: number) => Promise<NativeProcessIdentity | null>;
|
|
20
26
|
readonly lockTimeoutMs?: number;
|
|
21
27
|
}
|
|
22
28
|
/** Publish the exact live runtime that may acquire a delivery worktree. */
|
|
23
29
|
export declare function registerSessionRepositoryRuntime(identity: SessionRepositoryIdentity, runtimeId: string, runtimeIdentity: NativeProcessIdentity, options?: SessionRepositoryContextOptions): Promise<void>;
|
|
24
|
-
/** Revalidate a durable association and acquire its live claim for this runtime. */
|
|
30
|
+
/** Revalidate a durable association and acquire its live claim for this runtime's agent; sibling agents' claims stay. */
|
|
25
31
|
export declare function activateSessionRepositoryContext(identity: SessionRepositoryIdentity, runtimeId: string, runtimeIdentity: NativeProcessIdentity, options?: SessionRepositoryContextOptions): Promise<SessionRepositoryContext | null>;
|
|
26
|
-
/** Release
|
|
32
|
+
/** Release every agent's cooperative editing claims held by this exact runtime generation. */
|
|
27
33
|
export declare function releaseSessionRepositoryRuntime(runtimeId: string, runtimeIdentity: NativeProcessIdentity, options?: SessionRepositoryContextOptions): Promise<void>;
|
|
28
34
|
/** Persist one explicit same-repository worktree association after atomically claiming it. */
|
|
29
35
|
export declare function setSessionRepositoryContext(identity: SessionRepositoryIdentity, worktreePath: string, options?: SessionRepositoryContextOptions): Promise<SessionRepositoryContext>;
|
|
@@ -13,52 +13,60 @@ const MAX_WORKTREES = 256;
|
|
|
13
13
|
const GIT_TIMEOUT_MS = 5_000;
|
|
14
14
|
const LOCK_TIMEOUT_MS = 2_000;
|
|
15
15
|
const LOCK_RETRY_MS = 20;
|
|
16
|
+
/** Records without an agent id were written by a single-agent release and belong to its one agent. */
|
|
17
|
+
export const PRIMARY_SESSION_AGENT_ID = "primary";
|
|
18
|
+
const RECORD_FORMAT_VERSION = 2;
|
|
16
19
|
/** Publish the exact live runtime that may acquire a delivery worktree. */
|
|
17
20
|
export async function registerSessionRepositoryRuntime(identity, runtimeId, runtimeIdentity, options = {}) {
|
|
18
21
|
const header = await readSessionHeader(identity);
|
|
19
22
|
assertRuntimeId(runtimeId);
|
|
23
|
+
const agentId = requireAgentId(options);
|
|
20
24
|
assertProcessIdentity(runtimeIdentity);
|
|
21
|
-
await withClaimLock(options, async () => {
|
|
22
|
-
await writeRuntimeRecord(header, runtimeId, runtimeIdentity, options);
|
|
25
|
+
await withClaimLock({ ...options, runtimeIdentity }, async () => {
|
|
26
|
+
await writeRuntimeRecord(header, runtimeId, agentId, runtimeIdentity, options);
|
|
23
27
|
});
|
|
24
28
|
}
|
|
25
|
-
/** Revalidate a durable association and acquire its live claim for this runtime. */
|
|
29
|
+
/** Revalidate a durable association and acquire its live claim for this runtime's agent; sibling agents' claims stay. */
|
|
26
30
|
export async function activateSessionRepositoryContext(identity, runtimeId, runtimeIdentity, options = {}) {
|
|
27
31
|
const header = await readSessionHeader(identity);
|
|
28
32
|
assertRuntimeId(runtimeId);
|
|
33
|
+
const agentId = requireAgentId(options);
|
|
29
34
|
assertProcessIdentity(runtimeIdentity);
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
const owner = { runtimeId, agentId };
|
|
36
|
+
return await withClaimLock({ ...options, runtimeIdentity }, async () => {
|
|
37
|
+
await writeRuntimeRecord(header, runtimeId, agentId, runtimeIdentity, options);
|
|
32
38
|
const record = await readContextRecord(header, options);
|
|
33
39
|
if (record === null) {
|
|
34
|
-
await releaseClaimsOwnedBy(
|
|
40
|
+
await releaseClaimsOwnedBy(owner, null, options);
|
|
35
41
|
return null;
|
|
36
42
|
}
|
|
37
43
|
const selected = await validateContextRecord(header, record, options.signal).catch(() => null);
|
|
38
44
|
if (selected === null) {
|
|
39
|
-
await releaseClaimsOwnedBy(
|
|
45
|
+
await releaseClaimsOwnedBy(owner, null, options);
|
|
40
46
|
return null;
|
|
41
47
|
}
|
|
42
|
-
const status = await claimAvailability(selected,
|
|
48
|
+
const status = await claimAvailability(selected, owner, options);
|
|
43
49
|
if (status !== "available" && status !== "current") {
|
|
44
|
-
await releaseClaimsOwnedBy(
|
|
50
|
+
await releaseClaimsOwnedBy(owner, null, options);
|
|
45
51
|
return null;
|
|
46
52
|
}
|
|
47
|
-
await writeClaim(header, selected,
|
|
48
|
-
await releaseClaimsOwnedBy(
|
|
53
|
+
await writeClaim(header, selected, owner, runtimeIdentity, options);
|
|
54
|
+
await releaseClaimsOwnedBy(owner, selected.gitDir, options);
|
|
49
55
|
return { cwd: selected.root, branch: requireBranch(selected) };
|
|
50
56
|
});
|
|
51
57
|
}
|
|
52
|
-
/** Release
|
|
58
|
+
/** Release every agent's cooperative editing claims held by this exact runtime generation. */
|
|
53
59
|
export async function releaseSessionRepositoryRuntime(runtimeId, runtimeIdentity, options = {}) {
|
|
54
60
|
assertRuntimeId(runtimeId);
|
|
55
61
|
assertProcessIdentity(runtimeIdentity);
|
|
56
|
-
await withClaimLock(options, async () => {
|
|
57
|
-
const
|
|
58
|
-
|
|
62
|
+
await withClaimLock({ ...options, runtimeIdentity }, async () => {
|
|
63
|
+
const runtimes = (await readRuntimeRecordsOf(runtimeId, options))
|
|
64
|
+
.filter(({ record }) => record.pid === runtimeIdentity.pid && record.startIdentity === runtimeIdentity.startIdentity);
|
|
65
|
+
if (runtimes.length === 0)
|
|
59
66
|
return;
|
|
60
|
-
await
|
|
61
|
-
|
|
67
|
+
await releaseRuntimeClaims(runtimeId, options);
|
|
68
|
+
for (const { path } of runtimes)
|
|
69
|
+
await rm(path, { force: true });
|
|
62
70
|
});
|
|
63
71
|
}
|
|
64
72
|
/** Persist one explicit same-repository worktree association after atomically claiming it. */
|
|
@@ -69,27 +77,27 @@ export async function setSessionRepositoryContext(identity, worktreePath, option
|
|
|
69
77
|
if (!samePath(startup.commonDir, selected.commonDir)) {
|
|
70
78
|
throw contextError("associated worktree belongs to a different Git repository", "worktree-foreign-repository");
|
|
71
79
|
}
|
|
72
|
-
const
|
|
80
|
+
const owner = { runtimeId: requireRuntimeId(options), agentId: requireAgentId(options) };
|
|
73
81
|
return await withClaimLock(options, async () => {
|
|
74
|
-
const runtime = await requireLiveRuntime(header,
|
|
75
|
-
const status = await claimAvailability(selected,
|
|
82
|
+
const runtime = await requireLiveRuntime(header, owner, options);
|
|
83
|
+
const status = await claimAvailability(selected, owner, options);
|
|
76
84
|
if (status === "busy")
|
|
77
85
|
throw contextError("worktree is active in another session", "worktree-active-in-another-session");
|
|
78
86
|
if (status === "unverifiable")
|
|
79
87
|
throw contextError("worktree ownership cannot be verified", "worktree-owner-unverifiable");
|
|
80
88
|
const prior = await readContextRecord(header, options);
|
|
81
|
-
await writeClaim(header, selected,
|
|
89
|
+
await writeClaim(header, selected, owner, processIdentityOf(runtime), options);
|
|
82
90
|
try {
|
|
83
91
|
await writeContextRecord(header, startup, selected, options);
|
|
84
92
|
}
|
|
85
93
|
catch (error) {
|
|
86
94
|
if (status !== "current")
|
|
87
|
-
await removeMatchingClaim(selected,
|
|
95
|
+
await removeMatchingClaim(selected, owner, options);
|
|
88
96
|
throw error;
|
|
89
97
|
}
|
|
90
|
-
await releaseClaimsOwnedBy(
|
|
98
|
+
await releaseClaimsOwnedBy(owner, selected.gitDir, options);
|
|
91
99
|
if (prior !== null && !samePath(prior.worktreeGitDir, selected.gitDir)) {
|
|
92
|
-
await removeClaimByGitDir(prior.worktreeCommonDir, prior.worktreeGitDir,
|
|
100
|
+
await removeClaimByGitDir(prior.worktreeCommonDir, prior.worktreeGitDir, owner, options);
|
|
93
101
|
}
|
|
94
102
|
return { cwd: selected.root, branch: requireBranch(selected) };
|
|
95
103
|
});
|
|
@@ -101,11 +109,11 @@ export async function clearSessionRepositoryContext(identity, options = {}) {
|
|
|
101
109
|
await rm(contextRecordPath(header, options), { force: true });
|
|
102
110
|
return;
|
|
103
111
|
}
|
|
104
|
-
const
|
|
112
|
+
const owner = { runtimeId: requireRuntimeId(options), agentId: requireAgentId(options) };
|
|
105
113
|
await withClaimLock(options, async () => {
|
|
106
|
-
await requireLiveRuntime(header,
|
|
114
|
+
await requireLiveRuntime(header, owner, options);
|
|
107
115
|
await rm(contextRecordPath(header, options), { force: true });
|
|
108
|
-
await releaseClaimsOwnedBy(
|
|
116
|
+
await releaseClaimsOwnedBy(owner, null, options);
|
|
109
117
|
});
|
|
110
118
|
}
|
|
111
119
|
/** Read and revalidate a persisted association; malformed or stale state fails closed to absence. */
|
|
@@ -126,8 +134,8 @@ export async function readSessionRepositoryContext(identity, options = {}) {
|
|
|
126
134
|
/** List same-repository worktrees without acquiring, releasing, or repairing a claim. */
|
|
127
135
|
export async function listSessionRepositoryWorktrees(identity, options = {}) {
|
|
128
136
|
const header = await readSessionHeader(identity);
|
|
129
|
-
const
|
|
130
|
-
await requireLiveRuntime(header,
|
|
137
|
+
const owner = { runtimeId: requireRuntimeId(options), agentId: requireAgentId(options) };
|
|
138
|
+
await requireLiveRuntime(header, owner, options);
|
|
131
139
|
const startup = await readGitWorktreeIdentity(header.cwd, false, options.signal);
|
|
132
140
|
const worktrees = await listGitWorktrees(startup.root, startup.commonDir, options.signal);
|
|
133
141
|
if (worktrees.length > MAX_WORKTREES)
|
|
@@ -136,19 +144,17 @@ export async function listSessionRepositoryWorktrees(identity, options = {}) {
|
|
|
136
144
|
const entries = [];
|
|
137
145
|
for (const selected of worktrees) {
|
|
138
146
|
if (selected.branch === null) {
|
|
139
|
-
entries.push({ cwd: selected.root, branch: "(detached)", status: "unverifiable" });
|
|
147
|
+
entries.push({ cwd: selected.root, branch: "(detached)", status: "unverifiable", agentId: null });
|
|
140
148
|
continue;
|
|
141
149
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
status: contended ? "unverifiable" : await claimAvailability(selected, runtimeId, options),
|
|
146
|
-
});
|
|
150
|
+
const status = contended ? "unverifiable" : await claimAvailability(selected, owner, options);
|
|
151
|
+
const claim = status === "current" || status === "busy" ? await readClaim(selected, options) : null;
|
|
152
|
+
entries.push({ cwd: selected.root, branch: selected.branch, status, agentId: claim?.agentId ?? null });
|
|
147
153
|
}
|
|
148
154
|
return entries.sort((left, right) => pathKey(left.cwd).localeCompare(pathKey(right.cwd)));
|
|
149
155
|
}
|
|
150
|
-
async function requireLiveRuntime(header,
|
|
151
|
-
const runtime = await readRuntimeRecord(
|
|
156
|
+
async function requireLiveRuntime(header, owner, options) {
|
|
157
|
+
const runtime = await readRuntimeRecord(owner, options);
|
|
152
158
|
if (runtime === null || runtime.sessionId !== header.id || runtime.sessionFile !== header.file) {
|
|
153
159
|
throw contextError("active session runtime is unavailable", "session-runtime-unavailable");
|
|
154
160
|
}
|
|
@@ -165,14 +171,15 @@ async function requireLiveRuntime(header, runtimeId, options) {
|
|
|
165
171
|
}
|
|
166
172
|
return runtime;
|
|
167
173
|
}
|
|
168
|
-
async function claimAvailability(selected,
|
|
174
|
+
async function claimAvailability(selected, owner, options) {
|
|
169
175
|
const claim = await readClaim(selected, options);
|
|
170
176
|
if (claim === undefined)
|
|
171
177
|
return "available";
|
|
172
178
|
if (claim === null || !claimMatchesWorktree(claim, selected))
|
|
173
179
|
return "unverifiable";
|
|
174
|
-
if (claim
|
|
180
|
+
if (ownedBy(claim, owner))
|
|
175
181
|
return "current";
|
|
182
|
+
// Invariant: a sibling agent of this runtime is live by construction, so its claim reads busy below.
|
|
176
183
|
const inspection = requireInspector(options);
|
|
177
184
|
let observed;
|
|
178
185
|
try {
|
|
@@ -184,8 +191,8 @@ async function claimAvailability(selected, runtimeId, options) {
|
|
|
184
191
|
const live = observed !== null && observed.pid === claim.pid && observed.startIdentity === claim.startIdentity;
|
|
185
192
|
return live ? "busy" : "available";
|
|
186
193
|
}
|
|
187
|
-
async function writeRuntimeRecord(header, runtimeId, runtimeIdentity, options) {
|
|
188
|
-
const existing = await readRuntimeRecord(runtimeId, options);
|
|
194
|
+
async function writeRuntimeRecord(header, runtimeId, agentId, runtimeIdentity, options) {
|
|
195
|
+
const existing = await readRuntimeRecord({ runtimeId, agentId }, options);
|
|
189
196
|
if (existing !== null && (existing.pid !== runtimeIdentity.pid || existing.startIdentity !== runtimeIdentity.startIdentity)) {
|
|
190
197
|
const inspection = requireInspector(options);
|
|
191
198
|
let observed;
|
|
@@ -201,14 +208,16 @@ async function writeRuntimeRecord(header, runtimeId, runtimeIdentity, options) {
|
|
|
201
208
|
}
|
|
202
209
|
const record = {
|
|
203
210
|
schema: RUNTIME_SCHEMA,
|
|
211
|
+
formatVersion: RECORD_FORMAT_VERSION,
|
|
204
212
|
runtimeId,
|
|
213
|
+
agentId,
|
|
205
214
|
sessionId: header.id,
|
|
206
215
|
sessionFile: header.file,
|
|
207
216
|
pid: runtimeIdentity.pid,
|
|
208
217
|
startIdentity: runtimeIdentity.startIdentity,
|
|
209
218
|
updatedAt: new Date().toISOString(),
|
|
210
219
|
};
|
|
211
|
-
await atomicWrite(runtimeRecordPath(runtimeId, options), record);
|
|
220
|
+
await atomicWrite(runtimeRecordPath({ runtimeId, agentId }, options), record);
|
|
212
221
|
}
|
|
213
222
|
async function writeContextRecord(header, startup, selected, options) {
|
|
214
223
|
const record = {
|
|
@@ -224,10 +233,12 @@ async function writeContextRecord(header, startup, selected, options) {
|
|
|
224
233
|
};
|
|
225
234
|
await atomicWrite(contextRecordPath(header, options), record);
|
|
226
235
|
}
|
|
227
|
-
async function writeClaim(header, selected,
|
|
236
|
+
async function writeClaim(header, selected, owner, runtimeIdentity, options) {
|
|
228
237
|
const claim = {
|
|
229
238
|
schema: CLAIM_SCHEMA,
|
|
230
|
-
|
|
239
|
+
formatVersion: RECORD_FORMAT_VERSION,
|
|
240
|
+
runtimeId: owner.runtimeId,
|
|
241
|
+
agentId: owner.agentId,
|
|
231
242
|
sessionId: header.id,
|
|
232
243
|
worktreeRoot: selected.root,
|
|
233
244
|
worktreeCommonDir: selected.commonDir,
|
|
@@ -241,8 +252,34 @@ async function writeClaim(header, selected, runtimeId, runtimeIdentity, options)
|
|
|
241
252
|
async function readContextRecord(header, options) {
|
|
242
253
|
return await readJsonRecord(contextRecordPath(header, options), isContextRecord);
|
|
243
254
|
}
|
|
244
|
-
async function readRuntimeRecord(
|
|
245
|
-
|
|
255
|
+
async function readRuntimeRecord(owner, options) {
|
|
256
|
+
const record = await readRuntimeRecordAt(runtimeRecordPath(owner, options));
|
|
257
|
+
return record !== null && ownedBy(record, owner) ? record : null;
|
|
258
|
+
}
|
|
259
|
+
async function readRuntimeRecordAt(path) {
|
|
260
|
+
const stored = await readJsonRecord(path, isStoredRuntimeRecord);
|
|
261
|
+
return stored === null ? null : { ...stored, formatVersion: RECORD_FORMAT_VERSION, agentId: stored.agentId ?? PRIMARY_SESSION_AGENT_ID };
|
|
262
|
+
}
|
|
263
|
+
/** Every agent's runtime record of one runtime, with the file that holds it. */
|
|
264
|
+
async function readRuntimeRecordsOf(runtimeId, options) {
|
|
265
|
+
const directory = runtimesDirectory(options);
|
|
266
|
+
const names = await readdir(directory).catch(() => []);
|
|
267
|
+
if (names.length > MAX_WORKTREES * 4)
|
|
268
|
+
throw contextError("session runtime store exceeds its bounded limit", "session-runtime-limit");
|
|
269
|
+
const records = [];
|
|
270
|
+
for (const name of names) {
|
|
271
|
+
if (!name.endsWith(".json"))
|
|
272
|
+
continue;
|
|
273
|
+
const path = join(directory, name);
|
|
274
|
+
const record = await readRuntimeRecordAt(path);
|
|
275
|
+
if (record?.runtimeId === runtimeId)
|
|
276
|
+
records.push({ path, record });
|
|
277
|
+
}
|
|
278
|
+
return records;
|
|
279
|
+
}
|
|
280
|
+
async function readClaimRecordAt(path) {
|
|
281
|
+
const stored = await readJsonRecord(path, isStoredClaimRecord);
|
|
282
|
+
return stored === null ? null : normalizeClaim(stored);
|
|
246
283
|
}
|
|
247
284
|
/** Undefined means absent; null means present but invalid. */
|
|
248
285
|
async function readClaim(selected, options) {
|
|
@@ -252,7 +289,7 @@ async function readClaim(selected, options) {
|
|
|
252
289
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size === 0 || metadata.size > MAX_RECORD_BYTES)
|
|
253
290
|
return null;
|
|
254
291
|
const value = JSON.parse((await readFile(path, { signal: options.signal })).toString("utf8"));
|
|
255
|
-
return
|
|
292
|
+
return isStoredClaimRecord(value) ? normalizeClaim(value) : null;
|
|
256
293
|
}
|
|
257
294
|
catch (error) {
|
|
258
295
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
@@ -274,7 +311,15 @@ async function validateContextRecord(header, record, signal) {
|
|
|
274
311
|
throw new Error("associated worktree identity changed");
|
|
275
312
|
return selected;
|
|
276
313
|
}
|
|
277
|
-
|
|
314
|
+
/** Release one agent's claims except the one it keeps; sibling agents of the runtime are untouched. */
|
|
315
|
+
async function releaseClaimsOwnedBy(owner, keepGitDir, options) {
|
|
316
|
+
await releaseClaimsWhere(claim => ownedBy(claim, owner) && (keepGitDir === null || !samePath(claim.worktreeGitDir, keepGitDir)), options);
|
|
317
|
+
}
|
|
318
|
+
/** Release every agent's claims of one runtime; only process exit does this. */
|
|
319
|
+
async function releaseRuntimeClaims(runtimeId, options) {
|
|
320
|
+
await releaseClaimsWhere(claim => claim.runtimeId === runtimeId, options);
|
|
321
|
+
}
|
|
322
|
+
async function releaseClaimsWhere(release, options) {
|
|
278
323
|
const directory = claimsDirectory(options);
|
|
279
324
|
const names = await readdir(directory).catch(() => []);
|
|
280
325
|
if (names.length > MAX_WORKTREES * 4)
|
|
@@ -283,19 +328,18 @@ async function releaseClaimsOwnedBy(runtimeId, keepGitDir, options) {
|
|
|
283
328
|
if (!name.endsWith(".json"))
|
|
284
329
|
continue;
|
|
285
330
|
const path = join(directory, name);
|
|
286
|
-
const claim = await
|
|
287
|
-
if (claim
|
|
331
|
+
const claim = await readClaimRecordAt(path);
|
|
332
|
+
if (claim !== null && release(claim))
|
|
288
333
|
await rm(path, { force: true });
|
|
289
|
-
}
|
|
290
334
|
}
|
|
291
335
|
}
|
|
292
|
-
async function removeMatchingClaim(selected,
|
|
293
|
-
await removeClaimByGitDir(selected.commonDir, selected.gitDir,
|
|
336
|
+
async function removeMatchingClaim(selected, owner, options) {
|
|
337
|
+
await removeClaimByGitDir(selected.commonDir, selected.gitDir, owner, options);
|
|
294
338
|
}
|
|
295
|
-
async function removeClaimByGitDir(commonDir, gitDir,
|
|
339
|
+
async function removeClaimByGitDir(commonDir, gitDir, owner, options) {
|
|
296
340
|
const path = claimRecordPath(commonDir, gitDir, options);
|
|
297
|
-
const claim = await
|
|
298
|
-
if (claim
|
|
341
|
+
const claim = await readClaimRecordAt(path);
|
|
342
|
+
if (claim !== null && ownedBy(claim, owner))
|
|
299
343
|
await rm(path, { force: true });
|
|
300
344
|
}
|
|
301
345
|
async function readSessionHeader(identity) {
|
|
@@ -401,21 +445,30 @@ async function withClaimLock(options, operation) {
|
|
|
401
445
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
402
446
|
const path = join(directory, "mutation.lock");
|
|
403
447
|
const deadline = Date.now() + (options.lockTimeoutMs ?? LOCK_TIMEOUT_MS);
|
|
448
|
+
const holder = await lockHolderIdentity(options);
|
|
404
449
|
let handle;
|
|
405
|
-
|
|
450
|
+
for (let attempt = 0; handle === undefined; attempt++) {
|
|
406
451
|
try {
|
|
407
452
|
handle = await open(path, "wx", 0o600);
|
|
408
453
|
}
|
|
409
454
|
catch (error) {
|
|
410
455
|
if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
|
|
411
456
|
throw error;
|
|
457
|
+
if (attempt === 0 && await evictDeadLockHolder(path, options))
|
|
458
|
+
continue;
|
|
412
459
|
if (Date.now() >= deadline)
|
|
413
460
|
throw contextError("worktree claim mutation is busy", "worktree-claim-busy");
|
|
414
461
|
await new Promise(resolvePromise => setTimeout(resolvePromise, LOCK_RETRY_MS));
|
|
415
462
|
}
|
|
416
463
|
}
|
|
417
464
|
try {
|
|
418
|
-
|
|
465
|
+
const record = {
|
|
466
|
+
pid: process.pid,
|
|
467
|
+
...(holder === null ? {} : { startIdentity: holder.startIdentity }),
|
|
468
|
+
acquiredAt: new Date().toISOString(),
|
|
469
|
+
nonce: randomUUID(),
|
|
470
|
+
};
|
|
471
|
+
await handle.writeFile(`${JSON.stringify(record)}\n`);
|
|
419
472
|
return await operation();
|
|
420
473
|
}
|
|
421
474
|
finally {
|
|
@@ -423,6 +476,59 @@ async function withClaimLock(options, operation) {
|
|
|
423
476
|
await rm(path, { force: true }).catch(() => undefined);
|
|
424
477
|
}
|
|
425
478
|
}
|
|
479
|
+
const lockHolderIdentities = new WeakMap();
|
|
480
|
+
/** This process's identity as recorded in the lock; null leaves the lock unevictable, never wrongly evictable. */
|
|
481
|
+
async function lockHolderIdentity(options) {
|
|
482
|
+
if (options.runtimeIdentity?.pid === process.pid)
|
|
483
|
+
return options.runtimeIdentity;
|
|
484
|
+
const inspection = options.inspectProcess;
|
|
485
|
+
if (inspection === undefined)
|
|
486
|
+
return null;
|
|
487
|
+
let identity = lockHolderIdentities.get(inspection);
|
|
488
|
+
if (identity === undefined) {
|
|
489
|
+
identity = inspection(process.pid)
|
|
490
|
+
.then(observed => observed !== null && observed.pid === process.pid ? observed : null)
|
|
491
|
+
.catch(() => null);
|
|
492
|
+
lockHolderIdentities.set(inspection, identity);
|
|
493
|
+
}
|
|
494
|
+
return await identity;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Evict the mutation lock only when its recorded holder is proven dead: the pid is gone or now names a process
|
|
498
|
+
* with another start identity. A lock without a holder identity, or a holder that cannot be inspected, is kept.
|
|
499
|
+
*/
|
|
500
|
+
async function evictDeadLockHolder(path, options) {
|
|
501
|
+
const inspection = options.inspectProcess;
|
|
502
|
+
if (inspection === undefined)
|
|
503
|
+
return false;
|
|
504
|
+
const holder = await readJsonRecord(path, isMutationLockRecord);
|
|
505
|
+
if (holder === null || holder.startIdentity === undefined || holder.pid === process.pid)
|
|
506
|
+
return false;
|
|
507
|
+
let observed;
|
|
508
|
+
try {
|
|
509
|
+
observed = await inspection(holder.pid);
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
if (observed !== null && observed.pid === holder.pid && observed.startIdentity === holder.startIdentity)
|
|
515
|
+
return false;
|
|
516
|
+
const evicted = `${path}.${randomUUID()}.evicted`;
|
|
517
|
+
try {
|
|
518
|
+
await rename(path, evicted);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
const moved = await readJsonRecord(evicted, isMutationLockRecord);
|
|
524
|
+
if (moved?.nonce !== holder.nonce) {
|
|
525
|
+
// Concurrency: another waiter evicted first and a live holder took the lock since; hand it back untouched.
|
|
526
|
+
await rename(evicted, path).catch(() => undefined);
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
await rm(evicted, { force: true });
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
426
532
|
async function atomicWrite(path, value) {
|
|
427
533
|
const directory = dirname(path);
|
|
428
534
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
@@ -455,8 +561,13 @@ function contextRecordPath(header, options) {
|
|
|
455
561
|
function claimStoreRoot(options) {
|
|
456
562
|
return join(options.dataDir ?? resolveProductPaths().dataDir, PRODUCT_IDENTITY.state.sessionWorktreeClaimDirectory);
|
|
457
563
|
}
|
|
458
|
-
function
|
|
459
|
-
return join(claimStoreRoot(options), "runtimes"
|
|
564
|
+
function runtimesDirectory(options) {
|
|
565
|
+
return join(claimStoreRoot(options), "runtimes");
|
|
566
|
+
}
|
|
567
|
+
// Compatibility: the primary agent keeps the single-agent file name, so a version-1 runtime record is its record.
|
|
568
|
+
function runtimeRecordPath(owner, options) {
|
|
569
|
+
const key = owner.agentId === PRIMARY_SESSION_AGENT_ID ? owner.runtimeId : `${owner.runtimeId}\0${owner.agentId}`;
|
|
570
|
+
return join(runtimesDirectory(options), `${digest(key)}.json`);
|
|
460
571
|
}
|
|
461
572
|
function claimsDirectory(options) {
|
|
462
573
|
return join(claimStoreRoot(options), "worktrees");
|
|
@@ -472,6 +583,15 @@ function requireRuntimeId(options) {
|
|
|
472
583
|
assertRuntimeId(runtimeId);
|
|
473
584
|
return runtimeId;
|
|
474
585
|
}
|
|
586
|
+
function requireAgentId(options) {
|
|
587
|
+
const agentId = options.agentId ?? PRIMARY_SESSION_AGENT_ID;
|
|
588
|
+
if (!validSessionId(agentId))
|
|
589
|
+
throw contextError("session agent identity is invalid", "session-agent-invalid");
|
|
590
|
+
return agentId;
|
|
591
|
+
}
|
|
592
|
+
function ownedBy(record, owner) {
|
|
593
|
+
return record.runtimeId === owner.runtimeId && record.agentId === owner.agentId;
|
|
594
|
+
}
|
|
475
595
|
function requireInspector(options) {
|
|
476
596
|
if (options.inspectProcess === undefined)
|
|
477
597
|
throw contextError("native process identity inspection is unavailable", "process-inspection-unavailable");
|
|
@@ -520,19 +640,34 @@ function isContextRecord(value) {
|
|
|
520
640
|
&& validPath(value.worktreeRoot) && validPath(value.worktreeCommonDir) && validPath(value.worktreeGitDir)
|
|
521
641
|
&& typeof value.updatedAt === "string";
|
|
522
642
|
}
|
|
523
|
-
function
|
|
524
|
-
return isRecord(value) && value.schema === RUNTIME_SCHEMA &&
|
|
525
|
-
&& validPath(value.sessionFile) && Number.isSafeInteger(value.pid) && Number(value.pid) > 0
|
|
643
|
+
function isStoredRuntimeRecord(value) {
|
|
644
|
+
return isRecord(value) && value.schema === RUNTIME_SCHEMA && validRecordVersion(value) && validSessionId(value.runtimeId)
|
|
645
|
+
&& validSessionId(value.sessionId) && validPath(value.sessionFile) && Number.isSafeInteger(value.pid) && Number(value.pid) > 0
|
|
526
646
|
&& typeof value.startIdentity === "string" && value.startIdentity.length > 0 && value.startIdentity.length <= 512
|
|
527
647
|
&& !value.startIdentity.includes("\0") && typeof value.updatedAt === "string";
|
|
528
648
|
}
|
|
529
|
-
function
|
|
530
|
-
return isRecord(value) && value.schema === CLAIM_SCHEMA &&
|
|
649
|
+
function isStoredClaimRecord(value) {
|
|
650
|
+
return isRecord(value) && value.schema === CLAIM_SCHEMA && validRecordVersion(value) && validSessionId(value.runtimeId)
|
|
651
|
+
&& validSessionId(value.sessionId)
|
|
531
652
|
&& validPath(value.worktreeRoot) && validPath(value.worktreeCommonDir) && validPath(value.worktreeGitDir)
|
|
532
653
|
&& Number.isSafeInteger(value.pid) && Number(value.pid) > 0
|
|
533
654
|
&& typeof value.startIdentity === "string" && value.startIdentity.length > 0 && value.startIdentity.length <= 512
|
|
534
655
|
&& !value.startIdentity.includes("\0") && typeof value.updatedAt === "string";
|
|
535
656
|
}
|
|
657
|
+
function isMutationLockRecord(value) {
|
|
658
|
+
return isRecord(value) && Number.isSafeInteger(value.pid) && Number(value.pid) > 0
|
|
659
|
+
&& (value.startIdentity === undefined || (typeof value.startIdentity === "string" && value.startIdentity.length > 0
|
|
660
|
+
&& value.startIdentity.length <= 512 && !value.startIdentity.includes("\0")))
|
|
661
|
+
&& typeof value.acquiredAt === "string" && typeof value.nonce === "string" && value.nonce.length > 0;
|
|
662
|
+
}
|
|
663
|
+
function validRecordVersion(value) {
|
|
664
|
+
if (value.formatVersion === undefined)
|
|
665
|
+
return value.agentId === undefined;
|
|
666
|
+
return value.formatVersion === RECORD_FORMAT_VERSION && validSessionId(value.agentId);
|
|
667
|
+
}
|
|
668
|
+
function normalizeClaim(stored) {
|
|
669
|
+
return { ...stored, formatVersion: RECORD_FORMAT_VERSION, agentId: stored.agentId ?? PRIMARY_SESSION_AGENT_ID };
|
|
670
|
+
}
|
|
536
671
|
function isRecord(value) {
|
|
537
672
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
538
673
|
}
|