harnery 0.43.0 → 0.44.0

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 (278) hide show
  1. package/README.md +3 -3
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +41 -48
  4. package/dist/commands/browse.d.ts.map +1 -1
  5. package/dist/commands/browse.js +38 -11
  6. package/dist/commands/diagnostics.d.ts.map +1 -1
  7. package/dist/commands/diagnostics.js +10 -0
  8. package/dist/commands/doctor.d.ts +12 -0
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +22 -7
  11. package/dist/commands/governor.js +22 -10
  12. package/dist/commands/inbox.d.ts +1 -1
  13. package/dist/commands/inbox.d.ts.map +1 -1
  14. package/dist/commands/inbox.js +55 -2
  15. package/dist/commands/rm.d.ts.map +1 -1
  16. package/dist/commands/rm.js +28 -4
  17. package/dist/commands/servers.d.ts.map +1 -1
  18. package/dist/commands/servers.js +16 -1
  19. package/dist/commands/supervisor.d.ts.map +1 -1
  20. package/dist/commands/supervisor.js +7 -2
  21. package/dist/commands/toc.d.ts +14 -0
  22. package/dist/commands/toc.d.ts.map +1 -1
  23. package/dist/commands/toc.js +7 -4
  24. package/dist/commands/workflow.js +24 -30
  25. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  26. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  27. package/dist/core/agents/live-authority-v3.js +11 -0
  28. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  29. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  30. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  31. package/dist/core/agents/mailbox.d.ts +9 -8
  32. package/dist/core/agents/mailbox.d.ts.map +1 -1
  33. package/dist/core/agents/mailbox.js +24 -17
  34. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  35. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  36. package/dist/core/agents/render/prompt-context.js +8 -12
  37. package/dist/core/agents/render/session-context.d.ts +2 -8
  38. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  39. package/dist/core/agents/render/session-context.js +7 -30
  40. package/dist/core/agents/rules/stop-hook.js +3 -3
  41. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  42. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  43. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  44. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  45. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  46. package/dist/core/agents/state/pidmap.js +5 -0
  47. package/dist/core/agents/state/proc-start.d.ts +19 -0
  48. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  49. package/dist/core/agents/state/proc-start.js +41 -0
  50. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  51. package/dist/core/agents/state/stale-sweep.js +180 -70
  52. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  53. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  54. package/dist/core/agents/state/unchanged-file.js +390 -0
  55. package/dist/core/context/index.d.ts +22 -10
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +381 -119
  58. package/dist/core/context/observation.d.ts +26 -0
  59. package/dist/core/context/observation.d.ts.map +1 -0
  60. package/dist/core/context/observation.js +11 -0
  61. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  62. package/dist/core/diagnostics/bundle.js +25 -14
  63. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  64. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  65. package/dist/core/diagnostics/path-identity.js +12 -0
  66. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  67. package/dist/core/diagnostics/sanitize.js +4 -3
  68. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  69. package/dist/core/events/v3/authority-outbox.js +29 -0
  70. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  71. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  72. package/dist/core/events/v3/generation-custody.js +50 -0
  73. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  74. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  75. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  76. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  77. package/dist/core/events/v3/producers/recorder.js +77 -64
  78. package/dist/core/governor/planning.d.ts.map +1 -1
  79. package/dist/core/governor/planning.js +14 -1
  80. package/dist/core/governor/service-read.d.ts +10 -2
  81. package/dist/core/governor/service-read.d.ts.map +1 -1
  82. package/dist/core/governor/service-read.js +105 -22
  83. package/dist/core/governor/service.d.ts +20 -2
  84. package/dist/core/governor/service.d.ts.map +1 -1
  85. package/dist/core/governor/service.js +72 -47
  86. package/dist/core/hooks/cli.js +33 -5
  87. package/dist/core/inbox/contract.d.ts +2 -1
  88. package/dist/core/inbox/contract.d.ts.map +1 -1
  89. package/dist/core/inbox/index.d.ts +2 -0
  90. package/dist/core/inbox/index.d.ts.map +1 -1
  91. package/dist/core/inbox/index.js +2 -0
  92. package/dist/core/inbox/peer-messages.d.ts +37 -0
  93. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  94. package/dist/core/inbox/peer-messages.js +72 -0
  95. package/dist/core/inbox/service.d.ts +8 -1
  96. package/dist/core/inbox/service.d.ts.map +1 -1
  97. package/dist/core/inbox/service.js +67 -21
  98. package/dist/core/inbox/targets.d.ts +22 -0
  99. package/dist/core/inbox/targets.d.ts.map +1 -0
  100. package/dist/core/inbox/targets.js +108 -0
  101. package/dist/core/inbox/watcher.js +7 -3
  102. package/dist/core/journal/index.d.ts.map +1 -1
  103. package/dist/core/journal/index.js +17 -2
  104. package/dist/core/resources/contract.d.ts +3 -0
  105. package/dist/core/resources/contract.d.ts.map +1 -1
  106. package/dist/core/resources/service-status.d.ts +14 -1
  107. package/dist/core/resources/service-status.d.ts.map +1 -1
  108. package/dist/core/resources/service-status.js +23 -13
  109. package/dist/core/resources/service.d.ts +9 -1
  110. package/dist/core/resources/service.d.ts.map +1 -1
  111. package/dist/core/resources/service.js +47 -27
  112. package/dist/core/resources/status.d.ts +2 -5
  113. package/dist/core/resources/status.d.ts.map +1 -1
  114. package/dist/core/resources/status.js +36 -7
  115. package/dist/core/servers/index.d.ts +24 -13
  116. package/dist/core/servers/index.d.ts.map +1 -1
  117. package/dist/core/servers/index.js +132 -92
  118. package/dist/core/servers/tunnels.d.ts +3 -5
  119. package/dist/core/servers/tunnels.d.ts.map +1 -1
  120. package/dist/core/servers/tunnels.js +3 -9
  121. package/dist/core/supervisor/contract.d.ts +12 -0
  122. package/dist/core/supervisor/contract.d.ts.map +1 -1
  123. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  124. package/dist/core/supervisor/explanations.js +2 -1
  125. package/dist/core/supervisor/findings.d.ts.map +1 -1
  126. package/dist/core/supervisor/findings.js +54 -9
  127. package/dist/core/supervisor/service.d.ts +9 -1
  128. package/dist/core/supervisor/service.d.ts.map +1 -1
  129. package/dist/core/supervisor/service.js +59 -29
  130. package/dist/core/supervisor/services.d.ts.map +1 -1
  131. package/dist/core/supervisor/services.js +22 -1
  132. package/dist/core/supervisor/status.d.ts +17 -2
  133. package/dist/core/supervisor/status.d.ts.map +1 -1
  134. package/dist/core/supervisor/status.js +70 -12
  135. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  136. package/dist/core/supervisor/timeline.js +2 -1
  137. package/dist/core/work/state.js +7 -7
  138. package/dist/core/workflow/engine.d.ts.map +1 -1
  139. package/dist/core/workflow/engine.js +105 -32
  140. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  141. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  142. package/dist/core/workflow/failure-evidence.js +114 -0
  143. package/dist/core/workflow/index.d.ts +1 -1
  144. package/dist/core/workflow/index.d.ts.map +1 -1
  145. package/dist/core/workflow/index.js +1 -1
  146. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  147. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  148. package/dist/core/workflow/live-session-v3.js +126 -5
  149. package/dist/core/workflow/proof.d.ts +5 -5
  150. package/dist/core/workflow/proof.d.ts.map +1 -1
  151. package/dist/core/workflow/proof.js +115 -20
  152. package/dist/core/workflow/run-state.d.ts +3 -1
  153. package/dist/core/workflow/run-state.d.ts.map +1 -1
  154. package/dist/core/workflow/run-state.js +31 -27
  155. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  156. package/dist/core/workflow/spawn-claude.js +19 -0
  157. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  158. package/dist/core/workflow/spawn-codex.js +13 -0
  159. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  160. package/dist/core/workflow/spawn-cursor.js +19 -0
  161. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  162. package/dist/core/workflow/spawn-opencode.js +14 -0
  163. package/dist/core/workflow/types.d.ts +64 -17
  164. package/dist/core/workflow/types.d.ts.map +1 -1
  165. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  166. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  167. package/dist/core/workflow/workspaces/index.js +0 -1
  168. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  169. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  170. package/dist/core/workflow/workspaces/leases.js +30 -16
  171. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  172. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  173. package/dist/core/workflow/workspaces/local-git.js +270 -6
  174. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  175. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  176. package/dist/core/workflow/workspaces/ownership.js +194 -0
  177. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  178. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  179. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  180. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  181. package/dist/core/workflow/workspaces/recovery.js +21 -13
  182. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  183. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  184. package/dist/core/workflow/workspaces/state.js +21 -1
  185. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  186. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  187. package/dist/lib/browser/windows-chrome.js +262 -73
  188. package/dist/lib/docs-links.d.ts.map +1 -1
  189. package/dist/lib/docs-links.js +45 -2
  190. package/dist/lib/guarded-remove.d.ts +7 -0
  191. package/dist/lib/guarded-remove.d.ts.map +1 -1
  192. package/dist/lib/guarded-remove.js +106 -12
  193. package/dist/lib/identities/assume.d.ts.map +1 -1
  194. package/dist/lib/identities/assume.js +4 -0
  195. package/dist/lib/zip/index.d.ts +41 -0
  196. package/dist/lib/zip/index.d.ts.map +1 -0
  197. package/dist/lib/zip/index.js +398 -0
  198. package/package.json +7 -1
  199. package/src/commands/agents.ts +54 -63
  200. package/src/commands/browse.ts +50 -14
  201. package/src/commands/diagnostics.ts +10 -0
  202. package/src/commands/doctor.ts +26 -7
  203. package/src/commands/governor.ts +23 -10
  204. package/src/commands/inbox.ts +52 -2
  205. package/src/commands/rm.ts +43 -4
  206. package/src/commands/servers.ts +16 -4
  207. package/src/commands/supervisor.ts +7 -2
  208. package/src/commands/toc.ts +14 -6
  209. package/src/commands/workflow.ts +30 -30
  210. package/src/core/agents/live-authority-v3.ts +17 -0
  211. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  212. package/src/core/agents/mailbox.ts +26 -17
  213. package/src/core/agents/render/prompt-context.ts +10 -12
  214. package/src/core/agents/render/session-context.ts +9 -32
  215. package/src/core/agents/rules/stop-hook.ts +3 -3
  216. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  217. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  218. package/src/core/agents/state/pidmap.ts +13 -0
  219. package/src/core/agents/state/proc-start.ts +57 -0
  220. package/src/core/agents/state/stale-sweep.ts +247 -82
  221. package/src/core/agents/state/unchanged-file.ts +456 -0
  222. package/src/core/context/index.ts +446 -136
  223. package/src/core/context/observation.ts +29 -0
  224. package/src/core/diagnostics/bundle.ts +23 -18
  225. package/src/core/diagnostics/path-identity.ts +15 -0
  226. package/src/core/diagnostics/sanitize.ts +4 -3
  227. package/src/core/events/v3/authority-outbox.ts +56 -0
  228. package/src/core/events/v3/generation-custody.ts +59 -0
  229. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  230. package/src/core/events/v3/producers/recorder.ts +94 -68
  231. package/src/core/governor/planning.ts +20 -8
  232. package/src/core/governor/service-read.ts +128 -20
  233. package/src/core/governor/service.ts +119 -42
  234. package/src/core/hooks/cli.ts +33 -5
  235. package/src/core/inbox/contract.ts +2 -1
  236. package/src/core/inbox/index.ts +2 -0
  237. package/src/core/inbox/peer-messages.ts +94 -0
  238. package/src/core/inbox/service.ts +88 -22
  239. package/src/core/inbox/targets.ts +148 -0
  240. package/src/core/inbox/watcher.ts +8 -9
  241. package/src/core/journal/index.ts +19 -2
  242. package/src/core/resources/contract.ts +4 -0
  243. package/src/core/resources/service-status.ts +35 -12
  244. package/src/core/resources/service.ts +71 -24
  245. package/src/core/resources/status.ts +57 -8
  246. package/src/core/servers/index.ts +177 -91
  247. package/src/core/servers/tunnels.ts +3 -11
  248. package/src/core/supervisor/contract.ts +13 -0
  249. package/src/core/supervisor/explanations.ts +4 -1
  250. package/src/core/supervisor/findings.ts +68 -10
  251. package/src/core/supervisor/service.ts +88 -26
  252. package/src/core/supervisor/services.ts +26 -8
  253. package/src/core/supervisor/status.ts +89 -12
  254. package/src/core/supervisor/timeline.ts +2 -1
  255. package/src/core/work/state.ts +7 -7
  256. package/src/core/workflow/engine.ts +131 -49
  257. package/src/core/workflow/failure-evidence.ts +141 -0
  258. package/src/core/workflow/index.ts +0 -1
  259. package/src/core/workflow/live-session-v3.ts +146 -5
  260. package/src/core/workflow/proof.ts +134 -26
  261. package/src/core/workflow/run-state.ts +31 -29
  262. package/src/core/workflow/spawn-claude.ts +18 -0
  263. package/src/core/workflow/spawn-codex.ts +13 -0
  264. package/src/core/workflow/spawn-cursor.ts +18 -0
  265. package/src/core/workflow/spawn-opencode.ts +14 -0
  266. package/src/core/workflow/types.ts +51 -17
  267. package/src/core/workflow/workspaces/index.ts +0 -1
  268. package/src/core/workflow/workspaces/leases.ts +48 -17
  269. package/src/core/workflow/workspaces/local-git.ts +315 -5
  270. package/src/core/workflow/workspaces/ownership.ts +232 -0
  271. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  272. package/src/core/workflow/workspaces/recovery.ts +25 -13
  273. package/src/core/workflow/workspaces/state.ts +20 -0
  274. package/src/lib/browser/windows-chrome.ts +346 -86
  275. package/src/lib/docs-links.ts +49 -2
  276. package/src/lib/guarded-remove.ts +132 -14
  277. package/src/lib/identities/assume.ts +4 -0
  278. package/src/lib/zip/index.ts +483 -0
