@inetafrica/open-claudia 2.14.8 → 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.
- package/.env.example +30 -4
- package/CHANGELOG.md +21 -0
- package/README.md +87 -66
- package/bin/agent.js +44 -18
- package/bin/cli.js +30 -28
- package/bin/dream.js +5 -11
- package/bin/loopback-client.js +29 -5
- package/bin/schedule.js +19 -5
- package/bin/tool.js +23 -5
- package/bot-agent.js +5 -2350
- package/bot.js +105 -7
- package/channels/telegram/adapter.js +6 -1
- package/channels/voice/adapter.js +1 -0
- package/channels/voice/manage.js +43 -5
- package/config-dir.js +3 -11
- package/core/actions.js +149 -40
- package/core/approvals.js +136 -29
- package/core/auth-flow.js +103 -19
- package/core/config-dir.js +19 -0
- package/core/config.js +127 -69
- package/core/doctor.js +111 -57
- package/core/dream.js +219 -62
- package/core/enforcer.js +0 -0
- package/core/entities.js +2 -1
- package/core/fsutil.js +55 -0
- package/core/handlers.js +503 -217
- package/core/ideas.js +2 -1
- package/core/identity.js +8 -11
- package/core/io.js +24 -3
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +45 -8
- package/core/memory-mutation-queue.js +241 -0
- package/core/migration-backup.js +1060 -0
- package/core/pack-review.js +295 -88
- package/core/packs.js +4 -3
- package/core/persona.js +2 -1
- package/core/process-tree.js +19 -2
- package/core/provider-migration.js +39 -0
- package/core/provider-status.js +80 -0
- package/core/providers/claude-events.js +117 -0
- package/core/providers/claude-hook.js +114 -0
- package/core/providers/claude.js +296 -0
- package/core/providers/codex-events.js +121 -0
- package/core/providers/codex-hook.js +280 -0
- package/core/providers/codex.js +286 -0
- package/core/providers/contract.js +153 -0
- package/core/providers/env.js +148 -0
- package/core/providers/events.js +170 -0
- package/core/providers/hook-command.js +22 -0
- package/core/providers/index.js +240 -0
- package/core/providers/utility-policy.js +269 -0
- package/core/recall/classic.js +2 -2
- package/core/recall/discoverer.js +71 -22
- package/core/recall/metrics.js +27 -1
- package/core/recall/tuning.js +4 -1
- package/core/recall/warm-walker.js +151 -108
- package/core/recall-filter.js +86 -61
- package/core/router.js +79 -7
- package/core/run-context.js +185 -0
- package/core/runner.js +1415 -1282
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/state.js +1096 -95
- package/core/subagent.js +72 -98
- package/core/system-prompt.js +462 -279
- package/core/tool-guard.js +73 -39
- package/core/tools.js +22 -5
- package/core/transcripts.js +30 -1
- package/core/usage-log.js +19 -4
- package/core/utility-agent.js +654 -0
- package/core/web-auth.js +29 -13
- package/core/web-sessions.js +78 -0
- package/docs/CHANNEL_DESIGN.md +181 -0
- package/docs/MULTI_CHANNEL_PLAN.md +254 -0
- package/docs/PROVIDER_MIGRATION.md +67 -0
- package/health.js +50 -39
- package/package.json +51 -4
- package/setup.js +198 -38
- package/soul.md +39 -0
- package/test-ability-extraction.js +5 -0
- package/test-approval-async.js +63 -4
- package/test-cursor-removal.js +334 -0
- package/test-delivery-contract.js +88 -0
- package/test-enforcer.js +70 -27
- package/test-fixtures/current-provider-parity.json +132 -0
- package/test-fixtures/fake-agent-cli.js +477 -0
- package/test-fixtures/migrations/claude-only/jobs.json +3 -0
- package/test-fixtures/migrations/claude-only/sessions.json +5 -0
- package/test-fixtures/migrations/claude-only/state.json +5 -0
- package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
- package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
- package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
- package/test-fixtures/migrations/cursor-selected/state.json +6 -0
- package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
- package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
- package/test-fixtures/migrations/dual-provider/state.json +10 -0
- package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
- package/test-fixtures/migrations/malformed-partial/state.json +1 -0
- package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
- package/test-fixtures/migrations/missing-project/jobs.json +3 -0
- package/test-fixtures/migrations/missing-project/sessions.json +3 -0
- package/test-fixtures/migrations/missing-project/state.json +4 -0
- package/test-fixtures/migrations/multi-user/jobs.json +4 -0
- package/test-fixtures/migrations/multi-user/sessions.json +4 -0
- package/test-fixtures/migrations/multi-user/state.json +6 -0
- package/test-fixtures/provider-event-samples.json +17 -0
- package/test-learning-e2e.js +5 -0
- package/test-memory-mutation-queue.js +191 -0
- package/test-provider-boot-matrix.js +399 -0
- package/test-provider-bootstrap.js +158 -0
- package/test-provider-capabilities.js +232 -0
- package/test-provider-claude.js +206 -0
- package/test-provider-codex.js +228 -0
- package/test-provider-compaction.js +371 -0
- package/test-provider-core-prompt.js +264 -0
- package/test-provider-coupling-audit.js +90 -0
- package/test-provider-dream.js +312 -0
- package/test-provider-enforcer.js +252 -0
- package/test-provider-env-isolation.js +150 -0
- package/test-provider-events.js +141 -0
- package/test-provider-fixture.js +332 -0
- package/test-provider-gateway.js +508 -0
- package/test-provider-language.js +89 -0
- package/test-provider-migration-backup.js +424 -0
- package/test-provider-pack-review.js +436 -0
- package/test-provider-parity-e2e.js +537 -0
- package/test-provider-prompt-parity.js +89 -0
- package/test-provider-recall.js +271 -0
- package/test-provider-registry.js +251 -0
- package/test-provider-resume-parity.js +41 -0
- package/test-provider-run-lifecycle.js +349 -0
- package/test-provider-runner.js +271 -0
- package/test-provider-scheduler.js +689 -0
- package/test-provider-session-commands.js +185 -0
- package/test-provider-session-history.js +205 -0
- package/test-provider-side-chat.js +337 -0
- package/test-provider-state-migration.js +316 -0
- package/test-provider-stream-decoder.js +69 -0
- package/test-provider-subagent.js +228 -0
- package/test-provider-tool-hooks.js +360 -0
- package/test-recall-discoverer.js +1 -0
- package/test-recall-engine.js +3 -3
- package/test-recall-evolution.js +18 -0
- package/test-run-lock.js +63 -0
- package/test-runner-watchdog-static.js +16 -8
- package/test-single-runtime.js +35 -0
- package/test-telegram-poll-recovery.js +56 -0
- package/test-tool-guard.js +56 -0
- package/test-tools.js +19 -0
- package/test-unified-identity.js +3 -1
- package/test-usage-accounting.js +92 -20
- package/test-utility-provider-policy.js +486 -0
- package/test-web-sessions.js +74 -0
- package/web.js +159 -45
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// 1-2 hops; nodes over the firing threshold join the candidate set. This
|
|
7
7
|
// auto-pulls governed-by concerns (theme, commit style) the query never
|
|
8
8
|
// named, which is the whole point of the graph.
|
|
9
|
-
// 4. Walker (
|
|
9
|
+
// 4. Walker (provider-owned low tier) — reads the candidates' live sections and
|
|
10
10
|
// returns the truly-relevant set WITH a one-line "why" per node. Facts are
|
|
11
11
|
// quoted, not paraphrased. Fail-open: any error keeps the seeds (classic
|
|
12
12
|
// baseline), so the discoverer never recalls worse than keyword matching.
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
const graph = require("./graph");
|
|
19
19
|
const metrics = require("./metrics");
|
|
20
20
|
const tuning = require("./tuning");
|
|
21
|
-
const {
|
|
21
|
+
const { spawnUtilityAgent } = require("../utility-agent");
|
|
22
|
+
const { resolveUtilityProvider } = require("../providers/utility-policy");
|
|
22
23
|
const warmWalker = require("./warm-walker");
|
|
23
24
|
const toolsLib = require("../tools");
|
|
24
25
|
// Tool-graph is the directed-usage source of truth (pack↔tool edges). The
|
|
@@ -90,7 +91,6 @@ function renderToolGaps(gaps) {
|
|
|
90
91
|
let transcriptIndex = null;
|
|
91
92
|
try { transcriptIndex = require("../transcript-index"); } catch (e) { /* old node */ }
|
|
92
93
|
|
|
93
|
-
const WALKER_MODEL = process.env.RECALL_DISCOVERER_MODEL || "haiku";
|
|
94
94
|
const WALKER_TIMEOUT_MS = Number(process.env.RECALL_DISCOVERER_TIMEOUT_MS || 25000);
|
|
95
95
|
const WALKER_ENABLED = String(process.env.RECALL_DISCOVERER_WALKER || "on").toLowerCase() !== "off";
|
|
96
96
|
// Sizing/tier knobs (walker candidate cap, episode limit, seeds-tier width,
|
|
@@ -222,23 +222,71 @@ const WALKER_SYSTEM = [
|
|
|
222
222
|
// low latency; on any warm-path error fall back to a cold spawn — identical
|
|
223
223
|
// model/prompt/contract, so quality is unchanged and recall never silently
|
|
224
224
|
// degrades to the classic engine. Returns { text, costUsd, source }.
|
|
225
|
-
async function runWalker(prompt) {
|
|
226
|
-
const
|
|
227
|
-
|
|
225
|
+
async function runWalker(prompt, options = {}) {
|
|
226
|
+
const resolveSelection = options.resolveUtilityProvider || resolveUtilityProvider;
|
|
227
|
+
const runUtility = options.spawnUtilityAgent || spawnUtilityAgent;
|
|
228
|
+
const warm = options.warmWalker || warmWalker;
|
|
229
|
+
const selection = resolveSelection({
|
|
230
|
+
purpose: "recall",
|
|
231
|
+
activeProvider: options.provider || null,
|
|
232
|
+
tier: "low",
|
|
233
|
+
model: options.model || null,
|
|
234
|
+
modelEnvPrefix: "RECALL_DISCOVERER_MODEL",
|
|
235
|
+
legacyClaudeModelEnv: "RECALL_DISCOVERER_MODEL",
|
|
236
|
+
});
|
|
237
|
+
const opts = {
|
|
238
|
+
provider: selection.provider,
|
|
239
|
+
model: selection.model,
|
|
240
|
+
tier: "low",
|
|
241
|
+
systemPrompt: WALKER_SYSTEM,
|
|
242
|
+
timeoutMs: WALKER_TIMEOUT_MS,
|
|
243
|
+
};
|
|
244
|
+
if (warm.isEnabled(selection.provider)) {
|
|
228
245
|
try {
|
|
229
|
-
const
|
|
230
|
-
return {
|
|
246
|
+
const result = await warm.walkWarm(prompt, opts);
|
|
247
|
+
return {
|
|
248
|
+
text: result.text,
|
|
249
|
+
costUsd: result.costUsd,
|
|
250
|
+
source: "warm",
|
|
251
|
+
provider: selection.providerId,
|
|
252
|
+
model: selection.model,
|
|
253
|
+
};
|
|
231
254
|
} catch (e) { /* fall back to cold spawn below */ }
|
|
232
255
|
}
|
|
233
|
-
const
|
|
234
|
-
|
|
256
|
+
const result = await runUtility(prompt, {
|
|
257
|
+
purpose: "recall",
|
|
258
|
+
provider: selection.providerId,
|
|
259
|
+
tier: "low",
|
|
260
|
+
model: selection.model,
|
|
261
|
+
modelEnvPrefix: "RECALL_DISCOVERER_MODEL",
|
|
262
|
+
legacyClaudeModelEnv: "RECALL_DISCOVERER_MODEL",
|
|
263
|
+
systemPrompt: WALKER_SYSTEM,
|
|
264
|
+
readOnly: true,
|
|
265
|
+
outputSchema: {
|
|
266
|
+
type: "array",
|
|
267
|
+
items: {
|
|
268
|
+
type: "object",
|
|
269
|
+
required: ["id"],
|
|
270
|
+
properties: { id: { type: "string" }, why: { type: "string" } },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
timeoutMs: WALKER_TIMEOUT_MS,
|
|
274
|
+
cwd: options.cwd || process.cwd(),
|
|
275
|
+
});
|
|
276
|
+
return {
|
|
277
|
+
text: result.text,
|
|
278
|
+
costUsd: null,
|
|
279
|
+
source: "cold",
|
|
280
|
+
provider: result.provider || selection.providerId,
|
|
281
|
+
model: result.model || selection.model,
|
|
282
|
+
};
|
|
235
283
|
}
|
|
236
284
|
|
|
237
285
|
// Judge candidates. Returns { kept: Map|null, costUsd, source, walkerMs } —
|
|
238
286
|
// kept null means fail-open (walker off/failed/unparseable).
|
|
239
|
-
async function walk(userText, contextText, candidates) {
|
|
287
|
+
async function walk(userText, contextText, candidates, options = {}) {
|
|
240
288
|
if (!WALKER_ENABLED || candidates.length === 0) {
|
|
241
|
-
return { kept: null, costUsd: null, source: "off", walkerMs: 0 };
|
|
289
|
+
return { kept: null, costUsd: null, source: "off", walkerMs: 0, provider: options.provider || null, model: null };
|
|
242
290
|
}
|
|
243
291
|
const lines = candidates.map((c) => {
|
|
244
292
|
const tag = c.activated ? ` [linked via ${c.via || "graph"}]` : "";
|
|
@@ -256,12 +304,12 @@ async function walk(userText, contextText, candidates) {
|
|
|
256
304
|
].join("\n");
|
|
257
305
|
const t0 = Date.now();
|
|
258
306
|
try {
|
|
259
|
-
const { text, costUsd, source } = await runWalker(prompt);
|
|
307
|
+
const { text, costUsd, source, provider, model } = await runWalker(prompt, options);
|
|
260
308
|
const walkerMs = Date.now() - t0;
|
|
261
309
|
const match = String(text || "").match(/\[[\s\S]*\]/);
|
|
262
|
-
if (!match) return { kept: null, costUsd, source, walkerMs };
|
|
310
|
+
if (!match) return { kept: null, costUsd, source, walkerMs, provider, model };
|
|
263
311
|
const arr = JSON.parse(match[0]);
|
|
264
|
-
if (!Array.isArray(arr)) return { kept: null, costUsd, source, walkerMs };
|
|
312
|
+
if (!Array.isArray(arr)) return { kept: null, costUsd, source, walkerMs, provider, model };
|
|
265
313
|
const known = new Set(candidates.map((c) => c.id));
|
|
266
314
|
const out = new Map();
|
|
267
315
|
for (const item of arr) {
|
|
@@ -269,14 +317,14 @@ async function walk(userText, contextText, candidates) {
|
|
|
269
317
|
out.set(item.id, String(item.why || "").slice(0, 120));
|
|
270
318
|
}
|
|
271
319
|
}
|
|
272
|
-
return { kept: out, costUsd, source, walkerMs };
|
|
320
|
+
return { kept: out, costUsd, source, walkerMs, provider, model };
|
|
273
321
|
} catch (e) {
|
|
274
|
-
return { kept: null, costUsd: null, source: "failed", walkerMs: Date.now() - t0 };
|
|
322
|
+
return { kept: null, costUsd: null, source: "failed", walkerMs: Date.now() - t0, provider: options.provider || null, model: null };
|
|
275
323
|
}
|
|
276
324
|
}
|
|
277
325
|
|
|
278
326
|
async function run(ctx) {
|
|
279
|
-
const { userText, contextText, fullContext, packLimit, budget, helpers } = ctx;
|
|
327
|
+
const { userText, contextText, fullContext, packLimit, budget, helpers, provider } = ctx;
|
|
280
328
|
const { packsLib, entitiesLib, mergeMatches, buildPackBlock, buildEntityBlock } = helpers;
|
|
281
329
|
const started = Date.now();
|
|
282
330
|
|
|
@@ -314,7 +362,7 @@ async function run(ctx) {
|
|
|
314
362
|
// 1: tiered pre-gate.
|
|
315
363
|
const tier = recallTier(userText, seedCount);
|
|
316
364
|
if (tier === "skip") {
|
|
317
|
-
metrics.logTurn({ engine: "discoverer", model:
|
|
365
|
+
metrics.logTurn({ engine: "discoverer", provider: provider || "", model: "", query: userText, gated: true, tier, latencyMs: Date.now() - started });
|
|
318
366
|
return { packBlock: "", entityBlock: "", toolBlock: "", episodeBlock: "", packMatches: [], entityMatches: [], toolMatches: [], toolGaps: [], episodeMatches: [], why: {}, gated: true, tier };
|
|
319
367
|
}
|
|
320
368
|
|
|
@@ -453,7 +501,7 @@ async function run(ctx) {
|
|
|
453
501
|
}
|
|
454
502
|
}
|
|
455
503
|
|
|
456
|
-
const walkRes = await walk(userText, contextText || fullContext, judgeCands);
|
|
504
|
+
const walkRes = await walk(userText, contextText || fullContext, judgeCands, { provider: provider || null });
|
|
457
505
|
const whyById = walkRes.kept;
|
|
458
506
|
|
|
459
507
|
let shadowChecks = 0, shadowAgree = 0;
|
|
@@ -576,7 +624,8 @@ async function run(ctx) {
|
|
|
576
624
|
|
|
577
625
|
metrics.logTurn({
|
|
578
626
|
engine: "discoverer",
|
|
579
|
-
|
|
627
|
+
provider: walkRes.provider || provider || "",
|
|
628
|
+
model: walkRes.model || "",
|
|
580
629
|
query: userText,
|
|
581
630
|
tier,
|
|
582
631
|
seeds: seedNodes,
|
|
@@ -602,4 +651,4 @@ async function run(ctx) {
|
|
|
602
651
|
};
|
|
603
652
|
}
|
|
604
653
|
|
|
605
|
-
module.exports = { name: "discoverer", run, needsRecall, recallTier, walk, cascadeVerdict, scoutToolGaps, renderToolGaps, episodesAllowedForSpeaker };
|
|
654
|
+
module.exports = { name: "discoverer", run, runWalker, needsRecall, recallTier, walk, cascadeVerdict, scoutToolGaps, renderToolGaps, episodesAllowedForSpeaker };
|
package/core/recall/metrics.js
CHANGED
|
@@ -33,6 +33,7 @@ function logTurn(entry) {
|
|
|
33
33
|
const rec = {
|
|
34
34
|
ts: new Date().toISOString(),
|
|
35
35
|
v: VERSION,
|
|
36
|
+
provider: entry.provider || "",
|
|
36
37
|
model: entry.model || "",
|
|
37
38
|
engine: entry.engine || "discoverer",
|
|
38
39
|
query: String(entry.query || "").slice(0, 200),
|
|
@@ -106,6 +107,31 @@ function logReviewerSkip() {
|
|
|
106
107
|
} catch (e) {}
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
function logReviewerRun(entry = {}) {
|
|
111
|
+
if (!enabled()) return;
|
|
112
|
+
try {
|
|
113
|
+
const rec = {
|
|
114
|
+
ts: new Date().toISOString(),
|
|
115
|
+
v: VERSION,
|
|
116
|
+
reviewer: {
|
|
117
|
+
provider: String(entry.provider || ""),
|
|
118
|
+
model: String(entry.model || ""),
|
|
119
|
+
status: String(entry.status || "unknown"),
|
|
120
|
+
fallbackUsed: !!entry.fallbackUsed,
|
|
121
|
+
...(entry.code ? { code: String(entry.code).slice(0, 80) } : {}),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify(rec) + "\n");
|
|
125
|
+
const summary = readSummary();
|
|
126
|
+
summary.reviewerRuns = (summary.reviewerRuns || 0) + 1;
|
|
127
|
+
const key = `${rec.reviewer.provider || "unknown"}:${rec.reviewer.model || "default"}`;
|
|
128
|
+
summary.reviewerByProviderModel = summary.reviewerByProviderModel || {};
|
|
129
|
+
summary.reviewerByProviderModel[key] = (summary.reviewerByProviderModel[key] || 0) + 1;
|
|
130
|
+
if (rec.reviewer.status === "failed") summary.reviewerFailures = (summary.reviewerFailures || 0) + 1;
|
|
131
|
+
writeSummary(summary);
|
|
132
|
+
} catch (_) {}
|
|
133
|
+
}
|
|
134
|
+
|
|
109
135
|
function round(n) { return Math.round((Number(n) || 0) * 1000) / 1000; }
|
|
110
136
|
function today() { return new Date().toISOString().slice(0, 10); }
|
|
111
137
|
|
|
@@ -311,6 +337,6 @@ function _resetForTest() {
|
|
|
311
337
|
|
|
312
338
|
module.exports = {
|
|
313
339
|
LOG_FILE, SUMMARY_FILE, NODE_STATS_FILE, KPI_FILE, VERSION, enabled,
|
|
314
|
-
logTurn, logUse, logShadow, logReviewerSkip, appendKpi, readKpi,
|
|
340
|
+
logTurn, logUse, logShadow, logReviewerSkip, logReviewerRun, appendKpi, readKpi,
|
|
315
341
|
summary, nodeStats, evidence, _resetForTest,
|
|
316
342
|
};
|
package/core/recall/tuning.js
CHANGED
|
@@ -113,7 +113,10 @@ function lastChange() {
|
|
|
113
113
|
function history() { return readFile().history || []; }
|
|
114
114
|
|
|
115
115
|
function write(data) {
|
|
116
|
-
try {
|
|
116
|
+
try {
|
|
117
|
+
fs.mkdirSync(path.dirname(TUNING_FILE), { recursive: true });
|
|
118
|
+
fs.writeFileSync(TUNING_FILE, JSON.stringify(data, null, 2));
|
|
119
|
+
} catch (e) {}
|
|
117
120
|
_cache = null; _cacheMtime = -1;
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -1,162 +1,205 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// ~1.4s — init is paid once and amortised. Same model, same system prompt,
|
|
8
|
-
// same JSON contract as the cold path, so recall quality is unchanged; only
|
|
9
|
-
// the transport differs.
|
|
10
|
-
//
|
|
11
|
-
// Statelessness: stream-json keeps one growing session, so we recycle the
|
|
12
|
-
// process on a small message/char budget to keep accumulated history tiny and
|
|
13
|
-
// each walk effectively independent.
|
|
14
|
-
//
|
|
15
|
-
// Fail-open: any error here throws, and the caller (discoverer.walk) falls
|
|
16
|
-
// back to the cold spawnSubagent path — never worse than before, and never a
|
|
17
|
-
// drop to the classic engine.
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Optional low-latency transport for providers that explicitly advertise a
|
|
4
|
+
// persistent utility stream. The current native implementation is Claude's
|
|
5
|
+
// stream-json input protocol; Codex declares this capability unsupported and
|
|
6
|
+
// therefore always uses the identical cold utility prompt/contract.
|
|
18
7
|
|
|
19
8
|
const { spawn } = require("child_process");
|
|
20
|
-
const {
|
|
21
|
-
const {
|
|
9
|
+
const { StringDecoder } = require("string_decoder");
|
|
10
|
+
const { killProcessTree } = require("../process-tree");
|
|
22
11
|
const { redactSensitive } = require("../redact");
|
|
23
12
|
|
|
24
13
|
const MAX_MSGS = Number(process.env.RECALL_WARM_WALKER_MAX_MSGS || 12);
|
|
25
14
|
const MAX_CHARS = Number(process.env.RECALL_WARM_WALKER_MAX_CHARS || 24000);
|
|
26
15
|
const DEFAULT_TIMEOUT_MS = 25000;
|
|
16
|
+
const KILL_GRACE_MS = Number(process.env.RECALL_WARM_WALKER_KILL_GRACE_MS || 250);
|
|
17
|
+
const MAX_BUFFER_BYTES = Number(process.env.RECALL_WARM_WALKER_MAX_BUFFER_BYTES || 1024 * 1024);
|
|
18
|
+
|
|
19
|
+
let child = null;
|
|
20
|
+
let childProviderId = null;
|
|
21
|
+
let pending = null;
|
|
22
|
+
let chain = Promise.resolve();
|
|
23
|
+
let msgCount = 0;
|
|
24
|
+
let charCount = 0;
|
|
25
|
+
let costTotal = 0;
|
|
26
|
+
let stopPromise = null;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let costTotal = 0; // cumulative total_cost_usd reported by the session
|
|
28
|
+
function isEnabled(provider) {
|
|
29
|
+
if (String(process.env.RECALL_WARM_WALKER || "on").toLowerCase() === "off") return false;
|
|
30
|
+
return provider?.id === "claude"
|
|
31
|
+
&& provider.capabilities?.persistentUtilityStream?.support === "native";
|
|
32
|
+
}
|
|
34
33
|
|
|
35
|
-
function
|
|
36
|
-
|
|
34
|
+
function settle(kind, value) {
|
|
35
|
+
const current = pending;
|
|
36
|
+
pending = null;
|
|
37
|
+
if (!current) return;
|
|
38
|
+
clearTimeout(current.timer);
|
|
39
|
+
if (kind === "resolve") current.resolve(value);
|
|
40
|
+
else current.reject(value);
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
function
|
|
43
|
+
function stopChild(reason = "warm walker stopped") {
|
|
44
|
+
if (stopPromise) return stopPromise;
|
|
40
45
|
const proc = child;
|
|
41
46
|
child = null;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
childProviderId = null;
|
|
48
|
+
if (!proc) return Promise.resolve();
|
|
49
|
+
stopPromise = new Promise((resolve) => {
|
|
50
|
+
let done = false;
|
|
51
|
+
let killTimer = null;
|
|
52
|
+
const finish = () => {
|
|
53
|
+
if (done) return;
|
|
54
|
+
done = true;
|
|
55
|
+
if (killTimer) clearTimeout(killTimer);
|
|
56
|
+
stopPromise = null;
|
|
57
|
+
resolve();
|
|
58
|
+
};
|
|
59
|
+
proc.once("close", finish);
|
|
60
|
+
try { proc.stdin.end(); } catch (_) {}
|
|
61
|
+
try { killProcessTree(proc.pid, "SIGTERM"); } catch (_) { try { proc.kill("SIGTERM"); } catch (_) {} }
|
|
62
|
+
killTimer = setTimeout(() => {
|
|
63
|
+
try { killProcessTree(proc.pid, "SIGKILL"); } catch (_) { try { proc.kill("SIGKILL"); } catch (_) {} }
|
|
64
|
+
}, Math.max(1, KILL_GRACE_MS));
|
|
65
|
+
if (proc.exitCode !== null || proc.signalCode !== null) finish();
|
|
66
|
+
});
|
|
67
|
+
settle("reject", new Error(reason));
|
|
68
|
+
return stopPromise;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function persistentInvocation(cfg) {
|
|
72
|
+
const provider = cfg.provider;
|
|
73
|
+
if (!isEnabled(provider)) throw new Error(`${provider?.id || "provider"} has no enabled persistent utility stream`);
|
|
74
|
+
const invocation = provider.buildUtilityInvocation({
|
|
75
|
+
purpose: "recall",
|
|
76
|
+
tier: "low",
|
|
77
|
+
model: cfg.model || provider.tierModel("low"),
|
|
78
|
+
coreInstructions: cfg.systemPrompt || "You are Open Claudia's recall walker.",
|
|
79
|
+
dynamicContext: "",
|
|
80
|
+
userPrompt: "",
|
|
81
|
+
readOnly: true,
|
|
82
|
+
fresh: true,
|
|
83
|
+
});
|
|
84
|
+
const args = [...invocation.args];
|
|
85
|
+
const printAt = args.indexOf("-p");
|
|
86
|
+
args.splice(printAt >= 0 ? printAt + 1 : 0, 0, "--input-format", "stream-json");
|
|
87
|
+
return { ...invocation, args, stdin: null };
|
|
46
88
|
}
|
|
47
89
|
|
|
48
90
|
function spawnChild(cfg) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
const invocation = persistentInvocation(cfg);
|
|
92
|
+
const proc = spawn(invocation.binary, invocation.args, {
|
|
93
|
+
cwd: cfg.cwd || process.cwd(),
|
|
94
|
+
env: {
|
|
95
|
+
...invocation.env,
|
|
96
|
+
OC_PROVIDER: cfg.provider.id,
|
|
97
|
+
OC_UTILITY_PURPOSE: "recall",
|
|
98
|
+
},
|
|
99
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
100
|
+
detached: process.platform !== "win32",
|
|
101
|
+
shell: false,
|
|
102
|
+
});
|
|
61
103
|
child = proc;
|
|
104
|
+
childProviderId = cfg.provider.id;
|
|
62
105
|
msgCount = 0;
|
|
63
106
|
charCount = 0;
|
|
64
107
|
costTotal = 0;
|
|
65
|
-
let
|
|
66
|
-
let
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
pending = null;
|
|
71
|
-
if (!p) return;
|
|
72
|
-
clearTimeout(p.timer);
|
|
73
|
-
fn === "resolve" ? p.resolve(arg) : p.reject(arg);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
proc.stdout.on("data", (d) => {
|
|
108
|
+
let buffer = "";
|
|
109
|
+
let assistantText = "";
|
|
110
|
+
const stringDecoder = new StringDecoder("utf8");
|
|
111
|
+
|
|
112
|
+
proc.stdout.on("data", (data) => {
|
|
77
113
|
if (child !== proc) return;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
114
|
+
buffer += stringDecoder.write(data);
|
|
115
|
+
if (Buffer.byteLength(buffer, "utf8") > MAX_BUFFER_BYTES) {
|
|
116
|
+
stopChild("warm walker output exceeded its buffer limit").catch(() => {});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let newline;
|
|
120
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
121
|
+
const line = buffer.slice(0, newline).trim();
|
|
122
|
+
buffer = buffer.slice(newline + 1);
|
|
83
123
|
if (!line) continue;
|
|
84
|
-
let
|
|
85
|
-
try {
|
|
86
|
-
if (
|
|
87
|
-
for (const
|
|
88
|
-
if (
|
|
124
|
+
let event;
|
|
125
|
+
try { event = JSON.parse(line); } catch (_) { continue; }
|
|
126
|
+
if (event.type === "assistant" && Array.isArray(event.message?.content)) {
|
|
127
|
+
for (const block of event.message.content) {
|
|
128
|
+
if (block?.type === "text" && typeof block.text === "string") assistantText += block.text;
|
|
89
129
|
}
|
|
90
|
-
} else if (
|
|
91
|
-
const text =
|
|
92
|
-
|
|
93
|
-
// total_cost_usd is cumulative for the stream-json session; the delta
|
|
94
|
-
// from the previous result is this walk's cost.
|
|
130
|
+
} else if (event.type === "result") {
|
|
131
|
+
const text = typeof event.result === "string" && event.result ? event.result : assistantText;
|
|
132
|
+
assistantText = "";
|
|
95
133
|
let costUsd = null;
|
|
96
|
-
if (typeof
|
|
97
|
-
costUsd = Math.max(0,
|
|
98
|
-
costTotal =
|
|
134
|
+
if (typeof event.total_cost_usd === "number") {
|
|
135
|
+
costUsd = Math.max(0, event.total_cost_usd - costTotal);
|
|
136
|
+
costTotal = event.total_cost_usd;
|
|
99
137
|
}
|
|
100
|
-
if (text && !
|
|
101
|
-
else settle("reject", new Error("warm walker
|
|
138
|
+
if (text && !event.is_error) settle("resolve", { text: redactSensitive(String(text).trim()), costUsd });
|
|
139
|
+
else settle("reject", new Error("warm walker returned an empty or error result"));
|
|
102
140
|
}
|
|
103
141
|
}
|
|
104
142
|
});
|
|
105
|
-
proc.stderr.on("data", () => {});
|
|
106
|
-
proc.on("error", (
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
proc.stderr.on("data", () => {});
|
|
144
|
+
proc.stdin.on("error", (error) => settle("reject", error));
|
|
145
|
+
proc.on("error", (error) => {
|
|
146
|
+
if (child === proc) { child = null; childProviderId = null; }
|
|
147
|
+
settle("reject", error);
|
|
109
148
|
});
|
|
110
149
|
proc.on("close", () => {
|
|
111
|
-
if (child === proc) child = null;
|
|
112
|
-
settle("reject", new Error("warm walker
|
|
150
|
+
if (child === proc) { child = null; childProviderId = null; }
|
|
151
|
+
settle("reject", new Error("warm walker process closed"));
|
|
113
152
|
});
|
|
114
153
|
return proc;
|
|
115
154
|
}
|
|
116
155
|
|
|
117
|
-
function doWalk(promptText, opts) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
156
|
+
async function doWalk(promptText, opts) {
|
|
157
|
+
if (!isEnabled(opts.provider)) throw new Error(`${opts.provider?.id || "provider"} persistent recall is unavailable`);
|
|
158
|
+
if (child && (childProviderId !== opts.provider.id || msgCount >= MAX_MSGS || charCount >= MAX_CHARS)) {
|
|
159
|
+
await stopChild("warm walker recycled");
|
|
160
|
+
}
|
|
161
|
+
if (!child) spawnChild(opts);
|
|
122
162
|
const proc = child;
|
|
123
163
|
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
124
|
-
|
|
125
164
|
return new Promise((resolve, reject) => {
|
|
126
|
-
const timer = setTimeout(() => {
|
|
165
|
+
const timer = setTimeout(async () => {
|
|
166
|
+
if (pending?.proc !== proc) return;
|
|
167
|
+
const current = pending;
|
|
127
168
|
pending = null;
|
|
128
|
-
|
|
129
|
-
reject(new Error("warm walker
|
|
169
|
+
await stopChild("warm walker timed out");
|
|
170
|
+
current.reject(new Error("warm walker timed out"));
|
|
130
171
|
}, timeoutMs);
|
|
131
|
-
pending = { resolve, reject, timer };
|
|
132
|
-
msgCount
|
|
172
|
+
pending = { resolve, reject, timer, proc };
|
|
173
|
+
msgCount += 1;
|
|
133
174
|
charCount += promptText.length;
|
|
134
175
|
const payload = JSON.stringify({ type: "user", message: { role: "user", content: promptText } }) + "\n";
|
|
135
|
-
try {
|
|
136
|
-
|
|
137
|
-
} catch (e) {
|
|
176
|
+
try { proc.stdin.write(payload); }
|
|
177
|
+
catch (error) {
|
|
138
178
|
clearTimeout(timer);
|
|
139
179
|
pending = null;
|
|
140
|
-
|
|
141
|
-
reject(e);
|
|
180
|
+
stopChild("warm walker write failed").finally(() => reject(error));
|
|
142
181
|
}
|
|
143
182
|
});
|
|
144
183
|
}
|
|
145
184
|
|
|
146
|
-
// Serialise: one message in flight at a time. A failed walk must not poison the
|
|
147
|
-
// queue, so the chain swallows outcomes while callers still see their result.
|
|
148
|
-
// Resolves { text, costUsd } — costUsd is the per-walk delta of the session's
|
|
149
|
-
// cumulative total_cost_usd (null when the CLI doesn't report it).
|
|
150
185
|
function walkWarm(promptText, opts = {}) {
|
|
151
186
|
const run = () => doWalk(promptText, opts);
|
|
152
|
-
const
|
|
153
|
-
chain =
|
|
154
|
-
return
|
|
187
|
+
const result = chain.then(run, run);
|
|
188
|
+
chain = result.then(() => {}, () => {});
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function shutdown() {
|
|
193
|
+
await stopChild("warm walker shutdown");
|
|
155
194
|
}
|
|
156
195
|
|
|
157
|
-
function
|
|
158
|
-
|
|
196
|
+
function stats() {
|
|
197
|
+
return { alive: !!child, providerId: childProviderId, msgCount, charCount, MAX_MSGS, MAX_CHARS };
|
|
198
|
+
}
|
|
159
199
|
|
|
160
|
-
process.on("exit", () => {
|
|
200
|
+
process.on("exit", () => {
|
|
201
|
+
if (!child) return;
|
|
202
|
+
try { killProcessTree(child.pid, "SIGKILL"); } catch (_) {}
|
|
203
|
+
});
|
|
161
204
|
|
|
162
205
|
module.exports = { walkWarm, isEnabled, shutdown, stats };
|