@totalreclaw/totalreclaw 3.3.12-rc.9 → 3.3.12

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 (79) hide show
  1. package/SKILL.md +33 -27
  2. package/api-client.ts +17 -9
  3. package/batch-gate.ts +42 -0
  4. package/billing-cache.ts +72 -23
  5. package/claims-helper.ts +2 -1
  6. package/config.ts +95 -13
  7. package/consolidation.ts +6 -13
  8. package/contradiction-sync.ts +19 -14
  9. package/credential-provider.ts +184 -0
  10. package/crypto.ts +27 -160
  11. package/dist/api-client.js +17 -9
  12. package/dist/batch-gate.js +40 -0
  13. package/dist/billing-cache.js +54 -24
  14. package/dist/claims-helper.js +2 -1
  15. package/dist/config.js +91 -13
  16. package/dist/consolidation.js +6 -15
  17. package/dist/contradiction-sync.js +15 -15
  18. package/dist/credential-provider.js +145 -0
  19. package/dist/crypto.js +17 -137
  20. package/dist/download-ux.js +11 -7
  21. package/dist/embedder-loader.js +266 -0
  22. package/dist/embedding.js +36 -3
  23. package/dist/entry.js +123 -0
  24. package/dist/fs-helpers.js +75 -379
  25. package/dist/import-adapters/gemini-adapter.js +29 -159
  26. package/dist/index.js +864 -2631
  27. package/dist/memory-runtime.js +459 -0
  28. package/dist/native-memory.js +123 -0
  29. package/dist/onboarding-cli.js +4 -8
  30. package/dist/pair-cli-relay.js +1 -8
  31. package/dist/pair-cli.js +1 -1
  32. package/dist/pair-crypto.js +16 -358
  33. package/dist/pair-http.js +147 -4
  34. package/dist/relay.js +140 -0
  35. package/dist/reranker.js +13 -8
  36. package/dist/semantic-dedup.js +5 -7
  37. package/dist/skill-register.js +97 -0
  38. package/dist/subgraph-search.js +3 -1
  39. package/dist/subgraph-store.js +348 -290
  40. package/dist/tool-gating.js +39 -26
  41. package/dist/tools.js +367 -0
  42. package/dist/tr-cli-export-helper.js +3 -3
  43. package/dist/tr-cli.js +65 -156
  44. package/dist/trajectory-poller.js +155 -9
  45. package/dist/vault-crypto.js +551 -0
  46. package/download-ux.ts +12 -6
  47. package/embedder-loader.ts +293 -1
  48. package/embedding.ts +43 -3
  49. package/entry.ts +132 -0
  50. package/fs-helpers.ts +93 -458
  51. package/import-adapters/gemini-adapter.ts +38 -183
  52. package/index.ts +912 -2917
  53. package/memory-runtime.ts +723 -0
  54. package/native-memory.ts +196 -0
  55. package/onboarding-cli.ts +3 -9
  56. package/openclaw.plugin.json +5 -17
  57. package/package.json +6 -3
  58. package/pair-cli-relay.ts +0 -9
  59. package/pair-cli.ts +1 -1
  60. package/pair-crypto.ts +41 -483
  61. package/pair-http.ts +194 -5
  62. package/postinstall.mjs +138 -0
  63. package/relay.ts +172 -0
  64. package/reranker.ts +13 -8
  65. package/semantic-dedup.ts +5 -6
  66. package/skill-register.ts +146 -0
  67. package/skill.json +1 -1
  68. package/subgraph-search.ts +3 -1
  69. package/subgraph-store.ts +367 -307
  70. package/tool-gating.ts +39 -26
  71. package/tools.ts +499 -0
  72. package/tr-cli-export-helper.ts +3 -3
  73. package/tr-cli.ts +69 -182
  74. package/trajectory-poller.ts +162 -10
  75. package/vault-crypto.ts +705 -0
  76. package/auto-pair-on-load.ts +0 -308
  77. package/dist/auto-pair-on-load.js +0 -197
  78. package/dist/pair-pending-injection.js +0 -125
  79. package/pair-pending-injection.ts +0 -205
@@ -28,6 +28,7 @@
28
28
  */
29
29
  import fs from 'node:fs';
