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
@@ -6,16 +6,15 @@
6
6
  */
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { createHash, randomUUID } from "node:crypto";
9
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
- import { dirname, isAbsolute, join } from "node:path";
9
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
11
  import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
12
12
  import { resolveBinName } from "../config.js";
13
13
  import { peerInbox } from "../inbox/peer-messages.js";
14
14
  import { project } from "../inbox/service.js";
15
15
  import { stateFileMode } from "../storage/modes.js";
16
- import { acquireNoClobberLease } from "../workflow/workspaces/leases.js";
17
- import { observationsComparable } from "./observation.js";
18
- export { observationsComparable } from "./observation.js";
16
+ import { repoComparisonProblems } from "./observation.js";
17
+ export { repoComparisonProblems } from "./observation.js";
19
18
  export const CONTEXT_SCHEMA_VERSION = 1;
20
19
  export const MAX_CAPSULE_BYTES = 32 * 1024;
21
20
  const MAX_SNAPSHOT_PATHS = 15;
@@ -70,26 +69,24 @@ export function extractContextSample(payload, options) {
70
69
  }
71
70
  /** Persist a latest context sample. Identical measurements are a no-op. */
72
71
  export function recordContextSample(coordRoot, instanceId, sample) {
73
- return withContextMutation(coordRoot, sample.session_id, () => {
74
- const current = readContextState(coordRoot, sample.session_id);
75
- const state = current?.instance_id === instanceId
76
- ? current
77
- : {
78
- ...newState({ sessionId: sample.session_id, instanceId, now: sample.observed_at }),
79
- generation: current?.generation ?? 0,
80
- };
81
- const changed = !sameMeasurement(state.latest_context, sample);
82
- if (!changed)
83
- return { state, changed: false };
84
- const next = {
85
- ...state,
86
- instance_id: instanceId,
87
- latest_context: sample,
88
- updated_at: sample.observed_at,
72
+ const current = readContextState(coordRoot, sample.session_id);
73
+ const state = current?.instance_id === instanceId
74
+ ? current
75
+ : {
76
+ ...newState({ sessionId: sample.session_id, instanceId, now: sample.observed_at }),
77
+ generation: current?.generation ?? 0,
89
78
  };
90
- writeState(coordRoot, next);
91
- return { state: next, changed: true };
92
- });
79
+ const changed = !sameMeasurement(state.latest_context, sample);
80
+ if (!changed)
81
+ return { state, changed: false };
82
+ const next = {
83
+ ...state,
84
+ instance_id: instanceId,
85
+ latest_context: sample,
86
+ updated_at: sample.observed_at,
87
+ };
88
+ writeState(coordRoot, next);
89
+ return { state: next, changed: true };
93
90
  }
94
91
  /**
95
92
  * Snapshot external work state. Repeated PreCompact events reuse the existing
@@ -97,105 +94,100 @@ export function recordContextSample(coordRoot, instanceId, sample) {
97
94
  * create a new generation.
98
95
  */
99
96
  export function checkpointContext(coordRoot, input) {
100
- return withContextMutation(coordRoot, input.sessionId, () => {
101
- const now = new Date().toISOString();
102
- const previous = readContextState(coordRoot, input.sessionId);
103
- const current = previous?.instance_id === input.instanceId
104
- ? previous
105
- : {
106
- ...newState({ sessionId: input.sessionId, instanceId: input.instanceId, now }),
107
- generation: previous?.generation ?? 0,
97
+ const now = new Date().toISOString();
98
+ const previous = readContextState(coordRoot, input.sessionId);
99
+ const current = previous?.instance_id === input.instanceId
100
+ ? previous
101
+ : {
102
+ ...newState({ sessionId: input.sessionId, instanceId: input.instanceId, now }),
103
+ generation: previous?.generation ?? 0,
104
+ };
105
+ if (input.reason === "pre_compact" && current.phase === "checkpointed") {
106
+ const existing = readLatestCapsule(coordRoot, input.sessionId);
107
+ if (existing && capsuleMatchesState(current, existing, input) && current.latest_capsule) {
108
+ return {
109
+ capsule: existing,
110
+ path: join(coordRoot, ".harnery", current.latest_capsule),
111
+ state: current,
112
+ reused: true,
108
113
  };
109
- if (input.reason === "pre_compact" && current.phase === "checkpointed") {
110
- const existing = readLatestCapsule(coordRoot, input.sessionId);
111
- if (existing && capsuleMatchesState(current, existing, input) && current.latest_capsule) {
112
- return {
113
- capsule: existing,
114
- path: join(coordRoot, ".harnery", current.latest_capsule),
115
- state: current,
116
- reused: true,
117
- };
118
- }
119
114
  }
120
- const heartbeat = readHeartbeat(coordRoot, input.instanceId);
121
- const generation = current.generation + 1;
122
- const capsule = {
123
- schema_version: CONTEXT_SCHEMA_VERSION,
124
- capsule_id: randomUUID(),
125
- generation,
126
- created_at: now,
127
- reason: input.reason,
128
- session: {
129
- session_id: input.sessionId,
130
- instance_id: input.instanceId,
131
- adapter: input.adapter,
132
- model: input.model ?? current.latest_context?.model ?? heartbeat?.model,
133
- },
134
- context: current.latest_context,
135
- work: buildWorkSnapshot(heartbeat, input.continuationNote),
136
- repo: snapshotRepo(input.cwd),
137
- };
138
- const relativePath = capsuleRelativePath(input.sessionId, generation);
139
- const absolutePath = join(coordRoot, ".harnery", relativePath);
140
- writeBoundedJson(absolutePath, capsule, MAX_CAPSULE_BYTES);
141
- const next = {
142
- ...current,
115
+ }
116
+ const heartbeat = readHeartbeat(coordRoot, input.instanceId);
117
+ const generation = current.generation + 1;
118
+ const capsule = {
119
+ schema_version: CONTEXT_SCHEMA_VERSION,
120
+ capsule_id: randomUUID(),
121
+ generation,
122
+ created_at: now,
123
+ reason: input.reason,
124
+ session: {
125
+ session_id: input.sessionId,
143
126
  instance_id: input.instanceId,
144
- phase: "checkpointed",
145
- generation,
146
- latest_capsule: relativePath,
147
- updated_at: now,
148
- recovered_at: undefined,
149
- compaction_completed_at: undefined,
150
- degraded_reason: undefined,
151
- };
152
- writeState(coordRoot, next);
153
- return { capsule, path: absolutePath, state: next, reused: false };
154
- });
127
+ adapter: input.adapter,
128
+ model: input.model ?? current.latest_context?.model ?? heartbeat?.model,
129
+ },
130
+ context: current.latest_context,
131
+ work: buildWorkSnapshot(heartbeat, input.continuationNote),
132
+ repo: snapshotRepo(input.cwd),
133
+ };
134
+ const relativePath = capsuleRelativePath(input.sessionId, generation);
135
+ const absolutePath = join(coordRoot, ".harnery", relativePath);
136
+ writeBoundedJson(absolutePath, capsule, MAX_CAPSULE_BYTES);
137
+ const next = {
138
+ ...current,
139
+ instance_id: input.instanceId,
140
+ phase: "checkpointed",
141
+ generation,
142
+ latest_capsule: relativePath,
143
+ updated_at: now,
144
+ recovered_at: undefined,
145
+ compaction_completed_at: undefined,
146
+ degraded_reason: undefined,
147
+ };
148
+ writeState(coordRoot, next);
149
+ return { capsule, path: absolutePath, state: next, reused: false };
155
150
  }
156
151
  /** Prepare a briefing without claiming that a adapter has accepted it yet. */
157
152
  export function prepareContextRecovery(coordRoot, input) {
158
- return withContextMutation(coordRoot, input.sessionId, () => {
159
- const now = new Date().toISOString();
160
- const current = readContextState(coordRoot, input.sessionId) ??
161
- newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
162
- // A reused provider ID does not transfer the prior task's context.
163
- if (current.instance_id !== input.instanceId)
164
- return null;
165
- const capsule = readLatestCapsule(coordRoot, input.sessionId);
166
- if (!capsule || !capsuleMatchesState(current, capsule, input)) {
167
- const degraded = {
168
- ...current,
169
- instance_id: input.instanceId,
170
- phase: "degraded",
171
- updated_at: now,
172
- degraded_reason: capsule
173
- ? "continuity checkpoint does not match the requesting instance and state"
174
- : "post-compaction signal arrived without a continuity checkpoint",
175
- };
176
- writeState(coordRoot, degraded);
177
- return null;
178
- }
179
- const liveRepo = snapshotRepo(input.cwd);
180
- const briefing = [
181
- renderRecoveryBriefing(capsule, liveRepo),
182
- renderRecoveryInput(coordRoot, input.instanceId),
183
- ]
184
- .filter(Boolean)
185
- .join("\n\n");
186
- return {
187
- state: current,
188
- capsule,
189
- briefing,
190
- checkpoint: Object.freeze({
191
- session_id: input.sessionId,
192
- instance_id: input.instanceId,
193
- generation: capsule.generation,
194
- capsule_id: capsule.capsule_id,
195
- capsule_sha256: capsuleDigest(capsule),
196
- }),
153
+ const now = new Date().toISOString();
154
+ const current = readContextState(coordRoot, input.sessionId) ??
155
+ newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
156
+ // A reused provider ID does not transfer the prior task's context.
157
+ if (current.instance_id !== input.instanceId)
158
+ return null;
159
+ const capsule = readLatestCapsule(coordRoot, input.sessionId);
160
+ if (!capsule || !capsuleMatchesState(current, capsule, input)) {
161
+ const degraded = {
162
+ ...current,
163
+ instance_id: input.instanceId,
164
+ phase: "degraded",
165
+ updated_at: now,
166
+ degraded_reason: capsule
167
+ ? "continuity checkpoint does not match the requesting instance and state"
168
+ : "post-compaction signal arrived without a continuity checkpoint",
197
169
  };
198
- });
170
+ writeState(coordRoot, degraded);
171
+ return null;
172
+ }
173
+ const liveRepo = snapshotRepo(input.cwd);
174
+ const briefing = [
175
+ renderRecoveryBriefing(capsule, liveRepo),
176
+ renderRecoveryInput(coordRoot, input.instanceId),
177
+ ]
178
+ .filter(Boolean)
179
+ .join("\n\n");
180
+ return {
181
+ state: current,
182
+ capsule,
183
+ briefing,
184
+ checkpoint: Object.freeze({
185
+ session_id: input.sessionId,
186
+ instance_id: input.instanceId,
187
+ generation: capsule.generation,
188
+ capsule_id: capsule.capsule_id,
189
+ }),
190
+ };
199
191
  }
200
192
  /** Recovery lists input without surfacing it again or changing its receipts. */
201
193
  function renderRecoveryInput(coordRoot, instanceId) {
@@ -244,66 +236,61 @@ function inputLine(messageId, status, body) {
244
236
  }
245
237
  /**
246
238
  * Advance continuity only after the adapter accepted the prepared briefing.
247
- * Missing, altered, foreign or superseded checkpoints refuse without writing.
248
- * Repeating the same accepted checkpoint is an exact no-op.
239
+ * A foreign or superseded checkpoint refuses without writing, and repeating
240
+ * the same accepted checkpoint is an exact no-op. The write is a
241
+ * compare-and-write: if the state changed after it was read, for example
242
+ * because a newer checkpoint landed, completion refuses instead of
243
+ * overwriting it.
249
244
  */
250
245
  export function completeContextRecovery(coordRoot, input) {
251
- return withContextMutation(coordRoot, input.sessionId, () => {
252
- const current = readContextState(coordRoot, input.sessionId);
253
- const checkpoint = input.checkpoint;
254
- if (!current ||
255
- !checkpoint ||
256
- checkpoint.session_id !== input.sessionId ||
257
- checkpoint.instance_id !== input.instanceId ||
258
- !Number.isSafeInteger(checkpoint.generation) ||
259
- checkpoint.generation < 1 ||
260
- typeof checkpoint.capsule_id !== "string" ||
261
- !checkpoint.capsule_id ||
262
- !/^[a-f0-9]{64}$/.test(checkpoint.capsule_sha256) ||
263
- (current.phase !== "checkpointed" && current.phase !== "recovered"))
264
- return null;
265
- const capsule = readLatestCapsule(coordRoot, input.sessionId);
266
- if (!capsule ||
267
- !capsuleMatchesState(current, capsule, input) ||
268
- checkpoint.generation !== capsule.generation ||
269
- checkpoint.capsule_id !== capsule.capsule_id ||
270
- checkpoint.capsule_sha256 !== capsuleDigest(capsule))
271
- return null;
272
- if (current.phase === "recovered")
273
- return current;
274
- const now = new Date().toISOString();
275
- const recovered = {
276
- ...current,
277
- phase: "recovered",
278
- updated_at: now,
279
- recovered_at: now,
280
- degraded_reason: undefined,
281
- };
282
- writeState(coordRoot, recovered);
283
- return recovered;
284
- });
246
+ const path = statePath(coordRoot, input.sessionId);
247
+ const observed = readText(path);
248
+ if (observed === null)
249
+ return null;
250
+ const current = parseJson(observed);
251
+ const { checkpoint } = input;
252
+ if (!current ||
253
+ checkpoint.session_id !== input.sessionId ||
254
+ checkpoint.instance_id !== input.instanceId ||
255
+ checkpoint.generation !== current.generation ||
256
+ (current.phase !== "checkpointed" && current.phase !== "recovered"))
257
+ return null;
258
+ const capsule = readLatestCapsule(coordRoot, input.sessionId);
259
+ if (!capsule ||
260
+ !capsuleMatchesState(current, capsule, input) ||
261
+ capsule.capsule_id !== checkpoint.capsule_id)
262
+ return null;
263
+ if (current.phase === "recovered")
264
+ return current;
265
+ const now = new Date().toISOString();
266
+ const recovered = {
267
+ ...current,
268
+ phase: "recovered",
269
+ updated_at: now,
270
+ recovered_at: now,
271
+ degraded_reason: undefined,
272
+ };
273
+ return writeBoundedJson(path, recovered, MAX_CAPSULE_BYTES, observed) ? recovered : null;
285
274
  }
286
275
  /** Record that the adapter, rather than Harnery, reported compaction complete. */
287
276
  export function markContextCompactionCompleted(coordRoot, input) {
288
- return withContextMutation(coordRoot, input.sessionId, () => {
289
- const now = input.observedAt ?? new Date().toISOString();
290
- const current = readContextState(coordRoot, input.sessionId) ??
291
- newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
292
- if (current.instance_id !== input.instanceId)
293
- return current;
294
- const next = {
295
- ...current,
296
- instance_id: input.instanceId,
297
- phase: current.latest_capsule ? current.phase : "degraded",
298
- compaction_completed_at: now,
299
- updated_at: now,
300
- degraded_reason: current.latest_capsule
301
- ? current.degraded_reason
302
- : "post-compaction signal arrived without a continuity checkpoint",
303
- };
304
- writeState(coordRoot, next);
305
- return next;
306
- });
277
+ const now = input.observedAt ?? new Date().toISOString();
278
+ const current = readContextState(coordRoot, input.sessionId) ??
279
+ newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
280
+ if (current.instance_id !== input.instanceId)
281
+ return current;
282
+ const next = {
283
+ ...current,
284
+ instance_id: input.instanceId,
285
+ phase: current.latest_capsule ? current.phase : "degraded",
286
+ compaction_completed_at: now,
287
+ updated_at: now,
288
+ degraded_reason: current.latest_capsule
289
+ ? current.degraded_reason
290
+ : "post-compaction signal arrived without a continuity checkpoint",
291
+ };
292
+ writeState(coordRoot, next);
293
+ return next;
307
294
  }
308
295
  /** Convenience API for callers whose injection is synchronous and guaranteed. */
309
296
  export function recoverContext(coordRoot, input) {
@@ -337,25 +324,9 @@ export function readContextState(coordRoot, sessionId) {
337
324
  }
338
325
  export function readLatestCapsule(coordRoot, sessionId) {
339
326
  const state = readContextState(coordRoot, sessionId);
340
- if (typeof state?.latest_capsule !== "string" || !state.latest_capsule)
341
- return null;
342
- const capsule = readJson(join(coordRoot, ".harnery", state.latest_capsule));
343
- if (!capsule ||
344
- !record(capsule.repo) ||
345
- !record(capsule.work) ||
346
- typeof capsule.repo.cwd !== "string" ||
347
- !Array.isArray(capsule.repo.dirty_paths) ||
348
- !capsule.repo.dirty_paths.every((path) => typeof path === "string") ||
349
- !Array.isArray(capsule.work.files_held) ||
350
- !capsule.work.files_held.every((path) => typeof path === "string")) {
327
+ if (!state?.latest_capsule)
351
328
  return null;
352
- }
353
- capsule.repo.observation = repoObservation(capsule.repo.observation);
354
- capsule.repo.observation_detail = clampString(string(capsule.repo.observation_detail), MAX_OBSERVATION_DETAIL_CHARS);
355
- capsule.repo.branch = string(capsule.repo.branch)?.trim() || undefined;
356
- capsule.repo.head = string(capsule.repo.head)?.trim() || undefined;
357
- capsule.repo.root = string(capsule.repo.root)?.trim() || undefined;
358
- return capsule;
329
+ return readJson(join(coordRoot, ".harnery", state.latest_capsule));
359
330
  }
360
331
  export function renderRecoveryBriefing(capsule, liveRepo) {
361
332
  const lines = [
@@ -369,44 +340,20 @@ export function renderRecoveryBriefing(capsule, liveRepo) {
369
340
  if (capsule.work.files_held.length > 0) {
370
341
  lines.push(`Files held: ${capsule.work.files_held.join(", ")}`);
371
342
  }
372
- const unknown = [];
373
- if (!observationsComparable(capsule.repo, liveRepo)) {
374
- for (const [label, repo] of [
375
- ["checkpoint", capsule.repo],
376
- ["current repository", liveRepo],
377
- ]) {
378
- if (repo.observation !== "ok") {
379
- const detail = clampString(string(repo.observation_detail), MAX_OBSERVATION_DETAIL_CHARS);
380
- unknown.push(`${label}: ${repoObservation(repo.observation)}${detail ? ` (${detail})` : ""}`);
381
- }
382
- }
383
- }
384
- if (capsule.repo.dirty_paths_truncated || liveRepo.dirty_paths_truncated) {
385
- unknown.push("dirty path list is truncated");
386
- }
387
- if (!capsule.repo.root ||
388
- !liveRepo.root ||
389
- !isAbsolute(capsule.repo.root) ||
390
- !isAbsolute(liveRepo.root)) {
391
- unknown.push("repository root is unknown");
392
- }
393
- else if (capsule.repo.root !== liveRepo.root) {
394
- unknown.push("repository root differs from the checkpoint");
395
- }
396
- if (!capsule.repo.branch?.trim() || !liveRepo.branch?.trim())
397
- unknown.push("branch is unknown");
398
- if (!capsule.repo.head?.trim() || !liveRepo.head?.trim())
399
- unknown.push("HEAD is unknown");
343
+ const unknown = repoComparisonProblems(capsule.repo, liveRepo, [
344
+ "checkpoint",
345
+ "current repository",
346
+ ]);
400
347
  if (unknown.length > 0) {
401
348
  lines.push(`Repository could not be verified against the checkpoint: ${unknown.join("; ")}. Re-check before editing.`);
402
349
  return lines.join("\n");
403
350
  }
404
351
  const drift = [];
405
- if (capsule.repo.branch && liveRepo.branch && capsule.repo.branch !== liveRepo.branch) {
352
+ if (capsule.repo.branch !== liveRepo.branch) {
406
353
  drift.push(`branch changed ${capsule.repo.branch} -> ${liveRepo.branch}`);
407
354
  }
408
- if (capsule.repo.head && liveRepo.head && capsule.repo.head !== liveRepo.head) {
409
- drift.push(`HEAD changed ${capsule.repo.head.slice(0, 8)} -> ${liveRepo.head.slice(0, 8)}`);
355
+ if (capsule.repo.head !== liveRepo.head) {
356
+ drift.push(`HEAD changed ${capsule.repo.head?.slice(0, 8)} -> ${liveRepo.head?.slice(0, 8)}`);
410
357
  }
411
358
  const beforeDirty = new Set(capsule.repo.dirty_paths);
412
359
  const afterDirty = new Set(liveRepo.dirty_paths);
@@ -510,39 +457,13 @@ function newState(input) {
510
457
  updated_at: input.now,
511
458
  };
512
459
  }
460
+ /** The capsule is the one the state names, written for the requesting task. */
513
461
  function capsuleMatchesState(state, capsule, input) {
514
- return (state.session_id === input.sessionId &&
515
- state.instance_id === input.instanceId &&
516
- capsule.session?.session_id === input.sessionId &&
517
- capsule.session?.instance_id === input.instanceId &&
518
- capsule.schema_version === CONTEXT_SCHEMA_VERSION &&
519
- Number.isSafeInteger(capsule.generation) &&
520
- capsule.generation > 0 &&
462
+ return (state.instance_id === input.instanceId &&
463
+ capsule.session.instance_id === input.instanceId &&
521
464
  capsule.generation === state.generation &&
522
- typeof capsule.capsule_id === "string" &&
523
- capsule.capsule_id.length > 0 &&
524
465
  state.latest_capsule === capsuleRelativePath(input.sessionId, capsule.generation));
525
466
  }
526
- function capsuleDigest(capsule) {
527
- return createHash("sha256").update(JSON.stringify(capsule)).digest("hex");
528
- }
529
- /** Every state writer shares the same crash-recoverable session reservation. */
530
- function withContextMutation(coordRoot, sessionId, action) {
531
- const authority = createHash("sha256").update(sessionId).digest("hex");
532
- const lease = acquireNoClobberLease({
533
- path: join(contextDir(coordRoot, sessionId), "mutation.lease"),
534
- scope: "context-continuity",
535
- authoritySha256: authority,
536
- staleAfterMs: 30_000,
537
- validateStaleOwner: (owner) => owner.authority_sha256 === authority,
538
- });
539
- try {
540
- return action();
541
- }
542
- finally {
543
- lease.release();
544
- }
545
- }
546
467
  function writeState(coordRoot, state) {
547
468
  writeBoundedJson(statePath(coordRoot, state.session_id), state, MAX_CAPSULE_BYTES);
548
469
  }
@@ -558,7 +479,12 @@ function contextDir(coordRoot, sessionId) {
558
479
  function sessionKey(sessionId) {
559
480
  return createHash("sha256").update(sessionId).digest("hex").slice(0, 24);
560
481
  }
561
- function writeBoundedJson(path, value, maxBytes) {
482
+ /**
483
+ * Atomically replace a record. With `expected`, the replacement happens only
484
+ * while the file still holds exactly those bytes, so a concurrent writer's
485
+ * newer record is never overwritten by a decision made on older state.
486
+ */
487
+ function writeBoundedJson(path, value, maxBytes, expected) {
562
488
  const text = `${JSON.stringify(value, null, 2)}\n`;
563
489
  const bytes = Buffer.byteLength(text, "utf8");
564
490
  if (bytes > maxBytes) {
@@ -567,18 +493,33 @@ function writeBoundedJson(path, value, maxBytes) {
567
493
  mkdirSync(dirname(path), { recursive: true });
568
494
  const temp = `${path}.tmp.${process.pid}.${randomUUID()}`;
569
495
  writeFileSync(temp, text, { encoding: "utf8", mode: stateFileMode() });
496
+ if (expected !== undefined && readText(path) !== expected) {
497
+ unlinkSync(temp);
498
+ return false;
499
+ }
570
500
  renameSync(temp, path);
501
+ return true;
571
502
  }
572
- function readJson(path) {
573
- if (!existsSync(path))
503
+ function readText(path) {
504
+ try {
505
+ return readFileSync(path, "utf8");
506
+ }
507
+ catch {
574
508
  return null;
509
+ }
510
+ }
511
+ function parseJson(text) {
575
512
  try {
576
- return JSON.parse(readFileSync(path, "utf8"));
513
+ return JSON.parse(text);
577
514
  }
578
515
  catch {
579
516
  return null;
580
517
  }
581
518
  }
519
+ function readJson(path) {
520
+ const text = readText(path);
521
+ return text === null ? null : parseJson(text);
522
+ }
582
523
  function git(cwd, args) {
583
524
  try {
584
525
  const result = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 2_000 });
@@ -601,9 +542,6 @@ function git(cwd, args) {
601
542
  return { ok: false, reason: "spawn_error" };
602
543
  }
603
544
  }
604
- function repoObservation(value) {
605
- return value === "ok" || value === "partial" ? value : "unavailable";
606
- }
607
545
  function sameMeasurement(a, b) {
608
546
  if (!a)
609
547
  return false;
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Repository observation contract shared by context recovery and workflow proof.
3
3
  *
4
- * This module has no runtime imports so read-only consumers (the Codec
5
- * dashboard, proof readers) can use it without reaching the Git-running
6
- * snapshot code in ./index.ts.
4
+ * This module runs no Git and touches no files, so read-only consumers (the
5
+ * Codec dashboard, proof readers) can use it without reaching the
6
+ * Git-running snapshot code in ./index.ts.
7
7
  */
8
8
  export type RepoObservation = "ok" | "unavailable" | "partial";
9
9
  export interface RepoSnapshot {
@@ -17,10 +17,10 @@ export interface RepoSnapshot {
17
17
  dirty_paths: string[];
18
18
  dirty_paths_truncated?: boolean;
19
19
  }
20
- /** Only explicitly successful observations can establish repository drift. */
21
- export declare function observationsComparable(a: {
22
- observation?: string;
23
- }, b: {
24
- observation?: string;
25
- }): boolean;
20
+ /**
21
+ * Every reason two snapshots cannot establish repository drift. Only an empty
22
+ * list lets a caller compare branch, HEAD and dirty paths; anything else
23
+ * leaves the whole comparison unknown.
24
+ */
25
+ export declare function repoComparisonProblems(a: RepoSnapshot, b: RepoSnapshot, labels: readonly [string, string]): string[];
26
26
  //# sourceMappingURL=observation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../../../src/core/context/observation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,aAAa,GAAG,SAAS,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,eAAe,CAAC;IAC7B,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,8EAA8E;AAC9E,wBAAgB,sBAAsB,CACpC,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,EAC3B,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1B,OAAO,CAET"}
1
+ {"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../../../src/core/context/observation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,aAAa,GAAG,SAAS,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,eAAe,CAAC;IAC7B,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,CAAC,EAAE,YAAY,EACf,CAAC,EAAE,YAAY,EACf,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,MAAM,EAAE,CA0BV"}
@@ -1,11 +1,42 @@
1
1
  /**
2
2
  * Repository observation contract shared by context recovery and workflow proof.
3
3
  *
4
- * This module has no runtime imports so read-only consumers (the Codec
5
- * dashboard, proof readers) can use it without reaching the Git-running
6
- * snapshot code in ./index.ts.
4
+ * This module runs no Git and touches no files, so read-only consumers (the
5
+ * Codec dashboard, proof readers) can use it without reaching the
6
+ * Git-running snapshot code in ./index.ts.
7
7
  */
8
- /** Only explicitly successful observations can establish repository drift. */
9
- export function observationsComparable(a, b) {
10
- return a.observation === "ok" && b.observation === "ok";
8
+ import { isAbsolute } from "node:path";
9
+ /**
10
+ * Every reason two snapshots cannot establish repository drift. Only an empty
11
+ * list lets a caller compare branch, HEAD and dirty paths; anything else
12
+ * leaves the whole comparison unknown.
13
+ */
14
+ export function repoComparisonProblems(a, b, labels) {
15
+ const sides = [
16
+ [labels[0], a],
17
+ [labels[1], b],
18
+ ];
19
+ const failed = sides.flatMap(([label, repo]) => repo.observation === "ok"
20
+ ? []
21
+ : [
22
+ // Snapshots recorded before observations existed carry none.
23
+ `${label} snapshot is ${repo.observation ?? "unavailable"}${repo.observation_detail ? ` (${repo.observation_detail})` : ""}`,
24
+ ]);
25
+ if (failed.length > 0)
26
+ return failed;
27
+ const problems = [];
28
+ if (a.dirty_paths_truncated || b.dirty_paths_truncated) {
29
+ problems.push("dirty path list is truncated");
30
+ }
31
+ if (!a.root || !b.root || !isAbsolute(a.root) || !isAbsolute(b.root)) {
32
+ problems.push("repository root is unknown");
33
+ }
34
+ else if (a.root !== b.root) {
35
+ problems.push("repository roots differ");
36
+ }
37
+ if (!a.branch?.trim() || !b.branch?.trim())
38
+ problems.push("branch is unknown");
39
+ if (!a.head?.trim() || !b.head?.trim())
40
+ problems.push("HEAD is unknown");
41
+ return problems;
11
42
  }