bound inactive subagent timing to projection cut
This commit is contained in:
@@ -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/subagent/subagent/README.md
|
||||
README.md: ec4af55bcd9374b1abb55d7bb098eef568449684
|
||||
README.zh.md: 8323853ff0de2a15da6475fc1433a68f0074ee93
|
||||
README.md: e54f0b98ec3649cec428a47026e6657a9749608b
|
||||
README.zh.md: 1624fa59854d9b61770c5ef0f9d89f7882198da4
|
||||
|
||||
@@ -92,7 +92,7 @@ Provider additions and removals also emit `subagent/provider-added` and `subagen
|
||||
|
||||
Continuable children do not create `SubagentRun` or Tasks. The continuation manager directly owns one process-local Activation and retained `AgentHandle` per resident child Session, uses the Agent inbox as the only FIFO, and cold-resumes from the durable descriptor. Exact live direct-parent identity authorizes parent-to-child delivery. Exact live child identity authorizes reports; the manager derives the recipient from durable `parentSession`, and `MessageSource` remains provenance rather than authority.
|
||||
|
||||
When `ctx.sessionProjections` is available, the service registers `subagentTiming`. The projection resets at each descriptor so a fork seed's ancestor work cannot enter the child's total, then accumulates `turn/start` → `turn/end` active time and retains `activeSince` for an open turn. Only descriptors and turn boundaries change the value, so token chunks do not create timing updates.
|
||||
When `ctx.sessionProjections` is available, the service registers `subagentTiming`. The projection resets at each descriptor so a fork seed's ancestor work cannot enter the child's total, then accumulates `turn/start` → `turn/end` active time and retains same-cut `active.since` and `active.through` bounds for an open turn. While that turn remains open, `active.through` follows the latest folded event, giving an inactive consumer a conservative crash bound without mixing in newer session metadata.
|
||||
|
||||
`registerContinuableSetup()` lets optional packages add child-scoped capabilities without teaching the continuation manager their names. Contributions install synchronously before Activation publication, roll back with failed setup, and are released with the child scope. New grants wait for the next Activation, while contribution removal revokes every resident installation immediately.
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
||||
|
||||
可继续子级不会创建 `SubagentRun` 或 Task。延续管理器为每个驻留子 Session 直接拥有一个仅存在于当前进程的 Activation 和一个留存的 `AgentHandle`,使用 Agent inbox 作为唯一 FIFO,并从持久化描述符冷恢复。父到子投递由准确的实时直接父级身份授权。上报则由准确的实时子级身份授权;管理器根据持久化的 `parentSession` 推导接收方,`MessageSource` 仍只表示来源,不表示权限。
|
||||
|
||||
当 `ctx.sessionProjections` 可用时,服务会注册 `subagentTiming`。该投影会在每个描述符处重置,使 fork 种子中的祖先工作不会计入 child 总量,随后累加 `turn/start` → `turn/end` 活跃时间,并为未结束的轮次保留 `activeSince`。只有描述符和轮次边界会改变该值,因此 token 分片不会产生计时更新。
|
||||
当 `ctx.sessionProjections` 可用时,服务会注册 `subagentTiming`。该投影会在每个描述符处重置,使 fork 种子中的祖先工作不会计入 child 总量,随后累加 `turn/start` → `turn/end` 活跃时间,并为未结束的轮次保留同一切面的 `active.since` 和 `active.through` 边界。在该轮次保持未结束期间,`active.through` 会跟随最近折叠的事件,从而为 inactive 消费方提供保守的崩溃上界,又不会混入更新的会话元数据。
|
||||
|
||||
`registerContinuableSetup()` 允许可选包添加子级作用域功能,而无需让延续管理器知道这些功能的名称。贡献会在 Activation 发布前同步安装,在设置失败时一并回滚,并随子级作用域释放。新授权须等到下一个 Activation,移除贡献则会立即撤销每个驻留安装项。
|
||||
|
||||
|
||||
@@ -8,8 +8,13 @@
|
||||
export interface SubagentTimingProjection {
|
||||
/** Milliseconds accumulated across completed turns after the child's own descriptor. */
|
||||
settledMs: number
|
||||
/** Start of the currently open turn, when one has not reached `turn/end`. */
|
||||
activeSince?: number
|
||||
/** Same-cut bounds of the currently open turn, when one has not reached `turn/end`. */
|
||||
active?: {
|
||||
/** Start of the open turn. */
|
||||
since: number
|
||||
/** Latest event time folded into this projection cut. */
|
||||
through: number
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-session-projection/types' {
|
||||
|
||||
@@ -8,16 +8,25 @@ import { z } from 'zod'
|
||||
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
|
||||
import type { SubagentTimingProjection } from './projection-types.ts'
|
||||
|
||||
interface TimingState extends SubagentTimingProjection {
|
||||
interface TimingState {
|
||||
/** Milliseconds accumulated across completed post-descriptor turns. */
|
||||
settledMs: number
|
||||
/** Current open interval kept paired inside the fold. */
|
||||
active?: { since: number; through: number }
|
||||
/** Latest pre-descriptor turn start, promoted when the child's own descriptor arrives. */
|
||||
pendingTurnStart?: number
|
||||
/** Whether the fold has crossed a descriptor in this logical log. */
|
||||
descriptorSeen: boolean
|
||||
}
|
||||
|
||||
// Cast for the optional values: under exactOptionalPropertyTypes zod infers
|
||||
// `number | undefined` where the interface declares absent-or-number fields.
|
||||
const projectionSchema = z.object({
|
||||
settledMs: z.number().int().nonnegative(),
|
||||
activeSince: z.number().int().nonnegative().optional(),
|
||||
active: z.object({
|
||||
since: z.number().int().nonnegative(),
|
||||
through: z.number().int().nonnegative(),
|
||||
}).strict().optional(),
|
||||
}).strict() as unknown as z.ZodType<SubagentTimingProjection>
|
||||
|
||||
/**
|
||||
@@ -36,33 +45,38 @@ ProjectionDefinition<'subagentTiming', TimingState> = {
|
||||
apply: (state, event) => {
|
||||
if (event.type === 'turn/start') {
|
||||
return state.descriptorSeen
|
||||
? { ...state, activeSince: event.time }
|
||||
? { ...state, active: { since: event.time, through: event.time } }
|
||||
: { ...state, pendingTurnStart: event.time }
|
||||
}
|
||||
if (event.type === 'subagent/descriptor') {
|
||||
const activeSince = state.activeSince ?? state.pendingTurnStart
|
||||
const activeSince = state.active?.since ?? state.pendingTurnStart
|
||||
return {
|
||||
descriptorSeen: true,
|
||||
settledMs: 0,
|
||||
...(activeSince === undefined ? {} : { activeSince }),
|
||||
...(activeSince === undefined
|
||||
? {}
|
||||
: { active: { since: activeSince, through: event.time } }),
|
||||
}
|
||||
}
|
||||
if (event.type !== 'turn/end') return state
|
||||
if (!state.descriptorSeen) {
|
||||
if (state.pendingTurnStart === undefined) return state
|
||||
const { pendingTurnStart: _closed, ...next } = state
|
||||
return next
|
||||
}
|
||||
if (state.activeSince === undefined) return state
|
||||
const { activeSince, ...rest } = state
|
||||
return {
|
||||
...rest,
|
||||
settledMs: state.settledMs + Math.max(0, event.time - activeSince),
|
||||
if (event.type === 'turn/end') {
|
||||
if (!state.descriptorSeen) {
|
||||
if (state.pendingTurnStart === undefined) return state
|
||||
const { pendingTurnStart: _closed, ...next } = state
|
||||
return next
|
||||
}
|
||||
if (state.active === undefined) return state
|
||||
const { active, ...rest } = state
|
||||
return {
|
||||
...rest,
|
||||
settledMs: state.settledMs + Math.max(0, event.time - active.since),
|
||||
}
|
||||
}
|
||||
if (state.active === undefined) return state
|
||||
return { ...state, active: { ...state.active, through: event.time } }
|
||||
},
|
||||
view: state => ({
|
||||
settledMs: state.settledMs,
|
||||
...(state.activeSince === undefined ? {} : { activeSince: state.activeSince }),
|
||||
...(state.active === undefined ? {} : { active: state.active }),
|
||||
}),
|
||||
stateVersion: 1,
|
||||
stateVersion: 2,
|
||||
}
|
||||
|
||||
@@ -21,10 +21,13 @@ describe('subagent timing projection', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
await ctx.plugin(SubagentService)
|
||||
const serviceFiber = await ctx.plugin(SubagentService)
|
||||
|
||||
expect(ctx.sessionProjections.snapshot(ctx.sessions.create()).values.subagentTiming)
|
||||
.toEqual({ settledMs: 0 })
|
||||
await serviceFiber.dispose()
|
||||
expect(ctx.sessionProjections.snapshot(ctx.sessions.create()).values.subagentTiming)
|
||||
.toBeUndefined()
|
||||
})
|
||||
|
||||
it('resets inherited seed timing at the child descriptor and sums later completed turns', () => {
|
||||
@@ -47,7 +50,7 @@ describe('subagent timing projection', () => {
|
||||
event('turn/end', 2, 900),
|
||||
event('turn/start', 3, 2_000),
|
||||
event('assistant/chunk', 4, 2_500),
|
||||
])).toEqual({ settledMs: 0, activeSince: 2_000 })
|
||||
])).toEqual({ settledMs: 0, active: { since: 2_000, through: 2_500 } })
|
||||
})
|
||||
|
||||
it('ignores completed pre-descriptor turns and unrelated events', () => {
|
||||
|
||||
Reference in New Issue
Block a user