merge subagent timing review fixes

# Conflicts:
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md
#	packages/client/ui-subagent/README.i18n.yaml
#	packages/client/ui-subagent/README.md
#	packages/client/ui-subagent/README.zh.md
This commit is contained in:
kingwl
2026-08-03 11:51:24 +08:00
14 changed files with 70 additions and 42 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 .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
2026-07-27-web-subagent-conversations.md: f4d2035dfc7224cd7a11575449ff79cdae3fce48
2026-07-27-web-subagent-conversations.zh.md: 72ede6799de01961d82e93048652a70a7d3d83aa
2026-07-27-web-subagent-conversations.md: e0869cf4128b1cf0fdecb39dd7a23aa81b92c380
2026-07-27-web-subagent-conversations.zh.md: ceeb929ef73fe099fbd6dcbd8378745708ad2097

View File

@@ -41,7 +41,7 @@ The header action is absent only when a complete empty direct-catalog response a
`running` means the exact child Agent driver is draining work at the Host sampling boundary; `inactive` means that driver is idle or absent. The UI does not translate either value into success, failure, cancellation, completeness, or resumability. `subagent.list` supplies the current driver-status baseline, `host/session-status` updates known activity in place, request-local replay prevents an older in-flight list response from overwriting a newer transition, and `host/session-removed` returns a known row to `inactive`; reconnect reads a fresh baseline. A `host/session-added` frame for a direct subagent immediately flips any loaded parent row to `hasChildren: true`, and that positive hint survives an older in-flight catalog response; membership, labels, mode, diagnostics, and the authoritative snapshot still require a debounced `subagent.list` refresh while the affected branch is open. A prompt response remains delivery-time authority.
Healthy rows reuse the standard session projections retained in the list mirror. The token figure sums the four disjoint `tokenUsage` buckets across the durable log. `subagentTiming` resets at every descriptor so an inherited fork seed cannot enter the child's total, accumulates completed `turn/start``turn/end` spans, and carries the current turn's `activeSince`. The menu formats whole seconds and advances its local clock only while a known descendant is running; an inactive row uses settled duration, or the summary's last activity to bound an interrupted open turn, so reopening the menu never restarts completed work. Token chunks do not change `subagentTiming` and therefore do not add a per-token list update path. Neither metric implies a durable outcome.
Healthy rows reuse the standard session projections retained in the list mirror. The token figure sums the four disjoint `tokenUsage` buckets across the durable log. `subagentTiming` resets at every descriptor so an inherited fork seed cannot enter the child's total, accumulates completed `turn/start``turn/end` spans, and carries same-cut `active.since` and `active.through` bounds for an open turn. Existing session events advance `active.through` while that turn remains open; the menu adds no separate timer or log read, formats whole seconds, and advances its local clock only while a known descendant is running. An inactive row bounds an interrupted open turn with `active.through`, so a stale projection never borrows newer session metadata and reopening the menu never restarts completed work. Neither metric implies a durable outcome.
Selecting a row records its exact address before opening the resident client `Session`. History pagination, event folding, tool render intents, titles, and live mux reconciliation reuse the ordinary conversation machinery. Breadcrumbs use catalog labels, follow parent links only through `origin: 'subagent'` rows, include the first ordinary owner, and keep ordinary forks single-level. Forking an addressed subagent creates an ordinary fork with direct source lineage and attaches it to the nearest workspace-owning ancestor. The catalog is an ARIA tree with lazy ArrowRight/ArrowLeft disclosure, linear ArrowUp/ArrowDown navigation, Home/End, Escape, and focus restoration.

View File

