harnery 0.44.0 → 0.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/commands/workflow.d.ts.map +1 -1
  2. package/dist/commands/workflow.js +44 -0
  3. package/dist/core/agents/coord-client.d.ts.map +1 -1
  4. package/dist/core/agents/coord-client.js +18 -0
  5. package/dist/core/agents/live-authority-v3.d.ts +0 -1
  6. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  7. package/dist/core/agents/live-authority-v3.js +0 -11
  8. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
  9. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  10. package/dist/core/agents/live-lifecycle-v3.js +7 -28
  11. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  12. package/dist/core/agents/state/heartbeat-writer.js +14 -25
  13. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  14. package/dist/core/agents/state/live-coordination-writer.js +0 -5
  15. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  16. package/dist/core/agents/state/pidmap.js +0 -5
  17. package/dist/core/agents/state/proc-start.d.ts +5 -6
  18. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  19. package/dist/core/agents/state/proc-start.js +13 -9
  20. package/dist/core/agents/state/process-handle.d.ts +26 -0
  21. package/dist/core/agents/state/process-handle.d.ts.map +1 -0
  22. package/dist/core/agents/state/process-handle.js +121 -0
  23. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  24. package/dist/core/agents/state/stale-sweep.js +70 -180
  25. package/dist/core/artifacts/activity.d.ts +4 -1
  26. package/dist/core/artifacts/activity.d.ts.map +1 -1
  27. package/dist/core/artifacts/activity.js +8 -2
  28. package/dist/core/artifacts/index.d.ts.map +1 -1
  29. package/dist/core/artifacts/index.js +38 -12
  30. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  31. package/dist/core/events/v3/authority-outbox.js +0 -29
  32. package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
  33. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  34. package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
  35. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  36. package/dist/core/events/v3/producers/recorder.js +64 -77
  37. package/dist/core/hooks/cli.js +78 -48
  38. package/dist/core/resources/service.d.ts +2 -16
  39. package/dist/core/resources/service.d.ts.map +1 -1
  40. package/dist/core/resources/service.js +1 -78
  41. package/dist/core/semantic/service.d.ts.map +1 -1
  42. package/dist/core/semantic/service.js +2 -9
  43. package/dist/core/servers/index.d.ts +4 -0
  44. package/dist/core/servers/index.d.ts.map +1 -1
  45. package/dist/core/servers/index.js +21 -3
  46. package/dist/core/storage/durable-history.js +56 -22
  47. package/dist/core/storage/segments.d.ts.map +1 -1
  48. package/dist/core/storage/segments.js +73 -17
  49. package/dist/core/supervisor/service.d.ts +2 -4
  50. package/dist/core/supervisor/service.d.ts.map +1 -1
  51. package/dist/core/work/state.js +7 -7
  52. package/dist/core/workflow/engine.d.ts.map +1 -1
  53. package/dist/core/workflow/engine.js +21 -65
  54. package/dist/core/workflow/index.d.ts +4 -2
  55. package/dist/core/workflow/index.d.ts.map +1 -1
  56. package/dist/core/workflow/index.js +2 -1
  57. package/dist/core/workflow/live-session-v3.d.ts +0 -6
  58. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  59. package/dist/core/workflow/live-session-v3.js +5 -126
  60. package/dist/core/workflow/proof.d.ts +4 -4
  61. package/dist/core/workflow/proof.d.ts.map +1 -1
  62. package/dist/core/workflow/proof.js +4 -12
  63. package/dist/core/workflow/recovery-audit.d.ts +30 -0
  64. package/dist/core/workflow/recovery-audit.d.ts.map +1 -0
  65. package/dist/core/workflow/recovery-audit.js +116 -0
  66. package/dist/core/workflow/run-state.d.ts +19 -2
  67. package/dist/core/workflow/run-state.d.ts.map +1 -1
  68. package/dist/core/workflow/run-state.js +113 -8
  69. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  70. package/dist/core/workflow/spawn-claude.js +0 -19
  71. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  72. package/dist/core/workflow/spawn-codex.js +0 -13
  73. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  74. package/dist/core/workflow/spawn-cursor.js +0 -19
  75. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  76. package/dist/core/workflow/spawn-opencode.js +0 -14
  77. package/dist/core/workflow/types.d.ts +11 -54
  78. package/dist/core/workflow/types.d.ts.map +1 -1
  79. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  80. package/dist/core/workflow/workspaces/local-git.js +70 -14
  81. package/dist/core/workflow/workspaces/reclaim.d.ts +6 -1
  82. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  83. package/dist/core/workflow/workspaces/recovery.d.ts +2 -0
  84. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  85. package/dist/core/workflow/workspaces/recovery.js +24 -0
  86. package/dist/lib/identities/assume.d.ts.map +1 -1
  87. package/dist/lib/identities/assume.js +0 -4
  88. package/package.json +1 -1
  89. package/src/commands/workflow.ts +59 -0
  90. package/src/core/agents/coord-client.ts +20 -0
  91. package/src/core/agents/live-authority-v3.ts +0 -17
  92. package/src/core/agents/live-lifecycle-v3.ts +14 -52
  93. package/src/core/agents/state/heartbeat-writer.ts +12 -29
  94. package/src/core/agents/state/live-coordination-writer.ts +0 -29
  95. package/src/core/agents/state/pidmap.ts +0 -13
  96. package/src/core/agents/state/proc-start.ts +14 -11
  97. package/src/core/agents/state/process-handle.ts +165 -0
  98. package/src/core/agents/state/stale-sweep.ts +82 -247
  99. package/src/core/artifacts/activity.ts +9 -2
  100. package/src/core/artifacts/index.ts +45 -12
  101. package/src/core/events/v3/authority-outbox.ts +0 -56
  102. package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
  103. package/src/core/events/v3/producers/recorder.ts +68 -94
  104. package/src/core/hooks/cli.ts +85 -47
  105. package/src/core/resources/service.ts +3 -88
  106. package/src/core/semantic/service.ts +2 -8
  107. package/src/core/servers/index.ts +30 -3
  108. package/src/core/storage/durable-history.ts +52 -20
  109. package/src/core/storage/segments.ts +79 -17
  110. package/src/core/supervisor/service.ts +2 -3
  111. package/src/core/work/state.ts +7 -7
  112. package/src/core/workflow/engine.ts +21 -73
  113. package/src/core/workflow/index.ts +12 -1
  114. package/src/core/workflow/live-session-v3.ts +5 -146
  115. package/src/core/workflow/proof.ts +5 -21
  116. package/src/core/workflow/recovery-audit.ts +181 -0
  117. package/src/core/workflow/run-state.ts +137 -8
  118. package/src/core/workflow/spawn-claude.ts +0 -18
  119. package/src/core/workflow/spawn-codex.ts +0 -13
  120. package/src/core/workflow/spawn-cursor.ts +0 -18
  121. package/src/core/workflow/spawn-opencode.ts +0 -14
  122. package/src/core/workflow/types.ts +11 -41
  123. package/src/core/workflow/workspaces/local-git.ts +86 -15
  124. package/src/core/workflow/workspaces/reclaim.ts +4 -0
  125. package/src/core/workflow/workspaces/recovery.ts +23 -0
  126. package/src/lib/identities/assume.ts +0 -4
  127. package/dist/core/agents/state/unchanged-file.d.ts +0 -44
  128. package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
  129. package/dist/core/agents/state/unchanged-file.js +0 -390
  130. package/dist/core/events/v3/generation-custody.d.ts +0 -5
  131. package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
  132. package/dist/core/events/v3/generation-custody.js +0 -50
  133. package/dist/core/workflow/failure-evidence.d.ts +0 -16
  134. package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
  135. package/dist/core/workflow/failure-evidence.js +0 -114
  136. package/src/core/agents/state/unchanged-file.ts +0 -456
  137. package/src/core/events/v3/generation-custody.ts +0 -59
  138. package/src/core/workflow/failure-evidence.ts +0 -141