30
30
  import path from 'node:path';
31
+ import { envHomeDir } from './entry.js';
31
32
  // ---------------------------------------------------------------------------
32
33
  // MEMORY.md header ensure
33
34
  // ---------------------------------------------------------------------------
@@ -82,8 +83,7 @@ export function ensureMemoryHeaderFile(workspace, header, markerSubstring = 'Tot
82
83
  * covers the OpenClaw plugin sandbox case where the loaded module
83
84
  * lives at `<pluginRoot>/dist/index.js` while `package.json` lives
84
85
  * at `<pluginRoot>/package.json` (3.3.4-rc.1 fix — without this
85
- * walk-up, the `.loaded.json` manifest gets `version=unknown` and
86
- * all RC-gated logic that depends on the version string fails
86
+ * walk-up, version-dependent logic gets `version=unknown` and fails
87
87
  * silently in production OpenClaw deployments).
88
88
  * - Returns the `version` field, or `null` on any I/O / parse error.
89
89
  *
@@ -471,157 +471,15 @@ export function wipePartialInstall(pluginRootDir) {
471
471
  }
472
472
  }
473
473
  // ---------------------------------------------------------------------------
474
- // Plugin load manifest (.loaded.json / .error.json) — 3.3.2-rc.1 #186
474
+ // Plugin load manifest (.loaded.json / .error.json) — RETIRED Phase 3.4
475
475
  // ---------------------------------------------------------------------------
