Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a17c2ac
fix(maintenance): exclude undrainable access pressure
wefio Oct 11, 2026
30d370d
docs(recall): record existing-event signal feasibility
wefio Oct 15, 2026
1a15f68
docs(recall): close the first lexical tier calibration loop
wefio Oct 16, 2026
46e2465
docs(recall): evaluate two-tier retention with executed-action proxies
wefio Oct 16, 2026
fa3d8f6
fix(retrieval): resolve lexical ties before evidence caps
wefio Oct 10, 2026
3318c55
docs(board): define compatible event records and agent status
wefio Oct 17, 2026
dd2ecf9
feat(state): unify AG and board read/write entry
wefio Oct 13, 2026
c36265e
feat(daemon): add controlled code reload with fenced handover
wefio Oct 13, 2026
cf40ddc
fix(recall): recognize Chinese recollection questions
wefio Oct 14, 2026
b2c3598
feat(recall): add session-only intent cue trial
wefio Oct 15, 2026
40cd10f
feat(recall): piggyback bounded session cues on supplemental searches
wefio Oct 16, 2026
165bc86
fix(daemon): reconcile reload pickup with the aborted-call contract
wefio Oct 17, 2026
446ea1a
fix(design): drop an inherited stray conflict marker in the retrieval…
wefio Oct 17, 2026
fb73303
fix(integration): drive opencode board tools from the concrete operation
wefio Oct 18, 2026
9de262c
docs(experiments): track Google feedback-signal literature review
wefio Oct 20, 2026
9145dcb
Merge origin/main into integr/timing-rebase: reconcile docs with the …
wefio Oct 20, 2026
a5bad06
fix(daemon): treat a zombie owner as stopped so reload does not time out
wefio Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 109 additions & 18 deletions .pi/extensions/nmg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@
renderStateOutput,
STATE_BOARD_READ_OPERATIONS,
} from "../../../src/integration/state.ts";
import type { NmgStateResult } from "../../../src/cli/protocol.ts";
import type {
NmgStateResult,
NmgSessionActiveGraphResult,
NmgSearchParams,
} from "../../../src/cli/protocol.ts";
import {
canProposeSessionRecallCue,
matchSessionRecallCue,
} from "../../../src/core/session-recall.ts";
import { decideMemoryLoad } from "../../../src/core/gate.ts";
import type {
ExtensionAPI,
ExtensionCommandContext,
Expand Down Expand Up @@ -71,7 +80,10 @@
extractPromptText,
} from "../../../src/integration/agent-attribution.ts";
import { SessionRecallFlow } from "../../../src/integration/session-recall-flow.ts";
import { SessionTaskWindow } from "../../../src/integration/session-task-window.ts";
import {
type SessionRecallRequest,
SessionTaskWindow,
} from "../../../src/integration/session-task-window.ts";
import {
isMemorableToolResult,
isSuccessfulCommit,
Expand Down Expand Up @@ -248,6 +260,7 @@
| "sessionActiveGraph"
| "taskBoard"
| "state"
| "lab"
| "recordActiveGraphAttribution"
| "recordClaimOutcomes"
| "recordFeedback",
Expand Down Expand Up @@ -314,20 +327,58 @@
return disclosure === "header" ? formatSearchHeaders(context) : formatMemoryContext(context);
}
};
type RecallHandshake = Extract<NmgSessionActiveGraphResult, { action: "beginDisclosureTurn" }>;
let recallTurn:
| {
sessionId: string;
projectDir: string;
prompt: string;
skipped: boolean;
gate: NonNullable<RecallHandshake["recallGate"]>;
}
| undefined;
const beginDisclosureTurn = async (
sessionId: string,
enabled: boolean,
options: HttpCallOptions = {},
): Promise<void> => {
if (!enabled) return;
await invoke(
): Promise<RecallHandshake | undefined> => {
if (!enabled) return undefined;
recallTurn = undefined;
const result = (await invoke(
"sessionActiveGraph",
{
action: "beginDisclosureTurn",
sessionId,
},
{ action: "beginDisclosureTurn", sessionId, projectDir: projectDirectory() },
options,
).catch(() => undefined);
).catch(() => undefined)) as NmgSessionActiveGraphResult | undefined;
return result?.action === "beginDisclosureTurn" ? result : undefined;
};
const searchCueParams = (
sessionId: string,
phrase?: string,
revision?: number,
): Pick<NmgSearchParams, "recallCue" | "recallRevision" | "recallSource"> => {
if (!phrase) return {};
if (revision !== undefined) return { recallCue: phrase, recallRevision: revision };
const current = recallTurn;
if (
!current?.skipped ||
current.sessionId !== sessionId ||
current.projectDir !== projectDirectory()
)
return {};
if (
Date.parse(current.gate.expiresAt) <= Date.now() ||
!canProposeSessionRecallCue(current.prompt, phrase)
)
return {};
current.skipped = false; // At most one native proposal attempt per user turn.
return {
recallCue: phrase,
recallSource: {
prompt: current.prompt,
generation: current.gate.generation,
turn: current.gate.turn,
},
};
};

