@inetafrica/open-claudia 2.15.0 → 3.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.
Files changed (152) hide show
  1. package/.env.example +30 -4
  2. package/CHANGELOG.md +22 -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 +81 -3
  12. package/channels/telegram/adapter.js +65 -5
  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 +155 -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 +542 -224
  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 +121 -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 +125 -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 +171 -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 +1562 -1286
  60. package/core/scheduler.js +515 -210
  61. package/core/side-chat.js +346 -0
  62. package/core/single-instance.js +46 -0
  63. package/core/state.js +1084 -97
  64. package/core/subagent.js +72 -98
  65. package/core/system-prompt.js +462 -279
  66. package/core/tool-guard.js +73 -39
  67. package/core/tools.js +22 -5
  68. package/core/transcripts.js +30 -1
  69. package/core/usage-log.js +19 -4
  70. package/core/utility-agent.js +654 -0
  71. package/core/web-auth.js +29 -13
  72. package/docs/CHANNEL_DESIGN.md +181 -0
  73. package/docs/MULTI_CHANNEL_PLAN.md +254 -0
  74. package/docs/PROVIDER_MIGRATION.md +67 -0
  75. package/health.js +50 -39
  76. package/package.json +48 -4
  77. package/setup.js +198 -38
  78. package/soul.md +39 -0
  79. package/test-ability-extraction.js +5 -0
  80. package/test-approval-async.js +63 -4
  81. package/test-cursor-removal.js +337 -0
  82. package/test-enforcer.js +70 -27
  83. package/test-fixtures/current-provider-parity.json +132 -0
  84. package/test-fixtures/fake-agent-cli.js +509 -0
  85. package/test-fixtures/migrations/claude-only/jobs.json +3 -0
  86. package/test-fixtures/migrations/claude-only/sessions.json +5 -0
  87. package/test-fixtures/migrations/claude-only/state.json +5 -0
  88. package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
  89. package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
  90. package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
  91. package/test-fixtures/migrations/cursor-selected/state.json +6 -0
  92. package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
  93. package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
  94. package/test-fixtures/migrations/dual-provider/state.json +10 -0
  95. package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
  96. package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
  97. package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
  98. package/test-fixtures/migrations/malformed-partial/state.json +1 -0
  99. package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
  100. package/test-fixtures/migrations/missing-project/jobs.json +3 -0
  101. package/test-fixtures/migrations/missing-project/sessions.json +3 -0
  102. package/test-fixtures/migrations/missing-project/state.json +4 -0
  103. package/test-fixtures/migrations/multi-user/jobs.json +4 -0
  104. package/test-fixtures/migrations/multi-user/sessions.json +4 -0
  105. package/test-fixtures/migrations/multi-user/state.json +6 -0
  106. package/test-fixtures/provider-event-samples.json +17 -0
  107. package/test-learning-e2e.js +5 -0
  108. package/test-memory-mutation-queue.js +191 -0
  109. package/test-provider-boot-matrix.js +399 -0
  110. package/test-provider-bootstrap.js +158 -0
  111. package/test-provider-capabilities.js +232 -0
  112. package/test-provider-claude.js +206 -0
  113. package/test-provider-codex.js +228 -0
  114. package/test-provider-compaction.js +371 -0
  115. package/test-provider-core-prompt.js +264 -0
  116. package/test-provider-coupling-audit.js +90 -0
  117. package/test-provider-dream.js +312 -0
  118. package/test-provider-enforcer.js +252 -0
  119. package/test-provider-env-isolation.js +150 -0
  120. package/test-provider-events.js +171 -0
  121. package/test-provider-fixture.js +332 -0
  122. package/test-provider-gateway.js +508 -0
  123. package/test-provider-language.js +89 -0
  124. package/test-provider-migration-backup.js +424 -0
  125. package/test-provider-pack-review.js +436 -0
  126. package/test-provider-parity-e2e.js +537 -0
  127. package/test-provider-prompt-parity.js +89 -0
  128. package/test-provider-recall.js +271 -0
  129. package/test-provider-registry.js +251 -0
  130. package/test-provider-resume-parity.js +41 -0
  131. package/test-provider-run-lifecycle.js +349 -0
  132. package/test-provider-runner.js +271 -0
  133. package/test-provider-scheduler.js +689 -0
  134. package/test-provider-session-commands.js +185 -0
  135. package/test-provider-session-history.js +205 -0
  136. package/test-provider-side-chat.js +337 -0
  137. package/test-provider-state-migration.js +316 -0
  138. package/test-provider-stream-decoder.js +69 -0
  139. package/test-provider-subagent.js +228 -0
  140. package/test-provider-tool-hooks.js +360 -0
  141. package/test-recall-discoverer.js +1 -0
  142. package/test-recall-engine.js +3 -3
  143. package/test-recall-evolution.js +18 -0
  144. package/test-runner-watchdog-static.js +16 -8
  145. package/test-single-runtime.js +35 -0
  146. package/test-telegram-poll-recovery.js +93 -0
  147. package/test-tool-guard.js +56 -0
  148. package/test-tools.js +19 -0
  149. package/test-unified-identity.js +3 -1
  150. package/test-usage-accounting.js +92 -20
  151. package/test-utility-provider-policy.js +486 -0
  152. package/web.js +130 -35