476
- /**
477
- * Filenames written into the plugin root dir at the end of register() and
478
- * (on failure) from the surrounding try/catch. The presence of `.loaded.json`
479
- * is the canonical filesystem signal that the plugin's register() body ran
480
- * to completion AND the SDK tool/route/hook registries received calls.
481
- *
482
- * Acceptance criteria (issue #186):
483
- * - `cat ~/.openclaw/extensions/totalreclaw/.loaded.json` shows
484
- * `{loadedAt, tools, version}` after every successful gateway start.
485
- * - `cat ~/.openclaw/extensions/totalreclaw/.error.json` shows
486
- * `{loadedAt, error, stack}` if register() threw.
487
- * - Both are overwritten on each register() call so the agent can rely
488
- * on the timestamp matching the most recent gateway start.
489
- *
490
- * Why these MUST be synchronous writes (same constraint as
491
- * `registerHttpRoute` per the comment in index.ts around the route
492
- * registration site): the SDK loader treats register() returning as the
493
- * signal to freeze the registries. An async `fs.promises.writeFile` would
494
- * settle one microtask AFTER the loader has moved on, so the manifest
495
- * could miss tools that registered late OR drop entirely if the gateway
496
- * exits before the microtask runs. `writeFileSync` is the only safe choice.
497
- */
498
- export const PLUGIN_LOADED_MANIFEST = '.loaded.json';
499
- export const PLUGIN_ERROR_MANIFEST = '.error.json';
500
- /**
501
- * Resolve the plugin root dir from the loaded module's directory. The plugin
502
- * is shipped with `dist/index.js` as the entry, so `import.meta.url` resolves
503
- * to `<root>/dist/`. We walk up one level to put the manifests next to
504
- * `package.json`. Defensive: if the caller already passed the root (no
505
- * trailing `dist`), we still return a sensible path.
506
- */
507
- function resolvePluginRootForManifest(pluginDir) {
508
- const base = path.basename(pluginDir);
509
- return base === 'dist' ? path.resolve(pluginDir, '..') : pluginDir;
510
- }
511
- /**
512
- * Write the success manifest. SYNCHRONOUS — the SDK freezes the plugin
513
- * registries the moment register() returns; `fs.promises.writeFile` would
514
- * race that freeze and the manifest could miss late tool registrations or
515
- * never land at all if the process exits before the microtask runs.
516
- *
517
- * Best-effort: returns `true` on success, `false` on any I/O error. Never
518
- * throws — failing to write the manifest is a diagnostic loss, not a
519
- * correctness loss, so we don't propagate.
520
- *
521
- * The manifest is written at mode 0644 (world-readable). It contains no
522
- * secrets — only a timestamp, the (publicly known) tool names, and the
523
- * plugin version. Cleared first so a stale `.error.json` from a previous
524
- * failed boot doesn't survive a successful boot.
525
- */
526
- /**
527
- * Read the existing `.loaded.json` manifest for diagnostic surfaces
528
- * (3.3.7-rc.1 — issue #216). Returns `null` if the manifest is
529
- * missing, unreadable, or malformed. Best-effort: never throws.
530
- *
531
- * Scanner note: this helper lives in fs-helpers.ts (where all fs.*
532
- * operations are consolidated) so the diagnostic slash command in
533
- * `index.ts` doesn't have to introduce a fresh `readFileSync` call —
534
- * the OpenClaw scanner whole-file rule disallows fs.read* next to the
535
- * outbound-request trigger markers that index.ts already has in its
536
- * on-chain submission code paths.
537
- */
538
- export function readPluginLoadedManifest(pluginDir) {
539
- try {
540
- const root = resolvePluginRootForManifest(pluginDir);
541
- const loadedPath = path.join(root, PLUGIN_LOADED_MANIFEST);
542
- if (!fs.existsSync(loadedPath))
543
- return null;
544
- const raw = fs.readFileSync(loadedPath, 'utf-8');
545
- const parsed = JSON.parse(raw);
546
- if (typeof parsed.loadedAt !== 'number' || !Array.isArray(parsed.tools) || typeof parsed.version !== 'string') {
547
- return null;
548
- }
549
- return parsed;
550
- }
551
- catch {
552
- return null;
553
- }
554
- }
555
- export function writePluginManifest(pluginDir, manifest) {
556
- try {
557
- const root = resolvePluginRootForManifest(pluginDir);
558
- if (!fs.existsSync(root))
559
- return false;
560
- const loadedPath = path.join(root, PLUGIN_LOADED_MANIFEST);
561
- const errorPath = path.join(root, PLUGIN_ERROR_MANIFEST);
562
- // Best-effort error-file cleanup — a successful boot supersedes any
563
- // prior failure marker. If the unlink fails (e.g. permission), the
564
- // .loaded.json timestamp still tells the agent which is current.
565
- try {
566
- if (fs.existsSync(errorPath))
567
- fs.unlinkSync(errorPath);
568
- }
569
- catch {
570
- // Swallow — best-effort.
571
- }
572
- // 3.3.7-rc.1 (issue #216) — derive bootCount by reading the prior
573
- // manifest. Lets the user grep `.loaded.json` after a container
574
- // restart to verify register() actually ran. If the prior manifest
575
- // is unreadable we start at 1.
576
- let priorBootCount = 0;
577
- try {
578
- if (fs.existsSync(loadedPath)) {
579
- const prior = JSON.parse(fs.readFileSync(loadedPath, 'utf-8'));
580
- if (typeof prior.bootCount === 'number' && Number.isFinite(prior.bootCount)) {
581
- priorBootCount = prior.bootCount;
582
- }
583
- }
584
- }
585
- catch {
586
- // Swallow — if the prior manifest is corrupt we just start the counter fresh.
587
- }
588
- const enriched = {
589
- ...manifest,
590
- bootCount: priorBootCount + 1,
591
- bootAt: new Date(manifest.loadedAt).toISOString(),
592
- pid: process.pid,
593
- };
594
- fs.writeFileSync(loadedPath, JSON.stringify(enriched, null, 2));
595
- return true;
596
- }
597
- catch {
598
- return false;
599
- }
600
- }
601
- /**
602
- * Write the error manifest. SYNCHRONOUS for the same reason as
603
- * `writePluginManifest`. Called from the try/catch surrounding the
604
- * register() body so the agent has a filesystem signal that the plugin
605
- * registered AT LEAST attempted to load and failed in a specific way.
606
- *
607
- * Does NOT clear `.loaded.json` from a prior successful boot — keeping
608
- * the older success marker around lets the agent see "last good boot was
609
- * X, current boot failed at Y" without spelunking logs. The newer
610
- * `.error.json` timestamp wins as "current state".
611
- */
612
- export function writePluginError(pluginDir, error) {
613
- try {
614
- const root = resolvePluginRootForManifest(pluginDir);
615
- if (!fs.existsSync(root))
616
- return false;
617
- const errorPath = path.join(root, PLUGIN_ERROR_MANIFEST);
618
- fs.writeFileSync(errorPath, JSON.stringify(error, null, 2));
619
- return true;
620
- }
621
- catch {
622
- return false;
623
- }
624
- }
476
+ // The 3.3.2-rc.1 `.loaded.json` / `.error.json` manifest machinery (writer
477
+ // + reader + schema) was fully removed in Phase 3.4. Phase 3.1 already
478
+ // dropped the `writePluginManifest` call from register(); 3.4 retires the
479
+ // remaining reader (`readPluginLoadedManifest`) + the error-path writer
480
+ // (`writePluginError`) + the schema types now that no production path
481
+ // references them. The gateway log is the source of truth for register()
482
+ // failures; `/totalreclaw diag` reports pid + in-memory version only.
625
483
  /**
626
484
  * Drop the `.tr-partial-install` marker into `pluginRootDir`. Idempotent
627
485
  * (overwrites any existing marker) and best-effort — returns `true` on
@@ -933,14 +791,11 @@ export function resolveOnboardingState(credentialsPath, statePath) {
933
791
  * Auto-patch `~/.openclaw/openclaw.json` with the entries required by
934
792
  * OpenClaw 2026.5.x for clean operation (issues #225 + #226 + verbosity):
935
793
  *
936
- * 1. `plugins.slots.memory = "totalreclaw"` (gated on install record)
937
- * Claim the memory slot so the plugin loads instead of deferring to
938
- * the built-in `memory-core` tenant. As of 3.3.9-rc.4 this fix is
939
- * gated on `plugins.installs.totalreclaw.version` being present
940
- * writing the slot without an install record produces a startup
941
- * crash loop ("plugins.slots.memory: plugin not found: totalreclaw")
942
- * that survives container restarts until `openclaw plugins install`
943
- * repopulates the install record.
794
+ * NOTE (rc.20, #402): this helper no longer writes `plugins.slots.memory`
795
+ * (OpenClaw 2026.6.8 claims the memory slot natively on install/enable) nor
796
+ * `plugins.installs` (the former Fix #6 self-heal its record reader
797
+ * discards the whole installs map if any entry fails schema validation).
798
+ * Pre-existing values of both are left untouched. The remaining fixes are:
944
799
  *
945
800
  * 2. `plugins.entries.totalreclaw.hooks.allowConversationAccess = true`
946
801
  * Grant the plugin access to `agent_end` and `before_agent_start`
@@ -954,15 +809,11 @@ export function resolveOnboardingState(credentialsPath, statePath) {
954
809
  * this to "off" on first run for a clean UX. Existing explicit values
955
810
  * ("partial", "block", "progress") are preserved.
956
811
  *
957
- * 4. `plugins.bundledDiscovery = "compat"` (only if unset, and only when
958
- * `plugins.allow` is populated). When `plugins.allow` is a non-empty
959
- * array, OpenClaw 2026.5.x switches the loader into strict-allowlist
960
- * mode and silently rejects non-bundled plugins like totalreclaw
961
- * EVEN IF they are listed in the allow array. Setting
962
- * `bundledDiscovery: "compat"` restores the looser behavior so allow-
963
- * listed non-bundled plugins load. Without this fix, users with
964
- * telegram or any model provider configured before TR install (which
965
- * populates allow) get a silent plugin-skip on every gateway boot.
812
+ * Fix #4 (`plugins.bundledDiscovery="compat"`) and Fix #5 (appending
813
+ * "totalreclaw" to `plugins.allow`) were RETIRED together in rc.20 (#402):
814
+ * native install manages the allowlist, and TR growing it was the only
815
+ * thing that forced strict-allowlist mode (the sole reason the compat
816
+ * workaround existed). Pre-existing values of both keys are left untouched.
966
817
  *
967
818
  * Design constraints
968
819
  * ------------------
@@ -985,10 +836,10 @@ export function resolveOnboardingState(credentialsPath, statePath) {
985
836
  * Defaults to `<home>/.openclaw/openclaw.json`.
986
837
  */