// git commit via the bash tool is the strongest "milestone" signal available
Expand Down Expand Up @@ -494,36 +545,54 @@
}
return persistedContextResult(output, event.systemPrompt);
};
let recallRequest: SessionRecallRequest | null = null;
const prepare = async () => {
const isNewUserTurn = recallFlow.beginTurn(
sessionId,
piUserTurnKey(ctx.sessionManager, event.prompt),
);
await budget.run("connection", () => connection());
await budget.run("disclosure.begin", (signal) =>
recallRequest = taskWindow.prepare(sessionId, event.prompt);
const handshake = await budget.run("disclosure.begin", (signal) =>
beginDisclosureTurn(sessionId, isNewUserTurn, { signal }),
);
if (handshake?.recallGate && event.prompt.length <= 4096) {
recallTurn = {
sessionId,
projectDir: projectDirectory(),
prompt: event.prompt,
skipped: !recallRequest,
gate: handshake.recallGate,
};
if (!recallRequest && matchSessionRecallCue(event.prompt, handshake.recallGate.phrases)) {
const decision = decideMemoryLoad(event.prompt, { sessionCue: true });
if (decision.mode === "cue") {
recallRequest = { ...decision, mode: "cue", query: event.prompt };
recallTurn.skipped = false;
}
}
}
partial.base.push(...(await recallNudges(budget, sessionId, isNewUserTurn, event.prompt)));
// Each completed typed stage survives later optional work or a deadline.
partial.base.unshift(...(await reasoningCheckpointFor(budget, sessionId)));
partial.base.push(...(await readTurnAgContext(budget, sessionId)));
};
const automatic = async () => {
await prepare();
const recallRequest = taskWindow.prepare(sessionId, event.prompt);
if (!recallRequest) return surfaceResult(currentOutput());
const demand = recallRequest;
if (!demand) return surfaceResult(currentOutput());
let context = (await automaticInvoke("search", {
query: recallRequest.query,
query: demand.query,
autoRecallBudgetMs: Math.max(1, Math.floor(Math.min(4_500, budget.remainingMs - 100))),
projectDir: projectDirectory(),
sessionId,
maxTier: Math.min(configuredAutoRecallTier(), recallRequest.maxTier) as MemoryTier,
limit: Math.min(configuredAutoRecallLimit(), recallRequest.limit),
maxTier: Math.min(configuredAutoRecallTier(), demand.maxTier) as MemoryTier,
limit: Math.min(configuredAutoRecallLimit(), demand.limit),
initialEvidenceTarget: configuredInitialTarget(),
strongHitTopGap: configuredStrongHitTopGap(),
strongHitInitialTarget: configuredStrongHitInitialTarget(),
secondPass: qpp2Mode === "active",
graphHops: Math.min(1, recallRequest.graphHops),
graphHops: Math.min(1, demand.graphHops),
tieredDisclosure: true,
autoRecall: true,
})) as MemoryContext;
Expand Down Expand Up @@ -638,109 +707,109 @@
"info",
);
};
const nmgMenuHandler = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
const parts = args.trim().split(/\s+/).filter(Boolean);
const [sub, ...rest] = parts;
const restText = rest.join(" ");
switch (sub) {
case "recall": {
recallCommand(rest, ctx);
return;
}
case "wake": {
const current = readWakeConfig();
const [wcmd, wvalRaw] = restText.split(/\s+/);
const wval = Number(wvalRaw);
switch (wcmd) {
case "on":
writeWakeConfig({ ...current, enabled: true });
ctx.ui.notify("黑板唤醒已开启(世界频道+活跃频道有新 open 条目会唤醒)", "info");
return;
case "off":
writeWakeConfig({ ...current, enabled: false });
ctx.ui.notify("黑板唤醒已关闭", "info");
return;
case "status": {
const budgetText = current.budget === 0 ? "不限制" : `${current.budget}/天`;
const cooldownText =
current.cooldownMs === 0 ? "无" : `${Math.round(current.cooldownMs / 60_000)} 分`;
ctx.ui.notify(
`黑板唤醒:${current.enabled ? "开" : "关"} · 预算 ${budgetText} · 冷却 ${cooldownText} · 轮询 ${Math.round(current.intervalMs / 1_000)} 秒 · 世界广播 ${current.worldBroadcast ? "开" : "关"}`,
"info",
);
return;
}
case "budget": {
if (!Number.isFinite(wval)) {
ctx.ui.notify("用法:/nmg wake budget N(0=不限制)", "warning");
return;
}
const budget = Math.max(0, Math.min(100, Math.round(wval)));
writeWakeConfig({ ...current, budget });
ctx.ui.notify(`黑板唤醒预算已设为 ${budget === 0 ? "不限制" : `${budget}/天`}`, "info");
return;
}
case "cooldown": {
if (!Number.isFinite(wval)) {
ctx.ui.notify("用法:/nmg wake cooldown M(0=无冷却)", "warning");
return;
}
const cooldownMs = wval === 0 ? 0 : Math.max(30_000, Math.round(wval * 60_000));
writeWakeConfig({ ...current, cooldownMs });
ctx.ui.notify(
`黑板唤醒冷却已设为 ${cooldownMs === 0 ? "无" : `${Math.round(cooldownMs / 60_000)} 分钟`}`,
"info",
);
return;
}
case "interval": {
if (!Number.isFinite(wval)) {
ctx.ui.notify("用法:/nmg wake interval S(秒,最小 5)", "warning");
return;
}
const intervalMs = Math.max(5_000, Math.round(wval * 1_000));
writeWakeConfig({ ...current, intervalMs });
ctx.ui.notify(`黑板唤醒轮询已设为 ${Math.round(intervalMs / 1_000)} 秒`, "info");
return;
}
case "world": {
// 无参切换;0 关、1 开。
const world =
wvalRaw === "0" ? false : wvalRaw === "1" ? true : !current.worldBroadcast;
writeWakeConfig({ ...current, worldBroadcast: world });
ctx.ui.notify(
world
? "已开启世界频道协作广播:协作类新条目会同时发到世界频道拉其他 agent"
: "已关闭世界频道协作广播",
"info",
);
return;
}
default: {
const enabled = !current.enabled;
writeWakeConfig({ ...current, enabled });
ctx.ui.notify(enabled ? "黑板唤醒已开启" : "黑板唤醒已关闭", "info");
}
}
return;
}
default: {
// 无参数 → 交互式选择菜单(可上下键选,不用手敲);未知子命令 → 总览提示。
if (parts.length === 0) {
await nmgInteractiveMenu(ctx);
return;
}
const wake = readWakeConfig();
const budgetText = wake.budget === 0 ? "不限制" : `${wake.budget}/天`;
const cooldownText =
wake.cooldownMs === 0 ? "无" : `${Math.round(wake.cooldownMs / 60_000)} 分`;
ctx.ui.notify(
`NMG 菜单:/nmg recall(召回折叠 ${recallCollapsed ? "开" : "关"}) · /nmg wake on|off|status|budget N|cooldown M|interval S(唤醒 ${wake.enabled ? "开" : "关"},预算 ${budgetText},冷却 ${cooldownText},轮询 ${Math.round(wake.intervalMs / 1_000)} 秒)`,
"info",
);
}
}
};

