@pushary/agent-hooks 0.83.2 → 0.84.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/bin/pushary-bell-hook.js +5 -5
  3. package/dist/bin/pushary-bell.js +15 -14
  4. package/dist/bin/pushary-claude.js +72 -131
  5. package/dist/bin/pushary-clean.js +13 -12
  6. package/dist/bin/pushary-codex-bridge.d.ts +1 -0
  7. package/dist/bin/pushary-codex-bridge.js +348 -0
  8. package/dist/bin/pushary-codex-hook.js +15 -14
  9. package/dist/bin/pushary-codex.js +7 -7
  10. package/dist/bin/pushary-connect.js +13 -12
  11. package/dist/bin/pushary-daemon.js +313 -41
  12. package/dist/bin/pushary-doctor.js +48 -25
  13. package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
  14. package/dist/bin/pushary-gemini-bridge.js +298 -0
  15. package/dist/bin/pushary-gemini-hook.js +24 -20
  16. package/dist/bin/pushary-hook.js +14 -13
  17. package/dist/bin/pushary-login.js +15 -14
  18. package/dist/bin/pushary-logout.js +12 -11
  19. package/dist/bin/pushary-mode.js +10 -9
  20. package/dist/bin/pushary-notification-hook.js +5 -5
  21. package/dist/bin/pushary-permission-denied-hook.js +9 -9
  22. package/dist/bin/pushary-permission-hook.js +9 -9
  23. package/dist/bin/pushary-post-hook.js +5 -5
  24. package/dist/bin/pushary-prompt-hook.js +5 -5
  25. package/dist/bin/pushary-session-end-hook.js +5 -5
  26. package/dist/bin/pushary-session-start-hook.js +5 -5
  27. package/dist/bin/pushary-setup.js +42 -36
  28. package/dist/bin/pushary-stats.js +6 -5
  29. package/dist/bin/pushary-status.js +17 -16
  30. package/dist/bin/pushary-stop-hook.js +5 -5
  31. package/dist/bin/pushary-stopfailure-hook.js +5 -5
  32. package/dist/bin/pushary-suggestions.js +7 -6
  33. package/dist/bin/pushary-upgrade.js +18 -17
  34. package/dist/bin/pushary-wait.js +10 -9
  35. package/dist/bin/pushary.js +8 -6
  36. package/dist/{chunk-4QQOKEOS.js → chunk-37AD5K7W.js} +6 -6
  37. package/dist/{chunk-SAA3VBDK.js → chunk-437T77TY.js} +3 -3
  38. package/dist/{chunk-U27BZWFT.js → chunk-6LC4WSCL.js} +1 -1
  39. package/dist/{chunk-7WWZPII5.js → chunk-7PZPJSWI.js} +1 -1
  40. package/dist/chunk-CCH775Y6.js +343 -0
  41. package/dist/chunk-DINDL2CF.js +45 -0
  42. package/dist/{chunk-TXF4QZSQ.js → chunk-DOXSWT4Q.js} +1 -1
  43. package/dist/chunk-EVJ6ULHN.js +153 -0
  44. package/dist/{chunk-QR6MVVQ7.js → chunk-F52SIIDG.js} +4 -4
  45. package/dist/{chunk-26D2YMDU.js → chunk-FBGBJXVC.js} +1 -1
  46. package/dist/{chunk-QF7WTF2C.js → chunk-HZNOAJGB.js} +2 -2
  47. package/dist/{chunk-RRZZ7TFJ.js → chunk-J4KFVOCQ.js} +4 -2
  48. package/dist/{chunk-WDRRULBH.js → chunk-K7CSRGKZ.js} +4 -4
  49. package/dist/{chunk-ZUKH2NPJ.js → chunk-MGJBC6DD.js} +1 -44
  50. package/dist/{chunk-GQLB2GBJ.js → chunk-MHBC2XUP.js} +1 -1
  51. package/dist/{chunk-YWRJGYUH.js → chunk-MQMPG5X4.js} +28 -2
  52. package/dist/{chunk-3AGM3TDM.js → chunk-NJ7JT26G.js} +1 -1
  53. package/dist/chunk-NRGRUOAR.js +25 -0
  54. package/dist/chunk-NWYGW7HY.js +117 -0
  55. package/dist/{chunk-7PO2VEA4.js → chunk-OPMSMF5R.js} +38 -33
  56. package/dist/{chunk-DHAZ6SOT.js → chunk-QNBA5OXQ.js} +81 -25
  57. package/dist/{chunk-EHLTWCDZ.js → chunk-SEAHGHCE.js} +1 -1
  58. package/dist/{chunk-NDC3TBHI.js → chunk-UKSQWTBH.js} +1 -1
  59. package/dist/{chunk-UVAC2KHH.js → chunk-UXWEE3QI.js} +1 -1
  60. package/dist/{chunk-MUEW424A.js → chunk-VT4IVERX.js} +50 -3
  61. package/dist/chunk-YJCMI5GF.js +133 -0
  62. package/dist/src/index.d.ts +6 -3
  63. package/dist/src/index.js +9 -9
  64. package/package.json +3 -1
