mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
docs(cordis): address lifecycle audit review feedback
This commit is contained in:
1 parent
f6eb0c39f0
commit
59aec39e45
17 files changed
+49
-45
No files matched your search
+2
-2
@@ -18,5 +18,5 @@
|
||||
en: 5917a573cbdc5fa4
|
||||
zh: 3c3769070539a0a9
|
||||
/agent-note-handle-based-session-persistence/related:
|
||||
en: 3847c5ff5da94c18
|
||||
zh: cef77f8fcfd512c0
|
||||
en: 0ff54c05c2a64c07
|
||||
zh: f3bd6a5d1f37563f
|
||||
@@ -37,7 +37,6 @@ Resume, fork, subagent, ACP, webhook, and SDK sessions all persist through one e
|
||||
## Related
|
||||
|
||||
- [Cleanup dependency lifetimes](../../proposed/architecture/2026-10-09-cordis-cleanup-dependency-lifetimes.md) proposes retaining final events emitted during graceful cleanup, tightening this decision's permitted root-disposal tail loss while preserving handle ownership and storage format.
|
||||
|
||||
- [Session persistence as an abstract service](../../../../packages/session/session-persistence/README.md) — the seam this reshapes; its interface list reflects the handle API.
|
||||
- [Persistence export() and pre-release trims](../../archived/simplification/2026-08-27-persistence-export-and-pre-release-trims.md) — the preparatory removals, including the retired cold blank probe.
|
||||
- [Retain ignorable external session events](2026-08-30-retain-ignorable-external-session-events.md) — the read-side refusal contract, now shared through `storage-contract` helpers.
|
||||
|
||||
-1
@@ -37,7 +37,6 @@ Status: implemented
|
||||
## 相关
|
||||
|
||||
- [清理依赖生命周期](../../proposed/architecture/2026-10-09-cordis-cleanup-dependency-lifetimes.zh.md)提议保留有序清理期间发出的最终事件,收紧本决定允许的根释放尾部丢失,同时保留句柄所有权与存储格式。
|
||||
|
||||
- [作为抽象服务的会话持久化](../../../../packages/session/session-persistence/README.zh.md)——本 Note 重塑的 seam;其接口列表已反映句柄 API。
|
||||
- [持久化 export() 与预发布读取路径精简](../../archived/simplification/2026-08-27-persistence-export-and-pre-release-trims.md)——预备性的移除,包括已退役的冷空白探测。
|
||||
- [保留可忽略的外部会话事件](2026-08-30-retain-ignorable-external-session-events.zh.md)——读取侧的拒绝约定,现经由 `storage-contract` 辅助函数共享。
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 0187ac573d06a360
|
||||
zh: 13e08f4f22bacfff
|
||||
/agent-note-persistent-pty-sessions/decision/agent-ownership-and-identity:
|
||||
en: 9bf79081f102eb51
|
||||
zh: 34855888f0614b47
|
||||
en: c2a38b8c0403e1e4
|
||||
zh: 6063cf5e41a6582e
|
||||
/agent-note-persistent-pty-sessions/decision/security-and-process-boundary:
|
||||
en: 882bbfd3f0dc6e09
|
||||
zh: a547d6ff5e6ef7a1
|
||||
|
||||
@@ -36,6 +36,8 @@ There are no plugin-load auto-start sessions. `terminal_open` creates a session
|
||||
|
||||
Agent-scope disposal closes registrations first, then awaits quiescent teardown of every owned PTY. Unpublished backend setup is a tracked lifecycle operation: owner or service disposal aborts its service-owned signal, waits for backend settlement and rollback, and only then returns. Caller cancellation retains its exact `AbortSignal.reason` even when the backend rejects or returns a session whose rollback close fails; that cleanup failure remains tracked for later owner or service disposal instead of replacing the caller reason. A lifecycle-triggered rollback close failure rejects both the spawn and the disposing lifecycle, while `TerminalBackendCleanupError` lets a backend preserve its own failed startup cleanup for the disposing lifecycle without replacing a caller cancellation. When caller cancellation settles before disposal, the cleanup failure remains tracked owner activity until later owner or service disposal consumes and reports it, so sandbox-mode policy cannot mistake failed cleanup for quiescence. Backend or tool-plugin reload does not orphan sessions: ownership lives in `TerminalSessionService` until the agent ends, following the same service-owned-record pattern as [`ctx.jobs`](../../../../packages/jobs/jobs/README.md). The service reserves the session synchronously for one active send before returning its operation, including before a background job id becomes visible; a second send fails with `SEND_ACTIVE`, so output and cancellation cannot cross operation ownership.
|
||||
|
||||
The [cleanup dependency proposal](../../proposed/architecture/2026-10-09-cordis-cleanup-dependency-lifetimes.md) distinguishes early admission or optional-notification closure from releasing required completion observers. PTY ownership and quiescent teardown remain required.
|
||||
|
||||
### Security and process boundary
|
||||
|
||||
A registered `shell` backend constrains how a terminal starts; it does not constrain commands typed after startup. `dsh-terminal-bash` therefore applies two protections before spawning:
|
||||
|
||||
@@ -36,6 +36,8 @@ harness 可以运行前台与后台命令、编辑文件和委派工作,但无
|
||||
|
||||
agent scope dispose(资源释放)时先撤销注册,再等待全部所属 PTY 完全停稳。未发布的后端 setup 同样是受追踪的生命周期操作:owner 或服务 dispose 会中止服务自有的 signal,等待后端结算与回滚完成后才返回。即使后端 reject,或返回的会话在回滚 close 时失败,调用方取消仍会原样保留其 `AbortSignal.reason`;该清理失败不会替换调用方原因,而会继续受追踪,留待后续 owner 或服务 dispose 处理。由 lifecycle dispose 触发的回滚 close 失败会使 spawn 与该 lifecycle dispose 都 reject,而 `TerminalBackendCleanupError` 让后端在不替换调用方取消的前提下,为该 lifecycle dispose 保留自身的启动清理失败。若调用方取消在 dispose 开始前已经结算,该清理失败会继续作为受追踪的 owner activity 保留,直到后续 owner 或服务 dispose 消费并报告它,因此沙箱模式策略不会把清理失败误判为完全停稳。后端或工具插件 reload 不会遗留会话:所有权持续存放在 `TerminalSessionService` 中,直到 agent 结束,与 [`ctx.jobs`](../../../../packages/jobs/jobs/README.zh.md) 的服务持有记录模式一致。服务会先同步把会话预留给一次活跃发送,再返回该操作;后台发送同样会在 job id 对外可见前完成预留。第二次发送会以 `SEND_ACTIVE` 失败,因此输出与取消无法跨越操作所有权。
|
||||
|
||||
[清理依赖提案](../../proposed/architecture/2026-10-09-cordis-cleanup-dependency-lifetimes.zh.md)区分提前关闭准入或可选通知与释放必需的完成观察器。PTY 所有权与完全停稳的清理要求仍然成立。
|
||||
|
||||
### 安全与进程边界
|
||||
|
||||
注册的 `shell` 后端只约束终端如何启动,不约束启动后输入的命令。因此 `dsh-terminal-bash` 在 spawn 前应用两层保护:
|
||||
|
||||
+2
-2
@@ -42,8 +42,8 @@
|
||||
en: bf81660f7d4638fa
|
||||
zh: 4d59310d2e6476fd
|
||||
/agent-note-logical-session-and-storage-rebuild/references:
|
||||
en: 868c8934c3580103
|
||||
zh: 5b3b01c7f9a2f0e1
|
||||
en: 3a8115fc089df4dc
|
||||
zh: 155a6514bb296018
|
||||
/agent-note-logical-session-and-storage-rebuild/alternatives-considered:
|
||||
en: 4bc014b676d93c13
|
||||
zh: 1e0a04856da60bb0
|
||||
|
||||
@@ -142,7 +142,6 @@ The proposal preserves the shipped [handle-based persistence decision](../../imp
|
||||
## References
|
||||
|
||||
- [Cleanup dependency lifetimes](2026-10-09-cordis-cleanup-dependency-lifetimes.md) adds the graceful-teardown requirement for the stage 4 enqueue ownership change: retain writers and required listeners until producers finish publishing final events.
|
||||
|
||||
- [Sessions subsystem](../../../../docs/subsystems/session.md) defines the shipped session API and event semantics.
|
||||
- [Session persistence subsystem](../../../../docs/subsystems/persistence.md) defines the shipped durable format and lifecycle.
|
||||
- [`@deepseek-ai/dsh-session`](../../../../packages/core/session/README.md) owns the session package contract.
|
||||
|
||||
@@ -142,7 +142,6 @@ composition ──► SessionService ──► LogicalSession implementation
|
||||
## 参考资料
|
||||
|
||||
- [清理依赖生命周期](2026-10-09-cordis-cleanup-dependency-lifetimes.zh.md)为阶段 4 的入队所有权变更补充有序清理要求:保留写入器与必需监听器,直到生产者完成最终事件发布。
|
||||
|
||||
- [会话子系统](../../../../docs/subsystems/session.zh.md)定义已交付的会话 API 与事件语义。
|
||||
- [会话持久化子系统](../../../../docs/subsystems/persistence.zh.md)定义已交付的持久格式与生命周期。
|
||||
- [`@deepseek-ai/dsh-session`](../../../../packages/core/session/README.zh.md)拥有会话包约定。
|
||||
|
||||
+14
-14
@@ -6,8 +6,8 @@
|
||||
en: 5697ed719fc785dc
|
||||
zh: 6752482fc87287bd
|
||||
/agent-note-keep-cleanup-dependencies-usable-until-consumers-finish/problem:
|
||||
en: 6e888020a4aa2342
|
||||
zh: 34a0b69c2b6350e6
|
||||
en: 8097f1f18de77ada
|
||||
zh: 6ac996a2e319b376
|
||||
/agent-note-keep-cleanup-dependencies-usable-until-consumers-finish/proposal:
|
||||
en: cd7c3cd7a85e3653
|
||||
zh: fe76a2eddddec695
|
||||
@@ -15,23 +15,23 @@
|
||||
en: 630ab3bad5d44193
|
||||
zh: 71d3ba982c6f9a42
|
||||
/agent-note-keep-cleanup-dependencies-usable-until-consumers-finish/p0-findings/session-persistence-drops-events-emitted-during-cleanup:
|
||||
en: 768daa2d4923ba75
|
||||
zh: c9b330c97eea4b6e
|
||||
en: 392779b4aea2279d
|
||||
zh: 7228140dd5f47b40
|
||||
/agent-note-keep-cleanup-dependencies-usable-until-consumers-finish/p0-findings/storage-closes-before-schedule-commits-accepted-delivery:
|
||||
en: 0c5cc4df39139e5e
|
||||
zh: edba7ae9b1d37882
|
||||
en: 8a9578bbc700c0bf
|
||||
zh: 8ee46e5ef1cf3972
|
||||
/agent-note-keep-cleanup-dependencies-usable-until-consumers-finish/p0-findings/authorization-loses-required-observation-or-an-accepted-credential-write:
|
||||
en: 11db754f4a82518e
|
||||
zh: 320e40ba82f2edbd
|
||||
en: 33312dc2b9813ef9
|
||||
zh: ba2fb5fac770e698
|
||||
/agent-note-keep-cleanup-dependencies-usable-until-consumers-finish/p0-findings/ssh-disables-cleanup-requests-before-dependent-process-cleanup-finishes:
|
||||
en: 6bcf52fa7787d074
|
||||
zh: bb0d03d560faf11d
|
||||
en: 85fb2b564bc40230
|
||||
zh: 97c3e794124f8044
|
||||
/agent-note-keep-cleanup-dependencies-usable-until-consumers-finish/p0-findings/windows-sandbox-releases-temporary-process-resources-during-consumer-cleanup:
|
||||
en: 26f69066bae53851
|
||||
zh: 4f3447d068824fd3
|
||||
en: faa50d48dca589c5
|
||||
zh: a9e651ccff96ae31
|
||||
/agent-note-keep-cleanup-dependencies-usable-until-consumers-finish/scope-and-existing-decisions:
|
||||
en: f39906ee5595279e
|
||||
zh: f72402cf6b0f584f
|
||||
en: 8cf4a670022dcb47
|
||||
zh: 0611538847281f9a
|
||||
/agent-note-keep-cleanup-dependencies-usable-until-consumers-finish/alternatives-considered:
|
||||
en: 383a83fd09d3fe0d
|
||||
zh: cf55505415b6f197
|
||||
|
||||
+9
-7
@@ -8,7 +8,7 @@ English | [中文](2026-10-09-cordis-cleanup-dependency-lifetimes.zh.md)
|
||||
|
||||
A disposer can await its consumers while another disposer has already removed the listener or closed the resource those consumers need. Awaiting a provider's final disposal promise therefore does not establish resource survival. This affects shutdown and plugin replacement, including final Session events, accepted reminder delivery, credential commits, and remote process termination.
|
||||
|
||||
The [Cordis audit skill](../../../skills/cordis-styling-nitpicking/SKILL.md#p0-cleanup-dependency-released-before-consumer-completion) classifies this lifecycle violation as P0. [Fiber unload](../../../../vendor/cordis/src/fiber.ts#L675) starts sibling disposers concurrently; [service withdrawal](../../../../vendor/cordis/src/reflect.ts#L297) waits for dependent fibers only inside its own disposer. Service injection establishes dependency tracking but cannot prevent sibling resource closure. [Event subscriptions](../../../../vendor/cordis/src/events.ts#L254) are independently owned effects, including subscriptions made through a caller-traced service context.
|
||||
The [Cordis audit skill](../../../skills/cordis-styling-nitpicking/SKILL.md#p0-cleanup-dependency-released-before-consumer-completion) classifies this lifecycle violation as P0. [Fiber unload (`Fiber._unload`)](../../../../vendor/cordis/src/fiber.ts) starts sibling disposers concurrently; [service withdrawal (`ReflectService.provide`)](../../../../vendor/cordis/src/reflect.ts) waits for dependent fibers only inside its own disposer. Service injection establishes dependency tracking but cannot prevent sibling resource closure. [Event subscriptions (`EventsService.register`)](../../../../vendor/cordis/src/events.ts) are independently owned effects, including subscriptions made through a caller-traced service context.
|
||||
|
||||
## Proposal
|
||||
|
||||
@@ -20,7 +20,7 @@ The audit below records unresolved production paths inspected on 2026-10-09. It
|
||||
|
||||
### Session persistence drops events emitted during cleanup
|
||||
|
||||
**Owners and evidence.** The JSONL service installs its [tracker](../../../../packages/session/session-persistence-jsonl/src/index.ts#L276) on the provider fiber. The tracker's [event and flush listeners](../../../../packages/session/session-persistence-jsonl/src/storage.ts#L534) and [handle-close sweep](../../../../packages/session/session-persistence-jsonl/src/storage.ts#L555) are sibling effects. The Agent loop [cancels, awaits idle and scope disposal, then closes its handle](../../../../packages/core/agent-loop/src/index.ts#L554). Its [turn unwind](../../../../packages/core/agent-loop/src/agent.ts#L345) can publish recovered tool results and closing events after awaits.
|
||||
**Owners and evidence.** The JSONL service installs its [tracker (`JsonlSessionPersistence`)](../../../../packages/session/session-persistence-jsonl/src/index.ts) on the provider fiber. The tracker's [event and flush listeners (`JsonlBackendTracker.install`)](../../../../packages/session/session-persistence-jsonl/src/storage.ts) and [handle-close sweep (`JsonlBackendTracker.install`)](../../../../packages/session/session-persistence-jsonl/src/storage.ts) are sibling effects. The Agent loop [cancels, awaits idle and scope disposal, then closes its handle (`AgentLoop.prepare`)](../../../../packages/core/agent-loop/src/index.ts). The `AgentLoop.prepare` teardown comment explicitly requires backend live-event listeners to remain mounted through driver shutdown to receive closing events. Its [turn unwind (`ReactLoopAgent.turn`)](../../../../packages/core/agent-loop/src/agent.ts) can publish recovered tool results and closing events after awaits.
|
||||
|
||||
**Failure and correction.** During root disposal, pause the turn's cancellation unwind. JSONL can remove its routing listeners and finish closing the currently buffered prefix before the turn emits its final events. Those events reach the live Session but never enter the writer; a later idempotent handle close cannot recover them. Retain routing, writers, and write ownership until every producer settles, then perform the final drain. This is source-traced; a full Session reproduction remains required.
|
||||
|
||||
@@ -28,7 +28,7 @@ The [handle-based persistence decision](../../implemented/architecture/2026-08-2
|
||||
|
||||
### Storage closes before Schedule commits accepted delivery
|
||||
|
||||
**Owners and evidence.** The storage-domain plugin independently registers [facility closure and service provision](../../../../packages/storage/storage-domain/src/index.ts#L223). [Domain close](../../../../packages/storage/storage-domain/src/domain.ts#L236) marks the domain disposing, and [later enqueues reject](../../../../packages/storage/storage-domain/src/domain.ts#L263). Schedule [awaits its runtime and management work before closing its domain](../../../../packages/schedule/schedule/src/index.ts#L143). Accepted delivery [flushes the Session before retiring or advancing the task](../../../../packages/schedule/schedule/src/runtime.ts#L122).
|
||||
**Owners and evidence.** The storage-domain plugin independently registers [facility closure and service provision (`apply / DomainFacility.closeAll`)](../../../../packages/storage/storage-domain/src/index.ts). [Domain close (`DomainImpl.runClose`)](../../../../packages/storage/storage-domain/src/domain.ts) marks the domain disposing, and [later enqueues reject (`DomainImpl.enqueue`)](../../../../packages/storage/storage-domain/src/domain.ts). Schedule [awaits its runtime and management work before closing its domain (`ScheduleService`)](../../../../packages/schedule/schedule/src/index.ts). Accepted delivery [flushes the Session before retiring or advancing the task (`ScheduleRuntime.drive`)](../../../../packages/schedule/schedule/src/runtime.ts).
|
||||
|
||||
**Failure and correction.** Hold that Session flush, unload storage-domain, then release the flush. The provider's sibling closer can mark the domain closed while Schedule is still draining; the final task commit then fails. A reminder already persisted in the inbox remains active in task storage and can be delivered again after restart. Withdraw the service and await consumers before closing leftover domains; retain backend units through the domain drain. The [JSON backend](../../../../packages/storage/storage-json/src/index.ts) and [SQLite backend](../../../../packages/storage/storage-sqlite/src/index.ts) have the same sibling-close ownership concern and belong in that implementation review, not separate unproven consumer findings.
|
||||
|
||||
@@ -36,21 +36,21 @@ A focused in-memory experiment using the actual Cordis and storage-domain implem
|
||||
|
||||
### Authorization loses required observation or an accepted credential write
|
||||
|
||||
**Owners and evidence.** Authorization's [commit observer](../../../../packages/credentials/authorization/src/index.ts#L385) is a caller-owned `ctx.on` effect. Its [commit operation](../../../../packages/credentials/authorization/src/index.ts#L393) awaits credential storage, while [success requires the observed update](../../../../packages/credentials/authorization/src/index.ts#L432). The account service [leaves committing attempts un-aborted and awaits their completion](../../../../packages/credentials/deepseek-account-platform/src/index.ts#L152).
|
||||
**Owners and evidence.** Authorization's [commit observer (`AuthorizationService.attempt`)](../../../../packages/credentials/authorization/src/index.ts) is a caller-owned `ctx.on` effect. Its [commit operation (`AuthorizationService.attempt`)](../../../../packages/credentials/authorization/src/index.ts) awaits credential storage, while [success requires the observed update (`AuthorizationService.attempt`)](../../../../packages/credentials/authorization/src/index.ts). The account service [leaves committing attempts un-aborted and awaits their completion (`PlatformAccount`)](../../../../packages/credentials/deepseek-account-platform/src/index.ts).
|
||||
|
||||
**Successful write, false failure.** Pause a credential write, dispose the account consumer, then complete the write and emit its update. The observer can auto-unsubscribe alongside the account's drain; Authorization then rejects with `NOT_COMMITTED` despite successful storage. Keep observation owned by the operation until its result settles, or acknowledge successful `session.commit` directly while giving legacy direct-write observation an explicit lifetime. A focused experiment using actual Cordis and Authorization with a deferred in-memory credential provider reproduced `NOT_COMMITTED` after the provider write succeeded; the complete browser sign-in path is source-traced.
|
||||
|
||||
**Accepted write rejected during provider disposal.** Separately, credentials-local [sets `closed` before waiting for queued operations](../../../../packages/credentials/credentials-local/src/index.ts#L569), and [queued record writes check that flag again](../../../../packages/credentials/credentials-local/src/index.ts#L677). With an earlier document operation held, an already-committing account attempt can have its accepted write rejected when the queue advances after provider disposal begins. Distinguish new admission from accepted operations, and retain write capability until dependent commit drainage finishes. This variant is source-traced, not independently reproduced. Fixing the observer alone cannot fix it.
|
||||
**Accepted write rejected during provider disposal.** Separately, credentials-local [sets `closed` before waiting for queued operations (`LocalCredentialProvider[Service.init]`)](../../../../packages/credentials/credentials-local/src/index.ts), and [queued record writes check that flag again (`LocalCredentialProvider.modifyRecord`)](../../../../packages/credentials/credentials-local/src/index.ts). With an earlier document operation held, an already-committing account attempt can have its accepted write rejected when the queue advances after provider disposal begins. Distinguish new admission from accepted operations, and retain write capability until dependent commit drainage finishes. This variant is source-traced, not independently reproduced. Fixing the observer alone cannot fix it.
|
||||
|
||||
### SSH disables cleanup requests before dependent process cleanup finishes
|
||||
|
||||
**Owners and evidence.** The SSH service's [disposal](../../../../packages/ssh/ssh/src/index.ts#L203) sets `closed` and aborts its lifetime before awaiting helper shutdown. Its [request guard](../../../../packages/ssh/ssh/src/index.ts#L234) rejects subsequent use. The injected subprocess provider [terminates and awaits live processes and terminals during cleanup](../../../../packages/ssh/subprocess-ssh/src/index.ts#L228), which requires SSH requests.
|
||||
**Owners and evidence.** The SSH service's [disposal (`SshConnection.disposeOnce`)](../../../../packages/ssh/ssh/src/index.ts) sets `closed` and aborts its lifetime before awaiting helper shutdown. Its [request guard (`SshConnection.assertOpen`)](../../../../packages/ssh/ssh/src/index.ts) rejects subsequent use. The injected subprocess provider [terminates and awaits live processes and terminals during cleanup (`SshSubprocessRuntime`)](../../../../packages/ssh/subprocess-ssh/src/index.ts), which requires SSH requests.
|
||||
|
||||
**Failure and correction.** SSH provider disposal can close request admission while subprocess cleanup still needs termination and exit confirmation, despite `inject = ['ssh']`. The dependent reports that SSH process cleanup could not be confirmed. Keep the cleanup request capability available until dependents settle, then close the helper, sockets, and master. The helper's own close also attempts remote cleanup: the evidence establishes a lost cleanup capability and possible disposal errors, not guaranteed orphaned remote processes. A focused reproduction used real Cordis, SSH and subprocess providers with a mocked child-process/filesystem transport. It started a process and unloaded SSH after the exit-wait RPC was admitted. The handle rejected with `SSH connection is closed`; the helper received `close` without `process.terminate`. This verifies the composed-provider defect, not actual remote process quiescence.
|
||||
|
||||
### Windows sandbox releases temporary process resources during consumer cleanup
|
||||
|
||||
**Owners and evidence.** The local sandbox's [sibling cleanup effect](../../../../packages/sandbox/sandbox-local/src/index.ts#L304) calls [grant revocation and temporary-directory removal](../../../../packages/sandbox/sandbox-local/src/index.ts#L465). The [temporary capability](../../../../packages/sandbox/sandbox-local/src/index.ts#L426) belongs to the provider and is embedded in the confined runner arguments. The injected Node PTC runtime [aborts and awaits accepted runs](../../../../packages/ptc-runtime/ptc-runtime-node/src/index.ts#L92); [completion awaits process exit and output drainage](../../../../packages/ptc-runtime/ptc-runtime-node/src/index.ts#L168).
|
||||
**Owners and evidence.** The local sandbox's [sibling cleanup effect (`LocalSandboxProvider`)](../../../../packages/sandbox/sandbox-local/src/index.ts) calls [grant revocation and temporary-directory removal (`LocalSandboxProvider.revokeAclGrants`)](../../../../packages/sandbox/sandbox-local/src/index.ts). The [temporary capability (`LocalSandboxProvider.materializeAclGrant`)](../../../../packages/sandbox/sandbox-local/src/index.ts) belongs to the provider and is embedded in the confined runner arguments. The injected Node PTC runtime [aborts and awaits accepted runs (`NodePtcRuntime`)](../../../../packages/ptc-runtime/ptc-runtime-node/src/index.ts); [completion awaits process exit and output drainage (`NodePtcRuntime.execute`)](../../../../packages/ptc-runtime/ptc-runtime-node/src/index.ts).
|
||||
|
||||
**Failure and correction.** Unloading the sandbox can revoke a live process's temporary-directory permission and remove its directory while the PTC consumer is still awaiting process termination. Final temporary-file access can fail, and removal can encounter a still-running process. Retain temporary capabilities through the admitted process lifetime, then revoke and remove them after actual process quiescence. Standing workspace grants intentionally remain; this finding concerns revocable temporary resources. Holder-owned shell processes also require process lifetime ownership, so merely ordering executor unload is insufficient.
|
||||
|
||||
@@ -58,6 +58,8 @@ A controlled reproduction used actual Cordis, the local sandbox provider, and re
|
||||
|
||||
## Scope and existing decisions
|
||||
|
||||
The [persistent PTY decision](../../implemented/feature/2026-07-16-persistent-pty-sessions.md) remains active. Its early registration closure does not authorize removing observers required for quiescent teardown. Admission and optional notifications may stop first; completion dependencies must remain usable. This clarifies the scope of that teardown policy without replacing PTY ownership, rollback, or process-tree cleanup decisions.
|
||||
|
||||
Four independent directory scans covered core/session/jobs; execution, SSH, terminals, sandbox, subagents and workflow; host/client/API/boot/SDK/ACP/MCP/browser/computer packages and apps; and storage/schedule/credentials plus remaining provider groups. Candidate discovery used cleanup registrations, followed by consumer and resource-owner tracing. Surviving findings were rechecked against current master after refreshing the checkout. This is not an exhaustive semantic proof for every package.
|
||||
|
||||
The current AgentLoop composite retains projection registrations through ownership disposal, so the earlier projection candidate is excluded. Jobs cancellation already awaits settlement. Working-directory and title work revalidate cancellation after awaits. Projection-cache failures are explicitly fail-soft for rebuildable data; the inspected production telemetry consumer is on-demand. The Session history promotion candidate concerns admission after a drain snapshot, without an established prematurely released cleanup dependency, and is excluded from this focused rule.
|
||||
|
||||
+9
-7
@@ -8,7 +8,7 @@ Status: proposed
|
||||
|
||||
一个 disposer(清理函数)可能仍在等待消费者,另一个 disposer 却已经移除消费者需要的监听器或关闭资源。因此,等待 provider(提供者)的最终释放 promise,不能保证资源在此期间一直可用。这会影响关闭与插件替换,包括最终 Session 事件、已接受的提醒投递、凭据提交和远程进程终止。
|
||||
|
||||
[Cordis 审查 skill](../../../skills/cordis-styling-nitpicking/SKILL.md#p0-cleanup-dependency-released-before-consumer-completion) 将这种生命周期违规归为 P0。[Fiber 卸载](../../../../vendor/cordis/src/fiber.ts#L675) 并发启动同级 disposer;[服务撤销](../../../../vendor/cordis/src/reflect.ts#L297) 仅在自己的 disposer 内等待依赖 fiber。服务注入建立依赖追踪,却不能阻止同级资源关闭。[事件订阅](../../../../vendor/cordis/src/events.ts#L254) 是独立拥有的 effect,包括通过追踪调用方的服务上下文建立的订阅。
|
||||
[Cordis 审查 skill](../../../skills/cordis-styling-nitpicking/SKILL.md#p0-cleanup-dependency-released-before-consumer-completion) 将这种生命周期违规归为 P0。[Fiber 卸载 (`Fiber._unload`)](../../../../vendor/cordis/src/fiber.ts) 并发启动同级 disposer;[服务撤销 (`ReflectService.provide`)](../../../../vendor/cordis/src/reflect.ts) 仅在自己的 disposer 内等待依赖 fiber。服务注入建立依赖追踪,却不能阻止同级资源关闭。[事件订阅 (`EventsService.register`)](../../../../vendor/cordis/src/events.ts) 是独立拥有的 effect,包括通过追踪调用方的服务上下文建立的订阅。
|
||||
|
||||
## 提案
|
||||
|
||||
@@ -20,7 +20,7 @@ Status: proposed
|
||||
|
||||
### Session 持久化丢失清理期间发出的事件
|
||||
|
||||
**所有者与证据。** JSONL 服务在 provider fiber 上安装[追踪器](../../../../packages/session/session-persistence-jsonl/src/index.ts#L276)。追踪器的[事件与 flush 监听器](../../../../packages/session/session-persistence-jsonl/src/storage.ts#L534)和[句柄关闭清扫](../../../../packages/session/session-persistence-jsonl/src/storage.ts#L555)是同级 effect。Agent loop [取消运行,等待 idle 与 scope 释放,然后关闭句柄](../../../../packages/core/agent-loop/src/index.ts#L554)。其[轮次收尾](../../../../packages/core/agent-loop/src/agent.ts#L345)可能在 await 后发布补齐的工具结果与结束事件。
|
||||
**所有者与证据。** JSONL 服务在 provider fiber 上安装[追踪器 (`JsonlSessionPersistence`)](../../../../packages/session/session-persistence-jsonl/src/index.ts)。追踪器的[事件与 flush 监听器 (`JsonlBackendTracker.install`)](../../../../packages/session/session-persistence-jsonl/src/storage.ts)和[句柄关闭清扫 (`JsonlBackendTracker.install`)](../../../../packages/session/session-persistence-jsonl/src/storage.ts)是同级 effect。Agent loop [取消运行,等待 idle 与 scope 释放,然后关闭句柄 (`AgentLoop.prepare`)](../../../../packages/core/agent-loop/src/index.ts)。`AgentLoop.prepare` 的清理注释明确要求后端实时事件监听器在驱动器关闭期间保持挂载,以接收结束事件。其[轮次收尾 (`ReactLoopAgent.turn`)](../../../../packages/core/agent-loop/src/agent.ts)可能在 await 后发布补齐的工具结果与结束事件。
|
||||
|
||||
**失败与修正。** 根资源释放时,暂停轮次的取消收尾。JSONL 可以先移除路由监听器,并关闭当前已缓冲的前缀,然后轮次才发出最终事件。这些事件进入实时 Session,却从未进入写入器;之后幂等的句柄关闭无法恢复它们。应保留路由、写入器与写所有权,直到所有生产者结束,再执行最终排空。此项由源码追踪确认;仍需完整 Session 复现。
|
||||
|
||||
@@ -28,7 +28,7 @@ Status: proposed
|
||||
|
||||
### Storage 在 Schedule 提交已接受的投递前关闭
|
||||
|
||||
**所有者与证据。** storage-domain 插件独立注册[设施关闭与服务提供](../../../../packages/storage/storage-domain/src/index.ts#L223)。[Domain close](../../../../packages/storage/storage-domain/src/domain.ts#L236) 将 domain 标记为 disposing,[之后的入队会拒绝](../../../../packages/storage/storage-domain/src/domain.ts#L263)。Schedule [等待运行时与管理工作后才关闭自己的 domain](../../../../packages/schedule/schedule/src/index.ts#L143)。已接受的投递[先 flush Session,再结束任务或推进下一次执行](../../../../packages/schedule/schedule/src/runtime.ts#L122)。
|
||||
**所有者与证据。** storage-domain 插件独立注册[设施关闭与服务提供 (`apply / DomainFacility.closeAll`)](../../../../packages/storage/storage-domain/src/index.ts)。[Domain close (`DomainImpl.runClose`)](../../../../packages/storage/storage-domain/src/domain.ts) 将 domain 标记为 disposing,[之后的入队会拒绝 (`DomainImpl.enqueue`)](../../../../packages/storage/storage-domain/src/domain.ts)。Schedule [等待运行时与管理工作后才关闭自己的 domain (`ScheduleService`)](../../../../packages/schedule/schedule/src/index.ts)。已接受的投递[先 flush Session,再结束任务或推进下一次执行 (`ScheduleRuntime.drive`)](../../../../packages/schedule/schedule/src/runtime.ts)。
|
||||
|
||||
**失败与修正。** 暂停该 Session flush,卸载 storage-domain,然后放行 flush。provider 的同级关闭器可以在 Schedule 仍在排空时将 domain 标记为关闭,导致最终任务提交失败。已持久化到 inbox 的提醒在任务存储中仍处于活动状态,重启后可能再次投递。应先撤销服务并等待消费者,再关闭剩余 domain;在 domain 排空期间保留后端存储单元。[JSON 后端](../../../../packages/storage/storage-json/src/index.ts)与 [SQLite 后端](../../../../packages/storage/storage-sqlite/src/index.ts)也存在同级关闭的所有权风险,应纳入同一次实现审查,而不应在未确认消费者时另立发现。
|
||||
|
||||
@@ -36,21 +36,21 @@ Status: proposed
|
||||
|
||||
### Authorization 丢失必需的提交观察或已接受的凭据写入
|
||||
|
||||
**所有者与证据。** Authorization 的[提交观察器](../../../../packages/credentials/authorization/src/index.ts#L385)是调用方拥有的 `ctx.on` effect。[提交操作](../../../../packages/credentials/authorization/src/index.ts#L393)等待凭据存储,而[成功要求观察到更新](../../../../packages/credentials/authorization/src/index.ts#L432)。账号服务[不会中止正在提交的尝试,并等待其完成](../../../../packages/credentials/deepseek-account-platform/src/index.ts#L152)。
|
||||
**所有者与证据。** Authorization 的[提交观察器 (`AuthorizationService.attempt`)](../../../../packages/credentials/authorization/src/index.ts)是调用方拥有的 `ctx.on` effect。[提交操作 (`AuthorizationService.attempt`)](../../../../packages/credentials/authorization/src/index.ts)等待凭据存储,而[成功要求观察到更新 (`AuthorizationService.attempt`)](../../../../packages/credentials/authorization/src/index.ts)。账号服务[不会中止正在提交的尝试,并等待其完成 (`PlatformAccount`)](../../../../packages/credentials/deepseek-account-platform/src/index.ts)。
|
||||
|
||||
**写入成功,却报告失败。** 暂停凭据写入,释放账号消费者,再完成写入并发出更新。观察器可以与账号的排空并发自动退订;随后 Authorization 即使存储成功也会以 `NOT_COMMITTED` 拒绝。应让操作拥有观察器直到结果确定,或直接确认成功的 `session.commit`,同时给旧式直接写入观察明确的生命周期。使用实际 Cordis 与 Authorization、配合延迟的内存凭据 provider 的实验,复现了 provider 写入成功后返回 `NOT_COMMITTED`;完整浏览器登录路径由源码追踪确认。
|
||||
|
||||
**provider 释放期间拒绝已接受的写入。** 另一个问题是,credentials-local [先设置 `closed` 再等待队列操作](../../../../packages/credentials/credentials-local/src/index.ts#L569),而[排队中的记录写入再次检查该标志](../../../../packages/credentials/credentials-local/src/index.ts#L677)。当较早的文档操作暂停时,账号已进入提交阶段;provider 开始释放后队列继续推进,该已接受写入仍可能被拒绝。应区分新工作准入与已接受操作,并保留写入能力直到依赖者的提交排空。此变体由源码追踪确认,未单独复现。仅修复观察器无法解决它。
|
||||
**provider 释放期间拒绝已接受的写入。** 另一个问题是,credentials-local [先设置 `closed` 再等待队列操作 (`LocalCredentialProvider[Service.init]`)](../../../../packages/credentials/credentials-local/src/index.ts),而[排队中的记录写入再次检查该标志 (`LocalCredentialProvider.modifyRecord`)](../../../../packages/credentials/credentials-local/src/index.ts)。当较早的文档操作暂停时,账号已进入提交阶段;provider 开始释放后队列继续推进,该已接受写入仍可能被拒绝。应区分新工作准入与已接受操作,并保留写入能力直到依赖者的提交排空。此变体由源码追踪确认,未单独复现。仅修复观察器无法解决它。
|
||||
|
||||
### SSH 在依赖者的进程清理完成前禁用清理请求
|
||||
|
||||
**所有者与证据。** SSH 服务的[释放逻辑](../../../../packages/ssh/ssh/src/index.ts#L203)在等待 helper 关闭前设置 `closed` 并中止自身 lifetime。[请求检查](../../../../packages/ssh/ssh/src/index.ts#L234)拒绝后续使用。注入该服务的 subprocess provider [在清理时终止并等待存活进程与终端](../../../../packages/ssh/subprocess-ssh/src/index.ts#L228),这些工作需要 SSH 请求。
|
||||
**所有者与证据。** SSH 服务的[释放逻辑 (`SshConnection.disposeOnce`)](../../../../packages/ssh/ssh/src/index.ts)在等待 helper 关闭前设置 `closed` 并中止自身 lifetime。[请求检查 (`SshConnection.assertOpen`)](../../../../packages/ssh/ssh/src/index.ts)拒绝后续使用。注入该服务的 subprocess provider [在清理时终止并等待存活进程与终端 (`SshSubprocessRuntime`)](../../../../packages/ssh/subprocess-ssh/src/index.ts),这些工作需要 SSH 请求。
|
||||
|
||||
**失败与修正。** 即使有 `inject = ['ssh']`,SSH provider 释放仍可以在 subprocess 清理需要终止和退出确认时关闭请求准入。依赖者因此报告无法确认 SSH 进程清理。应保留清理请求能力直到依赖者结束,再关闭 helper、socket 和 master。helper 自身的 close 也会尝试远程清理:证据证明清理能力丢失与可能的释放错误,并不证明必然留下远程孤儿进程。聚焦复现使用实际 Cordis、SSH 与 subprocess provider,模拟子进程和文件系统传输。启动进程,并在退出等待 RPC 准入后卸载 SSH,句柄以 `SSH connection is closed` 拒绝;helper 收到 `close`,却未收到 `process.terminate`。这验证了 provider 组合中的缺陷,没有验证实际远程进程是否完全停止。
|
||||
|
||||
### Windows sandbox 在消费者清理期间释放进程临时资源
|
||||
|
||||
**所有者与证据。** 本地 sandbox 的[同级清理 effect](../../../../packages/sandbox/sandbox-local/src/index.ts#L304)调用[权限撤销与临时目录移除](../../../../packages/sandbox/sandbox-local/src/index.ts#L465)。[临时能力](../../../../packages/sandbox/sandbox-local/src/index.ts#L426)属于 provider,并被嵌入受限 runner 的参数中。注入该服务的 Node PTC runtime [中止并等待已接受的运行](../../../../packages/ptc-runtime/ptc-runtime-node/src/index.ts#L92);[完成流程等待进程退出与输出排空](../../../../packages/ptc-runtime/ptc-runtime-node/src/index.ts#L168)。
|
||||
**所有者与证据。** 本地 sandbox 的[同级清理 effect (`LocalSandboxProvider`)](../../../../packages/sandbox/sandbox-local/src/index.ts)调用[权限撤销与临时目录移除 (`LocalSandboxProvider.revokeAclGrants`)](../../../../packages/sandbox/sandbox-local/src/index.ts)。[临时能力 (`LocalSandboxProvider.materializeAclGrant`)](../../../../packages/sandbox/sandbox-local/src/index.ts)属于 provider,并被嵌入受限 runner 的参数中。注入该服务的 Node PTC runtime [中止并等待已接受的运行 (`NodePtcRuntime`)](../../../../packages/ptc-runtime/ptc-runtime-node/src/index.ts);[完成流程等待进程退出与输出排空 (`NodePtcRuntime.execute`)](../../../../packages/ptc-runtime/ptc-runtime-node/src/index.ts)。
|
||||
|
||||
**失败与修正。** 卸载 sandbox 时,PTC 消费者可能仍在等待进程终止,但存活进程的临时目录权限已被撤销,目录已被移除。最终临时文件访问可能失败,删除操作也可能遇到仍在运行的进程。应在已准入进程存活期间保留临时能力,在进程真正停止后再撤销权限并移除目录。长期工作区权限有意保留;本项只涉及可撤销的临时资源。由持有者管理的 shell 进程也需要与进程生命周期绑定的所有权,因此仅排列 executor 卸载顺序并不足够。
|
||||
|
||||
@@ -58,6 +58,8 @@ Status: proposed
|
||||
|
||||
## 范围与现有决定
|
||||
|
||||
[持久化 PTY 决定](../../implemented/feature/2026-07-16-persistent-pty-sessions.zh.md)继续生效。其提前撤销注册的规则不允许移除完全停稳所需的观察器。准入与可选通知可以先停止;完成操作所需的依赖必须保持可用。这里澄清该清理策略的范围,不替代 PTY 所有权、回滚或进程树清理决定。
|
||||
|
||||
四个独立目录扫描分别覆盖 core/session/jobs;执行、SSH、终端、sandbox、subagent 与 workflow;host/client/API/boot/SDK/ACP/MCP/browser/computer 包与 apps;storage/schedule/credentials 及其余 provider 分组。候选发现从清理注册开始,再追踪消费者与资源所有者。刷新检出后,对保留的发现按当前 master 重新验证。这不是对每个包的穷尽语义证明。
|
||||
|
||||
当前 AgentLoop 的组合 effect 在 ownership 释放期间保留 projection 注册,因此排除早先的 projection 候选。Jobs 取消已等待结束。Working-directory 和 title 工作在 await 后重新检查取消。Projection-cache 失败明确允许可重建数据降级;已检查的生产 telemetry 消费者按需调用。Session history 的 promotion 候选涉及排空快照之后接纳新工作,没有确认提前释放的清理依赖,因此不纳入本次聚焦规则。
|
||||
|
||||
@@ -3,7 +3,7 @@ name: cordis-styling-nitpicking
|
||||
description: Use during code review of DeepSeek Harness Cordis plugins and helpers reachable from their entrypoints, including pull requests, and during focused lifecycle audits. Checks singleton state, unreversed side effects, and cleanup dependencies released before consumers finish; reports substantiated P0 findings with ownership evidence and deterministic verification.
|
||||
---
|
||||
|
||||
# Cordis Styling Nitpicking
|
||||
# Cordis Ownership and Lifecycle Audit
|
||||
|
||||
This is a focused semantic review of Cordis ownership and lifecycle. Start a repository audit from exported plugin entrypoints (`apply` or a concrete default export invoked by Cordis), then trace module initialization, registered services, listeners, effects, disposers, and transitive calls reachable from those roots. Exclude unrelated helpers, build tools, CLI paths, and tests as finding targets. Read tests and vendored Cordis to establish behavior; report a generated defect against its owning source. Do not report defects in vendored code under this skill.
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 33085f2b58bab16c
|
||||
zh: be38e69a9c7d3f0e
|
||||
/defensive-patterns/dispose-must-reach-quiescence-not-just-request-it:
|
||||
en: 75c141c9b9429e1f
|
||||
zh: 79decb12b06b79ba
|
||||
en: 781f72b4b30dfcb9
|
||||
zh: 94aa456c1930fba5
|
||||
/defensive-patterns/contain-callback-exceptions-in-the-dispatcher:
|
||||
en: ae1788e76190db2c
|
||||
zh: a55ab7473bb0c8dd
|
||||
|
||||
@@ -18,7 +18,7 @@ When an implementation receives several representations of one outcome, normaliz
|
||||
|
||||
## Dispose must reach quiescence, not just request it
|
||||
|
||||
A teardown that issues kills/aborts but returns before the work stops leaves orphans. Make cleanup async and await the children's exit (kill → await `done`). Keep resources and listeners needed for completion or final persistence usable until consumers settle. Cordis awaits dependent unloading during service withdrawal, but sibling disposers run concurrently: `inject` alone does not order resource release. The [Cordis audit skill](../.agents/skills/cordis-styling-nitpicking/SKILL.md#p0-cleanup-dependency-released-before-consumer-completion) defines the ownership checks.
|
||||
Await child exit after killing or aborting. Optional notifications may stop early; resources and listeners needed for completion or final persistence must survive until consumers settle. See the [Cordis ownership checks](../.agents/skills/cordis-styling-nitpicking/SKILL.md#p0-cleanup-dependency-released-before-consumer-completion).
|
||||
|
||||
## Contain callback exceptions in the dispatcher
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
## dispose 必须达到完全停稳,而不仅仅是请求停止
|
||||
|
||||
如果清理流程只发出终止或中止信号便返回,而不等待工作真正停止,就会留下孤儿进程。清理逻辑应采用异步流程,并等待子进程退出(发出终止信号后等待 `done`)。完成操作或最终持久化所需的资源与监听器,必须保持可用直到消费者结束。Cordis 在撤销服务时会等待依赖者卸载,但同级 disposer(清理函数)并发运行:仅有 `inject` 不能建立资源释放顺序。[Cordis 审查 skill](../.agents/skills/cordis-styling-nitpicking/SKILL.md#p0-cleanup-dependency-released-before-consumer-completion) 定义了所有权检查步骤。
|
||||
发出终止或中止信号后,等待子进程退出。可选通知可以提前停止;完成操作或最终持久化所需的资源与监听器,必须保持可用直到消费者结束。参见 [Cordis 所有权检查](../.agents/skills/cordis-styling-nitpicking/SKILL.md#p0-cleanup-dependency-released-before-consumer-completion)。
|
||||
|
||||
## 在分发器中隔离回调异常
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
|
||||
这些都是踩坑总结得出的缺陷分类规范:下文每种范式都对应一类曾上线、或险些流入线上的问题,每条规范旨在杜绝同类问题复现。编写生命周期、并发、子进程、资源销毁相关代码前,请务必阅读本文档。
|
||||
|
||||
> **Dispose must reach quiescence, not just request it** — A teardown that issues kills/aborts but returns before the work stops leaves orphans. Make cleanup async and await the children's exit (kill → await `done`), and close listener/notification registries BEFORE killing so late completions stay silent. Tests prove disposal waited (pid gone right after `await fiber.dispose()`), not merely that the process eventually dies.
|
||||
> **Dispose must reach quiescence, not just request it** — Await child exit after killing or aborting. Optional notifications may stop early; resources and listeners needed for completion or final persistence must survive until consumers settle.
|
||||
|
||||
**dispose(资源释放)必须等待所有任务完全停稳,不能仅下发终止指令就返回**:如果清理过程只发出终止或中断信号,却不等任务停止就返回,就会留下孤儿进程。清理应采用异步方式,等待所有子任务彻底退出(先发出终止信号,再等待退出);发出信号前应先关闭监听器与通知注册表,使延迟到达的完成事件不再触发通知。测试要证明 dispose 的确等到清理完成:执行完 `await fiber.dispose()` 后进程 PID 立即消失,不能只检查进程最终会自行消亡。
|
||||
**dispose(资源释放)必须等待所有任务完全停稳,不能仅下发终止指令就返回**:发出终止或中止信号后,等待子进程退出。可选通知可以提前停止;完成操作或最终持久化所需的资源与监听器,必须保持可用直到消费者结束。
|
||||
|
||||
> **Async state is not synchronous state** — `agent.followup()` does not flip status before returning; a background job's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) instead of treating status as a per-follow-up result: several queued follow-ups run as consecutive turns under one `running` interval, while cancellation or disposal can discard unstarted items.
|
||||
|
||||
|
||||
Reference in new issue
Block a user