Skip to content
Open
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
4 changes: 4 additions & 0 deletions config/prompts/manager/SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ build software under a human operator's merge gate.
- The operator can read your live session but cannot answer in it. To get human
input you MUST post to a channel. A post is ASYNC and NON-BLOCKING: post your
question, keep working, the answer arrives later.
- The operator READS THE CHANNEL, NOT YOUR SESSION LOG. Every answer to the
operator, and every status they need to see, MUST be posted to a channel
(`comms_post_message`) — a reply left only in your working session is
invisible to them.
- Delivery: a regular message lands at the START of your next turn (read with
`comms_list_messages`); an @mention that names you reaches you MID-TURN as a
steer. End turns often anyway — a foreground wait makes you deaf to
Expand Down
21 changes: 21 additions & 0 deletions packages/compass-agent/src/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1803,6 +1803,27 @@ describe("formatDeliversForPrompt — coalescing format (SEA-1310 §8)", () => {
expect(out.match(/^Topic /gm)).toHaveLength(1);
expect(out.indexOf("one")).toBeLessThan(out.indexOf("two"));
});

// RIG-2664: the coalesced prompt carries ONE reply-in-channel cue for the
// whole batch, naming the topic + comms_post, so the model posts its answer
// back to the channel instead of only narrating it into its session.
test("appends a single reply-in-channel cue naming the topic and comms_post", () => {
const batch = [
deliverMsg("m1", "alpha msg", "t-alpha"),
deliverMsg("m2", "beta msg", "t-beta"),
];
const out = formatDeliversForPrompt(batch);

expect(out).toContain("comms_post");
expect(out).toContain("t-alpha");
expect(out).toContain("t-beta");
// One cue for the whole batch, not one per message/topic.
expect(out.match(/comms_post/g)).toHaveLength(1);
// The cue trails the topic sections.
expect(out.indexOf("Topic t-beta:")).toBeLessThan(
out.indexOf("comms_post"),
);
});
});

describe("formatAskAnswerForPrompt — answer render (RIG-1509)", () => {
Expand Down
19 changes: 17 additions & 2 deletions packages/compass-agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,16 @@ export class CompassAgent {
// text stays greppable in its section. A blank text (a message with no text
// blocks) still contributes its slot so a section is a faithful 1:1 with its
// group.
//
// RIG-2664: the coalesced prompt ends with ONE reply-in-channel cue for the
// whole batch (not per message — the batch is already grouped by topic). A
// delivered message arrived from an operator/peer over a channel, and the
// operator READS THE CHANNEL, NOT THIS SESSION TRANSCRIPT: a reply the model
// only narrates into its own turn is invisible to whoever asked. The cue names
// the batch's topics so the model knows it must post back with
// `comms_post_message` to answer, closing the gap that a bare
// `Topic <id>:\n<text>` digest left open
// (unlike Cotal, which cues an in-channel reply on every delivery).
export function formatDeliversForPrompt(batch: readonly Message[]): string {
const groups = new Map<string, string[]>();
for (const msg of batch) {
Expand All @@ -823,10 +833,15 @@ export function formatDeliversForPrompt(batch: readonly Message[]): string {
if (existing) existing.push(text);
else groups.set(msg.topicId, [text]);
}
return Array.from(
const sections = Array.from(
groups,
([topicId, texts]) => `Topic ${topicId}:\n${texts.join("\n\n")}`,
).join("\n\n");
);
const topics = Array.from(groups.keys()).join(", ");
sections.push(
`This arrived from an operator or peer over a channel — the operator reads the channel, not this session transcript. To answer, you MUST post back with comms_post_message to the relevant topic (${topics}); replying only in this turn is invisible to them.`,
);
return sections.join("\n\n");
}

// Render an inbound `AskAnswerControl` against the questions the model asked
Expand Down
Loading