Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
315 changes: 309 additions & 6 deletions index.ts

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions src/auto-capture-cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ function stripLeadingRuntimeWrappers(text: string): string {
continue;
}

// Bug fix: also strip known boilerplate continuation lines (e.g.
// "Results auto-announce to your requester.", "Do not use any memory tools.")
// that appear right after the wrapper prefix. These lines do NOT match the
// wrapper prefix regex but are part of the wrapper boilerplate.
if (strippingLeadIn) {
AUTO_CAPTURE_RUNTIME_WRAPPER_BOILERPLATE_RE.lastIndex = 0;
if (AUTO_CAPTURE_RUNTIME_WRAPPER_BOILERPLATE_RE.test(current)) {
continue;
}
}

strippingLeadIn = false;
cleanedLines.push(line);
}
Expand Down
91 changes: 91 additions & 0 deletions src/feedback-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* FeedbackConfigManager — Proposal A v3 Phase 3
* Provides configurable feedback amplitudes for dynamic importance adjustment.
*/
export interface FeedbackConfig {
importanceBoostOnUse: number; // default: 0.05
importanceBoostOnConfirm: number; // default: 0.15
importancePenaltyOnMiss: number; // default: 0.03
importancePenaltyOnError: number; // default: 0.10
minRecallCountForPenalty: number; // default: 2
minRecallCountForBoost: number; // default: 1
confirmKeywords: string[];
errorKeywords: string[];
}

export class FeedbackConfigManager {
constructor(private config: FeedbackConfig) {}

/**
* Compute the importance delta for a given event.
* @param event 'use' | 'confirm' | 'miss' | 'error'
* @param recallCount - number of times this memory was recalled (injected_count)
* @param badRecallCount - current bad_recall_count
*/
computeImportanceDelta(
event: 'use' | 'confirm' | 'miss' | 'error',
recallCount: number = 1,
badRecallCount: number = 0,
): number {
if (event === 'use') {
if (recallCount < this.config.minRecallCountForBoost) return 0;
return this.config.importanceBoostOnUse;
}
if (event === 'confirm') {
return this.config.importanceBoostOnConfirm;
}
if (event === 'miss') {
if (recallCount < this.config.minRecallCountForPenalty) return 0;
return -this.config.importancePenaltyOnMiss;
}
if (event === 'error') {
return -this.config.importancePenaltyOnError;
}
return 0;
}

isConfirmKeyword(text: string): boolean {
return this.config.confirmKeywords.some(k =>
text.toLowerCase().includes(k.toLowerCase()),
);
}

isErrorKeyword(text: string): boolean {
return this.config.errorKeywords.some(k =>
text.toLowerCase().includes(k.toLowerCase()),
);
}

static defaultConfig(): FeedbackConfig {
return {
importanceBoostOnUse: 0.05,
importanceBoostOnConfirm: 0.15,
importancePenaltyOnMiss: 0.03,
importancePenaltyOnError: 0.10,
minRecallCountForPenalty: 2,
minRecallCountForBoost: 1,
confirmKeywords: ['是對的', '確認', '正確', 'right'],
errorKeywords: ['錯誤', '不對', 'wrong', 'not right'],
};
}

static fromRaw(raw?: Record<string, unknown> | null): FeedbackConfigManager {
const cfg = raw ?? {};
return new FeedbackConfigManager({
importanceBoostOnUse:
typeof cfg.importanceBoostOnUse === 'number' ? cfg.importanceBoostOnUse : 0.05,
importanceBoostOnConfirm:
typeof cfg.importanceBoostOnConfirm === 'number' ? cfg.importanceBoostOnConfirm : 0.15,
importancePenaltyOnMiss:
typeof cfg.importancePenaltyOnMiss === 'number' ? cfg.importancePenaltyOnMiss : 0.03,
importancePenaltyOnError:
typeof cfg.importancePenaltyOnError === 'number' ? cfg.importancePenaltyOnError : 0.10,
minRecallCountForPenalty:
typeof cfg.minRecallCountForPenalty === 'number' ? cfg.minRecallCountForPenalty : 2,
minRecallCountForBoost:
typeof cfg.minRecallCountForBoost === 'number' ? cfg.minRecallCountForBoost : 1,
confirmKeywords: Array.isArray(cfg.confirmKeywords) ? cfg.confirmKeywords : ['是對的', '確認', '正確', 'right'],
errorKeywords: Array.isArray(cfg.errorKeywords) ? cfg.errorKeywords : ['錯誤', '不對', 'wrong', 'not right'],
});
}
}
95 changes: 95 additions & 0 deletions src/reflection-slices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,98 @@ export function extractReflectionSliceItems(reflectionText: string): ReflectionS
export function extractInjectableReflectionSliceItems(reflectionText: string): ReflectionSliceItem[] {
return buildReflectionSliceItemsFromSlices(extractInjectableReflectionSlices(reflectionText));
}

