@inetafrica/open-claudia 2.6.38 → 2.6.39
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/handlers.js +8 -0
- package/core/pack-review.js +44 -3
- package/core/runner.js +36 -2
- package/core/state.js +3 -0
- package/package.json +1 -1
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,35 @@ function guardCheck(a) {
|
|
|
164
167
|
return hit;
|
|
165
168
|
}
|
|
166
169
|
|
|
170
|
+
// Deterministic dedupe backstop for the create path. Even with the prompt rule,
|
|
171
|
+
// a cheap model tends to spawn a fresh pack for each version/release/facet of a
|
|
172
|
+
// project an existing pack already owns. This catches two robust signals — a
|
|
173
|
+
// slug that is a hyphen-boundary prefix/extension of an existing pack dir, and a
|
|
174
|
+
// version-named proposal whose name/description strongly matches an existing
|
|
175
|
+
// pack — and the caller folds the proposal into that pack instead. Conservative
|
|
176
|
+
// by design (only context packs; only a clear prefix relationship or a versioned
|
|
177
|
+
// proposal with a strong text match) so genuinely distinct topics still get their
|
|
178
|
+
// own pack. Mirrors the structural "no lesson without a trigger" enforcement.
|
|
179
|
+
function findDuplicatePack({ dir, name, description, tags }) {
|
|
180
|
+
const slug = packs.slugify(dir || name || "");
|
|
181
|
+
if (!slug) return null;
|
|
182
|
+
const contextDirs = packs.listPacks().filter((p) => p.kind !== "ability").map((p) => p.dir);
|
|
183
|
+
for (const d of contextDirs) {
|
|
184
|
+
if (d === slug) continue;
|
|
185
|
+
if (slug.startsWith(d + "-") || d.startsWith(slug + "-")) return d;
|
|
186
|
+
}
|
|
187
|
+
const tagStr = Array.isArray(tags) ? tags.join(" ") : "";
|
|
188
|
+
const versioned = /\bv?\d+\.\d+(?:\.\d+)?\b|\brelease\b|\bv\d+\b/i.test(`${slug} ${name || ""} ${description || ""} ${tagStr}`);
|
|
189
|
+
if (versioned) {
|
|
190
|
+
const ctx = new Set(contextDirs);
|
|
191
|
+
const text = `${name || ""} ${description || ""} ${tagStr}`.trim();
|
|
192
|
+
for (const m of packs.matchPacks(text, { limit: 3, threshold: 4 })) {
|
|
193
|
+
if (m.dir !== slug && ctx.has(m.dir)) return m.dir;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
167
199
|
function applyAction(a) {
|
|
168
200
|
if (!a || typeof a !== "object") return null;
|
|
169
201
|
const guard = guardCheck(a);
|
|
@@ -202,6 +234,15 @@ function applyAction(a) {
|
|
|
202
234
|
if (kind === "ability") for (const proj of projects) packs.recordApplied(dir, proj);
|
|
203
235
|
return { kind: "update", dir, name: a.name || dir, note: a.journal || "state updated" };
|
|
204
236
|
}
|
|
237
|
+
// Deterministic dedupe: a versioned/facet proposal folds INTO the project
|
|
238
|
+
// pack it belongs to rather than spawning a sibling (abilities are exempt —
|
|
239
|
+
// they are activity-named and cross-project by design).
|
|
240
|
+
const dupeDir = kind === "context" ? findDuplicatePack({ dir, name: a.name, description: a.description, tags: a.tags }) : null;
|
|
241
|
+
if (dupeDir && packs.readPack(dupeDir)) {
|
|
242
|
+
packs.updatePack(dupeDir, { journal: a.journal || "", state: a.state || "" }, "reviewer");
|
|
243
|
+
const ex = packs.readPack(dupeDir);
|
|
244
|
+
return { kind: "update", dir: dupeDir, name: (ex && ex.name) || dupeDir, note: a.journal || "folded a new-pack proposal into the existing pack" };
|
|
245
|
+
}
|
|
205
246
|
const pack = packs.createPack({
|
|
206
247
|
dir,
|
|
207
248
|
name: a.name,
|
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