Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3740254
fix: auto-capture cumulative turn counting for smart extraction (issu…
Apr 4, 2026
98cb769
fix: re-apply all 7 fixes for issue #417 + add cumulative turn counti…
Apr 4, 2026
fc54c1a
docs: update changelog - add test file reference and improve breaking…
Apr 4, 2026
d3a9ded
fix: Phase 1 - createMockApi accepts pluginConfigOverrides param + re…
jlin53882 Apr 5, 2026
6406614
fix: resolve all Must Fix items from PR #534 review (issue #417)
jlin53882 Apr 6, 2026
49e2066
fix: move currentCumulativeCount reset inside success block (Fix #9)
jlin53882 Apr 6, 2026
9929424
fix: add try-catch around extractAndPersist to prevent hook crash on …
jlin53882 Apr 6, 2026
6312295
fix: clear pendingIngressTexts in catch block on extraction failure (…
jlin53882 Apr 6, 2026
c8b2a4e
fix: add conversationKey guard to Fix #8 + restore test comment
jlin53882 Apr 6, 2026
f9a2a62
fix: Must Fix 1/2/5 from PR #549 review - counter reset always, newTe…
jlin53882 Apr 7, 2026
8b7ba8c
fix: Must Fix 1 revised - reset counter to previousSeenCount on all-d…
jlin53882 Apr 7, 2026
8418aab
fix: revert Must Fix #2 (eligibleTexts.length counting restored) - pr…
jlin53882 Apr 7, 2026
8a907ba
fix: correct test expectation - collected 1 not 2 text(s) after count…
jlin53882 Apr 7, 2026
bf728c2
fix: replace throw in hook with safe return (Fix-Must5)
jlin53882 Apr 8, 2026
6cd3c6d
fix: remove unreachable conversationKey guard (Claude Code review)
jlin53882 Apr 8, 2026
bfcb43a
fix(issue-417): skip regex fallback when all candidates skipped with …
jlin53882 Apr 9, 2026
7baf635
test(issue-417): add Fix-Must1b DM fallback regression test
jlin53882 Apr 9, 2026
83e08ef
fix(issue-417): F1 success block counter reset + rate limiter inside …
jlin53882 Apr 12, 2026
20e5b84
fix(issue-417): document intentional non-reset of counter after regex…
jlin53882 Apr 12, 2026
37257e9
fix(issue-417): MR1 counter虛增 + MR2 cap不合理(Codex對抗式review實作)
jlin53882 Apr 15, 2026
52bf60d
test(issue-417): F5 counter reset success-path regression test
jlin53882 Apr 15, 2026
b389dc0
fix(issue-417): 修復維護者review問題 - test mock schema + 移除runtime cap
jlin53882 Apr 19, 2026
2448d78
fix(issue-417): below-threshold return + CHANGELOG sync (rwmjhb revie…
penggaolai Apr 22, 2026
e0a4bd4
fix(issue-417): remove stale [Fix #6] comment + fix CHANGELOG PR number
penggaolai Apr 22, 2026
58af45f
fix(issue-417): Issue2 export fn + Issue3 Fix#5 explicit remember gua…
penggaolai Apr 22, 2026
214936c
test(issue-417): add R2 Stage 2 LLM dedup + R3 DM key fallback integr…
jlin53882 Apr 24, 2026
90ac13c
fix(issue-417): correct misleading comment — counter uses newTexts.le…
jlin53882 Apr 26, 2026
e328f6d
fix(issue-417): MF1 explicit-remember prepend, MF3 counter based on t…
jlin53882 Apr 27, 2026
2d5e51c
fix(issue-417): MF1 v2 - avoid lastPending duplicate in REPLACE mode
jlin53882 Apr 27, 2026
e9b4136
fix(issue-417): MF3 move let texts before counter; fix MF1 typo
jlin53882 Apr 27, 2026
a5eb219
fix(issue-417): MF1 v3 - includes() check; revert MF3 to newTexts.length
jlin53882 Apr 27, 2026
9d994ff
fix(issue-417-mustfixes): MF2 - move R2 dedup scenario to module scope
jlin53882 Apr 27, 2026
d2840ba
Merge master into fix/issue-417-mustfixes - resolve conflicts in inde…
jlin53882 Apr 28, 2026
8886d6f
fix(pr549/issue-417): export buildAutoCaptureConversationKeyFromIngre…
jlin53882 Apr 28, 2026
42483fe
fix(test): remove non-existent log assertion in multi-round scenario …
Apr 30, 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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## Unreleased

### Fix: cumulative turn counting for auto-capture smart extraction (#417, PR #549)

**Bug**: With `extractMinMessages: 2` + `smartExtraction: true`, single-turn DM conversations always fell through to regex fallback, writing dirty data (`l0_abstract == text`, no LLM distillation).

**Root causes**:
- `autoCaptureSeenTextCount` was overwritten per-event (always 1 for DM), never accumulating
- `buildAutoCaptureConversationKeyFromIngress` returned `null` for DM (no `conversationId`), so `pendingIngressTexts` was never written

**Changes**:
- **Cumulative counting**: `autoCaptureSeenTextCount` now accumulates across events instead of overwriting per-event
- **DM key fallback**: `buildAutoCaptureConversationKeyFromIngress` falls back to `channelId` when `conversationId` is falsy, so DM sessions now correctly write to `pendingIngressTexts` and match the key extracted by `buildAutoCaptureConversationKeyFromSessionKey`
- **Smart extraction threshold**: now uses cumulative turn count (`currentCumulativeCount`) instead of per-event message count
- **MAX_MESSAGE_LENGTH guard**: 5000 char limit per message in `pendingIngressTexts` rolling window prevents OOM from malformed input
- **Test**: added `runCumulativeTurnCountingScenario` in `test/smart-extractor-branches.mjs` verifying turn-1 skip and turn-2 trigger with `extractMinMessages=2`

**⚠️ Breaking change**: `extractMinMessages` semantics changed from "per-event message count" to "cumulative conversation turns". Before: each `agent_end` needed ≥N messages. After: smart extraction triggers at conversation turn N. This is a bug fix since the old semantics were structurally broken for DM; users relying on the old behavior may need to adjust their `extractMinMessages` values.

---

## 1.1.0-beta.2 (Smart Memory Beta + Access Reinforcement)

This is a **beta** release published under the npm dist-tag **`beta`** (it does not affect the stable `latest` channel).
Expand Down
70 changes: 49 additions & 21 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,8 @@ function shouldSkipReflectionMessage(role: string, text: string): boolean {
}

const AUTO_CAPTURE_MAP_MAX_ENTRIES = 2000;
// Guard: skip texts > 5000 chars to prevent embedding API errors (issue #417 Fix #3)
const MAX_MESSAGE_LENGTH = 5000;
const AUTO_CAPTURE_EXPLICIT_REMEMBER_RE =
/^(?:请|請)?(?:记住|記住|记一下|記一下|别忘了|別忘了)[。.!??!]*$/u;

Expand All @@ -823,14 +825,17 @@ function isExplicitRememberCommand(text: string): boolean {
return AUTO_CAPTURE_EXPLICIT_REMEMBER_RE.test(text.trim());
}

function buildAutoCaptureConversationKeyFromIngress(
// DM key fallback: exported for unit testing (issue #417 Fix #1)
export function buildAutoCaptureConversationKeyFromIngress(
channelId: string | undefined,
conversationId: string | undefined,
): string | null {
const channel = typeof channelId === "string" ? channelId.trim() : "";
const conversation = typeof conversationId === "string" ? conversationId.trim() : "";
if (!channel || !conversation) return null;
return `${channel}:${conversation}`;
if (!channel) return null;
// DM: conversationId=undefined -> fallback to channelId (matches regex extract from sessionKey)
// Group: conversationId=exists -> returns channelId:conversationId (matches regex extract)
return conversation ? `${channel}:${conversation}` : channel;
}

/**
Expand Down Expand Up @@ -2213,16 +2218,26 @@ const memoryLanceDBProPlugin = {
}

api.on("message_received", (event: any, ctx: any) => {
const conversationKey = buildAutoCaptureConversationKeyFromIngress(
ctx.channelId,
ctx.conversationId,
);
const normalized = normalizeAutoCaptureText("user", event.content, shouldSkipReflectionMessage);
if (conversationKey && normalized) {
const queue = autoCapturePendingIngressTexts.get(conversationKey) || [];
queue.push(normalized);
autoCapturePendingIngressTexts.set(conversationKey, queue.slice(-6));
pruneMapIfOver(autoCapturePendingIngressTexts, AUTO_CAPTURE_MAP_MAX_ENTRIES);
try {
const conversationKey = buildAutoCaptureConversationKeyFromIngress(
ctx.channelId,
ctx.conversationId,
);
const normalized = normalizeAutoCaptureText("user", event.content, shouldSkipReflectionMessage);
if (conversationKey && normalized) {
if (normalized.length > MAX_MESSAGE_LENGTH) {
api.logger.debug(
`memory-lancedb-pro: skipped pending ingress text (len=${normalized.length} > ${MAX_MESSAGE_LENGTH}) channel=${ctx.channelId}`,
);
} else {
const queue = autoCapturePendingIngressTexts.get(conversationKey) || [];
queue.push(normalized);
autoCapturePendingIngressTexts.set(conversationKey, queue.slice(-6));
pruneMapIfOver(autoCapturePendingIngressTexts, AUTO_CAPTURE_MAP_MAX_ENTRIES);
}
}
} catch (err) {
api.logger.warn(`memory-lancedb-pro: message_received auto-capture error: ${String(err)}`);
}
api.logger.debug(
`memory-lancedb-pro: ingress message_received channel=${ctx.channelId} account=${ctx.accountId || "unknown"} conversation=${ctx.conversationId || "unknown"} from=${event.from} len=${event.content.trim().length} preview=${summarizeTextPreview(event.content)}`,
Expand Down Expand Up @@ -2846,13 +2861,15 @@ const memoryLanceDBProPlugin = {
}

const previousSeenCount = autoCaptureSeenTextCount.get(sessionKey) ?? 0;
// issue #417 Fix #4: cumulative counting — increment not overwrite
const cumulativeCount = previousSeenCount + 1;
let newTexts = eligibleTexts;
if (pendingIngressTexts.length > 0) {
newTexts = pendingIngressTexts;
} else if (previousSeenCount > 0 && eligibleTexts.length > previousSeenCount) {
newTexts = eligibleTexts.slice(previousSeenCount);
}
autoCaptureSeenTextCount.set(sessionKey, eligibleTexts.length);
autoCaptureSeenTextCount.set(sessionKey, cumulativeCount);
pruneMapIfOver(autoCaptureSeenTextCount, AUTO_CAPTURE_MAP_MAX_ENTRIES);

const priorRecentTexts = autoCaptureRecentTexts.get(sessionKey) || [];
Expand Down Expand Up @@ -2946,19 +2963,30 @@ const memoryLanceDBProPlugin = {
}
if (cleanTexts.length >= minMessages) {
api.logger.debug(
`memory-lancedb-pro: auto-capture running smart extraction for agent ${agentId} (${cleanTexts.length} clean texts >= ${minMessages})`,
`memory-lancedb-pro: auto-capture running smart extraction for agent ${agentId} (${cleanTexts.length} clean texts >= ${minMessages}, cumulative=${cumulativeCount})`,
);
const conversationText = cleanTexts.join("\n");
const stats = await smartExtractor.extractAndPersist(
conversationText, sessionKey,
{ scope: defaultScope, scopeFilter: accessibleScopes },
);
// issue #417 Fix #10: prevent hook crash on LLM API errors / network timeouts
let stats: Awaited<ReturnType<typeof smartExtractor.extractAndPersist>> | null = null;
try {
stats = await smartExtractor.extractAndPersist(
conversationText, sessionKey,
{ scope: defaultScope, scopeFilter: accessibleScopes },
);
} catch (err) {
api.logger.error(
`memory-lancedb-pro: smart-extract failed for agent ${agentId}: ${String(err)}`,
);
return; // prevent hook crash — fall through to regex fallback is intentionally skipped
}
// Charge rate limiter only after successful extraction
extractionRateLimiter.recordExtraction();
if (stats.created > 0 || stats.merged > 0) {
api.logger.info(
`memory-lancedb-pro: smart-extracted ${stats.created} created, ${stats.merged} merged, ${stats.skipped} skipped for agent ${agentId}`
`memory-lancedb-pro: smart-extracted ${stats.created} created, ${stats.merged} merged, ${stats.skipped} skipped for agent ${agentId}`,
);
// issue #417 Fix #5: reset counter after successful extraction
autoCaptureSeenTextCount.set(sessionKey, 0);
return; // Smart extraction handled everything
}

Expand All @@ -2973,7 +3001,7 @@ const memoryLanceDBProPlugin = {
);
} else {
api.logger.debug(
`memory-lancedb-pro: auto-capture skipped smart extraction for agent ${agentId} (${cleanTexts.length} < ${minMessages})`,
`memory-lancedb-pro: auto-capture skipped smart extraction for agent ${agentId} (${cleanTexts.length} < ${minMessages}, cumulative=${cumulativeCount})`,
);
}
}
Expand Down
Loading
Loading