@deepseek-ai/dsh-cordis-client-runner 0.1.1-rc.2 → 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/extensions/cordis-client-runner/README.md
5
- README.md: ba60e3256ca6c80792645daa26c6872cfc846940
6
- README.zh.md: 2d8712de6b0444847eb731d84140fe2c1cad5b63
5
+ README.md: ba813e3498752902b80102ecad5802187d95a129
6
+ README.zh.md: 1b975550dc4d08aef90f8e7a79a81a67983dfce1
package/README.md CHANGED
@@ -1,35 +1,94 @@
1
+ ---
2
+ description: "Browser half of dynamic Cordis packages for users and maintainers choosing, composing, or debugging how a page answers run requests and loads browser-half code."
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-cordis-client-runner
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- Browser half of dynamic dual-half plugin packages. The host-side runner holds every definition's code in process memory and asks the open pages, over a `cordis/request-run` event, whether to run one; this package answers that request, turns the definition into a live browser plugin, and turns a `dynamicCordisRunner/retract` event back into a clean page.
10
+ ## Summary
11
+
12
+ `dsh-cordis-client-runner` lets a page run the browser half of a dynamic Cordis package: it answers the host's run requests, loads the browser-half source into the page as a live plugin, and removes it when the host retracts the run. A person approves or declines a run — or starts one directly — and the result this package reports back becomes the `cordis_run` tool result the model reads. Nothing loads at activation and nothing is restored after a refresh; a page runs a dynamic package only when someone answers a run request or asks for it here.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Mount this plugin in a web client whose composition also mounts the host runner — the host half runs in the process, this browser half in the page. When a dynamic package that has a browser half is run, the open pages receive a run request; this package carries out the load on this page, and the UI package (`ui-cordis`) renders the panel and cards a person uses to answer it. Host-only packages need no browser half and therefore no page: the host runs them itself.
29
+
30
+ ### What the page does
31
+
32
+ A browser half is written in plain JavaScript — no JSX, no TypeScript, no module imports — and runs as an async function. It receives a fixed set of names — `React`, `console`, `styles`, and `host` — while browser globals like `fetch` and `setTimeout` are unavailable. The plugin it returns can use the lifecycle verbs and only the services it declared in its own `inject`. Calling `host.call(method, args)` from the loaded half reaches its own host half. A crash that happens while React renders the loaded half is reported to the host with the slot, whether the crash removed the entry, and a message written for the author.
33
+
34
+ ### What the run surface offers
35
+
36
+ A run surface can answer a pending host request — approving it, optionally covering future versions of the same plugin, or declining it — and can start a definition at the user's own gesture, which authorizes it. Each definition has at most one in-flight activity, so an affordance built on that state survives a remount. What the surface shows about this page is page-local: the last render crash per package, why this page's own attempt failed, and whether a package is loaded here — never the host's view of what is running.
37
+
38
+ ### Lifecycle boundaries
39
+
40
+ Loading is idempotent: asking to load a revision this page already runs changes nothing, a newer revision replaces the loaded one, and the same revision after a retract loads afresh. Operations on a definition serialize. A refresh starts clean by design — the host still holds the definition, this page does not run it until asked again.
41
+
42
+ -----
6
43
 
7
- ## What it does
44
+ <a id="understand-the-implementation"></a>
45
+ ## Understand the implementation
8
46
 
9
- 1. **Event subscription** — the four announcements are forwarded host cordis events, so this package consumes `cordis/request-run`, `cordis/request-run-resolved`, and `dynamicCordisRunner/retract` through `ctx.remote.$on`, whose key set IS the api-remotes allowlist.
10
- 2. **Closure evaluation** the browser half's source runs as an async function body whose parameters are its symbol surface (`React`, `console`, `styles`, `host`, plus teaching traps shadowing `setTimeout`/`fetch`/`require`). No JSX, no TypeScript, no module imports.
11
- 3. **Guard facade** — `apply` receives a whitelisting proxy over the real fiber ctx: lifecycle verbs plus the services the returned plugin declared in its own `inject` (so the object form `{ inject: ['slots'], apply(ctx) {} }` is what reaches a service; a plain function has no declaration site and reaches none). The `slots` seat assigns the shadowing priority (registering IS shadowing, newest run wins); the `theme` seat pins the override layer's source to the package id and hangs its disposer on the fiber.
12
- 4. **Loader entries** — the guarded plugin is seated in the module table and mounted through `loader.create`, so a dynamic package rides the same activation gating, fiber-effect cleanup, and status projection as a static one. Unload is entry removal plus factory invalidation plus style removal.
13
- 5. **Run orchestration** — a `cordis/request-run` event asks this page whether to run a definition. Whoever answers drives the run in order: the host half first, then the source fetch, then the browser half, then one resolution carrying what happened. A user pressing "run" is itself the authorization and orchestrates the same way with nothing to answer — and for a host-only definition the run ends at the host half, because there is no second half to fetch or load here.
14
- 6. **Package-internal RPC** — a package's `host.call` routes to its own host half through the `dynamicCordisRunner` Remote namespace (`invoke`), and each routing failure code becomes its own teaching error. Both directions carry JSON only: an omitted argument travels as `null` (so `host.call('listServices')` is legal and the handler receives `null`), and a payload the generated codec refuses — a function, `undefined`, a class instance — becomes a teaching error naming the call and the contract instead of the codec's bare field name.
15
- 7. **Render-failure reflow** — the slot registry's supervision seam (`slots.onEntryError`) fires for every entry-boundary crash on the page; the ones belonging to a package this runner seated go to two outlets from that one observation: upstream to the authoring session (`reportRenderFailure`, for the model) and onto this package's own `renderFailures` face field (for the panel row). Ownership is keyed on component identity, recorded when the guard's `register` proxy seats it, because the registry stores the component verbatim — so no parallel ledger of entries has to be kept in step. This is post-settle diagnosis only: it carries no settle authority, never touches a run resolution, and a failed report is swallowed rather than turning one crash into two.
47
+ <details>
48
+ <summary>Implementation internalsclick to expand</summary>
16
49
 
