dsh-gitbash-shell 0.40.1 → 0.40.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/CHANGELOG.md CHANGED
@@ -3,8 +3,23 @@
3
3
  > 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
4
4
  > 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。
5
5
 
6
+ ## v0.40.2 — 2026-10-17
7
+
8
+ ### 适配 dsh 0.2.1-alpha.5(0.2.1-rc.1 与其同码):组合对齐重跑 + exit-status 解析器跟齐(feat / fix / test)
9
+
10
+ - **组合行集跟随官方 0.2.1-alpha.3..alpha.5**(行 id 零改名;`tests/align-official.mjs` 重 pin 四枚 SHA256 后全绿):
11
+ - 官方在 alpha.3 **删除**了 subagent/subagent_fork 的 `toolFilter.deny`(`schedule_*` 四项——官方提醒工具重新可达子代理):插件侧 `subagentDeny` 整体删除,任何探针组合都不再产出 deny,与官方逐字节一致。
12
+ - alpha.4 给三个全工具 preset 新增 `tool-plugin-market` 行(紧跟 `tool-plugin-manager`;standard/ptc `disabled: true`,cordis `!!js !ctx.get('profileContext')`):挂新探针 `pluginMarket`(探测 `@deepseek-ai/dsh-plugin-market/tools`),disabled 分法与 manager 相同。
13
+ - alpha.3 给 **minimal** 新增 image-only `tool-fs`(`{read:false, readImage:true, write:false, edit:false}`)与专属 compaction 组(`isolate` 仅 `{compaction:true}`,**无** tool-result-pruner 行):`minimalPluginsFor` 增加 `hostExtras` 参数,挂 `compaction` 探针(探测 `@deepseek-ai/dsh-compaction-basic`)——minimal 不再是「永不加行」。
14
+ - `probeHostExtras` 扩为四探针(新增 plugin-market/tools、compaction-basic),返回 `{ timeContext, toolSchedule, pluginMarket, compaction }`;注册日志行同步;缺包/旧宿主仍保持 0.1.7 逐字节行集。
15
+ - **exit-status 解析器 null 感知(fix)**:`parseExitStatus` 的 exit 正则 `/(\d+)/` → `/(-?\d+|null)/`、`'null'` → `null`。官方 0.2.1-alpha.5 把 `TerminalResultView.exitCode` 放宽为 `number | null`(正常结束但无退出码),客户端卡模型(ui-tool `terminal-card-model.ts`)已解析 `[exit code: null]`,而本插件的移植解析器会把该标记当「未匹配」错落 `exitCode: 0`(读作成功)。本解析喂的是 terminal 卡(镜像 client 侧读法),故对齐 client 实现;官方服务端 `dsh-shell render.ts` 的同名解析器仍为 `(\d+)` 未跟(如实记录,不抄)。生产端 `renderResult` 对 `exitCode: null` 本就打出 `[exit code: null]`(与官方 background 断言一致),bash 工具 schema 早已允许 `integer|null`,均无需改。
16
+ - **测试同步**:`smoke.mjs` era 测试重写(默认无行 + probe-on 官方槽位 + deny **不得再出现** + minimal 四行断言)、`probeHostExtras` deepEqual 四键形状、minimal 接线正则(现在传 `hostExtras`)、`parseExitStatus` 加 null 例;`compatibility-regressions.mjs` 加 producer/parser 的 null 往返;`host-preview.mjs` hostExtras 形状同步。
17
+ - **元数据**:version 0.40.2;`dsh.compatibility.dshReleases` 增 `0.2.1-alpha.3`/`alpha.4`/`alpha.5`/`0.2.1-rc.1`(rc.1 于 2026-10-17 发布,与 alpha.5 同码——全仓 diff 仅 355 个 package.json 版本号 bump,align-official 对 rc.1 树复跑四 SHA 全绿;依据:静态对齐 + 全量回归;**Windows 双端真机验证待做**,发版前按家族纪律补)。
18
+ - 相关:dsh v0.2.1-alpha.3/4/5 Release;`tests/align-official.mjs`(需 dsh checkout,不属于 `npm test`)。
19
+
6
20
  ## v0.40.1 — 2026-10-15
