harnery 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/commander.js +1 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +41 -48
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +38 -11
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +10 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +22 -7
- package/dist/commands/files.d.ts +2 -2
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +7 -4
- package/dist/commands/governor.js +22 -10
- package/dist/commands/inbox.d.ts +1 -1
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +55 -2
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +28 -4
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +26 -17
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +7 -2
- package/dist/commands/toc.d.ts +14 -0
- package/dist/commands/toc.d.ts.map +1 -1
- package/dist/commands/toc.js +7 -4
- package/dist/commands/tunnel.d.ts +15 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +453 -153
- package/dist/commands/turn-rollback.d.ts +5 -0
- package/dist/commands/turn-rollback.d.ts.map +1 -0
- package/dist/commands/turn-rollback.js +76 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +68 -30
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +18 -0
- package/dist/core/agents/live-authority-v3.d.ts +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +28 -7
- package/dist/core/agents/mailbox.d.ts +9 -8
- package/dist/core/agents/mailbox.d.ts.map +1 -1
- package/dist/core/agents/mailbox.js +24 -17
- package/dist/core/agents/render/prompt-context.d.ts +2 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +8 -12
- package/dist/core/agents/render/session-context.d.ts +2 -8
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +7 -30
- package/dist/core/agents/rules/stop-hook.js +3 -3
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +25 -14
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +5 -0
- package/dist/core/agents/state/proc-start.d.ts +19 -1
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +46 -1
- package/dist/core/agents/state/process-handle.d.ts +26 -0
- package/dist/core/agents/state/process-handle.d.ts.map +1 -0
- package/dist/core/agents/state/process-handle.js +121 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +180 -70
- package/dist/core/agents/state/unchanged-file.d.ts +44 -0
- package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
- package/dist/core/agents/state/unchanged-file.js +390 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +2 -1
- package/dist/core/context/index.d.ts +22 -10
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +381 -119
- package/dist/core/context/observation.d.ts +26 -0
- package/dist/core/context/observation.d.ts.map +1 -0
- package/dist/core/context/observation.js +11 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +25 -14
- package/dist/core/diagnostics/path-identity.d.ts +3 -0
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
- package/dist/core/diagnostics/path-identity.js +12 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
- package/dist/core/diagnostics/sanitize.js +4 -3
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +29 -0
- package/dist/core/events/v3/generation-custody.d.ts +5 -0
- package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
- package/dist/core/events/v3/generation-custody.js +50 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +77 -64
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +14 -1
- package/dist/core/governor/service-read.d.ts +10 -2
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +105 -22
- package/dist/core/governor/service.d.ts +20 -2
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +72 -47
- package/dist/core/hooks/cli.js +33 -5
- package/dist/core/inbox/contract.d.ts +2 -1
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/index.d.ts +2 -0
- package/dist/core/inbox/index.d.ts.map +1 -1
- package/dist/core/inbox/index.js +2 -0
- package/dist/core/inbox/peer-messages.d.ts +37 -0
- package/dist/core/inbox/peer-messages.d.ts.map +1 -0
- package/dist/core/inbox/peer-messages.js +72 -0
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +67 -21
- package/dist/core/inbox/targets.d.ts +22 -0
- package/dist/core/inbox/targets.d.ts.map +1 -0
- package/dist/core/inbox/targets.js +108 -0
- package/dist/core/inbox/watcher.js +7 -3
- package/dist/core/journal/index.d.ts.map +1 -1
- package/dist/core/journal/index.js +17 -2
- package/dist/core/resources/contract.d.ts +3 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/service-status.d.ts +14 -1
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +23 -13
- package/dist/core/resources/service.d.ts +7 -13
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +34 -91
- package/dist/core/resources/status.d.ts +2 -5
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +36 -7
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +2 -9
- package/dist/core/servers/index.d.ts +34 -22
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +297 -144
- package/dist/core/servers/tunnels.d.ts +7 -7
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +47 -10
- package/dist/core/supervisor/contract.d.ts +12 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.js +2 -1
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +54 -9
- package/dist/core/supervisor/service.d.ts +7 -1
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +59 -29
- package/dist/core/supervisor/services.d.ts.map +1 -1
- package/dist/core/supervisor/services.js +22 -1
- package/dist/core/supervisor/status.d.ts +17 -2
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +70 -12
- package/dist/core/supervisor/timeline.d.ts.map +1 -1
- package/dist/core/supervisor/timeline.js +2 -1
- package/dist/core/turn-rollback/content.d.ts +53 -0
- package/dist/core/turn-rollback/content.d.ts.map +1 -0
- package/dist/core/turn-rollback/content.js +396 -0
- package/dist/core/turn-rollback/index.d.ts +72 -0
- package/dist/core/turn-rollback/index.d.ts.map +1 -0
- package/dist/core/turn-rollback/index.js +443 -0
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +105 -32
- package/dist/core/workflow/failure-evidence.d.ts +16 -0
- package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
- package/dist/core/workflow/failure-evidence.js +114 -0
- package/dist/core/workflow/index.d.ts +5 -3
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +3 -2
- package/dist/core/workflow/live-session-v3.d.ts +6 -0
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +126 -5
- package/dist/core/workflow/proof.d.ts +5 -5
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +115 -20
- package/dist/core/workflow/recovery-audit.d.ts +30 -0
- package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
- package/dist/core/workflow/recovery-audit.js +116 -0
- package/dist/core/workflow/run-state.d.ts +20 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +141 -32
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +19 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +13 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +19 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +14 -0
- package/dist/core/workflow/types.d.ts +64 -17
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.d.ts +0 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.js +0 -1
- package/dist/core/workflow/workspaces/leases.d.ts +3 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +30 -16
- package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +328 -8
- package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/ownership.js +194 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts +40 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +14 -0
- package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +42 -10
- package/dist/core/workflow/workspaces/state.d.ts +1 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +21 -1
- package/dist/lib/browser/windows-chrome.d.ts +100 -12
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +262 -73
- package/dist/lib/docs-links.d.ts.map +1 -1
- package/dist/lib/docs-links.js +45 -2
- package/dist/lib/guarded-remove.d.ts +7 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +106 -12
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -0
- package/dist/lib/tunnel/state.d.ts +13 -6
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +58 -12
- package/dist/lib/zip/index.d.ts +41 -0
- package/dist/lib/zip/index.d.ts.map +1 -0
- package/dist/lib/zip/index.js +398 -0
- package/package.json +12 -1
- package/src/commander.ts +10 -0
- package/src/commands/agents.ts +54 -63
- package/src/commands/browse.ts +50 -14
- package/src/commands/diagnostics.ts +10 -0
- package/src/commands/doctor.ts +26 -7
- package/src/commands/files.ts +13 -5
- package/src/commands/governor.ts +23 -10
- package/src/commands/inbox.ts +52 -2
- package/src/commands/rm.ts +43 -4
- package/src/commands/servers.ts +27 -26
- package/src/commands/supervisor.ts +7 -2
- package/src/commands/toc.ts +14 -6
- package/src/commands/tunnel.ts +533 -166
- package/src/commands/turn-rollback.ts +111 -0
- package/src/commands/workflow.ts +89 -30
- package/src/core/agents/coord-client.ts +20 -0
- package/src/core/agents/live-authority-v3.ts +17 -0
- package/src/core/agents/live-lifecycle-v3.ts +52 -14
- package/src/core/agents/mailbox.ts +26 -17
- package/src/core/agents/render/prompt-context.ts +10 -12
- package/src/core/agents/render/session-context.ts +9 -32
- package/src/core/agents/rules/stop-hook.ts +3 -3
- package/src/core/agents/state/heartbeat-writer.ts +29 -12
- package/src/core/agents/state/live-coordination-writer.ts +29 -0
- package/src/core/agents/state/pidmap.ts +13 -0
- package/src/core/agents/state/proc-start.ts +62 -2
- package/src/core/agents/state/process-handle.ts +165 -0
- package/src/core/agents/state/stale-sweep.ts +247 -82
- package/src/core/agents/state/unchanged-file.ts +456 -0
- package/src/core/artifacts/delivery-card.ts +7 -1
- package/src/core/context/index.ts +446 -136
- package/src/core/context/observation.ts +29 -0
- package/src/core/diagnostics/bundle.ts +23 -18
- package/src/core/diagnostics/path-identity.ts +15 -0
- package/src/core/diagnostics/sanitize.ts +4 -3
- package/src/core/events/v3/authority-outbox.ts +56 -0
- package/src/core/events/v3/generation-custody.ts +59 -0
- package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
- package/src/core/events/v3/producers/recorder.ts +94 -68
- package/src/core/governor/planning.ts +20 -8
- package/src/core/governor/service-read.ts +128 -20
- package/src/core/governor/service.ts +119 -42
- package/src/core/hooks/cli.ts +33 -5
- package/src/core/inbox/contract.ts +2 -1
- package/src/core/inbox/index.ts +2 -0
- package/src/core/inbox/peer-messages.ts +94 -0
- package/src/core/inbox/service.ts +88 -22
- package/src/core/inbox/targets.ts +148 -0
- package/src/core/inbox/watcher.ts +8 -9
- package/src/core/journal/index.ts +19 -2
- package/src/core/resources/contract.ts +4 -0
- package/src/core/resources/service-status.ts +35 -12
- package/src/core/resources/service.ts +57 -95
- package/src/core/resources/status.ts +57 -8
- package/src/core/semantic/service.ts +2 -8
- package/src/core/servers/index.ts +376 -141
- package/src/core/servers/tunnels.ts +57 -13
- package/src/core/supervisor/contract.ts +13 -0
- package/src/core/supervisor/explanations.ts +4 -1
- package/src/core/supervisor/findings.ts +68 -10
- package/src/core/supervisor/service.ts +87 -26
- package/src/core/supervisor/services.ts +26 -8
- package/src/core/supervisor/status.ts +89 -12
- package/src/core/supervisor/timeline.ts +2 -1
- package/src/core/turn-rollback/content.ts +499 -0
- package/src/core/turn-rollback/index.ts +580 -0
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +131 -49
- package/src/core/workflow/failure-evidence.ts +141 -0
- package/src/core/workflow/index.ts +12 -2
- package/src/core/workflow/live-session-v3.ts +146 -5
- package/src/core/workflow/proof.ts +134 -26
- package/src/core/workflow/recovery-audit.ts +181 -0
- package/src/core/workflow/run-state.ts +165 -34
- package/src/core/workflow/spawn-claude.ts +18 -0
- package/src/core/workflow/spawn-codex.ts +13 -0
- package/src/core/workflow/spawn-cursor.ts +18 -0
- package/src/core/workflow/spawn-opencode.ts +14 -0
- package/src/core/workflow/types.ts +51 -17
- package/src/core/workflow/workspaces/index.ts +0 -1
- package/src/core/workflow/workspaces/leases.ts +48 -17
- package/src/core/workflow/workspaces/local-git.ts +388 -7
- package/src/core/workflow/workspaces/ownership.ts +232 -0
- package/src/core/workflow/workspaces/reclaim.ts +62 -0
- package/src/core/workflow/workspaces/recovery.ts +45 -10
- package/src/core/workflow/workspaces/state.ts +20 -0
- package/src/lib/browser/windows-chrome.ts +346 -86
- package/src/lib/docs-links.ts +49 -2
- package/src/lib/guarded-remove.ts +132 -14
- package/src/lib/identities/assume.ts +4 -0
- package/src/lib/tunnel/state.ts +70 -15
- package/src/lib/zip/index.ts +483 -0
|
@@ -8,15 +8,27 @@
|
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { dirname, join } from "node:path";
|
|
11
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
12
12
|
import type { Adapter } from "../adapter.ts";
|
|
13
13
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
|
|
14
|
+
import { resolveBinName } from "../config.ts";
|
|
15
|
+
import type { HarneryInboxMessageRecordV1 } from "../inbox/contract.ts";
|
|
16
|
+
import { peerInbox } from "../inbox/peer-messages.ts";
|
|
17
|
+
import { project } from "../inbox/service.ts";
|
|
14
18
|
import { stateFileMode } from "../storage/modes.ts";
|
|
19
|
+
import { acquireNoClobberLease } from "../workflow/workspaces/leases.ts";
|
|
20
|
+
import type { RepoObservation, RepoSnapshot } from "./observation.ts";
|
|
21
|
+
import { observationsComparable } from "./observation.ts";
|
|
22
|
+
|
|
23
|
+
export type { RepoObservation, RepoSnapshot } from "./observation.ts";
|
|
24
|
+
export { observationsComparable } from "./observation.ts";
|
|
15
25
|
|
|
16
26
|
export const CONTEXT_SCHEMA_VERSION = 1 as const;
|
|
17
27
|
export const MAX_CAPSULE_BYTES = 32 * 1024;
|
|
18
28
|
const MAX_SNAPSHOT_PATHS = 15;
|
|
19
29
|
const MAX_SNAPSHOT_PATH_CHARS = 500;
|
|
30
|
+
const MAX_OBSERVATION_DETAIL_CHARS = 500;
|
|
31
|
+
const MAX_RECOVERY_INPUT_BODY_BYTES = 160;
|
|
20
32
|
|
|
21
33
|
export type ContextSampleSource = "hook" | "native_event" | "result" | "transcript" | "estimate";
|
|
22
34
|
export type ContextSampleConfidence = "exact" | "reported" | "estimated";
|
|
@@ -49,15 +61,6 @@ export interface ContextContinuityState {
|
|
|
49
61
|
degraded_reason?: string;
|
|
50
62
|
}
|
|
51
63
|
|
|
52
|
-
export interface RepoSnapshot {
|
|
53
|
-
cwd: string;
|
|
54
|
-
root?: string;
|
|
55
|
-
branch?: string;
|
|
56
|
-
head?: string;
|
|
57
|
-
dirty_paths: string[];
|
|
58
|
-
dirty_paths_truncated?: boolean;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
64
|
export interface ContinuityCapsule {
|
|
62
65
|
schema_version: typeof CONTEXT_SCHEMA_VERSION;
|
|
63
66
|
capsule_id: string;
|
|
@@ -113,7 +116,20 @@ export interface RecoverContextResult {
|
|
|
113
116
|
recovered: boolean;
|
|
114
117
|
}
|
|
115
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Authority for accepting exactly the capsule supplied during preparation.
|
|
121
|
+
* It is returned by prepareContextRecovery, not reconstructed from latest state.
|
|
122
|
+
*/
|
|
123
|
+
export interface PreparedContextCheckpoint {
|
|
124
|
+
session_id: string;
|
|
125
|
+
instance_id: string;
|
|
126
|
+
generation: number;
|
|
127
|
+
capsule_id: string;
|
|
128
|
+
capsule_sha256: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
116
131
|
export interface PreparedContextRecovery {
|
|
132
|
+
checkpoint: Readonly<PreparedContextCheckpoint>;
|
|
117
133
|
state: ContextContinuityState;
|
|
118
134
|
capsule: ContinuityCapsule;
|
|
119
135
|
briefing: string;
|
|
@@ -190,19 +206,26 @@ export function recordContextSample(
|
|
|
190
206
|
instanceId: string,
|
|
191
207
|
sample: ContextSample,
|
|
192
208
|
): { state: ContextContinuityState; changed: boolean } {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
209
|
+
return withContextMutation(coordRoot, sample.session_id, () => {
|
|
210
|
+
const current = readContextState(coordRoot, sample.session_id);
|
|
211
|
+
const state =
|
|
212
|
+
current?.instance_id === instanceId
|
|
213
|
+
? current
|
|
214
|
+
: {
|
|
215
|
+
...newState({ sessionId: sample.session_id, instanceId, now: sample.observed_at }),
|
|
216
|
+
generation: current?.generation ?? 0,
|
|
217
|
+
};
|
|
218
|
+
const changed = !sameMeasurement(state.latest_context, sample);
|
|
219
|
+
if (!changed) return { state, changed: false };
|
|
220
|
+
const next: ContextContinuityState = {
|
|
221
|
+
...state,
|
|
222
|
+
instance_id: instanceId,
|
|
223
|
+
latest_context: sample,
|
|
224
|
+
updated_at: sample.observed_at,
|
|
225
|
+
};
|
|
226
|
+
writeState(coordRoot, next);
|
|
227
|
+
return { state: next, changed: true };
|
|
228
|
+
});
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
/**
|
|
@@ -214,57 +237,64 @@ export function checkpointContext(
|
|
|
214
237
|
coordRoot: string,
|
|
215
238
|
input: CheckpointContextInput,
|
|
216
239
|
): CheckpointContextResult {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
readContextState(coordRoot, input.sessionId)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
240
|
+
return withContextMutation(coordRoot, input.sessionId, () => {
|
|
241
|
+
const now = new Date().toISOString();
|
|
242
|
+
const previous = readContextState(coordRoot, input.sessionId);
|
|
243
|
+
const current =
|
|
244
|
+
previous?.instance_id === input.instanceId
|
|
245
|
+
? previous
|
|
246
|
+
: {
|
|
247
|
+
...newState({ sessionId: input.sessionId, instanceId: input.instanceId, now }),
|
|
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,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
230
260
|
}
|
|
231
|
-
}
|
|
232
261
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
262
|
+
const heartbeat = readHeartbeat(coordRoot, input.instanceId);
|
|
263
|
+
const generation = current.generation + 1;
|
|
264
|
+
const capsule: ContinuityCapsule = {
|
|
265
|
+
schema_version: CONTEXT_SCHEMA_VERSION,
|
|
266
|
+
capsule_id: randomUUID(),
|
|
267
|
+
generation,
|
|
268
|
+
created_at: now,
|
|
269
|
+
reason: input.reason,
|
|
270
|
+
session: {
|
|
271
|
+
session_id: input.sessionId,
|
|
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
|
+
};
|
|
251
280
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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,
|
|
286
|
+
instance_id: input.instanceId,
|
|
287
|
+
phase: "checkpointed",
|
|
288
|
+
generation,
|
|
289
|
+
latest_capsule: relativePath,
|
|
290
|
+
updated_at: now,
|
|
291
|
+
recovered_at: undefined,
|
|
292
|
+
compaction_completed_at: undefined,
|
|
293
|
+
degraded_reason: undefined,
|
|
294
|
+
};
|
|
295
|
+
writeState(coordRoot, next);
|
|
296
|
+
return { capsule, path: absolutePath, state: next, reused: false };
|
|
297
|
+
});
|
|
268
298
|
}
|
|
269
299
|
|
|
270
300
|
/** Prepare a briefing without claiming that a adapter has accepted it yet. */
|
|
@@ -272,47 +302,156 @@ export function prepareContextRecovery(
|
|
|
272
302
|
coordRoot: string,
|
|
273
303
|
input: { sessionId: string; instanceId: string; cwd: string },
|
|
274
304
|
): PreparedContextRecovery | null {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
305
|
+
return withContextMutation(coordRoot, input.sessionId, () => {
|
|
306
|
+
const now = new Date().toISOString();
|
|
307
|
+
const current =
|
|
308
|
+
readContextState(coordRoot, input.sessionId) ??
|
|
309
|
+
newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
|
|
310
|
+
// A reused provider ID does not transfer the prior task's context.
|
|
311
|
+
if (current.instance_id !== input.instanceId) return null;
|
|
312
|
+
const capsule = readLatestCapsule(coordRoot, input.sessionId);
|
|
313
|
+
if (!capsule || !capsuleMatchesState(current, capsule, input)) {
|
|
314
|
+
const degraded: ContextContinuityState = {
|
|
315
|
+
...current,
|
|
316
|
+
instance_id: input.instanceId,
|
|
317
|
+
phase: "degraded",
|
|
318
|
+
updated_at: now,
|
|
319
|
+
degraded_reason: capsule
|
|
320
|
+
? "continuity checkpoint does not match the requesting instance and state"
|
|
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
|
+
}),
|
|
287
345
|
};
|
|
288
|
-
|
|
289
|
-
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Recovery lists input without surfacing it again or changing its receipts. */
|
|
350
|
+
function renderRecoveryInput(coordRoot: string, instanceId: string): string {
|
|
351
|
+
const readCommand = `${resolveBinName(coordRoot)} agents inbox list ${instanceId}`;
|
|
352
|
+
try {
|
|
353
|
+
const records = peerInbox(coordRoot).records(instanceId);
|
|
354
|
+
if (
|
|
355
|
+
records.some(
|
|
356
|
+
(record) =>
|
|
357
|
+
(record.kind !== "message" && record.kind !== "surfaced") ||
|
|
358
|
+
typeof record.message_id !== "string" ||
|
|
359
|
+
Buffer.byteLength(record.message_id) > 128,
|
|
360
|
+
)
|
|
361
|
+
) {
|
|
362
|
+
throw new Error("inbox input cannot be listed safely");
|
|
363
|
+
}
|
|
364
|
+
const { surfaced } = project(records);
|
|
365
|
+
const messages = records.filter(
|
|
366
|
+
(record): record is HarneryInboxMessageRecordV1 =>
|
|
367
|
+
record.kind === "message" && record.recipient_instance_id === instanceId,
|
|
368
|
+
);
|
|
369
|
+
const pending = messages.filter((message) => !surfaced.has(message.message_id));
|
|
370
|
+
const recent = messages.filter((message) => surfaced.has(message.message_id)).slice(-10);
|
|
371
|
+
if (pending.length === 0 && recent.length === 0) return "";
|
|
372
|
+
const lines = [
|
|
373
|
+
"Coordination input after compaction (surfaced does not mean read):",
|
|
374
|
+
...pending
|
|
375
|
+
.slice(0, 25)
|
|
376
|
+
.map((message) => inputLine(message.message_id, "pending", message.body)),
|
|
377
|
+
...recent.map((message) =>
|
|
378
|
+
inputLine(message.message_id, "previously surfaced", message.body),
|
|
379
|
+
),
|
|
380
|
+
];
|
|
381
|
+
if (pending.length > 25)
|
|
382
|
+
lines.push(`${pending.length - 25} more pending messages are not listed.`);
|
|
383
|
+
lines.push(
|
|
384
|
+
`Recovery did not acknowledge these messages. Read pending input with \`${readCommand}\`.`,
|
|
385
|
+
);
|
|
386
|
+
return lines.join("\n");
|
|
387
|
+
} catch {
|
|
388
|
+
return `Coordination input could not be read. Check \`${readCommand}\` before continuing.`;
|
|
290
389
|
}
|
|
390
|
+
}
|
|
291
391
|
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
392
|
+
function inputLine(messageId: string, status: string, body: string): string {
|
|
393
|
+
const text = body.replace(/\s+/g, " ").trim();
|
|
394
|
+
let excerpt = "";
|
|
395
|
+
let bytes = 0;
|
|
396
|
+
for (const character of text) {
|
|
397
|
+
const size = Buffer.byteLength(character);
|
|
398
|
+
if (bytes + size > MAX_RECOVERY_INPUT_BODY_BYTES) break;
|
|
399
|
+
excerpt += character;
|
|
400
|
+
bytes += size;
|
|
401
|
+
}
|
|
402
|
+
return `- ${messageId} (${status}): ${excerpt}${excerpt.length < text.length ? " [excerpt]" : ""}`;
|
|
295
403
|
}
|
|
296
404
|
|
|
297
|
-
/**
|
|
405
|
+
/**
|
|
406
|
+
* Advance continuity only after the adapter accepted the prepared briefing.
|
|
407
|
+
* Missing, altered, foreign or superseded checkpoints refuse without writing.
|
|
408
|
+
* Repeating the same accepted checkpoint is an exact no-op.
|
|
409
|
+
*/
|
|
298
410
|
export function completeContextRecovery(
|
|
299
411
|
coordRoot: string,
|
|
300
|
-
input: {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
412
|
+
input: {
|
|
413
|
+
sessionId: string;
|
|
414
|
+
instanceId: string;
|
|
415
|
+
checkpoint: Readonly<PreparedContextCheckpoint>;
|
|
416
|
+
},
|
|
417
|
+
): ContextContinuityState | null {
|
|
418
|
+
return withContextMutation(coordRoot, input.sessionId, () => {
|
|
419
|
+
const current = readContextState(coordRoot, input.sessionId);
|
|
420
|
+
const checkpoint = input.checkpoint;
|
|
421
|
+
if (
|
|
422
|
+
!current ||
|
|
423
|
+
!checkpoint ||
|
|
424
|
+
checkpoint.session_id !== input.sessionId ||
|
|
425
|
+
checkpoint.instance_id !== input.instanceId ||
|
|
426
|
+
!Number.isSafeInteger(checkpoint.generation) ||
|
|
427
|
+
checkpoint.generation < 1 ||
|
|
428
|
+
typeof checkpoint.capsule_id !== "string" ||
|
|
429
|
+
!checkpoint.capsule_id ||
|
|
430
|
+
!/^[a-f0-9]{64}$/.test(checkpoint.capsule_sha256) ||
|
|
431
|
+
(current.phase !== "checkpointed" && current.phase !== "recovered")
|
|
432
|
+
)
|
|
433
|
+
return null;
|
|
434
|
+
const capsule = readLatestCapsule(coordRoot, input.sessionId);
|
|
435
|
+
if (
|
|
436
|
+
!capsule ||
|
|
437
|
+
!capsuleMatchesState(current, capsule, input) ||
|
|
438
|
+
checkpoint.generation !== capsule.generation ||
|
|
439
|
+
checkpoint.capsule_id !== capsule.capsule_id ||
|
|
440
|
+
checkpoint.capsule_sha256 !== capsuleDigest(capsule)
|
|
441
|
+
)
|
|
442
|
+
return null;
|
|
443
|
+
if (current.phase === "recovered") return current;
|
|
444
|
+
const now = new Date().toISOString();
|
|
445
|
+
const recovered: ContextContinuityState = {
|
|
446
|
+
...current,
|
|
447
|
+
phase: "recovered",
|
|
448
|
+
updated_at: now,
|
|
449
|
+
recovered_at: now,
|
|
450
|
+
degraded_reason: undefined,
|
|
451
|
+
};
|
|
452
|
+
writeState(coordRoot, recovered);
|
|
453
|
+
return recovered;
|
|
454
|
+
});
|
|
316
455
|
}
|
|
317
456
|
|
|
318
457
|
/** Record that the adapter, rather than Harnery, reported compaction complete. */
|
|
@@ -320,22 +459,25 @@ export function markContextCompactionCompleted(
|
|
|
320
459
|
coordRoot: string,
|
|
321
460
|
input: { sessionId: string; instanceId: string; observedAt?: string },
|
|
322
461
|
): ContextContinuityState {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
462
|
+
return withContextMutation(coordRoot, input.sessionId, () => {
|
|
463
|
+
const now = input.observedAt ?? new Date().toISOString();
|
|
464
|
+
const current =
|
|
465
|
+
readContextState(coordRoot, input.sessionId) ??
|
|
466
|
+
newState({ sessionId: input.sessionId, instanceId: input.instanceId, now });
|
|
467
|
+
if (current.instance_id !== input.instanceId) return current;
|
|
468
|
+
const next: ContextContinuityState = {
|
|
469
|
+
...current,
|
|
470
|
+
instance_id: input.instanceId,
|
|
471
|
+
phase: current.latest_capsule ? current.phase : "degraded",
|
|
472
|
+
compaction_completed_at: now,
|
|
473
|
+
updated_at: now,
|
|
474
|
+
degraded_reason: current.latest_capsule
|
|
475
|
+
? current.degraded_reason
|
|
476
|
+
: "post-compaction signal arrived without a continuity checkpoint",
|
|
477
|
+
};
|
|
478
|
+
writeState(coordRoot, next);
|
|
479
|
+
return next;
|
|
480
|
+
});
|
|
339
481
|
}
|
|
340
482
|
|
|
341
483
|
/** Convenience API for callers whose injection is synchronous and guaranteed. */
|
|
@@ -348,7 +490,19 @@ export function recoverContext(
|
|
|
348
490
|
const state = readContextState(coordRoot, input.sessionId)!;
|
|
349
491
|
return { state, capsule: null, briefing: "", recovered: false };
|
|
350
492
|
}
|
|
351
|
-
const recovered = completeContextRecovery(coordRoot,
|
|
493
|
+
const recovered = completeContextRecovery(coordRoot, {
|
|
494
|
+
sessionId: input.sessionId,
|
|
495
|
+
instanceId: input.instanceId,
|
|
496
|
+
checkpoint: prepared.checkpoint,
|
|
497
|
+
});
|
|
498
|
+
if (!recovered) {
|
|
499
|
+
return {
|
|
500
|
+
state: readContextState(coordRoot, input.sessionId) ?? prepared.state,
|
|
501
|
+
capsule: null,
|
|
502
|
+
briefing: "",
|
|
503
|
+
recovered: false,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
352
506
|
return {
|
|
353
507
|
state: recovered,
|
|
354
508
|
capsule: prepared.capsule,
|
|
@@ -366,8 +520,29 @@ export function readContextState(
|
|
|
366
520
|
|
|
367
521
|
export function readLatestCapsule(coordRoot: string, sessionId: string): ContinuityCapsule | null {
|
|
368
522
|
const state = readContextState(coordRoot, sessionId);
|
|
369
|
-
if (
|
|
370
|
-
|
|
523
|
+
if (typeof state?.latest_capsule !== "string" || !state.latest_capsule) return null;
|
|
524
|
+
const capsule = readJson<ContinuityCapsule>(join(coordRoot, ".harnery", state.latest_capsule));
|
|
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;
|
|
371
546
|
}
|
|
372
547
|
|
|
373
548
|
export function renderRecoveryBriefing(capsule: ContinuityCapsule, liveRepo: RepoSnapshot): string {
|
|
@@ -382,6 +557,42 @@ export function renderRecoveryBriefing(capsule: ContinuityCapsule, liveRepo: Rep
|
|
|
382
557
|
lines.push(`Files held: ${capsule.work.files_held.join(", ")}`);
|
|
383
558
|
}
|
|
384
559
|
|
|
560
|
+
const unknown: string[] = [];
|
|
561
|
+
if (!observationsComparable(capsule.repo, liveRepo)) {
|
|
562
|
+
for (const [label, repo] of [
|
|
563
|
+
["checkpoint", capsule.repo],
|
|
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");
|
|
589
|
+
if (unknown.length > 0) {
|
|
590
|
+
lines.push(
|
|
591
|
+
`Repository could not be verified against the checkpoint: ${unknown.join("; ")}. Re-check before editing.`,
|
|
592
|
+
);
|
|
593
|
+
return lines.join("\n");
|
|
594
|
+
}
|
|
595
|
+
|
|
385
596
|
const drift: string[] = [];
|
|
386
597
|
if (capsule.repo.branch && liveRepo.branch && capsule.repo.branch !== liveRepo.branch) {
|
|
387
598
|
drift.push(`branch changed ${capsule.repo.branch} -> ${liveRepo.branch}`);
|
|
@@ -405,23 +616,60 @@ export function renderRecoveryBriefing(capsule: ContinuityCapsule, liveRepo: Rep
|
|
|
405
616
|
|
|
406
617
|
export function snapshotRepo(cwd: string): RepoSnapshot {
|
|
407
618
|
const root = git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
408
|
-
if (!root)
|
|
619
|
+
if (!root.ok || !root.out.trim()) {
|
|
620
|
+
return {
|
|
621
|
+
cwd: clampString(cwd, 2_000) ?? cwd,
|
|
622
|
+
observation: "unavailable",
|
|
623
|
+
observation_detail: `top-level: ${root.ok ? "empty result" : root.reason}`,
|
|
624
|
+
dirty_paths: [],
|
|
625
|
+
};
|
|
626
|
+
}
|
|
409
627
|
const status = git(cwd, ["status", "--porcelain=v1", "--untracked-files=normal"]);
|
|
410
|
-
const
|
|
411
|
-
|
|
628
|
+
const branch = git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
629
|
+
const head = git(cwd, ["rev-parse", "HEAD"]);
|
|
630
|
+
const reads = [
|
|
631
|
+
["status", status],
|
|
632
|
+
["branch", branch],
|
|
633
|
+
["HEAD", head],
|
|
634
|
+
] as const;
|
|
635
|
+
const details = reads.flatMap(([label, read]) =>
|
|
636
|
+
!read.ok
|
|
637
|
+
? [`${label}: ${read.reason}`]
|
|
638
|
+
: label !== "status" && !read.out.trim()
|
|
639
|
+
? [`${label}: empty result`]
|
|
640
|
+
: [],
|
|
641
|
+
);
|
|
642
|
+
const dirty = status.ok
|
|
643
|
+
? status.out
|
|
412
644
|
.split("\n")
|
|
413
645
|
.map((line) => line.trimEnd())
|
|
414
646
|
.filter(Boolean)
|
|
415
|
-
.map((line) =>
|
|
647
|
+
.map((line) => line.slice(3).trim())
|
|
416
648
|
.filter(Boolean)
|
|
417
649
|
: [];
|
|
650
|
+
const truncated =
|
|
651
|
+
dirty.length > MAX_SNAPSHOT_PATHS ||
|
|
652
|
+
dirty.some((path) => path.length > MAX_SNAPSHOT_PATH_CHARS);
|
|
653
|
+
if (truncated) details.push("dirty path list is truncated");
|
|
418
654
|
return {
|
|
419
655
|
cwd: clampString(cwd, 2_000) ?? cwd,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
656
|
+
observation: reads.some(([, read]) => !read.ok && read.reason === "timed_out")
|
|
657
|
+
? "unavailable"
|
|
658
|
+
: details.length > 0
|
|
659
|
+
? "partial"
|
|
660
|
+
: "ok",
|
|
661
|
+
...(details.length > 0
|
|
662
|
+
? {
|
|
663
|
+
observation_detail: clampString(details.join("; "), MAX_OBSERVATION_DETAIL_CHARS),
|
|
664
|
+
}
|
|
665
|
+
: {}),
|
|
666
|
+
root: clampString(root.out.trim(), 2_000),
|
|
667
|
+
branch: branch.ok ? branch.out.trim() || undefined : undefined,
|
|
668
|
+
head: head.ok ? head.out.trim() || undefined : undefined,
|
|
669
|
+
dirty_paths: dirty
|
|
670
|
+
.slice(0, MAX_SNAPSHOT_PATHS)
|
|
671
|
+
.map((path) => path.slice(0, MAX_SNAPSHOT_PATH_CHARS)),
|
|
672
|
+
...(truncated ? { dirty_paths_truncated: true } : {}),
|
|
425
673
|
};
|
|
426
674
|
}
|
|
427
675
|
|
|
@@ -467,6 +715,47 @@ function newState(input: {
|
|
|
467
715
|
};
|
|
468
716
|
}
|
|
469
717
|
|
|
718
|
+
function capsuleMatchesState(
|
|
719
|
+
state: ContextContinuityState,
|
|
720
|
+
capsule: ContinuityCapsule,
|
|
721
|
+
input: { sessionId: string; instanceId: string },
|
|
722
|
+
): boolean {
|
|
723
|
+
return (
|
|
724
|
+
state.session_id === input.sessionId &&
|
|
725
|
+
state.instance_id === input.instanceId &&
|
|
726
|
+
capsule.session?.session_id === input.sessionId &&
|
|
727
|
+
capsule.session?.instance_id === input.instanceId &&
|
|
728
|
+
capsule.schema_version === CONTEXT_SCHEMA_VERSION &&
|
|
729
|
+
Number.isSafeInteger(capsule.generation) &&
|
|
730
|
+
capsule.generation > 0 &&
|
|
731
|
+
capsule.generation === state.generation &&
|
|
732
|
+
typeof capsule.capsule_id === "string" &&
|
|
733
|
+
capsule.capsule_id.length > 0 &&
|
|
734
|
+
state.latest_capsule === capsuleRelativePath(input.sessionId, capsule.generation)
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
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
|
+
|
|
470
759
|
function writeState(coordRoot: string, state: ContextContinuityState): void {
|
|
471
760
|
writeBoundedJson(statePath(coordRoot, state.session_id), state, MAX_CAPSULE_BYTES);
|
|
472
761
|
}
|
|
@@ -513,15 +802,36 @@ function readJson<T>(path: string): T | null {
|
|
|
513
802
|
}
|
|
514
803
|
}
|
|
515
804
|
|
|
516
|
-
|
|
805
|
+
type GitReadResult =
|
|
806
|
+
| { ok: true; out: string }
|
|
807
|
+
| { ok: false; reason: "not_repository" | "failed" | "timed_out" | "spawn_error" };
|
|
808
|
+
|
|
809
|
+
function git(cwd: string, args: string[]): GitReadResult {
|
|
517
810
|
try {
|
|
518
811
|
const result = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 2_000 });
|
|
519
|
-
|
|
812
|
+
if (result.error) {
|
|
813
|
+
return {
|
|
814
|
+
ok: false,
|
|
815
|
+
reason:
|
|
816
|
+
(result.error as NodeJS.ErrnoException).code === "ETIMEDOUT"
|
|
817
|
+
? "timed_out"
|
|
818
|
+
: "spawn_error",
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
if (result.status === 0) return { ok: true, out: result.stdout.trimEnd() };
|
|
822
|
+
return {
|
|
823
|
+
ok: false,
|
|
824
|
+
reason: /not a git repository/i.test(result.stderr) ? "not_repository" : "failed",
|
|
825
|
+
};
|
|
520
826
|
} catch {
|
|
521
|
-
return
|
|
827
|
+
return { ok: false, reason: "spawn_error" };
|
|
522
828
|
}
|
|
523
829
|
}
|
|
524
830
|
|
|
831
|
+
function repoObservation(value: unknown): RepoObservation {
|
|
832
|
+
return value === "ok" || value === "partial" ? value : "unavailable";
|
|
833
|
+
}
|
|
834
|
+
|
|
525
835
|
function sameMeasurement(a: ContextSample | undefined, b: ContextSample): boolean {
|
|
526
836
|
if (!a) return false;
|
|
527
837
|
return (
|