Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,44 @@ describe('native cue from kernel-built mention content', () => {
expect(wake).not.toMatch(/commonly_claim_message|must be claimed first/);
});

// Cofounder's follow-up on #2261: dropping a frame whole also dropped its
// NO_REPLY guidance. A frame that teaches NO_REPLY loses only the sentences
// naming a tool native lacks.
test('a reworded wake frame whose cut misses keeps NO_REPLY and loses only the claim sentence', async () => {
const reworded = (await wakeOnMessage())
.replace(' If you do act, the message must be claimed first', ' If you act, claim the message first');
expect(reworded).toContain('If you act, claim the message first (commonly_claim_message)');

const cue = nativeCueFromKernel(reworded, 'pod-1', NATIVE_TOOL_NAMES);

expect(cue).toMatch(/\[Wake-on-message: .*return NO_REPLY as your ENTIRE reply.*\]/);
expect(cue).not.toMatch(/commonly_claim_message|claim the message first/);
});

test('an agent-DM frame that teaches NO_REPLY and names a tool native lacks keeps NO_REPLY', () => {
const dmFrame = '[1:1 agent-DM with @peer (Peer) — talk directly to them, not a broadcast room. '
+ 'Reply only when your message materially advances the work; return NO_REPLY when the exchange '
+ 'reaches a natural conclusion. Open a side channel with commonly_open_dm if you need one.]';
const cue = nativeCueFromKernel(`${dmFrame}\n\nare we done?`, 'pod-1', NATIVE_TOOL_NAMES);

expect(cue).toBe('[1:1 agent-DM with @peer (Peer) — talk directly to them, not a broadcast room. '
+ 'Reply only when your message materially advances the work; return NO_REPLY when the exchange '
+ 'reaches a natural conclusion.]\n\nare we done?');
});

