refactor(agent): scope queue actions to edit and remove

This commit is contained in:
kingwl
2026-07-30 01:58:51 +08:00
parent e56c0699a9
commit 7ce330f0e4
49 changed files with 181 additions and 294 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/runtime/README.md
README.md: f0b287d31b3aa9dddd2fde8d3f95bddd290bcf8d
README.zh.md: b2e9c29f0f254fd514f201fcedf6a9cb5e05f86c
README.md: 766d8516225cd46cb1a3a80c832d1cf55e816140
README.zh.md: 9b514afca91f604b3e895187de3b5532bf22a692

View File

@@ -18,7 +18,7 @@ SlotsService gives the renderer separate bare observables for `useSessions` and
## Pending queue projection
`ConversationSnapshot.queue` is the Host's authoritative transient inbox snapshot. Each row carries its `InboxItemId`, complete editable text when every content block is text, a flattened preview, and the accepted queued-or-steering placement. `session/queue` replaces the whole projection; reconnect buffering retains only the latest snapshot, and neither durable turn events nor running-status changes guess that an item was claimed. `Session.updateQueue()` sends edit/remove/promote operations without optimistic mutation, so the next Host snapshot is the sole visible commit and a claim race can surface `queue-item-not-found`.
`ConversationSnapshot.queue` is the Host's authoritative transient Queue snapshot; pending steering stays outside this projection. Each row carries its `InboxItemId`, complete editable text when every content block is text, and a flattened preview. `session/queue` replaces the whole projection; reconnect buffering retains only the latest snapshot, and neither durable turn events nor running-status changes guess that an item was claimed. `Session.updateQueue()` sends edit/remove operations without optimistic mutation, so the next Host snapshot is the sole visible commit and a claim race can surface `queue-item-not-found`.
## Code Mode sub-dispatch index

View File

@@ -18,7 +18,7 @@ SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸
## 待处理队列投影
`ConversationSnapshot.queue` 是 Host 提供的权威瞬态 inbox 快照。每行都携带其 `InboxItemId`、所有内容块均为文本时的完整可编辑文本、扁平化预览,以及接受时确定的 queued 或 steering 放置方式。`session/queue` 会整体替换该投影;重连缓冲只保留最新快照,持久轮次事件和 running 状态变化都不会猜测某个项已被认领。`Session.updateQueue()` 发送编辑/移除/前移操作,不进行乐观更新,因此下一份 Host 快照是唯一可见的提交结果,认领竞态则会返回 `queue-item-not-found`。
`ConversationSnapshot.queue` 是 Host 提供的权威瞬态 Queue 快照;待处理 steering(中途引导)不进入此投影。每行都携带其 `InboxItemId`、所有内容块均为文本时的完整可编辑文本,以及扁平化预览。`session/queue` 会整体替换该投影;重连缓冲只保留最新快照,持久轮次事件和 running 状态变化都不会猜测某个项已被认领。`Session.updateQueue()` 发送编辑/移除操作,不进行乐观更新,因此下一份 Host 快照是唯一可见的提交结果,认领竞态则会返回 `queue-item-not-found`。
## Code Mode 子调用索引

View File

@@ -41,7 +41,7 @@ export interface ISession {
/**
* Apply one mutation to a still-pending queue occurrence.
* @param itemId - agent-owned inbox occurrence identity.
* @param action - edit, remove, or promote operation.
* @param action - edit or remove operation.
* @returns acceptance, or a business/transport error.
*/
updateQueue(itemId: InboxItemId, action: QueueAction): Promise<RpcResult<{ accepted: true }>>

View File

@@ -222,7 +222,6 @@ export interface QueuedMessage {
readonly preview: string
/** Complete editable text; null when the message contains non-text blocks. */
readonly text: string | null
readonly placement: 'queued' | 'steering'
}
/** In-progress assistant output (chunk accumulator product). */

View File

@@ -407,7 +407,6 @@ export class Session implements SessionFace {
id: item.id,
preview: queuePreviewOf(item.message.content),
text: queueTextOf(item.message.content),
placement: item.placement,
}))
this.queueRev++
this.notifier.markDirty()

View File

@@ -21,7 +21,6 @@ const iid = (id: string): InboxItemId => id as InboxItemId
interface QueueFixture {
id: string
body: string
placement?: 'queued' | 'steering'
content?: ContentBlock[]
}
@@ -36,7 +35,6 @@ function queueFrame(items: QueueFixture[]): MuxFrame {
content: item.content ?? text(item.body),
source: { kind: 'user', rpcId: rid(`rpc-${item.id}`) } as never,
}),
placement: item.placement ?? 'queued',
})),
}
}
@@ -46,15 +44,13 @@ function makeSession(): Session {
}
describe('queue snapshot intake', () => {
it('projects stable ids, flat previews, complete text, and placement', () => {
it('projects stable ids, flat previews, and complete text', () => {
const session = makeSession()
session.handleMuxEnvelope(rid('env-1'), queueFrame([
{ id: 'q-1', body: '第一条 排队\n消息' },
{ id: 'q-2', body: '插话', placement: 'steering' },
]))
expect(session.getSnapshot().queue).toEqual([
{ id: 'q-1', preview: '第一条 排队 消息', text: '第一条 排队\n消息', placement: 'queued' },
{ id: 'q-2', preview: '插话', text: '插话', placement: 'steering' },
{ id: 'q-1', preview: '第一条 排队 消息', text: '第一条 排队\n消息' },
])
})
@@ -66,7 +62,7 @@ describe('queue snapshot intake', () => {
content: [{ type: 'text', text: 'hi' }, { type: 'image', data: 'x' } as never],
}]))
expect(session.getSnapshot().queue).toEqual([
{ id: 'q-image', preview: 'hi [image]', text: null, placement: 'queued' },
{ id: 'q-image', preview: 'hi [image]', text: null },
])
})
@@ -90,7 +86,7 @@ describe('queue snapshot intake', () => {
{ id: 'q-2', body: 'two edited' },
]))
expect(session.getSnapshot().queue).toEqual([
{ id: 'q-2', preview: 'two edited', text: 'two edited', placement: 'queued' },
{ id: 'q-2', preview: 'two edited', text: 'two edited' },
])
session.handleMuxEnvelope(rid('env-6'), queueFrame([]))
expect(session.getSnapshot().queue).toEqual([])
@@ -112,12 +108,12 @@ describe('queue operation transport', () => {
session.handleMuxEnvelope(rid('env-op'), queueFrame([{ id: 'q-op', body: 'pending' }]))
const before = session.getSnapshot().queue
await expect(session.updateQueue(iid('q-op'), { kind: 'promote' }))
await expect(session.updateQueue(iid('q-op'), { kind: 'edit', content: text('next') }))
.resolves.toEqual({ ok: true, value: { accepted: true } })
expect(api.callsOf('session.updateQueue')).toEqual([{
sessionId: SID,
itemId: 'q-op',
action: { kind: 'promote' },
action: { kind: 'edit', content: text('next') },
}])
expect(session.getSnapshot().queue).toBe(before)
})