@karmaniverous/jeeves 0.6.0-0 → 0.6.0-2

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 CHANGED
@@ -227,7 +227,9 @@ OpenClaw must already be installed; `jeeves` checks for it and never installs it
227
227
  1. Renders the SOUL.md/AGENTS.md managed blocks (your content outside the markers is kept) and the core config if it's missing. It never writes TOOLS.md, HEARTBEAT.md, anything under `skills/` or the spec templates (those ship with the jeeves-design skill in jeeves-tools).
228
228
  2. For each plugin (default: `runner`, `watcher`, `server`, `meta` at `latest`), resolves an exact version with `npm view`, then runs `openclaw plugins install npm:<pkg>@<version> --pin --accept-capabilities --force`. `--force` is required for any non-ClawHub source, and it also overwrites an existing install, which is how updates land. The install is skipped when that exact version is already installed. The CLI reads OpenClaw's install records once with `openclaw plugins inspect --all --json` (no plugin code is loaded) and skips a plugin only if its record has `source: "npm"`, names the same package, and records the same version, and the loaded plugin reports that version too. A v0.x path install, a leftover legacy copy, or any record it cannot read means a reinstall. `--force-reinstall` always reinstalls. Steps 3 and 4 run either way.
229
229
  3. Removes any legacy `<openclaw dir>/extensions/<id>` copy left by the v0.x installer, but only if its `package.json` names the expected package.
