diff --git a/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.i18n.yaml index 78fba181dc..735730fd60 100644 --- a/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.i18n.yaml @@ -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/architecture/2026-07-26-job-registry-seam.md -2026-07-26-job-registry-seam.md: b48a0896b7db202b5d7439500f1fa4e54cee97e3 -2026-07-26-job-registry-seam.zh.md: 0b800430fdf4f77802e6203e948b9defea881d4f +2026-07-26-job-registry-seam.md: 52676050bd3adb30923d2640ea7a4cb08746b260 +2026-07-26-job-registry-seam.zh.md: ab86370dcb5adcfad0ca7e97a24e306470679999 diff --git a/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.md b/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.md index b48a0896b7..52676050bd 100644 --- a/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.md +++ b/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.md @@ -12,7 +12,7 @@ The [background-job runtime](2026-06-20-generic-long-running-tool-runtime.md) sh `jobs/` is now a three-package capability family in the bash-trio shape: -- **`@deepseek-ai/dsh-jobs` (Service Definition)** — the abstract `JobRegistry extends Service` owning `ctx.jobs`, the contract (`start`, the caller-bound `visibleTo` view with `list`, `get`, `read`, `readAt`, `kill`, and `wait`, the `events` stream, and `attachController`), all vocabulary types (`JobId`, `JobKindMap`, `JobSpec`, `JobHandle`, `JobHooks`, `JobOutcome`, `JobView`, `JobRead`, `JobEvent`), and the event-protocol invariant companion. The class-level JSDoc states the semantics every Service Provider owes: registrations outlive producer and controller fibers, owned access is session-fenced, settlement is first-wins with contained listeners, and `start` refuses work while no attached job controller serves the spec's owner (controllers and event subscriptions are scope-layered, so one process-wide registry answers both questions per owner). +- **`@deepseek-ai/dsh-jobs` (Service Definition)** — the abstract `JobRegistry extends Service` owning `ctx.jobs`, the contract (`start`, the caller-bound `forCaller` view with `list`, `get`, `read`, `readAt`, `kill`, and `wait`, the `events` stream, and `attachController`), all vocabulary types (`JobId`, `JobKindMap`, `JobSpec`, `JobHandle`, `JobHooks`, `JobOutcome`, `JobView`, `JobRead`, `JobEvent`), and the event-protocol invariant companion. The class-level JSDoc states the semantics every Service Provider owes: registrations outlive producer and controller fibers, owned access is session-fenced, settlement is first-wins with contained listeners, and `start` refuses work while no attached job controller serves the spec's owner (controllers and event subscriptions are scope-layered, so one process-wide registry answers both questions per owner). - **`@deepseek-ai/dsh-jobs-local` (Service Provider)** — `LocalJobRegistry`, the process-local registry: the in-memory store, per-kind id counters, waiter bookkeeping, `TASK_WAIT_TIMEOUT` deadline code, owner-cleanup effects, force-fail teardown, and the default-10 configurable admission policy. Admission derives `running` plus `stopping` capacity from the same records per exact owner, with one unowned bucket; it adds no public count or second state owner. The `dsh-timeout` dependency and Schemastery-owned provider config live here; the Service Definition package has no provider dependencies. - **`@deepseek-ai/dsh-tool-jobs` (Consumer)** — unchanged; it injects `'jobs'` and never imports provider types. diff --git a/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.zh.md b/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.zh.md index 0b800430fd..ab86370dcb 100644 --- a/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-26-job-registry-seam.zh.md @@ -12,7 +12,7 @@ Status: implemented `jobs/` 如今是一个 bash 三件套形态的三包能力家族: -- **`@deepseek-ai/dsh-jobs`(Service Definition)**——抽象的 `JobRegistry extends Service`,拥有 `ctx.jobs`、约定(`start`、绑定调用方的 `visibleTo` 视图及其 `list`、`get`、`read`、`readAt`、`kill`、`wait`、`events` 事件流,以及 `attachController`)、全部词汇类型(`JobId`、`JobKindMap`、`JobSpec`、`JobHandle`、`JobHooks`、`JobOutcome`、`JobView`、`JobRead`、`JobEvent`),以及事件协议不变式配套插件。类级 JSDoc 陈述了每个 Service Provider 都必须兑现的语义:注册的存续期长于生产方与控制器的 fiber,有所有者的访问以会话为界,结算遵循首次结果优先且监听器错误被隔离,并且当没有任何已附加的任务控制器服务于 spec 的所有者时 `start` 拒绝启动工作(控制器与事件订阅按 scope 分层,因此一个进程级注册表能逐所有者地回答这两个问题)。 +- **`@deepseek-ai/dsh-jobs`(Service Definition)**——抽象的 `JobRegistry extends Service`,拥有 `ctx.jobs`、约定(`start`、绑定调用方的 `forCaller` 视图及其 `list`、`get`、`read`、`readAt`、`kill`、`wait`、`events` 事件流,以及 `attachController`)、全部词汇类型(`JobId`、`JobKindMap`、`JobSpec`、`JobHandle`、`JobHooks`、`JobOutcome`、`JobView`、`JobRead`、`JobEvent`),以及事件协议不变式配套插件。类级 JSDoc 陈述了每个 Service Provider 都必须兑现的语义:注册的存续期长于生产方与控制器的 fiber,有所有者的访问以会话为界,结算遵循首次结果优先且监听器错误被隔离,并且当没有任何已附加的任务控制器服务于 spec 的所有者时 `start` 拒绝启动工作(控制器与事件订阅按 scope 分层,因此一个进程级注册表能逐所有者地回答这两个问题)。 - **`@deepseek-ai/dsh-jobs-local`(Service Provider)**——`LocalJobRegistry`,即进程内注册表:内存存储、按 kind 划分的 id 计数器、等待方簿记、`TASK_WAIT_TIMEOUT` deadline 代码、所有者清理 effect、强制失败的拆除,以及默认值为 10 且可配置的准入策略。准入从同一组记录中按确切 owner 派生 `running` 加 `stopping` 容量,并为无 owner 任务使用一个共享桶;它不新增公开计数或第二个状态 owner。`dsh-timeout` 依赖与由 Schemastery 管理的 Service Provider 配置都位于此包;Service Definition 包不含任何提供方依赖。 - **`@deepseek-ai/dsh-tool-jobs`(Consumer)**——保持不变;它注入 `'jobs'`,从不导入提供方类型。 diff --git a/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.i18n.yaml index 491a988b1f..845a12ee84 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.i18n.yaml @@ -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/architecture/2026-09-01-jobs-absorb-activity-record.md -2026-09-01-jobs-absorb-activity-record.md: a1da1ba6f47dd0f5d557542d6b9668d6723fd683 -2026-09-01-jobs-absorb-activity-record.zh.md: fd735f1d9764dc83c0469209e241d47176584f5a +2026-09-01-jobs-absorb-activity-record.md: 3e39d62a99283529f7a867a808f97cd46f26afcb +2026-09-01-jobs-absorb-activity-record.zh.md: a2904d01712213eff7f35075d732c3b318f04092 diff --git a/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.md b/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.md index a1da1ba6f4..3e39d62a99 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.md +++ b/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.md @@ -2,7 +2,7 @@ Status: implemented -Superseded: the record declaration (`JobStart.record`, `RecordingJob`, `readRecord`, `pumpJobOutput`) and the split wire (`SessionJob.record` on the session control stream, `ctx.jobOutput`) described below were consolidated into one output ring, `VisibleJobs`, and `ctx.jobs` — see [the jobs seam consolidation](2026-09-03-jobs-seam-consolidation.md). The argument for absorbing the standalone activity seam into `ctx.jobs` still holds. +Superseded: the record declaration (`JobStart.record`, `RecordingJob`, `readRecord`, `pumpJobOutput`) and the split wire (`SessionJob.record` on the session control stream, `ctx.jobOutput`) described below were consolidated into one output ring, `CallerJobs`, and `ctx.jobs` — see [the jobs seam consolidation](2026-09-03-jobs-seam-consolidation.md). The argument for absorbing the standalone activity seam into `ctx.jobs` still holds. English | [中文](2026-09-01-jobs-absorb-activity-record.zh.md) diff --git a/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.zh.md b/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.zh.md index fd735f1d97..a2904d0171 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-jobs-absorb-activity-record.zh.md @@ -2,7 +2,7 @@ Status: implemented -Superseded:下文描述的 record 声明(`JobStart.record`、`RecordingJob`、`readRecord`、`pumpJobOutput`)与拆分的线路(会话控制流上的 `SessionJob.record`、`ctx.jobOutput`)已收敛为一个输出环、`VisibleJobs` 与 `ctx.jobs`——见 [jobs seam 收敛](2026-09-03-jobs-seam-consolidation.zh.md)。把独立 activity seam 吸收进 `ctx.jobs` 的论证仍然成立。 +Superseded:下文描述的 record 声明(`JobStart.record`、`RecordingJob`、`readRecord`、`pumpJobOutput`)与拆分的线路(会话控制流上的 `SessionJob.record`、`ctx.jobOutput`)已收敛为一个输出环、`CallerJobs` 与 `ctx.jobs`——见 [jobs seam 收敛](2026-09-03-jobs-seam-consolidation.zh.md)。把独立 activity seam 吸收进 `ctx.jobs` 的论证仍然成立。 [English](2026-09-01-jobs-absorb-activity-record.md) | 中文 diff --git a/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.i18n.yaml index 72b568321b..8d42dba2b3 100644 --- a/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.i18n.yaml @@ -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/architecture/2026-09-03-jobs-seam-consolidation.md -2026-09-03-jobs-seam-consolidation.md: 0524870826d9721897b6d1b568f49f2d5da6cd1b -2026-09-03-jobs-seam-consolidation.zh.md: bdc74d820832635bab5fe805b9d0fe68112457d1 +2026-09-03-jobs-seam-consolidation.md: 2b7f9c5cab7bba20c50dded9d361d8701e0ad792 +2026-09-03-jobs-seam-consolidation.zh.md: 169bb52a2fe512e7e94911e42e0c7981c5fa7273 diff --git a/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.md b/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.md index 0524870826..2b7f9c5cab 100644 --- a/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.md +++ b/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.md @@ -11,10 +11,10 @@ The job seam had grown by accretion. Producers declared their output twice — a ## Decision - **One ring per job.** `JobSpec` replaces `JobStart`: a spec names optional pull `output` sources (`JobOutputSource`, the subprocess `readFrom` family) that the registry pumps at its own cadence (`pumpPollMs` on `dsh-jobs-local`), and every producer receives a `JobHandle` whose `append` pushes chunks into the same ring. The model's consuming read and every observer's absolute-offset read serve the same bytes; `readOutput` and the `record` discriminator are gone. Shell producers hand the registry their `observed` offset readers and fold sandbox facts into the terminal `detail`; the terminal producer adapts its consuming send reader through a byte-counting source; the subagent returns its answer as `JobOutcome.result`, which the first read after settlement carries once. -- **Two cursors, one storage.** `VisibleJobs.read(id)` is the stateful model cursor; `VisibleJobs.readAt(id, from)` is the stateless observer read. Model reads exclude `log` chunks (producer narration for observers) and render stdout, then one `[stderr]` section, exactly as the shell tools always did. +- **Two cursors, one storage.** `CallerJobs.read(id)` is the stateful model cursor; `CallerJobs.readAt(id, from)` is the stateless observer read. Model reads exclude `log` chunks (producer narration for observers) and render stdout, then one `[stderr]` section, exactly as the shell tools always did. - **`updateProgress` replaces `updateDetail`.** `JobView.progress` is the live line and is cleared at settlement; `JobView.detail` is the terminal reason, with the model's `job_kill` reason merged in. - **One projection.** `JobView` (client-safe leaf `@deepseek-ai/dsh-jobs/view`) is what the model tools, the browser roster, and the observation frames consume; `owner` is the session id, never the `Agent`. -- **One caller-bound view.** `JobRegistry.visibleTo(caller?)` binds a session once and returns `list`, `get`, `read`, `readAt`, `kill`, and `wait`; every method throws for an id outside the caller's set without distinguishing unknown from foreign. +- **One caller-bound view.** `JobRegistry.forCaller(caller)` binds a session once and returns `list`, `get`, `read`, `readAt`, `kill`, and `wait`; every method throws for an id outside the caller's set without distinguishing unknown from foreign. - **One event stream.** `events.subscribe(filter, listener)` with `{ owner }`, `{ owners: 'scope' }`, or `{ owners: 'all' }` replaces the three listener families. `settled` names its cause (`producer`, `kill`, `teardown`); `output` carries only the id and the new total. - **The notice ledger moves to `dsh-tool-jobs`.** The registry no longer tracks `reported`. The tool claims a job when a wait starts (and withdraws on timeout or abort) and when `job_kill` is accepted; the settlement event drops the entry, teardown settlements and unowned jobs are skipped, and the destination is the agent registered for the owner session at settlement. - **The roster leaves the session control stream.** `dsh-api-job-controller` owns `job.rows({ sessionId })` — whole-set frames after each coalesced burst of lifecycle commits, never per append — beside `job.observe`, and its client installs `ctx.jobs` (`watchRows`, `observe`, one snapshot). `dsh-api-session-controller` carries queues and projections only; `ui-jobs` watches its session's roster while mounted, and a row is expandable while live or once it left retained output behind. diff --git a/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.zh.md b/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.zh.md index bdc74d8208..169bb52a2f 100644 --- a/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-03-jobs-seam-consolidation.zh.md @@ -11,10 +11,10 @@ job seam 是逐步堆出来的。生产者要把输出声明两次——模型 ## 决策 - **每个 job 一个环。** `JobSpec` 取代 `JobStart`:spec 声明可选的拉取式 `output` 源(`JobOutputSource`,子进程的 `readFrom` 家族),由注册表按自己的节奏(`dsh-jobs-local` 的 `pumpPollMs`)泵送;每个生产者都收到一个 `JobHandle`,其 `append` 把块推进同一个环。模型的消耗式读取与每个观察者的绝对偏移读取服务同一批字节;`readOutput` 与 `record` 判别字段被移除。shell 生产者把它们的 `observed` 偏移读取器交给注册表,并把沙箱事实折进终态 `detail`;terminal 生产者通过一个计字节的源适配其消耗式发送读取器;subagent 把回答作为 `JobOutcome.result` 返回,结算后的第一次读取携带它一次。 -- **两个游标,一份存储。** `VisibleJobs.read(id)` 是有状态的模型游标;`VisibleJobs.readAt(id, from)` 是无状态的观察者读取。模型读取排除 `log` 块(给观察者的生产者叙述),并渲染 stdout,再接一段 `[stderr]`,与 shell 工具一贯的做法完全一致。 +- **两个游标,一份存储。** `CallerJobs.read(id)` 是有状态的模型游标;`CallerJobs.readAt(id, from)` 是无状态的观察者读取。模型读取排除 `log` 块(给观察者的生产者叙述),并渲染 stdout,再接一段 `[stderr]`,与 shell 工具一贯的做法完全一致。 - **`updateProgress` 取代 `updateDetail`。** `JobView.progress` 是实时行,结算时清除;`JobView.detail` 是终态原因,合并了模型 `job_kill` 给出的原因。 - **一个投影。** `JobView`(客户端安全叶子 `@deepseek-ai/dsh-jobs/view`)是模型工具、浏览器名册与观测帧共同消费的形状;`owner` 是会话 id,从不是 `Agent`。 -- **一个绑定调用方的视图。** `JobRegistry.visibleTo(caller?)` 绑定一次会话并返回 `list`、`get`、`read`、`readAt`、`kill` 与 `wait`;每个方法对调用方集合之外的 id 抛出,不区分未知与他人。 +- **一个绑定调用方的视图。** `JobRegistry.forCaller(caller)` 绑定一次会话并返回 `list`、`get`、`read`、`readAt`、`kill` 与 `wait`;每个方法对调用方集合之外的 id 抛出,不区分未知与他人。 - **一条事件流。** 带 `{ owner }`、`{ owners: 'scope' }` 或 `{ owners: 'all' }` 过滤的 `events.subscribe(filter, listener)` 取代三个监听器家族。`settled` 标出原因(`producer`、`kill`、`teardown`);`output` 只携带 id 与新的 total。 - **播报台账移到 `dsh-tool-jobs`。** 注册表不再跟踪 `reported`。工具在等待开始时认领 job(超时或中止则撤回),在 `job_kill` 被接受时也认领;结算事件删除条目,teardown 结算与无主 job 被跳过,投递目标是结算时登记在拥有者会话下的 agent。 - **名册离开会话控制流。** `dsh-api-job-controller` 在 `job.observe` 旁拥有 `job.rows({ sessionId })`——每轮聚合过的生命周期提交之后发一次整集帧,从不按追加发——其客户端安装 `ctx.jobs`(`watchRows`、`observe`、一份快照)。`dsh-api-session-controller` 只承载队列与投影;`ui-jobs` 在挂载期间关注其会话的名册,行在 job 进行中或留有保留输出时可展开。 diff --git a/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts b/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts index 25a7292e45..39cc9f95f7 100644 --- a/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts @@ -222,7 +222,7 @@ describe('PTC mode typed values: keyless real-worker contracts', () => { return await tools.bash({ command: 'sleep 10', description: 'Must never start', run_in_background: true }); `, pre.signal) expect(preResult.isError).toBe(true) - expect(ctx.jobs.visibleTo().list()).toEqual([]) + expect(ctx.jobs.forCaller(undefined).list()).toEqual([]) const afterPublication = new AbortController() const running = runCode(ctx, ` @@ -230,14 +230,14 @@ describe('PTC mode typed values: keyless real-worker contracts', () => { console.log(started.jobId); await new Promise(() => {}); `, afterPublication.signal) - for (let attempt = 0; attempt < 100 && ctx.jobs.visibleTo().list().length === 0; attempt++) { + for (let attempt = 0; attempt < 100 && ctx.jobs.forCaller(undefined).list().length === 0; attempt++) { await new Promise(resolve => setTimeout(resolve, 10)) } - const job = ctx.jobs.visibleTo().list()[0] + const job = ctx.jobs.forCaller(undefined).list()[0] expect(job).toMatchObject({ id: 'bash-1', status: 'running' }) afterPublication.abort('outer-call-cancelled') expect((await running).isError).toBe(true) - expect(ctx.jobs.visibleTo().list()[0]).toMatchObject({ id: job!.id, status: 'running' }) + expect(ctx.jobs.forCaller(undefined).list()[0]).toMatchObject({ id: job!.id, status: 'running' }) const killed = completion(await runCode(ctx, ` return await tools.job_kill({ job_id: ${JSON.stringify(job!.id)}, reason: 'test owns cancellation' }); @@ -261,7 +261,7 @@ describe('PTC mode typed values: keyless real-worker contracts', () => { const result = await pending expect(result.isError).toBe(true) expect(Date.now() - startedAt).toBeLessThan(5_000) - expect(ctx.jobs.visibleTo().list()).toEqual([]) + expect(ctx.jobs.forCaller(undefined).list()).toEqual([]) }, 15_000) it('uses versioned Cordis DTO ids directly for running and pending Plugins, then confirms removal', async () => { diff --git a/apps/web/tests/background-job-list.e2e.ts b/apps/web/tests/background-job-list.e2e.ts index 10007b9029..00d705ff61 100644 --- a/apps/web/tests/background-job-list.e2e.ts +++ b/apps/web/tests/background-job-list.e2e.ts @@ -111,7 +111,7 @@ describe.skipIf(MODE === 'record')('web e2e: background job list', () => { expect(tripwire.warnings).toEqual([]) phase = 'settled' - expect(scaffold.ctx.jobs.visibleTo(agent.id).kill(jobId, { reason: 'web e2e cancellation' })).toBe('requested') + expect(scaffold.ctx.jobs.forCaller(agent.id).kill(jobId, { reason: 'web e2e cancellation' })).toBe('requested') const idle = page.getByRole('button', { name: '1 background job', exact: true }) await idle.waitFor({ timeout: 20_000 }) diff --git a/apps/web/tests/live-job-stream.e2e.ts b/apps/web/tests/live-job-stream.e2e.ts index 7cb8db27c0..5f221b1a20 100644 --- a/apps/web/tests/live-job-stream.e2e.ts +++ b/apps/web/tests/live-job-stream.e2e.ts @@ -116,7 +116,7 @@ describe.skipIf(MODE === 'record')('web e2e: live job stream', () => { expect(tripwire.warnings).toEqual([]) phase = 'settled' - expect(scaffold.ctx.jobs.visibleTo(agent.id).kill(jobId, { reason: 'web e2e cancellation' })).toBe('requested') + expect(scaffold.ctx.jobs.forCaller(agent.id).kill(jobId, { reason: 'web e2e cancellation' })).toBe('requested') // Settlement arrives on the observation stream itself, so the open panel // flips without any further interaction; the roster trigger follows. diff --git a/docs/subsystems/jobs.i18n.yaml b/docs/subsystems/jobs.i18n.yaml index 4310e9b6ef..fefccc7a23 100644 --- a/docs/subsystems/jobs.i18n.yaml +++ b/docs/subsystems/jobs.i18n.yaml @@ -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 docs/subsystems/jobs.md -jobs.md: d312ed3d49b951a1a03e29b8528ec8acb47400d4 -jobs.zh.md: ec303b0f3e3f0e0d77da60fdc3719e8b2ffeb94e +jobs.md: 2123e1d7896ab29af734becee0e9352a4b2dbaeb +jobs.zh.md: c09caaef7f3f6f75cd836b7faef2f0796841c386 diff --git a/docs/subsystems/jobs.md b/docs/subsystems/jobs.md index d312ed3d49..2123e1d789 100644 --- a/docs/subsystems/jobs.md +++ b/docs/subsystems/jobs.md @@ -122,14 +122,14 @@ interface JobOutcome { status: 'completed' | 'killed' | 'failed' /** * Terminal reason rendered into status lines (`exit code: 3`, `max-tokens`). - * When the job settles `killed` after a {@link VisibleJobs.kill} with a + * When the job settles `killed` after a {@link CallerJobs.kill} with a * reason, the registry appends that reason. */ detail?: string /** * Return value for jobs whose result is a value rather than a stream (a * workflow's rendered result, a subagent's report). The output ring carries - * the stream; this is handed out once by the model's next {@link VisibleJobs.read}. + * the stream; this is handed out once by the model's next {@link CallerJobs.read}. */ result?: string } @@ -172,7 +172,7 @@ interface JobSourceRead { ## The output ring -Every job owns one bounded ring. Pull sources are pumped into it and `JobHandle.append` pushes land whole; the model consumes the ring through a registry-kept cursor (`VisibleJobs.read`), any number of observers read it at absolute byte offsets (`VisibleJobs.readAt`), and neither disturbs the other. `JobChannel` labels `stdout`, `stderr`, and `log`; `log` is producer narration that reaches observers only, never the model's consuming read. Settlement ends the stream and trims retention to the settled cap — the ring has no separate lifecycle. Browsers reach the roster and the ring through `job.rows` and `job.observe`, the Remote streams of [`dsh-api-job-controller`](../../packages/api/job-controller/README.md), whose frames are listed under its Cordis API section below. +Every job owns one bounded ring. Pull sources are pumped into it and `JobHandle.append` pushes land whole; the model consumes the ring through a registry-kept cursor (`CallerJobs.read`), any number of observers read it at absolute byte offsets (`CallerJobs.readAt`), and neither disturbs the other. `JobChannel` labels `stdout`, `stderr`, and `log`; `log` is producer narration that reaches observers only, never the model's consuming read. Settlement ends the stream and trims retention to the settled cap — the ring has no separate lifecycle. Browsers reach the roster and the ring through `job.rows` and `job.observe`, the Remote streams of [`dsh-api-job-controller`](../../packages/api/job-controller/README.md), whose frames are listed under its Cordis API section below. ```ts type-equiv /** One chunk of a job's output ring: absolute offset, text, and its provenance. */ @@ -191,7 +191,7 @@ interface JobChunk { ``` ```ts type-equiv -/** Result of one non-consuming {@link VisibleJobs.readAt}. */ +/** Result of one non-consuming {@link CallerJobs.readAt}. */ interface JobOutputRead { /** Retained chunks overlapping `[from, total)`, in offset order. */ chunks: readonly JobChunk[] @@ -251,10 +251,10 @@ interface JobView { } ``` -`JobRegistry.visibleTo(caller)` binds one caller's identity and returns its operations; a caller sees its own jobs and every unowned job, and every operation throws for an id outside that set. +`JobRegistry.forCaller(caller)` binds one caller's identity and returns its operations; a caller sees its own jobs and every unowned job, and every operation throws for an id outside that set. ```ts type-equiv -/** Output and post-read state returned by the consuming {@link VisibleJobs.read}. */ +/** Output and post-read state returned by the consuming {@link CallerJobs.read}. */ interface JobRead { /** Ring chunks appended since the model cursor, in offset order; every channel included. */ chunks: readonly JobChunk[] @@ -269,11 +269,11 @@ interface JobRead { ```ts type-equiv /** - * The operations one caller may perform, bound by {@link JobRegistry.visibleTo}. + * The operations one caller may perform, bound by {@link JobRegistry.forCaller}. * A caller sees its own jobs and every unowned job; every method throws for * an id outside that set, without distinguishing unknown from foreign. */ -interface VisibleJobs { +interface CallerJobs { /** * List the visible jobs in registration order. * @returns fresh projections. @@ -334,7 +334,7 @@ The registry announces every commit through one filtered stream. Lifecycle event /** * One lifecycle or output event. Lifecycle events carry the job's projection * after the commit they announce; `output` carries only the id and the new - * total, so an observer schedules a {@link VisibleJobs.readAt} from its own + * total, so an observer schedules a {@link CallerJobs.readAt} from its own * cursor and the registry never pushes payloads. */ type JobEvent = @@ -373,7 +373,7 @@ type JobEventFilter = ## Service behavior -The abstract [`JobRegistry`](../../packages/jobs/jobs/src/index.ts) Service Definition specifies atomic `start`, caller-bound `visibleTo` (`list`, `get`, the consuming `read`, the non-consuming `readAt`, `kill`, and bounded `wait`), the filtered `events` stream, and `attachController`; [`LocalJobRegistry`](../../packages/jobs/jobs-local/src/index.ts) is the process-local Service Provider. Authorization compares owner sessions; owner cleanup and admission use the live `Agent` registered under the owner session when the job starts. The local provider's positive-safe-integer `maxConcurrentJobsPerOwner` config defaults to `10` and counts `running` plus `stopping` records per exact owner, with one shared bucket for unowned jobs; terminal producer settlement releases capacity; `retainBytes` (default 262144) and `settledRetainBytes` (default 16384) bound each ring's live and settled retention, and `pumpPollMs` (default 150) is the pull cadence. See [`dsh-jobs`](../../packages/jobs/jobs/README.md) for the Service Definition contract, [`dsh-jobs-local`](../../packages/jobs/jobs-local/README.md) for the registry lifecycle and admission policy, and [`dsh-tool-jobs`](../../packages/jobs/tool-jobs/README.md) for the model-facing Consumer. +The abstract [`JobRegistry`](../../packages/jobs/jobs/src/index.ts) Service Definition specifies atomic `start`, caller-bound `forCaller` (`list`, `get`, the consuming `read`, the non-consuming `readAt`, `kill`, and bounded `wait`), the filtered `events` stream, and `attachController`; [`LocalJobRegistry`](../../packages/jobs/jobs-local/src/index.ts) is the process-local Service Provider. Authorization compares owner sessions; owner cleanup and admission use the live `Agent` registered under the owner session when the job starts. The local provider's positive-safe-integer `maxConcurrentJobsPerOwner` config defaults to `10` and counts `running` plus `stopping` records per exact owner, with one shared bucket for unowned jobs; terminal producer settlement releases capacity; `retainBytes` (default 262144) and `settledRetainBytes` (default 16384) bound each ring's live and settled retention, and `pumpPollMs` (default 150) is the pull cadence. See [`dsh-jobs`](../../packages/jobs/jobs/README.md) for the Service Definition contract, [`dsh-jobs-local`](../../packages/jobs/jobs-local/README.md) for the registry lifecycle and admission policy, and [`dsh-tool-jobs`](../../packages/jobs/tool-jobs/README.md) for the model-facing Consumer. @@ -443,13 +443,13 @@ abstract start(spec: JobSpec): JobId /** * Bind the caller's identity once and return its operations. A caller sees - * its own jobs and every unowned job; a caller-less view sees unowned jobs - * only. The view resolves visibility on every call, so it stays valid as - * jobs come and go. - * @param caller - the reading session, or undefined for an anonymous caller. + * its own jobs and every unowned job; an anonymous caller sees unowned jobs + * only. The operations resolve visibility on every call, so they stay valid + * as jobs come and go. + * @param caller - the calling session, or `undefined` for an anonymous caller. * @returns the operations available to that caller. */ -abstract visibleTo(caller?: SessionId): VisibleJobs +abstract forCaller(caller: SessionId | undefined): CallerJobs /** * Attach an effect-scoped controller that can read and stop jobs. It serves the diff --git a/docs/subsystems/jobs.zh.md b/docs/subsystems/jobs.zh.md index ec303b0f3e..c09caaef7f 100644 --- a/docs/subsystems/jobs.zh.md +++ b/docs/subsystems/jobs.zh.md @@ -122,14 +122,14 @@ interface JobOutcome { status: 'completed' | 'killed' | 'failed' /** * Terminal reason rendered into status lines (`exit code: 3`, `max-tokens`). - * When the job settles `killed` after a {@link VisibleJobs.kill} with a + * When the job settles `killed` after a {@link CallerJobs.kill} with a * reason, the registry appends that reason. */ detail?: string /** * Return value for jobs whose result is a value rather than a stream (a * workflow's rendered result, a subagent's report). The output ring carries - * the stream; this is handed out once by the model's next {@link VisibleJobs.read}. + * the stream; this is handed out once by the model's next {@link CallerJobs.read}. */ result?: string } @@ -172,7 +172,7 @@ interface JobSourceRead { ## 输出环 -每个 job 拥有一个有界的环。拉取源被泵入其中,`JobHandle.append` 的推送整块落地;模型通过注册表保管的游标(`VisibleJobs.read`)消耗该环,任意数量的观察者按绝对字节偏移读取它(`VisibleJobs.readAt`),二者互不干扰。`JobChannel` 标记 `stdout`、`stderr` 与 `log`;`log` 是只到达观察者、从不进入模型消耗式读取的生产方叙述。结算即封流并把保留量裁剪到结算上限——环没有独立的生命周期。浏览器通过 [`dsh-api-job-controller`](../../packages/api/job-controller/README.zh.md) 的 Remote 流 `job.rows` 与 `job.observe` 触达名册与环,其帧列于下文的 Cordis API 一节。 +每个 job 拥有一个有界的环。拉取源被泵入其中,`JobHandle.append` 的推送整块落地;模型通过注册表保管的游标(`CallerJobs.read`)消耗该环,任意数量的观察者按绝对字节偏移读取它(`CallerJobs.readAt`),二者互不干扰。`JobChannel` 标记 `stdout`、`stderr` 与 `log`;`log` 是只到达观察者、从不进入模型消耗式读取的生产方叙述。结算即封流并把保留量裁剪到结算上限——环没有独立的生命周期。浏览器通过 [`dsh-api-job-controller`](../../packages/api/job-controller/README.zh.md) 的 Remote 流 `job.rows` 与 `job.observe` 触达名册与环,其帧列于下文的 Cordis API 一节。 ```ts type-equiv /** One chunk of a job's output ring: absolute offset, text, and its provenance. */ @@ -191,7 +191,7 @@ interface JobChunk { ``` ```ts type-equiv -/** Result of one non-consuming {@link VisibleJobs.readAt}. */ +/** Result of one non-consuming {@link CallerJobs.readAt}. */ interface JobOutputRead { /** Retained chunks overlapping `[from, total)`, in offset order. */ chunks: readonly JobChunk[] @@ -251,10 +251,10 @@ interface JobView { } ``` -`JobRegistry.visibleTo(caller)` 绑定一个调用方的身份并返回其操作;调用方看得到自己的 job 与所有无主 job,每个操作对该集合之外的 id 抛出。 +`JobRegistry.forCaller(caller)` 绑定一个调用方的身份并返回其操作;调用方看得到自己的 job 与所有无主 job,每个操作对该集合之外的 id 抛出。 ```ts type-equiv -/** Output and post-read state returned by the consuming {@link VisibleJobs.read}. */ +/** Output and post-read state returned by the consuming {@link CallerJobs.read}. */ interface JobRead { /** Ring chunks appended since the model cursor, in offset order; every channel included. */ chunks: readonly JobChunk[] @@ -269,11 +269,11 @@ interface JobRead { ```ts type-equiv /** - * The operations one caller may perform, bound by {@link JobRegistry.visibleTo}. + * The operations one caller may perform, bound by {@link JobRegistry.forCaller}. * A caller sees its own jobs and every unowned job; every method throws for * an id outside that set, without distinguishing unknown from foreign. */ -interface VisibleJobs { +interface CallerJobs { /** * List the visible jobs in registration order. * @returns fresh projections. @@ -334,7 +334,7 @@ interface VisibleJobs { /** * One lifecycle or output event. Lifecycle events carry the job's projection * after the commit they announce; `output` carries only the id and the new - * total, so an observer schedules a {@link VisibleJobs.readAt} from its own + * total, so an observer schedules a {@link CallerJobs.readAt} from its own * cursor and the registry never pushes payloads. */ type JobEvent = @@ -373,7 +373,7 @@ type JobEventFilter = ## 服务行为 -抽象的 [`JobRegistry`](../../packages/jobs/jobs/src/index.ts) Service Definition 规定了原子化的 `start`、绑定调用方的 `visibleTo`(`list`、`get`、消耗式 `read`、非消耗的 `readAt`、`kill` 与有界的 `wait`)、带过滤的 `events` 流,以及 `attachController`;[`LocalJobRegistry`](../../packages/jobs/jobs-local/src/index.ts) 是进程本地的 Service Provider。授权比较拥有者会话;拥有者清理与准入使用 job 启动时登记在该拥有者会话下的活体 `Agent`。本地提供方的正安全整数配置 `maxConcurrentJobsPerOwner` 默认为 `10`,按精确拥有者统计 `running` 加 `stopping` 记录,无主任务共享一个桶;生产方的终态结算释放容量;`retainBytes`(默认 262144)与 `settledRetainBytes`(默认 16384)约束每个环的运行期与结算后保留量,`pumpPollMs`(默认 150)是拉取节奏。参见 [`dsh-jobs`](../../packages/jobs/jobs/README.zh.md) 了解 Service Definition 约定,[`dsh-jobs-local`](../../packages/jobs/jobs-local/README.zh.md) 了解注册表生命周期与准入策略,[`dsh-tool-jobs`](../../packages/jobs/tool-jobs/README.zh.md) 了解面向模型的 Consumer。 +抽象的 [`JobRegistry`](../../packages/jobs/jobs/src/index.ts) Service Definition 规定了原子化的 `start`、绑定调用方的 `forCaller`(`list`、`get`、消耗式 `read`、非消耗的 `readAt`、`kill` 与有界的 `wait`)、带过滤的 `events` 流,以及 `attachController`;[`LocalJobRegistry`](../../packages/jobs/jobs-local/src/index.ts) 是进程本地的 Service Provider。授权比较拥有者会话;拥有者清理与准入使用 job 启动时登记在该拥有者会话下的活体 `Agent`。本地提供方的正安全整数配置 `maxConcurrentJobsPerOwner` 默认为 `10`,按精确拥有者统计 `running` 加 `stopping` 记录,无主任务共享一个桶;生产方的终态结算释放容量;`retainBytes`(默认 262144)与 `settledRetainBytes`(默认 16384)约束每个环的运行期与结算后保留量,`pumpPollMs`(默认 150)是拉取节奏。参见 [`dsh-jobs`](../../packages/jobs/jobs/README.zh.md) 了解 Service Definition 约定,[`dsh-jobs-local`](../../packages/jobs/jobs-local/README.zh.md) 了解注册表生命周期与准入策略,[`dsh-tool-jobs`](../../packages/jobs/tool-jobs/README.zh.md) 了解面向模型的 Consumer。 @@ -443,13 +443,13 @@ abstract start(spec: JobSpec): JobId /** * Bind the caller's identity once and return its operations. A caller sees - * its own jobs and every unowned job; a caller-less view sees unowned jobs - * only. The view resolves visibility on every call, so it stays valid as - * jobs come and go. - * @param caller - the reading session, or undefined for an anonymous caller. + * its own jobs and every unowned job; an anonymous caller sees unowned jobs + * only. The operations resolve visibility on every call, so they stay valid + * as jobs come and go. + * @param caller - the calling session, or `undefined` for an anonymous caller. * @returns the operations available to that caller. */ -abstract visibleTo(caller?: SessionId): VisibleJobs +abstract forCaller(caller: SessionId | undefined): CallerJobs /** * Attach an effect-scoped controller that can read and stop jobs. It serves the diff --git a/packages/api/job-controller/README.i18n.yaml b/packages/api/job-controller/README.i18n.yaml index 06c2debed0..2666639cbd 100644 --- a/packages/api/job-controller/README.i18n.yaml +++ b/packages/api/job-controller/README.i18n.yaml @@ -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/api/job-controller/README.md -README.md: f38d29b40caf57821d66ce6d8d39c6215014c7be -README.zh.md: 7a0fab218eb67ff6220cbbdff124555bc0b762c8 +README.md: 7ce3022b1266ef8634d90b2055df2a2bf9cee8a9 +README.zh.md: 71ea23dd3827ad25db4b1c5c1dcd16741b89e51f diff --git a/packages/api/job-controller/README.md b/packages/api/job-controller/README.md index f38d29b40c..7ce3022b12 100644 --- a/packages/api/job-controller/README.md +++ b/packages/api/job-controller/README.md @@ -22,7 +22,7 @@ English | [中文](README.zh.md) ## Use this package -The Host controller requires the job registry (`dsh-jobs-local` in the shipped compositions) and fails to load without it. `job.rows({ sessionId })` yields the session's visible set — its own jobs plus every unowned job — once on open and again after each coalesced burst of lifecycle commits (registration, progress, stopping, settlement, removal); output appends never refresh the roster, since a settled projection already carries the final byte count. `job.observe({ sessionId?, jobId, from? })` reads through `visibleTo(sessionId)` — an unowned job needs no session — and yields one `opened` anchor carrying the job projection, coalesced `output` frames, then one terminal `status` once the job has settled and its ring is drained, after which the stream closes normally; a removal announced mid-stream (the owner's teardown) closes it with the removed job's terminal projection. Both reads are non-consuming: the model's `job_output` cursor and notice state never observe them. +The Host controller requires the job registry (`dsh-jobs-local` in the shipped compositions) and fails to load without it. `job.rows({ sessionId })` yields the session's visible set — its own jobs plus every unowned job — once on open and again after each coalesced burst of lifecycle commits (registration, progress, stopping, settlement, removal); output appends never refresh the roster, since a settled projection already carries the final byte count. `job.observe({ sessionId?, jobId, from? })` reads through `forCaller(sessionId)` — an unowned job needs no session — and yields one `opened` anchor carrying the job projection, coalesced `output` frames, then one terminal `status` once the job has settled and its ring is drained, after which the stream closes normally; a removal announced mid-stream (the owner's teardown) closes it with the removed job's terminal projection. Both reads are non-consuming: the model's `job_output` cursor and notice state never observe them. The Client entry provides `ClientJobs` (`ctx.jobs`) over `ClientJobsModel`. `watchRows(sessionId)` keeps one roster stream open per watched session however many viewers hold it and drops the rows after the last release; a reconnect's first frame is already the whole truth. `observe(sessionId, jobId)` opens one Gateway stream per job however many viewers expand it, keeps a bounded render tail per job with `gapBefore` marking eviction or resume gaps, closes the view on the terminal frame or records a stream failure on it, and drops the view after the last viewer releases. The plugin resolves the Gateway stream factory and the `job` namespace while its own context is current, because stream (re)opens run on caller stacks whose dynamic context has not declared `remote.job`. @@ -52,7 +52,7 @@ No direct effect; observation reads never touch model requests. - The ring is a best-effort live preview, not a terminal transcript: producers' pull sources are copied per poll round, so two streams' writes inside one poll window land stdout first, and the client concatenates chunks regardless of `channel`. - Both streams are process-local: a Host restart loses every ring and roster; a resumed observation then anchors on an empty registry, and a re-opened roster starts empty. -- Per-session fencing is enforced by the registry's `visibleTo` read; the Remote layer itself serves any connected browser. +- Per-session fencing is enforced by the registry's `forCaller` read; the Remote layer itself serves any connected browser. ### Dev Note diff --git a/packages/api/job-controller/README.zh.md b/packages/api/job-controller/README.zh.md index 7a0fab218e..71ea23dd38 100644 --- a/packages/api/job-controller/README.zh.md +++ b/packages/api/job-controller/README.zh.md @@ -22,7 +22,7 @@ kind: "package-reference" ## 使用本包 -Host 控制器要求 job 注册表(已发布组合里是 `dsh-jobs-local`),缺少则不加载。`job.rows({ sessionId })` 产出该会话可见的集合——自己的 job 加上所有无主 job——打开时一次,之后每一轮聚合过的生命周期提交(注册、进度、停止中、结算、移除)后再一次;输出追加从不刷新名册,因为结算后的投影已经带着最终字节数。`job.observe({ sessionId?, jobId, from? })` 经 `visibleTo(sessionId)` 读取——无主 job 不需要 session——先产出一个携带 job 投影的 `opened` 锚帧,再是聚合的 `output` 帧,job 结算且环排干后产出一个终态 `status`,随后流正常关闭;流中若通告了移除(拥有者 teardown),则以被移除 job 的终态投影收尾。两种读取都是非消耗的:模型侧 `job_output` 游标与完成播报状态永远观察不到它们。 +Host 控制器要求 job 注册表(已发布组合里是 `dsh-jobs-local`),缺少则不加载。`job.rows({ sessionId })` 产出该会话可见的集合——自己的 job 加上所有无主 job——打开时一次,之后每一轮聚合过的生命周期提交(注册、进度、停止中、结算、移除)后再一次;输出追加从不刷新名册,因为结算后的投影已经带着最终字节数。`job.observe({ sessionId?, jobId, from? })` 经 `forCaller(sessionId)` 读取——无主 job 不需要 session——先产出一个携带 job 投影的 `opened` 锚帧,再是聚合的 `output` 帧,job 结算且环排干后产出一个终态 `status`,随后流正常关闭;流中若通告了移除(拥有者 teardown),则以被移除 job 的终态投影收尾。两种读取都是非消耗的:模型侧 `job_output` 游标与完成播报状态永远观察不到它们。 Client 入口在 `ClientJobsModel` 之上提供 `ClientJobs`(`ctx.jobs`)。`watchRows(sessionId)` 不论多少查看器持有都只为每个被关注的会话开一条名册流,最后一个释放后丢弃行;重连后的首帧已经是完整事实。`observe(sessionId, jobId)` 不论多少查看器展开同一 job 都只开一条 Gateway 流,按 job 保留有界的渲染尾部并用 `gapBefore` 标记淘汰或续读缺口,在终态帧上关闭视图或把流失败记到视图上,最后一个查看器释放后丢弃视图。插件在自己的上下文仍是当前上下文时解析 Gateway 流工厂与 `job` namespace,因为流的(重)开启跑在未声明 `remote.job` 的调用栈上。 @@ -52,7 +52,7 @@ Client 入口在 `ClientJobsModel` 之上提供 `ClientJobs`(`ctx.jobs`)。` - 环是尽力而为的实时预览,不是终端转录:生产者的拉取源按轮询轮次复制,同一轮询窗口内两条流的写入先 stdout 落地,客户端拼接 chunk 时也不区分 `channel`。 - 两条流都是进程本地的:Host 重启丢失全部环与名册,续读的观测随后锚定在空注册表上,重开的名册从空开始。 -- 按 session 的围栏由注册表的 `visibleTo` 读取强制;Remote 层本身服务任何已连接浏览器。 +- 按 session 的围栏由注册表的 `forCaller` 读取强制;Remote 层本身服务任何已连接浏览器。 ### 开发备注 diff --git a/packages/api/job-controller/src/observe.ts b/packages/api/job-controller/src/observe.ts index b2afe16e75..94a1891534 100644 --- a/packages/api/job-controller/src/observe.ts +++ b/packages/api/job-controller/src/observe.ts @@ -65,7 +65,7 @@ export async function* observeJobOutput( waiter.wake() }) try { - const visible = registry.visibleTo(request.sessionId) + const visible = registry.forCaller(request.sessionId) let job = visible.get(id) let cursor = request.from ?? job.output.earliest yield { type: 'opened', job, from: cursor } diff --git a/packages/api/job-controller/src/rows.ts b/packages/api/job-controller/src/rows.ts index 9076f12424..d3ae9d511c 100644 --- a/packages/api/job-controller/src/rows.ts +++ b/packages/api/job-controller/src/rows.ts @@ -39,7 +39,7 @@ export async function* streamJobRows( if (owner === undefined || owner === request.sessionId) waiter.wake() }) try { - const visible = registry.visibleTo(request.sessionId) + const visible = registry.forCaller(request.sessionId) yield { type: 'rows', jobs: visible.list() } while (true) { await waiter.wait(signal) diff --git a/packages/api/job-controller/tests/observe.host.spec.ts b/packages/api/job-controller/tests/observe.host.spec.ts index 51aa360436..89e56567a2 100644 --- a/packages/api/job-controller/tests/observe.host.spec.ts +++ b/packages/api/job-controller/tests/observe.host.spec.ts @@ -194,7 +194,7 @@ describe('observeJobOutput', () => { await done expect(frames.map(frame => frame.type)).toEqual(['opened', 'output', 'status']) expect(frames.at(-1)).toMatchObject({ type: 'status', job: { id: job.id, status: 'killed' } }) - expect(() => ctx.jobs.visibleTo(owner.id).get(job.id)).toThrow(/unknown job/) + expect(() => ctx.jobs.forCaller(owner.id).get(job.id)).toThrow(/unknown job/) }) it('stops cleanly on abort while waiting for output', async () => { diff --git a/packages/api/job-controller/tests/rows.host.spec.ts b/packages/api/job-controller/tests/rows.host.spec.ts index f246425605..6609ade58c 100644 --- a/packages/api/job-controller/tests/rows.host.spec.ts +++ b/packages/api/job-controller/tests/rows.host.spec.ts @@ -43,7 +43,7 @@ describe('streamJobRows', () => { job.progress('3/10') expect((await next(iterator))?.jobs[0]).toMatchObject({ progress: '3/10' }) - ctx.jobs.visibleTo(alice.id).kill(job.id, { reason: 'enough' }) + ctx.jobs.forCaller(alice.id).kill(job.id, { reason: 'enough' }) expect((await next(iterator))?.jobs[0]).toMatchObject({ status: 'stopping', progress: '3/10' }) startJob(ctx, { label: 'second', owner: alice.id }) expect((await next(iterator))?.jobs.map(entry => entry.label)).toEqual(['first', 'second']) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index f091275774..c685d8ce2d 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -1162,9 +1162,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'the registry-issued `-N` id.', }, { - signature: 'abstract visibleTo(caller?: SessionId): VisibleJobs', - description: 'Bind the caller\'s identity once and return its operations. A caller sees its own jobs and every unowned job; a caller-less view sees unowned jobs only. The view resolves visibility on every call, so it stays valid as jobs come and go.', - parameters: [{ name: 'caller', description: 'the reading session, or undefined for an anonymous caller.' }], + signature: 'abstract forCaller(caller: SessionId | undefined): CallerJobs', + description: 'Bind the caller\'s identity once and return its operations. A caller sees its own jobs and every unowned job; an anonymous caller sees unowned jobs only. The operations resolve visibility on every call, so they stay valid as jobs come and go.', + parameters: [{ name: 'caller', description: 'the calling session, or `undefined` for an anonymous caller.' }], returns: 'the operations available to that caller.', }, { @@ -3701,6 +3701,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'BrandedNumber', declaration: 'export type BrandedNumber = number & {\n readonly [BRAND]: B;\n};', }, + { + name: 'CallerJobs', + declaration: 'export interface CallerJobs {\n list(): JobView[];\n get(id: JobId): JobView;\n read(id: JobId): JobRead;\n readAt(id: JobId, from: number): JobOutputRead;\n kill(id: JobId, options?: JobKillOptions): \'requested\' | \'already-finished\';\n wait(id: JobId, timeoutMs: number, signal?: AbortSignal): Promise;\n}', + }, { name: 'ClientArtifactBaseline', declaration: 'export interface ClientArtifactBaseline {\n readonly path: string;\n readonly mtimeMs: number;\n readonly size: number;\n}', @@ -6165,10 +6169,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'VerifiedWebhookDelivery', declaration: 'export interface VerifiedWebhookDelivery {\n readonly kind: K;\n readonly source: WebhookSourceId;\n readonly deliveryId: WebhookDeliveryId;\n readonly event: WebhookEventOf;\n readonly receivedAt: number;\n}', }, - { - name: 'VisibleJobs', - declaration: 'export interface VisibleJobs {\n list(): JobView[];\n get(id: JobId): JobView;\n read(id: JobId): JobRead;\n readAt(id: JobId, from: number): JobOutputRead;\n kill(id: JobId, options?: JobKillOptions): \'requested\' | \'already-finished\';\n wait(id: JobId, timeoutMs: number, signal?: AbortSignal): Promise;\n}', - }, { name: 'WebBootBatch', declaration: 'export interface WebBootBatch {\n phase: WebBootBatchPhase;\n url: string;\n rev: string;\n entries: string[];\n}', diff --git a/packages/jobs/jobs-local/README.i18n.yaml b/packages/jobs/jobs-local/README.i18n.yaml index 27f7aa65ec..6e303f292c 100644 --- a/packages/jobs/jobs-local/README.i18n.yaml +++ b/packages/jobs/jobs-local/README.i18n.yaml @@ -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/jobs/jobs-local/README.md -README.md: da48df0917986b7ecc03ee83d2ab31b5b6e8fad7 -README.zh.md: f217341b3f89864b6fc80d9f76c968ac0dc3f72c +README.md: 00a07c328cbdc46d3dbeebcdddbbce37f1d67d7a +README.zh.md: b95b80d316819982dbd800abd0dced3495d08a6f diff --git a/packages/jobs/jobs-local/README.md b/packages/jobs/jobs-local/README.md index da48df0917..00a07c328c 100644 --- a/packages/jobs/jobs-local/README.md +++ b/packages/jobs/jobs-local/README.md @@ -95,7 +95,7 @@ This section explains the design decisions behind the registry and points at the ### Admission and settlement -`activeTaskCount` counts authoritative records per exact owner or in the shared unowned bucket. `settle` records the terminal outcome once (merging a recorded kill reason into a `killed` detail), clears the progress line, trims the ring to the settled cap (keeping every byte the model cursor has not consumed), resolves every waiter, then emits `settled` with its cause and the ring's final `output` signal. The cause is `kill` after `VisibleJobs.kill`, `teardown` after an owner or service cancel, and `producer` otherwise; `dsh-tool-jobs` uses it to skip notices nobody can read. +`activeTaskCount` counts authoritative records per exact owner or in the shared unowned bucket. `settle` records the terminal outcome once (merging a recorded kill reason into a `killed` detail), clears the progress line, trims the ring to the settled cap (keeping every byte the model cursor has not consumed), resolves every waiter, then emits `settled` with its cause and the ring's final `output` signal. The cause is `kill` after `CallerJobs.kill`, `teardown` after an owner or service cancel, and `producer` otherwise; `dsh-tool-jobs` uses it to skip notices nobody can read. ### Teardown diff --git a/packages/jobs/jobs-local/README.zh.md b/packages/jobs/jobs-local/README.zh.md index f217341b3f..b95b80d316 100644 --- a/packages/jobs/jobs-local/README.zh.md +++ b/packages/jobs/jobs-local/README.zh.md @@ -95,7 +95,7 @@ kind: "package-reference" ### 准入与结算 -`activeTaskCount` 按精确所有者或共享无主桶统计权威记录。`settle` 只记录一次终止结果(把记录下来的 kill 原因合并进 `killed` 的 detail),清除进度行,把环裁剪到结算保留量(保留模型游标尚未消费的全部字节),解析每个等待方,然后发出带原因的 `settled` 与环的最终 `output` 信号。原因在 `VisibleJobs.kill` 之后为 `kill`,在所有者或服务取消之后为 `teardown`,否则为 `producer`;`dsh-tool-jobs` 据此跳过没人能读的通知。 +`activeTaskCount` 按精确所有者或共享无主桶统计权威记录。`settle` 只记录一次终止结果(把记录下来的 kill 原因合并进 `killed` 的 detail),清除进度行,把环裁剪到结算保留量(保留模型游标尚未消费的全部字节),解析每个等待方,然后发出带原因的 `settled` 与环的最终 `output` 信号。原因在 `CallerJobs.kill` 之后为 `kill`,在所有者或服务取消之后为 `teardown`,否则为 `producer`;`dsh-tool-jobs` 据此跳过没人能读的通知。 ### 销毁 diff --git a/packages/jobs/jobs-local/src/index.ts b/packages/jobs/jobs-local/src/index.ts index 981be78b5a..b4cc567637 100644 --- a/packages/jobs/jobs-local/src/index.ts +++ b/packages/jobs/jobs-local/src/index.ts @@ -19,7 +19,7 @@ import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout' import { JobRegistry, JobId } from '@deepseek-ai/dsh-jobs' import type { JobAppendOptions, JobEvent, JobEvents, JobHandle, JobKillOptions, JobKind, JobOutcome, JobOutputRead, JobOutputSource, - JobRead, JobSettleCause, JobSpec, JobStatus, JobView, VisibleJobs, + JobRead, JobSettleCause, JobSpec, JobStatus, JobView, CallerJobs, } from '@deepseek-ai/dsh-jobs' import { JobEventHub, JobLayer } from './events.ts' import { startPump } from './pump.ts' @@ -83,7 +83,7 @@ interface TrackedJob { cancel: (reason?: string) => void status: JobStatus ring: OutputRing - /** The model's consuming cursor; {@link VisibleJobs.readAt} never moves it. */ + /** The model's consuming cursor; {@link CallerJobs.readAt} never moves it. */ modelCursor: number /** Whether the first post-settlement read already handed out `result`. */ resultDelivered: boolean @@ -94,7 +94,7 @@ interface TrackedJob { result: string | undefined startedAt: number finishedAt: number | undefined - /** Reason recorded by {@link VisibleJobs.kill}, merged into a `killed` settlement's detail. */ + /** Reason recorded by {@link CallerJobs.kill}, merged into a `killed` settlement's detail. */ killReason: string | undefined /** Set once a kill or teardown cancel ran; settlement reports it as the cause. */ settleCause: JobSettleCause | undefined @@ -292,7 +292,7 @@ export class LocalJobRegistry extends JobRegistry { return id } - visibleTo(caller?: SessionId): VisibleJobs { + forCaller(caller: SessionId | undefined): CallerJobs { const reach = (id: JobId): TrackedJob => { const job = this.expect(id) this.assertAccess(job, caller) diff --git a/packages/jobs/jobs-local/tests/jobs.spec.ts b/packages/jobs/jobs-local/tests/jobs.spec.ts index c93d764a36..cd0f1edd96 100644 --- a/packages/jobs/jobs-local/tests/jobs.spec.ts +++ b/packages/jobs/jobs-local/tests/jobs.spec.ts @@ -7,7 +7,7 @@ import { bindScopeParent, createScope, scopeOf } from '@deepseek-ai/dsh-scope' import type { ScopeKey } from '@deepseek-ai/dsh-scope' import { JobId } from '@deepseek-ai/dsh-jobs' import type { - JobEvent, JobEventFilter, JobHandle, JobHooks, JobKind, JobOutcome, JobOutputSource, JobSpec, JobView, VisibleJobs, + JobEvent, JobEventFilter, JobHandle, JobHooks, JobKind, JobOutcome, JobOutputSource, JobSpec, JobView, CallerJobs, } from '@deepseek-ai/dsh-jobs' import LocalJobRegistry, { type Config as JobsConfig } from '@deepseek-ai/dsh-jobs-local' @@ -112,8 +112,8 @@ async function harness(config: JobsConfig = {}) { } /** The caller-bound view for an agent, or the anonymous view. */ -function jobsOf(ctx: Context, agent?: Agent): VisibleJobs { - return ctx.jobs.visibleTo(agent?.id) +function jobsOf(ctx: Context, agent?: Agent): CallerJobs { + return ctx.jobs.forCaller(agent?.id) } /** Collect events matching `filter`; `types` narrows what is recorded. */ diff --git a/packages/jobs/jobs/README.i18n.yaml b/packages/jobs/jobs/README.i18n.yaml index 11407349e0..17df643c09 100644 --- a/packages/jobs/jobs/README.i18n.yaml +++ b/packages/jobs/jobs/README.i18n.yaml @@ -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/jobs/jobs/README.md -README.md: 491f082d8083e75a1149dbed33f6c697e2f785ac -README.zh.md: a15b4f7b458fde206bd548e02a6e8b91993ea0ba +README.md: cde6b84f4574224305915410de24ea57175125d7 +README.zh.md: 93f466076c629bb989248f2647bd7815f69525a4 diff --git a/packages/jobs/jobs/README.md b/packages/jobs/jobs/README.md index 491f082d80..cde6b84f45 100644 --- a/packages/jobs/jobs/README.md +++ b/packages/jobs/jobs/README.md @@ -77,14 +77,14 @@ This section explains the design decisions behind the contract and points at the | File | Role | |---|---| | [`src/index.ts`](src/index.ts) | Plugin entry: the abstract `JobRegistry` service and its contract | -| [`src/types.ts`](src/types.ts) | Shared vocabulary: `JobSpec`, `JobHandle`, `JobHooks`, `JobOutcome`, `VisibleJobs`, `JobEvent`, and the read results | +| [`src/types.ts`](src/types.ts) | Shared vocabulary: `JobSpec`, `JobHandle`, `JobHooks`, `JobOutcome`, `CallerJobs`, `JobEvent`, and the read results | | [`src/view.ts`](src/view.ts) | Client-safe leaf: `JobView`, `JobChunk`, `JobStatus`, and the merge-extensible `JobKindMap` | | [`src/brand.ts`](src/brand.ts) | `JobId` branded identifier, importable without the agent dependency | | [`src/invariant.ts`](src/invariant.ts) | Invariant companion: checks the announced event protocol per job (registered first, one settlement, removal last) and each announced projection against the registry's own read | ### Service operations -Every operation is a thin projection over the registered jobs, bound to one caller by `visibleTo`: `list` and `get` return fresh projections, `read` advances the model's cursor and hands out the producer's result once after settlement, `readAt` reads retained chunks at an absolute offset without consuming anything, `kill` invokes producer cancellation before changing status and records the reason for the terminal `detail`, `wait` blocks up to a timeout, and `start()` preflights access, validation, and admission before invoking the producer's `run()` once while refusing any owner no attached controller serves; `events.subscribe` delivers registration, progress, stopping, settlement, removal, and output commits at owner, scope, or process granularity. +Every operation is a thin projection over the registered jobs, bound to one caller by `forCaller`: `list` and `get` return fresh projections, `read` advances the model's cursor and hands out the producer's result once after settlement, `readAt` reads retained chunks at an absolute offset without consuming anything, `kill` invokes producer cancellation before changing status and records the reason for the terminal `detail`, `wait` blocks up to a timeout, and `start()` preflights access, validation, and admission before invoking the producer's `run()` once while refusing any owner no attached controller serves; `events.subscribe` delivers registration, progress, stopping, settlement, removal, and output commits at owner, scope, or process granularity. diff --git a/packages/jobs/jobs/README.zh.md b/packages/jobs/jobs/README.zh.md index a15b4f7b45..93f466076c 100644 --- a/packages/jobs/jobs/README.zh.md +++ b/packages/jobs/jobs/README.zh.md @@ -77,14 +77,14 @@ kind: "package-reference" | 文件 | 职责 | |---|---| | [`src/index.ts`](src/index.ts) | 插件入口:抽象 `JobRegistry` 服务及其约定 | -| [`src/types.ts`](src/types.ts) | 共享词汇:`JobSpec`、`JobHandle`、`JobHooks`、`JobOutcome`、`VisibleJobs`、`JobEvent` 与读取结果 | +| [`src/types.ts`](src/types.ts) | 共享词汇:`JobSpec`、`JobHandle`、`JobHooks`、`JobOutcome`、`CallerJobs`、`JobEvent` 与读取结果 | | [`src/view.ts`](src/view.ts) | 客户端安全叶子:`JobView`、`JobChunk`、`JobStatus` 与可合并扩展的 `JobKindMap` | | [`src/brand.ts`](src/brand.ts) | `JobId` 带类型标记的标识符,无需 agent 依赖即可导入 | | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件:校验每个 job 的事件协议(先 registered、恰一次结算、最后 removed)以及每个通告的投影与注册表自身读取的一致性 | ### 服务操作 -每个操作都是已注册任务之上的薄投影,由 `visibleTo` 绑定到一个调用方:`list` 与 `get` 返回全新投影,`read` 推进模型游标并在结算后把生产方的 result 交出一次,`readAt` 按绝对偏移读取保留块且不消耗任何东西,`kill` 在改变状态前调用生产方取消并为终态 `detail` 记录原因,`wait` 阻塞至超时,`start()` 在调用生产方 `run()` 一次之前预检访问、校验与准入,同时拒绝任何没有已附加控制器服务的所有者;`events.subscribe` 按所有者、scope 或进程粒度投递注册、进度、停止中、结算、移除与输出提交。 +每个操作都是已注册任务之上的薄投影,由 `forCaller` 绑定到一个调用方:`list` 与 `get` 返回全新投影,`read` 推进模型游标并在结算后把生产方的 result 交出一次,`readAt` 按绝对偏移读取保留块且不消耗任何东西,`kill` 在改变状态前调用生产方取消并为终态 `detail` 记录原因,`wait` 阻塞至超时,`start()` 在调用生产方 `run()` 一次之前预检访问、校验与准入,同时拒绝任何没有已附加控制器服务的所有者;`events.subscribe` 按所有者、scope 或进程粒度投递注册、进度、停止中、结算、移除与输出提交。 diff --git a/packages/jobs/jobs/src/index.ts b/packages/jobs/jobs/src/index.ts index a1c60cbeef..7245cfa430 100644 --- a/packages/jobs/jobs/src/index.ts +++ b/packages/jobs/jobs/src/index.ts @@ -11,7 +11,7 @@ import { Context, Service } from '@deepseek-ai/cordis' import type { SessionId } from '@deepseek-ai/dsh-session' -import type { JobEvents, JobId, JobSpec, VisibleJobs } from './types.ts' +import type { JobEvents, JobId, JobSpec, CallerJobs } from './types.ts' export { JobId } from './types.ts' export type { @@ -36,7 +36,7 @@ export type { JobSpec, JobStatus, JobView, - VisibleJobs, + CallerJobs, } from './types.ts' declare module '@deepseek-ai/cordis' { @@ -105,13 +105,13 @@ export abstract class JobRegistry extends Service { /** * Bind the caller's identity once and return its operations. A caller sees - * its own jobs and every unowned job; a caller-less view sees unowned jobs - * only. The view resolves visibility on every call, so it stays valid as - * jobs come and go. - * @param caller - the reading session, or undefined for an anonymous caller. + * its own jobs and every unowned job; an anonymous caller sees unowned jobs + * only. The operations resolve visibility on every call, so they stay valid + * as jobs come and go. + * @param caller - the calling session, or `undefined` for an anonymous caller. * @returns the operations available to that caller. */ - abstract visibleTo(caller?: SessionId): VisibleJobs + abstract forCaller(caller: SessionId | undefined): CallerJobs /** * Attach an effect-scoped controller that can read and stop jobs. It serves the diff --git a/packages/jobs/jobs/src/invariant.ts b/packages/jobs/jobs/src/invariant.ts index 529690804e..30f4232625 100644 --- a/packages/jobs/jobs/src/invariant.ts +++ b/packages/jobs/jobs/src/invariant.ts @@ -24,7 +24,7 @@ export const inject = ['invariants'] /** The registry's own current projection of `id`, or undefined once the record is gone. */ function readBack(ctx: Context, id: JobId, owner: JobView['owner']): JobView | undefined { try { - return ctx.jobs.visibleTo(owner).get(id) + return ctx.jobs.forCaller(owner).get(id) } catch { // The registry throws for an id outside the caller's set; after a removal that is the expected answer. return undefined diff --git a/packages/jobs/jobs/src/types.ts b/packages/jobs/jobs/src/types.ts index cfbfb49d92..5acd56d1ce 100644 --- a/packages/jobs/jobs/src/types.ts +++ b/packages/jobs/jobs/src/types.ts @@ -18,14 +18,14 @@ export interface JobOutcome { status: 'completed' | 'killed' | 'failed' /** * Terminal reason rendered into status lines (`exit code: 3`, `max-tokens`). - * When the job settles `killed` after a {@link VisibleJobs.kill} with a + * When the job settles `killed` after a {@link CallerJobs.kill} with a * reason, the registry appends that reason. */ detail?: string /** * Return value for jobs whose result is a value rather than a stream (a * workflow's rendered result, a subagent's report). The output ring carries - * the stream; this is handed out once by the model's next {@link VisibleJobs.read}. + * the stream; this is handed out once by the model's next {@link CallerJobs.read}. */ result?: string } @@ -155,7 +155,7 @@ export interface JobSpec { run(job: JobHandle): JobHooks } -/** Options for {@link VisibleJobs.kill}. */ +/** Options for {@link CallerJobs.kill}. */ export interface JobKillOptions { /** * Cancellation reason: forwarded verbatim to the producer's cancel hook, and @@ -165,7 +165,7 @@ export interface JobKillOptions { reason?: string } -/** Output and post-read state returned by the consuming {@link VisibleJobs.read}. */ +/** Output and post-read state returned by the consuming {@link CallerJobs.read}. */ export interface JobRead { /** Ring chunks appended since the model cursor, in offset order; every channel included. */ chunks: readonly JobChunk[] @@ -177,7 +177,7 @@ export interface JobRead { job: JobView } -/** Result of one non-consuming {@link VisibleJobs.readAt}. */ +/** Result of one non-consuming {@link CallerJobs.readAt}. */ export interface JobOutputRead { /** Retained chunks overlapping `[from, total)`, in offset order. */ chunks: readonly JobChunk[] @@ -193,11 +193,11 @@ export interface JobOutputRead { } /** - * The operations one caller may perform, bound by {@link JobRegistry.visibleTo}. + * The operations one caller may perform, bound by {@link JobRegistry.forCaller}. * A caller sees its own jobs and every unowned job; every method throws for * an id outside that set, without distinguishing unknown from foreign. */ -export interface VisibleJobs { +export interface CallerJobs { /** * List the visible jobs in registration order. * @returns fresh projections. @@ -251,7 +251,7 @@ export interface VisibleJobs { /** * Why a job settled: its producer finished (`producer`), a - * {@link VisibleJobs.kill} ran first (`kill`), or owner or service teardown + * {@link CallerJobs.kill} ran first (`kill`), or owner or service teardown * cancelled it (`teardown`) — a settlement whose owner has no reader left. */ export type JobSettleCause = 'producer' | 'kill' | 'teardown' @@ -259,7 +259,7 @@ export type JobSettleCause = 'producer' | 'kill' | 'teardown' /** * One lifecycle or output event. Lifecycle events carry the job's projection * after the commit they announce; `output` carries only the id and the new - * total, so an observer schedules a {@link VisibleJobs.readAt} from its own + * total, so an observer schedules a {@link CallerJobs.readAt} from its own * cursor and the registry never pushes payloads. */ export type JobEvent = diff --git a/packages/jobs/jobs/tests/invariant.spec.ts b/packages/jobs/jobs/tests/invariant.spec.ts index 51d0188dc4..c97b0a1ca1 100644 --- a/packages/jobs/jobs/tests/invariant.spec.ts +++ b/packages/jobs/jobs/tests/invariant.spec.ts @@ -28,7 +28,7 @@ async function setup() { const reads = new Map() let listener: JobEventListener | undefined const probe = { - visibleTo: (caller?: SessionId) => ({ + forCaller: (caller?: SessionId) => ({ get: (id: JobId) => { const view = reads.get(String(id)) if (view === undefined || view.owner !== caller) throw new Error(`unknown job ${String(id)}`) diff --git a/packages/jobs/jobs/tests/service.spec.ts b/packages/jobs/jobs/tests/service.spec.ts index 8b62cf2356..143afa85bc 100644 --- a/packages/jobs/jobs/tests/service.spec.ts +++ b/packages/jobs/jobs/tests/service.spec.ts @@ -3,7 +3,7 @@ import { Context } from '@deepseek-ai/cordis' import { SessionId } from '@deepseek-ai/dsh-session' import { JobId, JobRegistry } from '@deepseek-ai/dsh-jobs' import type { - JobEvent, JobEventFilter, JobEventListener, JobHooks, JobOutputSource, JobRead, JobSpec, JobView, VisibleJobs, + JobEvent, JobEventFilter, JobEventListener, JobHooks, JobOutputSource, JobRead, JobSpec, JobView, CallerJobs, } from '@deepseek-ai/dsh-jobs' /** Compile-time probe: does `Shape` carry a `Key` member? */ @@ -45,7 +45,7 @@ class StubJobRegistry extends JobRegistry { return id } - visibleTo(caller?: SessionId): VisibleJobs { + forCaller(caller: SessionId | undefined): CallerJobs { const view = (id: JobId): JobView => this.view(id, caller) return { list: () => [view(JobId('bash-1'))], @@ -73,7 +73,7 @@ describe('JobRegistry seam', () => { const id = ctx.jobs.start({ kind: 'bash', label: 'sleep 60', owner: caller, run: () => hooks }) expect(id).toBe('bash-1') - const jobs = ctx.jobs.visibleTo(caller) + const jobs = ctx.jobs.forCaller(caller) expect(jobs.list()).toHaveLength(1) expect(jobs.get(id).status).toBe('running') expect(jobs.get(id).owner).toBe(caller) @@ -82,7 +82,7 @@ describe('JobRegistry seam', () => { expect(jobs.kill(id, { reason: 'seam test' })).toBe('requested') await expect(jobs.wait(id, 5)).resolves.toMatchObject({ id }) // A caller-less view is the unowned-only view. - expect(ctx.jobs.visibleTo().get(id).owner).toBeUndefined() + expect(ctx.jobs.forCaller(undefined).get(id).owner).toBeUndefined() const events: JobEvent[] = [] const unsubscribe = ctx.jobs.events.subscribe({ owners: 'all' }, (event) => { events.push(event) }) diff --git a/packages/jobs/tool-jobs/src/index.ts b/packages/jobs/tool-jobs/src/index.ts index b7fd9f274a..925b1ad8cf 100644 --- a/packages/jobs/tool-jobs/src/index.ts +++ b/packages/jobs/tool-jobs/src/index.ts @@ -15,7 +15,7 @@ import { TextRetainer } from '@deepseek-ai/dsh-output-retention' import { defineTool } from '@deepseek-ai/dsh-tools' import type { GenericCallView, ToolDefinition, ToolExecution } from '@deepseek-ai/dsh-tools' import { JobId } from '@deepseek-ai/dsh-jobs' -import type { JobView, VisibleJobs } from '@deepseek-ai/dsh-jobs' +import type { JobView, CallerJobs } from '@deepseek-ai/dsh-jobs' import type { Agent } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-agent' import { publicJob, renderModelDelta, statusLine } from './render.ts' @@ -158,7 +158,7 @@ function visibleOutputLimit(ctx: Context, exec: ToolExecution): number | undefin if (exec.name !== 'job_output' && exec.name !== 'job_kill') return undefined const jobId = (exec.arguments as { job_id?: unknown } | null | undefined)?.job_id if (typeof jobId !== 'string' || jobId.length === 0) return undefined - return ctx.jobs.visibleTo(exec.agent?.id).list().find(job => job.id === jobId)?.outputLimitBytes + return ctx.jobs.forCaller(exec.agent?.id).list().find(job => job.id === jobId)?.outputLimitBytes } /** Validate the non-empty constraint that ParameterSchemaSpec cannot express. */ @@ -180,7 +180,7 @@ function isTerminal(job: JobView): boolean { } /** The consuming read as the model sees it: the delta, then the result once, then the status line. */ -function readBody(jobs: VisibleJobs, id: JobId): { text: string; job: PublicJobSnapshot } { +function readBody(jobs: CallerJobs, id: JobId): { text: string; job: PublicJobSnapshot } { const read = jobs.read(id) const delta = renderModelDelta(read.chunks, read.lossy) const text = read.result === undefined @@ -349,7 +349,7 @@ export function apply(ctx: Context, config: Config): void { }, async execute(args, exec) { const id = validateJobId(args.job_id) - const jobs = ctx.jobs.visibleTo(exec.agent?.id) + const jobs = ctx.jobs.forCaller(exec.agent?.id) if (args.wait === true) { const timeout = Math.min(args.timeout_ms ?? waitDefault, waitCap) const withdraw = claim(id) @@ -380,7 +380,7 @@ export function apply(ctx: Context, config: Config): void { }], }, execute(_args, exec) { - const jobs = ctx.jobs.visibleTo(exec.agent?.id).list() + const jobs = ctx.jobs.forCaller(exec.agent?.id).list() return Promise.resolve(jobs.map(publicJob)) }, presentCall: () => presentTaskCall('List background jobs', 'read'), @@ -416,7 +416,7 @@ export function apply(ctx: Context, config: Config): void { }, execute(args, exec) { const id = validateJobId(args.job_id) - const jobs = ctx.jobs.visibleTo(exec.agent?.id) + const jobs = ctx.jobs.forCaller(exec.agent?.id) const result = jobs.kill(id, args.reason === undefined ? {} : { reason: args.reason }) // The model's own kill is its delivery: the settlement notice would only // repeat what this tool result already said. diff --git a/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts b/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts index e98a60554b..efa54ab6a0 100644 --- a/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts +++ b/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts @@ -558,7 +558,7 @@ describe('job_kill', () => { expect(result.isError).toBe(true) expect(Buffer.byteLength(text(result))).toBeLessThanOrEqual(64) expect(text(result)).toContain('[result truncated]') - expect(ctx.jobs.visibleTo().get(JobId('bash-1')).status).toBe('running') + expect(ctx.jobs.forCaller(undefined).get(JobId('bash-1')).status).toBe('running') }) it('bounds single-text post policy while preserving structured policy results', async () => { diff --git a/packages/shell/tool-bash/tests/background.spec.ts b/packages/shell/tool-bash/tests/background.spec.ts index 0be0f645d2..88d0cbde13 100644 --- a/packages/shell/tool-bash/tests/background.spec.ts +++ b/packages/shell/tool-bash/tests/background.spec.ts @@ -63,7 +63,7 @@ async function until(read: () => T | undefined, timeoutMs = 5_000): Promise chunk.text).join('') + return ctx.jobs.forCaller(caller?.id).readAt(id, 0).chunks.map(chunk => chunk.text).join('') } describe('background bash output', () => { @@ -75,7 +75,7 @@ describe('background bash output', () => { run_in_background: true, }) expect(text(ack)).toContain('started background job') - const jobs = ctx.jobs.visibleTo() + const jobs = ctx.jobs.forCaller(undefined) const job = jobs.list()[0] expect(job).toBeDefined() @@ -97,7 +97,7 @@ describe('background bash output', () => { it('a killed background job settles killed with the kill reason merged into its detail', async () => { const ctx = await setup() await call(ctx, { command: 'sleep 60', description: 'test command', run_in_background: true }) - const jobs = ctx.jobs.visibleTo() + const jobs = ctx.jobs.forCaller(undefined) const job = jobs.list()[0] jobs.kill(job!.id, { reason: 'test cleanup' }) await until(() => jobs.get(job!.id).status === 'killed' ? true : undefined) @@ -113,7 +113,7 @@ describe('background bash output', () => { run_in_background: true, }) expect(text(started)).toMatch(/^started background job bash-\d+$/) - const jobs = ctx.jobs.visibleTo() + const jobs = ctx.jobs.forCaller(undefined) const job = jobs.list()[0] await until(() => jobs.get(job!.id).status === 'killed' ? true : undefined) const read = text(await ctx.tools.execute({ @@ -132,7 +132,7 @@ describe('background bash output', () => { description: 'test command', run_in_background: true, }) - const jobs = ctx.jobs.visibleTo() + const jobs = ctx.jobs.forCaller(undefined) const job = jobs.list()[0] await until(() => jobs.get(job!.id).status === 'completed' ? true : undefined) const chunks = jobs.readAt(job!.id, 0).chunks @@ -190,7 +190,7 @@ describe('owned background output', () => { arguments: { command: 'echo owned', description: 'test command', run_in_background: true }, agent: owner, }) - const owned = ctx.jobs.visibleTo(owner.id) + const owned = ctx.jobs.forCaller(owner.id) const job = owned.list()[0] expect(job).toBeDefined() expect(job!.owner).toBe(owner.id) diff --git a/packages/shell/tool-pwsh/tests/background.spec.ts b/packages/shell/tool-pwsh/tests/background.spec.ts index 4ebc349a9f..63effc1e87 100644 --- a/packages/shell/tool-pwsh/tests/background.spec.ts +++ b/packages/shell/tool-pwsh/tests/background.spec.ts @@ -114,7 +114,7 @@ describe('background pwsh output', () => { pwsh.backgroundHandler = () => scripted.proc await call(ctx, { command: 'Get-Progress', description: 'test command', run_in_background: true }) - const jobs = ctx.jobs.visibleTo() + const jobs = ctx.jobs.forCaller(undefined) const job = jobs.list()[0] expect(job).toBeDefined() expect(job!.kind).toBe('pwsh') @@ -158,7 +158,7 @@ describe('background pwsh output', () => { } pwsh.backgroundHandler = () => proc await call(ctx, { command: 'Get-Broken', description: 'test command', run_in_background: true }) - const jobs = ctx.jobs.visibleTo() + const jobs = ctx.jobs.forCaller(undefined) const job = jobs.list()[0] await until(() => warn.mock.calls.some(args => String(args[0]).includes('output source for')) ? true : undefined) // The reader already failed; the job must wait for real settlement rather @@ -179,7 +179,7 @@ describe('background pwsh output', () => { pwsh.backgroundHandler = () => scripted.proc await call(ctx, { command: 'Start-Job', description: 'test command', run_in_background: true }) - const jobs = ctx.jobs.visibleTo() + const jobs = ctx.jobs.forCaller(undefined) const job = jobs.list()[0] expect(jobs.readAt(job!.id, 0).chunks).toEqual([]) @@ -208,11 +208,11 @@ describe('owned background output (pwsh)', () => { arguments: { command: 'Get-Slow', description: 'test command', run_in_background: true }, agent: owner, }) - const owned = ctx.jobs.visibleTo(owner.id) + const owned = ctx.jobs.forCaller(owner.id) const job = owned.list()[0] expect(job).toBeDefined() expect(job!.owner).toBe(owner.id) - expect(() => ctx.jobs.visibleTo().readAt(job!.id, 0)).toThrow(/belongs to another session/) + expect(() => ctx.jobs.forCaller(undefined).readAt(job!.id, 0)).toThrow(/belongs to another session/) expect(owned.readAt(job!.id, 0).chunks).toEqual([]) scripted.finish() await until(() => owned.get(job!.id).status === 'completed' ? true : undefined) diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index c1b2046182..2b676fc99c 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -972,7 +972,7 @@ describe('dsh-tool-subagent background mode', () => { const result = await resultPromise expect(result.isError).toBe(true) - expect(ctx.jobs.visibleTo(parent.id).list()).toEqual([]) + expect(ctx.jobs.forCaller(parent.id).list()).toEqual([]) }) it('rejects startup when the provider changes during asynchronous route preflight', async () => { @@ -1241,7 +1241,7 @@ describe('dsh-tool-subagent continuable background mode', () => { expect(match).not.toBeNull() const [, childId] = match! // No Task was created for the continuable child. - expect(ctx.jobs.visibleTo(parent.id).list()).toEqual([]) + expect(ctx.jobs.forCaller(parent.id).list()).toEqual([]) await vi.waitFor(() => { expect(ctx.agents.get(SessionId(childId!))).toBeUndefined() @@ -1272,7 +1272,7 @@ describe('dsh-tool-subagent continuable background mode', () => { if (result.isError) throw new Error('expected foreground subagent success') expect(result.value).toMatchObject({ kind: 'foreground' }) expect(text(result)).toBe('continuable answer') - expect(ctx.jobs.visibleTo(parent.id).list()).toEqual([]) + expect(ctx.jobs.forCaller(parent.id).list()).toEqual([]) }) it('isolates a cancelled continuable preparation from a concurrent sibling', async () => { diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index e07bcaccd0..de94f55c2b 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -545,7 +545,7 @@ export const LINK_MAP: Readonly> = { JobEventListener: 'jobs.md', JobEvents: 'jobs.md', JobSettleCause: 'jobs.md', - VisibleJobs: 'jobs.md', + CallerJobs: 'jobs.md', CreateTeamTaskRequest: 'agent-team.md', SendTeamMessageRequest: 'agent-team.md', SendTeamMessageResult: 'agent-team.md', diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 16bb79660d..09b4adec56 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -1078,7 +1078,7 @@ }, { "doc": "docs/subsystems/jobs.md", - "symbol": "VisibleJobs", + "symbol": "CallerJobs", "source": "packages/jobs/jobs/src/types.ts" }, {