@addai/node 0.6.0 → 0.7.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.
@@ -1,6 +1,9 @@
1
1
  export type RestartHook = (opts: {
2
2
  commandId: string;
3
3
  version: string;
4
+ /** Bounce this daemon on the version it is already running: no npm
5
+ * install, no version change. See runUpdateRuntime. */
6
+ restartOnly?: boolean;
4
7
  }) => Promise<void>;
5
8
  export declare function setRestartHook(fn: RestartHook): void;
6
9
  /** Called from index.ts when a heartbeat reports pending_commands > 0.
@@ -479,26 +479,45 @@ async function runLogout(cmd, spec) {
479
479
  /* ── update_runtime ──────────────────────────────────────────────────────
480
480
  * Roll THIS node to a version (default: npm latest) and come back up.
481
481
  *
482
+ * Two shapes share the command:
483
+ *
484
+ * {} roll to npm latest
485
+ * {"version": "0.6.0"} roll to a pinned version
486
+ * {"restart_only": true} bounce the daemon on the version it is already
487
+ * running — no npm install, no version change
488
+ *
489
+ * restart_only exists because the reasons to bounce a node are mostly not
490
+ * version reasons: a credential edit the running daemon has cached, a wedged
491
+ * harness, a machine that has been up for a fortnight. Doing that through a
492
+ * version roll meant an unwanted upgrade every time, and could not be done at
493
+ * all on a node running from a source checkout — where a restart is exactly
494
+ * the safe half of the operation.
495
+ *
496
+ * An older daemon that has never heard of restart_only still does the right
497
+ * thing, because the caller also pins `version` to the version it can see the
498
+ * node running: the old code installs the version already installed and
499
+ * restarts. Same destination, one wasted npm call.
500
+ *
482
501
  * The command is deliberately left `running` here: this process is about to
483
502
  * stop existing, so it cannot honestly report the outcome. index.ts's boot
484
503
  * path completes it from the handoff file once the replacement daemon is
485
504
  * actually up, with whatever version it actually came up as. */