17
- ## Lifecycle
50
+ This section explains the design behind the browser half; the observable behavior is fully covered in [Use this package](#use-this-package).
18
51
 
19
- Loads converge by `(id, rev)` against live state: loading a revision this page already runs answers from live state without reloading (so a replayed run does not look unanswered), a newer revision replaces it, and the same revision after a retract loads afresh. Operations serialize per definition.
52
+ ### Design philosophy
20
53
 
21
- Nothing loads at activation, and nothing is restored after a refresha page runs a dynamic package only when someone answers a run request or asks for it here.
54
+ The browser half is built on one principle: a dynamic package must ride the same activation gating, fiber-effect cleanup, and status projection as a static one. The evaluated plugin is seated in the module table and mounted through `loader.create`; unload is entry removal plus factory invalidation plus style removal. The guard is a whitelistlifecycle verbs plus declared services — that mirrors the host-side sandbox facade, so a package author meets one contract on both halves. One observer feeds two outlets: the slot registry's entry-error seam is watched only here, and a crash belonging to a package this runner seated goes upstream to the host for the model and onto this package's own `renderFailures` for the panel.
22
55
 
23
- ## What a run surface reads and calls
56
+ ### Source map
24
57
 
25
- `ctx.dynamicCordisRunner` is the whole face:
58
+ | File | Role |
59
+ |---|---|
60
+ | [`src/client/index.ts`](src/client/index.ts) | Plugin entry: runner, orchestrator, inspect registry, forwarded-event subscription |
61
+ | [`src/client/runtime.ts`](src/client/runtime.ts) | Load engine: convergence by run identity, guarded mount, retract |
62
+ | [`src/client/orchestrator.ts`](src/client/orchestrator.ts) | Run orchestration: host half first, source fetch, browser half, one resolution |
63
+ | [`src/client/evaluator.ts`](src/client/evaluator.ts) | Closure evaluation: the symbol surface and its teaching traps |
64
+ | [`src/client/guard.ts`](src/client/guard.ts) | The whitelisting `ctx` façade for loaded browser halves |
65
+ | [`src/client/inspect-registry.ts`](src/client/inspect-registry.ts) | Client Inspect Providers and the pending-query router |
66
+ | [`src/client/providers.ts`](src/client/providers.ts) | First-party client Inspect Providers (slots, theme, events) |
67
+ | [`src/client/timer.ts`](src/client/timer.ts) | The client timer service dynamic packages inject |
26
68
 
27
- - `activeRuns` each definition's single in-flight activity: `awaiting-approval` (the request id to answer plus the ask's session, package name, and purpose) or `orchestrating` (the session the run is being carried out for). Both arms name the session because grouping belongs to the run, not to its phase; the waiting arm carries the ask's own text because `cordis_define` broadcasts nothing, so a request can name a definition the last registry read does not cover and then this entry is the only source that row has. A surface renders from it and keeps no copy, which is what makes the affordance survive a remount.
28
- - `renderFailures` — this page's last render crash per definition (slot, teaching message, and whether the crash retired the entry from its cell), on the same notification channel as the live set. Page-local and current by construction: it clears when the package stops, is retracted, or loads again, so a row can render it directly. The host keeps its own last-across-pages copy for the model — the two have different owners and lifetimes, and a surface must not read the host's back in place of this one.
29
- - `lastRunError` — why this page's own attempt failed, per definition. It outlives the activity, because the host disposes only the half a failed request started: a page can be looking at a definition the host reports as running while having nothing loaded itself.
30
- - `approve(requestId)` / `decline(requestId)` / `startUserRun({ agentId, id, hasClientHalf })` — the two entries. All three are idempotent (per request id, and per definition for the user's own run), so a double press cannot start two runs. `hasClientHalf` is required: a host-only definition has no source to fetch, so the caller states the shape from the registry row it is acting on rather than the orchestrator learning it from a failed fetch. An answerable request always has a browser half, because the host runs a host-only definition itself instead of asking a page.
31
- - `subscribe()` / `getSnapshot()` / `isLoaded(id)` — what this page has loaded. `isLoaded` is page-local truth, never the host's "it is running".
69
+ ### How a run is carried out
32
70
 
71
+ A `cordis/request-run` event asks this page whether to run a definition. Whoever answers — the page after an approval, or the user pressing run — drives the orchestration: the host half first (so a host-half failure short-circuits before the browser has moved), then the source fetch, then the browser half, then one resolution carrying what happened. The browser half's source is evaluated as an async function body with the symbol surface as parameters, the returned plugin is guard-wrapped and mounted through the loader, and the resolution reports the loaded revision or the failing stage with the closure's, guard's, or fiber's message. `host.call` routes through the Remote namespace; an omitted argument travels as `null`, and a payload the generated codec refuses becomes a teaching error naming the call and the contract.
72
+
73
+ </details>
74
+
75
+ -----
76
+
77
+ <a id="further-exploration"></a>
78
+ ## Further Exploration
79
+
80
+ Read these pages when the package-level contract is not enough. They move from the browser half to the host that asks it, the tools whose runs it answers, and the surface that renders it.
81
+
82
+ - [Host runner](../cordis-host-runner/README.md) — the registry and run round trip this package answers.
83
+ - [Tool package](../tool-cordis/README.md) — the model-facing tools whose run requests reach this page.
84
+ - [UI package](../ui-cordis/README.md) — the panel and cards that operate this face.
85
+ - [Extensions subsystem](../../../docs/subsystems/extensions.md) — the generated `ctx.dynamicCordisRunner` API and `cordis/*` events.
86
+ - [Dynamic client render and attachment ownership Agent Note](../../../.agents/notes/implemented/architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.md) — how browser plugins own their rendering and CSS.
87
+ - [Client shells and dynamic packages Agent Note](../../../.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md) — package placement and build faces for the client halves.
88
+
89
+ -----
90
+
91
+ <a id="model-experience"></a>
33
92
  ## Model Experience
34
93
 
35
94
  ### Run resolution, when a model asked for the run
@@ -50,19 +109,34 @@ Append-only. A resolution reaches the model only as the tool result for the requ
50
109
 
51
110
  #### What the model sees
52
111
 
53
- A browser half that loads cleanly can still crash when React renders it, and that crash lands after the run was answered — so the model would otherwise be told "ok" and never learn. Every entry-boundary crash of a package this page seated is sent to the host (`reportRenderFailure`) naming the slot, whether the crash retired the entry from its cell (`abdicated`: the package's UI is gone, not merely broken), and a message written for the author: the crash text, plus the redirect for a withheld browser global the text names but does not teach — `window.setInterval` around the closure trap crashes as `is not a function`, which explains nothing on its own. The host keeps the last one per package and shows it through `cordis_inspect`; nothing here reaches a run resolution. The same observation also lands on `renderFailures` for the page's own surface — one observer, two outlets, because "the last crash across pages, for the model" and "what this page is showing now" are different facts with different lifetimes.
112
+ A browser half that loads cleanly can still crash when React renders it, and that crash lands after the run was answered — so the model would otherwise be told "ok" and never learn. Every entry-boundary crash of a package this page seated is sent to the host (`reportRenderFailure`) naming the slot, whether the crash retired the entry from its cell (`abdicated`: the package's UI is gone, not merely broken), and a message written for the author. The host keeps the last one per package, steers the owning session with it, and exposes it through `cordis_inspect_self`; nothing here reaches a run resolution.
54
113
 
55
114
  #### Token effect
56
115
 
57
- Conditional and bounded by the host's retention, not by this page: one report per crash, and the host keeps only the latest per package, so a repeatedly crashing entry costs the model one paragraph rather than a growing list. The report never enters a tool result of its own — the model pays for it only when it asks.
116
+ Conditional and bounded by the host's retention, not by this page: one report per crash, and the host keeps only the latest per package, so a repeatedly crashing entry costs the model one message rather than a growing list. The report never enters a tool result of its own — the model pays for it only when it is steered or asks.
58
117
 
59
118
  #### KV Cache effect
60
119
 
61
- None of its own. Reports travel over RPC and are stored, not appended to the conversation; the model reads them through an inspection it chose to make, which extends the tail like any other tool result.
120
+ None of its own. Reports travel over RPC and are stored, not appended to the conversation; the model reads them through a steering message or an inspection it chose to make, which extends the tail like any other tool result.
62
121
 
63
122
  ## Known Limitations and Deferred Work
64
123
 
65
- - **A refused resolution is not retried.** The acknowledgement of `resolveRequestRun` is not read, so when the host declines a stale success (`accepted: false`, because the definition's revision moved on while this page was loading) the page keeps what it loaded and does not orchestrate again. The request stays answerable — another page's answer or the caller's cancellation settles it — and the stop that bumped the revision retracts the stale load. Retrying was evaluated and deferred: the window is one revision bump inside a single round trip.
66
- - The plugin declares `remote.dynamic`, so it stays parked until the host-side namespace exists rather than loading packages whose host half it could never reach.
67
- - Slot admission (allow/deny lists per deployment) has no carrier: the dispatched row declares services, not target slots.
68
- - Guard whitelists are hand-mirrored twins of the host-side sandbox facade; sharing one specification is deferred.
124
+ <a id="known-limitations-and-deferred-work"></a>
125
+
126
+
127
+ These limits define where the browser half needs special care. They are current package constraints, not a task backlog.
128
+
129
+ - **A refused resolution is not retried** — the acknowledgement of `resolveRequestRun` is not read, so when the host declines a stale success (`accepted: false`, because the definition's revision moved on while this page was loading) the page keeps what it loaded and does not orchestrate again. The request stays answerable — another page's answer or the caller's cancellation settles it — and the stop that bumped the revision retracts the stale load.
130
+ - **The plugin stays parked until the host namespace exists** — it declares `remote.dynamicCordisRunner`, so it loads no browser half whose host half it could never reach.
131
+ - **Slot admission has no carrier** — the dispatched row declares services, not target slots, so per-deployment allow or deny lists for slot admission have nowhere to ride.
132
+ - **Guard whitelists are hand-mirrored twins** — the browser guard replicates the host-side sandbox facade; sharing one specification is deferred.
133
+
134
+ <a id="dev-note"></a>
135
+ ### Dev Note
136
+
137
+ <details>
138
+ <summary>Working context for maintainers — click to expand</summary>
139
+
140
+ None.
141
+
142
+ </details>
package/README.zh.md CHANGED
@@ -1,68 +1,142 @@
1
+ ---
2
+ description: "动态 Cordis 包的浏览器半说明,供选择、组合或排查页面如何应答运行请求并装载浏览器半代码的用户与维护者阅读。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-cordis-client-runner
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- 动态双半插件包的浏览器半。host 侧 runner 把每个定义的代码留在进程内存里,并经一条 `cordis/request-run` 事件向打开的页面发问「要不要运行它」;本包回答这个请求、把定义变成活的浏览器插件,并把 `dynamicCordisRunner/retract` 事件变回干净的页面。
10
+ ## 概述
11
+
12
+ `dsh-cordis-client-runner` 让页面运行动态 Cordis 包的浏览器半:它应答 host 的运行请求、把浏览器半源码装载进页面成为活插件,并在 host 撤回该次运行时把它移除。人可以批准或拒绝一次运行——也可以直接启动一次——而本包回报的结果变成模型读到的 `cordis_run` 工具结果。激活时什么都不装载,刷新后也不恢复;一页只在有人应答运行请求或在此主动要求时,才运行动态包。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 在组合里同时挂载了 host runner 的 web 客户端中挂载本插件——host 半跑在进程里,浏览器半跑在页面里。当某个带浏览器半的动态包被运行时,打开的页面会收到一次运行请求;本包在本页执行装载,UI 包(`ui-cordis`)则渲染人用来应答它的面板与卡片。纯 host 包不需要浏览器半,也就不需要页面:host 自己运行它们。
29
+
30
+ ### 页面会做什么
31
+
32
+ 浏览器半用纯 JavaScript 编写——无 JSX、无 TypeScript、不能 import 模块——并作为一个 async 函数运行。它拿到一组固定的名字——`React`、`console`、`styles` 与 `host`——而 `fetch`、`setTimeout` 这类浏览器全局不可用。返回的插件只能使用生命周期动词,以及它自己在 `inject` 里声明的服务。从已装载半调用 `host.call(method, args)` 会到达它自己的 host 半。React 渲染已装载半时发生的崩溃会上报 host,点名槽位、崩溃是否已把条目摘掉,以及写给作者的 message。
33
+
34
+ ### 运行界面提供什么
35
+
36
+ 运行界面可以应答一次待审批的 host 请求——批准它(可选地同时覆盖同一插件的未来版本)或拒绝它——也可以按用户自己的手势启动一个定义,该手势本身就是授权。每个定义最多有一个在途活动,因此基于该状态构建的控件能在 remount 后存活。界面就本页显示的内容都是页面本地的:每包最后一次渲染崩溃、本页自己的尝试为何失败,以及某个包是否已在本页装载——绝不是 host 眼中「在跑」的视图。
37
+
38
+ ### 生命周期边界
39
+
40
+ 装载是幂等的:要求装载这一页已在运行的 revision 不会改变任何东西,更新的 revision 顶替已装载的那个,同一 revision 在 retract 之后再装则重新装载。同一定义的操作串行执行。刷新按设计从干净状态开始——host 仍持有定义,本页在再次被要求之前不运行它。
41
+
42
+ -----
6
43
 
7
- ## 它做什么
44
+ <a id="understand-the-implementation"></a>
45
+ ## 理解实现
8
46
 
9
- 1. **事件订阅** —— 四条公告是转发的 host cordis 事件,所以本包经 `ctx.remote.$on` 消费 `cordis/request-run`、`cordis/request-run-resolved` 与 `dynamicCordisRunner/retract`,而 `$on` 的键面就是 api-remotes 的白名单。
10
- 2. **闭包求值** —— 浏览器半的源码作为一个 async 函数体运行,其参数即符号面(`React`、`console`、`styles`、`host`,外加遮蔽 `setTimeout`/`fetch`/`require` 的教学陷阱)。无 JSX、无 TypeScript、不能 import 模块。
11
- 3. **guard 门面** —— `apply` 收到的是真 fiber ctx 之上的白名单代理:生命周期动词,加上**返回的 plugin 自己在 `inject` 里声明**的服务(所以要用对象形态 `{ inject: ['slots'], apply(ctx) {} }` 才拿得到服务;裸函数没有声明位,拿不到任何服务)。`slots` 座位分配遮蔽 priority(注册即遮蔽,最新一次运行者胜出);`theme` 座位把覆盖层的 source 钉成包 id,并把它的 disposer 挂到 fiber 上。
12
- 4. **loader entry** —— 加了 guard 的插件被塞进模块表,再经 `loader.create` 挂载,于是动态包与静态包共享同一套激活门控、fiber effect 清理与状态投影。卸载 = 移除 entry + 失效 factory + 撤下样式。
13
- 5. **run 编排** —— 一条 `cordis/request-run` 事件问这一页要不要运行某个定义。回答的那一方按顺序把 run 跑完:先 host 半、再取源码、再浏览器半,最后一次回答带上结果。用户按下「运行」本身就是授权,同样走这条编排,只是没有要回答的对象;而纯 host 定义的 run 到 host 半就结束了 —— 这里没有第二半可取、也没有第二半可装。
14
- 6. **包内 RPC** —— 包内的 `host.call` 经 `dynamicCordisRunner` Remote namespace(`invoke`)转给它自己的 host 半,三种路由失败码各自变成对应的教学错误。两个方向都只驮 JSON:省略入参会以 `null` 过线(所以 `host.call('listServices')` 合法,handler 收到 `null`),而生成的 codec 拒收的载荷(函数、`undefined`、类实例)会变成一条点明「哪次调用 + 约定是什么」的教学错误,而不是 codec 那个光秃秃的字段名。
15
- 7. **渲染期失败回流** —— 槽位注册表的 supervision 接缝(`slots.onEntryError`)对页面上每一次 entry 边界崩溃都会通知;凡属于本 runner 落座过的包,那**一次**观察会分两个出口:一路上行给撰写它的会话(`reportRenderFailure`,给模型看),一路发布到本包 face 上的 `renderFailures`(给面板那一行看)。归属以 component 身份为键,在 guard 的 `register` 代理落座时记下 —— 注册表原样保存 component,所以不需要再维护一份与之同步的 entry 台账。这条通道纯属事后诊断:不驮任何 settle 权威、绝不触碰 run 的最终回答,而且报告本身失败时只吞不抛 —— 不让一次崩溃变成两次。
47
+ <details>
48
+ <summary>实现细节——点击展开</summary>
16
49
 
17
- ## 生命周期
50
+ 本节解释浏览器半背后的设计;可观察行为已在[使用本包](#use-this-package)中完整说明。
18
51
 
19
- 装载按 `(id, rev)` 对 live 态收敛:装载这一页已在运行的那个 revision 会**直接从 live 态回答**而不重装(所以被重播的 run 不会看起来没人回答),更新的 revision 顶替旧的,同一 revision 在 retract 之后再装则重新装载。同一定义的操作串行执行。
52
+ ### 设计理念
20
53
 
21
- 激活时什么都不装,刷新之后也不恢复 —— 一页只在有人回答了一次 run 请求、或有人在这一页主动要求时,才运行动态包。
54
+ 浏览器半建立在一个原则之上:动态包必须与静态包共享同一套激活门控、fiber effect 清理与状态投影。求值后的插件被塞进模块表,并经 `loader.create` 挂载;卸载 = 移除 entry + 失效 factory + 撤下样式。guard 是一份白名单——生命周期动词加已声明服务——与 host 侧沙箱门面对称,因此包作者在两侧面对同一个约定。一个观察者供两个出口:只有这里监视槽位注册表的 entry 错误接缝,凡属于本 runner 落座过的包的崩溃,一路上行给 host(给模型),一路发布到本包自己的 `renderFailures`(给面板)。
22
55
 
23
- ## run 界面读什么、调什么
56
+ ### 源码地图
24
57
 
25
- `ctx.dynamicCordisRunner` 就是全部的面:
58
+ | 文件 | 职责 |
59
+ |---|---|
60
+ | [`src/client/index.ts`](src/client/index.ts) | 插件入口:runner、编排器、inspect 注册表、转发事件订阅 |
61
+ | [`src/client/runtime.ts`](src/client/runtime.ts) | 装载引擎:按运行标识收敛、guard 挂载、撤回 |
62
+ | [`src/client/orchestrator.ts`](src/client/orchestrator.ts) | 运行编排:先 host 半、再取源码、再浏览器半、一次结算 |
63
+ | [`src/client/evaluator.ts`](src/client/evaluator.ts) | 闭包求值:符号面及其教学陷阱 |
64
+ | [`src/client/guard.ts`](src/client/guard.ts) | 已装载浏览器半收到的白名单 `ctx` façade |
65
+ | [`src/client/inspect-registry.ts`](src/client/inspect-registry.ts) | Client Inspect Provider 与待答查询路由器 |
66
+ | [`src/client/providers.ts`](src/client/providers.ts) | 第一方 client Inspect Provider(slots、theme、events) |
67
+ | [`src/client/timer.ts`](src/client/timer.ts) | 动态包注入的 client 定时器服务 |
26
68
 
27
- - `activeRuns` —— 每个定义唯一的在途活动:`awaiting-approval`(要回答的 requestId,加上这次询问的会话、包名与用途)或 `orchestrating`(这次 run 是为哪个会话在跑)。两条臂都带会话,因为归组属于这次 run 而不属于它的阶段;待确认那条还带着询问自己的文字,因为 `cordis_define` 什么都不播 —— 一个请求可以点名上一次注册表读取没覆盖到的定义,那时这条活动就是那一行唯一的来源。界面从它渲染、自己不留副本 —— 这正是控件能活过 remount 的原因。
28
- - `renderFailures` —— **本页**最后一次渲染崩溃,按定义索引(槽位、教学 message、以及这次崩溃是否已把 entry 从格位上摘掉),与 live 集合共用同一条通知通道。它按构造就是「本页当前」:包 stop、被 retract、或重新装载成功时即清空,所以界面可以直接照着渲染。host 那边另存一份「跨页面最后一次」给模型 —— 两份的归属与寿命本来就不同,界面**不要**改成回读 host 那份。
29
- - `lastRunError` —— 本页自己那次尝试为何失败,按定义索引。它比活动活得更久:host 只拆失败请求自己启动的那半,所以一个页面可能看着 host 报告为「在跑」的定义,而自己什么都没装上。
30
- - `approve(requestId)` / `decline(requestId)` / `startUserRun({ agentId, id, hasClientHalf })` —— 两条入口。三者都幂等(按 requestId,用户自发的 run 按定义 id),所以连点两次不会起两次 run。`hasClientHalf` 是必填:纯 host 定义没有源码可取,所以由调用方从它正在操作的注册表行里把这个事实说出来,而不是让编排器从一次失败的取码里反推。可回答的请求必然带浏览器半 —— 纯 host 定义是 host 自己起的,它不会去问页面。
31
- - `subscribe()` / `getSnapshot()` / `isLoaded(id)` —— 这一页装了什么。`isLoaded` 是页面本地的事实,永远不等于 host 说的「在跑」。
69
+ ### 一次 run 如何执行
32
70
 
71
+ 一条 `cordis/request-run` 事件问这一页要不要运行某个定义。作答的一方——审批后的页面,或按下运行的用户——驱动编排:先 host 半(host 半失败会在浏览器动作之前短路),再取源码,再浏览器半,最后一次结算带上发生的一切。浏览器半源码作为 async 函数体求值,符号面就是参数;返回的插件经 guard 包装后通过 loader 挂载;结算报告已装载的 revision,或失败阶段加闭包、guard 或 fiber 的消息。`host.call` 经 Remote namespace 路由;省略的入参以 `null` 过线,而生成 codec 拒收的载荷会变成一条点名「哪次调用 + 约定是什么」的教学错误。
72
+
73
+ </details>
74
+
75
+ -----
76
+
77
+ <a id="further-exploration"></a>
78
+ ## 进一步探索
79
+
80
+ 当包级约定不够用时阅读以下页面。它们从浏览器半逐步进入发问的 host、其运行被应答的工具,以及渲染它的界面。
81
+
82
+ - [Host runner](../cordis-host-runner/README.zh.md)——本包应答的注册表与运行往返。
83
+ - [工具包](../tool-cordis/README.zh.md)——运行请求到达本页的模型侧工具。
84
+ - [UI 包](../ui-cordis/README.zh.md)——操作这个面的面板与卡片。
85
+ - [extensions 子系统](../../../docs/subsystems/extensions.zh.md)——生成的 `ctx.dynamicCordisRunner` API 与 `cordis/*` 事件。
86
+ - [动态客户端渲染与附件归属 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.zh.md)——浏览器插件如何拥有自己的渲染与 CSS。
87
+ - [客户端外壳与动态包 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md)——浏览器半的包归属与构建面。
88
+
89
+ -----
90
+
91
+ <a id="model-experience"></a>
33
92
  ## 模型体验
34
93
 
35
94
  ### 由模型发起那次 run 的最终回答
36
95
 
37
- #### 模型看到什么
96
+ #### 模型看到的内容
38
97
 
39
- 本包自己不贡献任何工具、提示词或上下文;它为一次 `cordis/request-run` 往返发回的回答,是它撰写并到达模型的第一样内容 —— host 把它变成那个被阻塞的 `cordis_run` 的结果。成功时带上已装载的 revision,以及(当浏览器半挂在这一页没有的服务上时)那些服务的名字。失败时带一个 reason:用户拒绝的 `rejected`、`host-half-failed`、或 `client-half-failed`;后者还带上本包自己的文本 —— 出错阶段(`evaluate` / `module-import` / `activate`)加上闭包、guard 或 fiber 的消息。guard 的教学错误(未声明的服务、被遮蔽的浏览器全局、返回值里没有 `apply`)正是经这个字段到达模型的。而装载之后、React 渲染时才发生的崩溃,走下面那条独立的事后通道。
98
+ 本包自己不贡献任何工具、提示词或上下文;它撰写并到达模型的第一样内容,是为一次 `cordis/request-run` 往返发回的回答——host 把它变成那个被阻塞的 `cordis_run` 的结果。成功时带上已装载的 revision,以及(当浏览器半挂在这一页没有的服务上时)那些服务的名字。失败时带一个 reason:用户拒绝的 `rejected`、`host-half-failed` `client-half-failed`;后者还带上本包自己的文本——出错阶段(`evaluate`、`module-import` `activate`)加上闭包、guard 或 fiber 的消息。guard 的教学错误(未声明的服务、被遮蔽的浏览器全局、返回值里没有 `apply`)正是经这个字段到达模型的。而装载之后、React 渲染时才发生的崩溃,走下面那条独立的事后通道。
40
99
 
41
- #### token 影响
100
+ #### Token 影响
42
101
 
43
- 有条件且有界:每次 run 请求最多一个回答,花在 host 本来就会发出的那个 `cordis_run` 结果里。文本随数据而定(某个定义自己的错误消息),本包跨请求不留存任何东西 —— 一页后续的装载失败是页面本地诊断,在模型侧没有任何承载物。
102
+ 有条件且有界:每次 run 请求最多一个回答,花在 host 本来就会发出的那个 `cordis_run` 结果里。文本随数据而定(某个定义自己的错误消息),本包跨请求不留存任何东西——一页后续的装载失败是页面本地诊断,在模型侧没有任何承载物。
44
103
 
45
- #### KV cache 影响
104
+ #### KV Cache 影响
46
105
 
47
106
  只追加。回答只作为「本来就在途的那次请求」的工具结果到达模型、延长历史尾部;本包撰写的内容不会重写或重排更早的请求 token,因此原本可复用的前缀仍然可复用。同一定义的多次运行各自产出各自的结果,而不是替换更早那一个。
48
107
 
49
108
  ### run 落定之后的渲染期失败
50
109
 
51
- #### 模型看到什么
110
+ #### 模型看到的内容
111
+
112
+ 一个装载得干干净净的浏览器半,仍可能在 React 渲染时崩溃,而那次崩溃发生在 run 已经被回答之后——否则模型只会被告知「ok」,永远学不到。凡是本页落座过的包,其 entry 边界的每一次崩溃都会发回 host(`reportRenderFailure`):点名槽位、说明这次崩溃是否已把 entry 从格位上摘掉(`abdicated`:包的 UI 是没了、而不只是坏了),以及一条写给作者的 message。host 每包只留最后一条,用它 steer 所属会话,并经由 `cordis_inspect_self` 暴露;这条通道上的任何东西都不会进入 run 的最终回答。
113
+
114
+ #### Token 影响
115
+
116
+ 有条件,且其上界由 host 的留存策略决定、不由这一页决定:每次崩溃一条报告,而 host 每包只留最新一条——所以一个反复崩溃的 entry 对模型的代价是一条消息,而不是一张越来越长的清单。报告本身不会自带任何工具结果:模型只在被 steer 或主动去问的时候才为它付费。
117
+
118
+ #### KV Cache 影响
119
+
120
+ 自身没有。报告经 RPC 送出并被存起来,而不是追加进对话;模型通过一条 steer 消息或自己发起的查看读到它们,那次查看与任何工具结果一样只延长尾部。
121
+
122
+ ## 已知限制与延期工作
123
+
124
+ <a id="known-limitations-and-deferred-work"></a>
52
125
 
53
- 一个装载得干干净净的浏览器半,仍可能在 React 渲染时崩溃,而那次崩溃发生在 run 已经被回答之后 —— 否则模型只会被告知「ok」,永远学不到。凡是本页落座过的包,其 entry 边界的每一次崩溃都会发回 host(`reportRenderFailure`):点名槽位、说明这次崩溃是否已把 entry 从格位上摘掉(`abdicated`:包的 UI 是没了、而不只是坏了),以及一条写给作者的 message —— 崩溃文本,外加「文本里点到了某个被摘掉的浏览器全局、但文本自己没教」时补上的那句教学:绕过闭包陷阱的 `window.setInterval` 只会崩成 `is not a function`,它自己什么都解释不了。host 每包只留最后一条,经 `cordis_inspect` 透给模型;这条通道上的任何东西都不会进入 run 的最终回答。同一次观察还会落到 `renderFailures` 上给本页界面用 —— 一个观察者、两个出口,因为「跨页面最后一次崩溃(给模型)」与「这一页此刻正在显示什么」是两件寿命不同的事实。
54
126
 
55
- #### token 影响
127
+ 这些限制说明浏览器半何时需要特别小心。它们是当前包约束,不是任务积压。
56
128
 
57
- 有条件,且其上界由 host 的留存策略决定、不由这一页决定:每次崩溃一条报告,而 host 每包只留最新一条 —— 所以一个反复崩溃的 entry 对模型的代价是一段话,而不是一张越来越长的清单。报告本身不会自带任何工具结果:模型只在主动去问的时候才为它付费。
129
+ - **被拒绝的回答不会重试**——`resolveRequestRun` ack 不读,所以当 host 拒绝一个陈旧的成功答复(`accepted: false`——这一页装载期间定义的 revision 被顶掉了),这一页会保留已装的东西、也不再重新编排。那次请求仍可作答(别的页面作答或调用方取消都能收尾),而顶掉 revision 的那次 stop 会 retract 掉陈旧装载。
130
+ - **host namespace 存在之前插件一直挂起**——它声明 `remote.dynamicCordisRunner`,因此绝不会装载一个永远够不到自己 host 半的浏览器半。
131
+ - **槽位准入没有载体**——下发行声明的是服务,不是目标槽位,因此按部署的槽位允许/拒绝清单无处可驮。
132
+ - **guard 白名单是手抄的孪生**——浏览器 guard 复刻 host 侧沙箱门面;抽取共享规格留待后续。
58
133
 
59
- #### KV cache 影响
134
+ <a id="dev-note"></a>
135
+ ### 开发备注
60
136
 
61
- 自身没有。报告经 RPC 送出并被存起来,而不是追加进对话;模型是通过自己发起的一次查看读到它的,那次查看与任何工具结果一样只延长尾部。
137
+ <details>
138
+ <summary>维护者的工作上下文——点击展开</summary>
62
139
 
63
- ## 已知限制与欠账
140
+ 无。
64
141
 
65
- - **被拒绝的回答不会重试。** `resolveRequestRun` 的 ack 不读,所以当 host 拒绝一个陈旧的成功答复(`accepted: false` —— 这一页装载期间定义的 revision 被顶掉了),这一页会保留已装的东西、也不再重新编排。那次请求仍可作答(别的页面作答或调用方取消都能收尾),而顶掉 revision 的那次 stop 会 retract 掉这一页的陈旧装载。重试评估过、延后:竞态窗口只是一次往返内的一次 revision 递增。
66
- - 插件声明了 `remote.dynamic`,因此在 host 侧 namespace 存在之前一直挂起,而不是装载一些永远够不到自己 host 半的包。
67
- - 槽位准入(按部署的允许/拒绝清单)没有载体:下发行声明的是服务,不是目标槽位。
68
- - guard 白名单是 host 侧沙箱门面的手抄孪生;抽取共享规格留待后续。
142
+ </details>