harnery 0.45.0 → 0.45.2

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 (235) hide show
  1. package/dist/commander.js +0 -1
  2. package/dist/commands/files.d.ts +2 -2
  3. package/dist/commands/files.d.ts.map +1 -1
  4. package/dist/commands/files.js +4 -7
  5. package/dist/commands/servers.d.ts.map +1 -1
  6. package/dist/commands/servers.js +17 -11
  7. package/dist/commands/tunnel.d.ts +0 -15
  8. package/dist/commands/tunnel.d.ts.map +1 -1
  9. package/dist/commands/tunnel.js +153 -453
  10. package/dist/commands/workflow.d.ts.map +1 -1
  11. package/dist/commands/workflow.js +6 -45
  12. package/dist/core/agents/coord-client.d.ts.map +1 -1
  13. package/dist/core/agents/coord-client.js +0 -18
  14. package/dist/core/agents/live-authority-v3.d.ts +0 -1
  15. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  16. package/dist/core/agents/live-authority-v3.js +0 -11
  17. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
  18. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  19. package/dist/core/agents/live-lifecycle-v3.js +7 -28
  20. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  21. package/dist/core/agents/state/heartbeat-writer.js +14 -25
  22. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  23. package/dist/core/agents/state/live-coordination-writer.js +0 -5
  24. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  25. package/dist/core/agents/state/pidmap.js +2 -7
  26. package/dist/core/agents/state/proc-start.d.ts +20 -31
  27. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  28. package/dist/core/agents/state/proc-start.js +37 -70
  29. package/dist/core/agents/state/service-process.d.ts +61 -0
  30. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  31. package/dist/core/agents/state/service-process.js +143 -0
  32. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  33. package/dist/core/agents/state/stale-sweep.js +70 -180
  34. package/dist/core/artifacts/activity.d.ts +4 -1
  35. package/dist/core/artifacts/activity.d.ts.map +1 -1
  36. package/dist/core/artifacts/activity.js +8 -2
  37. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  38. package/dist/core/artifacts/delivery-card.js +1 -2
  39. package/dist/core/artifacts/index.d.ts.map +1 -1
  40. package/dist/core/artifacts/index.js +38 -12
  41. package/dist/core/context/index.d.ts +6 -4
  42. package/dist/core/context/index.d.ts.map +1 -1
  43. package/dist/core/context/index.js +194 -256
  44. package/dist/core/context/observation.d.ts +9 -9
  45. package/dist/core/context/observation.d.ts.map +1 -1
  46. package/dist/core/context/observation.js +37 -6
  47. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  48. package/dist/core/events/v3/authority-outbox.js +0 -29
  49. package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
  50. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  51. package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
  52. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  53. package/dist/core/events/v3/producers/recorder.js +64 -77
  54. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  55. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  56. package/dist/core/events/v3/runtime-build.js +44 -0
  57. package/dist/core/governor/index.d.ts +2 -1
  58. package/dist/core/governor/index.d.ts.map +1 -1
  59. package/dist/core/governor/index.js +2 -1
  60. package/dist/core/governor/service-read.d.ts +3 -10
  61. package/dist/core/governor/service-read.d.ts.map +1 -1
  62. package/dist/core/governor/service-read.js +10 -94
  63. package/dist/core/governor/service.d.ts +3 -7
  64. package/dist/core/governor/service.d.ts.map +1 -1
  65. package/dist/core/governor/service.js +17 -182
  66. package/dist/core/hooks/cli.js +78 -48
  67. package/dist/core/hooks/resolve/owner.js +2 -2
  68. package/dist/core/resources/sampler.js +2 -2
  69. package/dist/core/resources/service-status.d.ts +2 -14
  70. package/dist/core/resources/service-status.d.ts.map +1 -1
  71. package/dist/core/resources/service-status.js +3 -23
  72. package/dist/core/resources/service.d.ts +3 -7
  73. package/dist/core/resources/service.d.ts.map +1 -1
  74. package/dist/core/resources/service.js +12 -101
  75. package/dist/core/resources/status.d.ts.map +1 -1
  76. package/dist/core/resources/status.js +3 -2
  77. package/dist/core/semantic/service.d.ts.map +1 -1
  78. package/dist/core/semantic/service.js +9 -2
  79. package/dist/core/servers/index.d.ts +11 -10
  80. package/dist/core/servers/index.d.ts.map +1 -1
  81. package/dist/core/servers/index.js +82 -192
  82. package/dist/core/servers/tunnels.d.ts +3 -5
  83. package/dist/core/servers/tunnels.d.ts.map +1 -1
  84. package/dist/core/servers/tunnels.js +2 -45
  85. package/dist/core/storage/durable-history.js +56 -22
  86. package/dist/core/storage/segments.d.ts.map +1 -1
  87. package/dist/core/storage/segments.js +73 -17
  88. package/dist/core/supervisor/service.d.ts +4 -7
  89. package/dist/core/supervisor/service.d.ts.map +1 -1
  90. package/dist/core/supervisor/service.js +23 -122
  91. package/dist/core/supervisor/services.js +2 -2
  92. package/dist/core/supervisor/status.d.ts +3 -17
  93. package/dist/core/supervisor/status.d.ts.map +1 -1
  94. package/dist/core/supervisor/status.js +7 -70
  95. package/dist/core/work/state.js +7 -7
  96. package/dist/core/workflow/engine.d.ts.map +1 -1
  97. package/dist/core/workflow/engine.js +23 -66
  98. package/dist/core/workflow/index.d.ts +3 -4
  99. package/dist/core/workflow/index.d.ts.map +1 -1
  100. package/dist/core/workflow/index.js +2 -2
  101. package/dist/core/workflow/live-session-v3.d.ts +0 -6
  102. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  103. package/dist/core/workflow/live-session-v3.js +5 -126
  104. package/dist/core/workflow/proof.d.ts +4 -4
  105. package/dist/core/workflow/proof.d.ts.map +1 -1
  106. package/dist/core/workflow/proof.js +34 -86
  107. package/dist/core/workflow/resume-lock.d.ts +17 -0
  108. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  109. package/dist/core/workflow/resume-lock.js +158 -0
  110. package/dist/core/workflow/run-state.d.ts +0 -21
  111. package/dist/core/workflow/run-state.d.ts.map +1 -1
  112. package/dist/core/workflow/run-state.js +3 -174
  113. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  114. package/dist/core/workflow/spawn-claude.js +0 -19
  115. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  116. package/dist/core/workflow/spawn-codex.js +0 -13
  117. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  118. package/dist/core/workflow/spawn-cursor.js +0 -19
  119. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  120. package/dist/core/workflow/spawn-opencode.js +0 -14
  121. package/dist/core/workflow/types.d.ts +11 -54
  122. package/dist/core/workflow/types.d.ts.map +1 -1
  123. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  124. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  125. package/dist/core/workflow/workspaces/leases.js +1 -1
  126. package/dist/core/workflow/workspaces/local-git.js +91 -210
  127. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  128. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  129. package/dist/core/workflow/workspaces/ownership.js +3 -130
  130. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  131. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  132. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  133. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  134. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  135. package/dist/core/workflow/workspaces/recovery.js +1 -1
  136. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  137. package/dist/core/workflow/workspaces/state.js +1 -4
  138. package/dist/lib/identities/assume.d.ts.map +1 -1
  139. package/dist/lib/identities/assume.js +0 -4
  140. package/dist/lib/tunnel/state.d.ts +6 -13
  141. package/dist/lib/tunnel/state.d.ts.map +1 -1
  142. package/dist/lib/tunnel/state.js +12 -58
  143. package/package.json +1 -6
  144. package/src/commander.ts +0 -10
  145. package/src/commands/files.ts +5 -13
  146. package/src/commands/servers.ts +23 -12
  147. package/src/commands/tunnel.ts +166 -533
  148. package/src/commands/workflow.ts +8 -58
  149. package/src/core/agents/coord-client.ts +0 -20
  150. package/src/core/agents/live-authority-v3.ts +0 -17
  151. package/src/core/agents/live-lifecycle-v3.ts +14 -52
  152. package/src/core/agents/state/heartbeat-writer.ts +12 -29
  153. package/src/core/agents/state/live-coordination-writer.ts +0 -29
  154. package/src/core/agents/state/pidmap.ts +2 -15
  155. package/src/core/agents/state/proc-start.ts +41 -69
  156. package/src/core/agents/state/service-process.ts +217 -0
  157. package/src/core/agents/state/stale-sweep.ts +82 -247
  158. package/src/core/artifacts/activity.ts +9 -2
  159. package/src/core/artifacts/delivery-card.ts +1 -7
  160. package/src/core/artifacts/index.ts +45 -12
  161. package/src/core/context/index.ts +207 -272
  162. package/src/core/context/observation.ts +40 -9
  163. package/src/core/events/v3/authority-outbox.ts +0 -56
  164. package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
  165. package/src/core/events/v3/producers/recorder.ts +68 -94
  166. package/src/core/events/v3/runtime-build.ts +62 -0
  167. package/src/core/governor/index.ts +1 -2
  168. package/src/core/governor/service-read.ts +15 -114
  169. package/src/core/governor/service.ts +36 -220
  170. package/src/core/hooks/cli.ts +85 -47
  171. package/src/core/hooks/resolve/owner.ts +2 -2
  172. package/src/core/resources/sampler.ts +3 -2
  173. package/src/core/resources/service-status.ts +6 -35
  174. package/src/core/resources/service.ts +23 -133
  175. package/src/core/resources/status.ts +3 -2
  176. package/src/core/semantic/service.ts +8 -2
  177. package/src/core/servers/index.ts +86 -228
  178. package/src/core/servers/tunnels.ts +3 -55
  179. package/src/core/storage/durable-history.ts +52 -20
  180. package/src/core/storage/segments.ts +79 -17
  181. package/src/core/supervisor/service.ts +33 -155
  182. package/src/core/supervisor/services.ts +2 -2
  183. package/src/core/supervisor/status.ts +10 -85
  184. package/src/core/work/state.ts +7 -7
  185. package/src/core/workflow/engine.ts +22 -74
  186. package/src/core/workflow/index.ts +2 -13
  187. package/src/core/workflow/live-session-v3.ts +5 -146
  188. package/src/core/workflow/proof.ts +40 -104
  189. package/src/core/workflow/resume-lock.ts +179 -0
  190. package/src/core/workflow/run-state.ts +3 -207
  191. package/src/core/workflow/spawn-claude.ts +0 -18
  192. package/src/core/workflow/spawn-codex.ts +0 -13
  193. package/src/core/workflow/spawn-cursor.ts +0 -18
  194. package/src/core/workflow/spawn-opencode.ts +0 -14
  195. package/src/core/workflow/types.ts +11 -41
  196. package/src/core/workflow/workspaces/leases.ts +1 -1
  197. package/src/core/workflow/workspaces/local-git.ts +120 -239
  198. package/src/core/workflow/workspaces/ownership.ts +3 -158
  199. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  200. package/src/core/workflow/workspaces/recovery.ts +1 -1
  201. package/src/core/workflow/workspaces/state.ts +1 -5
  202. package/src/lib/identities/assume.ts +0 -4
  203. package/src/lib/tunnel/state.ts +15 -70
  204. package/dist/commands/turn-rollback.d.ts +0 -5
  205. package/dist/commands/turn-rollback.d.ts.map +0 -1
  206. package/dist/commands/turn-rollback.js +0 -76
  207. package/dist/core/agents/state/process-handle.d.ts +0 -26
  208. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  209. package/dist/core/agents/state/process-handle.js +0 -121
  210. package/dist/core/agents/state/unchanged-file.d.ts +0 -44
  211. package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
  212. package/dist/core/agents/state/unchanged-file.js +0 -390
  213. package/dist/core/events/v3/generation-custody.d.ts +0 -5
  214. package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
  215. package/dist/core/events/v3/generation-custody.js +0 -50
  216. package/dist/core/turn-rollback/content.d.ts +0 -53
  217. package/dist/core/turn-rollback/content.d.ts.map +0 -1
  218. package/dist/core/turn-rollback/content.js +0 -396
  219. package/dist/core/turn-rollback/index.d.ts +0 -72
  220. package/dist/core/turn-rollback/index.d.ts.map +0 -1
  221. package/dist/core/turn-rollback/index.js +0 -443
  222. package/dist/core/workflow/failure-evidence.d.ts +0 -16
  223. package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
  224. package/dist/core/workflow/failure-evidence.js +0 -114
  225. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  226. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  227. package/dist/core/workflow/recovery-audit.js +0 -116
  228. package/src/commands/turn-rollback.ts +0 -111
  229. package/src/core/agents/state/process-handle.ts +0 -165
  230. package/src/core/agents/state/unchanged-file.ts +0 -456
  231. package/src/core/events/v3/generation-custody.ts +0 -59
  232. package/src/core/turn-rollback/content.ts +0 -499
  233. package/src/core/turn-rollback/index.ts +0 -580
  234. package/src/core/workflow/failure-evidence.ts +0 -141
  235. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -10,30 +10,28 @@ import {
10
10
  renameSync,
11
11
  rmSync,
12
12
  statSync,
13
- unlinkSync,
14
13
  writeFileSync,
15
14
  } from "node:fs";
