@gajae-code/coding-agent 0.15.6 → 0.16.1

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 (270) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/root-flags.d.ts +14 -0
  6. package/dist/types/cli/update-cli.d.ts +14 -0
  7. package/dist/types/cli.d.ts +7 -0
  8. package/dist/types/commands/launch.d.ts +10 -0
  9. package/dist/types/commands/sdk.d.ts +2 -0
  10. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  11. package/dist/types/config/billing-path.d.ts +25 -0
  12. package/dist/types/config/config-schema-version.d.ts +13 -1
  13. package/dist/types/config/file-lock.d.ts +5 -0
  14. package/dist/types/config/model-registry.d.ts +22 -0
  15. package/dist/types/config/settings-schema.d.ts +27 -8
  16. package/dist/types/config/settings.d.ts +2 -0
  17. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  18. package/dist/types/crash/record-loader.d.ts +15 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  20. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  21. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  22. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  23. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  24. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  26. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  27. package/dist/types/main.d.ts +1 -0
  28. package/dist/types/master-mode/context.d.ts +21 -0
  29. package/dist/types/master-mode/first-request.d.ts +34 -0
  30. package/dist/types/master-mode/snapshot.d.ts +32 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  32. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  33. package/dist/types/modes/components/model-selector.d.ts +4 -0
  34. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  35. package/dist/types/modes/components/session-selector.d.ts +1 -1
  36. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  37. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  38. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  39. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  40. package/dist/types/modes/interactive-mode.d.ts +16 -2
  41. package/dist/types/modes/print-mode.d.ts +17 -0
  42. package/dist/types/modes/theme/theme.d.ts +2 -1
  43. package/dist/types/modes/types.d.ts +2 -0
  44. package/dist/types/modes/ui-language.d.ts +7 -1
  45. package/dist/types/sdk/broker/broker.d.ts +79 -2
  46. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  47. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  48. package/dist/types/sdk/broker/index.d.ts +3 -0
  49. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  50. package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
  51. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  52. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  53. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  54. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  55. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  56. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  57. package/dist/types/sdk/broker/state-version.d.ts +21 -1
  58. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  59. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  60. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  61. package/dist/types/sdk/bus/index.d.ts +9 -1
  62. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  63. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  64. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  65. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  67. package/dist/types/sdk/cli/index.d.ts +1 -0
  68. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  69. package/dist/types/sdk/cli/rows.d.ts +12 -5
  70. package/dist/types/sdk/cli/session-cli.d.ts +20 -3
  71. package/dist/types/sdk/client/client.d.ts +3 -2
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/events.d.ts +1 -0
  77. package/dist/types/sdk/host/host.d.ts +10 -0
  78. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  79. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  80. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +70 -17
  82. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  83. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  84. package/dist/types/sdk/lifecycle-session.d.ts +3 -1
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  86. package/dist/types/sdk/receipt-state.d.ts +2 -0
  87. package/dist/types/sdk/router/session-router.d.ts +6 -0
  88. package/dist/types/sdk/session.d.ts +16 -0
  89. package/dist/types/sdk/startup-capability.d.ts +2 -1
  90. package/dist/types/sdk/turn-result.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +52 -7
  92. package/dist/types/session/credential-store-errors.d.ts +1 -0
  93. package/dist/types/session/fold-coordinator.d.ts +29 -2
  94. package/dist/types/session/session-manager.d.ts +79 -2
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/tools/ask.d.ts +1 -0
  98. package/dist/types/tools/bash.d.ts +16 -0
  99. package/dist/types/tools/browser/launch.d.ts +10 -0
  100. package/dist/types/tools/browser/registry.d.ts +8 -0
  101. package/dist/types/tools/gh.d.ts +8 -0
  102. package/dist/types/tools/index.d.ts +10 -10
  103. package/dist/types/tools/job.d.ts +3 -1
  104. package/dist/types/utils/external-editor.d.ts +7 -0
  105. package/examples/extensions/README.md +1 -1
  106. package/examples/extensions/pirate.ts +7 -4
  107. package/package.json +7 -7
  108. package/scripts/generate-sdk-operation-inventory.ts +13 -1
  109. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  110. package/src/async/job-manager.ts +107 -9
  111. package/src/browser-backend.ts +29 -0
  112. package/src/cli/accounts-cli.ts +9 -5
  113. package/src/cli/args.ts +17 -0
  114. package/src/cli/fast-help.ts +1 -0
  115. package/src/cli/plugin-cli.ts +29 -0
  116. package/src/cli/root-flags.ts +5 -0
  117. package/src/cli/update-cli.ts +140 -23
  118. package/src/commands/launch.ts +86 -18
  119. package/src/commands/sdk.ts +135 -28
  120. package/src/config/autorouting-tier-map.ts +12 -0
  121. package/src/config/billing-path.ts +42 -0
  122. package/src/config/config-schema-version.ts +18 -1
  123. package/src/config/file-lock.ts +158 -83
  124. package/src/config/model-preset-registry.ts +178 -23
  125. package/src/config/model-profiles.ts +5 -5
  126. package/src/config/model-registry.ts +291 -42
  127. package/src/config/model-resolver.ts +5 -0
  128. package/src/config/settings-schema.ts +27 -8
  129. package/src/config/settings.ts +51 -15
  130. package/src/coordinator-mcp/projection-scan.ts +27 -6
  131. package/src/coordinator-mcp/question-state.ts +9 -11
  132. package/src/coordinator-mcp/server.ts +115 -21
  133. package/src/crash/index-store.ts +2 -0
  134. package/src/crash/record-loader.ts +10 -0
  135. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  136. package/src/extensibility/extensions/runner.ts +27 -0
  137. package/src/extensibility/extensions/types.ts +8 -3
  138. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  139. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  140. package/src/extensibility/skills.ts +22 -12
  141. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  142. package/src/gjc-runtime/launch-worktree.ts +285 -4
  143. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  144. package/src/gjc-runtime/session-state-lock.ts +213 -14
  145. package/src/gjc-runtime/session-state-sidecar.ts +938 -39
  146. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  147. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
  148. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  149. package/src/hooks/skill-state.ts +6 -0
  150. package/src/internal-urls/docs-index.generated.ts +15 -15
  151. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  152. package/src/main.ts +61 -3
  153. package/src/master-mode/context.ts +50 -0
  154. package/src/master-mode/first-request.ts +65 -0
  155. package/src/master-mode/snapshot.ts +205 -0
  156. package/src/modes/acp/acp-agent.ts +610 -161
  157. package/src/modes/components/footer.ts +4 -2
  158. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  159. package/src/modes/components/model-selector.ts +76 -8
  160. package/src/modes/components/oauth-selector.ts +6 -1
  161. package/src/modes/components/session-selector.ts +23 -15
  162. package/src/modes/components/sessions-dashboard.ts +8 -5
  163. package/src/modes/components/settings-selector.ts +1 -0
  164. package/src/modes/components/status-line/model-name.ts +53 -0
  165. package/src/modes/components/status-line/priority-row.ts +107 -0
  166. package/src/modes/components/status-line/segments.ts +23 -9
  167. package/src/modes/components/tool-status-header.ts +151 -15
  168. package/src/modes/controllers/command-controller.ts +1 -1
  169. package/src/modes/controllers/event-controller.ts +30 -4
  170. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  171. package/src/modes/controllers/input-controller.ts +30 -2
  172. package/src/modes/controllers/selector-controller.ts +200 -98
  173. package/src/modes/interactive-mode.ts +36 -12
  174. package/src/modes/print-mode.ts +64 -0
  175. package/src/modes/prompt-action-autocomplete.ts +30 -4
  176. package/src/modes/runtime-init.ts +12 -9
  177. package/src/modes/theme/theme.ts +5 -0
  178. package/src/modes/types.ts +2 -0
  179. package/src/modes/ui-language.ts +49 -2
  180. package/src/modes/utils/ui-helpers.ts +22 -3
  181. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  182. package/src/prompts/system/master-mode.md +28 -0
  183. package/src/prompts/tools/ask.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -0
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/sdk/acp/adapter.ts +5 -0
  187. package/src/sdk/broker/broker.ts +1646 -27
  188. package/src/sdk/broker/endpoint-authority.ts +86 -0
  189. package/src/sdk/broker/ensure.ts +81 -3
  190. package/src/sdk/broker/index.ts +3 -0
  191. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  192. package/src/sdk/broker/lifecycle.ts +440 -75
  193. package/src/sdk/broker/master-capability.ts +232 -0
  194. package/src/sdk/broker/session-index.ts +121 -17
  195. package/src/sdk/broker/session-scope.ts +272 -0
  196. package/src/sdk/broker/spawn-authority.ts +1031 -0
  197. package/src/sdk/broker/spawn-substrate.ts +591 -0
  198. package/src/sdk/broker/startup-budget.ts +144 -2
  199. package/src/sdk/broker/state-version.ts +43 -14
  200. package/src/sdk/broker/transport.ts +1 -0
  201. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  202. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  203. package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
  204. package/src/sdk/bus/discord-daemon.ts +50 -10
  205. package/src/sdk/bus/index.ts +84 -7
  206. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  207. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  208. package/src/sdk/bus/notification-service.ts +22 -5
  209. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  210. package/src/sdk/bus/reconciliation-store.ts +55 -31
  211. package/src/sdk/bus/slack-conversation.ts +1 -1
  212. package/src/sdk/bus/slack-daemon.ts +72 -38
  213. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  214. package/src/sdk/cli/index.ts +1 -0
  215. package/src/sdk/cli/master-cli.ts +259 -0
  216. package/src/sdk/cli/rows.ts +41 -15
  217. package/src/sdk/cli/session-cli.ts +297 -26
  218. package/src/sdk/client/client.ts +14 -8
  219. package/src/sdk/host/control/dispatch.ts +101 -40
  220. package/src/sdk/host/control/index.ts +2 -0
  221. package/src/sdk/host/control/operations.ts +2 -0
  222. package/src/sdk/host/control/runtime-gate.ts +203 -11
  223. package/src/sdk/host/events.ts +1 -0
  224. package/src/sdk/host/host.ts +29 -2
  225. package/src/sdk/host/query/cursor.ts +52 -7
  226. package/src/sdk/host/query/handlers.ts +33 -12
  227. package/src/sdk/host/query/revision-store.ts +12 -0
  228. package/src/sdk/host/session-runtime.test.ts +673 -14
  229. package/src/sdk/host/session-runtime.ts +674 -103
  230. package/src/sdk/host/surface-policy.ts +3 -3
  231. package/src/sdk/lifecycle/broker-client.ts +28 -0
  232. package/src/sdk/lifecycle/service.ts +279 -20
  233. package/src/sdk/lifecycle-session.ts +17 -4
  234. package/src/sdk/prompt-deadline-manager.ts +2 -1
  235. package/src/sdk/protocol/operation-inventory.generated.json +105 -3
  236. package/src/sdk/protocol/operation-registry.ts +14 -4
  237. package/src/sdk/receipt-state.ts +10 -0
  238. package/src/sdk/router/session-router.ts +219 -118
  239. package/src/sdk/session.ts +416 -195
  240. package/src/sdk/startup-capability.ts +1 -0
  241. package/src/sdk/turn-result.ts +22 -0
  242. package/src/session/agent-session.ts +1628 -318
  243. package/src/session/blob-store.ts +15 -5
  244. package/src/session/credential-store-errors.ts +2 -0
  245. package/src/session/fold-coordinator.ts +40 -3
  246. package/src/session/internal/managed-session-storage.ts +153 -78
  247. package/src/session/session-manager.ts +850 -97
  248. package/src/session/yield-queue.ts +24 -10
  249. package/src/setup/frictionless-onboarding.ts +2 -0
  250. package/src/slash-commands/builtin-registry.ts +41 -0
  251. package/src/task/executor.ts +9 -4
  252. package/src/telemetry/events.ts +1138 -12
  253. package/src/tools/ask-contract.test.ts +64 -0
  254. package/src/tools/ask-contract.ts +36 -1
  255. package/src/tools/ask.ts +10 -8
  256. package/src/tools/bash-interactive.ts +11 -2
  257. package/src/tools/bash.ts +412 -75
  258. package/src/tools/browser/launch.ts +37 -0
  259. package/src/tools/browser/registry.ts +99 -9
  260. package/src/tools/cron.ts +6 -4
  261. package/src/tools/descriptors.test.ts +15 -0
  262. package/src/tools/descriptors.ts +5 -1
  263. package/src/tools/gh.ts +104 -48
  264. package/src/tools/github-cache.ts +2 -2
  265. package/src/tools/index.ts +10 -6
  266. package/src/tools/job.ts +4 -1
  267. package/src/tools/monitor.ts +12 -9
  268. package/src/tools/resolve.ts +30 -10
  269. package/src/tools/tool-catalog.generated.ts +2 -2
  270. package/src/utils/external-editor.ts +72 -1
