@kage-core/kage-graph-mcp 2.5.4 → 2.5.6
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/dist/cli.js +16 -0
- package/dist/kernel.js +169 -48
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -708,6 +708,22 @@ async function main() {
|
|
|
708
708
|
}
|
|
709
709
|
return;
|
|
710
710
|
}
|
|
711
|
+
if (command === "gen-plugin-hooks") {
|
|
712
|
+
// Maintainer tool: regenerate plugin/hooks/* from the setupAgent("claude-code") templates
|
|
713
|
+
// so the plugin and the npm install path ship identical hooks (one source of truth).
|
|
714
|
+
const pluginDir = takeArg(args, "--plugin-dir") ?? (0, node_path_1.join)(process.cwd(), "plugin");
|
|
715
|
+
const result = (0, kernel_js_1.generatePluginHooks)(pluginDir);
|
|
716
|
+
if (args.includes("--json")) {
|
|
717
|
+
console.log(JSON.stringify({ plugin_dir: pluginDir, ...result }, null, 2));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
console.log(`Generated plugin hooks in ${(0, node_path_1.join)(pluginDir, "hooks")}`);
|
|
721
|
+
console.log(` scripts: ${result.scripts.join(", ")}`);
|
|
722
|
+
console.log(` events: ${result.events.join(", ")}`);
|
|
723
|
+
if (result.removed.length)
|
|
724
|
+
console.log(` removed: ${result.removed.join(", ")}`);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
711
727
|
if (command === "daemon") {
|
|
712
728
|
const action = args[1];
|
|
713
729
|
const projectDir = projectArg(args);
|
package/dist/kernel.js
CHANGED
|
@@ -102,6 +102,7 @@ exports.compactProject = compactProject;
|
|
|
102
102
|
exports.installAgentPolicy = installAgentPolicy;
|
|
103
103
|
exports.createDenseEmbeddingProvider = createDenseEmbeddingProvider;
|
|
104
104
|
exports.buildEmbeddingIndex = buildEmbeddingIndex;
|
|
105
|
+
exports.isUngroundedConversationalCapture = isUngroundedConversationalCapture;
|
|
105
106
|
exports.recall = recall;
|
|
106
107
|
exports.recallWithEmbeddings = recallWithEmbeddings;
|
|
107
108
|
exports.queryCodeGraph = queryCodeGraph;
|
|
@@ -150,6 +151,7 @@ exports.capture = capture;
|
|
|
150
151
|
exports.createPublicCandidate = createPublicCandidate;
|
|
151
152
|
exports.registryRecommendations = registryRecommendations;
|
|
152
153
|
exports.setupAgent = setupAgent;
|
|
154
|
+
exports.generatePluginHooks = generatePluginHooks;
|
|
153
155
|
exports.setupDoctor = setupDoctor;
|
|
154
156
|
exports.verifyAgentActivation = verifyAgentActivation;
|
|
155
157
|
exports.observe = observe;
|
|
@@ -2133,9 +2135,18 @@ function evaluateMemoryAdmission(projectDir, packet) {
|
|
|
2133
2135
|
score -= 10;
|
|
2134
2136
|
risks.push("too little context");
|
|
2135
2137
|
}
|
|
2138
|
+
// Ungrounded conversational chatter (a path-less, repo-reference-free user outburst) is not
|
|
2139
|
+
// durable memory regardless of its other signals — keywords like "issue"/"before" can
|
|
2140
|
+
// otherwise score it as a candidate. Hard-block admission so it can never auto-promote.
|
|
2141
|
+
if (isUngroundedConversationalCapture(packet)) {
|
|
2142
|
+
score -= 45;
|
|
2143
|
+
risks.push("ungrounded conversational utterance, not durable knowledge");
|
|
2144
|
+
}
|
|
2136
2145
|
const bounded = Math.max(0, Math.min(100, score));
|
|
2137
2146
|
return {
|
|
2138
|
-
admit: bounded >= 45
|
|
2147
|
+
admit: bounded >= 45
|
|
2148
|
+
&& risks.indexOf("session bookkeeping, not durable knowledge") === -1
|
|
2149
|
+
&& risks.indexOf("ungrounded conversational utterance, not durable knowledge") === -1,
|
|
2139
2150
|
class: bounded >= 72 ? "high_signal" : bounded >= 45 ? "candidate" : "episodic_only",
|
|
2140
2151
|
score: bounded,
|
|
2141
2152
|
reasons,
|
|
@@ -7004,10 +7015,11 @@ function gcProject(projectDir, options = {}) {
|
|
|
7004
7015
|
skipped.push({ id: packet.id, title: packet.title, reason: "already deprecated" });
|
|
7005
7016
|
continue;
|
|
7006
7017
|
}
|
|
7007
|
-
// Serialized transcript / tool-output / file-content dumps
|
|
7008
|
-
//
|
|
7009
|
-
//
|
|
7010
|
-
|
|
7018
|
+
// Serialized transcript / tool-output / file-content dumps and ungrounded conversational
|
|
7019
|
+
// chatter (a path-less rant at the assistant) carry no durable knowledge and bloat recall +
|
|
7020
|
+
// the digest. Always delete them (deprecating would leave the blob on disk) — this also
|
|
7021
|
+
// reclaims legacy junk written before the capture-time guards existed.
|
|
7022
|
+
if (isSerializedDumpTitle(packet.title) || isSerializedDumpBody(packet.body) || isUngroundedConversationalCapture(packet)) {
|
|
7011
7023
|
if (!options.dryRun)
|
|
7012
7024
|
(0, node_fs_1.unlinkSync)(path);
|
|
7013
7025
|
deleted.push({ id: packet.id, title: packet.title });
|
|
@@ -7970,6 +7982,73 @@ function isSerializedDumpBody(body) {
|
|
|
7970
7982
|
return isSerializedDumpTitle(t)
|
|
7971
7983
|
|| /<task-notification\b|<tool-use-id\b|"hookSpecificOutput"|"isImage"\s*:|"noOutputExpected"\s*:|"interrupted"\s*:\s*(true|false)/i.test(t.slice(0, 4000));
|
|
7972
7984
|
}
|
|
7985
|
+
// Repo grounding signals — does the text point at something concrete in the codebase? These
|
|
7986
|
+
// three predicates are the shared vocabulary for "grounded": observationSignalScore weights
|
|
7987
|
+
// them into its durable-knowledge score, and the ungrounded-utterance guard uses their union
|
|
7988
|
+
// to decide whether a path-less capture references the repo at all. Kept as one source of
|
|
7989
|
+
// truth so the scorer and the guard can never drift on what "grounded" means.
|
|
7990
|
+
function textCitesPath(text) {
|
|
7991
|
+
return /\b[\w.-]+\/[\w./-]+\b/.test(text)
|
|
7992
|
+
|| /\b[\w-]+\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|json|ya?ml|toml|md|sh|css|html|sql)\b/i.test(text);
|
|
7993
|
+
}
|
|
7994
|
+
function textHasCodeIdentifier(text) {
|
|
7995
|
+
return /\b[a-z][a-z0-9]*[A-Z]\w*\b/.test(text) // camelCase
|
|
7996
|
+
|| /\b[a-z0-9]+_[a-z0-9_]+\b/.test(text) // snake_case
|
|
7997
|
+
|| /`[^`]+`/.test(text)
|
|
7998
|
+
|| /\b\w+\(\)/.test(text);
|
|
7999
|
+
}
|
|
8000
|
+
function textHasCommand(text) {
|
|
8001
|
+
return /(^|\s)(npm|pnpm|yarn|npx|node|git|cargo|make|pytest|go|tsc|kage)\s+[\w.-]/.test(text);
|
|
8002
|
+
}
|
|
8003
|
+
function hasRepoGroundingSignal(text) {
|
|
8004
|
+
const t = text ?? "";
|
|
8005
|
+
if (!t.trim())
|
|
8006
|
+
return false;
|
|
8007
|
+
return textCitesPath(t) || textHasCodeIdentifier(t) || textHasCommand(t);
|
|
8008
|
+
}
|
|
8009
|
+
// A raw conversational user utterance: a frustrated/rhetorical/imperative chat message aimed
|
|
8010
|
+
// at the agent — e.g. "why are you asking me???!!! it's your job, don't stop before you...".
|
|
8011
|
+
// It is plain prose, so the serialized-dump guard waves it through, yet it is chatter, not
|
|
8012
|
+
// durable repo knowledge. Detection is deliberately narrow — a run of emphatic terminal
|
|
8013
|
+
// punctuation, or one of a curated set of rhetorical / second-person-at-the-assistant
|
|
8014
|
+
// phrases — so it stays clear of normal declarative learnings (which read as statements, not
|
|
8015
|
+
// outbursts addressed to "you").
|
|
8016
|
+
// Phrases that rant AT the assistant ("why are you...", "it's your job", "don't stop"). These
|
|
8017
|
+
// never appear in a curated learning, so they mark chatter even when the outburst name-drops a
|
|
8018
|
+
// platform word (github, pr, x, linkedin) that the loose grounding matcher would otherwise
|
|
8019
|
+
// treat as a repo reference. This is the override that closes the leak where a frustrated
|
|
8020
|
+
// message peppered with nouns slipped through and was captured as approved memory.
|
|
8021
|
+
function looksFrustratedAtAssistant(text) {
|
|
8022
|
+
return /\b(why are you|why did you|why would you|why aren'?t you|are you (kidding|serious|even|really)|it'?s your job|that'?s your job|do your job|don'?t stop|stop asking|stop before you|keep going|hurry up|just do it|figure it out yourself|i (already )?told you)\b/i.test(text ?? "");
|
|
8023
|
+
}
|
|
8024
|
+
function looksLikeRawUserUtterance(text) {
|
|
8025
|
+
const t = (text ?? "").trim();
|
|
8026
|
+
if (!t)
|
|
8027
|
+
return false;
|
|
8028
|
+
// Emphatic or mixed terminal punctuation ("???", "!!", "?!") — common in venting chat,
|
|
8029
|
+
// essentially absent from curated memory prose.
|
|
8030
|
+
if (/[!?]{2,}/.test(t))
|
|
8031
|
+
return true;
|
|
8032
|
+
// Rhetorical questions / second-person-imperative frustration directed at the assistant.
|
|
8033
|
+
return looksFrustratedAtAssistant(t);
|
|
8034
|
+
}
|
|
8035
|
+
// Capture-noise guard for ungrounded chat. A packet trips it when it cites zero repo paths and
|
|
8036
|
+
// either (a) it rants at the assistant — which overrides incidental repo-ish words, since the
|
|
8037
|
+
// grounding matcher false-positives on bare platform names — or (b) it reads as a raw outburst
|
|
8038
|
+
// and carries no repo grounding signal at all. The override + conjunction keep it safe: a real
|
|
8039
|
+
// ungrounded decision or convention is declarative (no "why are you.../don't stop") and usually
|
|
8040
|
+
// names a symbol, file, command, or rule, so it is never caught. Such packets route to pending
|
|
8041
|
+
// (not auto-approved) at capture time and are withheld from recall, like serialized dumps.
|
|
8042
|
+
function isUngroundedConversationalCapture(packet) {
|
|
8043
|
+
if (packet.paths && packet.paths.length > 0)
|
|
8044
|
+
return false;
|
|
8045
|
+
const text = `${packet.title ?? ""}\n${packet.body ?? ""}`;
|
|
8046
|
+
if (looksFrustratedAtAssistant(text))
|
|
8047
|
+
return true;
|
|
8048
|
+
if (hasRepoGroundingSignal(text))
|
|
8049
|
+
return false;
|
|
8050
|
+
return looksLikeRawUserUtterance(text);
|
|
8051
|
+
}
|
|
7973
8052
|
// Collapse whitespace and hard-cap a value rendered inline in a context block, so one
|
|
7974
8053
|
// oversized field (e.g. a graph fact whose body is a raw transcript) can never blow up
|
|
7975
8054
|
// the assembled output — the 270k-char overflow that motivated this guard.
|
|
@@ -8055,7 +8134,7 @@ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, i
|
|
|
8055
8134
|
];
|
|
8056
8135
|
return { packet, score: score_breakdown.final, relevance, why_matched: unique(why).slice(0, 12), score_breakdown };
|
|
8057
8136
|
})
|
|
8058
|
-
.filter((entry) => entry.relevance > 0 && !isSerializedDumpTitle(entry.packet.title))
|
|
8137
|
+
.filter((entry) => entry.relevance > 0 && !isSerializedDumpTitle(entry.packet.title) && !isUngroundedConversationalCapture(entry.packet))
|
|
8059
8138
|
.sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title));
|
|
8060
8139
|
const scored = diversifyRecallEntries(rankedScored, limit)
|
|
8061
8140
|
.map(({ relevance, ...entry }) => entry);
|
|
@@ -8067,7 +8146,7 @@ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, i
|
|
|
8067
8146
|
const { score, why } = pendingLexicalScores.get(packet.id) ?? { score: 0, why: [] };
|
|
8068
8147
|
return { packet, score, why_matched: why };
|
|
8069
8148
|
})
|
|
8070
|
-
.filter((entry) => entry.score > 0)
|
|
8149
|
+
.filter((entry) => entry.score > 0 && !isUngroundedConversationalCapture(entry.packet))
|
|
8071
8150
|
.sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
|
|
8072
8151
|
.filter((entry) => {
|
|
8073
8152
|
const key = `${entry.packet.type}:${entry.packet.title.toLowerCase()}:${entry.packet.paths.join(",")}`;
|
|
@@ -13711,6 +13790,20 @@ function capture(input) {
|
|
|
13711
13790
|
if (missingPaths.length) {
|
|
13712
13791
|
warnings.push(`Some referenced paths do not exist in this repo: ${missingPaths.join(", ")}`);
|
|
13713
13792
|
}
|
|
13793
|
+
// Ungrounded conversational chatter — a frustrated/rhetorical user message with no cited repo
|
|
13794
|
+
// paths — is not durable memory. Don't hard-reject (a reviewer may still salvage it); instead
|
|
13795
|
+
// deny it the auto-approve fast path so it lands in the pending inbox, tagged with why. Recall
|
|
13796
|
+
// withholds it regardless of status. Explicit pendingReview captures already route to review.
|
|
13797
|
+
const ungroundedUtterance = isUngroundedConversationalCapture({
|
|
13798
|
+
title: input.title,
|
|
13799
|
+
body: input.body,
|
|
13800
|
+
paths: groundedPaths,
|
|
13801
|
+
});
|
|
13802
|
+
const routeToPending = Boolean(input.pendingReview) || ungroundedUtterance;
|
|
13803
|
+
if (ungroundedUtterance && !input.pendingReview) {
|
|
13804
|
+
warnings.push("Routed to pending review: this reads as an ungrounded conversational message with no cited repo paths, " +
|
|
13805
|
+
"not a durable learning. Cite a repo path or restate it as a reusable insight to approve it.");
|
|
13806
|
+
}
|
|
13714
13807
|
const createdAt = nowIso();
|
|
13715
13808
|
// Agent-asserted links to code-graph nodes (PRD `graph_nodes`): the agent recording the
|
|
13716
13809
|
// memory knows which symbol/route/file the rule is about, so let it declare them instead
|
|
@@ -13729,9 +13822,9 @@ function capture(input) {
|
|
|
13729
13822
|
scope: "repo",
|
|
13730
13823
|
visibility: "team",
|
|
13731
13824
|
sensitivity: "internal",
|
|
13732
|
-
status:
|
|
13825
|
+
status: routeToPending ? "pending" : "approved",
|
|
13733
13826
|
confidence: DEFAULT_CONFIDENCE,
|
|
13734
|
-
tags: input.tags ?? [],
|
|
13827
|
+
tags: ungroundedUtterance ? unique([...(input.tags ?? []), "needs-grounding"]) : (input.tags ?? []),
|
|
13735
13828
|
paths: groundedPaths,
|
|
13736
13829
|
stack: input.stack ?? [],
|
|
13737
13830
|
source_refs: [
|
|
@@ -13793,7 +13886,7 @@ function capture(input) {
|
|
|
13793
13886
|
...evaluateMemoryQuality(input.projectDir, packet),
|
|
13794
13887
|
...(contradictions.length ? { contradicts: contradictions.map((c) => c.packet_id) } : {}),
|
|
13795
13888
|
};
|
|
13796
|
-
const path = writePacket(input.projectDir, packet,
|
|
13889
|
+
const path = writePacket(input.projectDir, packet, routeToPending ? "pending" : "packets");
|
|
13797
13890
|
recordMemoryAudit(input.projectDir, "capture", [packet], {
|
|
13798
13891
|
type: packet.type,
|
|
13799
13892
|
status: packet.status,
|
|
@@ -14354,6 +14447,58 @@ exit 0
|
|
|
14354
14447
|
setSnippet(paths[agent] || null, universal, [`Merge this MCP stdio config into ${agent}'s MCP settings.`, "Restart the agent after updating config."]);
|
|
14355
14448
|
return result;
|
|
14356
14449
|
}
|
|
14450
|
+
// Single source of truth for Claude Code hooks. The plugin's hook scripts and hooks.json are
|
|
14451
|
+
// GENERATED from the exact same setupAgent("claude-code") output the npm install path writes —
|
|
14452
|
+
// so a hook fix lands in both channels and they can't silently drift. The hook scripts call the
|
|
14453
|
+
// kage CLI and never reference their own location, so the same bodies serve both the ~/.claude
|
|
14454
|
+
// install and the plugin; only the wiring's command path is re-targeted to ${CLAUDE_PLUGIN_ROOT}.
|
|
14455
|
+
// Run via `kage gen-plugin-hooks`; a unit test asserts the committed plugin/hooks match this.
|
|
14456
|
+
function generatePluginHooks(pluginDir) {
|
|
14457
|
+
const tmpHome = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-plugin-home-"));
|
|
14458
|
+
const tmpProject = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-plugin-proj-"));
|
|
14459
|
+
try {
|
|
14460
|
+
setupAgent("claude-code", tmpProject, { write: true, homeDir: tmpHome });
|
|
14461
|
+
const srcHookDir = (0, node_path_1.join)(tmpHome, ".claude", "kage", "hooks");
|
|
14462
|
+
const settings = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(tmpHome, ".claude", "settings.json"), "utf8"));
|
|
14463
|
+
const hooksOut = (0, node_path_1.join)(pluginDir, "hooks");
|
|
14464
|
+
(0, node_fs_1.mkdirSync)(hooksOut, { recursive: true });
|
|
14465
|
+
// Copy the generated hook scripts verbatim (path-agnostic — they shell out to `kage`).
|
|
14466
|
+
const scripts = (0, node_fs_1.readdirSync)(srcHookDir).filter((f) => f.endsWith(".sh")).sort();
|
|
14467
|
+
for (const file of scripts) {
|
|
14468
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(hooksOut, file), (0, node_fs_1.readFileSync)((0, node_path_1.join)(srcHookDir, file), "utf8"), { mode: 0o755 });
|
|
14469
|
+
}
|
|
14470
|
+
// Re-target the event wiring from the install path's `~/.claude/kage/hooks/` to the plugin's
|
|
14471
|
+
// `${CLAUDE_PLUGIN_ROOT}/hooks/`, preserving events, matchers, and timeouts exactly.
|
|
14472
|
+
const events = Object.keys(settings.hooks ?? {});
|
|
14473
|
+
const pluginHooks = { hooks: {} };
|
|
14474
|
+
for (const event of events) {
|
|
14475
|
+
pluginHooks.hooks[event] = (settings.hooks ?? {})[event].map((entry) => ({
|
|
14476
|
+
...(entry.matcher !== undefined ? { matcher: entry.matcher } : {}),
|
|
14477
|
+
hooks: entry.hooks.map((h) => {
|
|
14478
|
+
const m = h.command.match(/^bash ~\/\.claude\/kage\/hooks\/(.+)$/);
|
|
14479
|
+
return {
|
|
14480
|
+
type: h.type,
|
|
14481
|
+
command: m ? `bash "\${CLAUDE_PLUGIN_ROOT}/hooks/${m[1]}"` : h.command,
|
|
14482
|
+
...(h.timeout !== undefined ? { timeout: h.timeout } : {}),
|
|
14483
|
+
};
|
|
14484
|
+
}),
|
|
14485
|
+
}));
|
|
14486
|
+
}
|
|
14487
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(hooksOut, "hooks.json"), `${JSON.stringify(pluginHooks, null, 2)}\n`, "utf8");
|
|
14488
|
+
// Drop committed scripts the install path no longer emits (e.g. kage-prompt-context.sh,
|
|
14489
|
+
// now subsumed by observe.sh handling UserPromptSubmit).
|
|
14490
|
+
const removed = [];
|
|
14491
|
+
for (const stale of (0, node_fs_1.readdirSync)(hooksOut).filter((f) => f.endsWith(".sh") && !scripts.includes(f))) {
|
|
14492
|
+
(0, node_fs_1.unlinkSync)((0, node_path_1.join)(hooksOut, stale));
|
|
14493
|
+
removed.push(stale);
|
|
14494
|
+
}
|
|
14495
|
+
return { scripts, removed, events };
|
|
14496
|
+
}
|
|
14497
|
+
finally {
|
|
14498
|
+
(0, node_fs_1.rmSync)(tmpHome, { recursive: true, force: true });
|
|
14499
|
+
(0, node_fs_1.rmSync)(tmpProject, { recursive: true, force: true });
|
|
14500
|
+
}
|
|
14501
|
+
}
|
|
14357
14502
|
function upsertJsonMcpServer(path, name, server) {
|
|
14358
14503
|
ensureDir((0, node_path_1.dirname)(path));
|
|
14359
14504
|
let config = {};
|
|
@@ -14756,19 +14901,13 @@ function observationSignalScore(observation) {
|
|
|
14756
14901
|
const causalHits = SIGNAL_CAUSAL_MARKERS.filter((marker) => lower.includes(marker)).length;
|
|
14757
14902
|
if (causalHits > 0)
|
|
14758
14903
|
score += Math.min(0.35, 0.25 + (causalHits - 1) * 0.05);
|
|
14759
|
-
const citesPath = Boolean(observation.path)
|
|
14760
|
-
|| /\b[\w.-]+\/[\w./-]+\b/.test(prose)
|
|
14761
|
-
|| /\b[\w-]+\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|json|ya?ml|toml|md|sh|css|html|sql)\b/i.test(prose);
|
|
14904
|
+
const citesPath = Boolean(observation.path) || textCitesPath(prose);
|
|
14762
14905
|
if (citesPath)
|
|
14763
14906
|
score += 0.2;
|
|
14764
|
-
const codeIdentifier =
|
|
14765
|
-
|| /\b[a-z0-9]+_[a-z0-9_]+\b/.test(prose) // snake_case
|
|
14766
|
-
|| /`[^`]+`/.test(prose)
|
|
14767
|
-
|| /\b\w+\(\)/.test(prose);
|
|
14907
|
+
const codeIdentifier = textHasCodeIdentifier(prose);
|
|
14768
14908
|
if (codeIdentifier)
|
|
14769
14909
|
score += 0.15;
|
|
14770
|
-
const commandLine = Boolean(observation.command)
|
|
14771
|
-
|| /(^|\s)(npm|pnpm|yarn|npx|node|git|cargo|make|pytest|go|tsc|kage)\s+[\w.-]/.test(prose);
|
|
14910
|
+
const commandLine = Boolean(observation.command) || textHasCommand(prose);
|
|
14772
14911
|
if (commandLine)
|
|
14773
14912
|
score += 0.15;
|
|
14774
14913
|
const words = combined.split(/\s+/).filter(Boolean).length;
|
|
@@ -15488,20 +15627,17 @@ function kageResume(projectDir) {
|
|
|
15488
15627
|
updated_at: packetRecency(packet),
|
|
15489
15628
|
age: humanPacketAge(packetRecency(packet)),
|
|
15490
15629
|
}));
|
|
15491
|
-
|
|
15492
|
-
|
|
15630
|
+
// A new session starts on a fresh task, so SessionStart does NOT replay last session's files,
|
|
15631
|
+
// commands, or a recency-ranked memory list — that was pre-task noise (it fires before the
|
|
15632
|
+
// first prompt, so it can't be task-targeted) and the surface that leaked raw command dumps and
|
|
15633
|
+
// junk packets. Task-relevant memory is pulled on the first prompt via prompt-context; recall
|
|
15634
|
+
// at the moment a file is read via file-context. SessionStart carries only always-on curated
|
|
15635
|
+
// repo facts (the pinned block, below) plus a few actionable open-thread pointers.
|
|
15636
|
+
const hasSessionContent = Boolean(lastChangeMemory || pendingAutoDistilled || reconciliation.unresolved_count);
|
|
15637
|
+
const hasContent = hasSessionContent;
|
|
15493
15638
|
const lines = [];
|
|
15494
|
-
if (
|
|
15495
|
-
lines.push("#
|
|
15496
|
-
if (lastSession) {
|
|
15497
|
-
lines.push(`Last session ${lastSession.session_id} (${lastSession.observations} observation${lastSession.observations === 1 ? "" : "s"}, ended ${lastSession.last_at}).`);
|
|
15498
|
-
if (lastSession.paths.length)
|
|
15499
|
-
lines.push(`Worked on: ${lastSession.paths.join(", ")}`);
|
|
15500
|
-
if (lastSession.commands.length)
|
|
15501
|
-
lines.push(`Commands: ${lastSession.commands.join("; ")}`);
|
|
15502
|
-
if (lastSession.distilled_titles.length)
|
|
15503
|
-
lines.push(`Learned: ${lastSession.distilled_titles.join("; ")}`);
|
|
15504
|
-
}
|
|
15639
|
+
if (hasSessionContent) {
|
|
15640
|
+
lines.push("# Open threads (Kage)");
|
|
15505
15641
|
if (lastChangeMemory) {
|
|
15506
15642
|
lines.push(`Change memory: ${lastChangeMemory.title} — ${lastChangeMemory.summary}`);
|
|
15507
15643
|
}
|
|
@@ -15514,22 +15650,7 @@ function kageResume(projectDir) {
|
|
|
15514
15650
|
lines.push(` - ${item.packet_id}: ${item.title}`);
|
|
15515
15651
|
}
|
|
15516
15652
|
}
|
|
15517
|
-
|
|
15518
|
-
// only for the newest few, hard-capped to the resume token budget.
|
|
15519
|
-
const block = lines.slice(0, 15);
|
|
15520
|
-
if (recentPackets.length) {
|
|
15521
|
-
block.push("", "## Recent memory");
|
|
15522
|
-
recentPackets.forEach((packet, position) => {
|
|
15523
|
-
const entry = `[${packet.id.slice(0, 12)}] ${packet.type} ${packet.title} (${humanPacketAge(packetRecency(packet))})`;
|
|
15524
|
-
const candidate = [entry];
|
|
15525
|
-
if (position < RESUME_TIMELINE_DETAILED && packet.summary) {
|
|
15526
|
-
const summary = packet.summary.replace(/\s+/g, " ").trim();
|
|
15527
|
-
candidate.push(` ${summary.length > 160 ? `${summary.slice(0, 157)}...` : summary}`);
|
|
15528
|
-
}
|
|
15529
|
-
if (estimateTokens([...block, ...candidate].join("\n")) <= RESUME_CONTEXT_TOKEN_BUDGET)
|
|
15530
|
-
block.push(...candidate);
|
|
15531
|
-
});
|
|
15532
|
-
}
|
|
15653
|
+
const block = lines.slice(0, 12);
|
|
15533
15654
|
// Lead the SessionStart injection with the team's pinned, always-on repo memory (the
|
|
15534
15655
|
// curated high-signal facts), not just the recent-timeline digest — parity with recall's
|
|
15535
15656
|
// context block, so a new session starts already holding the key knowledge, not only a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kage-core/kage-graph-mcp",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.6",
|
|
4
4
|
"description": "Team memory for coding agents: captures the decisions, runbooks, and bug fixes that get lost, verified against your code and shared via git. MCP server, zero deps, no account.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|