@@ -41,7 +41,7 @@ Figma 中的 [subagent 列表](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5
`running` 表示在 Host 采样边界,确切 child Agent driver 正在处理工作;`inactive` 表示该 driver 空闲或不存在。UI 不会把任一值解释为成功、失败、取消、完成状态或可恢复性。`subagent.list` 提供当前 driver 状态基线,`host/session-status` 会就地更新已知活动状态,请求内回放会阻止更早发起但尚未完成的列表响应覆盖较新的状态转换,`host/session-removed` 则会使已知行恢复为 `inactive`;重连时会读取新的基线。直接 subagent 的 `host/session-added` 帧会立即把任何已加载的 parent 行翻转为 `hasChildren: true`并使这项正向提示不被更早发起但尚未完成的目录响应覆盖受影响分支打开期间成员、label、mode、diagnostic 与权威快照仍需要通过去抖动的 `subagent.list` 刷新来更新。消息投递时仍以提示词响应为权威依据。
健康行会复用列表镜像中保留的标准会话投影。token 用量数值会汇总持久化日志中四个互不重叠的 `tokenUsage` 桶。`subagentTiming` 会在每个描述符处重置,使继承的 fork 种子不会计入 child 总量;它会累加已完成的 `turn/start``turn/end` 时段,并携带当前轮次`activeSince`。菜单会以整秒格式化时间,且仅在有已知后代处于运行状态时才推进其本地时钟对 inactive 行,菜单使用已结算耗时或以摘要的最后活动为被中断未结束轮次的上界因此重新打开菜单绝不会让已完成工作重新计时。token 分片不会改变 `subagentTiming`,因此不会增加按 token 更新列表的路径。这两项指标都不蕴含持久化结果语义。
健康行会复用列表镜像中保留的标准会话投影。token 用量数值会汇总持久化日志中四个互不重叠的 `tokenUsage` 桶。`subagentTiming` 会在每个描述符处重置,使继承的 fork 种子不会计入 child 总量;它会累加已完成的 `turn/start``turn/end` 时段,并携带未结束轮次同一切面`active.since``active.through` 边界。该轮次保持未结束期间,现有会话事件会推进 `active.through`;菜单不会增加单独的计时器或日志读取,它会以整秒格式化时间,且仅在有已知后代处于运行状态时才推进其本地时钟对 inactive 行,菜单`active.through` 为被中断未结束轮次的上界,因此陈旧投影绝不会借用更新的会话元数据,且重新打开菜单绝不会让已完成工作重新计时。这两项指标都不蕴含持久化结果语义。
选择一行后,系统会先记录其确切地址,再打开常驻客户端 `Session`。历史分页、事件 fold、工具渲染意图、title 与实时 mux 归并都会复用普通对话机制。面包屑导航使用目录 label只会沿 `origin: 'subagent'` 行的父链接逐级回溯,包含第一个普通 owner并让普通 fork 保持单层。从已寻址 subagent 创建 fork 时,会生成具有直接源谱系的普通 fork并将其附加到最近拥有 Workspace 的祖先。目录是一棵 ARIA 树,支持懒加载式 ArrowRightArrowLeft 展开与折叠、线性 ArrowUpArrowDown 导航、HomeEnd、Escape 以及焦点恢复。

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/ui-subagent/README.md
README.md: 538daeffb61f642b2e430cc1a6e2b1f3e3d5f55e
README.zh.md: 10320fdde6fe2a881e0e1dfc218c87562c4d61b0
README.md: e13532050cf1a0cbd01ef845eafdc41787c19807
README.zh.md: f5bf05978d02676458576dd25f05f3d88419c9a5

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Web subagent feature owner: contributes the lazily expandable catalog tree to `conversation.session.header.actions`, reason-specific read-only replacements to the conversation composer chain, and the existing `@` reference source to `ctx.slash`.
The header action reads `subagentsByParent` and session summaries through the standard `useSessions` hook. After a non-empty direct catalog arrives, its trigger counts the complete subagent-only descendant lineage, stops at ordinary forks, and shows ongoing activity when any counted descendant is running. The compact tree remains direct-catalog authoritative: continuable and one-shot rows display mode plus `running`/`inactive` activity and an optional log-backed title, while the trailing column stacks total durable provider usage above active-turn duration to the second; an unlabeled one-shot row falls back to its session id, while corrupt, unsupported, or unavailable rows remain readable but disabled. Token totals sum the four disjoint `tokenUsage` buckets. Duration sums completed `subagentTiming` turns, advances once per second only for an open turn on a running child, and freezes after the child becomes inactive; an interrupted open turn is bounded by the session summary's last activity. Each healthy row's `hasChildren` hint determines disclosure before interaction, so known leaves never show an arrow; expanding a branch immediately reserves one disabled loading row per known direct descendant, then lazily replaces them with that child's authoritative catalog. Every visible branch is reported to the runtime so membership frames cause a debounced refresh only where the tree is being consumed. Selecting any depth calls `SessionsService.openSubagent()` with the row's exact `{parentSessionId, childSessionId, mode}` address. Component-local state owns tree visibility, expanded branches, keyboard focus, and the running-duration clock. ArrowRight/ArrowLeft expand and collapse branches; ArrowUp/ArrowDown, Home, End, and Escape navigate or close the tree; closing returns focus to the trigger. Styling uses tokens only.
The header action reads `subagentsByParent` and session summaries through the standard `useSessions` hook. After a non-empty direct catalog arrives, its trigger counts the complete subagent-only descendant lineage, stops at ordinary forks, and shows ongoing activity when any counted descendant is running. The compact tree remains direct-catalog authoritative: continuable and one-shot rows display mode plus `running`/`inactive` activity and an optional log-backed title, while the trailing column stacks total durable provider usage above active-turn duration to the second; an unlabeled one-shot row falls back to its session id, while corrupt, unsupported, or unavailable rows remain readable but disabled. Token totals sum the four disjoint `tokenUsage` buckets. Duration sums completed `subagentTiming` turns, advances once per second only for an open turn on a running child, and freezes after the child becomes inactive; an interrupted open turn is bounded by its same-cut `active.through`, never by newer session metadata. Each healthy row's `hasChildren` hint determines disclosure before interaction, so known leaves never show an arrow; expanding a branch immediately reserves one disabled loading row per known direct descendant, then lazily replaces them with that child's authoritative catalog. Every visible branch is reported to the runtime so membership frames cause a debounced refresh only where the tree is being consumed. Selecting any depth calls `SessionsService.openSubagent()` with the row's exact `{parentSessionId, childSessionId, mode}` address. Component-local state owns tree visibility, expanded branches, keyboard focus, and the running-duration clock. ArrowRight/ArrowLeft expand and collapse branches; ArrowUp/ArrowDown, Home, End, and Escape navigate or close the tree; closing returns focus to the trigger. Styling uses tokens only.
A one-shot child always elects a read-only composer that identifies the transcript as a completed execution record. A continuable child does so only when its exact parent is unavailable, with copy explaining the recovery path. A continuable child with a live parent keeps the ordinary input chrome, whose Session routes through `subagent.prompt`; running input remains Send because every follow-up joins the child's FIFO inbox, and addressed sessions never expose Stop. This package never receives host context or calls a model-facing tool. The catalog and composer behavior are specified by the [Web subagent conversations Agent Note](../../../.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md).

View File

@@ -4,7 +4,7 @@
Web subagent 功能 owner`conversation.session.header.actions` 贡献可懒加载展开的目录树,向会话编辑器链贡献按原因区分的只读替代呈现,并保留注册到 `ctx.slash` 的既有 `@` 引用 source。
页头操作通过标准 `useSessions` 钩子读取 `subagentsByParent` 与会话摘要。非空直接目录到达后,其触发器会统计仅含 subagent 的完整后代谱系,在普通 fork 处停止,并在任一计入统计的后代处于 `running` 时显示活动仍在进行。紧凑树仍以直接目录为权威依据:可继续和 one-shot 行会显示 mode、`running``inactive` 活动状态和由日志支撑的可选 title尾随列则在上行显示提供方的持久化 token 用量总计,在下行显示精确到秒的活跃轮次耗时;没有 label 的 one-shot 行会回退到其会话 id而损坏、不受支持或不可用的行仍保持可读但禁用。token 用量总计为四个互不重叠的 `tokenUsage` 桶之和。耗时会累加已完成的 `subagentTiming` 轮次,仅在运行中 child 存在未结束轮次时每秒递增一次,并在 child 变为 inactive 后冻结;被中断的未结束轮次以会话摘要中的最后活动为上界。每个健康行的 `hasChildren` 提示会在交互前决定是否显示展开控件,因此已知叶子节点从不显示箭头;展开分支时,会立即为每个已知直接后代预留一行禁用的加载行,随后再用该 child 的权威目录懒加载结果替换这些占位行。每个可见分支都会上报给运行时,使成员帧只在树正被消费的位置触发去抖动刷新。选择任意深度的条目都会使用该行的确切地址 `{parentSessionId, childSessionId, mode}` 调用 `SessionsService.openSubagent()`。组件局部状态负责树的可见性、已展开分支、键盘焦点与运行中耗时时钟。ArrowRightArrowLeft 展开和折叠分支ArrowUpArrowDown、Home、End 与 Escape 用于导航或关闭树;关闭后焦点返回触发器。样式只使用 token。
页头操作通过标准 `useSessions` 钩子读取 `subagentsByParent` 与会话摘要。非空直接目录到达后,其触发器会统计仅含 subagent 的完整后代谱系,在普通 fork 处停止,并在任一计入统计的后代处于 `running` 时显示活动仍在进行。紧凑树仍以直接目录为权威依据:可继续和 one-shot 行会显示 mode、`running``inactive` 活动状态和由日志支撑的可选 title尾随列则在上行显示提供方的持久化 token 用量总计,在下行显示精确到秒的活跃轮次耗时;没有 label 的 one-shot 行会回退到其会话 id而损坏、不受支持或不可用的行仍保持可读但禁用。token 用量总计为四个互不重叠的 `tokenUsage` 桶之和。耗时会累加已完成的 `subagentTiming` 轮次,仅在运行中 child 存在未结束轮次时每秒递增一次,并在 child 变为 inactive 后冻结;被中断的未结束轮次以其同一切面的 `active.through` 为上界,绝不使用更新的会话元数据。每个健康行的 `hasChildren` 提示会在交互前决定是否显示展开控件,因此已知叶子节点从不显示箭头;展开分支时,会立即为每个已知直接后代预留一行禁用的加载行,随后再用该 child 的权威目录懒加载结果替换这些占位行。每个可见分支都会上报给运行时,使成员帧只在树正被消费的位置触发去抖动刷新。选择任意深度的条目都会使用该行的确切地址 `{parentSessionId, childSessionId, mode}` 调用 `SessionsService.openSubagent()`。组件局部状态负责树的可见性、已展开分支、键盘焦点与运行中耗时时钟。ArrowRightArrowLeft 展开和折叠分支ArrowUpArrowDown、Home、End 与 Escape 用于导航或关闭树;关闭后焦点返回触发器。样式只使用 token。
one-shot child 始终选用只读编辑器,并将 transcript文本记录说明为已完成的执行记录。可继续 child 仅在其确切 parent 不可用时选用只读编辑器,并以文案说明恢复路径。确切 parent 存活时,可继续 child 保留普通输入 chrome其 Session 会通过 `subagent.prompt` 路由child 运行期间,输入操作仍为 Send因为每条后续消息都会进入 child 的 FIFO inbox且已寻址会话绝不公开 Stop。本包绝不接收宿主 context也不调用面向模型的工具。目录与编辑器行为由 [Web subagent 对话 Agent Note](../../../.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md)规定。

View File

@@ -90,9 +90,11 @@ function activityDuration(
const timing: SessionProjectionMap['subagentTiming'] | undefined
= summary.projectionValues?.subagentTiming
if (timing === undefined) return undefined
if (timing.activeSince === undefined) return timing.settledMs
const end = activity === 'running' ? now : summary.updatedAt
return timing.settledMs + Math.max(0, end - timing.activeSince)
if (timing.active === undefined) return timing.settledMs
const end = activity === 'running'
? now
: timing.active.through
return timing.settledMs + Math.max(0, end - timing.active.since)
}
/** Format a non-negative duration to seconds without dropping larger units. */

View File

@@ -245,9 +245,9 @@ describe('SubagentCatalogAction', () => {
vi.useFakeTimers()
vi.setSystemTime(now)
const rows = [
['running', 'running', 65_000, now - 5_000, now],
['finished', 'inactive', 3_723_000, undefined, now - 60_000],
['interrupted', 'inactive', 2_000, now - 7_000, now - 3_000],
['running', 'running', 65_000, now - 5_000, now - 1_000, now],
['finished', 'inactive', 3_723_000, undefined, undefined, now - 60_000],
['interrupted', 'inactive', 2_000, now - 7_000, now - 3_000, now + 60_000],
] as const
const usageById = {
running: {
@@ -277,7 +277,9 @@ describe('SubagentCatalogAction', () => {
activity,
hasChildren: false,
}))
const summaries = Object.fromEntries(rows.map(([id, activity, settledMs, activeSince, updatedAt]) => {
const summaries = Object.fromEntries(rows.map(([
id, activity, settledMs, activeSince, activeThrough, updatedAt,
]) => {
const childId = id as SessionId
return [id, {
...summary(childId, updatedAt),
@@ -287,7 +289,9 @@ describe('SubagentCatalogAction', () => {
projectionValues: {
subagentTiming: {
settledMs,
...(activeSince === undefined ? {} : { activeSince }),
...(activeSince === undefined || activeThrough === undefined
? {}
: { active: { since: activeSince, through: activeThrough } }),
},
tokenUsage: usageById[id],
},

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/subagent/subagent/README.md
README.md: ec4af55bcd9374b1abb55d7bb098eef568449684
README.zh.md: 8323853ff0de2a15da6475fc1433a68f0074ee93
README.md: e54f0b98ec3649cec428a47026e6657a9749608b
README.zh.md: 1624fa59854d9b61770c5ef0f9d89f7882198da4

View File

@@ -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.

View File

@@ -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移除贡献则会立即撤销每个驻留安装项。

View File

@@ -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' {

View File

@@ -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
}
// Zod's optional output includes explicit `undefined`; with
// exactOptionalPropertyTypes the public interface permits omission only.
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,
}

View File

@@ -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', () => {