987
838
  export function patchOpenClawConfig(configPath,
988
- // 3.3.12-rc.3 plugin version (used by Fix #6 to self-heal a stripped
989
- // `plugins.installs.totalreclaw` record so Fix #1 (slot) can fire).
839
+ // Retained on the signature (register() passes it) but no longer consumed:
840
+ // the Fix #6 install-record self-heal it fed was retired in rc.20 (#402).
990
841
  pluginVersion) {
991
- const home = process.env.HOME ?? '/home/node';
842
+ const home = envHomeDir();
992
843
  const target = configPath ?? path.join(home, '.openclaw', 'openclaw.json');
993
844
  // `'skipped'` when the config file is absent — this host may not be
994
845
  // running OpenClaw, or may use a non-standard config location.
@@ -1003,109 +854,44 @@ pluginVersion) {
1003
854
  cfg.plugins = {};
1004
855
  }
1005
856
  let mutated = false;
1006
- // --- Fix #6 (3.3.12-rc.3): self-heal `plugins.installs.totalreclaw` ---
1007
- //
1008
- // OpenClaw 2026.5.6 has a config-rewrite-after-restart behaviour
1009
- // observed on Pedro's pop-os QA host (2026-05-08): `openclaw plugins
1010
- // install` writes the install record, gateway restart fires, but
1011
- // after the restart something STRIPS `plugins.installs.totalreclaw` (and
1012
- // sometimes `plugins.allow`, `plugins.entries.totalreclaw`,
1013
- // `plugins.slots.memory`) from openclaw.json. The plugin's binary
1014
- // remains in `~/.openclaw/npm/node_modules/@totalreclaw/totalreclaw/`,
1015
- // but `openclaw plugins list` shows it as `disabled` because no
1016
- // install record + no allow entry.
1017
- //
1018
- // Defensive self-heal: when this register() runs (which means the
1019
- // plugin IS physically loaded by the gateway), if the install record
1020
- // is missing or has no version, write a minimal record. This unlocks
1021
- // Fix #1 (slot) and avoids the user-visible "plugin disabled"
1022
- // condition without requiring `openclaw plugins install --force`.
857
+ // --- Fix #6 (installs self-heal) RETIRED in rc.20 (#402) ---
1023
858
  //
