Skip to content

Commit e42bcee

Browse files
committed
fix: persist orphaned user leaf repair
1 parent 56de90e commit e42bcee

3 files changed

Lines changed: 90 additions & 7 deletions

File tree

extensions/discord/src/monitor/message-handler.process.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -645,10 +645,10 @@ export async function processDiscordMessage(
645645
? (payload) => draftPreview.updateFromPartial(payload.text)
646646
: undefined,
647647
onAssistantMessageStart: draftPreview.draftStream
648-
? draftPreview.handleAssistantMessageBoundary
648+
? () => draftPreview.handleAssistantMessageBoundary()
649649
: undefined,
650650
onReasoningEnd: draftPreview.draftStream
651-
? draftPreview.handleAssistantMessageBoundary
651+
? () => draftPreview.handleAssistantMessageBoundary()
652652
: undefined,
653653
onModelSelected,
654654
suppressDefaultToolProgressMessages:

src/agents/pi-embedded-runner/run/attempt.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
isPrimaryBootstrapRun,
1515
mergeOrphanedTrailingUserPrompt,
1616
normalizeMessagesForLlmBoundary,
17+
removeSessionManagerLeafEntry,
1718
prependSystemPromptAddition,
1819
remapInjectedContextFilesToWorkspace,
1920
resetEmbeddedAgentBaseStreamFnCacheForTest,
@@ -479,6 +480,54 @@ describe("remapInjectedContextFilesToWorkspace", () => {
479480
});
480481
});
481482

483+
describe("removeSessionManagerLeafEntry", () => {
484+
it("removes the persisted leaf entry before rebuilding context", () => {
485+
const orphan = { type: "message", id: "orphan-user", parentId: "assistant-1" };
486+
const fileEntries = [
487+
{ type: "session", id: "session-1" },
488+
{ type: "message", id: "user-1", parentId: null },
489+
{ type: "message", id: "assistant-1", parentId: "user-1" },
490+
orphan,
491+
];
492+
const manager = {
493+
fileEntries,
494+
byId: new Map(fileEntries.map((entry) => [entry.id, entry])),
495+
leafId: "orphan-user" as string | null,
496+
branch: vi.fn(),
497+
resetLeaf: vi.fn(),
498+
_rewriteFile: vi.fn(),
499+
};
500+
501+
removeSessionManagerLeafEntry({ sessionManager: manager, leafEntry: orphan });
502+
503+
expect(manager.fileEntries.map((entry) => entry.id)).toEqual([
504+
"session-1",
505+
"user-1",
506+
"assistant-1",
507+
]);
508+
expect(manager.byId.has("orphan-user")).toBe(false);
509+
expect(manager.leafId).toBe("assistant-1");
510+
expect(manager._rewriteFile).toHaveBeenCalledTimes(1);
511+
expect(manager.branch).not.toHaveBeenCalled();
512+
expect(manager.resetLeaf).not.toHaveBeenCalled();
513+
});
514+
515+
it("falls back to in-memory branching when the manager cannot rewrite", () => {
516+
const manager = {
517+
branch: vi.fn(),
518+
resetLeaf: vi.fn(),
519+
};
520+
521+
removeSessionManagerLeafEntry({
522+
sessionManager: manager,
523+
leafEntry: { id: "orphan-user", parentId: "assistant-1" },
524+
});
525+
526+
expect(manager.branch).toHaveBeenCalledWith("assistant-1");
527+
expect(manager.resetLeaf).not.toHaveBeenCalled();
528+
});
529+
});
530+
482531
describe("shouldWarnOnOrphanedUserRepair", () => {
483532
it("warns for user and manual runs", () => {
484533
expect(shouldWarnOnOrphanedUserRepair("user")).toBe(true);

src/agents/pi-embedded-runner/run/attempt.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,44 @@ function isMidTurnPrecheckAssistantError(message: AgentMessage | undefined): boo
563563
return record.stopReason === "error" && record.errorMessage === MID_TURN_PRECHECK_ERROR_MESSAGE;
564564
}
565565

566+
export function removeSessionManagerLeafEntry(params: {
567+
sessionManager: {
568+
branch: (branchFromId: string) => void;
569+
resetLeaf: () => void;
570+
};
571+
leafEntry: { id?: string; parentId?: string | null };
572+
}): void {
573+
const mutableSessionManager = params.sessionManager as unknown as {
574+
fileEntries?: Array<{ id?: string; type?: string }>;
575+
byId?: Map<string, unknown>;
576+
leafId?: string | null;
577+
_rewriteFile?: () => void;
578+
};
579+
const leafId = params.leafEntry.id;
580+
const fileEntries = mutableSessionManager.fileEntries;
581+
const leafIndex = leafId ? fileEntries?.findIndex((entry) => entry.id === leafId) : -1;
582+
583+
if (
584+
leafId &&
585+
fileEntries &&
586+
leafIndex !== undefined &&
587+
leafIndex >= 0 &&
588+
typeof mutableSessionManager._rewriteFile === "function"
589+
) {
590+
fileEntries.splice(leafIndex, 1);
591+
mutableSessionManager.byId?.delete(leafId);
592+
mutableSessionManager.leafId = params.leafEntry.parentId ?? null;
593+
mutableSessionManager._rewriteFile();
594+
return;
595+
}
596+
597+
if (params.leafEntry.parentId) {
598+
params.sessionManager.branch(params.leafEntry.parentId);
599+
} else {
600+
params.sessionManager.resetLeaf();
601+
}
602+
}
603+
566604
function removeTrailingMidTurnPrecheckAssistantError(params: {
567605
activeSession: { agent: { state: { messages: AgentMessage[] } } };
568606
sessionManager: ReturnType<typeof guardSessionManager>;
@@ -2738,11 +2776,7 @@ export async function runEmbeddedAttempt(
27382776
});
27392777
effectivePrompt = orphanPromptMerge.prompt;
27402778
if (orphanPromptMerge.removeLeaf) {
2741-
if (leafEntry.parentId) {
2742-
sessionManager.branch(leafEntry.parentId);
2743-
} else {
2744-
sessionManager.resetLeaf();
2745-
}
2779+
removeSessionManagerLeafEntry({ sessionManager, leafEntry });
27462780
const sessionContext = sessionManager.buildSessionContext();
27472781
activeSession.agent.state.messages = sessionContext.messages;
27482782
}

0 commit comments

Comments
 (0)