230
- 4. Sets `plugins.entries.<id>.hooks.allowConversationAccess: true` for plugins that [declare conversation hooks](#declaring-conversation-hooks), and the plugin config (`plugins.entries.<id>.config.<key>`, see [Plugin config](#plugin-config)), with one `openclaw config set --batch-file <file>` call. The file is created owner-only in a fresh temp directory (mode `0600` in a `0700` directory on Linux/macOS; on Windows the directory ACL is reduced to the current user with `icacls`) and deleted afterwards, so no value, secret or not, appears on a command line. Every write targets a leaf path, so unrelated keys are kept. `plugins.installs` is never written.
230
+ 4. Runs `openclaw plugins inspect --all --json` once (a migration sweep, see below; its output is not shown and a failure is only logged), then sets `plugins.entries.<id>.hooks.allowConversationAccess: true` for plugins that [declare conversation hooks](#declaring-conversation-hooks), and the plugin config (`plugins.entries.<id>.config.<key>`, see [Plugin config](#plugin-config)), with one `openclaw config set --batch-file <file>` call. The file is created owner-only in a fresh temp directory (mode `0600` in a `0700` directory on Linux/macOS; on Windows the directory ACL is reduced to the current user with `icacls`) and deleted afterwards, so no value, secret or not, appears on a command line. Every write targets a leaf path, so unrelated keys are kept. `plugins.installs` is never written.
231
+
232
+ Right after an install, OpenClaw can refuse to edit a plugin's config until it finishes that plugin's data/settings upgrade (`Plugin "<id>" data/settings upgrade is unfinished: ... has not converged`). This happens when the gateway started with `plugins.entries.<id>` configured before the package was installed: OpenClaw records a pending migration for that plugin, installing it does not clear the record, and a refused `openclaw config set` doesn't either. OpenClaw resolves such records in the startup preflight of ordinary CLI commands (not `config`, and not `plugins list`), which is why the sweep runs `plugins inspect` before the batch. Only that refusal is retried: the sweep runs again, then the same batch file is resubmitted after 2s, 4s, 8s, 16s, then every 30s, for up to 120s of waiting in total, with one log line per retry. If the plugin still hasn't converged, the command fails and names the plugin; wait and rerun it (the writes are idempotent). Any other error fails at once. The `openclaw config unset` repairs after an uninstall follow the same rule. A dry run never retries.
231
233
 
232
234
  Plugin specs can be short (`watcher`, `watcher@1.2.3`, `runner@^1`) or full (`@karmaniverous/jeeves-watcher-openclaw@1.2.3`). Only `@karmaniverous/jeeves-*-openclaw` packages are accepted. `--content-only` skips the plugins.
233
235
 
@@ -303,6 +305,7 @@ $ jeeves install watcher --dry-run
303
305
  …
304
306
  [dry-run] openclaw plugins install npm:@karmaniverous/jeeves-watcher-openclaw@0.16.0 --pin --accept-capabilities --force
305
307
  [dry-run] remove legacy plugin copy: /home/jeeves/.openclaw/extensions/jeeves-watcher-openclaw
308
+ [dry-run] openclaw plugins inspect --all --json (lets OpenClaw clear pending plugin migrations)
306
309
  [dry-run] openclaw config set --batch-file <private temp file>
307
310
  [dry-run] batch file content: [{"path":"plugins.entries.jeeves-watcher-openclaw.hooks.allowConversationAccess","value":true}]
308
311
  ```
@@ -320,6 +323,7 @@ Plugin config:
320
323
  …
321
324
  [dry-run] set keys._plugin = <redacted> in /srv/jeeves/config/jeeves-server/config.json (currently unset; backup /srv/jeeves/config/jeeves-server/config.json.bak-<timestamp> first, then atomic write; restart jeeves-server afterwards)
322
325
  …
326
+ [dry-run] openclaw plugins inspect --all --json (lets OpenClaw clear pending plugin migrations)
323
327
  [dry-run] openclaw config set --batch-file <private temp file>
324
328
  [dry-run] batch file content: [{"path":"plugins.entries.jeeves-server-openclaw.config.configRoot","value":"/srv/jeeves/config"},{"path":"plugins.entries.jeeves-server-openclaw.config.apiUrl","value":"http://127.0.0.1:1934"},{"path":"plugins.entries.jeeves-server-openclaw.config.pluginKey","value":"<redacted>"}]
325
329
  ```
@@ -140,14 +140,14 @@ const DEFAULT_PORTS = {
140
140
  * Core library version, inlined at build time.
141
141
  *
142
142
  * @remarks
143
- * The `0.5.12` placeholder is replaced by
143
+ * The `0.6.0-1` placeholder is replaced by
144
144
  * `@rollup/plugin-replace` during the build with the actual version
145
145
  * from `package.json`. This ensures the correct version survives
146
146
  * when consumers bundle core into their own dist (where runtime
147
147
  * `import.meta.url`-based resolution would find the wrong package.json).
148
148
  */
149
149
  /** The core library version from package.json (inlined at build time). */
150
- const CORE_VERSION = '0.5.12';
150
+ const CORE_VERSION = '0.6.0-1';
151
151
 
152
152
  /**
153
153
  * Shared internal utility functions.
@@ -1626,6 +1626,106 @@ function configuredPluginIds(plugins, isJeeves) {
1626
1626
  .sort();
1627
1627
  }
1628
1628
 
1629
+ /**
1630
+ * Retry OpenClaw config writes while freshly installed plugins converge.
1631
+ *
1632
+ * @remarks
1633
+ * Right after `openclaw plugins install`, OpenClaw can refuse to edit a
1634
+ * plugin's retained config until that plugin's deferred data/settings
1635
+ * migration finishes. OpenClaw v2026.9.6 throws (from
1636
+ * `src/config/deferred-plugin-migration-config.ts`, message built by
1637
+ * `formatDeferredPluginMigration` in `src/infra/deferred-plugin-migrations.ts`):
1638
+ *
1639
+ * `Cannot edit retained config at "<path>". Plugin "<id>" data/settings upgrade is unfinished: <reason> ...`
1640
+ *
1641
+ * Only that signal is retried, with exponential backoff inside a fixed wait
1642
+ * budget; before each wait the optional `beforeRetry` hook runs (the plan
1643
+ * executor uses it to let OpenClaw clear pending migration records, see
1644
+ * `migrationSweep.ts`, since a refused `config set` never clears them); any other failure is rethrown at once. Config writes are idempotent
1645
+ * leaf sets/unsets, so repeating one is safe.
1646
+ *
1647
+ * @module
1648
+ */
1649
+ /** Default policy: 2s, 4s, 8s, 16s, then 30s steps; 120s of waiting in total. */
1650
+ const DEFAULT_CONVERGENCE_RETRY = {
1651
+ initialDelayMs: 2_000,
1652
+ maxDelayMs: 30_000,
1653
+ budgetMs: 120_000,
1654
+ };
1655
+ /** Default sleep adapter (real timer). */
1656
+ const timerSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1657
+ const UNCONVERGED_PLUGIN = /Plugin "([^"]+)" data\/settings upgrade is unfinished/g;
1658
+ const UNCONVERGED_ANY = /upgrade is unfinished|has not converged/i;
1659
+ /**
1660
+ * Detect OpenClaw's "plugin not yet converged" refusal in a failure.
1661
+ *
1662
+ * @param error - The thrown value.
1663
+ * @returns The plugin ids still converging (`['unknown plugin']` when the
1664
+ * signal is present without an id), or `undefined` when this is some other
1665
+ * failure.
1666
+ */
1667
+ function unconvergedPlugins(error) {
1668
+ if (!(error instanceof CommandFailedError))
1669
+ return undefined;
1670
+ const text = `${error.result.stderr}\n${error.result.stdout}`;
1671
+ if (!UNCONVERGED_ANY.test(text))
1672
+ return undefined;
1673
+ const ids = [
1674
+ ...new Set([...text.matchAll(UNCONVERGED_PLUGIN)].map((m) => m[1])),
1675
+ ];
1676
+ return ids.length > 0 ? ids : ['unknown plugin'];
1677
+ }
1678
+ /**
1679
+ * Delays for a policy: doubling from `initialDelayMs`, capped at
1680
+ * `maxDelayMs`, until the next delay would exceed the budget.
1681
+ *
1682
+ * @param policy - Retry policy.
1683
+ * @returns Delays in ms.
1684
+ */
1685
+ function retryDelays(policy) {
1686
+ const delays = [];
1687
+ let total = 0;
1688
+ let next = policy.initialDelayMs;
1689
+ while (next > 0 && total + next <= policy.budgetMs) {
1690
+ delays.push(next);
1691
+ total += next;
1692
+ next = Math.min(next * 2, policy.maxDelayMs);
1693
+ }
1694
+ return delays;
1695
+ }
1696
+ /**
1697
+ * Run an idempotent OpenClaw config write, retrying only while plugins are
1698
+ * still converging.
1699
+ *
1700
+ * @param action - The write.
1701
+ * @param options - Sleep, logger, policy, pre-retry hook.
1702
+ * @throws The original error for any other failure; an `Error` naming the
1703
+ * plugins still converging once the budget is spent.
1704
+ */
1705
+ async function withConvergenceRetry(action, options) {
1706
+ const delays = retryDelays(options.policy ?? DEFAULT_CONVERGENCE_RETRY);
1707
+ let waited = 0;
1708
+ for (let attempt = 0;; attempt++) {
1709
+ try {
1710
+ await action();
1711
+ return;
1712
+ }
1713
+ catch (error) {
1714
+ const plugins = unconvergedPlugins(error);
1715
+ if (!plugins)
1716
+ throw error;
1717
+ const delay = delays.at(attempt);
1718
+ if (delay === undefined) {
1719
+ throw new Error(`OpenClaw still refuses the config write after ${String(waited / 1000)}s: plugin(s) ${plugins.join(', ')} have not finished converging after install. Wait, then rerun the command (or run "openclaw doctor --fix").`, { cause: error });
1720
+ }
1721
+ options.log(`OpenClaw has not finished converging plugin(s) ${plugins.join(', ')}; retrying in ${String(delay / 1000)}s (retry ${String(attempt + 1)}/${String(delays.length)})`);
1722
+ await options.beforeRetry?.();
1723
+ await options.sleep(delay);
1724
+ waited += delay;
1725
+ }
1726
+ }
1727
+ }
1728
+
1629
1729
  /**
1630
1730
  * Pure builders for every `openclaw` / `npm` argument vector the jeeves CLI
1631
1731
  * runs. No I/O.
@@ -1777,6 +1877,57 @@ const npmViewVersionArgs = (packageName, range) => ['view', `${packageName}@${ra
1777
1877
  */
1778
1878
  const npmViewFieldArgs = (packageName, version, field) => ['view', `${packageName}@${version}`, field, '--json'];
1779
1879
 
1880
+ /**
1881
+ * Let OpenClaw resolve pending plugin-migration records before config writes.
1882
+ *
1883
+ * @remarks
1884
+ * When the gateway starts with `plugins.entries` for plugins that are not yet
1885
+ * installed, OpenClaw records a pending (deferred) data/settings migration for
1886
+ * each. Installing the plugin does not clear that record, and
1887
+ * `openclaw config set` refuses to edit the plugin's retained config while it
1888
+ * exists. `config` commands never clear it either: OpenClaw v2026.9.6 runs the
1889
+ * startup state-migration preflight (`runDoctorConfigPreflight` with
1890
+ * `migrateState: true`, which resolves finished records through
1891
+ * `recordDeferredPluginMigrations`, see
1892
+ * `src/commands/doctor-config-preflight-plugin-migrations.ts:160`) from
1893
+ * `ensureConfigReady` (`src/cli/program/config-guard.ts:256-328`) for every
1894
+ * guarded command except `config`, `health`, `logs`, `sessions` and a few
1895
+ * others. `plugins list` skips the guard (`configGuard: "skip"`,
1896
+ * `src/cli/command-catalog.ts:570-577`); `plugins inspect` has no catalog
1897
+ * entry, so it takes the default `configGuard: "run"`
1898
+ * (`src/cli/command-path-policy.ts:14`) and sweeps.
1899
+ *
1900
+ * The sweep is best effort: a failure is logged and never fails the plan (the
1901
+ * config write that follows reports any real problem).
1902
+ *
1903
+ * @module
1904
+ */
1905
+ /** Dry-run/log description of the sweep. */
1906
+ const MIGRATION_SWEEP_LINE = `${formatCommand(OPENCLAW_BIN, pluginsInspectAllArgs())} (lets OpenClaw clear pending plugin migrations)`;
1907
+ const warn = (log, reason) => {
1908
+ log(`plugin migration sweep failed (continuing): ${reason}`);
1909
+ };
1910
+ /**
1911
+ * Run `openclaw plugins inspect --all --json` so OpenClaw's startup preflight
1912
+ * resolves pending plugin-migration records. Output is not echoed.
1913
+ *
1914
+ * @param runner - Command runner.
1915
+ * @param log - Line logger.
1916
+ */
1917
+ async function runMigrationSweep(runner, log) {
1918
+ const args = pluginsInspectAllArgs();
1919
+ log(`$ ${MIGRATION_SWEEP_LINE}`);
1920
+ try {
1921
+ const result = await runner(OPENCLAW_BIN, args);
1922
+ if (result.exitCode !== 0) {
1923
+ warn(log, describeExit(OPENCLAW_BIN, args, result.exitCode));
1924
+ }
1925
+ }
1926
+ catch (error) {
1927
+ warn(log, String(error));
1928
+ }
1929
+ }
1930
+
1780
1931
  /**
1781
1932
  * Human-readable lines for plan steps: the exact command lines, batch file
1782
1933
  * contents with secrets redacted, and conditional repairs. Pure; used for
@@ -1821,6 +1972,8 @@ function describeStep(step) {
1821
1972
  return [formatCommand(step.command, step.args)];
1822
1973
  case 'configSetBatch':
1823
1974
  return describeConfigBatchLines(step.ops, step.redact);
1975
+ case 'migrationSweep':
1976
+ return [MIGRATION_SWEEP_LINE];
1824
1977
  case 'removeDir':
1825
1978
  return [`remove legacy plugin copy: ${step.path}`];
1826
1979
  case 'serverKeyWrite':
@@ -2046,10 +2199,21 @@ function createNodePrivateTempFiles(runner, platform = process.platform, baseDir
2046
2199
  * Live: runs steps in order and stops at the first failure; a non-zero exit
2047
2200
  * from any `openclaw` command throws {@link CommandFailedError}, which the CLI
2048
2201
  * turns into a non-zero exit. Config batches are written to an owner-only
2049
- * temp file, passed with `--batch-file`, and deleted afterwards.
2202
+ * temp file, passed with `--batch-file`, and deleted afterwards. Config
2203
+ * writes (batches and unsets) are retried with bounded backoff only while
2204
+ * OpenClaw reports that a freshly installed plugin has not converged yet
2205
+ * (see {@link withConvergenceRetry}); before each wait the migration sweep
2206
+ * runs again (see {@link runMigrationSweep}), and the batch file is kept for
2207
+ * the retries and deleted once.
2050
2208
  *
2051
2209
  * @module
2052
2210
  */
2211
+ /** Retry an idempotent config write while plugins converge. */
2212
+ const retryingConfigWrite = (ctx, action) => withConvergenceRetry(action, {
2213
+ sleep: ctx.sleep ?? timerSleep,
2214
+ log: ctx.log,
2215
+ beforeRetry: () => runMigrationSweep(ctx.runner, ctx.log),
2216
+ });
2053
2217
  /** Run one command (echoing its output), logging its command line first. */
2054
2218
  async function runLogged(ctx, command, args) {
2055
2219
  ctx.log(`$ ${formatCommand(command, args)}`);
@@ -2067,12 +2231,9 @@ async function runConfigBatch(ctx, ops, redact) {
2067
2231
  ctx.log(`$ ${command}`);
2068
2232
  for (const line of details)
2069
2233
  ctx.log(line);
2070
- await withPrivateTempFile(ctx.tempFiles, BATCH_FILE_NAME, configBatchPayload(ops), async (path) => {
2071
- await runChecked(ctx.runner, OPENCLAW_BIN, configSetBatchFileArgs(path), {
2072
- echo: true,
2073
- ...(redact ? { redact } : {}),
2074
- });
2075
- }, ctx.log);
2234
+ await withPrivateTempFile(ctx.tempFiles, BATCH_FILE_NAME, configBatchPayload(ops), (path) => retryingConfigWrite(ctx, async () => {
2235
+ await runChecked(ctx.runner, OPENCLAW_BIN, configSetBatchFileArgs(path), { echo: true, ...(redact ? { redact } : {}) });
2236
+ }), ctx.log);
2076
2237
  }
2077
2238
  /** Execute one step for real. */
2078
2239
  async function executeStep(step, ctx) {
@@ -2083,6 +2244,9 @@ async function executeStep(step, ctx) {
2083
2244
  case 'configSetBatch':
2084
2245
  await runConfigBatch(ctx, step.ops, step.redact);
2085
2246
  return;
2247
+ case 'migrationSweep':
2248
+ await runMigrationSweep(ctx.runner, ctx.log);
2249
+ return;
2086
2250
  case 'removeDir':
2087
2251
  if (ctx.fs.isDirectory(step.path)) {
2088
2252
  ctx.fs.removeDir(step.path);
@@ -2101,7 +2265,7 @@ async function executeStep(step, ctx) {
2101
2265
  const after = await readPluginsConfig(ctx.runner);
2102
2266
  const repair = computePostUninstallRepair(step.before, after, step.pluginIds);
2103
2267
  for (const path of repair.unsetPaths) {
2104
- await runLogged(ctx, OPENCLAW_BIN, configUnsetArgs(path));
2268
+ await retryingConfigWrite(ctx, () => runLogged(ctx, OPENCLAW_BIN, configUnsetArgs(path)));
2105
2269
  }
2106
2270
  if (repair.setOps.length > 0)
2107
2271
  await runConfigBatch(ctx, repair.setOps);
@@ -3041,6 +3205,8 @@ const openclaw = (args) => ({
3041
3205
  * @returns The server `keys._plugin` write (if planned; first, so a failure
3042
3206
  * there, e.g. a held lock, leaves OpenClaw untouched), then install steps
3043
3207
  * (targets not yet installed at their version), then legacy removals, then
3208
+ * (when there is config to write) a migration sweep so OpenClaw clears the
3209
+ * pending migration records it keeps for freshly installed plugins, then
3044
3210
  * one config batch with hook access (only for targets that declare
3045
3211
  * conversation hooks) and plugin config. Re-running after a later failure
3046
3212
  * converges: the server then has the key and the plugin side copies it.
@@ -3066,6 +3232,7 @@ function buildInstallPlan(targets, plugins, config) {
3066
3232
  ];
3067
3233
  if (ops.length > 0) {
3068
3234
  const secrets = config?.secrets ?? [];
3235
+ steps.push({ kind: 'migrationSweep' });
3069
3236
  steps.push({
3070
3237
  kind: 'configSetBatch',
3071
3238
  ops,
package/dist/index.js CHANGED
@@ -142,14 +142,14 @@ const DEFAULT_PORTS = {
142
142
  * Core library version, inlined at build time.
143
143
  *
144
144
  * @remarks
145
- * The `0.5.12` placeholder is replaced by
145
+ * The `0.6.0-1` placeholder is replaced by
146
146
  * `@rollup/plugin-replace` during the build with the actual version
147
147
  * from `package.json`. This ensures the correct version survives
148
148
  * when consumers bundle core into their own dist (where runtime
149
149
  * `import.meta.url`-based resolution would find the wrong package.json).
150
150
  */
151
151
  /** The core library version from package.json (inlined at build time). */
152
- const CORE_VERSION = '0.5.12';
152
+ const CORE_VERSION = '0.6.0-1';
153
153
 
154
154
  /**
155
155
  * Workspace and config root initialization.
package/package.json CHANGED
@@ -133,7 +133,7 @@
133
133
  },
134
134
  "type": "module",
135
135
  "types": "dist/index.d.ts",
136
- "version": "0.6.0-0",
136
+ "version": "0.6.0-2",
137
137
  "allowScripts": {
138
138
  "lefthook": true
139
139
  }