@@ -0,0 +1,232 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import * as path from "node:path";
3
+ import { readEndpointFile } from "./endpoint-authority";
4
+ import {
5
+ type IndexedSession,
6
+ isSessionAuthorityEligible,
7
+ type MasterRoleAttestationV2,
8
+ type SessionIndex,
9
+ } from "./session-index";
10
+ import type { MasterCapabilityVerifier } from "./spawn-authority";
11
+
12
+ const MASTER_CAPABILITY_VERIFY_TIMEOUT_MS = 2_000;
13
+
14
+ export type SessionEndpoint = {
15
+ url: string;
16
+ token: string;
17
+ };
18
+
19
+ function processIdentity(record: Pick<IndexedSession, "hostIncarnation" | "processIncarnation">): string | undefined {
20
+ return record.hostIncarnation ?? record.processIncarnation;
21
+ }
22
+
23
+ function matchesAttestation(left: MasterRoleAttestationV2, right: MasterRoleAttestationV2): boolean {
24
+ return (
25
+ left.version === right.version &&
26
+ left.ownerSessionId === right.ownerSessionId &&
27
+ left.launchPid === right.launchPid &&
28
+ left.launchProcessIncarnation === right.launchProcessIncarnation &&
29
+ left.role === right.role &&
30
+ left.attestationEpoch === right.attestationEpoch
31
+ );
32
+ }
33
+
34
+ function isEffectiveMasterHost(
35
+ record: IndexedSession,
36
+ ownerSessionId: string,
37
+ attestationEpoch: string,
38
+ ): record is IndexedSession & { masterRole: MasterRoleAttestationV2 } {
39
+ const attestation = record.masterRole;
40
+ return (
41
+ record.endpointGeneration > 0 &&
42
+ record.live &&
43
+ !record.terminal &&
44
+ !record.terminalUncertain &&
45
+ isSessionAuthorityEligible(record) &&
46
+ attestation !== undefined &&
47
+ attestation.version === 2 &&
48
+ attestation.role === "master" &&
49
+ attestation.ownerSessionId === ownerSessionId &&
50
+ attestation.attestationEpoch === attestationEpoch &&
51
+ attestation.launchPid === record.pid &&
52
+ attestation.launchProcessIncarnation === processIdentity(record)
53
+ );
54
+ }
55
+
56
+ function sameAttachment(left: IndexedSession, right: IndexedSession): boolean {
57
+ return (
58
+ left.sessionId === right.sessionId &&
59
+ left.endpointGeneration === right.endpointGeneration &&
60
+ left.pid === right.pid &&
61
+ left.endpointMtimeMs === right.endpointMtimeMs &&
62
+ left.endpointFileId === right.endpointFileId &&
63
+ left.hostIncarnation === right.hostIncarnation &&
64
+ left.processIncarnation === right.processIncarnation &&
65
+ left.indexSeq === right.indexSeq &&
66
+ left.masterRole !== undefined &&
67
+ right.masterRole !== undefined &&
68
+ matchesAttestation(left.masterRole, right.masterRole)
69
+ );
70
+ }
71
+
72
+ function endpointFileId(stat: { dev: bigint; ino: bigint }): string {
73
+ return `${stat.dev}:${stat.ino}`;
74
+ }
75
+
76
+ function adoptedDirectAttestation(
77
+ rows: readonly IndexedSession[],
78
+ effective: IndexedSession & { masterRole: MasterRoleAttestationV2 },
79
+ ): boolean {
80
+ return rows.some(row => {
81
+ const direct = row.masterRole;
82
+ return (
83
+ row.sessionId === effective.sessionId &&
84
+ row.endpointGeneration === 0 &&
85
+ row.pid === effective.masterRole.launchPid &&
86
+ processIdentity(row) === effective.masterRole.launchProcessIncarnation &&
87
+ direct !== undefined &&
88
+ matchesAttestation(direct, effective.masterRole)
89
+ );
90
+ });
91
+ }
92
+
93
+ /** Reads and validates a registered host's authenticated endpoint record. */
94
+ export async function readEndpoint(record: IndexedSession): Promise<SessionEndpoint | undefined> {
95
+ if (record.endpointMtimeMs === undefined || record.endpointFileId === undefined) return undefined;
96
+ try {
97
+ const endpointPath = path.join(record.locator.stateRoot, "sdk", `${record.sessionId}.json`);
98
+ const file = await readEndpointFile(endpointPath);
99
+ if (!file) return undefined;
100
+ const endpoint = JSON.parse(file.source) as Record<string, unknown>;
101
+ if (
102
+ endpoint.sessionId !== record.sessionId ||
103
+ endpoint.pid !== record.pid ||
104
+ typeof endpoint.url !== "string" ||
105
+ typeof endpoint.token !== "string" ||
106
+ endpoint.url.length === 0 ||
107
+ endpoint.token.length === 0 ||
108
+ endpointFileId(file) !== record.endpointFileId ||
109
+ Math.abs(file.mtimeMs - record.endpointMtimeMs) > 0.001
110
+ )
111
+ return undefined;
112
+ const url = new URL(endpoint.url);
113
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") return undefined;
114
+ return { url: endpoint.url, token: endpoint.token };
115
+ } catch {
116
+ return undefined;
117
+ }
118
+ }
119
+
120
+ async function verifyOverAttachment(input: {
121
+ endpoint: SessionEndpoint;
122
+ nonce: string;
123
+ attestationEpoch: string;
124
+ capability: string;
125
+ }): Promise<boolean> {
126
+ const settled = Promise.withResolvers<boolean>();
127
+ let complete = false;
128
+ const settle = (allowed: boolean): void => {
129
+ if (complete) return;
130
+ complete = true;
131
+ settled.resolve(allowed);
132
+ };
133
+ let socket: WebSocket | undefined;
134
+ try {
135
+ const url = new URL(input.endpoint.url);
136
+ url.searchParams.set("token", input.endpoint.token);
137
+ socket = new WebSocket(url);
138
+ const onClose = (): void => {
139
+ settle(false);
140
+ };
141
+ const onError = (): void => {
142
+ settle(false);
143
+ };
144
+ const onMessage = (event: MessageEvent): void => {
145
+ let frame: Record<string, unknown>;
146
+ try {
147
+ const parsed = JSON.parse(String(event.data));
148
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
149
+ frame = parsed as Record<string, unknown>;
150
+ } catch {
151
+ return;
152
+ }
153
+ if (frame.type === "hello") {
154
+ try {
155
+ socket?.send(JSON.stringify({ type: "hello", protocolVersion: 3, capabilities: [] }));
156
+ socket?.send(
157
+ JSON.stringify({
158
+ type: "master_capability_verify",
159
+ id: input.nonce,
160
+ nonce: input.nonce,
161
+ attestationEpoch: input.attestationEpoch,
162
+ capability: input.capability,
163
+ }),
164
+ );
165
+ } catch {
166
+ settle(false);
167
+ }
168
+ return;
169
+ }
170
+ if (frame.type !== "master_capability_verify_result") return;
171
+ settle(
172
+ frame.id === input.nonce &&
173
+ frame.ok === true &&
174
+ frame.nonce === input.nonce &&
175
+ frame.attestationEpoch === input.attestationEpoch,
176
+ );
177
+ };
178
+ socket.addEventListener("error", onError);
179
+ socket.addEventListener("close", onClose);
180
+ socket.addEventListener("message", onMessage);
181
+ return await Promise.race([settled.promise, Bun.sleep(MASTER_CAPABILITY_VERIFY_TIMEOUT_MS).then(() => false)]);
182
+ } catch {
183
+ return false;
184
+ } finally {
185
+ try {
186
+ socket?.close();
187
+ } catch {
188
+ // Closing an already failed attachment is best effort.
189
+ }
190
+ }
191
+ }
192
+
193
+ /** Creates the ephemeral effective-host verifier used by broker spawn admission. */
194
+ export function createMasterCapabilityVerifier(index: SessionIndex): MasterCapabilityVerifier {
195
+ return {
196
+ async verifyMasterCapability(
197
+ ownerSessionId: string,
198
+ rawCapability: string,
199
+ attestationEpoch: string,
200
+ ): Promise<{ allowed: boolean }> {
201
+ try {
202
+ await index.refresh();
203
+ const rows = index.listSessionIdentities();
204
+ const effective = rows.filter(row => isEffectiveMasterHost(row, ownerSessionId, attestationEpoch));
205
+ if (effective.length !== 1) return { allowed: false };
206
+ const attachment = effective[0]!;
207
+ if (!adoptedDirectAttestation(rows, attachment)) return { allowed: false };
208
+ const endpoint = await readEndpoint(attachment);
209
+ if (!endpoint) return { allowed: false };
210
+ await index.refresh();
211
+ const current = index.listSessionIdentities().find(row => sameAttachment(attachment, row));
212
+ if (!current || !isEffectiveMasterHost(current, ownerSessionId, attestationEpoch))
213
+ return { allowed: false };
214
+ const nonce = randomBytes(32).toString("base64url");
215
+ const allowed = await verifyOverAttachment({
216
+ endpoint,
217
+ nonce,
218
+ attestationEpoch,
219
+ capability: rawCapability,
220
+ });
221
+ await index.refresh();
222
+ const after = index.listSessionIdentities().find(row => sameAttachment(attachment, row));
223
+ return {
224
+ allowed:
225
+ allowed && after !== undefined && isEffectiveMasterHost(after, ownerSessionId, attestationEpoch),
226
+ };
227
+ } catch {
228
+ return { allowed: false };
229
+ }
230
+ },
231
+ };
232
+ }
@@ -1,14 +1,16 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { logger, resolveEquivalentPath } from "@gajae-code/utils";
5
5
  import { nativeProcessBindings } from "@gajae-code/utils/native-process";
