@gajae-code/coding-agent 0.18.11 → 0.18.12

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 +293 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/cli/accounts-cli.d.ts +2 -2
  7. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  8. package/dist/types/cli/install-activation.d.ts +10 -4
  9. package/dist/types/cli/public-command-errors.d.ts +1 -0
  10. package/dist/types/cli/update-cli.d.ts +14 -1
  11. package/dist/types/cli-ordinary.d.ts +2 -2
  12. package/dist/types/commands/accounts.d.ts +2 -1
  13. package/dist/types/commands/migrate.d.ts +1 -1
  14. package/dist/types/commands/sdk.d.ts +5 -0
  15. package/dist/types/commit/changelog/index.d.ts +1 -0
  16. package/dist/types/config/autorouting-tier-map.d.ts +30 -0
  17. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  18. package/dist/types/config/config-hot-reload.d.ts +39 -0
  19. package/dist/types/config/file-lock.d.ts +8 -1
  20. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  21. package/dist/types/config/model-profile-activation.d.ts +62 -6
  22. package/dist/types/config/model-registry.d.ts +56 -5
  23. package/dist/types/config/model-resolver.d.ts +2 -0
  24. package/dist/types/config/models-config-schema.d.ts +10 -0
  25. package/dist/types/config/settings-schema.d.ts +46 -3
  26. package/dist/types/config/settings.d.ts +33 -1
  27. package/dist/types/dap/config.d.ts +8 -4
  28. package/dist/types/debug/system-info.d.ts +6 -1
  29. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  30. package/dist/types/edit/modes/patch.d.ts +5 -0
  31. package/dist/types/edit/modes/replace.d.ts +3 -0
  32. package/dist/types/exec/bash-executor.d.ts +12 -0
  33. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  35. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  36. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  37. package/dist/types/migrate/adapters/index.d.ts +10 -1
  38. package/dist/types/migrate/executor.d.ts +3 -1
  39. package/dist/types/migrate/types.d.ts +15 -3
  40. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  42. package/dist/types/modes/theme/theme.d.ts +1 -0
  43. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  44. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  45. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  46. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  47. package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
  48. package/dist/types/sdk/broker/broker.d.ts +7 -1
  49. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  50. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  51. package/dist/types/sdk/broker/ensure.d.ts +136 -7
  52. package/dist/types/sdk/broker/hop.d.ts +36 -0
  53. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  54. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +4 -1
  56. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  57. package/dist/types/sdk/bus/index.d.ts +23 -0
  58. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  59. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  60. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  61. package/dist/types/sdk/host/session-runtime.d.ts +32 -0
  62. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  63. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  64. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  65. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  66. package/dist/types/sdk/session-list.d.ts +1 -1
  67. package/dist/types/session/agent-session.d.ts +58 -2
  68. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  69. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  70. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  71. package/dist/types/session/messages.d.ts +5 -1
  72. package/dist/types/session/session-manager.d.ts +13 -3
  73. package/dist/types/session/startup-auth-config.d.ts +24 -2
  74. package/dist/types/session/streaming-output.d.ts +11 -0
  75. package/dist/types/session/terminal-abort.d.ts +3 -1
  76. package/dist/types/session-import/service.d.ts +1 -1
  77. package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
  78. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  79. package/dist/types/task/executor.d.ts +19 -2
  80. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  81. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  82. package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
  83. package/dist/types/tools/descriptors.d.ts +2 -0
  84. package/dist/types/tools/fetch.d.ts +1 -1
  85. package/dist/types/tools/index.d.ts +2 -0
  86. package/dist/types/tools/python.d.ts +1 -1
  87. package/dist/types/tools/report-issue.d.ts +41 -0
  88. package/dist/types/tools/subagent.d.ts +23 -0
  89. package/dist/types/web/scrapers/types.d.ts +8 -1
  90. package/dist/types/web/search/providers/insane.d.ts +7 -1
  91. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  92. package/package.json +8 -8
  93. package/scripts/build-binary.ts +2 -1
  94. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  95. package/scripts/generate-docs-index.ts +8 -1
  96. package/scripts/generate-sdk-operation-inventory.ts +22 -4
  97. package/src/async/job-manager.ts +356 -56
  98. package/src/async/result-formatting.ts +40 -0
  99. package/src/autoresearch/git.ts +9 -2
  100. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  101. package/src/capability/fs.ts +27 -19
  102. package/src/capability/index.ts +2 -2
  103. package/src/cli/accounts-cli.ts +176 -8
  104. package/src/cli/auth-broker-cli.ts +21 -11
  105. package/src/cli/doctor/install-repairs.ts +35 -5
  106. package/src/cli/fast-help.ts +1 -1
  107. package/src/cli/install-activation.ts +47 -4
  108. package/src/cli/migrate-cli.ts +7 -5
  109. package/src/cli/public-command-errors.ts +4 -0
  110. package/src/cli/root-flags.ts +2 -2
  111. package/src/cli/stats-cli.ts +9 -5
  112. package/src/cli/update-cli.ts +224 -7
  113. package/src/cli-ordinary.ts +7 -2
  114. package/src/commands/accounts.ts +10 -5
  115. package/src/commands/migrate.ts +3 -3
  116. package/src/commands/sdk.ts +199 -42
  117. package/src/commands/ultragoal.ts +16 -8
  118. package/src/commit/changelog/index.ts +39 -1
  119. package/src/commit/message.ts +10 -3
  120. package/src/config/autorouting-tier-map.ts +30 -0
  121. package/src/config/config-hot-reload-trust.ts +148 -0
  122. package/src/config/config-hot-reload.ts +734 -0
  123. package/src/config/file-lock.ts +216 -56
  124. package/src/config/model-bindings-applier.ts +85 -47
  125. package/src/config/model-discovery-manager.ts +10 -0
  126. package/src/config/model-profile-activation.ts +716 -113
  127. package/src/config/model-registry.ts +1334 -158
  128. package/src/config/model-resolver.ts +3 -1
  129. package/src/config/models-config-schema.ts +6 -0
  130. package/src/config/settings-schema.ts +60 -3
  131. package/src/config/settings.ts +458 -24
  132. package/src/coordinator-mcp/policy.ts +26 -1
  133. package/src/crash/sanitize.ts +4 -0
  134. package/src/customization/import.ts +3 -1
  135. package/src/dap/config.ts +31 -11
  136. package/src/dap/defaults.json +1 -1
  137. package/src/debug/crash-diagnostics.ts +2 -1
  138. package/src/debug/report-bundle.ts +19 -0
  139. package/src/debug/system-info.ts +8 -3
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  141. package/src/discovery/agents-md.ts +1 -1
  142. package/src/discovery/helpers.ts +1 -2
  143. package/src/discovery/mcp-compat.ts +120 -17
  144. package/src/edit/apply-patch/index.ts +12 -1
  145. package/src/edit/index.ts +13 -17
  146. package/src/edit/modes/patch.ts +187 -8
  147. package/src/edit/modes/replace.ts +136 -0
  148. package/src/eval/js/context-manager.ts +29 -3
  149. package/src/eval/js/worker-entry.ts +51 -18
  150. package/src/eval/py/executor.ts +14 -4
  151. package/src/eval/py/runner.py +144 -5
  152. package/src/exec/bash-executor.ts +81 -4
  153. package/src/exec/bash-shell-guardian.ts +4 -1
  154. package/src/exec/bash-shell-supervisor.ts +8 -4
  155. package/src/exec/isolated-shell.ts +5 -1
  156. package/src/export/html/index.ts +46 -6
  157. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  158. package/src/extensibility/extensions/loader.ts +8 -1
  159. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  160. package/src/gjc-runtime/gc-runtime.ts +33 -6
  161. package/src/gjc-runtime/launch-tmux.ts +7 -2
  162. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  163. package/src/gjc-runtime/launch-worktree.ts +101 -14
  164. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  165. package/src/gjc-runtime/session-resolution.ts +38 -6
  166. package/src/gjc-runtime/session-state-lock.ts +59 -14
  167. package/src/gjc-runtime/state-runtime.ts +23 -19
  168. package/src/hooks/native-skill-hook.ts +14 -11
  169. package/src/internal-urls/docs-index.generated.ts +14 -13
  170. package/src/internal-urls/local-protocol.ts +142 -8
  171. package/src/lsp/index.ts +29 -0
  172. package/src/lsp/workspace-path.ts +23 -0
  173. package/src/migrate/action-planner.ts +57 -0
  174. package/src/migrate/adapters/claude-code.ts +23 -4
  175. package/src/migrate/adapters/codex.ts +22 -4
  176. package/src/migrate/adapters/index.ts +22 -4
  177. package/src/migrate/adapters/opencode.ts +6 -1
  178. package/src/migrate/executor.ts +84 -7
  179. package/src/migrate/report.ts +19 -2
  180. package/src/migrate/types.ts +14 -4
  181. package/src/modes/DESIGN.md +51 -0
  182. package/src/modes/acp/permission-mode.ts +18 -1
  183. package/src/modes/components/assistant-message.ts +55 -1
  184. package/src/modes/components/settings-selector.ts +103 -37
  185. package/src/modes/controllers/command-controller.ts +38 -2
  186. package/src/modes/controllers/plan-mode-controller.ts +17 -7
  187. package/src/modes/controllers/selector-controller.ts +62 -6
  188. package/src/modes/interactive-mode.ts +68 -0
  189. package/src/modes/theme/theme.ts +15 -0
  190. package/src/prompts/tools/debug.md +1 -1
  191. package/src/prompts/tools/read.md +1 -1
  192. package/src/prompts/tools/report-issue.md +13 -0
  193. package/src/prompts/tools/task.md +1 -1
  194. package/src/runtime-credential-selector.ts +2 -2
  195. package/src/runtime-mcp/content-limits.ts +7 -0
  196. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  197. package/src/runtime-mcp/manager.ts +32 -0
  198. package/src/runtime-mcp/oauth-flow.ts +32 -6
  199. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  200. package/src/runtime-mcp/render.ts +16 -1
  201. package/src/runtime-mcp/tool-bridge.ts +126 -17
  202. package/src/sdk/broker/broker-exit.ts +8 -5
  203. package/src/sdk/broker/broker.ts +342 -82
  204. package/src/sdk/broker/daemon-entry.ts +338 -116
  205. package/src/sdk/broker/doctor-restart.ts +2 -1
  206. package/src/sdk/broker/ensure.ts +1143 -265
  207. package/src/sdk/broker/hop.ts +274 -0
  208. package/src/sdk/broker/lifecycle-ledger.ts +35 -11
  209. package/src/sdk/broker/lock-host-identity.ts +29 -0
  210. package/src/sdk/broker/session-index.ts +62 -12
  211. package/src/sdk/bus/chat-command-policy.ts +16 -3
  212. package/src/sdk/bus/index.ts +52 -21
  213. package/src/sdk/bus/reconciliation-store.ts +201 -8
  214. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  215. package/src/sdk/cli/session-cli.ts +21 -6
  216. package/src/sdk/host/session-runtime.test.ts +118 -23
  217. package/src/sdk/host/session-runtime.ts +352 -47
  218. package/src/sdk/lifecycle/service.ts +1 -0
  219. package/src/sdk/prompt-deadline-flush.ts +78 -5
  220. package/src/sdk/prompt-deadline-manager.ts +68 -14
  221. package/src/sdk/protocol/adapter-validation.ts +53 -11
  222. package/src/sdk/protocol/operation-inventory.generated.json +101 -2
  223. package/src/sdk/reconciliation-extensions.ts +1 -0
  224. package/src/sdk/session-list.ts +1 -0
  225. package/src/sdk/session.ts +114 -61
  226. package/src/session/agent-session.ts +1043 -81
  227. package/src/session/internal/managed-session-scope.ts +126 -39
  228. package/src/session/internal/managed-session-storage.ts +14 -11
  229. package/src/session/internal/native-publish-outcome.ts +5 -1
  230. package/src/session/messages.ts +47 -6
  231. package/src/session/session-manager.ts +236 -14
  232. package/src/session/startup-auth-config.ts +116 -8
  233. package/src/session/streaming-output.ts +262 -4
  234. package/src/session/terminal-abort.ts +14 -2
  235. package/src/session-import/service.ts +1 -0
  236. package/src/skill-state/workflow-mutation-guard.ts +491 -32
  237. package/src/slash-commands/builtin-registry.ts +15 -3
  238. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  239. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  240. package/src/task/executor.ts +103 -12
  241. package/src/task/index.ts +389 -175
  242. package/src/tools/ast-edit-write-path.ts +64 -0
  243. package/src/tools/ast-edit.ts +18 -1
  244. package/src/tools/atomic-file-write.ts +46 -15
  245. package/src/tools/auto-generated-guard.ts +3 -0
  246. package/src/tools/bash-allowed-prefixes.ts +153 -5
  247. package/src/tools/bash-interactive.ts +14 -2
  248. package/src/tools/bash.ts +14 -0
  249. package/src/tools/debug.ts +7 -6
  250. package/src/tools/descriptors.test.ts +44 -0
  251. package/src/tools/descriptors.ts +34 -2
  252. package/src/tools/fetch.ts +3 -2
  253. package/src/tools/index.ts +2 -0
  254. package/src/tools/output-meta.ts +23 -2
  255. package/src/tools/python.ts +1 -1
  256. package/src/tools/read.ts +17 -35
  257. package/src/tools/report-issue.ts +184 -0
  258. package/src/tools/subagent-render.ts +114 -12
  259. package/src/tools/subagent.ts +108 -1
  260. package/src/tools/todo-write.ts +41 -3
  261. package/src/tools/tool-catalog.generated.ts +31 -3
  262. package/src/tools/write.ts +35 -3
  263. package/src/utils/commit-message-generator.ts +9 -2
  264. package/src/utils/file-mentions.ts +29 -1
  265. package/src/utils/title-generator.ts +7 -2
  266. package/src/web/scrapers/index.ts +3 -1
  267. package/src/web/scrapers/types.ts +8 -0
  268. package/src/web/scrapers/youtube.ts +3 -2
  269. package/src/web/search/providers/insane.ts +91 -28
  270. package/src/web/search/providers/searxng.ts +62 -10
