harnery 0.43.0 → 0.44.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/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/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 +16 -1
- 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/workflow.js +24 -30
- 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 -0
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +41 -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/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 +9 -1
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +47 -27
- 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/servers/index.d.ts +24 -13
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +132 -92
- 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 +3 -9
- 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 +9 -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/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 +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -1
- 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/run-state.d.ts +3 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +31 -27
- 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 +270 -6
- 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 +35 -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.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +21 -13
- 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/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 +7 -1
- 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/governor.ts +23 -10
- package/src/commands/inbox.ts +52 -2
- package/src/commands/rm.ts +43 -4
- package/src/commands/servers.ts +16 -4
- package/src/commands/supervisor.ts +7 -2
- package/src/commands/toc.ts +14 -6
- package/src/commands/workflow.ts +30 -30
- 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 +57 -0
- package/src/core/agents/state/stale-sweep.ts +247 -82
- package/src/core/agents/state/unchanged-file.ts +456 -0
- 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 +71 -24
- package/src/core/resources/status.ts +57 -8
- package/src/core/servers/index.ts +177 -91
- package/src/core/servers/tunnels.ts +3 -11
- 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 +88 -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/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 +0 -1
- package/src/core/workflow/live-session-v3.ts +146 -5
- package/src/core/workflow/proof.ts +134 -26
- package/src/core/workflow/run-state.ts +31 -29
- 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 +315 -5
- package/src/core/workflow/workspaces/ownership.ts +232 -0
- package/src/core/workflow/workspaces/reclaim.ts +58 -0
- package/src/core/workflow/workspaces/recovery.ts +25 -13
- 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/zip/index.ts +483 -0
|
@@ -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.
|
|
@@ -12,9 +12,8 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { resolveBinName, resolveHooksSetupHint } from "../../config.ts";
|
|
14
14
|
import { harneryVersion, loadAdapterWiring } from "../../hooks/adapter/wiring.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { formatPeerInbox, peerInbox } from "../../inbox/peer-messages.ts";
|
|
16
16
|
import { readRemoteMachines } from "../../presence/index.ts";
|
|
17
|
-
import { drainMailbox, formatMailboxDelivery, type MailboxMessageV1 } from "../mailbox.ts";
|
|
18
17
|
import { readLiveCoordinationRows } from "../state/live-coordination-view.ts";
|
|
19
18
|
import { forkRelationLabels, readForkState } from "../state/names.ts";
|
|
20
19
|
import type { AgentActivity, TaskState } from "../state/session-state.ts";
|
|
@@ -42,6 +41,8 @@ export interface RenderOpts {
|
|
|
42
41
|
/** Adapter label rendered in parens after the self-name (e.g. "Cursor", "Codex").
|
|
43
42
|
* Claude Code omits it. */
|
|
44
43
|
platformLabel?: string;
|
|
44
|
+
/** The emitter acknowledges exactly these IDs only after output succeeds. */
|
|
45
|
+
onInboxPrepared?: (messageIds: readonly string[]) => void;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
|
@@ -92,15 +93,12 @@ export function renderSessionContext(opts: RenderOpts): string {
|
|
|
92
93
|
);
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
// 3.
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
recordDeliveredMessages(instanceId, delivered);
|
|
102
|
-
messages.push(formatMailboxDelivery(coordRoot, delivered));
|
|
103
|
-
}
|
|
96
|
+
// 3. Rendering never consumes a message. The native emitter acknowledges the
|
|
97
|
+
// prepared instance-bound batch only after it has flushed the output.
|
|
98
|
+
const delivered = peerInbox(coordRoot).prepareSurface(instanceId);
|
|
99
|
+
if (delivered.length > 0) {
|
|
100
|
+
opts.onInboxPrepared?.(delivered.map(({ message_id }) => message_id));
|
|
101
|
+
messages.push(formatPeerInbox(coordRoot, delivered));
|
|
104
102
|
}
|
|
105
103
|
|
|
106
104
|
// 4. Council invites
|
|
@@ -147,27 +145,6 @@ export function renderSessionContext(opts: RenderOpts): string {
|
|
|
147
145
|
return messages.join("\n\n");
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
/**
|
|
151
|
-
* Write delivered peer messages into the recipient's own journal, so they
|
|
152
|
-
* survive context compaction the way any other coordination breadcrumb does.
|
|
153
|
-
* A message the sender already journaled (the recipient was live when it was
|
|
154
|
-
* sent) is skipped to avoid a duplicate entry.
|
|
155
|
-
*/
|
|
156
|
-
export function recordDeliveredMessages(
|
|
157
|
-
instanceId: string,
|
|
158
|
-
messages: readonly MailboxMessageV1[],
|
|
159
|
-
): void {
|
|
160
|
-
for (const m of messages) {
|
|
161
|
-
if (m.journaled) continue;
|
|
162
|
-
try {
|
|
163
|
-
appendEntry(instanceId, "handoff", `from agent-${m.from_name}: ${m.body}`);
|
|
164
|
-
} catch {
|
|
165
|
-
// A journal write failure must never cost the delivery itself; the
|
|
166
|
-
// message is already in the rendered context the model reads.
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
148
|
/** One-line-per-agent view of sessions on other machines (presence refs). */
|
|
172
149
|
function formatRemoteMachines(coordRoot: string): string {
|
|
173
150
|
try {
|
|
@@ -424,9 +424,9 @@ function sessionNameBlock(coordRoot: string, instanceId: string): VerdictResult
|
|
|
424
424
|
} catch {
|
|
425
425
|
// The ledger evidence still authorizes the block; the cache is UX only.
|
|
426
426
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
427
|
+
// The first post-mint reply was already rejected. Repeating its title later
|
|
428
|
+
// cannot change that evidence; the JSON retry result opens a new window.
|
|
429
|
+
const instruction = `Run \`${resolveBinName(coordRoot)} agents suggest-name --json\` as your only tool call to create a fresh verification boundary, then display its suggested_session_name verbatim by itself in a fenced code block${name ? `: ${name}` : "."}`;
|
|
430
430
|
return {
|
|
431
431
|
allow: false,
|
|
432
432
|
exit_code: 2,
|
|
@@ -24,6 +24,11 @@ import {
|
|
|
24
24
|
writeFileSync,
|
|
25
25
|
} from "node:fs";
|
|
26
26
|
import { dirname, join } from "node:path";
|
|
27
|
+
import {
|
|
28
|
+
assertCurrentGenerationV3,
|
|
29
|
+
withGenerationCustodyV3,
|
|
30
|
+
} from "../../events/v3/generation-custody.ts";
|
|
31
|
+
import { liveInstanceIdV3 } from "../../events/v3/live-routing.ts";
|
|
27
32
|
import { canonicalClaimPath } from "../claim-path.ts";
|
|
28
33
|
import { type Heartbeat, heartbeatPath, readHeartbeat } from "./heartbeat-reader.ts";
|
|
29
34
|
import type { TaskState } from "./session-state.ts";
|
|
@@ -63,12 +68,15 @@ function mutate(
|
|
|
63
68
|
instanceId: string,
|
|
64
69
|
fn: (hb: Heartbeat) => Heartbeat,
|
|
65
70
|
): Heartbeat | null {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
return withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(instanceId)], () => {
|
|
72
|
+
const hb = readHeartbeat(coordRoot, instanceId);
|
|
73
|
+
if (!hb) return null;
|
|
74
|
+
const next = fn(hb);
|
|
75
|
+
next.last_heartbeat = nowIsoSeconds();
|
|
76
|
+
assertCurrentGenerationV3(coordRoot, liveInstanceIdV3(instanceId), hb.v3_generation_id);
|
|
77
|
+
atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(next, null, 2));
|
|
78
|
+
return next;
|
|
79
|
+
});
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
export function setTask(coordRoot: string, instanceId: string, task: string): Heartbeat | null {
|
|
@@ -100,12 +108,15 @@ export function setIdentityCache(
|
|
|
100
108
|
name: string,
|
|
101
109
|
agentId: string,
|
|
102
110
|
): Heartbeat | null {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
return withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(instanceId)], () => {
|
|
112
|
+
const heartbeat = readHeartbeat(coordRoot, instanceId);
|
|
113
|
+
if (!heartbeat) return null;
|
|
114
|
+
heartbeat.name = name;
|
|
115
|
+
heartbeat.agent_id = agentId;
|
|
116
|
+
assertCurrentGenerationV3(coordRoot, liveInstanceIdV3(instanceId), heartbeat.v3_generation_id);
|
|
117
|
+
atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(heartbeat, null, 2));
|
|
118
|
+
return heartbeat;
|
|
119
|
+
});
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
/** Restore the pool-assigned display name on a generation that was
|
|
@@ -328,6 +339,12 @@ export function clearCoordinationCache(coordRoot: string, instanceId: string): b
|
|
|
328
339
|
}
|
|
329
340
|
|
|
330
341
|
export function healPidmap(coordRoot: string, instanceId: string, pid: number): void {
|
|
342
|
+
withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(instanceId)], () =>
|
|
343
|
+
healPidmapUnderCustody(coordRoot, instanceId, pid),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function healPidmapUnderCustody(coordRoot: string, instanceId: string, pid: number): void {
|
|
331
348
|
const dir = join(coordRoot, ".harnery", "pid-map");
|
|
332
349
|
mkdirSync(dir, { recursive: true });
|
|
333
350
|
const hb = readHeartbeat(coordRoot, instanceId);
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
readCoordinationViewV3,
|
|
6
6
|
requireAuthoritySafeCoordinationViewV3,
|
|
7
7
|
} from "../../events/v3/coordination-view.ts";
|
|
8
|
+
import {
|
|
9
|
+
assertCurrentGenerationV3,
|
|
10
|
+
withGenerationCustodyV3,
|
|
11
|
+
} from "../../events/v3/generation-custody.ts";
|
|
8
12
|
import { liveInstanceIdV3, resolveLiveEventLedgerRouteV3 } from "../../events/v3/live-routing.ts";
|
|
9
13
|
import { type Heartbeat, readHeartbeat } from "./heartbeat-reader.ts";
|
|
10
14
|
import {
|
|
@@ -64,6 +68,26 @@ function materializeLiveCoordinationHeartbeat(
|
|
|
64
68
|
_adapter: Adapter,
|
|
65
69
|
model: string | undefined,
|
|
66
70
|
force: boolean,
|
|
71
|
+
): Heartbeat | null {
|
|
72
|
+
return withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(nativeInstanceId)], () =>
|
|
73
|
+
materializeHeartbeatUnderCustody(
|
|
74
|
+
coordRoot,
|
|
75
|
+
nativeInstanceId,
|
|
76
|
+
nativeSessionId,
|
|
77
|
+
_adapter,
|
|
78
|
+
model,
|
|
79
|
+
force,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function materializeHeartbeatUnderCustody(
|
|
85
|
+
coordRoot: string,
|
|
86
|
+
nativeInstanceId: string,
|
|
87
|
+
nativeSessionId: string,
|
|
88
|
+
_adapter: Adapter,
|
|
89
|
+
model: string | undefined,
|
|
90
|
+
force: boolean,
|
|
67
91
|
): Heartbeat | null {
|
|
68
92
|
const route = resolveLiveEventLedgerRouteV3(coordRoot);
|
|
69
93
|
if (route.state === "blocked") throw new Error(`event_v3_coordination_view:${route.reason}`);
|
|
@@ -133,6 +157,11 @@ function materializeLiveCoordinationHeartbeat(
|
|
|
133
157
|
v3_projection_event_id: generation.last_event_id,
|
|
134
158
|
v3_task_state: generation.task_state === "set" ? "set" : "cleared",
|
|
135
159
|
};
|
|
160
|
+
assertCurrentGenerationV3(
|
|
161
|
+
coordRoot,
|
|
162
|
+
liveInstanceIdV3(nativeInstanceId),
|
|
163
|
+
generation.generation_id,
|
|
164
|
+
);
|
|
136
165
|
writeHeartbeatCache(coordRoot, nativeInstanceId, materialized);
|
|
137
166
|
return materialized;
|
|
138
167
|
}
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
writeFileSync,
|
|
25
25
|
} from "node:fs";
|
|
26
26
|
import { dirname, join } from "node:path";
|
|
27
|
+
import { withGenerationCustodyV3 } from "../../events/v3/generation-custody.ts";
|
|
28
|
+
import { liveInstanceIdV3 } from "../../events/v3/live-routing.ts";
|
|
27
29
|
import { checkPidToken, processStartToken } from "./proc-start.ts";
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -126,6 +128,17 @@ export function writePidmapRow(
|
|
|
126
128
|
pid: number,
|
|
127
129
|
instanceId: string,
|
|
128
130
|
platform: string,
|
|
131
|
+
): void {
|
|
132
|
+
withGenerationCustodyV3(coordRoot, [liveInstanceIdV3(instanceId)], () =>
|
|
133
|
+
writePidmapRowUnderCustody(coordRoot, pid, instanceId, platform),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function writePidmapRowUnderCustody(
|
|
138
|
+
coordRoot: string,
|
|
139
|
+
pid: number,
|
|
140
|
+
instanceId: string,
|
|
141
|
+
platform: string,
|
|
129
142
|
): void {
|
|
130
143
|
const dir = join(coordRoot, ".harnery", "pid-map");
|
|
131
144
|
const path = join(dir, String(pid));
|
|
@@ -160,6 +160,63 @@ export function processStartToken(pid: number): string | null {
|
|
|
160
160
|
return startTokenProbe() === "procfs" ? procfsStartToken(pid) : psStartToken(pid);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
export type ProcessGenerationVerdict = "same" | "different" | "unknown";
|
|
164
|
+
|
|
165
|
+
type ReadProcessStartToken = (pid: number) => string | null;
|
|
166
|
+
|
|
167
|
+
/** A missing or unreadable token never establishes process identity. */
|
|
168
|
+
export function verifyProcessGeneration(
|
|
169
|
+
pid: number,
|
|
170
|
+
recordedToken: string | null | undefined,
|
|
171
|
+
readToken: ReadProcessStartToken = processStartToken,
|
|
172
|
+
): ProcessGenerationVerdict {
|
|
173
|
+
if (
|
|
174
|
+
!Number.isInteger(pid) ||
|
|
175
|
+
pid <= 0 ||
|
|
176
|
+
typeof recordedToken !== "string" ||
|
|
177
|
+
!recordedToken.trim()
|
|
178
|
+
)
|
|
179
|
+
return "unknown";
|
|
180
|
+
// A bootless Linux token cannot identify a process across machine boots.
|
|
181
|
+
if (/^l\d+$/.test(recordedToken)) return "unknown";
|
|
182
|
+
try {
|
|
183
|
+
const currentToken = readToken(pid);
|
|
184
|
+
if (typeof currentToken !== "string" || !currentToken.trim()) return "unknown";
|
|
185
|
+
if (/^l\d+$/.test(currentToken)) return "unknown";
|
|
186
|
+
return currentToken === recordedToken ? "same" : "different";
|
|
187
|
+
} catch {
|
|
188
|
+
return "unknown";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface ProcessGenerationSignalDependencies {
|
|
193
|
+
readToken?: ReadProcessStartToken;
|
|
194
|
+
kill?: (pid: number, signal: NodeJS.Signals | number) => unknown;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Recheck the process generation immediately before sending the signal.
|
|
199
|
+
*
|
|
200
|
+
* The process can still exit and its PID can be reused between the token read
|
|
201
|
+
* and the signal. Eliminating that race requires a generation-bound OS handle.
|
|
202
|
+
*/
|
|
203
|
+
export function signalIfSameGeneration(
|
|
204
|
+
pid: number,
|
|
205
|
+
recordedToken: string | null | undefined,
|
|
206
|
+
signal: NodeJS.Signals | number,
|
|
207
|
+
dependencies: ProcessGenerationSignalDependencies = {},
|
|
208
|
+
): { sent: boolean; verdict: ProcessGenerationVerdict } {
|
|
209
|
+
const kill = dependencies.kill ?? process.kill;
|
|
210
|
+
const verdict = verifyProcessGeneration(pid, recordedToken, dependencies.readToken);
|
|
211
|
+
if (verdict !== "same") return { sent: false, verdict };
|
|
212
|
+
try {
|
|
213
|
+
kill(pid, signal);
|
|
214
|
+
return { sent: true, verdict };
|
|
215
|
+
} catch {
|
|
216
|
+
return { sent: false, verdict };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
163
220
|
/**
|
|
164
221
|
* Do two tokens describe the same run?
|
|
165
222
|
*
|