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
63 changes: 63 additions & 0 deletions apps/desktop/src/main/__tests__/workhub-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { WORKHUB_COORDINATION_SESSION_ID } from '@maka/core/session';
import { workHubTasksSchema } from '../../shared/workhub-tool-schema.js';
import { createWorkHubRuntime } from '../workhub-runtime.js';

const scope = { hostId: 'host', targetEpoch: 'epoch' };
Expand Down Expand Up @@ -59,6 +60,68 @@ test('task delegation binds the tool action to the Host turn and trusted creatio
assert.deepEqual(f.changes, [[scope, 'created', 'target']]);
});

test('linked task operations remain operations at the Host protocol boundary', async () => {
const f = fixture();
await f.runtime.actTasks(scope, 'turn', 'correct-action', {
operation: 'correct',
replacesActionId: 'old-action',
candidateSetId: 'set',
target: { disposition: 'delegate_existing', candidateRef: 'candidate' },
text: 'Move the delegated work',
});
await f.runtime.actTasks(scope, 'turn', 'stop-action', {
operation: 'stop',
targetSessionId: 'target',
});
await f.runtime.actTasks(scope, 'turn', 'resume-action', {
operation: 'resume',
targetSessionId: 'target',
resumesActionId: 'stop-action',
});

assert.deepEqual(f.requests, [
{
turnId: 'turn',
actionId: 'correct-action',
proposal: {
operation: 'correct',
replacesActionId: 'old-action',
target: { disposition: 'delegate_existing', candidateRef: 'candidate' },
},
delegationText: 'Move the delegated work',
candidateSetId: 'set',
},
{
turnId: 'turn',
actionId: 'stop-action',
proposal: { operation: 'stop', expects: { targetSessionId: 'target' } },
},
{
turnId: 'turn',
actionId: 'resume-action',
proposal: {
operation: 'resume',
resumesActionId: 'stop-action',
expects: { targetSessionId: 'target' },
},
},
]);
});

test('the task tool exposes correction as a linked operation, not a disposition', () => {
const correction = {
operation: 'correct',
replacesActionId: 'old-action',
target: { disposition: 'create_new', title: 'Replacement' },
text: 'Correct the earlier delegation',
};
assert.deepEqual(workHubTasksSchema.parse(correction), correction);
assert.equal(
workHubTasksSchema.safeParse({ ...correction, operation: 'replace' }).success,
false,
);
});

