diff --git a/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts b/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts index d3547e6658..302d644243 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts @@ -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' }, ]); @@ -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', @@ -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: { diff --git a/apps/desktop/src/main/__tests__/runtime-host-workhub-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-workhub-ipc-main.test.ts index 2aa190fb92..70a84dedf9 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-workhub-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-workhub-ipc-main.test.ts @@ -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 unknown>(); let resolveCalls = 0; - const answers: unknown[] = []; const records: unknown[] = []; const actions: unknown[] = []; const changes: unknown[] = []; @@ -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; @@ -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', @@ -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', diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts index 5a39dba44b..61c6990f11 100644 --- a/apps/desktop/src/main/runtime-host-client.ts +++ b/apps/desktop/src/main/runtime-host-client.ts @@ -977,11 +977,7 @@ export class DesktopRuntimeHostClient { return this.request("workhub.coordination.act", input); } - answerWorkHubCoordination( - input: OperationInput<"workhub.coordination.answer">, - ): Promise> { - return this.request("workhub.coordination.answer", input); - } + recordWorkHubCoordination( input: OperationInput<"workhub.coordination.record">, diff --git a/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts b/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts index 4210a0c38c..1ebca81bf6 100644 --- a/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-workhub-ipc-main.ts @@ -31,7 +31,6 @@ import type { ReconnectableReadIpcMain } from './ipc-reconnect-policy.js'; type RuntimeHostWorkHubClient = Pick< DesktopRuntimeHostClient, | 'actWorkHubCoordination' - | 'answerWorkHubCoordination' | 'listWorkHubCoordinationCandidates' | 'recordWorkHubCoordination' | 'resolveWorkHubCoordinationSession' @@ -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), ); diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index fe84b755f4..055caf837e 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -1035,11 +1035,6 @@ export interface MakaBridge { workHub: { /** Resolve the active Runtime Host's stable coordination conversation. */ resolveCoordinationSession(): Promise; - /** 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, @@ -1054,11 +1049,6 @@ export interface MakaBridge { coordinationSessionId: string, input: Omit, 'create'>, ): Promise>; - /** Create an ordinary Session on the exact Host owning the resolved conversation. */ - createSession( - coordinationSessionId: string, - input: { name: string }, - ): Promise; }; sessions: { list(filter?: SessionListFilter): Promise; diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index ce26162376..c0714d7f81 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -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 }, @@ -1962,16 +1952,6 @@ const makaBridge = { }, }; }, - async createSession( - coordinationSessionId: string, - input: { name: string }, - ): Promise { - const scope = await resolveDesktopWorkHubCoordinationCreateScope( - coordinationSessionId, - runtimeHostSessionRef, - ); - return createDesktopSessionOnScope(scope, input); - }, }, sessions: { list(filter?: SessionListFilter): Promise { diff --git a/packages/core/src/__tests__/workhub-creation-intent.test.ts b/packages/core/src/__tests__/workhub-creation-intent.test.ts index 5919ddedb8..04cc0f8bb6 100644 --- a/packages/core/src/__tests__/workhub-creation-intent.test.ts +++ b/packages/core/src/__tests__/workhub-creation-intent.test.ts @@ -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); + } +}); diff --git a/packages/core/src/workhub-creation-intent.ts b/packages/core/src/workhub-creation-intent.ts index c767cf5442..f55679b311 100644 --- a/packages/core/src/workhub-creation-intent.ts +++ b/packages/core/src/workhub-creation-intent.ts @@ -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; @@ -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); @@ -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 {