1024
- // Phrase-safety: writes only metadata (version, spec, source,
1025
- // installedAt). No mnemonic / userId / SA leakage.
1026
- if (pluginVersion) {
1027
- if (typeof cfg.plugins.installs !== 'object' || cfg.plugins.installs === null) {
1028
- cfg.plugins.installs = {};
1029
- }
1030
- const existing = cfg.plugins.installs.totalreclaw;
1031
- const existingVersion = (typeof existing === 'object' && existing !== null && typeof existing.version === 'string')
1032
- ? existing.version
1033
- : null;
1034
- if (!existingVersion) {
1035
- cfg.plugins.installs.totalreclaw = {
1036
- ...(typeof existing === 'object' && existing !== null ? existing : {}),
1037
- version: pluginVersion,
1038
- spec: '@totalreclaw/totalreclaw',
1039
- source: 'self-heal',
1040
- installedAt: new Date().toISOString(),
1041
- };
1042
- mutated = true;
1043
- }
1044
- }
1045
- // --- Fix #5 (3.3.12-rc.3): plugins.allow includes "totalreclaw" ---
859
+ // patchOpenClawConfig used to fabricate a `plugins.installs.totalreclaw`
860
+ // record (version/spec/source:"self-heal"/installedAt) whenever a version
861
+ // was passed and no record existed. Removed because it was inert at best
862
+ // and harmful at worst on OpenClaw 2026.6.8:
863
+ // (a) the record reader validates the WHOLE installs map against a
864
+ // schema where `source` must be a valid PluginInstallSource — the
865
+ // fabricated `source:"self-heal"` (and any source-less record) fails
866
+ // safeParse, discarding the ENTIRE installs map at load; and
867
+ // (b) the native installer never persists install records into
868
+ // openclaw.json (it strips them via withoutPluginInstallRecords and
869
+ // commits to the SQLite index, which always wins on merge).
870
+ // So the plugin no longer writes `plugins.installs` at all; a pre-existing
871
+ // record is left byte-identical. `pluginVersion` is retained on the
872
+ // signature (register() passes it) but is no longer consumed here.
873
+ void pluginVersion;
874
+ // --- Fix #5 (plugins.allow self-append) RETIRED in rc.20 (#402) ---
1046
875
  //