486
505
  async function runUpdateRuntime(cmd) {
487
- const target = (cmd.input?.version ?? '').trim() || 'latest';
488
506
  if (!restartHook) {
489
507
  await update(cmd.id, 'failed', {}, 'this daemon is too old to restart itself');
490
508
  return;
491
509
  }
492
510
  const mode = (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
493
- if (mode === 'source' && target !== 'latest') {
494
- // A git checkout's version comes from the working tree, not npm — we'd
495
- // restart and report the same old version, looking like a silent no-op.
496
- await update(cmd.id, 'failed', { mode }, 'this node runs from a source checkout — pull and rebuild it there; a remote version bump cannot apply');
511
+ const { target, restartOnly, refusal } = (0, self_update_1.planRoll)(cmd.input, mode, VERSION);
512
+ if (refusal) {
513
+ await update(cmd.id, 'failed', { mode }, refusal);
497
514
  return;
498
515
  }
499
- await update(cmd.id, 'running', { step: 'draining', mode, target_version: target, from_version: VERSION });
516
+ await update(cmd.id, 'running', {
517
+ step: 'draining', mode, target_version: target, from_version: VERSION, restart_only: restartOnly,
518
+ });
500
519
  try {
501
- await restartHook({ commandId: cmd.id, version: target });
520
+ await restartHook({ commandId: cmd.id, version: target, restartOnly });
502
521
  }
503
522
  catch (err) {
504
523
  // The hook owns the point of no return (it writes the handoff only once
package/dist/index.js CHANGED
@@ -248,11 +248,17 @@ async function start() {
248
248
  // daemon for one node — the failure the lockfile-identity fix was about. So
249
249
  // when launchd is holding us up, the roll simply STANDS DOWN and lets
250
250
  // KeepAlive do the starting.
251
- (0, command_runner_1.setRestartHook)(async ({ commandId, version }) => {
251
+ //
252
+ // A restart-only bounce takes the same path minus the install: `version` is
253
+ // already this process's version, so the respawn plan pins the replacement
254
+ // to the same bits (which matters for npx, whose cache dir is per-version)
255
+ // and npm is never called — nothing to fetch, and nothing that can fail
256
+ // offline.
257
+ (0, command_runner_1.setRestartHook)(async ({ commandId, version, restartOnly }) => {
252
258
  const mode = (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
253
259
  const plan = (0, self_update_1.planRespawn)(mode, process.argv[1] ?? '', version, process.execPath, process.argv.slice(2));
254
260
  const supervised = (0, autostart_1.isSupervised)();
255
- if (plan.installFirst) {
261
+ if (plan.installFirst && !restartOnly) {
256
262
  const err = await (0, self_update_1.installGlobal)(version);
257
263
  if (err)
258
264
  throw new Error(err);
@@ -276,14 +282,16 @@ async function start() {
276
282
  at: new Date().toISOString(),
277
283
  drained: lastDrain.drained,
278
284
  timed_out: lastDrain.timedOut,
285
+ restart_only: restartOnly === true,
279
286
  });
280
287
  }
281
288
  catch (err) {
282
289
  console.error('[restart] handoff write failed — the roll still happened, it just cannot self-report:', err.message);
283
290
  }
291
+ const what = restartOnly ? `${mode} → restart on ${version}` : `${mode} → ${version}`;
284
292
  console.log(supervised
285
- ? `[restart] standing down for launchd to restart us (${mode} → ${version}); exiting`
286
- : `[restart] handed over to ${plan.file} (${mode} → ${version}); exiting`);
293
+ ? `[restart] standing down for launchd to restart us (${what}); exiting`
294
+ : `[restart] handed over to ${plan.file} (${what}); exiting`);
287
295
  setTimeout(() => process.exit(0), 250).unref?.();
288
296
  });
289
297
  process.once('SIGINT', () => { stop().finally(() => process.exit(0)); });
@@ -395,6 +403,7 @@ async function finalizeRestartHandoff() {
395
403
  version: VERSION,
396
404
  mode: h.mode,
397
405
  restarted: true,
406
+ restart_only: h.restart_only === true,
398
407
  version_changed: changed,
399
408
  drained: h.drained ?? 0,
400
409
  // >0 means the drain ceiling expired with work still running — the
@@ -45,6 +45,27 @@ export declare function planRespawn(mode: LaunchMode, entryPath: string, targetV
45
45
  * slice(2)) — carried across the restart so a node launched as
46
46
  * `… cli.js run --foo` doesn't silently come back up without them. */
47
47
  userArgs?: string[]): RespawnPlan;
48
+ export interface RollPlan {
49
+ /** Version the replacement must come up as. */
50
+ target: string;
51
+ /** A bounce on the running version: nothing is installed, nothing moves. */
52
+ restartOnly: boolean;
53
+ /** Non-null = refuse before anything drains, with this explanation. */
54
+ refusal: string | null;
55
+ }
56
+ /**
57
+ * Read an `update_runtime` command's input into what this node should do.
58
+ *
59
+ * Pure so the one rule that is easy to get wrong stays testable: a source
60
+ * checkout may not be version-bumped remotely (its version comes from the
61
+ * working tree, so it would restart and report the same version — a silent
62
+ * no-op dressed up as success) but it MAY be restarted, which is the half of
63
+ * the operation that works there.
64
+ */
65
+ export declare function planRoll(input: {
66
+ version?: string;
67
+ restart_only?: boolean;
68
+ } | null, mode: LaunchMode, currentVersion: string): RollPlan;
48
69
  export interface RestartHandoff {
49
70
  command_id: string;
50
71
  from_version: string;
@@ -56,6 +77,10 @@ export interface RestartHandoff {
56
77
  * work unfinished rather than claiming a clean restart either way. */
57
78
  drained?: number;
58
79
  timed_out?: number;
80
+ /** True when this was a bounce on the running version rather than a roll,
81
+ * so the completed command says which one actually happened instead of
82
+ * leaving it to be inferred from two versions that match. */
83
+ restart_only?: boolean;
59
84
  }
60
85
  export declare function handoffPath(runtimeDir: string): string;
61
86
  export declare function writeHandoff(runtimeDir: string, h: RestartHandoff): void;
@@ -55,6 +55,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
55
55
  exports.PACKAGE_NAME = void 0;
56
56
  exports.detectLaunchMode = detectLaunchMode;
57
57
  exports.planRespawn = planRespawn;
58
+ exports.planRoll = planRoll;
58
59
  exports.handoffPath = handoffPath;
59
60
  exports.writeHandoff = writeHandoff;
60
61
  exports.takeHandoff = takeHandoff;
@@ -112,6 +113,26 @@ userArgs = []) {
112
113
  canChangeVersion: mode === 'global',
113
114
  };
114
115
  }
116
+ /**
117
+ * Read an `update_runtime` command's input into what this node should do.
118
+ *
119
+ * Pure so the one rule that is easy to get wrong stays testable: a source
120
+ * checkout may not be version-bumped remotely (its version comes from the
121
+ * working tree, so it would restart and report the same version — a silent
122
+ * no-op dressed up as success) but it MAY be restarted, which is the half of
123
+ * the operation that works there.
124
+ */
125
+ function planRoll(input, mode, currentVersion) {
126
+ const restartOnly = input?.restart_only === true;
127
+ // A restart targets the version already running, which keeps the boot-side
128
+ // "did we come up as what was asked for?" check meaningful instead of
129
+ // special-cased — and pins npx, whose cache dir is per-version.
130
+ const target = restartOnly ? currentVersion : (input?.version ?? '').trim() || 'latest';
131
+ const refusal = !restartOnly && mode === 'source' && target !== 'latest'
132
+ ? 'this node runs from a source checkout — pull and rebuild it there; a remote version bump cannot apply'
133
+ : null;
134
+ return { target, restartOnly, refusal };
135
+ }
115
136
  function handoffPath(runtimeDir) {
116
137
  return path.join(runtimeDir, 'restart.json');
117
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
5
  "license": "MIT",
6
6
  "keywords": [