@@ -0,0 +1,274 @@
1
+ import * as childProcess from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as process from "node:process";
5
+ import type * as stream from "node:stream";
6
+ import type { Process } from "@gajae-code/natives";
7
+ import { nativeProcessBindings } from "@gajae-code/utils/native-process";
8
+ import type { BrokerHopMessage } from "./ensure";
9
+
10
+ export const BROKER_HANDOFF_ACKNOWLEDGEMENT = "GJC_BROKER_HANDOFF_ACK\n";
11
+ export const BROKER_HANDOFF_COMMIT = "GJC_BROKER_HANDOFF_COMMIT\n";
12
+ export const BROKER_HANDOFF_ABORT = "GJC_BROKER_HANDOFF_ABORT\n";
13
+
14
+ /**
15
+ * Windows broker hop: spawns the real broker with detached:true and reports its pid.
16
+ *
17
+ * The hop is needed because a broker spawned with detached:true IS still killed when the
18
+ * parent process tree is terminated (e.g., by taskkill /T /F on Windows). Using an
19
+ * intermediate hop process that exits after spawning the real broker allows the broker
20
+ * to survive, because the hop's parent (the initiating client) can be killed without
21
+ * affecting the broker (the hop's pid is no longer valid).
22
+ *
23
+ * Invoked by gjc internals only. Usage:
24
+ * gjc internal broker-hop <json-encoded-args>
25
+ *
26
+ * Exits with code 0 after writing the broker pid/incarnation to stdout and
27
+ * receiving the parent's acknowledgement. If stdin closes first, it terminates
28
+ * the pinned broker and exits with code 1.
29
+ */
30
+
31
+ export async function runBrokerHopFromArgv(argv: string[]): Promise<void> {
32
+ if (argv.length !== 1) fail("broker hop requires exactly one argument");
33
+ let message: BrokerHopMessage;
34
+ try {
35
+ message = JSON.parse(argv[0]) as BrokerHopMessage;
36
+ } catch (error) {
37
+ fail(`broker hop JSON parse error: ${error instanceof Error ? error.message : String(error)}`);
38
+ }
39
+ if (!message.command?.file || !Array.isArray(message.command.args)) fail("broker hop message missing command");
40
+ try {
41
+ // stderr is opened here by path: a parent fd number is meaningless in this process.
42
+ const stderr = message.stderrLogPath ? fs.openSync(message.stderrLogPath, "a") : "ignore";
43
+ // The broker inherits this process's environment, which the parent set to the
44
+ // broker environment; it is never carried on the command line.
45
+ const child = childProcess.spawn(message.command.file, message.command.args, {
46
+ detached: true,
47
+ windowsHide: true,
48
+ stdio: ["ignore", "ignore", stderr],
49
+ env: process.env,
50
+ ...(message.cwd ? { cwd: message.cwd } : {}),
51
+ });
52
+ if (typeof stderr === "number") fs.closeSync(stderr);
53
+ const spawned = Promise.withResolvers<void>();
54
+ child.once("spawn", spawned.resolve);
55
+ child.once("error", spawned.reject);
56
+ await spawned.promise;
57
+ const pid = child.pid;
58
+ if (pid === undefined) fail("broker hop spawn succeeded but child pid unavailable");
59
+ const childReference = getSpawnedChildReference(pid);
60
+ if (!childReference) {
61
+ const cleanup = await terminateUnverifiableWindowsChild(child);
62
+ fail(`broker process identity could not be bound to the spawned child; ${cleanup}`);
63
+ }
64
+ const handoff = await writeBrokerHopReply(child, childReference, pid, writeBrokerHopStdout, () =>
65
+ waitForBrokerHandoffAcknowledgement(process.stdin),
66
+ );
67
+ if (handoff.kind === "failed") fail(handoff.reason);
68
+ process.exit(0);
69
+ } catch (error) {
70
+ fail(`broker hop spawn failed: ${error instanceof Error ? error.message : String(error)}`);
71
+ }
72
+ }
73
+
74
+ type BrokerHopWriteCallback = (error?: Error | null) => void;
75
+ type BrokerHopWriter = (chunk: string, callback: BrokerHopWriteCallback) => void;
76
+ type BrokerHopAcknowledger = () => Promise<boolean>;
77
+
78
+ async function writeBrokerHopReply(
79
+ child: childProcess.ChildProcess,
80
+ reference: Process,
81
+ pid: number,
82
+ write: BrokerHopWriter,
83
+ waitForAcknowledgement: BrokerHopAcknowledger,
84
+ ): Promise<{ kind: "written" } | { kind: "failed"; reason: string }> {
85
+ const completion = Promise.withResolvers<void>();
86
+ const acknowledgementPromise = waitForAcknowledgement();
87
+ let settled = false;
88
+ const settle = (error?: Error | null): void => {
89
+ if (settled) return;
90
+ settled = true;
91
+ if (error) completion.reject(error);
92
+ else completion.resolve();
93
+ };
94
+ try {
95
+ write(`${JSON.stringify({ pid, incarnation: reference.incarnation })}\n`, settle);
96
+ } catch (error) {
97
+ settle(error instanceof Error ? error : new Error(String(error)));
98
+ }
99
+ try {
100
+ await completion.promise;
101
+ } catch (error) {
102
+ const writeError = error instanceof Error ? error : new Error(String(error));
103
+ const cleanup = await terminateBrokerAfterFailedHandoff(reference);
104
+ return {
105
+ kind: "failed",
106
+ reason: `broker hop handoff failed for pid ${pid}: ${cleanup}; ${writeError.message}`,
107
+ };
108
+ }
109
+ let acknowledged = false;
110
+ let acknowledgementError: string | undefined;
111
+ try {
112
+ acknowledged = await acknowledgementPromise;
113
+ } catch (error) {
114
+ acknowledgementError = error instanceof Error ? error.message : String(error);
115
+ }
116
+ if (!acknowledged) {
117
+ const cleanup = await terminateBrokerAfterFailedHandoff(reference);
118
+ return {
119
+ kind: "failed",
120
+ reason: `broker hop handoff was not acknowledged for pid ${pid}: ${cleanup}${acknowledgementError ? `; ${acknowledgementError}` : ""}`,
121
+ };
122
+ }
123
+ child.unref();
124
+ return { kind: "written" };
125
+ }
126
+
127
+ export function writeBrokerHopReplyForTest(
128
+ child: childProcess.ChildProcess,
129
+ reference: Process,
130
+ pid: number,
131
+ write: BrokerHopWriter,
132
+ waitForAcknowledgement: BrokerHopAcknowledger = async () => true,
133
+ ): Promise<{ kind: "written" } | { kind: "failed"; reason: string }> {
134
+ return writeBrokerHopReply(child, reference, pid, write, waitForAcknowledgement);
135
+ }
136
+
137
+ export function waitForBrokerHandoffAcknowledgement(input: stream.Readable): Promise<boolean> {
138
+ const acknowledgment = Promise.withResolvers<boolean>();
139
+ let received = "";
140
+ let settled = false;
141
+ const finish = (accepted: boolean): void => {
142
+ if (settled) return;
143
+ settled = true;
144
+ input.removeListener("data", onData);
145
+ input.removeListener("end", onEnd);
146
+ input.removeListener("close", onEnd);
147
+ input.removeListener("error", onEnd);
148
+ acknowledgment.resolve(accepted);
149
+ };
150
+ const onData = (chunk: Buffer | string): void => {
151
+ received += chunk.toString();
152
+ if (received.length > BROKER_HANDOFF_ACKNOWLEDGEMENT.length) {
153
+ finish(false);
154
+ return;
155
+ }
156
+ if (received.includes("\n")) finish(received === BROKER_HANDOFF_ACKNOWLEDGEMENT);
157
+ };
158
+ const onEnd = (): void => finish(false);
159
+ input.on("data", onData);
160
+ input.once("end", onEnd);
161
+ input.once("close", onEnd);
162
+ input.once("error", onEnd);
163
+ input.resume();
164
+ return acknowledgment.promise;
165
+ }
166
+
167
+ /** Wait for a validated identity ACK followed by the parent's publication decision. */
168
+ export function waitForBrokerHandoffDecision(input: stream.Readable, timeoutMs = 30_000): Promise<"commit" | "abort"> {
169
+ const decision = Promise.withResolvers<"commit" | "abort">();
170
+ let received = "";
171
+ let settled = false;
172
+ const finish = (value: "commit" | "abort"): void => {
173
+ if (settled) return;
174
+ settled = true;
175
+ clearTimeout(timer);
176
+ input.removeListener("data", onData);
177
+ input.removeListener("end", onEnd);
178
+ input.removeListener("close", onEnd);
179
+ input.removeListener("error", onEnd);
180
+ decision.resolve(value);
181
+ };
182
+ const onData = (chunk: Buffer | string): void => {
183
+ received += chunk.toString();
184
+ if (received.length > BROKER_HANDOFF_ACKNOWLEDGEMENT.length + BROKER_HANDOFF_COMMIT.length) {
185
+ finish("abort");
186
+ return;
187
+ }
188
+ if (received.length < BROKER_HANDOFF_ACKNOWLEDGEMENT.length) {
189
+ if (!BROKER_HANDOFF_ACKNOWLEDGEMENT.startsWith(received)) finish("abort");
190
+ return;
191
+ }
192
+ if (!received.startsWith(BROKER_HANDOFF_ACKNOWLEDGEMENT)) {
193
+ finish("abort");
194
+ return;
195
+ }
196
+ const action = received.slice(BROKER_HANDOFF_ACKNOWLEDGEMENT.length);
197
+ if (action === BROKER_HANDOFF_COMMIT) finish("commit");
198
+ else if (action === BROKER_HANDOFF_ABORT) finish("abort");
199
+ else if (!BROKER_HANDOFF_COMMIT.startsWith(action) && !BROKER_HANDOFF_ABORT.startsWith(action)) finish("abort");
200
+ };
201
+ const onEnd = (): void => finish("abort");
202
+ const timer = setTimeout(() => finish("abort"), timeoutMs);
203
+ input.on("data", onData);
204
+ input.once("end", onEnd);
205
+ input.once("close", onEnd);
206
+ input.once("error", onEnd);
207
+ input.resume();
208
+ return decision.promise;
209
+ }
210
+
211
+ const writeBrokerHopStdout: BrokerHopWriter = (chunk, callback): void => {
212
+ const onError = (error: Error): void => {
213
+ process.stdout.removeListener("error", onError);
214
+ callback(error);
215
+ };
216
+ process.stdout.once("error", onError);
217
+ try {
218
+ process.stdout.write(chunk, error => {
219
+ process.stdout.removeListener("error", onError);
220
+ callback(error);
221
+ });
222
+ } catch (error) {
223
+ process.stdout.removeListener("error", onError);
224
+ callback(error instanceof Error ? error : new Error(String(error)));
225
+ }
226
+ };
227
+
228
+ /** Pin only the still-running direct child so a reused PID cannot be adopted. */
229
+ function getSpawnedChildReference(pid: number): Process | undefined {
230
+ try {
231
+ const reference = nativeProcessBindings().Process.fromPid(pid);
232
+ if (!reference || reference.pid !== pid || reference.ppid !== process.pid || reference.status() !== "running")
233
+ return undefined;
234
+ return reference;
235
+ } catch {
236
+ return undefined;
237
+ }
238
+ }
239
+
240
+ async function terminateUnverifiableWindowsChild(child: childProcess.ChildProcess): Promise<string> {
241
+ if (process.platform !== "win32") return "no signal was sent without a verified process identity";
242
+ if (child.exitCode !== null || child.signalCode !== null) return "the spawned child already exited";
243
+ const exited = Promise.withResolvers<void>();
244
+ child.once("exit", exited.resolve);
245
+ child.once("close", exited.resolve);
246
+ child.on("error", () => {});
247
+ try {
248
+ child.kill("SIGKILL");
249
+ } catch {
250
+ // The process handle remains exact; inspect its exit state below.
251
+ }
252
+ await Promise.race([exited.promise, Bun.sleep(500)]);
253
+ return child.exitCode !== null || child.signalCode !== null
254
+ ? "the exact spawned child was terminated"
255
+ : "termination of the exact spawned child was not confirmed";
256
+ }
257
+
258
+ async function terminateBrokerAfterFailedHandoff(reference: Process): Promise<string> {
259
+ let cleanup: string;
260
+ try {
261
+ if (reference.status() === "running") reference.signalRoot(os.constants.signals.SIGKILL);
262
+ const exited = await reference.waitForExit({ timeoutMs: 500 });
263
+ cleanup =
264
+ exited || reference.status() !== "running" ? "broker terminated" : "broker termination was not confirmed";
265
+ } catch (error) {
266
+ cleanup = `broker termination failed: ${error instanceof Error ? error.message : String(error)}`;
267
+ }
268
+ return cleanup;
269
+ }
270
+
271
+ function fail(message: string): never {
272
+ process.stderr.write(`gjc: ${message}\n`);
273
+ process.exit(1);
274
+ }
@@ -130,6 +130,10 @@ type BoundedSourceRead =
130
130
 