1047
- // OpenClaw 2026.5.x: when `plugins.allow` is a non-empty array, the
1048
- // gateway switches into strict-allowlist mode. Plugins NOT in the
1049
- // allow list are silently rejected at load time — even bundled ones
1050
- // are gated. Pedro's pop-os 2026-05-08 QA had `plugins.allow` =
1051
- // ['device-pair', 'google', 'telegram', 'zai'] AFTER `openclaw
1052
- // plugins install @totalreclaw/totalreclaw@rc` ran. The install
1053
- // command did NOT add 'totalreclaw' to the allow list. Plugin
1054
- // shipped as `disabled`. Setup never proceeded.
876
+ // patchOpenClawConfig used to append "totalreclaw" to a non-empty
877
+ // `plugins.allow`. Removed as a pair with Fix #4 (below): OpenClaw's native
878
+ // `persistPluginInstall` already calls `addInstalledPluginToAllowlist`, and
879
+ // an empty/absent allowlist means allow-all (no write needed). TR
880
+ // unconditionally growing an allowlist was the very thing that flipped
881
+ // hosts into strict-allowlist enforcement which then required the Fix #4
882
+ // `bundledDiscovery:"compat"` workaround. Dropping both lets native install
883
+ // own the allowlist; existing hosts keep whatever is already on disk.
884
+ // --- Fix #1 (memory-slot write) RETIRED in rc.20 (#402) ---
1055
885
  //
1056
- // Defensive: when allow is a non-empty array and 'totalreclaw' is
1057
- // not in it, append. Don't touch null/undefined allow (means
1058
- // auto-discover mode plugin is reachable without explicit allow).
1059
- if (Array.isArray(cfg.plugins.allow) && cfg.plugins.allow.length > 0) {
1060
- if (!cfg.plugins.allow.includes('totalreclaw')) {
1061
- cfg.plugins.allow.push('totalreclaw');
1062
- mutated = true;
1063
- }
1064
- }
1065
- // --- Fix #1: plugins.slots.memory = "totalreclaw" (gated on install) ---
1066
- //
1067
- // DEFENSIVE GATE (3.3.9-rc.4 — 2026-05-05): only write the slot when
1068
- // the plugin is genuinely INSTALLED (`plugins.installs.totalreclaw`
1069
- // present with a `version`). Writing the slot unconditionally
1070
- // produced a startup crash loop on Pedro's pop-os QA host on
1071
- // 2026-05-05 — after a config reset, `plugins.installs.totalreclaw`
1072
- // was missing but a previously-written `slots.memory = "totalreclaw"`
1073
- // had survived. OpenClaw's startup validator refuses to start with
1074
- //
1075
- // Gateway failed to start: Error: Invalid config at openclaw.json.
1076
- // plugins.slots.memory: plugin not found: totalreclaw
1077
- // Run "openclaw doctor --fix" to repair, then retry.
1078
- //
1079
- // The container restart-loop drained ~13 attempts (12:10-12:23 UTC)
1080
- // until `openclaw plugins install` was re-run and re-populated
1081
- // `plugins.installs.totalreclaw`. With this gate, future installs
1082
- // that wipe `plugins.installs` (config reset, `doctor --fix`,
1083
- // migration tools) cannot regress into the same boot loop — slot is
1084
- // only ever written when the install record exists, and the install
1085
- // record is the install pipeline's authoritative signal that the
1086
- // plugin is on disk and registered with the gateway.
1087
- //
1088
- // The hooks patch (Fix #2) and Telegram streaming patch (Fix #3) are
1089
- // not gated this way — they write under `plugins.entries` and
1090
- // `channels` which are inert without an install record, so they can
1091
- // never trip the validator.
1092
- const installsRoot = cfg.plugins.installs;
1093
- const installEntry = typeof installsRoot === 'object' && installsRoot !== null
1094
- ? installsRoot.totalreclaw
1095
- : undefined;
1096
- const pluginIsInstalled = typeof installEntry === 'object'
1097
- && installEntry !== null
1098
- && typeof installEntry.version === 'string'
1099
- && installEntry.version.length > 0;
1100
- if (pluginIsInstalled) {
1101
- if (typeof cfg.plugins.slots !== 'object' || cfg.plugins.slots === null) {
1102
- cfg.plugins.slots = {};
1103
- }
1104
- if (cfg.plugins.slots.memory !== 'totalreclaw') {
1105
- cfg.plugins.slots.memory = 'totalreclaw';
1106
- mutated = true;
1107
- }
1108
- }
886
+ // patchOpenClawConfig used to write `plugins.slots.memory = "totalreclaw"`
887
+ // (install-gated, to dodge a startup crash loop). OpenClaw 2026.6.8 now
888
+ // claims the memory slot NATIVELY during `plugins install`/`enable` (its
889
+ // persistPluginInstall "slot selection" phase), so the hand-written write
890
+ // was redundant on the good path and twice failed in production. The
891
+ // plugin no longer touches the memory slot — a pre-existing value is left
892
+ // byte-identical. Fix numbering below is preserved for continuity with the
893
+ // #225/#226 history. The Fix #6 install-record self-heal was retired
894
+ // above too (rc.20, #402).
1109
895
  // --- Fix #2: plugins.entries.totalreclaw.hooks.allowConversationAccess = true ---