@@ -24,11 +24,6 @@ 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";
32
27
  import { canonicalClaimPath } from "../claim-path.ts";
33
28
  import { type Heartbeat, heartbeatPath, readHeartbeat } from "./heartbeat-reader.ts";
34
29
  import type { TaskState } from "./session-state.ts";
@@ -68,15 +63,12 @@ function mutate(
68
63
  instanceId: string,
69
64
  fn: (hb: Heartbeat) => Heartbeat,
70
65
  ): Heartbeat | null {
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
- });
66
+ const hb = readHeartbeat(coordRoot, instanceId);
67
+ if (!hb) return null;
68
+ const next = fn(hb);
69
+ next.last_heartbeat = nowIsoSeconds();
70
+ atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(next, null, 2));
71
+ return next;
80
72
  }
81
73
 
82
74
  export function setTask(coordRoot: string, instanceId: string, task: string): Heartbeat | null {
@@ -108,15 +100,12 @@ export function setIdentityCache(
108
100
  name: string,
109
101
  agentId: string,
110
102
  ): Heartbeat | null {
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
- });
103
+ const heartbeat = readHeartbeat(coordRoot, instanceId);
104
+ if (!heartbeat) return null;
105
+ heartbeat.name = name;
106
+ heartbeat.agent_id = agentId;
107
+ atomicWrite(heartbeatPath(coordRoot, instanceId), JSON.stringify(heartbeat, null, 2));
108
+ return heartbeat;
120
109
  }