7
21
 
22
+
8
23
  ### 清理 gitbash_native 的两个死配置读取(chore)
9
24
 
10
25
  - 删除 `gitbash_native` 模块对 `config.enableRunInBackground`/`config.promoteOnTimeout` 的读取([v0.40.0 已知项](#v0400--2026-10-15)就此销账):该模块由 inline 接线**手动挂载在 agent scope**(`scope.ctx.plugin({...module})` 无参调用),行/执行器 Config 不可达,两个字段自 v0.40.0 起读到的恒为 undefined、按 `!== false` 恒开——是"没接线的开关面板"而非活门控。清理后行为一个字节不变:`background` 仅由 Jobs 注册表在场决定(这正是实际生效的逻辑),超时转 Job 跟随后台可用性;用户级开关仍是 `experimentalNative` 总闸(要关 native 后台只能整体关闭 native/path,官方 tool-bash 的同名活开关不受影响)。
package/README.md CHANGED
@@ -70,10 +70,13 @@ Git Bash 增量;跟随 dsh 升级的对齐由冒烟测试
70
70
  **preset id 保持 `code-gitbash` 不变**——会话钉在 id 上,dsh 0.1.2 把内置 `code`
71
71
  改名 `ptc` 时本插件的变体 id 就没有跟随(改名会让已固定的会话报 preset not found)。
72
72
 
73
- **dsh 0.2.1 增量自动随行(v0.34.0)**:官方 0.2.1 给全工具 preset 加了时间上下文
74
- (`time-context`)与提醒工具(`schedule_*`,子代理不可用)——本插件在 0.2.1+ 宿主上
75
- 探测到对应包即自动把这两行与 subagent 的 schedule_* deny 纳入三个全工具变体
76
- (极简变体不加,与官方一致);旧宿主行为逐字节不变,**无需为此升级 dsh**。
73
+ **dsh 0.2.1 增量自动随行(v0.34.0 起,v0.40.2 对齐 alpha.5)**:官方 0.2.1 给全工具
74
+ preset 加了时间上下文(`time-context`)与提醒工具(`schedule_*`);alpha.3 又把这两行
75
+ 从 subagent 的 `toolFilter.deny` 里移除(提醒工具重新可达子代理),给极简变体加了
76
+ image-only `tool-fs` 与专属 compaction 组,alpha.4 给全工具 preset 加了插件市场
77
+ (`tool-plugin-market`)。本插件在 0.2.1+ 宿主上探测到对应包即自动把上述行纳入变体
78
+ (全工具三变体随探针取 `time-context`/`tool-schedule`/`tool-plugin-market`,极简
79
+ 变体只取 compaction 增量,与官方一致);旧宿主行为逐字节不变,**无需为此升级 dsh**。
77
80
 
78
81
  ## 安装(公开 npm 插件,推荐)
79
82
 
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-gitbash-shell",
3
- "version": "0.40.1",
3
+ "version": "0.40.2",
4
4
  "main": "./src/index.js",
5
5
  "description": "Windows 全模式 Git Bash:自带 bash 工具(私有 Git Bash 执行器实例,官方 PowerShell 执行器原样保留),官方四个模式经行内工具补位直接获得 Git Bash(v0.35.0 起;v0.39.0 起不再替换宿主执行器),四个 *·Git Bash 变体 preset 可开关;官方 pwsh 工具默认隐藏、开关可见(真 PowerShell);附带 gb_* FastCtx 文件工具,检测到 dsh-ops 自动让位。联动:dsh-ptc-cordis-preset(PTC 创造模式带 Git Bash 行且可去重)+ dsh-better-sidebar(终端标签自动切 Git Bash)。",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-gitbash-shell",
3
- "version": "0.40.1",
3
+ "version": "0.40.2",
4
4
  "description": "Windows 上给 agent 一个真正的 Git Bash 命令工具:私有 Git Bash 执行器实例(官方 PowerShell 执行器原样保留),统一 MSYS 盘根路径方言,官方 pwsh 工具默认隐藏可开关,物化四个内置预设的 Git Bash 变体,侧栏终端接管可开关。· Give agents a real Git Bash tool on Windows: a private Git Bash executor instance (the official PowerShell executor stays untouched), a unified MSYS drive-root path dialect, an opt-in official pwsh tool, and four Git Bash preset variants.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -36,7 +36,11 @@
36
36
  "dshReleases": {
37
37
  "0.2.0-rc.2": "compatible",
38
38
  "0.2.1-alpha.1": "compatible",
39
- "0.2.1-alpha.2": "compatible"
39
+ "0.2.1-alpha.2": "compatible",
40
+ "0.2.1-alpha.3": "compatible",
41
+ "0.2.1-alpha.4": "compatible",
42
+ "0.2.1-alpha.5": "compatible",
43
+ "0.2.1-rc.1": "compatible"
40
44
  }
41
45
  }
42
46
  },
