Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
83 changes: 67 additions & 16 deletions lib/project-mate-interaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,19 @@ function attachMateInteraction(ctx) {
} catch (e) {}

// Combined gate + digest in one prompt (saves a full round-trip vs separate gate)
var prompt = [
var promptParts = [
"[SYSTEM: Memory Gate + Digest]",
"You are a memory system for an AI Mate (role: " + (mateRole || "assistant") + ").",
"",
"Conversation (" + job.type + "):",
job.conversationContent,
];
if (job.priorSummary) {
promptParts.push("Prior conversation context (memory summary so far):");
promptParts.push(job.priorSummary);
promptParts.push("");
}
promptParts.push("Conversation (" + job.type + "):");
promptParts.push(job.conversationContent);
var prompt = promptParts.concat([
"",
"STEP 1: Should this be saved to memory?",
'Answer "no" ONLY if the entire conversation is trivial (e.g. just "hi"/"hello").',
Expand Down Expand Up @@ -171,7 +178,7 @@ function attachMateInteraction(ctx) {
"user_observations: OPTIONAL array. Include ONLY if you noticed meaningful patterns about the USER themselves (not the topic).",
"Categories: pattern (repeated behavior 2+ times), decision (explicit choice with reasoning), reaction (emotional/attitude signal), preference (tool/style/communication preference).",
"Omit the field entirely if nothing notable about the user.",
].join("\n");
]).join("\n");

function handleResult(text) {
var cleaned = text.trim();
Expand Down Expand Up @@ -225,9 +232,14 @@ function attachMateInteraction(ctx) {
}
}

updateMemorySummary(job.mateCtx, job.mateId, digestObj);
maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
if (job.onDone) job.onDone();
// Skip summary update for failed parses — the fallback digest would degrade quality
if (digestObj.topic !== "parse_failed") {
updateMemorySummary(job.mateCtx, job.mateId, digestObj);
maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
if (job.onDone) job.onDone();
} else {
if (job.onError) job.onError(new Error("parse_failed"));
}
processDigestQueue();
}

Expand All @@ -249,7 +261,7 @@ function attachMateInteraction(ctx) {
console.error("[digest-worker] Error:", err);
_digestWorker = null;
_digestWorkerTurns = 0;
if (job.onDone) job.onDone();
if (job.onError) job.onError(err);
processDigestQueue();
},
});
Expand All @@ -265,11 +277,11 @@ function attachMateInteraction(ctx) {
onError: function (err) {
console.error("[digest-worker] Create error:", err);
_digestWorker = null;
if (job.onDone) job.onDone();
if (job.onError) job.onError(err);
processDigestQueue();
},
}).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function () {
if (job.onDone) job.onDone();
}).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function (err) {
if (job.onError) job.onError(err || new Error("digest worker creation failed"));
processDigestQueue();
});
}
Expand Down Expand Up @@ -303,22 +315,41 @@ function attachMateInteraction(ctx) {
type: "mention",
conversationContent: conversationContent,
onDone: function () { mentionSession._digesting = false; },
onError: function () { mentionSession._digesting = false; },
});
}

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

// Collect full conversation from session history (all user + mate turns)
// Track digest index per session so switching sessions doesn't misalign.
// On resumed sessions (after restart), recover index from the last
// digest_checkpoint entry in history so undigested turns aren't lost.
if (typeof session._dmLastDigestedIndex !== "number") {
session._dmLastDigestedIndex = 0;
for (var ci = session.history.length - 1; ci >= 0; ci--) {
if (session.history[ci].type === "digest_checkpoint") {
session._dmLastDigestedIndex = session.history[ci].digestedIndex;
break;
}
}
}

// Collect only new turns since the last successful digest
var conversationParts = [];
var totalLen = 0;
var CONV_CAP = 6000;
for (var hi = 0; hi < session.history.length; hi++) {
var startIndex = session._dmLastDigestedIndex;
for (var hi = startIndex; hi < session.history.length; hi++) {
if (totalLen >= CONV_CAP) break;
var entry = session.history[hi];
if (entry.type === "user_message" && entry.text) {
var uText = entry.text;
Expand All @@ -335,8 +366,8 @@ function attachMateInteraction(ctx) {
conversationParts.push("Mate: " + aText);
totalLen += aText.length;
}
if (totalLen >= CONV_CAP) break;
}
// If the latest response hasn't landed in history yet, append it
var lastResponseText = responsePreview || "";
if (lastResponseText && conversationParts.length > 0) {
var lastPart = conversationParts[conversationParts.length - 1];
Expand All @@ -362,14 +393,34 @@ function attachMateInteraction(ctx) {
});
}

// Read existing summary to give the digest worker context for delta content
var priorSummary = "";
try {
if (fs.existsSync(summaryFile)) {
priorSummary = fs.readFileSync(summaryFile, "utf8").trim();
}
} catch (e) {}

_dmDigestPending = true;
var snapshotIndex = session.history.length;

enqueueDigest({
mateCtx: mateCtx,
mateId: mateId,
type: "dm",
priorSummary: priorSummary || "",
conversationContent: conversationParts.join("\n"),
onDone: function () { _dmDigestPending = false; },
onDone: function () {
session._dmLastDigestedIndex = snapshotIndex;
// Persist checkpoint so resumed sessions know where to continue
var checkpoint = { type: "digest_checkpoint", digestedIndex: snapshotIndex };
session.history.push(checkpoint);
sm.appendToSessionFile(session, checkpoint);
_dmDigestPending = false;
},
onError: function () {
_dmDigestPending = false;
},
});
}

Expand Down
4 changes: 4 additions & 0 deletions lib/project-sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,10 @@ function attachSessions(ctx) {
}
session.history = session.history.slice(0, trimTo);
session.messageUUIDs = session.messageUUIDs.slice(0, targetIdx);
// Reset digest checkpoint if it points past the trimmed history
if (typeof session._dmLastDigestedIndex === "number" && session._dmLastDigestedIndex > trimTo) {
session._dmLastDigestedIndex = trimTo;
}
}

var kept = session.messageUUIDs;
Expand Down
2 changes: 2 additions & 0 deletions lib/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ function createSessionManager(opts) {

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