harnery 0.45.1 → 0.46.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/dist/commander.d.ts +13 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commands/admission.d.ts.map +1 -1
- package/dist/commands/admission.js +214 -27
- package/dist/commands/agents.d.ts +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +102 -25
- package/dist/commands/browse.d.ts +3 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +172 -8
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +8 -2
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +40 -0
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +4 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +33 -6
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +6 -45
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +0 -18
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -4
- package/dist/core/agents/render/session-context.d.ts +0 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +1 -6
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -4
- package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +6 -2
- package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
- package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference-contract.js +1 -0
- package/dist/core/agents/state/parent-reference.d.ts +18 -0
- package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference.js +119 -0
- package/dist/core/agents/state/pidmap.js +2 -2
- package/dist/core/agents/state/proc-start.d.ts +20 -31
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +37 -70
- package/dist/core/agents/state/service-process.d.ts +61 -0
- package/dist/core/agents/state/service-process.d.ts.map +1 -0
- package/dist/core/agents/state/service-process.js +143 -0
- package/dist/core/config.d.ts +39 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +63 -0
- package/dist/core/context/index.d.ts +6 -4
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +194 -256
- package/dist/core/context/observation.d.ts +9 -9
- package/dist/core/context/observation.d.ts.map +1 -1
- package/dist/core/context/observation.js +37 -6
- package/dist/core/diagnostics/path-identity.d.ts +1 -1
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
- package/dist/core/diagnostics/path-identity.js +51 -6
- package/dist/core/events/v3/canonical.js +43 -14
- package/dist/core/events/v3/index.d.ts +1 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -1
- package/dist/core/events/v3/producers/recorder.d.ts +34 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +117 -13
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +53 -13
- package/dist/core/events/v3/runtime-build.d.ts +14 -0
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +44 -0
- package/dist/core/events/v3/validate.d.ts.map +1 -1
- package/dist/core/events/v3/validate.js +36 -1
- package/dist/core/governor/index.d.ts +2 -1
- package/dist/core/governor/index.d.ts.map +1 -1
- package/dist/core/governor/index.js +2 -1
- package/dist/core/governor/service-read.d.ts +3 -10
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +10 -94
- package/dist/core/governor/service.d.ts +3 -7
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +17 -182
- package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
- package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
- package/dist/core/hooks/adapter/native-parent.js +91 -0
- package/dist/core/hooks/cli.js +79 -4
- package/dist/core/hooks/resolve/owner.d.ts +0 -11
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +2 -25
- package/dist/core/hooks/shell-admission.d.ts +84 -0
- package/dist/core/hooks/shell-admission.d.ts.map +1 -0
- package/dist/core/hooks/shell-admission.js +622 -0
- package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
- package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
- package/dist/core/hooks/shell-commit-tree.js +104 -0
- package/dist/core/inbox/contract.d.ts +12 -0
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/contract.js +16 -0
- package/dist/core/inbox/peer-messages.d.ts +41 -16
- package/dist/core/inbox/peer-messages.d.ts.map +1 -1
- package/dist/core/inbox/peer-messages.js +372 -15
- 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 +83 -3
- package/dist/core/inbox/targets.d.ts +17 -3
- package/dist/core/inbox/targets.d.ts.map +1 -1
- package/dist/core/inbox/targets.js +77 -2
- package/dist/core/presence/git.d.ts +18 -12
- package/dist/core/presence/git.d.ts.map +1 -1
- package/dist/core/presence/git.js +32 -38
- package/dist/core/presence/index.d.ts +2 -2
- package/dist/core/presence/index.d.ts.map +1 -1
- package/dist/core/presence/index.js +8 -6
- package/dist/core/presence/mail.d.ts +133 -0
- package/dist/core/presence/mail.d.ts.map +1 -0
- package/dist/core/presence/mail.js +464 -0
- package/dist/core/resources/sampler.js +2 -2
- package/dist/core/resources/service-status.d.ts +2 -14
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +3 -23
- package/dist/core/resources/service.d.ts +3 -7
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +12 -101
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +3 -2
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +9 -2
- package/dist/core/servers/index.d.ts +0 -2
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +3 -18
- package/dist/core/storage/modes.d.ts +4 -0
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +4 -0
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +5 -4
- package/dist/core/supervisor/service.d.ts +4 -7
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +23 -122
- package/dist/core/supervisor/services.js +2 -2
- package/dist/core/supervisor/status.d.ts +3 -17
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +7 -70
- package/dist/core/workflow/engine.js +2 -1
- package/dist/core/workflow/index.d.ts +3 -4
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +30 -74
- package/dist/core/workflow/resume-lock.d.ts +17 -0
- package/dist/core/workflow/resume-lock.d.ts.map +1 -0
- package/dist/core/workflow/resume-lock.js +158 -0
- package/dist/core/workflow/run-state.d.ts +0 -21
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -174
- package/dist/core/workflow/workspaces/leases.d.ts +4 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +1 -1
- package/dist/core/workflow/workspaces/local-git.js +91 -210
- package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/ownership.js +3 -130
- package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +0 -4
- package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +1 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +1 -4
- package/dist/lib/admission-history.d.ts +96 -0
- package/dist/lib/admission-history.d.ts.map +1 -0
- package/dist/lib/admission-history.js +196 -0
- package/dist/lib/admission.d.ts +56 -7
- package/dist/lib/admission.d.ts.map +1 -1
- package/dist/lib/admission.js +161 -19
- package/dist/lib/browser/client.d.ts +18 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +100 -12
- package/dist/lib/browser/content-checks.d.ts +3 -1
- package/dist/lib/browser/content-checks.d.ts.map +1 -1
- package/dist/lib/browser/content-checks.js +10 -2
- package/dist/lib/browser/critique.d.ts +32 -0
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +72 -3
- package/dist/lib/browser/geometry.d.ts +2 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +61 -15
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -1
- package/dist/lib/browser/page-review-contracts.d.ts +6 -1
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
- package/dist/lib/browser/page-review-contracts.js +6 -1
- package/dist/lib/browser/page-review-judge.d.ts +3 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +41 -11
- package/dist/lib/browser/page-review-pack.d.ts +3 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +5 -2
- package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +11 -4
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +73 -16
- package/dist/lib/browser/runts.d.ts +4 -2
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +84 -33
- package/dist/lib/browser/windows-chrome.d.ts +14 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +42 -0
- package/dist/lib/browser/windows-login-sync.d.ts +110 -0
- package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
- package/dist/lib/browser/windows-login-sync.js +171 -0
- package/dist/lib/cookies/client.d.ts +30 -5
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +55 -10
- package/dist/lib/cookies/index.d.ts +1 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +1 -1
- package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
- package/dist/lib/docs-metadata-sync.js +41 -2
- package/dist/lib/durable-job.d.ts +8 -0
- package/dist/lib/durable-job.d.ts.map +1 -1
- package/dist/lib/durable-job.js +10 -0
- package/dist/lib/http/client.d.ts +9 -8
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +44 -2
- package/dist/lib/instructions/templates.js +4 -4
- package/package.json +2 -2
- package/src/commander.ts +13 -0
- package/src/commands/admission.ts +272 -29
- package/src/commands/agents.ts +132 -27
- package/src/commands/browse.ts +208 -6
- package/src/commands/fetch.ts +1 -0
- package/src/commands/ledger-v3.ts +8 -2
- package/src/commands/presence.ts +49 -0
- package/src/commands/qa-run.ts +4 -3
- package/src/commands/review-pack.ts +41 -5
- package/src/commands/workflow.ts +8 -58
- package/src/core/agents/coord-client.ts +0 -20
- package/src/core/agents/index.ts +6 -0
- package/src/core/agents/live-authority-v3.ts +11 -4
- package/src/core/agents/render/session-context.ts +1 -7
- package/src/core/agents/session-events.ts +18 -3
- package/src/core/agents/state/heartbeat-reader.ts +3 -0
- package/src/core/agents/state/live-coordination-view.ts +13 -6
- package/src/core/agents/state/parent-reference-contract.ts +14 -0
- package/src/core/agents/state/parent-reference.ts +179 -0
- package/src/core/agents/state/pidmap.ts +2 -2
- package/src/core/agents/state/proc-start.ts +41 -69
- package/src/core/agents/state/service-process.ts +217 -0
- package/src/core/config.ts +85 -0
- package/src/core/context/index.ts +207 -272
- package/src/core/context/observation.ts +40 -9
- package/src/core/diagnostics/path-identity.ts +52 -6
- package/src/core/events/v3/canonical.ts +46 -13
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/producers/recorder.ts +151 -13
- package/src/core/events/v3/reader.ts +55 -11
- package/src/core/events/v3/runtime-build.ts +62 -0
- package/src/core/events/v3/validate.ts +42 -1
- package/src/core/governor/index.ts +1 -2
- package/src/core/governor/service-read.ts +15 -114
- package/src/core/governor/service.ts +36 -220
- package/src/core/hooks/adapter/native-parent.ts +109 -0
- package/src/core/hooks/cli.ts +83 -4
- package/src/core/hooks/resolve/owner.ts +2 -25
- package/src/core/hooks/shell-admission.ts +634 -0
- package/src/core/hooks/shell-commit-tree.ts +102 -0
- package/src/core/inbox/contract.ts +18 -0
- package/src/core/inbox/peer-messages.ts +501 -15
- package/src/core/inbox/service.ts +108 -3
- package/src/core/inbox/targets.ts +94 -3
- package/src/core/presence/git.ts +45 -37
- package/src/core/presence/index.ts +16 -8
- package/src/core/presence/mail.ts +620 -0
- package/src/core/resources/sampler.ts +3 -2
- package/src/core/resources/service-status.ts +6 -35
- package/src/core/resources/service.ts +23 -133
- package/src/core/resources/status.ts +3 -2
- package/src/core/semantic/service.ts +8 -2
- package/src/core/servers/index.ts +3 -23
- package/src/core/storage/modes.ts +6 -0
- package/src/core/storage/segments.ts +5 -4
- package/src/core/supervisor/service.ts +33 -155
- package/src/core/supervisor/services.ts +2 -2
- package/src/core/supervisor/status.ts +10 -85
- package/src/core/workflow/engine.ts +1 -1
- package/src/core/workflow/index.ts +2 -13
- package/src/core/workflow/proof.ts +35 -83
- package/src/core/workflow/resume-lock.ts +179 -0
- package/src/core/workflow/run-state.ts +3 -207
- package/src/core/workflow/workspaces/leases.ts +1 -1
- package/src/core/workflow/workspaces/local-git.ts +120 -239
- package/src/core/workflow/workspaces/ownership.ts +3 -158
- package/src/core/workflow/workspaces/reclaim.ts +3 -9
- package/src/core/workflow/workspaces/recovery.ts +1 -1
- package/src/core/workflow/workspaces/state.ts +1 -5
- package/src/lib/admission-history.ts +267 -0
- package/src/lib/admission.ts +217 -19
- package/src/lib/browser/client.ts +147 -35
- package/src/lib/browser/content-checks.ts +14 -3
- package/src/lib/browser/critique.ts +113 -3
- package/src/lib/browser/geometry.ts +63 -14
- package/src/lib/browser/index.ts +5 -0
- package/src/lib/browser/page-review-contracts.ts +6 -1
- package/src/lib/browser/page-review-judge.ts +58 -13
- package/src/lib/browser/page-review-pack.ts +13 -2
- package/src/lib/browser/qa-run-contracts.ts +30 -4
- package/src/lib/browser/qa-run.ts +78 -17
- package/src/lib/browser/runts.ts +85 -37
- package/src/lib/browser/windows-chrome.ts +50 -0
- package/src/lib/browser/windows-login-sync.ts +275 -0
- package/src/lib/cookies/client.ts +62 -10
- package/src/lib/cookies/index.ts +3 -0
- package/src/lib/docs-metadata-sync.ts +44 -2
- package/src/lib/durable-job.ts +15 -0
- package/src/lib/http/client.ts +63 -1
- package/src/lib/instructions/templates.ts +4 -4
- package/dist/core/agents/state/process-handle.d.ts +0 -26
- package/dist/core/agents/state/process-handle.d.ts.map +0 -1
- package/dist/core/agents/state/process-handle.js +0 -121
- package/dist/core/workflow/recovery-audit.d.ts +0 -30
- package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
- package/dist/core/workflow/recovery-audit.js +0 -116
- package/src/core/agents/state/process-handle.ts +0 -165
- package/src/core/workflow/recovery-audit.ts +0 -181
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readCoordinationViewV3 } from "../../events/v3/coordination-view.js";
|
|
2
|
+
import { liveInstanceIdV3, nativeInstanceIdV3 } from "../../events/v3/live-route-observer.js";
|
|
3
|
+
import { readNativeParentReference, } from "../../hooks/adapter/native-parent.js";
|
|
4
|
+
import { resolveName } from "./names.js";
|
|
5
|
+
/** Display/recipient evidence only. Never use native evidence as a claim or lifecycle waiver. */
|
|
6
|
+
export function resolveAgentParentReference(coordRoot, child, options = {}) {
|
|
7
|
+
try {
|
|
8
|
+
const view = options.view ?? readCoordinationViewV3(coordRoot);
|
|
9
|
+
if (!view.source_complete || !view.authority_safe)
|
|
10
|
+
return unknown("parent_authority_unsafe");
|
|
11
|
+
if (child.generation_id &&
|
|
12
|
+
child.v3_generation_id &&
|
|
13
|
+
child.generation_id !== child.v3_generation_id) {
|
|
14
|
+
return unknown("child_generation_mismatch");
|
|
15
|
+
}
|
|
16
|
+
const instance = liveInstanceIdV3(child.instance_id);
|
|
17
|
+
const generationId = child.generation_id ?? child.v3_generation_id;
|
|
18
|
+
const generations = [
|
|
19
|
+
...Object.values(view.instances),
|
|
20
|
+
...Object.values(view.terminal_generations),
|
|
21
|
+
];
|
|
22
|
+
const generation = generationId
|
|
23
|
+
? generations.find((row) => row.generation_id === generationId && row.instance_id === instance)
|
|
24
|
+
: view.instances[instance];
|
|
25
|
+
if (!generation)
|
|
26
|
+
return unknown("child_generation_unavailable");
|
|
27
|
+
if (!safe(view, generation))
|
|
28
|
+
return unknown("child_authority_unsafe");
|
|
29
|
+
const canonical = generation.parent_generation_id
|
|
30
|
+
? generations.find((row) => row.generation_id === generation.parent_generation_id)
|
|
31
|
+
: undefined;
|
|
32
|
+
if (generation.parent_generation_id && !canonical)
|
|
33
|
+
return unknown("parent_generation_unavailable");
|
|
34
|
+
if (canonical && cyclic(generations, generation)) {
|
|
35
|
+
return { state: "conflict", reason: "parent_lineage_cycle" };
|
|
36
|
+
}
|
|
37
|
+
const adapter = generation.runtime_attestation.adapter;
|
|
38
|
+
const platform = adapter.state === "observed" ? adapter.value.id : child.platform;
|
|
39
|
+
const nativeId = child.native_session_id ?? nativeInstanceIdV3(instance);
|
|
40
|
+
const native = platform === "codex" && liveInstanceIdV3(nativeId) === instance
|
|
41
|
+
? (options.readNativeParent ?? readNativeParentReference)({
|
|
42
|
+
adapter: platform,
|
|
43
|
+
native_session_id: nativeId,
|
|
44
|
+
})
|
|
45
|
+
: { state: "unknown", reason: "native_child_identity_unavailable" };
|
|
46
|
+
if (canonical) {
|
|
47
|
+
if (native.state === "none" ||
|
|
48
|
+
(native.state === "observed" &&
|
|
49
|
+
liveInstanceIdV3(native.parent_session_id) !== canonical.instance_id)) {
|
|
50
|
+
return { state: "conflict", reason: "parent_sources_disagree" };
|
|
51
|
+
}
|
|
52
|
+
return reference(coordRoot, view, canonical.instance_id, canonical, "v3-session-link");
|
|
53
|
+
}
|
|
54
|
+
if (native.state === "observed") {
|
|
55
|
+
const parentInstance = liveInstanceIdV3(native.parent_session_id);
|
|
56
|
+
if (parentInstance === instance)
|
|
57
|
+
return { state: "conflict", reason: "parent_self_reference" };
|
|
58
|
+
const parent = recipientGeneration(view, parentInstance);
|
|
59
|
+
if (parent && cyclic(generations, generation, parent)) {
|
|
60
|
+
return { state: "conflict", reason: "parent_lineage_cycle" };
|
|
61
|
+
}
|
|
62
|
+
return reference(coordRoot, view, parentInstance, parent, "native-session-metadata", native.source_witness);
|
|
63
|
+
}
|
|
64
|
+
return { state: native.state, reason: native.reason };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return unknown("parent_reference_unavailable");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function reference(root, view, instance, parent, source, witness) {
|
|
71
|
+
const native = nativeInstanceIdV3(instance);
|
|
72
|
+
const name = resolveName(root, native)?.name ?? null;
|
|
73
|
+
const recipient = recipientGeneration(view, instance);
|
|
74
|
+
return {
|
|
75
|
+
state: "verified",
|
|
76
|
+
relationship: source === "v3-session-link" ? "delegated-child" : "native-child",
|
|
77
|
+
parent_instance_id: native,
|
|
78
|
+
parent_v3_instance_id: instance,
|
|
79
|
+
parent_generation_id: parent?.generation_id ?? null,
|
|
80
|
+
parent_session_id: parent?.session_id ?? null,
|
|
81
|
+
parent_name: name,
|
|
82
|
+
source,
|
|
83
|
+
...(witness ? { source_witness: witness } : {}),
|
|
84
|
+
addressable: Boolean(parent && recipient?.generation_id === parent.generation_id && safe(view, parent) && name),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function recipientGeneration(view, instance) {
|
|
88
|
+
return (view.instances[instance] ??
|
|
89
|
+
Object.values(view.terminal_generations)
|
|
90
|
+
.filter((row) => row.instance_id === instance)
|
|
91
|
+
.sort((a, b) => b.source_position.segment_ordinal - a.source_position.segment_ordinal ||
|
|
92
|
+
b.source_position.byte_offset - a.source_position.byte_offset)[0]);
|
|
93
|
+
}
|
|
94
|
+
function safe(view, generation) {
|
|
95
|
+
return !(view.diagnostics_by_generation[generation.generation_id] ?? []).some((d) => d.authority_blocking);
|
|
96
|
+
}
|
|
97
|
+
function cyclic(generations, child, first) {
|
|
98
|
+
const seen = new Set([child.generation_id]);
|
|
99
|
+
let cursor = first ?? child;
|
|
100
|
+
for (let depth = 0; depth <= generations.length; depth++) {
|
|
101
|
+
if (cursor !== child) {
|
|
102
|
+
if (seen.has(cursor.generation_id) || cursor.instance_id === child.instance_id)
|
|
103
|
+
return true;
|
|
104
|
+
seen.add(cursor.generation_id);
|
|
105
|
+
}
|
|
106
|
+
if (!cursor.parent_generation_id)
|
|
107
|
+
return false;
|
|
108
|
+
const next = generations.find((g) => g.generation_id === cursor.parent_generation_id);
|
|
109
|
+
if (!next)
|
|
110
|
+
return false;
|
|
111
|
+
if (seen.has(next.generation_id))
|
|
112
|
+
return true;
|
|
113
|
+
cursor = next;
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
function unknown(reason) {
|
|
118
|
+
return { state: "unknown", reason };
|
|
119
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
|
-
import {
|
|
18
|
+
import { processStartToken, verifyProcessGeneration } from "./proc-start.js";
|
|
19
19
|
/**
|
|
20
20
|
* Row count that triggers a dead-row sweep on the next write.
|
|
21
21
|
*
|
|
@@ -44,7 +44,7 @@ export function parsePidmapRow(row) {
|
|
|
44
44
|
function rowStillNamesItsProcess(pid, startToken) {
|
|
45
45
|
if (!pidIsAlive(pid))
|
|
46
46
|
return false;
|
|
47
|
-
return
|
|
47
|
+
return verifyProcessGeneration(pid, startToken) !== "different";
|
|
48
48
|
}
|
|
49
49
|
function atomicWrite(path, content) {
|
|
50
50
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -30,9 +30,8 @@
|
|
|
30
30
|
* through the caller's locale and timezone, so both are pinned; without that,
|
|
31
31
|
* two callers reading the same live process disagree.
|
|
32
32
|
*/
|
|
33
|
-
import { signalProcessWithHandle } from "./process-handle.js";
|
|
34
33
|
/** Which probe answers "when did this process start?" on this machine. */
|
|
35
|
-
export type StartTokenProbe = "procfs" | "ps"
|
|
34
|
+
export type StartTokenProbe = "procfs" | "ps";
|
|
36
35
|
/**
|
|
37
36
|
* Which probe this machine uses, decided once.
|
|
38
37
|
*
|
|
@@ -50,7 +49,7 @@ export declare function resetStartTokenCaches(): void;
|
|
|
50
49
|
* than by splitting the whole line, because comm is the executable name in
|
|
51
50
|
* parentheses and may itself contain spaces and parens. The boot segment is
|
|
52
51
|
* dropped rather than faked when the kernel will not name its boot, which
|
|
53
|
-
* yields the pre-boot-id shape that `
|
|
52
|
+
* yields the pre-boot-id shape that `verifyProcessGeneration` compares by ticks.
|
|
54
53
|
*/
|
|
55
54
|
export declare function procfsStartToken(pid: number): string | null;
|
|
56
55
|
/**
|
|
@@ -71,43 +70,33 @@ export declare function psStartToken(pid: number, run?: (pid: number) => {
|
|
|
71
70
|
export declare function processStartToken(pid: number): string | null;
|
|
72
71
|
export type ProcessGenerationVerdict = "same" | "different" | "unknown";
|
|
73
72
|
type ReadProcessStartToken = (pid: number) => string | null;
|
|
74
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Does `pid` still name the run that wrote `recordedToken`?
|
|
75
|
+
*
|
|
76
|
+
* - `"same"`: both tokens present and equal.
|
|
77
|
+
* - `"different"`: the pid was recycled. A liveness check cannot see this,
|
|
78
|
+
* because a recycled pid is alive.
|
|
79
|
+
* - `"unknown"`: a token is missing, malformed, or unreadable. Never treat
|
|
80
|
+
* this as proof of identity.
|
|
81
|
+
*
|
|
82
|
+
* A Linux token written without a boot segment cannot identify a run across
|
|
83
|
+
* reboots, so it is compared on its tick count: a different count proves a
|
|
84
|
+
* different run, and an equal count stays unknown.
|
|
85
|
+
*/
|
|
75
86
|
export declare function verifyProcessGeneration(pid: number, recordedToken: string | null | undefined, readToken?: ReadProcessStartToken): ProcessGenerationVerdict;
|
|
76
87
|
export interface ProcessGenerationSignalDependencies {
|
|
77
88
|
readToken?: ReadProcessStartToken;
|
|
78
|
-
|
|
89
|
+
kill?: (pid: number, signal: NodeJS.Signals | number) => unknown;
|
|
79
90
|
}
|
|
80
91
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
92
|
+
* Recheck the process generation immediately before sending the signal.
|
|
93
|
+
*
|
|
94
|
+
* The process can still exit and its PID can be reused between the token read
|
|
95
|
+
* and the signal. Eliminating that race requires a generation-bound OS handle.
|
|
83
96
|
*/
|
|
84
97
|
export declare function signalIfSameGeneration(pid: number, recordedToken: string | null | undefined, signal: NodeJS.Signals | number, dependencies?: ProcessGenerationSignalDependencies): {
|
|
85
98
|
sent: boolean;
|
|
86
99
|
verdict: ProcessGenerationVerdict;
|
|
87
100
|
};
|
|
88
|
-
/**
|
|
89
|
-
* Do two tokens describe the same run?
|
|
90
|
-
*
|
|
91
|
-
* Equality, plus one concession: a row written before tokens carried a boot
|
|
92
|
-
* segment recorded the tick count alone, so it is compared on the part it
|
|
93
|
-
* actually recorded. Refusing that would call every pre-upgrade row a recycled
|
|
94
|
-
* pid and prune a working machine's live rows the first time the new code ran.
|
|
95
|
-
* Such a row loses its reboot protection until the next write rewrites it,
|
|
96
|
-
* which is a strictly better position than the one it came from.
|
|
97
|
-
*/
|
|
98
|
-
export declare function tokensAgree(recorded: string, current: string): boolean;
|
|
99
|
-
/**
|
|
100
|
-
* Does `pid` still name the process a row recorded?
|
|
101
|
-
*
|
|
102
|
-
* Three answers, and the difference between the last two matters:
|
|
103
|
-
*
|
|
104
|
-
* - `"match"` — recorded token, current token, same run. The row is trustworthy.
|
|
105
|
-
* - `"mismatch"` — both present and different. The pid was recycled. This is
|
|
106
|
-
* the case a liveness check cannot see, because a recycled pid is alive.
|
|
107
|
-
* - `"unverified"` — no recorded token (a row written before tokens existed) or
|
|
108
|
-
* no current one (an unsupported platform). Callers treat this the way they
|
|
109
|
-
* treated every row previously: trust it if the pid is alive.
|
|
110
|
-
*/
|
|
111
|
-
export declare function checkPidToken(pid: number, recorded: string | undefined): "match" | "mismatch" | "unverified";
|
|
112
101
|
export {};
|
|
113
102
|
//# sourceMappingURL=proc-start.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proc-start.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/proc-start.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;
|
|
1
|
+
{"version":3,"file":"proc-start.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/proc-start.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAMH,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,IAAI,CAAC;AAc9C;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,eAAe,CAOjD;AAyBD,uDAAuD;AACvD,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc3D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAiB,GAC7E,MAAM,GAAG,IAAI,CAUf;AAWD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5D;AAED,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;AAExE,KAAK,qBAAqB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,SAAS,GAAE,qBAAyC,GACnD,wBAAwB,CAuB1B;AAOD,MAAM,WAAW,mCAAmC;IAClD,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,KAAK,OAAO,CAAC;CAClE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,EAC/B,YAAY,GAAE,mCAAwC,GACrD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,wBAAwB,CAAA;CAAE,CAUtD"}
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
import { spawnSync } from "node:child_process";
|
|
34
34
|
import { existsSync, readFileSync } from "node:fs";
|
|
35
35
|
import { coordEnv } from "../../../lib/env.js";
|
|
36
|
-
import { signalProcessWithHandle, windowsProcessStartToken } from "./process-handle.js";
|
|
37
36
|
/**
|
|
38
37
|
* The `ps` probe is text formatted by `strftime("%c", localtime(...))`, so its
|
|
39
38
|
* output moves with `TZ` and `LC_TIME`. A hook running under `LC_ALL=C` (git
|
|
@@ -55,8 +54,6 @@ export function startTokenProbe() {
|
|
|
55
54
|
const override = coordEnv("PID_PROBE");
|
|
56
55
|
if (override === "procfs" || override === "ps")
|
|
57
56
|
return override;
|
|
58
|
-
if (process.platform === "win32")
|
|
59
|
-
return "windows";
|
|
60
57
|
if (cachedProbe === undefined) {
|
|
61
58
|
cachedProbe = existsSync("/proc/self/stat") ? "procfs" : "ps";
|
|
62
59
|
}
|
|
@@ -98,7 +95,7 @@ export function resetStartTokenCaches() {
|
|
|
98
95
|
* than by splitting the whole line, because comm is the executable name in
|
|
99
96
|
* parentheses and may itself contain spaces and parens. The boot segment is
|
|
100
97
|
* dropped rather than faked when the kernel will not name its boot, which
|
|
101
|
-
* yields the pre-boot-id shape that `
|
|
98
|
+
* yields the pre-boot-id shape that `verifyProcessGeneration` compares by ticks.
|
|
102
99
|
*/
|
|
103
100
|
export function procfsStartToken(pid) {
|
|
104
101
|
let stat;
|
|
@@ -152,93 +149,63 @@ function defaultPsRun(pid) {
|
|
|
152
149
|
export function processStartToken(pid) {
|
|
153
150
|
if (!Number.isFinite(pid) || pid <= 0)
|
|
154
151
|
return null;
|
|
155
|
-
|
|
156
|
-
return probe === "windows"
|
|
157
|
-
? windowsProcessStartToken(pid)
|
|
158
|
-
: probe === "procfs"
|
|
159
|
-
? procfsStartToken(pid)
|
|
160
|
-
: psStartToken(pid);
|
|
152
|
+
return startTokenProbe() === "procfs" ? procfsStartToken(pid) : psStartToken(pid);
|
|
161
153
|
}
|
|
162
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* Does `pid` still name the run that wrote `recordedToken`?
|
|
156
|
+
*
|
|
157
|
+
* - `"same"`: both tokens present and equal.
|
|
158
|
+
* - `"different"`: the pid was recycled. A liveness check cannot see this,
|
|
159
|
+
* because a recycled pid is alive.
|
|
160
|
+
* - `"unknown"`: a token is missing, malformed, or unreadable. Never treat
|
|
161
|
+
* this as proof of identity.
|
|
162
|
+
*
|
|
163
|
+
* A Linux token written without a boot segment cannot identify a run across
|
|
164
|
+
* reboots, so it is compared on its tick count: a different count proves a
|
|
165
|
+
* different run, and an equal count stays unknown.
|
|
166
|
+
*/
|
|
163
167
|
export function verifyProcessGeneration(pid, recordedToken, readToken = processStartToken) {
|
|
164
168
|
if (!Number.isInteger(pid) ||
|
|
165
169
|
pid <= 0 ||
|
|
166
170
|
typeof recordedToken !== "string" ||
|
|
167
171
|
!recordedToken.trim())
|
|
168
172
|
return "unknown";
|
|
169
|
-
|
|
170
|
-
if (/^l\d+$/.test(recordedToken))
|
|
171
|
-
return "unknown";
|
|
173
|
+
let currentToken;
|
|
172
174
|
try {
|
|
173
|
-
|
|
174
|
-
if (typeof currentToken !== "string" || !currentToken.trim())
|
|
175
|
-
return "unknown";
|
|
176
|
-
if (/^l\d+$/.test(currentToken))
|
|
177
|
-
return "unknown";
|
|
178
|
-
return currentToken === recordedToken ? "same" : "different";
|
|
175
|
+
currentToken = readToken(pid);
|
|
179
176
|
}
|
|
180
177
|
catch {
|
|
181
178
|
return "unknown";
|
|
182
179
|
}
|
|
180
|
+
if (typeof currentToken !== "string" || !currentToken.trim())
|
|
181
|
+
return "unknown";
|
|
182
|
+
const bootless = (token) => /^l\d+$/.test(token);
|
|
183
|
+
if (recordedToken[0] === "l" &&
|
|
184
|
+
currentToken[0] === "l" &&
|
|
185
|
+
(bootless(recordedToken) || bootless(currentToken)))
|
|
186
|
+
return ticksOf(recordedToken) === ticksOf(currentToken) ? "unknown" : "different";
|
|
187
|
+
return currentToken === recordedToken ? "same" : "different";
|
|
188
|
+
}
|
|
189
|
+
function ticksOf(token) {
|
|
190
|
+
const dot = token.indexOf(".");
|
|
191
|
+
return dot === -1 ? token.slice(1) : token.slice(dot + 1);
|
|
183
192
|
}
|
|
184
193
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
194
|
+
* Recheck the process generation immediately before sending the signal.
|
|
195
|
+
*
|
|
196
|
+
* The process can still exit and its PID can be reused between the token read
|
|
197
|
+
* and the signal. Eliminating that race requires a generation-bound OS handle.
|
|
187
198
|
*/
|
|
188
199
|
export function signalIfSameGeneration(pid, recordedToken, signal, dependencies = {}) {
|
|
200
|
+
const kill = dependencies.kill ?? process.kill;
|
|
189
201
|
const verdict = verifyProcessGeneration(pid, recordedToken, dependencies.readToken);
|
|
190
202
|
if (verdict !== "same")
|
|
191
203
|
return { sent: false, verdict };
|
|
192
204
|
try {
|
|
193
|
-
|
|
205
|
+
kill(pid, signal);
|
|
206
|
+
return { sent: true, verdict };
|
|
194
207
|
}
|
|
195
208
|
catch {
|
|
196
|
-
return { sent: false, verdict
|
|
209
|
+
return { sent: false, verdict };
|
|
197
210
|
}
|
|
198
211
|
}
|
|
199
|
-
/**
|
|
200
|
-
* Do two tokens describe the same run?
|
|
201
|
-
*
|
|
202
|
-
* Equality, plus one concession: a row written before tokens carried a boot
|
|
203
|
-
* segment recorded the tick count alone, so it is compared on the part it
|
|
204
|
-
* actually recorded. Refusing that would call every pre-upgrade row a recycled
|
|
205
|
-
* pid and prune a working machine's live rows the first time the new code ran.
|
|
206
|
-
* Such a row loses its reboot protection until the next write rewrites it,
|
|
207
|
-
* which is a strictly better position than the one it came from.
|
|
208
|
-
*/
|
|
209
|
-
export function tokensAgree(recorded, current) {
|
|
210
|
-
if (recorded === current)
|
|
211
|
-
return true;
|
|
212
|
-
// Only the Linux form has a boot segment to disagree about, and only when
|
|
213
|
-
// exactly one side carries one is this a shape difference rather than two
|
|
214
|
-
// genuinely different start instants.
|
|
215
|
-
if (recorded[0] !== "l" || current[0] !== "l")
|
|
216
|
-
return false;
|
|
217
|
-
if (recorded.includes(".") === current.includes("."))
|
|
218
|
-
return false;
|
|
219
|
-
return ticksOf(recorded) === ticksOf(current);
|
|
220
|
-
}
|
|
221
|
-
function ticksOf(token) {
|
|
222
|
-
const dot = token.indexOf(".");
|
|
223
|
-
return dot === -1 ? token.slice(1) : token.slice(dot + 1);
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Does `pid` still name the process a row recorded?
|
|
227
|
-
*
|
|
228
|
-
* Three answers, and the difference between the last two matters:
|
|
229
|
-
*
|
|
230
|
-
* - `"match"` — recorded token, current token, same run. The row is trustworthy.
|
|
231
|
-
* - `"mismatch"` — both present and different. The pid was recycled. This is
|
|
232
|
-
* the case a liveness check cannot see, because a recycled pid is alive.
|
|
233
|
-
* - `"unverified"` — no recorded token (a row written before tokens existed) or
|
|
234
|
-
* no current one (an unsupported platform). Callers treat this the way they
|
|
235
|
-
* treated every row previously: trust it if the pid is alive.
|
|
236
|
-
*/
|
|
237
|
-
export function checkPidToken(pid, recorded) {
|
|
238
|
-
if (!recorded)
|
|
239
|
-
return "unverified";
|
|
240
|
-
const current = processStartToken(pid);
|
|
241
|
-
if (!current)
|
|
242
|
-
return "unverified";
|
|
243
|
-
return tokensAgree(recorded, current) ? "match" : "mismatch";
|
|
244
|
-
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type ProcessGenerationSignalDependencies, type ProcessGenerationVerdict } from "./proc-start.js";
|
|
2
|
+
export interface ServiceProcessDependencies extends ProcessGenerationSignalDependencies {
|
|
3
|
+
/** Throws ESRCH, or returns false, when the process is gone. */
|
|
4
|
+
probePid?: (pid: number) => unknown;
|
|
5
|
+
}
|
|
6
|
+
export type ServiceLiveness = "running" | "stopped" | "unknown";
|
|
7
|
+
export interface ServiceProcessOwner {
|
|
8
|
+
pid: number;
|
|
9
|
+
host: string;
|
|
10
|
+
start_token?: string;
|
|
11
|
+
state?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A stale heartbeat does not prove that a process has exited. Another host, a
|
|
15
|
+
* probe error other than ESRCH, or an unverifiable token answers unknown. A
|
|
16
|
+
* recorded `stopped` state counts only while the generation still matches.
|
|
17
|
+
*/
|
|
18
|
+
export declare function observeServiceProcess(owner: ServiceProcessOwner, dependencies?: ServiceProcessDependencies): {
|
|
19
|
+
generation: ProcessGenerationVerdict;
|
|
20
|
+
liveness: ServiceLiveness;
|
|
21
|
+
};
|
|
22
|
+
/** Refuse a start while another owner runs or cannot be identified. */
|
|
23
|
+
export declare function serviceStartRefusal(label: string, stopCommand: string, pid: number | undefined, liveness: ServiceLiveness): Error;
|
|
24
|
+
export interface ServiceLease {
|
|
25
|
+
pid: number;
|
|
26
|
+
start_token?: string;
|
|
27
|
+
host: string;
|
|
28
|
+
nonce: string;
|
|
29
|
+
created_at?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function readServiceLease(path: string): ServiceLease | undefined;
|
|
32
|
+
/** An absent lease is stopped; an unreadable one cannot be identified. */
|
|
33
|
+
export declare function serviceLeaseOwner(path: string): {
|
|
34
|
+
lease?: ServiceLease;
|
|
35
|
+
liveness: ServiceLiveness;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Take the singleton lease with an exclusive create. A lease whose owner is
|
|
39
|
+
* proven stopped is replaced; any other owner refuses the start.
|
|
40
|
+
*/
|
|
41
|
+
export declare function acquireServiceLease(path: string, refuse: (pid: number | undefined, liveness: ServiceLiveness) => Error, fields?: Record<string, unknown>): {
|
|
42
|
+
nonce: string;
|
|
43
|
+
release: () => void;
|
|
44
|
+
};
|
|
45
|
+
export interface ServiceStopFields {
|
|
46
|
+
signal_sent: boolean;
|
|
47
|
+
signal_verdict: ProcessGenerationVerdict;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Write the durable stop request, then SIGTERM the owner `status` observed as
|
|
51
|
+
* running on this host, after a fresh generation check. The request stays for
|
|
52
|
+
* the daemon either way; `read` reports the state afterward.
|
|
53
|
+
*/
|
|
54
|
+
export declare function requestServiceStop<S extends {
|
|
55
|
+
liveness: ServiceLiveness;
|
|
56
|
+
generation: ProcessGenerationVerdict;
|
|
57
|
+
record?: ServiceProcessOwner;
|
|
58
|
+
}>(status: S, read: () => S, stopPath: string, dependencies?: ServiceProcessDependencies, options?: {
|
|
59
|
+
signal?: boolean;
|
|
60
|
+
}): S & ServiceStopFields;
|
|
61
|
+
//# sourceMappingURL=service-process.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-process.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/service-process.ts"],"names":[],"mappings":"AAmBA,OAAO,EACL,KAAK,mCAAmC,EACxC,KAAK,wBAAwB,EAI9B,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,0BAA2B,SAAQ,mCAAmC;IACrF,gEAAgE;IAChE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACrC;AAED,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,mBAAmB,EAC1B,YAAY,GAAE,0BAA+B,GAC5C;IAAE,UAAU,EAAE,wBAAwB,CAAC;IAAC,QAAQ,EAAE,eAAe,CAAA;CAAE,CAuBrE;AAED,uEAAuE;AACvE,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,QAAQ,EAAE,eAAe,GACxB,KAAK,CAMP;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAmBvE;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG;IAC/C,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,eAAe,CAAC;CAC3B,CAIA;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,eAAe,KAAK,KAAK,EACrE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CA0CxC;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,wBAAwB,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,CAAC,SAAS;IACR,QAAQ,EAAE,eAAe,CAAC;IAC1B,UAAU,EAAE,wBAAwB,CAAC;IACrC,MAAM,CAAC,EAAE,mBAAmB,CAAC;CAC9B,EAED,MAAM,EAAE,CAAC,EACT,IAAI,EAAE,MAAM,CAAC,EACb,QAAQ,EAAE,MAAM,EAChB,YAAY,GAAE,0BAA+B,EAC7C,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,CAAC,GAAG,iBAAiB,CAevB"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One owner check for every singleton background service (governor, local
|
|
3
|
+
* supervisor, legacy resource observer): is the recorded process still the
|
|
4
|
+
* run that wrote the record, and how is a stop requested?
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
8
|
+
import { hostname } from "node:os";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
import { writePrivateJsonAtomic } from "../../storage/atomic-json.js";
|
|
11
|
+
import { stateDirMode, stateFileMode } from "../../storage/modes.js";
|
|
12
|
+
import { processStartToken, signalIfSameGeneration, verifyProcessGeneration, } from "./proc-start.js";
|
|
13
|
+
/**
|
|
14
|
+
* A stale heartbeat does not prove that a process has exited. Another host, a
|
|
15
|
+
* probe error other than ESRCH, or an unverifiable token answers unknown. A
|
|
16
|
+
* recorded `stopped` state counts only while the generation still matches.
|
|
17
|
+
*/
|
|
18
|
+
export function observeServiceProcess(owner, dependencies = {}) {
|
|
19
|
+
if (owner.host !== hostname())
|
|
20
|
+
return { generation: "unknown", liveness: "unknown" };
|
|
21
|
+
try {
|
|
22
|
+
if ((dependencies.probePid ?? ((pid) => process.kill(pid, 0)))(owner.pid) === false)
|
|
23
|
+
return { generation: "unknown", liveness: "stopped" };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return {
|
|
27
|
+
generation: "unknown",
|
|
28
|
+
liveness: error.code === "ESRCH" ? "stopped" : "unknown",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const generation = verifyProcessGeneration(owner.pid, owner.start_token, dependencies.readToken);
|
|
32
|
+
return {
|
|
33
|
+
generation,
|
|
34
|
+
liveness: generation === "same"
|
|
35
|
+
? owner.state === "stopped"
|
|
36
|
+
? "stopped"
|
|
37
|
+
: "running"
|
|
38
|
+
: generation === "different"
|
|
39
|
+
? "stopped"
|
|
40
|
+
: "unknown",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Refuse a start while another owner runs or cannot be identified. */
|
|
44
|
+
export function serviceStartRefusal(label, stopCommand, pid, liveness) {
|
|
45
|
+
return new Error(liveness === "running"
|
|
46
|
+
? `${label} is already running under pid ${pid}`
|
|
47
|
+
: `${label} process generation is unknown${pid ? ` for pid ${pid}` : ""}; run \`${stopCommand}\` to write a durable stop request before starting it again`);
|
|
48
|
+
}
|
|
49
|
+
export function readServiceLease(path) {
|
|
50
|
+
try {
|
|
51
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
52
|
+
if (!Number.isSafeInteger(value.pid) ||
|
|
53
|
+
(value.pid ?? 0) <= 0 ||
|
|
54
|
+
typeof value.host !== "string" ||
|
|
55
|
+
!value.host ||
|
|
56
|
+
typeof value.nonce !== "string" ||
|
|
57
|
+
!value.nonce)
|
|
58
|
+
return undefined;
|
|
59
|
+
return {
|
|
60
|
+
...value,
|
|
61
|
+
start_token: typeof value.start_token === "string" ? value.start_token : undefined,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** An absent lease is stopped; an unreadable one cannot be identified. */
|
|
69
|
+
export function serviceLeaseOwner(path) {
|
|
70
|
+
if (!existsSync(path))
|
|
71
|
+
return { liveness: "stopped" };
|
|
72
|
+
const lease = readServiceLease(path);
|
|
73
|
+
return { lease, liveness: lease ? observeServiceProcess(lease).liveness : "unknown" };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Take the singleton lease with an exclusive create. A lease whose owner is
|
|
77
|
+
* proven stopped is replaced; any other owner refuses the start.
|
|
78
|
+
*/
|
|
79
|
+
export function acquireServiceLease(path, refuse, fields = {}) {
|
|
80
|
+
mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
|
|
81
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
82
|
+
const startToken = processStartToken(process.pid);
|
|
83
|
+
const lease = {
|
|
84
|
+
...fields,
|
|
85
|
+
pid: process.pid,
|
|
86
|
+
...(startToken ? { start_token: startToken } : {}),
|
|
87
|
+
host: hostname(),
|
|
88
|
+
nonce: randomUUID(),
|
|
89
|
+
created_at: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
const fd = openSync(path, "wx", stateFileMode());
|
|
93
|
+
try {
|
|
94
|
+
writeFileSync(fd, `${JSON.stringify(lease)}\n`, "utf8");
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
closeSync(fd);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
nonce: lease.nonce,
|
|
101
|
+
release: () => {
|
|
102
|
+
if (readServiceLease(path)?.nonce !== lease.nonce)
|
|
103
|
+
return;
|
|
104
|
+
try {
|
|
105
|
+
unlinkSync(path);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// A missing lease is already released.
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code !== "EEXIST")
|
|
115
|
+
throw error;
|
|
116
|
+
const owner = serviceLeaseOwner(path);
|
|
117
|
+
if (owner.liveness !== "stopped")
|
|
118
|
+
throw refuse(owner.lease?.pid, owner.liveness);
|
|
119
|
+
try {
|
|
120
|
+
unlinkSync(path);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// A concurrent starter may have replaced or removed the stale lease.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`could not acquire the singleton lease at ${path}`);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Write the durable stop request, then SIGTERM the owner `status` observed as
|
|
131
|
+
* running on this host, after a fresh generation check. The request stays for
|
|
132
|
+
* the daemon either way; `read` reports the state afterward.
|
|
133
|
+
*/
|
|
134
|
+
export function requestServiceStop(status, read, stopPath, dependencies = {}, options = {}) {
|
|
135
|
+
writePrivateJsonAtomic(stopPath, {
|
|
136
|
+
requested_at: new Date().toISOString(),
|
|
137
|
+
requested_by_pid: process.pid,
|
|
138
|
+
});
|
|
139
|
+
const signal = options.signal !== false && status.liveness === "running" && status.record?.host === hostname()
|
|
140
|
+
? signalIfSameGeneration(status.record.pid, status.record.start_token, "SIGTERM", dependencies)
|
|
141
|
+
: { sent: false, verdict: status.generation };
|
|
142
|
+
return { ...read(), signal_sent: signal.sent, signal_verdict: signal.verdict };
|
|
143
|
+
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -152,6 +152,45 @@ export declare function endOfTurnStatusCommand(coordRoot?: string | null): strin
|
|
|
152
152
|
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
153
153
|
*/
|
|
154
154
|
export declare function ripgrepAutoInstall(coordRoot?: string | null): boolean;
|
|
155
|
+
/** Wait before queueing while Linux memory pressure is high (PSI `full avg10`). */
|
|
156
|
+
export interface AdmissionPressureConfig {
|
|
157
|
+
psi_full_avg10: number;
|
|
158
|
+
max_wait_seconds: number;
|
|
159
|
+
}
|
|
160
|
+
/** Per-resource defaults for `admission run`; command-line flags win. */
|
|
161
|
+
export interface AdmissionResourceConfig {
|
|
162
|
+
capacity?: number;
|
|
163
|
+
timeout_minutes?: number;
|
|
164
|
+
defer_on_pressure?: AdmissionPressureConfig;
|
|
165
|
+
}
|
|
166
|
+
export interface AdmissionProjectConfig {
|
|
167
|
+
resources: Record<string, AdmissionResourceConfig>;
|
|
168
|
+
/** Present when the agent hook should wrap heavy shell commands in a slot. */
|
|
169
|
+
guard?: {
|
|
170
|
+
resource: string;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Admission settings from `.harnery/config.jsonc`:
|
|
175
|
+
*
|
|
176
|
+
* ```jsonc
|
|
177
|
+
* "admission": {
|
|
178
|
+
* "resources": {
|
|
179
|
+
* "build": {
|
|
180
|
+
* "capacity": 2,
|
|
181
|
+
* "timeout_minutes": 30,
|
|
182
|
+
* "defer_on_pressure": { "psi_full_avg10": 40, "max_wait_seconds": 300 }
|
|
183
|
+
* }
|
|
184
|
+
* },
|
|
185
|
+
* "guard": { "resource": "build" }
|
|
186
|
+
* }
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* Project config only, like other machine-shaping policy. Malformed values are
|
|
190
|
+
* dropped one by one rather than rejecting the whole block. Undefined when the
|
|
191
|
+
* project declares no admission settings.
|
|
192
|
+
*/
|
|
193
|
+
export declare function readAdmissionConfig(coordRoot?: string | null): AdmissionProjectConfig | undefined;
|
|
155
194
|
/**
|
|
156
195
|
* Whether this repo pins workflow runs to subscription billing:
|
|
157
196
|
* `.harnery/config.jsonc` `{ "workflow": { "subscriptionOnly": true } }`.
|