16
15
  import { hostname } from "node:os";
17
16
  import { dirname, join, resolve } from "node:path";
18
17
  import { coordEnv } from "../../lib/env.ts";
18
+ import { type ProcessGenerationVerdict, processStartToken } from "../agents/state/proc-start.ts";
19
19
  import {
20
- type ProcessGenerationSignalDependencies,
21
- type ProcessGenerationVerdict,
22
- processStartToken,
23
- signalIfSameGeneration,
24
- } from "../agents/state/proc-start.ts";
20
+ acquireServiceLease,
21
+ requestServiceStop,
22
+ type ServiceLiveness,
23
+ type ServiceProcessDependencies,
24
+ serviceStartRefusal,
25
+ } from "../agents/state/service-process.ts";
25
26
  import { resolveBinName } from "../config.ts";
27
+ import { installedHarneryBuild } from "../events/v3/runtime-build.ts";
26
28
  import { type NormalizedPolicy, normalizePolicy, type PolicyIsolation } from "../policy/index.ts";
27
29
  import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/logger.ts";
28
30
  import { stateDirMode, stateFileMode } from "../storage/modes.ts";
29
31
  import { RotatingTextSink } from "../storage/process-log.ts";
30
32
  import type { RunWorkItemInput } from "../work/index.ts";