@@ -48,16 +48,21 @@ export function escalationHintMarker(subject) {
48
48
  export const ESCALATION_TARGETS = Object.freeze(['workspace-write', 'danger-full-access'])
49
49
 
50
50
  /**
51
- * The official exit-status parse (dsh-shell render.ts:37): the LAST marker
52
- * line anchors the split between the output body and the exit pill.
51
+ * The official exit-status parse (the producer twin is dsh-shell render.ts,
52
+ * the reader twin is ui-tool terminal-card-model.ts — the card reader, which
53
+ * is what this parse feeds): the LAST marker line anchors the split between
54
+ * the output body and the exit pill. `null` is the official 0.2.1-alpha.5
55
+ * widening — the process ENDED without reporting a code; the card model reads
56
+ * an explicit null as a failure, while the marker-less fallback below still
57
+ * reads as success.
53
58
  * @param {string} text - the rendered result text.
54
- * @returns {{ body: string, exitCode?: number, signal?: string }}
59
+ * @returns {{ body: string, exitCode?: number | null, signal?: string }}
55
60
  */
56
61
  export function parseExitStatus(text) {
57
62
  const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text)
58
63
  if (signal && signal[1] !== undefined) return { body: text.slice(0, signal.index), signal: signal[1] }
59
- const exit = /\n\[exit code: (\d+)\]$/.exec(text)
60
- if (exit && exit[1] !== undefined) return { body: text.slice(0, exit.index), exitCode: Number(exit[1]) }
64
+ const exit = /\n\[exit code: (-?\d+|null)\]$/.exec(text)
65
+ if (exit && exit[1] !== undefined) return { body: text.slice(0, exit.index), exitCode: exit[1] === 'null' ? null : Number(exit[1]) }
61
66
  return { body: text, exitCode: 0 }
62
67
  }
63
68
 
@@ -7,12 +7,14 @@
7
7
  * of the committed assets/<variant>/agent.cordis*.yml texts the materializer
8
8
  * writes on older hosts.
9
9
  *
10
- * Every variant mirrors the official 0.1.7 preset it replaces (packages/
11
- * bundle/web-app/presets/{standard,minimal,ptc,cordis}.patch.yml) with the
12
- * Git Bash routing: full variants keep the official platform-gated shell
13
- * rows, and the inline patch adds OUR bash over a private executor on Windows.
14
- * The minimal variant instead keeps its persistent Git Bash terminal rows.
15
- * On non-Windows hosts the official stack stays native. A conditional delta exists while
10
+ * Every variant mirrors the official preset it replaces (packages/bundle/
11
+ * web-app/presets/{standard,minimal,ptc,cordis}.patch.yml) with the Git Bash
12
+ * routing: full variants keep the official platform-gated shell rows, and the
13
+ * inline patch adds OUR bash over a private executor on Windows. The minimal
14
+ * variant instead keeps its persistent Git Bash terminal rows. On non-Windows
15
+ * hosts the official stack stays native. Host-release deltas ride hostExtras
16
+ * probes (the block below the header rows) — each probe keeps the base set
17
+ * byte-identical to the 0.1.7 mirror. A conditional delta exists while
16
18
  * dsh-ptc-cordis-preset reports its experimental-CPython backend ACTIVE: the