@@ -3,12 +3,19 @@ import { hostname } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { gunzipSync } from "node:zlib";
5
5
  import { coordEnv } from "../../lib/env.ts";
6
+ import {
7
+ type ProcessGenerationVerdict,
8
+ verifyProcessGeneration,
9
+ } from "../agents/state/proc-start.ts";
10
+ import { currentHarneryRuntimeBuild } from "../events/v3/runtime-build.ts";
6
11
  import { createStorageCatalog } from "../storage/catalog.ts";
7
12
  import type { HarneryLogRecordV1 } from "../storage/jsonl.ts";
8
13
  import { parseLogRecord } from "../storage/jsonl.ts";
9
14
  import { familyLogDirectory, readSegmentManifest } from "../storage/segments.ts";
10
15
  import type {
16
+ GovernorServiceBuild,
11
17
  GovernorServiceConfig,
18
+ GovernorServiceProcessDependencies,
12
19
  GovernorServiceRuntime,
13
20
  GovernorServiceStatus,
14
21
  GovernorServiceStatusRecord,
@@ -17,7 +24,106 @@ import type {
17
24
  const MAX_FILE_BYTES = 512 * 1024;
18
25
  const MAX_LOG_RECORDS = 10_000;
19
26
  const MAX_GOALS = 100;
20
- const FOREIGN_STATUS_STALE_MS = 30_000;
27
+
28
+ export function currentGovernorServiceBuild(): GovernorServiceBuild {
29
+ let version = "unknown";
30
+ let revision = "unknown";
31
+ try {
32
+ const manifest = JSON.parse(
33
+ readFileSync(new URL("../../../package.json", import.meta.url), "utf8"),
34
+ ) as {
35
+ version?: unknown;
36
+ };
37
+ if (typeof manifest.version === "string" && manifest.version.length <= 100) {
38
+ version = manifest.version;
39
+ }
40
+ } catch {
41
+ // Missing package metadata cannot establish that the running build is current.
42
+ }
43
+ try {
44
+ revision = currentHarneryRuntimeBuild();
45
+ } catch {
46
+ // Status still reports the unavailable build instead of guessing a match.
47
+ }
48
+ return { version, revision };
49
+ }
50
+
51
+ export function governorServiceBuildStatus(
52
+ record: GovernorServiceStatusRecord | undefined,
53
+ ): Pick<
54
+ GovernorServiceStatus,
55
+ "running_build" | "installed_build" | "restart_needed" | "build_status"
56
+ > {
57
+ const installed_build = currentGovernorServiceBuild();
58
+ const candidate = record?.running_build;
59
+ const running_build =
60
+ candidate &&
61
+ typeof candidate.version === "string" &&
62
+ candidate.version.length > 0 &&
63
+ candidate.version.length <= 100 &&
64
+ typeof candidate.revision === "string" &&
65
+ candidate.revision.length > 0 &&
66
+ candidate.revision.length <= 100 &&
67
+ candidate.version !== "unknown" &&
68
+ candidate.revision !== "unknown"
69
+ ? candidate
70
+ : null;
71
+ const restart_needed =
72
+ running_build === null ||
73
+ installed_build.version === "unknown" ||
74
+ installed_build.revision === "unknown" ||
75
+ running_build.version !== installed_build.version ||
76
+ running_build.revision !== installed_build.revision;
77
+ return {
78
+ running_build,
79
+ installed_build,
80
+ restart_needed,
81
+ build_status:
82
+ running_build === null
83
+ ? "running build unknown; restart needed"
84
+ : restart_needed
85
+ ? "restart needed"
86
+ : "current",
87
+ };
88
+ }
89
+
90
+ /** An unknown token or process probe cannot establish a running or stopped owner. */
91
+ export function observeGovernorServiceProcess(
92
+ record: Pick<GovernorServiceStatusRecord, "pid" | "host" | "state" | "start_token"> | undefined,
93
+ dependencies: GovernorServiceProcessDependencies = {},
94
+ ): { generation: ProcessGenerationVerdict; liveness: GovernorServiceStatus["liveness"] } {
95
+ if (!record) return { generation: "unknown", liveness: "stopped" };
96
+ const generation =
97
+ record.host === hostname()
98
+ ? verifyProcessGeneration(record.pid, record.start_token, dependencies.readToken)
99
+ : "unknown";
100
+ if (record.host !== hostname()) return { generation, liveness: "unknown" };
101
+ try {
102
+ const present = (dependencies.probePid ?? probePid)(record.pid);
103
+ if (!present) return { generation, liveness: "stopped" };
104
+ } catch (error) {
105
+ return {
106
+ generation,
107
+ liveness: (error as NodeJS.ErrnoException).code === "ESRCH" ? "stopped" : "unknown",
108
+ };
109
+ }
110
+ return {
111
+ generation,
112
+ liveness:
113
+ generation === "same"
114
+ ? record.state === "stopped"
115
+ ? "stopped"
116
+ : "running"
117
+ : generation === "different"
118
+ ? "stopped"
119
+ : "unknown",
120
+ };
121
+ }
122
+
123
+ function probePid(pid: number): boolean {
124
+ process.kill(pid, 0);
125
+ return true;
126
+ }
21
127
 
22
128
  export function readGovernorServiceConfig(coordRootRaw: string): GovernorServiceConfig {
23
129
  const coordRoot = resolve(coordRootRaw);
@@ -67,7 +173,10 @@ export function readGovernorServiceRuntime(
67
173
  return runtime;
68
174
  }
69
175
 
70
- export function readGovernorServiceStatus(coordRootRaw: string): GovernorServiceStatus {
176
+ export function readGovernorServiceStatus(
177
+ coordRootRaw: string,
178
+ dependencies: GovernorServiceProcessDependencies = {},
179
+ ): GovernorServiceStatus {
71
180
  const coordRoot = resolve(coordRootRaw);
72
181
  const record = readStatusRecord(coordRoot);
73
182
  let config: GovernorServiceConfig | undefined;
@@ -82,9 +191,19 @@ export function readGovernorServiceStatus(coordRootRaw: string): GovernorService
82
191
  } catch {
83
192
  // Runtime is recoverable and must not make the dashboard unreadable.
84
193
  }
85
- if (!record) return { running: false, stale: false, config, runtime };
86
- const running = statusOwnerIsLive(record);
87
- return { running, stale: !running && record.state !== "stopped", record, config, runtime };
194
+ const observation = observeGovernorServiceProcess(record, dependencies);
195
+ if (!record && existsSync(join(serviceDir(coordRoot), "status.json"))) {
196
+ observation.liveness = "unknown";
197
+ }
198
+ return {
199
+ running: observation.liveness === "running" && record?.state !== "stopped",
200
+ stale: observation.liveness === "stopped" && record !== undefined && record.state !== "stopped",
201
+ ...observation,
202
+ ...governorServiceBuildStatus(record),
203
+ record,
204
+ config,
205
+ runtime,
206
+ };
88
207
  }
89
208
 
90
209
  export function governorServiceLogPath(coordRoot: string): string {
@@ -418,26 +537,15 @@ function readStatusRecord(coordRoot: string): GovernorServiceStatusRecord | unde
418
537
  ) {
419
538
  return undefined;
420
539
  }
421
- return value;
540
+ return {
541
+ ...value,
542
+ start_token: typeof value.start_token === "string" ? value.start_token : undefined,
543
+ };
422
544
  } catch {
423
545
  return undefined;
424
546
  }
425
547
  }
426
548
 
427
- function statusOwnerIsLive(record: GovernorServiceStatusRecord): boolean {
428
- if (record.state === "stopped") return false;
429
- if (record.host === hostname()) {
430
- try {
431
- process.kill(record.pid, 0);
432
- return true;
433
- } catch (error) {
434
- return (error as NodeJS.ErrnoException).code === "EPERM";
435
- }
436
- }
437
- const age = Date.now() - Date.parse(record.heartbeat_at);
438
- return Number.isFinite(age) && age < FOREIGN_STATUS_STALE_MS;
439
- }
440
-
441
549
  function readBoundedJson<T>(path: string, label: string): T {
442
550
  if (!existsSync(path)) throw new Error(`${label} does not exist`);
443
551
  const size = statSync(path).size;
@@ -16,12 +16,24 @@ import {
16
16
  import { hostname } from "node:os";
17
17
  import { dirname, join, resolve } from "node:path";
18
18
  import { coordEnv } from "../../lib/env.ts";
19
+ import {
20
+ type ProcessGenerationSignalDependencies,
21
+ type ProcessGenerationVerdict,
22
+ processStartToken,
23
+ signalIfSameGeneration,
24
+ } from "../agents/state/proc-start.ts";
25
+ import { resolveBinName } from "../config.ts";
19
26
  import { type NormalizedPolicy, normalizePolicy, type PolicyIsolation } from "../policy/index.ts";
20
27
  import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/logger.ts";
21
28
  import { stateDirMode, stateFileMode } from "../storage/modes.ts";
22
29
  import { RotatingTextSink } from "../storage/process-log.ts";
23
30
  import type { RunWorkItemInput } from "../work/index.ts";
24
31
  import { type GovernorRunReport, type GovernorStopReason, runGovernor } from "./runner.ts";
32
+ import {
33
+ currentGovernorServiceBuild,
34
+ governorServiceBuildStatus,
35
+ observeGovernorServiceProcess,
36
+ } from "./service-read.ts";
25
37
  import { type GovernorRecord, readGovernor } from "./state.ts";
26
38
 
27
39
  export const GOVERNOR_SERVICE_CONFIG_SCHEMA_VERSION = 1 as const;
@@ -31,7 +43,6 @@ export const GOVERNOR_SERVICE_STATUS_SCHEMA_VERSION = 1 as const;
31
43
  const MAX_FILE_BYTES = 512 * 1024;
32
44
  const MAX_GOALS = 100;
33
45
  const MAX_ERROR = 2_000;
34
- const FOREIGN_STATUS_STALE_MS = 30_000;
35
46
 
36
47
  export interface GovernorServiceEngineConfig {
37
48
  default_adapter?: string;
@@ -85,9 +96,20 @@ export interface GovernorServiceRuntime {
85
96
 
86
97
  export type GovernorServiceProcessState = "starting" | "running" | "stopping" | "stopped";
87
98
 
99
+ export interface GovernorServiceBuild {
100
+ version: string;
101
+ revision: string;
102
+ }
103
+
104
+ export interface GovernorServiceProcessDependencies extends ProcessGenerationSignalDependencies {
105
+ probePid?: (pid: number) => boolean;
106
+ }
107
+
88
108
  export interface GovernorServiceStatusRecord {
89
109
  schema_version: typeof GOVERNOR_SERVICE_STATUS_SCHEMA_VERSION;
90
110
  pid: number;
111
+ start_token?: string;
112
+ running_build?: GovernorServiceBuild;
91
113
  host: string;
92
114
  nonce: string;
93
115
  state: GovernorServiceProcessState;
@@ -105,6 +127,14 @@ export interface GovernorServiceStatusRecord {
105
127
  export interface GovernorServiceStatus {
106
128
  running: boolean;
107
129
  stale: boolean;
130
+ generation: ProcessGenerationVerdict;
131
+ liveness: "running" | "stopped" | "unknown";
132
+ running_build: GovernorServiceBuild | null;
133
+ installed_build: GovernorServiceBuild;
134
+ restart_needed: boolean;
135
+ build_status: string;
136
+ signal_sent?: boolean;
137
+ signal_verdict?: ProcessGenerationVerdict;
108
138
  record?: GovernorServiceStatusRecord;
109
139
  config?: GovernorServiceConfig;
110
140
  runtime?: GovernorServiceRuntime;
@@ -145,6 +175,8 @@ export interface RunGovernorServiceDaemonInput {
145
175
 
146
176
  interface ServiceLease {
147
177
  pid: number;
178
+ start_token?: string;
179
+ running_build?: GovernorServiceBuild;
148
180
  host: string;
149
181
  nonce: string;
150
182
  created_at: string;
@@ -155,8 +187,12 @@ export function configureGovernorService(
155
187
  ): GovernorServiceConfig {
156
188
  const coordRoot = resolve(input.coordRoot);
157
189
  const active = readStatusRecord(coordRoot);
158
- if (active && statusOwnerIsLive(active)) {
159
- throw new Error(`stop governor service pid ${active.pid} before changing its configuration`);
190
+ if (!active && existsSync(serviceStatusPath(coordRoot))) {
191
+ throw serviceStartRefusal(coordRoot, undefined, "unknown");
192
+ }
193
+ if (active) {
194
+ const liveness = statusOwnerIsLive(active);
195
+ if (liveness !== "stopped") throw serviceStartRefusal(coordRoot, active.pid, liveness);
160
196
  }
161
197
  const goalIds = unique(input.goalIds.map((value) => value.trim()).filter(Boolean));
162
198
  if (goalIds.length < 1 || goalIds.length > MAX_GOALS) {
@@ -235,7 +271,10 @@ export function readGovernorServiceRuntime(
235
271
  return runtime;
236
272
  }
237
273
 
238
- export function readGovernorServiceStatus(coordRootRaw: string): GovernorServiceStatus {
274
+ export function readGovernorServiceStatus(
275
+ coordRootRaw: string,
276
+ dependencies: GovernorServiceProcessDependencies = {},
277
+ ): GovernorServiceStatus {
239
278
  const coordRoot = resolve(coordRootRaw);
240
279
  const record = readStatusRecord(coordRoot);
241
280
  let config: GovernorServiceConfig | undefined;
@@ -250,17 +289,25 @@ export function readGovernorServiceStatus(coordRootRaw: string): GovernorService
250
289
  } catch {
251
290
  // Status remains readable when recoverable runtime state is corrupt.
252
291
  }
253
- if (!record) return { running: false, stale: false, config, runtime };
254
- const running = statusOwnerIsLive(record);
255
- return { running, stale: !running && record.state !== "stopped", record, config, runtime };
292
+ const observation = observeGovernorServiceProcess(record, dependencies);
293
+ if (!record && existsSync(serviceStatusPath(coordRoot))) observation.liveness = "unknown";
294
+ return {
295
+ running: observation.liveness === "running" && record?.state !== "stopped",
296
+ stale: observation.liveness === "stopped" && record !== undefined && record.state !== "stopped",
297
+ ...observation,
298
+ ...governorServiceBuildStatus(record),
299
+ record,
300
+ config,
301
+ runtime,
302
+ };
256
303
  }
257
304
 
258
305
  export async function spawnGovernorService(coordRootRaw: string): Promise<GovernorServiceStatus> {
259
306
  const coordRoot = resolve(coordRootRaw);
260
307
  readGovernorServiceConfig(coordRoot);
261
308
  const current = readGovernorServiceStatus(coordRoot);
262
- if (current.running) {
263
- throw new Error(`governor service is already running under pid ${current.record?.pid}`);
309
+ if (current.liveness !== "stopped") {
310
+ throw serviceStartRefusal(coordRoot, current.record?.pid, current.liveness);
264
311
  }
265
312
  mkdirSync(serviceDir(coordRoot), { recursive: true, mode: stateDirMode() });
266
313
  const sharedLogs = coordEnv("SHARED_LOGS") !== "0";
@@ -298,22 +345,33 @@ export async function spawnGovernorService(coordRootRaw: string): Promise<Govern
298
345
  throw new Error(`governor service failed to start; inspect ${governorServiceLogPath(coordRoot)}`);
299
346
  }
300
347
 
301
- export function requestGovernorServiceStop(coordRootRaw: string): GovernorServiceStatus {
348
+ export function requestGovernorServiceStop(
349
+ coordRootRaw: string,
350
+ dependencies: GovernorServiceProcessDependencies = {},
351
+ ): GovernorServiceStatus {
302
352
  const coordRoot = resolve(coordRootRaw);
303
- const status = readGovernorServiceStatus(coordRoot);
304
- if (!status.running || !status.record) return status;
353
+ const status = readGovernorServiceStatus(coordRoot, dependencies);
354
+ if ((status.record?.state === "stopped" || !status.record) && status.liveness === "stopped") {
355
+ return { ...status, signal_sent: false, signal_verdict: status.generation };
356
+ }
305
357
  writePrivateJsonAtomic(serviceStopPath(coordRoot), {
306
358
  requested_at: new Date().toISOString(),
307
359
  requested_by_pid: process.pid,
308
360
  });
309
- if (status.record.host === hostname()) {
310
- try {
311
- process.kill(status.record.pid, "SIGTERM");
312
- } catch {
313
- // The durable stop request remains for a racing or restarted daemon.
314
- }
315
- }
316
- return readGovernorServiceStatus(coordRoot);
361
+ const signal =
362
+ status.record?.host === hostname() && status.record.state !== "stopped"
363
+ ? signalIfSameGeneration(
364
+ status.record.pid,
365
+ status.record.start_token,
366
+ "SIGTERM",
367
+ dependencies,
368
+ )
369
+ : { sent: false, verdict: status.generation };
370
+ return {
371
+ ...readGovernorServiceStatus(coordRoot, dependencies),
372
+ signal_sent: signal.sent,
373
+ signal_verdict: signal.verdict,
374
+ };
317
375
  }
318
376
 
319
377
  export async function runGovernorServiceSweep(
@@ -461,12 +519,22 @@ export async function runGovernorServiceDaemon(
461
519
  ): Promise<GovernorServiceStatusRecord> {
462
520
  const coordRoot = resolve(input.coordRoot);
463
521
  const config = readGovernorServiceConfig(coordRoot);
522
+ const existing = readStatusRecord(coordRoot);
523
+ if (!existing && existsSync(serviceStatusPath(coordRoot))) {
524
+ throw serviceStartRefusal(coordRoot, undefined, "unknown");
525
+ }
526
+ if (existing) {
527
+ const liveness = statusOwnerIsLive(existing);
528
+ if (liveness !== "stopped") throw serviceStartRefusal(coordRoot, existing.pid, liveness);
529
+ }
464
530
  const release = acquireServiceLease(coordRoot);
465
531
  rmSync(serviceStopPath(coordRoot), { force: true });
466
532
  const now = new Date().toISOString();
467
533
  const status: GovernorServiceStatusRecord = {
468
534
  schema_version: GOVERNOR_SERVICE_STATUS_SCHEMA_VERSION,
469
535
  pid: process.pid,
536
+ start_token: processStartToken(process.pid) ?? undefined,
537
+ running_build: currentGovernorServiceBuild(),
470
538
  host: hostname(),
471
539
  nonce: randomUUID(),
472
540
  state: "starting",
@@ -493,6 +561,8 @@ export async function runGovernorServiceDaemon(
493
561
  appendGovernorServiceDiagnostic(coordRoot, {
494
562
  event: "service.started",
495
563
  pid: process.pid,
564
+ start_token: status.start_token,
565
+ running_build: status.running_build,
496
566
  goals: config.goal_ids,
497
567
  config_created_at: config.created_at,
498
568
  });
@@ -716,6 +786,8 @@ function acquireServiceLease(coordRoot: string): () => void {
716
786
  mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
717
787
  const owner: ServiceLease = {
718
788
  pid: process.pid,
789
+ start_token: processStartToken(process.pid) ?? undefined,
790
+ running_build: currentGovernorServiceBuild(),
719
791
  host: hostname(),
720
792
  nonce: randomUUID(),
721
793
  created_at: new Date().toISOString(),
@@ -736,8 +808,9 @@ function acquireServiceLease(coordRoot: string): () => void {
736
808
  };
737
809
  if (!acquire()) {
738
810
  const existing = readLease(path);
739
- if (existing && leaseOwnerIsLive(existing)) {
740
- throw new Error(`governor service is already running under pid ${existing.pid}`);
811
+ const liveness = existing ? leaseOwnerIsLive(existing) : "unknown";
812
+ if (liveness !== "stopped") {
813
+ throw serviceStartRefusal(coordRoot, existing?.pid, liveness);
741
814
  }
742
815
  unlinkSync(path);
743
816
  if (!acquire()) throw new Error("governor service lease raced with another process");
@@ -760,19 +833,18 @@ function readLease(path: string): ServiceLease | null {
760
833
  typeof value.host === "string" &&
761
834
  typeof value.nonce === "string" &&
762
835
  typeof value.created_at === "string"
763
- ? (value as ServiceLease)
836
+ ? ({
837
+ ...value,
838
+ start_token: typeof value.start_token === "string" ? value.start_token : undefined,
839
+ } as ServiceLease)
764
840
  : null;
765
841
  } catch {
766
842
  return null;
767
843
  }
768
844
  }
769
845
 
770
- function leaseOwnerIsLive(lease: ServiceLease): boolean {
771
- if (lease.host !== hostname()) {
772
- const age = Date.now() - Date.parse(lease.created_at);
773
- return Number.isFinite(age) && age < FOREIGN_STATUS_STALE_MS;
774
- }
775
- return pidAlive(lease.pid);
846
+ function leaseOwnerIsLive(lease: ServiceLease): GovernorServiceStatus["liveness"] {
847
+ return observeGovernorServiceProcess({ ...lease, state: "running" }).liveness;
776
848
  }
777
849
 
778
850
  function readStatusRecord(coordRoot: string): GovernorServiceStatusRecord | undefined {
@@ -793,26 +865,31 @@ function readStatusRecord(coordRoot: string): GovernorServiceStatusRecord | unde
793
865
  ) {
794
866
  return undefined;
795
867
  }
796
- return value;
868
+ return {
869
+ ...value,
870
+ start_token: typeof value.start_token === "string" ? value.start_token : undefined,
871
+ };
797
872
  } catch {
798
873
  return undefined;
799
874
  }
800
875
  }
801
876
 
802
- function statusOwnerIsLive(record: GovernorServiceStatusRecord): boolean {
803
- if (record.state === "stopped") return false;
804
- if (record.host === hostname()) return pidAlive(record.pid);
805
- const age = Date.now() - Date.parse(record.heartbeat_at);
806
- return Number.isFinite(age) && age < FOREIGN_STATUS_STALE_MS;
877
+ function statusOwnerIsLive(record: GovernorServiceStatusRecord): GovernorServiceStatus["liveness"] {
878
+ return observeGovernorServiceProcess(record).liveness;
807
879
  }
808
880
 
809
- function pidAlive(pid: number): boolean {
810
- try {
811
- process.kill(pid, 0);
812
- return true;
813
- } catch (error) {
814
- return (error as NodeJS.ErrnoException).code === "EPERM";
815
- }
881
+ function serviceStartRefusal(
882
+ coordRoot: string,
883
+ pid: number | undefined,
884
+ liveness: GovernorServiceStatus["liveness"],
885
+ ): Error {
886
+ const reason =
887
+ liveness === "unknown"
888
+ ? `governor service process generation is unknown${pid ? ` for pid ${pid}` : ""}`
889
+ : `governor service process is still running under pid ${pid}`;
890
+ return new Error(
891
+ `${reason}; run \`${resolveBinName(coordRoot)} governor service stop\` to write a durable stop request before starting or reconfiguring it`,
892
+ );
816
893
  }
817
894
 
818
895
  export function appendGovernorServiceDiagnostic(
@@ -2298,12 +2298,16 @@ async function emitUserPromptSubmitSystemMessage(
2298
2298
  if (coordEnv("WORKFLOW_CHILD") === "1") return false;
2299
2299
  const behavior = adapterBehavior(adapter);
2300
2300
  const { renderPromptContext } = await import("../agents/render/prompt-context.ts");
2301
+ let peerMessageIds: readonly string[] = [];
2301
2302
  let additionalContext = renderPromptContext({
2302
2303
  coordRoot,
2303
2304
  instanceId,
2304
2305
  sessionId,
2305
2306
  sessionNameNudge: true,
2306
2307
  ...behavior.promptContextNudges,
2308
+ onInboxPrepared: (ids) => {
2309
+ peerMessageIds = ids;
2310
+ },
2307
2311
  }).trim();
2308
2312
  const fileLinkContext = behavior.fileLinkContext?.(coordRoot, workspaceCwd) ?? "";
2309
2313
  if (fileLinkContext) {
@@ -2318,7 +2322,12 @@ async function emitUserPromptSubmitSystemMessage(
2318
2322
  if (!additionalContext) return false;
2319
2323
 
2320
2324
  const { emitContext } = await import("./adapter/output.ts");
2321
- return emitContext(adapter, "UserPromptSubmit", additionalContext);
2325
+ const emitted = emitContext(adapter, "UserPromptSubmit", additionalContext);
2326
+ if (emitted) {
2327
+ const { acknowledgeEmittedPeerInbox } = await import("../inbox/peer-messages.ts");
2328
+ await acknowledgeEmittedPeerInbox(coordRoot, instanceId, peerMessageIds);
2329
+ }
2330
+ return emitted;
2322
2331
  }
2323
2332
 
2324
2333
  async function emitSubagentStartContext(
@@ -2342,19 +2351,26 @@ async function emitSubagentStartContext(
2342
2351
  // is around. Reuse prompt-context (which dedups against the per-owner hash);
2343
2352
  // first call will always emit.
2344
2353
  let combined = message;
2354
+ let peerMessageIds: readonly string[] = [];
2345
2355
  const { renderPromptContext } = await import("../agents/render/prompt-context.ts");
2346
2356
  const context = renderPromptContext({
2347
2357
  coordRoot,
2348
2358
  instanceId,
2349
2359
  sessionId,
2350
2360
  agentName: subagentName,
2361
+ onInboxPrepared: (ids) => {
2362
+ peerMessageIds = ids;
2363
+ },
2351
2364
  }).trim();
2352
2365
  if (context) combined = `${message}\n\n${context}`;
2353
2366
 
2354
2367
  // Use SubagentStart event-name in CC's hookSpecificOutput shape; cursor's
2355
2368
  // flat `additional_context` works the same way.
2356
2369
  const { emitContext } = await import("./adapter/output.ts");
2357
- emitContext(adapter, "SubagentStart", combined);
2370
+ if (emitContext(adapter, "SubagentStart", combined)) {
2371
+ const { acknowledgeEmittedPeerInbox } = await import("../inbox/peer-messages.ts");
2372
+ await acknowledgeEmittedPeerInbox(coordRoot, instanceId, peerMessageIds);
2373
+ }
2358
2374
  }
2359
2375
 
2360
2376
  function adapterPlatform(adapter: Adapter): string {
@@ -2373,6 +2389,7 @@ async function emitSessionStartSystemMessage(
2373
2389
  const behavior = adapterBehavior(adapter);
2374
2390
  const workflowChild = coordEnv("WORKFLOW_CHILD") === "1";
2375
2391
  let additionalContext = "";
2392
+ let peerMessageIds: readonly string[] = [];
2376
2393
  // Opportunistic reconciliation makes normal session starts a failsafe for
2377
2394
  // archive, idle, cascade, and host lifecycle observations. It also runs the
2378
2395
  // stale cache sweep through the shared ADR 0077 composition.
@@ -2392,6 +2409,9 @@ async function emitSessionStartSystemMessage(
2392
2409
  sessionId,
2393
2410
  agentName: agentName || undefined,
2394
2411
  platformLabel: behavior.sessionStartPlatformLabel,
2412
+ onInboxPrepared: (ids) => {
2413
+ peerMessageIds = ids;
2414
+ },
2395
2415
  }).trim();
2396
2416
  }
2397
2417
 
@@ -2420,7 +2440,12 @@ async function emitSessionStartSystemMessage(
2420
2440
  }
2421
2441
  }
2422
2442
  const { emitContext } = await import("./adapter/output.ts");
2423
- return emitContext(adapter, "SessionStart", additionalContext);
2443
+ const emitted = emitContext(adapter, "SessionStart", additionalContext);
2444
+ if (emitted) {
2445
+ const { acknowledgeEmittedPeerInbox } = await import("../inbox/peer-messages.ts");
2446
+ await acknowledgeEmittedPeerInbox(coordRoot, instanceId, peerMessageIds);
2447
+ }
2448
+ return emitted;
2424
2449
  }
2425
2450
 
2426
2451
  function completeRecoveryInjection(
@@ -2429,8 +2454,11 @@ function completeRecoveryInjection(
2429
2454
  sessionId: string,
2430
2455
  recovery: PreparedContextRecovery,
2431
2456
  ): void {
2432
- completeContextRecovery(coordRoot, { sessionId, instanceId });
2433
- void recovery;
2457
+ completeContextRecovery(coordRoot, {
2458
+ sessionId,
2459
+ instanceId,
2460
+ checkpoint: recovery.checkpoint,
2461
+ });
2434
2462
  }
2435
2463
 
2436
2464
  main()
@@ -19,7 +19,8 @@ export interface HarneryInboxMessageRecordV1 {
19
19
  schema: typeof HARNERY_INBOX_RECORD_SCHEMA;
20
20
  kind: "message";
21
21
  message_id: string;
22
- sender_instance_id: string;
22
+ /** A required null identifies an operator; strings identify real agent instances. */
23
+ sender_instance_id: string | null;
23
24
  sender_display_name: string;
24
25
  recipient_instance_id: string;
25
26
  recipient_display_name: string;
@@ -1,4 +1,6 @@
1
1
  export * from "./contract.ts";
2
2
  export * from "./lifecycle.ts";
3
+ export * from "./peer-messages.ts";
3
4
  export * from "./service.ts";
5
+ export * from "./targets.ts";
4
6
  export * from "./watcher.ts";