test('the native pod frame never shows the upload syntax, which an agent repeats verbatim', async () => {
const cue = nativeCueFromKernel(await referenceWake(), 'pod-1', NATIVE_TOOL_NAMES);
expect(cue).not.toMatch(/\[\[upload/);
});

test('a frame that still names a tool native lacks is dropped, not shown', () => {
const content = '[Trigger: raised by **alice**.]\n[Some new cue: call commonly_brand_new_tool.]\n\nhi';
const cue = nativeCueFromKernel(content, 'pod-1', NATIVE_TOOL_NAMES);
expect(cue).toBe('[Trigger: raised by **alice**.]\n\nhi');
// Without NO_REPLY to keep, a frame goes whole, even when only one of its
// sentences names the tool: its remaining guidance was written around it.
const twoSentences = '[Other cue: be brief. Then call commonly_brand_new_tool.]\n\nhi';
expect(nativeCueFromKernel(twoSentences, 'pod-1', NATIVE_TOOL_NAMES)).toBe('hi');
});

test('content that is not kernel-framed passes through unchanged', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,33 @@ describe('AgentMessageService phantom-upload-directive footer', () => {
expect(persistedDoc.content).toContain('`[[upload:abc.png|abc.png|12|image]]`');
});

// QA walk 2026-10-16: explaining that it cannot open attachments, an agent
// typed the grammar's placeholder and drew a note it then retracted.
it.each([
["I can't open attached files ([[upload:...]]) in this pod — paste the text instead."],
['Shared files show up as [[upload:…]] in chat; paste the text and I will work from that.'],
])('a placeholder directive with no letter or digit is not a file reference: %s', async (content) => {
mockFindAgainst({});

await AgentMessageService.postMessage({
agentName: 'openclaw', instanceId: 'aria', podId: '6a0da39bae757028b39f87a6', content,
});
expect(persistedDoc.content).not.toContain('system note');
expect(persistedDoc.content).toContain(content);
});

it('a made-up name still draws the note beside a placeholder', async () => {
mockFindAgainst({});

await AgentMessageService.postMessage({
agentName: 'openclaw', instanceId: 'aria', podId: '6a0da39bae757028b39f87a6',
content: 'Like [[upload:...]] — here it is: [[upload:report.md]]',
});

expect(persistedDoc.content).toContain('no matching attachment was found');
expect(persistedDoc.content).toContain('report.md');
});

it('does NOT append a footer when a File row matches the directive', async () => {
// Real upload exists → no phantom.
mockFindAgainst({
Expand Down
6 changes: 5 additions & 1 deletion backend/services/agentMessageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1155,7 +1155,11 @@ class AgentMessageService {
.trim()
.replace(/[^A-Za-z0-9._\-/+ ]/g, '')
.slice(0, 256);
if (sanitized && !referencedNames.includes(sanitized)) {
// A name with no letter or digit (`[[upload:...]]`, `[[upload:…]]`)
// is the grammar's placeholder, not a file: an agent explaining
// that it cannot open attachments typed it and drew a "no matching
// attachment" note it then had to retract (QA walk, 2026-10-16).
if (sanitized && /[A-Za-z0-9]/.test(sanitized) && !referencedNames.includes(sanitized)) {
referencedNames.push(sanitized);
}
}
Expand Down
36 changes: 28 additions & 8 deletions backend/services/nativeKernelCue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ import {
// itself; the tool-agnostic @handle rule is kept.
export const nativePodContextFrame = (podId: string): string => (
`[${KERNEL_FRAME_TAGS.podContext}: this conversation is in pod \`${podId}\`. Reply with `
+ 'commonly_post_message; it posts as you. You cannot open or attach files here: if a '
+ 'message points to an attached file ([[upload:...]]), say so and ask whoever posted it '
+ 'to paste the content. To ask a person for something, @-mention their handle: a bare name '
+ 'reaches no one.]'
+ 'commonly_post_message; it posts as you. You cannot open or attach files here: if someone '
+ 'refers to a file they shared, say so and ask them to paste its content. To ask a person '
+ 'for something, @-mention their handle: a bare name reaches no one.]'
);

// A frame cut just before `marker`, closed again; null when the marker is
Expand All @@ -44,6 +43,22 @@ const CUT_MARKERS: Readonly<Record<string, string>> = Object.freeze({

const TOOL_NAME = /commonly_[a-z_]+/g;

// A frame that teaches NO_REPLY keeps it. Dropping such a frame whole because
// one sentence names a tool native lacks would also drop "return NO_REPLY as
// your ENTIRE reply", and an agent without it posts its private reasoning
// (AX-43). So there only the offending sentences go (Cofounder's follow-up on
// #2261); a frame left without NO_REPLY, or still naming a foreign tool, is
// dropped.
const SENTENCE_BREAK = /(?<=[.!?])\s+/;
const withoutSentencesNaming = (frame: string, foreign: readonly string[]): string | null => {
const close = frame.lastIndexOf(']');
const sentences = frame.slice(1, close).split(SENTENCE_BREAK);
const kept = sentences.filter((sentence) => !foreign.some((name) => sentence.includes(name)));
if (!kept.length) return null;
const stripped = `[${kept.join(' ')}]`;
return stripped.includes('NO_REPLY') ? stripped : null;
};

/**
* The kernel's mention content as a native turn should read it. `nativeTools`
* is every tool a native turn can be offered: after the rewrites above, a
Expand All @@ -63,11 +78,16 @@ export const nativeCueFromKernel = (content: string, podId: string, nativeTools:
else if (tag === KERNEL_FRAME_TAGS.trigger) next = frame.replace(/commonly_get_messages/g, 'commonly_read_context');
else if (tag && CUT_MARKERS[tag]) next = cutBefore(frame, CUT_MARKERS[tag]) ?? frame;
const foreign = [...new Set(next.match(TOOL_NAME) || [])].filter((name) => !allowed.has(name));
if (foreign.length) {
console.warn(`[native-cue] dropped a kernel frame (${tag || 'untagged'}) naming ${foreign.join(', ')}`);
return [];
if (!foreign.length) return [next];
const stripped = next.includes('NO_REPLY') ? withoutSentencesNaming(next, foreign) : null;
if (stripped && !foreign.some((name) => stripped.includes(name))) {
console.warn(
`[native-cue] kept a kernel frame (${tag || 'untagged'}) for its NO_REPLY, without ${foreign.join(', ')}`,
);
return [stripped];
}
return [next];
console.warn(`[native-cue] dropped a kernel frame (${tag || 'untagged'}) naming ${foreign.join(', ')}`);
return [];
});
return joinKernelFrames(rewritten, body);
};
Loading