Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
504bdb3
feat(coding-agent): surface refinement and queue prompts
snimu Jul 16, 2026
8fdef04
Merge branch 'main' into feat/refinement-status-queue
snimu Jul 16, 2026
b36e9f8
Merge branch 'main' into feat/refinement-status-queue
snimu Jul 16, 2026
3130f0e
fix(coding-agent): preserve internal prompt handoff semantics
snimu Jul 16, 2026
85fd12d
Merge branch 'main' into feat/refinement-status-queue
snimu Jul 17, 2026
0ec478c
Merge branch 'main' into feat/refinement-status-queue
snimu Jul 17, 2026
ccf55b1
Merge branch 'main' into feat/refinement-status-queue
snimu Jul 17, 2026
f0300e2
Merge branch 'main' into feat/refinement-status-queue
snimu Jul 17, 2026
9fbd069
Merge branch 'main' into feat/refinement-status-queue
snimu Jul 17, 2026
d51b8a1
fix(coding-agent): preserve refinement queue order
snimu Jul 17, 2026
a0b8b15
Merge branch 'main' into feat/refinement-status-queue
snimu Jul 18, 2026
a73a1ca
fix: preserve queued prompt semantics
snimu Jul 18, 2026
5f9ebb5
Merge main and surface refinement outcomes
sethkarten Aug 18, 2026
1d9cf0a
feat(coding-agent): show exact refinement edits
sethkarten Aug 18, 2026
76df42d
Merge origin/main; emit refinement status only around the apply phase
snimu Aug 19, 2026
d3a3bb8
chore: restore main's package-lock.json
snimu Aug 19, 2026
a9b5bf6
test: assert isRefining stays false while a public refine waits for idle
snimu Aug 19, 2026
a0fe821
review: bump daemon schema to 17, drop duplicate attach-terminal refi…
snimu Aug 19, 2026
e74629a
test: executable compatibility assertions for revision-17 refinement …
snimu Aug 19, 2026
b9df96f
fix: mirror compaction_outcome handling for refinement_outcome in pri…
snimu Aug 19, 2026
9fff250
Render refinement outcomes like compaction and skill messages
snimu Aug 19, 2026
eb25ef7
Strip the live-status plumbing; keep only the durable outcome message
snimu Aug 19, 2026
8ea6c96
Show a live loader for user-issued /refine by reusing existing edges
snimu Aug 19, 2026
e9a5edb
Extract the shared expandable custom-message card skeleton
snimu Aug 20, 2026
8ebcdcd
Merge remote-tracking branch 'origin/main' into feat/refinement-statu…
snimu Aug 20, 2026
113d50c
Emit refine_failed when a queued /refine command fails
snimu Aug 20, 2026
07bdea9
Align the refinement card layout with compaction and shield its loader
snimu Aug 20, 2026
3832fb8
Merge remote-tracking branch 'origin/main' into feat/refinement-statu…
snimu Aug 20, 2026
26e5a49
Truncate the collapsed refinement summary instead of wrapping
snimu Aug 20, 2026
b248b3e
Remount the refine loader after compaction and hard-clip the collapse…
snimu Aug 20, 2026
6564b96
Merge remote-tracking branch 'origin/main' into feat/refinement-statu…
snimu Aug 20, 2026
e5ba93c
chore: merge main and convert changelog entry to fragment
snimu Aug 20, 2026
d049814
fix: settle the /refine loader on its own result row and discard it o…
snimu Aug 20, 2026
8475c76
Merge remote-tracking branch 'origin/main' into feat/refinement-statu…
sethkarten Aug 20, 2026
6f101fd
fix(coding-agent): correlate refine loader settlement
sethkarten Aug 20, 2026
80c3b6e
fix: emit refine_failed only for the refinement itself
snimu Aug 21, 2026
c2e7cbc
Merge remote-tracking branch 'origin/main' into feat/refinement-statu…
sethkarten Aug 21, 2026
3c2e3ea
fix(coding-agent): remove refine loader on teardown
sethkarten Aug 21, 2026
85f432e
Merge remote-tracking branch 'origin/main' into feat/refinement-statu…
sethkarten Aug 21, 2026
25474d1
fix(coding-agent): retain outcomes when refine audit write fails
sethkarten Aug 21, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added a durable `[refinement]` transcript message after each refinement showing the applied harness edits (expandable to exact before/after diffs via the shared tool-output toggle), and a live loader while a user-issued /refine runs.
91 changes: 72 additions & 19 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ import {
type CustomMessage,
createCompactionOutcomeMessage,
createHeartbeatPromptMessage,
createRefinementOutcomeMessage,
createRlmChildFailureMessage,
createRlmChildTerminalNoticeMessage,
createSessionSlashCommandMessage,
Expand Down Expand Up @@ -1089,7 +1090,7 @@ export class AgentSession {
private _agentMessageOutcomes = new Map<string, AgentMessageOutcome>();
private _lateIpythonSentAgentMessages = new Map<string, KernelSentAgentMessage[]>();
/** Outcome disclosures whose session-file append failed; retained for context rebuilds. */
private readonly _unpersistedCompactionOutcomes: CustomMessage[] = [];
private readonly _unpersistedOutcomes: CustomMessage[] = [];

private _bashAbortController: AbortController | undefined = undefined;
private _userBashRunning = false;
Expand Down Expand Up @@ -4196,12 +4197,12 @@ export class AgentSession {
for (const message of context.messages) {
this._applyLateIpythonSentAgentMessages(message);
}
this._mergeUnpersistedCompactionOutcomes(context.messages);
this._mergeUnpersistedOutcomes(context.messages);
return context;
}

private _mergeUnpersistedCompactionOutcomes(messages: AgentMessage[]): void {
for (const outcome of this._unpersistedCompactionOutcomes) {
private _mergeUnpersistedOutcomes(messages: AgentMessage[]): void {
for (const outcome of this._unpersistedOutcomes) {
let insertAt = messages.length;
while (insertAt > 0 && messages[insertAt - 1]!.timestamp > outcome.timestamp) {
insertAt -= 1;
Expand Down Expand Up @@ -6018,17 +6019,27 @@ export class AgentSession {
const input = action.payload;
try {
let resultText: string | undefined;
let displayResult = true;
switch (input.command.name) {
case "compact":
await this.compact(input.command.args || undefined, {
skipAbort: true,
});
break;
case "refine": {
const options = parseRefineCommandOptions(input.command.args);
const result = await this.refine(options, { skipAbort: true });
const applied = result.appliedEdits.filter((appliedEdit) => appliedEdit.applied).length;
let result: RefinementResult;
try {
const options = parseRefineCommandOptions(input.command.args);
result = await this.refine(options, { skipAbort: true });
} catch (error) {
// Only a failure of the refinement itself is a refine failure; a later
// result-row persist error must not report a completed refinement as failed.
this._emitRefineFailed(this._asError(error));
throw error;
}
const applied = result.appliedEdits.filter((edit) => edit.applied).length;
resultText = `Refined continual harness state: ${applied} edit${applied === 1 ? "" : "s"} applied.`;
displayResult = false;
break;
}
case "goal":
Expand All @@ -6042,7 +6053,7 @@ export class AgentSession {
break;
}
if (resultText) {
this._appendDurableSessionCommandMessage(resultText, input.command, true, false);
this._appendDurableSessionCommandMessage(resultText, input.command, true, false, displayResult);
}
} catch (error) {
if (error instanceof CompactionSkippedError) return;
Expand All @@ -6055,7 +6066,15 @@ export class AgentSession {
true,
);
} catch {
// Surfacing the command failure matters more than persisting its row.
// The result row is also the command-correlated UI settle edge.
const message = createSessionSlashCommandResultMessage(`Command failed: ${commandError.message}`, {
command: input.command,
success: false,
severity: "error",
error: commandError.message,
});
this._emit({ type: "message_start", message });
this._emit({ type: "message_end", message });
}
throw commandError;
}
Expand All @@ -6066,14 +6085,19 @@ export class AgentSession {
command: SessionSlashCommand,
isResult: boolean,
isError = false,
display = true,
): void {
const message: CustomMessage = isResult
? createSessionSlashCommandResultMessage(content, {
command,
success: !isError,
severity: isError ? "error" : "info",
...(isError ? { error: content.replace(/^Command failed:\s*/, "") } : {}),
})
? createSessionSlashCommandResultMessage(
content,
{
command,
success: !isError,
severity: isError ? "error" : "info",
...(isError ? { error: content.replace(/^Command failed:\s*/, "") } : {}),
},
display,
)
: createSessionSlashCommandMessage(command);
// Persist before touching live state so a failed write cannot leave an
// unsaved leaf that the next entry would silently parent onto.
Expand Down Expand Up @@ -7403,7 +7427,7 @@ export class AgentSession {
);
const newEntries = this.sessionManager.getEntries();
this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
this._mergeUnpersistedCompactionOutcomes(this.agent.state.messages);
this._mergeUnpersistedOutcomes(this.agent.state.messages);
this._restoreLateIpythonSentAgentMessages();

const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary) as
Expand Down Expand Up @@ -8047,6 +8071,24 @@ export class AgentSession {
return { ...plan, baselineState };
}

private _recordRefinementOutcome(result: RefinementResult): void {
const message = createRefinementOutcomeMessage(result);
try {
this.sessionManager.appendCustomMessageEntryWithRollback(
message.customType,
message.content,
message.display,
message.details,
);
} catch {
// Not in the session file, so context rebuilds would drop the outcome.
this._unpersistedOutcomes.push(message);
}
this.agent.state.messages.push(message);
this._emit({ type: "message_start", message });
this._emit({ type: "message_end", message });
}
Comment thread
cursor[bot] marked this conversation as resolved.

/**
* Synchronous application phase: disconnects from the agent, aborts any
* in-flight agent run, applies the refinement plan to disk and memory, then
Expand Down Expand Up @@ -8119,7 +8161,18 @@ export class AgentSession {
if (targetScope === "global") {
appendGlobalRefinement(globalHarnessStateDir, result);
}
this.sessionManager.appendCustomEntry("prime-agent.refinement", result);
let refinementAuditAppendError: { error: unknown } | undefined;
try {
this.sessionManager.appendCustomEntry("prime-agent.refinement", result);
} catch (error) {
refinementAuditAppendError = { error };
}
try {
this._recordRefinementOutcome(result);
} catch (error) {
if (!refinementAuditAppendError) throw error;
}
if (refinementAuditAppendError) throw refinementAuditAppendError.error;
this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
this.agent.state.systemPrompt = this._baseSystemPrompt;
try {
Expand Down Expand Up @@ -8342,7 +8395,7 @@ export class AgentSession {
{ reason, outcome },
);
// Not in the session file, so context rebuilds would drop the disclosure.
this._unpersistedCompactionOutcomes.push(outcomeMessage);
this._unpersistedOutcomes.push(outcomeMessage);
}
this.agent.state.messages.push(outcomeMessage);
this._emit({ type: "message_start", message: outcomeMessage });
Expand Down Expand Up @@ -11307,7 +11360,7 @@ export class AgentSession {

const sessionContext = this.sessionManager.buildSessionContext();
this.agent.state.messages = sessionContext.messages;
this._mergeUnpersistedCompactionOutcomes(this.agent.state.messages);
this._mergeUnpersistedOutcomes(this.agent.state.messages);
this._restoreLateIpythonSentAgentMessages();
this._reloadGoalStateFromBranch();
this._reloadRlmMaxDepthFromBranch();
Expand Down
61 changes: 60 additions & 1 deletion packages/coding-agent/src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { AgentMessage } from "@earendil-works/pi-agent-core";
import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai";
import type { AgentCronJob } from "./cron-jobs.js";
import type { AppliedRefinementEdit, HarnessScope, RefinementResult } from "./refinement/refinement.js";
import { isSessionSlashCommandName, parseSessionSlashCommand, type SessionSlashCommand } from "./slash-commands.js";

export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
Expand All @@ -31,6 +32,7 @@ export const IPYTHON_STATE_RESTORED_CUSTOM_TYPE = "ipython_state_restored";
export const SESSION_SLASH_COMMAND_CUSTOM_TYPE = "session_slash_command";
export const SESSION_SLASH_COMMAND_RESULT_CUSTOM_TYPE = "session_slash_command_result";
export const COMPACTION_OUTCOME_CUSTOM_TYPE = "compaction_outcome";
export const REFINEMENT_OUTCOME_CUSTOM_TYPE = "refinement_outcome";
export const RLM_CHILD_FAILURE_CUSTOM_TYPE = "rlm_child_failure";
export const RLM_CHILD_TERMINAL_NOTICE_CUSTOM_TYPE = "rlm_child_terminal_notice";

Expand Down Expand Up @@ -73,6 +75,20 @@ export interface CompactionOutcomeMessage extends CustomMessage<CompactionOutcom
details: CompactionOutcomeDetails;
}

export interface RefinementOutcomeDetails {
refinementId: string;
summary: string;
scope: HarnessScope;
rollbackOf?: string;
edits: AppliedRefinementEdit[];
}

export interface RefinementOutcomeMessage extends CustomMessage<RefinementOutcomeDetails> {
customType: typeof REFINEMENT_OUTCOME_CUSTOM_TYPE;
content: string;
details: RefinementOutcomeDetails;
}

export interface RlmChildFailureDetails {
childId: string;
sessionName: string;
Expand Down Expand Up @@ -324,6 +340,27 @@ export function createCompactionOutcomeMessage(
};
}

export function createRefinementOutcomeMessage(
result: RefinementResult,
display = true,
timestamp = Date.now(),
): RefinementOutcomeMessage {
return {
role: "custom",
customType: REFINEMENT_OUTCOME_CUSTOM_TYPE,
content: `Refinement complete: ${result.summary}`,
display,
details: {
refinementId: result.id,
summary: result.summary,
scope: result.scope ?? "local",
...(result.rollbackOf ? { rollbackOf: result.rollbackOf } : {}),
edits: result.appliedEdits,
},
timestamp,
};
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
Expand Down Expand Up @@ -397,6 +434,27 @@ export function isCompactionOutcomeMessage(message: unknown): message is Compact
);
}

function isAppliedRefinementEdit(value: unknown): value is AppliedRefinementEdit {
return (
isRecord(value) &&
(value.action === "create" || value.action === "update" || value.action === "delete") &&
typeof value.kind === "string" &&
typeof value.id === "string" &&
typeof value.applied === "boolean"
);
}

export function isRefinementOutcomeMessage(message: unknown): message is RefinementOutcomeMessage {
if (!isRecord(message) || !hasValidCustomMessageEnvelope(message, REFINEMENT_OUTCOME_CUSTOM_TYPE)) return false;
if (!isRecord(message.details)) return false;
return (
typeof message.details.summary === "string" &&
(message.details.scope === "local" || message.details.scope === "global") &&
Array.isArray(message.details.edits) &&
message.details.edits.every(isAppliedRefinementEdit)
);
}

export function createHeartbeatPromptMessage(
job: AgentCronJob,
timestamp = Date.now(),
Expand Down Expand Up @@ -443,7 +501,8 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
if (
m.customType === SESSION_SLASH_COMMAND_CUSTOM_TYPE ||
m.customType === SESSION_SLASH_COMMAND_RESULT_CUSTOM_TYPE ||
m.customType === COMPACTION_OUTCOME_CUSTOM_TYPE
m.customType === COMPACTION_OUTCOME_CUSTOM_TYPE ||
m.customType === REFINEMENT_OUTCOME_CUSTOM_TYPE
) {
return undefined;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/coding-agent/src/modes/headless-completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type CompactionOutcomeMessage,
isCompactionOutcomeMessage,
isSessionSlashCommandResultMessage,
REFINEMENT_OUTCOME_CUSTOM_TYPE,
type SessionSlashCommandResultMessage,
} from "../core/messages.js";

Expand All @@ -37,7 +38,11 @@ export function selectHeadlessTerminalResult(messages: readonly AgentMessage[]):
}
// A corrupt outcome is still part of the terminal outcome suffix. Skip it
// without letting it hide earlier valid outcomes or their failure status.
if (message.role === "custom" && message.customType === COMPACTION_OUTCOME_CUSTOM_TYPE) {
if (
message.role === "custom" &&
(message.customType === COMPACTION_OUTCOME_CUSTOM_TYPE ||
message.customType === REFINEMENT_OUTCOME_CUSTOM_TYPE)
) {
index--;
continue;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,24 @@
import { Box, Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui";
import { Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui";
import type { CompactionSummaryMessage } from "../../../core/messages.js";
import { getMarkdownTheme, theme } from "../theme/theme.js";
import { customMessageLabel, ExpandableCustomMessageBox } from "./expandable-custom-message.js";
import { expandCollapseHint } from "./keybinding-hints.js";

/**
* Component that renders a compaction message with collapsed/expanded state.
* Uses same background color as custom messages for visual consistency.
*/
export class CompactionSummaryMessageComponent extends Box {
private expanded = false;
private message: CompactionSummaryMessage;
private markdownTheme: MarkdownTheme;

constructor(message: CompactionSummaryMessage, markdownTheme: MarkdownTheme = getMarkdownTheme()) {
super(1, 1, (t) => theme.bg("customMessageBg", t));
this.message = message;
this.markdownTheme = markdownTheme;
this.updateDisplay();
}

setExpanded(expanded: boolean): void {
this.expanded = expanded;
this.updateDisplay();
}

override invalidate(): void {
super.invalidate();
/** Compaction summary card: full markdown summary when expanded. */
export class CompactionSummaryMessageComponent extends ExpandableCustomMessageBox {
constructor(
private readonly message: CompactionSummaryMessage,
private readonly markdownTheme: MarkdownTheme = getMarkdownTheme(),
) {
super();
this.updateDisplay();
}

private updateDisplay(): void {
protected updateDisplay(): void {
this.clear();

const tokenStr = this.message.tokensBefore.toLocaleString();
const label = theme.fg("customMessageLabel", `\x1b[1m[compaction]\x1b[22m`);
const label = customMessageLabel("compaction");
this.addChild(new Text(label, 0, 0));
this.addChild(new Spacer(1));

Expand Down
Loading
Loading