31
33
  import { type GovernorRunReport, type GovernorStopReason, runGovernor } from "./runner.ts";
32
- import {
33
- currentGovernorServiceBuild,
34
- governorServiceBuildStatus,
35
- observeGovernorServiceProcess,
36
- } from "./service-read.ts";
34
+ import { readGovernorServiceRuntime, readGovernorServiceStatus } from "./service-read.ts";
37
35
  import { type GovernorRecord, readGovernor } from "./state.ts";
38
36
 
39
37
  export const GOVERNOR_SERVICE_CONFIG_SCHEMA_VERSION = 1 as const;
@@ -101,10 +99,6 @@ export interface GovernorServiceBuild {
101
99
  revision: string;
102
100
  }
103
101
 
104
- export interface GovernorServiceProcessDependencies extends ProcessGenerationSignalDependencies {
105
- probePid?: (pid: number) => boolean;
106
- }
107
-
108
102
  export interface GovernorServiceStatusRecord {
109
103
  schema_version: typeof GOVERNOR_SERVICE_STATUS_SCHEMA_VERSION;
110
104
  pid: number;
@@ -173,27 +167,13 @@ export interface RunGovernorServiceDaemonInput {
173
167
  onLog?: (line: string) => void;
174
168
  }
175
169
 
176
- interface ServiceLease {
177
- pid: number;
178
- start_token?: string;
179
- running_build?: GovernorServiceBuild;
180
- host: string;
181
- nonce: string;
182
- created_at: string;
183
- }
184
-
185
170
  export function configureGovernorService(
186
171
  input: ConfigureGovernorServiceInput,
187
172
  ): GovernorServiceConfig {
188
173
  const coordRoot = resolve(input.coordRoot);
189
- const active = readStatusRecord(coordRoot);
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);
196
- }
174
+ const current = readGovernorServiceStatus(coordRoot);
175
+ if (current.liveness !== "stopped")
176
+ throw startRefusal(coordRoot, current.record?.pid, current.liveness);
197
177
  const goalIds = unique(input.goalIds.map((value) => value.trim()).filter(Boolean));
