fix(session): reject retired tool results in V4 Team checkpoints
Central V4 event admission already rejects tool-result content in Team queue records, but projection checkpoints use the Team state schema directly. After restoring the baseline Team parser during extraction, the production SessionProjectionRegistry.restore() path accepted a decoded native V4 checkpoint containing the retired wrapper without reading an event. Remove only that V4-incompatible schema arm. Keep the existing unknown-JSON parser unchanged; its independent own-key preservation fix remains in the standalone master PR, where recursive V3 tool results are still valid. Exercise the real checkpoint restoration recipe with a valid text state and an invalid retired-result state, plus queued-event projection. The new checkpoint rejection failed before this change; central event admission and the complete Team owner suite pass afterward with 100% projection coverage. No Session schema, format version or projection state version is changed: the rejected value is already outside the native V4 content vocabulary.
This commit is contained in:
1 parent
88897a16c6
commit
4d9c4e7a12
5 files changed
+34
-12
No files matched your search
@@ -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/experimental/agent-team/README.md
|
||||
README.md: 6e2b55018971ec96802de0e5be042d49b45c674a
|
||||
README.zh.md: b497d3790e43a9bc843e2a6b88c9755b698034b5
|
||||
README.md: 31ea956d2e39dfe25410929be2a574b4c3f07a7d
|
||||
README.zh.md: 03344285ceaee965c4154f939f22b592c833840b
|
||||
@@ -145,6 +145,8 @@ Tasks are complete versioned snapshots; every mutation carries `expectedRevision
|
||||
|
||||
Team events are appended to the exact live Lead Session and flushed before the operation reports success or wakes waiters. `team/member`, `team/task`, `team/message/queued`, and `team/message/delivered` are log-only: they never enter the conversation surface, so derived model history is untouched by coordination records. Session event `seq` and `time` own ordering and timing; snapshots do not duplicate them. The `./invariant` companion replays each candidate Team event against its committed prefix and rejects invalid transitions before append.
|
||||
|
||||
Native V4 Team event and checkpoint admission reject retired `tool-result` content before it can enter mailbox state. Historical conversion belongs to the Session-format migration; the Team projection does not convert old wrappers.
|
||||
|
||||
### Disposal
|
||||
|
||||
Disposal closes admission, aborts and awaits admitted creation and mailbox-dispatch transactions, then asks the continuation owner to release the roster's exact live direct children and their descendants; non-Team continuable children of the Lead remain untouched. Cleanup failures make disposal fail visibly, bounded by `disposalTimeoutMs`.
|
||||
|
||||
@@ -145,6 +145,8 @@ Lead 可以停止 teammate 的当前轮次,而不会删除其排队的消息
|
||||
|
||||
Team 事件追加到精确的 live Lead 会话,并在操作报告成功或唤醒等待者之前 flush。`team/member`、`team/task`、`team/message/queued` 与 `team/message/delivered` 仅存在于日志:它们从不进入会话表面,因此派生模型历史不受协作记录影响。顺序与时间由会话事件的 `seq` 与 `time` 负责,快照不重复保存。`./invariant` 伴生插件把每条候选 Team 事件对照已提交前缀回放,并在 append 前拒绝非法转换。
|
||||
|
||||
原生 V4 的 Team 事件及检查点准入会拒绝退役的 `tool-result` 内容,防止它进入邮箱状态。历史转换由 Session 格式迁移负责,Team 投影不转换旧包装。
|
||||
|
||||
### Dispose
|
||||
|
||||
dispose 会关闭准入、中止并等待已获准的创建与 mailbox dispatch 事务,再让 continuation owner 释放 roster 中确切的 live direct child 及其后代;Lead 的非 Team continuable child 不受影响。cleanup 失败会让 dispose 明确失败,并以 `disposalTimeoutMs` 为上限。
|
||||
|
||||
@@ -40,8 +40,8 @@ const imageAttachmentSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
}).strict()
|
||||
|
||||
// ContentBlockMap is merge-extensible. Validate every core variant exactly,
|
||||
// while retaining JSON-decoded plugin variants under an unknown type tag.
|
||||
// Validate the listed variants; retired tool-result tags cannot enter the
|
||||
// merge-extensible fallback for JSON-decoded plugin content.
|
||||
const contentBlockSchema: z.ZodType<ContentBlock> = z.lazy(() => z.union([
|
||||
z.object({ type: z.literal('text'), text: z.string() }).strict(),
|
||||
z.object({ type: z.literal('reasoning'), text: z.string() }).strict(),
|
||||
@@ -52,12 +52,6 @@ const contentBlockSchema: z.ZodType<ContentBlock> = z.lazy(() => z.union([
|
||||
name: z.string(),
|
||||
arguments: z.string(),
|
||||
}).strict(),
|
||||
z.object({
|
||||
type: z.literal('tool-result'),
|
||||
toolCallId: z.string().min(1),
|
||||
content: z.array(contentBlockSchema),
|
||||
isError: z.boolean().optional(),
|
||||
}).strict(),
|
||||
z.object({ type: z.string().min(1) }).loose().refine(
|
||||
block => !coreContentBlockTypes.has(block.type),
|
||||
{ message: 'known content block types must match their declared fields' },
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SESSION_FORMAT_VERSION, SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
|
||||
import { describe, expect, it, onTestFinished } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SessionEventMap, SessionEventType } from '@deepseek-ai/dsh-session'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import { teamProjectionDefinition } from '../src/projection.ts'
|
||||
import type { TeamProjectionState, TeamState } from '../src/projection.ts'
|
||||
import { TeamId, TeamMessageId, TeamTaskId } from '../src/types.ts'
|
||||
@@ -82,6 +84,28 @@ function message(overrides: Partial<TeamMessageSnapshot> = {}): TeamMessageSnaps
|
||||
}
|
||||
|
||||
describe('Agent Teams projection events', () => {
|
||||
it('rejects retired tool-result content when restoring a native V4 Team checkpoint', async () => {
|
||||
const ctx = new Context()
|
||||
onTestFinished(() => ctx.fiber.dispose())
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
ctx.effect(() => ctx.sessionProjections.register(teamProjectionDefinition))
|
||||
const session = ctx.sessions.create(ROOT)
|
||||
const restore = (val: unknown) => ctx.sessionProjections.restore({
|
||||
agentTeam: { ver: teamProjectionDefinition.stateVersion, seq: -1, val },
|
||||
}, [], SessionLogOffset(0), session.header, SessionLogOffset(0))
|
||||
const valid = { ...project(ROOT, []), messages: [message()] }
|
||||
expect(restore(JSON.parse(JSON.stringify(valid))).checkpoint['agentTeam']?.val).toEqual(valid)
|
||||
const retiredMessage = message({ content: [{
|
||||
type: 'tool-result', toolCallId: 'retired', content: [{ type: 'text', text: 'old result' }],
|
||||
}] as unknown as TeamMessageSnapshot['content'] })
|
||||
const retired = { ...valid, messages: [retiredMessage] }
|
||||
expect(() => restore(JSON.parse(JSON.stringify(retired)))).toThrow()
|
||||
expect(() => projectTeam(ROOT, [event('team/message/queued', {
|
||||
version: 2, teamId: TEAM, message: retiredMessage,
|
||||
}, SessionSeq(0))])).toThrow(/team\/message\/queued payload is invalid/)
|
||||
})
|
||||
|
||||
it('projects current-team records independently from inherited records', () => {
|
||||
const records: SessionEvent[] = [
|
||||
event('team/member', { version: 2, teamId: TeamId('ancestor'), member: member() }, SessionSeq(0)),
|
||||
|
||||
Reference in new issue
Block a user