harnery 0.42.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/commander.d.ts +16 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +18 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +95 -52
- 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/tunnel.d.ts +65 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +290 -101
- package/dist/commands/web-fetch.d.ts +3 -2
- package/dist/commands/web-fetch.d.ts.map +1 -1
- package/dist/commands/web-fetch.js +25 -19
- package/dist/commands/web.d.ts +7 -1
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +9 -3
- 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 +19 -17
- 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 +64 -43
- 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/names.d.ts +43 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +87 -3
- 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/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
- 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 +120 -23
- package/dist/core/hooks/fork-lineage.d.ts +40 -0
- package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
- package/dist/core/hooks/fork-lineage.js +62 -0
- package/dist/core/hooks/resolve/transcript.d.ts +16 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +84 -0
- package/dist/core/hooks/shell-rm.d.ts +2 -0
- package/dist/core/hooks/shell-rm.d.ts.map +1 -0
- package/dist/core/hooks/shell-rm.js +105 -0
- package/dist/core/hooks/shell-tokens.d.ts +12 -0
- package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
- package/dist/core/hooks/shell-tokens.js +97 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
- package/dist/core/hooks/shell-waiter.js +3 -93
- 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/semantic/contract.d.ts +5 -5
- 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/storage/process-log.d.ts +12 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +10 -2
- 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/tunnel/allowlist.d.ts +44 -0
- package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
- package/dist/lib/tunnel/allowlist.js +204 -0
- package/dist/lib/tunnel/current-address.d.ts +41 -0
- package/dist/lib/tunnel/current-address.d.ts.map +1 -0
- package/dist/lib/tunnel/current-address.js +123 -0
- package/dist/lib/tunnel/gate.js +13 -6
- package/dist/lib/tunnel/processes.d.ts +39 -0
- package/dist/lib/tunnel/processes.d.ts.map +1 -0
- package/dist/lib/tunnel/processes.js +121 -0
- package/dist/lib/tunnel/state.d.ts +28 -3
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +98 -27
- 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 +8 -1
- package/src/commander.ts +32 -0
- package/src/commands/agents.ts +151 -74
- 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/tunnel.ts +394 -116
- package/src/commands/web-fetch.ts +30 -19
- package/src/commands/web.ts +9 -3
- 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 +35 -20
- package/src/core/agents/render/session-context.ts +92 -48
- 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/names.ts +117 -6
- 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/events/v3/support-storage/pack-reader.ts +45 -19
- 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 +139 -22
- package/src/core/hooks/fork-lineage.ts +82 -0
- package/src/core/hooks/resolve/transcript.ts +88 -0
- package/src/core/hooks/shell-rm.ts +103 -0
- package/src/core/hooks/shell-tokens.ts +96 -0
- package/src/core/hooks/shell-waiter.ts +4 -91
- 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/storage/process-log.ts +17 -2
- 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/tunnel/allowlist.ts +208 -0
- package/src/lib/tunnel/current-address.ts +158 -0
- package/src/lib/tunnel/gate.ts +16 -8
- package/src/lib/tunnel/processes.ts +149 -0
- package/src/lib/tunnel/state.ts +123 -27
- package/src/lib/zip/index.ts +483 -0
|
@@ -12,11 +12,10 @@ 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
|
-
import {
|
|
18
|
+
import { forkRelationLabels, readForkState } from "../state/names.ts";
|
|
20
19
|
import type { AgentActivity, TaskState } from "../state/session-state.ts";
|
|
21
20
|
|
|
22
21
|
interface HeartbeatRow {
|
|
@@ -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
|
/**
|
|
@@ -54,7 +55,17 @@ export function renderSessionContext(opts: RenderOpts): string {
|
|
|
54
55
|
|
|
55
56
|
// 1. Self-name line + peer table (folded if peers present)
|
|
56
57
|
const peers = readActivePeers(coordRoot, instanceId);
|
|
57
|
-
const
|
|
58
|
+
const forkState = readForkState(coordRoot, instanceId);
|
|
59
|
+
const forkParent = forkState?.parent ?? null;
|
|
60
|
+
const parentRow = forkParent
|
|
61
|
+
? peers.find((peer) => peer.instance_id === forkParent.instance_id)
|
|
62
|
+
: undefined;
|
|
63
|
+
const relations = forkRelationLabels(
|
|
64
|
+
coordRoot,
|
|
65
|
+
instanceId,
|
|
66
|
+
new Set(peers.map((peer) => peer.instance_id ?? "")),
|
|
67
|
+
);
|
|
68
|
+
const localTable = formatPeerTable(peers, sessionId, relations);
|
|
58
69
|
// Cross-machine presence (ADR 0016): sessions on other machines (advisory).
|
|
59
70
|
const peerTable = [localTable, formatRemoteMachines(coordRoot)].filter(Boolean).join("\n\n");
|
|
60
71
|
if (agentName) {
|
|
@@ -64,13 +75,11 @@ export function renderSessionContext(opts: RenderOpts): string {
|
|
|
64
75
|
// asserts the parent's name, claims, and task. When recorded fork lineage
|
|
65
76
|
// (.name-history forked_from) names the parent, say so specifically — the
|
|
66
77
|
// fork is about to read a transcript full of that exact name. Otherwise
|
|
67
|
-
// fall back to the generic clause, which needs no fork detection.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
? `This conversation was branched from another session (${forkParent.instance_id}); any agent name in earlier context belongs to that session, not to you.`
|
|
73
|
-
: "Any different agent name in earlier context was inherited from another session; this one is authoritative.";
|
|
78
|
+
// fall back to the generic clause, which needs no fork detection. A live
|
|
79
|
+
// parent is the dangerous case (2026-10-07: a fork read its running
|
|
80
|
+
// parent as its own earlier self and offered to close it), so the notice
|
|
81
|
+
// names what the parent is doing and forbids ending it.
|
|
82
|
+
const authority = forkAuthorityClause(forkParent, forkState?.fork === true, parentRow);
|
|
74
83
|
const selfLine = `You are agent-${agentName}${suffix}. ${authority}`;
|
|
75
84
|
messages.push(peerTable ? `${selfLine}\n\n${peerTable}` : selfLine);
|
|
76
85
|
} else if (peerTable) {
|
|
@@ -84,15 +93,12 @@ export function renderSessionContext(opts: RenderOpts): string {
|
|
|
84
93
|
);
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
// 3.
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
recordDeliveredMessages(instanceId, delivered);
|
|
94
|
-
messages.push(formatMailboxDelivery(coordRoot, delivered));
|
|
95
|
-
}
|
|
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));
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
// 4. Council invites
|
|
@@ -139,27 +145,6 @@ export function renderSessionContext(opts: RenderOpts): string {
|
|
|
139
145
|
return messages.join("\n\n");
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
/**
|
|
143
|
-
* Write delivered peer messages into the recipient's own journal, so they
|
|
144
|
-
* survive context compaction the way any other coordination breadcrumb does.
|
|
145
|
-
* A message the sender already journaled (the recipient was live when it was
|
|
146
|
-
* sent) is skipped to avoid a duplicate entry.
|
|
147
|
-
*/
|
|
148
|
-
export function recordDeliveredMessages(
|
|
149
|
-
instanceId: string,
|
|
150
|
-
messages: readonly MailboxMessageV1[],
|
|
151
|
-
): void {
|
|
152
|
-
for (const m of messages) {
|
|
153
|
-
if (m.journaled) continue;
|
|
154
|
-
try {
|
|
155
|
-
appendEntry(instanceId, "handoff", `from agent-${m.from_name}: ${m.body}`);
|
|
156
|
-
} catch {
|
|
157
|
-
// A journal write failure must never cost the delivery itself; the
|
|
158
|
-
// message is already in the rendered context the model reads.
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
148
|
/** One-line-per-agent view of sessions on other machines (presence refs). */
|
|
164
149
|
function formatRemoteMachines(coordRoot: string): string {
|
|
165
150
|
try {
|
|
@@ -194,13 +179,64 @@ function readActivePeers(coordRoot: string, selfInstanceId: string): HeartbeatRo
|
|
|
194
179
|
);
|
|
195
180
|
}
|
|
196
181
|
|
|
182
|
+
const DO_NOT_END =
|
|
183
|
+
"Do not end, release, or finalize that session unless the operator explicitly tells you to.";
|
|
184
|
+
|
|
185
|
+
function forkAuthorityClause(
|
|
186
|
+
parent: { instance_id: string; name: string | null } | null,
|
|
187
|
+
fork: boolean,
|
|
188
|
+
parentRow: HeartbeatRow | undefined,
|
|
189
|
+
): string {
|
|
190
|
+
if (parent?.name && parentRow) {
|
|
191
|
+
const files = parentRow.files_touched ?? [];
|
|
192
|
+
const task = parentRow.task ? ` on "${parentRow.task.slice(0, 80)}"` : "";
|
|
193
|
+
const holds =
|
|
194
|
+
files.length > 0
|
|
195
|
+
? ` and holds ${files.length} file${files.length === 1 ? "" : "s"} (${fmtFileList(files)})`
|
|
196
|
+
: "";
|
|
197
|
+
const owned =
|
|
198
|
+
files.length > 0
|
|
199
|
+
? parentRow.task
|
|
200
|
+
? "That name, task, and those files"
|
|
201
|
+
: "That name and those files"
|
|
202
|
+
: parentRow.task
|
|
203
|
+
? "That name and task"
|
|
204
|
+
: "That name";
|
|
205
|
+
return (
|
|
206
|
+
`This conversation was forked from agent-${parent.name}, which is still running as a separate session${task}${holds}. ` +
|
|
207
|
+
`${owned} belong${owned === "That name" ? "s" : ""} to agent-${parent.name}, not to you. ${DO_NOT_END}`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (parent?.name) {
|
|
211
|
+
return `This conversation was branched from agent-${parent.name}'s session: earlier context showing agent-${parent.name}'s name, task, or file claims belongs to the pre-fork session, not to you.`;
|
|
212
|
+
}
|
|
213
|
+
if (parent) {
|
|
214
|
+
return `This conversation was branched from another session (${parent.instance_id}); any agent name in earlier context belongs to that session, not to you.`;
|
|
215
|
+
}
|
|
216
|
+
if (fork) {
|
|
217
|
+
return `This conversation was forked from another session, which may still be running. Any agent name, task, or file claim in earlier context belongs to that session, not to you. ${DO_NOT_END}`;
|
|
218
|
+
}
|
|
219
|
+
return "Any different agent name in earlier context was inherited from another session; this one is authoritative.";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function fmtFileList(files: string[]): string {
|
|
223
|
+
const sorted = [...files].sort();
|
|
224
|
+
return sorted.length <= 3
|
|
225
|
+
? sorted.join(", ")
|
|
226
|
+
: `${sorted.slice(0, 3).join(", ")}, +${sorted.length - 3} more`;
|
|
227
|
+
}
|
|
228
|
+
|
|
197
229
|
/**
|
|
198
230
|
* Renders the peer table as
|
|
199
231
|
* two subsections: "Other agent groups active" (blocking) and "Your group"
|
|
200
232
|
* (subagents/siblings, no mutual block). Folds transient subagents' files
|
|
201
233
|
* into their parent session.
|
|
202
234
|
*/
|
|
203
|
-
function formatPeerTable(
|
|
235
|
+
function formatPeerTable(
|
|
236
|
+
peers: HeartbeatRow[],
|
|
237
|
+
mySessionId: string,
|
|
238
|
+
relations: Map<string, string> = new Map(),
|
|
239
|
+
): string {
|
|
204
240
|
if (peers.length === 0) return "";
|
|
205
241
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
206
242
|
|
|
@@ -214,16 +250,20 @@ function formatPeerTable(peers: HeartbeatRow[], mySessionId: string): string {
|
|
|
214
250
|
}
|
|
215
251
|
|
|
216
252
|
// Build rows with display_files (own files + folded transient files).
|
|
217
|
-
type RowExt = HeartbeatRow & { display_files: string[] };
|
|
253
|
+
type RowExt = HeartbeatRow & { display_files: string[]; relation?: string };
|
|
218
254
|
const rows: RowExt[] = peers
|
|
219
255
|
.filter((p) => (p.kind ?? "unknown") !== "transient")
|
|
220
256
|
.map((p) => {
|
|
221
257
|
const folded = fold[p.instance_id ?? ""] ?? [];
|
|
222
258
|
const display = Array.from(new Set([...(p.files_touched ?? []), ...folded])).sort();
|
|
223
|
-
|
|
259
|
+
const relation = relations.get(p.instance_id ?? "");
|
|
260
|
+
return { ...p, display_files: display, ...(relation ? { relation } : {}) };
|
|
224
261
|
});
|
|
225
262
|
|
|
226
|
-
|
|
263
|
+
// Related sessions sort first so the parent never hides in "+N more".
|
|
264
|
+
const blocking = rows
|
|
265
|
+
.filter((p) => p.session_id !== mySessionId)
|
|
266
|
+
.sort((a, b) => Number(!!b.relation) - Number(!!a.relation) || byStartedAt(a, b));
|
|
227
267
|
const group = rows.filter((p) => p.session_id === mySessionId).sort(byStartedAt);
|
|
228
268
|
|
|
229
269
|
const sections: string[] = [];
|
|
@@ -248,7 +288,7 @@ function byStartedAt(a: HeartbeatRow, b: HeartbeatRow): number {
|
|
|
248
288
|
}
|
|
249
289
|
|
|
250
290
|
function renderSubtable(
|
|
251
|
-
rows: Array<HeartbeatRow & { display_files: string[] }>,
|
|
291
|
+
rows: Array<HeartbeatRow & { display_files: string[]; relation?: string }>,
|
|
252
292
|
header: string,
|
|
253
293
|
nowSec: number,
|
|
254
294
|
): string {
|
|
@@ -258,7 +298,10 @@ function renderSubtable(
|
|
|
258
298
|
return `${header}\n${first.join("\n")}${overflow}`;
|
|
259
299
|
}
|
|
260
300
|
|
|
261
|
-
function formatRow(
|
|
301
|
+
function formatRow(
|
|
302
|
+
r: HeartbeatRow & { display_files: string[]; relation?: string },
|
|
303
|
+
nowSec: number,
|
|
304
|
+
): string {
|
|
262
305
|
const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
|
|
263
306
|
const activity = r.activity ?? "unknown";
|
|
264
307
|
const lifecycle = r.task_state ?? "active";
|
|
@@ -266,7 +309,8 @@ function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number
|
|
|
266
309
|
lifecycle === "blocked" && r.task_state_reason ? `: ${r.task_state_reason.slice(0, 80)}` : "";
|
|
267
310
|
const ageFrom = fmtAge(nowSec - parseIsoSec(r.started_at));
|
|
268
311
|
const filesPart = fmtFiles(r.display_files);
|
|
269
|
-
|
|
312
|
+
const relation = r.relation ? ` [${r.relation}]` : "";
|
|
313
|
+
return ` - agent-${r.name ?? "unknown"}${relation}${taskPart} (activity=${activity}, lifecycle=${lifecycle}${reason}, ${ageFrom}, ${filesPart})`;
|
|
270
314
|
}
|
|
271
315
|
|
|
272
316
|
function fmtFiles(files: string[]): string {
|
|
@@ -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
|
}
|
|
@@ -331,13 +331,20 @@ export interface NameHistoryRow {
|
|
|
331
331
|
ts: string;
|
|
332
332
|
/** Durable persona UUID. Present after `agents identity assume`. */
|
|
333
333
|
agent_id?: string;
|
|
334
|
-
/** Audit marker distinguishing an explicit role adoption from pool assignment
|
|
335
|
-
|
|
334
|
+
/** Audit marker distinguishing an explicit role adoption from pool assignment,
|
|
335
|
+
* and a lineage row appended after the name was already assigned. */
|
|
336
|
+
source?: "pool" | "identity.assume" | "fork.detected";
|
|
336
337
|
previous_name?: string;
|
|
337
338
|
/** Instance this session was forked/branched from (recorded fork lineage).
|
|
338
339
|
* Stamped only on the row that first assigns this instance, when the adapter
|
|
339
340
|
* layer detected or supplied a parent conversation. */
|
|
340
341
|
forked_from?: string;
|
|
342
|
+
/** The adapter declared or inherited-history proved this instance is a fork,
|
|
343
|
+
* even when its parent could not be resolved yet. */
|
|
344
|
+
fork?: true;
|
|
345
|
+
/** Fork detection has run against this instance's transcript; the tool-path
|
|
346
|
+
* heal skips instances carrying it so the scan runs once per instance. */
|
|
347
|
+
fork_checked?: true;
|
|
341
348
|
}
|
|
342
349
|
|
|
343
350
|
function atomicWrite(path: string, content: string): void {
|
|
@@ -516,7 +523,13 @@ export function assignName(
|
|
|
516
523
|
coordRoot: string,
|
|
517
524
|
instanceId: string,
|
|
518
525
|
kind: NameKind,
|
|
519
|
-
opts: {
|
|
526
|
+
opts: {
|
|
527
|
+
freshnessSecs: number;
|
|
528
|
+
forkedFrom?: string;
|
|
529
|
+
fork?: boolean;
|
|
530
|
+
forkChecked?: boolean;
|
|
531
|
+
nowMs?: number;
|
|
532
|
+
},
|
|
520
533
|
): string {
|
|
521
534
|
// Check 1: existing history row → original name. A resume re-enters here,
|
|
522
535
|
// which also makes fork stamping naturally idempotent: lineage lands only on
|
|
@@ -558,13 +571,111 @@ export function assignName(
|
|
|
558
571
|
kind,
|
|
559
572
|
source: "pool",
|
|
560
573
|
...(forkedFrom && forkedFrom !== instanceId ? { forked_from: forkedFrom } : {}),
|
|
574
|
+
...(opts.fork || (forkedFrom && forkedFrom !== instanceId) ? { fork: true as const } : {}),
|
|
575
|
+
...(opts.forkChecked ? { fork_checked: true as const } : {}),
|
|
561
576
|
ts: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
562
577
|
});
|
|
563
578
|
return name;
|
|
564
579
|
}
|
|
565
580
|
|
|
581
|
+
export interface ForkState {
|
|
582
|
+
/** True when the instance is a recorded fork, parent known or not. */
|
|
583
|
+
fork: boolean;
|
|
584
|
+
/** Detection already ran for this instance. */
|
|
585
|
+
checked: boolean;
|
|
586
|
+
parent: { instance_id: string; name: string | null } | null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Recorded fork state for <instanceId>, or null when it has no history row. */
|
|
590
|
+
export function readForkState(coordRoot: string, instanceId: string): ForkState | null {
|
|
591
|
+
const history = readHistory(coordRoot);
|
|
592
|
+
let seen = false;
|
|
593
|
+
let fork = false;
|
|
594
|
+
let checked = false;
|
|
595
|
+
let parentId: string | undefined;
|
|
596
|
+
for (const row of history) {
|
|
597
|
+
if (row.instance_id !== instanceId) continue;
|
|
598
|
+
seen = true;
|
|
599
|
+
if (row.fork || row.forked_from) fork = true;
|
|
600
|
+
if (row.fork_checked) checked = true;
|
|
601
|
+
if (row.forked_from && row.forked_from !== instanceId) parentId = row.forked_from;
|
|
602
|
+
}
|
|
603
|
+
if (!seen) return null;
|
|
604
|
+
const parent = parentId
|
|
605
|
+
? { instance_id: parentId, name: resolveName(coordRoot, parentId)?.name ?? null }
|
|
606
|
+
: null;
|
|
607
|
+
return { fork, checked, parent };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Record fork detection that ran after the instance was already named (the
|
|
612
|
+
* tool-path heal). Appends a copy of the latest identity row, so the name,
|
|
613
|
+
* kind, and persona are unchanged, plus `fork_checked` and any lineage found.
|
|
614
|
+
* A no-op for an instance with no history row or one already checked.
|
|
615
|
+
*/
|
|
616
|
+
export function recordForkDetection(
|
|
617
|
+
coordRoot: string,
|
|
618
|
+
instanceId: string,
|
|
619
|
+
detection: { fork: boolean; forkedFrom?: string },
|
|
620
|
+
): boolean {
|
|
621
|
+
const history = readHistory(coordRoot);
|
|
622
|
+
let latest: NameHistoryRow | undefined;
|
|
623
|
+
for (const row of history) {
|
|
624
|
+
if (row.instance_id !== instanceId) continue;
|
|
625
|
+
if (row.fork_checked) return false;
|
|
626
|
+
latest = row;
|
|
627
|
+
}
|
|
628
|
+
if (!latest) return false;
|
|
629
|
+
const forkedFrom =
|
|
630
|
+
detection.forkedFrom && detection.forkedFrom !== instanceId ? detection.forkedFrom : undefined;
|
|
631
|
+
appendHistory(coordRoot, {
|
|
632
|
+
instance_id: instanceId,
|
|
633
|
+
name: latest.name,
|
|
634
|
+
kind: latest.kind,
|
|
635
|
+
...(latest.agent_id ? { agent_id: latest.agent_id } : {}),
|
|
636
|
+
source: "fork.detected",
|
|
637
|
+
...(forkedFrom ? { forked_from: forkedFrom } : {}),
|
|
638
|
+
...(detection.fork || forkedFrom || latest.fork ? { fork: true as const } : {}),
|
|
639
|
+
fork_checked: true,
|
|
640
|
+
ts: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
641
|
+
});
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Peer labels for recorded fork relations, keyed by instance id: this
|
|
647
|
+
* session's fork parent and its direct forks. `liveIds` marks which of them
|
|
648
|
+
* are running, so a fork can tell its live parent from its own earlier self.
|
|
649
|
+
*/
|
|
650
|
+
export function forkRelationLabels(
|
|
651
|
+
coordRoot: string,
|
|
652
|
+
instanceId: string,
|
|
653
|
+
liveIds: Set<string>,
|
|
654
|
+
): Map<string, string> {
|
|
655
|
+
const out = new Map<string, string>();
|
|
656
|
+
const parent = readForkParent(coordRoot, instanceId);
|
|
657
|
+
if (parent) {
|
|
658
|
+
out.set(
|
|
659
|
+
parent.instance_id,
|
|
660
|
+
liveIds.has(parent.instance_id) ? "your fork parent, live" : "your fork parent",
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
for (const child of readForkChildren(coordRoot, instanceId)) out.set(child, "your fork");
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Instance ids recorded as direct forks of <instanceId>. */
|
|
668
|
+
export function readForkChildren(coordRoot: string, instanceId: string): Set<string> {
|
|
669
|
+
const out = new Set<string>();
|
|
670
|
+
for (const row of readHistory(coordRoot)) {
|
|
671
|
+
if (row.forked_from === instanceId && row.instance_id !== instanceId) out.add(row.instance_id);
|
|
672
|
+
}
|
|
673
|
+
return out;
|
|
674
|
+
}
|
|
675
|
+
|
|
566
676
|
/** One step of recorded fork lineage: the latest row for <instanceId> that
|
|
567
|
-
* carries `forked_from
|
|
677
|
+
* carries `forked_from`. Later rows without lineage (an identity assumption)
|
|
678
|
+
* do not erase it: a session never stops being a fork. */
|
|
568
679
|
export function readForkParent(
|
|
569
680
|
coordRoot: string,
|
|
570
681
|
instanceId: string,
|
|
@@ -572,8 +683,8 @@ export function readForkParent(
|
|
|
572
683
|
const history = readHistory(coordRoot);
|
|
573
684
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
574
685
|
const row = history[i]!;
|
|
575
|
-
if (row.instance_id !== instanceId) continue;
|
|
576
|
-
if (
|
|
686
|
+
if (row.instance_id !== instanceId || !row.forked_from) continue;
|
|
687
|
+
if (row.forked_from === instanceId) return null;
|
|
577
688
|
const parent = resolveName(coordRoot, row.forked_from);
|
|
578
689
|
return { instance_id: row.forked_from, name: parent?.name ?? null };
|
|
579
690
|
}
|
|
@@ -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
|
*
|