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
15 changes: 13 additions & 2 deletions apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,14 @@ test('drives Desktop Session operations through a real Runtime Host connection',
},
'turn.message.submit': async (input) => {
assert.equal(input.originHostEpoch, hostEpoch);
return { ok: true, result: { disposition: 'steering', queueRevision: 1 } };
return {
ok: true,
result: {
disposition: 'steering',
queueRevision: 1,
skillInvocation: { loaded: [], failed: [], receipts: [] },
},
};
},
}),
beginDrain() {},
Expand Down Expand Up @@ -107,7 +114,11 @@ test('drives Desktop Session operations through a real Runtime Host connection',
content: { text: 'Continue with the new constraints.' },
placement: 'current_turn',
}),
{ disposition: 'steering', queueRevision: 1 },
{
disposition: 'steering',
queueRevision: 1,
skillInvocation: { loaded: [], failed: [], receipts: [] },
},
);

await client.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,11 @@ test("sends canonical content and uploads owned Attachment bytes through the Hos
},
submitMessage: async (input) => {
starts.push(input);
return { disposition: "turn_started", turnId: "turn-1" };
return {
disposition: "turn_started",
turnId: "turn-1",
skillInvocation: { loaded: [], failed: [], receipts: [] },
};
},
});
const ipc = ipcHarness();
Expand Down Expand Up @@ -466,7 +470,11 @@ test("uploads a selected workspace file as a Host-owned Session Artifact", async
},
submitMessage: async (input) => {
starts.push(input);
return { disposition: "turn_started", turnId: "turn-1" };
return {
disposition: "turn_started",
turnId: "turn-1",
skillInvocation: { loaded: [], failed: [], receipts: [] },
};
},
}),
observer: unusedObserver(),
Expand Down Expand Up @@ -566,7 +574,11 @@ test("submits an ordinary composer message once under its stable message identit
getSession: async () => session(),
submitMessage: async (input) => {
submits.push(input);
return { disposition: "turn_started", turnId: "host-turn" };
return {
disposition: "turn_started",
turnId: "host-turn",
skillInvocation: { loaded: [], failed: [], receipts: [] },
};
},
}),
observer: unusedObserver(),
Expand Down Expand Up @@ -679,14 +691,19 @@ test('submits a slash Skill message and reports the Host Skill outcome', async (
test("queues a mid-turn send as steering when the Host reports the session busy", async () => {
const submits: unknown[] = [];
const changes: unknown[] = [];
const skillInvocation = {
loaded: [{ id: 'review', name: 'Review' }],
failed: [{ request: 'typo', reason: 'not_found' as const }],
receipts: [],
};
const ipc = ipcHarness();
registerExecutionIpc(
{
client: executionClient({
getSession: async () => session(),
submitMessage: async (input) => {
submits.push(input);
return { disposition: "steering", queueRevision: 1 };
return { disposition: "steering", queueRevision: 1, skillInvocation };
},
}),
observer: unusedObserver(),
Expand Down Expand Up @@ -721,7 +738,7 @@ test("queues a mid-turn send as steering when the Host reports the session busy"
turnId: "turn-1",
attachments: [],
inlineReferences: [],
skillInvocation: { loaded: [], failed: [], receipts: [] },
skillInvocation,
});
assert.deepEqual(changes, [
{ reason: "status-change", sessionId: "session-1" },
Expand Down Expand Up @@ -805,7 +822,11 @@ test("retries a dispatched send with its original message identity", async () =>
"connection_lost",
);
}
return { disposition: "steering", queueRevision: 1 };
return {
disposition: "steering",
queueRevision: 1,
skillInvocation: { loaded: [], failed: [], receipts: [] },
};
},
}),
newId: () => "id-1",
Expand Down Expand Up @@ -884,6 +905,7 @@ test("answers a send with the Turn the Host started for it", async () => {
return {
disposition: "turn_started",
turnId: "turn-9",
skillInvocation: { loaded: [], failed: [], receipts: [] },
};
},
}),
Expand Down Expand Up @@ -981,7 +1003,11 @@ test("lets the Host queue a textual Skill token as steering", async () => {
getSession: async () => session(),
submitMessage: async (input) => {
submits.push(input);
return { disposition: "steering", queueRevision: 1 };
return {
disposition: "steering",
queueRevision: 1,
skillInvocation: { loaded: [], failed: [], receipts: [] },
};
},
}),
newId: () => "id-1",
Expand Down Expand Up @@ -1052,14 +1078,19 @@ test("reports a Host-blocked Skill send as a Skill failure", async () => {
test("queues explicit Desktop follow-ups", async () => {
const submits: unknown[] = [];
let sequence = 0;
const skillInvocation = {
loaded: [{ id: 'writer', name: 'Writer' }],
failed: [{ request: 'missing', reason: 'not_found' as const }],
receipts: [],
};
const ipc = ipcHarness();
registerExecutionIpc(
{
client: executionClient({
getSession: async () => session(),
submitMessage: async (input) => {
submits.push(input);
return { disposition: "followup", queueRevision: 4 };
return { disposition: "followup", queueRevision: 4, skillInvocation };
},
}),
observer: unusedObserver(),
Expand Down Expand Up @@ -1109,7 +1140,7 @@ test("queues explicit Desktop follow-ups", async () => {
},
],
inlineReferences: [],
skillInvocation: { loaded: [], failed: [], receipts: [] },
skillInvocation,
},
);
assert.deepEqual(submits, [
Expand Down Expand Up @@ -1278,7 +1309,11 @@ test("binds steer and stop to Host-owned queue and active Turn identities", asyn
'Message disposition cannot be proven in this Host Epoch',
);
}
return { disposition: "steering", queueRevision: 2 };
return {
disposition: "steering",
queueRevision: 2,
skillInvocation: { loaded: [], failed: [], receipts: [] },
};
},
interruptTurn: async (input) => {
stopLifecycle.push("interrupt");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ export function registerRuntimeHostSessionExecutionIpc(
turnId: submitted.turnId,
attachments,
inlineReferences,
skillInvocation: submitted.skillInvocation ?? EMPTY_SKILL_INVOCATION,
skillInvocation: submitted.skillInvocation,
};
}
// The sending surface believed this Session idle; nudge it to refresh so
Expand All @@ -385,7 +385,7 @@ export function registerRuntimeHostSessionExecutionIpc(
...(sideConversation ? { messageId } : {}),
attachments,
inlineReferences,
skillInvocation: EMPTY_SKILL_INVOCATION,
skillInvocation: submitted.skillInvocation,
};
},
);
Expand Down Expand Up @@ -487,7 +487,7 @@ export function registerRuntimeHostSessionExecutionIpc(
turnId: result.turnId,
attachments,
inlineReferences,
skillInvocation: result.skillInvocation ?? EMPTY_SKILL_INVOCATION,
skillInvocation: result.skillInvocation,
};
}
// The submitting surface believed this Session idle when it steered;
Expand All @@ -498,7 +498,7 @@ export function registerRuntimeHostSessionExecutionIpc(
disposition: result.disposition,
attachments,
inlineReferences,
skillInvocation: EMPTY_SKILL_INVOCATION,
skillInvocation: result.skillInvocation,
};
},
);
Expand Down
4 changes: 3 additions & 1 deletion docs/desktop-message-queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ Runtime Host already owns the durable message semantics:
- `current_turn` queues steering for the next provider boundary.
- `next_turn` queues a successor turn.
- queue projections are authoritative.
- queue projections carry the canonical queued message content; mutation results return only queue state.
- queue projections carry the canonical queued message content; queue mutation results return only
queue state, while `turn.message.submit` also returns the Skill admission outcome for every
disposition.

## Desktop Behavior

Expand Down
52 changes: 50 additions & 2 deletions packages/cli/src/__tests__/pi-tui-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5049,6 +5049,44 @@ describe('Maka Pi TUI runner', () => {
}
});

