@ouro.bot/cli 0.1.0-alpha.270 → 0.1.0-alpha.272

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/changelog.json CHANGED
@@ -1,6 +1,20 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.272",
6
+ "changes": [
7
+ "fix(daemon): the daemon's periodic update checker can now actually auto-update itself. The `onUpdate` callback in `daemon.ts` invoked `performStagedRestart` which (a) ran `npm install -g @ouro.bot/cli@{version}` to install to the global node prefix, and (b) tried to find the new code via `node -e \"console.log(require.resolve('@ouro.bot/cli/package.json'))\"` which depends on the daemon process's NODE_PATH. Neither path was actually reachable from the daemon process running out of `~/.ouro-cli/versions/{version}/...`, so every auto-update attempt bailed at `daemon.staged_restart_path_failed` (\"could not resolve new code path\") and the daemon never updated itself. The user had to manually run `ouro up` to pick up new versions. Fix: switch the staged restart to use the version-managed installer (same one the CLI's `up` flow uses) — `installVersion(version)` puts files at `~/.ouro-cli/versions/{version}/node_modules/@ouro.bot/cli` (deterministic, computable), then `activateVersion(version)` flips the CurrentVersion symlink so the next user-driven `ouro up` sees the same version the daemon is running. `performStagedRestart` gained an optional `installNewVersion` dep that production callers inject; the legacy `npm install -g` fallback path is preserved for tests. Two new regression tests in `staged-restart.test.ts`.",
8
+ "fix(cli): `ouro up` no longer prints the 'ouro updated to ...' message twice during npx invocations. Three independent paths can detect a CLI version change: (1) `checkForCliUpdate` finds a newer version on npm and re-execs (cross-process print), (2) `ensureCurrentVersionInstalled` flips the CurrentVersion symlink during `performSystemSetup` because the running package version is newer than what the symlink pointed at (path 2, in-process), (3) `bundle-meta.json`'s stored runtime version differs from the running version (path 3, in-process fallback). Path 3's existing guard `linkedVersionBeforeUp !== currentVersion` correctly skipped path 3 when path 1 had already printed in a different process, but did NOT catch the case where path 2 fired in the same process. Verified live on 2026-04-08: `npx --yes @ouro.bot/cli@alpha up` printed the message twice. Fix: track an in-process `printedUpdateMessage` flag set by path 2; path 3 checks it before printing. Path 3 still acts as a fallback when path 2 didn't fire. New regression test in `daemon-cli-update-flow.test.ts` simulates the npx scenario and asserts exactly one print.",
9
+ "fix(hooks): Claude Code lifecycle hooks (`ouro hook session-start|stop|post-tool-use`) now short-circuit when the daemon socket file doesn't exist, instead of attempting `sendDaemonCommand` and logging two ENOENT errors per hook fire (one for `message.send`, one for `inner.wake`). Every Claude Code event during a daemon-down window was producing noisy `connect ENOENT /tmp/ouroboros-daemon.sock` entries in `ouro.ndjson`, which made it hard to read logs around outages. The hook is best-effort by design — dropping notifications when the daemon is down is correct behavior; we just don't want to log spam about it. New nerves event `daemon.hook_skipped_no_socket` (info level) when the short-circuit fires."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.271",
14
+ "changes": [
15
+ "fix(daemon): unblock daemon.stop deadlock that hung `ouro up` after a CLI auto-update. When the running daemon's version drifted from the local CLI version, `ensureDaemonRunning` would send `daemon.stop` over the socket, the daemon's command handler would `await this.stop()`, and `stop()` would `await server.close()`. server.close() resolves only after every open connection has closed — but the calling client's connection was the ONE thing keeping the server open: its `flushResponse()` was awaiting THIS function call. Both processes sat in kevent forever. Verified live on 2026-04-08: alpha.268 daemon hung at `daemon.server_end` log line for 5+ minutes after a fresh alpha.270 ouro process sent daemon.stop, and the alpha.270 ouro process hung waiting for the response. The deadlock had existed since the original `await server.close()` line was added (2026-03-05) but was masked for weeks by the half-close behavior in socket-client: the client called `client.end()` after writing its command, which (with `allowHalfOpen: false`) caused node to auto-tear-down the server's writable side, incidentally unblocking server.close() before the response was sent. The fix in #303/#334/#339 (which removed `client.end()` and switched to `allowHalfOpen: true` to stop dropping long-running responses like agent.senseTurn) accidentally exposed the underlying deadlock. Fix: don't `await` server.close() in stop() — just fire it. Once stop() returns, the daemon.stop case returns its response, flushResponse calls connection.end(response), the connection closes, and server.close()'s pending callback fires asynchronously. Includes a new daemon-stop-deadlock.test.ts that uses real net sockets to drive daemon.stop and asserts the response comes back within 2s — the test fails (24s timeout) without the fix and passes (110ms) with it."
16
+ ]
17
+ },
4
18
  {
5
19
  "version": "0.1.0-alpha.270",
6
20
  "changes": [
@@ -791,6 +791,27 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
791
791
  }
792
792
  }