@@ -2,43 +2,66 @@
2
2
  import {
3
3
  HEARTBEAT_INTERVAL_MS,
4
4
  sendHeartbeat
5
- } from "../chunk-GQLB2GBJ.js";
5
+ } from "../chunk-MHBC2XUP.js";
6
+ import {
7
+ DAEMON_LEASE_RENEW_INTERVAL_MS,
8
+ acquireDaemonLease,
9
+ readDaemonStatus,
10
+ requestDaemonHandoff
11
+ } from "../chunk-CCH775Y6.js";
12
+ import {
13
+ CODEX_BRIDGE_CAPABILITY,
14
+ CODEX_RESUME_CAPABILITY,
15
+ geminiLiveCapabilities
16
+ } from "../chunk-NRGRUOAR.js";
17
+ import {
18
+ ownsLiveProcess
19
+ } from "../chunk-EVJ6ULHN.js";
6
20
  import {
7
21
  spawnClaude
8
22
  } from "../chunk-XHKBHWLX.js";
9
23
  import {
10
- resolveGlobalBinary
11
- } from "../chunk-MUEW424A.js";
24
+ resolveBinary,
25
+ resolveGlobalPackageVersion,
26
+ resolvePackageVersionFromBinary
27
+ } from "../chunk-VT4IVERX.js";
28
+ import "../chunk-SEAHGHCE.js";
12
29
  import {
13
30
  getMachineId
14
31
  } from "../chunk-RN3NOEJF.js";
15
32
  import {
16
33
  exitOnHelpFlag
17
- } from "../chunk-RRZZ7TFJ.js";
18
- import "../chunk-ZUKH2NPJ.js";
34
+ } from "../chunk-J4KFVOCQ.js";
35
+ import "../chunk-MGJBC6DD.js";
36
+ import "../chunk-DINDL2CF.js";
37
+ import "../chunk-6MTNS63X.js";
38
+ import {
39
+ EXIT
40
+ } from "../chunk-KZERVKTD.js";
19
41
  import {
20
42
  getApiKey,
21
43
  getBaseUrl
22
44
  } from "../chunk-2UMNXADU.js";
23
- import {
24
- EXIT
25
- } from "../chunk-KZERVKTD.js";
45
+ import "../chunk-MQMPG5X4.js";
26
46
 
27
47
  // src/spawn-daemon.ts
28
- import { spawn } from "child_process";
48
+ import { execFileSync, spawn } from "child_process";
29
49
  import { existsSync } from "fs";
30
50
  import { basename, dirname, join } from "path";
31
51
 
32
52
  // src/stop-requests.ts
33
53
  var STOP_CAPABILITY = "stop";
54
+ var STOP_ACK_CAPABILITY = "stop-ack";
55
+ var windowsTaskkillArgs = (pid) => ["/PID", String(pid), "/T", "/F"];
34
56
  var isStopRequest = (value) => {
35
57
  if (!value || typeof value !== "object") return false;
36
58
  const candidate = value;
37
- return typeof candidate.sessionId === "string" && candidate.sessionId.length > 0 && typeof candidate.pid === "number" && Number.isInteger(candidate.pid) && candidate.pid > 1;
59
+ return (candidate.id === void 0 || typeof candidate.id === "string" && candidate.id.length > 0 && candidate.id.length <= 64) && (candidate.spawnId === void 0 || typeof candidate.spawnId === "string" && candidate.spawnId.length > 0 && candidate.spawnId.length <= 64) && typeof candidate.sessionId === "string" && candidate.sessionId.length > 0 && typeof candidate.pid === "number" && Number.isInteger(candidate.pid) && candidate.pid > 1;
38
60
  };
39
61
  var parseStopRequests = (value) => Array.isArray(value) ? value.filter(isStopRequest) : [];