17
19
  * two workflow rows follow the official Python composition and go off (see
18
20
  * `pythonActive` below). While that backend is off — the default, and also
@@ -82,18 +84,18 @@ export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hos
82
84
  const bashDisabled = win
83
85
  const pwshDisabled = !win
84
86
  const workflowOn = kind !== 'ptc' && pythonActive !== true
85
- // dsh 0.2.1 added two rows to every full-tool official preset — time-context
86
- // (durable clock readings) and tool-schedule (the reminder tools) — plus a
87
- // toolFilter deny of the four schedule_* tools on the subagent/subagent_fork
88
- // configs (reminders never reach a subagent). A preset row whose package is
89
- // absent rejects the WHOLE mount, so these ride a host probe (index.js
90
- // probeHostExtras, resolved from ctx.baseUrl — the base rows mount from);
91
- // absent/older hosts keep the byte-identical 0.1.7 row set. The deny list
92
- // follows toolSchedule alone: denying tools that do not exist is noise.
93
- const { timeContext = false, toolSchedule = false } = hostExtras
94
- const subagentDeny = toolSchedule
95
- ? { toolFilter: { deny: ['schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update'] } }
96
- : {}
87
+ // dsh 0.2.1 grew the official full-tool presets across releases, all
88
+ // row-id-stable: alpha.2 added time-context (durable clock readings) and
89
+ // tool-schedule (the reminder tools); alpha.3 DELETED the schedule_*
90
+ // toolFilter deny from the subagent/subagent_fork configs (official
91
+ // reminders reach subagents again — the deny was never load-bearing,
92
+ // nothing replaces it); alpha.4 added a tool-plugin-market row to every
93
+ // full preset. A preset row whose package is absent rejects the WHOLE
94
+ // mount, so these ride a host probe (index.js probeHostExtras, resolved
95
+ // from ctx.baseUrl — the base rows mount from); absent/older hosts keep
96
+ // the byte-identical 0.1.7 row set. The minimal variant carries its own
97
+ // 0.2.1-alpha.3 delta (tool-fs + compaction) behind the compaction flag.
98
+ const { timeContext = false, toolSchedule = false, pluginMarket = false, compaction = false } = hostExtras
97
99
  const rows = [
98
100
  {
99
101
  id: 'persona',
@@ -170,12 +172,12 @@ export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hos
170
172
  {
171
173
  id: 'tool-subagent',
172
174
  name: '@deepseek-ai/dsh-tool-subagent',
173
- config: { provider: 'spawn', toolName: 'subagent', modelSelectionSettings: true, ...(timeContext ? {} : { backgroundMode: 'continuable' }), ...subagentDeny },
175
+ config: { provider: 'spawn', toolName: 'subagent', modelSelectionSettings: true, ...(timeContext ? {} : { backgroundMode: 'continuable' }) },
174
176
  },
175
177
  {
176
178
  id: 'tool-subagent-fork',
177
179
  name: '@deepseek-ai/dsh-tool-subagent',
178
- config: { provider: 'fork', toolName: 'subagent_fork', ...(timeContext ? {} : { backgroundMode: 'continuable' }), ...subagentDeny },
180
+ config: { provider: 'fork', toolName: 'subagent_fork', ...(timeContext ? {} : { backgroundMode: 'continuable' }) },
179
181
  },
180
182
  {
181
183
  id: 'workflow-ptc',
@@ -226,6 +228,14 @@ export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hos
226
228
  // on wherever a profile context exists (every web deployment does).
227
229
  ...off(kind !== 'cordis'),
228
230
  })
231
+ if (pluginMarket) {
232
+ rows.push({
233
+ // The official 0.2.1-alpha.4 rows: same disabled split as the manager.
234
+ id: 'tool-plugin-market',
235
+ name: '@deepseek-ai/dsh-plugin-market/tools',
236
+ ...off(kind !== 'cordis'),
237
+ })
238
+ }
229
239
  return rows
230
240
  }
231
241
 