198
178
  if (goalIds.length < 1 || goalIds.length > MAX_GOALS) {
199
179
  throw new Error(`governor service requires from 1 to ${MAX_GOALS} unique goal ids`);
@@ -249,65 +229,12 @@ export function readGovernorServiceConfig(coordRootRaw: string): GovernorService
249
229
  return config;
250
230
  }
251
231
 
252
- export function readGovernorServiceRuntime(
253
- coordRootRaw: string,
254
- ): GovernorServiceRuntime | undefined {
255
- const coordRoot = resolve(coordRootRaw);
256
- if (!existsSync(serviceRuntimePath(coordRoot))) return undefined;
257
- const runtime = readBoundedJson<GovernorServiceRuntime>(
258
- serviceRuntimePath(coordRoot),
259
- "governor service runtime",
260
- );
261
- if (
262
- runtime.schema_version !== GOVERNOR_SERVICE_RUNTIME_SCHEMA_VERSION ||
263
- !validTimestamp(runtime.config_created_at) ||
264
- !validTimestamp(runtime.updated_at) ||
265
- !runtime.goals ||
266
- typeof runtime.goals !== "object" ||
267
- Array.isArray(runtime.goals)
268
- ) {
269
- throw new Error("governor service runtime has an unsupported schema");
270
- }
271
- return runtime;
272
- }
273
-
274
- export function readGovernorServiceStatus(
275
- coordRootRaw: string,
276
- dependencies: GovernorServiceProcessDependencies = {},
277
- ): GovernorServiceStatus {
278
- const coordRoot = resolve(coordRootRaw);
279
- const record = readStatusRecord(coordRoot);
280
- let config: GovernorServiceConfig | undefined;
281
- let runtime: GovernorServiceRuntime | undefined;
282
- try {
283
- config = readGovernorServiceConfig(coordRoot);
284
- } catch {
285
- // A service has no usable configuration until explicitly configured.
286
- }
287
- try {
288
- runtime = readGovernorServiceRuntime(coordRoot);
289
- } catch {
290
- // Status remains readable when recoverable runtime state is corrupt.
291
- }
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
- };
303
- }
304
-
305
232
  export async function spawnGovernorService(coordRootRaw: string): Promise<GovernorServiceStatus> {
306
233
  const coordRoot = resolve(coordRootRaw);
307
234
  readGovernorServiceConfig(coordRoot);
308
235
  const current = readGovernorServiceStatus(coordRoot);
309
236
  if (current.liveness !== "stopped") {
310
- throw serviceStartRefusal(coordRoot, current.record?.pid, current.liveness);
237
+ throw startRefusal(coordRoot, current.record?.pid, current.liveness);
311
238
  }
312
239
  mkdirSync(serviceDir(coordRoot), { recursive: true, mode: stateDirMode() });
313
240
  const sharedLogs = coordEnv("SHARED_LOGS") !== "0";
@@ -347,31 +274,19 @@ export async function spawnGovernorService(coordRootRaw: string): Promise<Govern
347
274
 
348
275
  export function requestGovernorServiceStop(
349
276
  coordRootRaw: string,
350
- dependencies: GovernorServiceProcessDependencies = {},
277
+ dependencies: ServiceProcessDependencies = {},
351
278
  ): GovernorServiceStatus {
352
279
  const coordRoot = resolve(coordRootRaw);
353
280
  const status = readGovernorServiceStatus(coordRoot, dependencies);
354
281
  if ((status.record?.state === "stopped" || !status.record) && status.liveness === "stopped") {
355
282
  return { ...status, signal_sent: false, signal_verdict: status.generation };
356
283
  }
357
- writePrivateJsonAtomic(serviceStopPath(coordRoot), {
358
- requested_at: new Date().toISOString(),
359
- requested_by_pid: process.pid,
360
- });
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
- };
284
+ return requestServiceStop(
285
+ status,
286
+ () => readGovernorServiceStatus(coordRoot, dependencies),
287
+ serviceStopPath(coordRoot),
288
+ dependencies,
289
+ );
375
290
  }
