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.
- package/dist/commander.js +0 -1
- package/dist/commands/files.d.ts +2 -2
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +4 -7
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +17 -11
- package/dist/commands/tunnel.d.ts +0 -15
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +153 -453
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +6 -45
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +0 -18
- package/dist/core/agents/live-authority-v3.d.ts +0 -1
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +0 -11
- package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +7 -28
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +14 -25
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +0 -5
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +2 -7
- package/dist/core/agents/state/proc-start.d.ts +20 -31
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +37 -70
- package/dist/core/agents/state/service-process.d.ts +61 -0
- package/dist/core/agents/state/service-process.d.ts.map +1 -0
- package/dist/core/agents/state/service-process.js +143 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +70 -180
- package/dist/core/artifacts/activity.d.ts +4 -1
- package/dist/core/artifacts/activity.d.ts.map +1 -1
- package/dist/core/artifacts/activity.js +8 -2
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +1 -2
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +38 -12
- package/dist/core/context/index.d.ts +6 -4
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +194 -256
- package/dist/core/context/observation.d.ts +9 -9
- package/dist/core/context/observation.d.ts.map +1 -1
- package/dist/core/context/observation.js +37 -6
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +0 -29
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +64 -77
- package/dist/core/events/v3/runtime-build.d.ts +14 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +44 -0
- package/dist/core/governor/index.d.ts +2 -1
- package/dist/core/governor/index.d.ts.map +1 -1
- package/dist/core/governor/index.js +2 -1
- package/dist/core/governor/service-read.d.ts +3 -10
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +10 -94
- package/dist/core/governor/service.d.ts +3 -7
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +17 -182
- package/dist/core/hooks/cli.js +78 -48
- package/dist/core/hooks/resolve/owner.js +2 -2
- package/dist/core/resources/sampler.js +2 -2
- package/dist/core/resources/service-status.d.ts +2 -14
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +3 -23
- package/dist/core/resources/service.d.ts +3 -7
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +12 -101
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +3 -2
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +9 -2
- package/dist/core/servers/index.d.ts +11 -10
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +82 -192
- package/dist/core/servers/tunnels.d.ts +3 -5
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +2 -45
- package/dist/core/storage/durable-history.js +56 -22
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +73 -17
- package/dist/core/supervisor/service.d.ts +4 -7
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +23 -122
- package/dist/core/supervisor/services.js +2 -2
- package/dist/core/supervisor/status.d.ts +3 -17
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +7 -70
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +23 -66
- package/dist/core/workflow/index.d.ts +3 -4
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -2
- package/dist/core/workflow/live-session-v3.d.ts +0 -6
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +5 -126
- package/dist/core/workflow/proof.d.ts +4 -4
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +34 -86
- package/dist/core/workflow/resume-lock.d.ts +17 -0
- package/dist/core/workflow/resume-lock.d.ts.map +1 -0
- package/dist/core/workflow/resume-lock.js +158 -0
- package/dist/core/workflow/run-state.d.ts +0 -21
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -174
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +0 -19
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +0 -13
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +0 -19
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +0 -14
- package/dist/core/workflow/types.d.ts +11 -54
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.d.ts +4 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -1
- package/dist/core/workflow/workspaces/local-git.js +91 -210
- package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/ownership.js +3 -130
- package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +0 -4
- package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +1 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +1 -4
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +0 -4
- package/dist/lib/tunnel/state.d.ts +6 -13
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +12 -58
- package/package.json +1 -6
- package/src/commander.ts +0 -10
- package/src/commands/files.ts +5 -13
- package/src/commands/servers.ts +23 -12
- package/src/commands/tunnel.ts +166 -533
- package/src/commands/workflow.ts +8 -58
- package/src/core/agents/coord-client.ts +0 -20
- package/src/core/agents/live-authority-v3.ts +0 -17
- package/src/core/agents/live-lifecycle-v3.ts +14 -52
- package/src/core/agents/state/heartbeat-writer.ts +12 -29
- package/src/core/agents/state/live-coordination-writer.ts +0 -29
- package/src/core/agents/state/pidmap.ts +2 -15
- package/src/core/agents/state/proc-start.ts +41 -69
- package/src/core/agents/state/service-process.ts +217 -0
- package/src/core/agents/state/stale-sweep.ts +82 -247
- package/src/core/artifacts/activity.ts +9 -2
- package/src/core/artifacts/delivery-card.ts +1 -7
- package/src/core/artifacts/index.ts +45 -12
- package/src/core/context/index.ts +207 -272
- package/src/core/context/observation.ts +40 -9
- package/src/core/events/v3/authority-outbox.ts +0 -56
- package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
- package/src/core/events/v3/producers/recorder.ts +68 -94
- package/src/core/events/v3/runtime-build.ts +62 -0
- package/src/core/governor/index.ts +1 -2
- package/src/core/governor/service-read.ts +15 -114
- package/src/core/governor/service.ts +36 -220
- package/src/core/hooks/cli.ts +85 -47
- package/src/core/hooks/resolve/owner.ts +2 -2
- package/src/core/resources/sampler.ts +3 -2
- package/src/core/resources/service-status.ts +6 -35
- package/src/core/resources/service.ts +23 -133
- package/src/core/resources/status.ts +3 -2
- package/src/core/semantic/service.ts +8 -2
- package/src/core/servers/index.ts +86 -228
- package/src/core/servers/tunnels.ts +3 -55
- package/src/core/storage/durable-history.ts +52 -20
- package/src/core/storage/segments.ts +79 -17
- package/src/core/supervisor/service.ts +33 -155
- package/src/core/supervisor/services.ts +2 -2
- package/src/core/supervisor/status.ts +10 -85
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +22 -74
- package/src/core/workflow/index.ts +2 -13
- package/src/core/workflow/live-session-v3.ts +5 -146
- package/src/core/workflow/proof.ts +40 -104
- package/src/core/workflow/resume-lock.ts +179 -0
- package/src/core/workflow/run-state.ts +3 -207
- package/src/core/workflow/spawn-claude.ts +0 -18
- package/src/core/workflow/spawn-codex.ts +0 -13
- package/src/core/workflow/spawn-cursor.ts +0 -18
- package/src/core/workflow/spawn-opencode.ts +0 -14
- package/src/core/workflow/types.ts +11 -41
- package/src/core/workflow/workspaces/leases.ts +1 -1
- package/src/core/workflow/workspaces/local-git.ts +120 -239
- package/src/core/workflow/workspaces/ownership.ts +3 -158
- package/src/core/workflow/workspaces/reclaim.ts +3 -9
- package/src/core/workflow/workspaces/recovery.ts +1 -1
- package/src/core/workflow/workspaces/state.ts +1 -5
- package/src/lib/identities/assume.ts +0 -4
- package/src/lib/tunnel/state.ts +15 -70
- package/dist/commands/turn-rollback.d.ts +0 -5
- package/dist/commands/turn-rollback.d.ts.map +0 -1
- package/dist/commands/turn-rollback.js +0 -76
- package/dist/core/agents/state/process-handle.d.ts +0 -26
- package/dist/core/agents/state/process-handle.d.ts.map +0 -1
- package/dist/core/agents/state/process-handle.js +0 -121
- package/dist/core/agents/state/unchanged-file.d.ts +0 -44
- package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
- package/dist/core/agents/state/unchanged-file.js +0 -390
- package/dist/core/events/v3/generation-custody.d.ts +0 -5
- package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
- package/dist/core/events/v3/generation-custody.js +0 -50
- package/dist/core/turn-rollback/content.d.ts +0 -53
- package/dist/core/turn-rollback/content.d.ts.map +0 -1
- package/dist/core/turn-rollback/content.js +0 -396
- package/dist/core/turn-rollback/index.d.ts +0 -72
- package/dist/core/turn-rollback/index.d.ts.map +0 -1
- package/dist/core/turn-rollback/index.js +0 -443
- package/dist/core/workflow/failure-evidence.d.ts +0 -16
- package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
- package/dist/core/workflow/failure-evidence.js +0 -114
- package/dist/core/workflow/recovery-audit.d.ts +0 -30
- package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
- package/dist/core/workflow/recovery-audit.js +0 -116
- package/src/commands/turn-rollback.ts +0 -111
- package/src/core/agents/state/process-handle.ts +0 -165
- package/src/core/agents/state/unchanged-file.ts +0 -456
- package/src/core/events/v3/generation-custody.ts +0 -59
- package/src/core/turn-rollback/content.ts +0 -499
- package/src/core/turn-rollback/index.ts +0 -580
- package/src/core/workflow/failure-evidence.ts +0 -141
- package/src/core/workflow/recovery-audit.ts +0 -181
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
-
import {
|
|
11
|
-
import { dirname,
|
|
10
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
12
|
import type { Adapter } from "../adapter.ts";
|
|
13
13
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
|
|
14
14
|
import { resolveBinName } from "../config.ts";
|
|
@@ -16,12 +16,11 @@ import type { HarneryInboxMessageRecordV1 } from "../inbox/contract.ts";
|
|
|
16
16
|
import { peerInbox } from "../inbox/peer-messages.ts";
|
|
17
17
|
import { project } from "../inbox/service.ts";
|
|
18
18
|
import { stateFileMode } from "../storage/modes.ts";
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
import { observationsComparable } from "./observation.ts";
|
|
19
|
+
import type { RepoSnapshot } from "./observation.ts";
|
|
20
|
+
import { repoComparisonProblems } from "./observation.ts";
|
|
22
21
|
|
|
23
22
|
export type { RepoObservation, RepoSnapshot } from "./observation.ts";
|
|
24
|
-
export {
|
|
23
|
+
export { repoComparisonProblems } from "./observation.ts";
|
|
25
24
|
|
|
26
25
|
export const CONTEXT_SCHEMA_VERSION = 1 as const;
|
|
27
26
|
export const MAX_CAPSULE_BYTES = 32 * 1024;
|
|
@@ -125,7 +124,6 @@ export interface PreparedContextCheckpoint {
|
|
|
125
124
|
instance_id: string;
|
|
126
125
|
generation: number;
|
|
127
126
|
capsule_id: string;
|
|
128
|
-
capsule_sha256: string;
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
export interface PreparedContextRecovery {
|
|
@@ -206,26 +204,24 @@ export function recordContextSample(
|
|
|
206
204
|
instanceId: string,
|
|
207
205
|
sample: ContextSample,
|
|
208
206
|
): { state: ContextContinuityState; changed: boolean } {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
current
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return { state: next, changed: true };
|
|
228
|
-
});
|
|
207
|
+
const current = readContextState(coordRoot, sample.session_id);
|
|
208
|
+
const state =
|
|
209
|
+
current?.instance_id === instanceId
|
|
210
|
+
? current
|
|
211
|
+
: {
|
|
212
|
+
...newState({ sessionId: sample.session_id, instanceId, now: sample.observed_at }),
|
|
213
|
+
generation: current?.generation ?? 0,
|
|
214
|
+
};
|
|
215
|
+
const changed = !sameMeasurement(state.latest_context, sample);
|
|
216
|
+
if (!changed) return { state, changed: false };
|
|
217
|
+
const next: ContextContinuityState = {
|
|
218
|
+
...state,
|
|
219
|
+
instance_id: instanceId,
|
|
220
|
+
latest_context: sample,
|
|
221
|
+
updated_at: sample.observed_at,
|
|
222
|
+
};
|
|
223
|
+
writeState(coordRoot, next);
|
|
224
|
+
return { state: next, changed: true };
|
|
229
225
|
}
|
|
230
226
|
|
|
231
227
|
/**
|
|
@@ -237,64 +233,62 @@ export function checkpointContext(
|
|
|
237
233
|
coordRoot: string,
|
|
238
234
|
input: CheckpointContextInput,
|
|
239
235
|
): CheckpointContextResult {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
previous
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
generation: previous?.generation ?? 0,
|
|
249
|
-
};
|
|
250
|
-
if (input.reason === "pre_compact" && current.phase === "checkpointed") {
|
|
251
|
-
const existing = readLatestCapsule(coordRoot, input.sessionId);
|
|
252
|
-
if (existing && capsuleMatchesState(current, existing, input) && current.latest_capsule) {
|
|
253
|
-
return {
|
|
254
|
-
capsule: existing,
|
|
255
|
-
path: join(coordRoot, ".harnery", current.latest_capsule),
|
|
256
|
-
state: current,
|
|
257
|
-
reused: true,
|
|
236
|
+
const now = new Date().toISOString();
|
|
237
|
+
const previous = readContextState(coordRoot, input.sessionId);
|
|
238
|
+
const current =
|
|
239
|
+
previous?.instance_id === input.instanceId
|
|
240
|
+
? previous
|
|
241
|
+
: {
|
|
242
|
+
...newState({ sessionId: input.sessionId, instanceId: input.instanceId, now }),
|
|
243
|
+
generation: previous?.generation ?? 0,
|
|
258
244
|
};
|
|
259
|
-
|
|
245
|
+
if (input.reason === "pre_compact" && current.phase === "checkpointed") {
|
|
246
|
+
const existing = readLatestCapsule(coordRoot, input.sessionId);
|
|
247
|
+
if (existing && capsuleMatchesState(current, existing, input) && current.latest_capsule) {
|
|
248
|
+
return {
|
|
249
|
+
capsule: existing,
|
|
250
|
+
path: join(coordRoot, ".harnery", current.latest_capsule),
|
|
251
|
+
state: current,
|
|
252
|
+
reused: true,
|
|
253
|
+
};
|
|
260
254
|
}
|
|
255
|
+
}
|
|
261
256
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
instance_id: input.instanceId,
|
|
273
|
-
adapter: input.adapter,
|
|
274
|
-
model: input.model ?? current.latest_context?.model ?? heartbeat?.model,
|
|
275
|
-
},
|
|
276
|
-
context: current.latest_context,
|
|
277
|
-
work: buildWorkSnapshot(heartbeat, input.continuationNote),
|
|
278
|
-
repo: snapshotRepo(input.cwd),
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
const relativePath = capsuleRelativePath(input.sessionId, generation);
|
|
282
|
-
const absolutePath = join(coordRoot, ".harnery", relativePath);
|
|
283
|
-
writeBoundedJson(absolutePath, capsule, MAX_CAPSULE_BYTES);
|
|
284
|
-
const next: ContextContinuityState = {
|
|
285
|
-
...current,
|
|
257
|
+
const heartbeat = readHeartbeat(coordRoot, input.instanceId);
|
|
258
|
+
const generation = current.generation + 1;
|
|
259
|
+
const capsule: ContinuityCapsule = {
|
|
260
|
+
schema_version: CONTEXT_SCHEMA_VERSION,
|
|
261
|
+
capsule_id: randomUUID(),
|
|
262
|
+
generation,
|
|
263
|
+
created_at: now,
|
|
264
|
+
reason: input.reason,
|
|
265
|
+
session: {
|
|
266
|
+
session_id: input.sessionId,
|
|
286
267
|
instance_id: input.instanceId,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
268
|
+
adapter: input.adapter,
|
|
269
|
+
model: input.model ?? current.latest_context?.model ?? heartbeat?.model,
|
|
270
|
+
},
|
|
271
|
+
context: current.latest_context,
|
|
272
|
+
work: buildWorkSnapshot(heartbeat, input.continuationNote),
|
|
273
|
+
repo: snapshotRepo(input.cwd),
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const relativePath = capsuleRelativePath(input.sessionId, generation);
|
|
277
|
+
const absolutePath = join(coordRoot, ".harnery", relativePath);
|
|
278
|
+
writeBoundedJson(absolutePath, capsule, MAX_CAPSULE_BYTES);
|
|
279
|
+
const next: ContextContinuityState = {
|
|
280
|
+
...current,
|
|
281
|
+
instance_id: input.instanceId,
|
|
282
|
+
phase: "checkpointed",
|
|
283
|
+
generation,
|
|
284
|
+
latest_capsule: relativePath,
|
|
285
|
+
updated_at: now,
|
|
286
|
+
recovered_at: undefined,
|
|
287
|
+
compaction_completed_at: undefined,
|
|
288
|
+
degraded_reason: undefined,
|
|
289
|
+
};
|
|
290
|
+
writeState(coordRoot, next);
|
|
291
|
+
return { capsule, path: absolutePath, state: next, reused: false };
|
|
298
292
|
}
|
|
299
293
|
|
|
300
294
|
/** Prepare a briefing without claiming that a adapter has accepted it yet. */
|
|
@@ -302,48 +296,45 @@ export function prepareContextRecovery(
|
|
|
302
296
|
coordRoot: string,
|
|
303
297
|
input: { sessionId: string; instanceId: string; cwd: string },
|
|
304
298
|
): PreparedContextRecovery | null {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
: "post-compaction signal arrived without a continuity checkpoint",
|
|
322
|
-
};
|
|
323
|
-
writeState(coordRoot, degraded);
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const liveRepo = snapshotRepo(input.cwd);
|
|
328
|
-
const briefing = [
|
|
329
|
-
renderRecoveryBriefing(capsule, liveRepo),
|
|
330
|
-
renderRecoveryInput(coordRoot, input.instanceId),
|
|
331
|
-
]
|
|
332
|
-
.filter(Boolean)
|
|
333
|
-
.join("\n\n");
|
|
334
|
-
return {
|
|
335
|
-
state: current,
|
|
336
|
-
capsule,
|
|
337
|
-
briefing,
|
|
338
|
-
checkpoint: Object.freeze({
|
|
339
|
-
session_id: input.sessionId,
|
|
340
|
-
instance_id: input.instanceId,
|
|
341
|
-
generation: capsule.generation,
|
|
342
|
-
capsule_id: capsule.capsule_id,
|
|
343
|
-
capsule_sha256: capsuleDigest(capsule),
|
|
344
|
-
}),
|
|
299
|
+
const now = new Date().toISOString();
|
|
300
|
+
const current =
|
|
301
|
+
readContextState(coordRoot, input.sessionId) ??
|
|
302
|
+
newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
|
|
303
|
+
// A reused provider ID does not transfer the prior task's context.
|
|
304
|
+
if (current.instance_id !== input.instanceId) return null;
|
|
305
|
+
const capsule = readLatestCapsule(coordRoot, input.sessionId);
|
|
306
|
+
if (!capsule || !capsuleMatchesState(current, capsule, input)) {
|
|
307
|
+
const degraded: ContextContinuityState = {
|
|
308
|
+
...current,
|
|
309
|
+
instance_id: input.instanceId,
|
|
310
|
+
phase: "degraded",
|
|
311
|
+
updated_at: now,
|
|
312
|
+
degraded_reason: capsule
|
|
313
|
+
? "continuity checkpoint does not match the requesting instance and state"
|
|
314
|
+
: "post-compaction signal arrived without a continuity checkpoint",
|
|
345
315
|
};
|
|
346
|
-
|
|
316
|
+
writeState(coordRoot, degraded);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const liveRepo = snapshotRepo(input.cwd);
|
|
321
|
+
const briefing = [
|
|
322
|
+
renderRecoveryBriefing(capsule, liveRepo),
|
|
323
|
+
renderRecoveryInput(coordRoot, input.instanceId),
|
|
324
|
+
]
|
|
325
|
+
.filter(Boolean)
|
|
326
|
+
.join("\n\n");
|
|
327
|
+
return {
|
|
328
|
+
state: current,
|
|
329
|
+
capsule,
|
|
330
|
+
briefing,
|
|
331
|
+
checkpoint: Object.freeze({
|
|
332
|
+
session_id: input.sessionId,
|
|
333
|
+
instance_id: input.instanceId,
|
|
334
|
+
generation: capsule.generation,
|
|
335
|
+
capsule_id: capsule.capsule_id,
|
|
336
|
+
}),
|
|
337
|
+
};
|
|
347
338
|
}
|
|
348
339
|
|
|
349
340
|
/** Recovery lists input without surfacing it again or changing its receipts. */
|
|
@@ -404,8 +395,11 @@ function inputLine(messageId: string, status: string, body: string): string {
|
|
|
404
395
|
|
|
405
396
|
/**
|
|
406
397
|
* Advance continuity only after the adapter accepted the prepared briefing.
|
|
407
|
-
*
|
|
408
|
-
*
|
|
398
|
+
* A foreign or superseded checkpoint refuses without writing, and repeating
|
|
399
|
+
* the same accepted checkpoint is an exact no-op. The write is a
|
|
400
|
+
* compare-and-write: if the state changed after it was read, for example
|
|
401
|
+
* because a newer checkpoint landed, completion refuses instead of
|
|
402
|
+
* overwriting it.
|
|
409
403
|
*/
|
|
410
404
|
export function completeContextRecovery(
|
|
411
405
|
coordRoot: string,
|
|
@@ -415,43 +409,36 @@ export function completeContextRecovery(
|
|
|
415
409
|
checkpoint: Readonly<PreparedContextCheckpoint>;
|
|
416
410
|
},
|
|
417
411
|
): ContextContinuityState | null {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
updated_at: now,
|
|
449
|
-
recovered_at: now,
|
|
450
|
-
degraded_reason: undefined,
|
|
451
|
-
};
|
|
452
|
-
writeState(coordRoot, recovered);
|
|
453
|
-
return recovered;
|
|
454
|
-
});
|
|
412
|
+
const path = statePath(coordRoot, input.sessionId);
|
|
413
|
+
const observed = readText(path);
|
|
414
|
+
if (observed === null) return null;
|
|
415
|
+
const current = parseJson<ContextContinuityState>(observed);
|
|
416
|
+
const { checkpoint } = input;
|
|
417
|
+
if (
|
|
418
|
+
!current ||
|
|
419
|
+
checkpoint.session_id !== input.sessionId ||
|
|
420
|
+
checkpoint.instance_id !== input.instanceId ||
|
|
421
|
+
checkpoint.generation !== current.generation ||
|
|
422
|
+
(current.phase !== "checkpointed" && current.phase !== "recovered")
|
|
423
|
+
)
|
|
424
|
+
return null;
|
|
425
|
+
const capsule = readLatestCapsule(coordRoot, input.sessionId);
|
|
426
|
+
if (
|
|
427
|
+
!capsule ||
|
|
428
|
+
!capsuleMatchesState(current, capsule, input) ||
|
|
429
|
+
capsule.capsule_id !== checkpoint.capsule_id
|
|
430
|
+
)
|
|
431
|
+
return null;
|
|
432
|
+
if (current.phase === "recovered") return current;
|
|
433
|
+
const now = new Date().toISOString();
|
|
434
|
+
const recovered: ContextContinuityState = {
|
|
435
|
+
...current,
|
|
436
|
+
phase: "recovered",
|
|
437
|
+
updated_at: now,
|
|
438
|
+
recovered_at: now,
|
|
439
|
+
degraded_reason: undefined,
|
|
440
|
+
};
|
|
441
|
+
return writeBoundedJson(path, recovered, MAX_CAPSULE_BYTES, observed) ? recovered : null;
|
|
455
442
|
}
|
|
456
443
|
|
|
457
444
|
/** Record that the adapter, rather than Harnery, reported compaction complete. */
|
|
@@ -459,25 +446,23 @@ export function markContextCompactionCompleted(
|
|
|
459
446
|
coordRoot: string,
|
|
460
447
|
input: { sessionId: string; instanceId: string; observedAt?: string },
|
|
461
448
|
): ContextContinuityState {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
return next;
|
|
480
|
-
});
|
|
449
|
+
const now = input.observedAt ?? new Date().toISOString();
|
|
450
|
+
const current =
|
|
451
|
+
readContextState(coordRoot, input.sessionId) ??
|
|
452
|
+
newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
|
|
453
|
+
if (current.instance_id !== input.instanceId) return current;
|
|
454
|
+
const next: ContextContinuityState = {
|
|
455
|
+
...current,
|
|
456
|
+
instance_id: input.instanceId,
|
|
457
|
+
phase: current.latest_capsule ? current.phase : "degraded",
|
|
458
|
+
compaction_completed_at: now,
|
|
459
|
+
updated_at: now,
|
|
460
|
+
degraded_reason: current.latest_capsule
|
|
461
|
+
? current.degraded_reason
|
|
462
|
+
: "post-compaction signal arrived without a continuity checkpoint",
|
|
463
|
+
};
|
|
464
|
+
writeState(coordRoot, next);
|
|
465
|
+
return next;
|
|
481
466
|
}
|
|
482
467
|
|
|
483
468
|
/** Convenience API for callers whose injection is synchronous and guaranteed. */
|
|
@@ -520,29 +505,8 @@ export function readContextState(
|
|
|
520
505
|
|
|
521
506
|
export function readLatestCapsule(coordRoot: string, sessionId: string): ContinuityCapsule | null {
|
|
522
507
|
const state = readContextState(coordRoot, sessionId);
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
if (
|
|
526
|
-
!capsule ||
|
|
527
|
-
!record(capsule.repo) ||
|
|
528
|
-
!record(capsule.work) ||
|
|
529
|
-
typeof capsule.repo.cwd !== "string" ||
|
|
530
|
-
!Array.isArray(capsule.repo.dirty_paths) ||
|
|
531
|
-
!capsule.repo.dirty_paths.every((path) => typeof path === "string") ||
|
|
532
|
-
!Array.isArray(capsule.work.files_held) ||
|
|
533
|
-
!capsule.work.files_held.every((path) => typeof path === "string")
|
|
534
|
-
) {
|
|
535
|
-
return null;
|
|
536
|
-
}
|
|
537
|
-
capsule.repo.observation = repoObservation(capsule.repo.observation);
|
|
538
|
-
capsule.repo.observation_detail = clampString(
|
|
539
|
-
string(capsule.repo.observation_detail),
|
|
540
|
-
MAX_OBSERVATION_DETAIL_CHARS,
|
|
541
|
-
);
|
|
542
|
-
capsule.repo.branch = string(capsule.repo.branch)?.trim() || undefined;
|
|
543
|
-
capsule.repo.head = string(capsule.repo.head)?.trim() || undefined;
|
|
544
|
-
capsule.repo.root = string(capsule.repo.root)?.trim() || undefined;
|
|
545
|
-
return capsule;
|
|
508
|
+
if (!state?.latest_capsule) return null;
|
|
509
|
+
return readJson<ContinuityCapsule>(join(coordRoot, ".harnery", state.latest_capsule));
|
|
546
510
|
}
|
|
547
511
|
|
|
548
512
|
export function renderRecoveryBriefing(capsule: ContinuityCapsule, liveRepo: RepoSnapshot): string {
|
|
@@ -557,35 +521,10 @@ export function renderRecoveryBriefing(capsule: ContinuityCapsule, liveRepo: Rep
|
|
|
557
521
|
lines.push(`Files held: ${capsule.work.files_held.join(", ")}`);
|
|
558
522
|
}
|
|
559
523
|
|
|
560
|
-
const unknown
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
["current repository", liveRepo],
|
|
565
|
-
] as const) {
|
|
566
|
-
if (repo.observation !== "ok") {
|
|
567
|
-
const detail = clampString(string(repo.observation_detail), MAX_OBSERVATION_DETAIL_CHARS);
|
|
568
|
-
unknown.push(
|
|
569
|
-
`${label}: ${repoObservation(repo.observation)}${detail ? ` (${detail})` : ""}`,
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
if (capsule.repo.dirty_paths_truncated || liveRepo.dirty_paths_truncated) {
|
|
575
|
-
unknown.push("dirty path list is truncated");
|
|
576
|
-
}
|
|
577
|
-
if (
|
|
578
|
-
!capsule.repo.root ||
|
|
579
|
-
!liveRepo.root ||
|
|
580
|
-
!isAbsolute(capsule.repo.root) ||
|
|
581
|
-
!isAbsolute(liveRepo.root)
|
|
582
|
-
) {
|
|
583
|
-
unknown.push("repository root is unknown");
|
|
584
|
-
} else if (capsule.repo.root !== liveRepo.root) {
|
|
585
|
-
unknown.push("repository root differs from the checkpoint");
|
|
586
|
-
}
|
|
587
|
-
if (!capsule.repo.branch?.trim() || !liveRepo.branch?.trim()) unknown.push("branch is unknown");
|
|
588
|
-
if (!capsule.repo.head?.trim() || !liveRepo.head?.trim()) unknown.push("HEAD is unknown");
|
|
524
|
+
const unknown = repoComparisonProblems(capsule.repo, liveRepo, [
|
|
525
|
+
"checkpoint",
|
|
526
|
+
"current repository",
|
|
527
|
+
]);
|
|
589
528
|
if (unknown.length > 0) {
|
|
590
529
|
lines.push(
|
|
591
530
|
`Repository could not be verified against the checkpoint: ${unknown.join("; ")}. Re-check before editing.`,
|
|
@@ -594,11 +533,11 @@ export function renderRecoveryBriefing(capsule: ContinuityCapsule, liveRepo: Rep
|
|
|
594
533
|
}
|
|
595
534
|
|
|
596
535
|
const drift: string[] = [];
|
|
597
|
-
if (capsule.repo.branch
|
|
536
|
+
if (capsule.repo.branch !== liveRepo.branch) {
|
|
598
537
|
drift.push(`branch changed ${capsule.repo.branch} -> ${liveRepo.branch}`);
|
|
599
538
|
}
|
|
600
|
-
if (capsule.repo.head
|
|
601
|
-
drift.push(`HEAD changed ${capsule.repo.head
|
|
539
|
+
if (capsule.repo.head !== liveRepo.head) {
|
|
540
|
+
drift.push(`HEAD changed ${capsule.repo.head?.slice(0, 8)} -> ${liveRepo.head?.slice(0, 8)}`);
|
|
602
541
|
}
|
|
603
542
|
const beforeDirty = new Set(capsule.repo.dirty_paths);
|
|
604
543
|
const afterDirty = new Set(liveRepo.dirty_paths);
|
|
@@ -715,47 +654,20 @@ function newState(input: {
|
|
|
715
654
|
};
|
|
716
655
|
}
|
|
717
656
|
|
|
657
|
+
/** The capsule is the one the state names, written for the requesting task. */
|
|
718
658
|
function capsuleMatchesState(
|
|
719
659
|
state: ContextContinuityState,
|
|
720
660
|
capsule: ContinuityCapsule,
|
|
721
661
|
input: { sessionId: string; instanceId: string },
|
|
722
662
|
): boolean {
|
|
723
663
|
return (
|
|
724
|
-
state.session_id === input.sessionId &&
|
|
725
664
|
state.instance_id === input.instanceId &&
|
|
726
|
-
capsule.session
|
|
727
|
-
capsule.session?.instance_id === input.instanceId &&
|
|
728
|
-
capsule.schema_version === CONTEXT_SCHEMA_VERSION &&
|
|
729
|
-
Number.isSafeInteger(capsule.generation) &&
|
|
730
|
-
capsule.generation > 0 &&
|
|
665
|
+
capsule.session.instance_id === input.instanceId &&
|
|
731
666
|
capsule.generation === state.generation &&
|
|
732
|
-
typeof capsule.capsule_id === "string" &&
|
|
733
|
-
capsule.capsule_id.length > 0 &&
|
|
734
667
|
state.latest_capsule === capsuleRelativePath(input.sessionId, capsule.generation)
|
|
735
668
|
);
|
|
736
669
|
}
|
|
737
670
|
|
|
738
|
-
function capsuleDigest(capsule: ContinuityCapsule): string {
|
|
739
|
-
return createHash("sha256").update(JSON.stringify(capsule)).digest("hex");
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/** Every state writer shares the same crash-recoverable session reservation. */
|
|
743
|
-
function withContextMutation<T>(coordRoot: string, sessionId: string, action: () => T): T {
|
|
744
|
-
const authority = createHash("sha256").update(sessionId).digest("hex");
|
|
745
|
-
const lease = acquireNoClobberLease({
|
|
746
|
-
path: join(contextDir(coordRoot, sessionId), "mutation.lease"),
|
|
747
|
-
scope: "context-continuity",
|
|
748
|
-
authoritySha256: authority,
|
|
749
|
-
staleAfterMs: 30_000,
|
|
750
|
-
validateStaleOwner: (owner) => owner.authority_sha256 === authority,
|
|
751
|
-
});
|
|
752
|
-
try {
|
|
753
|
-
return action();
|
|
754
|
-
} finally {
|
|
755
|
-
lease.release();
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
671
|
function writeState(coordRoot: string, state: ContextContinuityState): void {
|
|
760
672
|
writeBoundedJson(statePath(coordRoot, state.session_id), state, MAX_CAPSULE_BYTES);
|
|
761
673
|
}
|
|
@@ -781,7 +693,17 @@ function sessionKey(sessionId: string): string {
|
|
|
781
693
|
return createHash("sha256").update(sessionId).digest("hex").slice(0, 24);
|
|
782
694
|
}
|
|
783
695
|
|
|
784
|
-
|
|
696
|
+
/**
|
|
697
|
+
* Atomically replace a record. With `expected`, the replacement happens only
|
|
698
|
+
* while the file still holds exactly those bytes, so a concurrent writer's
|
|
699
|
+
* newer record is never overwritten by a decision made on older state.
|
|
700
|
+
*/
|
|
701
|
+
function writeBoundedJson(
|
|
702
|
+
path: string,
|
|
703
|
+
value: unknown,
|
|
704
|
+
maxBytes: number,
|
|
705
|
+
expected?: string,
|
|
706
|
+
): boolean {
|
|
785
707
|
const text = `${JSON.stringify(value, null, 2)}\n`;
|
|
786
708
|
const bytes = Buffer.byteLength(text, "utf8");
|
|
787
709
|
if (bytes > maxBytes) {
|
|
@@ -790,18 +712,35 @@ function writeBoundedJson(path: string, value: unknown, maxBytes: number): void
|
|
|
790
712
|
mkdirSync(dirname(path), { recursive: true });
|
|
791
713
|
const temp = `${path}.tmp.${process.pid}.${randomUUID()}`;
|
|
792
714
|
writeFileSync(temp, text, { encoding: "utf8", mode: stateFileMode() });
|
|
715
|
+
if (expected !== undefined && readText(path) !== expected) {
|
|
716
|
+
unlinkSync(temp);
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
793
719
|
renameSync(temp, path);
|
|
720
|
+
return true;
|
|
794
721
|
}
|
|
795
722
|
|
|
796
|
-
function
|
|
797
|
-
if (!existsSync(path)) return null;
|
|
723
|
+
function readText(path: string): string | null {
|
|
798
724
|
try {
|
|
799
|
-
return
|
|
725
|
+
return readFileSync(path, "utf8");
|
|
800
726
|
} catch {
|
|
801
727
|
return null;
|
|
802
728
|
}
|
|
803
729
|
}
|
|
804
730
|
|
|
731
|
+
function parseJson<T>(text: string): T | null {
|
|
732
|
+
try {
|
|
733
|
+
return JSON.parse(text) as T;
|
|
734
|
+
} catch {
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function readJson<T>(path: string): T | null {
|
|
740
|
+
const text = readText(path);
|
|
741
|
+
return text === null ? null : parseJson<T>(text);
|
|
742
|
+
}
|
|
743
|
+
|
|
805
744
|
type GitReadResult =
|
|
806
745
|
| { ok: true; out: string }
|
|
807
746
|
| { ok: false; reason: "not_repository" | "failed" | "timed_out" | "spawn_error" };
|
|
@@ -828,10 +767,6 @@ function git(cwd: string, args: string[]): GitReadResult {
|
|
|
828
767
|
}
|
|
829
768
|
}
|
|
830
769
|
|
|
831
|
-
function repoObservation(value: unknown): RepoObservation {
|
|
832
|
-
return value === "ok" || value === "partial" ? value : "unavailable";
|
|
833
|
-
}
|
|
834
|
-
|
|
835
770
|
function sameMeasurement(a: ContextSample | undefined, b: ContextSample): boolean {
|
|
836
771
|
if (!a) return false;
|
|
837
772
|
return (
|