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 @@ -100,7 +100,6 @@ test('resolves WorkHub coordination through the dedicated Host operation', async
{ sessionId: 'maka_workhub_coordination' },
{ candidateSetId: `sha256:${'a'.repeat(64)}`, candidates: [] },
{ disposition: 'answer_here', coordinationTurnId: 'action-turn' },
{ turnId: 'answer-turn' },
{ turnId: 'summary-turn' },
]);

Expand All @@ -119,10 +118,6 @@ test('resolves WorkHub coordination through the dedicated Host operation', async
}),
{ disposition: 'answer_here', coordinationTurnId: 'action-turn' },
);
assert.deepEqual(
await client.answerWorkHubCoordination({ turnId: 'answer-turn', text: 'Question' }),
{ turnId: 'answer-turn' },
);
assert.deepEqual(
await client.recordWorkHubCoordination({
turnId: 'summary-turn',
Expand All @@ -142,10 +137,6 @@ test('resolves WorkHub coordination through the dedicated Host operation', async
proposal: { disposition: 'answer_here' },
},
},
{
operation: 'workhub.coordination.answer',
input: { turnId: 'answer-turn', text: 'Question' },
},
{
operation: 'workhub.coordination.record',
input: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { registerRuntimeHostWorkHubIpc } from '../runtime-host-workhub-ipc-main.
test('projects WorkHub coordination resolution through its dedicated IPC domain', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
let resolveCalls = 0;
const answers: unknown[] = [];
const records: unknown[] = [];
const actions: unknown[] = [];
const changes: unknown[] = [];
Expand All @@ -36,10 +35,6 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
resolveCalls += 1;
return { sessionId: 'maka_workhub_coordination' };
},
answerWorkHubCoordination: async (input: { turnId: string; text: string }) => {
answers.push(input);
return { turnId: input.turnId };
},
recordWorkHubCoordination: async (input: {
turnId: string;
userText: string;
Expand Down Expand Up @@ -79,10 +74,6 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
assert.ok(handler);
assert.deepEqual(await handler({}), { sessionId: 'maka_workhub_coordination' });
assert.equal(resolveCalls, 1);
assert.deepEqual(
await handlers.get('workhub:answer')?.({}, { turnId: 'answer', text: 'Question' }),
{ turnId: 'answer' },
);
assert.deepEqual(
await handlers.get('workhub:record')?.({}, {
turnId: 'record',
Expand All @@ -91,7 +82,6 @@ test('projects WorkHub coordination resolution through its dedicated IPC domain'
}),
{ turnId: 'record' },
);
assert.deepEqual(answers, [{ turnId: 'answer', text: 'Question' }]);
assert.deepEqual(records, [{
turnId: 'record',
userText: 'Request',
Expand Down
6 changes: 1 addition & 5 deletions apps/desktop/src/main/runtime-host-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -977,11 +977,7 @@ export class DesktopRuntimeHostClient {
return this.request("workhub.coordination.act", input);
}

answerWorkHubCoordination(
input: OperationInput<"workhub.coordination.answer">,
): Promise<OperationOutput<"workhub.coordination.answer">> {
return this.request("workhub.coordination.answer", input);
}


recordWorkHubCoordination(
input: OperationInput<"workhub.coordination.record">,
Expand Down
4 changes: 0 additions & 4 deletions apps/desktop/src/main/runtime-host-workhub-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import type { ReconnectableReadIpcMain } from './ipc-reconnect-policy.js';
type RuntimeHostWorkHubClient = Pick<
DesktopRuntimeHostClient,
| 'actWorkHubCoordination'
| 'answerWorkHubCoordination'
| 'listWorkHubCoordinationCandidates'
| 'recordWorkHubCoordination'
| 'resolveWorkHubCoordinationSession'
Expand All @@ -53,9 +52,6 @@ export function registerRuntimeHostWorkHubIpc(
ipcMain.handle('workhub:resolveCoordinationSession', () =>
client.resolveWorkHubCoordinationSession(),
);
ipcMain.handle('workhub:answer', (_event, input) =>
client.answerWorkHubCoordination(input),
);
ipcMain.handle('workhub:record', (_event, input) =>
client.recordWorkHubCoordination(input),
);
Expand Down
10 changes: 0 additions & 10 deletions apps/desktop/src/preload/bridge-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,11 +1035,6 @@ export interface MakaBridge {
workHub: {
/** Resolve the active Runtime Host's stable coordination conversation. */
resolveCoordinationSession(): Promise<string>;
/** Answer an ordinary question inside the persistent Coordination Session. */
answer(
coordinationSessionId: string,
input: { turnId: string; text: string },
): Promise<{ turnId: string }>;
/** Persist one deterministic clarification or routing summary. */
record(
coordinationSessionId: string,
Expand All @@ -1054,11 +1049,6 @@ export interface MakaBridge {
coordinationSessionId: string,
input: Omit<OperationInput<'workhub.coordination.act'>, 'create'>,
): Promise<OperationOutcome<'workhub.coordination.act'>>;
/** Create an ordinary Session on the exact Host owning the resolved conversation. */
createSession(
coordinationSessionId: string,
input: { name: string },
): Promise<DesktopSessionSummary>;
};
sessions: {
list(filter?: SessionListFilter): Promise<DesktopSessionSummary[]>;
Expand Down
20 changes: 0 additions & 20 deletions apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1897,16 +1897,6 @@ const makaBridge = {
(scope) => ipcRenderer.invoke('workhub:resolveCoordinationSession', scope),
);
},
async answer(
coordinationSessionId: string,
input: { turnId: string; text: string },
): Promise<{ turnId: string }> {
const scope = await resolveDesktopWorkHubCoordinationCreateScope(
coordinationSessionId,
runtimeHostSessionRef,
);
return ipcRenderer.invoke('workhub:answer', scope, input) as Promise<{ turnId: string }>;
},
async record(
coordinationSessionId: string,
input: { turnId: string; userText: string; assistantText: string },
Expand Down Expand Up @@ -1962,16 +1952,6 @@ const makaBridge = {
},
};
},
async createSession(
coordinationSessionId: string,
input: { name: string },
): Promise<DesktopSessionSummary> {
const scope = await resolveDesktopWorkHubCoordinationCreateScope(
coordinationSessionId,
runtimeHostSessionRef,
);
return createDesktopSessionOnScope(scope, input);
},
},
sessions: {
list(filter?: SessionListFilter): Promise<DesktopSessionSummary[]> {
Expand Down
60 changes: 60 additions & 0 deletions packages/core/src/__tests__/workhub-creation-intent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1066,3 +1066,63 @@ test('requires a direct, explicitly named command for WorkHub stop authority', (
);
}
});

test('a quoted Session name is a title, not a reason to refuse the request', () => {
// The literal mask blanks quoted spans so a quoted word can never be read as
// a command. That is right for commands and wrong for the one place the
// quotes mark the object itself: naming a Session. Quoting the name is the
// natural way to write it, and it used to be the one way that did not work.
for (const [quoted, bare] of [
['Create a new Session called "Payments"', 'Create a new Session called Payments'],
['请创建一个新会话名为"支付任务"', '请创建一个新会话名为支付任务'],
] as const) {
const quotedIntent = intentFor(quoted);
const bareIntent = intentFor(bare);
assert.equal(quotedIntent.execution, 'imperative', quoted);
assert.equal(quotedIntent.execution, bareIntent.execution, quoted);
assert.deepEqual(quotedIntent.creation.naming, bareIntent.creation.naming, quoted);
}

// The mask still decides everything else. None of these may be promoted.
for (const text of [
'Should we create a new Session called "Payments"?',
'Maybe create a new Session called "Payments" later',
'Do not create a new Session called "Payments"',
'Create a new Session called "',
]) {
assert.notEqual(intentFor(text).execution, 'imperative', text);
}
});

test('a spoken Chinese stop is a stop, in the same range English already covers', () => {
// `停掉` and `停下` are how the request is usually spoken. Without them
// `停掉支付任务` carried no stop cue at all, so the sentence was routed as
// ordinary work and delivered to Payments — asking to stop it started more.
for (const text of ['停掉支付任务', '停下支付任务', '请停掉支付任务']) {
assert.deepEqual(
readWorkHubRequestIntent(text).stop,
{ cue: true, imperative: true, target: '支付任务' },
text,
);
}

// Anaphora is a stop that names nothing, exactly as `Stop it` is: the cue is
// read so the user can be asked which work, and no target is claimed.
for (const text of ['停掉它', '停下它']) {
assert.deepEqual(readWorkHubRequestIntent(text).stop, { cue: true, imperative: false }, text);
// Same shape English gives `Stop it`: a stop was asked for, and no target
// was claimed, so the surface asks which work rather than guessing.
assert.deepEqual(
readWorkHubRequestIntent(text).stop,
readWorkHubRequestIntent('Stop it').stop,
text,
);
}

// Nothing here widens what counts as a stop. `关掉` reads as "switch off",
// which is usually work to do inside a Session, and English admits no
// equivalent; questions and negations stay refused.
for (const text of ['关掉调试日志', '不要停掉支付任务', '怎么停掉支付任务?']) {
assert.equal(readWorkHubRequestIntent(text).stop.cue, false, text);
}
});
29 changes: 22 additions & 7 deletions packages/core/src/workhub-creation-intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,15 @@ const NAMED_CREATION_TITLE_INTRODUCER =
const LEADING_CORRECTION_SEPARATOR = /^[\s,.;:!?,。;:!?—–-]+/u;
const DIRECT_STOP_REQUEST =
/^\s*(?:(?:please|kindly)\s+)?(?:stop|cancel|terminate|halt)\s+(?:(?:the|this)\s+)?(?:(?:session|work|task|job)\s+)?(.+?)\s*[.!。!]?\s*$/iu;
// `停掉` and `停下` are the spoken forms of `停止`, as ordinary as the written
// one. Without them `停掉支付任务` was not a stop at all, so the words were
// delivered to Payments as new work — the opposite of what was asked. English
// covers its own colloquial range with stop/cancel/terminate/halt; this is the
// same range, not a wider claim. `关掉` stays out: it reads as "switch off",
// which is usually work to do inside a Session, and English admits no
// equivalent either.
const DIRECT_CHINESE_STOP_REQUEST =
/^\s*(?:(?:请|请帮我|帮我|麻烦你?)\s*)?(?:停止|取消|终止|中止)\s*(?:(?:这个|该)?(?:会话|工作|任务)\s*)?(.+?)\s*[。!]?\s*$/iu;
/^\s*(?:(?:请|请帮我|帮我|麻烦你?)\s*)?(?:停止|停掉|停下|取消|终止|中止)\s*(?:(?:这个|该)?(?:会话|工作|任务)\s*)?(.+?)\s*[。!]?\s*$/iu;
const UNSAFE_STOP_TARGET =
/^(?:it|this|that|one|everything|all|current|session|work|task|job|(?:this|that|current)\s+(?:session|work|task|job)|它|这个|那个|全部|当前|会话|工作|任务|(?:这个|那个|当前)(?:会话|工作|任务))$/iu;

Expand Down Expand Up @@ -248,15 +255,23 @@ function hasNegatedWorkHubCreationRequest(value: string): boolean {
}

/** Whether already-normalized, literal-masked text is an executable instruction. */
function isImperativeWorkHubNewTopicRequest(normalized: string): boolean {
/**
* `naming` is resolved from the unmasked text by the caller. A quoted title is
* a literal span, so the mask blanks it, and re-deriving the title from masked
* text would read every quoted name as an unusable one — the reason
* `Create a new Session called "Payments"` was refused while the same sentence
* without quotes was admitted. The mask still decides everything else here: it
* exists so quoted words cannot be read as commands, and that is unchanged.
*/
function isImperativeWorkHubNewTopicRequest(
normalized: string,
naming: WorkHubCreationNaming,
): boolean {
const actions = allMatches(normalized, EXECUTION_ACTION);
if (isDeliberative(normalized) || hasUnquotedTerminalWithdrawal(normalized)) {
return false;
}
if (
hasWorkHubNamedCreationClause(normalized) &&
!affirmativeWorkHubNamedCreationTitle(normalized)
) {
if (naming.kind === 'unusable') {
return false;
}
const explicitCreation = isExplicitWorkHubCreationRequest(normalized);
Expand Down Expand Up @@ -309,7 +324,7 @@ export function readWorkHubRequestIntent(value: string): WorkHubRequestIntent {
? 'non_executable'
: hasAmbiguousAdvisoryCommand(masked, actions)
? 'ambiguous'
: isImperativeWorkHubNewTopicRequest(masked)
: isImperativeWorkHubNewTopicRequest(masked, naming)
? 'imperative'
: 'non_executable';
return {
Expand Down