1110
896
  if (typeof cfg.plugins.entries !== 'object' || cfg.plugins.entries === null) {
1111
897
  cfg.plugins.entries = {};
@@ -1148,36 +934,15 @@ pluginVersion) {
1148
934
  }
1149
935
  }
1150
936
  }
1151
- // --- Fix #4: plugins.bundledDiscovery = "compat" (3.3.11-rc.4) ---
1152
- //
1153
- // OpenClaw 2026.5.x: when `plugins.allow` is populated (any non-empty
1154
- // array), the gateway's plugin loader switches into strict-allowlist
1155
- // mode. In strict mode, NON-BUNDLED plugins like totalreclaw are
1156
- // silently rejected even when listed in `plugins.allow`, unless
1157
- // `plugins.bundledDiscovery = "compat"` is explicitly set. Pedro's
1158
- // 2026-05-07 QA on pop-os surfaced this — the gateway booted with only
1159
- // the bundled providers (telegram, device-pair) and skipped totalreclaw
1160
- // despite it being in the allow list. `openclaw doctor --fix` cures it
1161
- // by setting `bundledDiscovery: "compat"`, but users shouldn't need
1162
- // to run doctor manually for the plugin to load.
937
+ // --- Fix #4 (plugins.bundledDiscovery="compat") RETIRED in rc.20 (#402) ---
1163
938
  //
1164
- // Fix: when `plugins.allow` is a non-empty array AND
1165
- // `plugins.bundledDiscovery` is unset, set it to "compat". If the user
1166
- // explicitly chose "allowlist" (the stricter mode), preserve their
1167
- // choice only first-run defaults are touched.
1168
- //
1169
- // This bug was missed by the auto-QA harness because the harness ran
1170
- // on a fresh canonical container with `plugins.allow=null`, hitting
1171
- // the auto-discover code path. Real users with telegram + a model
1172
- // provider configured before TR install have a populated allow list,
1173
- // hitting the strict-mode path. The auto-QA harness in 3.3.11-rc.4
1174
- // adds a populated-allow scenario to catch future regressions.
1175
- if (Array.isArray(cfg.plugins.allow) && cfg.plugins.allow.length > 0) {
1176
- if (cfg.plugins.bundledDiscovery === undefined) {
1177
- cfg.plugins.bundledDiscovery = 'compat';
1178
- mutated = true;
1179
- }
1180
- }
939
+ // patchOpenClawConfig used to set `plugins.bundledDiscovery = "compat"`
940
+ // whenever `plugins.allow` was populated, to keep non-bundled totalreclaw
941
+ // loadable under strict-allowlist mode. Retired as a pair with Fix #5: the
942
+ // ONLY reason a host was in strict-allowlist mode was that TR itself grew
943
+ // the allowlist (Fix #5). With Fix #5 gone, native install manages the
944
+ // allowlist and this compat workaround is unnecessary. An existing
945
+ // `bundledDiscovery` value on disk is left untouched.
1181
946
  if (!mutated)