121
110
 
122
111
  /** Restore the pool-assigned display name on a generation that was
@@ -339,12 +328,6 @@ export function clearCoordinationCache(coordRoot: string, instanceId: string): b
339
328
  }
340
329
 
341
330
  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 {
348
331
  const dir = join(coordRoot, ".harnery", "pid-map");
349
332
  mkdirSync(dir, { recursive: true });
350
333
  const hb = readHeartbeat(coordRoot, instanceId);
@@ -5,10 +5,6 @@ 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";
12
8
  import { liveInstanceIdV3, resolveLiveEventLedgerRouteV3 } from "../../events/v3/live-routing.ts";
13
9
  import { type Heartbeat, readHeartbeat } from "./heartbeat-reader.ts";
14
10
  import {
@@ -68,26 +64,6 @@ function materializeLiveCoordinationHeartbeat(
68
64
  _adapter: Adapter,
69
65
  model: string | undefined,
70
66
  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,
91
67
  ): Heartbeat | null {
92
68
  const route = resolveLiveEventLedgerRouteV3(coordRoot);
93
69
  if (route.state === "blocked") throw new Error(`event_v3_coordination_view:${route.reason}`);
@@ -157,11 +133,6 @@ function materializeHeartbeatUnderCustody(
157
133
  v3_projection_event_id: generation.last_event_id,
158
134
  v3_task_state: generation.task_state === "set" ? "set" : "cleared",
159
135
  };
160
- assertCurrentGenerationV3(
161
- coordRoot,
162
- liveInstanceIdV3(nativeInstanceId),
163
- generation.generation_id,
164
- );
165
136
  writeHeartbeatCache(coordRoot, nativeInstanceId, materialized);
166
137
  return materialized;
167
138
  }
@@ -24,8 +24,6 @@ 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";
29
27
  import { checkPidToken, processStartToken } from "./proc-start.ts";
30
28
 
31
29
  /**
@@ -128,17 +126,6 @@ export function writePidmapRow(
128
126
  pid: number,
129
127
  instanceId: string,
130
128
  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,
142
129
  ): void {
143
130
  const dir = join(coordRoot, ".harnery", "pid-map");
144
131
  const path = join(dir, String(pid));
@@ -34,9 +34,10 @@
34
34
  import { spawnSync } from "node:child_process";
35
35
  import { existsSync, readFileSync } from "node:fs";
36
36
  import { coordEnv } from "../../../lib/env.ts";
37
+ import { signalProcessWithHandle, windowsProcessStartToken } from "./process-handle.ts";
37
38
 
38
39
  /** Which probe answers "when did this process start?" on this machine. */
