mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 00:02:54 +00:00
* feat(native-chat): a queued card can record a Dispatch's task as its source A chat worker's task is held in the chat's queue like any agent message, so the card needs to say which Dispatch it is from: the sender, run, task and Dispatch ids. An older build reads an unknown kind as the person's card and sends it as written. * feat(orchestration): a chat can be a dispatch worker, like a terminal agent `dispatch --to orca_session_id:<id>` and `worker-start --terminal orca_session_id:<id>` now accept a chat on this host instead of refusing it. - The chat is refused only where mail to it would be: unknown, a provider id, another host, or closed. A chat can't be its own coordinator's worker. - Its task goes through sendAgentTurn as a queued send, as mail notices do: an idle chat starts a turn, a busy one holds a card naming the Dispatch. The operation id is derived from the Dispatch, so a resend replays. - worker-start reads that outcome as the terminal path reads its write; a card held behind a running turn is handed over with its start unobserved. - The Dispatch names the chat by its /clear root, with no pane or process, and records its Orca session id, so its own sub-dispatches nest under it. - Mail to dispatch:<id> reaches the chat; worker-show/list/read read the chat's session records; stop and abandon never close the chat; closing the chat fails its Dispatch as closing a terminal does. - A dispatch preamble for a structured session names the CLI the structured mail lane names. * test(orchestration): a chat worker's reach, and its Dispatch across a /clear At rest is live, a closed chat has exited, and another host or nothing to read is unverifiable. A /clear keeps the chat's Dispatch, and the session that continues the chat reports as it. * fix(orchestration): chat worker review round 1 - dispatch --inject is a keepalive-backed wait, so a chat that takes a while to accept its task no longer reads as a dead runtime at the 30 s idle cut. - An injected task whose delivery is unknown keeps its Dispatch open, as an unknown worker-start does, instead of failing it while the chat may run it. - A busy chat's worker-start receipt says the task waits as a card in the chat's queue, and that worker-abandon does not remove that card. - A close that puts the chat's tab back no longer fails its Dispatch: the closed-chat settle runs after the close's outcome, not at its hide. - worker-start adopting a chat says it gave the task to the chat, instead of claiming it started a terminal agent; the mode value is unchanged. - Test: a chat whose agent has not taken its task reads outcome_unknown. * test(native-chat): a close's hide defers its hidden notice until the close settles The rollback suite pinned the hide's arguments; it now expects the deferred notice, and that the notice is sent once, after a rolled-back tab is back. * fix(orchestration): a chat worker whose successor is unknown is unverifiable, not exited A /clear successor this host has no record of is missing evidence, not proof the chat is gone. Only a closed chat reads exited, so only a closed chat settles its Dispatch; a lost successor reads unverifiable with that reason. * fix(orchestration): chat worker review round 2 - A chat's close notice settles only that chat's Dispatch. A scan of every chat Dispatch read another chat whose close was still in flight (its tab hidden, maybe to be put back) as closed and failed its Dispatch. - Only a dispatch --inject into a chat is a long poll; a terminal inject writes and returns, and keeps its short-RPC slot. - Receipt wording: worker-start says it gave the task to the chat, and a queued task is sent when the chat's queue reaches it. * fix(orchestration): chat worker wording, review round 3 - worker-start's mode sentence for a chat states the placement, which is true whether the task is delivered, queued or refused. - Comments and a test title no longer claim a close notice re-derives every chat worker, or that a queued task waits for the current turn. * fix(orchestration): build a structured worker's task sender from the start's own ids The minted worker's preamble names who its task is from with the run, task and Dispatch ids worker-start already holds, so it reads no Dispatch row. * fix(orchestration): point a chat worker at its held Dispatch mail again A chat worker's coordinator mail lands in its Dispatch's mailbox, but a chat's idle edge re-derived the Dispatch mailbox only for a party with a terminal handle. A pointer lost with the provider then waited for new mail after a restart or a /clear. The re-derivation now looks the Dispatch up by the party's address, which names a worker by its handle and a chat by its root. * fix(native-chat): open a task card's sender by address; a task carries no mail Opening an agent message's sender looked up the mail it carried, which only a mail notice has; with the task kind in the union that read no longer typed. A task's coordinator is found by its address alone.
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import {
|
||
agentMessageSendersShown,
|
||
readAgentMessageSource,
|
||
type AgentMessageSource
|
||
} from './agent-session-message-source'
|
||
import { testOrcaSessionId } from './orca-session-address-test-fixture'
|
||
|
||
const SESSION = '4a1f6c2e-8b3d-4e7a-9c15-0d2b6e8f1a37'
|
||
|
||
const FROM: AgentMessageSource = {
|
||
kind: 'agent',
|
||
senders: [
|
||
{
|
||
party: { address: 'term_a', terminalHandle: 'term_a', orcaSessionId: null },
|
||
name: 'Build tab'
|
||
},
|
||
{
|
||
party: {
|
||
address: `orca_session_id:${SESSION}`,
|
||
terminalHandle: null,
|
||
orcaSessionId: testOrcaSessionId(SESSION)
|
||
},
|
||
name: null
|
||
}
|
||
],
|
||
orchestration: {
|
||
message: 'mail-notice',
|
||
mailbox: 'run:r1',
|
||
dispatchId: null,
|
||
messages: [{ messageId: 'm1', runId: 'r1', from: 'term_a' }]
|
||
}
|
||
}
|
||
|
||
describe("a message's sender, read through its one reader", () => {
|
||
it('reads absence as the person, and a valid sender back whole', () => {
|
||
expect(readAgentMessageSource(undefined)).toBeUndefined()
|
||
expect(readAgentMessageSource(structuredClone(FROM))).toEqual(FROM)
|
||
})
|
||
|
||
it("reads a Dispatch's task back whole", () => {
|
||
const task: AgentMessageSource = {
|
||
...FROM,
|
||
orchestration: { message: 'task', runId: 'r1', taskId: 't1', dispatchId: 'ctx_1' }
|
||
}
|
||
expect(readAgentMessageSource(structuredClone(task))).toEqual(task)
|
||
})
|
||
|
||
it('keeps an agent an agent when its orchestration kind is newer than this build', () => {
|
||
const newer = { ...FROM, orchestration: { message: 'review', dispatchId: 'ctx_1' } }
|
||
expect(readAgentMessageSource(newer)).toEqual({ ...FROM, orchestration: null })
|
||
// A newer sender kind is still not the person.
|
||
expect(readAgentMessageSource({ ...FROM, kind: 'automation' })).toMatchObject({
|
||
kind: 'agent'
|
||
})
|
||
})
|
||
|
||
it('drops a sender it cannot read and bounds a name, keeping the rest', () => {
|
||
const read = readAgentMessageSource({
|
||
...FROM,
|
||
senders: [
|
||
{ party: { address: 'term_a', terminalHandle: 'term_a', orcaSessionId: 'term_a' } },
|
||
{ party: { address: 'term_b', terminalHandle: 'term_b', orcaSessionId: null }, name: 7 },
|
||
{
|
||
party: { address: 'term_c', terminalHandle: 'term_c', orcaSessionId: null },
|
||
name: `ab\n${'x'.repeat(300)}`
|
||
}
|
||
]
|
||
})
|
||
expect(read?.senders.map(({ party, name }) => [party.address, name?.length ?? null])).toEqual([
|
||
['term_b', null],
|
||
['term_c', 200]
|
||
])
|
||
expect(read?.senders[1]?.name?.startsWith('a b ')).toBe(true)
|
||
})
|
||
|
||
it("reads a value that is not a sender at all as the person's message", () => {
|
||
for (const value of [null, 'nobody', 42, [], { senders: [] }]) {
|
||
expect(readAgentMessageSource(value)).toBeUndefined()
|
||
}
|
||
})
|
||
|
||
it('names at most a few senders and counts the rest', () => {
|
||
const many: AgentMessageSource = {
|
||
...FROM,
|
||
senders: Array.from({ length: 5 }, (_, index) => ({
|
||
party: { address: `term_${index}`, terminalHandle: `term_${index}`, orcaSessionId: null },
|
||
name: `Agent ${index}`
|
||
}))
|
||
}
|
||
const { shown, more } = agentMessageSendersShown(many)
|
||
expect(shown.map((sender) => sender.name)).toEqual(['Agent 0', 'Agent 1', 'Agent 2'])
|
||
expect(more).toBe(2)
|
||
})
|
||
})
|