@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.
Files changed (116) hide show
  1. package/dist/app/session-shell/clipboard-services.d.ts +19 -0
  2. package/dist/app/session-shell/clipboard-services.js +7 -0
  3. package/dist/app/session-shell/exit-notice.d.ts +9 -5
  4. package/dist/app/session-shell/exit-notice.js +5 -5
  5. package/dist/app/session-shell/index.d.ts +4 -2
  6. package/dist/app/session-shell/index.js +2 -1
  7. package/dist/app/session-shell/paste-preparation-client.d.ts +3 -0
  8. package/dist/app/session-shell/paste-preparation-client.js +5 -2
  9. package/dist/app/session-shell/path-chip-presentation.d.ts +10 -3
  10. package/dist/app/session-shell/path-chip-presentation.js +48 -6
  11. package/dist/app/session-shell/prompt-chips.js +30 -4
  12. package/dist/app/session-shell/quit-outro.d.ts +3 -1
  13. package/dist/app/session-shell/quit-outro.js +23 -8
  14. package/dist/app/session-shell/response-copy-transport.d.ts +6 -1
  15. package/dist/app/session-shell/response-copy-transport.js +8 -2
  16. package/dist/app/session-shell/session-shell-root.d.ts +29 -11
  17. package/dist/app/session-shell/session-shell-root.js +62 -53
  18. package/dist/app/session-shell/session-shell.d.ts +55 -29
  19. package/dist/app/session-shell/session-shell.js +748 -1132
  20. package/dist/app/session-shell/shell-host.d.ts +105 -0
  21. package/dist/app/session-shell/shell-host.js +523 -0
  22. package/dist/cli/session-context.js +6 -3
  23. package/dist/composition/owned-ui.d.ts +20 -2
  24. package/dist/composition/owned-ui.js +107 -56
  25. package/dist/composition/session-info-reference.d.ts +2 -2
  26. package/dist/composition/session-info-reference.js +2 -2
  27. package/dist/contracts/agent-engine/capability-ports.d.ts +1 -85
  28. package/dist/contracts/agent-engine/capability-ports.js +1 -35
  29. package/dist/contracts/agent-engine/index.d.ts +1 -7
  30. package/dist/contracts/agent-engine/index.js +0 -4
  31. package/dist/contracts/owned-ui/index.d.ts +5 -1
  32. package/dist/contracts/owned-ui/index.js +1 -0
  33. package/dist/contracts/owned-ui/model.d.ts +6 -0
  34. package/dist/contracts/owned-ui/session-backend.d.ts +119 -0
  35. package/dist/contracts/owned-ui/session-presenters.d.ts +82 -0
  36. package/dist/contracts/owned-ui/session-presenters.js +1 -0
  37. package/dist/contracts/owned-ui/session-workflows.d.ts +342 -0
  38. package/dist/contracts/owned-ui/session-workflows.js +30 -0
  39. package/dist/features/launch/index.d.ts +1 -1
  40. package/dist/features/launch/index.js +1 -1
  41. package/dist/features/launch/session-context.d.ts +1 -0
  42. package/dist/features/launch/session-context.js +1 -0
  43. package/dist/features/owned-ui/settings-app.js +35 -15
  44. package/dist/foundation/lifecycle/index.d.ts +1 -1
  45. package/dist/foundation/lifecycle/index.js +1 -1
  46. package/dist/foundation/lifecycle/session-repository-context.d.ts +8 -2
  47. package/dist/foundation/lifecycle/session-repository-context.js +200 -65
  48. package/dist/foundation/release/bootstrap.js +1 -2
  49. package/dist/foundation/release/release-gc.js +1 -1
  50. package/dist/foundation/release/update.js +0 -4
  51. package/dist/foundation/storage/control-store.js +0 -12
  52. package/dist/integrations/pi/components/components.js +0 -16
  53. package/dist/integrations/pi/components/index.d.ts +3 -3
  54. package/dist/integrations/pi/components/index.js +2 -2
  55. package/dist/integrations/pi/components/models-dialog.js +1 -30
  56. package/dist/integrations/pi/components/shell-editor-autocomplete.js +11 -9
  57. package/dist/integrations/pi/components/shell-extension-ui.d.ts +3 -3
  58. package/dist/integrations/pi/components/shell-extension-ui.js +6 -2
  59. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -4
  60. package/dist/integrations/pi/components/shell-shared-facade.d.ts +21 -1
  61. package/dist/integrations/pi/components/shell-shared-facade.js +18 -2
  62. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +5 -1
  63. package/dist/integrations/pi/components/upstream/theme/theme.js +20 -2
  64. package/dist/integrations/pi/engine/adapter.d.ts +44 -24
  65. package/dist/integrations/pi/engine/adapter.js +130 -22
  66. package/dist/integrations/pi/engine/command-dispatch.d.ts +2 -5
  67. package/dist/integrations/pi/engine/extension-ui-binding.d.ts +2 -11
  68. package/dist/integrations/pi/engine/host-ports.d.ts +20 -0
  69. package/dist/integrations/pi/engine/host-ports.js +13 -0
  70. package/dist/integrations/pi/engine/host.d.ts +70 -0
  71. package/dist/integrations/pi/engine/host.js +147 -0
  72. package/dist/integrations/pi/engine/index.d.ts +8 -4
  73. package/dist/integrations/pi/engine/index.js +3 -1
  74. package/dist/integrations/pi/engine/resource-catalog.d.ts +4 -23
  75. package/dist/integrations/pi/engine/runtime-integration.d.ts +8 -2
  76. package/dist/integrations/pi/engine/runtime-integration.js +12 -7
  77. package/dist/integrations/pi/engine/session-integration.d.ts +3 -8
  78. package/dist/integrations/pi/engine/session-integration.js +0 -49
  79. package/dist/integrations/pi/engine/session-runtime.d.ts +18 -3
  80. package/dist/integrations/pi/engine/session-runtime.js +18 -7
  81. package/dist/integrations/pi/engine/settings-bridge.d.ts +2 -2
  82. package/dist/integrations/pi/engine/settings-bridge.js +0 -2
  83. package/dist/integrations/pi/engine/settings-port.d.ts +2 -0
  84. package/dist/integrations/pi/engine/settings-port.js +3 -5
  85. package/dist/integrations/pi/engine/workflows.d.ts +33 -260
  86. package/dist/integrations/pi/engine/workflows.js +6 -28
  87. package/dist/integrations/pi/session-presenters/index.d.ts +2 -0
  88. package/dist/integrations/pi/session-presenters/index.js +1 -0
  89. package/dist/integrations/pi/session-presenters/presenters.d.ts +16 -0
  90. package/dist/integrations/pi/session-presenters/presenters.js +341 -0
  91. package/dist/integrations/pi/tui-runtime/adapter.js +0 -2
  92. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +18 -0
  93. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +84 -3
  94. package/dist/integrations/pi/tui-runtime/index.d.ts +1 -1
  95. package/dist/integrations/pi/tui-runtime/index.js +1 -1
  96. package/dist/native/darwin-arm64/manifest.json +1 -1
  97. package/dist/native/linux-x64/manifest.json +1 -1
  98. package/dist/native/win32-x64/manifest.json +2 -2
  99. package/dist/native/win32-x64/process-guardian.exe +0 -0
  100. package/dist/ui/settings/declarations.d.ts +13 -1
  101. package/dist/ui/settings/declarations.js +12 -1
  102. package/dist/ui/settings/migrations.js +7 -0
  103. package/docs/architecture/boundaries.md +9 -1
  104. package/docs/architecture/project-structure.md +3 -2
  105. package/docs/architecture/ui-reference-provenance.md +1 -1
  106. package/package.json +1 -1
  107. package/dist/contracts/agent-engine/model.d.ts +0 -78
  108. package/dist/contracts/agent-engine/model.js +0 -1
  109. package/dist/contracts/agent-engine/ports.d.ts +0 -18
  110. package/dist/contracts/agent-engine/serialization.d.ts +0 -7
  111. package/dist/contracts/agent-engine/serialization.js +0 -58
  112. package/dist/contracts/agent-engine/validation.d.ts +0 -5
  113. package/dist/contracts/agent-engine/validation.js +0 -128
  114. package/dist/integrations/pi/components/upstream/theme/system-theme.d.ts +0 -61
  115. package/dist/integrations/pi/components/upstream/theme/system-theme.js +0 -709
  116. /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, isThumbRow, layoutList, moveSelection, rowKey, scrollForSelection, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, ScrollbarRails, scrollbarWheelRows, selectableIndexes, } from "../../ui/components/index.js";