Check notice on line 812 in .pi/extensions/nmg/index.ts

View check run for this annotation

codefactor.io / CodeFactor

.pi/extensions/nmg/index.ts#L710-L812

Complex Method
// 交互式选择菜单:/nmg(无参数)弹出可上下键选的列表,避免手敲。带参数的
// 形式(/nmg recall、/nmg wake on 等)仍保留,供脚本/快捷路径直接调用。
// 挡位选择(select 预设值,避免随意输入溢出/无效);自由数字只走命令形式
Expand Down Expand Up @@ -807,67 +876,67 @@
await workingStateView.setPreview(onOff === "显示", ctx);
ctx.ui.notify(onOff === "显示" ? "状态预览已显示" : "状态预览已隐藏(状态本身不变)", "info");
};
const nmgInteractiveMenu = async (ctx: ExtensionCommandContext): Promise<void> => {
if (!ctx.hasUI || typeof ctx.ui.select !== "function") {
ctx.ui.notify(
"当前模式不支持交互菜单,可用:/nmg recall、/nmg state on|off、/nmg wake on|off|status|budget N|cooldown M|interval S",
"warning",
);
return;
}
const choice = await ctx.ui.select(
"NMG 控制台",
nmgMenuOptions(recallCollapsed, readWakeConfig()),
);
if (!choice) return; // 用户取消
if (choice.startsWith("召回")) {
await nmgMenuHandler("recall", ctx);
return;
}
if (choice.startsWith("唤醒:开启/关闭")) {
const onOff = await ctx.ui.select("黑板唤醒", ["开启", "关闭"]);
if (!onOff) return;
const current = readWakeConfig();
writeWakeConfig({ ...current, enabled: onOff === "开启" });
ctx.ui.notify(onOff === "开启" ? "黑板唤醒已开启" : "黑板唤醒已关闭", "info");
return;
}
if (choice.startsWith("唤醒:世界广播")) {
const onOff = await ctx.ui.select("世界频道协作广播", ["开启", "关闭"]);
if (!onOff) return;
const current = readWakeConfig();
writeWakeConfig({ ...current, worldBroadcast: onOff === "开启" });
ctx.ui.notify(
onOff === "开启"
? "已开启世界频道协作广播:协作类新条目会同时发到世界频道拉其他 agent"
: "已关闭世界频道协作广播",
"info",
);
return;
}
if (choice.startsWith("唤醒:参数设置")) {
await nmgWakeParamMenu(ctx);
return;
}
if (choice.startsWith("当前状态:查看完整正文")) {
await workingStateView.readForUser(ctx);
return;
}
if (choice.startsWith("当前状态:显示/隐藏预览")) {
await nmgStatePreviewMenu(ctx);
return;
}
{
const wake = readWakeConfig();
const budgetText = wake.budget === 0 ? "不限制" : `${wake.budget}/天`;
const cooldownText =
wake.cooldownMs === 0 ? "无" : `${Math.round(wake.cooldownMs / 60_000)} 分`;
ctx.ui.notify(
`NMG:召回折叠 ${recallCollapsed ? "开" : "关"} · 唤醒 ${wake.enabled ? "开" : "关"}(预算 ${budgetText},冷却 ${cooldownText},轮询 ${Math.round(wake.intervalMs / 1_000)} 秒)`,
"info",
);
}
};

