@karmaniverous/jeeves 0.6.0-0 → 0.6.0-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.
package/README.md CHANGED
@@ -229,6 +229,8 @@ OpenClaw must already be installed; `jeeves` checks for it and never installs it
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
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.
231
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`). Only that refusal is retried: 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.
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
 
234
236
  `jeeves update` runs steps 2 to 4 for the named packages, or for every Jeeves plugin that has a `plugins.entries` record, at `latest`. It takes the same plugin options as `jeeves install` (including `--plugin-config` and `--force-reinstall`) and fills in missing plugin config with the same precedence: existing values are kept unless you pass them, and a missing required value fails the command before anything changes.
@@ -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-0` 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-0';
151
151
 
152
152
  /**
153
153
  * Shared internal utility functions.
@@ -1626,6 +1626,103 @@ 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; any other failure is rethrown at once. Config writes are idempotent
1643
+ * leaf sets/unsets, so repeating one is safe.
1644
+ *
1645
+ * @module
1646
+ */
1647
+ /** Default policy: 2s, 4s, 8s, 16s, then 30s steps; 120s of waiting in total. */
1648
+ const DEFAULT_CONVERGENCE_RETRY = {
1649
+ initialDelayMs: 2_000,
1650
+ maxDelayMs: 30_000,
1651
+ budgetMs: 120_000,
1652
+ };
1653
+ /** Default sleep adapter (real timer). */
1654
+ const timerSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1655
+ const UNCONVERGED_PLUGIN = /Plugin "([^"]+)" data\/settings upgrade is unfinished/g;
1656
+ const UNCONVERGED_ANY = /upgrade is unfinished|has not converged/i;
1657
+ /**
1658
+ * Detect OpenClaw's "plugin not yet converged" refusal in a failure.
1659
+ *
1660
+ * @param error - The thrown value.
1661
+ * @returns The plugin ids still converging (`['unknown plugin']` when the
1662
+ * signal is present without an id), or `undefined` when this is some other
1663
+ * failure.
1664
+ */
1665
+ function unconvergedPlugins(error) {
1666
+ if (!(error instanceof CommandFailedError))
1667
+ return undefined;
1668
+ const text = `${error.result.stderr}\n${error.result.stdout}`;
1669
+ if (!UNCONVERGED_ANY.test(text))
1670
+ return undefined;
1671
+ const ids = [
1672
+ ...new Set([...text.matchAll(UNCONVERGED_PLUGIN)].map((m) => m[1])),
1673
+ ];
1674
+ return ids.length > 0 ? ids : ['unknown plugin'];
1675
+ }
1676
+ /**
1677
+ * Delays for a policy: doubling from `initialDelayMs`, capped at
1678
+ * `maxDelayMs`, until the next delay would exceed the budget.
1679
+ *
1680
+ * @param policy - Retry policy.
1681
+ * @returns Delays in ms.
1682
+ */
1683
+ function retryDelays(policy) {
1684
+ const delays = [];
1685
+ let total = 0;
1686
+ let next = policy.initialDelayMs;
1687
+ while (next > 0 && total + next <= policy.budgetMs) {
1688
+ delays.push(next);
1689
+ total += next;
1690
+ next = Math.min(next * 2, policy.maxDelayMs);
1691
+ }
1692
+ return delays;
1693
+ }
1694
+ /**
1695
+ * Run an idempotent OpenClaw config write, retrying only while plugins are
1696
+ * still converging.
1697
+ *
1698
+ * @param action - The write.
1699
+ * @param options - Sleep, logger, policy.
1700
+ * @throws The original error for any other failure; an `Error` naming the
1701
+ * plugins still converging once the budget is spent.
1702
+ */
1703
+ async function withConvergenceRetry(action, options) {
1704
+ const delays = retryDelays(options.policy ?? DEFAULT_CONVERGENCE_RETRY);
1705
+ let waited = 0;
1706
+ for (let attempt = 0;; attempt++) {
1707
+ try {
1708
+ await action();
1709
+ return;
1710
+ }
1711
+ catch (error) {
1712
+ const plugins = unconvergedPlugins(error);
1713
+ if (!plugins)
1714
+ throw error;
1715
+ const delay = delays.at(attempt);
1716
+ if (delay === undefined) {
1717
+ 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 });
1718
+ }
1719
+ options.log(`OpenClaw has not finished converging plugin(s) ${plugins.join(', ')}; retrying in ${String(delay / 1000)}s (retry ${String(attempt + 1)}/${String(delays.length)})`);
1720
+ await options.sleep(delay);
1721
+ waited += delay;
1722
+ }
1723
+ }
1724
+ }
1725
+
1629
1726
  /**
1630
1727
  * Pure builders for every `openclaw` / `npm` argument vector the jeeves CLI
1631
1728
  * runs. No I/O.
@@ -2046,10 +2143,19 @@ function createNodePrivateTempFiles(runner, platform = process.platform, baseDir
2046
2143
  * Live: runs steps in order and stops at the first failure; a non-zero exit
2047
2144
  * from any `openclaw` command throws {@link CommandFailedError}, which the CLI
2048
2145
  * turns into a non-zero exit. Config batches are written to an owner-only
2049
- * temp file, passed with `--batch-file`, and deleted afterwards.
2146
+ * temp file, passed with `--batch-file`, and deleted afterwards. Config
2147
+ * writes (batches and unsets) are retried with bounded backoff only while
2148
+ * OpenClaw reports that a freshly installed plugin has not converged yet
2149
+ * (see {@link withConvergenceRetry}); the batch file is kept for the retries
2150
+ * and deleted once.
2050
2151
  *
2051
2152
  * @module
2052
2153
  */
2154
+ /** Retry an idempotent config write while plugins converge. */
2155
+ const retryingConfigWrite = (ctx, action) => withConvergenceRetry(action, {
2156
+ sleep: ctx.sleep ?? timerSleep,
2157
+ log: ctx.log,
2158
+ });
2053
2159
  /** Run one command (echoing its output), logging its command line first. */
2054
2160
  async function runLogged(ctx, command, args) {
2055
2161
  ctx.log(`$ ${formatCommand(command, args)}`);
@@ -2067,12 +2173,9 @@ async function runConfigBatch(ctx, ops, redact) {
2067
2173
  ctx.log(`$ ${command}`);
2068
2174
  for (const line of details)
2069
2175
  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);
2176
+ await withPrivateTempFile(ctx.tempFiles, BATCH_FILE_NAME, configBatchPayload(ops), (path) => retryingConfigWrite(ctx, async () => {
2177
+ await runChecked(ctx.runner, OPENCLAW_BIN, configSetBatchFileArgs(path), { echo: true, ...(redact ? { redact } : {}) });
2178
+ }), ctx.log);
2076
2179
  }
2077
2180
  /** Execute one step for real. */
2078
2181
  async function executeStep(step, ctx) {
@@ -2101,7 +2204,7 @@ async function executeStep(step, ctx) {
2101
2204
  const after = await readPluginsConfig(ctx.runner);
2102
2205
  const repair = computePostUninstallRepair(step.before, after, step.pluginIds);
2103
2206
  for (const path of repair.unsetPaths) {
2104
- await runLogged(ctx, OPENCLAW_BIN, configUnsetArgs(path));
2207
+ await retryingConfigWrite(ctx, () => runLogged(ctx, OPENCLAW_BIN, configUnsetArgs(path)));
2105
2208
  }
2106
2209
  if (repair.setOps.length > 0)
2107
2210
  await runConfigBatch(ctx, repair.setOps);
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-0` 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-0';
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-1",
137
137
  "allowScripts": {
138
138
  "lefthook": true
139
139
  }