40
62
  var applyStop = (request, deps) => {
41
63
  if (request.pid === deps.selfPid) return "skipped_self";
64
+ if (!deps.ownsProcess(request)) return "refused";
42
65
  try {
43
66
  deps.kill(request.pid, "SIGTERM");
44
67
  return "stopped";
@@ -47,17 +70,71 @@ var applyStop = (request, deps) => {
47
70
  return code === "ESRCH" ? "already_gone" : "refused";
48
71
  }
49
72
  };
73
+ var applyStopAndVerify = async (request, deps, attempts = 100, intervalMs = 100) => {
74
+ if (!deps.isAlive(request.pid)) return "already_gone";
75
+ if (!await deps.ownsProcess(request)) return "refused";
76
+ const outcome = applyStop(request, { ...deps, ownsProcess: () => true });
77
+ if (outcome !== "stopped") return outcome;
78
+ for (let attempt = 0; attempt < attempts; attempt++) {
79
+ await deps.sleep(intervalMs);
80
+ if (!deps.isAlive(request.pid)) return "stopped";
81
+ }
82
+ return "signal_sent";
83
+ };
50
84
 
51
85
  // src/spawn-daemon.ts
52
86
  var DRAIN_PATH = "/api/agent/spawn/drain";
87
+ var ACK_PATH = "/api/agent/spawn/ack";
88
+ var SPAWN_PROTOCOL_VERSION = 2;
53
89
  var FAST_POLL_MS = 3e3;
54
90
  var SLOW_POLL_MS = 3e4;
55
91
  var FAST_DECAY_TICKS = 3;
56
92
  var MAX_ERROR_BACKOFF_MS = 12e4;
57
93
  var MAX_ERROR_STREAK = 6;
58
94
  var REQUEST_TIMEOUT_MS = 1e4;
95
+ var STOP_PROTOCOL_VERSION = 2;
59
96
  var MAX_SPAWNS_PER_WINDOW = 5;
60
97
  var SPAWN_WINDOW_MS = 6e4;
98
+ var VERSION_CHECK_INTERVAL_MS = 2e3;
99
+ var MAX_TRACKED_PROCESSES = 100;
100
+ var MAX_COMPLETED_STOPS = 20;
101
+ var daemonSignals = (platform = process.platform) => platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
102
+ var parseSpawnClaim = (value) => {
103
+ if (typeof value !== "object" || value === null) return null;
104
+ const request = value;
105
+ if (typeof request.id !== "string" || typeof request.prompt !== "string") return null;
106
+ return {
107
+ id: request.id,
108
+ prompt: request.prompt,
109
+ ...typeof request.claimId === "string" ? { claimId: request.claimId } : {},
110
+ ...Number.isInteger(request.attemptCount) ? { attemptCount: request.attemptCount } : {},
111
+ ...typeof request.cwd === "string" ? { cwd: request.cwd } : {},
112
+ ...typeof request.agent === "string" ? { agent: request.agent } : {},
113
+ ...typeof request.model === "string" ? { model: request.model } : {},
114
+ ...typeof request.resumeSessionId === "string" ? { resumeSessionId: request.resumeSessionId } : {}
115
+ };
116
+ };
117
+ var stopAcknowledgement = (request, machineId, state) => request.id ? { stopId: request.id, machineId, state } : null;
118
+ var spawnAcknowledgement = (request, machineId, outcome) => {
119
+ if (!request.claimId) return null;
120
+ return outcome.state === "started" ? {
121
+ spawnId: request.id,
122
+ claimId: request.claimId,
123
+ machineId,
124
+ state: "started",
125
+ pid: outcome.pid ?? null
126
+ } : {
127
+ spawnId: request.id,
128
+ claimId: request.claimId,
129
+ machineId,
130
+ state: "failed",
131
+ failure: {
132
+ code: "launch_failed",
133
+ message: outcome.message.slice(0, 240),
134
+ retryable: false
135
+ }
136
+ };
137
+ };
61
138
  var DrainError = class extends Error {
62
139
  constructor(status) {
63
140
  super(`drain ${status}`);
@@ -71,7 +148,6 @@ var isAuthFailure = (err) => {
71
148
  if (candidate.name !== "DrainError") return false;
72
149
  return candidate.status === 401 || candidate.status === 403;
73
150
  };
74
- var NATIVE_AGENT_BINARIES = { codex: "codex", gemini: "gemini" };
75
151
  var isFlagShapedPrompt = (prompt) => prompt.trimStart().startsWith("-");
76
152
  var buildSpawnLaunch = (req, opts) => {
77
153
  if (isFlagShapedPrompt(req.prompt)) return null;
@@ -81,9 +157,20 @@ var buildSpawnLaunch = (req, opts) => {
81
157
  const claudeArgs = ["claude", "--remote", ...model, "-p", req.prompt];
82
158
  return opts.spawnDirect ? { direct: true, command: opts.execPath, args: [opts.siblingEntry, ...claudeArgs], agent } : { direct: false, command: opts.pusharyBin, args: claudeArgs, agent };
83
159
  }
84
- const command = opts.resolveNative(NATIVE_AGENT_BINARIES[agent]);
85
- const args = agent === "codex" ? ["exec", ...model, req.prompt] : [...model, "-p", req.prompt];
86
- return { direct: false, command, args, agent };
160
+ if (agent === "codex") {
161
+ return {
162
+ direct: true,
163
+ command: opts.execPath,
164
+ args: [opts.codexBridgeEntry],
165
+ agent
166
+ };
167
+ }
168
+ return {
169
+ direct: true,
170
+ command: opts.execPath,
171
+ args: [opts.geminiBridgeEntry],
172
+ agent
173
+ };
87
174
  };
88
175
  var spawnRateAllows = (timestamps, now, maxPerWindow = MAX_SPAWNS_PER_WINDOW, windowMs = SPAWN_WINDOW_MS) => {
89
176
  while (timestamps.length > 0 && now - timestamps[0] > windowMs) timestamps.shift();
@@ -102,69 +189,226 @@ var runSpawnDaemon = async () => {
102
189
  }
103
190
  const machineId = getMachineId();
104
191
  const baseUrl = getBaseUrl();
105
- const pusharyBin = resolveGlobalBinary("pushary") ?? "pushary";
192
+ const pusharyBin = resolveBinary("pushary") ?? "pushary";
193
+ const codexBin = resolveBinary("codex");
194
+ const geminiBin = resolveBinary("gemini");
195
+ const geminiCapabilities = geminiBin ? geminiLiveCapabilities(
196
+ resolvePackageVersionFromBinary(geminiBin, "@google/gemini-cli") ?? resolveGlobalPackageVersion("@google/gemini-cli")
197
+ ) : [];
106
198
  const siblingEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary.js") : "";
199
+ const codexBridgeEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary-codex-bridge.js") : "pushary-codex-bridge";
200
+ const geminiBridgeEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary-gemini-bridge.js") : "pushary-gemini-bridge";
107
201
  const spawnDirect = siblingEntry.length > 0 && existsSync(siblingEntry);
202
+ const existing = readDaemonStatus();
203
+ let handoff;
204
+ try {
205
+ handoff = await requestDaemonHandoff(existing);
206
+ } catch (err) {
207
+ stderr(`[pushary] daemon could not request a handoff: ${err instanceof Error ? err.message : String(err)}
208
+ `);
209
+ return 1;
210
+ }
211
+ if (existing.pid !== null && handoff.pid === existing.pid && handoff.processAlive === true) {
212
+ stderr("[pushary] daemon could not replace the existing process; run `pushary doctor` for details\n");
213
+ return 1;
214
+ }
215
+ const lifecycle = acquireDaemonLease({ machineId });
216
+ if (lifecycle.kind === "already-running") {
217
+ stderr(`[pushary] daemon already online as pid ${lifecycle.status.pid ?? "unknown"}
218
+ `);
219
+ return 0;
220
+ }
221
+ if (lifecycle.kind === "unavailable") {
222
+ stderr(`[pushary] daemon could not acquire its local lease: ${lifecycle.detail}
223
+ `);
224
+ return 1;
225
+ }
226
+ const daemonLease = lifecycle.lease;
108
227
  let stopped = false;
109
228
  let idleTicks = 0;
110
229
  let errorStreak = 0;
111
230
  const spawnTimestamps = [];
231
+ const launchedProcesses = /* @__PURE__ */ new Map();
232
+ const completedStops = /* @__PURE__ */ new Map();
112
233
  let timer;
113
234
  let inflight;
114
235
  let fatalCode = null;
115
236
  let finish = () => {
116
237
  };
117
238
  let heartbeat;
239
+ let versionWatch;
240
+ let leaseRenewal;
118
241
  const launch = (req) => {
242
+ if (launchedProcesses.size >= MAX_TRACKED_PROCESSES) {
243
+ return Promise.reject(new Error("This daemon is already tracking 100 active sessions"));
244
+ }
119
245
  spawnTimestamps.push(Date.now());
120
246
  const cwd = req.cwd && existsSync(req.cwd) ? req.cwd : process.cwd();
121
- const env = { ...process.env, PUSHARY_SPAWN_ID: req.id };
247
+ const env = {
248
+ ...process.env,
249
+ PUSHARY_SPAWN_ID: req.id,
250
+ PUSHARY_SPAWN_CLAIM_ID: req.claimId,
251
+ ...req.agent === "codex" || req.agent === "gemini" ? {
252
+ PUSHARY_BRIDGE_CWD: cwd,
253
+ PUSHARY_BRIDGE_PROMPT: req.prompt || void 0,
254
+ PUSHARY_BRIDGE_MODEL: req.model || void 0,
255
+ PUSHARY_BRIDGE_RESUME_SESSION_ID: req.resumeSessionId || void 0
256
+ } : {},
257
+ ...req.agent === "codex" ? {
258
+ PUSHARY_CODEX_COMMAND: codexBin ?? "codex"
259
+ } : {},
260
+ ...req.agent === "gemini" ? {
261
+ PUSHARY_GEMINI_COMMAND: geminiBin ?? "gemini"
262
+ } : {}
263
+ };
122
264
  const plan = buildSpawnLaunch(req, {
123
265
  spawnDirect,
124
266
  execPath: process.execPath,
125
267
  siblingEntry,
126
268
  pusharyBin,
127
- resolveNative: (name) => resolveGlobalBinary(name) ?? name
269
+ codexBridgeEntry,
270
+ geminiBridgeEntry
128
271
  });
129
272
  if (!plan) {
130
- stderr("[pushary] refused a phone-requested session: the prompt looks like a command-line flag\n");
131
- return;
273
+ return Promise.reject(new Error("The prompt looks like a command-line flag"));
132
274
  }
133
- try {
134
- const child = plan.direct ? spawn(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env }) : spawnClaude(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env });
135
- child.on("error", (err) => stderr(`[pushary] could not launch session: ${err.message}
136
- `));
137
- child.unref();
138
- stderr(`[pushary] launched a phone-requested ${plan.agent} session in ${basename(cwd)}
275
+ return new Promise((resolve, reject) => {
276
+ try {
277
+ const child = plan.direct ? spawn(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env }) : spawnClaude(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env });
278
+ let started = false;
279
+ child.once("error", (err) => {
280
+ if (!started) reject(err);
281
+ else stderr(`[pushary] launched session reported an error: ${err.message}
139
282
  `);
140
- } catch (err) {
141
- stderr(`[pushary] could not launch session: ${err instanceof Error ? err.message : String(err)}
283
+ });
284
+ child.once("spawn", () => {
285
+ started = true;
286
+ if (typeof child.pid === "number") {
287
+ launchedProcesses.set(child.pid, req.id);
288
+ child.once("close", () => launchedProcesses.delete(child.pid));
289
+ }
290
+ child.unref();
291
+ stderr(`[pushary] launched a phone-requested ${plan.agent} session in ${basename(cwd)}
142
292
  `);
143
- }
293
+ resolve(typeof child.pid === "number" ? child.pid : null);
294
+ });
295
+ } catch (err) {
296
+ reject(err);
297
+ }
298
+ });
144
299
  };
