Skip to content

Commit 19f179e

Browse files
committed
fix(mates): delta-based DM digests to prevent content loss past 6k chars
The original digest system collected the full conversation from index 0 on every turn, capped at 6000 chars. After ~5 turns, new content was truncated and never reached the memory summary. - Delta collection: only digest new turns since last successful checkpoint - Per-session checkpoint persistence via digest_checkpoint history entries so the index survives server restarts - Prior memory summary injected into digest worker prompt so Haiku has context for small delta fragments - onDone(err) convention: checkpoint only advances on success (null err), preventing permanent data loss on Haiku failures - Skip updateMemorySummary on parse_failed digests - Reset digest index when history is trimmed (rewind) - Filter digest_checkpoint entries from replayHistory
1 parent 56e6944 commit 19f179e

2 files changed

Lines changed: 71 additions & 16 deletions

File tree

lib/project.js

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2272,6 +2272,10 @@ function createProjectContext(opts) {
22722272
}
22732273
session.history = session.history.slice(0, trimTo);
22742274
session.messageUUIDs = session.messageUUIDs.slice(0, targetIdx);
2275+
// Reset digest checkpoint if it points past the trimmed history
2276+
if (typeof session._dmLastDigestedIndex === "number" && session._dmLastDigestedIndex > trimTo) {
2277+
session._dmLastDigestedIndex = trimTo;
2278+
}
22752279
}
22762280

22772281
var kept = session.messageUUIDs;
@@ -3983,12 +3987,19 @@ function createProjectContext(opts) {
39833987
} catch (e) {}
39843988

39853989
// Combined gate + digest in one prompt (saves a full round-trip vs separate gate)
3986-
var prompt = [
3990+
var promptParts = [
39873991
"[SYSTEM: Memory Gate + Digest]",
39883992
"You are a memory system for an AI Mate (role: " + (mateRole || "assistant") + ").",
39893993
"",
3990-
"Conversation (" + job.type + "):",
3991-
job.conversationContent,
3994+
];
3995+
if (job.priorSummary) {
3996+
promptParts.push("Prior conversation context (memory summary so far):");
3997+
promptParts.push(job.priorSummary);
3998+
promptParts.push("");
3999+
}
4000+
promptParts.push("Conversation (" + job.type + "):");
4001+
promptParts.push(job.conversationContent);
4002+
var prompt = promptParts.concat([
39924003
"",
39934004
"STEP 1: Should this be saved to memory?",
39944005
'Answer "no" ONLY if the entire conversation is trivial (e.g. just "hi"/"hello").',
@@ -4034,7 +4045,7 @@ function createProjectContext(opts) {
40344045

40354046
if (digestObj && digestObj.skip) {
40364047
console.log("[digest-worker] Gate declined for " + job.mateId);
4037-
if (job.onDone) job.onDone();
4048+
if (job.onDone) job.onDone(null);
40384049
processDigestQueue();
40394050
return;
40404051
}
@@ -4072,9 +4083,14 @@ function createProjectContext(opts) {
40724083
}
40734084
}
40744085

4075-
updateMemorySummary(job.mateCtx, job.mateId, digestObj);
4076-
maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
4077-
if (job.onDone) job.onDone();
4086+
// Skip summary update for failed parses — the fallback digest would degrade quality
4087+
if (digestObj.topic !== "parse_failed") {
4088+
updateMemorySummary(job.mateCtx, job.mateId, digestObj);
4089+
maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
4090+
if (job.onDone) job.onDone(null);
4091+
} else {
4092+
if (job.onDone) job.onDone(new Error("parse_failed"));
4093+
}
40784094
processDigestQueue();
40794095
}
40804096

@@ -4096,7 +4112,7 @@ function createProjectContext(opts) {
40964112
console.error("[digest-worker] Error:", err);
40974113
_digestWorker = null;
40984114
_digestWorkerTurns = 0;
4099-
if (job.onDone) job.onDone();
4115+
if (job.onDone) job.onDone(err);
41004116
processDigestQueue();
41014117
},
41024118
});
@@ -4112,11 +4128,11 @@ function createProjectContext(opts) {
41124128
onError: function (err) {
41134129
console.error("[digest-worker] Create error:", err);
41144130
_digestWorker = null;
4115-
if (job.onDone) job.onDone();
4131+
if (job.onDone) job.onDone(err);
41164132
processDigestQueue();
41174133
},
4118-
}).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function () {
4119-
if (job.onDone) job.onDone();
4134+
}).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function (err) {
4135+
if (job.onDone) job.onDone(err || new Error("digest worker creation failed"));
41204136
processDigestQueue();
41214137
});
41224138
}
@@ -4153,19 +4169,37 @@ function createProjectContext(opts) {
41534169
});
41544170
}
41554171

