DSH / Atlas
2026-09-30implementedbug-fix

Persist owned job lifecycle facts

持久化有所有者 job 的生命周期事实

SDK consumers receive Session events, while job registration and settlement previously existed only in the live registry. A background tool can return before work finishes, and output collection can happen much later or never. Tool invocation times therefore cannot reconstruct a job's execution interval. Runtime-local ids also repeat after a registry restart.

English

Problem

SDK consumers receive Session events, while job registration and settlement previously existed only in the live registry. A background tool can return before work finishes, and output collection can happen much later or never. Tool invocation times therefore cannot reconstruct a job's execution interval. Runtime-local ids also repeat after a registry restart.

Decision

The local provider captures job/registered and job/settled facts for owned jobs, retaining the registry's epoch-millisecond timestamps, original owner, producer kind, label, and optional creating tool call. A UUID-backed JobId identifies both live controls and historical records across registry restarts and copied history. Unowned work has no Session journal.

The terminal record distinguishes a producer report from forced bookkeeping: producerReported is true when JobHooks.done fulfills, and false when that promise rejects or teardown cancellation throws. It reports fulfillment, not independently confirmed resource release. Missing terminal records remain incomplete and are not reconstructed from later reads or process restart.

Facts are appended synchronously at the registry commit before live hub reactions and completion waiters resume. Recording errors warn independently: already-started work still has its control id, and a failed journal append cannot prevent cleanup or release of waiters. The Session's ordinary persistence and SDK notification paths own delivery after append. SDK run() results cover bounded observation windows and can receive a settlement whose registration belongs to an earlier run; settlement records retain the registration facts for that consumer.

The registry contract and seam consolidation note own live execution, resource ownership, and event routing. This journal adds durable observations without replacing those responsibilities.

Alternatives considered

Add timestamps only to job_output. A model may never collect a job or may collect it after a long delay. Logging lifecycle commits records both cases without additional model calls or consuming output.

Keep a reusable control id alongside an execution UUID. Both values would identify one job. A unique JobId serves live controls and historical pairing.

Add a launcher cleanup registry. A separate pre-exit disposer path duplicates Cordis resource ownership and adds a launcher requirement to headless. Keeping persistence available through the final job teardown requires a separate cross-fiber lifecycle change; this journal retains the existing disposal mechanism.

Defer publication or propagate recording errors. Deferral lets a synchronous hub subscriber journal a completion notice before the settlement that caused it. Throwing after the starter succeeds can hide an already-running job from its caller. Synchronous, contained recording preserves causal order without abandoning execution.

Consequences

Application teardown can stop Session routing or close its writer before the final job settlement reaches storage. Normal one-shot completion and signal shutdown can therefore leave incomplete history, as can hard process loss. This change does not enforce cross-fiber disposal order or synthesize a missing terminal record.

The log and both SDKs expose execution intervals without changing tool schemas, output cursors, or job scheduling. Model-facing controls and notices carry the unique JobId; a bounded notice points to job_list when the full id cannot fit. These intervals run from registration commit through registry settlement, including the producer work and cleanup between those points; they are neither CPU time nor a directly additive rollout-latency metric. An append failure can also leave incomplete history. Persisted facts do not resume an external process or its output ring.

Focused tests cover original timestamps, nested call identity, output preservation, delayed cancellation, producer rejection, forced teardown, late completion, unique control ids, causal publication order, and recording-failure containment. SDK snapshots cover the durable and protocol projections.

中文

Problem

SDK 消费方接收 Session 事件,而 job 注册和结算此前只存在于运行期注册表中。后台工具可能在工作结束前返回,输出收集则可能晚得多,甚至永远不发生。因此工具调用时间无法重建 job 的执行区间。运行期 id 也会在注册表重启后重复。

Decision

本地提供方为有所有者的 job 捕获 job/registered 与 job/settled 事实,保留注册表的 Unix 毫秒时间戳、原始所有者、生产方 kind、label 和可选的创建工具调用。由 UUID 构成的 JobId 在注册表重启和历史复制后同时标识运行中的控制对象和历史记录。无主工作没有 Session 日志。

终态记录区分生产方报告与强制记账:producerReported 在 JobHooks.done 兑现时为 true,在该 promise 拒绝或销毁取消抛错时为 false。它记录兑现情况,不独立确认资源已释放。缺少终态记录时保持不完整,不根据较晚的读取或进程重启重建。

事实在注册表提交时同步追加,先于运行期 hub 的反应和完成等待方恢复。记录错误单独发出警告:已启动的工作仍有可控制的 id,日志追加失败不能阻止清理或释放等待方。追加后的投递由 Session 既有的持久化和 SDK 通知路径负责。SDK run() 结果只覆盖有界观察窗口,可能收到注册属于更早运行的结算;结算记录为这个消费方保留注册事实。

注册表约定和seam 收敛说明负责运行期执行、资源归属和事件路由。本日志增加持久化观测,不替代这些职责。

Alternatives considered

仅向 job_output 添加时间戳。 模型可能永远不收集某个 job,也可能很晚才收集。记录生命周期提交能覆盖这两种情况,无需额外模型调用或消费输出。

保留可重用的控制 id,另设执行 UUID。 两个值会标识同一个 job。唯一的 JobId 同时服务运行期控制和历史配对。

增加启动器清理注册表。 独立的退出前 disposer 路径会重复 Cordis 的资源归属机制,并增加 headless 对启动器的要求。让持久化持续可用直至最后一个 job 销毁,需要单独修改跨 fiber 生命周期;本日志保留既有销毁机制。

延后发布或传播记录错误。 延后发布让同步 hub 订阅者可能先记录完成通知、后记录产生通知的结算事实。starter 成功后抛错可能使调用方拿不到已运行 job 的 id。同步并隔离记录错误既保留因果顺序,也不会放弃执行。

Consequences

应用销毁可能在最后一条 job 结算写入存储前停止 Session 路由或关闭写句柄。因此正常的一次性运行结束、信号关停和进程硬退出都可能留下不完整历史。本次变更不强制跨 fiber 的销毁顺序,也不虚构缺失的终态记录。

日志与两种 SDK 暴露执行区间,不改变工具 schema、输出游标或 job 调度。模型侧控制和通知使用唯一的 JobId;有界通知无法容纳完整 id 时指向 job_list。区间从注册提交延续到注册表结算,包含这两个时刻之间的生产方工作与清理;它既不是 CPU 时间,也不是可直接相加的 rollout 延迟指标。追加失败也可能留下不完整历史。持久化事实不会恢复外部进程或其输出环。

定向测试覆盖原始时间戳、嵌套调用标识、输出保留、延后取消、生产方拒绝、强制销毁、较晚完成、唯一控制 id、因果发布顺序,以及记录错误隔离。SDK 快照覆盖持久化与协议投影。