145
- const runStop = (stop) => {
146
- const outcome = applyStop(stop, { kill: process.kill.bind(process), selfPid: process.pid });
300
+ const runStop = async (stop) => {
301
+ const kill = process.platform === "win32" ? (pid) => {
302
+ execFileSync("taskkill", windowsTaskkillArgs(pid), { stdio: "ignore" });
303
+ } : process.kill.bind(process);
304
+ const outcome = stop.id ? completedStops.get(stop.id) ?? await apply() : await apply();
305
+ if (stop.id && !completedStops.has(stop.id)) {
306
+ if (completedStops.size === MAX_COMPLETED_STOPS) {
307
+ const oldest = completedStops.keys().next().value;
308
+ if (oldest) completedStops.delete(oldest);
309
+ }
310
+ completedStops.set(stop.id, outcome);
311
+ }
147
312
  const said = {
148
313
  stopped: `stopped session ${stop.sessionId}`,
149
314
  already_gone: `session ${stop.sessionId} had already exited`,
315
+ signal_sent: `sent the stop signal to session ${stop.sessionId}; shutdown is still finishing`,
150
316
  refused: `could not stop session ${stop.sessionId}, the system refused the signal`,
151
317
  skipped_self: `refused to stop session ${stop.sessionId}: that pid is this daemon`
152
318
  };
153
319
  stderr(`[pushary] ${said[outcome]}
154
320
  `);
321
+ const acknowledgement = stopAcknowledgement(stop, machineId, outcome);
322
+ if (acknowledgement) {
323
+ for (let attempt = 0; attempt < 3 && !stopped; attempt++) {
324
+ const response = await fetch(`${baseUrl}/api/agent/stop/ack`, {
325
+ method: "POST",
326
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
327
+ body: JSON.stringify(acknowledgement),
328
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
329
+ }).catch(() => null);
330
+ if (response?.ok) {
331
+ completedStops.delete(acknowledgement.stopId);
332
+ break;
333
+ }
334
+ if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 200 * (attempt + 1)));
335
+ }
336
+ }
337
+ async function apply() {
338
+ return applyStopAndVerify(stop, {
339
+ kill,
340
+ ownsProcess: (request) => {
341
+ const launched = request.spawnId ? launchedProcesses.get(request.pid) === request.spawnId : !request.id && launchedProcesses.has(request.pid);
342
+ return launched || ownsLiveProcess(request);
343
+ },
344
+ selfPid: process.pid,
345
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
346
+ isAlive: (pid) => {
347
+ try {
348
+ process.kill(pid, 0);
349
+ return true;
350
+ } catch (err) {
351
+ return err.code === "EPERM";
352
+ }
353
+ }
354
+ });
355
+ }
155
356
  };