3
- import { SETTINGS_APP_ID, SETTINGS_ROUTE } from "./settings-route.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, 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 = null;
339
+ this.#closeMenu(menu);
338
340
  return { consumed: true };
339
341
  }
340
342
  const value = menu.choices[overRow];
341
- this.#menu = null;
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
- // Invariant: the opening press targeted the setting value, not an option. Keep the
465
- // effective value as the keyboard origin without painting it as pointer-picked.
466
- this.#menu = { entry, current, anchorKey: `${entry.backend}:${entry.id}`, choices: entry.choices, index: -1 };
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 = null;
666
+ this.#closeMenu(menu);
644
667
  this.#undo();
645
668
  return { consumed: true };
646
669
  }
647
670
  if (key === "escape") {
648
- this.#menu = null;
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 = null;
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 only this exact runtime generation's cooperative editing claims. */
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
- return await withClaimLock(options, async () => {
31
- await writeRuntimeRecord(header, runtimeId, runtimeIdentity, options);
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(runtimeId, null, options);
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(runtimeId, null, options);
45
+ await releaseClaimsOwnedBy(owner, null, options);
40
46
  return null;
41
47
  }
42
- const status = await claimAvailability(selected, runtimeId, options);
48
+ const status = await claimAvailability(selected, owner, options);
43
49
  if (status !== "available" && status !== "current") {
44
- await releaseClaimsOwnedBy(runtimeId, null, options);
50
+ await releaseClaimsOwnedBy(owner, null, options);
45
51
  return null;
46
52
  }
47
- await writeClaim(header, selected, runtimeId, runtimeIdentity, options);
48
- await releaseClaimsOwnedBy(runtimeId, selected.gitDir, options);
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 only this exact runtime generation's cooperative editing claims. */
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 runtime = await readRuntimeRecord(runtimeId, options);
58
- if (runtime === null || runtime.pid !== runtimeIdentity.pid || runtime.startIdentity !== runtimeIdentity.startIdentity)
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 releaseClaimsOwnedBy(runtimeId, null, options);
61
- await rm(runtimeRecordPath(runtimeId, options), { force: true });
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 runtimeId = requireRuntimeId(options);
80
+ const owner = { runtimeId: requireRuntimeId(options), agentId: requireAgentId(options) };
73
81
  return await withClaimLock(options, async () => {
74
- const runtime = await requireLiveRuntime(header, runtimeId, options);
75
- const status = await claimAvailability(selected, runtimeId, options);
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, runtimeId, processIdentityOf(runtime), options);
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, runtimeId, options);
95
+ await removeMatchingClaim(selected, owner, options);
88
96
  throw error;
89
97
  }
90
- await releaseClaimsOwnedBy(runtimeId, selected.gitDir, options);
98
+ await releaseClaimsOwnedBy(owner, selected.gitDir, options);
91
99
  if (prior !== null && !samePath(prior.worktreeGitDir, selected.gitDir)) {
92
- await removeClaimByGitDir(prior.worktreeCommonDir, prior.worktreeGitDir, runtimeId, options);
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 runtimeId = requireRuntimeId(options);
112
+ const owner = { runtimeId: requireRuntimeId(options), agentId: requireAgentId(options) };
105
113
  await withClaimLock(options, async () => {
106
- await requireLiveRuntime(header, runtimeId, options);
114
+ await requireLiveRuntime(header, owner, options);
107
115
  await rm(contextRecordPath(header, options), { force: true });
108
- await releaseClaimsOwnedBy(runtimeId, null, options);
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 runtimeId = requireRuntimeId(options);
130
- await requireLiveRuntime(header, runtimeId, options);
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
- entries.push({
143
- cwd: selected.root,
144
- branch: selected.branch,
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, runtimeId, options) {
151
- const runtime = await readRuntimeRecord(runtimeId, options);
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, runtimeId, options) {
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.runtimeId === runtimeId)
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, runtimeId, runtimeIdentity, options) {
236
+ async function writeClaim(header, selected, owner, runtimeIdentity, options) {
228
237
  const claim = {
229
238
  schema: CLAIM_SCHEMA,
230
- runtimeId,
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(runtimeId, options) {
245
- return await readJsonRecord(runtimeRecordPath(runtimeId, options), isRuntimeRecord);
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 isClaimRecord(value) ? value : null;
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
- async function releaseClaimsOwnedBy(runtimeId, keepGitDir, options) {
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 readJsonRecord(path, isClaimRecord);
287
- if (claim?.runtimeId === runtimeId && (keepGitDir === null || !samePath(claim.worktreeGitDir, keepGitDir))) {
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, runtimeId, options) {
293
- await removeClaimByGitDir(selected.commonDir, selected.gitDir, runtimeId, options);
336
+ async function removeMatchingClaim(selected, owner, options) {
337
+ await removeClaimByGitDir(selected.commonDir, selected.gitDir, owner, options);
294
338
  }
295
- async function removeClaimByGitDir(commonDir, gitDir, runtimeId, options) {
339
+ async function removeClaimByGitDir(commonDir, gitDir, owner, options) {
296
340
  const path = claimRecordPath(commonDir, gitDir, options);
297
- const claim = await readJsonRecord(path, isClaimRecord);
298
- if (claim?.runtimeId === runtimeId)
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
- while (handle === undefined) {
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
- await handle.writeFile(`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`);
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 runtimeRecordPath(runtimeId, options) {
459
- return join(claimStoreRoot(options), "runtimes", `${digest(runtimeId)}.json`);
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 isRuntimeRecord(value) {
524
- return isRecord(value) && value.schema === RUNTIME_SCHEMA && validSessionId(value.runtimeId) && validSessionId(value.sessionId)
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 isClaimRecord(value) {
530
- return isRecord(value) && value.schema === CLAIM_SCHEMA && validSessionId(value.runtimeId) && validSessionId(value.sessionId)
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
  }