test('shows partial Skill feedback when the Host queues the Message', async () => {
const terminal = new FakeTerminal();
const driver = new HostSkillDriver(
{
loaded: [{ id: 'alpha', name: 'Alpha' }],
failed: [{ request: 'typo', reason: 'not_found' }],
receipts: [],
},
'steering',
);
const run = runMakaPiTui({
title: 'Maka',
driver,
cwd: '/repo',
model: 'claude-sonnet-4-5',
connectionSlug: 'claude-subscription',
permissionMode: 'ask',
terminal,
listSkills: async () => [],
});

terminal.input('/skill:alpha /skill:typo 帮我整理');
terminal.input('\r');
await waitFor(() => driver.prompts.length === 1);
await waitFor(() => {
const output = plainTerminalOutput(terminal.output());
return output.includes('已加载技能:Alpha') && output.includes('/skill:typo(未找到)');
});

exitMaka(terminal);
await Promise.race([
run,
delay(CLOSE_BUDGET_MS).then(() => {
throw new Error('TUI did not close during test cleanup');
}),
]);
});

test('does not create a turn when every skill token fails to resolve', async () => {
{
const terminal = new FakeTerminal();
Expand Down Expand Up @@ -7561,7 +7599,10 @@ class SlashCommandDriver extends FakeSessionDriver {
}

class HostSkillDriver extends SlashCommandDriver {
constructor(private readonly skillInvocation: SkillInvocationResult) {
constructor(
private readonly skillInvocation: SkillInvocationResult,
private readonly admittedDisposition: 'turn_started' | 'steering' = 'turn_started',
) {
super();
}

Expand All @@ -7582,6 +7623,9 @@ class HostSkillDriver extends SlashCommandDriver {
// Admitted: the receipt for what was resolved rides the answer, which is
// the client's only sight of it.
const admitted = await super.submitMessage(text, options);
if (this.admittedDisposition === 'steering') {
return { disposition: 'steering', queueRevision: 1, skillInvocation: this.skillInvocation };
}
return admitted?.disposition === 'turn_started'
? { ...admitted, skillInvocation: this.skillInvocation }
: admitted;
Expand Down Expand Up @@ -8318,7 +8362,11 @@ async function admitMessageAsTurn(
summary: { ...fakeSessionSummary(turn.sessionId), ...driver.hostSummary },
}),
);
return { disposition: 'turn_started', turnId: turn.turnId };
return {
disposition: 'turn_started',
turnId: turn.turnId,
skillInvocation: { loaded: [], failed: [], receipts: [] },
};
}

function fakeSessionSummary(
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/pi-tui-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -983,7 +983,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
// was dropped, and the submit answer is the only place it appears: the
// Turn arrives through the started-Turn subscription, which carries
// Session state rather than this Message's admission.
if (result?.disposition === 'turn_started' && result.skillInvocation) {
if (result) {
const { loaded, failed } = result.skillInvocation;
if (loaded.length > 0 || failed.length > 0) showSkillInvocation(result.skillInvocation);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,11 @@ function createMessages(
startFromMessage: async () => {
throw new Error('unexpected root start');
},
prepareMessage: async (input) => ({ kind: 'ready', content: input.content }),
prepareMessage: async (input) => ({
kind: 'ready',
content: input.content,
skillInvocation: { loaded: [], failed: [], receipts: [] },
}),
claimStop: async () => {
throw new Error('unexpected root stop');
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ export class ExecutionFixture {
submittedPlacement: 'current_turn',
placement: 'current_turn',
disposition: 'steering',
skillInvocation: { loaded: [], failed: [], receipts: [] },
admittedAt,
});
const result = await stores.agentRunStore.admitRootTurn({
Expand Down
Loading