156
357
  const drain = async (signal) => {
157
358
  const res = await fetch(`${baseUrl}${DRAIN_PATH}`, {
158
359
  method: "POST",
159
360
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
160
- body: JSON.stringify({ machineId }),
361
+ body: JSON.stringify({ machineId, spawnProtocol: SPAWN_PROTOCOL_VERSION, stopProtocol: STOP_PROTOCOL_VERSION }),
161
362
  signal
162
363
  });
163
364
  if (!res.ok) throw new DrainError(res.status);
164
365
  const data = await res.json();
165
- for (const stop of parseStopRequests(data.stops)) runStop(stop);
166
- const spawn2 = data.spawn;
167
- return spawn2 && typeof spawn2.prompt === "string" && typeof spawn2.id === "string" ? spawn2 : null;
366
+ const response = typeof data === "object" && data !== null ? data : {};
367
+ await Promise.all(parseStopRequests(response.stops).map(runStop));
368
+ return parseSpawnClaim(response.spawn);
369
+ };
370
+ const acknowledge = async (body) => {
371
+ if (!body) return;
372
+ let lastError;
373
+ for (let attempt = 0; attempt < 3 && !stopped; attempt++) {
374
+ const controller = new AbortController();
375
+ inflight = controller;
376
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
377
+ try {
378
+ const response = await fetch(`${baseUrl}${ACK_PATH}`, {
379
+ method: "POST",
380
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
381
+ body: JSON.stringify(body),
382
+ signal: controller.signal
383
+ });
384
+ if (response.ok) return;
385
+ const error = new Error(`spawn acknowledgement failed (${response.status})`);
386
+ if (response.status < 500) {
387
+ lastError = error;
388
+ break;
389
+ }
390
+ lastError = error;
391
+ } catch (err) {
392
+ lastError = err;
393
+ } finally {
394
+ clearTimeout(timeout);
395
+ if (inflight === controller) inflight = void 0;
396
+ }
397
+ }
398
+ throw lastError ?? new Error("spawn acknowledgement stopped");
399
+ };
400
+ const launchClaim = async (request) => {
401
+ let pid;
402
+ try {
403
+ pid = await launch(request);
404
+ } catch (err) {
405
+ const message = err instanceof Error ? err.message : String(err);
406
+ stderr(`[pushary] could not launch session: ${message}
407
+ `);
408
+ await acknowledge(spawnAcknowledgement(request, machineId, { state: "failed", message }));
409
+ return;
410
+ }
411
+ await acknowledge(spawnAcknowledgement(request, machineId, { state: "started", pid }));
168
412
  };