39
- export type StartTokenProbe = "procfs" | "ps";
40
+ export type StartTokenProbe = "procfs" | "ps" | "windows";
40
41
 
41
42
  /**
42
43
  * The `ps` probe is text formatted by `strftime("%c", localtime(...))`, so its
@@ -60,6 +61,7 @@ let cachedBootId: string | null | undefined;
60
61
  export function startTokenProbe(): StartTokenProbe {
61
62
  const override = coordEnv("PID_PROBE");
62
63
  if (override === "procfs" || override === "ps") return override;
64
+ if (process.platform === "win32") return "windows";
63
65
  if (cachedProbe === undefined) {
64
66
  cachedProbe = existsSync("/proc/self/stat") ? "procfs" : "ps";
65
67
  }
@@ -157,7 +159,12 @@ function defaultPsRun(pid: number): { status: number | null; stdout: string } {
157
159
  */
158
160
  export function processStartToken(pid: number): string | null {
159
161
  if (!Number.isFinite(pid) || pid <= 0) return null;
160
- return startTokenProbe() === "procfs" ? procfsStartToken(pid) : psStartToken(pid);
162
+ const probe = startTokenProbe();
163
+ return probe === "windows"
164
+ ? windowsProcessStartToken(pid)
165
+ : probe === "procfs"
166
+ ? procfsStartToken(pid)
167
+ : psStartToken(pid);
161
168
  }
162
169
 
163
170
  export type ProcessGenerationVerdict = "same" | "different" | "unknown";