/**
* Check if a recall was actually used by the agent.
* This function determines whether the agent's response shows awareness of the injected memories.
*
* @param responseText - The agent's response text
* @param injectedIds - Array of memory IDs that were injected
* @param injectedSummaries - Optional array of summary text lines that were injected;
* if the response contains any of these verbatim or partially,
* it is a strong usage signal even without explicit markers or IDs.
* @returns true if the response shows evidence of using the recalled information
*/
export function isRecallUsed(
responseText: string,
injectedIds: string[],
injectedSummaries?: string[],
): boolean {
if (!responseText || responseText.length <= 24) {
return false;
}
if ((!injectedIds || injectedIds.length === 0) && (!injectedSummaries || injectedSummaries.length === 0)) {
return false;
}

const responseLower = responseText.toLowerCase();

// Step 1: Check if the response contains any specific injected memory ID.
// This is a prerequisite for confirming actual usage.
const hasSpecificRecall = injectedIds.some(
(id) => id && responseLower.includes(id.toLowerCase()),
);

// Step 2: If a specific ID is present, also check for generic usage phrases.
// Both conditions must be met (AND logic) to confirm the recall was used.
if (hasSpecificRecall) {
const usageMarkers = [
"remember",
"之前",
"记得",
"according to",
"based on what",
"as you mentioned",
"如前所述",
"如您所說",
"如您所说的",
"我記得",
"我记得",
"之前你說",
"之前你说",
"之前提到",
"之前提到的",
"根据之前",
"依据之前",
"按照之前",
"照您之前",
"照你说的",
"from previous",
"earlier you",
"in the memory",
"the memory mentioned",
"the memories show",
];

for (const marker of usageMarkers) {
if (responseLower.includes(marker.toLowerCase())) {
return true;
}
}
}

// Bug 1 fix (isRecallUsed): when summaries are provided, check if the response
// contains any of the injected summary text verbatim or as a near-identical
// substring. This catches the case where the agent directly uses the memory
// content without any explicit marker phrase.
if (injectedSummaries && injectedSummaries.length > 0) {
const responseTrimmedLower = responseText.trim().toLowerCase();
for (const summary of injectedSummaries) {
if (summary && summary.trim().length > 0) {
const summaryLower = summary.trim().toLowerCase();
// Check for verbatim or near-verbatim presence (at least 10 chars to avoid
// false positives on very short fragments).
if (
summaryLower.length >= 10 &&
(responseTrimmedLower.includes(summaryLower) ||
// Also check the reverse (summary contains response snippet — agent echoed it)
summaryLower.includes(responseTrimmedLower.slice(0, Math.min(50, responseTrimmedLower.length))))
) {
return true;
}
}
}
}

return false;
}
9 changes: 7 additions & 2 deletions src/retriever.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@ export class MemoryRetriever {
);
} else {
results = await this.hybridRetrieval(
query, safeLimit, scopeFilter, category, trace,
query, safeLimit, scopeFilter, category, trace, source,
);
}

Expand Down Expand Up @@ -749,7 +749,12 @@ export class MemoryRetriever {
);

failureStage = "vector.postProcess";
const recencyBoosted = this.applyRecencyBoost(mapped);
// Bug 7 fix: when decayEngine is active, skip applyRecencyBoost here
// because applyDecayBoost already incorporates recency into its composite
// score. Calling both double-counts recency for vector-only results.
const recencyBoosted = this.decayEngine
? mapped
: this.applyRecencyBoost(mapped);
if (diagnostics) diagnostics.stageCounts.afterRecency = recencyBoosted.length;
const weighted = this.decayEngine
? recencyBoosted
Expand Down
2 changes: 1 addition & 1 deletion src/smart-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function stripEnvelopeMetadata(text: string): string {
// 0. Strip runtime orchestration wrappers that should never become memories
// (sub-agent task scaffolding is execution metadata, not conversation content).
let cleaned = text.replace(
/^\[(?:Subagent Context|Subagent Task)\]\s*(?:You are running as a subagent.*?(?:$|(?<=\.)\s+)|Results auto-announce to your requester\.?\s*|do not busy-poll for status\.?\s*|Reply with a brief acknowledgment only\.?\s*|Do not use any memory tools\.?\s*)?/gim,
/^\[(?:Subagent Context|Subagent Task)\]\s*(?:You are running as a subagent\b.*?(?:$|(?<=\.)\s+)|Results auto-announce to your requester\.?\s*|do not busy-poll for status\.?\s*|Reply with a brief acknowledgment only\.?\s*|Do not use any memory tools\.?\s*)?/gim,
"",
);
cleaned = cleaned.replace(
Expand Down
Loading