169
413
  const nextDelay = () => {
170
414
  if (errorStreak > 0) return Math.min(SLOW_POLL_MS * 2 ** (errorStreak - 1), MAX_ERROR_BACKOFF_MS);
@@ -173,6 +417,10 @@ var runSpawnDaemon = async () => {
173
417
  const jitter = (ms) => Math.max(1, Math.round(ms * (0.85 + Math.random() * 0.3)));
174
418
  const tick = async () => {
175
419
  if (stopped) return;
420
+ if (!daemonLease.isOwner()) {
421
+ finish();
422
+ return;
423
+ }
176
424
  if (!spawnRateAllows(spawnTimestamps, Date.now())) {
177
425
  timer = setTimeout(tick, jitter(nextDelay()));
178
426
  return;
@@ -182,10 +430,14 @@ var runSpawnDaemon = async () => {
182
430
  const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
183
431
  try {
184
432
  const req = await drain(controller.signal);
433
+ if (!daemonLease.isOwner()) {
434
+ finish();
435
+ return;
436
+ }
185
437
  errorStreak = 0;
186
438
  if (req) {
187
439
  idleTicks = 0;
188
- launch(req);
440
+ await launchClaim(req);
189
441
  } else {
190
442
  idleTicks++;
191
443
  }
@@ -211,14 +463,33 @@ var runSpawnDaemon = async () => {
211
463
  stopped = true;
212
464
  if (timer) clearTimeout(timer);
213
465
  if (heartbeat) clearInterval(heartbeat);
466
+ if (versionWatch) clearInterval(versionWatch);
467
+ if (leaseRenewal) clearInterval(leaseRenewal);
214
468
  inflight?.abort();
469
+ try {
470
+ daemonLease.release();
471
+ } catch (err) {
472
+ stderr(`[pushary] daemon could not release its local lease: ${err instanceof Error ? err.message : String(err)}
473
+ `);
474
+ }
215
475
  };
216
476
  const beat = async () => {
217
477
  if (stopped) return;
478
+ if (!daemonLease.isOwner()) {
479
+ finish();
480
+ return;
481
+ }
218
482
  const result = await sendHeartbeat(apiKey, {
219
483
  repoKey: basename(process.cwd()),
220
- capabilities: ["spawn", STOP_CAPABILITY]
484
+ capabilities: [
485
+ "spawn",
486
+ STOP_CAPABILITY,
487
+ STOP_ACK_CAPABILITY,
488
+ ...codexBin ? [CODEX_BRIDGE_CAPABILITY, CODEX_RESUME_CAPABILITY] : [],
489
+ ...geminiCapabilities
490
+ ]
221
491
  });
492
+ if (result.kind === "ok") daemonLease.recordSuccessfulHeartbeat();
222
493
  if (result.kind === "unauthorized" && !stopped) {
223
494
  fatalCode = EXIT.UNAUTHENTICATED;
224
495
  stderr("[pushary] daemon stopping: this machine's key was rejected. Run `pushary login` to sign in again.\n");
@@ -227,17 +498,18 @@ var runSpawnDaemon = async () => {
227
498
  };
228
499
  void beat();
229
500
  heartbeat = setInterval(() => void beat(), HEARTBEAT_INTERVAL_MS);
501
+ leaseRenewal = setInterval(() => daemonLease.renew(), DAEMON_LEASE_RENEW_INTERVAL_MS);
502
+ versionWatch = setInterval(() => {
503
+ if (daemonLease.shouldYield()) finish();
504
+ }, VERSION_CHECK_INTERVAL_MS);
230
505
  stderr(
231
506
  `[pushary] daemon online \u2014 this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.
232
507
  `
233
508
  );
234
509
  timer = setTimeout(tick, FAST_POLL_MS);
235
- const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
510
+ const signals = daemonSignals();
236
511
  await new Promise((resolve) => {
237
- const onSignal = () => {
238
- teardown();
239
- resolve();
240
- };
512
+ const onSignal = () => finish();
241
513
  for (const signal of signals) process.on(signal, onSignal);
242
514
  finish = () => {
243
515
  teardown();
@@ -6,7 +6,10 @@ import {
6
6
  claudeWired,
7
7
  codexWired,
8
8
  geminiWired
9
- } from "../chunk-QF7WTF2C.js";
9
+ } from "../chunk-HZNOAJGB.js";
10
+ import {
11
+ readDaemonStatus
12
+ } from "../chunk-CCH775Y6.js";
10
13
  import {
11
14
  agentProbes,
12
15
  detectAgent,
@@ -15,12 +18,16 @@ import {
15
18
  isDetected,
16
19
  isPluginRegistered,
17
20
  vscodeSettingsTargets
18
- } from "../chunk-UVAC2KHH.js";
21
+ } from "../chunk-UXWEE3QI.js";
19
22
  import {
20
23
  GEMINI_HOOK_BINARY,
21
24
  hasGeminiHooks,
22
25
  missingGeminiHookEvents
23
- } from "../chunk-TXF4QZSQ.js";
26
+ } from "../chunk-DOXSWT4Q.js";
27
+ import "../chunk-XHKBHWLX.js";
28
+ import {
29
+ execNpm
30
+ } from "../chunk-VT4IVERX.js";
24
31
  import {
25
32
  claudeJson,
26
33
  claudeSettings,
@@ -39,62 +46,60 @@ import {
39
46
  readCodexMcpAuth,
40
47
  untrustedCodexHookEvents,
41
48
  vscodePluginDir
42
- } from "../chunk-EHLTWCDZ.js";
49
+ } from "../chunk-SEAHGHCE.js";
43
50
  import {
44
51
  readLedgerSummary
45
- } from "../chunk-3AGM3TDM.js";
52
+ } from "../chunk-NJ7JT26G.js";
53
+ import {
54
+ getMachineId
55
+ } from "../chunk-RN3NOEJF.js";
46
56
  import {
47
57
  describeReach,
48
58
  describeRoundTrip,
49
59
  fetchChannels,
50
60
  reachVerdict,
51
61
  verifyRoundTrip
52
- } from "../chunk-U27BZWFT.js";
62
+ } from "../chunk-6LC4WSCL.js";
53
63
  import "../chunk-3EGEA4KH.js";
54
64
  import {
55
65
  checkApiKey,
56
66
  describeKeyCheck
57
- } from "../chunk-QR6MVVQ7.js";
67
+ } from "../chunk-F52SIIDG.js";
58
68
  import {
59
69
  createIo
60
70
  } from "../chunk-5RUIFDTP.js";
61
71
  import {
62
72
  keyPrefix,
63
73
  readKeySource
64
- } from "../chunk-WDRRULBH.js";
65
- import "../chunk-6MTNS63X.js";
74
+ } from "../chunk-K7CSRGKZ.js";
66
75
  import {
67
76
  parseFlags
68
77
  } from "../chunk-GMXKITVA.js";
78
+ import {
79
+ exitOnHelpFlag
80
+ } from "../chunk-J4KFVOCQ.js";
81
+ import "../chunk-MGJBC6DD.js";
69
82
  import "../chunk-BC3VCZ3E.js";
83
+ import {
84
+ getPackageVersion
85
+ } from "../chunk-DINDL2CF.js";
86
+ import "../chunk-6MTNS63X.js";
70
87
  import {
71
88
  callMcpTool,
72
89
  sendMcpRequest
73
90
  } from "../chunk-BSZYIAZL.js";
74
91
  import "../chunk-SAF6HGAA.js";
75
- import "../chunk-SAA3VBDK.js";
92
+ import "../chunk-437T77TY.js";
76
93
  import {
77
94
  UsageError
78
95
  } from "../chunk-7QLSKOSU.js";
79
- import "../chunk-YWRJGYUH.js";
80
- import {
81
- execNpm
82
- } from "../chunk-MUEW424A.js";
83
96
  import {
84
- getMachineId
85
- } from "../chunk-RN3NOEJF.js";
86
- import {
87
- exitOnHelpFlag
88
- } from "../chunk-RRZZ7TFJ.js";
89
- import {
90
- getPackageVersion
91
- } from "../chunk-ZUKH2NPJ.js";
97
+ EXIT
98
+ } from "../chunk-KZERVKTD.js";
92
99
  import {
93
100
  getBaseUrl
94
101
  } from "../chunk-2UMNXADU.js";
95
- import {
96
- EXIT
97
- } from "../chunk-KZERVKTD.js";
102
+ import "../chunk-MQMPG5X4.js";
98
103
 
99
104
  // bin/pushary-doctor.ts
100
105
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -321,6 +326,23 @@ var main = async () => {
321
326
  break;
322
327
  }
323
328
  }
329
+ const daemon = readDaemonStatus();
330
+ const daemonOk = daemon.kind === "running";
331
+ const daemonFailure = daemonOk ? null : `local state is ${daemon.kind}`;
332
+ const daemonDetail = daemonFailure ? `${daemonFailure}; run \`pushary daemon\` in a terminal to inspect it` : null;
333
+ check(
334
+ daemonOk,
335
+ "Remote-control daemon",
336
+ [
337
+ daemonDetail,
338
+ daemon.kind,
339
+ `pid ${daemon.pid ?? "none"}`,
340
+ `daemon ${daemon.daemonVersion ?? "none"}`,
341
+ `installed ${daemon.installedVersion}`,
342
+ `machine ${daemon.machineId ?? "none"}`,
343
+ `heartbeat ${daemon.lastSuccessfulHeartbeatAt ?? "none yet"}`
344
+ ].filter((value) => value !== null).join(" \xB7 ")
345
+ );
324
346
  const CLAUDE_JSON = claudeJson();
325
347
  const claudeInPlay = claudeWired({
326
348
  claudeJson: readJson(CLAUDE_JSON),
@@ -799,6 +821,7 @@ var main = async () => {
799
821
  ok: exitCode === EXIT.OK,
800
822
  exitCode,
801
823
  keyCheck: keyVerdict?.kind ?? null,
824
+ daemon,
802
825
  passed: results.filter((r) => r.passed).length,
803
826
  failed: failed.length,
804
827
  checks: results.map((r) => ({ label: r.label, passed: r.passed, detail: r.detail ?? null }))
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node