docs(notes): fold projection compatibility into the registration handles note

Record the deprecated registration and keyed-read compatibility in the retained-reader decision instead of a separate note, and remove the 2026-10-19 compatibility note.
This commit is contained in:
imccyu committed 2026-10-20 00:42:33 +08:00
1 parent 33101ad29f
commit 8f348ee760
6 files changed
+22 -87

No files matched your search

@@ -9,11 +9,11 @@
en: 47ba548b869be190
zh: c2e5038a56fe741c
/agent-note-asynchronous-projection-registration-and-retained-readers/decision:
en: 099e3ffc0ba784be
zh: e2fd6f0f252c1f9d
en: 0d9a072d670c01c0
zh: 0afbce6753fcd11b
/agent-note-asynchronous-projection-registration-and-retained-readers/alternatives-considered:
en: ee5b0cd715ed1fef
zh: 6b1478dcde21916d
en: 2ffb22b9d59f4a06
zh: 09c0aec189a1dfd2
/agent-note-asynchronous-projection-registration-and-retained-readers/consequences:
en: 6f0e04ef7924e2c6
zh: e3730273e323440d
en: ccd252074b735ca8
zh: 11f1790b912fa91b
@@ -10,12 +10,14 @@ Host readers coupled to registry key lookup lose access when a plugin unloads, a
## Decision
`SessionProjectionRegistry.register` returns an awaitable registration for a typed handle. Domain owners await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Each domain service keeps its definitions and handles internal and exposes methods returning business data. Consumers depend on that service rather than registering its definitions or receiving projection readers. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules. [Deprecated compatibility](2026-10-19-session-projection-compatibility.md) permits legacy registration and keyed reads during migration without changing retained-reader behavior.
`SessionProjectionRegistry.register` returns an awaitable registration for a typed handle. Domain owners await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Each domain service keeps its definitions and handles internal and exposes methods returning business data. Consumers depend on that service rather than registering its definitions or receiving projection readers. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules.
A handle owns one reference to a shared definition and its per-Session cells. Its disposer releases global participation exactly once; the final release removes the unit from live drives, snapshots, and checkpoints. Its reader remains usable after release, folding unseen events with the original definition. A later registration at a different state version does not replace an older handle's definition. Registration checks owner liveness after its asynchronous handoff and rejects if the owner unloaded before receiving the handle.
AgentRegistry owns turn and step state and exposes `turnBoundaryOf(session)`. AgentLoop acquires its inbox handle once before publishing its factory, reads the last turn before creating an Agent scope, and passes an inbox callback plus that turn number into synchronous Agent construction. The callback retains its handle through cleanup after the loop fiber removes global registrations. This preserves the existing creation/setup sequence without per-Agent registration or an extra initialization state.
Plugins compiled against the synchronous API keep running during migration. The awaitable registration is also the original Cordis effect disposer, so calling it releases the registration and a composing effect adopts its ownership; that call is deprecated. Registry-level `stateOf(session, key)` remains a deprecated read returning `unknown`; it loses access after the final release and logs one `ctx.logger.warn` per key per registry. Registration itself does not warn, because an ignored result is indistinguishable from one awaited later. A legacy plugin may never observe the registration, so the registry attaches its own rejection handler: once the current turn ends, a rejection nobody observed through `then`, `catch`, or `finally` is logged through the calling plugin's logger while the plugin stays active, and one whose owner is no longer loading or active is dropped. TypeScript consumers migrate their declarations; the host state map is not restored.
## Alternatives considered
**Keep registry-level lookup and delay unregistering.** This couples unrelated plugin disposal order to every reader's final access. Retained readers make cleanup independent of that order.
@@ -26,6 +28,10 @@ AgentRegistry owns turn and step state and exposes `turnBoundaryOf(session)`. Ag
**Convert function plugins to classes.** Cordis already awaits promises returned by `apply`. Preserving namespace exports avoids a published entry-point and diagnostic-name break.
**Require every external plugin to migrate immediately.** Compiled plugins fail at runtime on the missing disposer and keyed read, although the projection keys and state fields they read are unchanged.
**Return a new wrapper around the disposer.** Cordis composition adopts effects by their returned identity. A wrapper leaves the original registration owned by the fiber, so owner disposal can release it before the composed cleanup finishes.
## Consequences
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown.
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. Compatibility tests and a real Loader subprocess cover callable and composed disposal, keyed reads and their warnings, ignored-rejection logging, and silent rejection after owner unload.
@@ -10,12 +10,14 @@ Status: implemented
## Decision
`SessionProjectionRegistry.register` 返回可等待的注册结果,用于获取类型化句柄。领域所有者在激活期间等待它,再提供依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。领域服务在内部持有定义和句柄,通过方法返回业务数据。消费方依赖该服务,无需注册其定义或接收投影读取句柄。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。[已弃用的兼容接口](2026-10-19-session-projection-compatibility.zh.md)允许迁移期间使用旧注册和按键读取,不改变保留读取句柄的行为。
`SessionProjectionRegistry.register` 返回可等待的注册结果,用于获取类型化句柄。领域所有者在激活期间等待它,再提供依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。领域服务在内部持有定义和句柄,通过方法返回业务数据。消费方依赖该服务,无需注册其定义或接收投影读取句柄。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。
句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进、快照和检查点中移除单元。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。
AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`。AgentLoop 在发布工厂前只获取一次 inbox 句柄,在创建 Agent 作用域前读取最后轮次,并将 inbox 回调与该轮次编号传入同步 Agent 构造过程。loop fiber 移除全局注册后,回调仍在清理期间保留其句柄。这保留既有创建与 setup 顺序,无需逐 Agent 注册或增加初始化状态。
按同步 API 编译的插件在迁移期间继续运行。可等待的注册结果同时就是原有的 Cordis effect 释放函数:调用它会释放注册,组合 effect 也会接管其所有权;该调用已弃用。注册表级 `stateOf(session, key)` 作为已弃用的读取保留,返回 `unknown`;最后一次释放后它失去访问,并且每个 registry 对每个键输出一次 `ctx.logger.warn`。注册本身不产生警告,因为被忽略的结果与稍后才等待的结果无法区分。旧插件可能从不观察注册结果,因此 registry 挂上自己的拒绝处理函数:当前轮次结束时,没有任何调用方通过 `then`、`catch` 或 `finally` 观察的拒绝会通过调用方插件的 logger 记录,插件保持激活;owner 已不在加载或激活状态的拒绝会被丢弃。TypeScript 消费方仍需迁移声明;宿主状态映射不再恢复。
## Alternatives considered
**保留注册表级查询并延迟注销。** 这使无关插件的释放顺序依赖每个读取方的最后一次访问。保留读取句柄使清理不再依赖该顺序。
@@ -26,6 +28,10 @@ AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`
**将函数插件改为类。** Cordis 已等待 `apply` 返回的 Promise。保留命名空间导出可避免发布入口与诊断名称的破坏性变更。
**要求所有外部插件立即迁移。** 已编译的插件会因缺失的释放函数和按键读取在运行时失败,尽管它们读取的投影键与状态字段都没有变化。
**返回包装释放函数的新函数。** Cordis 组合按返回值身份接管 effect。包装函数会让原注册继续归 fiber 所有,因此 owner 卸载可能在组合清理结束前释放它。
## Consequences
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。兼容测试与真实 Loader 子进程覆盖可调用与组合释放、按键读取及其警告、被忽略拒绝的日志记录,以及 owner 卸载后的静默拒绝。
@@ -1,19 +0,0 @@
# Bilingual-pair consistency record for 2026-10-19-session-projection-compatibility.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-19-session-projection-compatibility.md
/agent-note-deprecated-projection-registration-compatibility:
en: 16dafcb465745bda
zh: e919e4b1ffe225a9
/agent-note-deprecated-projection-registration-compatibility/problem:
en: bf506a273f12562e
zh: 5e97cd5f975f771d
/agent-note-deprecated-projection-registration-compatibility/decision:
en: 0027344b38d87be2
zh: d3338cebd1994bbc
/agent-note-deprecated-projection-registration-compatibility/alternatives-considered:
en: 9a71e47f7692dfd3
zh: 9cd497ede37faca9
/agent-note-deprecated-projection-registration-compatibility/consequences:
en: 67bc3ea8d1aaa56c
zh: 453e0c79887d823f
@@ -1,29 +0,0 @@
# Agent Note: Deprecated projection registration compatibility
Status: implemented
English | [中文](2026-10-19-session-projection-compatibility.zh.md)
## Problem
Existing external plugins use the disposer returned by projection registration and read state through the registry. Asynchronous retained readers improve acquisition and cleanup ownership, but removing those runtime operations forces unrelated plugins to migrate together.
## Decision
The [retained-reader decision](2026-10-12-session-projection-registration-handles.md) remains the normal API. A registration is Promise-compatible and callable: awaiting it yields a handle, while calling it preserves the old disposal operation. The callable value is the original Cordis effect disposer so a composed effect can adopt its ownership and control release order. Keyed reads remain a deprecated runtime API returning `unknown`. TypeScript consumers migrate their declarations; the old host state map and generic signatures are not restored. The first keyed read of each key logs one deprecation warning through `ctx.logger.warn`; each registry instance owns that per-key state, which has no process-global owner. Registration and calls to its result do not warn: an ignored result is indistinguishable from one awaited later, so detecting legacy registration would need a startup deadline.
Definition installation stays synchronous and handle delivery stays asynchronous, exactly as in the retained-reader decision. Registration errors reject the returned value. A legacy plugin may never observe it, and an unhandled rejection would end a `dsh` process where the old API failed only that plugin's activation. When no caller has observed a rejection by the end of the current turn, the registry logs it through the calling plugin's logger and leaves the plugin active without that unit; a rejection whose owner is no longer loading or active is dropped, because nothing can use its handle. An observed rejection remains the caller's responsibility.
This partially supersedes the earlier decision's unconditional migration requirement. Retained handles still fold after release, keyed reads still lose access after the final release, and new code uses private handles or owning domain services. Synchronous `try/catch` cannot catch asynchronous acquisition failure. Compatibility relies on the current synchronous definition installation; introducing asynchronous preparation requires revisiting legacy consumers that read before activation rather than silently returning incomplete state.
## Alternatives considered
**Require every external plugin to migrate immediately.** This makes the retained-reader change a deployment-wide coordination requirement even for plugins that only register during initialization and read after activation.
**Return a new wrapper around the disposer.** Cordis composition adopts effects by their original returned identities. A wrapper leaves the original registration independently owned, so owner disposal can release it before the composed cleanup finishes.
**Automatically delay every effect or event listener.** These operations already have synchronous consumers and initialization-time observers. The compatibility requirement belongs to projection acquisition; changing unrelated effect timing would create a larger migration.
## Consequences
Legacy plugins can migrate independently while new code keeps explicit asynchronous acquisition. The compatibility layer retains synchronous runtime composition and once-per-key keyed-read diagnostics. An async function or async generator assimilates the callable thenable into its handle, so legacy async return/yield composition still requires migration. Native-Promise-only consumers use `Promise.resolve(registration)`. Focused tests cover modern error recovery, ignored-rejection logging, silent rejection after owner unload, callable and composed disposal, owner unload during acquisition, and per-registry warning ownership. A real Loader subprocess exercises an unchanged-style plugin against the package entry points and verifies its reads and its keyed-read warning.
@@ -1,29 +0,0 @@
# Agent Note: 已弃用的投影注册兼容接口
Status: implemented
[English](2026-10-19-session-projection-compatibility.md) | 中文
## Problem
现有外部插件使用投影注册返回的 disposer,并通过 registry 读取状态。异步保留读取句柄改善了获取和清理所有权,但删除这些运行时操作会迫使彼此无关的插件一起迁移。
## Decision
[保留读取句柄的决策](2026-10-12-session-projection-registration-handles.zh.md)仍是常规 API。注册返回值兼容 Promise 且可调用:等待它得到句柄,调用它保留旧的释放操作。这个可调用值就是原有 Cordis effect disposer,使组合 effect 能接管其所有权并控制释放顺序。按键读取作为已弃用的运行时 API 保留,返回类型为 `unknown`。TypeScript 消费方仍需迁移声明;旧宿主状态映射和泛型签名不再恢复。每个键的首次按键读取通过 `ctx.logger.warn` 输出一次弃用警告;该逐键状态归各 registry 实例所有,不归进程全局对象所有。注册及调用其返回值不产生警告:被忽略的返回值与稍后才等待的返回值无法区分,检测旧式注册需要一个启动截止点。
定义安装保持同步,句柄交付保持异步,与保留读取句柄的决策完全一致。注册错误使返回值拒绝。旧插件可能从不观察返回值;未处理的拒绝会结束 `dsh` 进程,而旧 API 只会令该插件激活失败。当前轮次结束前没有调用方观察该拒绝时,registry 通过调用方插件的 logger 记录它,并让插件保持激活但缺少该单元;owner 已不在加载或激活状态的拒绝会被丢弃,因为已没有人能使用其句柄。被观察的拒绝仍由调用方负责。
这部分取代了先前决策中无条件迁移的要求。保留的句柄仍可在释放后折叠事件,按键读取仍在最后一次释放后失去访问,新代码使用私有句柄或所属领域服务。同步 `try/catch` 无法捕获异步获取失败。兼容性依赖当前同步安装定义的实现;引入异步准备时,需要重新处理在激活前读取的旧消费方,不能静默返回不完整状态。
## Alternatives considered
**要求所有外部插件立即迁移。** 即使插件只在初始化期间注册并在激活后读取,也会让保留读取句柄的变化成为整个部署需要协调的事项。
**返回 disposer 的新包装函数。** Cordis 组合按原始返回值身份接管 effect。包装函数会让原注册继续独立归属,从而使 owner 卸载在组合清理结束前释放它。
**自动延后所有 effect 或事件监听器。** 这些操作已有同步消费方和初始化期间的观察方。兼容要求属于投影获取;改变无关 effect 的时机会造成更大范围的迁移。
## Consequences
旧插件可以独立迁移,新代码保留显式异步获取。兼容层保留同步运行时组合和按键读取的逐键一次诊断。异步函数或异步生成器会把可调用的 thenable 吸收为句柄,因此旧的异步 return/yield 组合仍需迁移。仅接受原生 Promise 的消费方使用 `Promise.resolve(registration)`。定向测试覆盖现代错误恢复、被忽略拒绝的日志记录、owner 卸载后的静默拒绝、可调用与组合释放、获取期间 owner 卸载,以及逐 registry 的警告所有权。真实 Loader 子进程通过包入口运行保持旧写法的插件,并验证读取结果和按键读取警告。