Check notice on line 939 in .pi/extensions/nmg/index.ts

View check run for this annotation

codefactor.io / CodeFactor

.pi/extensions/nmg/index.ts#L879-L939

Complex Method
// `/nmg state [on|off]` is the read-only state view and its preview switch.
// It dispatches before the menu handler so that handler keeps its own shape.
const nmgStateCommand = async (rest: string[], ctx: ExtensionCommandContext): Promise<void> => {
Expand Down Expand Up @@ -1081,6 +1150,7 @@
const sessionId = ctx.sessionManager.getSessionId();
taskWindow.clear(sessionId);
recallFlow.clear(sessionId);
recallTurn = undefined;
agentAttributionFlow.clear(sessionId);
controllerShadow.clear(sessionId);
if (!connectionPromise) return;
Expand Down Expand Up @@ -1312,6 +1382,7 @@
capability: Type.Optional(
Type.Union([
Type.Literal("reasoning_workspace"),
Type.Literal("recall_gate"),
Type.Literal("memory_graph_reasoner"),
Type.Literal("controller_shadow"),
Type.Literal("controller_controlled"),
Expand All @@ -1333,6 +1404,13 @@
if (params.action === "enable" && !params.reason) throw new Error("enable requires reason");
if (params.action === "invoke" && !params.operation)
throw new Error("invoke requires operation");
if (
params.capability === "recall_gate" &&
["enable", "disable", "clear", "correct", "remove"].includes(
params.action === "invoke" ? (params.operation ?? "") : params.action,
)
)
recallTurn = undefined;
const active = await connection();
const result = await invokeDaemon(active, "lab", {
action: params.action,
Expand Down Expand Up @@ -1941,6 +2019,15 @@
description: nmgPrompts.search_queries_parameter_description,
}),
),
recallCue: Type.Optional(
Type.String({ description: nmgPrompts.search_recall_cue_parameter_description }),
),
recallRevision: Type.Optional(
Type.Integer({
minimum: 1,
description: nmgPrompts.search_recall_revision_parameter_description,
}),
),
nodeName: Type.Optional(Type.String()),
maxTier: Type.Optional(
Type.Union([Type.Literal(0), Type.Literal(1), Type.Literal(2), Type.Literal(3)]),
Expand All @@ -1965,8 +2052,11 @@
nmgPrompts.search_progression_required,
);
}
const { recallCue, recallRevision, ...searchParams } = params;
const baseParams = {
...params,
...searchParams,
// Unknown model-supplied properties cannot impersonate a native observation.
recallSource: undefined,
// Automatic recall remains shallow. An explicit search is the agent's
// request to look beyond that cache, so expose all tiers by default;
// result/token budgets still bound what is returned.
Expand Down Expand Up @@ -1994,6 +2084,7 @@
}
let result = (await invoke("search", {
...baseParams,
...searchCueParams(sessionId, recallCue, recallRevision),
secondPass: params.secondPass ?? qpp2Mode === "active",
...(activeGraphBudget ? { activeGraphBudget, limit: activeGraphBudget.maxEvidence } : {}),
})) as MemoryContext;
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,11 @@ requires an explicit command. `memory delete` retains immutable source history.

The service rejects a second daemon for the same database, opens SQLite lazily,
and removes stale process leases. Use the same `--data-dir` or `--db` for daemon
and client commands.
and client commands. At a coordinated code-update boundary, `nmg daemon reload`
drains accepted work and replaces a capable owner without clients racing to
start a second one. Persistent memory/board data survives; volatile AG and writer
leases are cleared. An older daemon without this capability is refused, not
silently restarted. See [controlled reload](docs/design/daemon-lifecycle-design.md#controlled-code-reload).

Node identity maintenance starts automatically on write: exact canonical names
and case/spacing/punctuation-only variants of the same node kind reuse the
Expand Down
5 changes: 4 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,10 @@ CLI/管理能力,不是额外的模型侧 Pi 工具。保留候选筛选是 dr
L4/L5 或删除其语义解释需要显式命令。`memory delete` 保留不可变来源历史。

服务拒绝同库的第二个 daemon,惰性打开 SQLite,并清理过期的进程租约。daemon
和客户端命令要用同一个 `--data-dir` 或 `--db`。
和客户端命令要用同一个 `--data-dir` 或 `--db`。在协调的代码升级窗口,
`nmg daemon reload` 等待已接受工作完成并受控换代,防止客户端争抢第二 owner;
持久记忆/黑板保留,临时 AG 与 writer 租约清空。缺少能力的旧 daemon 明确拒绝,
不会被偷偷重启。见[受控 reload](docs/design/daemon-lifecycle-design.md#controlled-code-reload)。

节点身份维护在写入时自动启动:同一节点种类的精确规范名和仅大小写/空格/标点
差异的变体复用现有节点。更大范围的语义合并不靠嵌入相似度单独触发;它们需要
Expand Down
19 changes: 15 additions & 4 deletions claude-plugins/nmg-memory/agents/memory-copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@
description: nmgPrompts.search_description,
inputSchema: {
query: z.string().describe("Focused recall query"),
recallCue: z.string().optional().describe(nmgPrompts.search_recall_cue_parameter_description),
recallRevision: z
.number()
.int()
.positive()
.optional()
.describe(nmgPrompts.search_recall_revision_parameter_description),
limit: z.number().int().min(1).max(50).default(8),
maxTier: z.number().int().min(0).max(3).optional(),
graphHops: z.number().int().min(0).max(3).optional(),
Expand All @@ -68,10 +75,13 @@
},
},
async (params) => {
await invokeDaemon(connection, "sessionActiveGraph", {
action: "beginDisclosureTurn",
sessionId: BOARD_SESSION_ID,
}).catch(() => undefined);
// An explicit preparation must survive until its piggybacked search.
if (params.recallRevision === undefined) {
await invokeDaemon(connection, "sessionActiveGraph", {
action: "beginDisclosureTurn",
sessionId: BOARD_SESSION_ID,
}).catch(() => undefined);
}
const r = (await invokeDaemon(connection, "search", {
...params,
sessionId: BOARD_SESSION_ID,
Expand Down Expand Up @@ -202,145 +212,145 @@
.describe(nmgPrompts.remember_board_source_parameter_description),
},
},
async (params) => {
if (params.action === "claim_outcome") {
if (
!params.memoryId ||
!params.claimOutcome ||
!params.semanticTaskId?.trim() ||
!params.claimSourceLineage?.trim()
) {
throw new Error(
"action=claim_outcome requires memoryId, claimOutcome, semanticTaskId, and claimSourceLineage",
);
}
const result = await invokeDaemon(connection, "recordClaimOutcomes", {
semanticTaskId: params.semanticTaskId,
activeGraphId: params.activeGraphId,
sessionId: BOARD_SESSION_ID,
collectionOrigin: "natural",
votes: [
{
memoryId: params.memoryId,
claimIndexes: params.claimIndexes,
outcome: params.claimOutcome,
source: "task",
sourceLineage: params.claimSourceLineage,
weight: params.claimWeight,
},
],
});
return {
content: [
{
type: "text",
text: `Task-attributed claim outcome recorded. ${JSON.stringify(result)}`,
},
],
};
}
if (params.action === "forget") {
if (!params.memoryId) throw new Error("action=forget requires memoryId");
const result = await invokeDaemon(connection, "resolveRemember", {
action: "forget",
memoryId: params.memoryId,
sessionId: BOARD_SESSION_ID,
});
return {
content: [
{
type: "text",
text: `Memory withdrawn from normal retrieval. ${JSON.stringify(result)}`,
},
],
};
}
if (params.action === "resolve" || params.action === "reopen") {
if (!params.memoryId) throw new Error(`action=${params.action} requires memoryId`);
const result = await invokeDaemon(connection, "resolveRemember", {
action: params.action,
memoryId: params.memoryId,
relatedMemoryIds: params.relatedMemoryIds,
reason: params.resolutionReason,
sessionId: BOARD_SESSION_ID,
});
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
if (params.action === "supersede") {
if (!params.newMemoryId || !params.supersededMemoryId) {
throw new Error("action=supersede requires newMemoryId and supersededMemoryId");
}
const result = await invokeDaemon(connection, "resolveRemember", {
action: "supersede",
newMemoryId: params.newMemoryId,
supersededMemoryId: params.supersededMemoryId,
reason: params.resolutionReason,
sessionId: BOARD_SESSION_ID,
});
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
if (params.action === "relate") {
if (!params.newMemoryId || !params.relatedMemoryId || !params.relationJudgement) {
throw new Error(
"action=relate requires newMemoryId, relatedMemoryId, and relationJudgement",
);
}
const result = await invokeDaemon(connection, "resolveRemember", {
action: "relate",
newMemoryId: params.newMemoryId,
relatedMemoryId: params.relatedMemoryId,
relationJudgement: params.relationJudgement,
confidence: params.relationConfidence,
sessionId: BOARD_SESSION_ID,
});
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
if (!params.statement?.trim() || !params.nodeName?.trim()) {
throw new Error("action=save requires statement and nodeName");
}
const boardSource = params.boardSource;
const memory: Record<string, unknown> = { ...params };
for (const key of [
"action",
"boardSource",
"memoryId",
"newMemoryId",
"supersededMemoryId",
"relatedMemoryId",
"relationJudgement",
"relationConfidence",
"resolutionReason",
"relatedMemoryIds",
"activeGraphId",
"semanticTaskId",
"claimOutcome",
"claimSourceLineage",
"claimIndexes",
"claimWeight",
]) {
delete memory[key];
}
const r = (await invokeDaemon(connection, "remember", {
...memory,
sessionId: BOARD_SESSION_ID,
...(boardSource
? {
markers: [
{
kind: "board_origin",
attributes: {
taskId: boardSource.taskId,
entryId: boardSource.entryId,
},
},
],
}
: {}),
})) as Parameters<typeof renderRememberSurface>[0];
return {
content: [{ type: "text", text: renderRememberSurface(r) }],
};
},

Check notice on line 353 in claude-plugins/nmg-memory/agents/memory-copilot.ts

View check run for this annotation

codefactor.io / CodeFactor

claude-plugins/nmg-memory/agents/memory-copilot.ts#L215-L353

Complex Method
);

server.registerTool(
Expand All @@ -352,6 +362,7 @@
capability: z
.enum([
"reasoning_workspace",
"recall_gate",
"memory_graph_reasoner",
"controller_shadow",
"controller_controlled",
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ reading sequence.

- **Product introduction:** [concept map](guides/concept-map.md) for terminology and owner
routes, then the executable [first-recall tutorial](guides/first-recall.md).
- **Shared coordination design:** [event records and Agent task status](design/board-event-records.md) owns the additive event-space contract; delivery status stays in the completion audit.
- **`guides/`** — non-normative navigation and tested walkthroughs.
- **`design/`** — architecture, data models, algorithms, and process contracts.
Superseded designs move to `design/archived/`. The
Expand Down
38 changes: 24 additions & 14 deletions docs/decisions/implemented/2026-09-30-lexical-tie-ranking.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Resolve lexical score ties after evidence selection
# Resolve lexical score ties before evidence budgets

[中文](2026-09-30-lexical-tie-ranking.zh-CN.md) · [Terms used](2026-09-30-lexical-tie-ranking.terms.yaml)

Expand All @@ -12,28 +12,38 @@ Equal first-stage lexical scores leave some selected evidence in a weak order.
The tested small neural ranker did not demonstrate a material, reliable gain over
product ranking on real recall traces. The measured outcomes are in
[the retrieval experiment](../../experiments/retrieval-quality/lexical-tie-ranking-2026-09-30.md).
Resolving ties only after selection cannot recover the preferred record once a
direct cap or AG budget has removed it. A one-record query can therefore return
a different first record from the same two-record query.

## Decision

The FTS5-only path reorders adjacent, exactly equal-`combinedScore` records after
the Active Graph budget has selected them. The tie-break uses IDF-weighted coverage
of the query's word terms over a selected statement and its first 500 evidence
characters. It preserves the selected set, non-tied order, QPP decisions, and
hybrid retrieval. The small neural ranker is a failed experiment for this task;
no neural ranker or model artifact is added by this change.
The FTS5-only path resolves adjacent, exactly equal-`combinedScore` records
before the direct result cap and before AG selection. The existing tie-break
uses IDF-weighted query-word coverage over a statement and its first 500 evidence
characters, with term frequency calculated over the pool at that stage.
It preserves scores and non-tied order, not the selected prefix or QPP observations.
Candidate generation, budget limits, gate parameters, semantic retrieval and forced
surface supplements remain unchanged. Open attachments keep their reserved positions.
No neural ranker or model artifact is added.

## Alternatives considered

- Add the tested MLP to product ranking: rejected because its same-candidate
result was weaker than the existing ranker, and its top-tie gain was not
statistically supported.
- Reorder all candidates or alter candidate generation: outside the measured
rule and would change the evidence budget or recall set.
- Keep existing lexical tie order: loses the measured LoCoMo ranking gain.
- Keep post-selection tie sorting: preserves the selected set but cannot help a
preferred record below either cap, which defeats this small-budget slice.
- Change scores, reorder non-tied candidates or alter candidate generation:
outside this change and unsupported by the controlled comparison.

## Consequences

The rule uses transient CPU work and no model weights. It improves LoCoMo ranking
on the pinned sample while LongMemEval and BEAM have small regressions; R@20 and
candidate membership do not improve. It is not evidence of better semantic
candidate recovery or answer quality.
The rule uses transient CPU work and no model weights. The original post-selection
experiment improved LoCoMo ranking while LongMemEval and BEAM had small regressions;
its unchanged R@20 applies only to that former same-selected-set comparison.
The pre-budget extension has separate
[controlled small-budget evidence](../../experiments/retrieval-quality/lexical-budget-2026-10-10.md).
It can change selected membership, QPP decisions and estimated context cost, and
runs coverage over up to the existing 500-record pool. Neither experiment proves
current online noise rate, general semantic recovery, or answer quality.
Loading
Loading