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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
3
|
+
import { monorepoRoot } from "../core/agents/index.ts";
|
|
4
|
+
import {
|
|
5
|
+
applyTurnRollback,
|
|
6
|
+
beginTurnRollback,
|
|
7
|
+
inspectTurnRollback,
|
|
8
|
+
prepareTurnRollback,
|
|
9
|
+
sealTurnRollback,
|
|
10
|
+
type TurnRollbackPlan,
|
|
11
|
+
} from "../core/turn-rollback/index.ts";
|
|
12
|
+
import { readJsonRecord } from "../core/workflow/durable-record.ts";
|
|
13
|
+
import { readWorkspaceBinding } from "../core/workflow/workspaces/state.ts";
|
|
14
|
+
|
|
15
|
+
/** Explicit turn boundaries; hooks cannot infer provider ownership or path scope. */
|
|
16
|
+
export function registerTurnRollbackCommand(
|
|
17
|
+
program: Command,
|
|
18
|
+
emit: EmitContext,
|
|
19
|
+
context?: HarneryProgramContext,
|
|
20
|
+
): void {
|
|
21
|
+
const command = program
|
|
22
|
+
.command("turn-rollback")
|
|
23
|
+
.description("Capture and review exact-path content rollback in an owned workspace");
|
|
24
|
+
type Options = {
|
|
25
|
+
binding: string;
|
|
26
|
+
turn?: string;
|
|
27
|
+
generation?: string;
|
|
28
|
+
path?: string[];
|
|
29
|
+
yes?: boolean;
|
|
30
|
+
actor?: string;
|
|
31
|
+
reason?: string;
|
|
32
|
+
plan?: string;
|
|
33
|
+
};
|
|
34
|
+
const run =
|
|
35
|
+
(operation: (coordRoot: string, options: Options) => unknown | Promise<unknown>) =>
|
|
36
|
+
async (options: Options) => {
|
|
37
|
+
try {
|
|
38
|
+
const coordRoot = context?.resolveCoordRoot?.() ?? context?.repoRoot ?? monorepoRoot();
|
|
39
|
+
if (!coordRoot) throw new Error("project coordination root is unavailable");
|
|
40
|
+
emit.config({ format: "json" });
|
|
41
|
+
const result = await operation(coordRoot, options);
|
|
42
|
+
emit.data(result);
|
|
43
|
+
if (
|
|
44
|
+
result &&
|
|
45
|
+
typeof result === "object" &&
|
|
46
|
+
"status" in result &&
|
|
47
|
+
result.status === "unknown"
|
|
48
|
+
)
|
|
49
|
+
emit.setExitCode(1);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
emit.error({ code: "turn_rollback_refused", message: String(error) });
|
|
52
|
+
emit.setExitCode(1);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const binding = (root: string, options: Options) => {
|
|
56
|
+
const value = readWorkspaceBinding(root, "local-git-worktree", options.binding);
|
|
57
|
+
if (!value) throw new Error("immutable workspace binding was not found");
|
|
58
|
+
return value;
|
|
59
|
+
};
|
|
60
|
+
command
|
|
61
|
+
.command("begin")
|
|
62
|
+
.description("Capture exact literal paths before a turn")
|
|
63
|
+
.requiredOption("--binding <id>", "Owned workspace binding ID")
|
|
64
|
+
.requiredOption("--turn <id>", "Caller turn identity")
|
|
65
|
+
.requiredOption("--generation <number>", "Increasing turn generation")
|
|
66
|
+
.requiredOption("--path <file...>", "Exact relative file paths, including planned new files")
|
|
67
|
+
.action(
|
|
68
|
+
run((root, options) =>
|
|
69
|
+
beginTurnRollback(root, {
|
|
70
|
+
binding: binding(root, options),
|
|
71
|
+
turn: { turn_id: options.turn!, generation: Number(options.generation) },
|
|
72
|
+
paths: options.path!,
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
for (const operation of ["seal", "prepare", "show"] as const) {
|
|
77
|
+
command
|
|
78
|
+
.command(`${operation} <capture-id>`)
|
|
79
|
+
.description(`${operation} an exact turn capture`)
|
|
80
|
+
.requiredOption("--binding <id>", "Owned workspace binding ID")
|
|
81
|
+
.action((id: string, options: Options) =>
|
|
82
|
+
run((root, opts) => {
|
|
83
|
+
const input = { binding: binding(root, opts), capture_id: id };
|
|
84
|
+
return operation === "seal"
|
|
85
|
+
? sealTurnRollback(root, input)
|
|
86
|
+
: operation === "prepare"
|
|
87
|
+
? prepareTurnRollback(root, input)
|
|
88
|
+
: inspectTurnRollback(root, input);
|
|
89
|
+
})(options),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
command
|
|
93
|
+
.command("apply")
|
|
94
|
+
.description("Preview a saved proposal, or apply it after exact confirmation")
|
|
95
|
+
.requiredOption("--binding <id>", "Owned workspace binding ID")
|
|
96
|
+
.requiredOption("--plan <file>", "Saved JSON proposal returned by prepare")
|
|
97
|
+
.option("--yes", "Apply this exact proposal; preview is the default")
|
|
98
|
+
.option("--actor <name>", "Who authorized the restore")
|
|
99
|
+
.option("--reason <text>", "Why this turn is being restored")
|
|
100
|
+
.action(
|
|
101
|
+
run((root, options) =>
|
|
102
|
+
applyTurnRollback(root, {
|
|
103
|
+
binding: binding(root, options),
|
|
104
|
+
plan: readJsonRecord<TurnRollbackPlan>(options.plan!, "rollback proposal"),
|
|
105
|
+
yes: options.yes,
|
|
106
|
+
actor: options.actor,
|
|
107
|
+
reason: options.reason,
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
}
|
package/src/commands/workflow.ts
CHANGED
|
@@ -73,6 +73,15 @@ interface WorkflowConfirmedMutationOpts {
|
|
|
73
73
|
|
|
74
74
|
interface WorkflowReclaimOpts extends WorkflowConfirmedMutationOpts {
|
|
75
75
|
discard?: boolean;
|
|
76
|
+
unknownLiveness?: string;
|
|
77
|
+
actor?: string;
|
|
78
|
+
reason?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface WorkflowUnlockOpts extends WorkflowConfirmedMutationOpts {
|
|
82
|
+
lockSha256?: string;
|
|
83
|
+
actor?: string;
|
|
84
|
+
reason?: string;
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
export function registerWorkflowCommand(program: Command, emit: EmitContext): void {
|
|
@@ -618,11 +627,17 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
618
627
|
workflow
|
|
619
628
|
.command("reclaim <run-id>")
|
|
620
629
|
.description(
|
|
621
|
-
"
|
|
630
|
+
"Preview reclaim of an inactive owned workspace, or confirm salvage or discard and release.",
|
|
622
631
|
)
|
|
623
632
|
.option("--yes", "Confirm that this commits or discards uncommitted work, then releases")
|
|
624
633
|
.option("--discard", "Throw the uncommitted work away instead of salvaging it")
|
|
625
|
-
.option(
|
|
634
|
+
.option(
|
|
635
|
+
"--unknown-liveness <sha256>",
|
|
636
|
+
"Recover unknown evidence pinned by the preview digest; active evidence still refuses",
|
|
637
|
+
)
|
|
638
|
+
.option("--actor <id>", "Actor responsible for an audited recovery")
|
|
639
|
+
.option("--reason <text>", "Reason for an audited recovery")
|
|
640
|
+
.option("--json", "Emit the reclaim preview or confirmed result as JSON")
|
|
626
641
|
.action(async (runId: string, opts: WorkflowReclaimOpts) => {
|
|
627
642
|
const coordRoot = resolveCoordRoot();
|
|
628
643
|
if (!coordRoot) {
|
|
@@ -632,53 +647,97 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
632
647
|
});
|
|
633
648
|
process.exit(1);
|
|
634
649
|
}
|
|
635
|
-
if (!opts.yes) {
|
|
636
|
-
emit.error({
|
|
637
|
-
code: "reclaim_confirmation_required",
|
|
638
|
-
message: opts.discard
|
|
639
|
-
? "reclaim --discard permanently destroys uncommitted work; pass --yes to confirm"
|
|
640
|
-
: "workspace reclaim commits uncommitted work to its branch and then releases the worktree; pass --yes to confirm",
|
|
641
|
-
});
|
|
642
|
-
process.exit(1);
|
|
643
|
-
}
|
|
644
650
|
try {
|
|
645
|
-
const {
|
|
651
|
+
const { createLocalGitWorktreeProvider, readWorkflowRunManifest } = await import(
|
|
646
652
|
"../core/workflow/index.ts"
|
|
647
653
|
);
|
|
648
|
-
const
|
|
649
|
-
if (
|
|
650
|
-
emit.error({ code: "workflow_reclaim_failed", message: inspection.error });
|
|
651
|
-
process.exit(1);
|
|
652
|
-
}
|
|
653
|
-
const allocation = inspection.value.allocation;
|
|
654
|
-
if (!allocation) {
|
|
654
|
+
const binding = readWorkflowRunManifest(coordRoot, runId).execution.workspace_binding;
|
|
655
|
+
if (binding?.provider.id !== "local-git-worktree") {
|
|
655
656
|
emit.error({
|
|
656
657
|
code: "workflow_reclaim_unavailable",
|
|
657
658
|
message: `run ${runId} has no provider-owned Git workspace to reclaim`,
|
|
658
659
|
});
|
|
659
660
|
process.exit(1);
|
|
660
661
|
}
|
|
661
|
-
const
|
|
662
|
-
|
|
662
|
+
const provider = createLocalGitWorktreeProvider({ coordRoot });
|
|
663
|
+
const result = await provider.reclaim(binding, {
|
|
663
664
|
mode: opts.discard ? "discard" : "salvage",
|
|
664
|
-
|
|
665
|
+
yes: opts.yes,
|
|
666
|
+
unknownLiveness: opts.unknownLiveness
|
|
667
|
+
? {
|
|
668
|
+
evidenceSha256: opts.unknownLiveness,
|
|
669
|
+
actor: opts.actor!,
|
|
670
|
+
reason: opts.reason!,
|
|
671
|
+
}
|
|
672
|
+
: undefined,
|
|
665
673
|
});
|
|
666
|
-
// Cleanup still owns removal. Reclaim only changes whether the tree it
|
|
667
|
-
// finds is dirty, so there is one audited release path, not two.
|
|
668
|
-
const provider = await builtInProviderForRun(coordRoot, runId);
|
|
669
|
-
const cleanup = await cleanupWorkspace({ coordRoot, runId, provider });
|
|
670
674
|
if (opts.json) {
|
|
671
675
|
emit.config({ format: "json" });
|
|
672
|
-
emit.data(
|
|
676
|
+
emit.data(result);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (result.action === "preview") {
|
|
680
|
+
const holders = result.lease_holder.map(
|
|
681
|
+
(holder) =>
|
|
682
|
+
`${holder.scope}: ${holder.owner_id ?? "unknown"}${holder.pid ? ` (pid ${holder.pid})` : ""}${holder.detail ? `; ${holder.detail}` : ""}`,
|
|
683
|
+
);
|
|
684
|
+
emit.text(
|
|
685
|
+
`reclaim preview: ${result.run_id}\n` +
|
|
686
|
+
`mode: ${result.mode}\nliveness: ${result.liveness}\nownership: ${result.ownership}\n` +
|
|
687
|
+
`evidence sha256: ${result.evidence_sha256}\n` +
|
|
688
|
+
`lease holder: ${holders.length > 0 ? holders.join(", ") : "none observed"}\n` +
|
|
689
|
+
`dirty paths: ${result.dirty_paths === null ? "unknown" : result.dirty_paths.length > 0 ? result.dirty_paths.join(", ") : "none"}\n` +
|
|
690
|
+
(result.ownership_detail ? `ownership detail: ${result.ownership_detail}\n` : "") +
|
|
691
|
+
`Pass --yes to confirm ${result.mode} and release after all checks pass.\n`,
|
|
692
|
+
);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
emit.text(
|
|
696
|
+
`reclaim ${result.preparation.action}: ${result.preparation.detail}\n` +
|
|
697
|
+
`workspace cleanup ${result.cleanup.status}: ${result.cleanup.binding_id}\n`,
|
|
698
|
+
);
|
|
699
|
+
} catch (error) {
|
|
700
|
+
emit.error({
|
|
701
|
+
code:
|
|
702
|
+
error instanceof Error && "code" in error && typeof error.code === "string"
|
|
703
|
+
? error.code
|
|
704
|
+
: "workflow_reclaim_failed",
|
|
705
|
+
message: error instanceof Error ? error.message : String(error),
|
|
706
|
+
});
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
workflow
|
|
712
|
+
.command("unlock <run-id>")
|
|
713
|
+
.description(
|
|
714
|
+
"Preview or explicitly retire one exact resume lock whose local owner is proven dead.",
|
|
715
|
+
)
|
|
716
|
+
.option("--lock-sha256 <sha256>", "Exact lock identity digest returned by the preview")
|
|
717
|
+
.option("--actor <id>", "Actor responsible for the recovery")
|
|
718
|
+
.option("--reason <text>", "Reason for the recovery")
|
|
719
|
+
.option("--yes", "Confirm removal after dead-owner and exact-lock checks")
|
|
720
|
+
.option("--json", "Emit the recovery preview or result as JSON")
|
|
721
|
+
.action(async (runId: string, opts: WorkflowUnlockOpts) => {
|
|
722
|
+
try {
|
|
723
|
+
const coordRoot = resolveCoordRoot();
|
|
724
|
+
if (!coordRoot) throw new Error("no .harnery/ coordination root found");
|
|
725
|
+
const { recoverWorkflowResumeLease } = await import("../core/workflow/index.ts");
|
|
726
|
+
const result = recoverWorkflowResumeLease(coordRoot, runId, opts);
|
|
727
|
+
if (opts.json) {
|
|
728
|
+
emit.config({ format: "json" });
|
|
729
|
+
emit.data(result);
|
|
673
730
|
return;
|
|
674
731
|
}
|
|
675
732
|
emit.text(
|
|
676
|
-
`
|
|
677
|
-
|
|
733
|
+
`resume lock ${result.action}: ${runId}\nowner: ${result.owner_state}\nlock sha256: ${result.lock_sha256 ?? "absent"}\n` +
|
|
734
|
+
(result.action === "preview"
|
|
735
|
+
? "Pass --lock-sha256, --actor, --reason and --yes to confirm exact dead-owner recovery.\n"
|
|
736
|
+
: `recovery: ${result.recovery_id}\n`),
|
|
678
737
|
);
|
|
679
738
|
} catch (error) {
|
|
680
739
|
emit.error({
|
|
681
|
-
code: "
|
|
740
|
+
code: "workflow_resume_unlock_failed",
|
|
682
741
|
message: error instanceof Error ? error.message : String(error),
|
|
683
742
|
});
|
|
684
743
|
process.exit(1);
|
|
@@ -265,7 +265,27 @@ export function parsePidmapRowStartToken(row: string): string | undefined {
|
|
|
265
265
|
* a test can hold this one against `processStartToken` and fail on drift.
|
|
266
266
|
*/
|
|
267
267
|
export function pidStartToken(pid: number): string | null {
|
|
268
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
268
269
|
const forced = process.env.HARNERY_PID_PROBE;
|
|
270
|
+
if (process.platform === "win32" && forced !== "procfs" && forced !== "ps") {
|
|
271
|
+
try {
|
|
272
|
+
const out = spawnSync(
|
|
273
|
+
"powershell.exe",
|
|
274
|
+
[
|
|
275
|
+
"-NoLogo",
|
|
276
|
+
"-NoProfile",
|
|
277
|
+
"-NonInteractive",
|
|
278
|
+
"-Command",
|
|
279
|
+
`$process = [Diagnostics.Process]::GetProcessById(${pid}); try { 'w' + $process.StartTime.ToFileTimeUtc().ToString([Globalization.CultureInfo]::InvariantCulture) } finally { $process.Dispose() }`,
|
|
280
|
+
],
|
|
281
|
+
{ encoding: "utf8", timeout: 10_000, windowsHide: true },
|
|
282
|
+
);
|
|
283
|
+
const token = String(out.stdout ?? "").trim();
|
|
284
|
+
return out.status === 0 && /^w\d+$/.test(token) ? token : null;
|
|
285
|
+
} catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
269
289
|
const useProcfs = forced === "procfs" || (forced !== "ps" && existsSync("/proc/self/stat"));
|
|
270
290
|
// One machine, one probe. Falling back to the other on a read failure would
|
|
271
291
|
// answer in the wrong dialect and read as a recycled pid.
|
|
@@ -88,6 +88,7 @@ interface LiveAuthorityBaseV3 {
|
|
|
88
88
|
nativeSessionId: string;
|
|
89
89
|
adapter: Adapter;
|
|
90
90
|
observationId?: string;
|
|
91
|
+
expectedGenerationId?: `gen_${string}`;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
@@ -648,6 +649,13 @@ function recordLiveAuthority<S extends CoordinationAuthoritySignalV3>(
|
|
|
648
649
|
platform: livePlatformV3(),
|
|
649
650
|
expected_prior_state_digest: coordinationAuthorityStateDigestV3(expected),
|
|
650
651
|
desired_state_digest: coordinationAuthorityStateDigestV3(desired),
|
|
652
|
+
expected_generation_id:
|
|
653
|
+
input.expectedGenerationId ??
|
|
654
|
+
((subject === input.owner
|
|
655
|
+
? expected.v3_generation_id
|
|
656
|
+
: readLiveCoordinationRow(input.coordRoot, input.owner)?.v3_generation_id) as
|
|
657
|
+
| `gen_${string}`
|
|
658
|
+
| undefined),
|
|
651
659
|
reconciler: {
|
|
652
660
|
readStateDigest: () => {
|
|
653
661
|
const heartbeat = readHeartbeat(input.coordRoot, subject);
|
|
@@ -681,6 +689,12 @@ export function liveCoordinationWriteModeV3(coordRoot: string): EventV3WriteMode
|
|
|
681
689
|
}
|
|
682
690
|
|
|
683
691
|
function requireHeartbeat(input: LiveAuthorityBaseV3, owner: string): Heartbeat {
|
|
692
|
+
if (
|
|
693
|
+
input.expectedGenerationId &&
|
|
694
|
+
readLiveCoordinationRow(input.coordRoot, owner)?.v3_generation_id !== input.expectedGenerationId
|
|
695
|
+
) {
|
|
696
|
+
throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
|
|
697
|
+
}
|
|
684
698
|
const heartbeat = ensureLiveCoordinationHeartbeat(
|
|
685
699
|
input.coordRoot,
|
|
686
700
|
owner,
|
|
@@ -688,6 +702,9 @@ function requireHeartbeat(input: LiveAuthorityBaseV3, owner: string): Heartbeat
|
|
|
688
702
|
input.adapter,
|
|
689
703
|
);
|
|
690
704
|
if (!heartbeat) throw new LiveCoordinationAuthorityV3Error(`heartbeat_missing:${owner}`);
|
|
705
|
+
if (input.expectedGenerationId && heartbeat.v3_generation_id !== input.expectedGenerationId) {
|
|
706
|
+
throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
|
|
707
|
+
}
|
|
691
708
|
return heartbeat;
|
|
692
709
|
}
|
|
693
710
|
|
|
@@ -5,6 +5,7 @@ import { normalizeNativeIdV3 } from "../events/v3/canonical.ts";
|
|
|
5
5
|
import type { EventV3 } from "../events/v3/contract.ts";
|
|
6
6
|
import { readEventV3ControlState } from "../events/v3/control.ts";
|
|
7
7
|
import { fingerprintContextV3 } from "../events/v3/fingerprint-keys.ts";
|
|
8
|
+
import { withGenerationCustodyV3 } from "../events/v3/generation-custody.ts";
|
|
8
9
|
import {
|
|
9
10
|
liveInstanceIdV3,
|
|
10
11
|
livePlatformV3,
|
|
@@ -25,17 +26,34 @@ interface LiveLifecycleObservationBaseV3 {
|
|
|
25
26
|
observedAt?: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
interface LiveSweepObservationV3Input extends LiveLifecycleObservationBaseV3 {
|
|
30
|
+
expectedGenerationId: `gen_${string}` | null;
|
|
31
|
+
observation: "stale_heartbeat" | "killed" | "unparseable_heartbeat" | "missing_timestamp";
|
|
32
|
+
ageMs: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
export function recordLiveSweepObservationV3(
|
|
29
|
-
input:
|
|
30
|
-
observation: "stale_heartbeat" | "killed" | "unparseable_heartbeat" | "missing_timestamp";
|
|
31
|
-
ageMs: number;
|
|
32
|
-
},
|
|
36
|
+
input: LiveSweepObservationV3Input,
|
|
33
37
|
): LiveLifecycleObservationV3Result {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
input = { ...input };
|
|
39
|
+
// Reject stale observations before preparing durable custody state. The
|
|
40
|
+
// in-custody checks cover replacement while acquiring custody and reentry.
|
|
41
|
+
requireSweepHookState(input);
|
|
42
|
+
return withGenerationCustodyV3(input.coordRoot, [liveInstanceIdV3(input.owner)], () => {
|
|
43
|
+
requireSweepHookState(input);
|
|
44
|
+
return recordObservation(
|
|
45
|
+
input,
|
|
46
|
+
"lifecycle.sweep_observed",
|
|
47
|
+
{
|
|
48
|
+
subject_instance_id: liveInstanceIdV3(input.owner),
|
|
49
|
+
observation: input.observation,
|
|
50
|
+
provisional: true,
|
|
51
|
+
age_ms: Math.max(0, Math.floor(input.ageMs)),
|
|
52
|
+
},
|
|
53
|
+
() => {
|
|
54
|
+
requireSweepHookState(input);
|
|
55
|
+
},
|
|
56
|
+
);
|
|
39
57
|
});
|
|
40
58
|
}
|
|
41
59
|
|
|
@@ -44,11 +62,16 @@ export function recordLiveResumeObservationV3(
|
|
|
44
62
|
): LiveLifecycleObservationV3Result {
|
|
45
63
|
const normalized = withAttestedAdapter(input);
|
|
46
64
|
const hook = requireHookState(normalized);
|
|
47
|
-
return recordObservation(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
return recordObservation(
|
|
66
|
+
normalized,
|
|
67
|
+
"session.resumed",
|
|
68
|
+
{
|
|
69
|
+
prior_generation_id: hook.generation_id,
|
|
70
|
+
continuity: "native",
|
|
71
|
+
evidence_reference: "heartbeat_recreated",
|
|
72
|
+
},
|
|
73
|
+
() => {},
|
|
74
|
+
);
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
function recordObservation<T extends "lifecycle.sweep_observed" | "session.resumed">(
|
|
@@ -66,6 +89,7 @@ function recordObservation<T extends "lifecycle.sweep_observed" | "session.resum
|
|
|
66
89
|
continuity: "native";
|
|
67
90
|
evidence_reference: string;
|
|
68
91
|
},
|
|
92
|
+
beforeWrite: () => void,
|
|
69
93
|
): LiveLifecycleObservationV3Result {
|
|
70
94
|
const route = resolveLiveEventLedgerRouteV3(input.coordRoot);
|
|
71
95
|
if (route.state === "blocked") throw new LiveCoordinationAuthorityV3Error(route.reason);
|
|
@@ -121,6 +145,7 @@ function recordObservation<T extends "lifecycle.sweep_observed" | "session.resum
|
|
|
121
145
|
monotonic_ns: process.hrtime.bigint().toString(),
|
|
122
146
|
};
|
|
123
147
|
const event = buildEventV3(eventType, { ...common, payload } as never) as EventV3;
|
|
148
|
+
beforeWrite();
|
|
124
149
|
const durability = writeEventV3(input.coordRoot, event, {
|
|
125
150
|
expectedGenesisId: control.genesis.event.payload.genesis_id as `gex_${string}`,
|
|
126
151
|
});
|
|
@@ -130,6 +155,19 @@ function recordObservation<T extends "lifecycle.sweep_observed" | "session.resum
|
|
|
130
155
|
return { state: "recorded", event };
|
|
131
156
|
}
|
|
132
157
|
|
|
158
|
+
function requireSweepHookState(input: LiveSweepObservationV3Input): void {
|
|
159
|
+
if (
|
|
160
|
+
typeof input.expectedGenerationId !== "string" ||
|
|
161
|
+
!/^gen_[a-zA-Z0-9._-]+$/.test(input.expectedGenerationId)
|
|
162
|
+
) {
|
|
163
|
+
throw new LiveCoordinationAuthorityV3Error("hook_generation_not_joinable");
|
|
164
|
+
}
|
|
165
|
+
const hook = requireHookState(withAttestedAdapter(input));
|
|
166
|
+
if (hook.generation_id !== input.expectedGenerationId) {
|
|
167
|
+
throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
133
171
|
function withAttestedAdapter<T extends LiveLifecycleObservationBaseV3>(input: T): T {
|
|
134
172
|
const adapter = liveCoordinationAdapterV3(input.coordRoot, input.owner);
|
|
135
173
|
if (!adapter) return input;
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* shown to them, because nothing surfaces another session's journal.
|
|
10
10
|
*
|
|
11
11
|
* This module addresses the durable persona name instead of the instance, so a
|
|
12
|
-
* message can be queued before its recipient exists.
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
12
|
+
* message can be queued before its recipient exists. The recipient's next
|
|
13
|
+
* SessionStart or prompt can drain the queue for injection. A drain does not
|
|
14
|
+
* confirm that the adapter emitted the context or that anyone read it.
|
|
15
15
|
*
|
|
16
16
|
* Storage: `.harnery/mailbox/<slug>.jsonl`, one JSON record per line, appended
|
|
17
|
-
* by senders and removed by an atomic claim on the recipient's side.
|
|
18
|
-
* records are appended to `.harnery/mailbox/delivered/<slug>.jsonl` for audit
|
|
17
|
+
* by senders and removed by an atomic claim on the recipient's side. Drained
|
|
18
|
+
* records are appended to `.harnery/mailbox/delivered/<slug>.jsonl` for audit,
|
|
19
|
+
* with injection explicitly unconfirmed.
|
|
19
20
|
*
|
|
20
21
|
* Related but distinct: `core/inbox` is an instance-keyed transport with a
|
|
21
22
|
* storage-framework service behind it. This mailbox is name-keyed, file-only,
|
|
@@ -48,8 +49,8 @@ export const MAILBOX_MESSAGE_SCHEMA = "harnery.mailbox-message/v1" as const;
|
|
|
48
49
|
export const MAX_BODY_BYTES = 4000;
|
|
49
50
|
/** Pending messages held for one name before senders are refused. */
|
|
50
51
|
export const MAX_PENDING = 25;
|
|
51
|
-
/**
|
|
52
|
-
const
|
|
52
|
+
/** Drain-archive ceiling; the file is truncated from the head past this. */
|
|
53
|
+
const MAX_ARCHIVE_BYTES = 256 * 1024;
|
|
53
54
|
|
|
54
55
|
export interface MailboxMessageV1 {
|
|
55
56
|
schema: typeof MAILBOX_MESSAGE_SCHEMA;
|
|
@@ -104,7 +105,7 @@ export function mailboxPath(coordRoot: string, name: string): string {
|
|
|
104
105
|
return join(mailboxDir(coordRoot), `${slugFor(name)}.jsonl`);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
function
|
|
108
|
+
function archivePath(coordRoot: string, name: string): string {
|
|
108
109
|
return join(mailboxDir(coordRoot), "delivered", `${slugFor(name)}.jsonl`);
|
|
109
110
|
}
|
|
110
111
|
|
|
@@ -243,10 +244,10 @@ export function peekMailbox(coordRoot: string, name: string): MailboxMessageV1[]
|
|
|
243
244
|
/**
|
|
244
245
|
* Claim and return every pending message for a name.
|
|
245
246
|
*
|
|
246
|
-
* The claim is an atomic rename, so two sessions sharing a name cannot
|
|
247
|
+
* The claim is an atomic rename, so two sessions sharing a name cannot claim
|
|
247
248
|
* the same message twice: whichever rename wins owns the batch. Claimed records
|
|
248
|
-
* are
|
|
249
|
-
*
|
|
249
|
+
* are archived before the claim file is removed. The archive records only the
|
|
250
|
+
* drain; output, recipient read, and turn completion remain unconfirmed.
|
|
250
251
|
*/
|
|
251
252
|
export function drainMailbox(coordRoot: string, name: string): MailboxMessageV1[] {
|
|
252
253
|
let path: string;
|
|
@@ -279,12 +280,20 @@ export function drainMailbox(coordRoot: string, name: string): MailboxMessageV1[
|
|
|
279
280
|
|
|
280
281
|
if (messages.length > 0) {
|
|
281
282
|
try {
|
|
282
|
-
const archive =
|
|
283
|
+
const archive = archivePath(coordRoot, name);
|
|
283
284
|
mkdirSync(join(mailboxDir(coordRoot), "delivered"), { recursive: true });
|
|
284
|
-
const
|
|
285
|
+
const drainedAt = new Date().toISOString();
|
|
285
286
|
appendFileSync(
|
|
286
287
|
archive,
|
|
287
|
-
`${messages
|
|
288
|
+
`${messages
|
|
289
|
+
.map((m) =>
|
|
290
|
+
JSON.stringify({
|
|
291
|
+
...m,
|
|
292
|
+
drained_at: drainedAt,
|
|
293
|
+
delivery_status: "injection-unconfirmed",
|
|
294
|
+
}),
|
|
295
|
+
)
|
|
296
|
+
.join("\n")}\n`,
|
|
288
297
|
"utf8",
|
|
289
298
|
);
|
|
290
299
|
trimArchive(archive);
|
|
@@ -336,7 +345,7 @@ function sweepStaleClaims(coordRoot: string): void {
|
|
|
336
345
|
|
|
337
346
|
function trimArchive(path: string): void {
|
|
338
347
|
try {
|
|
339
|
-
if (statSync(path).size <=
|
|
348
|
+
if (statSync(path).size <= MAX_ARCHIVE_BYTES) return;
|
|
340
349
|
const lines = readFileSync(path, "utf8").split("\n").filter(Boolean);
|
|
341
350
|
const keep = lines.slice(Math.floor(lines.length / 2));
|
|
342
351
|
writeFileSync(path, `${keep.join("\n")}\n`, "utf8");
|
|
@@ -354,8 +363,8 @@ export function formatMailboxDelivery(
|
|
|
354
363
|
const bin = resolveBinName(coordRoot);
|
|
355
364
|
const header =
|
|
356
365
|
messages.length === 1
|
|
357
|
-
? `Message from a peer agent (
|
|
358
|
-
: `${messages.length} messages from peer agents (
|
|
366
|
+
? `Message from a peer agent (prepared for injection, recorded in your journal):`
|
|
367
|
+
: `${messages.length} messages from peer agents (prepared for injection, recorded in your journal):`;
|
|
359
368
|
const body = messages
|
|
360
369
|
.map((m) => {
|
|
361
370
|
const when = m.created_at.replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
@@ -48,9 +48,9 @@ import { join } from "node:path";
|
|
|
48
48
|
import { coordEnv } from "../../../lib/env.ts";
|
|
49
49
|
import { endOfTurnStatusCommand, hostPromptReminder, resolveBinName } from "../../config.ts";
|
|
50
50
|
import type { PressureWorkloadClass } from "../../diagnostics/contract.ts";
|
|
51
|
+
import { formatPeerInbox, peerInbox } from "../../inbox/peer-messages.ts";
|
|
51
52
|
import { type RemoteMachine, readRemoteMachines } from "../../presence/index.ts";
|
|
52
53
|
import { readResourceStatus } from "../../resources/status.ts";
|
|
53
|
-
import { drainMailbox, formatMailboxDelivery } from "../mailbox.ts";
|
|
54
54
|
import { sessionNameDisplayBlock, sessionNameDisplayPending } from "../session-name-display.ts";
|
|
55
55
|
import { heartbeatPath } from "../state/heartbeat-reader.ts";
|
|
56
56
|
import {
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
} from "../state/live-coordination-view.ts";
|
|
60
60
|
import { forkRelationLabels } from "../state/names.ts";
|
|
61
61
|
import type { AgentActivity, TaskState } from "../state/session-state.ts";
|
|
62
|
-
import { formatPendingCouncils
|
|
62
|
+
import { formatPendingCouncils } from "./session-context.ts";
|
|
63
63
|
|
|
64
64
|
interface HeartbeatRow {
|
|
65
65
|
instance_id?: string;
|
|
@@ -103,6 +103,8 @@ export interface PromptContextOpts {
|
|
|
103
103
|
* being bounced into a retry. Not hash-deduped, same rationale as the
|
|
104
104
|
* status footer. */
|
|
105
105
|
turnRitualNudge?: string;
|
|
106
|
+
/** The emitter acknowledges exactly these IDs only after output succeeds. */
|
|
107
|
+
onInboxPrepared?: (messageIds: readonly string[]) => void;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
/**
|
|
@@ -132,16 +134,12 @@ export function renderPromptContext(opts: PromptContextOpts): string {
|
|
|
132
134
|
// skipped without it.
|
|
133
135
|
const resolvedName = agentName ?? readLiveCoordinationRow(coordRoot, instanceId)?.name;
|
|
134
136
|
|
|
135
|
-
// 1.
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (delivered.length > 0) {
|
|
142
|
-
recordDeliveredMessages(instanceId, delivered);
|
|
143
|
-
sections.push(formatMailboxDelivery(coordRoot, delivered));
|
|
144
|
-
}
|
|
137
|
+
// 1. Instance-bound messages. Rendering is read-only; failed output leaves
|
|
138
|
+
// this same batch pending rather than losing it before a prompt sees it.
|
|
139
|
+
const delivered = peerInbox(coordRoot).prepareSurface(instanceId);
|
|
140
|
+
if (delivered.length > 0) {
|
|
141
|
+
opts.onInboxPrepared?.(delivered.map(({ message_id }) => message_id));
|
|
142
|
+
sections.push(formatPeerInbox(coordRoot, delivered));
|
|
145
143
|
}
|
|
146
144
|
|
|
147
145
|
// 2. Peer table with hash dedup.
|