@@ -241,9 +251,14 @@ export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hos
241
251
  * pins the historical default, so a missing Git stays a VISIBLE spawn
242
252
  * failure — never a substitute shell (issue #15: the hardcoded default
243
253
  * broke every PTY spawn where Git lives outside `C:/Program Files/Git`).
254
+ * @param {object} [input.hostExtras] - the host probe (probeHostExtras):
255
+ * `compaction` true adds the official 0.2.1-alpha.3 minimal rows (an
256
+ * image-read-only tool-fs plus the pruner-less compaction group) ahead of
257
+ * the terminal.
244
258
  * @returns {object[]} the declarative plugins list.
245
259
  */
246
- export function minimalPluginsFor({ bashPath } = {}) {
260
+ export function minimalPluginsFor({ bashPath, hostExtras = {} } = {}) {
261
+ const { compaction = false } = hostExtras
247
262
  const win = typeof process !== 'undefined' && process.platform === 'win32'
248
263
  return [
249
264
  {
@@ -255,6 +270,27 @@ export function minimalPluginsFor({ bashPath } = {}) {
255
270
  includeRuntimeContext: false,
256
271
  },
257
272
  },
273
+ ...(compaction ? [
274
+ {
275
+ // Official 0.2.1-alpha.3 minimal: image reads ride the fs tool; every
276
+ // other fs verb stays with the persistent terminal.
277
+ id: 'tool-fs',
278
+ name: '@deepseek-ai/dsh-tool-fs',
279
+ config: { read: false, readImage: true, write: false, edit: false },
280
+ },
281
+ {
282
+ // Official 0.2.1-alpha.3 minimal compaction group: NO pruner row
283
+ // (isolate carries compaction alone, unlike the full presets).
284
+ id: 'compaction',
285
+ name: 'cordis:group',
286
+ group: true,
287
+ isolate: { compaction: true },
288
+ config: [
289
+ { id: 'compaction-basic', name: '@deepseek-ai/dsh-compaction-basic' },
290
+ { id: 'command-compact', name: '@deepseek-ai/dsh-command-compact' },
291
+ ],
292
+ },
293
+ ] : []),
258
294
  {
259
295
  id: 'persistent-shell',
260
296
  name: 'cordis:group',
package/src/index.js CHANGED
@@ -1765,14 +1765,16 @@ function cleanupLegacyTrees(presetIds) {
1765
1765
  * package is absent rejects the WHOLE mount, so the rows ride this probe: each
1766
1766
  * is added only when its package resolves from the HOST's module base —
1767
1767
  * `ctx.baseUrl`, the exact base `prepareProfileEntries` mounts preset rows
1768
- * from, so the probe can never disagree with the mounter. On dsh <= 0.2.0 both
1768
+ * from, so the probe can never disagree with the mounter. On dsh <= 0.2.0 all
1769
1769
  * stay false and the row set stays byte-identical to the 0.1.7 mirror. Probed
1770
1770
  * once per boot; the profile's package set does not change under a running
1771
- * host. minimal never gains the rows (the official minimal preset has none).
1771
+ * host. `compaction` is the one flag minimal consumes (the official
1772
+ * 0.2.1-alpha.3 minimal rows); the rest only shape the full variants.
1772
1773
  * @param {object} ctx - cordis context (any fiber of the running host).
1773
1774
  * @param {Function} [resolve] - predicate override for tests; receives a
1774
1775
  * package specifier, resolves like `require.resolve`, returns boolean.
1775
- * @returns {Promise<{timeContext: boolean, toolSchedule: boolean}>}
1776
+ * @returns {Promise<{timeContext: boolean, toolSchedule: boolean,
1777
+ * pluginMarket: boolean, compaction: boolean}>}
1776
1778
  */
1777
1779
  export async function probeHostExtras(ctx, resolve) {
1778
1780
  let ok = resolve
@@ -1787,11 +1789,13 @@ export async function probeHostExtras(ctx, resolve) {
1787
1789
  ok = () => false
1788
1790
  }
1789
1791
  }
1790
- const [timeContext, toolSchedule] = await Promise.all([
1792
+ const [timeContext, toolSchedule, pluginMarket, compaction] = await Promise.all([
1791
1793
  Promise.resolve(safeProbe(ok, '@deepseek-ai/dsh-time-context')).then(Boolean),
1792
1794
  Promise.resolve(safeProbe(ok, '@deepseek-ai/dsh-tool-schedule')).then(Boolean),
1795
+ Promise.resolve(safeProbe(ok, '@deepseek-ai/dsh-plugin-market/tools')).then(Boolean),
1796
+ Promise.resolve(safeProbe(ok, '@deepseek-ai/dsh-compaction-basic')).then(Boolean),
1793
1797
  ])
1794
- return { timeContext, toolSchedule }
1798
+ return { timeContext, toolSchedule, pluginMarket, compaction }
1795
1799
  }
1796
1800
 
1797
1801
  /** Run one probe predicate; ANY failure reads as "absent" (a throwing
@@ -1871,7 +1875,7 @@ async function registerVariant(ctx, presetId, { gitBashActive, skillsDir, python
1871
1875
  const meta = PRESET_META[presetId]
1872
1876
  if (!meta) return undefined
1873
1877
  const plugins = meta.kind === 'minimal'
1874
- ? minimalPluginsFor({ bashPath: minimalPtyBashPath(readBashResolution) })
1878
+ ? minimalPluginsFor({ bashPath: minimalPtyBashPath(readBashResolution), hostExtras })
1875
1879
  : pluginsFor({ kind: meta.kind, gitBash: gitBashActive, skillsDir, pythonActive, hostExtras })
1876
1880
  return ctx.agentPresets.register({
1877
1881
  id: presetId,
@@ -1908,9 +1912,10 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
1908
1912
  cleanupLegacyTrees(presetIds)
1909
1913
  const gitBashActive = process.platform === 'win32'
1910
1914
  const skillsDir = await resolveSkillsDir()
1911
- // The dsh 0.2.1 additions (time-context / tool-schedule rows) probe once per
1912
- // boot and ride every (re-)registration below — the profile's package set
1913
- // cannot change under a running host.
1915
+ // The dsh 0.2.1 additions (time-context / tool-schedule / plugin-market /
1916
+ // minimal compaction rows) probe once per boot and ride every
1917
+ // (re-)registration below — the profile's package set cannot change under a
1918
+ // running host.
1914
1919
  const hostExtras = await probeHostExtras(ctx)
1915
1920
  /** presetId -> unregister, i.e. exactly what is live right now. */
1916
1921
  const live = new Map()
@@ -2007,8 +2012,13 @@ async function runDeclarativeEra(ctx, presetIds, readSuppress, readBashResolutio
2007
2012
  console.log(TAG + ' registered ' + live.size + ' preset(s) declaratively (' + [...live.keys()].join(', ') + ')'
2008
2013
  + (live.size === 0 ? ' — variantPresets is off: the official presets carry Git Bash through the inline tool patch (see the CHANGELOG)'
2009
2014
  : '')
2010
- + (hostExtras.timeContext || hostExtras.toolSchedule
2011
- ? ' [dsh 0.2.1 additions: ' + (hostExtras.timeContext ? 'time-context' : '') + (hostExtras.timeContext && hostExtras.toolSchedule ? ' + ' : '') + (hostExtras.toolSchedule ? 'tool-schedule (subagents denied)' : '') + ']'
2015
+ + (hostExtras.timeContext || hostExtras.toolSchedule || hostExtras.pluginMarket || hostExtras.compaction
2016
+ ? ' [dsh 0.2.1 additions: ' + [
2017
+ hostExtras.timeContext ? 'time-context' : '',
2018
+ hostExtras.toolSchedule ? 'tool-schedule' : '',
2019
+ hostExtras.pluginMarket ? 'plugin-market' : '',
2020
+ hostExtras.compaction ? 'minimal compaction' : '',
2021
+ ].filter(Boolean).join(' + ') + ']'
2012
2022
  : ''))
2013
2023
 
2014
2024
  // The peer's capability: `ctx.inject` makes this independent of row order,