test('a Host switch while resolving the workspace prevents delegation', async () => {
const f = fixture();
const original = f.deps.createContext;
Expand Down
13 changes: 8 additions & 5 deletions apps/desktop/src/main/workhub-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,15 @@ export function createWorkHubRuntime(deps: WorkHubRuntimeDeps) {
switch (input.operation) {
case 'delegate_existing': proposal = { disposition: 'delegate_existing', candidateRef: input.candidateRef }; break;
case 'create_new': proposal = { disposition: 'create_new', title: input.title }; break;
case 'replace': proposal = { disposition: 'replace', replacesActionId: input.replacesActionId, target: input.target }; break;
case 'stop': proposal = { disposition: 'stop_work', expects: { targetSessionId: input.targetSessionId } }; break;
case 'resume': proposal = { disposition: 'resume_work', resumesActionId: input.resumesActionId, expects: { targetSessionId: input.targetSessionId } }; break;
case 'correct': proposal = { operation: 'correct', replacesActionId: input.replacesActionId, target: input.target }; break;
case 'stop': proposal = { operation: 'stop', expects: { targetSessionId: input.targetSessionId } }; break;
case 'resume': proposal = { operation: 'resume', resumesActionId: input.resumesActionId, expects: { targetSessionId: input.targetSessionId } }; break;
}
const createsTarget = proposal.disposition === 'create_new' ||
(proposal.disposition === 'replace' && proposal.target.disposition === 'create_new');
const createsTarget =
('disposition' in proposal && proposal.disposition === 'create_new') ||
('operation' in proposal &&
proposal.operation === 'correct' &&
proposal.target.disposition === 'create_new');
const context = createsTarget ? await deps.createContext(scope) : undefined;
requireCurrent(scope);
const result = await client.actWorkHubCoordinationFromTurn({
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/shared/workhub-tool-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export const workHubTasksSchema = z.discriminatedUnion("operation", [
),
z
.object({
operation: z.literal("replace"),
operation: z.literal("correct"),
replacesActionId: z.string().min(1),
candidateSetId: z.string().min(1).optional(),
target: delegationTarget,
Expand Down
66 changes: 42 additions & 24 deletions docs/architecture/workhub-coordination-session-adr.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ durable conversation and execution substrate.
The role is provisioned lazily when WorkHub first needs it and resolves to the same
Session after Runtime Host or application restarts. The Session role representation,
lookup, recovery, and per-Host UI resolution enforce this lifecycle contract. The
coordination transcript and typed dispositions use that same Session substrate.
coordination transcript and typed action proposals use that same Session substrate.

The per-Host boundary is intentional. A Coordination Session coordinates only the
ordinary Sessions belonging to the same Runtime Host. Switching Runtime Hosts
Expand All @@ -76,9 +76,9 @@ ordinary navigation and target discovery.
The Coordination Session is authoritative only for the coordination conversation.
It never acquires authority over an ordinary Session's execution or lifecycle.

## Dispositions and action admission
## Routing dispositions, linked operations, and admission

Every WorkHub input resolves to exactly one proposed **disposition**:
Every ordinary routing input resolves to exactly one proposed **routing disposition**:

- `answer_here`: answer in the Coordination Session.
- `delegate_existing`: delegate concrete work to one bounded, valid ordinary
Expand All @@ -87,36 +87,54 @@ Every WorkHub input resolves to exactly one proposed **disposition**:
- `clarify`: continue clarification in the Coordination Session without guessing a
target or creating a Session.

Linked correction is a user-confirmed coordination operation over a prior durable
delegation, not a model disposition. Its replacement target is still restricted to
`delegate_existing` or explicit `create_new` admission.
Correction, stop, and resume are **linked operations** over a prior durable
delegation, not additional routing dispositions. Correction's replacement target
is still restricted to `delegate_existing` or explicit `create_new` admission.
Stop and resume follow the durable delegation-to-Session-to-Turn lineage rather
than inferring an operation target from a similarly named Session.

The decision flow is:

```text
user input
-> intent analysis
-> Session Resolver or linked-target resolution
-> Coordination policy
-> routing disposition or linked-operation proposal
-> deterministic Action Gate
-> owning Host / Session
```

Intent describes what the user wants; it does not select authority. The Session
Resolver returns bounded existing-Session evidence and never creates a Session.
Linked-target resolution starts from a bounded WorkHub-owned delegation and
follows its durable Message, Turn, and continuation lineage. Coordination policy
combines that evidence into an advisory proposal. Missing, stale, or ambiguous
linkage fails closed instead of falling back to name similarity.

All model and routing output is advisory. Before any write, a deterministic
**Action Gate** admits or rejects the proposed disposition and operation. The gate
**Action Gate** admits or rejects the proposed routing disposition or linked
operation. The gate
enforces Runtime Host and target validity, archive and waiting state, self-route
exclusion, explicit `create_new`, and existing tool and permission ceilings. For a
replacement, the gate additionally requires explicit correction evidence in the
trusted user text, claims the source delegation in Coordination transcript order,
and rejects any later competing replacement intent. Neither a model nor a routing
policy can directly authorize a write, Stop, or expansion of execution authority.

Routing experiments replace Action Intent classification and/or Session Resolver
recall behind the fixed Action Policy and unchanged Action Gate. A strategy names
one Intent component and one Resolver component; it has no proposal-producing
`resolve()` method and owns no visit focus. R2.4 pairs deterministic components;
R3-A pairs model-assisted intent with model-ranked recall; R3-B pairs model-assisted
intent with deterministic recall. These are experiment configurations, not separate
policy implementations or a production model rollout.

Intent output contains no target. Resolver output contains only ranked or ambiguous
opaque candidate references, or no match; it cannot return creation or a disposition.
The controller shares one bounded candidate context across arms; deterministic
components retain full request text, while model adapters bound text at the model
call boundary. The controller passes validated evidence through the same Policy with the same trusted Session snapshot.
A model recall budget does not hide known Sessions from exact-name or correction
rules in that fixed Policy. Policy retains trusted-text creation,
ambiguity, correction and focus constraints. Model ranking alone cannot authorize
work, and every resulting proposal still goes through the Host-owned Gate.
The current production path performs intent analysis and Coordination policy through
the Coordination model's active-Turn task actions. Bounded candidate discovery is
the Session Resolver input to that decision; the transient proposal interface keeps
routing dispositions separate from linked operations. `answer_here` and `clarify`
remain transcript outcomes; only the side-effecting routing subset crosses the Host
proposal boundary. Historical R2.4/R3 experiment configurations remain evaluation
evidence, not a second production policy or a renderer-owned language authority.

Intent output contains no target. Session Resolver output contains only bounded
opaque candidate references; it cannot return creation or a disposition. Linked
target evidence is likewise advisory and cannot prove ownership. Model ranking or
tool selection alone cannot authorize work, and every resulting proposal still goes
through the Host-owned Gate with the original trusted user request.

## Delegation links rather than copies transcripts

Expand Down
35 changes: 29 additions & 6 deletions docs/workhub-domain-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,32 @@ its state without copying its execution transcript or acquiring ownership of
unrelated user work.

**Coordination model** interprets the admitted user request. Local answers and
clarifications are normal model output. Task tools can propose `delegate_existing`,
`create_new`, `replace`, `stop_work`, or `resume_work`; there is no parallel
renderer classifier, exact-name resolver, or synthetic-summary writer.
clarifications are normal model output. Its task actions produce advisory routing
or linked-operation proposals; there is no parallel renderer classifier,
exact-name resolver, or synthetic-summary writer.

**Routing intent** begins a new coordination decision, such as discussing,
executing, creating, or continuing work. _Avoid_: linked intent, disposition.

**Linked intent** refers to a delegation WorkHub already owns and asks to correct,
stop, or resume it. _Avoid_: routing intent, disposition.

**Routing disposition** is the closed outcome of a new routing decision:
`answer_here`, `delegate_existing`, `create_new`, or `clarify`. _Avoid_: stop,
resume, correction.

**Linked operation** changes an existing delegation relationship: `correct`,
`stop`, or `resume`. It is not a routing disposition.

**Session Resolver** recalls and ranks bounded existing ordinary Sessions for a
routing intent. It returns target evidence, never creation or execution authority.

**Linked-target resolution** starts from a bounded WorkHub-owned delegation and
follows its Session, Message, Turn, and continuation lineage. It does not infer an
operation target from a similar display name.

**Coordination policy** combines intent and resolved evidence into either a routing
disposition or a linked-operation proposal. Its output remains advisory.

**Active-Turn action** names the currently executing coordination Turn. The Host
checks its live execution, durable admission and coordination tool profile, then
Expand All @@ -48,9 +71,9 @@ supply its own user-originated authority or attachment locators. Model-prepared

**Action Gate** validates the resulting operation against durable ownership,
candidate freshness, Session identity, archive/waiting state and existing claims.
The model selects an operation; the gate determines whether that exact operation
can be admitted. Creation workspace context comes from Desktop main, outside the
model proposal. Each target still executes under its own permission boundary.
The policy proposes an action; the gate determines whether that exact action can
be admitted. Creation workspace context comes from Desktop main, outside the model
proposal. Each target still executes under its own permission boundary.

## Delegation and recovery

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/workhub-action-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@

import { isRecord } from './record-schema.js';

/**
* Durable coordination receipt schema. The linked-operation variants retain
* their historical `disposition` tags so existing records and replay
* fingerprints stay compatible; transient proposals use `operation` instead.
*/
export type WorkHubActionResult =
| { readonly disposition: 'answer_here'; readonly coordinationTurnId: string }
| { readonly disposition: 'clarify'; readonly coordinationTurnId: string }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,7 @@ test('WorkHub creates new work through the production assignment composition', a
actionId: 'workhub-create-stop-action',
userText: 'Stop Login stability',
proposal: {
disposition: 'stop_work',
operation: 'stop',
expects: { targetSessionId },
},
},
Expand Down Expand Up @@ -1141,7 +1141,7 @@ test('WorkHub Resume and Stop follow logical lineage across repeated physical ha
actionId: 'workhub-resume-stop-resume',
userText: 'Resume Payments',
proposal: {
disposition: 'resume_work',
operation: 'resume',
resumesActionId: 'workhub-resume-stop-delegation',
expects: { targetSessionId: target.id },
},
Expand Down Expand Up @@ -1185,7 +1185,7 @@ test('WorkHub Resume and Stop follow logical lineage across repeated physical ha
actionId: 'workhub-resume-stop-resume',
userText: 'Resume Payments',
proposal: {
disposition: 'resume_work' as const,
operation: 'resume' as const,
resumesActionId: 'workhub-resume-stop-delegation',
expects: { targetSessionId: target.id },
},
Expand Down Expand Up @@ -1243,7 +1243,7 @@ test('WorkHub Resume and Stop follow logical lineage across repeated physical ha
actionId: 'workhub-resume-stop-stop',
userText: 'Stop Payments',
proposal: {
disposition: 'stop_work',
operation: 'stop',
expects: { targetSessionId: target.id },
},
},
Expand Down Expand Up @@ -1340,7 +1340,7 @@ test('WorkHub does not record resume while safe-boundary resume is disabled', as
actionId,
userText: 'Resume Payments',
proposal: {
disposition: 'resume_work',
operation: 'resume',
resumesActionId: 'workhub-disabled-resume-delegation',
expects: { targetSessionId: target.id },
},
Expand Down Expand Up @@ -1459,7 +1459,7 @@ test('WorkHub correction replaces its link without stopping a shared manual Turn
actionId: 'workhub-stop-shared-action',
userText: `Stop ${sourceCandidate.sessionName}`,
proposal: {
disposition: 'stop_work',
operation: 'stop',
expects: { targetSessionId: source.id },
},
},
Expand Down Expand Up @@ -1510,7 +1510,7 @@ test('WorkHub correction replaces its link without stopping a shared manual Turn
userText: `No, move this to ${correctionDestination.sessionName} instead`,
candidateSetId: correctionCandidates.result.candidateSetId,
proposal: {
disposition: 'replace',
operation: 'correct',
replacesActionId: assignment.actionId,
target: {
disposition: 'delegate_existing',
Expand Down
Loading