131
131
  const terminal = (s: LifecycleState) => s === "terminal_ok" || s === "terminal_error";
132
132
  const final = (s: LifecycleState) => terminal(s) || s === "terminal_uncertain";
133
+ const recordedSessionIds = (entry: LifecycleLedgerEntry): string[] =>
134
+ [entry.intendedSessionId, entry.resultSessionId].filter(
135
+ (sessionId): sessionId is string => typeof sessionId === "string" && sessionId.length > 0,
136
+ );
133
137
  export interface LifecycleLedgerLimits {
134
138
  maxBytes?: number;
135
139
  maxLineBytes?: number;
@@ -795,27 +799,47 @@ export class LifecycleLedger {
795
799
  return [...this.#byIdentity.values()].findLast(entry => entry.operationKey === operationKey);
796
800
  }
797
801
  /**
798
- * Live or uncertain legacy rows predate the operation/key index. Their opaque
799
- * identities cannot establish that a different target is safe, so callers
800
- * must reject rather than create a second admission. A fresh session.create
801
- * may ignore completed terminal legacy rows so old successful operations do
802
- * not block coordinator startup. Other operations retain the legacy fence
803
- * because target-bound legacy identities cannot prove key uniqueness. For
804
- * target-bound terminal creates, the opaque legacy identity also cannot reveal
805
- * the original caller key, so this exception retires that legacy key history;
806
- * exact key-only legacy creates are still rejected by the earlier lookup.
802
+ * Live legacy rows predate the operation/key index. Their opaque identities
803
+ * cannot establish that a different target is safe, so callers must reject
804
+ * rather than create a second admission. A fresh session.create may ignore
805
+ * final legacy rows: exact legacy identities are still resolved before this
806
+ * check, and uncertain rows retain their session-scoped fences.
807
+ *
808
+ * A session-targeted request passes `sessionTarget` to ignore terminal legacy
809
+ * rows that record only other sessions, unless the row is the caller's key
810
+ * reused across targets (`keyReuseIdentities`). Terminal rows recording the
811
+ * target or no session, uncertain rows, and live rows keep the fence because
812
+ * they cannot prove the requested target is unrelated.
807
813
  */
808
814
  hasLegacyIdentity(
809
815
  excludedIdentities?: ReadonlySet<string>,
810
- options: { ignoreTerminalRows?: boolean } = {},
816
+ options: {
817
+ ignoreFinalRows?: boolean;
818
+ sessionTarget?: { sessionId: string; keyReuseIdentities: ReadonlySet<string> };
819
+ } = {},
811
820
  ): boolean {
821
+ const target = options.sessionTarget;
822
+ const unrelatedTerminal = (entry: LifecycleLedgerEntry): boolean => {
823
+ if (!target || !terminal(entry.state) || target.keyReuseIdentities.has(entry.identity)) return false;
824
+ const sessionIds = recordedSessionIds(entry);
825
+ return sessionIds.length > 0 && !sessionIds.includes(target.sessionId);
826
+ };
812
827
  return [...this.#byIdentity.values()].some(
813
828
  entry =>
814
829
  entry.operationKey === undefined &&
815
- !(options.ignoreTerminalRows && terminal(entry.state)) &&
830
+ !(options.ignoreFinalRows && final(entry.state)) &&
831
+ !unrelatedTerminal(entry) &&
816
832
  !excludedIdentities?.has(entry.identity),
817
833
  );
818
834
  }
835
+ /** Sessions recorded by terminal legacy rows, for deriving key-reuse identities. */
836
+ terminalLegacySessionIds(): Set<string> {
837
+ const sessionIds = new Set<string>();
838
+ for (const entry of this.#byIdentity.values())
839
+ if (entry.operationKey === undefined && terminal(entry.state))
840
+ for (const sessionId of recordedSessionIds(entry)) sessionIds.add(sessionId);
841
+ return sessionIds;
842
+ }
819
843
  async migrateIdentity(
820
844
  from: string,
821
845
  to: string,
@@ -0,0 +1,29 @@
1
+ import path from "node:path";
2
+ import { loadInstallationHostId, loadLegacyInstallationHostId } from "../../config/machine-identity";
3
+
4
+ export interface BrokerLockHostIdentity {
5
+ ownerHostId: string;
6
+ previousOwnerHostIds: readonly string[];
7
+ }
8
+
9
+ const brokerLockHostIdentityPromises = new Map<string, Promise<BrokerLockHostIdentity>>();
10
+
11
+ export async function brokerLockHostIdentity(agentDir: string): Promise<BrokerLockHostIdentity> {
12
+ const coordinationRoot = path.resolve(agentDir, "sdk");
13
+ const pending =
14
+ brokerLockHostIdentityPromises.get(coordinationRoot) ??
15
+ Promise.all([loadInstallationHostId({ configRootDir: coordinationRoot }), loadLegacyInstallationHostId()]).then(
16
+ ([ownerHostId, legacyHostId]) => ({
17
+ ownerHostId,
18
+ previousOwnerHostIds: legacyHostId === ownerHostId ? [] : [legacyHostId],
19
+ }),
20
+ );
21
+ brokerLockHostIdentityPromises.set(coordinationRoot, pending);
22
+ try {
23
+ return await pending;
24
+ } catch (error) {
25
+ if (brokerLockHostIdentityPromises.get(coordinationRoot) === pending)
26
+ brokerLockHostIdentityPromises.delete(coordinationRoot);
27
+ throw error;
28
+ }
29
+ }
@@ -3,7 +3,12 @@ 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
- import { type FileLockOptions, isFileLockAcquireTimeout, withFileLock } from "../../config/file-lock";
6
+ import {
7
+ FILE_LOCK_ORPHAN_TRANSITION_ADOPTION_AGE_MS,
8
+ type FileLockOptions,
9
+ isFileLockAcquireTimeout,
10
+ withFileLock,
11
+ } from "../../config/file-lock";
7
12
  import { repo } from "../../utils/git";
8
13
  import { endpointIncarnation } from "./endpoint-authority";
9
14
  import { processIncarnation } from "./process-incarnation";
@@ -574,6 +579,7 @@ function projectIdentity(
574
579
  ambiguous: boolean,
575
580
  now: number,
576
581
  probedIncarnations?: ReadonlyMap<string, string | undefined>,
582
+ skipOsProcessProbes = false,
577
583
  ): IndexedSession {
578
584
  const { latest, heartbeat } = state;
579
585
  const terminal = isTerminalEvent(latest);
@@ -582,7 +588,7 @@ function projectIdentity(
582
588
  latest.hostUnregisteredReason === "process_exited" || latest.hostUnregisteredReason === "detached_idle"
583
589
  ? latest.hostUnregisteredReason
584
590
  : undefined;
585
- const pidAlive = alive(latest.pid);
591
+ const pidAlive = !skipOsProcessProbes && alive(latest.pid);
586
592
  // Liveness evidence is host-written: a checkpointed heartbeat, or the
587
593
  // `host_registered` event the host appended itself. Counting registration
588
594
  // closes the up-to-one-interval window where a just-registered session
@@ -602,7 +608,7 @@ function projectIdentity(
602
608
  // costing a powershell.exe spawn per stale registration on Windows.
603
609
  const probeKey = `${latest.sessionId}\u0000${latest.endpointGeneration}\u0000${latest.pid}`;
604
610
  const currentIncarnation =
605
- recordedIncarnation === undefined || !pidAlive
611
+ skipOsProcessProbes || recordedIncarnation === undefined || !pidAlive
606
612
  ? undefined
607
613
  : probedIncarnations !== undefined
608
614
  ? probedIncarnations.get(probeKey)
@@ -654,7 +660,9 @@ function reduceEvents(
654
660
  agentDir: string,
655
661
  probedIncarnations?: ReadonlyMap<string, string | undefined>,
656
662
  sessionIds?: ReadonlySet<string>,
663
+ options: { readonly skipOsProcessProbes?: boolean } = {},
657
664
  ): SessionIndexProjection {
665
+ const skipOsProcessProbes = options.skipOsProcessProbes === true;
658
666
  // Resolved once: the fence-row check runs per root, and only this side of the
659
667
  // comparison is ours to normalize.
660
668
  const agentDirIdentity = resolveEquivalentPath(agentDir);
@@ -741,7 +749,8 @@ function reduceEvents(
741
749
  const fencing = fencingRoots.get(sessionId);
742
750
  const unresolved = unresolvedRoots.get(sessionId);
743
751
  const ambiguous = (fencing?.size ?? 0) > 1;
744
- for (const state of states) identities.push(projectIdentity(state, ambiguous, now, probedIncarnations));
752
+ for (const state of states)
753
+ identities.push(projectIdentity(state, ambiguous, now, probedIncarnations, skipOsProcessProbes));
745
754
  const defaultAuthority = preferredIdentity(states);
746
755
  if (defaultAuthority === undefined || defaultAuthority.latest.type === "session_deleted") continue;
747
756
  let authority = defaultAuthority;
@@ -754,14 +763,42 @@ function reduceEvents(
754
763
  const survivingAuthority = onlyRoot ? preferredIdentity(onlyRoot) : undefined;
755
764
  if (survivingAuthority !== undefined) authority = survivingAuthority;
756
765
  }
757
- sessions.push(projectIdentity(authority, ambiguous, now, probedIncarnations));
766
+ sessions.push(projectIdentity(authority, ambiguous, now, probedIncarnations, skipOsProcessProbes));
758
767
  }
759
768
  return { identities, sessions };
760
769
  }
761
770
 
762
771
  // Global launch bursts may queue behind legitimate long index transactions. Keep
763
772
  // this bounded at one minute while the shared lock's exact dead-owner recovery runs.
764
- const SESSION_INDEX_LOCK_OPTIONS = { retries: 600, retryDelayMs: 100 } as const;
773
+ const SESSION_INDEX_LOCK_OPTIONS = {
774
+ retries: 600,
775
+ retryDelayMs: 100,
776
+ orphanTransitionAgeMs: FILE_LOCK_ORPHAN_TRANSITION_ADOPTION_AGE_MS,
777
+ } as const;
778
+ // Leave one retry interval beyond the transition's safe-adoption age so a fresh
779
+ // ownerless transition can be reclaimed during ordinary startup.
780
+ const SESSION_INDEX_STARTUP_LOCK_WAIT_MS =
781
+ FILE_LOCK_ORPHAN_TRANSITION_ADOPTION_AGE_MS + SESSION_INDEX_LOCK_OPTIONS.retryDelayMs * 2;
782
+
783
+ /** Bound ordinary bootstrap waits; authorized doctor successors may use the full prepared window. */
784
+ export function sessionIndexStartupLockOptions(
785
+ deadline: number,
786
+ signal?: AbortSignal,
787
+ policy: "ordinary" | "authorized-successor" = "ordinary",
788
+ ): FileLockOptions {
789
+ const boundedDeadline =
790
+ policy === "authorized-successor"
791
+ ? deadline
792
+ : Math.min(deadline, performance.now() + SESSION_INDEX_STARTUP_LOCK_WAIT_MS);
793
+ return {
794
+ ...SESSION_INDEX_LOCK_OPTIONS,
795
+ retries: Math.min(
796
+ SESSION_INDEX_LOCK_OPTIONS.retries,
797
+ Math.max(1, Math.ceil((boundedDeadline - performance.now()) / SESSION_INDEX_LOCK_OPTIONS.retryDelayMs)),
798
+ ),
799
+ ...(signal === undefined ? {} : { signal }),
800
+ };
801
+ }
765
802
 
766
803
  /**
767
804
  * Observation window for one locked index transaction (#4544). This never times
@@ -1183,7 +1220,7 @@ export class SessionIndex {
1183
1220
  });
1184
1221
  return promise;
1185
1222
  }
1186
- async open(): Promise<this> {
1223
+ async open(options: FileLockOptions = SESSION_INDEX_LOCK_OPTIONS): Promise<this> {
1187
1224
  const indexPath = path.resolve(logFor(this.#agentDir));
1188
1225
  let group = SessionIndex.#openGroups.get(indexPath);
1189
1226
  if (!group || group.closed) {
@@ -1193,7 +1230,7 @@ export class SessionIndex {
1193
1230
  }
1194
1231
  await group.promise;
1195
1232
  await SessionIndex.#enqueue(indexPath, () =>
1196
- withSessionIndexLock("replay", this.#agentDir, () => this.#replayUnderLock()),
1233
+ withSessionIndexLock("replay", this.#agentDir, () => this.#replayUnderLock(), options),
1197
1234
  );
1198
1235
  return this;
1199
1236
  }
@@ -2455,14 +2492,25 @@ export class SessionIndex {
2455
2492
  // revalidation that never probes the OS under the lock: the next pass
2456
2493
  // re-probes from scratch.
2457
2494
  const probed = new Map<string, string | undefined>();
2458
- for (const row of reduceEvents(this.#events, now, this.#agentDir).sessions) {
2495
+ // This projection is only used to select structurally eligible rows; its
2496
+ // `live` field is not consumed here. Skip OS probes during projection so
2497
+ // the deadline-aware loop below performs only the required observations.
2498
+ const candidates = reduceEvents(this.#events, now, this.#agentDir, undefined, undefined, {
2499
+ skipOsProcessProbes: true,
2500
+ }).sessions;
2501
+ if (signal.aborted || performance.now() >= deadline) return 0;
2502
+ for (const row of candidates) {
2503
+ if (signal.aborted || performance.now() >= deadline) return 0;
2459
2504
  if (!isSessionAuthorityEligible(row) || row.terminal || row.terminalUncertain) continue;
2460
2505
  if (row.lastHeartbeatAt !== undefined && now - row.lastHeartbeatAt < SESSION_HEARTBEAT_INTERVAL_MS)
2461
2506
  continue;
2462
- if (!alive(row.pid)) continue;
2463
2507
  const recordedIncarnation = row.hostIncarnation ?? row.processIncarnation;
2464
2508
  if (recordedIncarnation === undefined) continue;
2465
- probed.set(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`, processIncarnation(row.pid));
2509
+ if (!alive(row.pid)) continue;
2510
+ if (signal.aborted || performance.now() >= deadline) return 0;
2511
+ const currentIncarnation = processIncarnation(row.pid);
2512
+ if (signal.aborted || performance.now() >= deadline) return 0;
2513
+ probed.set(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`, currentIncarnation);
2466
2514
  }
2467
2515
  const probedAt = performance.now();
2468
2516
  if (signal.aborted || probedAt >= deadline) return 0;
@@ -2492,7 +2540,9 @@ export class SessionIndex {
2492
2540
  if (performance.now() - probedAt > SESSION_INDEX_REPLAY_FRESHNESS_MS) return STALE_OBSERVATION;
2493
2541
  if (this.#corruptSuffix) return 0;
2494
2542
  const events: SessionIndexEvent[] = [];
2495
- const rows = reduceEvents(this.#events, now, this.#agentDir, probed).sessions;
2543
+ const rows = reduceEvents(this.#events, now, this.#agentDir, probed, undefined, {
2544
+ skipOsProcessProbes: true,
2545
+ }).sessions;
2496
2546
  for (const row of rows) {
2497
2547
  if (!isSessionAuthorityEligible(row) || row.terminal || row.terminalUncertain) continue;
2498
2548
  if (row.lastHeartbeatAt !== undefined && now - row.lastHeartbeatAt < SESSION_HEARTBEAT_INTERVAL_MS)
@@ -1,11 +1,15 @@
1
- import { validateAdapterSecretFields } from "../protocol/adapter-validation";
1
+ import {
2
+ type AdapterValidationError,
3
+ validateAdapterControl,
4
+ validateAdapterSecretFields,
5
+ } from "../protocol/adapter-validation";
2
6
  import { type Adapter, OPERATIONS, type Operation, type OperationKind } from "../protocol/operation-registry";
3
7
 
4
8
  export type ChatTransport = Extract<Adapter, "telegram" | "discord" | "slack">;
5
9
  export type ChatOperationDisposition = "allowed" | "unsupported_on_chat";
6
10
 
7
11
  export interface ChatCommandError {
8
- code: "unsupported_on_chat" | "secret_input_forbidden";
12
+ code: "unsupported_on_chat" | "secret_input_forbidden" | "ultrafast_tier_forbidden" | AdapterValidationError["code"];
9
13
  message: string;
10
14
  }
11
15
 
@@ -57,12 +61,21 @@ export function authorizeChatOperation(transport: ChatTransport, request: ChatOp
57
61
  candidate => candidate.kind === request.kind && candidate.sdkId === request.operation,
58
62
  );
59
63
  if (!operation) return unsupported();
60
- if (validateAdapterSecretFields(operation.sdkId, (request.input ?? {}) as Record<string, unknown>)) {
64
+ const secretValidation = validateAdapterSecretFields(
65
+ operation.sdkId,
66
+ (request.input ?? {}) as Record<string, unknown>,
67
+ );
68
+ if (secretValidation?.code === "secret_field_forbidden") {
61
69
  return {
62
70
  ok: false,
63
71
  error: { code: "secret_input_forbidden", message: "Secret configuration input is forbidden on chat." },
64
72
  };
65
73
  }
74
+ if (secretValidation) return { ok: false, error: secretValidation };
75
+ if (operation.kind === "control") {
76
+ const invalid = validateAdapterControl(operation.sdkId, (request.input ?? {}) as Record<string, unknown>);
77
+ if (invalid) return { ok: false, error: invalid };
78
+ }
66
79
  return CHAT_OPERATION_POLICY[transport][operation.id] === "allowed" ? { ok: true } : unsupported();
67
80
  }
68
81