376
291
 
377
292
  export async function runGovernorServiceSweep(
@@ -519,22 +434,21 @@ export async function runGovernorServiceDaemon(
519
434
  ): Promise<GovernorServiceStatusRecord> {
520
435
  const coordRoot = resolve(input.coordRoot);
521
436
  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
- }
530
- const release = acquireServiceLease(coordRoot);
437
+ const current = readGovernorServiceStatus(coordRoot);
438
+ if (current.liveness !== "stopped")
439
+ throw startRefusal(coordRoot, current.record?.pid, current.liveness);
440
+ const { release } = acquireServiceLease(
441
+ serviceLeasePath(coordRoot),
442
+ (pid, liveness) => startRefusal(coordRoot, pid, liveness),
443
+ { running_build: installedHarneryBuild() },
444
+ );
531
445
  rmSync(serviceStopPath(coordRoot), { force: true });
532
446
  const now = new Date().toISOString();
533
447
  const status: GovernorServiceStatusRecord = {
534
448
  schema_version: GOVERNOR_SERVICE_STATUS_SCHEMA_VERSION,
535
449
  pid: process.pid,
536
450
  start_token: processStartToken(process.pid) ?? undefined,
537
- running_build: currentGovernorServiceBuild(),
451
+ running_build: installedHarneryBuild(),
538
452
  host: hostname(),
539
453
  nonce: randomUUID(),
540
454
  state: "starting",
@@ -781,114 +695,16 @@ function normalizeIsolation(value: unknown): PolicyIsolation | undefined {
781
695
  throw new Error("service isolation must be shared, worktree, sandbox, or remote");
782
696
  }
783
697
 
784
- function acquireServiceLease(coordRoot: string): () => void {
785
- const path = serviceLeasePath(coordRoot);
786
- mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
787
- const owner: ServiceLease = {
788
- pid: process.pid,
789
- start_token: processStartToken(process.pid) ?? undefined,
790
- running_build: currentGovernorServiceBuild(),
791
- host: hostname(),
792
- nonce: randomUUID(),
793
- created_at: new Date().toISOString(),
794
- };
795
- const acquire = (): boolean => {
796
- try {
797
- const fd = openSync(path, "wx", stateFileMode());
798
- try {
799
- writeFileSync(fd, `${JSON.stringify(owner)}\n`, "utf8");
800
- } finally {
801
- closeSync(fd);
802
- }
803
- return true;
804
- } catch (error) {
805
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
806
- return false;
807
- }
808
- };
809
- if (!acquire()) {
810
- const existing = readLease(path);
811
- const liveness = existing ? leaseOwnerIsLive(existing) : "unknown";
812
- if (liveness !== "stopped") {
813
- throw serviceStartRefusal(coordRoot, existing?.pid, liveness);
814
- }
815
- unlinkSync(path);
816
- if (!acquire()) throw new Error("governor service lease raced with another process");
817
- }
818
- return () => {
819
- try {
820
- const existing = readLease(path);
821
- if (existing?.nonce === owner.nonce) unlinkSync(path);
822
- } catch {
823
- // A stale lease is recoverable by the next explicit service start.
824
- }
825
- };
826
- }
827
-
828
- function readLease(path: string): ServiceLease | null {
829
- try {
830
- const value = JSON.parse(readFileSync(path, "utf8")) as Partial<ServiceLease>;
831
- return Number.isSafeInteger(value.pid) &&
832
- (value.pid ?? 0) > 0 &&
833
- typeof value.host === "string" &&
834
- typeof value.nonce === "string" &&
835
- typeof value.created_at === "string"
836
- ? ({
837
- ...value,
838
- start_token: typeof value.start_token === "string" ? value.start_token : undefined,
839
- } as ServiceLease)
840
- : null;
841
- } catch {
842
- return null;
843
- }
844
- }
845
-
846
- function leaseOwnerIsLive(lease: ServiceLease): GovernorServiceStatus["liveness"] {
847
- return observeGovernorServiceProcess({ ...lease, state: "running" }).liveness;
848
- }
849
-
850
- function readStatusRecord(coordRoot: string): GovernorServiceStatusRecord | undefined {
851
- if (!existsSync(serviceStatusPath(coordRoot))) return undefined;
852
- try {
853
- const value = readBoundedJson<GovernorServiceStatusRecord>(
854
- serviceStatusPath(coordRoot),
855
- "governor service status",
856
- );
857
- if (
858
- value.schema_version !== GOVERNOR_SERVICE_STATUS_SCHEMA_VERSION ||
859
- !Number.isSafeInteger(value.pid) ||
860
- value.pid < 1 ||
861
- typeof value.host !== "string" ||
862
- typeof value.nonce !== "string" ||
863
- !validTimestamp(value.started_at) ||
864
- !validTimestamp(value.heartbeat_at)
865
- ) {
866
- return undefined;
867
- }
868
- return {
869
- ...value,
870
- start_token: typeof value.start_token === "string" ? value.start_token : undefined,
871
- };
872
- } catch {
873
- return undefined;
874
- }
875
- }
876
-
877
- function statusOwnerIsLive(record: GovernorServiceStatusRecord): GovernorServiceStatus["liveness"] {
878
- return observeGovernorServiceProcess(record).liveness;
879
- }
880
-
881
- function serviceStartRefusal(
698
+ function startRefusal(
882
699
  coordRoot: string,
883
700
  pid: number | undefined,
884
- liveness: GovernorServiceStatus["liveness"],
701
+ liveness: ServiceLiveness,
885
702
  ): 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`,
703
+ return serviceStartRefusal(
704
+ "governor service",
705
+ `${resolveBinName(coordRoot)} governor service stop`,
706
+ pid,
707
+ liveness,
892
708
  );
893
709
  }
894
710
 
@@ -844,6 +844,9 @@ async function main(): Promise<number> {
844
844
  if (eventName === "runtime-context-retry") {
845
845
  return runRuntimeContextRetryWorker(coordRoot, adapter, extra);
846
846
  }
847
+ if (eventName === SESSION_MAINTENANCE_EVENT) {
848
+ return runSessionMaintenanceWorker(coordRoot, adapter);
849
+ }
847
850
  const ledgerRoute = resolveLiveEventLedgerRouteV3(coordRoot);
848
851
 
849
852
  // Always log a breadcrumb, useful when an event_type maps to null or owner
@@ -1274,7 +1277,7 @@ function recordContextTelemetry(run: HookRun): void {
1274
1277
  }
1275
1278
 
1276
1279
  async function handleSessionStarted(run: HookRun): Promise<void> {
1277
- const { coordRoot, adapter, behavior, payload, owner, sessionId, data } = run;
1280
+ const { coordRoot, adapter, payload, owner, sessionId, data } = run;
1278
1281
  // A configured backup runs out of process and is freshness-gated per host.
1279
1282
  // Startup never waits for restic or a remote provider; the child owns no
1280
1283
  // hook stdio, so the harness is not held open until it finishes. A failed
@@ -1285,52 +1288,10 @@ async function handleSessionStarted(run: HookRun): Promise<void> {
1285
1288
  } catch (err) {
1286
1289
  logError(coordRoot, err, { phase: "backup-schedule" });
1287
1290
  }
1288
- // Effect (journal-maintaining adapters): prune stale journal archives +
1289
- // sweep orphans. The recovery-cue is merged into the session-start
1290
- // additionalContext inside emitSessionStartSystemMessage.
1291
- if (behavior.journalMaintenance) journalJanitor(coordRoot);
1292
- imageJanitor(coordRoot);
1293
- // Session start is the only caller of the artifact janitor and the storage
1294
- // maintenance pass, so both load here instead of on every tool call.
1295
- const { autoCleanArtifacts } = await import("../artifacts/index.ts");
1296
- const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.ts");
1297
- const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import(
1298
- "../storage/maintenance-providers.ts"
1299
- );
1300
- // Effect: throttled hourly artifact sweep (see autoCleanArtifacts). It
1301
- // deletes expired workspaces, plus unheld workspaces over the per-bundle
1302
- // ceiling or repository budget once they have been idle past
1303
- // artifacts.idle_grace_hours. Each deletion is rechecked first and recorded
1304
- // in .harnery/artifact-deletions.jsonl. Best-effort like every janitor
1305
- // here: a failure logs and never blocks session start.
1306
- try {
1307
- autoCleanArtifacts(coordRoot);
1308
- } catch (err) {
1309
- logError(coordRoot, err, { phase: "artifact-auto-clean" });
1310
- }
1311
- // Effect: apply the same daily throttle to closed V3 epochs. The newest
1312
- // recovery epochs remain protected by the archive retention policy.
1313
- try {
1314
- autoCleanEventV3Archives(coordRoot);
1315
- } catch (err) {
1316
- logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
1317
- }
1318
- // Plan one bounded, claim-first global slice from cached pressure only.
1319
- // Existing owner janitors above remain the only active executors until
1320
- // each provider receives a separate production activation.
1321
- try {
1322
- await runAutomaticMaintenancePass(
1323
- createAutomaticMaintenanceComposition(coordRoot, {
1324
- journal: () => {
1325
- if (behavior.journalMaintenance) journalJanitor(coordRoot);
1326
- },
1327
- images: () => imageJanitor(coordRoot),
1328
- artifacts: () => autoCleanArtifacts(coordRoot),
1329
- }),
1330
- );
1331
- } catch (err) {
1332
- logError(coordRoot, err, { phase: "storage-maintenance-auto" });
1333
- }
1291
+ // Storage maintenance can walk large stores. It runs in a detached worker so
1292
+ // session start emits its context without waiting for it; an adapter that
1293
+ // stops a slow hook would otherwise drop that context entirely.
1294
+ scheduleSessionMaintenance(coordRoot, adapter);
1334
1295
  let recovery: PreparedContextRecovery | null = null;
1335
1296
  if (payload?.source === "compact") {
1336
1297
  try {
@@ -1814,6 +1775,83 @@ async function handleCompletedResponse(run: CompletedResponseRun): Promise<numbe
1814
1775
  return 0;
1815
1776
  }
1816
1777
 
1778
+ const SESSION_MAINTENANCE_EVENT = "session-maintenance";
1779
+
1780
+ function scheduleSessionMaintenance(coordRoot: string, adapter: Adapter): void {
1781
+ const modulePath = process.argv[1];
1782
+ if (!modulePath) return;
1783
+ try {
1784
+ const child = spawn(
1785
+ process.execPath,
1786
+ [modulePath, SESSION_MAINTENANCE_EVENT, "--adapter", adapter],
1787
+ {
1788
+ cwd: coordRoot,
1789
+ detached: true,
1790
+ stdio: "ignore",
1791
+ env: childEnv(coordRoot),
1792
+ },
1793
+ );
1794
+ child.unref();
1795
+ } catch (err) {
1796
+ logError(coordRoot, err, { phase: "session-maintenance-spawn" });
1797
+ }
1798
+ }
1799
+
1800
+ /**
1801
+ * Session-start storage maintenance, run out of the hook's process. Every step
1802
+ * is best-effort and throttled by its own stamp: a failure logs and never
1803
+ * reaches the session.
1804
+ */
1805
+ async function runSessionMaintenanceWorker(
1806
+ coordRoot: string,
1807
+ adapter: Adapter | null,
1808
+ ): Promise<number> {
1809
+ const journalMaintenance = adapter ? adapterBehavior(adapter).journalMaintenance : false;
1810
+ // Effect (journal-maintaining adapters): prune stale journal archives +
1811
+ // sweep orphans. The recovery cue is read separately at session start.
1812
+ if (journalMaintenance) journalJanitor(coordRoot);
1813
+ imageJanitor(coordRoot);
1814
+ const { autoCleanArtifacts } = await import("../artifacts/index.ts");
1815
+ const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.ts");
1816
+ const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import(
1817
+ "../storage/maintenance-providers.ts"
1818
+ );
1819
+ // Effect: throttled hourly artifact sweep (see autoCleanArtifacts). It
1820
+ // deletes expired workspaces, plus unheld workspaces over the per-bundle
1821
+ // ceiling or repository budget once they have been idle past
1822
+ // artifacts.idle_grace_hours. Each deletion is rechecked first and recorded
1823
+ // in .harnery/artifact-deletions.jsonl.
1824
+ try {
1825
+ autoCleanArtifacts(coordRoot);
1826
+ } catch (err) {
1827
+ logError(coordRoot, err, { phase: "artifact-auto-clean" });
1828
+ }
1829
+ // Effect: apply the same daily throttle to closed V3 epochs. The newest
1830
+ // recovery epochs remain protected by the archive retention policy.
1831
+ try {
1832
+ autoCleanEventV3Archives(coordRoot);
1833
+ } catch (err) {
1834
+ logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
1835
+ }
1836
+ // Plan one bounded, claim-first global slice from cached pressure only.
1837
+ // Existing owner janitors above remain the only active executors until
1838
+ // each provider receives a separate production activation.
1839
+ try {
1840
+ await runAutomaticMaintenancePass(
1841
+ createAutomaticMaintenanceComposition(coordRoot, {
1842
+ journal: () => {
1843
+ if (journalMaintenance) journalJanitor(coordRoot);
1844
+ },
1845
+ images: () => imageJanitor(coordRoot),
1846
+ artifacts: () => autoCleanArtifacts(coordRoot),
1847
+ }),
1848
+ );
1849
+ } catch (err) {
1850
+ logError(coordRoot, err, { phase: "storage-maintenance-auto" });
1851
+ }
1852
+ return 0;
1853
+ }
1854
+
1817
1855
  function scheduleRuntimeContextRetry(
1818
1856
  coordRoot: string,
1819
1857
  adapter: Adapter,
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { coordEnv } from "../../../lib/env.ts";
5
5
  import { resolveOwnerBySessionEnv } from "../../agents/coord-client.ts";
6
6
  import { readLiveCoordinationRow } from "../../agents/state/live-coordination-view.ts";
7
- import { checkPidToken } from "../../agents/state/proc-start.ts";
7
+ import { verifyProcessGeneration } from "../../agents/state/proc-start.ts";
8
8
 
9
9
  /**
10
10
  * Resolve the canonical `instance_id` for the current hook invocation.
@@ -98,7 +98,7 @@ export function resolveOwner(opts: {
98
98
  // A row whose start token disagrees with the process now holding this
99
99
  // pid is about a different, earlier process. Believing it hands this
100
100
  // session another agent's identity, so walk past it.
101
- const recycled = checkPidToken(pid, startToken || undefined) === "mismatch";
101
+ const recycled = verifyProcessGeneration(pid, startToken) === "different";
102
102
  if (owner && owner.length > 0 && !recycled) {
103
103
  return {
104
104
  instance_id: owner,
@@ -5,7 +5,7 @@ import { basename, join } from "node:path";
5
5
  import { performance } from "node:perf_hooks";
6
6
  import { readLiveCoordinationRows } from "../agents/state/live-coordination-view.ts";
7
7
  import { parsePidmapRow } from "../agents/state/pidmap.ts";
8
- import { checkPidToken } from "../agents/state/proc-start.ts";
8
+ import { verifyProcessGeneration } from "../agents/state/proc-start.ts";
9
9
  import {
10
10
  RESOURCE_SNAPSHOT_SCHEMA_VERSION,
11
11
  type ResourceProcessGroup,
@@ -386,7 +386,8 @@ function readOwnershipAnchors(
386
386
  if (!byPid.has(pid)) continue;
387
387
  try {
388
388
  const row = parsePidmapRow(readFileSync(join(directory, entry), "utf8"));
389
- if (!row.instanceId || checkPidToken(pid, row.startToken) === "mismatch") continue;
389
+ if (!row.instanceId || verifyProcessGeneration(pid, row.startToken) === "different")
390
+ continue;
390
391
  anchors.set(pid, { kind: "agent", id: row.instanceId, source: "pid-map" });
391
392
  } catch {
392
393
  // An unreadable or racing row cannot prove ownership.
@@ -1,9 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
- import { hostname } from "node:os";
3
2
  import {
4
- type ProcessGenerationVerdict,
5
- verifyProcessGeneration,
6
- } from "../agents/state/proc-start.ts";
3
+ observeServiceProcess,
4
+ type ServiceProcessDependencies,
5
+ } from "../agents/state/service-process.ts";
7
6
  import {
8
7
  RESOURCE_SERVICE_STATUS_SCHEMA_VERSION,
9
8
  type ResourceServiceStatus,
@@ -13,15 +12,10 @@ import { readResourceServiceRecord, resourcePaths } from "./storage.ts";
13
12
 
14
13
  const STALE_HEARTBEAT_MS = 15_000;
15
14
 
16
- export interface ResourceProcessDependencies {
17
- readToken?: (pid: number) => string | null;
18
- probePid?: (pid: number) => unknown;
19
- }
20
-
21
15
  export function readResourceServiceStatus(
22
16
  coordRoot: string,
23
17
  nowMs = Date.now(),
24
- dependencies: ResourceProcessDependencies = {},
18
+ dependencies: ServiceProcessDependencies = {},
25
19
  ): ResourceServiceStatus {
26
20
  const paths = resourcePaths(coordRoot);
27
21
  const record = readResourceServiceRecord(coordRoot);
@@ -37,13 +31,11 @@ export function readResourceServiceStatus(
37
31
  }
38
32
  const heartbeatAt = Date.parse(record.heartbeat_at);
39
33
  const stale = !Number.isFinite(heartbeatAt) || nowMs - heartbeatAt > STALE_HEARTBEAT_MS;
40
- const owner = resourceProcessLiveness(record, dependencies);
41
- const liveness =
42
- owner.generation === "same" && record.state === "stopped" ? "stopped" : owner.liveness;
34
+ const { generation, liveness } = observeServiceProcess(record, dependencies);
43
35
  return {
44
36
  running: liveness === "running" && !stale,
45
37
  stale,
46
- generation: owner.generation,
38
+ generation,
47
39
  liveness,
48
40
  record,
49
41
  status_path: paths.service,
@@ -62,24 +54,3 @@ function validRecord(
62
54
  typeof value.heartbeat_at === "string"
63
55
  );
64
56
  }
65
-
66
- export function resourceProcessLiveness(
67
- owner: { pid: number; host: string; start_token?: string },
68
- dependencies: ResourceProcessDependencies = {},
69
- ): { generation: ProcessGenerationVerdict; liveness: ResourceServiceStatus["liveness"] } {
70
- if (owner.host !== hostname()) return { generation: "unknown", liveness: "unknown" };
71
- try {
72
- (dependencies.probePid ?? ((pid) => process.kill(pid, 0)))(owner.pid);
73
- } catch (error) {
74
- return {
75
- generation: "unknown",
76
- liveness: (error as NodeJS.ErrnoException).code === "ESRCH" ? "stopped" : "unknown",
77
- };
78
- }
79
- const generation = verifyProcessGeneration(owner.pid, owner.start_token, dependencies.readToken);
80
- return {
81
- generation,
82
- liveness:
83
- generation === "same" ? "running" : generation === "different" ? "stopped" : "unknown",
84
- };
85
- }