@inetafrica/open-claudia 2.15.0 → 3.0.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.
Files changed (151) hide show
  1. package/.env.example +30 -4
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +87 -66
  4. package/bin/agent.js +44 -18
  5. package/bin/cli.js +30 -28
  6. package/bin/dream.js +5 -11
  7. package/bin/loopback-client.js +29 -5
  8. package/bin/schedule.js +19 -5
  9. package/bin/tool.js +23 -5
  10. package/bot-agent.js +5 -2350
  11. package/bot.js +66 -3
  12. package/channels/telegram/adapter.js +6 -1
  13. package/channels/voice/adapter.js +1 -0
  14. package/channels/voice/manage.js +43 -5
  15. package/config-dir.js +3 -11
  16. package/core/actions.js +149 -40
  17. package/core/approvals.js +136 -29
  18. package/core/auth-flow.js +103 -19
  19. package/core/config-dir.js +19 -0
  20. package/core/config.js +115 -69
  21. package/core/doctor.js +111 -57
  22. package/core/dream.js +219 -62
  23. package/core/enforcer.js +0 -0
  24. package/core/entities.js +2 -1
  25. package/core/fsutil.js +21 -4
  26. package/core/handlers.js +503 -217
  27. package/core/ideas.js +2 -1
  28. package/core/jobs.js +589 -72
  29. package/core/lessons.js +3 -2
  30. package/core/loopback.js +42 -6
  31. package/core/memory-mutation-queue.js +241 -0
  32. package/core/migration-backup.js +1060 -0
  33. package/core/pack-review.js +295 -88
  34. package/core/packs.js +4 -3
  35. package/core/persona.js +2 -1
  36. package/core/process-tree.js +19 -2
  37. package/core/provider-migration.js +39 -0
  38. package/core/provider-status.js +80 -0
  39. package/core/providers/claude-events.js +117 -0
  40. package/core/providers/claude-hook.js +114 -0
  41. package/core/providers/claude.js +296 -0
  42. package/core/providers/codex-events.js +121 -0
  43. package/core/providers/codex-hook.js +280 -0
  44. package/core/providers/codex.js +286 -0
  45. package/core/providers/contract.js +153 -0
  46. package/core/providers/env.js +148 -0
  47. package/core/providers/events.js +170 -0
  48. package/core/providers/hook-command.js +22 -0
  49. package/core/providers/index.js +240 -0
  50. package/core/providers/utility-policy.js +269 -0
  51. package/core/recall/classic.js +2 -2
  52. package/core/recall/discoverer.js +71 -22
  53. package/core/recall/metrics.js +27 -1
  54. package/core/recall/tuning.js +4 -1
  55. package/core/recall/warm-walker.js +151 -108
  56. package/core/recall-filter.js +86 -61
  57. package/core/router.js +79 -7
  58. package/core/run-context.js +185 -0
  59. package/core/runner.js +1414 -1290
  60. package/core/scheduler.js +515 -210
  61. package/core/side-chat.js +346 -0
  62. package/core/state.js +1084 -97
  63. package/core/subagent.js +72 -98
  64. package/core/system-prompt.js +462 -279
  65. package/core/tool-guard.js +73 -39
  66. package/core/tools.js +22 -5
  67. package/core/transcripts.js +30 -1
  68. package/core/usage-log.js +19 -4
  69. package/core/utility-agent.js +654 -0
  70. package/core/web-auth.js +29 -13
  71. package/docs/CHANNEL_DESIGN.md +181 -0
  72. package/docs/MULTI_CHANNEL_PLAN.md +254 -0
  73. package/docs/PROVIDER_MIGRATION.md +67 -0
  74. package/health.js +50 -39
  75. package/package.json +48 -4
  76. package/setup.js +198 -38
  77. package/soul.md +39 -0
  78. package/test-ability-extraction.js +5 -0
  79. package/test-approval-async.js +63 -4
  80. package/test-cursor-removal.js +334 -0
  81. package/test-enforcer.js +70 -27
  82. package/test-fixtures/current-provider-parity.json +132 -0
  83. package/test-fixtures/fake-agent-cli.js +477 -0
  84. package/test-fixtures/migrations/claude-only/jobs.json +3 -0
  85. package/test-fixtures/migrations/claude-only/sessions.json +5 -0
  86. package/test-fixtures/migrations/claude-only/state.json +5 -0
  87. package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
  88. package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
  89. package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
  90. package/test-fixtures/migrations/cursor-selected/state.json +6 -0
  91. package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
  92. package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
  93. package/test-fixtures/migrations/dual-provider/state.json +10 -0
  94. package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
  95. package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
  96. package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
  97. package/test-fixtures/migrations/malformed-partial/state.json +1 -0
  98. package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
  99. package/test-fixtures/migrations/missing-project/jobs.json +3 -0
  100. package/test-fixtures/migrations/missing-project/sessions.json +3 -0
  101. package/test-fixtures/migrations/missing-project/state.json +4 -0
  102. package/test-fixtures/migrations/multi-user/jobs.json +4 -0
  103. package/test-fixtures/migrations/multi-user/sessions.json +4 -0
  104. package/test-fixtures/migrations/multi-user/state.json +6 -0
  105. package/test-fixtures/provider-event-samples.json +17 -0
  106. package/test-learning-e2e.js +5 -0
  107. package/test-memory-mutation-queue.js +191 -0
  108. package/test-provider-boot-matrix.js +399 -0
  109. package/test-provider-bootstrap.js +158 -0
  110. package/test-provider-capabilities.js +232 -0
  111. package/test-provider-claude.js +206 -0
  112. package/test-provider-codex.js +228 -0
  113. package/test-provider-compaction.js +371 -0
  114. package/test-provider-core-prompt.js +264 -0
  115. package/test-provider-coupling-audit.js +90 -0
  116. package/test-provider-dream.js +312 -0
  117. package/test-provider-enforcer.js +252 -0
  118. package/test-provider-env-isolation.js +150 -0
  119. package/test-provider-events.js +141 -0
  120. package/test-provider-fixture.js +332 -0
  121. package/test-provider-gateway.js +508 -0
  122. package/test-provider-language.js +89 -0
  123. package/test-provider-migration-backup.js +424 -0
  124. package/test-provider-pack-review.js +436 -0
  125. package/test-provider-parity-e2e.js +537 -0
  126. package/test-provider-prompt-parity.js +89 -0
  127. package/test-provider-recall.js +271 -0
  128. package/test-provider-registry.js +251 -0
  129. package/test-provider-resume-parity.js +41 -0
  130. package/test-provider-run-lifecycle.js +349 -0
  131. package/test-provider-runner.js +271 -0
  132. package/test-provider-scheduler.js +689 -0
  133. package/test-provider-session-commands.js +185 -0
  134. package/test-provider-session-history.js +205 -0
  135. package/test-provider-side-chat.js +337 -0
  136. package/test-provider-state-migration.js +316 -0
  137. package/test-provider-stream-decoder.js +69 -0
  138. package/test-provider-subagent.js +228 -0
  139. package/test-provider-tool-hooks.js +360 -0
  140. package/test-recall-discoverer.js +1 -0
  141. package/test-recall-engine.js +3 -3
  142. package/test-recall-evolution.js +18 -0
  143. package/test-runner-watchdog-static.js +16 -8
  144. package/test-single-runtime.js +35 -0
  145. package/test-telegram-poll-recovery.js +56 -0
  146. package/test-tool-guard.js +56 -0
  147. package/test-tools.js +19 -0
  148. package/test-unified-identity.js +3 -1
  149. package/test-usage-accounting.js +92 -20
  150. package/test-utility-provider-policy.js +486 -0
  151. package/web.js +130 -35