1182
947
  return 'unchanged';
1183
948
  // Write back with 2-space indent to match OpenClaw's own write style.
@@ -1227,81 +992,12 @@ export function checkCredentialsFileMode(credPath, logger) {
1227
992
  }
1228
993
  return 'ok';
1229
994
  }
1230
- /**
1231
- * Load `.pair-pending.json` if it exists and parses. Returns null on
1232
- * missing-file, corrupt JSON, or unknown schema version. Never throws.
1233
- */
1234
- export function loadPairPendingFile(pendingPath) {
1235
- try {
1236
- if (!fs.existsSync(pendingPath))
1237
- return null;
1238
- const raw = fs.readFileSync(pendingPath, 'utf-8');
1239
- const parsed = JSON.parse(raw);
1240
- if (parsed.v !== 1)
1241
- return null;
1242
- if (typeof parsed.url !== 'string' || !parsed.url)
1243
- return null;
1244
- if (typeof parsed.pin !== 'string' || !parsed.pin)
1245
- return null;
1246
- if (typeof parsed.sid !== 'string' || !parsed.sid)
1247
- return null;
1248
- if (typeof parsed.expires_at_ms !== 'number')
1249
- return null;
1250
- if (typeof parsed.created_at_ms !== 'number')
1251
- return null;
1252
- if (parsed.mode !== 'generate'
1253
- && parsed.mode !== 'import'
1254
- && parsed.mode !== 'either')
1255
- return null;
1256
- return parsed;
1257
- }
1258
- catch {
1259
- return null;
1260
- }
1261
- }
1262
- /**
1263
- * Write `.pair-pending.json` atomically-ish (single `writeFileSync`). Creates
1264
- * the parent directory if missing. Uses mode `0o600` to match credentials.json
1265
- * even though the contents are NOT secret (URL + PIN are surfaced to chat
1266
- * anyway) — defensive default for the .totalreclaw/ dir.
1267
- *
1268
- * Returns `true` on success, `false` on any I/O error.
1269
- */
1270
- export function writePairPendingFile(pendingPath, payload) {
1271
- try {
1272
- const dir = path.dirname(pendingPath);
1273
- if (!fs.existsSync(dir))
1274
- fs.mkdirSync(dir, { recursive: true });
1275
- fs.writeFileSync(pendingPath, JSON.stringify(payload), { mode: 0o600 });
1276
- return true;
1277
- }
1278
- catch {
1279
- return false;
1280
- }
1281
- }
1282
- /**
1283
- * Delete the pair-pending sentinel. Used by:
1284
- * - the pair-completion path (after credentials.json is finalized)
1285
- * - the auto-pair flow when it detects an expired pending session
1286
- *
1287
- * Returns `true` if a file was deleted, `false` otherwise (no file or error).
1288
- */
1289
- export function deletePairPendingFile(pendingPath) {
1290
- try {
1291
- if (!fs.existsSync(pendingPath))
1292
- return false;
1293
- fs.unlinkSync(pendingPath);
1294
- return true;
1295
- }
1296
- catch {
1297
- return false;
1298
- }
1299
- }
1300
- /**
1301
- * Default `.pair-pending.json` path derived from a credentials.json path:
1302
- * the sentinel lives in the same directory under the dot-prefixed name.
1303
- * Callers MAY pass an explicit path (tests, custom layouts) instead.
1304
- */
1305
- export function defaultPairPendingPath(credentialsPath) {
1306
- return path.join(path.dirname(credentialsPath), '.pair-pending.json');
1307
- }
995
+ // ---------------------------------------------------------------------------
996
+ // Pair-pending sentinel RETIRED Phase 3.4
997
+ // ---------------------------------------------------------------------------
998
+ // The 3.3.13 `~/.totalreclaw/.pair-pending.json` sentinel + its utilities
999
+ // (loadPairPendingFile / writePairPendingFile / deletePairPendingFile /
1000
+ // defaultPairPendingPath / PairPendingFile) were removed in Phase 3.4.
1001
+ // Pairing is now user-initiated QR (`tr pair` CLI / the pair HTTP route)
1002
+ // per the native-integration design — the auto-pair-on-load state machine
1003
+ // that wrote + consumed this sentinel is gone.