793
793
  await performSystemSetup(deps);
794
+ // Track whether we've already printed the "ouro updated to" message
795
+ // this turn so the bundle-meta-fallback path below doesn't double-print.
796
+ // There are three independent paths that can detect "the binary just
797
+ // got newer":
798
+ // 1. checkForCliUpdate found a newer version on npm (above) — this
799
+ // path always re-execs, so the duplicate-detect runs in a
800
+ // different process and the in-process tracker doesn't apply.
801
+ // 2. ensureCurrentVersionInstalled (called from performSystemSetup)
802
+ // flipped the CurrentVersion symlink because the running package
803
+ // version is newer than what the symlink pointed at. This is the
804
+ // common npx path.
805
+ // 3. bundle-meta.json's stored runtime version differs from the
806
+ // running version (fallback for when path 2 couldn't activate
807
+ // but the binary is still newer).
808
+ //
809
+ // Verified live on 2026-04-08: npx download triggered both path 2 and
810
+ // path 3 in the same process and the user saw the message printed twice.
811
+ // Path 3's existing `linkedVersionBeforeUp !== currentVersion` guard
812
+ // catches the cross-process re-exec case (path 1) but not the
813
+ // in-process double-fire case (path 2 + path 3 in the same process).
814
+ let printedUpdateMessage = false;
794
815
  const linkedVersionAfterSetup = deps.getCurrentCliVersion?.() ?? null;
795
816
  const runtimeVersion = (0, bundle_manifest_1.getPackageVersion)();
796
817
  if (linkedVersionBeforeUp && linkedVersionBeforeUp !== runtimeVersion && linkedVersionAfterSetup === runtimeVersion) {
@@ -799,6 +820,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
799
820
  if (changelogCommand) {
800
821
  deps.writeStdout(`review changes with: ${changelogCommand}`);
801
822
  }
823
+ printedUpdateMessage = true;
802
824
  }
803
825
  // Run update hooks before starting daemon so user sees the output