package/core/dream.js CHANGED
@@ -12,9 +12,11 @@
12
12
  const fs = require("fs");
13
13
  const path = require("path");
14
14
  const cron = require("node-cron");
15
+ const { atomicWriteFileSync } = require("./fsutil");
16
+ const { memoryMutationQueue } = require("./memory-mutation-queue");
15
17
 
16
18
  const CONFIG_DIR = require("../config-dir");
17
- const { config, DEFAULT_CLAUDE_MODEL } = require("./config");
19
+ const { config } = require("./config");
18
20
  const packs = require("./packs");
19
21
  const entities = require("./entities");
20
22
  const lessons = require("./lessons");
@@ -22,27 +24,49 @@ const ideas = require("./ideas");
22
24
  const daySeeds = require("./day-seeds");
23
25
  const packGuard = require("./pack-guard");
24
26
  const persona = require("./persona");
25
- const { spawnSubagent } = require("./subagent");
27
+ const { spawnUtilityAgent } = require("./utility-agent");
26
28
  const recallMetrics = require("./recall/metrics");
27
29
  const recallTuning = require("./recall/tuning");
28
30
  const recallGraphLib = require("./recall/graph");
29
31
 
30
- // Graph surgery + threshold tuning is the highest-judgment work in the recall
31
- // system, so the dream runs on the best available high tier. Honour an explicit
32
- // DREAM_MODEL override; otherwise prefer opus (strongest reliably-available
33
- // Claude tier) with sonnet as graceful fallback. Provider-specific high tiers
34
- // (e.g. a connected Codex 5.5) can extend pickDreamModel later.
35
- function pickDreamModel() {
36
- if (process.env.DREAM_MODEL) return process.env.DREAM_MODEL;
37
- const tier = String(process.env.DREAM_TIER || "high").toLowerCase();
38
- if (tier === "low") return "haiku";
39
- if (tier === "medium") return "sonnet";
40
- return DEFAULT_CLAUDE_MODEL; // high → the bot's top model (opus 4.8); tracks future upgrades
32
+ // Graph surgery + threshold tuning is the highest-judgment recall work, so the
33
+ // utility policy chooses a provider-owned model tier. The old DREAM_MODEL key
34
+ // remains a Claude-only compatibility override; provider-specific pins are
35
+ // DREAM_MODEL_CLAUDE and DREAM_MODEL_CODEX.
36
+ function dreamTier() {
37
+ const tier = String(process.env.DREAM_TIER || "high").trim().toLowerCase();
38
+ return new Set(["low", "medium", "high"]).has(tier) ? tier : "high";
41
39
  }
42
- const DREAM_MODEL = pickDreamModel();
43
40
  // The dream is the foundation of long-term memory quality, so it runs at max
44
41
  // reasoning effort by default. Overridable via DREAM_EFFORT (low|medium|high|xhigh|max).
45
42
  const DREAM_EFFORT = process.env.DREAM_EFFORT || "max";
43
+ const DREAM_OUTPUT_SCHEMA = Object.freeze({
44
+ type: "object",
45
+ properties: {
46
+ merges: { type: "array", items: { type: "object" } },
47
+ umbrellas: { type: "array", items: { type: "object" } },
48
+ parents: { type: "array", items: { type: "object" } },
49
+ retag: { type: "array", items: { type: "object" } },
50
+ entity_merges: { type: "array", items: { type: "object" } },
51
+ entity_notes: { type: "array", items: { type: "object" } },
52
+ archive: { type: "array", items: { type: "object" } },
53
+ lessons: { type: "array", items: { type: "object" } },
54
+ persona: {},
55
+ tuning: {},
56
+ report: { type: "string" },
57
+ },
58
+ });
59
+ const INTROSPECTION_OUTPUT_SCHEMA = Object.freeze({
60
+ type: "object",
61
+ properties: {
62
+ lessons_add: { type: "array", items: { type: "object" } },
63
+ doc_edits: { type: "array", items: { type: "object" } },
64
+ entity_edits: { type: "array", items: { type: "object" } },
65
+ ideas: { type: "array", items: { type: "object" } },
66
+ persona: {},
67
+ report: { type: "string" },
68
+ },
69
+ });
46
70
  const DREAM_CRON = process.env.DREAM_CRON || "0 4 * * *";
47
71
  // Consolidation model timeout. The model weighs the WHOLE corpus in one pass,
48
72
  // so a fixed budget that was fine at 30 packs starves at 130 (the timeout that
@@ -74,8 +98,6 @@ const PROMOTE_MIN_PROJECTS = Number(process.env.DREAM_PROMOTE_MIN_PROJECTS || 3)
74
98
  const ARCHIVE_IDLE_DAYS = Number(process.env.DREAM_ARCHIVE_IDLE_DAYS || 30);
75
99
  const ARCHIVE_MAX_USAGE = Number(process.env.DREAM_ARCHIVE_MAX_USAGE || 3);
76
100
 
77
- let _dreaming = false;
78
-
79
101
  function enabled() {
80
102
  return (process.env.DREAM || "on").toLowerCase() !== "off";
81
103
  }
@@ -119,7 +141,7 @@ function readDreamState() {
119
141
  try { return JSON.parse(fs.readFileSync(DREAM_STATE_FILE, "utf8")) || {}; } catch (e) { return {}; }
120
142
  }
121
143
  function writeDreamState(s) {
122
- try { fs.writeFileSync(DREAM_STATE_FILE, JSON.stringify(s, null, 2)); } catch (e) {}
144
+ try { atomicWriteFileSync(DREAM_STATE_FILE, JSON.stringify(s, null, 2), { mode: 0o600, backup: true }); } catch (e) {}
123
145
  }
124
146
 
125
147
  // Windowed metric deltas between two summary snapshots (counters only grow).
@@ -660,8 +682,8 @@ function applyDream(decision, backupRoot) {
660
682
  // A SECOND dream pass that runs with read-only tools. It reviews the day's
661
683
  // compaction seeds (what was actually worked on), reflects on its current self
662
684
  // with that self injected, may read its own code to understand how it works
663
- // (read-only it has no tools to edit code or soul.md, enforced by the
664
- // --allowedTools whitelist in subagent.js), then proposes improvements to its
685
+ // (provider-native read-only mode prevents edits to code or soul.md), then
686
+ // proposes improvements to its
665
687
  // LEARNED MEMORY only: lessons promoted from missed corrections, sharpened
666
688
  // pack/entity notes, and self-improvement ideas. All writes happen here in
667
689
  // Node, bounded to memory docs, guard-scanned, and backed up first.
@@ -851,9 +873,15 @@ function writeDreamReport(data) {
851
873
  fs.mkdirSync(DREAMS_DIR, { recursive: true, mode: 0o700 });
852
874
  const stamp = new Date().toISOString();
853
875
  const file = path.join(DREAMS_DIR, `${stamp.slice(0, 10)}.md`);
876
+ const consolidationIntelligence = data.intelligence || {};
877
+ const introspectionIntelligence = data.introspectionIntelligence || {};
878
+ const provider = consolidationIntelligence.provider || data.provider || "unknown";
879
+ const model = consolidationIntelligence.model || data.model || "unknown";
880
+ const effort = consolidationIntelligence.effort || data.effort || "provider default";
854
881
  const sec = [
855
882
  `# Dream — ${stamp}`,
856
- `Model: ${data.model} · effort: ${data.effort} · trigger: ${data.trigger || "?"}`,
883
+ `Provider: ${provider} · Model: ${model} · effort: ${effort} · trigger: ${data.trigger || "?"}`,
884
+ `Introspection: ${introspectionIntelligence.provider || "not run"} · Model: ${introspectionIntelligence.model || "not run"} · status: ${introspectionIntelligence.status || "unknown"}`,
857
885
  "",
858
886
  "## Consolidation",
859
887
  data.consolidation.report || "(no report)",
@@ -873,7 +901,7 @@ function writeDreamReport(data) {
873
901
  const block = sec.join("\n") + "\n\n---\n\n";
874
902
  let existing = "";
875
903
  try { existing = fs.readFileSync(file, "utf-8"); } catch (e) {}
876
- fs.writeFileSync(file, existing + block, { mode: 0o600 });
904
+ atomicWriteFileSync(file, existing + block, { mode: 0o600, backup: true });
877
905
  return file;
878
906
  } catch (e) { return null; }
879
907
  }
@@ -990,16 +1018,66 @@ function negativeHebbianPhase(ev, { cap = 12 } = {}) {
990
1018
  return { lines, nodes, weakened, pruned };
991
1019
  }
992
1020
 
993
- async function runDream({ trigger = "manual" } = {}) {
994
- if (!enabled()) return { skipped: "dream is disabled (DREAM=off)" };
995
- if (_dreaming) return { skipped: "a dream is already in progress" };
1021
+ function dreamUtilityOptions(purpose, { provider = null, timeoutMs, outputSchema, systemPrompt, cwd = process.cwd() } = {}) {
1022
+ return {
1023
+ purpose,
1024
+ provider,
1025
+ tier: dreamTier(),
1026
+ modelEnvPrefix: "DREAM_MODEL",
1027
+ legacyClaudeModelEnv: "DREAM_MODEL",
1028
+ readOnly: true,
1029
+ outputSchema,
1030
+ effort: DREAM_EFFORT,
1031
+ timeoutMs,
1032
+ cwd,
1033
+ systemPrompt,
1034
+ };
1035
+ }
1036
+
1037
+ function utilityPayload(result) {
1038
+ return result?.json ? JSON.stringify(result.json) : String(result?.text || "");
1039
+ }
1040
+
1041
+ function intelligenceMetadata(result, status = "succeeded") {
1042
+ return {
1043
+ provider: result?.provider || null,
1044
+ model: result?.model || null,
1045
+ effort: result?.effort || null,
1046
+ fallbackUsed: !!result?.fallbackUsed,
1047
+ status,
1048
+ };
1049
+ }
1050
+
1051
+ function failedIntelligenceMetadata(error) {
1052
+ const result = error?.result || null;
1053
+ return {
1054
+ ...intelligenceMetadata(result, "failed"),
1055
+ code: String(error?.code || result?.error?.code || "DREAM_UTILITY_FAILED").slice(0, 80),
1056
+ };
1057
+ }
1058
+
1059
+ async function previewDreamWith(runUtility, { provider = null } = {}) {
1060
+ const result = await runUtility(buildDreamPrompt(), dreamUtilityOptions("dream", {
1061
+ provider,
1062
+ timeoutMs: 8 * 60 * 1000,
1063
+ outputSchema: DREAM_OUTPUT_SCHEMA,
1064
+ systemPrompt: "You are a background memory consolidation process. Reply with ONLY the requested JSON object. No prose, no markdown, no tool use.",
1065
+ }));
1066
+ const decision = parseDream(utilityPayload(result));
1067
+ if (!decision) {
1068
+ const error = new Error("Dream utility returned unreadable output");
1069
+ error.code = "DREAM_OUTPUT_INVALID";
1070
+ throw error;
1071
+ }
1072
+ return { decision, ...intelligenceMetadata(result) };
1073
+ }
1074
+
1075
+ async function runDreamMutation({ trigger = "manual", provider = null, runUtility = spawnUtilityAgent } = {}) {
996
1076
  const packCount = packs.listPacks().length;
997
1077
  const entityCount = entities.listEntities().length;
998
1078
  if (packCount === 0 && entityCount === 0) return { skipped: "no memory to consolidate yet" };
999
1079
 
1000
- _dreaming = true;
1001
- try {
1002
- const nowISO = new Date().toISOString();
1080
+ const nowISO = new Date().toISOString();
1003
1081
  const dreamState = readDreamState();
1004
1082
  const fullSweep = !dreamState.lastFullSweep ||
1005
1083
  (Date.now() - Date.parse(dreamState.lastFullSweep)) / 86400000 >= FULL_SWEEP_DAYS;
@@ -1050,16 +1128,25 @@ async function runDream({ trigger = "manual" } = {}) {
1050
1128
  // decision (applyDream no-ops), note it for the report, and carry on.
1051
1129
  let decision = null;
1052
1130
  let modelFailNote = "";
1131
+ let consolidationIntelligence = intelligenceMetadata(null, "not_run");
1053
1132
  try {
1054
- const { text } = await spawnSubagent(buildDreamPrompt({ sinceTs: dreamState.lastRun || null, fullSweep, clusters, evidence: ev, windowDelta }), {
1055
- model: DREAM_MODEL,
1056
- effort: DREAM_EFFORT,
1057
- timeoutMs: dreamTimeoutMs(packCount),
1058
- systemPrompt: "You are a background memory consolidation process. Reply with ONLY the requested JSON object. No prose, no markdown, no tool use.",
1059
- });
1060
- decision = parseDream(text);
1061
- if (!decision) modelFailNote = "the AI consolidation step returned unreadable output";
1133
+ const dreamRun = await runUtility(
1134
+ buildDreamPrompt({ sinceTs: dreamState.lastRun || null, fullSweep, clusters, evidence: ev, windowDelta }),
1135
+ dreamUtilityOptions("dream", {
1136
+ provider,
1137
+ timeoutMs: dreamTimeoutMs(packCount),
1138
+ outputSchema: DREAM_OUTPUT_SCHEMA,
1139
+ systemPrompt: "You are a background memory consolidation process. Reply with ONLY the requested JSON object. No prose, no markdown, no tool use.",
1140
+ }),
1141
+ );
1142
+ consolidationIntelligence = intelligenceMetadata(dreamRun);
1143
+ decision = parseDream(utilityPayload(dreamRun));
1144
+ if (!decision) {
1145
+ consolidationIntelligence.status = "invalid_output";
1146
+ modelFailNote = "the AI consolidation step returned unreadable output";
1147
+ }
1062
1148
  } catch (e) {
1149
+ consolidationIntelligence = failedIntelligenceMetadata(e);
1063
1150
  modelFailNote = `the AI consolidation step didn't finish (${e.message})`;
1064
1151
  }
1065
1152
  if (!decision) decision = { report: "" };
@@ -1102,37 +1189,36 @@ async function runDream({ trigger = "manual" } = {}) {
1102
1189
  }
1103
1190
 
1104
1191
  // Phase 2: self-improvement introspection. Reviews the day's seeds, reads
1105
- // its own code/memory READ-ONLY (allowedTools whitelist), promotes lessons
1192
+ // its own code/memory through provider-native read-only mode, promotes lessons
1106
1193
  // from missed corrections, refreshes docs, captures ideas. Bounded to memory
1107
1194
  // docs; never code or soul. Applied unless DREAM_SELF_APPLY=off, in which
1108
1195
  // case the proposal is reported for morning approval instead.
1109
1196
  let introReport = "";
1110
1197
  let introApplied = [];
1111
1198
  let introProposed = null;
1199
+ let introspectionIntelligence = intelligenceMetadata(null, introspectEnabled() ? "not_run" : "disabled");
1112
1200
  if (introspectEnabled()) {
1113
1201
  try {
1114
- const introRun = await spawnSubagent(buildIntrospectionPrompt(), {
1115
- model: DREAM_MODEL,
1116
- effort: DREAM_EFFORT,
1202
+ const introRun = await runUtility(buildIntrospectionPrompt(), dreamUtilityOptions("introspection", {
1203
+ provider,
1117
1204
  cwd: CODE_ROOT,
1118
1205
  timeoutMs: 12 * 60 * 1000,
1119
- // Plan mode is the load-bearing read-only guarantee. Verified: a tool
1120
- // whitelist alone does NOT restrict, because the bot's default
1121
- // --dangerously-skip-permissions overrides --allowedTools. Plan mode
1122
- // (without skip-permissions) is the only setting that actually blocks
1123
- // writes/Bash while still allowing Read/Glob/Grep — so introspection
1124
- // can read its own code + memory but cannot edit code or soul.
1125
- permissionMode: "plan",
1126
- allowedTools: ["Read", "Glob", "Grep"],
1127
- systemPrompt: "You are Open Claudia's nightly self-introspection. You may READ files (your code, memory, soul) to understand yourself, but you have no tools to write or run anything. After investigating, reply with ONLY the requested JSON object — no prose, no code fences.",
1128
- });
1129
- const intro = parseIntrospection(introRun.text);
1206
+ outputSchema: INTROSPECTION_OUTPUT_SCHEMA,
1207
+ systemPrompt: "You are Open Claudia's nightly self-introspection. You may READ files (your code, memory, soul) to understand yourself, but read-only mode forbids product side effects. After investigating, reply with ONLY the requested JSON object — no prose, no code fences.",
1208
+ }));
1209
+ introspectionIntelligence = intelligenceMetadata(introRun);
1210
+ const intro = parseIntrospection(utilityPayload(introRun));
1130
1211
  if (intro) {
1131
1212
  introReport = intro.report || "";
1132
1213
  if (selfApplyEnabled()) introApplied = applyIntrospection(intro, backupRoot, { personaAlreadyChanged: !!decision.persona });
1133
1214
  else introProposed = intro;
1215
+ } else {
1216
+ introspectionIntelligence.status = "invalid_output";
1134
1217
  }
1135
- } catch (e) { console.warn(`[dream] introspection phase failed: ${e.message}`); }
1218
+ } catch (e) {
1219
+ introspectionIntelligence = failedIntelligenceMetadata(e);
1220
+ console.warn(`[dream] introspection phase failed: ${e.message}`);
1221
+ }
1136
1222
  }
1137
1223
 
1138
1224
  // Recall-graph maintenance: refresh structural edges, decay reinforced
@@ -1271,10 +1357,15 @@ async function runDream({ trigger = "manual" } = {}) {
1271
1357
  try {
1272
1358
  recallMetrics.appendKpi({
1273
1359
  trigger, fullSweep,
1360
+ providers: {
1361
+ dream: consolidationIntelligence.provider,
1362
+ introspection: introspectionIntelligence.provider,
1363
+ },
1274
1364
  models: {
1275
- walker: process.env.RECALL_DISCOVERER_MODEL || "haiku",
1276
- reviewer: process.env.PACK_REVIEW_MODEL || "claude-sonnet-4-6",
1277
- dream: DREAM_MODEL,
1365
+ walker: process.env.RECALL_DISCOVERER_MODEL || "provider tier:low",
1366
+ reviewer: process.env.PACK_REVIEW_MODEL || "provider tier:medium",
1367
+ dream: consolidationIntelligence.model,
1368
+ introspection: introspectionIntelligence.model,
1278
1369
  },
1279
1370
  window: windowDelta || null,
1280
1371
  health: windowDelta ? healthOf(windowDelta) : null,
@@ -1287,11 +1378,22 @@ async function runDream({ trigger = "manual" } = {}) {
1287
1378
  dreamState.lastRun = nowISO;
1288
1379
  if (fullSweep) dreamState.lastFullSweep = nowISO;
1289
1380
  if (ev?.summary) dreamState.summarySnapshot = ev.summary;
1381
+ dreamState.provider = consolidationIntelligence.provider;
1382
+ dreamState.model = consolidationIntelligence.model;
1383
+ dreamState.effort = consolidationIntelligence.effort;
1384
+ dreamState.fallbackUsed = consolidationIntelligence.fallbackUsed;
1385
+ dreamState.lastRunStatus = consolidationIntelligence.status;
1386
+ dreamState.introspection = introspectionIntelligence;
1290
1387
  writeDreamState(dreamState);
1291
1388
  } catch (e) { console.warn(`[dream] state write failed: ${e.message}`); }
1292
1389
 
1293
1390
  const reportPath = writeDreamReport({
1294
- model: DREAM_MODEL, effort: DREAM_EFFORT, trigger,
1391
+ provider: consolidationIntelligence.provider,
1392
+ model: consolidationIntelligence.model,
1393
+ effort: consolidationIntelligence.effort || DREAM_EFFORT,
1394
+ intelligence: consolidationIntelligence,
1395
+ introspectionIntelligence,
1396
+ trigger,
1295
1397
  consolidation: { report: decision.report, lines: consolidationLines },
1296
1398
  introspection: { report: introReport, lines: introApplied, proposed: introProposed },
1297
1399
  healthNote, graphNote, episodicNote, toolNote, staleNote, backupRoot,
@@ -1312,16 +1414,70 @@ async function runDream({ trigger = "manual" } = {}) {
1312
1414
  if (episodicNote) parts.push(episodicNote);
1313
1415
  if (toolNote) parts.push(toolNote);
1314
1416
  if (staleNote) parts.push(staleNote);
1417
+ if (consolidationIntelligence.provider || consolidationIntelligence.model) {
1418
+ const introEngine = introspectionIntelligence.provider
1419
+ ? ` · introspection ${introspectionIntelligence.provider}/${introspectionIntelligence.model || "default"}`
1420
+ : "";
1421
+ parts.push(`⚙️ Dream intelligence: ${consolidationIntelligence.provider || "unknown"}/${consolidationIntelligence.model || "default"}${introEngine}`);
1422
+ }
1315
1423
  if (dreamLines.length) parts.push(`🗄 Anything changed or merged away is backed up under ${backupRoot}`);
1316
1424
  if (reportPath) parts.push(`📔 Full dream log: ${reportPath}`);
1317
1425
  const message = parts.length ? parts.join("\n\n") : "";
1318
1426
 
1319
- return { applied: dreamLines, report, introReport, message, healthNote, staleNote, graphNote, episodicNote, toolNote, reportPath, trigger, fullSweep };
1320
- } finally {
1321
- _dreaming = false;
1427
+ return {
1428
+ applied: dreamLines,
1429
+ report,
1430
+ introReport,
1431
+ message,
1432
+ healthNote,
1433
+ staleNote,
1434
+ graphNote,
1435
+ episodicNote,
1436
+ toolNote,
1437
+ reportPath,
1438
+ trigger,
1439
+ fullSweep,
1440
+ provider: consolidationIntelligence.provider,
1441
+ model: consolidationIntelligence.model,
1442
+ effort: consolidationIntelligence.effort,
1443
+ fallbackUsed: consolidationIntelligence.fallbackUsed,
1444
+ introspectionProvider: introspectionIntelligence.provider,
1445
+ introspectionModel: introspectionIntelligence.model,
1446
+ introspectionFallbackUsed: introspectionIntelligence.fallbackUsed,
1447
+ };
1448
+ }
1449
+
1450
+ function createDreamService(dependencies = {}) {
1451
+ const runUtility = dependencies.spawnUtilityAgent || spawnUtilityAgent;
1452
+ const queue = dependencies.mutationQueue || memoryMutationQueue;
1453
+ let dreaming = false;
1454
+
1455
+ async function runDream({ trigger = "manual", provider = null } = {}) {
1456
+ if (!enabled()) return { skipped: "dream is disabled (DREAM=off)" };
1457
+ if (dreaming) return { skipped: "a dream is already in progress" };
1458
+ dreaming = true;
1459
+ try {
1460
+ return await queue.enqueue("dream", () => runDreamMutation({ trigger, provider, runUtility }), {
1461
+ purpose: "dream",
1462
+ trigger,
1463
+ provider,
1464
+ });
1465
+ } finally {
1466
+ dreaming = false;
1467
+ }
1322
1468
  }
1469
+
1470
+ return {
1471
+ isDreaming: () => dreaming,
1472
+ previewDream: (options) => previewDreamWith(runUtility, options),
1473
+ runDream,
1474
+ };
1323
1475
  }
1324
1476
 
1477
+ const defaultDreamService = createDreamService();
1478
+ const runDream = defaultDreamService.runDream;
1479
+ const previewDream = defaultDreamService.previewDream;
1480
+
1325
1481
  // In-bot scheduler: one quiet-hours cron, reporting to the owner's
1326
1482
  // Telegram chat. When the dream summary is on (default) it posts a short
1327
1483
  // note after every run — including the charming "all tidy" line when
@@ -1346,14 +1502,15 @@ function initDream(adapters) {
1346
1502
  console.error("dream: run failed:", e.message);
1347
1503
  }
1348
1504
  });
1349
- console.log(`dream: scheduled (${DREAM_CRON}, model ${DREAM_MODEL}, effort ${DREAM_EFFORT}, introspect ${introspectEnabled() ? "on" : "off"})`);
1505
+ console.log(`dream: scheduled (${DREAM_CRON}, provider policy tier ${dreamTier()}, effort ${DREAM_EFFORT}, introspect ${introspectEnabled() ? "on" : "off"})`);
1350
1506
  }
1351
1507
 
1352
1508
  module.exports = {
1353
- runDream, initDream, buildDreamPrompt, parseDream, applyDream, manageAbilityTiers, nestPrefixFamilies, dreamTimeoutMs,
1509
+ createDreamService, previewDream, runDream, initDream, buildDreamPrompt, parseDream, applyDream, manageAbilityTiers, nestPrefixFamilies, dreamTimeoutMs,
1354
1510
  buildIntrospectionPrompt, parseIntrospection, applyIntrospection, writeDreamReport,
1355
1511
  summaryDelta, healthOf, mergeClusters, selectDumpDirs, journalBackfillDedupe,
1356
1512
  coRescueEdgesPhase, negativeHebbianPhase, readDreamState, writeDreamState,
1357
- enabled, summaryEnabled, introspectEnabled, selfApplyEnabled,
1358
- DREAM_CRON, DREAM_MODEL, DREAM_EFFORT, PROMOTE_MIN_PROJECTS, DREAM_STATE_FILE, FULL_SWEEP_DAYS,
1513
+ enabled, summaryEnabled, introspectEnabled, selfApplyEnabled, dreamTier,
1514
+ DREAM_CRON, DREAM_EFFORT, DREAM_OUTPUT_SCHEMA, INTROSPECTION_OUTPUT_SCHEMA,
1515
+ PROMOTE_MIN_PROJECTS, DREAM_STATE_FILE, FULL_SWEEP_DAYS,
1359
1516
  };
package/core/enforcer.js CHANGED
Binary file
package/core/entities.js CHANGED
@@ -18,6 +18,7 @@
18
18
 
19
19
  const fs = require("fs");
20
20
  const path = require("path");
21
+ const { atomicWriteFileSync } = require("./fsutil");
21
22
 
22
23
  let DatabaseSync = null;
23
24
  try { ({ DatabaseSync } = require("node:sqlite")); } catch (e) { /* old node — matching disabled */ }
@@ -168,7 +169,7 @@ function findEntity(nameOrSlug) {
168
169
 
169
170
  function writeEntity(ent) {
170
171
  ensureDir();
171
- fs.writeFileSync(entityFile(ent.slug), serialize(ent), { mode: 0o600 });
172
+ atomicWriteFileSync(entityFile(ent.slug), serialize(ent), { mode: 0o600, backup: true });
172
173
  markIndexDirty();
173
174
  return ent.slug;
174
175
  }
package/core/fsutil.js CHANGED
@@ -9,10 +9,11 @@
9
9
  const fs = require("fs");
10
10
  const path = require("path");
11
11
 
12
- function atomicWriteFileSync(file, data, { backup = false } = {}) {
12
+ function atomicWriteFileSync(file, data, { backup = false, mode = 0o600 } = {}) {
13
13
  fs.mkdirSync(path.dirname(file), { recursive: true });
14
14
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
15
- const fd = fs.openSync(tmp, "w");
15
+ const fd = fs.openSync(tmp, "w", mode);
16
+ let renamed = false;
16
17
  try {
17
18
  fs.writeSync(fd, data);
18
19
  fs.fsyncSync(fd);
@@ -22,9 +23,25 @@ function atomicWriteFileSync(file, data, { backup = false } = {}) {
22
23
  // Snapshot the previous good version before we overwrite, so a corrupt or
23
24
  // wrong write is still recoverable via readJsonWithFallback.
24
25
  if (backup) {
25
- try { if (fs.existsSync(file)) fs.copyFileSync(file, `${file}.bak`); } catch (e) {}
26
+ try {
27
+ if (fs.existsSync(file)) {
28
+ fs.copyFileSync(file, `${file}.bak`);
29
+ fs.chmodSync(`${file}.bak`, mode);
30
+ }
31
+ } catch (e) {}
32
+ }
33
+ try {
34
+ fs.renameSync(tmp, file);
35
+ renamed = true;
36
+ try {
37
+ const dirFd = fs.openSync(path.dirname(file), "r");
38
+ try { fs.fsyncSync(dirFd); } finally { fs.closeSync(dirFd); }
39
+ } catch (_) { /* directory fsync is not supported on every platform */ }
40
+ } finally {
41
+ if (!renamed) {
42
+ try { fs.unlinkSync(tmp); } catch (_) {}
43
+ }
26
44
  }
27
- fs.renameSync(tmp, file);
28
45
  }
29
46
 
30
47
  // Parse JSON from `file`, transparently falling back to `<file>.bak` if the