harnery 0.44.0 → 0.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +44 -0
- 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 +0 -1
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +0 -11
- package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +7 -28
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +14 -25
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +0 -5
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +0 -5
- package/dist/core/agents/state/proc-start.d.ts +5 -6
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +13 -9
- 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 +70 -180
- package/dist/core/artifacts/activity.d.ts +4 -1
- package/dist/core/artifacts/activity.d.ts.map +1 -1
- package/dist/core/artifacts/activity.js +8 -2
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +38 -12
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +0 -29
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +64 -77
- package/dist/core/hooks/cli.js +78 -48
- package/dist/core/resources/service.d.ts +2 -16
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +1 -78
- 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 +4 -0
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +21 -3
- package/dist/core/storage/durable-history.js +56 -22
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +73 -17
- package/dist/core/supervisor/service.d.ts +2 -4
- package/dist/core/supervisor/service.d.ts.map +1 -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 +21 -65
- package/dist/core/workflow/index.d.ts +4 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -1
- package/dist/core/workflow/live-session-v3.d.ts +0 -6
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +5 -126
- package/dist/core/workflow/proof.d.ts +4 -4
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +4 -12
- 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 +19 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +113 -8
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +0 -19
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +0 -13
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +0 -19
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +0 -14
- package/dist/core/workflow/types.d.ts +11 -54
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +70 -14
- package/dist/core/workflow/workspaces/reclaim.d.ts +6 -1
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- 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 +24 -0
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +0 -4
- package/package.json +1 -1
- package/src/commands/workflow.ts +59 -0
- package/src/core/agents/coord-client.ts +20 -0
- package/src/core/agents/live-authority-v3.ts +0 -17
- package/src/core/agents/live-lifecycle-v3.ts +14 -52
- package/src/core/agents/state/heartbeat-writer.ts +12 -29
- package/src/core/agents/state/live-coordination-writer.ts +0 -29
- package/src/core/agents/state/pidmap.ts +0 -13
- package/src/core/agents/state/proc-start.ts +14 -11
- package/src/core/agents/state/process-handle.ts +165 -0
- package/src/core/agents/state/stale-sweep.ts +82 -247
- package/src/core/artifacts/activity.ts +9 -2
- package/src/core/artifacts/index.ts +45 -12
- package/src/core/events/v3/authority-outbox.ts +0 -56
- package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
- package/src/core/events/v3/producers/recorder.ts +68 -94
- package/src/core/hooks/cli.ts +85 -47
- package/src/core/resources/service.ts +3 -88
- package/src/core/semantic/service.ts +2 -8
- package/src/core/servers/index.ts +30 -3
- package/src/core/storage/durable-history.ts +52 -20
- package/src/core/storage/segments.ts +79 -17
- package/src/core/supervisor/service.ts +2 -3
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +21 -73
- package/src/core/workflow/index.ts +12 -1
- package/src/core/workflow/live-session-v3.ts +5 -146
- package/src/core/workflow/proof.ts +5 -21
- package/src/core/workflow/recovery-audit.ts +181 -0
- package/src/core/workflow/run-state.ts +137 -8
- package/src/core/workflow/spawn-claude.ts +0 -18
- package/src/core/workflow/spawn-codex.ts +0 -13
- package/src/core/workflow/spawn-cursor.ts +0 -18
- package/src/core/workflow/spawn-opencode.ts +0 -14
- package/src/core/workflow/types.ts +11 -41
- package/src/core/workflow/workspaces/local-git.ts +86 -15
- package/src/core/workflow/workspaces/reclaim.ts +4 -0
- package/src/core/workflow/workspaces/recovery.ts +23 -0
- package/src/lib/identities/assume.ts +0 -4
- package/dist/core/agents/state/unchanged-file.d.ts +0 -44
- package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
- package/dist/core/agents/state/unchanged-file.js +0 -390
- package/dist/core/events/v3/generation-custody.d.ts +0 -5
- package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
- package/dist/core/events/v3/generation-custody.js +0 -50
- package/dist/core/workflow/failure-evidence.d.ts +0 -16
- package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
- package/dist/core/workflow/failure-evidence.js +0 -114
- package/src/core/agents/state/unchanged-file.ts +0 -456
- package/src/core/events/v3/generation-custody.ts +0 -59
- package/src/core/workflow/failure-evidence.ts +0 -141
|
@@ -33,7 +33,6 @@ import { markObservedClockRegressionV3 } from "../clock-order.ts";
|
|
|
33
33
|
import type { EventV3 } from "../contract.ts";
|
|
34
34
|
import { type EventV3WriteMode, readEventV3ControlState } from "../control.ts";
|
|
35
35
|
import { fingerprintContextV3 } from "../fingerprint-keys.ts";
|
|
36
|
-
import { assertCurrentGenerationV3, withGenerationCustodyV3 } from "../generation-custody.ts";
|
|
37
36
|
import { clockIdV3, spanIdV3 } from "../ids.ts";
|
|
38
37
|
import { closeSpanStateV3, type OpenSpanStateV3, openSpanStateV3 } from "../span-state.ts";
|
|
39
38
|
import { EVENT_V3_LEDGER_RELATIVE_ROOT } from "../writer.ts";
|
|
@@ -104,7 +103,6 @@ export interface RecordCoordinationAuthorityV3Input<
|
|
|
104
103
|
bridge?: "codex-wsl";
|
|
105
104
|
expected_prior_state_digest: `sha256:${string}`;
|
|
106
105
|
desired_state_digest: `sha256:${string}`;
|
|
107
|
-
expected_generation_id?: `gen_${string}`;
|
|
108
106
|
monotonic_ns?: string;
|
|
109
107
|
observed_at?: string;
|
|
110
108
|
reconciler: AuthorityReconcilerV3;
|
|
@@ -131,16 +129,6 @@ export type RecordCoordinationAuthorityV3Result =
|
|
|
131
129
|
*/
|
|
132
130
|
export function recordCoordinationAuthorityV3<S extends CoordinationAuthoritySignalV3>(
|
|
133
131
|
input: RecordCoordinationAuthorityV3Input<S>,
|
|
134
|
-
): RecordCoordinationAuthorityV3Result {
|
|
135
|
-
return withGenerationCustodyV3(
|
|
136
|
-
input.coordRoot,
|
|
137
|
-
[input.actor_instance_id, input.subject_instance_id],
|
|
138
|
-
() => recordCoordinationAuthorityWithCustodyV3(input),
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function recordCoordinationAuthorityWithCustodyV3<S extends CoordinationAuthoritySignalV3>(
|
|
143
|
-
input: RecordCoordinationAuthorityV3Input<S>,
|
|
144
132
|
): RecordCoordinationAuthorityV3Result {
|
|
145
133
|
const control = readEventV3ControlState(input.coordRoot);
|
|
146
134
|
if (control.state !== input.mode) return { state: "gate_closed", reason: control.state };
|
|
@@ -153,8 +141,6 @@ function recordCoordinationAuthorityWithCustodyV3<S extends CoordinationAuthorit
|
|
|
153
141
|
if (
|
|
154
142
|
!hook ||
|
|
155
143
|
hook.terminal ||
|
|
156
|
-
(input.expected_generation_id !== undefined &&
|
|
157
|
-
hook.generation_id !== input.expected_generation_id) ||
|
|
158
144
|
hook.instance_id !== input.actor_instance_id ||
|
|
159
145
|
!hook.last_event_id
|
|
160
146
|
) {
|
|
@@ -342,7 +328,6 @@ function recordCoordinationAuthorityWithCustodyV3<S extends CoordinationAuthorit
|
|
|
342
328
|
mutation: normalized.mutation,
|
|
343
329
|
event: normalized.event,
|
|
344
330
|
});
|
|
345
|
-
assertCurrentGenerationV3(input.coordRoot, state.actor_instance_id, state.generation_id);
|
|
346
331
|
if (input.signal === "wait-ended" && waitId) delete state.open_waits[waitId];
|
|
347
332
|
state.pending = { source_id: sourceId, transaction };
|
|
348
333
|
publishCoordinationState(path, state);
|
|
@@ -43,7 +43,6 @@ import { markObservedClockRegressionV3 } from "../clock-order.ts";
|
|
|
43
43
|
import type { EventV3 } from "../contract.ts";
|
|
44
44
|
import { type EventV3WriteMode, readEventV3ControlState } from "../control.ts";
|
|
45
45
|
import { fingerprintContextV3 } from "../fingerprint-keys.ts";
|
|
46
|
-
import { withGenerationCustodyV3 } from "../generation-custody.ts";
|
|
47
46
|
import {
|
|
48
47
|
attestationIdV3,
|
|
49
48
|
clockIdV3,
|
|
@@ -659,17 +658,6 @@ function processHookSignalLocked(
|
|
|
659
658
|
input: RecordHookSignalV3Input,
|
|
660
659
|
sessionHash: `hid_${string}`,
|
|
661
660
|
path: string,
|
|
662
|
-
): RecordHookSignalV3Result {
|
|
663
|
-
return withGenerationCustodyV3(input.coordRoot, [input.instance_id], () =>
|
|
664
|
-
processHookSignalWithCustodyLocked(control, input, sessionHash, path),
|
|
665
|
-
);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function processHookSignalWithCustodyLocked(
|
|
669
|
-
control: OpenHookControlStateV3,
|
|
670
|
-
input: RecordHookSignalV3Input,
|
|
671
|
-
sessionHash: `hid_${string}`,
|
|
672
|
-
path: string,
|
|
673
661
|
): RecordHookSignalV3Result {
|
|
674
662
|
const behavior = adapterBehavior(input.adapter);
|
|
675
663
|
const rootId = control.genesis.event.scope.root_id as `root_${string}`;
|
|
@@ -3221,14 +3209,6 @@ function suppressClosedSpanSignal(
|
|
|
3221
3209
|
*/
|
|
3222
3210
|
export function recordApprovedSessionEndV3(
|
|
3223
3211
|
input: RecordApprovedSessionEndV3Input,
|
|
3224
|
-
): RecordApprovedSessionEndV3Result {
|
|
3225
|
-
return withGenerationCustodyV3(input.coordRoot, [input.instance_id], () =>
|
|
3226
|
-
recordApprovedSessionEndWithCustodyV3(input),
|
|
3227
|
-
);
|
|
3228
|
-
}
|
|
3229
|
-
|
|
3230
|
-
function recordApprovedSessionEndWithCustodyV3(
|
|
3231
|
-
input: RecordApprovedSessionEndV3Input,
|
|
3232
3212
|
): RecordApprovedSessionEndV3Result {
|
|
3233
3213
|
const control = readEventV3ControlState(input.coordRoot);
|
|
3234
3214
|
if (control.state !== input.mode) {
|
|
@@ -3576,81 +3556,77 @@ export function reanchorArchivedHookProducersV3(
|
|
|
3576
3556
|
for (const archived of listArchivedHookProducerStatesV3(input.archivedEpoch)) {
|
|
3577
3557
|
const previous = archived.state;
|
|
3578
3558
|
if (previous.terminal || !previous.started_event_id) continue;
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3559
|
+
const path = producerStatePath(input.coordRoot, previous.adapter, archived.sessionHash);
|
|
3560
|
+
const lease = acquireStateLeaseWithRetry(input.coordRoot, path, 40);
|
|
3561
|
+
if (!lease) {
|
|
3562
|
+
throw new Error(`event_v3_rotation_reanchor_busy:${previous.instance_id}`);
|
|
3563
|
+
}
|
|
3564
|
+
try {
|
|
3565
|
+
if (existsSync(path)) {
|
|
3566
|
+
const current = readProducerState(path);
|
|
3567
|
+
if (current.epoch_genesis_id === genesisId) continue;
|
|
3584
3568
|
}
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3569
|
+
const sourceEvent = `${previous.adapter}.epoch-rotation-reanchor`;
|
|
3570
|
+
const rotationInput: RecordHookSignalV3Input = {
|
|
3571
|
+
coordRoot: input.coordRoot,
|
|
3572
|
+
mode: input.mode,
|
|
3573
|
+
signal: "session-start",
|
|
3574
|
+
payload: {
|
|
3575
|
+
raw: {},
|
|
3576
|
+
...(previous.current_native_turn_id ? { turn_id: previous.current_native_turn_id } : {}),
|
|
3577
|
+
...(adapterBehavior(previous.adapter).tracksExecutionMode && previous.cursor_mode
|
|
3578
|
+
? { cursor_mode: previous.cursor_mode }
|
|
3579
|
+
: {}),
|
|
3580
|
+
},
|
|
3581
|
+
adapter: previous.adapter,
|
|
3582
|
+
instance_id: previous.instance_id,
|
|
3583
|
+
producer_id: "prd_agent-hook",
|
|
3584
|
+
build_id: input.build_id,
|
|
3585
|
+
platform: input.platform,
|
|
3586
|
+
observed_at: observedAt,
|
|
3587
|
+
writerOptions: { expectedGenesisId: genesisId },
|
|
3588
|
+
};
|
|
3589
|
+
const state = newProducerState(
|
|
3590
|
+
rotationInput,
|
|
3591
|
+
previous.session_id,
|
|
3592
|
+
epochId,
|
|
3593
|
+
boundaryEventId as `evt_${string}`,
|
|
3594
|
+
genesisId,
|
|
3595
|
+
);
|
|
3596
|
+
const sessionStart = buildMidFlightSessionStart(rotationInput, state, rootId);
|
|
3597
|
+
if (sessionStart.event_type !== "session.started") {
|
|
3598
|
+
throw new Error("rotation re-anchor session start could not be normalized");
|
|
3599
|
+
}
|
|
3600
|
+
sessionStart.provenance = {
|
|
3601
|
+
...sessionStart.provenance,
|
|
3602
|
+
source_event: sourceEvent,
|
|
3603
|
+
attestation: "derived",
|
|
3604
|
+
confidence: "high",
|
|
3605
|
+
attribution: {
|
|
3606
|
+
method: "session_env",
|
|
3607
|
+
state: "verified",
|
|
3608
|
+
subject_instance_id: state.instance_id,
|
|
3609
|
+
},
|
|
3610
|
+
};
|
|
3611
|
+
sessionStart.payload.resume = {
|
|
3612
|
+
state: "unknown",
|
|
3613
|
+
reason: "epoch_rotation_reanchor",
|
|
3614
|
+
};
|
|
3615
|
+
assertEventV3(sessionStart);
|
|
3616
|
+
commitEventLocked(rotationInput, state, path, sessionStart);
|
|
3617
|
+
sessions += 1;
|
|
3618
|
+
|
|
3619
|
+
if (previous.current_turn_id && previous.current_turn_span) {
|
|
3620
|
+
commitMidFlightTurnStart(rotationInput, state, path, rootId, {
|
|
3625
3621
|
source_event: sourceEvent,
|
|
3626
|
-
attestation: "derived",
|
|
3627
3622
|
confidence: "high",
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
subject_instance_id: state.instance_id,
|
|
3632
|
-
},
|
|
3633
|
-
};
|
|
3634
|
-
sessionStart.payload.resume = {
|
|
3635
|
-
state: "unknown",
|
|
3636
|
-
reason: "epoch_rotation_reanchor",
|
|
3637
|
-
};
|
|
3638
|
-
assertEventV3(sessionStart);
|
|
3639
|
-
commitEventLocked(rotationInput, state, path, sessionStart);
|
|
3640
|
-
sessions += 1;
|
|
3641
|
-
|
|
3642
|
-
if (previous.current_turn_id && previous.current_turn_span) {
|
|
3643
|
-
commitMidFlightTurnStart(rotationInput, state, path, rootId, {
|
|
3644
|
-
source_event: sourceEvent,
|
|
3645
|
-
confidence: "high",
|
|
3646
|
-
attribution_method: "session_env",
|
|
3647
|
-
});
|
|
3648
|
-
turns += 1;
|
|
3649
|
-
}
|
|
3650
|
-
} finally {
|
|
3651
|
-
lease.release();
|
|
3623
|
+
attribution_method: "session_env",
|
|
3624
|
+
});
|
|
3625
|
+
turns += 1;
|
|
3652
3626
|
}
|
|
3653
|
-
}
|
|
3627
|
+
} finally {
|
|
3628
|
+
lease.release();
|
|
3629
|
+
}
|
|
3654
3630
|
}
|
|
3655
3631
|
return { sessions, turns };
|
|
3656
3632
|
}
|
|
@@ -3888,9 +3864,7 @@ export function readJoinableHookProducerStateV3(
|
|
|
3888
3864
|
instanceId: `inst_${string}`,
|
|
3889
3865
|
): HookProducerStateV3 | undefined {
|
|
3890
3866
|
const direct = readHookProducerStateV3(coordRoot, adapter, nativeSessionId);
|
|
3891
|
-
|
|
3892
|
-
// not permission to adopt a replacement published under the same instance.
|
|
3893
|
-
if (direct?.instance_id === instanceId) return direct.terminal ? undefined : direct;
|
|
3867
|
+
if (direct?.instance_id === instanceId && !direct.terminal) return direct;
|
|
3894
3868
|
const byInstance = readHookProducerStateByInstanceV3(coordRoot, instanceId);
|
|
3895
3869
|
return byInstance?.adapter === adapter && !byInstance.terminal ? byInstance : undefined;
|
|
3896
3870
|
}
|
package/src/core/hooks/cli.ts
CHANGED
|
@@ -844,6 +844,9 @@ async function main(): Promise<number> {
|
|
|
844
844
|
if (eventName === "runtime-context-retry") {
|
|
845
845
|
return runRuntimeContextRetryWorker(coordRoot, adapter, extra);
|
|
846
846
|
}
|
|
847
|
+
if (eventName === SESSION_MAINTENANCE_EVENT) {
|
|
848
|
+
return runSessionMaintenanceWorker(coordRoot, adapter);
|
|
849
|
+
}
|
|
847
850
|
const ledgerRoute = resolveLiveEventLedgerRouteV3(coordRoot);
|
|
848
851
|
|
|
849
852
|
// Always log a breadcrumb, useful when an event_type maps to null or owner
|
|
@@ -1274,7 +1277,7 @@ function recordContextTelemetry(run: HookRun): void {
|
|
|
1274
1277
|
}
|
|
1275
1278
|
|
|
1276
1279
|
async function handleSessionStarted(run: HookRun): Promise<void> {
|
|
1277
|
-
const { coordRoot, adapter,
|
|
1280
|
+
const { coordRoot, adapter, payload, owner, sessionId, data } = run;
|
|
1278
1281
|
// A configured backup runs out of process and is freshness-gated per host.
|
|
1279
1282
|
// Startup never waits for restic or a remote provider; the child owns no
|
|
1280
1283
|
// hook stdio, so the harness is not held open until it finishes. A failed
|
|
@@ -1285,52 +1288,10 @@ async function handleSessionStarted(run: HookRun): Promise<void> {
|
|
|
1285
1288
|
} catch (err) {
|
|
1286
1289
|
logError(coordRoot, err, { phase: "backup-schedule" });
|
|
1287
1290
|
}
|
|
1288
|
-
//
|
|
1289
|
-
//
|
|
1290
|
-
//
|
|
1291
|
-
|
|
1292
|
-
imageJanitor(coordRoot);
|
|
1293
|
-
// Session start is the only caller of the artifact janitor and the storage
|
|
1294
|
-
// maintenance pass, so both load here instead of on every tool call.
|
|
1295
|
-
const { autoCleanArtifacts } = await import("../artifacts/index.ts");
|
|
1296
|
-
const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.ts");
|
|
1297
|
-
const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import(
|
|
1298
|
-
"../storage/maintenance-providers.ts"
|
|
1299
|
-
);
|
|
1300
|
-
// Effect: throttled hourly artifact sweep (see autoCleanArtifacts). It
|
|
1301
|
-
// deletes expired workspaces, plus unheld workspaces over the per-bundle
|
|
1302
|
-
// ceiling or repository budget once they have been idle past
|
|
1303
|
-
// artifacts.idle_grace_hours. Each deletion is rechecked first and recorded
|
|
1304
|
-
// in .harnery/artifact-deletions.jsonl. Best-effort like every janitor
|
|
1305
|
-
// here: a failure logs and never blocks session start.
|
|
1306
|
-
try {
|
|
1307
|
-
autoCleanArtifacts(coordRoot);
|
|
1308
|
-
} catch (err) {
|
|
1309
|
-
logError(coordRoot, err, { phase: "artifact-auto-clean" });
|
|
1310
|
-
}
|
|
1311
|
-
// Effect: apply the same daily throttle to closed V3 epochs. The newest
|
|
1312
|
-
// recovery epochs remain protected by the archive retention policy.
|
|
1313
|
-
try {
|
|
1314
|
-
autoCleanEventV3Archives(coordRoot);
|
|
1315
|
-
} catch (err) {
|
|
1316
|
-
logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
|
|
1317
|
-
}
|
|
1318
|
-
// Plan one bounded, claim-first global slice from cached pressure only.
|
|
1319
|
-
// Existing owner janitors above remain the only active executors until
|
|
1320
|
-
// each provider receives a separate production activation.
|
|
1321
|
-
try {
|
|
1322
|
-
await runAutomaticMaintenancePass(
|
|
1323
|
-
createAutomaticMaintenanceComposition(coordRoot, {
|
|
1324
|
-
journal: () => {
|
|
1325
|
-
if (behavior.journalMaintenance) journalJanitor(coordRoot);
|
|
1326
|
-
},
|
|
1327
|
-
images: () => imageJanitor(coordRoot),
|
|
1328
|
-
artifacts: () => autoCleanArtifacts(coordRoot),
|
|
1329
|
-
}),
|
|
1330
|
-
);
|
|
1331
|
-
} catch (err) {
|
|
1332
|
-
logError(coordRoot, err, { phase: "storage-maintenance-auto" });
|
|
1333
|
-
}
|
|
1291
|
+
// Storage maintenance can walk large stores. It runs in a detached worker so
|
|
1292
|
+
// session start emits its context without waiting for it; an adapter that
|
|
1293
|
+
// stops a slow hook would otherwise drop that context entirely.
|
|
1294
|
+
scheduleSessionMaintenance(coordRoot, adapter);
|
|
1334
1295
|
let recovery: PreparedContextRecovery | null = null;
|
|
1335
1296
|
if (payload?.source === "compact") {
|
|
1336
1297
|
try {
|
|
@@ -1814,6 +1775,83 @@ async function handleCompletedResponse(run: CompletedResponseRun): Promise<numbe
|
|
|
1814
1775
|
return 0;
|
|
1815
1776
|
}
|
|
1816
1777
|
|
|
1778
|
+
const SESSION_MAINTENANCE_EVENT = "session-maintenance";
|
|
1779
|
+
|
|
1780
|
+
function scheduleSessionMaintenance(coordRoot: string, adapter: Adapter): void {
|
|
1781
|
+
const modulePath = process.argv[1];
|
|
1782
|
+
if (!modulePath) return;
|
|
1783
|
+
try {
|
|
1784
|
+
const child = spawn(
|
|
1785
|
+
process.execPath,
|
|
1786
|
+
[modulePath, SESSION_MAINTENANCE_EVENT, "--adapter", adapter],
|
|
1787
|
+
{
|
|
1788
|
+
cwd: coordRoot,
|
|
1789
|
+
detached: true,
|
|
1790
|
+
stdio: "ignore",
|
|
1791
|
+
env: childEnv(coordRoot),
|
|
1792
|
+
},
|
|
1793
|
+
);
|
|
1794
|
+
child.unref();
|
|
1795
|
+
} catch (err) {
|
|
1796
|
+
logError(coordRoot, err, { phase: "session-maintenance-spawn" });
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Session-start storage maintenance, run out of the hook's process. Every step
|
|
1802
|
+
* is best-effort and throttled by its own stamp: a failure logs and never
|
|
1803
|
+
* reaches the session.
|
|
1804
|
+
*/
|
|
1805
|
+
async function runSessionMaintenanceWorker(
|
|
1806
|
+
coordRoot: string,
|
|
1807
|
+
adapter: Adapter | null,
|
|
1808
|
+
): Promise<number> {
|
|
1809
|
+
const journalMaintenance = adapter ? adapterBehavior(adapter).journalMaintenance : false;
|
|
1810
|
+
// Effect (journal-maintaining adapters): prune stale journal archives +
|
|
1811
|
+
// sweep orphans. The recovery cue is read separately at session start.
|
|
1812
|
+
if (journalMaintenance) journalJanitor(coordRoot);
|
|
1813
|
+
imageJanitor(coordRoot);
|
|
1814
|
+
const { autoCleanArtifacts } = await import("../artifacts/index.ts");
|
|
1815
|
+
const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.ts");
|
|
1816
|
+
const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import(
|
|
1817
|
+
"../storage/maintenance-providers.ts"
|
|
1818
|
+
);
|
|
1819
|
+
// Effect: throttled hourly artifact sweep (see autoCleanArtifacts). It
|
|
1820
|
+
// deletes expired workspaces, plus unheld workspaces over the per-bundle
|
|
1821
|
+
// ceiling or repository budget once they have been idle past
|
|
1822
|
+
// artifacts.idle_grace_hours. Each deletion is rechecked first and recorded
|
|
1823
|
+
// in .harnery/artifact-deletions.jsonl.
|
|
1824
|
+
try {
|
|
1825
|
+
autoCleanArtifacts(coordRoot);
|
|
1826
|
+
} catch (err) {
|
|
1827
|
+
logError(coordRoot, err, { phase: "artifact-auto-clean" });
|
|
1828
|
+
}
|
|
1829
|
+
// Effect: apply the same daily throttle to closed V3 epochs. The newest
|
|
1830
|
+
// recovery epochs remain protected by the archive retention policy.
|
|
1831
|
+
try {
|
|
1832
|
+
autoCleanEventV3Archives(coordRoot);
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
|
|
1835
|
+
}
|
|
1836
|
+
// Plan one bounded, claim-first global slice from cached pressure only.
|
|
1837
|
+
// Existing owner janitors above remain the only active executors until
|
|
1838
|
+
// each provider receives a separate production activation.
|
|
1839
|
+
try {
|
|
1840
|
+
await runAutomaticMaintenancePass(
|
|
1841
|
+
createAutomaticMaintenanceComposition(coordRoot, {
|
|
1842
|
+
journal: () => {
|
|
1843
|
+
if (journalMaintenance) journalJanitor(coordRoot);
|
|
1844
|
+
},
|
|
1845
|
+
images: () => imageJanitor(coordRoot),
|
|
1846
|
+
artifacts: () => autoCleanArtifacts(coordRoot),
|
|
1847
|
+
}),
|
|
1848
|
+
);
|
|
1849
|
+
} catch (err) {
|
|
1850
|
+
logError(coordRoot, err, { phase: "storage-maintenance-auto" });
|
|
1851
|
+
}
|
|
1852
|
+
return 0;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1817
1855
|
function scheduleRuntimeContextRetry(
|
|
1818
1856
|
coordRoot: string,
|
|
1819
1857
|
adapter: Adapter,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
2
|
import {
|
|
4
3
|
closeSync,
|
|
@@ -10,9 +9,10 @@ import {
|
|
|
10
9
|
unlinkSync,
|
|
11
10
|
writeFileSync,
|
|
12
11
|
} from "node:fs";
|
|
13
|
-
import { hostname
|
|
12
|
+
import { hostname } from "node:os";
|
|
14
13
|
import { resolve } from "node:path";
|
|
15
14
|
import {
|
|
15
|
+
type ProcessGenerationSignalDependencies,
|
|
16
16
|
type ProcessGenerationVerdict,
|
|
17
17
|
processStartToken,
|
|
18
18
|
signalIfSameGeneration,
|
|
@@ -55,80 +55,6 @@ export interface RunResourceServiceInput {
|
|
|
55
55
|
wait?: (milliseconds: number) => Promise<void>;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export type EnsureResourceServiceResult =
|
|
59
|
-
| { state: "running" | "started"; status: ResourceServiceStatus }
|
|
60
|
-
| { state: "unsupported" | "unavailable"; status: ResourceServiceStatus; error: string };
|
|
61
|
-
|
|
62
|
-
export async function spawnResourceService(
|
|
63
|
-
coordRootRaw: string,
|
|
64
|
-
options: { intervalMs?: number } = {},
|
|
65
|
-
): Promise<ResourceServiceStatus> {
|
|
66
|
-
const coordRoot = resolve(coordRootRaw);
|
|
67
|
-
const current = readResourceServiceStatus(coordRoot);
|
|
68
|
-
if (current.liveness === "unknown") throw unknownResourceOwnerError();
|
|
69
|
-
if (current.liveness === "running") {
|
|
70
|
-
throw new Error(`resource observer is already running under pid ${current.record?.pid}`);
|
|
71
|
-
}
|
|
72
|
-
const intervalMs = normalizeInterval(options.intervalMs);
|
|
73
|
-
const paths = resourcePaths(coordRoot);
|
|
74
|
-
assertResourceLeaseAvailable(paths.lease);
|
|
75
|
-
mkdirSync(paths.root, { recursive: true, mode: stateDirMode() });
|
|
76
|
-
const harnBin = new URL("../../../bin/harn", import.meta.url).pathname;
|
|
77
|
-
if (!existsSync(harnBin)) throw new Error(`cannot find harn executable at ${harnBin}`);
|
|
78
|
-
let spawnError: Error | undefined;
|
|
79
|
-
const child = spawn(
|
|
80
|
-
harnBin,
|
|
81
|
-
["resources", "service", "daemon", "--root", coordRoot, "--interval-ms", String(intervalMs)],
|
|
82
|
-
{
|
|
83
|
-
cwd: coordRoot,
|
|
84
|
-
detached: true,
|
|
85
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
86
|
-
env: {
|
|
87
|
-
...process.env,
|
|
88
|
-
HARNERY_COORD_ROOT_OVERRIDE: coordRoot,
|
|
89
|
-
HARNERY_OUTPUT_SESSION_TEE: "0",
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
child.once("error", (error) => {
|
|
94
|
-
spawnError = error;
|
|
95
|
-
});
|
|
96
|
-
child.unref();
|
|
97
|
-
const deadline = Date.now() + 5_000;
|
|
98
|
-
while (Date.now() < deadline) {
|
|
99
|
-
await delay(50);
|
|
100
|
-
if (spawnError) throw spawnError;
|
|
101
|
-
const status = readResourceServiceStatus(coordRoot);
|
|
102
|
-
if (status.running && status.record?.pid === child.pid) return status;
|
|
103
|
-
if (child.exitCode !== null) break;
|
|
104
|
-
}
|
|
105
|
-
throw new Error(`resource observer failed to start; inspect ${paths.service}`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function ensureResourceServiceRunning(
|
|
109
|
-
coordRoot: string,
|
|
110
|
-
): Promise<EnsureResourceServiceResult> {
|
|
111
|
-
const status = readResourceServiceStatus(coordRoot);
|
|
112
|
-
if (status.running) return { state: "running", status };
|
|
113
|
-
if (platform() !== "linux") {
|
|
114
|
-
return {
|
|
115
|
-
state: "unsupported",
|
|
116
|
-
status,
|
|
117
|
-
error: `resource observer does not yet support ${platform()}`,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
const started = await spawnResourceService(coordRoot);
|
|
122
|
-
return { state: "started", status: started };
|
|
123
|
-
} catch (error) {
|
|
124
|
-
return {
|
|
125
|
-
state: "unavailable",
|
|
126
|
-
status: readResourceServiceStatus(coordRoot),
|
|
127
|
-
error: error instanceof Error ? error.message : String(error),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
58
|
export interface ResourceServiceStopResult extends ResourceServiceStatus {
|
|
133
59
|
signal_sent: boolean;
|
|
134
60
|
signal_verdict: ProcessGenerationVerdict;
|
|
@@ -136,9 +62,7 @@ export interface ResourceServiceStopResult extends ResourceServiceStatus {
|
|
|
136
62
|
|
|
137
63
|
export function requestResourceServiceStop(
|
|
138
64
|
coordRootRaw: string,
|
|
139
|
-
dependencies: ResourceProcessDependencies & {
|
|
140
|
-
kill?: (pid: number, signal: NodeJS.Signals | number) => unknown;
|
|
141
|
-
} = {},
|
|
65
|
+
dependencies: ResourceProcessDependencies & ProcessGenerationSignalDependencies = {},
|
|
142
66
|
): ResourceServiceStopResult {
|
|
143
67
|
const coordRoot = resolve(coordRootRaw);
|
|
144
68
|
const paths = resourcePaths(coordRoot);
|
|
@@ -329,15 +253,6 @@ function leaseIsLive(lease: ResourceServiceLease): ResourceServiceStatus["livene
|
|
|
329
253
|
return resourceProcessLiveness(lease).liveness;
|
|
330
254
|
}
|
|
331
255
|
|
|
332
|
-
function assertResourceLeaseAvailable(path: string): void {
|
|
333
|
-
if (!existsSync(path)) return;
|
|
334
|
-
const lease = readLease(path);
|
|
335
|
-
const liveness = lease ? leaseIsLive(lease) : "unknown";
|
|
336
|
-
if (liveness === "unknown") throw unknownResourceOwnerError();
|
|
337
|
-
if (liveness === "running")
|
|
338
|
-
throw new Error(`resource observer lease is held by pid ${lease?.pid}`);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
256
|
function unknownResourceOwnerError(): Error {
|
|
342
257
|
return new Error(
|
|
343
258
|
`resource observer process identity is unknown; run ${resolveBinName()} supervisor stop to write the durable stop request before starting again`,
|
|
@@ -225,19 +225,13 @@ export async function ensureSemanticServiceRunning(
|
|
|
225
225
|
|
|
226
226
|
export function requestSemanticServiceStop(coordRootRaw: string): SemanticServiceStatus {
|
|
227
227
|
const coordRoot = resolve(coordRootRaw);
|
|
228
|
-
const status = readSemanticServiceStatus(coordRoot);
|
|
229
228
|
mkdirSync(semanticPaths(coordRoot).root, { recursive: true, mode: stateDirMode() });
|
|
230
229
|
writePrivateJsonAtomic(semanticPaths(coordRoot).stop, {
|
|
231
230
|
requested_at: new Date().toISOString(),
|
|
232
231
|
requested_by_pid: process.pid,
|
|
233
232
|
});
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
process.kill(status.record.pid, "SIGTERM");
|
|
237
|
-
} catch {
|
|
238
|
-
// The durable stop request remains for a racing or restarted daemon.
|
|
239
|
-
}
|
|
240
|
-
}
|
|
233
|
+
// This record has no generation witness. The durable request is the stop
|
|
234
|
+
// authority; a numeric PID cannot authorize an additional signal.
|
|
241
235
|
return readSemanticServiceStatus(coordRoot);
|
|
242
236
|
}
|
|
243
237
|
|
|
@@ -52,6 +52,13 @@ import {
|
|
|
52
52
|
scanSupport,
|
|
53
53
|
} from "./net.ts";
|
|
54
54
|
|
|
55
|
+
// Host supervisors verify and signal their own children with the same checks.
|
|
56
|
+
export {
|
|
57
|
+
processStartToken,
|
|
58
|
+
signalIfSameGeneration,
|
|
59
|
+
verifyProcessGeneration,
|
|
60
|
+
} from "../agents/state/proc-start.ts";
|
|
61
|
+
export { acquireNoClobberLease } from "../workflow/workspaces/leases.ts";
|
|
55
62
|
export {
|
|
56
63
|
ancestry,
|
|
57
64
|
establishedConnectionCounts,
|
|
@@ -591,6 +598,26 @@ export interface StopResult {
|
|
|
591
598
|
|
|
592
599
|
export interface ServerProcessDependencies extends ProcessGenerationSignalDependencies {
|
|
593
600
|
probePid?: (pid: number) => boolean;
|
|
601
|
+
/** Numeric signal, used for the process group and in tests. */
|
|
602
|
+
kill?: (pid: number, signal: NodeJS.Signals) => void;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Recheck the generation, then signal. A supplied `kill` replaces the handle path. */
|
|
606
|
+
function signalVerified(
|
|
607
|
+
pid: number,
|
|
608
|
+
token: string | undefined,
|
|
609
|
+
name: NodeJS.Signals,
|
|
610
|
+
dependencies: ServerProcessDependencies,
|
|
611
|
+
): { sent: boolean; verdict: ProcessGenerationVerdict } {
|
|
612
|
+
if (!dependencies.kill) return signalIfSameGeneration(pid, token, name, dependencies);
|
|
613
|
+
const verdict = verifyProcessGeneration(pid, token, dependencies.readToken);
|
|
614
|
+
if (verdict !== "same") return { sent: false, verdict };
|
|
615
|
+
try {
|
|
616
|
+
dependencies.kill(pid, name);
|
|
617
|
+
return { sent: true, verdict };
|
|
618
|
+
} catch {
|
|
619
|
+
return { sent: false, verdict };
|
|
620
|
+
}
|
|
594
621
|
}
|
|
595
622
|
|
|
596
623
|
/**
|
|
@@ -653,7 +680,7 @@ export async function stopServer(
|
|
|
653
680
|
const result =
|
|
654
681
|
target.pid === record.pid
|
|
655
682
|
? signalServerLeader(target.pid, target.token, name, dependencies)
|
|
656
|
-
:
|
|
683
|
+
: signalVerified(target.pid, target.token, name, dependencies);
|
|
657
684
|
if (result.verdict !== "same") return refuse(target.pid, result.verdict);
|
|
658
685
|
}
|
|
659
686
|
const observed = await waitForExit(targets, name === "SIGTERM" ? graceMs : 2_000, dependencies);
|
|
@@ -679,13 +706,13 @@ function signalServerLeader(
|
|
|
679
706
|
// The leader's generation authorizes its group. A failed group signal must
|
|
680
707
|
// not reuse that observation for the direct-PID fallback.
|
|
681
708
|
const kill = dependencies.kill ?? process.kill;
|
|
682
|
-
const group =
|
|
709
|
+
const group = signalVerified(pid, token, name, {
|
|
683
710
|
readToken: dependencies.readToken,
|
|
684
711
|
kill: (leader, signal) => kill(-leader, signal),
|
|
685
712
|
});
|
|
686
713
|
return group.sent || group.verdict !== "same"
|
|
687
714
|
? group
|
|
688
|
-
:
|
|
715
|
+
: signalVerified(pid, token, name, dependencies);
|
|
689
716
|
}
|
|
690
717
|
|
|
691
718
|
async function waitForExit(
|