Session opening cache identity and read ordering
会话首帧读取的缓存身份与调用时序
Opening a historical Session can make `session.follow`, composer catalog prewarming, and RPCs with Agent parameters access the same cold log concurrently. `skills/list` requests a complete observation itself; `commands/list`, `goals/get`, `fileReferences/list`, and `sessionReferenceResolver/candidates` can initiate resume through the Agent lookup during parameter resolution. Auxiliary requests that do not wait for th
English
Problem
Opening a historical Session can make session.follow, composer catalog prewarming, and RPCs with Agent parameters access the same cold log concurrently. skills/list requests a complete observation itself; commands/list, goals/get, fileReferences/list, and sessionReferenceResolver/candidates can initiate resume through the Agent lookup during parameter resolution. Auxiliary requests that do not wait for the history opening snapshot compete with the main conversation for reading, parsing, and projection computation instead of reusing completed results.
Reuse of completed observations has a separate problem: Cordis can return different proxy objects for accesses to the same service. Comparing the object references returned by ctx.get('sessionPersistence') can mistake one underlying service for different instances. Removing instance identity checks entirely is also unsafe because persistence revisions are comparable only within the corresponding service instance and Session identity.
Subagent navigation combines two needs: displaying a conversation at a known address, and discovering its parent or descendant catalogs. Refreshing parent projections before navigating through an existing parent-child address adds prerequisite work and can reject an address that the Host could validate and open when the Client catalog cache is absent.
Decision
Identify the cache producer with a service-owned Symbol
SessionPersistence.identity is a Symbol('sessionPersistence') created once per service instance. Proxy access preserves that value, while a replacement service instance receives a different value. The prepared observation cache looks up the Session id and compares both persistence.identity and stat().revision; the same instance and revision can reuse a completed preparation.
This identity exists only within the process and does not enter Session headers, events, or persisted files. Existing cache capacity, leases, live precedence, and revision invalidation remain unchanged. This fix does not merge Promises for unfinished cold reads; stable identity prevents false invalidation after completion, not duplicate concurrent work across all callers.
Auxiliary RPCs wait for the current conversation's initial open to succeed
The Client uses the existing sessions.using() at the actual RPC call site, rather than only delaying prewarm hooks. Menu candidates, explicit reference queries, and later catalog refreshes therefore use the same waiting entry point. Each operation first checks that its target already has a Client binding, so background prewarming does not reopen a closed Session.
| Client consumer | RPC after waiting | Temporary reference source |
|---|---|---|
| Skill catalog | skills/list | skillCatalog |
| Command catalog | commands/list | commandCatalog |
| Goal activation state | goals/get | goalActivation |
@ file and Session candidates | fileReferences/list, sessionReferenceResolver/candidates | Shared referenceCandidates |
sessions.using() waits for reference.ready, then for the operation's returned Promise, and finally releases its reference. ready means that the initial Session.open() attempt has settled, not that it succeeded; these consumers also require openState === 'open' and send no auxiliary RPC after a failed open. The Goal reader additionally checks that its captured binding has not been replaced and logs rejected reads without clearing existing display state.
Skills retain the shared catalog request's own cancellation signal; @ queries use the current candidate request's signal for the history wait and both RPCs. Unquoted @ queries still fetch files and Sessions concurrently after waiting, while quoted paths still query files only. Commands and Goals gain no new cancellation protocol. These references retain Client data and follow streams, grant no new Host Agent authority, and do not prohibit normal Agent resume after the opening snapshot.
Conversations use follow; catalog branches retain explicit reads
The main conversation's opening follow snapshot already carries that Session's complete projection baseline. Later changes to shared projection values notify subscribers, so switching Sessions or opening a menu does not require another read of the same baseline.
- Main-view navigation does not separately refresh the selected Session's projections; restoring a saved subagent address does not prewarm its parent.
- The Sidebar retains the target child directly from its complete parent-child address, without making a parent-catalog read a prerequisite for opening the conversation.
- The header dropdown's
changeOpenonly changes presentation state and does not refreshrootSessionId. Child expansion and failed-read retries retain explicit refreshes under the UI callback namerefreshProjection; the underlyingsessions.refreshProjectionsAPI is not renamed. - Team navigation uses the existing Lead and roster member ids to open
{ parentSessionId, childSessionId, mode: 'continuable' }directly. It neither refreshes the parent catalog nor requires the target to be present in the Client catalog; member-role and main-view ownership checks, plus the UI restrictions on provisioning and failed members, remain unchanged.
A known address does not waive authorization checks. When opening child history, the Host still checks the target's own header parent and origin, plus the mode and descriptor ownership in its own subagent projection. Later continuation remains subject to the existing direct-parent authorization rules.
Implementation scope
- Session formats, migrations, the Host observation's
all | nonepolicy, and the response fields ofsession.projectionsare unchanged. - There is no global cold-read singleflight, and Agent lookups are not universally replaced with read-only lookups.
SessionManager.handleConnected()still refreshes previously requested catalogs in bulk; this decision does not guarantee one cold read on reconnect.- Tool-side
listDescendantsobserves each reachable child catalog. An observation can reuse live state or a valid prepared Session; otherwise it reads the cold log. Expanding a child node in the Client can also cold-read that node. Ordinary main-session opening is not equivalent to every descendant-enumeration scenario. @Session candidates still enumerate headers and obtain names from live projections or projection caches, falling back to ids on misses. Existing ranking, the default limit of 50, and direct-subagent grouping remain unchanged; discovery does not recursively traversesubagentCatalog.
Alternatives considered
Remove persistence-instance comparison. Rejected. A matching Session id and revision cannot establish that two persistence services provide the same data; replacing a service must still invalidate old preparations.
Unwrap Cordis proxies in callers or cache service objects per Context. Rejected. The cache needs the producer's identity, not the identity of an object returned by a particular service access. A per-instance Symbol expresses that requirement without propagating proxy implementation details.
Add shared singleflight for every cold read. Outside this implementation. It could merge more concurrent calls, but would require separate rules for independent waiter cancellation, leases, and task ownership during service replacement. It also would not replace the ordering requirement that auxiliary RPCs must not resume an Agent ahead of the opening snapshot.
Delay only warm hooks, or merely await the extra projection request. Rejected. Explicit menu queries could still bypass warm hooks, and serializing a redundant projection request does not remove its work. The wait belongs at the actual auxiliary RPC entry point; reads already supplied by follow are removed.
Keep Team's parent-catalog presence check. Rejected. The Team roster already provides member identity, and the existing Host history entry point can validate an explicit address. Requiring a Client catalog cache hit for navigation mistakes data that has not loaded for a child that does not exist.
Consequences
An ordinary main Session completes its own history and projection read before auxiliary operations reuse the completed observation or attached state. The Client's direct-child list consumes the main Session's catalog without reading each child's log merely to display its row. Catalog and conversation opening are separate operations, so navigating to a known child no longer adds parent-projection prewarming.
Auxiliary catalogs become available after the conversation's opening snapshot, and operations retain temporary Client references until they finish. This wait covers only the current Client generation's initial open, not readiness after every underlying follow replacement. Reconnect and exceptional retry scenarios still require their own verification.
A missing header root catalog remains a specific coverage gap: if it belongs to an unopened parent and no other source supplies the catalog, opening the dropdown alone does not fetch it, and the current UI may continue to display a loading notice. This implementation adds neither a new parent-catalog loading entry point nor a shared unknown-state interaction protocol.
Verification and known gaps
During local navigation to a long historical main Session, the user observed repeated readColdSessionLog calls converge to one using temporary timers and call stacks, and verified direct teammate navigation. This result describes that operation's cold-read helper invocation count, not every underlying file I/O, every reconnect scenario, or a single unit of end-to-end cost. Private Session content and identities from diagnosis are not test fixtures.
Focused TypeScript builds and bundles were run for affected packages during development; the complete change has no gate, behavior-test, or assembled Web snapshot verification result. Existing tests have only partial callback renames and still contain old fixture names, root-menu-opening refresh expectations, Sidebar parent-refresh prerequisites, and Team rejection of missing catalogs. These are known verification gaps, not passing checks.
Relationship to existing records
- Session observations and projection-owned Client state retains ownership of read cuts and shared values. This record adds cache-producer identity and opening-snapshot consumption order; the older claim of sharing unfinished cold loads does not match the current reader and is not evidence that this change implements singleflight.
- Client Session references, reference sources, and UI state retains ownership of
retain,ready,using, and explicit-address semantics. This record limits temporary references to four classes of auxiliary reads as independent asynchronous operations, rather than adding references to every UI action. - Web subagent catalogs consume shared projections retains ownership of the generic projection store, explicit catalog reads, and control updates. This implementation applies its rule that opening a conversation uses the follow baseline, without replacing the catalog mechanism.
- Web subagent catalogs and human continuation retains ownership of presentation and continuation authorization. This record changes only prerequisite navigation reads and root-menu refresh timing, not the child's own Host validation; the older description of interactive loading for missing root catalogs must be reconciled with the coverage gap recorded here.
中文
问题
打开历史会话时,session.follow、输入框目录预热和依赖 Agent 参数的 RPC 可能同时访问同一份冷日志。skills/list 自行请求完整 observation;commands/list、goals/get、fileReferences/list 与 sessionReferenceResolver/candidates 则可以在参数解析阶段经 Agent lookup 发起恢复。辅助请求不等待历史首帧,会与主对话争用读取、解析和 projection 计算,而不是复用已经完成的结果。
完成后的 observation 复用还有一个独立问题:Cordis 对同一服务的访问可以返回不同代理对象。以 ctx.get('sessionPersistence') 返回对象的引用相等性判断服务身份,会把同一底层服务误判成另一个实例。另一方面,彻底去掉实例身份检查也不安全,因为持久化 revision 只在对应服务实例和 Session 身份内可比较。
子会话导航混合了两种需求:展示一个已知地址的对话,以及发现该会话的父目录或后代目录。已有父子地址的导航再次刷新 parent projection,既增加前置工作,也会在目录缓存缺席时拒绝一个本可由 Host 校验并打开的地址。
决策
用服务自身的 Symbol 标识缓存生产者
SessionPersistence.identity 是每个服务实例创建一次的 Symbol('sessionPersistence')。代理访问保留这个值,服务被替换后新实例获得不同值。Prepared observation cache 按 Session id 查找,并同时比较 persistence.identity 与 stat().revision;相同实例、相同 revision 可以复用已完成的 preparation。
该身份只存在于进程内,不进入 Session header、事件或持久化文件。现有缓存容量、lease、live 优先级和 revision 失效规则不变。这个修复不增加尚未完成的冷读取之间的 Promise 合并;稳定身份解决的是完成后的误失效,不是所有调用方的并发去重。
辅助 RPC 等待当前对话的首次打开成功
Client 在实际 RPC 发送位置使用既有 sessions.using(),不只延迟预热钩子。菜单候选、显式引用查询和后续目录刷新因而经过相同的等待入口。每个操作先确认目标已经有 Client binding,不因后台预热重新打开一个已经关闭的会话。
| Client 消费方 | 等待后的 RPC | 临时引用来源 |
|---|---|---|
| Skill 目录 | skills/list | skillCatalog |
| Command 目录 | commands/list | commandCatalog |
| Goal 激活状态 | goals/get | goalActivation |
@ 文件与会话候选 | fileReferences/list、sessionReferenceResolver/candidates | 共用 referenceCandidates |
sessions.using() 先等待 reference.ready,再等待操作返回的 Promise,最后释放自己的引用。ready 表示首次 Session.open() 尝试已结算,不保证成功;这些消费方还检查 openState === 'open',失败时不发送辅助 RPC。Goal 读方还确认捕获的 binding 未被替代,并记录读取拒绝而不清除已有展示状态。
Skill 保留共享目录请求自己的取消信号;@ 查询将当前候选请求的信号用于历史等待和两个 RPC。未加引号的 @ 在等待后仍并行查询文件和会话,加引号的路径仍只查询文件。Command 与 Goal 不新增取消协议。这里的引用持有 Client 数据和跟随流,不赋予 Host Agent 新权限,也不禁止首帧之后发生正常 Agent 恢复。
对话使用 follow,目录分支保留显式读取
主对话的 follow 首帧已经携带该 Session 的完整 projection baseline。后续共享 projection 值变化会通知订阅者,不需要在切换会话或打开菜单时再次读取相同 baseline。
- 主视图切换不再额外刷新所选 Session 的 projection;恢复保存的子会话地址不再预先刷新其 parent。
- 侧栏从完整父子地址直接保留目标子会话,不把父目录读取作为打开对话的前提。
- Header 下拉菜单的
changeOpen只改变展示状态,不刷新rootSessionId。展开子节点和失败重试仍调用显式刷新,UI 回调命名为refreshProjection;底层sessions.refreshProjectionsAPI 不改名。 - Team 点击使用已有 lead 与 roster 成员 id,直接打开
{ parentSessionId, childSessionId, mode: 'continuable' }。它不刷新 parent catalog,也不要求目标预先出现在 Client catalog 中;成员角色、主视图持有检查及 UI 对 provisioning、failed 成员的限制保持不变。
已知地址不等于免除授权校验。Host 在打开子会话历史时,仍检查目标自身 header 的 parent、origin,以及自身 subagent projection 的 mode 和描述符归属。后续 continuation 仍受直接 parent 的既有授权规则约束。
实现边界
- 没有修改 Session 格式、迁移、Host observation 的
all | none策略或session.projections返回字段。 - 没有实现全局 cold-read singleflight,也没有把所有 Agent lookup 改成只读 lookup。
- 没有删除
SessionManager.handleConnected()对此前请求目录的批量刷新;本决策不保证重连只有一次冷读。 - 工具侧
listDescendants观察每个可达子级目录。观察可以复用实时状态或有效的 prepared Session,否则会读取冷日志。前端展开子节点也可能冷读该节点。普通主会话首开不等于所有后代枚举场景。 @会话候选仍枚举 header,并从 live projection 或 projection cache 取名称;缓存缺失回退到 id。原有排序、默认 50 条上限和直接 subagent 分组规则不变,没有改成沿subagentCatalog递归发现。
考虑过的替代方案
取消 persistence 实例比较。 不采用。同一个 Session id 和 revision 不能证明两个持久化服务提供同一份数据;服务替换仍必须使旧 preparation 失效。
在调用方解包 Cordis 代理或按 Context 缓存服务对象。 不采用。缓存需要的是生产者身份,而不是某次服务访问返回对象的身份;每实例 Symbol 足够表达这个要求,不需要传播代理实现细节。
为所有冷读增加统一 singleflight。 未纳入本次实现。它可以合并更多并发调用,但需要另外定义独立等待方取消、lease 和服务替换期间的任务归属;它也不能代替“辅助 RPC 不抢在首帧前恢复 Agent”的时序要求。
只延迟 warm,或只给额外 projection 请求补 await。 不采用。显式菜单查询仍能绕过 warm;串行执行一个重复 projection 请求也没有删除重复工作。等待放在实际辅助 RPC 入口,已有 follow 能提供的读取直接取消。
保留 Team 的父目录存在性检查。 不采用。Team roster 已给出成员身份,显式地址可以由现有 Host 历史入口验证;把 Client catalog 缓存命中作为导航前提,会把数据尚未加载误当成子会话不存在。
后果
普通主会话先完成自己的历史和 projection 读取,辅助操作再复用已完成的 observation 或已挂载状态。前端直接子代列表消费主会话的 catalog,不为显示每一行读取对应子会话正文。目录与对话的打开入口分开,导航到一个已知子会话不再附带 parent projection 预热。
代价是辅助目录首次可用时间晚于对话首帧;操作还会持有临时 Client 引用直到结束。该等待只对应当前 Client generation 的首次打开,不是每次底层 follow 重建的就绪屏障。所有重连和异常重试场景仍需各自验证。
Header 根目录缺失仍是明确的覆盖缺口:若它属于未打开的 parent,且没有其他来源提供 catalog,仅打开下拉菜单不会补读;现有 UI 仍可能显示加载提示。本次没有增加新的父目录加载入口,也没有实现统一的 unknown 交互协议。
验证与已知缺口
用户在本地长历史主会话切换流程中,通过临时 readColdSessionLog 计时和调用栈观察到重复调用收敛为一次,并验证了 teammate 直接导航。这个结果只描述该操作的冷读辅助函数调用次数,不代表所有底层文件 I/O、全部重连场景或端到端性能都只有一次成本。诊断期间的私有 Session 内容和身份不作为测试 fixture。
开发期对相关包执行过定向 TypeScript 构建与打包;尚无这批完整变更的门禁、行为测试或组装 Web 快照验证结果。现有测试只完成部分回调改名,仍有旧 fixture 名称、打开根菜单即刷新、侧栏先刷新 parent,以及 Team 拒绝缺失 catalog 的预期需要调整。这些是已知验证缺口,不是已经通过的检查。
与既有记录的关系
- Session observation 与 projection 所有的客户端状态继续拥有读取切面和共享值的职责划分。本记录补充缓存生产者身份与首帧消费顺序;旧记录中“包括未完成冷加载的共享”描述与当前 reader 实现不一致,不能作为本次 singleflight 已实现的依据。
- Client Session 引用、引用来源与 UI 状态继续拥有
retain、ready、using与显式地址语义。本记录限定四类辅助读取作为独立异步操作持有临时引用,不把额外引用扩展到所有 UI 动作。 - Web 子代理目录消费共享 projection继续拥有通用 projection store、显式目录读取和 control 更新。本次实现落实“打开会话使用 follow baseline”的规则,不替换整个目录机制。
- Web subagent 目录与用户继续交互继续拥有展示和 continuation 授权。本记录只调整导航前置读取及根菜单刷新时机,不取消子会话自身的 Host 校验;旧记录对缺失根目录交互加载的描述需与这里的覆盖缺口一起核对。