@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
package/core/pack-review.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
// mutation is announced in chat (one line), per the no-silent-learning
|
|
7
7
|
// rule.
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const crypto = require("crypto");
|
|
10
|
+
const { spawnUtilityAgent, cancelUtilityAgents } = require("./utility-agent");
|
|
11
|
+
const { memoryMutationQueue } = require("./memory-mutation-queue");
|
|
10
12
|
const packs = require("./packs");
|
|
11
13
|
const entities = require("./entities");
|
|
12
14
|
const lessons = require("./lessons");
|
|
@@ -15,15 +17,21 @@ const { redactSensitive } = require("./redact");
|
|
|
15
17
|
|
|
16
18
|
const MIN_TURN_CHARS = 400;
|
|
17
19
|
const MAX_TEXT_CHARS = 16000;
|
|
18
|
-
// Create/merge decisions need real judgement (the cheap model over-created
|
|
19
|
-
// per-version/per-facet packs), so default to the latest Sonnet. Override with
|
|
20
|
-
// PACK_REVIEW_MODEL if cost/latency of running it on every substantial turn matters.
|
|
21
|
-
const REVIEW_MODEL = process.env.PACK_REVIEW_MODEL || "claude-sonnet-4-6";
|
|
22
20
|
const MAX_ACTIONS = 2;
|
|
23
21
|
const MAX_ENTITY_ACTIONS = 3;
|
|
24
22
|
// Lessons are precious always-on budget — at most one promotion per turn,
|
|
25
23
|
// and only when the turn shows a genuine miss.
|
|
26
24
|
const MAX_LESSON_ACTIONS = 1;
|
|
25
|
+
const REVIEW_OUTPUT_SCHEMA = Object.freeze({
|
|
26
|
+
type: "object",
|
|
27
|
+
required: ["actions", "entities", "lessons"],
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
properties: {
|
|
30
|
+
actions: { type: "array", items: { type: "object" } },
|
|
31
|
+
entities: { type: "array", items: { type: "object" } },
|
|
32
|
+
lessons: { type: "array", items: { type: "object" } },
|
|
33
|
+
},
|
|
34
|
+
});
|
|
27
35
|
|
|
28
36
|
function enabled() {
|
|
29
37
|
return (process.env.PACK_REVIEW || "on").toLowerCase() !== "off";
|
|
@@ -381,8 +389,9 @@ function shouldSkipReview({ userText, assistantText, signals = {} }) {
|
|
|
381
389
|
return null;
|
|
382
390
|
}
|
|
383
391
|
|
|
384
|
-
//
|
|
385
|
-
//
|
|
392
|
+
// Callers may hand this off without awaiting it, but each review stays tracked
|
|
393
|
+
// until model, serialized mutation, and announcement settlement. Announcement
|
|
394
|
+
// failures never escape into the foreground turn.
|
|
386
395
|
// Provenance (2.5): stamp appended Log/Journal lines with the contributing
|
|
387
396
|
// person when they're NOT the owner. The owner is the default author, so
|
|
388
397
|
// tagging their contributions would be pure noise — the signal is precisely
|
|
@@ -393,100 +402,298 @@ function provenanceTag(source) {
|
|
|
393
402
|
return `(via ${String(source.name).trim()}) `;
|
|
394
403
|
}
|
|
395
404
|
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
405
|
+
function contentVersion(value) {
|
|
406
|
+
return crypto.createHash("sha256").update(JSON.stringify(value ?? null)).digest("hex");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function snapshotMemoryVersions() {
|
|
410
|
+
const packVersions = {};
|
|
411
|
+
const entityVersions = {};
|
|
412
|
+
try {
|
|
413
|
+
for (const pack of packs.listPacks()) packVersions[pack.dir] = contentVersion(pack);
|
|
414
|
+
} catch (_) {}
|
|
415
|
+
try {
|
|
416
|
+
for (const entity of entities.listEntities()) entityVersions[entity.slug] = contentVersion(entity);
|
|
417
|
+
} catch (_) {}
|
|
418
|
+
return { packs: packVersions, entities: entityVersions };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function unchangedPack(dir, versions) {
|
|
422
|
+
const known = Object.prototype.hasOwnProperty.call(versions?.packs || {}, dir);
|
|
423
|
+
if (!known) {
|
|
424
|
+
try { return packs.readPack(dir) === null; }
|
|
425
|
+
catch (_) { return false; }
|
|
426
|
+
}
|
|
427
|
+
const expected = versions.packs[dir];
|
|
428
|
+
try { return contentVersion(packs.readPack(dir)) === expected; }
|
|
429
|
+
catch (_) { return false; }
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function safeEntityAction(action, versions) {
|
|
433
|
+
let existing = null;
|
|
434
|
+
try { existing = entities.findEntity(action?.name); } catch (_) {}
|
|
435
|
+
if (!existing) return action;
|
|
436
|
+
const known = Object.prototype.hasOwnProperty.call(versions?.entities || {}, existing.slug);
|
|
437
|
+
const expected = known ? versions.entities[existing.slug] : null;
|
|
438
|
+
if (known && contentVersion(existing) === expected) return action;
|
|
439
|
+
// A dream or another review changed this entity while the model was
|
|
440
|
+
// deciding. Keep the append-only observation, but never replace newer
|
|
441
|
+
// current-truth/persona sections with a stale proposal.
|
|
442
|
+
return {
|
|
443
|
+
name: action.name,
|
|
444
|
+
type: action.type || existing.type,
|
|
445
|
+
log: action.log || "",
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function applyDecisionBatch({ decision, activePacks, versions, source }) {
|
|
450
|
+
const activeSet = new Set(Array.isArray(activePacks) ? activePacks : []);
|
|
451
|
+
for (const dir of [...activeSet]) if (!unchangedPack(dir, versions)) activeSet.delete(dir);
|
|
452
|
+
const lines = [];
|
|
453
|
+
const usedIds = [];
|
|
454
|
+
const errors = [];
|
|
455
|
+
const tag = provenanceTag(source);
|
|
456
|
+
|
|
457
|
+
for (const original of decision.actions) {
|
|
458
|
+
const action = structuredClone(original);
|
|
459
|
+
try {
|
|
460
|
+
if (tag && typeof action.journal === "string" && action.journal.trim()) action.journal = tag + action.journal.trim();
|
|
461
|
+
const result = applyAction(action, activeSet);
|
|
462
|
+
if (result && (result.kind === "update" || result.kind === "create") && result.dir) usedIds.push(`pack:${result.dir}`);
|
|
463
|
+
if (result?.kind === "skipped") {
|
|
464
|
+
lines.push(`🛡️ Skipped a memory write that looked like an injected instruction (${result.reason}).`);
|
|
465
|
+
} else if (result?.kind === "create" && result.ability) {
|
|
466
|
+
lines.push(`🧩 New ability: ${result.name}${result.learned_on ? ` (learned on ${result.learned_on})` : ""}\n${clipWords(result.note, 180)}\n↳ open-claudia pack show ${result.dir}`);
|
|
467
|
+
} else if (result?.kind === "create") {
|
|
468
|
+
lines.push(`📦 New pack: ${result.name}${result.parent ? ` (filed under ${result.parent})` : ""}\n${clipWords(result.note, 180)}\n↳ open-claudia pack show ${result.dir}`);
|
|
469
|
+
} else if (result) {
|
|
470
|
+
const marks = [];
|
|
471
|
+
if (result.appliedTo) marks.push(` 🧩 (now also applies to ${result.appliedTo})`);
|
|
472
|
+
if (result.overwroteUserState) marks.push(" ⚠️ replaced a user-written State — check it kept your facts");
|
|
473
|
+
if (result.blindSkipped) marks.push(" (section rewrite dropped: pack wasn't active or changed while review was pending — journal only)");
|
|
474
|
+
lines.push(`✏️ ${result.name} — ${clipWords(result.note, 180)}${marks.join("")}\n↳ open-claudia pack show ${result.dir}`);
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
errors.push(error);
|
|
478
|
+
console.warn(`[pack-review] apply failed: ${error.message}`);
|
|
402
479
|
}
|
|
403
|
-
return;
|
|
404
480
|
}
|
|
405
481
|
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const lines = [];
|
|
418
|
-
const usedIds = []; // nodes this turn's work demonstrably touched
|
|
419
|
-
const tag = provenanceTag(source);
|
|
420
|
-
for (const a of decision.actions) {
|
|
421
|
-
try {
|
|
422
|
-
if (tag && typeof a.journal === "string" && a.journal.trim()) a.journal = tag + a.journal.trim();
|
|
423
|
-
const r = applyAction(a, activeSet);
|
|
424
|
-
if (r && (r.kind === "update" || r.kind === "create") && r.dir) usedIds.push(`pack:${r.dir}`);
|
|
425
|
-
if (r && r.kind === "skipped") {
|
|
426
|
-
lines.push(`🛡️ Skipped a memory write that looked like an injected instruction (${r.reason}).`);
|
|
427
|
-
} else if (r && r.kind === "create" && r.ability) {
|
|
428
|
-
lines.push(`🧩 New ability: ${r.name}${r.learned_on ? ` (learned on ${r.learned_on})` : ""}\n${clipWords(r.note, 180)}\n↳ open-claudia pack show ${r.dir}`);
|
|
429
|
-
} else if (r && r.kind === "create") {
|
|
430
|
-
lines.push(`📦 New pack: ${r.name}${r.parent ? ` (filed under ${r.parent})` : ""}\n${clipWords(r.note, 180)}\n↳ open-claudia pack show ${r.dir}`);
|
|
431
|
-
} else if (r) {
|
|
432
|
-
const marks = [];
|
|
433
|
-
if (r.appliedTo) marks.push(` 🧩 (now also applies to ${r.appliedTo})`);
|
|
434
|
-
if (r.overwroteUserState) marks.push(" ⚠️ replaced a user-written State — check it kept your facts");
|
|
435
|
-
if (r.blindSkipped) marks.push(" (section rewrite dropped: pack wasn't active this turn — journal only)");
|
|
436
|
-
lines.push(`✏️ ${r.name} — ${clipWords(r.note, 180)}${marks.join("")}\n↳ open-claudia pack show ${r.dir}`);
|
|
437
|
-
}
|
|
438
|
-
} catch (e) {
|
|
439
|
-
console.warn(`[pack-review] apply failed: ${e.message}`);
|
|
482
|
+
for (const original of decision.entities) {
|
|
483
|
+
const action = structuredClone(original);
|
|
484
|
+
try {
|
|
485
|
+
if (tag && typeof action.log === "string" && action.log.trim()) action.log = tag + action.log.trim();
|
|
486
|
+
const result = applyEntityAction(safeEntityAction(action, versions));
|
|
487
|
+
if (result) {
|
|
488
|
+
if (result.slug) usedIds.push(`entity:${result.slug}`);
|
|
489
|
+
const emoji = ENTITY_EMOJI[result.type] || ENTITY_EMOJI.thing;
|
|
490
|
+
lines.push(result.kind === "create"
|
|
491
|
+
? `${emoji} Now tracking ${result.name} (${result.type}) — ${clipWords(result.note, 160)}`
|
|
492
|
+
: `${emoji} ${result.name} — ${clipWords(result.note, 160)}`);
|
|
440
493
|
}
|
|
494
|
+
} catch (error) {
|
|
495
|
+
errors.push(error);
|
|
496
|
+
console.warn(`[pack-review] entity apply failed: ${error.message}`);
|
|
441
497
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (usedIds.length) {
|
|
501
|
+
try { require("./recall/metrics").logUse(usedIds, "reviewer"); } catch (_) {}
|
|
502
|
+
try { if (usedIds.length > 1) require("./recall/graph").reinforceSet(usedIds); } catch (_) {}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
for (const original of decision.lessons || []) {
|
|
506
|
+
try {
|
|
507
|
+
const result = applyLessonAction(structuredClone(original));
|
|
508
|
+
if (!result) continue;
|
|
509
|
+
if (result.kind === "skipped") {
|
|
510
|
+
if (result.reason && /exfil|override|base64/.test(result.reason)) {
|
|
511
|
+
lines.push(`🛡️ Skipped a lesson that looked like an injected instruction (${result.reason}).`);
|
|
452
512
|
}
|
|
453
|
-
}
|
|
454
|
-
|
|
513
|
+
} else if (result.kind === "create") {
|
|
514
|
+
lines.push(`📌 New lesson — I got this wrong before, so I'll always keep it loaded now:\n"${clipWords(result.text, 200)}"${result.overCap ? "\n(That's over the lessons cap — the nightly dream will tidy.)" : ""}`);
|
|
515
|
+
} else if (result.kind === "reinforce") {
|
|
516
|
+
lines.push(`📌 Reinforced a lesson I keep needing — clearly worth keeping front of mind:\n"${clipWords(result.text, 200)}"`);
|
|
455
517
|
}
|
|
518
|
+
} catch (error) {
|
|
519
|
+
errors.push(error);
|
|
520
|
+
console.warn(`[pack-review] lesson apply failed: ${error.message}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (errors.length) {
|
|
525
|
+
const error = new AggregateError(errors, `${errors.length} memory review mutation(s) failed`);
|
|
526
|
+
error.applied = { lines, usedIds };
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
return { lines, usedIds };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function reviewerMetric(entry) {
|
|
533
|
+
try { require("./recall/metrics").logReviewerRun(entry); } catch (_) {}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function createPackReviewer(dependencies = {}) {
|
|
537
|
+
const runUtility = dependencies.spawnUtilityAgent || spawnUtilityAgent;
|
|
538
|
+
const queue = dependencies.mutationQueue || memoryMutationQueue;
|
|
539
|
+
const cancelUtilities = dependencies.cancelUtilityAgents || cancelUtilityAgents;
|
|
540
|
+
const handles = new Map();
|
|
541
|
+
let accepting = true;
|
|
542
|
+
|
|
543
|
+
function track(promise, metadata) {
|
|
544
|
+
const id = typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
|
|
545
|
+
handles.set(id, { id, promise, ...metadata });
|
|
546
|
+
promise.then(() => handles.delete(id), () => handles.delete(id));
|
|
547
|
+
return promise;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function reviewTurn({ userText, assistantText, channelId, announce, signals, activePacks, source, provider, runId } = {}) {
|
|
551
|
+
if (!enabled()) return Promise.resolve({ status: "skipped", reason: "disabled" });
|
|
552
|
+
const skip = shouldSkipReview({ userText, assistantText, signals });
|
|
553
|
+
if (skip) {
|
|
554
|
+
if (skip !== "short") { try { require("./recall/metrics").logReviewerSkip(); } catch (_) {} }
|
|
555
|
+
return Promise.resolve({ status: "skipped", reason: skip });
|
|
456
556
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
// have been injected). Feeds the same precision metric as 📖 opens and
|
|
461
|
-
// reinforces co-use edges. This is co-WORK, never co-recall — surfacing
|
|
462
|
-
// alone still reinforces nothing.
|
|
463
|
-
if (usedIds.length) {
|
|
464
|
-
try { require("./recall/metrics").logUse(usedIds, "reviewer"); } catch (e) {}
|
|
465
|
-
try { if (usedIds.length > 1) require("./recall/graph").reinforceSet(usedIds); } catch (e) {}
|
|
557
|
+
if (!accepting) {
|
|
558
|
+
queue.record({ id: null, label: "review", status: "pending_intent_rejected", provider: provider || null, runId: runId || null });
|
|
559
|
+
return Promise.resolve({ status: "not_admitted", reason: "review shutdown in progress" });
|
|
466
560
|
}
|
|
467
|
-
|
|
561
|
+
|
|
562
|
+
const capturedProvider = provider || null;
|
|
563
|
+
const versions = snapshotMemoryVersions();
|
|
564
|
+
const prompt = buildReviewPrompt(
|
|
565
|
+
redactSensitive(String(userText || "")),
|
|
566
|
+
redactSensitive(String(assistantText || "")),
|
|
567
|
+
activePacks,
|
|
568
|
+
);
|
|
569
|
+
const work = (async () => {
|
|
570
|
+
let utilityResult = null;
|
|
468
571
|
try {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
572
|
+
utilityResult = await runUtility(prompt, {
|
|
573
|
+
purpose: "review",
|
|
574
|
+
activeProvider: capturedProvider,
|
|
575
|
+
tier: "medium",
|
|
576
|
+
modelEnvPrefix: "PACK_REVIEW_MODEL",
|
|
577
|
+
legacyClaudeModelEnv: "PACK_REVIEW_MODEL",
|
|
578
|
+
readOnly: true,
|
|
579
|
+
outputSchema: REVIEW_OUTPUT_SCHEMA,
|
|
580
|
+
timeoutMs: 3 * 60 * 1000,
|
|
581
|
+
cwd: process.cwd(),
|
|
582
|
+
systemPrompt: "You are a background memory reviewer. Reply with ONLY the requested JSON object. No prose, no markdown, no tool use.",
|
|
583
|
+
});
|
|
584
|
+
const decision = parseDecision(utilityResult.json ? JSON.stringify(utilityResult.json) : utilityResult.text);
|
|
585
|
+
if (!decision || (decision.actions.length === 0 && decision.entities.length === 0 && decision.lessons.length === 0)) {
|
|
586
|
+
reviewerMetric({ provider: utilityResult.provider, model: utilityResult.model, status: "no_change", fallbackUsed: utilityResult.fallbackUsed });
|
|
587
|
+
return { status: "no_change", provider: utilityResult.provider, model: utilityResult.model, fallbackUsed: !!utilityResult.fallbackUsed };
|
|
588
|
+
}
|
|
589
|
+
const applied = await queue.enqueue("review", () => applyDecisionBatch({
|
|
590
|
+
decision,
|
|
591
|
+
activePacks,
|
|
592
|
+
versions,
|
|
593
|
+
source,
|
|
594
|
+
}), {
|
|
595
|
+
provider: utilityResult.provider,
|
|
596
|
+
model: utilityResult.model,
|
|
597
|
+
purpose: "review",
|
|
598
|
+
runId,
|
|
599
|
+
});
|
|
600
|
+
let announcementFailed = false;
|
|
601
|
+
if (applied.lines.length && typeof announce === "function") {
|
|
602
|
+
try { await announce(formatAnnouncement(applied.lines)); }
|
|
603
|
+
catch (_) { announcementFailed = true; }
|
|
604
|
+
}
|
|
605
|
+
reviewerMetric({ provider: utilityResult.provider, model: utilityResult.model, status: "applied", fallbackUsed: utilityResult.fallbackUsed });
|
|
606
|
+
return {
|
|
607
|
+
status: "applied",
|
|
608
|
+
provider: utilityResult.provider,
|
|
609
|
+
model: utilityResult.model,
|
|
610
|
+
fallbackUsed: !!utilityResult.fallbackUsed,
|
|
611
|
+
lines: applied.lines,
|
|
612
|
+
announcementFailed,
|
|
613
|
+
};
|
|
614
|
+
} catch (error) {
|
|
615
|
+
const resolvedProvider = utilityResult?.provider || capturedProvider || "";
|
|
616
|
+
const partialLines = error.applied?.lines || [];
|
|
617
|
+
let announcementFailed = false;
|
|
618
|
+
if (partialLines.length && typeof announce === "function") {
|
|
619
|
+
try { await announce(formatAnnouncement(partialLines)); }
|
|
620
|
+
catch (_) { announcementFailed = true; }
|
|
479
621
|
}
|
|
480
|
-
|
|
481
|
-
console.warn(`[pack-review]
|
|
622
|
+
reviewerMetric({ provider: resolvedProvider, model: utilityResult?.model || "", status: "failed", code: error.code || "REVIEW_FAILED" });
|
|
623
|
+
console.warn(`[pack-review] reviewer failed: ${error.message}`);
|
|
624
|
+
return {
|
|
625
|
+
status: "failed",
|
|
626
|
+
provider: resolvedProvider || null,
|
|
627
|
+
model: utilityResult?.model || null,
|
|
628
|
+
lines: partialLines,
|
|
629
|
+
announcementFailed,
|
|
630
|
+
error: { code: error.code || "REVIEW_FAILED", message: redactSensitive(error.message) },
|
|
631
|
+
};
|
|
482
632
|
}
|
|
633
|
+
})();
|
|
634
|
+
return track(work, { provider: capturedProvider, channelId: channelId || null, runId: runId || null });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function stopAdmission(reason = "shutdown") {
|
|
638
|
+
accepting = false;
|
|
639
|
+
queue.record({ id: null, label: "review", status: "review_admission_stopped", reason: String(reason).slice(0, 160) });
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async function drainReviews({ timeoutMs = 5000, cancelOnTimeout = false } = {}) {
|
|
643
|
+
stopAdmission("shutdown drain");
|
|
644
|
+
queue.stopAdmission("shutdown drain");
|
|
645
|
+
const snapshot = [...handles.values()];
|
|
646
|
+
let timer;
|
|
647
|
+
const settled = Promise.allSettled(snapshot.map((handle) => handle.promise)).then(() => true);
|
|
648
|
+
const drained = await Promise.race([
|
|
649
|
+
settled,
|
|
650
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(false), timeoutMs); }),
|
|
651
|
+
]);
|
|
652
|
+
clearTimeout(timer);
|
|
653
|
+
if (!drained) {
|
|
654
|
+
for (const handle of snapshot.filter((entry) => handles.has(entry.id))) {
|
|
655
|
+
queue.record({ id: handle.id, label: "review", status: "pending_on_shutdown", provider: handle.provider, runId: handle.runId });
|
|
656
|
+
}
|
|
657
|
+
if (cancelOnTimeout && typeof cancelUtilities === "function") await cancelUtilities({ timeoutMs });
|
|
483
658
|
}
|
|
484
|
-
|
|
485
|
-
|
|
659
|
+
let queueResult = await queue.drain({ timeoutMs });
|
|
660
|
+
if (!queueResult.drained && cancelOnTimeout && typeof cancelUtilities === "function") {
|
|
661
|
+
await cancelUtilities({ timeoutMs });
|
|
662
|
+
queueResult = await queue.drain({ timeoutMs });
|
|
486
663
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
664
|
+
return {
|
|
665
|
+
drained: drained && queueResult.drained,
|
|
666
|
+
pendingReviews: [...handles.values()].map(({ id, provider: providerId, runId: reviewRunId }) => ({ id, provider: providerId, runId: reviewRunId })),
|
|
667
|
+
pendingMutations: queueResult.pending,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
activeReviewCount: () => handles.size,
|
|
673
|
+
drainReviews,
|
|
674
|
+
reviewTurn,
|
|
675
|
+
stopAdmission,
|
|
676
|
+
};
|
|
490
677
|
}
|
|
491
678
|
|
|
492
|
-
|
|
679
|
+
const defaultReviewer = createPackReviewer();
|
|
680
|
+
|
|
681
|
+
module.exports = {
|
|
682
|
+
ENTITY_EMOJI,
|
|
683
|
+
REVIEW_OUTPUT_SCHEMA,
|
|
684
|
+
applyAction,
|
|
685
|
+
applyDecisionBatch,
|
|
686
|
+
applyEntityAction,
|
|
687
|
+
applyLessonAction,
|
|
688
|
+
buildReviewPrompt,
|
|
689
|
+
clipWords,
|
|
690
|
+
contentVersion,
|
|
691
|
+
createPackReviewer,
|
|
692
|
+
drainReviews: defaultReviewer.drainReviews,
|
|
693
|
+
parseDecision,
|
|
694
|
+
provenanceTag,
|
|
695
|
+
reviewTurn: defaultReviewer.reviewTurn,
|
|
696
|
+
shouldSkipReview,
|
|
697
|
+
snapshotMemoryVersions,
|
|
698
|
+
stopReviewAdmission: defaultReviewer.stopAdmission,
|
|
699
|
+
};
|
package/core/packs.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require("fs");
|
|
13
13
|
const path = require("path");
|
|
14
|
+
const { atomicWriteFileSync } = require("./fsutil");
|
|
14
15
|
|
|
15
16
|
let DatabaseSync = null;
|
|
16
17
|
try { ({ DatabaseSync } = require("node:sqlite")); } catch (e) { /* old node — matching disabled */ }
|
|
@@ -246,7 +247,7 @@ function writePack(pack) {
|
|
|
246
247
|
ensureDir();
|
|
247
248
|
const dir = path.join(PACKS_DIR, pack.dir);
|
|
248
249
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
249
|
-
|
|
250
|
+
atomicWriteFileSync(packFile(pack.dir), serialize(pack), { mode: 0o600, backup: true });
|
|
250
251
|
markIndexDirty();
|
|
251
252
|
return pack.dir;
|
|
252
253
|
}
|
|
@@ -313,7 +314,7 @@ function setProvenance(dir, sectionNames, origin) {
|
|
|
313
314
|
cur.ts = new Date().toISOString();
|
|
314
315
|
try {
|
|
315
316
|
fs.mkdirSync(path.join(PACKS_DIR, dir), { recursive: true, mode: 0o700 });
|
|
316
|
-
|
|
317
|
+
atomicWriteFileSync(provFile(dir), JSON.stringify(cur, null, 2), { mode: 0o600, backup: true });
|
|
317
318
|
} catch (e) {}
|
|
318
319
|
}
|
|
319
320
|
|
|
@@ -465,7 +466,7 @@ function archivePack(nameOrDir) {
|
|
|
465
466
|
ensureDir();
|
|
466
467
|
fs.mkdirSync(archiveRoot(), { recursive: true, mode: 0o700 });
|
|
467
468
|
pack.archived = new Date().toISOString();
|
|
468
|
-
try {
|
|
469
|
+
try { atomicWriteFileSync(packFile(pack.dir), serialize(pack), { mode: 0o600, backup: true }); } catch (e) {}
|
|
469
470
|
const dest = path.join(archiveRoot(), pack.dir);
|
|
470
471
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
471
472
|
fs.renameSync(path.join(PACKS_DIR, pack.dir), dest);
|
package/core/persona.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require("fs");
|
|
9
9
|
const path = require("path");
|
|
10
|
+
const { atomicWriteFileSync } = require("./fsutil");
|
|
10
11
|
|
|
11
12
|
const CONFIG_DIR = require("../config-dir");
|
|
12
13
|
const PERSONA_FILE = process.env.PERSONA_FILE ? path.resolve(process.env.PERSONA_FILE) : path.join(CONFIG_DIR, "persona.md");
|
|
@@ -40,7 +41,7 @@ function savePersona(text) {
|
|
|
40
41
|
throw new Error(`persona must be ${MIN_PERSONA_CHARS}-${MAX_PERSONA_CHARS} chars (got ${t.length})`);
|
|
41
42
|
}
|
|
42
43
|
fs.mkdirSync(path.dirname(PERSONA_FILE), { recursive: true, mode: 0o700 });
|
|
43
|
-
|
|
44
|
+
atomicWriteFileSync(PERSONA_FILE, t + "\n", { mode: 0o600, backup: true });
|
|
44
45
|
return PERSONA_FILE;
|
|
45
46
|
}
|
|
46
47
|
|
package/core/process-tree.js
CHANGED
|
@@ -28,13 +28,30 @@ function descendantPids(rootPid) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function killProcessTree(rootPid, signal = "SIGTERM") {
|
|
31
|
-
if (!rootPid) return;
|
|
31
|
+
if (!rootPid) return [];
|
|
32
32
|
const descendants = descendantPids(rootPid);
|
|
33
33
|
try { process.kill(-rootPid, signal); } catch (e) {}
|
|
34
34
|
try { process.kill(rootPid, signal); } catch (e) {}
|
|
35
35
|
for (const pid of descendants) {
|
|
36
36
|
try { process.kill(pid, signal); } catch (e) {}
|
|
37
37
|
}
|
|
38
|
+
return descendants;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
function pidExists(pid) {
|
|
42
|
+
try { process.kill(pid, 0); return true; }
|
|
43
|
+
catch (error) { return error.code === "EPERM"; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function waitForPidsExit(pids, timeoutMs = 1000) {
|
|
47
|
+
const pending = new Set((pids || []).map(Number).filter((pid) => Number.isSafeInteger(pid) && pid > 0));
|
|
48
|
+
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
|
|
49
|
+
while (pending.size) {
|
|
50
|
+
for (const pid of pending) if (!pidExists(pid)) pending.delete(pid);
|
|
51
|
+
if (!pending.size || Date.now() >= deadline) break;
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
53
|
+
}
|
|
54
|
+
return [...pending];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { descendantPids, killProcessTree, waitForPidsExit };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
defaultMigrationSources,
|
|
5
|
+
ensureMigrationSnapshot,
|
|
6
|
+
requireProviderRemovalReady,
|
|
7
|
+
} = require("./migration-backup");
|
|
8
|
+
|
|
9
|
+
function completeProviderMigration(options = {}) {
|
|
10
|
+
const runtime = options.runtime || require("./config");
|
|
11
|
+
const configDir = options.configDir || runtime.CONFIG_DIR;
|
|
12
|
+
const sources = options.sources || defaultMigrationSources({
|
|
13
|
+
configDir,
|
|
14
|
+
stateFile: options.stateFile || runtime.STATE_FILE,
|
|
15
|
+
sessionsFile: options.sessionsFile || runtime.SESSIONS_FILE,
|
|
16
|
+
jobsFile: options.jobsFile || runtime.JOBS_FILE,
|
|
17
|
+
cronsFile: options.cronsFile || runtime.CRONS_FILE,
|
|
18
|
+
});
|
|
19
|
+
const snapshot = options.snapshot || ensureMigrationSnapshot({ configDir, sources });
|
|
20
|
+
|
|
21
|
+
// Load persistence modules only after the byte-for-byte rollback snapshot is
|
|
22
|
+
// durable. Each save owns its prepare -> atomic write -> verify -> activate
|
|
23
|
+
// cycle, so an interrupted run resumes safely on the next startup.
|
|
24
|
+
const state = options.stateApi || require("./state");
|
|
25
|
+
const jobs = options.jobsApi || require("./jobs");
|
|
26
|
+
state.saveState({ strict: true });
|
|
27
|
+
state.saveSessions(state.loadSessions(), { strict: true });
|
|
28
|
+
jobs.ensureMigrated({ strict: true });
|
|
29
|
+
|
|
30
|
+
const ready = requireProviderRemovalReady({ configDir, sources });
|
|
31
|
+
return {
|
|
32
|
+
snapshotDir: ready.snapshotDir,
|
|
33
|
+
stage: ready.journal.stage,
|
|
34
|
+
components: ready.journal.components,
|
|
35
|
+
reusedSnapshot: snapshot.reused === true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { completeProviderMigration };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const config = require("./config");
|
|
4
|
+
const defaultRegistry = require("./providers");
|
|
5
|
+
|
|
6
|
+
function recoveryFor(provider, status) {
|
|
7
|
+
if (status.availability.state === "unconfigured") {
|
|
8
|
+
return `Install ${provider.label}, or configure ${provider.id === "claude" ? "CLAUDE_PATH" : "CODEX_PATH"}.`;
|
|
9
|
+
}
|
|
10
|
+
if (status.availability.state === "missing") {
|
|
11
|
+
return `Fix ${provider.id === "claude" ? "CLAUDE_PATH" : "CODEX_PATH"}, or remove it to use PATH discovery.`;
|
|
12
|
+
}
|
|
13
|
+
if (status.compatibility.state === "incompatible") {
|
|
14
|
+
return `Update ${provider.label}: ${status.compatibility.reason || "the installed CLI is incompatible"}`;
|
|
15
|
+
}
|
|
16
|
+
if (status.compatibility.state !== "compatible") {
|
|
17
|
+
return `Could not confirm ${provider.label} compatibility: ${status.compatibility.reason || "local probe failed"}`;
|
|
18
|
+
}
|
|
19
|
+
if (status.auth.state === "unauthenticated") return provider.authHelp();
|
|
20
|
+
if (status.auth.state === "unknown") {
|
|
21
|
+
return `Could not confirm ${provider.label} authentication: ${status.auth.reason || "local status probe failed"}`;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function inspectProviderConfiguration(options = {}) {
|
|
27
|
+
const registry = options.registry || defaultRegistry;
|
|
28
|
+
const preferred = options.defaultProvider === undefined
|
|
29
|
+
? config.DEFAULT_PROVIDER
|
|
30
|
+
: options.defaultProvider;
|
|
31
|
+
const executableStatus = options.executableStatus || ((providerId) => (
|
|
32
|
+
config.providerExecutableStatus(providerId, options.env || process.env)
|
|
33
|
+
));
|
|
34
|
+
const probeAuth = options.probeAuth !== false;
|
|
35
|
+
const providers = registry.listProviders().map((provider) => {
|
|
36
|
+
const executable = executableStatus(provider.id);
|
|
37
|
+
if (executable.state !== "available") {
|
|
38
|
+
const status = {
|
|
39
|
+
id: provider.id,
|
|
40
|
+
label: provider.label,
|
|
41
|
+
configured: executable.configured === true,
|
|
42
|
+
availability: {
|
|
43
|
+
state: executable.state,
|
|
44
|
+
executable: null,
|
|
45
|
+
reason: executable.reason,
|
|
46
|
+
},
|
|
47
|
+
compatibility: { state: "not_checked", version: null, reason: "Provider executable is unavailable" },
|
|
48
|
+
auth: { state: "not_checked", reason: "Provider executable is unavailable" },
|
|
49
|
+
};
|
|
50
|
+
return { ...status, recovery: recoveryFor(provider, status) };
|
|
51
|
+
}
|
|
52
|
+
const registryStatus = registry.providerStatus(provider.id, { probeAuth });
|
|
53
|
+
const status = {
|
|
54
|
+
...registryStatus,
|
|
55
|
+
configured: executable.configured === true,
|
|
56
|
+
availability: {
|
|
57
|
+
...registryStatus.availability,
|
|
58
|
+
executable: executable.executable || registryStatus.availability.executable,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
return { ...status, recovery: recoveryFor(provider, status) };
|
|
62
|
+
});
|
|
63
|
+
const selected = registry.selectDefaultProvider(preferred);
|
|
64
|
+
const selectedProvider = selected.providerId
|
|
65
|
+
? providers.find((provider) => provider.id === selected.providerId)
|
|
66
|
+
: null;
|
|
67
|
+
return {
|
|
68
|
+
providers,
|
|
69
|
+
defaultProvider: {
|
|
70
|
+
...selected,
|
|
71
|
+
label: selectedProvider?.label || null,
|
|
72
|
+
configuredValue: preferred || null,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
inspectProviderConfiguration,
|
|
79
|
+
recoveryFor,
|
|
80
|
+
};
|