804
826
  (0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
@@ -812,9 +834,14 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
812
834
  // Notify about CLI binary update (npx downloaded a new version).
813
835
  // Skip when the symlink already points to the running version — that
814
836
  // means path 1 (checkForCliUpdate + reExecFromNewVersion) already
815
- // printed the update message before re-exec.
837
+ // printed the update message before re-exec. Also skip when path 2
838
+ // (the symlink-flip detector above) already printed in this same
839
+ // process — otherwise npx invocations print the message twice.
816
840
  /* v8 ignore start -- CLI update detection: tested via daemon-cli-version-detect.test.ts @preserve */
817
- if (previousCliVersion && previousCliVersion !== currentVersion && linkedVersionBeforeUp !== currentVersion) {
841
+ if (!printedUpdateMessage
842
+ && previousCliVersion
843
+ && previousCliVersion !== currentVersion
844
+ && linkedVersionBeforeUp !== currentVersion) {
818
845
  deps.writeStdout(`ouro updated to ${currentVersion} (was ${previousCliVersion})`);
819
846
  const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(previousCliVersion, currentVersion);
820
847
  /* v8 ignore next -- buildChangelogCommand is non-null when previous/current runtime versions differ @preserve */
@@ -1122,12 +1149,29 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1122
1149
  else {
1123
1150
  content = `[Claude Code hook: ${eventType} in session ${sessionId}]`;
1124
1151
  }
1125
- // Send to the specific agent configured for this hook
1126
- try {
1127
- await deps.sendCommand(deps.socketPath, { kind: "message.send", from: `claude-code:${sessionId}`, to: command.agent, content }).catch(() => { });
1128
- await deps.sendCommand(deps.socketPath, { kind: "inner.wake", agent: command.agent }).catch(() => { });
1152
+ // Send to the specific agent configured for this hook. Short-circuit
1153
+ // when the daemon socket file doesn't exist — otherwise every
1154
+ // Claude Code lifecycle event during a daemon-down window logs two
1155
+ // ENOENT errors in ouro.ndjson (one for message.send, one for
1156
+ // inner.wake) which makes it hard to read the log around outages.
1157
+ // The hook is best-effort: dropping notifications when the daemon
1158
+ // is down is the correct behavior; we just don't want to log spam
1159
+ // about it.
1160
+ if (require("fs").existsSync(deps.socketPath)) {
1161
+ try {
1162
+ await deps.sendCommand(deps.socketPath, { kind: "message.send", from: `claude-code:${sessionId}`, to: command.agent, content }).catch(() => { });
1163
+ await deps.sendCommand(deps.socketPath, { kind: "inner.wake", agent: command.agent }).catch(() => { });
1164
+ }
1165
+ catch { /* daemon not running — silent */ }
1166
+ }
1167
+ else {
1168
+ (0, runtime_1.emitNervesEvent)({
1169
+ component: "daemon",
1170
+ event: "daemon.hook_skipped_no_socket",
1171
+ message: "claude code hook skipped — daemon socket missing",
1172
+ meta: { socketPath: deps.socketPath, eventType, agent: command.agent },
1173
+ });
1129
1174
  }
1130
- catch { /* daemon not running — silent */ }
1131
1175
  // Output for Claude Code hook system
1132
1176
  deps.writeStdout(JSON.stringify({ continue: true }));
1133
1177
  return JSON.stringify({ continue: true });
@@ -54,6 +54,7 @@ const agent_config_v2_1 = require("./hooks/agent-config-v2");
54
54
  const bundle_manifest_1 = require("../../mind/bundle-manifest");
55
55
  const update_checker_1 = require("../versioning/update-checker");
56
56
  const staged_restart_1 = require("../versioning/staged-restart");
57
+ const ouro_version_manager_1 = require("../versioning/ouro-version-manager");
57
58
  const child_process_1 = require("child_process");
58
59
  const pending_1 = require("../../mind/pending");
59
60
  const agent_service_1 = require("./agent-service");
@@ -427,17 +428,34 @@ class OuroDaemon {
427
428
  onUpdate: /* v8 ignore start -- integration: real npm install + process spawn @preserve */ async (result) => {
428
429
  if (!result.latestVersion)
429
430
  return;
431
+ // Install via the version manager (NOT `npm install -g`). The
432
+ // global install path doesn't end up on the daemon process's
433
+ // NODE_PATH, so the previous `require.resolve('@ouro.bot/cli')`
434
+ // -based path lookup always returned null and the staged restart
435
+ // never actually completed. Verified live on 2026-04-08:
436
+ // alpha.268 daemon detected alpha.270 was available, ran the
437
+ // staged restart, and bailed at `staged_restart_path_failed` —
438
+ // meaning the daemon could never auto-update itself and required
439
+ // manual `ouro up` to pick up new versions.
440
+ //
441
+ // Switch to the version-managed layout the CLI itself uses:
442
+ // installVersion(version) puts files at
443
+ // ~/.ouro-cli/versions/{version}/node_modules/@ouro.bot/cli
444
+ // which is a known path we can compute deterministically.
445
+ // Then activateVersion(version) flips the CurrentVersion symlink
446
+ // so the next `ouro up` from the user sees the same version
447
+ // the daemon is running.
448
+ const cliHome = (0, ouro_version_manager_1.getOuroCliHome)();
430
449
  await (0, staged_restart_1.performStagedRestart)(result.latestVersion, {
431
450
  execSync: (cmd) => (0, child_process_1.execSync)(cmd, { stdio: "inherit" }),
432
451
  spawnSync: child_process_1.spawnSync,
433
- resolveNewCodePath: (_version) => {
434
- try {
435
- const resolved = (0, child_process_1.execSync)(`node -e "console.log(require.resolve('@ouro.bot/cli/package.json'))"`, { encoding: "utf-8" }).trim();
436
- return resolved ? path.dirname(resolved) : null;
437
- }
438
- catch {
439
- return null;
440
- }
452
+ installNewVersion: (version) => {
453
+ (0, ouro_version_manager_1.installVersion)(version, {});
454
+ (0, ouro_version_manager_1.activateVersion)(version, {});
455
+ },
456
+ resolveNewCodePath: (version) => {
457
+ const versionPath = path.join(cliHome, "versions", version, "node_modules", "@ouro.bot", "cli");
458
+ return fs.existsSync(versionPath) ? versionPath : null;
441
459
  },
442
460
  gracefulShutdown: () => daemon.stop(),
443
461
  spawnNewDaemon: (entryPath, sock) => {
@@ -745,9 +763,36 @@ class OuroDaemon {
745
763
  await this.processManager.stopAll();
746
764
  await this.senseManager?.stopAll();
747
765
  if (this.server) {
748
- await new Promise((resolve) => {
749
- this.server?.close(() => resolve());
750
- });
766
+ // DO NOT `await` server.close() here. server.close() resolves only
767
+ // after every open connection has closed. When stop() is invoked
768
+ // from the daemon.stop command handler, the calling client's
769
+ // connection is STILL open — its flushResponse() is currently
770
+ // awaiting THIS function. Awaiting close() creates a deadlock:
771
+ //
772
+ // client → flushResponse → handleRawPayload → daemon.stop case
773
+ // → stop() → await server.close() (waits for client's connection)
774
+ // → client's connection waits for flushResponse to call
775
+ // connection.end() → DEADLOCK
776
+ //
777
+ // Both processes sit in kevent forever. Verified live on
778
+ // 2026-04-08: alpha.268 daemon hung at `daemon.server_end` log
779
+ // line for 5+ minutes after a client sent daemon.stop, while the
780
+ // client (alpha.270 ouro up) hung waiting for the response.
781
+ //
782
+ // This regressed when #303/#334/#339 stopped half-closing the
783
+ // client socket and switched the server to allowHalfOpen: true.
784
+ // Previously, the client called .end() after writing its command,
785
+ // which (with allowHalfOpen: false) caused node to auto-tear-down
786
+ // the server's writable side — incidentally unblocking
787
+ // server.close() before the response was sent. The half-close
788
+ // breakage masked this deadlock; the fix exposed it.
789
+ //
790
+ // Solution: fire close() and let it complete asynchronously. Once
791
+ // stop() returns, the daemon.stop case returns its response,
792
+ // flushResponse() calls connection.end(response), the connection
793
+ // closes, and server.close()'s pending callback fires. The event
794
+ // loop drains and the daemon exits cleanly.
795
+ this.server.close();
751
796
  this.server = null;
752
797
  }
753
798
  /* v8 ignore start -- outlookServer stop: only reachable when the outlook HTTP server successfully bound to port 6876 during start(). Coverage fluctuates locally because a running production daemon holds the port and tests get EADDRINUSE, leaving outlookServer null. In CI the port is free and this block runs. Not worth fighting port contention to cover two trivial lines. @preserve */
@@ -45,14 +45,22 @@ async function performStagedRestart(version, deps) {
45
45
  });
46
46
  // Step 1: Install new version
47
47
  try {
48
- deps.execSync(`npm install -g @ouro.bot/cli@${version}`);
48
+ if (deps.installNewVersion) {
49
+ deps.installNewVersion(version);
50
+ }
51
+ else {
52
+ // Backward-compat fallback for callers that haven't migrated to the
53
+ // version-managed installer. Tests use this path with a mocked
54
+ // execSync; production callers inject installNewVersion.
55
+ deps.execSync(`npm install -g @ouro.bot/cli@${version}`);
56
+ }
49
57
  }
50
58
  catch (err) {
51
59
  const errorMessage = err instanceof Error ? err.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(err);
52
60
  (0, runtime_1.emitNervesEvent)({
53
61
  component: "daemon",
54
62
  event: "daemon.staged_restart_install_failed",
55
- message: "npm install failed",
63
+ message: "install failed",
56
64
  meta: { version, error: errorMessage },
57
65
  });
58
66
  return { ok: false, error: errorMessage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.270",
3
+ "version": "0.1.0-alpha.272",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",