@@ -0,0 +1,7 @@
1
+ {
2
+ "owner@example.test": {
3
+ "parity": [
4
+ { "id": "claude-session-parity", "title": "Parity", "created": "2026-02-01T00:00:00.000Z", "lastUsed": "2026-02-02T00:00:00.000Z" }
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "users": {
3
+ "owner@example.test": {
4
+ "currentSession": { "name": "parity", "dir": "/workspace/parity" },
5
+ "lastSessionId": "claude-session-parity",
6
+ "codexSessionId": "codex-thread-parity",
7
+ "settings": { "backend": "codex", "model": "gpt-fixture" }
8
+ }
9
+ }
10
+ }
@@ -0,0 +1 @@
1
+ [{"id":"partial-job","kind":"wakeup","project":null
@@ -0,0 +1 @@
1
+ {"fallback":[{"id":"truncated-session"
@@ -0,0 +1,3 @@
1
+ {
2
+ "fallback": [{ "id": "fallback-session", "title": "Fallback" }]
3
+ }
@@ -0,0 +1 @@
1
+ {"users":{"broken":{"currentSession":
@@ -0,0 +1,5 @@
1
+ {
2
+ "currentSession": { "name": "fallback", "dir": "/workspace/fallback" },
3
+ "lastSessionId": "fallback-session",
4
+ "settings": { "backend": "claude" }
5
+ }
@@ -0,0 +1,3 @@
1
+ [
2
+ { "id": "orphan-job", "kind": "wakeup", "project": null, "sessionKey": "lastSessionId", "sessionId": "orphan-session", "prompt": "unresolved project" }
3
+ ]
@@ -0,0 +1,3 @@
1
+ {
2
+ "unknown-project": [{ "id": "orphan-session", "title": "Missing project selection" }]
3
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "lastSessionId": "orphan-session",
3
+ "settings": { "backend": "claude" }
4
+ }
@@ -0,0 +1,4 @@
1
+ [
2
+ { "id": "job-one", "kind": "wakeup", "canonicalUserId": "telegram:100", "project": "one", "sessionKey": "lastSessionId", "sessionId": "session-one", "prompt": "one" },
3
+ { "id": "job-two", "kind": "cron", "canonicalUserId": "person@example.test", "project": "two", "sessionKey": "codexSessionId", "sessionId": "thread-two", "prompt": "two" }
4
+ ]
@@ -0,0 +1,4 @@
1
+ {
2
+ "telegram:100": { "one": [{ "id": "session-one", "title": "One" }] },
3
+ "person@example.test": { "two": [{ "id": "provider-ambiguous-two", "title": "Two" }] }
4
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "users": {
3
+ "telegram:100": { "currentSession": { "name": "one", "dir": "/workspace/one" }, "lastSessionId": "session-one", "settings": { "backend": "claude" } },
4
+ "person@example.test": { "currentSession": { "name": "two", "dir": "/workspace/two" }, "codexSessionId": "thread-two", "settings": { "backend": "codex" } }
5
+ }
6
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "claude": {
3
+ "session": { "type": "system", "subtype": "init", "session_id": "claude-session-1" },
4
+ "delta": { "type": "stream_event", "event": { "type": "content_block_delta", "delta": { "type": "text_delta", "text": "Hello " } }, "session_id": "claude-session-1" },
5
+ "assistant": { "type": "assistant", "message": { "role": "assistant", "content": [{ "type": "text", "text": "world" }], "usage": { "input_tokens": 12, "output_tokens": 3 } }, "session_id": "claude-session-1" },
6
+ "toolUse": { "type": "assistant", "message": { "role": "assistant", "content": [{ "type": "tool_use", "id": "tool-1", "name": "Read", "input": { "file_path": "fixture.txt" } }] }, "session_id": "claude-session-1" },
7
+ "toolResult": { "type": "user", "message": { "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "tool-1", "content": "fixture output", "is_error": false }] }, "session_id": "claude-session-1" },
8
+ "result": { "type": "result", "subtype": "success", "is_error": false, "session_id": "claude-session-1", "result": "Hello world", "usage": { "input_tokens": 12, "output_tokens": 3 }, "total_cost_usd": 0 }
9
+ },
10
+ "codex": {
11
+ "session": { "type": "thread.started", "thread_id": "codex-thread-1" },
12
+ "toolStart": { "type": "item.started", "item": { "id": "item-1", "type": "command_execution", "command": "printf fixture", "status": "in_progress" } },
13
+ "toolEnd": { "type": "item.completed", "item": { "id": "item-1", "type": "command_execution", "command": "printf fixture", "aggregated_output": "fixture", "exit_code": 0, "status": "completed" } },
14
+ "assistant": { "type": "item.completed", "item": { "id": "item-final", "type": "agent_message", "text": "Hello from Codex" } },
15
+ "result": { "type": "turn.completed", "usage": { "input_tokens": 20, "cached_input_tokens": 5, "output_tokens": 4 } }
16
+ }
17
+ }
@@ -73,6 +73,11 @@ if (haveFts) {
73
73
  const transferred = packs.recordCoUse(["pack:mobile-app-deploy", "pack:chat-mobile"]);
74
74
  assert.deepStrictEqual(transferred.map((t) => t.project), ["chat-mobile"], "Stage 4: reuse recorded from real use");
75
75
  assert.deepStrictEqual(packs.readPack("mobile-app-deploy").applied_on, ["spaces", "chat-mobile"], "Stage 4: applied_on grew");
76
+ const learnedAbilityFile = path.join(process.env.PACKS_DIR, "mobile-app-deploy", "PACK.md");
77
+ assert.ok(fs.existsSync(`${learnedAbilityFile}.bak`), "learning updates retain a last-good pack backup");
78
+ assert.strictEqual(fs.statSync(learnedAbilityFile).mode & 0o777, 0o600, "learning files stay owner-only");
79
+ assert.deepStrictEqual(fs.readdirSync(path.dirname(learnedAbilityFile)).filter((name) => name.endsWith(".tmp")), [],
80
+ "atomic learning writes leave no partial temporary file");
76
81
  if (haveGraph) {
77
82
  graph.syncFromCorpus(packs, entStub);
78
83
  const fromChat = graph.expand([{ id: "pack:chat-mobile", score: 4 }], {});
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const assert = require("assert");
6
+ const fs = require("fs");
7
+ const os = require("os");
8
+ const path = require("path");
9
+
10
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-memory-queue-"));
11
+ process.env.OPEN_CLAUDIA_TEST = "1";
12
+ process.env.OPEN_CLAUDIA_CONFIG_DIR = path.join(root, "config");
13
+ fs.mkdirSync(process.env.OPEN_CLAUDIA_CONFIG_DIR, { recursive: true });
14
+
15
+ const {
16
+ MemoryMutationQueue,
17
+ MutationQueueError,
18
+ } = require("./core/memory-mutation-queue");
19
+ const { atomicWriteFileSync } = require("./core/fsutil");
20
+
21
+ function deferred() {
22
+ let resolve;
23
+ let reject;
24
+ const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
25
+ return { promise, resolve, reject };
26
+ }
27
+
28
+ function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
29
+
30
+ async function main() {
31
+ const lockDir = path.join(root, "shared.lock");
32
+ const journalFile = path.join(root, "mutation-journal.jsonl");
33
+ const firstQueue = new MemoryMutationQueue({ lockDir, journalFile, maxPending: 5, lockPollMs: 5 });
34
+ const secondQueue = new MemoryMutationQueue({ lockDir, journalFile, maxPending: 5, lockPollMs: 5 });
35
+ const gate = deferred();
36
+ const events = [];
37
+
38
+ const first = firstQueue.enqueue("review", async () => {
39
+ events.push("review:start");
40
+ await gate.promise;
41
+ events.push("review:end");
42
+ return "reviewed";
43
+ }, { provider: "codex", model: "fixture" });
44
+ while (!events.includes("review:start")) await wait(5);
45
+ const second = secondQueue.enqueue("dream", async () => {
46
+ events.push("dream:start");
47
+ await wait(5);
48
+ events.push("dream:end");
49
+ return "dreamed";
50
+ });
51
+ await wait(30);
52
+ assert.deepStrictEqual(events, ["review:start"], "independent queue instances serialize through the shared lock");
53
+ gate.resolve();
54
+ assert.deepStrictEqual(await Promise.all([first, second]), ["reviewed", "dreamed"]);
55
+ assert.deepStrictEqual(events, ["review:start", "review:end", "dream:start", "dream:end"]);
56
+ assert.ok(!fs.existsSync(lockDir), "shared lock is removed after every mutation");
57
+
58
+ const liveLockDir = path.join(root, "live-stale.lock");
59
+ const liveGate = deferred();
60
+ const liveOwner = new MemoryMutationQueue({
61
+ lockDir: liveLockDir,
62
+ journalFile,
63
+ maxPending: 2,
64
+ lockPollMs: 5,
65
+ staleLockMs: 10,
66
+ });
67
+ const liveContender = new MemoryMutationQueue({
68
+ lockDir: liveLockDir,
69
+ journalFile,
70
+ maxPending: 2,
71
+ lockPollMs: 5,
72
+ staleLockMs: 10,
73
+ lockTimeoutMs: 50,
74
+ });
75
+ const liveMutation = liveOwner.enqueue("long-dream", async () => liveGate.promise);
76
+ while (!fs.existsSync(path.join(liveLockDir, "owner.json"))) await wait(5);
77
+ await wait(20);
78
+ await assert.rejects(
79
+ liveContender.enqueue("must-not-steal", async () => "stolen"),
80
+ (error) => error.code === "MEMORY_MUTATION_LOCK_TIMEOUT",
81
+ "an old lock owned by a live process cannot be stolen from a long dream mutation",
82
+ );
83
+ liveGate.resolve("complete");
84
+ assert.strictEqual(await liveMutation, "complete");
85
+
86
+ const deadLockDir = path.join(root, "dead-stale.lock");
87
+ fs.mkdirSync(deadLockDir);
88
+ fs.writeFileSync(path.join(deadLockDir, "owner.json"), JSON.stringify({ token: "dead", pid: 2147483647 }));
89
+ const old = new Date(Date.now() - 10_000);
90
+ fs.utimesSync(deadLockDir, old, old);
91
+ const deadRecovery = new MemoryMutationQueue({
92
+ lockDir: deadLockDir,
93
+ journalFile,
94
+ lockPollMs: 5,
95
+ staleLockMs: 10,
96
+ lockTimeoutMs: 200,
97
+ });
98
+ assert.strictEqual(await deadRecovery.enqueue("recover-dead-lock", async () => "recovered"), "recovered");
99
+
100
+ const failureQueue = new MemoryMutationQueue({
101
+ lockDir: path.join(root, "failure.lock"),
102
+ journalFile,
103
+ maxPending: 3,
104
+ lockPollMs: 5,
105
+ });
106
+ await assert.rejects(
107
+ failureQueue.enqueue("broken-review", async () => { throw new Error("write fixture failed"); }),
108
+ /write fixture failed/,
109
+ );
110
+ assert.strictEqual(await failureQueue.enqueue("after-failure", async () => "still runs"), "still runs",
111
+ "one failed mutation never poisons the serial chain");
112
+
113
+ const boundedGate = deferred();
114
+ const bounded = new MemoryMutationQueue({
115
+ lockDir: path.join(root, "bounded.lock"),
116
+ journalFile,
117
+ maxPending: 1,
118
+ lockPollMs: 5,
119
+ });
120
+ const held = bounded.enqueue("held", async () => boundedGate.promise);
121
+ await wait(10);
122
+ await assert.rejects(
123
+ bounded.enqueue("overflow", async () => {}),
124
+ (error) => error instanceof MutationQueueError && error.code === "MEMORY_MUTATION_QUEUE_FULL",
125
+ );
126
+ boundedGate.resolve("released");
127
+ assert.strictEqual(await held, "released");
128
+
129
+ const shutdownGate = deferred();
130
+ const shutdownQueue = new MemoryMutationQueue({
131
+ lockDir: path.join(root, "shutdown.lock"),
132
+ journalFile,
133
+ maxPending: 3,
134
+ lockPollMs: 5,
135
+ });
136
+ const pending = shutdownQueue.enqueue("pending-review", async () => shutdownGate.promise, { provider: "claude" });
137
+ await wait(10);
138
+ shutdownQueue.stopAdmission("test shutdown");
139
+ await assert.rejects(
140
+ shutdownQueue.enqueue("late-review", async () => {}),
141
+ (error) => error.code === "MEMORY_MUTATION_QUEUE_CLOSED",
142
+ );
143
+ const timedDrain = await shutdownQueue.drain({ timeoutMs: 25 });
144
+ assert.strictEqual(timedDrain.drained, false);
145
+ assert.strictEqual(timedDrain.pending.length, 1);
146
+ assert.strictEqual(timedDrain.pending[0].label, "pending-review");
147
+ const journalBeforeRelease = fs.readFileSync(journalFile, "utf8");
148
+ assert.match(journalBeforeRelease, /"status":"pending_on_shutdown"/);
149
+ assert.ok(!journalBeforeRelease.includes("provider prompt"), "queue journal stores metadata only, never prompts");
150
+ shutdownGate.resolve("done");
151
+ assert.strictEqual(await pending, "done");
152
+ const finalDrain = await shutdownQueue.drain({ timeoutMs: 100 });
153
+ assert.strictEqual(finalDrain.drained, true);
154
+ assert.deepStrictEqual(finalDrain.pending, []);
155
+
156
+ const atomicFile = path.join(root, "atomic", "memory.md");
157
+ atomicWriteFileSync(atomicFile, "old complete memory\n", { mode: 0o600, backup: true });
158
+ const originalRename = fs.renameSync;
159
+ fs.renameSync = () => { throw new Error("rename fault fixture"); };
160
+ try {
161
+ assert.throws(
162
+ () => atomicWriteFileSync(atomicFile, "new incomplete memory\n", { mode: 0o600, backup: true }),
163
+ /rename fault fixture/,
164
+ );
165
+ } finally {
166
+ fs.renameSync = originalRename;
167
+ }
168
+ assert.strictEqual(fs.readFileSync(atomicFile, "utf8"), "old complete memory\n",
169
+ "failed atomic replacement preserves the complete previous file");
170
+ assert.deepStrictEqual(
171
+ fs.readdirSync(path.dirname(atomicFile)).filter((name) => name.endsWith(".tmp")),
172
+ [],
173
+ "failed atomic replacement cleans its temporary file",
174
+ );
175
+ atomicWriteFileSync(atomicFile, "new complete memory\n", { mode: 0o600, backup: true });
176
+ assert.strictEqual(fs.readFileSync(atomicFile, "utf8"), "new complete memory\n");
177
+ assert.strictEqual(fs.readFileSync(`${atomicFile}.bak`, "utf8"), "old complete memory\n");
178
+ assert.strictEqual(fs.statSync(atomicFile).mode & 0o777, 0o600);
179
+
180
+ const journal = fs.readFileSync(journalFile, "utf8").trim().split("\n").map(JSON.parse);
181
+ assert.ok(journal.some((entry) => entry.label === "broken-review" && entry.status === "failed"));
182
+ assert.ok(journal.some((entry) => entry.label === "after-failure" && entry.status === "completed"));
183
+ assert.ok(journal.every((entry) => !Object.prototype.hasOwnProperty.call(entry, "payload")));
184
+
185
+ console.log("memory mutation queue OK");
186
+ }
187
+
188
+ main().catch((error) => {
189
+ console.error(error.stack || error.message);
190
+ process.exitCode = 1;
191
+ }).finally(() => fs.rmSync(root, { recursive: true, force: true }));