@inetafrica/open-claudia 2.6.38 → 2.6.40
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/core/dream.js +39 -5
- package/core/handlers.js +8 -0
- package/core/pack-review.js +47 -5
- package/core/packs.js +30 -1
- package/core/runner.js +36 -2
- package/core/state.js +3 -0
- package/package.json +4 -3
- package/test-pack-nesting.js +100 -0
package/core/dream.js
CHANGED
|
@@ -149,7 +149,7 @@ Your job — decide what consolidation, if any, is warranted:
|
|
|
149
149
|
|
|
150
150
|
1. merges: packs that are clearly the SAME topic under different names get merged into one. Supply the merged Stance/Procedure/State (synthesised, not concatenated; null leaves the target's section alone) and a one-sentence journal note. Be conservative: when in doubt, do not merge. A Stance marked USER-AUTHORED is off-limits — leave its stance null; the system preserves it verbatim regardless. Packs marked [ABILITY] are reusable cross-project how-tos, NOT project trackers: only ever merge an [ABILITY] into another [ABILITY] (the merged result must stay an ability), and never merge a project pack into an [ABILITY] or an [ABILITY] into a project pack — that would lose the reusable how-to or pollute it with one project's specifics.
|
|
151
151
|
2. umbrellas: when 3+ packs are siblings under one theme, create an umbrella pack whose State is a 3-6 line map of the family ("for X see pack Y"), and list its children. The umbrella is a router, not a duplicate.
|
|
152
|
-
3. parents: assign an existing pack as parent of another (sub-topic relationship) without creating anything.
|
|
152
|
+
3. parents: assign an existing pack as parent of another (sub-topic relationship) without creating anything. NOTE: a separate deterministic pass already auto-files packs whose slug is a prefix-child of another (e.g. foo-bar under foo), so DON'T spend a parents op on those — reserve parents (and umbrellas) for semantic groupings the slug prefix can't catch (differently-named siblings of one theme).
|
|
153
153
|
4. retag: tighten descriptions and tags. The router FTS-matches incoming messages against name/description/tags, so generic words there cause false matches. Descriptions should be one specific line; tags specific nouns.
|
|
154
154
|
5. entity_merges: the same real-world entity recorded twice gets merged (the better slug wins).
|
|
155
155
|
6. entity_notes: rewrite an entity's Notes to be current and cross-linked — mention related packs as [[pack-dir]] and related entities by name.
|
|
@@ -700,6 +700,33 @@ function staleTaskReport() {
|
|
|
700
700
|
} catch (e) { return ""; }
|
|
701
701
|
}
|
|
702
702
|
|
|
703
|
+
// Deterministic hierarchy backfill. Files every orphan context pack under the
|
|
704
|
+
// LONGEST existing context pack whose dir is a slug-prefix of it (kazee-people-x
|
|
705
|
+
// → kazee-people). Runs every dream AFTER the model's own parents/umbrellas
|
|
706
|
+
// decision, so it only mops up the obvious prefix families the model left flat.
|
|
707
|
+
// Prefix parents are always shorter, so this can't cycle; never overrides a
|
|
708
|
+
// parent already set (by user, reviewer, or this run's model). Backs up each
|
|
709
|
+
// pack it moves. Returns per-parent summary lines + a count.
|
|
710
|
+
function nestPrefixFamilies(backupRoot) {
|
|
711
|
+
const all = packs.listPacks();
|
|
712
|
+
const contextDirs = all.filter((p) => p.kind !== "ability").map((p) => p.dir);
|
|
713
|
+
const byParent = new Map();
|
|
714
|
+
for (const p of all) {
|
|
715
|
+
if (p.kind === "ability" || p.parent) continue;
|
|
716
|
+
const parent = packs.parentByPrefix(p.dir, contextDirs);
|
|
717
|
+
if (!parent || !packs.readPack(parent) || wouldCycle(p.dir, parent)) continue;
|
|
718
|
+
try {
|
|
719
|
+
if (backupRoot) backupPack(p.dir, backupRoot);
|
|
720
|
+
packs.setParent(p.dir, parent);
|
|
721
|
+
byParent.set(parent, (byParent.get(parent) || 0) + 1);
|
|
722
|
+
} catch (e) { console.warn(`[dream] nest failed for ${p.dir}: ${e.message}`); }
|
|
723
|
+
}
|
|
724
|
+
const lines = [...byParent.entries()].map(([parent, n]) =>
|
|
725
|
+
`🌳 Filed ${n} sub-pack${n === 1 ? "" : "s"} under ${parent}`);
|
|
726
|
+
const filed = [...byParent.values()].reduce((a, b) => a + b, 0);
|
|
727
|
+
return { lines, filed };
|
|
728
|
+
}
|
|
729
|
+
|
|
703
730
|
async function runDream({ trigger = "manual" } = {}) {
|
|
704
731
|
if (!enabled()) return { skipped: "dream is disabled (DREAM=off)" };
|
|
705
732
|
if (_dreaming) return { skipped: "a dream is already in progress" };
|
|
@@ -720,7 +747,14 @@ async function runDream({ trigger = "manual" } = {}) {
|
|
|
720
747
|
|
|
721
748
|
const backupRoot = makeBackupRoot();
|
|
722
749
|
const applied = applyDream(decision, backupRoot);
|
|
723
|
-
|
|
750
|
+
// Deterministic hierarchy backfill (kazee-people-* → kazee-people), after the
|
|
751
|
+
// model's parents/umbrellas decision so it only fills the obvious prefix
|
|
752
|
+
// families left flat. Backed up + announced like everything else.
|
|
753
|
+
let nestLines = [];
|
|
754
|
+
try { nestLines = nestPrefixFamilies(backupRoot).lines; }
|
|
755
|
+
catch (e) { console.warn(`[dream] nesting pass failed: ${e.message}`); }
|
|
756
|
+
const consolidationLines = applied.concat(nestLines);
|
|
757
|
+
const report = decision.report || (consolidationLines.length > 0 ? "Tidied up my memory overnight." : "");
|
|
724
758
|
|
|
725
759
|
// Phase 2: self-improvement introspection. Reviews the day's seeds, reads
|
|
726
760
|
// its own code/memory READ-ONLY (allowedTools whitelist), promotes lessons
|
|
@@ -767,11 +801,11 @@ async function runDream({ trigger = "manual" } = {}) {
|
|
|
767
801
|
} catch (e) { /* graph is best-effort */ }
|
|
768
802
|
|
|
769
803
|
const staleNote = staleTaskReport();
|
|
770
|
-
const dreamLines =
|
|
804
|
+
const dreamLines = consolidationLines.concat(introApplied);
|
|
771
805
|
|
|
772
806
|
const reportPath = writeDreamReport({
|
|
773
807
|
model: DREAM_MODEL, effort: DREAM_EFFORT, trigger,
|
|
774
|
-
consolidation: { report: decision.report, lines:
|
|
808
|
+
consolidation: { report: decision.report, lines: consolidationLines },
|
|
775
809
|
introspection: { report: introReport, lines: introApplied, proposed: introProposed },
|
|
776
810
|
graphNote, staleNote, backupRoot,
|
|
777
811
|
});
|
|
@@ -826,7 +860,7 @@ function initDream(adapters) {
|
|
|
826
860
|
}
|
|
827
861
|
|
|
828
862
|
module.exports = {
|
|
829
|
-
runDream, initDream, buildDreamPrompt, parseDream, applyDream, manageAbilityTiers,
|
|
863
|
+
runDream, initDream, buildDreamPrompt, parseDream, applyDream, manageAbilityTiers, nestPrefixFamilies,
|
|
830
864
|
buildIntrospectionPrompt, parseIntrospection, applyIntrospection, writeDreamReport,
|
|
831
865
|
enabled, summaryEnabled, introspectEnabled, selfApplyEnabled,
|
|
832
866
|
DREAM_CRON, DREAM_MODEL, DREAM_EFFORT, PROMOTE_MIN_PROJECTS,
|
package/core/handlers.js
CHANGED
|
@@ -966,8 +966,16 @@ register({
|
|
|
966
966
|
setTimeout(() => killProcessTree(pid, "SIGKILL"), 3000);
|
|
967
967
|
state.runningProcess = null;
|
|
968
968
|
if (state.streamInterval) clearTimeout(state.streamInterval);
|
|
969
|
+
if (state.typingHeartbeat) { clearInterval(state.typingHeartbeat); state.typingHeartbeat = null; }
|
|
969
970
|
state.messageQueue = [];
|
|
970
971
|
await send("Cancelled.");
|
|
972
|
+
} else if (state.preparingRun) {
|
|
973
|
+
// Turn is mid-recall/compaction — no process to kill yet. Flag it so
|
|
974
|
+
// runClaude bails at its pre-spawn checkpoint, and stop typing now.
|
|
975
|
+
state.cancelRequested = true;
|
|
976
|
+
state.messageQueue = [];
|
|
977
|
+
if (state.typingHeartbeat) { clearInterval(state.typingHeartbeat); state.typingHeartbeat = null; }
|
|
978
|
+
await send("Cancelled.");
|
|
971
979
|
} else await send("Nothing running.");
|
|
972
980
|
},
|
|
973
981
|
});
|
package/core/pack-review.js
CHANGED
|
@@ -14,8 +14,11 @@ const packGuard = require("./pack-guard");
|
|
|
14
14
|
const { redactSensitive } = require("./redact");
|
|
15
15
|
|
|
16
16
|
const MIN_TURN_CHARS = 400;
|
|
17
|
-
const MAX_TEXT_CHARS =
|
|
18
|
-
|
|
17
|
+
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";
|
|
19
22
|
const MAX_ACTIONS = 2;
|
|
20
23
|
const MAX_ENTITY_ACTIONS = 3;
|
|
21
24
|
// Lessons are precious always-on budget — at most one promotion per turn,
|
|
@@ -111,7 +114,7 @@ Decide. Rules:
|
|
|
111
114
|
- Bias toward action: if the turn did real work on an identifiable topic (a named system, project, server, app, person, or domain), record it — at minimum a journal line. Most working turns deserve one. Reserve empty actions for small talk, pure status checks, and turns that contain nothing new.
|
|
112
115
|
- UPDATE an existing pack when the turn touched that topic: append a journal line, and rewrite State if where-things-stand changed. Update Stance when the user expressed a lasting preference or rule; Procedure when a verified working method emerged.
|
|
113
116
|
- PROMOTE durable conclusions out of the Journal. A confirmed root-cause diagnosis, an established fact, or a settled design decision must go into Stance/Procedure/State (the parts always injected in full), NOT only a Journal line — Journal is truncated to the last few lines on injection, so a conclusion left only there will silently fall out of recall over time.
|
|
114
|
-
- CREATE a pack when the turn worked on a durable topic
|
|
117
|
+
- CREATE a pack ONLY when the turn worked on a durable topic that NO existing pack covers. Before creating, scan the existing packs above for one already about this project/system/domain — if one exists, UPDATE it instead. A new pack must be a genuinely DISTINCT topic, never a narrower facet, version, release, sub-feature, phase, or component of a topic an existing pack already owns. In particular: a release or version of a project (e.g. a pack named after "<project> v2.6.39") is a Journal line + State update on that project's EXISTING pack — NEVER its own pack. Likewise sub-features of one system belong in that system's pack, not in siblings. When unsure whether something is a new topic or a facet of an existing one, treat it as a facet and UPDATE. Do not create packs for one-off trivia.
|
|
115
118
|
- ABILITY vs CONTEXT: most creates are context packs (a project/system tracker). Create an ability (kind:"ability") only for a genuinely reusable how-to as defined above, and always set learned_on + applied_on to the originating project dir. When a turn re-used an EXISTING ability on a NEW project, do NOT create anything — UPDATE that ability and put the new project dir in "applied_on" so the cross-project reuse is recorded.
|
|
116
119
|
- Never store secrets, tokens, passwords, or credentials.
|
|
117
120
|
- State should be concise (under 150 words). Journal entries one sentence. Never start a journal or log sentence with a date — dates are prepended automatically.
|
|
@@ -164,6 +167,28 @@ function guardCheck(a) {
|
|
|
164
167
|
return hit;
|
|
165
168
|
}
|
|
166
169
|
|
|
170
|
+
// Deterministic dedupe backstop for the create path. A cheap model tends to
|
|
171
|
+
// spawn a fresh pack for each VERSION/RELEASE of a project an existing pack
|
|
172
|
+
// already owns ("<project> v2.6.39"); this folds such a proposal into the
|
|
173
|
+
// project pack instead of spawning a sibling. Scoped tightly: only context
|
|
174
|
+
// packs, only a *versioned* proposal whose name/description strongly matches an
|
|
175
|
+
// existing pack. NOTE: a non-versioned prefix-child (kazee-people-leave under
|
|
176
|
+
// kazee-people) is NOT folded here — it is filed as a sub-pack under its parent
|
|
177
|
+
// at creation (see birth-time parenting in applyAction), which keeps sub-area
|
|
178
|
+
// detail as its own node instead of collapsing it into the parent's State.
|
|
179
|
+
function findDuplicatePack({ dir, name, description, tags }) {
|
|
180
|
+
const tagStr = Array.isArray(tags) ? tags.join(" ") : "";
|
|
181
|
+
const text = `${name || ""} ${description || ""} ${tagStr}`.trim();
|
|
182
|
+
const versioned = /\bv?\d+\.\d+(?:\.\d+)?\b|\brelease\b|\bv\d+\b/i.test(text);
|
|
183
|
+
if (!versioned) return null;
|
|
184
|
+
const slug = packs.slugify(dir || name || "");
|
|
185
|
+
const ctx = new Set(packs.listPacks().filter((p) => p.kind !== "ability").map((p) => p.dir));
|
|
186
|
+
for (const m of packs.matchPacks(text, { limit: 3, threshold: 4 })) {
|
|
187
|
+
if (m.dir !== slug && ctx.has(m.dir)) return m.dir;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
167
192
|
function applyAction(a) {
|
|
168
193
|
if (!a || typeof a !== "object") return null;
|
|
169
194
|
const guard = guardCheck(a);
|
|
@@ -202,11 +227,28 @@ function applyAction(a) {
|
|
|
202
227
|
if (kind === "ability") for (const proj of projects) packs.recordApplied(dir, proj);
|
|
203
228
|
return { kind: "update", dir, name: a.name || dir, note: a.journal || "state updated" };
|
|
204
229
|
}
|
|
230
|
+
// Deterministic dedupe: a versioned/facet proposal folds INTO the project
|
|
231
|
+
// pack it belongs to rather than spawning a sibling (abilities are exempt —
|
|
232
|
+
// they are activity-named and cross-project by design).
|
|
233
|
+
const dupeDir = kind === "context" ? findDuplicatePack({ dir, name: a.name, description: a.description, tags: a.tags }) : null;
|
|
234
|
+
if (dupeDir && packs.readPack(dupeDir)) {
|
|
235
|
+
packs.updatePack(dupeDir, { journal: a.journal || "", state: a.state || "" }, "reviewer");
|
|
236
|
+
const ex = packs.readPack(dupeDir);
|
|
237
|
+
return { kind: "update", dir: dupeDir, name: (ex && ex.name) || dupeDir, note: a.journal || "folded a new-pack proposal into the existing pack" };
|
|
238
|
+
}
|
|
239
|
+
// Birth-time hierarchy: a new context pack whose slug is a prefix-child of an
|
|
240
|
+
// existing pack (kazee-people-leave → kazee-people) is filed under it at
|
|
241
|
+
// creation, so families never accumulate flat at the top level. Abilities are
|
|
242
|
+
// cross-project and never prefix-nested.
|
|
243
|
+
const parentDir = kind === "context"
|
|
244
|
+
? packs.parentByPrefix(dir, packs.listPacks().filter((p) => p.kind !== "ability").map((p) => p.dir))
|
|
245
|
+
: null;
|
|
205
246
|
const pack = packs.createPack({
|
|
206
247
|
dir,
|
|
207
248
|
name: a.name,
|
|
208
249
|
description: a.description,
|
|
209
250
|
tags: Array.isArray(a.tags) ? a.tags.slice(0, 6) : [],
|
|
251
|
+
parent: parentDir && packs.readPack(parentDir) ? parentDir : null,
|
|
210
252
|
stance: a.stance || "",
|
|
211
253
|
procedure: a.procedure || "",
|
|
212
254
|
state: a.state || "",
|
|
@@ -215,7 +257,7 @@ function applyAction(a) {
|
|
|
215
257
|
learned_on: kind === "ability" ? learned_on : "",
|
|
216
258
|
applied_on: kind === "ability" ? projects : [],
|
|
217
259
|
}, "reviewer");
|
|
218
|
-
return { kind: "create", dir: pack.dir, name: pack.name, note: a.description || "", ability: kind === "ability", learned_on: pack.learned_on };
|
|
260
|
+
return { kind: "create", dir: pack.dir, name: pack.name, note: a.description || "", ability: kind === "ability", learned_on: pack.learned_on, parent: pack.parent };
|
|
219
261
|
}
|
|
220
262
|
return null;
|
|
221
263
|
}
|
|
@@ -278,7 +320,7 @@ function reviewTurn({ userText, assistantText, channelId, announce }) {
|
|
|
278
320
|
} else if (r && r.kind === "create" && r.ability) {
|
|
279
321
|
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}`);
|
|
280
322
|
} else if (r && r.kind === "create") {
|
|
281
|
-
lines.push(`📦 New pack: ${r.name}\n${clipWords(r.note, 180)}\n↳ open-claudia pack show ${r.dir}`);
|
|
323
|
+
lines.push(`📦 New pack: ${r.name}${r.parent ? ` (filed under ${r.parent})` : ""}\n${clipWords(r.note, 180)}\n↳ open-claudia pack show ${r.dir}`);
|
|
282
324
|
} else if (r) {
|
|
283
325
|
lines.push(`✏️ ${r.name} — ${clipWords(r.note, 180)}${r.appliedTo ? ` 🧩 (now also applies to ${r.appliedTo})` : ""}\n↳ open-claudia pack show ${r.dir}`);
|
|
284
326
|
}
|
package/core/packs.js
CHANGED
|
@@ -171,6 +171,35 @@ function setKind(nameOrDir, kind) {
|
|
|
171
171
|
return pack;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// The natural parent of a pack within a slug family: the LONGEST existing dir
|
|
175
|
+
// that is a hyphen-boundary prefix of `dir` (kazee-people-mobile-x →
|
|
176
|
+
// kazee-people if it exists, else kazee). Pure — the caller supplies candidate
|
|
177
|
+
// dirs (so it can scope to context packs / exclude self). A strict prefix is
|
|
178
|
+
// always shorter than the slug, so a prefix parent can never be a descendant:
|
|
179
|
+
// nesting by prefix cannot create a cycle.
|
|
180
|
+
function parentByPrefix(dir, candidateDirs) {
|
|
181
|
+
const slug = slugify(dir);
|
|
182
|
+
if (!slug) return null;
|
|
183
|
+
let best = null;
|
|
184
|
+
for (const d of candidateDirs || []) {
|
|
185
|
+
if (!d || d === slug) continue;
|
|
186
|
+
if (slug.startsWith(d + "-") && (!best || d.length > best.length)) best = d;
|
|
187
|
+
}
|
|
188
|
+
return best;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Structural setter for a pack's parent (hierarchy). Separate from updatePack:
|
|
192
|
+
// parent is frontmatter metadata, not an authored content section, so it carries
|
|
193
|
+
// no provenance and never touches Stance/State/etc. Pass null/"" to un-parent.
|
|
194
|
+
function setParent(nameOrDir, parentDir) {
|
|
195
|
+
const pack = findPack(nameOrDir);
|
|
196
|
+
if (!pack) return null;
|
|
197
|
+
pack.parent = parentDir ? slugify(parentDir) : null;
|
|
198
|
+
pack.updated = new Date().toISOString();
|
|
199
|
+
writePack(pack);
|
|
200
|
+
return pack;
|
|
201
|
+
}
|
|
202
|
+
|
|
174
203
|
// Record that an ability was applied on a project (provenance of reuse): appends
|
|
175
204
|
// to applied_on (deduped) and sets learned_on to the first project if unset.
|
|
176
205
|
function recordApplied(nameOrDir, project) {
|
|
@@ -562,7 +591,7 @@ function matchPacks(text, { limit = 3, threshold = null } = {}) {
|
|
|
562
591
|
module.exports = {
|
|
563
592
|
PACKS_DIR, SECTIONS, slugify,
|
|
564
593
|
listPacks, findPack, readPack, writePack, createPack, updatePack, removePack,
|
|
565
|
-
listSkillPacks, setSkill, listAbilities, setKind, recordApplied, recordCoUse,
|
|
594
|
+
listSkillPacks, setSkill, listAbilities, setKind, parentByPrefix, setParent, recordApplied, recordCoUse,
|
|
566
595
|
touchUsed, packNameFromPath, matchPacks, reindex, markIndexDirty,
|
|
567
596
|
archivePack, restorePack, listArchived,
|
|
568
597
|
readProvenance, provenanceOf, setProvenance, recordForegroundWrite,
|
package/core/runner.js
CHANGED
|
@@ -794,7 +794,21 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
794
794
|
resumeSessionId: opts.resumeSessionId || null,
|
|
795
795
|
}, state, getActiveSessionId);
|
|
796
796
|
|
|
797
|
-
|
|
797
|
+
// Typing heartbeat: keep the indicator alive continuously from message
|
|
798
|
+
// receipt through the recall/discoverer phase and into streaming, not just
|
|
799
|
+
// for the ~5s a single typing action lasts. Stored on state so /stop can
|
|
800
|
+
// clear it instantly. Cleared on close/error/cancel.
|
|
801
|
+
const startTyping = () => {
|
|
802
|
+
if (!adapter || !channelId) return;
|
|
803
|
+
adapter.typing(channelId).catch(() => {});
|
|
804
|
+
if (!state.typingHeartbeat) state.typingHeartbeat = setInterval(() => adapter.typing(channelId).catch(() => {}), 4000);
|
|
805
|
+
};
|
|
806
|
+
const stopTyping = () => { if (state.typingHeartbeat) { clearInterval(state.typingHeartbeat); state.typingHeartbeat = null; } };
|
|
807
|
+
// Pre-spawn window (recall/compaction/auth): /stop has no process to kill, so
|
|
808
|
+
// it sets state.cancelRequested and we bail at the checkpoint before spawning.
|
|
809
|
+
state.preparingRun = true;
|
|
810
|
+
state.cancelRequested = false;
|
|
811
|
+
startTyping();
|
|
798
812
|
state.statusMessageId = null;
|
|
799
813
|
state.streamBuffer = "";
|
|
800
814
|
let assistantText = "";
|
|
@@ -883,7 +897,14 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
883
897
|
} catch (e) { /* announcements are best-effort */ }
|
|
884
898
|
};
|
|
885
899
|
|
|
886
|
-
|
|
900
|
+
let args;
|
|
901
|
+
try {
|
|
902
|
+
args = await buildClaudeArgs(prompt, opts);
|
|
903
|
+
} catch (e) {
|
|
904
|
+
state.preparingRun = false;
|
|
905
|
+
stopTyping();
|
|
906
|
+
throw e;
|
|
907
|
+
}
|
|
887
908
|
// Recall announcements are now fired at READ time, not injection time:
|
|
888
909
|
// matched packs/entities enter context only as small headlines (see
|
|
889
910
|
// system-prompt.js recallHeadline). The "📖 Recalled my notes on …" line is
|
|
@@ -902,6 +923,14 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
902
923
|
else if (r.gated) send(`🧠 <b>Recall</b> (${esc(r.engine)}): skipped by pre-gate — trivial turn.`).catch(() => {});
|
|
903
924
|
}
|
|
904
925
|
} catch (e) { /* best-effort */ }
|
|
926
|
+
// /stop landed during the pre-spawn window (recall/compaction): bail before
|
|
927
|
+
// spawning. The /stop handler already acknowledged in chat, so stay silent.
|
|
928
|
+
if (state.cancelRequested) {
|
|
929
|
+
state.cancelRequested = false;
|
|
930
|
+
state.preparingRun = false;
|
|
931
|
+
stopTyping();
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
905
934
|
const binaryPath = getActiveBinary();
|
|
906
935
|
const proc = spawn(binaryPath, args, {
|
|
907
936
|
cwd,
|
|
@@ -911,6 +940,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
911
940
|
});
|
|
912
941
|
|
|
913
942
|
state.runningProcess = proc;
|
|
943
|
+
state.preparingRun = false;
|
|
914
944
|
const startTime = Date.now();
|
|
915
945
|
let longRunningNotified = false;
|
|
916
946
|
|
|
@@ -1094,6 +1124,8 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1094
1124
|
|
|
1095
1125
|
proc.on("close", (code) => chatContext.run(store, async () => {
|
|
1096
1126
|
state.runningProcess = null;
|
|
1127
|
+
state.preparingRun = false;
|
|
1128
|
+
stopTyping();
|
|
1097
1129
|
clearTimeout(state.streamInterval); state.streamInterval = null;
|
|
1098
1130
|
clearTimeout(processTimeout);
|
|
1099
1131
|
|
|
@@ -1240,6 +1272,8 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1240
1272
|
|
|
1241
1273
|
proc.on("error", (err) => chatContext.run(store, async () => {
|
|
1242
1274
|
state.runningProcess = null;
|
|
1275
|
+
state.preparingRun = false;
|
|
1276
|
+
stopTyping();
|
|
1243
1277
|
clearTimeout(state.streamInterval);
|
|
1244
1278
|
clearTimeout(processTimeout);
|
|
1245
1279
|
await send(`Error: ${err.message}`);
|
package/core/state.js
CHANGED
|
@@ -59,6 +59,9 @@ function createUserState(userId) {
|
|
|
59
59
|
channelId: currentChannelId(),
|
|
60
60
|
currentSession: saved.currentSession || null,
|
|
61
61
|
runningProcess: null,
|
|
62
|
+
preparingRun: false,
|
|
63
|
+
cancelRequested: false,
|
|
64
|
+
typingHeartbeat: null,
|
|
62
65
|
statusMessageId: null,
|
|
63
66
|
streamBuffer: "",
|
|
64
67
|
streamInterval: null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.40",
|
|
4
4
|
"description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"setup": "node setup.js",
|
|
11
11
|
"start": "node bot.js",
|
|
12
|
-
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js"
|
|
12
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bot.js",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"test-ability-transfer.js",
|
|
42
42
|
"test-ability-tiers.js",
|
|
43
43
|
"test-ability-merge-guard.js",
|
|
44
|
-
"test-learning-e2e.js"
|
|
44
|
+
"test-learning-e2e.js",
|
|
45
|
+
"test-pack-nesting.js"
|
|
45
46
|
],
|
|
46
47
|
"keywords": [
|
|
47
48
|
"claude",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Hierarchy nesting: the deterministic prefix-family pass that keeps pack
|
|
2
|
+
// families (kazee-people-*) filed under their parent instead of sprawling flat
|
|
3
|
+
// at the top level. Covers the pure selector (parentByPrefix), the structural
|
|
4
|
+
// setter (setParent), reviewer birth-time parenting (applyAction create), the
|
|
5
|
+
// dream backfill (nestPrefixFamilies), and the versioned-only fold guard.
|
|
6
|
+
const assert = require("assert");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
11
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pack-nesting-"));
|
|
12
|
+
process.env.PACKS_DIR = path.join(tmp, "packs");
|
|
13
|
+
|
|
14
|
+
const packs = require("./core/packs");
|
|
15
|
+
const review = require("./core/pack-review");
|
|
16
|
+
const dream = require("./core/dream");
|
|
17
|
+
|
|
18
|
+
// ── parentByPrefix: pure longest-prefix selection ──
|
|
19
|
+
assert.strictEqual(
|
|
20
|
+
packs.parentByPrefix("kazee-people-leave-display", ["kazee", "kazee-people", "other"]),
|
|
21
|
+
"kazee-people",
|
|
22
|
+
"longest matching prefix wins"
|
|
23
|
+
);
|
|
24
|
+
assert.strictEqual(packs.parentByPrefix("kazee", ["kazee"]), null, "a pack is never its own parent");
|
|
25
|
+
assert.strictEqual(packs.parentByPrefix("kazeebot", ["kazee"]), null, "prefix must end on a dash boundary");
|
|
26
|
+
assert.strictEqual(packs.parentByPrefix("orphan", ["kazee", "kazee-people"]), null, "no prefix → no parent");
|
|
27
|
+
assert.strictEqual(packs.parentByPrefix("anything", []), null, "no candidates → null");
|
|
28
|
+
|
|
29
|
+
// ── setParent: structural setter writes + clears frontmatter ──
|
|
30
|
+
packs.createPack({ dir: "np-root", name: "NP Root", description: "a root" });
|
|
31
|
+
packs.createPack({ dir: "np-solo", name: "NP Solo", description: "a leaf" });
|
|
32
|
+
packs.setParent("np-solo", "np-root");
|
|
33
|
+
assert.strictEqual(packs.readPack("np-solo").parent, "np-root", "setParent persists parent");
|
|
34
|
+
packs.setParent("np-solo", null);
|
|
35
|
+
assert.strictEqual(packs.readPack("np-solo").parent, null, "setParent(null) un-parents");
|
|
36
|
+
|
|
37
|
+
// ── reviewer birth-time parenting: a new prefix-child files under its parent ──
|
|
38
|
+
packs.createPack({ dir: "kazee", name: "Kazee", description: "the platform" });
|
|
39
|
+
packs.createPack({ dir: "kazee-people", name: "Kazee People", description: "the HR module" });
|
|
40
|
+
const born = review.applyAction({
|
|
41
|
+
action: "create", dir: "kazee-people-payroll", name: "Kazee People Payroll",
|
|
42
|
+
description: "payroll inside the HR module", state: "started",
|
|
43
|
+
});
|
|
44
|
+
assert.strictEqual(born.kind, "create", "a fresh prefix-child is created, not folded");
|
|
45
|
+
assert.strictEqual(born.parent, "kazee-people", "birth-time parent is the LONGEST prefix (kazee-people, not kazee)");
|
|
46
|
+
assert.strictEqual(packs.readPack("kazee-people-payroll").parent, "kazee-people", "parent persisted to disk");
|
|
47
|
+
|
|
48
|
+
const orphanBorn = review.applyAction({
|
|
49
|
+
action: "create", dir: "standalone-thing", name: "Standalone Thing",
|
|
50
|
+
description: "belongs to no family", state: "started",
|
|
51
|
+
});
|
|
52
|
+
assert.strictEqual(orphanBorn.parent, null, "a pack with no prefix-parent stays top-level");
|
|
53
|
+
|
|
54
|
+
// abilities are cross-project and must never be prefix-nested
|
|
55
|
+
const abilityBorn = review.applyAction({
|
|
56
|
+
action: "create", dir: "kazee-people-export", name: "Kazee People Export",
|
|
57
|
+
description: "reusable export how-to", kind: "ability", learned_on: "kazee",
|
|
58
|
+
});
|
|
59
|
+
assert.strictEqual(abilityBorn.parent, null, "an ability is never prefix-nested");
|
|
60
|
+
|
|
61
|
+
// ── dream.nestPrefixFamilies: backfills existing flat families ──
|
|
62
|
+
// Three flat siblings + the root, plus an ability that must be left alone.
|
|
63
|
+
packs.createPack({ dir: "billing", name: "Billing", description: "billing system" });
|
|
64
|
+
packs.createPack({ dir: "billing-invoices", name: "Billing Invoices", description: "invoice generation" });
|
|
65
|
+
packs.createPack({ dir: "billing-refunds", name: "Billing Refunds", description: "refund flow" });
|
|
66
|
+
packs.createPack({ dir: "billing-cli", name: "Billing CLI", description: "reusable billing CLI", kind: "ability", learned_on: "billing" });
|
|
67
|
+
|
|
68
|
+
const res = dream.nestPrefixFamilies(null); // null backupRoot → skip backups in test
|
|
69
|
+
assert.strictEqual(packs.readPack("billing-invoices").parent, "billing", "flat sibling filed under root");
|
|
70
|
+
assert.strictEqual(packs.readPack("billing-refunds").parent, "billing", "second flat sibling filed under root");
|
|
71
|
+
assert.strictEqual(packs.readPack("billing").parent, null, "the family root stays top-level");
|
|
72
|
+
assert.strictEqual(packs.readPack("billing-cli").parent, null, "the billing ability is NOT nested");
|
|
73
|
+
assert.ok(res.filed >= 2, "nest pass reports what it filed");
|
|
74
|
+
assert.ok(res.lines.some((l) => l.includes("billing")), "nest pass announces the family it filed");
|
|
75
|
+
|
|
76
|
+
// running it again is a no-op (idempotent — everything already parented)
|
|
77
|
+
const again = dream.nestPrefixFamilies(null);
|
|
78
|
+
assert.strictEqual(again.filed, 0, "second pass files nothing new");
|
|
79
|
+
|
|
80
|
+
// ── versioned-only fold (FTS-gated): a versioned proposal folds into its pack,
|
|
81
|
+
// a non-versioned near-duplicate does NOT. Skipped when node:sqlite is absent. ──
|
|
82
|
+
if (packs.reindex()) {
|
|
83
|
+
packs.createPack({ dir: "payroll-tax-filing", name: "Payroll Tax Filing", description: "quarterly payroll tax filing" });
|
|
84
|
+
packs.reindex();
|
|
85
|
+
const folded = review.applyAction({
|
|
86
|
+
action: "create", dir: "payroll-tax-filing-v2", name: "Payroll Tax Filing v2 release",
|
|
87
|
+
description: "payroll tax filing notes", state: "v2 shipped",
|
|
88
|
+
});
|
|
89
|
+
assert.strictEqual(folded.kind, "update", "a versioned near-dup folds into the existing pack");
|
|
90
|
+
assert.strictEqual(folded.dir, "payroll-tax-filing", "fold targets the existing pack");
|
|
91
|
+
|
|
92
|
+
const notFolded = review.applyAction({
|
|
93
|
+
action: "create", dir: "payroll-tax-summary", name: "Payroll Tax Summary",
|
|
94
|
+
description: "payroll tax filing summary view", state: "started",
|
|
95
|
+
});
|
|
96
|
+
assert.strictEqual(notFolded.kind, "create", "a non-versioned near-dup is a real new pack, not a fold");
|
|
97
|
+
console.log("pack nesting OK (incl. versioned-fold)");
|
|
98
|
+
} else {
|
|
99
|
+
console.log("pack nesting OK (versioned-fold skipped — no node:sqlite)");
|
|
100
|
+
}
|