4156-
// Digest DM turn for mate projects - uses shared digest worker
4172+
// Digest DM turn for mate projects - uses shared digest worker.
4173+
// Delta-based: only collects new turns since the last successful digest.
4174+
// Concurrency debounce: turns that arrive while a digest is in-flight
4175+
// are naturally batched into the next flush.
41574176
var _dmDigestPending = false;
41584177
function digestDmTurn(session, responsePreview) {
41594178
if (!isMate || _dmDigestPending) return;
41604179
var mateId = path.basename(cwd);
41614180
var mateCtx = matesModule.buildMateCtx(projectOwnerId);
41624181
if (!matesModule.isMate(mateCtx, mateId)) return;
41634182

4164-
// Collect full conversation from session history (all user + mate turns)
4183+
// Track digest index per session so switching sessions doesn't misalign.
4184+
// On resumed sessions (after restart), recover index from the last
4185+
// digest_checkpoint entry in history so undigested turns aren't lost.
4186+
if (typeof session._dmLastDigestedIndex !== "number") {
4187+
session._dmLastDigestedIndex = 0;
4188+
for (var ci = session.history.length - 1; ci >= 0; ci--) {
4189+
if (session.history[ci].type === "digest_checkpoint") {
4190+
session._dmLastDigestedIndex = session.history[ci].digestedIndex;
4191+
break;
4192+
}
4193+
}
4194+
}
4195+
4196+
// Collect only new turns since the last successful digest
41654197
var conversationParts = [];
41664198
var totalLen = 0;
41674199
var CONV_CAP = 6000;
4168-
for (var hi = 0; hi < session.history.length; hi++) {
4200+
var startIndex = session._dmLastDigestedIndex;
4201+
for (var hi = startIndex; hi < session.history.length; hi++) {
4202+
if (totalLen >= CONV_CAP) break;
41694203
var entry = session.history[hi];
41704204
if (entry.type === "user_message" && entry.text) {
41714205
var uText = entry.text;
@@ -4182,8 +4216,8 @@ function createProjectContext(opts) {
41824216
conversationParts.push("Mate: " + aText);
41834217
totalLen += aText.length;
41844218
}
4185-
if (totalLen >= CONV_CAP) break;
41864219
}
4220+
// If the latest response hasn't landed in history yet, append it
41874221
var lastResponseText = responsePreview || "";
41884222
if (lastResponseText && conversationParts.length > 0) {
41894223
var lastPart = conversationParts[conversationParts.length - 1];
@@ -4209,14 +4243,33 @@ function createProjectContext(opts) {
42094243
});
42104244
}
42114245

4246+
// Read existing summary to give the digest worker context for delta content
4247+
var priorSummary = "";
4248+
try {
4249+
if (fs.existsSync(summaryFile)) {
4250+
priorSummary = fs.readFileSync(summaryFile, "utf8").trim();
4251+
}
4252+
} catch (e) {}
4253+
42124254
_dmDigestPending = true;
4255+
var snapshotIndex = session.history.length;
42134256

42144257
enqueueDigest({
42154258
mateCtx: mateCtx,
42164259
mateId: mateId,
42174260
type: "dm",
4261+
priorSummary: priorSummary || "",
42184262
conversationContent: conversationParts.join("\n"),
4219-
onDone: function () { _dmDigestPending = false; },
4263+
onDone: function (err) {
4264+
if (!err) {
4265+
session._dmLastDigestedIndex = snapshotIndex;
4266+
// Persist checkpoint so resumed sessions know where to continue
4267+
var checkpoint = { type: "digest_checkpoint", digestedIndex: snapshotIndex };
4268+
session.history.push(checkpoint);
4269+
sm.appendToSessionFile(session, checkpoint);
4270+
}
4271+
_dmDigestPending = false;
4272+
},
42204273
});
42214274
}
42224275

lib/sessions.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,8 @@ function createSessionManager(opts) {
344344

345345
for (var i = fromIndex; i < total; i++) {
346346
var _item = session.history[i];
347+
// Skip internal bookkeeping entries not meant for the UI
348+
if (_item && _item.type === "digest_checkpoint") continue;
347349
if (_item && (_item.type === "mention_user" || _item.type === "mention_response")) {
348350
console.log("[DEBUG replayHistory] sending mention at index=" + i + " from=" + fromIndex + " total=" + total + " type=" + _item.type + " mate=" + (_item.mateName || ""));
349351
}

0 commit comments

Comments
 (0)