6
6
  import { withFileLock } from "../../config/file-lock";
7
+ import { repo } from "../../utils/git";
7
8
  import { processIncarnation } from "./process-incarnation";
8
9
  import {
10
+ assertSupportedSessionIndexEventVersion,
9
11
  assertSupportedSnapshotVersion,
10
- assertSupportedStateVersion,
11
12
  SDK_STATE_VERSION,
13
+ SESSION_INDEX_EVENT_VERSION,
12
14
  SESSION_INDEX_SNAPSHOT_VERSION,
13
15
  UnsupportedStateVersionError,
14
16
  } from "./state-version";
@@ -53,12 +55,79 @@ export interface SessionIndexProcessObservation {
53
55
  readonly incarnation: string;
54
56
  readonly isRunning: () => boolean;
55
57
  }
58
+ /**
59
+ * Canonical location facts for one SDK session.
60
+ *
61
+ * `cwd` is always a realpath-canonicalized directory. `worktreeRoot` is the
62
+ * canonical Git worktree root when `cwd` is in a worktree, otherwise `null`.
63
+ * `stateRoot` retains its host-provided authority spelling and is never derived
64
+ * from either canonical path.
65
+ */
66
+ export type SessionLocatorV2 = {
67
+ cwd: string;
68
+ worktreeRoot: string | null;
69
+ stateRoot: string;
70
+ };
71
+
72
+ /** Durable master-role facts. `attestationEpoch` is opaque random broker state. */
73
+ export type MasterRoleAttestationV2 = {
74
+ version: 2;
75
+ ownerSessionId: string;
76
+ launchPid: number;
77
+ launchProcessIncarnation: string;
78
+ role: "master";
79
+ attestationEpoch: string;
80
+ };
81
+
82
+ export function newMasterAttestationEpoch(): string {
83
+ return randomBytes(32).toString("base64url");
84
+ }
85
+
86
+ /** Canonicalize a cwd without making an unavailable path a launch failure. */
87
+ export async function canonicalSessionCwd(cwd: string): Promise<string> {
88
+ try {
89
+ return await fs.realpath(cwd);
90
+ } catch {
91
+ return path.resolve(cwd);
92
+ }
93
+ }
94
+
95
+ /** Resolve the canonical worktree root; non-Git and probe failures are `null`. */
96
+ export async function sessionWorktreeRoot(cwd: string): Promise<string | null> {
97
+ try {
98
+ const repository = await repo.resolve(cwd);
99
+ return repository ? await canonicalSessionCwd(repository.repoRoot) : null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ /** Build the only permitted locator shape for a newly registered session. */
106
+ export async function resolveSessionLocator(cwd: string, stateRoot: string): Promise<SessionLocatorV2> {
107
+ const canonicalCwd = await canonicalSessionCwd(cwd);
108
+ return { cwd: canonicalCwd, worktreeRoot: await sessionWorktreeRoot(canonicalCwd), stateRoot };
109
+ }
110
+
111
+ function sessionLocatorV2(locator: unknown): locator is SessionLocatorV2 {
112
+ if (typeof locator !== "object" || locator === null || Array.isArray(locator)) return false;
113
+ const keys = Object.keys(locator);
114
+ if (keys.length !== 3 || !keys.every(key => key === "cwd" || key === "worktreeRoot" || key === "stateRoot"))
115
+ return false;
116
+ return (
117
+ typeof (locator as { cwd?: unknown }).cwd === "string" &&
118
+ (locator as { cwd: string }).cwd.length > 0 &&
119
+ (typeof (locator as { worktreeRoot?: unknown }).worktreeRoot === "string" ||
120
+ (locator as { worktreeRoot?: unknown }).worktreeRoot === null) &&
121
+ typeof (locator as { stateRoot?: unknown }).stateRoot === "string" &&
122
+ (locator as { stateRoot: string }).stateRoot.length > 0
123
+ );
124
+ }
56
125
  export interface SessionIndexEvent {
57
- version: typeof SDK_STATE_VERSION;
126
+ version: typeof SDK_STATE_VERSION | typeof SESSION_INDEX_EVENT_VERSION;
58
127
  indexSeq: number;
59
128
  type: SessionIndexEventType;
60
129
  sessionId: string;
61
- locator: { repo: string; stateRoot: string };
130
+ locator: SessionLocatorV2;
62
131
  endpointGeneration: number;
63
132
  pid: number;
64
133
  /**
@@ -69,23 +138,28 @@ export interface SessionIndexEvent {
69
138
  */
70
139
  processIncarnation?: string;
71
140
  endpointMtimeMs?: number;
141
+ /** Immutable endpoint file identity captured by the broker at registration. */
142
+ endpointFileId?: string;
72
143
  lifecycleRequestId?: string;
73
144
  terminalUncertain?: boolean;
74
145
  /** OS process incarnation (C1); absent on legacy v1/v2 events. */
75
146
  hostIncarnation?: string;
76
147
  /** Present on host_heartbeat checkpoints (C2). */
77
148
  activity?: SessionActivity;
149
+ masterRole?: MasterRoleAttestationV2;
78
150
  ts: number;
79
151
  checksum: string;
80
152
  }
81
153
  export interface IndexedSession {
82
154
  sessionId: string;
83
- locator: { repo: string; stateRoot: string };
155
+ locator: SessionLocatorV2;
84
156
  endpointGeneration: number;
85
157
  pid: number;
86
158
  /** OS start incarnation of `pid` as published by its own host at registration. */
87
159
  processIncarnation?: string;
88
160
  endpointMtimeMs?: number;
161
+ /** Immutable endpoint file identity captured by the broker at registration. */
162
+ endpointFileId?: string;
89
163
  live: boolean;
90
164
  indexSeq: number;
91
165
  lifecycleRequestId?: string;
@@ -95,6 +169,7 @@ export interface IndexedSession {
95
169
  activity?: SessionActivity;
96
170
  /** Wall-clock timestamp of the latest admitted heartbeat, when one exists. */
97
171
  lastHeartbeatAt?: number;
172
+ masterRole?: MasterRoleAttestationV2;
98
173
  /** True when more than one unresolved authority-fencing state-root identity claims this session id. */
99
174
  ambiguous: boolean;
100
175
  /** True when the identity's latest event is terminal (DR-1 retains stopped rows for inspection/offline tail). */
@@ -164,7 +239,10 @@ interface SessionIndexScan {
164
239
  }
165
240
 
166
241
  /** Admission-fence rejection codes recorded in the durable index audit (C5/C4). */
167
- export type SessionIndexAuditCode = "rejected_superseded_incarnation" | "rejected_after_tombstone";
242
+ export type SessionIndexAuditCode =
243
+ | "rejected_superseded_incarnation"
244
+ | "rejected_after_tombstone"
245
+ | "rejected_legacy_locator";
168
246
  export interface SessionIndexAuditRecord {
169
247
  version: typeof SDK_STATE_VERSION;
170
248
  code: SessionIndexAuditCode;
@@ -268,6 +346,14 @@ const tupleKey = (event: SessionIndexEvent) =>
268
346
  const effectiveIncarnation = (event: SessionIndexEvent) => event.hostIncarnation ?? event.processIncarnation;
269
347
  const identityKey = (event: SessionIndexEvent) => `${tupleKey(event)}\u0000${effectiveIncarnation(event) ?? ""}`;
270
348
 
349
+ function hasSessionLocatorV2(event: SessionIndexEvent): boolean {
350
+ return sessionLocatorV2(event.locator);
351
+ }
352
+
353
+ function legacyLocatorDiagnostic(event: SessionIndexEvent): string {
354
+ return `Session ${event.sessionId} has a legacy locator row and must re-register.`;
355
+ }
356
+
271
357
  interface ResolvedRetentionPolicy {
272
358
  clock: () => number;
273
359
  maxAgeMs: number;
@@ -305,7 +391,17 @@ interface Admission {
305
391
  */
306
392
  function admitEvents(events: SessionIndexEvent[]): Admission {
307
393
  const authoritative = new Map<string, { incarnation: string | undefined; indexSeq: number }>();
394
+ const rejected: RejectedEvent[] = [];
308
395
  for (const event of events) {
396
+ if (!hasSessionLocatorV2(event)) {
397
+ rejected.push({
398
+ code: "rejected_legacy_locator",
399
+ event,
400
+ supersededByIncarnation: undefined,
401
+ supersededByIndexSeq: event.indexSeq,
402
+ });
403
+ continue;
404
+ }
309
405
  if (event.type !== "host_registered") continue;
310
406
  const key = tupleKey(event);
311
407
  const current = authoritative.get(key);
@@ -314,8 +410,8 @@ function admitEvents(events: SessionIndexEvent[]): Admission {
314
410
  }
315
411
  }
316
412
  const admitted: SessionIndexEvent[] = [];
317
- const rejected: RejectedEvent[] = [];
318
413
  for (const event of events) {
414
+ if (!hasSessionLocatorV2(event)) continue;
319
415
  const authority = authoritative.get(tupleKey(event));
320
416
  if (
321
417
  authority !== undefined &&
@@ -378,7 +474,7 @@ function auditRecords(events: SessionIndexEvent[], ts: number): SessionIndexAudi
378
474
  indexSeq: rejection.event.indexSeq,
379
475
  sessionId: rejection.event.sessionId,
380
476
  endpointGeneration: rejection.event.endpointGeneration,
381
- stateRoot: rejection.event.locator.stateRoot,
477
+ stateRoot: hasSessionLocatorV2(rejection.event) ? rejection.event.locator.stateRoot : "unknown",
382
478
  ...(rejection.event.hostIncarnation !== undefined ? { hostIncarnation: rejection.event.hostIncarnation } : {}),
383
479
  ...(rejection.supersededByIncarnation !== undefined
384
480
  ? { supersededByIncarnation: rejection.supersededByIncarnation }
@@ -499,10 +595,12 @@ function projectIdentity(
499
595
  pid: latest.pid,
500
596
  processIncarnation: latest.processIncarnation,
501
597
  endpointMtimeMs: latest.endpointMtimeMs,
598
+ endpointFileId: latest.endpointFileId,
502
599
  lifecycleRequestId: latest.lifecycleRequestId,
503
600
  terminalUncertain,
504
601
  indexSeq: latest.indexSeq,
505
602
  hostIncarnation: latest.hostIncarnation,
603
+ masterRole: latest.masterRole,
506
604
  identityProvenance: recordedIncarnation === undefined ? "legacy" : "composite",
507
605
  activity: heartbeat?.activity,
508
606
  lastHeartbeatAt: heartbeat?.ts,
@@ -1140,6 +1238,9 @@ export class SessionIndex {
1140
1238
  if (scan.diagnosis.status === "unsupported") throw scan.unsupportedError!;
1141
1239
  this.#events = [...scan.snapshotEvents, ...scan.validLogEvents];
1142
1240
  this.#warnings = [];
1241
+ for (const event of this.#events) {
1242
+ if (!hasSessionLocatorV2(event)) this.#warn(legacyLocatorDiagnostic(event));
1243
+ }
1143
1244
  this.#logOffset = scan.logContents?.length ?? 0;
1144
1245
  this.#corruptSuffix = scan.diagnosis.status === "corrupt";
1145
1246
  if (scan.diagnosis.reason === "invalid snapshot") this.#warnings.push("Invalid session index snapshot");
@@ -1195,7 +1296,7 @@ export class SessionIndex {
1195
1296
  if (Array.isArray(snapshot.events)) {
1196
1297
  try {
1197
1298
  for (const event of snapshot.events) {
1198
- assertSupportedStateVersion(snapshotFor(this.#agentDir), event);
1299
+ assertSupportedSessionIndexEventVersion(snapshotFor(this.#agentDir), event);
1199
1300
  supportedEvents.push(event as SessionIndexEvent);
1200
1301
  }
1201
1302
  } catch (error) {
@@ -1234,7 +1335,7 @@ export class SessionIndex {
1234
1335
  if (!line) continue;
1235
1336
  try {
1236
1337
  const event = JSON.parse(line) as SessionIndexEvent;
1237
- assertSupportedStateVersion(logFor(this.#agentDir), event);
1338
+ assertSupportedSessionIndexEventVersion(logFor(this.#agentDir), event);
1238
1339
  const { checksum, ...unsigned } = event;
1239
1340
  if (checksum !== sessionIndexChecksum(unsigned)) {
1240
1341
  corrupt = true;
@@ -1401,7 +1502,7 @@ export class SessionIndex {
1401
1502
  let event: SessionIndexEvent;
1402
1503
  try {
1403
1504
  event = JSON.parse(line) as SessionIndexEvent;
1404
- assertSupportedStateVersion(logFor(this.#agentDir), event);
1505
+ assertSupportedSessionIndexEventVersion(logFor(this.#agentDir), event);
1405
1506
  } catch (error) {
1406
1507
  if (error instanceof UnsupportedStateVersionError) throw error;
1407
1508
  corrupt = true;
@@ -1485,7 +1586,7 @@ export class SessionIndex {
1485
1586
  }
1486
1587
  const unsigned: Omit<SessionIndexEvent, "checksum"> = {
1487
1588
  ...input,
1488
- version: SDK_STATE_VERSION,
1589
+ version: SESSION_INDEX_EVENT_VERSION,
1489
1590
  indexSeq: this.indexSeq + 1,
1490
1591
  ts: input.ts ?? Date.now(),
1491
1592
  };
@@ -1535,7 +1636,7 @@ export class SessionIndex {
1535
1636
  session.processIncarnation === expected.processIncarnation &&
1536
1637
  (session.hostIncarnation ?? session.processIncarnation) ===
1537
1638
  (expected.hostIncarnation ?? expected.processIncarnation) &&
1538
- resolveEquivalentPath(session.locator.repo) === resolveEquivalentPath(expected.locator.repo) &&
1639
+ resolveEquivalentPath(session.locator.cwd) === resolveEquivalentPath(expected.locator.cwd) &&
1539
1640
  path.resolve(session.locator.stateRoot) === path.resolve(expected.locator.stateRoot),
1540
1641
  );
1541
1642
  let currentRoot: IndexedSession | undefined;
@@ -1543,7 +1644,7 @@ export class SessionIndex {
1543
1644
  if (
1544
1645
  session.sessionId !== expected.sessionId ||
1545
1646
  session.terminal ||
1546
- resolveEquivalentPath(session.locator.repo) !== resolveEquivalentPath(expected.locator.repo) ||
1647
+ resolveEquivalentPath(session.locator.cwd) !== resolveEquivalentPath(expected.locator.cwd) ||
1547
1648
  path.resolve(session.locator.stateRoot) !== path.resolve(expected.locator.stateRoot)
1548
1649
  )
1549
1650
  continue;
@@ -1564,7 +1665,7 @@ export class SessionIndex {
1564
1665
  )
1565
1666
  return false;
1566
1667
  const unsigned: Omit<SessionIndexEvent, "checksum"> = {
1567
- version: SDK_STATE_VERSION,
1668
+ version: SESSION_INDEX_EVENT_VERSION,
1568
1669
  indexSeq: this.indexSeq + 1,
1569
1670
  ts: Date.now(),
1570
1671
  type: "host_unregistered",
@@ -2100,7 +2201,7 @@ export class SessionIndex {
2100
2201
  const current = probed.get(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`);
2101
2202
  if (current === undefined || current !== recordedIncarnation) continue;
2102
2203
  const unsigned: Omit<SessionIndexEvent, "checksum"> = {
2103
- version: SDK_STATE_VERSION,
2204
+ version: SESSION_INDEX_EVENT_VERSION,
2104
2205
  indexSeq: this.indexSeq + events.length + 1,
2105
2206
  type: "host_heartbeat",
2106
2207
  sessionId: row.sessionId,
@@ -2109,6 +2210,7 @@ export class SessionIndex {
2109
2210
  pid: row.pid,
2110
2211
  ...(row.processIncarnation === undefined ? {} : { processIncarnation: row.processIncarnation }),
2111
2212
  ...(row.hostIncarnation === undefined ? {} : { hostIncarnation: row.hostIncarnation }),
2213
+ ...(row.masterRole === undefined ? {} : { masterRole: row.masterRole }),
2112
2214
  activity: { state: "active", at: now },
2113
2215
  ts: now,
2114
2216
  };
@@ -2145,7 +2247,7 @@ export class SessionIndex {
2145
2247
  item.sessionId === registration.sessionId &&
2146
2248
  item.endpointGeneration === registration.endpointGeneration &&
2147
2249
  item.pid === registration.pid &&
2148
- resolveEquivalentPath(item.locator.repo) === resolveEquivalentPath(registration.locator.repo) &&
2250
+ resolveEquivalentPath(item.locator.cwd) === resolveEquivalentPath(registration.locator.cwd) &&
2149
2251
  path.resolve(item.locator.stateRoot) === path.resolve(registration.locator.stateRoot) &&
2150
2252
  (lifecycleRequestId === undefined || item.lifecycleRequestId === lifecycleRequestId) &&
2151
2253
  (incarnation === undefined || (item.hostIncarnation ?? item.processIncarnation) === incarnation),
@@ -2180,10 +2282,12 @@ export class SessionIndex {
2180
2282
  pid: event.pid,
2181
2283
  processIncarnation: event.processIncarnation,
2182
2284
  endpointMtimeMs: event.endpointMtimeMs,
2285
+ endpointFileId: event.endpointFileId,
2183
2286
  lifecycleRequestId: event.lifecycleRequestId,
2184
2287
  terminalUncertain: false,
2185
2288
  indexSeq: event.indexSeq,
2186
2289
  hostIncarnation: event.hostIncarnation,
2290
+ masterRole: event.masterRole,
2187
2291
  identityProvenance: event.hostIncarnation === undefined ? "legacy" : "composite",
2188
2292
  ambiguous: false,
2189
2293
  live: alive(event.pid),