@@ -191,14 +198,12 @@ export function verifyProcessGeneration(
191
198
 
192
199
  export interface ProcessGenerationSignalDependencies {
193
200
  readToken?: ReadProcessStartToken;
194
- kill?: (pid: number, signal: NodeJS.Signals | number) => unknown;
201
+ signalProcess?: typeof signalProcessWithHandle;
195
202
  }
196
203
 
197
204
  /**
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.
205
+ * Observations may refuse a signal, but only the retained native process handle
206
+ * can authorize it. No numeric-PID effect follows the preliminary token read.
202
207
  */
203
208
  export function signalIfSameGeneration(
204
209
  pid: number,
@@ -206,14 +211,12 @@ export function signalIfSameGeneration(
206
211
  signal: NodeJS.Signals | number,
207
212
  dependencies: ProcessGenerationSignalDependencies = {},
208
213
  ): { sent: boolean; verdict: ProcessGenerationVerdict } {
209
- const kill = dependencies.kill ?? process.kill;
210
214
  const verdict = verifyProcessGeneration(pid, recordedToken, dependencies.readToken);
211
215
  if (verdict !== "same") return { sent: false, verdict };
212
216
  try {
213
- kill(pid, signal);
214
- return { sent: true, verdict };
217
+ return (dependencies.signalProcess ?? signalProcessWithHandle)(pid, recordedToken!, signal);
215
218
  } catch {
216
- return { sent: false, verdict };
219
+ return { sent: false, verdict: "unknown" };
217
220
  }
218
221
  }
219
222
 
@@ -0,0 +1,165 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { constants } from "node:os";
3
+ import type { ProcessGenerationVerdict } from "./proc-start.ts";
4
+
5
+ export interface ProcessHandleSignalResult {
6
+ sent: boolean;
7
+ verdict: ProcessGenerationVerdict;
8
+ }
9
+
10
+ interface NativeHelperResult extends ProcessHandleSignalResult {
11
+ token?: string;
12
+ }
13
+
14
+ export type NativeProcessHelperRunner = (
15
+ command: string,
16
+ args: string[],
17
+ options: {
18
+ input: string;
19
+ encoding: "utf8";
20
+ timeout: number;
21
+ maxBuffer: number;
22
+ windowsHide: boolean;
23
+ },
24
+ ) => { status: number | null; stdout: string | Buffer | null; error?: Error };
25
+
26
+ // The helper owns the descriptor throughout observation and effect. The second
27
+ // input line authorizes the effect; EOF releases the handle without signaling.
28
+ export const LINUX_PROCESS_HANDLE_SCRIPT = `
29
+ import json, os, pathlib, signal, sys
30
+ result = {"sent": False, "verdict": "unknown"}
31
+ fd = None
32
+ try:
33
+ request = json.loads(sys.stdin.readline())
34
+ if not hasattr(os, "pidfd_open") or not hasattr(signal, "pidfd_send_signal"):
35
+ raise RuntimeError("pidfd support unavailable")
36
+ fd = os.pidfd_open(request["pid"])
37
+ stat = pathlib.Path("/proc/%d/stat" % request["pid"]).read_text()
38
+ ticks = stat[stat.rfind(") ") + 2:].split()[19]
39
+ boot = pathlib.Path("/proc/sys/kernel/random/boot_id").read_text().strip().replace("-", "")[:8]
40
+ token = "l%s.%s" % (boot, ticks)
41
+ result["token"] = token
42
+ recorded = request.get("token")
43
+ result["verdict"] = "same" if token == recorded else "different"
44
+ if request["mode"] == "signal" and result["verdict"] == "same":
45
+ print(json.dumps({"phase": "verified"}), flush=True)
46
+ if sys.stdin.readline().strip() == "signal":
47
+ signal.pidfd_send_signal(fd, request["signal"])
48
+ result["sent"] = True
49
+ except Exception:
50
+ pass
51
+ finally:
52
+ if fd is not None:
53
+ os.close(fd)
54
+ print(json.dumps(result), flush=True)
55
+ `;
56
+
57
+ export const WINDOWS_PROCESS_HANDLE_SCRIPT = `
58
+ $ErrorActionPreference = 'Stop'
59
+ $result = @{ sent = $false; verdict = 'unknown' }
60
+ $handle = [IntPtr]::Zero
61
+ try {
62
+ Add-Type -TypeDefinition @'
63
+ using System;
64
+ using System.Runtime.InteropServices;
65
+ public static class HarneryProcessHandle {
66
+ [DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr OpenProcess(uint access, bool inherit, uint processId);
67
+ [DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetProcessTimes(IntPtr handle, out long creation, out long exit, out long kernel, out long user);
68
+ [DllImport("kernel32.dll", SetLastError=true)] public static extern bool TerminateProcess(IntPtr handle, uint exitCode);
69
+ [DllImport("kernel32.dll")] public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
70
+ [DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr handle);
71
+ }
72
+ '@
73
+ $request = [Console]::In.ReadLine() | ConvertFrom-Json
74
+ $access = [uint32]0x101000
75
+ if ($request.mode -eq 'signal') { $access = $access -bor 1 }
76
+ $handle = [HarneryProcessHandle]::OpenProcess($access, $false, [uint32]$request.pid)
77
+ if ($handle -eq [IntPtr]::Zero) { throw 'process handle unavailable' }
78
+ [long]$creation = 0; [long]$exitTime = 0; [long]$kernel = 0; [long]$userTime = 0
79
+ if (-not [HarneryProcessHandle]::GetProcessTimes($handle, [ref]$creation, [ref]$exitTime, [ref]$kernel, [ref]$userTime)) { throw 'process time unavailable' }
80
+ $token = 'w' + $creation.ToString([Globalization.CultureInfo]::InvariantCulture)
81
+ $result.token = $token
82
+ $result.verdict = if ($token -ceq $request.token) { 'same' } else { 'different' }
83
+ if ($request.mode -eq 'signal' -and $result.verdict -eq 'same') {
84
+ [Console]::Out.WriteLine('{"phase":"verified"}')
85
+ [Console]::Out.Flush()
86
+ if ([Console]::In.ReadLine() -eq 'signal' -and [HarneryProcessHandle]::WaitForSingleObject($handle, 0) -eq 258) {
87
+ $result.sent = [HarneryProcessHandle]::TerminateProcess($handle, 1)
88
+ }
89
+ }
90
+ } catch {} finally {
91
+ if ($handle -ne [IntPtr]::Zero) { [void][HarneryProcessHandle]::CloseHandle($handle) }
92
+ }
93
+ $result | ConvertTo-Json -Compress
94
+ `;
95
+
96
+ function runHelper(
97
+ request: { mode: "probe" | "signal"; pid: number; token?: string; signal?: number },
98
+ platform: NodeJS.Platform,
99
+ run: NativeProcessHelperRunner,
100
+ ): NativeHelperResult {
101
+ const unknown = { sent: false, verdict: "unknown" } as const;
102
+ if (!Number.isInteger(request.pid) || request.pid <= 0) return unknown;
103
+ if (platform !== "linux" && platform !== "win32") return unknown;
104
+ try {
105
+ const command = platform === "linux" ? "python3" : "powershell.exe";
106
+ const args =
107
+ platform === "linux"
108
+ ? ["-I", "-c", LINUX_PROCESS_HANDLE_SCRIPT]
109
+ : ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", WINDOWS_PROCESS_HANDLE_SCRIPT];
110
+ const output = run(command, args, {
111
+ input: `${JSON.stringify(request)}\nsignal\n`,
112
+ encoding: "utf8",
113
+ timeout: 10_000,
114
+ maxBuffer: 64 * 1024,
115
+ windowsHide: true,
116
+ });
117
+ if (output.error || output.status !== 0) return unknown;
118
+ const lines = String(output.stdout ?? "")
119
+ .trim()
120
+ .split(/\r?\n/);
121
+ const parsed = JSON.parse(lines.at(-1) ?? "") as NativeHelperResult;
122
+ if (
123
+ typeof parsed.sent !== "boolean" ||
124
+ !["same", "different", "unknown"].includes(parsed.verdict) ||
125
+ (parsed.sent && parsed.verdict !== "same")
126
+ )
127
+ return unknown;
128
+ return parsed;
129
+ } catch {
130
+ return unknown;
131
+ }
132
+ }
133
+
134
+ /** Windows creation FILETIME, read from an OpenProcess handle. */
135
+ export function windowsProcessStartToken(
136
+ pid: number,
137
+ run: NativeProcessHelperRunner = spawnSync,
138
+ ): string | null {
139
+ const result = runHelper({ mode: "probe", pid }, "win32", run);
140
+ return typeof result.token === "string" && /^w\d+$/.test(result.token) ? result.token : null;
141
+ }
142
+
143
+ /** No numeric-PID signaling fallback is permitted when an exact handle fails. */
144
+ export function signalProcessWithHandle(
145
+ pid: number,
146
+ recordedToken: string,
147
+ signal: NodeJS.Signals | number,
148
+ options: { platform?: NodeJS.Platform; run?: NativeProcessHelperRunner } = {},
149
+ ): ProcessHandleSignalResult {
150
+ const platform = options.platform ?? process.platform;
151
+ const number = typeof signal === "number" ? signal : constants.signals[signal];
152
+ if (
153
+ !Number.isInteger(number) ||
154
+ number <= 0 ||
155
+ (platform === "linux" && !/^l[0-9a-f]{8}\.\d+$/.test(recordedToken)) ||
156
+ (platform === "win32" && (!/^w\d+$/.test(recordedToken) || ![9, 15].includes(number)))
157
+ )
158
+ return { sent: false, verdict: "unknown" };
159
+ const result = runHelper(
160
+ { mode: "signal", pid, token: recordedToken, signal: number },
161
+ platform,
162
+ options.run ?? spawnSync,
163
+ );
164
+ return { sent: result.sent, verdict: result.verdict };
165
+ }