diff --git a/.agents/notes/README.i18n.yaml b/.agents/notes/README.i18n.yaml index f6289cf96f..4b6ef324b9 100644 --- a/.agents/notes/README.i18n.yaml +++ b/.agents/notes/README.i18n.yaml @@ -1,6 +1,46 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/README.md -README.md: 6872de0a1390fa5a1707b9f464c9e0edeb2da4be -README.zh.md: ced81f77fa904ad0b5cb5219383ef651d87392b8 +/agent-notes: + en: a10991a192c7b55d + zh: c2e5b938ece4f366 +/agent-notes/layout-and-naming: + en: 6b38cb0eb38c2baf + zh: 535234fb23fc8921 +/agent-notes/classification: + en: 055a5e49f9af3dc2 + zh: 9f7c78409cccfded +/agent-notes/archiving-and-deletion: + en: 57ccc5fec461ebb0 + zh: 69e8f9e79abc53aa +/agent-notes/when-to-write-one: + en: 2beb1b94d8fb633c + zh: 889b5542bb771205 +/agent-notes/the-file-format: + en: 601f85df49f80fc7 + zh: acbd0955d6543b13 +/agent-notes/the-file-format/the-header-block: + en: 16fdaab0c74a5bb5 + zh: 2553f145a1eb878f +/agent-notes/the-file-format/the-body-skeleton: + en: 740b7e964b9aec37 + zh: 8551ddef1ffa776a +/agent-notes/the-file-format/the-body-skeleton/proposed: + en: acc0c035c5ca6fd7 + zh: 8a65816dfac9e580 +/agent-notes/the-file-format/the-body-skeleton/implemented: + en: f3523d7e40cc4e2f + zh: 574fa8959f8829a1 +/agent-notes/the-file-format/the-body-skeleton/rejected: + en: f0ff5d05191f2315 + zh: 3f9df0dd01a5cbed +/agent-notes/the-file-format/alternatives-considered-mandatory: + en: fe1bf385aee54033 + zh: 37cb4ea86ec6d99d +/agent-notes/the-file-format/moving-between-lifecycles: + en: d3f18ba194dfb677 + zh: 3d202868deda2f31 +/agent-notes/the-file-format/chinese-counterparts: + en: d72a33b7c63722b2 + zh: f8320c91b1f77ae6 diff --git a/.agents/notes/README.md b/.agents/notes/README.md index 6872de0a13..8cee939fcf 100644 --- a/.agents/notes/README.md +++ b/.agents/notes/README.md @@ -39,7 +39,7 @@ Delete an implemented Agent Note that only describes small UI adjustments or pur The archive is path-encoded as `archived/{class}/yyyy-mm-dd-topic-title.md`; `implemented` is deliberately absent because only implemented notes can enter it. An archival change moves the complete English/Chinese/sidecar triplet, retains `Status: implemented`, inserts the same `Archived: YYYY-MM-DD` line immediately below that status in both language files, re-records the sidecar, and repairs or deletes inbound links. These are the only permitted content changes during archival. Existing title punctuation, blank-line layout, and language-switcher wording do not block archival; preserve them with the body. -Once sealed, every archived triplet is permanently frozen. Do not edit, translate, reformat, update, move, or delete it, and do not treat it as authority for current behavior. Documentation gates skip archived sources, including their outbound links; active prose may still link into an archived note when it intentionally cites history. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) enforces the closed class tree, complete triplets, archive metadata, sidecar hashes, and the append-only frozen-content manifest. The [archive-policy Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.md) owns the rationale. +Once sealed, every archived triplet is permanently frozen. Do not edit, translate, reformat, update, move, or delete it, and do not treat it as authority for current behavior. Documentation gates skip archived sources, including their outbound links; active prose may still link into an archived note when it intentionally cites history. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) enforces the closed class tree, complete triplets, archive metadata, sidecar hashes, and the append-only frozen-content manifest, except for the exact seal transitions documented in [archive instructions](archived/AGENTS.md). The [archive-policy Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.md) owns the rationale. ## When to write one diff --git a/.agents/notes/README.zh.md b/.agents/notes/README.zh.md index ced81f77fa..6339f56f7c 100644 --- a/.agents/notes/README.zh.md +++ b/.agents/notes/README.zh.md @@ -41,7 +41,7 @@ 归档路径编码为 `archived/{class}/yyyy-mm-dd-topic-title.md`;其中有意省略 `implemented`,因为只有 implemented Agent Note 可以进入归档。归档变更会移动完整的英文、中文和伴随记录三个文件,保留 `Status: implemented`,在两种语言的文件中紧接该状态行插入相同的 `Archived: YYYY-MM-DD` 行,重新记录伴随记录,并修复或删除入站链接。归档时只允许对内容做这些更改。 既有标题标点、空行布局和语言切换栏措辞不阻止归档;与正文一起保留。 -封存后,每组归档文件都永久冻结。禁止编辑、翻译、重新格式化、更新、移动或删除,也不得将其视为当前行为的权威依据。文档门禁会跳过归档源文件,包括其中的出站链接;当活跃文档有意引用历史时,仍可链接到归档 Agent Note。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 强制执行封闭的类别目录树、完整的三文件配对、归档元数据、伴随记录 hash,以及仅追加的冻结内容 manifest(元数据清单)。[归档政策 Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.zh.md) 记录了设计依据。 +封存后,每组归档文件都永久冻结。禁止编辑、翻译、重新格式化、更新、移动或删除,也不得将其视为当前行为的权威依据。文档门禁会跳过归档源文件,包括其中的出站链接;当活跃文档有意引用历史时,仍可链接到归档 Agent Note。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 强制执行封闭的类别目录树、完整的三文件配对、归档元数据、伴随记录 hash,以及仅追加的冻结内容 manifest(元数据清单),但[归档说明](archived/AGENTS.md)中记录的精确封存哈希转换除外。[归档政策 Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.zh.md) 记录了设计依据。 diff --git a/.agents/notes/archived/AGENTS.md b/.agents/notes/archived/AGENTS.md index 2ac4518a6a..d013ed068e 100644 --- a/.agents/notes/archived/AGENTS.md +++ b/.agents/notes/archived/AGENTS.md @@ -5,3 +5,5 @@ Archived Agent Note triplets under the kind directories are frozen historical sn The archival change may only relocate a complete English/Chinese/sidecar triplet, insert the identical `Archived: YYYY-MM-DD` line below both `Status: implemented` lines, re-record the sidecar, and repair or delete inbound links. Do not inspect, verify, or repair links out of archived notes. Run the [`dsh-archive-agent-notes`](../../skills/dsh-archive-agent-notes/SKILL.md) workflow and append new artifact hashes with `pnpm run verify-archived-agent-notes --write`. The normal verifier rejects changed or missing sealed artifacts, incomplete triplets, unknown kind folders, and invalid archive metadata. + +The authorized Figma-link removal from `feature/2026-08-10-durable-workflow-runs-in-chat` has three exact old-to-new seal exceptions in `scripts/archived-agent-notes.ts` for its English, Chinese, and pairing files. These exceptions permit no other content change, reversal, or deletion. diff --git a/packages/settings/settings-file/README.i18n.yaml b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml similarity index 51% rename from packages/settings/settings-file/README.i18n.yaml rename to .agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml index 795c07205e..b5321c036f 100644 --- a/packages/settings/settings-file/README.i18n.yaml +++ b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # 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/settings/settings-file/README.md -README.md: ea578a1739e1bd5d7b7f47be596998b124337ed5 -README.zh.md: 668d771bd64738701de87ca28066234284941873 +# pnpm run verify-translation-pairing --write +2026-09-24-web-default-schedule-composition.md: 3f27bb09ebc00d42c32563e17a1de82c8e525be1 +2026-09-24-web-default-schedule-composition.zh.md: 9e0e1fb77ce89b579e1bc8b0d4d0a1b9f4e9fea5 diff --git a/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.md b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.md new file mode 100644 index 0000000000..3f27bb09eb --- /dev/null +++ b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.md @@ -0,0 +1,40 @@ +# Agent Note: Schedule in the shipped Web composition + +Status: implemented +Archived: 2026-09-25 + +English | [中文](2026-09-24-web-default-schedule-composition.zh.md) + +## Problem + +The Automation tasks page, the Session reminder catalog, and the `schedule_*` tools reached a Web deployment only through `--patch apps/cli/config/examples/schedule/cordis.yml`. `packages/bundle/web-app/cordis.patch.yml` carried the `ui-schedule` client row with `disabled: true`, and the Host rows `time-context` and `schedule` existed in that overlay alone. A person running the shipped `web` profile therefore saw no Automation tasks entry and no reminder tools, while every consumer that wanted them repeated the same three rows: the repository preview image kept its own overlay list, and two Web suites hard-coded the overlay path. + +## Decision + +`packages/bundle/web-app/cordis.patch.yml` inserts `time-context` and `schedule` in its Host row list and leaves `ui-schedule` enabled; `packages/bundle/web-app/package.json` declares both packages, which `verify-cordis-config` requires for a bare row name in a bundle patch. The `web` profile consequently ships the Automation tasks page, the Session-header reminder clock, the idle Session row's clock mark and hover list, the right-Sidebar task tab, and `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete` on every live root Agent. + +`apps/cli/config/examples/schedule/cordis.yml` is deleted. `applyEntryPatches` appends an `insert` list without de-duplicating ids, so keeping the overlay would mount `time-context` and `schedule` a second time; the two Web suites and the preview packer that named the overlay compose the shipped profile alone. + +`time-context` ships with Schedule because a reminder request states a wall-clock target. The plugin appends one durable user message per eligible step carrying the sampled instant, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. The sampling instant is what lets the model turn a request such as "tomorrow at nine" into an offset-bearing `at` value; the [Schedule subsystem](../../../../docs/subsystems/schedule.md) owns that interpretation boundary and the explicit-zone requirement it feeds. + +The Host service, its storage domain, and the client half are unchanged. This decision moves which composition mounts them, and the [Host-owned scheduled messages decision](2026-09-16-host-schedule-storage.md) continues to own task storage, activation, dispatch, and delivery records. + +## Recorded sessions + +A Web scenario that drives a live step now logs one time-context reading per step, so its committed Session fixture and header sidecars were refreshed. A reading's sampled instant, browser zone, and elapsed duration are volatile, and `normalizeWebSessionVolatiles` in `apps/web/tests/scaffold.ts` replaces them with `{{timeContextTimestamp}}`, `{{clientTimeZone}}`, and `{{elapsed}}`. The turn and step numbers and the preceding-event baseline stay readable, so a fixture still shows what the model received. Refresh writes the current writer generation (`session.v4.jsonl`) beside the retained predecessors, which remain committed replay baselines. + +## Alternatives considered + +**Keep the overlay and change nothing.** The shipped surface would keep contradicting the product documentation that describes the Automation tasks entry, and each consumer would keep its own copy of the three rows. The overlay also cannot satisfy the request that the default `dsh web` process exposes the page without a flag. + +**Ship the Schedule rows without `time-context`.** The model would then have no sampling instant of its own, so an unqualified date or time in a reminder request would depend on a separate clock source; the overlay paired the two rows for that reason. + +**Reach for a separate optional bundle.** `OPTIONAL_BUNDLES` exists for bundles a person switches on from the plugin manager, which is the same opt-in shape as the overlay; the decision here is that a default Web surface owns the page and the tools. + +## Consequences + +- Every Web session's request header carries four additional tool schemas, and every eligible step appends one durable user message. A conversation that never creates a reminder pays that token cost. +- The clock reading is model-visible and durable, so it replays, compacts, and appears in exported Session logs like any other user message. +- A deployment that wants the previous behavior disables the `time-context`, `schedule`, and `ui-schedule` rows in its own profile patch layer; the capability itself is untouched. +- The `cordis_inspect_query` `listTools` answer for a full preset table now passes the base composition's 12,500-token inline budget, so the spill policy retains that answer's head and tail with a spill path instead of the complete JSON. +- The repository preview image and both Web suites drop their overlay arguments, so one composition change reaches every surface at once. diff --git a/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.zh.md b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.zh.md new file mode 100644 index 0000000000..9e0e1fb77c --- /dev/null +++ b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.zh.md @@ -0,0 +1,40 @@ +# Agent Note: 默认 Web 组合中的 Schedule + +Status: implemented +Archived: 2026-09-25 + +[English](2026-09-24-web-default-schedule-composition.md) | 中文 + +## Problem + +自动化任务页面、Session 提醒目录以及 `schedule_*` 工具此前只能通过 `--patch apps/cli/config/examples/schedule/cordis.yml` 进入某个 Web 部署:`packages/bundle/web-app/cordis.patch.yml` 里的 `ui-schedule` 客户端行带 `disabled: true`,而宿主行 `time-context` 与 `schedule` 只存在于那个 overlay 中。因此运行发布版 `web` profile 的人既看不到自动化任务入口,也拿不到提醒工具;同时每个需要它们的消费者都要重复同样的三行:仓库预览镜像自行维护了一份 overlay 列表,两个 Web 测试套件各自硬编码了 overlay 路径。 + +## Decision + +`packages/bundle/web-app/cordis.patch.yml` 在其宿主行列表中插入 `time-context` 与 `schedule`,并让 `ui-schedule` 保持启用;`packages/bundle/web-app/package.json` 声明这两个包,这是 `verify-cordis-config` 对 bundle patch 中裸包名的要求。因此 `web` profile 会交付自动化任务页面、Session 页头的提醒时钟、空闲 Session 行的时钟标记与悬停列表、右侧栏任务页签,并在每个 live 根 Agent 上注册 `schedule_create`、`schedule_list`、`schedule_update` 与 `schedule_delete`。 + +`apps/cli/config/examples/schedule/cordis.yml` 已删除。`applyEntryPatches` 追加 `insert` 列表时不对 id 去重,保留该 overlay 会把 `time-context` 与 `schedule` 挂载两次;原本引用它的两个 Web 测试套件与预览打包器现在只组合发布版 profile。 + +`time-context` 随 Schedule 一起发布,因为提醒请求本身给出的是墙上时钟目标。该插件在每个符合条件的步骤追加一条持久 user 消息,包含采样瞬时、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。正是采样瞬时让模型能把「明天九点」这类请求落成带偏移量的 `at` 值;该解释边界及其要求的显式时区由 [Schedule 子系统](../../../../docs/subsystems/schedule.zh.md)负责。 + +宿主服务、其存储 domain 与客户端半边都没有改动。本决策改变的是由哪个组合挂载它们,任务存储、激活、投递与投递记录仍由[宿主定时消息决策](2026-09-16-host-schedule-storage.zh.md)负责。 + +## Recorded sessions + +驱动 live 步骤的 Web 场景现在每步记录一条 time-context 读数,因此其已提交的 Session 夹具与页头 sidecar 都已重刷。一条读数中的采样瞬时、浏览器时区与经过时长是易变值,`apps/web/tests/scaffold.ts` 的 `normalizeWebSessionVolatiles` 会把它们替换为 `{{timeContextTimestamp}}`、`{{clientTimeZone}}` 和 `{{elapsed}}`。turn 与 step 序号以及"前一条事件"的基线保持可读,因此夹具仍能显示模型收到的内容。重刷会在已保留的上一代旁写入当前 writer 代次(`session.v4.jsonl`),旧代次继续作为已提交的回放基线保留。 + +## Alternatives considered + +**保留 overlay,不做任何改动。** 发布版界面会继续与描述该自动化任务入口的产品文档相矛盾,而每个消费者都要各自保留那三行的副本。overlay 也无法满足「默认 `dsh web` 进程不带 flag 就暴露该页面」这一要求。 + +**只发布 Schedule 两行,不带 `time-context`。** 模型将没有自己的采样瞬时,于是提醒请求中未明确限定时区的日期或时间要依赖另一个时钟来源;overlay 当初正是为此把两行配对。 + +**另建一个可选 bundle。** `OPTIONAL_BUNDLES` 面向的是用户在插件管理器里主动开启的 bundle,与 overlay 是同一种 opt-in 形态;本决策要的是默认 Web 界面自己拥有该页面与这些工具。 + +## Consequences + +- 每个 Web 会话的请求头多出四个工具 schema,且每个符合条件的步骤追加一条持久 user 消息。从不创建提醒的对话也要承担这份 token 成本。 +- 时钟读数对模型可见且持久,因此它与其他 user 消息一样参与回放、压缩,并出现在导出的 Session 日志中。 +- 想要恢复旧行为的部署,可在自己的 profile patch 层禁用 `time-context`、`schedule` 与 `ui-schedule` 三行;能力本身没有改动。 +- 完整预设表的 `cordis_inspect_query` `listTools` 答案现在超过基础组合 12,500 token 的内联预算,因此 spill 策略改为保留该答案的首尾并给出 spill 路径,而不是完整 JSON。 +- 仓库预览镜像与两个 Web 测试套件都不再传 overlay 参数,一处组合改动即同时作用于所有界面。 diff --git a/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.i18n.yaml b/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.i18n.yaml index 284f43658a..871a8eefb8 100644 --- a/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.i18n.yaml +++ b/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.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/archived/feature/2026-08-10-durable-workflow-runs-in-chat.md -2026-08-10-durable-workflow-runs-in-chat.md: 01c59ef25996d6349ab2904f148bf4e72fdd5f09 -2026-08-10-durable-workflow-runs-in-chat.zh.md: 5f6b39b59beca5e2363261605ec58fbdcf71ea6f +2026-08-10-durable-workflow-runs-in-chat.md: a49cb75e44bd65ef694679eeb295cfef564c7545 +2026-08-10-durable-workflow-runs-in-chat.zh.md: 753e02238cd8c00d0494cb673ba9896d61ff4cd8 diff --git a/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.md b/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.md index 01c59ef259..a49cb75e44 100644 --- a/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.md +++ b/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.md @@ -25,7 +25,7 @@ The renderer gives each level a distinct visual responsibility. The run uses a 3 Navigation is derived from two current authorities rather than persisted. A member row is interactive only while its durable member state is running and the current ordinary Session list contains the same id with `origin: 'subagent'`, `parentId` equal to the displayed parent, and `running: true`. Underlined member text is the only visible affordance; keyboard focus draws a two-pixel business-primary ring around the name area, and the fixed status label remains the lifecycle word rather than an action instruction. The renderer invokes only the injected ordinary `sessions.open(id)` callback. Addressed-only, remote, wrong-parent, and terminal members remain visible but static. -The [seven-state Figma reference](https://www.figma.com/design/tguwzZRmHCjbq58mfsqT0M?node-id=5-2) fixes the information hierarchy for running expanded/collapsed, completed history/expanded, failed plus cancelled, interrupted recovery, and dark narrow presentation. Repository `DisclosureRow`, `StateDot`, icons, semantic tokens, and keyed-node behavior remain the implementation authority; the reference introduces no runtime field or state owner. +The seven-state Figma reference fixes the information hierarchy for running expanded/collapsed, completed history/expanded, failed plus cancelled, interrupted recovery, and dark narrow presentation. Repository `DisclosureRow`, `StateDot`, icons, semantic tokens, and keyed-node behavior remain the implementation authority; the reference introduces no runtime field or state owner. ## Verification diff --git a/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.zh.md b/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.zh.md index 5f6b39b59b..753e02238c 100644 --- a/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.zh.md +++ b/.agents/notes/archived/feature/2026-08-10-durable-workflow-runs-in-chat.zh.md @@ -25,7 +25,7 @@ renderer 为每一层分配不同视觉职责。运行使用 32 像素 module-pl 导航从两个当前权威派生,不写入持久记录。只有持久成员状态仍为运行中,且当前普通 Session 列表包含同一 id、`origin: 'subagent'`、`parentId` 等于当前父 Session、`running: true` 时,成员行才可交互。带下划线的成员文字是唯一可见提示;键盘聚焦时,名称区显示 2 像素 business-primary 焦点环,固定状态列继续只表达生命周期,而不写动作说明。renderer 只调用注入的普通 `sessions.open(id)` 回调。仅地址化、远程、父级不符或终态成员继续可见,但保持静态。 -[七状态 Figma 参考](https://www.figma.com/design/tguwzZRmHCjbq58mfsqT0M?node-id=5-2)固定运行展开/收起、完成历史/展开、失败与取消、恢复后中断以及暗色窄列的信息层级。仓库的 `DisclosureRow`、`StateDot`、图标、语义 token 和 keyed-node 行为仍是实现权威;参考稿不引入运行时字段或状态 owner。 +七状态 Figma 参考固定运行展开/收起、完成历史/展开、失败与取消、恢复后中断以及暗色窄列的信息层级。仓库的 `DisclosureRow`、`StateDot`、图标、语义 token 和 keyed-node 行为仍是实现权威;参考稿不引入运行时字段或状态 owner。 ## 验证 diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json index 6fc82fe8a6..44178cc0ec 100644 --- a/.agents/notes/archived/manifest.json +++ b/.agents/notes/archived/manifest.json @@ -376,6 +376,9 @@ "architecture/2026-09-11-wasm-preview-font-and-image-budgets.i18n.yaml": "sha256:af027c7dae53a181e4d73fd2309f69e9ae3c48df0d3b1dfc53cae274aa60d69e", "architecture/2026-09-11-wasm-preview-font-and-image-budgets.md": "sha256:44dd52839cdb67644c0dbcbc4beb6f36802628c4a8d17a0aad11db1f9be649ed", "architecture/2026-09-11-wasm-preview-font-and-image-budgets.zh.md": "sha256:8285432ee1676be8b79a05953c4b6ac4a7ca5d8e701173b3a2b533608b584d0a", + "architecture/2026-09-24-web-default-schedule-composition.i18n.yaml": "sha256:8c3d5c614924d0e81f361a299427ac697aefc6189c41eb3f4151f5c42e371ca0", + "architecture/2026-09-24-web-default-schedule-composition.md": "sha256:3a139593ade13f95cbb67094c1eec66cb0a2c14bcc154512799803b2a7d22ab4", + "architecture/2026-09-24-web-default-schedule-composition.zh.md": "sha256:59908f7987058742d1226dc8a801d03c265c84d51172249ba51716b91f084f0c", "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.i18n.yaml": "sha256:8be5b0afd8820c593e2ec254d35fb8c384267814104f6234bb9af824c5f974ba", "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md": "sha256:861e6130e893489271478def5f54f212a106a1580b744e6e40f56356f73c7e10", "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.zh.md": "sha256:3549257e06f074591de580ebfaa71284c7c384fb0ffb1963c30258d7a7c26db7", @@ -754,12 +757,12 @@ "bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml": "sha256:a4404fe8e35ca5b6371f9cdb58fea6724cf3e7737bb2db8a3a321d534454b17c", "bug-fix/2026-09-09-content-sized-diagram-previews.md": "sha256:5239e98e5c057d6f7e10c3f10a0f67273778d99b0c4f10b31a1921c3c6474b80", "bug-fix/2026-09-09-content-sized-diagram-previews.zh.md": "sha256:c1ee3790a9d5c3561e1d93d5e649afa904c7d19b9d77248c85ef16daccd953e2", - "bug-fix/2026-09-16-movable-mandatory-update-window.i18n.yaml": "sha256:e526ce14ae9557e24364e3686778b130090445ef2152d976b7cc93e76eac6401", - "bug-fix/2026-09-16-movable-mandatory-update-window.md": "sha256:67546cea0b3de2b3e32957a508dbde097d91dd919fe067c8dbecb2c65b50c366", - "bug-fix/2026-09-16-movable-mandatory-update-window.zh.md": "sha256:20a0aade6ee71c89e84d70e23c89bb51e8a38c674d103dd43022feabb8d27be3", "bug-fix/2026-09-11-session-controller-fork-turn-cut.i18n.yaml": "sha256:4442529ced1c8e48d30f7b50c65ff03eb92dce9815beb438cedbf29f234bce47", "bug-fix/2026-09-11-session-controller-fork-turn-cut.md": "sha256:a996f24a896c7fc5d81e015db3eae27d8a46c853287572e815623ce35eae1b12", "bug-fix/2026-09-11-session-controller-fork-turn-cut.zh.md": "sha256:6486f0eacf5067ef9850dc32f1c27b2109a7d4e9e3c498366f5c38742bb53ac5", + "bug-fix/2026-09-16-movable-mandatory-update-window.i18n.yaml": "sha256:e526ce14ae9557e24364e3686778b130090445ef2152d976b7cc93e76eac6401", + "bug-fix/2026-09-16-movable-mandatory-update-window.md": "sha256:67546cea0b3de2b3e32957a508dbde097d91dd919fe067c8dbecb2c65b50c366", + "bug-fix/2026-09-16-movable-mandatory-update-window.zh.md": "sha256:20a0aade6ee71c89e84d70e23c89bb51e8a38c674d103dd43022feabb8d27be3", "feature/2026-06-14-acp-agent-client-protocol.i18n.yaml": "sha256:006795baa43ae962a8d125cc0f1e9f134bc2ee9fb758b6e7669e3fa0126e1918", "feature/2026-06-14-acp-agent-client-protocol.md": "sha256:6828c0af74bb3fb96206ca6b21c0e56a000b50e4744aad4bc2c05092f3a5a31b", "feature/2026-06-14-acp-agent-client-protocol.zh.md": "sha256:ba104e841a1fb84edbd3b6c8119d50445b7785255a7a8d13bb9ac8a2cb4d2e69", @@ -1264,9 +1267,9 @@ "feature/2026-08-10-creator-guidance-introduce-cue.i18n.yaml": "sha256:74f519839f0cf82c7304bdeae41ae1cab8bb930bfb94f79ab44708acd3b72128", "feature/2026-08-10-creator-guidance-introduce-cue.md": "sha256:3e25409dda498de150de18943ee332e1760963e377a40a365902b66f667fdc9f", "feature/2026-08-10-creator-guidance-introduce-cue.zh.md": "sha256:203847010cab9e9d13c3969f17921d3a5aa0d69377eccfef555c7ff96572f162", - "feature/2026-08-10-durable-workflow-runs-in-chat.i18n.yaml": "sha256:0db3c21a3c8785e56d5d7b90df990b158069cd1cea664e62fc4786c6d7588450", - "feature/2026-08-10-durable-workflow-runs-in-chat.md": "sha256:f9f5290cd880908d17b1080ae5776f22e182f5253471f90cf8bead418b33b502", - "feature/2026-08-10-durable-workflow-runs-in-chat.zh.md": "sha256:f4ffd2700bc9c2a0cc4b9d90f84e1d0a576939e2e6f0b130c5b142fd8cd518b0", + "feature/2026-08-10-durable-workflow-runs-in-chat.i18n.yaml": "sha256:03eadb60c8ba4fde262cd61e3f911d8cd68ea4a58f57adeb8dd7bb9a97d08ca2", + "feature/2026-08-10-durable-workflow-runs-in-chat.md": "sha256:6018de4a89ca99d6cbfd618aff1c8136b23d8e4854a30f7b12a602f0417cbdc2", + "feature/2026-08-10-durable-workflow-runs-in-chat.zh.md": "sha256:0080859221773122022d5880efa3d52f29c0872b52d63e170250d94f214ff37a", "feature/2026-08-10-minimal-read-image-tool.i18n.yaml": "sha256:2311b6453b90a1f81d4fcbdaa723ab233cc0998f34393956364c8b92bc4d1213", "feature/2026-08-10-minimal-read-image-tool.md": "sha256:debe6b570d11634f57a80eaa5891e4fd1e7a5aac19f10fb7dd1683c316515150", "feature/2026-08-10-minimal-read-image-tool.zh.md": "sha256:572d080340682b354cb1f8a0f74af153d6542bd3dffbe3b72b32591e5d5d3baf", @@ -1561,6 +1564,9 @@ "process/2026-08-08-api-remotes-generated-contract-build.i18n.yaml": "sha256:da10469121712bd6ac4fb1f665ca8bd59597a94b56324d2fe4bdef5235498b6e", "process/2026-08-08-api-remotes-generated-contract-build.md": "sha256:8c3d7712cce4913cd3c02d1ac2b87def3f7104a7503681afc3c755896ac45d1f", "process/2026-08-08-api-remotes-generated-contract-build.zh.md": "sha256:e32e1f53c763e769d152c8c41cf8f8977210e82a0d3f7cc7f0421da7e589e9ad", + "process/2026-08-08-automatic-translation-pairing-merges.i18n.yaml": "sha256:5398ef0c48258d3f3a9881e2b34fc4e2086a0b8084b8d7100798dbca6cc39887", + "process/2026-08-08-automatic-translation-pairing-merges.md": "sha256:fb1f3df3246151da920c14fdf195b0cb3bf595a2eed639c84baf70e3f56d36ce", + "process/2026-08-08-automatic-translation-pairing-merges.zh.md": "sha256:9ce7ff6f511ce973637f001b119ce323a5efe0be57cc11d12691aaa36199a8b8", "process/2026-08-08-lightweight-routine-documentation-translation.i18n.yaml": "sha256:f3b44e6148941de9840ef54be60e60ed6ec32ee04b42a1a472e38838b6aa7b14", "process/2026-08-08-lightweight-routine-documentation-translation.md": "sha256:264fe2ee2338053a11fe0f0d5ccf12772897fe9fd4f4a7e8a1d6ba226f853e14", "process/2026-08-08-lightweight-routine-documentation-translation.zh.md": "sha256:3d309e2fc34a6a99bbce4891c32d681e0a23b233791b29a550884f3e753d593c", diff --git a/.agents/notes/archived/process/2026-08-08-automatic-translation-pairing-merges.i18n.yaml b/.agents/notes/archived/process/2026-08-08-automatic-translation-pairing-merges.i18n.yaml new file mode 100644 index 0000000000..48d9fbaaac --- /dev/null +++ b/.agents/notes/archived/process/2026-08-08-automatic-translation-pairing-merges.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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/archived/process/2026-08-08-automatic-translation-pairing-merges.md +2026-08-08-automatic-translation-pairing-merges.md: ed693e376d056378b27a8995a6f26fefcdba34b2 +2026-08-08-automatic-translation-pairing-merges.zh.md: 827475a92824c22ee6f4c558de81b645c3c2b8dc diff --git a/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md b/.agents/notes/archived/process/2026-08-08-automatic-translation-pairing-merges.md similarity index 99% rename from .agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md rename to .agents/notes/archived/process/2026-08-08-automatic-translation-pairing-merges.md index ac4e0c1129..ed693e376d 100644 --- a/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md +++ b/.agents/notes/archived/process/2026-08-08-automatic-translation-pairing-merges.md @@ -1,6 +1,7 @@ # Agent Note: Automatically compose translation pairing records Status: implemented +Archived: 2026-09-23 English | [中文](2026-08-08-automatic-translation-pairing-merges.zh.md) diff --git a/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.zh.md b/.agents/notes/archived/process/2026-08-08-automatic-translation-pairing-merges.zh.md similarity index 99% rename from .agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.zh.md rename to .agents/notes/archived/process/2026-08-08-automatic-translation-pairing-merges.zh.md index 820ebdbe8c..827475a928 100644 --- a/.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.zh.md +++ b/.agents/notes/archived/process/2026-08-08-automatic-translation-pairing-merges.zh.md @@ -1,6 +1,7 @@ # Agent Note: 自动组合翻译配对记录 Status: implemented +Archived: 2026-09-23 [English](2026-08-08-automatic-translation-pairing-merges.md) | 中文 diff --git a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml index b62c09f26c..5e1614518c 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml @@ -1,6 +1,19 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-11-content-block-vocabulary.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md -2026-06-11-content-block-vocabulary.md: 917dd5225fdc4ab391e9e4629769abbb68095731 -2026-06-11-content-block-vocabulary.zh.md: 9d228eb34dafbdebab44b45dc3b290c9327d3b2b +/agent-note-provider-neutral-content-block-vocabulary-owned-by-dsh-llm: + en: 64935f5c8e315004 + zh: 6a9cbb4578eaa437 +/agent-note-provider-neutral-content-block-vocabulary-owned-by-dsh-llm/problem: + en: 4e81df7bca1c1638 + zh: f7483d180a613237 +/agent-note-provider-neutral-content-block-vocabulary-owned-by-dsh-llm/decision: + en: 9526dfcf4da1d000 + zh: ea2bf79a02b26b39 +/agent-note-provider-neutral-content-block-vocabulary-owned-by-dsh-llm/alternatives-considered: + en: 1de2e19e711105e6 + zh: a2cd5a64cc4e9e73 +/agent-note-provider-neutral-content-block-vocabulary-owned-by-dsh-llm/consequences: + en: 999d10e69e447e44 + zh: 38370da5b900ac30 diff --git a/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml index 6578c0b2a2..d01dd83fdb 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml @@ -1,6 +1,37 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-11-dev-invariants-over-deep-readonly.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md -2026-06-11-dev-invariants-over-deep-readonly.md: 0784960397f4d198e136e284ef9232b4ed3e2c2c -2026-06-11-dev-invariants-over-deep-readonly.zh.md: 7183813130560bf82426c8719570b668ba86b7e5 +/agent-note-source-owned-session-immutability-and-dev-mode-invariants: + en: 052aa271e87074c0 + zh: 3ccffc50fc1a28d4 +/agent-note-source-owned-session-immutability-and-dev-mode-invariants/problem: + en: 6ea85a4531d5e59f + zh: b4b568eb347f016a +/agent-note-source-owned-session-immutability-and-dev-mode-invariants/decision: + en: 6e701ec64c3bbfae + zh: b8ed0f1cb7a0477a +/agent-note-source-owned-session-immutability-and-dev-mode-invariants/decision/session-owns-immutable-history: + en: ff47e58c377a8dc0 + zh: 5edbe4f3642a3fe6 +/agent-note-source-owned-session-immutability-and-dev-mode-invariants/decision/derived-requests-remain-detached: + en: 975df574ee6e6df8 + zh: 96d765058482835a +/agent-note-source-owned-session-immutability-and-dev-mode-invariants/decision/package-owned-invariant-companions-check-relationships: + en: a88e3a896eedca91 + zh: 03e96c7870cba932 +/agent-note-source-owned-session-immutability-and-dev-mode-invariants/alternatives-considered: + en: 6119e2aff3b6be5a + zh: 76136ef5249c23eb +/agent-note-source-owned-session-immutability-and-dev-mode-invariants/alternatives-considered/pervasive-deep-readonly-types: + en: ec54af2931616616 + zh: 5fd9bc469a6fd3a5 +/agent-note-source-owned-session-immutability-and-dev-mode-invariants/alternatives-considered/development-only-freezing: + en: 85591197bb62eabf + zh: ba69b9ccf596a6d7 +/agent-note-source-owned-session-immutability-and-dev-mode-invariants/alternatives-considered/clone-only-when-deriving-messages: + en: a9848dec50664b92 + zh: 7c558550b054627b +/agent-note-source-owned-session-immutability-and-dev-mode-invariants/consequences: + en: bcccf7d464a070db + zh: eccd442a21e2c99e diff --git a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml index 868d215253..60451f71cc 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml @@ -1,6 +1,19 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-11-event-sourced-sessions.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md -2026-06-11-event-sourced-sessions.md: b6d17d2db1d9b489f3d4224683b61e62be485014 -2026-06-11-event-sourced-sessions.zh.md: 11e5740dc2636fc002ed7158167b1dfbb39a971d +/agent-note-event-sourced-sessions-with-derived-message-history: + en: da3abc821101e4b3 + zh: c206fa4830920592 +/agent-note-event-sourced-sessions-with-derived-message-history/problem: + en: 63b684d349a48a15 + zh: 7603f64b445db129 +/agent-note-event-sourced-sessions-with-derived-message-history/decision: + en: d60f161037a10269 + zh: 799a15fae2534ec4 +/agent-note-event-sourced-sessions-with-derived-message-history/alternatives-considered: + en: 73541e0833eb9559 + zh: ddca382d54e498e5 +/agent-note-event-sourced-sessions-with-derived-message-history/consequences: + en: d2ea77a5c8e30949 + zh: 19dc61cb0d29315f diff --git a/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml index 57843018c6..f23e22ad36 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml @@ -1,6 +1,19 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-11-microkernel-event-taxonomy.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md -2026-06-11-microkernel-event-taxonomy.md: 6cacbc30f29c863de0f8ddfa4df7251a64bc80ca -2026-06-11-microkernel-event-taxonomy.zh.md: 1c7919392b835c491b9dc606d46f638a708d2948 +/agent-note-microkernel-extension-via-cordis-event-taxonomy-one-concrete-loop: + en: e5775d7ec6f1cdc3 + zh: a364697f860a545a +/agent-note-microkernel-extension-via-cordis-event-taxonomy-one-concrete-loop/problem: + en: 3bc74749db819441 + zh: 19b5497ed3a9378b +/agent-note-microkernel-extension-via-cordis-event-taxonomy-one-concrete-loop/decision: + en: 2153b0cb5773cb08 + zh: 82d7ad1b250cd52d +/agent-note-microkernel-extension-via-cordis-event-taxonomy-one-concrete-loop/alternatives-considered: + en: e528c5a20e0c55a3 + zh: 004d3bf06241cc26 +/agent-note-microkernel-extension-via-cordis-event-taxonomy-one-concrete-loop/consequences: + en: 6acb80cdaa7c010f + zh: acca672d8094d985 diff --git a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml index 92ea844d93..18a5b7dcf2 100644 --- a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml @@ -1,6 +1,22 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-13-capability-seams.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-13-capability-seams.md -2026-06-13-capability-seams.md: 798739b7ef203b10ea657f9e1c178ea7eab094ad -2026-06-13-capability-seams.zh.md: c8f568aa569fe74146f1e7fcc46e00774a39fb84 +/agent-note-capability-seams-service-definition-service-provider-consumer-roles: + en: 4c1875882be571bb + zh: 90ef4d8b27587c6b +/agent-note-capability-seams-service-definition-service-provider-consumer-roles/problem: + en: 3215928333de66f7 + zh: 2c72fcacccf25741 +/agent-note-capability-seams-service-definition-service-provider-consumer-roles/decision: + en: 774c54c0d3d9acad + zh: 7dd4bfaf0ba2181b +/agent-note-capability-seams-service-definition-service-provider-consumer-roles/terminology-seam-names-the-trio-not-the-interface: + en: 8dca727800bea2f4 + zh: 4c4e028520967ca0 +/agent-note-capability-seams-service-definition-service-provider-consumer-roles/alternatives-considered: + en: cf6e4b1f188d9e4a + zh: bf2485a5f539746c +/agent-note-capability-seams-service-definition-service-provider-consumer-roles/consequences: + en: 941d8b343162edea + zh: 70904b0716de151d diff --git a/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml index d9ebd3c9de..30a57122ad 100644 --- a/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml @@ -1,6 +1,19 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-13-twin-llm-adapters.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md -2026-06-13-twin-llm-adapters.md: fe8b0b55e0e027e29eb0920e64a1760d0dc35aee -2026-06-13-twin-llm-adapters.zh.md: 4248b5afeb9f3fd4503914e53203c674e3a80dbf +/agent-note-two-llm-adapters-as-a-design-verification-twin: + en: dcb28a02ba26cb31 + zh: 096d0ab60a3ad638 +/agent-note-two-llm-adapters-as-a-design-verification-twin/problem: + en: feb8f5213be03a5c + zh: 5ca85048792db7d8 +/agent-note-two-llm-adapters-as-a-design-verification-twin/decision: + en: 0dd6ebb29aebc164 + zh: 20065ae72f4d131d +/agent-note-two-llm-adapters-as-a-design-verification-twin/alternatives-considered: + en: c523a9b402588189 + zh: bced37eb1040360a +/agent-note-two-llm-adapters-as-a-design-verification-twin/consequences: + en: 71cc34560fc2e012 + zh: 91ea73e3e009a9a5 diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml index f827897a1e..995fba81fe 100644 --- a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml @@ -1,6 +1,19 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-14-session-persistence.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-14-session-persistence.md -2026-06-14-session-persistence.md: d79975e1fedb6efcd0e4ea83bc799bb158082e41 -2026-06-14-session-persistence.zh.md: f70b8005a254cc6a9ea8ada31c32e5552ee0c8b2 +/agent-note-session-persistence-as-an-abstract-service-over-the-existing-sessionevent: + en: d7cf18702f4ed830 + zh: 7e0cc6db7c90a7bd +/agent-note-session-persistence-as-an-abstract-service-over-the-existing-sessionevent/problem: + en: fa1ffb258aee90f6 + zh: c25cead52e72f97d +/agent-note-session-persistence-as-an-abstract-service-over-the-existing-sessionevent/decision: + en: a7076073971de601 + zh: 5ea9a87705340d91 +/agent-note-session-persistence-as-an-abstract-service-over-the-existing-sessionevent/alternatives-considered: + en: e5c9929ca92cc12e + zh: 3e0668f6a822577c +/agent-note-session-persistence-as-an-abstract-service-over-the-existing-sessionevent/consequences: + en: 4f00ed0b90442440 + zh: 994a5b03c862178b diff --git a/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml index 1939ff2f17..db7edf6873 100644 --- a/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml @@ -1,6 +1,37 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-18-session-surface.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-18-session-surface.md -2026-06-18-session-surface.md: 26780d54a1ea12283900572c6fd0e8f740de5524 -2026-06-18-session-surface.zh.md: 328a9ee9f1bed9c52996cfd307549a7745e16d4b +/agent-note-session-surface-an-ordered-projection-over-the-event-log: + en: 9ea7a8765e7f3259 + zh: 17e0d51645f01edd +/agent-note-session-surface-an-ordered-projection-over-the-event-log/problem: + en: 4f7a9d20d71a6ab3 + zh: 958abbdfefec4f21 +/agent-note-session-surface-an-ordered-projection-over-the-event-log/decision: + en: a9944b568ec51fad + zh: 6717d30db31b81d7 +/agent-note-session-surface-an-ordered-projection-over-the-event-log/decision/top-level-surface-metadata-on-sessionevent: + en: 788fc16ecdedc252 + zh: 14d05c2a1272de5d +/agent-note-session-surface-an-ordered-projection-over-the-event-log/decision/surfaceop-two-operations: + en: ff075eec0881094a + zh: 1f7768f28b8996d5 +/agent-note-session-surface-an-ordered-projection-over-the-event-log/decision/surfacemanager-delta-based-not-full-rebuild: + en: 0b10fd9c5166d17b + zh: 02de01f70c03f70e +/agent-note-session-surface-an-ordered-projection-over-the-event-log/decision/persistence: + en: 9bb1f81b86b24760 + zh: fe4948645dd0c66d +/agent-note-session-surface-an-ordered-projection-over-the-event-log/decision/crash-recovery: + en: ee71f9c0f12b6a0e + zh: 403b1723f808391d +/agent-note-session-surface-an-ordered-projection-over-the-event-log/decision/invariants: + en: 24b1342fb3b5be29 + zh: a62692b0250c072a +/agent-note-session-surface-an-ordered-projection-over-the-event-log/alternatives-considered: + en: 193157129e65e487 + zh: a0a73b8a9cbd78e6 +/agent-note-session-surface-an-ordered-projection-over-the-event-log/consequences: + en: 801b67f8da564a22 + zh: 3c0433ed07083050 diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml index 22bf3c01c4..ff239e85c9 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml @@ -1,6 +1,58 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-20-generic-long-running-tool-runtime.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md -2026-06-20-generic-long-running-tool-runtime.md: 5960eb268656d7e316c763631451db1c6d1c6e49 -2026-06-20-generic-long-running-tool-runtime.zh.md: ea63ae82962a735b3a96e1803708172a33f83e9d +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools: + en: 56e269b2a2839796 + zh: 8fc971036985ac0d +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/problem: + en: 70e26df62568c337 + zh: a9e6f5f8a5482d0a +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/decision: + en: 277869395ad6f792 + zh: 93f13a97d52f10a5 +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/runtime-contract: + en: eeb7ed8eec13668a + zh: 3f236f63c72fbba7 +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/authorization-and-owner-lifecycle: + en: 3819a73ca70ecdfd + zh: c1a4f461a4e45978 +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/service-api: + en: a1a884816aac3d7f + zh: b21129e5cd93b9a1 +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/model-facing-control-api: + en: dd3f4d85d0ec136f + zh: ca4209cb856271dd +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/producer-opt-in: + en: a39343bf6f655051 + zh: d9894d39e7f4d6e1 +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/producer-integrations: + en: ae4a6d6e0d83d3bd + zh: 9072890ba9374426 +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/alternatives-considered: + en: 6119e2aff3b6be5a + zh: e01a6c669a9ec6aa +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/alternatives-considered/per-capability-control-tools: + en: 9781c302dc72fd8e + zh: a78e1dd8da4d8ca8 +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/alternatives-considered/an-immediate-abstract-task-runtime-backend: + en: 404a5c7c45a71a55 + zh: 213621e4a9618a9c +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/alternatives-considered/consumer-owned-authorization-or-cleanup-events: + en: d99de803718d9ca6 + zh: e03a97314851ed0f +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/alternatives-considered/blocking-output-or-a-separate-wait-tool: + en: e3beca50b6a57ce0 + zh: a77663b817d33247 +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/alternatives-considered/runtime-owned-output-sinks: + en: 42eeeba9d84ae681 + zh: 1d540f9740675bee +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/alternatives-considered/random-ids-promotion-or-lifecycle-session-events: + en: e706b1b57241d3d2 + zh: 1779b7fa05f48283 +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/testing: + en: e891db1723828da8 + zh: 657a62ed595bd081 +/agent-note-the-background-job-runtime-ctx-jobs-and-generic-task-control-tools/consequences: + en: b95ecd6052465ad3 + zh: 1527c38d1b4631d6 diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml index 0956b2aa0b..2b8a5f380d 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml @@ -1,6 +1,43 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-21-bounded-llm-request-recovery.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md -2026-06-21-bounded-llm-request-recovery.md: b0a4ebb11b76c4c0068490c9924a88b80a8bc99f -2026-06-21-bounded-llm-request-recovery.zh.md: e8c25f9f6410dc12e34a0272e01e874431624c28 +/agent-note-bounded-recovery-for-transient-llm-request-failures: + en: fb8e336ecd4bf571 + zh: 23e40e4cb224163f +/agent-note-bounded-recovery-for-transient-llm-request-failures/problem: + en: 83a361538a092dae + zh: e8f33cb95d0d3e09 +/agent-note-bounded-recovery-for-transient-llm-request-failures/decision: + en: a15c7c13f167e5ac + zh: 63bcc2ef60c2f344 +/agent-note-bounded-recovery-for-transient-llm-request-failures/decision/preserve-failure-facts-without-embedding-policy: + en: 943322e6cdbfa440 + zh: 82288531f6194ed2 +/agent-note-bounded-recovery-for-transient-llm-request-failures/decision/put-retry-policy-on-the-existing-failed-step-extension-point: + en: ed4d295378d94a26 + zh: 1fee5ef4947bac8f +/agent-note-bounded-recovery-for-transient-llm-request-failures/decision/make-one-layer-own-visible-attempts: + en: 57dbdd385f48bb3e + zh: 5e15463fae4be582 +/agent-note-bounded-recovery-for-transient-llm-request-failures/decision/bound-stalled-streams-where-they-can-be-stopped: + en: f7369086e87192b9 + zh: e2839ed8cc5c23b8 +/agent-note-bounded-recovery-for-transient-llm-request-failures/decision/keep-attempts-separate-in-the-existing-log: + en: 8f57db4bfd6e1c22 + zh: 67ac62217bb68570 +/agent-note-bounded-recovery-for-transient-llm-request-failures/out-of-scope: + en: 985720529b56cda2 + zh: 8a649bfa9cd8d93f +/agent-note-bounded-recovery-for-transient-llm-request-failures/alternatives-considered: + en: a6d0306740d470ee + zh: 700bd4b0aa58f3e8 +/agent-note-bounded-recovery-for-transient-llm-request-failures/verification: + en: db343f335a9d7cad + zh: 49b8300de55dcfeb +/agent-note-bounded-recovery-for-transient-llm-request-failures/consequences: + en: 08659e8606455ada + zh: 862080f8df5f28ec +/agent-note-bounded-recovery-for-transient-llm-request-failures/related: + en: e85a6ad7387c881b + zh: bb8fb4173ffe613d diff --git a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml index 2b6c63d334..0856d5ac56 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml @@ -1,6 +1,25 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-21-mandatory-app-attribution-headers.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md -2026-06-21-mandatory-app-attribution-headers.md: 9e0c029dc03c722512680a563c24e470128ee322 -2026-06-21-mandatory-app-attribution-headers.zh.md: 1427daf8f6065ec2dee324075a8381a625d9e960 +/agent-note-mandatory-user-agent-attribution-for-provider-requests: + en: 3f804e0236d13aba + zh: ef8b1b7b9c3d8376 +/agent-note-mandatory-user-agent-attribution-for-provider-requests/problem: + en: f65797760084b93a + zh: d1fb36ef61a93003 +/agent-note-mandatory-user-agent-attribution-for-provider-requests/investigation: + en: 5ea11f54f507ba64 + zh: 22a83a0714a2fda3 +/agent-note-mandatory-user-agent-attribution-for-provider-requests/decision: + en: b3e29e609e3f0966 + zh: e3df0cb13df1cd5d +/agent-note-mandatory-user-agent-attribution-for-provider-requests/verification: + en: 567737dbccdb3a2a + zh: be92a5303247b889 +/agent-note-mandatory-user-agent-attribution-for-provider-requests/alternatives-considered: + en: 016678c52ef64e51 + zh: a113b57b9e9ddeb5 +/agent-note-mandatory-user-agent-attribution-for-provider-requests/consequences: + en: a9010e68445a0008 + zh: 1ec6104ab2865919 diff --git a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml index bbdb39100a..490d5c5d24 100644 --- a/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.i18n.yaml @@ -1,6 +1,76 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-24-web-capability-seam.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md -2026-06-24-web-capability-seam.md: 8c6c088ea5d7345f9955892b2d6054cfae518bbd -2026-06-24-web-capability-seam.zh.md: 4921c4a4647d180dbb00e6493a19d38584f99bdc +/agent-note-web-capability-seam-stable-tools-over-multiple-providers: + en: ca120635fc40431c + zh: 6be7c5ae3c86cdcc +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/problem: + en: c8b932b02a4e1c80 + zh: 96c466be1a521f3f +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/decision: + en: b57eb0f268033ff1 + zh: 4143382a50181414 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/package-topology: + en: 5ea3ddfae82df181 + zh: 2d05d95e51081696 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/ctx-web-contract: + en: b432f80a424c3e49 + zh: faa26925fdd98387 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/provider-availability-and-selection: + en: dff78c46432311a1 + zh: 58d5db4c3c85b886 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/search-request-and-result-schema: + en: ec3703b7e7c6b23d + zh: 325755e6e90092da +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/fetch-request-and-result-schema: + en: f734fdc57a6546c9 + zh: 027c28cbc584a8c2 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/tool-consumer-behavior: + en: 62127883453dd79c + zh: 01503a8177a0ddab +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/errors: + en: 3440f2b34b93f4a1 + zh: 57a8f784c7e26eff +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/testing: + en: 7f73699ac4459432 + zh: c16be49ac50c34c8 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered: + en: 6119e2aff3b6be5a + zh: 76136ef5249c23eb +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered/let-each-provider-register-its-own-model-facing-tool: + en: e0d84a9b1b9c2123 + zh: 9e116a7b96751563 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered/put-provider-dispatch-directly-in-dsh-tool-web: + en: dc01d014d03b49d2 + zh: b956b02f41c62682 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered/split-search-and-fetch-into-two-seams-dsh-search-dsh-fetch: + en: 3fb72ba3e554f20a + zh: aff5f0add5773388 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered/choose-the-first-registered-provider: + en: 1800706ea0f6b90b + zh: ced985b7657e2d7f +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered/treat-firecrawl-exa-tavily-parallel-extraction-as-fetch: + en: fc22e6a8bfaeaa20 + zh: 64032ea6ab61b836 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered/mirror-claude-code-s-url-prompt-webfetch-shape: + en: 56a080de39e96056 + zh: ae4d070aa7657503 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered/validate-dns-and-then-call-an-ordinary-fetch: + en: aa6c1e7e6374a875 + zh: 045c8af6cd44c3e1 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered/block-private-looking-hostname-strings-without-pinning-resolved-addresses: + en: 3d906f3056733479 + zh: d652723eb5372007 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered/require-per-call-approval-before-public-fetches: + en: f8d1fc8e9c9659cf + zh: da5792520e35cba0 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/consequences: + en: 12fd20ba54818215 + zh: c1ca000b13666701 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/deferred-work: + en: 48b7b8e7310cd56b + zh: d406cd6a8a649e58 +/agent-note-web-capability-seam-stable-tools-over-multiple-providers/open-questions: + en: 63848c6ef9a6ab44 + zh: 041b340802091e60 diff --git a/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml index 7db4727f48..3addc01168 100644 --- a/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml @@ -1,6 +1,43 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-26-file-context-as-event-gate.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md -2026-06-26-file-context-as-event-gate.md: 2943f11190533f84f3aebea0a25a88372d1204f1 -2026-06-26-file-context-as-event-gate.zh.md: 053f4269b13c8583def01c144badd17b10a5a41a +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface: + en: 950d6bb4d83b39d8 + zh: b40458972583b12b +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/problem: + en: 765a679e88ecda60 + zh: fccabc8ee516ea00 +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/decision: + en: eb8355d4b7a6c66b + zh: 712c6f932f26a0d3 +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/the-policy-is-enforced-by-provider-cas-not-by-dsh-fs-observation-policy-stat: + en: 2edc42b8210c995b + zh: 48f4354c9ec625ee +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/provider-contract-change-the-version-guard-is-optional: + en: a282600f92568e07 + zh: 3e6b2bec22c170c6 +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/event-vocabulary-owned-by-dsh-fs: + en: f57494c21daf90ea + zh: 1a1f8623dc0cbef3 +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/tool-contract-dsh-tool-fs: + en: c7f4457a83739520 + zh: f2d55d018c7dab4f +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/policy-plugin-contract-dsh-fs-observation-policy: + en: a239deb5dde631df + zh: 68cc6c954b247596 +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/bare-provider-behavior-no-dsh-fs-observation-policy: + en: 5aed537454b78157 + zh: 9e8e7108be6a9537 +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/supersedes: + en: 5cc21706fd0f00ad + zh: 2b51da75a95f1777 +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/verification: + en: 1e3de2836160b4c2 + zh: a8af173d161f0949 +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/alternatives-considered: + en: 768c09662714cf25 + zh: cb641e8a77a2e32c +/agent-note-make-dsh-fs-observation-policy-an-event-gate-plugin-not-a-method-interface/consequences: + en: a1c00220fa5c6ce1 + zh: 4691374274aa8df9 diff --git a/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml index 4a4342d1f2..9afc68c549 100644 --- a/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml @@ -1,6 +1,16 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-06-30-event-domain-semantics.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md -2026-06-30-event-domain-semantics.md: 7e0aee220a62777ebe1d8f6a5018aba0f5fa20ee -2026-06-30-event-domain-semantics.zh.md: 14568c7a4660c42cecc47f094373d299380fa563 +/agent-note-event-domain-semantics-session-is-the-fact-log-agent-is-the-live-event-channel: + en: e7cdaa7a6029c259 + zh: ed13f8158ee0eaeb +/agent-note-event-domain-semantics-session-is-the-fact-log-agent-is-the-live-event-channel/problem: + en: 1222c2b55882b1e5 + zh: f81f481f5eb9a684 +/agent-note-event-domain-semantics-session-is-the-fact-log-agent-is-the-live-event-channel/decision: + en: ccc0fed9b7388112 + zh: 3cc527b11affd941 +/agent-note-event-domain-semantics-session-is-the-fact-log-agent-is-the-live-event-channel/consequences: + en: e0c5aa60322e67a1 + zh: 7495550f0c863eda diff --git a/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml index 933b3d2826..6e01984aee 100644 --- a/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml @@ -1,6 +1,37 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-02-tool-render-intent-union.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md -2026-07-02-tool-render-intent-union.md: 52626d3d9200146df95aee7836d37bbaf7e6a9ec -2026-07-02-tool-render-intent-union.zh.md: bc91b89c53c3c43b6c483dc0d4876a00f5a936b7 +/agent-note-tagged-render-intent-union-for-tool-call-presentation: + en: 599dd6f91059580e + zh: b6a46767bb4198e6 +/agent-note-tagged-render-intent-union-for-tool-call-presentation/problem: + en: 3a4b92dca651b979 + zh: 6b10ce7ba3681180 +/agent-note-tagged-render-intent-union-for-tool-call-presentation/decision: + en: aff6d789bde63f17 + zh: 73782526b46a3ee5 +/agent-note-tagged-render-intent-union-for-tool-call-presentation/decision/why-a-tagged-union-beats-the-field-bag: + en: ed7b15039050e65e + zh: a719562b5fcdbd4e +/agent-note-tagged-render-intent-union-for-tool-call-presentation/decision/producer-mapping: + en: c87f22a29caef002 + zh: 7bfd28c1df80b8a1 +/agent-note-tagged-render-intent-union-for-tool-call-presentation/decision/terminal-fallback-ownership: + en: 0bb2f60726afc4d9 + zh: 60040141447b4a2e +/agent-note-tagged-render-intent-union-for-tool-call-presentation/decision/purity-preserved: + en: 1ba6a480967c524d + zh: 90535e843f346bcf +/agent-note-tagged-render-intent-union-for-tool-call-presentation/alternatives-considered: + en: 2cd4801f3162227b + zh: 76afaa23bc2b9eb0 +/agent-note-tagged-render-intent-union-for-tool-call-presentation/consequences: + en: 81d8366d826285c2 + zh: b437b862794c146e +/agent-note-tagged-render-intent-union-for-tool-call-presentation/non-goals: + en: e9d60967a9b3f9bd + zh: 6dfa9ac30d847879 +/agent-note-tagged-render-intent-union-for-tool-call-presentation/related: + en: fe27f777b3728fa4 + zh: 4b648dbf731ace00 diff --git a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml index 57dba7f52b..670619cca9 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml @@ -1,6 +1,40 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-05-prompt-variables-and-tool-guidance-ownership.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md -2026-07-05-prompt-variables-and-tool-guidance-ownership.md: 8f5542cb09930e62fd1e26960166fbdd6d3f6745 -2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: 348ae36cf6549d412adab1d3cdb5cc03f0badd01 +/agent-note-prompt-variables-and-tool-guidance-ownership: + en: 6f8b4a9d26ab3e50 + zh: 9741e5718e647f0a +/agent-note-prompt-variables-and-tool-guidance-ownership/problem: + en: 8ccef8fbd9260dfe + zh: 106b2bafe969c676 +/agent-note-prompt-variables-and-tool-guidance-ownership/decision: + en: c551d888282d9c73 + zh: 889b5ca3cdf25aff +/agent-note-prompt-variables-and-tool-guidance-ownership/decision/assemble-context: + en: 46e67248692e333a + zh: 51c5f2c6152c94d7 +/agent-note-prompt-variables-and-tool-guidance-ownership/decision/prompt-variables: + en: 04b623ed2879fdf1 + zh: c111be62834fda93 +/agent-note-prompt-variables-and-tool-guidance-ownership/decision/persona-as-the-order-0-section: + en: c2d32c0ac5d28759 + zh: 59cd252ba6da6e59 +/agent-note-prompt-variables-and-tool-guidance-ownership/decision/tool-guidance-ownership: + en: 308c0d2ef4e41f56 + zh: d38f84a517da20df +/agent-note-prompt-variables-and-tool-guidance-ownership/decision/the-subagent-conversation-history-descriptor: + en: 1cb9644f22918749 + zh: fb9fdee133159ab2 +/agent-note-prompt-variables-and-tool-guidance-ownership/alternatives-considered: + en: 9a114359d2e8988b + zh: ce39453408b2af7f +/agent-note-prompt-variables-and-tool-guidance-ownership/out-of-scope: + en: bbfecc70d0b457fe + zh: 7f207a37a86484f4 +/agent-note-prompt-variables-and-tool-guidance-ownership/shipped-invariants: + en: 31ba63101fca5790 + zh: af4ceefab2033142 +/agent-note-prompt-variables-and-tool-guidance-ownership/consequences: + en: 993f17bf35f6a2e0 + zh: acb429176f0eebcc diff --git a/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml index ac7f2acd4e..987abc5f0e 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml @@ -1,6 +1,28 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-05-reconstructable-requests.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md -2026-07-05-reconstructable-requests.md: f47d0938b91edf6c9b544223ac44925f02dd8c1f -2026-07-05-reconstructable-requests.zh.md: d2499a6264a3e6bebf31944d8eb056265e082c68 +/agent-note-every-llm-request-is-reconstructable-from-the-session-log: + en: 115dd45f40b0d7d8 + zh: 536b5a0399e7d06a +/agent-note-every-llm-request-is-reconstructable-from-the-session-log/problem: + en: f8f31da854a5fe2e + zh: ef88856c2c7bdfb8 +/agent-note-every-llm-request-is-reconstructable-from-the-session-log/decision: + en: a15c7c13f167e5ac + zh: 63bcc2ef60c2f344 +/agent-note-every-llm-request-is-reconstructable-from-the-session-log/decision/the-principle: + en: 6da15cf986d59a91 + zh: 40282908b54295f9 +/agent-note-every-llm-request-is-reconstructable-from-the-session-log/decision/the-mechanism: + en: 7d86e5058dbab2fb + zh: 9a97481325c8eded +/agent-note-every-llm-request-is-reconstructable-from-the-session-log/decision/the-minicode-shape-adopted-with-the-event-log-as-the-source: + en: e5f2ffaa3d7de0cc + zh: 5ba6b36787a0e189 +/agent-note-every-llm-request-is-reconstructable-from-the-session-log/alternatives-considered: + en: 96f19ecf38fae991 + zh: 9692252135ef3476 +/agent-note-every-llm-request-is-reconstructable-from-the-session-log/consequences: + en: 0d2e97bc421e67cc + zh: 0a000a69b8cc4b4e diff --git a/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.i18n.yaml index cc278292d9..2848f7cd4b 100644 --- a/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.i18n.yaml @@ -1,6 +1,19 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-05-windows-jsonl-durable-publish.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.md -2026-07-05-windows-jsonl-durable-publish.md: 546cde6086f3c84e22b4d4de144a48bb3425cd4e -2026-07-05-windows-jsonl-durable-publish.zh.md: b99032e9d7e72e2028f39c5af91659278fd81343 +/agent-note-windows-native-durable-jsonl-publication: + en: db7fe80708911f2f + zh: 42c6639defea2bde +/agent-note-windows-native-durable-jsonl-publication/problem: + en: dcc3c6238131d302 + zh: 4d4e8513a4069af3 +/agent-note-windows-native-durable-jsonl-publication/decision: + en: 385565b209f1e739 + zh: 0eab0997c49a3c33 +/agent-note-windows-native-durable-jsonl-publication/alternatives-considered: + en: 83d5bf5781f89e29 + zh: 068835993789e09d +/agent-note-windows-native-durable-jsonl-publication/consequences: + en: 4be0ad03a334c257 + zh: 95d473ed5b94e270 diff --git a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml index b2ebf2057f..9db3e2596d 100644 --- a/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml @@ -1,6 +1,28 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-06-timeout-deadline-library.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md -2026-07-06-timeout-deadline-library.md: 6f726938731f18386105ff1180c79fef981c95bf -2026-07-06-timeout-deadline-library.zh.md: 48bd7b4b91efd0045c03e3e1e1e9e37656b4dcf6 +/agent-note-a-shared-timeout-deadline-primitive-with-hard-kill-left-to-each-capability: + en: 4a9921fef5e65cc8 + zh: 672cd27d68310cb1 +/agent-note-a-shared-timeout-deadline-primitive-with-hard-kill-left-to-each-capability/problem: + en: 110ff68b35bd43cb + zh: f1cea15fb29a9ae9 +/agent-note-a-shared-timeout-deadline-primitive-with-hard-kill-left-to-each-capability/decision: + en: 18645d82d9e40107 + zh: 73e510f1ed1106e6 +/agent-note-a-shared-timeout-deadline-primitive-with-hard-kill-left-to-each-capability/decision/the-library-api: + en: c62681afef60a0db + zh: 69827b4b3165b3da +/agent-note-a-shared-timeout-deadline-primitive-with-hard-kill-left-to-each-capability/decision/the-division-of-labor: + en: 263c8e17bbc87ba0 + zh: d75a74f47713e79f +/agent-note-a-shared-timeout-deadline-primitive-with-hard-kill-left-to-each-capability/decision/how-each-capability-consumes-it: + en: 333eab4967d753ec + zh: dd7537bb82cc717f +/agent-note-a-shared-timeout-deadline-primitive-with-hard-kill-left-to-each-capability/consequences: + en: 832f4dbd93a47f94 + zh: 65ada43f9f23a39b +/agent-note-a-shared-timeout-deadline-primitive-with-hard-kill-left-to-each-capability/alternatives-considered: + en: 7b3445622becbd36 + zh: bb358acf2ff28e26 diff --git a/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml index 4cacefaca8..617b3a053b 100644 --- a/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml @@ -1,6 +1,28 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-07-tool-call-timeout-policy.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md -2026-07-07-tool-call-timeout-policy.md: 60d82c1d591f0a32350b1a21ed74e148c4c93576 -2026-07-07-tool-call-timeout-policy.zh.md: 49ca7f397d15c1a351cba4837d4d531e54120ea6 +/agent-note-tool-call-timeout-policy-as-a-plugin: + en: 46fa7d1eac4fe061 + zh: 7a62f8f7b1c66722 +/agent-note-tool-call-timeout-policy-as-a-plugin/problem: + en: b5fffa80b58fb184 + zh: aadf28f95924b1ad +/agent-note-tool-call-timeout-policy-as-a-plugin/decision: + en: 04889f664ab7657d + zh: 2af5913f5e9c1927 +/agent-note-tool-call-timeout-policy-as-a-plugin/decision/the-tools-execute-around-dispatch-extension-point: + en: 8c3b46fa8a48a863 + zh: 662eb09bb7542be8 +/agent-note-tool-call-timeout-policy-as-a-plugin/decision/the-timeout-policy-plugin: + en: 96f5932514a33e41 + zh: 5951d9451ccef1bf +/agent-note-tool-call-timeout-policy-as-a-plugin/decision/existing-tool-adaptation: + en: 43add9b1f723ce89 + zh: f3ee56f97faa2e60 +/agent-note-tool-call-timeout-policy-as-a-plugin/alternatives-considered: + en: dc6da8743b4266fb + zh: 5c28667d952b230a +/agent-note-tool-call-timeout-policy-as-a-plugin/consequences: + en: f7429a5c1fe6b6a1 + zh: 748401be79f89b05 diff --git a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml index aff7b32029..cfb0644c92 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml @@ -1,6 +1,46 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-08-agent-scope-contexts.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md -2026-07-08-agent-scope-contexts.md: 45e635b7bc3138d4e90a25a06ff23b3b57a9415e -2026-07-08-agent-scope-contexts.zh.md: aac860734e744843c3b9e7d55e5bc7a150763090 +/agent-note-the-agent-is-a-registration-scope: + en: f767894b8308f1a0 + zh: 4ff0aead6ee4e27b +/agent-note-the-agent-is-a-registration-scope/problem: + en: 87634b46a5642328 + zh: 2adce8b041626437 +/agent-note-the-agent-is-a-registration-scope/decision: + en: ac58e62f780b0879 + zh: 6b077226de04a8fa +/agent-note-the-agent-is-a-registration-scope/decision/registration-origin-chooses-visibility-and-cleanup: + en: 1ec8e54543272e1b + zh: b5b1695a805574a2 +/agent-note-the-agent-is-a-registration-scope/decision/the-operation-chooses-the-view: + en: 2e236d399efdc0b2 + zh: 00a7ba20f90862c6 +/agent-note-the-agent-is-a-registration-scope/decision/scoped-events-keep-routing-separate-from-event-data: + en: df5f5e4c1de44474 + zh: d851c02959d02380 +/agent-note-the-agent-is-a-registration-scope/decision/creation-publishes-last-and-disposal-revokes-last: + en: f46920b4945e0e6d + zh: 870a157205883144 +/agent-note-the-agent-is-a-registration-scope/security-and-authority-are-non-goals: + en: 5ffc5a8f65ce7dfe + zh: 3362f236356a8033 +/agent-note-the-agent-is-a-registration-scope/alternatives-considered: + en: bf43310538280f62 + zh: e4940ff47c5d47b5 +/agent-note-the-agent-is-a-registration-scope/alternatives-considered/pass-an-agent-option-to-every-registration: + en: 37e7487b1f1eb87a + zh: eeddbaf21eee7ebc +/agent-note-the-agent-is-a-registration-scope/alternatives-considered/filter-events-while-keeping-registries-global: + en: 7eb2628d51ea9ed0 + zh: e58f05faf0e95712 +/agent-note-the-agent-is-a-registration-scope/alternatives-considered/create-one-service-graph-per-agent: + en: 2fd1c41c3d1bc6ca + zh: cffad44a2c69330e +/agent-note-the-agent-is-a-registration-scope/alternatives-considered/inherit-parent-registration-scopes: + en: 01f989446aab1cf1 + zh: 8f5c9018b7b84c94 +/agent-note-the-agent-is-a-registration-scope/consequences: + en: 65c8f47d1607b276 + zh: 950b86b4682f7a0e diff --git a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml index 072caec450..1325797191 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml @@ -1,6 +1,40 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-08-tool-output-spill-files.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md -2026-07-08-tool-output-spill-files.md: fa6697aad63c97b0ef580fe1ca46e50541e3d682 -2026-07-08-tool-output-spill-files.zh.md: a995eafc7878b342a2164c5116da1f43ada791a5 +/agent-note-tool-output-spill-policy: + en: 06b99197145a4080 + zh: 698a61dcd2385985 +/agent-note-tool-output-spill-policy/problem: + en: 858cbb874e213697 + zh: 1693b618886ac42d +/agent-note-tool-output-spill-policy/decision: + en: 89536b0d2d7b4c92 + zh: 341240f445885549 +/agent-note-tool-output-spill-policy/decision/spill-seam: + en: 1e92bc9a0548def9 + zh: c99cf6878981ba71 +/agent-note-tool-output-spill-policy/decision/spill-policy: + en: 3c49c0246188c513 + zh: 4b6749c30726903d +/agent-note-tool-output-spill-policy/showcase-web-fetch: + en: d5c334b50f458925 + zh: 7631307b5d9fbc55 +/agent-note-tool-output-spill-policy/relationship-to-retention-and-early-spill: + en: 7314ba8b0a03c0d9 + zh: 8429f79ff3a7bd1f +/agent-note-tool-output-spill-policy/non-goals: + en: f82b5399ecddd8a1 + zh: 9675b58631bb5c90 +/agent-note-tool-output-spill-policy/deferred: + en: 094f8c71672ea726 + zh: 252dbe825d6e34ca +/agent-note-tool-output-spill-policy/testing: + en: c98759345e1aebfa + zh: d1faeadb99bd748a +/agent-note-tool-output-spill-policy/consequences: + en: e023260e52a246d6 + zh: 2e49237e564a83da +/agent-note-tool-output-spill-policy/alternatives-considered: + en: d53fb144141de557 + zh: 1b851eb64c421803 diff --git a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md index fa6697aad6..e32aec72f4 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md +++ b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md @@ -66,36 +66,7 @@ interface SpillRef { ### Spill policy -`dsh-spill-policy` is a `tools/post-execute` result transformer with one configuration knob: - -```ts ignore-check -interface Config { - /** Omitted means no automatic spill policy. Present means apply to oversized plain text tool results. */ - maxInlineBytes?: number -} -``` - -When `maxInlineBytes` is omitted the plugin registers nothing (a true no-op). When set, it applies a default policy to final plain-text tool results: - -1. Let the tool run normally, delegating via `next()` so a downstream listener settles the result first. -2. Flatten the accepted final `ContentBlock[]` only when it is entirely plain text; a result with any non-text block is left untouched. -3. If its UTF-8 byte size is at or below `maxInlineBytes`, leave it unchanged. -4. If it is larger, call `ctx.spillStore.saveText()` with the full final text. -5. Replace the model-facing result with a retained head/tail preview plus the spill reference. - -The preview is an implementation default owned by the policy: a head/tail split of `maxInlineBytes` via the retention library's `TextRetainer`. Future config can expose preview sizing only after a second deployment needs it. - -The replacement text is intentionally generic because the policy only knows the final formatted tool result, not the tool's internal resource: - -```text - - -(Omitted N bytes. Full formatted result stored at: /.../session-.../....txt. Use read with offset/limit, or grep this path to search within it.) -``` - -If `ctx.spillStore.saveText()` fails (permissions, ENOSPC, backend unavailable), or the call has no session owner, or no backend is loaded, the plugin logs the reason and returns the original result unchanged. Spill failure never turns a successful tool call into an `isError` result or hides the inline result. - -The policy skips `read` to avoid a circular `read -> spill file -> read again` loop. Additional opt-out configuration is deferred until a real second tool needs it. +`dsh-spill-policy` retains ordered text/image ends under `maxInlineTokens` after post-execute policy accepts the result. `saveText()` stores the complete result with readable image paths; image bytes remain in attachment storage. The [multimodal retention decision](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.md) owns image projection order, atomic image omission, and route pricing. An omitted budget installs no listeners, recovery failures keep the original result, and model-facing `read` results skip retention to avoid a read/spill loop. ## Showcase: web_fetch @@ -129,7 +100,7 @@ With `dsh-spill-policy` configured, a large formatted fetch result is automatica - id: spill-policy name: '@deepseek-ai/dsh-spill-policy' config: - maxInlineBytes: 50000 + maxInlineTokens: 12500 ``` This separation is important. `web-fetch-http` still owns resource caps (`maxResponseBytes`, `maxBodyChars`) to protect network, memory, and decoding work. `spill-policy` owns only the model-facing context cap after the result already exists. If the provider already returned `truncated: true`, the spill file contains the full formatted result the tool returned, not the full original webpage; the policy does not claim otherwise. @@ -186,11 +157,11 @@ The local-backend value proposition depends on the existing `read`/`grep` tools **Snapshot gap.** No ACP snapshot scenario covers the transcript-visible `web_fetch` spill notice yet. The ACP snapshot harness replays keyless and cannot hit the live web, and a `web_fetch` spill requires a real over-cap HTTP body; a deterministic scenario would need a seeded loopback fetch target the replay tree does not currently wire (the examples do not load `tool-web` at all). The behavior is covered instead by the `dsh-tool-web` integration test against a loopback server. Closing the gap is follow-up work: wire `tool-web` + a seeded fetch target into the ACP example, then record a `web-fetch-spill` scenario. -The policy can become too large if it starts owning tool-specific semantics. It stays narrow: plain-text final results only. Tool-owned early spill remains future work. +The policy can become too large if it starts owning tool-specific semantics. It handles accepted text/image sequences without interpreting tool-specific text. Tool-owned early spill remains future work. ## Alternatives considered -**Require each tool to opt in with a retention declaration.** Rejected: the goal is a default behavior similar to Claude Code's generic tool-result persistence. A single `maxInlineBytes` deployment knob is enough to prove the shape. +**Require each tool to opt in with a retention declaration.** Rejected: the goal is a default behavior similar to Claude Code's generic tool-result persistence. A single `maxInlineTokens` deployment knob is enough to prove the shape. **Make `tool-results` a broad tool-result platform.** Rejected: a broad package name invites retention policy, result replacement, preview wording, search, and early spill into one seam. The shared storage part is smaller: save text and return a locator plus retrieval hint. diff --git a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md index a995eafc78..2941fc5167 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md @@ -66,36 +66,7 @@ interface SpillRef { ### spill 策略 -`dsh-spill-policy` 是一个 `tools/post-execute` 结果转换器,只提供一个配置项: - -```ts ignore-check -interface Config { - /** Omitted means no automatic spill policy. Present means apply to oversized plain text tool results. */ - maxInlineBytes?: number -} -``` - -省略 `maxInlineBytes` 时,插件不会注册任何内容,是真正的无操作。设置该值后,它会对最终的纯文本工具结果应用默认策略: - -1. 让工具正常运行,通过 `next()` 委托,使下游监听器先结算结果。 -2. 仅当已接受的最终 `ContentBlock[]` 全部是纯文本时,才将其展平;含任何非文本块的结果保持不变。 -3. 如果 UTF-8 字节大小不超过 `maxInlineBytes`,保持不变。 -4. 如果超出上限,使用完整的最终文本调用 `ctx.spillStore.saveText()`。 -5. 把模型可见结果替换为保留的首尾预览和 spill 引用。 - -预览属于策略所有的实现默认值:以 `maxInlineBytes` 为上限,使用保留库的 `TextRetainer` 进行首尾分割。只有第二个部署证明有此需求后,未来配置才会公开预览大小。 - -替换文本刻意保持通用,因为策略只知道最终格式化的工具结果,不了解工具的内部资源: - -```text - - -(Omitted N bytes. Full formatted result stored at: /.../session-.../....txt. Use read with offset/limit, or grep this path to search within it.) -``` - -如果 `ctx.spillStore.saveText()` 失败(权限、ENOSPC、后端不可用),或调用没有会话所有者,或未加载后端,插件会记录原因并原样返回结果。spill 失败绝不会把成功的工具调用变为 `isError` 结果,也不会隐藏内联结果。 - -策略跳过 `read`,以避免形成 `read -> spill file -> read again` 循环。额外的选择退出配置要等确实出现第二个有此需求的工具后再引入。 +`dsh-spill-policy` 在执行后策略接受结果之后,按 `maxInlineTokens` 对文字和图片共同计量并保留首尾。完整结果仍通过 `saveText()` 保存,图片本体留在附件存储,结果文件记录可读取路径。图片投影顺序、整图省略和模型计量由[图文结果保留决策](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.zh.md)负责。省略配置时不安装监听器,存储失败时保留原结果;`read` 的模型可见结果跳过自动省略,避免重复读取产生循环。 ## 示例:web_fetch @@ -129,7 +100,7 @@ ctx.tools.register(defineTool({ - id: spill-policy name: '@deepseek-ai/dsh-spill-policy' config: - maxInlineBytes: 50000 + maxInlineTokens: 12500 ``` 这项分离很重要。`web-fetch-http` 仍负责资源上限(`maxResponseBytes`、`maxBodyChars`),用来保护网络、内存和解码工作。`spill-policy` 只负责结果已经存在后针对模型上下文的上限。如果提供方已经返回 `truncated: true`,spill 文件包含的是工具返回的完整格式化结果,而不是原始网页全文;策略不会做出其他承诺。 @@ -186,11 +157,11 @@ ctx.tools.register(defineTool({ **快照缺口。** 目前没有 ACP 快照场景覆盖 transcript(文本记录)可见的 `web_fetch` spill 提示。ACP 快照 harness 在无密钥环境中回放,无法访问实时 web,而 `web_fetch` spill 需要一个真实的超上限 HTTP 正文;确定性场景需要一个预置的 loopback fetch 目标,但当前回放树尚未接线(示例根本没有加载 `tool-web`)。该行为改由 `dsh-tool-web` 针对 loopback server 的集成测试覆盖。弥补该缺口属于后续工作:把 `tool-web` 和预置 fetch 目标接入 ACP 示例,然后录制 `web-fetch-spill` 场景。 -如果策略开始负责工具专用语义,就会膨胀得过大。它的范围保持狭窄:只处理纯文本最终结果。由工具负责的提前 spill 仍留作未来工作。 +如果策略开始负责工具专用语义,就会膨胀得过大。它处理已接受的图文序列,不解释工具专用的文字语义。由工具负责的提前 spill 仍留作未来工作。 ## 考虑过的替代方案 -**要求每个工具通过保留声明选择加入。**不予采纳,因为目标是实现类似 Claude Code 通用工具结果持久化的默认行为。只需一个 `maxInlineBytes` 部署配置项即可验证该形态。 +**要求每个工具通过保留声明选择加入。**不予采纳,因为目标是实现类似 Claude Code 通用工具结果持久化的默认行为。只需一个 `maxInlineTokens` 部署配置项即可验证该形态。 **把 `tool-results` 建成宽泛的工具结果平台。** 不予采纳:宽泛的包名会诱使系统把保留策略、结果替换、预览措辞、搜索和提前 spill 合并进一个 seam。可共享的存储部分更小:保存文本,并返回定位符与检索提示。 diff --git a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml index ec2f5318d6..ee57a4b8cf 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml @@ -1,6 +1,31 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md -2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md: 89c8917484c11057ff2e6b55e2311e83f724f7ed -2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: 48f954f2c4720894548ecab387965d3a3d1799b0 +/agent-note-after-call-compaction-pressure-and-context-overflow-recovery: + en: 8bfe0d8b79d9840f + zh: 6921dc88e5ef9c83 +/agent-note-after-call-compaction-pressure-and-context-overflow-recovery/problem: + en: 036da3e545debe29 + zh: 82e031a3120ca36c +/agent-note-after-call-compaction-pressure-and-context-overflow-recovery/decision: + en: a15c7c13f167e5ac + zh: 63bcc2ef60c2f344 +/agent-note-after-call-compaction-pressure-and-context-overflow-recovery/decision/successful-pressure-runs-at-the-next-pre-step-boundary: + en: 5f5a656b38730ec6 + zh: aece66b85cc610cf +/agent-note-after-call-compaction-pressure-and-context-overflow-recovery/decision/request-recovery-is-limited-to-the-final-model-boundary: + en: fb7da851bf6a020e + zh: b40dc932d6c3101c +/agent-note-after-call-compaction-pressure-and-context-overflow-recovery/decision/compactionengine-exposes-intent-not-token-accounting: + en: be9505b3d73c7f6b + zh: ed753f1afec2787a +/agent-note-after-call-compaction-pressure-and-context-overflow-recovery/testing: + en: b0e1704bd3724301 + zh: 2279353ff615f183 +/agent-note-after-call-compaction-pressure-and-context-overflow-recovery/alternatives-considered: + en: 3cb5363732ef5758 + zh: 07746411cc80878f +/agent-note-after-call-compaction-pressure-and-context-overflow-recovery/consequences: + en: dfe515fb04127eb5 + zh: 1924b2e1e9997910 diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index e09f7db568..5e078a7885 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -1,6 +1,43 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-10-single-file-executable-sdk-runtime-distribution.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md -2026-07-10-single-file-executable-sdk-runtime-distribution.md: 7260d11c84f8fb07cf6c4691357f92e0d00fd8f3 -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 653fb3d9b782d9010f693e4af8ad94475abb9d94 +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe: + en: 00b5d91d3e44a9f8 + zh: eabd3c4b338249b3 +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/problem: + en: 112178b76e5209f6 + zh: 68726254ae530566 +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision: + en: a15c7c13f167e5ac + zh: 63bcc2ef60c2f344 +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/packaging-route-yao-pkg-pkg-s-sea-mode: + en: f1c2b60e9474111d + zh: 5de398b5e8554429 +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/the-serving-interface-is-a-plugin-inside-the-dsh-application: + en: 5fdde26e9e8c6108 + zh: 629559d733c1f0c5 +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/plugin-resolution-the-vfs-holds-a-real-package-tree-the-closure-manifest-is-the-deploy-root: + en: c35ea4b858448b49 + zh: 1f8ff0a9f82ca881 +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/build-pipeline-and-artifacts: + en: 53ba44431d2ce411 + zh: 4159475549273422 +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/python-sdk-distribution-two-carriers-exe-for-production-node-for-development: + en: 9bd60549d4a52ec0 + zh: e2343b7210212ee0 +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/naming-lineage: + en: 31a5aecb8bf828bb + zh: 2dffb6fd2e0f8194 +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/packaged-workflow-and-code-execution: + en: 0426f598f3f44e64 + zh: fc66728e17f3bcfb +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/testing: + en: b3c9820f4dc7c6c8 + zh: 3b5768ef1770bfcb +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/alternatives-considered: + en: 50d965ca81a401ba + zh: 0c6296245b97b9ac +/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/consequences: + en: 601a72b3a7a91fe4 + zh: 5baaa866e789923e diff --git a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml index 16591b4f3e..6d5a779689 100644 --- a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml @@ -1,6 +1,157 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-12-agent-scope-runtime-design.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md -2026-07-12-agent-scope-runtime-design.md: b9b2c837f560c2b8b98da37e3656caf298b705e2 -2026-07-12-agent-scope-runtime-design.zh.md: 4b94d1b8449bd619c47f6bee0712ec8ed0b39593 +/agent-note-agent-scope-runtime-design-and-correctness: + en: b63ecbd21fc1f427 + zh: 4990d96d76888802 +/agent-note-agent-scope-runtime-design-and-correctness/problem: + en: ae7b688bd991a6df + zh: 73b6b28755852054 +/agent-note-agent-scope-runtime-design-and-correctness/decision: + en: cfd6abca8404c7d4 + zh: a9372aae85e49f55 +/agent-note-agent-scope-runtime-design-and-correctness/cordis-model-context-fiber-effect-receiver-and-waterfall: + en: 4d4bdb629a57139d + zh: 7b4d21d9eb42d94b +/agent-note-agent-scope-runtime-design-and-correctness/cordis-model-context-fiber-effect-receiver-and-waterfall/a-context-is-an-ownership-path-through-one-service-graph: + en: 201a1882518455e2 + zh: 830e25eab7ed37db +/agent-note-agent-scope-runtime-design-and-correctness/cordis-model-context-fiber-effect-receiver-and-waterfall/fibers-and-effects-make-cleanup-structural: + en: d0ef6e1b5d453350 + zh: 4a774ac9daf20849 +/agent-note-agent-scope-runtime-design-and-correctness/cordis-model-context-fiber-effect-receiver-and-waterfall/receivers-route-listeners-waterfalls-compose-decisions: + en: 594e198a8b566049 + zh: 50378e7a2a533dff +/agent-note-agent-scope-runtime-design-and-correctness/scope-routing-one-opaque-key-selects-one-layer: + en: fa59d22549e9e44a + zh: 80904729042e87b3 +/agent-note-agent-scope-runtime-design-and-correctness/scope-routing-one-opaque-key-selects-one-layer/scope-identity-uses-object-identity: + en: 3a05a7bfb2197f58 + zh: fbb599031788b70b +/agent-note-agent-scope-runtime-design-and-correctness/scope-routing-one-opaque-key-selects-one-layer/registry-reads-overlay-one-exact-layer: + en: 28636ae6d55935d7 + zh: 33a989f049de2f3e +/agent-note-agent-scope-runtime-design-and-correctness/scope-routing-one-opaque-key-selects-one-layer/fused-dispatch-helpers-prevent-subject-drift: + en: 638e3711206e3c41 + zh: 61d370123d30c1ed +/agent-note-agent-scope-runtime-design-and-correctness/agent-creation-one-transaction-owns-the-complete-operation: + en: ef58215c8554180b + zh: 44571074148d2467 +/agent-note-agent-scope-runtime-design-and-correctness/agent-creation-one-transaction-owns-the-complete-operation/registry-entries-are-the-only-live-identity-records: + en: 092b49671e83df98 + zh: 330ea6e45c4deb99 +/agent-note-agent-scope-runtime-design-and-correctness/agent-creation-one-transaction-owns-the-complete-operation/the-transaction-owns-preparation-before-awaiting-it: + en: 3940920908ec6066 + zh: 888e36ca43611d30 +/agent-note-agent-scope-runtime-design-and-correctness/agent-creation-one-transaction-owns-the-complete-operation/setup-is-trusted-composition-inside-a-private-world: + en: 40a73b8eede856ee + zh: 7dab8f2548cb92ea +/agent-note-agent-scope-runtime-design-and-correctness/agent-creation-one-transaction-owns-the-complete-operation/publication-has-one-ordered-commit-path: + en: 227eff7a5adacd0e + zh: b9ee423c2dd4212f +/agent-note-agent-scope-runtime-design-and-correctness/agent-creation-one-transaction-owns-the-complete-operation/teardown-preserves-work-before-revoking-registrations: + en: 1cbe77cff192815d + zh: 7f517936a8aa469e +/agent-note-agent-scope-runtime-design-and-correctness/session-append-materialize-validate-commit-notify: + en: cc37e93081fea4cb + zh: daf2234f84e4c9bf +/agent-note-agent-scope-runtime-design-and-correctness/session-append-materialize-validate-commit-notify/durable-data-is-materialized-once: + en: d0a57b0ac227d84c + zh: e9ae6ced0e65fe49 +/agent-note-agent-scope-runtime-design-and-correctness/session-append-materialize-validate-commit-notify/pre-commit-listeners-can-veto-post-commit-observers-cannot: + en: 15255d3d0b62fac0 + zh: c1995d5aa3c78f0a +/agent-note-agent-scope-runtime-design-and-correctness/trust-boundaries-copy-only-when-ownership-actually-changes: + en: 22b381902fe903f3 + zh: 8a2efe9a8b458bba +/agent-note-agent-scope-runtime-design-and-correctness/tools-and-prompts-one-view-authoritative-assembly-committed-outcomes: + en: 456ac0359d3c02f6 + zh: 9099e15d22139751 +/agent-note-agent-scope-runtime-design-and-correctness/tools-and-prompts-one-view-authoritative-assembly-committed-outcomes/one-resolver-defines-the-tool-view: + en: 120e1c79f0b99b1a + zh: 1c21a5b1d6b08235 +/agent-note-agent-scope-runtime-design-and-correctness/tools-and-prompts-one-view-authoritative-assembly-committed-outcomes/tool-execution-owns-identity-and-boundary-materialization: + en: 41f1f2ad73d83d73 + zh: e6bff63958ede05e +/agent-note-agent-scope-runtime-design-and-correctness/tools-and-prompts-one-view-authoritative-assembly-committed-outcomes/the-assembly-waterfall-owns-the-final-model-visible-composition: + en: 5c5880ccea139058 + zh: 3e5b7d10cb4c6ff8 +/agent-note-agent-scope-runtime-design-and-correctness/tools-and-prompts-one-view-authoritative-assembly-committed-outcomes/structured-output-commits-only-authoritative-outcomes: + en: a9a711cf31559765 + zh: e9083b000fc89128 +/agent-note-agent-scope-runtime-design-and-correctness/tools-and-prompts-one-view-authoritative-assembly-committed-outcomes/three-execution-boundaries-are-deliberately-one-way: + en: a109f3fadf338061 + zh: 12eb46fe775fa46a +/agent-note-agent-scope-runtime-design-and-correctness/tools-and-prompts-one-view-authoritative-assembly-committed-outcomes/skill-and-approval-services-trust-typed-callers: + en: 9572833d47f779e6 + zh: 7ff491d309060030 +/agent-note-agent-scope-runtime-design-and-correctness/subagents-publication-is-the-start-promise: + en: b9216021f8f50bcc + zh: fda52f9434cc8191 +/agent-note-agent-scope-runtime-design-and-correctness/subagents-publication-is-the-start-promise/the-service-contract-has-one-cancellation-channel: + en: 84a4be83b78c045b + zh: 043e14cf628413cd +/agent-note-agent-scope-runtime-design-and-correctness/subagents-publication-is-the-start-promise/in-process-providers-reuse-the-core-transaction: + en: b5e0b3dcf0dff750 + zh: 63905010383781b9 +/agent-note-agent-scope-runtime-design-and-correctness/subagents-publication-is-the-start-promise/acp-providers-own-the-process-until-readiness-or-cleanup: + en: aa0c00cee368c7e6 + zh: 767539b0f4d56f55 +/agent-note-agent-scope-runtime-design-and-correctness/workflows-and-acp-processes-retain-only-independent-async-facts: + en: effbb0acf7f66bed + zh: e0cc9ad6ac0777de +/agent-note-agent-scope-runtime-design-and-correctness/workflows-and-acp-processes-retain-only-independent-async-facts/workflow-children-are-pending-starts-or-published-records: + en: 9f05eed800f3413c + zh: 8230a28a0f94bffa +/agent-note-agent-scope-runtime-design-and-correctness/workflows-and-acp-processes-retain-only-independent-async-facts/terminal-result-and-physical-cleanup-remain-separate: + en: 5ad141c6df9f4fc3 + zh: 63effa4f14f3c530 +/agent-note-agent-scope-runtime-design-and-correctness/workflows-and-acp-processes-retain-only-independent-async-facts/acp-prompt-settlement-does-not-depend-on-update-delivery: + en: d39f73f50f0370bd + zh: 3889c1db46de560e +/agent-note-agent-scope-runtime-design-and-correctness/correctness-enforcement: + en: 840564e766d2f05c + zh: 059243afea7eb9c0 +/agent-note-agent-scope-runtime-design-and-correctness/correctness-enforcement/types-make-the-ordinary-path-hard-to-misuse: + en: 9183eb29dddfd97f + zh: e6ce13295e6c3e65 +/agent-note-agent-scope-runtime-design-and-correctness/correctness-enforcement/runtime-invariants-cover-cross-service-facts: + en: fabafbebc0ab4f95 + zh: 06d94a98ed5aa40c +/agent-note-agent-scope-runtime-design-and-correctness/correctness-enforcement/generated-artifacts-keep-public-contracts-aligned: + en: d0ddc5f38289225d + zh: 781d910228d5e334 +/agent-note-agent-scope-runtime-design-and-correctness/alternatives-considered: + en: 5eef7873a795c8f0 + zh: 3e8ba109a6e29ba5 +/agent-note-agent-scope-runtime-design-and-correctness/alternatives-considered/use-a-transparent-proxy-as-the-scope-carrier: + en: 199c7345a237e7e9 + zh: b262d5ad0c7cb562 +/agent-note-agent-scope-runtime-design-and-correctness/alternatives-considered/reserve-agent-and-session-ids-before-setup: + en: 3a8cb6969139dd84 + zh: 2913bb68433c9ec7 +/agent-note-agent-scope-runtime-design-and-correctness/alternatives-considered/snapshot-every-typed-same-process-argument: + en: 3e1f19f909f8d138 + zh: 0727777e34964afe +/agent-note-agent-scope-runtime-design-and-correctness/alternatives-considered/give-readiness-cancellation-and-disposal-separate-controllers: + en: f2c6ecbd139bbdb0 + zh: 0ff5b1e192dcb376 +/agent-note-agent-scope-runtime-design-and-correctness/alternatives-considered/keep-synchronous-subagent-start-plus-run-started: + en: 7f5669933cb35c9c + zh: c55e35d4c175af36 +/agent-note-agent-scope-runtime-design-and-correctness/alternatives-considered/restore-selected-prompt-or-tool-contributions-after-assembly: + en: a2b2e48d9ccda310 + zh: 3a4e925fde9d05b0 +/agent-note-agent-scope-runtime-design-and-correctness/alternatives-considered/remove-worker-process-lifecycle-guards-with-same-process-hardening: + en: 662e0904d90a3a94 + zh: 84c5e49f7cc59b32 +/agent-note-agent-scope-runtime-design-and-correctness/consequences: + en: 0d781b2dc4c41ddb + zh: 844652a4aaac37aa +/agent-note-agent-scope-runtime-design-and-correctness/consequences/what-the-design-guarantees: + en: eb2001592d7bd7b1 + zh: e534dbb6daa9af32 +/agent-note-agent-scope-runtime-design-and-correctness/consequences/costs-and-limits: + en: e9383713d56b9e10 + zh: d3ab00aea8ecf4e0 diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml index 07a0a60247..7d88d4ba73 100644 --- a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml @@ -1,6 +1,37 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-14-provider-routed-llm-adapters.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md -2026-07-14-provider-routed-llm-adapters.md: 1001b10e18837399b41578658ffe62065e294ba8 -2026-07-14-provider-routed-llm-adapters.zh.md: c5bff0ef407c8a2c22e8cad7e7d32c58c8b0e2da +/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend: + en: e57698907c40f783 + zh: 47004d8f68af1d7d +/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/problem: + en: 2e51f5fa703b0677 + zh: 385937219b88c447 +/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/decision: + en: a15c7c13f167e5ac + zh: 63bcc2ef60c2f344 +/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/decision/provider-is-the-adapter-registration-key: + en: 4921a060c657b790 + zh: 2f9632469c6f0b60 +/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/decision/explicit-pi-ai-provider-profiles: + en: 5461685553dac663 + zh: e5c7e0ad7ac84e29 +/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/decision/recorded-assistant-route-and-replay-state: + en: 8439e90c9f6d7642 + zh: daeda3ce5146c9f5 +/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/decision/propagate-the-target-through-every-request-producer: + en: ba1908bc1517ed23 + zh: 1c9353a422cf74e1 +/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/alternatives-considered: + en: ad0d610ea92aa9b8 + zh: fe09d62762e73f01 +/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/consequences: + en: a0c98bced8a5e6fb + zh: 23caf082a08ba507 +/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/testing: + en: 5a36121f12701d08 + zh: 90f0f8ed44f7234f +/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/risks: + en: 11db2a349f9f26c2 + zh: 550d6017d8639854 diff --git a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml index 99577f250e..a90633f326 100644 --- a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml @@ -1,6 +1,22 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-15-agent-initiator-scope.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md -2026-07-15-agent-initiator-scope.md: ab11da116a463cd706418e797eb58f1bc4ab9b1c -2026-07-15-agent-initiator-scope.zh.md: 343acba5f99379b6d2c3af41368e8fbe90b20611 +/agent-note-initiating-agent-scope-over-asynclocalstorage: + en: b0a59e3a34c41826 + zh: 7131428a14901d14 +/agent-note-initiating-agent-scope-over-asynclocalstorage/problem: + en: a26991200892c735 + zh: 04a7dfaca64e3f0c +/agent-note-initiating-agent-scope-over-asynclocalstorage/decision: + en: 43a4f0c503b30d95 + zh: a6a780584f94ed73 +/agent-note-initiating-agent-scope-over-asynclocalstorage/verification: + en: 312f5c4b843d515e + zh: a8993e3b84844fab +/agent-note-initiating-agent-scope-over-asynclocalstorage/alternatives-considered: + en: fd2ae1369fb3fbc6 + zh: e2a3d7f9331fc8ac +/agent-note-initiating-agent-scope-over-asynclocalstorage/consequences: + en: 625c6afc49489e29 + zh: ed7e5c5e56c781fb diff --git a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml index a3dae54b9b..a7c6542f24 100644 --- a/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.i18n.yaml @@ -1,6 +1,22 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-16-explicit-turn-cancellation.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md -2026-07-16-explicit-turn-cancellation.md: d59accf2dc3f894ffab226e5ddc9d2e746f4d93e -2026-07-16-explicit-turn-cancellation.zh.md: 8fb0728eb2bbe8fa01dbefebe7e02aa8c836617a +/agent-note-explicit-turn-cancellation-capability: + en: 729ed6291e9d4d2c + zh: 32ae526ef41199da +/agent-note-explicit-turn-cancellation-capability/problem: + en: 0a7826345be2a509 + zh: 430fccb5c4d6a5e2 +/agent-note-explicit-turn-cancellation-capability/decision: + en: 2c15edb35f38e7e5 + zh: fa5145d26e3bff20 +/agent-note-explicit-turn-cancellation-capability/verification: + en: a7ab1d06542be1c0 + zh: 1687979b7f2460fa +/agent-note-explicit-turn-cancellation-capability/alternatives-considered: + en: bf69bbf7eb280306 + zh: a434a0ecb5008b2b +/agent-note-explicit-turn-cancellation-capability/consequences: + en: 1066aff1aec4ed01 + zh: 31d6a60496e2e730 diff --git a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml index 41d4664d7d..7354602835 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.i18n.yaml @@ -1,6 +1,34 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-19-cooperative-tool-cancellation.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md -2026-07-19-cooperative-tool-cancellation.md: 5ca2b44b24a4af189df27c6f724021a7bf290e2b -2026-07-19-cooperative-tool-cancellation.zh.md: d6d49fb44c5d5629729a8b5b4bad07ea9b2f7ee9 +/agent-note-cooperative-tool-cancellation-at-the-registry-boundary: + en: 84c827aa3baa331a + zh: 10cad03e58b10e31 +/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/problem: + en: e386b22491b468c1 + zh: 82015f5145b679cd +/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/decision: + en: ca66d3b6af50be10 + zh: f054b10d0c965e82 +/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/decision/mutability-follows-the-pipeline-stage: + en: 0e1399cad4151f87 + zh: 960fa0ccbe0f8fea +/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/decision/cancellation-codes-record-whether-dispatch-occurred: + en: c787fba8731ac147 + zh: 21b5968ae6f8cd59 +/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/decision/pre-aborted-entry-short-circuits-after-materialization: + en: 53362db6a0c71698 + zh: 311de45ca26d3fdc +/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/decision/started-work-still-reaches-quiescence: + en: 26e4b71366bebd1b + zh: 10598e12b28c586e +/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/verification: + en: fbd101d4576afa92 + zh: 35f5d3c2f6cad6a8 +/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/alternatives-considered: + en: 1f205f1a78d4fd5b + zh: 6c17e35da2909302 +/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/consequences: + en: 24a9ad2c4c7c86b5 + zh: debdd29d272e07ea diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml index a64c2c83c2..b48fcf3403 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml @@ -1,6 +1,40 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-19-gui-web-client-architecture.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md -2026-07-19-gui-web-client-architecture.md: 4d62d7ddbeddd2d5c02e42194b035ee08b5cf041 -2026-07-19-gui-web-client-architecture.zh.md: aeb6c89e677d7b65daef03d5d31372f484ae3117 +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer: + en: e940be9ef95509ff + zh: c06cea53bb49c718 +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer/problem: + en: c5a3dda6ccd57c9e + zh: 122307b113bb1c09 +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer/decision: + en: 29742775aee9fbc3 + zh: 54a2df8816e2ea2e +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer/the-client-cordis-tree-and-the-loading-chain: + en: 23b0b6c3f29e4f5e + zh: 53bf897b502c645b +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer/the-slot-system-how-the-page-composes: + en: 1661f3dd74dc9df6 + zh: 3152467c7dd8e6ef +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer/services-and-scope-addressing: + en: 9da9311f677c5da6 + zh: 9cb16a92cee87055 +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer/the-data-object-layer-packages-api-session-controller-src-client: + en: 909682e20c0d23a1 + zh: ed1defbcf7745e8e +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer/the-react-face-packages-client-ui-renderer: + en: 0971d6196b5358fd + zh: 26175d648feccfd9 +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer/directory-shape: + en: 74c45680c8aab51c + zh: fd431ae5d3656038 +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer/how-to-develop: + en: cbc6abcd9f291868 + zh: 0078f15f2d08a02e +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer/consequences: + en: 56a9086da5783306 + zh: 8147576101d22aa0 +/agent-note-web-client-architecture-the-client-cordis-plugin-tree-the-slot-system-and-the-react-free-object-layer/alternatives-considered: + en: 8bedf2e688b14322 + zh: 7cda12f7474832ee diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml index ec285a946f..f8fe485684 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml @@ -1,6 +1,28 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-19-package-invariant-runtime-contracts.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md -2026-07-19-package-invariant-runtime-contracts.md: a1b635cc40844f1846c04e203dbb842d1c7328ed -2026-07-19-package-invariant-runtime-contracts.zh.md: d315c440f1c100911386f57d6f82d5f16be631ca +/agent-note-meaningful-package-invariant-contracts: + en: e5db3b3373f72761 + zh: 312b83d320d9aaab +/agent-note-meaningful-package-invariant-contracts/problem: + en: 220a4ceb3cd9b9c9 + zh: 9eac93e1762b2914 +/agent-note-meaningful-package-invariant-contracts/decision: + en: a15c7c13f167e5ac + zh: 63bcc2ef60c2f344 +/agent-note-meaningful-package-invariant-contracts/decision/published-assertions-must-be-meaningful: + en: 11a1c080592def10 + zh: d1189de665f9e327 +/agent-note-meaningful-package-invariant-contracts/decision/representative-implemented-checks: + en: 2f7ae4ed588d6de8 + zh: ed7ea9ccc6c54e87 +/agent-note-meaningful-package-invariant-contracts/decision/repository-gate-and-tests: + en: 37e646754f13dd65 + zh: 355ebf3ce1403117 +/agent-note-meaningful-package-invariant-contracts/alternatives-considered: + en: 74c6a5a0cfadbf88 + zh: e41356bb1a8c1fdf +/agent-note-meaningful-package-invariant-contracts/consequences: + en: 1f8d53063fb5903e + zh: 5edd23739749b065 diff --git a/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.i18n.yaml index 5e0c9ab302..9e8a30179e 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.i18n.yaml @@ -1,6 +1,31 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-19-zstandard-jsonl-session-logs.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md -2026-07-19-zstandard-jsonl-session-logs.md: 96b76febee1c8853f70d7e4897894ea26390f524 -2026-07-19-zstandard-jsonl-session-logs.zh.md: 4cf184ce107b9de6dbbf3f0e571d4009d50ca636 +/agent-note-zstandard-jsonl-session-logs: + en: b46a6b48493de93e + zh: 0e394abf1d11fd43 +/agent-note-zstandard-jsonl-session-logs/problem: + en: da8b3d94d10b5bc4 + zh: da9c6d98a300b7bb +/agent-note-zstandard-jsonl-session-logs/decision: + en: a15c7c13f167e5ac + zh: 63bcc2ef60c2f344 +/agent-note-zstandard-jsonl-session-logs/decision/configuration-and-suffix-ownership: + en: 1f29970aecc71980 + zh: 94414765645bace0 +/agent-note-zstandard-jsonl-session-logs/decision/frame-and-write-path: + en: da213ed32dfa6000 + zh: a0f3cd11684dbdc9 +/agent-note-zstandard-jsonl-session-logs/decision/read-listing-and-crash-recovery: + en: 63d4fe7d9c2a0a8a + zh: 2c4ef3e6aad73195 +/agent-note-zstandard-jsonl-session-logs/decision/consumers-and-verification: + en: 6d9d16d610b80e55 + zh: efa083ffaa7301e0 +/agent-note-zstandard-jsonl-session-logs/alternatives-considered: + en: c5f447029fb644f7 + zh: 5b84162e87bc6c86 +/agent-note-zstandard-jsonl-session-logs/consequences: + en: e721552f16340e4c + zh: d5be8c3ffd1c7cc2 diff --git a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml index cf9b3216dc..531d05cdc9 100644 --- a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml @@ -1,6 +1,19 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-20-canonical-tool-output-contract.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md -2026-07-20-canonical-tool-output-contract.md: 4dbcac3da8381e69809b15a653cdb4987af4e0e7 -2026-07-20-canonical-tool-output-contract.zh.md: c2f4ebdfc7266495570766c69b3aa264f91172cc +/agent-note-canonical-tool-output-contract: + en: 01677f7875da9579 + zh: 41bae59177ba5125 +/agent-note-canonical-tool-output-contract/problem: + en: d0bc9840998f0530 + zh: 2601671ec694a78d +/agent-note-canonical-tool-output-contract/decision: + en: db088b8225a4bf3d + zh: bfb0caa015b2b7df +/agent-note-canonical-tool-output-contract/alternatives-considered: + en: d82a0ac0461ab120 + zh: bedea17fb4fb4f51 +/agent-note-canonical-tool-output-contract/consequences: + en: 32724ff8f6b3b03f + zh: bc7512ff05d00157 diff --git a/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.i18n.yaml index 081f618b1a..a21bb41371 100644 --- a/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.i18n.yaml @@ -1,6 +1,34 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-20-routed-model-context-and-compaction-policy.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md -2026-07-20-routed-model-context-and-compaction-policy.md: d276354cb21f45eaec1c4e3202c7be1770745efb -2026-07-20-routed-model-context-and-compaction-policy.zh.md: b6e2a0b914f78e19675d80b86d8d962e8ba8c80f +/agent-note-routed-model-context-and-compaction-policy: + en: eca190695d4857f5 + zh: f9d6411b8a8612ae +/agent-note-routed-model-context-and-compaction-policy/problem: + en: f7d1f343e487d725 + zh: c7f736180e44d586 +/agent-note-routed-model-context-and-compaction-policy/decision: + en: a15c7c13f167e5ac + zh: 63bcc2ef60c2f344 +/agent-note-routed-model-context-and-compaction-policy/decision/adapters-own-exact-route-capacity: + en: 7bcf0ae2c74a0eb7 + zh: bc0e04a25492d5ba +/agent-note-routed-model-context-and-compaction-policy/decision/token-measurement-remains-model-agnostic: + en: 51d1b39d06407320 + zh: 85c88f6b5d8508d6 +/agent-note-routed-model-context-and-compaction-policy/decision/compact-basic-resolves-a-target-spec: + en: d8827f9fa70aa65f + zh: a9d29e05dcb0cfdb +/agent-note-routed-model-context-and-compaction-policy/decision/target-specific-pressure-failures-preserve-optional-composition: + en: 1bd81ae0a1409e24 + zh: 635c2937d5a94df7 +/agent-note-routed-model-context-and-compaction-policy/testing: + en: 1ac13596df50571f + zh: 8605a6f0de049dfc +/agent-note-routed-model-context-and-compaction-policy/alternatives-considered: + en: 0ca0a4b1318188ec + zh: de98cd4494a6c0d0 +/agent-note-routed-model-context-and-compaction-policy/consequences: + en: d00f83e85ba5e431 + zh: 32665480f0cdc4ea diff --git a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml index 152bae16ac..bf011b57b9 100644 --- a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml @@ -1,6 +1,19 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-20-unified-json-value-schema-dsl.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.md -2026-07-20-unified-json-value-schema-dsl.md: 5de3523eab15a91ea32dc09e2e239146fadea6f1 -2026-07-20-unified-json-value-schema-dsl.zh.md: 0dc38dc289a32269c0f9e545ae53a8098ba02189 +/agent-note-unified-json-value-schema-dsl: + en: 8ba380205c472f4e + zh: de92e20dc90e8037 +/agent-note-unified-json-value-schema-dsl/problem: + en: 5c7e79e37c007896 + zh: 759f5803141b0960 +/agent-note-unified-json-value-schema-dsl/decision: + en: 8f08bbafc8939a46 + zh: 2ed8255c1e2b812b +/agent-note-unified-json-value-schema-dsl/alternatives-considered: + en: 1d32d6195d6fca08 + zh: ea3a30126dca95f7 +/agent-note-unified-json-value-schema-dsl/consequences: + en: 14d0a6a337503e80 + zh: c7ffec8760a710af diff --git a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml index f633a831d3..eeeda135fe 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml @@ -1,6 +1,46 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-22-slot-type-chain-implementation.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md -2026-07-22-slot-type-chain-implementation.md: 98585a42f71844594da4ada19d7438a7d8321702 -2026-07-22-slot-type-chain-implementation.zh.md: d1093537ccbc5ee5ff2dc18c234233ffca39879d +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat: + en: 43ed5b2dfd8043fc + zh: 04b6fdd0435a265a +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/problem: + en: 4f8d6891a8ba7061 + zh: 5a619e562e00458d +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision: + en: a81cddbebc1a97a3 + zh: aa57738c9f08c07e +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/root-is-the-only-a-priori-slot: + en: 43b856b4a8cf371f + zh: cde298ab8b8a3815 +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/register-is-the-single-api-children-declaration-authorization-runtime-spec: + en: ce28268021a38b89 + zh: 7802540e4ef04752 +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/component-props-four-shares-each-from-its-own-source-of-truth: + en: f3fca569fa0c5168 + zh: 647a82662f6a887d +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/the-chain-kind-entries-self-nominate-first-match-renders: + en: 56278be7f3688805 + zh: 16602ffbf4baef85 +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/the-store-seat-framework-engine-registrant-schema: + en: 2645acc9d29c20ec + zh: a8a2fd293ef5da04 +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/inject-the-registrant-s-business-face-on-its-own-ctx: + en: 66b311fad441400a + zh: 34f031e7f04d29e6 +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/data-boundary-discipline: + en: 07b919360c8f65be + zh: 4c8a9915a2ed20af +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/tree-context-and-the-renderer-contract: + en: d77ebd3f6b166194 + zh: 57ba9fe864908d46 +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/type-chain-implementation-rulings: + en: 752bc30428dad374 + zh: 3d729646468ecfde +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/consequences: + en: 07228332f3d3c227 + zh: 32525e1972acf156 +/agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/alternatives-considered: + en: 2bd28715f741ac04 + zh: 8d6a0cb78a006533 diff --git a/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.i18n.yaml index 730cfb413c..142497a1d9 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.i18n.yaml @@ -1,6 +1,40 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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: +# Bilingual-pair consistency record for 2026-07-23-client-plugin-loading-model.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md -2026-07-23-client-plugin-loading-model.md: a8d016a70792ad4ac6a71083677ce5d82a30af0a -2026-07-23-client-plugin-loading-model.zh.md: 1b94909c9611f01e5a6d7fcb50ec3f91ee88fab1 +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload: + en: 874618fd5ad1d06a + zh: a5e4bfad490fab34 +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/problem: + en: 46c4b208707e024e + zh: b6f86a6374b251da +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision: + en: a15c7c13f167e5ac + zh: a15c7c13f167e5ac +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/package-membership-and-module-requests: + en: 6af228b03e5e533f + zh: 276a522af6be5d7a +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/one-module-system-one-plugin-governor: + en: 575aad62490f32e9 + zh: a6c18eb63965fc1e +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/combo-external-script-arrival-and-source-maps: + en: 0a4240d88cca240a + zh: 8f5f1527caf571cd +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/the-loading-flow-end-to-end: + en: 7dac236d6add3c3c + zh: 2a43a923a0646bb0 +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/live-graph-reconciliation: + en: 37e2fa1fe5cb2b95 + zh: 80e513db87709cb4 +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/hot-reload-one-driver-plugin-self-watched-bundles: + en: f8b591d4c41ef492 + zh: bc70f1cd540b7753 +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/package-ownership: + en: 94ca0d09e6fd1e3a + zh: d5949a2bdec795ba +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/consequences: + en: 6a96d9ac06c222e5 + zh: b2814ab58348e831 +/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/alternatives-considered: + en: b8f036f50cff7cd8 + zh: 6d623c4a1f4314b1 diff --git a/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md b/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md index a8d016a707..b8c23108a7 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md +++ b/.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md @@ -46,7 +46,7 @@ A `require.async("./client..js")` call requests the exact revisioned `/plu The shared tsdown preset emits a map for every plugin entry and chunk and rewrites first-party source paths into the browser-resolvable repository form `/packages///src/...`. The production Client pass consumes `lib/types`; the preset supplies each tsc map to Rolldown and fills `sourcesContent` from the original files, so the final map reaches TypeScript/TSX instead of stopping at emitted JavaScript. Other workspace sources inlined into a bundle likewise resolve to their `packages/` owner, while dependency paths remain unchanged. Combo generation strips each local debug directive, records its generated-line offset, resolves every authored source against the original per-plugin map URL, and emits an Indexed Source Map v3. An authored map supplies its section; otherwise an identity section embeds the generated bundle and uses the packer's `sourceURL` as its source name when present. The absolute combo map URL mirrors the script resource list by changing every `client.js` suffix to `client.js.map`, so `/plugins/??/client.js,/client.js&rev=` points to `/plugins/??/client.js.map,/client.js.map&rev=`. A chunk script likewise points to its own map URL. Source-map files are read and combined only when those map URLs receive `GET`; `HEAD` materializes neither script nor map bodies. The Vite shell also emits source maps, letting shell code and externally loaded plugins map stacks and performance profiles back to TypeScript/TSX. -The graph retains each row's revisioned one-resource combo URL for HMR and adds a revisioned descriptor for every startup combo request; several descriptors may carry the same scheduling phase. Initial row revisions are opaque process nonces rather than content hashes; they keep the snapshotted one-resource response immutable without hashing every plugin at startup. The shared preset stamps the entry after every package output is written. After the watcher observes that completed-build stamp, `rebuilt(id)` hashes the entry bytes together with its timestamp and publishes the resulting revision; a chunk-only build therefore advances the owner revision without a Host-side sibling scan. Startup combo revisions derive from the ordered row revisions. Script bodies are assembled on their first `GET`; source-map files are read and combined separately on their first map `GET`. `HEAD` materializes neither body. Versioned scripts and maps use immutable caching, and unrelated graph recomposition retains already-materialized chunk responses at the same revision. The Host serves only exact generated URLs; stale revisions and unadvertised resource lists return 404 instead of aliasing different bytes. An external script's `error` event exposes neither response status nor body, so failure diagnostics name only the URL; the same-origin Host and build-stamped registration id form the identity boundary, while the post-`load` factory-presence check rejects an artifact that did not register the expected id. +The graph retains each row's revisioned one-resource combo URL for HMR and adds a revisioned descriptor for every startup combo request; several descriptors may carry the same scheduling phase. Initial publication and `rebuilt(id)` use the same hash of entry mtime, ctime, and size; artifact contents never enter the hash. Ctime distinguishes rewrites that preserve mtime and size. A Host restart over unchanged artifacts therefore retains every row revision, so the reconnecting SSE snapshot does not trigger browser plugin replacement. The shared preset stamps the entry after every package output is written; a chunk-only build therefore advances the owner revision without a Host-side sibling scan. Startup combo revisions derive from the ordered row revisions. Script bodies are assembled on their first `GET`; source-map files are read and combined separately on their first map `GET`. `HEAD` materializes neither body. Versioned scripts and maps use immutable caching, and unrelated graph recomposition retains already-materialized chunk responses at the same revision. The Host serves only exact generated URLs; stale revisions and unadvertised resource lists return 404 instead of aliasing different bytes. An external script's `error` event exposes neither response status nor body, so failure diagnostics name only the URL; the same-origin Host and build-stamped registration id form the identity boundary, while the post-`load` factory-presence check rejects an artifact that did not register the expected id. ### The loading flow, end to end @@ -56,7 +56,7 @@ What happens between `dsh web` starting and the UI appearing? Three stages: the 1. The composing app (`apps/cli`) ships the roster as ordinary rows in its `cordis.yml` config tree — client plugin packages are entry rows like every host plugin, including the always-mounted `client-hmr` row. `auditStartupEntries` reports failed imports, activation errors with their original stacks, and pending dependencies; optional entries warn and required entries reject startup ([startup policy](2026-09-09-consumer-owned-startup-strictness.md)). 2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) resolves each live Loader entry through the same `name` and owning-tree `baseUrl` inputs that imported its Host face, then reads the nearest owning package.json `dsh.client` declaration and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }], batches: [{ phase, url, rev, entries }] }`. The manifest package name is the browser module identity even when an overlay names a relative source or built entry file. Distinct active Loader sources resolving to one package name fail composition; after one source unloads, the surviving source supplies the row without a fiber restart. The row's three optional fields come from manifests, never hand-copied. Composition orders requested dynamic rows before their consumers, rejects synchronous request cycles, and assigns every row to exactly one initial batch. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the Host audit reports either error from the FAILED fiber. -3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per entry name and owning-tree base URL for the process lifetime and bundle re-hashing reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Initial rows receive an opaque process nonce plus sequence without hashing their artifacts; startup combo revisions hash the combined script inputs plus indexed map, and the rows plus batch descriptors hash into `graph.rev`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph, while modules registers the combo route and contributes structured index-injection rows. +3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per entry name and owning-tree base URL for the process lifetime and changed bundle publication reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Initial rows hash their pre-read filesystem metadata; startup combo revisions hash the ordered row identities and revisions, and the rows plus batch descriptors hash into `graph.rev`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph, while modules registers the combo route and contributes structured index-injection rows. Why is the roster yml rows and not a scan? Because which plugins compose into a deployment is a composition decision, not a package property — a package declaring `dsh.client` in the repo does not mean this deployment mounts it, so discovery-by-scan cannot make that call; the node half scans only what the tree actually mounted. @@ -80,7 +80,7 @@ The Host SSE adapter forwards existing graph-change notifications and sends the Hot reload is a composition decision: the web bundle mounts the `client-hmr` row (a normal plugin package) unconditionally; its node half brings the bundle watch and the SSE channel, and the chain stays idle until a rebuild watcher rewrites client bundles. A composition that must not expose it disables the row. -How does a rebuilt bundle become a reload signal? The hmr node half observes it itself — no builder tells it. Before reading each startup snapshot, the module host captures the entry stat baseline and exposes it through `ctx.clientModules.artifactBaseline(id)`. One HMR-owned interval compares every current graph row with that baseline. An unchanged row starts watching without a content read or hash; a write after baseline capture is already a stat delta and only that row enters `rebuilt(id)`. This avoids both an initial all-row re-hash and `fs.watchFile`, whose asynchronous first baseline can silently absorb a construction-time rebuild. Watch membership follows `onGraphChanged`; vanished rows drop out, and an entry missing at poll time keeps its row dirty so reappearance forces a new revision even with identical metadata. The shared tsdown preset stamps `client.js` after every sibling output has been written; on an entry mtime or size delta, or a dirty row, `rebuilt(id)` derives the revision from the entry bytes and completed-build timestamp. A chunk-only edit therefore changes the revision, and a partial write followed by the completion stamp supplies a later stat change that self-heals. When the `rev` changes, the node half broadcasts a `rebuilt` frame on `GET /plugins/events` — a system SSE channel that sends the full graph on connect and `rebuilt` frames on change, presentation-only wire that never enters the session log. Polling is deliberate because inotify does not fire on the weka network mount, the same reason the build-side watcher needs `--poll`; one entry stat per row and interval is sufficient, the interval is a validated config field (default 500ms), and disposal clears the one timer. Rebuilding artifacts is any process using the shared Client tsdown preset — `scripts/dev-web.ts` remains the watch-build entry point, discovering its package list through `dsh.client` while scanning `packages/*/*/package.json` at startup — and builder and host share no notification protocol. +How does a rebuilt bundle become a reload signal? The hmr node half observes it itself — no builder tells it. Before reading each startup snapshot, the module host captures the entry stat baseline and exposes it through `ctx.clientModules.artifactBaseline(id)`. One HMR-owned interval compares every current graph row with that baseline. An unchanged row starts watching without a content read or hash; a write after baseline capture is already a stat delta and only that row enters `rebuilt(id)`. This avoids redundant reads when installing the watcher and avoids `fs.watchFile`, whose asynchronous first baseline can silently absorb a construction-time rebuild. Watch membership follows `onGraphChanged`; vanished rows drop out, and an entry missing at poll time keeps its row dirty so reappearance retries publication even when mtime and size match the baseline. The shared tsdown preset stamps `client.js` after every sibling output has been written; on an entry mtime, ctime, or size delta, or a dirty row, `rebuilt(id)` derives the revision from those three metadata values. It reads the new bundle snapshot only if the revision changes. A chunk-only edit therefore changes the revision, and a partial write followed by the completion stamp supplies a later stat change that self-heals. When the `rev` changes, the node half broadcasts a `rebuilt` frame on `GET /plugins/events` — a system SSE channel that sends the full graph on connect and `rebuilt` frames on change, presentation-only wire that never enters the session log. Polling is deliberate because inotify does not fire on the weka network mount, the same reason the build-side watcher needs `--poll`; one entry stat per row and interval is sufficient, the interval is a validated config field (default 500ms), and disposal clears the one timer. Rebuilding artifacts is any process using the shared Client tsdown preset — `scripts/dev-web.ts` remains the watch-build entry point, discovering its package list through `dsh.client` while scanning `packages/*/*/package.json` at startup — and builder and host share no notification protocol. On the browser side, the transport delegates code replacement to the same modules controller that owns graph reconciliation: @@ -120,4 +120,4 @@ Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordi | Full ctx-ification now (React and libraries via services, no module table) | Static UI libraries still expose synchronous values, so removing the table would leave those imports without a shared identity | | Eager instantiation with a frozen table | Runs bundle side effects at script arrival; lazy registration keeps execution at Cordis import while recursive `require` materializes registered requests | | Fetch response text, then inject an inline ` + + +
+ + diff --git a/apps/desktop/resources/icon-windows.png b/apps/desktop/resources/icon-windows.png index 02bc5931d3..cc8a09e311 100644 Binary files a/apps/desktop/resources/icon-windows.png and b/apps/desktop/resources/icon-windows.png differ diff --git a/apps/desktop/resources/icon-windows.svg b/apps/desktop/resources/icon-windows.svg index 4ab25b9e70..212560ed20 100644 --- a/apps/desktop/resources/icon-windows.svg +++ b/apps/desktop/resources/icon-windows.svg @@ -1,9 +1,9 @@ - - - - + + + + diff --git a/apps/desktop/resources/tray-windows.ico b/apps/desktop/resources/tray-windows.ico new file mode 100644 index 0000000000..e60f1cb885 Binary files /dev/null and b/apps/desktop/resources/tray-windows.ico differ diff --git a/apps/desktop/scripts/desktop-build-commit.d.mts b/apps/desktop/scripts/desktop-build-commit.d.mts new file mode 100644 index 0000000000..82118e24e1 --- /dev/null +++ b/apps/desktop/scripts/desktop-build-commit.d.mts @@ -0,0 +1,32 @@ +/** Commit a build came from, and whether its checkout carried uncommitted changes. */ +export interface DesktopBuildCommit { + readonly commit: string + readonly dirty: boolean +} + +/** Environment variable that carries the packaged commit. */ +export const DESKTOP_BUILD_COMMIT_ENV: 'DSH_DESKTOP_BUILD_COMMIT' + +/** Environment variable that records whether the packaged checkout had uncommitted changes. */ +export const DESKTOP_BUILD_DIRTY_ENV: 'DSH_DESKTOP_BUILD_DIRTY' + +/** + * Read the checkout's current commit and whether it carries uncommitted changes. + * @param repositoryRoot - Directory to inspect. + * @returns The commit being packaged and whether its tree was modified. + */ +export function readDesktopBuildCommit(repositoryRoot: string): DesktopBuildCommit + +/** + * Resolve the commit a parent packaging process recorded. + * @param env - Packaging environment. + * @returns The packaged commit, or undefined outside a packaging run. + */ +export function resolveDesktopBuildCommit(env: NodeJS.ProcessEnv): DesktopBuildCommit | undefined + +/** + * Describe the packaged commit as the environment variables child processes read. + * @param packaged - Commit and tree state to pass down. + * @returns Variables to merge into a child environment. + */ +export function desktopBuildCommitEnvironment(packaged: DesktopBuildCommit): Record diff --git a/apps/desktop/scripts/desktop-build-commit.mjs b/apps/desktop/scripts/desktop-build-commit.mjs new file mode 100644 index 0000000000..28d7cda513 --- /dev/null +++ b/apps/desktop/scripts/desktop-build-commit.mjs @@ -0,0 +1,57 @@ +/** + * Carry the commit a build came from into its artifacts. + * + * A build that reaches a colleague or a test feed is not reachable from a tag, + * so the only way back to its sources is what the build recorded about itself. + * The packaging entry reads the checkout once and passes the result to every + * child process, which run against a build tree rather than a checkout. + */ + +import { execFileSync } from 'node:child_process' + +/** Environment variable that carries the packaged commit. */ +export const DESKTOP_BUILD_COMMIT_ENV = 'DSH_DESKTOP_BUILD_COMMIT' + +/** Environment variable that records whether the packaged checkout had uncommitted changes. */ +export const DESKTOP_BUILD_DIRTY_ENV = 'DSH_DESKTOP_BUILD_DIRTY' + +/** + * Read the checkout's current commit and whether it carries uncommitted changes. + * @param {string} repositoryRoot - Directory to inspect. + * @returns {{ commit: string, dirty: boolean }} The commit being packaged and whether its tree was modified. + */ +export function readDesktopBuildCommit(repositoryRoot) { + const git = (args) => execFileSync('git', args, { cwd: repositoryRoot, encoding: 'utf8' }).trim() + return { + commit: git(['rev-parse', 'HEAD']), + dirty: git(['status', '--porcelain', '--untracked-files=normal']) !== '', + } +} + +/** + * Resolve the commit a parent packaging process recorded. + * @param {NodeJS.ProcessEnv} env - Packaging environment. + * @returns {{ commit: string, dirty: boolean } | undefined} The packaged commit, or undefined outside a packaging run. + */ +export function resolveDesktopBuildCommit(env) { + const commit = env[DESKTOP_BUILD_COMMIT_ENV]?.trim() + if (commit === undefined || commit === '') return undefined + if (!/^[0-9a-f]{40}$/u.test(commit)) throw new Error(`desktop build commit: ${DESKTOP_BUILD_COMMIT_ENV} must be a commit hash`) + const dirty = env[DESKTOP_BUILD_DIRTY_ENV]?.trim() + if (dirty !== undefined && dirty !== '' && dirty !== '0' && dirty !== '1') { + throw new Error(`desktop build commit: ${DESKTOP_BUILD_DIRTY_ENV} must be 0 or 1`) + } + return { commit, dirty: dirty === '1' } +} + +/** + * Describe the packaged commit as the environment variables child processes read. + * @param {{ commit: string, dirty: boolean }} packaged - Commit and tree state to pass down. + * @returns {Record} Variables to merge into a child environment. + */ +export function desktopBuildCommitEnvironment(packaged) { + return { + [DESKTOP_BUILD_COMMIT_ENV]: packaged.commit, + [DESKTOP_BUILD_DIRTY_ENV]: packaged.dirty ? '1' : '0', + } +} diff --git a/apps/desktop/scripts/desktop-build-paths.d.mts b/apps/desktop/scripts/desktop-build-paths.d.mts index b26c633d9c..9a8e408ed1 100644 --- a/apps/desktop/scripts/desktop-build-paths.d.mts +++ b/apps/desktop/scripts/desktop-build-paths.d.mts @@ -36,6 +36,17 @@ export function resolveDesktopBuildTarget( */ export function desktopTargetBuildPaths(target: DesktopAutoUpdateTarget): DesktopTargetBuildPaths +/** + * Return the platform and architecture of the payload one release target prepares. + * Windows is prepared as x64 only, so this differs from the build host on an arm64 Windows machine. + * @param target - Supported Desktop target name. + * @returns Platform and architecture of the prepared payload. + */ +export function desktopTargetPlatform(target: DesktopAutoUpdateTarget): { + readonly platform: 'darwin' | 'win32' + readonly arch: 'arm64' | 'x64' +} + /** * Resolve the paths owned by the target selected in a packaging environment. * @param env - Packaging environment. @@ -48,3 +59,16 @@ export function resolveDesktopTargetBuildPaths( hostPlatform?: NodeJS.Platform, hostArch?: string, ): DesktopTargetBuildPaths + +/** + * Resolve the primary-runtime directory an unpackaged development launch uses. + * @param env - Packaging environment. + * @param hostPlatform - Build-host platform used when no target override exists. + * @param hostArch - Build-host architecture used when no target override exists. + * @returns Primary-runtime directory prepared for the selected target. + */ +export function developmentRuntimeDirectory( + env?: NodeJS.ProcessEnv, + hostPlatform?: NodeJS.Platform, + hostArch?: string, +): string diff --git a/apps/desktop/scripts/desktop-build-paths.mjs b/apps/desktop/scripts/desktop-build-paths.mjs index 841da2c4ba..4f33e804c3 100644 --- a/apps/desktop/scripts/desktop-build-paths.mjs +++ b/apps/desktop/scripts/desktop-build-paths.mjs @@ -29,15 +29,19 @@ export function resolveDesktopBuildTarget( return /** @type {'mac-arm64' | 'mac-x64' | 'win-x64'} */ (target) } +function assertSupportedTarget(target) { + if (!SUPPORTED_TARGETS.has(target)) { + throw new Error(`desktop build paths: unsupported target ${String(target)}`) + } +} + /** * Return the mutable preparation and artifact directories owned by one release target. * @param {'mac-arm64' | 'mac-x64' | 'win-x64'} target - Supported Desktop target name. * @returns {{ root: string, artifacts: string, unsignedArtifacts: string, runtime: string, packageSet: string, dsh: string, dshPnpm: string, electron: string, packedDsh: string, packedVendor: string, packedLandlock: string, downloads: string }} Target paths plus the shared immutable download cache. */ export function desktopTargetBuildPaths(target) { - if (!SUPPORTED_TARGETS.has(target)) { - throw new Error(`desktop build paths: unsupported target ${String(target)}`) - } + assertSupportedTarget(target) const root = join(BUILD_ROOT, 'targets', target) const packed = join(root, 'packed') return { @@ -56,6 +60,20 @@ export function desktopTargetBuildPaths(target) { } } +/** + * Return the platform and architecture of the payload one release target prepares. + * Windows is prepared as x64 only, so this differs from the build host on an arm64 Windows machine. + * @param {'mac-arm64' | 'mac-x64' | 'win-x64'} target - Supported Desktop target name. + * @returns {{ platform: 'darwin' | 'win32', arch: 'arm64' | 'x64' }} Platform and architecture of the prepared payload. + */ +export function desktopTargetPlatform(target) { + assertSupportedTarget(target) + return { + platform: /** @type {'darwin' | 'win32'} */ (target === 'win-x64' ? 'win32' : 'darwin'), + arch: /** @type {'arm64' | 'x64'} */ (target === 'mac-arm64' ? 'arm64' : 'x64'), + } +} + /** * Resolve the paths owned by the target selected in a packaging environment. * @param {NodeJS.ProcessEnv} env - Packaging environment. @@ -70,3 +88,20 @@ export function resolveDesktopTargetBuildPaths( ) { return desktopTargetBuildPaths(resolveDesktopBuildTarget(env, hostPlatform, hostArch)) } + +/** + * Resolve the primary-runtime directory an unpackaged development launch uses. + * The build target fixes Windows to x64, so the shell cannot derive this directory from + * the architecture of the process that launched it. + * @param {NodeJS.ProcessEnv} env - Packaging environment. + * @param {NodeJS.Platform} hostPlatform - Build-host platform used when no target override exists. + * @param {string} hostArch - Build-host architecture used when no target override exists. + * @returns {string} Primary-runtime directory prepared for the selected target. + */ +export function developmentRuntimeDirectory( + env = process.env, + hostPlatform = process.platform, + hostArch = process.arch, +) { + return join(resolveDesktopTargetBuildPaths(env, hostPlatform, hostArch).runtime, 'primary-runtime') +} diff --git a/apps/desktop/scripts/desktop-build-version-discovery.ts b/apps/desktop/scripts/desktop-build-version-discovery.ts new file mode 100644 index 0000000000..6bb7a1b045 --- /dev/null +++ b/apps/desktop/scripts/desktop-build-version-discovery.ts @@ -0,0 +1,156 @@ +/** + * Suggest the next build version for a product version, so the sequence number + * is derived rather than remembered. + * + * `apps/desktop/README.md` gives the form as `..` + * for a prerelease base and `-test..` for a + * stable one. What makes an index correct is which ones are taken, and that is + * recorded where the builds went: the update bucket for an uploaded build, the + * local output directory for one that was only handed over. Asking the bucket + * first is what keeps a test feed's numbering unique across machines, and a + * fresh CI checkout has no local artifacts to fall back on. + * + * Reusing an index would overwrite a published installer and invalidate the + * checksum in the feed that already points at it, so a bucket that answers + * must answer completely: the listing follows its pages, and a query that + * cannot finish within the deadline falls back to the local scan rather than + * numbering against a partial answer. + */ + +import { readdir } from 'node:fs/promises' +import { parse } from 'semver' +import { desktopBuildVersionPrefix, validateDesktopBuildVersion } from './desktop-build-version.mjs' +import { DESKTOP_AUTO_UPDATE_ENV, resolveDesktopUploadConfig } from './desktop-auto-update-environment.mjs' +import { createDesktopCos, DESKTOP_COS_REGION } from './desktop-cos.ts' +import type { DesktopPackageTargetName } from './package-target.ts' + +/** How long the whole bucket listing may take before the suggestion falls back to local artifacts. */ +const LISTING_DEADLINE_MS = 8_000 + +/** Objects one listing page may return. */ +const LISTING_PAGE_SIZE = 1000 + +/** Artifact name electron-builder writes for one build, on either platform; unsigned Windows builds add a suffix. */ +const ARTIFACT = /(?:^|\/)deepseek-harness-(?.+)-(?:mac|win)-(?:arm64|x64)(?:-unsigned)?\.(?:exe|dmg|zip)$/u + +/** Inputs that decide which versions are already taken. */ +export interface DesktopBuildVersionSuggestionOptions { + readonly productVersion: string + readonly target: DesktopPackageTargetName + readonly environment: NodeJS.ProcessEnv + /** Date segment to number within; defaults to today where the build runs. */ + readonly date?: string + /** Directory electron-builder writes installers into for this build. */ + readonly artifactsRoot: string +} + +/** + * Format a date as the convention's segment. + * @param date - Date to format. + * @returns The date as `YYYYMMDD` in the build host's own time zone. + */ +export function desktopBuildDateSegment(date: Date = new Date()): string { + const month = String(date.getMonth() + 1).padStart(2, '0') + return `${String(date.getFullYear())}${month}${String(date.getDate()).padStart(2, '0')}` +} + +/** + * Read the sequence numbers already used for one product version and date. + * @param versions - Versions found in a bucket or directory. + * @param prefix - Everything a numbered build carries before its index. + * @returns Every index present, unordered. + */ +function sequenceNumbers(versions: Iterable, prefix: string): number[] { + const numbers: number[] = [] + for (const version of versions) { + if (!version.startsWith(prefix)) continue + const index = version.slice(prefix.length) + if (/^\d+$/u.test(index)) numbers.push(Number(index)) + } + return numbers +} + +/** + * Read the versions one output directory already holds. + * @param artifactsRoot - Directory electron-builder wrote installers into. + * @returns Versions parsed from artifact names. + */ +async function localVersions(artifactsRoot: string): Promise { + const entries = await readdir(artifactsRoot).catch(() => []) + return entries.map(entry => ARTIFACT.exec(entry)?.groups?.version) + .filter((version): version is string => version !== undefined && parse(version) !== null) +} + +/** + * Read every version one update bucket publishes for a target. + * @param options - Product version, target, and environment naming the bucket. + * @returns Versions parsed from object names, or undefined when the bucket cannot be listed completely in time. + */ +async function remoteVersions(options: DesktopBuildVersionSuggestionOptions): Promise { + const platform = options.target === 'win-x64' ? 'win32' as const : 'darwin' as const + const arch = options.target === 'mac-arm64' ? 'arm64' : 'x64' + // An unconfigured destination has nothing to be unique against; an invalid one must not be mistaken for it. + if (options.environment[DESKTOP_AUTO_UPDATE_ENV] === undefined + && options.environment.DOWNLOAD_TEST_ORIGIN === undefined) return undefined + const update = resolveDesktopUploadConfig(options.environment, platform, arch) + const secretId = options.environment[update.secretIdEnvName]?.trim() + const secretKey = options.environment[update.secretKeyEnvName]?.trim() + if (secretId === undefined || secretId === '' || secretKey === undefined || secretKey === '') return undefined + // The SDK has no per-request abort, so its own timeout is what releases an unanswered socket. + const cos = createDesktopCos({ secretId, secretKey }, LISTING_DEADLINE_MS) + const deadline = Date.now() + LISTING_DEADLINE_MS + const versions: string[] = [] + let marker: string | undefined + try { + do { + const page = await Promise.race([ + new Promise<{ keys: string[]; next: string | undefined }>((resolveListing, rejectListing) => { + cos.getBucket({ + Bucket: update.bucket, Region: DESKTOP_COS_REGION, MaxKeys: LISTING_PAGE_SIZE, + Prefix: `${update.binaryKeyPrefix}/deepseek-harness-`, ...marker === undefined ? {} : { Marker: marker }, + }, (error, data) => { + if (error !== null && error !== undefined) rejectListing(error instanceof Error ? error : new Error(String(error))) + else { + resolveListing({ + keys: (data?.Contents ?? []).map(object => object.Key), + next: data?.IsTruncated === 'true' ? data.NextMarker ?? data.Contents?.at(-1)?.Key : undefined, + }) + } + }) + }), + new Promise((_resolveDeadline, rejectDeadline) => { + setTimeout(() => rejectDeadline(new Error('listing deadline')), Math.max(0, deadline - Date.now())).unref() + }), + ]) + for (const key of page.keys) { + const version = ARTIFACT.exec(key)?.groups?.version + if (version !== undefined && parse(version) !== null) versions.push(version) + } + marker = page.next + } while (marker !== undefined) + } + catch (unfinished) { + // Numbering against a partial listing could reuse a published index, so an unfinished query yields nothing. + process.stdout.write(`desktop package: the update bucket did not answer completely (${ + unfinished instanceof Error ? unfinished.message : String(unfinished)}); numbering from local artifacts\n`) + return undefined + } + return versions +} + +/** + * Suggest the next build version for today, numbering after what is already taken. + * @param options - Product version, target, and environment to search. + * @returns A validated build version whose index is free. + */ +export async function suggestDesktopBuildVersion(options: DesktopBuildVersionSuggestionOptions): Promise { + const prefix = `${desktopBuildVersionPrefix(options.productVersion)}${options.date ?? desktopBuildDateSegment()}.` + const published = await remoteVersions(options) + const taken = published ?? await localVersions(options.artifactsRoot) + const used = sequenceNumbers(taken, prefix) + const next = used.length === 0 ? 1 : Math.max(...used) + 1 + const suggestion = validateDesktopBuildVersion(`${prefix}${String(next)}`, options.productVersion) + process.stdout.write(`desktop package: numbering ${suggestion} after ${String(used.length)} ${ + published === undefined ? 'local artifact' : 'published build'}${used.length === 1 ? '' : 's'} for ${prefix}*\n`) + return suggestion +} diff --git a/apps/desktop/scripts/desktop-build-version.d.mts b/apps/desktop/scripts/desktop-build-version.d.mts new file mode 100644 index 0000000000..969b601043 --- /dev/null +++ b/apps/desktop/scripts/desktop-build-version.d.mts @@ -0,0 +1,25 @@ +/** Environment variable that carries the build version through one packaging and upload run. */ +export const DESKTOP_BUILD_VERSION_ENV: 'DSH_DESKTOP_BUILD_VERSION' + +/** + * Validate a build version against the product version it extends. + * @param buildVersion - Version this build publishes. + * @param productVersion - Version the manifests declare. + * @returns The version as semver normalizes it, which is what the artifacts will carry. + */ +export function validateDesktopBuildVersion(buildVersion: string, productVersion: string): string + +/** + * Resolve the version a build publishes. + * @param env - Packaging or upload environment. + * @param productVersion - Version the manifests declare. + * @returns The build version when one is present, otherwise the product version. + */ +export function resolveDesktopBuildVersion(env: NodeJS.ProcessEnv, productVersion: string): string + +/** + * Everything a build version carries before its date, including the trailing separator. + * @param productVersion - Version the manifests declare. + * @returns The prefix shared by every build version of that product version. + */ +export function desktopBuildVersionPrefix(productVersion: string): string diff --git a/apps/desktop/scripts/desktop-build-version.mjs b/apps/desktop/scripts/desktop-build-version.mjs new file mode 100644 index 0000000000..e2152ce0d3 --- /dev/null +++ b/apps/desktop/scripts/desktop-build-version.mjs @@ -0,0 +1,100 @@ +/** + * Resolve the version one build publishes, which is not always the version the + * repository declares. + * + * A production release publishes the version in the manifests, aligned with the + * `dsh` npm package. A test build publishes a version that appends a date and a + * sequence number, so one test feed can carry several builds of a single + * product version: `0.1.6-alpha.1.20260916.1` from a prerelease base and + * `0.1.6-test.20260916.1` from a stable one, as the release versions table in + * `apps/desktop/README.md` gives them. Passing that version here keeps it out + * of the manifests, so the tracked version stays the product's while the build + * version reaches electron-builder, the update feed, and the upload validation + * as one input. + * + * `electron-updater` compares feed versions with `semver.gt` against the + * installed `app.getVersion()`, so validation uses the same library. Sequence + * numbers order builds within a feed under either form. The forms differ only + * against the base itself: builds extending a prerelease outrank it, while + * `0.1.6-test.1` ranks below `0.1.6`, which is why the stable form belongs to a + * test feed that never serves the production release. + */ + +import { parse } from 'semver' + +/** Environment variable that carries the build version through one packaging and upload run. */ +export const DESKTOP_BUILD_VERSION_ENV = 'DSH_DESKTOP_BUILD_VERSION' + +/** Prerelease field that opens a test build's suffix on a stable product version. */ +const STABLE_TEST_FIELD = 'test' + +/** + * Parse a version the updater would accept. + * @param {string} version - Version to read. + * @param {string} label - Description used in failures. + * @returns {import('semver').SemVer} The parsed version. + */ +function parseVersion(version, label) { + const parsed = parse(version, { loose: false }) + if (parsed === null) throw new Error(`desktop build version: ${label} ${version} is not a version`) + if (parsed.build.length > 0) { + // Build metadata does not participate in precedence, so two builds would compare equal to the updater. + throw new Error(`desktop build version: ${label} ${version} cannot carry build metadata`) + } + return parsed +} + +/** + * The prerelease fields every build version for one product version starts with. + * @param {import('semver').SemVer} product - Parsed product version. + * @returns {readonly (string | number)[]} Fields a build version must repeat before its date. + */ +function requiredFields(product) { + return product.prerelease.length === 0 ? [STABLE_TEST_FIELD] : product.prerelease +} + +/** + * Validate a build version against the product version it extends. + * @param {string} buildVersion - Version this build publishes. + * @param {string} productVersion - Version the manifests declare. + * @returns {string} The version as semver normalizes it, which is what the artifacts will carry. + */ +export function validateDesktopBuildVersion(buildVersion, productVersion) { + const build = parseVersion(buildVersion, 'build version') + const product = parseVersion(productVersion, 'product version') + if (build.version === product.version) return build.version + if (build.compareMain(product) !== 0) { + throw new Error(`desktop build version: ${buildVersion} must extend product version ${productVersion}`) + } + const required = requiredFields(product) + const extendsProduct = build.prerelease.length > required.length + && required.every((field, index) => build.prerelease[index] === field) + if (!extendsProduct) { + throw new Error(`desktop build version: ${buildVersion} must extend ${productVersion} as ${ + desktopBuildVersionPrefix(productVersion)}.`) + } + return build.version +} + +/** + * Resolve the version a build publishes. + * @param {NodeJS.ProcessEnv} env - Packaging or upload environment. + * @param {string} productVersion - Version the manifests declare. + * @returns {string} The build version when one is present, otherwise the product version. + */ +export function resolveDesktopBuildVersion(env, productVersion) { + const buildVersion = env[DESKTOP_BUILD_VERSION_ENV]?.trim() + if (buildVersion === undefined || buildVersion === '') return productVersion + return validateDesktopBuildVersion(buildVersion, productVersion) +} + +/** + * Everything a build version carries before its date, including the trailing separator. + * @param {string} productVersion - Version the manifests declare. + * @returns {string} The prefix shared by every build version of that product version. + */ +export function desktopBuildVersionPrefix(productVersion) { + const product = parseVersion(productVersion, 'product version') + const [release] = product.version.split('-') + return `${release}-${requiredFields(product).join('.')}.` +} diff --git a/apps/desktop/scripts/desktop-bundle-imports.d.mts b/apps/desktop/scripts/desktop-bundle-imports.d.mts new file mode 100644 index 0000000000..3743e8ea39 --- /dev/null +++ b/apps/desktop/scripts/desktop-bundle-imports.d.mts @@ -0,0 +1,46 @@ +import type { Rolldown } from 'tsdown' + +/** Bare specifiers one Desktop bundle may leave for the runtime to resolve. */ +export interface BundleImportPolicy { + /** Package names present at runtime, matched by package name so subpaths pass. */ + readonly packages: ReadonlySet + /** + * Whether Node builtins resolve at runtime: true for the Electron main + * process; false for sandboxed preloads, whose `require` polyfill offers only + * the modules listed in `packages`. + */ + readonly nodeBuiltins: boolean +} + +/** + * Package name of a bare import specifier. + * @param specifier - Bare specifier as rolldown reports it, such as `@scope/name/subpath`. + * @returns The package name without its subpath. + */ +export function importPackageName(specifier: string): string + +/** + * Whether a module id rolldown reports is a bare specifier, i.e. an import the + * bundle leaves external. Bundled modules carry absolute ids; virtual modules + * start with `\0`. + * @param id - Module id from `importedIds` or `dynamicallyImportedIds`. + * @returns True for a bare specifier. + */ +export function isBareSpecifier(id: string): boolean + +/** + * Bare imports the packaged application cannot resolve. + * @param imports - External specifiers a module imports statically, dynamically, or through `require()`. + * @param policy - What this bundle may leave external. + * @returns Offending specifiers in import order, each once. + */ +export function unpackagedImports(imports: readonly string[], policy: BundleImportPolicy): string[] + +/** + * Rolldown plugin that fails a bundle whose external imports the packaged + * application cannot resolve. The failure names the importing module and every + * offending specifier, and no output is written. + * @param policy - What this bundle may leave external. + * @returns The plugin. + */ +export function packagedImportsPlugin(policy: BundleImportPolicy): Rolldown.Plugin diff --git a/apps/desktop/scripts/desktop-bundle-imports.mjs b/apps/desktop/scripts/desktop-bundle-imports.mjs new file mode 100644 index 0000000000..d1aa8f5b9d --- /dev/null +++ b/apps/desktop/scripts/desktop-bundle-imports.mjs @@ -0,0 +1,90 @@ +/** + * Keep every bare import a Desktop bundle leaves to the runtime resolvable + * inside the packaged application. + * + * electron-builder copies only the manifest's `dependencies` into + * `app.asar/node_modules`, so a bare import of anything else bundles without + * complaint and fails at launch with `ERR_MODULE_NOT_FOUND`. The main-process + * bundle inlines its workspace devDependencies; when one of their `lib/` + * outputs is missing at bundle time, rolldown reports `UNRESOLVED_IMPORT` as a + * warning and keeps the specifier as an external import, which is exactly the + * broken artifact. This check fails the bundle instead of the installed + * application. + * + * The check runs in `moduleParsed`, where rolldown reports every import record + * of a module: static imports, dynamic `import()`, and `require()` calls. A + * bundled import resolves to an absolute module id; an external one keeps its + * bare specifier. Chunk metadata (`imports`/`dynamicImports`) omits external + * `import()` and `require()` targets, so it cannot serve this check. + */ + +import { isBuiltin } from 'node:module' +import { isAbsolute, relative } from 'node:path' + +/** + * Bare specifiers one Desktop bundle may leave for the runtime to resolve. + * @typedef {object} BundleImportPolicy + * @property {ReadonlySet} packages - Package names present at runtime, matched by package name so subpaths pass. + * @property {boolean} nodeBuiltins - Whether Node builtins resolve at runtime (true for the Electron main process; false for sandboxed preloads, whose `require` polyfill offers only the modules listed in `packages`). + */ + +/** + * Package name of a bare import specifier. + * @param {string} specifier - Bare specifier as rolldown reports it, such as `@scope/name/subpath`. + * @returns {string} The package name without its subpath. + */ +export function importPackageName(specifier) { + const segments = specifier.split('/') + return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0] +} + +/** + * Whether a module id rolldown reports is a bare specifier, i.e. an import the + * bundle leaves external. Bundled modules carry absolute ids; virtual modules + * start with `\0`. + * @param {string} id - Module id from `importedIds` or `dynamicallyImportedIds`. + * @returns {boolean} True for a bare specifier. + */ +export function isBareSpecifier(id) { + return !id.startsWith('.') && !id.startsWith('\0') && !isAbsolute(id) +} + +/** + * Bare imports the packaged application cannot resolve. + * @param {readonly string[]} imports - External specifiers a module imports statically, dynamically, or through `require()`. + * @param {BundleImportPolicy} policy - What this bundle may leave external. + * @returns {string[]} Offending specifiers in import order, each once. + */ +export function unpackagedImports(imports, policy) { + const offending = new Set() + for (const specifier of imports) { + if (policy.nodeBuiltins && isBuiltin(specifier)) continue + if (policy.packages.has(importPackageName(specifier))) continue + offending.add(specifier) + } + return [...offending] +} + +/** + * Rolldown plugin that fails a bundle whose external imports the packaged + * application cannot resolve. The failure names the importing module and every + * offending specifier, and no output is written. + * @param {BundleImportPolicy} policy - What this bundle may leave external. + * @returns {import('tsdown').Rolldown.Plugin} The plugin. + */ +export function packagedImportsPlugin(policy) { + const allowed = [...policy.packages].join(', ') + (policy.nodeBuiltins ? ', and Node builtins' : '') + return { + name: 'desktop-packaged-imports', + moduleParsed(info) { + const external = [...info.importedIds, ...info.dynamicallyImportedIds].filter(isBareSpecifier) + const offending = unpackagedImports(external, policy) + if (offending.length === 0) return + this.error( + `desktop bundle: ${relative(process.cwd(), info.id)} imports ${offending.join(', ')}, which the packaged application does not ship. ` + + `This bundle may leave only ${allowed} as bare imports; anything else must be bundled, ` + + 'which requires its lib/ output to exist before this bundle runs (pnpm run build:lib:host).', + ) + }, + } +} diff --git a/apps/desktop/scripts/desktop-cos.ts b/apps/desktop/scripts/desktop-cos.ts index 68306ffc64..6074d84a05 100644 --- a/apps/desktop/scripts/desktop-cos.ts +++ b/apps/desktop/scripts/desktop-cos.ts @@ -29,9 +29,10 @@ interface CosRequestOptions { * caller names none; that header is removed here so uploading leaves cache policy to deployment * infrastructure. * @param credentials SecretId and SecretKey for the selected deployment. + * @param timeoutMs Request-level inactivity deadline; a caller that must not wait out a transfer passes its own. * @returns A COS client that sends HTTPS requests to the region named by each call. */ -export function createDesktopCos(credentials: DesktopCosCredentials): COS { +export function createDesktopCos(credentials: DesktopCosCredentials, timeoutMs: number = TRANSFER_TIMEOUT_MS): COS { const cos = new COS({ SecretId: credentials.secretId, SecretKey: credentials.secretKey, @@ -41,7 +42,7 @@ export function createDesktopCos(credentials: DesktopCosCredentials): COS { AutoSwitchHost: false, CorrectClockSkew: false, ChunkRetryTimes: 0, - Timeout: TRANSFER_TIMEOUT_MS, + Timeout: timeoutMs, UploadCheckContentMd5: false, }) cos.on('before-send', (options: CosRequestOptions) => { diff --git a/apps/desktop/scripts/desktop-package-environment.mjs b/apps/desktop/scripts/desktop-package-environment.mjs index 71dd9a6655..3addf3ff65 100644 --- a/apps/desktop/scripts/desktop-package-environment.mjs +++ b/apps/desktop/scripts/desktop-package-environment.mjs @@ -4,7 +4,7 @@ import { accessSync, constants, readFileSync, statSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { parseEnv } from 'node:util' -import { resolveDesktopAppId, resolveMacOSNotarizationEnvironment, resolveMacOSSigningEnvironment } from './desktop-release-environment.mjs' +import { resolveDesktopAppId, resolveMacOSNotarizationEnvironment, resolveMacOSSigningEnvironment, resolveNpmRegistry } from './desktop-release-environment.mjs' import { resolveDesktopAutoUpdateConfig } from './desktop-auto-update-environment.mjs' import { createWindowsTokenSigner } from './windows-sign.mjs' import { resolveDesktopPolicyEnvironment } from './desktop-policy-environment.mjs' @@ -13,7 +13,7 @@ import { resolveWindowsSignatureCacheDirectory } from './windows-signature-cache import { resolveWindowsPackageSettings } from './windows-package-settings.mjs' const APP_ROOT = fileURLToPath(new URL('..', import.meta.url)) -const SHARED_SETTING = /^(?:DSH_DESKTOP_(?:APP_ID|AUTO_UPDATE_ENV|MANDATORY_UPDATE_(?:CONFIG|(?:TEST|PROD)_ORIGIN))|DOWNLOAD_TEST_RELEASE_ID|DOWNLOAD_(?:TEST|PROD)_(?:ORIGIN|COS_BUCKET|COS_SECRET_ID|COS_SECRET_KEY))$/u +const SHARED_SETTING = /^(?:DSH_DESKTOP_(?:APP_ID|AUTO_UPDATE_ENV|NPM_REGISTRY|MANDATORY_UPDATE_(?:CONFIG|(?:TEST|PROD)_ORIGIN))|DOWNLOAD_TEST_RELEASE_ID|DOWNLOAD_(?:TEST|PROD)_(?:ORIGIN|COS_BUCKET|COS_SECRET_ID|COS_SECRET_KEY))$/u const WINDOWS_SETTING = /^DSH_DESKTOP_WINDOWS_(?:CER_FILE|SIGNTOOL|KEY_CONTAINER|TOKEN_PIN|SIGNATURE_CACHE_DIR|SIGNATURE_CACHE_CONCURRENCY)$/u const MACOS_SETTING = /^(?:DSH_DESKTOP_MACOS_(?:SIGNING_IDENTITY|TEAM_ID|PACK_CONCURRENCY|DOWNLOAD_PROXY|NOTARIZATION_PROXY)|APPLE_(?:API_KEY|API_KEY_ID|API_ISSUER|ID|APP_SPECIFIC_PASSWORD|TEAM_ID|KEYCHAIN|KEYCHAIN_PROFILE)|CSC_(?:LINK|KEY_PASSWORD))$/u const AMBIENT_RELEASE_SETTING = /^(?:DSH_DESKTOP_(?:APP_ID|AUTO_UPDATE_ENV|MANDATORY_UPDATE_.*|WINDOWS_.*|MACOS_.*)|APPLE_.*|(?:WIN_)?CSC_.*|DOWNLOAD_(?:TEST|PROD)_.*)$/iu @@ -78,6 +78,7 @@ function requireReadableFile(environment, name) { */ export function validateDesktopPackageEnvironment(environment, target, options = {}) { resolveDesktopAppId(environment) + resolveNpmRegistry(environment) resolveDesktopPolicyEnvironment(environment) if (target.platform === 'darwin') resolveMacOSPackageSettings(environment) else resolveWindowsPackageSettings(environment) diff --git a/apps/desktop/scripts/desktop-release-environment.d.mts b/apps/desktop/scripts/desktop-release-environment.d.mts index ad48933f65..26190ab2b8 100644 --- a/apps/desktop/scripts/desktop-release-environment.d.mts +++ b/apps/desktop/scripts/desktop-release-environment.d.mts @@ -7,6 +7,9 @@ export const MACOS_SIGNING_IDENTITY_ENV: 'DSH_DESKTOP_MACOS_SIGNING_IDENTITY' /** Environment variable that supplies the expected Apple Developer Team ID. */ export const MACOS_TEAM_ID_ENV: 'DSH_DESKTOP_MACOS_TEAM_ID' +/** Environment variable that selects the npm registry used for the bundled runtime install. */ +export const NPM_REGISTRY_ENV: 'DSH_DESKTOP_NPM_REGISTRY' + /** Public identity expected on a macOS release. */ export interface MacOSSigningEnvironment { readonly signingIdentity: string @@ -46,6 +49,13 @@ export type MacOSNotarizationEnvironment = */ export function resolveDesktopAppId(env: NodeJS.ProcessEnv): string +/** + * Resolve the npm registry used to materialize the bundled runtime and its external dependencies. + * @param env - Packaging environment. + * @returns Registry origin; the public registry unless a local mirror is configured. + */ +export function resolveNpmRegistry(env: NodeJS.ProcessEnv): string + /** * Resolve and validate the public identity expected on a macOS release. * @param env - Packaging environment. diff --git a/apps/desktop/scripts/desktop-release-environment.mjs b/apps/desktop/scripts/desktop-release-environment.mjs index 46746428d9..635a57a3c9 100644 --- a/apps/desktop/scripts/desktop-release-environment.mjs +++ b/apps/desktop/scripts/desktop-release-environment.mjs @@ -9,6 +9,11 @@ export const MACOS_SIGNING_IDENTITY_ENV = 'DSH_DESKTOP_MACOS_SIGNING_IDENTITY' /** Environment variable that supplies the expected Apple Developer Team ID. */ export const MACOS_TEAM_ID_ENV = 'DSH_DESKTOP_MACOS_TEAM_ID' +/** Environment variable that selects the npm registry used for the bundled runtime install. */ +export const NPM_REGISTRY_ENV = 'DSH_DESKTOP_NPM_REGISTRY' + +const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/' + const APPLE_API_KEY_ENV = 'APPLE_API_KEY' const APPLE_API_KEY_ID_ENV = 'APPLE_API_KEY_ID' const APPLE_API_ISSUER_ENV = 'APPLE_API_ISSUER' @@ -32,6 +37,24 @@ function requireEnvironmentValue(env, name) { return value } +/** + * Resolve the npm registry used to materialize the bundled runtime and its external dependencies. + * @param {NodeJS.ProcessEnv} env - Packaging environment. + * @returns {string} Registry origin; the public registry unless a local mirror is configured. + */ +export function resolveNpmRegistry(env) { + const configured = env[NPM_REGISTRY_ENV]?.trim() ?? '' + if (configured === '') return DEFAULT_NPM_REGISTRY + let url + try { url = new URL(configured) } + catch { throw new Error(`desktop release environment: ${NPM_REGISTRY_ENV} must be an HTTPS origin`) } + if (url.protocol !== 'https:' || url.username !== '' || url.password !== '' || url.search !== '' || url.hash !== '' + || (url.pathname !== '/' && url.pathname !== '')) { + throw new Error(`desktop release environment: ${NPM_REGISTRY_ENV} must be an HTTPS origin without credentials, path, query, or fragment`) + } + return url.origin +} + /** * Resolve and validate the application identifier shared by every platform target. * @param {NodeJS.ProcessEnv} env - Packaging environment. diff --git a/apps/desktop/scripts/desktop-release-tag.ts b/apps/desktop/scripts/desktop-release-tag.ts new file mode 100644 index 0000000000..6b31938f55 --- /dev/null +++ b/apps/desktop/scripts/desktop-release-tag.ts @@ -0,0 +1,80 @@ +/** + * Record a production release in git after it reaches the download domain. + * + * A build that only goes to a colleague or a test feed carries its commit + * inside the artifact, which is enough to trace it. A build that external users + * install should also be reachable from the repository, so a production upload + * tags the commit it was packaged from. The tag is written after the upload + * succeeds: a tag for a release that never shipped would be worse than none. + * + * Tagging never fails an upload that already completed. The artifacts are + * public by then, and nothing about a missing tag can be repaired by reporting + * the upload as failed; the operator is told what to run instead. + */ + +import { execFileSync } from 'node:child_process' + +/** What tagging one uploaded release did, or why it did nothing. */ +export interface DesktopReleaseTagResult { + readonly tag: string + readonly status: 'created' | 'present' | 'failed' + /** Commands that finish the tagging a failure interrupted, in order. */ + readonly recovery?: readonly string[] + readonly detail?: string +} + +/** + * Name the tag one published version carries. + * @param version - Version the release published. + * @returns The tag name, distinct from the `dsh-v*` tags the npm family publishes from. + */ +export function desktopReleaseTag(version: string): string { + return `desktop-v${version}` +} + +/** + * Tag and push the commit a production release was packaged from. + * @param options - Published version, packaged commit, repository, and remote. + * @returns What tagging did; a failure is reported rather than thrown. + */ +export function tagDesktopRelease(options: { + readonly version: string + readonly commit: string + readonly repositoryRoot: string + readonly remote?: string + readonly run?: (command: string, args: readonly string[]) => string +}): DesktopReleaseTagResult { + const tag = desktopReleaseTag(options.version) + const remote = options.remote ?? 'origin' + const run = options.run + ?? ((command: string, args: readonly string[]) => + execFileSync(command, [...args], { cwd: options.repositoryRoot, encoding: 'utf8' }).trim()) + const create = `git tag ${tag} ${options.commit}` + const push = `git push ${remote} ${tag}` + // A local tag that already exists makes `git tag` fail, so recovery must name only the steps still outstanding. + let tagged = false + try { + const existing = run('git', ['tag', '--list', tag]) + if (existing !== '') { + const named = run('git', ['rev-list', '-n', '1', tag]) + if (named !== options.commit) { + return { tag, status: 'failed', detail: `${tag} already names ${named}, not the uploaded commit ${options.commit}` } + } + tagged = true + run('git', ['push', remote, tag]) + return { tag, status: 'present' } + } + run('git', ['tag', tag, options.commit]) + tagged = true + run('git', ['push', remote, tag]) + return { tag, status: 'created' } + } + catch (error) { + return { + tag, + status: 'failed', + recovery: tagged ? [push] : [create, push], + detail: error instanceof Error ? error.message : String(error), + } + } +} diff --git a/apps/desktop/scripts/desktop-toolchain-preflight.ts b/apps/desktop/scripts/desktop-toolchain-preflight.ts new file mode 100644 index 0000000000..ff855cfd09 --- /dev/null +++ b/apps/desktop/scripts/desktop-toolchain-preflight.ts @@ -0,0 +1,102 @@ +/** + * Probe the external tools a packaging run will shell out to, before it starts. + * + * The packaging sequence reaches its first archive step minutes in, and its + * first installer step later still, so a tool that is absent or behaves + * differently than the scripts assume wastes a whole run. Each probe here + * exercises the specific behaviour the run depends on rather than merely + * checking that the executable resolves. + */ + +import { execFile } from 'node:child_process' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { basename, dirname, join } from 'node:path' +import { promisify } from 'node:util' + +const run = promisify(execFile) + +/** One tool the packaging run requires, and what went wrong with it. */ +export interface DesktopToolchainProbeFailure { + readonly tool: string + readonly detail: string +} + +async function probeTar(): Promise { + // GNU tar reads the colon in an absolute Windows path as a remote-host separator, which the + // release scripts avoid by running beside the archive; this probe confirms the archive reader works there. + const directory = await mkdtemp(join(tmpdir(), 'dsh-tar-probe-')) + try { + const member = join(directory, 'probe.txt') + await writeFile(member, 'probe\n') + const archive = join(directory, 'probe.tar.gz') + await run('tar', ['-czf', basename(archive), basename(member)], { cwd: directory, windowsHide: true, timeout: 20_000 }) + const listed = await run('tar', ['-tzf', basename(archive)], { cwd: dirname(archive), windowsHide: true, timeout: 20_000 }) + if (!listed.stdout.includes('probe.txt')) return `tar listed ${JSON.stringify(listed.stdout)} instead of the archived member` + return undefined + } + catch (error) { + return error instanceof Error ? error.message : String(error) + } + finally { + await rm(directory, { recursive: true, force: true }) + } +} + +async function probeWindowsInstallerToolchain(environment: NodeJS.ProcessEnv): Promise { + const failures: DesktopToolchainProbeFailure[] = [] + const programFiles = environment['ProgramFiles(x86)'] + const vswhere = programFiles === undefined + ? undefined + : join(programFiles, 'Microsoft Visual Studio/Installer/vswhere.exe') + if (vswhere === undefined) { + failures.push({ tool: 'vswhere', detail: 'ProgramFiles(x86) is not set, so Visual Studio cannot be located' }) + } + else { + try { + const located = await run(vswhere, ['-latest', '-products', '*', + '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', '-property', 'installationPath'], + { windowsHide: true, timeout: 30_000 }) + if (located.stdout.trim() === '') { + failures.push({ tool: 'vswhere', detail: 'Visual Studio C++ build tools are not installed, so the installer helper cannot compile' }) + } + } + catch (error) { + failures.push({ tool: 'vswhere', detail: error instanceof Error ? error.message : String(error) }) + } + } + return failures +} + +/** + * Probe every external tool one packaging run needs. + * @param platform - Target platform; a Windows target already requires a Windows build host. + * @param environment - Packaging environment used to locate Windows tooling. + * @returns Every probe that failed, empty when the host can run the packaging sequence. + */ +export async function probeDesktopToolchain( + platform: 'darwin' | 'win32', + environment: NodeJS.ProcessEnv = process.env, +): Promise { + const failures: DesktopToolchainProbeFailure[] = [] + const tar = await probeTar() + if (tar !== undefined) failures.push({ tool: 'tar', detail: tar }) + if (platform === 'win32') failures.push(...await probeWindowsInstallerToolchain(environment)) + return failures +} + +/** + * Probe the toolchain and fail with every problem the host has. + * @param platform - Target platform; a Windows target already requires a Windows build host. + * @param environment - Packaging environment used to locate Windows tooling. + * @returns Resolves when every probe passes. + */ +export async function requireDesktopToolchain( + platform: 'darwin' | 'win32', + environment: NodeJS.ProcessEnv = process.env, +): Promise { + const failures = await probeDesktopToolchain(platform, environment) + if (failures.length === 0) return + throw new Error(`desktop package: the build host cannot run this packaging sequence:\n${ + failures.map(failure => ` ${failure.tool}: ${failure.detail}`).join('\n')}`) +} diff --git a/apps/desktop/scripts/desktop-upload-plan.ts b/apps/desktop/scripts/desktop-upload-plan.ts index 32f891dcc8..e14228cf69 100644 --- a/apps/desktop/scripts/desktop-upload-plan.ts +++ b/apps/desktop/scripts/desktop-upload-plan.ts @@ -13,6 +13,7 @@ import { resolveDesktopUploadConfig, } from './desktop-auto-update-environment.mjs' import { desktopTargetBuildPaths } from './desktop-build-paths.mjs' +import { validateDesktopBuildVersion } from './desktop-build-version.mjs' const APP_ROOT = resolve(import.meta.dirname, '..') const REPOSITORY_ROOT = resolve(APP_ROOT, '..', '..') @@ -47,6 +48,10 @@ export interface DesktopUploadPlan { readonly secretIdEnvName: string readonly secretKeyEnvName: string readonly artifacts: readonly DesktopUploadArtifact[] + /** Commit the artifacts were packaged from, absent for a package built before builds recorded it. */ + readonly commit?: string + /** Whether that checkout carried uncommitted changes. */ + readonly dirty?: boolean } /** Filesystem and environment inputs used to validate one upload. */ @@ -191,15 +196,24 @@ export async function createDesktopUploadPlan( join(artifactsRoot, desktopBuildRecordFilename(targetName)), `${targetName} package completion record`, ) + // Packaging wrote the version it published; reading it back keeps release settings out of shell variables. + const recordedVersion = stringField(buildRecord.version, `${targetName} package completion record.version`) + let buildVersion: string + try { + buildVersion = validateDesktopBuildVersion(recordedVersion, dshVersion) + } + catch (error) { + throw new Error(`desktop upload: ${targetName} package completion record holds ${recordedVersion}, which is not a build of dsh ${dshVersion}: ${ + error instanceof Error ? error.message : String(error)}`) + } if (buildRecord.schemaVersion !== 1 || buildRecord.target !== targetName - || buildRecord.version !== dshVersion || buildRecord.environment !== update.environment || buildRecord.publicUrl !== update.publicUrl) { - throw new Error(`desktop upload: ${targetName} package completion record does not match dsh ${dshVersion} and ${update.environment} update destination`) + throw new Error(`desktop upload: ${targetName} package completion record for ${buildVersion} does not match the ${update.environment} update destination`) } - const metadataFilename = desktopUpdateMetadataFilename(dshVersion, target.platform) + const metadataFilename = desktopUpdateMetadataFilename(buildVersion, target.platform) const metadataPath = join(artifactsRoot, metadataFilename) let metadataValue: unknown try { @@ -210,14 +224,14 @@ export async function createDesktopUploadPlan( } const metadata = object(metadataValue, metadataFilename) const metadataVersion = stringField(metadata.version, `${metadataFilename}.version`) - if (metadataVersion !== dshVersion) { - throw new Error(`desktop upload: ${metadataFilename} version ${metadataVersion} does not match current dsh version ${dshVersion}`) + if (metadataVersion !== buildVersion) { + throw new Error(`desktop upload: ${metadataFilename} version ${metadataVersion} does not match published version ${buildVersion}`) } if (!Array.isArray(metadata.files) || metadata.files.length !== 1) { throw new Error(`desktop upload: ${metadataFilename}.files must contain exactly one target update file`) } - const base = `deepseek-harness-${dshVersion}-${target.os}-${target.arch}` + const base = `deepseek-harness-${buildVersion}-${target.os}-${target.arch}` const updaterExtension = target.platform === 'darwin' ? 'zip' : 'exe' const updaterInfo = updateFileInfo(metadata.files[0], `${metadataFilename}.files[0]`, `${base}.${updaterExtension}`) const updaterPath = await verifyChecksummedArtifact(artifactsRoot, updaterInfo) @@ -254,18 +268,20 @@ export async function createDesktopUploadPlan( contents: dump(published), } artifacts.push(channelArtifact) - if (prerelease(dshVersion) === null) { + if (prerelease(buildVersion) === null) { const stableFilename = metadataFilename.replace('nightly', 'latest') artifacts.push({ ...channelArtifact, filename: stableFilename, key: `${update.keyPrefix}/${stableFilename}` }) } return { environment: update.environment, target: targetName, - version: dshVersion, + version: buildVersion, publicUrl: update.publicUrl, bucket: update.bucket, secretIdEnvName: update.secretIdEnvName, secretKeyEnvName: update.secretKeyEnvName, artifacts, + ...typeof buildRecord.commit === 'string' ? { commit: buildRecord.commit } : {}, + ...typeof buildRecord.dirty === 'boolean' ? { dirty: buildRecord.dirty } : {}, } } diff --git a/apps/desktop/scripts/dev.ts b/apps/desktop/scripts/dev.ts index 05e1bdbf0e..1ae2f3e952 100644 --- a/apps/desktop/scripts/dev.ts +++ b/apps/desktop/scripts/dev.ts @@ -7,7 +7,9 @@ import { join, resolve } from 'node:path' import { parseArgs } from 'node:util' import { DESKTOP_HOST_PROTOCOL_VERSION } from '../src/host-protocol.ts' import type { DesktopRelease } from '../src/release.ts' +import { developmentRuntimeDirectory, resolveDesktopBuildTarget } from './desktop-build-paths.mjs' import { prepareDevelopmentProject } from './development-project.ts' +import { prepareDevelopmentApp } from './development-app.ts' import { preparePrimaryRuntime } from './prepare-primary-runtime.ts' const APP_ROOT = resolve(import.meta.dirname, '..') @@ -62,16 +64,24 @@ async function launchElectron(): Promise { const rendererPort = debugPort('DSH_DESKTOP_RENDERER_DEBUG_PORT', 9222) const hostPort = debugPort('DSH_DESKTOP_HOST_INSPECT_PORT', 9230) const home = resolve(process.env.DSH_HOME ?? join(DEVELOPMENT_ROOT, 'home')) - const userData = join(DEVELOPMENT_ROOT, 'electron-user-data') + const userData = resolve(process.env.DSH_DESKTOP_USER_DATA_DIR ?? join(DEVELOPMENT_ROOT, 'electron-user-data')) const environment: NodeJS.ProcessEnv = { ...process.env, DSH_HOME: home, + DSH_DESKTOP_PRIMARY_RUNTIME_DIR: process.env.DSH_DESKTOP_PRIMARY_RUNTIME_DIR ?? developmentRuntimeDirectory(), DSH_DESKTOP_HOST_INSPECT_PORT: String(hostPort), DSH_DESKTOP_OPEN_DEVTOOLS: process.env.DSH_DESKTOP_OPEN_DEVTOOLS ?? '1', ELECTRON_ENABLE_LOGGING: process.env.ELECTRON_ENABLE_LOGGING ?? '1', } console.log(`desktop development: DSH_HOME=${home}`) + console.log(`desktop development: userData=${userData}`) console.log(`desktop development: inspectors main=${String(mainPort)}, renderer=${String(rendererPort)}, host=${String(hostPort)}`) + if (process.platform === 'darwin') { + const executable = prepareDevelopmentApp({ electron, appRoot: APP_ROOT, directory: DEVELOPMENT_ROOT, home, userData, + mainPort, rendererPort, hostPort, openDevtools: environment.DSH_DESKTOP_OPEN_DEVTOOLS! }) + await run(executable, [], APP_ROOT, environment) + return + } await run(electron, [ `--inspect=127.0.0.1:${String(mainPort)}`, `--remote-debugging-port=${String(rendererPort)}`, @@ -108,6 +118,7 @@ async function main(): Promise { hostDir: join(REPOSITORY_ROOT, 'apps', 'desktop-host'), dependencyDir: join(REPOSITORY_ROOT, 'node_modules', '.pnpm', 'node_modules'), release, + target: resolveDesktopBuildTarget(), }) await preparePrimaryRuntime() await launchElectron() diff --git a/apps/desktop/scripts/development-app.ts b/apps/desktop/scripts/development-app.ts new file mode 100644 index 0000000000..ff09b8b783 --- /dev/null +++ b/apps/desktop/scripts/development-app.ts @@ -0,0 +1,84 @@ +/** macOS development bundle that loads the current workspace through Electron. */ +import { execFileSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' + +/** Workspace and runtime settings captured for Launch Services cold starts. */ +export interface DevelopmentAppOptions { + readonly electron: string + readonly appRoot: string + readonly directory: string + readonly home: string + readonly userData: string + readonly mainPort: number + readonly rendererPort: number + readonly hostPort: number + readonly openDevtools: string +} + +function quote(value: string): string { return `'${value.replaceAll("'", "'\\''")}'` } + +/** + * Prepare and register a disposable .app without changing the installed Electron package. + * @param options - Current workspace, Electron binary, and private development locations. + * @returns executable used by the supported desktop development launcher. + */ +export function prepareDevelopmentApp(options: DevelopmentAppOptions): string { + const source = dirname(dirname(dirname(options.electron))) + const bundle = join(options.directory, 'Harness Dev.app') + const executable = join(bundle, 'Contents', 'MacOS', 'HarnessDev') + const stamp = join(bundle, 'Contents', 'Resources', 'dsh-development.json') + const launcher = developmentLauncher(options, bundle) + const identity = JSON.stringify({ ...options, launcher, plist: readFileSync(join(source, 'Contents', 'Info.plist'), 'utf8') }) + if (!existsSync(stamp) || readFileSync(stamp, 'utf8') !== identity) { + rmSync(bundle, { recursive: true, force: true }) + execFileSync('/usr/bin/ditto', [source, bundle]) + const plist = join(bundle, 'Contents', 'Info.plist') + const values = { + CFBundleIdentifier: `com.deepseek.harness.dev.${createHash('sha256').update(options.appRoot).digest('hex').slice(0, 12)}`, + CFBundleName: 'Harness Dev', + CFBundleDisplayName: 'Harness Dev', + CFBundleExecutable: 'HarnessDev', + CFBundleURLTypes: [{ CFBundleURLName: 'DeepSeek Harness', CFBundleURLSchemes: ['dsh'], CFBundleTypeRole: 'Viewer' }], + } + for (const [key, value] of Object.entries(values)) { + execFileSync('/usr/bin/plutil', ['-replace', key, '-json', JSON.stringify(value), plist]) + } + writeFileSync(executable, launcher, { mode: 0o755 }) + writeFileSync(stamp, identity, { mode: 0o600 }) + execFileSync('/usr/bin/codesign', ['--force', '--deep', '--sign', '-', bundle], { stdio: 'pipe' }) + } + execFileSync('/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister', ['-f', bundle]) + return executable +} + +/** + * Create the development executable used by both the CLI and Launch Services. + * @param options - Persisted development locations and debugging settings. + * @param bundle - Generated application bundle. + * @returns shell program with literal arguments and environment values. + */ +export function developmentLauncher(options: DevelopmentAppOptions, bundle: string): string { + const environment = { + DSH_HOME: options.home, + DSH_DESKTOP_DEV_APP: '1', + DSH_DESKTOP_HOST_INSPECT_PORT: String(options.hostPort), + DSH_DESKTOP_OPEN_DEVTOOLS: options.openDevtools, + ELECTRON_ENABLE_LOGGING: '1', + } + const args = [ + join(bundle, 'Contents', 'MacOS', 'Electron'), + `--inspect=127.0.0.1:${String(options.mainPort)}`, + `--remote-debugging-port=${String(options.rendererPort)}`, + `--user-data-dir=${options.userData}`, + options.appRoot, + ] + return [ + '#!/bin/sh', + ...Object.entries(environment).map(([key, value]) => `export ${key}=${quote(value)}`), + `cd ${quote(options.appRoot)}`, + `exec ${args.map(quote).join(' ')} "$@"`, + '', + ].join('\n') +} diff --git a/apps/desktop/scripts/development-project.ts b/apps/desktop/scripts/development-project.ts index eddd683161..d544ce6d36 100644 --- a/apps/desktop/scripts/development-project.ts +++ b/apps/desktop/scripts/development-project.ts @@ -16,10 +16,13 @@ import { dirname, join } from 'node:path' import { createDevelopmentProjectMetadata } from '../src/project-manager.ts' import type { DesktopRelease } from '../src/release.ts' import { DESKTOP_RUNTIME_FILE, type DesktopRuntimeDescriptor } from '../src/runtime-tree.ts' +import type { DesktopAutoUpdateTarget } from './desktop-auto-update-environment.mjs' +import { desktopTargetPlatform } from './desktop-build-paths.mjs' interface PackageManifest { readonly name?: string readonly version?: string + readonly dependencies?: Readonly> } /** Inputs whose locations differ between the launcher and isolated tests. */ @@ -34,6 +37,8 @@ export interface DevelopmentProjectOptions { readonly dependencyDir: string /** Release identity written into the disposable project metadata. */ readonly release: DesktopRelease + /** Build target whose prepared payload the disposable project runs against. */ + readonly target: DesktopAutoUpdateTarget } function readManifest(path: string): PackageManifest { @@ -86,9 +91,35 @@ function mirrorDependencyLinks(sourceRoot: string, destinationRoot: string): str return names } +/** Configured workspace plugins must resolve from the profile even when pnpm does not hoist them. */ +function mirrorWorkspaceDependencies(roots: readonly string[], destinationRoot: string): string[] { + const names: string[] = [] + const visited = new Set() + const visit = (directory: string): void => { + const source = realpathSync(directory) + if (visited.has(source)) return + visited.add(source) + const manifest = readManifest(join(source, 'package.json')) + for (const [name, specifier] of Object.entries(manifest.dependencies ?? {})) { + if (!specifier.startsWith('workspace:')) continue + const dependency = join(source, 'node_modules', ...name.split('/')) + if (!existsSync(dependency)) { + throw new Error(`desktop development: ${name} is missing from ${source}; run pnpm install`) + } + const destination = join(destinationRoot, ...name.split('/')) + removeOwnedPath(destination) + linkDirectory(dependency, destination) + names.push(name) + visit(dependency) + } + } + for (const directory of roots) visit(directory) + return names +} + /** * Replace one disposable project with links to the current built workspace. - * @param options - Project destination, CLI package, and release identity. + * @param options - Project destination, CLI package, release identity, and build target. * @returns the absolute project directory supplied by the caller. */ export function prepareDevelopmentProject(options: DevelopmentProjectOptions): string { @@ -117,7 +148,10 @@ export function prepareDevelopmentProject(options: DevelopmentProjectOptions): s createDevelopmentProjectMetadata(options.projectDir, options.release) const destinationModules = join(options.projectDir, 'node_modules') mkdirSync(destinationModules, { recursive: true }) - const names = mirrorDependencyLinks(options.dependencyDir, destinationModules) + const names = [ + ...mirrorDependencyLinks(options.dependencyDir, destinationModules), + ...mirrorWorkspaceDependencies([options.cliDir, options.hostDir], destinationModules), + ] const dshLink = join(destinationModules, '@deepseek-ai', 'dsh') removeOwnedPath(dshLink) linkDirectory(options.cliDir, dshLink) @@ -129,7 +163,7 @@ export function prepareDevelopmentProject(options: DevelopmentProjectOptions): s return typeof manifest.version === 'string' ? [{ name, version: manifest.version, path: `node_modules/${name}` }] : [] }) const runtime: DesktopRuntimeDescriptor = { schemaVersion: 1, release: options.release, - platform: process.platform, arch: process.arch, sharedPackages, files: [] } + ...desktopTargetPlatform(options.target), sharedPackages, files: [] } writeFileSync(join(options.projectDir, DESKTOP_RUNTIME_FILE), `${JSON.stringify(runtime, undefined, 2)}\n`) return options.projectDir } diff --git a/apps/desktop/scripts/electron-builder-config.mjs b/apps/desktop/scripts/electron-builder-config.mjs index 3ce50573d1..bab440ea61 100644 --- a/apps/desktop/scripts/electron-builder-config.mjs +++ b/apps/desktop/scripts/electron-builder-config.mjs @@ -1,6 +1,8 @@ +import { officePackageDirectories } from '../../../scripts/libreoffice-packages.mjs' import { X509Certificate } from 'node:crypto' +import { readFileSync } from 'node:fs' import { readFile } from 'node:fs/promises' -import { join } from 'node:path' +import { join, relative, sep } from 'node:path' import { fileURLToPath } from 'node:url' import { execFile } from 'node:child_process' import { promisify } from 'node:util' @@ -18,6 +20,8 @@ import { scrubWindowsSigningEnvironment, } from './windows-sign.mjs' import { resolveDesktopAutoUpdateConfig } from './desktop-auto-update-environment.mjs' +import { resolveDesktopBuildCommit } from './desktop-build-commit.mjs' +import { resolveDesktopBuildVersion } from './desktop-build-version.mjs' import { resolveDesktopPolicyEnvironment } from './desktop-policy-environment.mjs' import { desktopTargetBuildPaths, resolveDesktopBuildTarget } from './desktop-build-paths.mjs' import { installWindowsDirectoryInstaller } from './windows-directory-installer.mjs' @@ -36,6 +40,7 @@ import { * @param {NodeJS.Platform} hostPlatform - Build-host platform used when no explicit target is present. * @param {string} hostArch - Build-host architecture used when no explicit target is present. * @param {string | undefined} preparedRuntime - Verified private dsh tree for installed-update qualification; ordinary releases use the target tree. + * @param {string | undefined} preparedRuntimeVersion - Version that private tree declares, which qualification rewrites away from the product version. * @returns {object} electron-builder configuration. */ export function createElectronBuilderConfig( @@ -43,6 +48,7 @@ export function createElectronBuilderConfig( hostPlatform = process.platform, hostArch = process.arch, preparedRuntime = undefined, + preparedRuntimeVersion = undefined, ) { const appId = resolveDesktopAppId(env) const policy = resolveDesktopPolicyEnvironment(env) @@ -63,7 +69,7 @@ export function createElectronBuilderConfig( let primaryRuntimeDestination let dshDestination let windowsCode = [] - const unpack = ['**/*.{node,dylib,dll,so,exe}', '**/*.so.*', '**/spawn-helper', '**/@vscode/ripgrep/bin/rg', + const unpack = ['**/*.{node,dylib,dll,so,exe}', '**/*.so.*', '**/spawn-helper', '**/@vscode/ripgrep-*/bin/rg', `**/node_modules/@deepseek-ai/libreoffice-kit-${resolvedPlatform}-${resolvedArch}/**/*`] const windowsSigner = packagesWindows && !unsigned ? createWindowsTokenSigner({ @@ -86,11 +92,23 @@ export function createElectronBuilderConfig( } const update = unsigned ? undefined : resolveDesktopAutoUpdateConfig(env, resolvedPlatform, resolvedArch) if (preparedRuntime !== undefined) buildPaths.dsh = preparedRuntime + // electron-builder merges extraMetadata into the packaged manifest, so a build version here reaches + // the artifact names, the update feed, and the installed app.getVersion() the updater compares against. + const productVersion = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8')).version + const buildVersion = resolveDesktopBuildVersion(env, productVersion) + const packaged = resolveDesktopBuildCommit(env) return { appId, - extraMetadata: { dshDesktopAppId: appId, dshMandatoryUpdatePolicy: policy }, + protocols: [{ name: 'DeepSeek Harness', schemes: ['dsh'] }], + extraMetadata: { + dshDesktopAppId: appId, + dshMandatoryUpdatePolicy: policy, + ...buildVersion === productVersion ? {} : { version: buildVersion }, + ...packaged === undefined ? {} : { dshBuildCommit: packaged.commit, dshBuildDirty: packaged.dirty }, + }, productName: 'DeepSeek Harness', - artifactName: 'deepseek-harness-${version}-${os}-${arch}.${ext}', + // Unsigned builds carry their own suffix so a shared file can never pass for a release artifact. + artifactName: `deepseek-harness-\${version}-\${os}-\${arch}${unsigned ? '-unsigned' : ''}.\${ext}`, directories: { output: unsigned ? buildPaths.unsignedArtifacts : buildPaths.artifacts }, asar: true, electronDist: buildPaths.electron, @@ -110,9 +128,12 @@ export function createElectronBuilderConfig( }, files: [ 'lib/main.js', + 'lib/welcome/**/*', 'lib/preload-app.cjs', 'lib/preload-mandatory.cjs', + 'lib/preload-platform-account.cjs', 'lib/preload-update-dialog.cjs', + 'lib/preload-welcome.cjs', 'renderer/**/*', 'package.json', { from: buildPaths.dsh, to: 'dsh', filter: ['**/*'] }, @@ -123,6 +144,8 @@ export function createElectronBuilderConfig( extraResources: [ { from: buildPaths.runtime, to: 'runtime' }, { from: fileURLToPath(new URL('../resources/icon-windows.png', import.meta.url)), to: 'icon.png' }, + // Windows tray bitmaps; macOS keeps the Dock and ships no menu bar icon. + ...(packagesWindows ? [{ from: fileURLToPath(new URL('../resources/tray-windows.ico', import.meta.url)), to: 'tray.ico' }] : []), ], mac: { icon: fileURLToPath(new URL('../resources/icon-macos.png', import.meta.url)), @@ -143,6 +166,10 @@ export function createElectronBuilderConfig( writeUpdateInfo: false, }, beforePack: async context => { + const office = await officePackageDirectories(buildPaths.dsh, { platform: resolvedPlatform, arch: resolvedArch }) + const patterns = office.map(directory => `**/${relative(buildPaths.dsh, directory).split(sep).join('/')}/**/*`) + const existing = context.packager.config.asarUnpack ?? [] + context.packager.config.asarUnpack = [...(typeof existing === 'string' ? [existing] : existing), ...patterns] if (packagesWindows) windowsCode = await prepareWindowsAsarUnpack(context, buildPaths.dsh) if (windowsSigner !== undefined) { primaryRuntimeDestination = join(context.appOutDir, 'resources', 'runtime', 'primary-runtime') @@ -159,8 +186,10 @@ export function createElectronBuilderConfig( await writeMacOSAppUpdateConfig(resourcesDir, resolveMacOSAppUpdateFeed(context.packager.config.publish), context.packager.appInfo.updaterCacheDirName) } + // The bundled runtime declares whichever version prepared it: the product version for an ordinary + // release, and a rewritten one for installed-update qualification. await verifyDesktopRuntime(buildPaths.dsh, - context.packager.appInfo.version, { platform: resolvedPlatform, arch: resolvedArch }) + preparedRuntimeVersion ?? productVersion, { platform: resolvedPlatform, arch: resolvedArch }) // Unsigned Windows builds skip electron-builder's afterSign hook. if (packagesWindows && unsigned) await verifyWindowsAsarUnpack(buildPaths.dsh, resourcesDir, windowsCode) }, diff --git a/apps/desktop/scripts/flock-entry.ts b/apps/desktop/scripts/flock-entry.ts new file mode 100644 index 0000000000..7c2f3e75da --- /dev/null +++ b/apps/desktop/scripts/flock-entry.ts @@ -0,0 +1,94 @@ +/** Load the POSIX flock entry on first use so packaging entry scripts start on a checkout that has not built `native/system`. */ + +import { spawn } from 'node:child_process' +import { once } from 'node:events' +import { existsSync, readFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { dirname, join, resolve } from 'node:path' + +type FlockEntry = typeof import('@deepseek-ai/node-addon-system/flock') + +const REPOSITORY_ROOT = resolve(import.meta.dirname, '..', '..', '..') +/** Root package script that compiles the host addon, then the `native/system` script that emits the entry's JavaScript. */ +const BUILD_COMMANDS: readonly (readonly string[])[] = [ + ['run', 'build:native-system'], + ['--dir', 'native/system', 'run', 'build:ts'], +] +const BUILD_COMMAND_TEXT = BUILD_COMMANDS.map(args => `pnpm ${args.join(' ')}`).join(' && ') + +/** Loader steps; tests replace the module import, the addon probe, and the build. */ +export interface FlockEntryLoaderSteps { + /** Import `@deepseek-ai/node-addon-system/flock`; rejects with `ERR_MODULE_NOT_FOUND` while its `lib/` is unbuilt. */ + readonly importEntry: () => Promise + /** Whether every binary the host platform package declares in `prebuilds.json` exists. */ + readonly hostAddonBuilt: () => boolean + /** Compile the host addon and emit the entry's JavaScript; rejects when a build fails. */ + readonly build: () => Promise +} + +function isModuleNotFound(error: unknown): boolean { + return (error as NodeJS.ErrnoException | undefined)?.code === 'ERR_MODULE_NOT_FOUND' +} + +/** + * Create a memoized loader that imports the flock entry, building `native/system` once when its entry or host addon is missing. + * @param steps - Module import, addon probe, and build used by the loader. + * @returns Loader that resolves the same entry on every call; a failed load is retried by the next call. + */ +export function createFlockEntryLoader(steps: FlockEntryLoaderSteps): () => Promise { + let loading: Promise | undefined + const load = async (): Promise => { + let entry: FlockEntry | undefined + try { + entry = await steps.importEntry() + } catch (error) { + if (!isModuleNotFound(error)) throw error + } + if (entry !== undefined && steps.hostAddonBuilt()) return entry + process.stderr.write(`desktop package: flock addon is not built; running ${BUILD_COMMAND_TEXT}\n`) + await steps.build() + return steps.importEntry() + } + return () => { + loading ??= load().catch((error: unknown) => { loading = undefined; throw error }) + return loading + } +} + +function hostAddonBuilt(): boolean { + const { platform, arch } = process + // flock itself rejects other platforms; the probe only reports missing builds for supported ones. + if (platform !== 'darwin' && platform !== 'linux') return true + const entryManifest = createRequire(import.meta.url).resolve('@deepseek-ai/node-addon-system/package.json') + let prebuilds: string + try { + prebuilds = createRequire(entryManifest).resolve(`@deepseek-ai/node-addon-system-${platform}-${arch}/prebuilds.json`) + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND') throw error + return false + } + const manifest = JSON.parse(readFileSync(prebuilds, 'utf8')) as { readonly binaries: readonly { readonly path: string }[] } + return manifest.binaries.every(binary => existsSync(join(dirname(prebuilds), binary.path))) +} + +async function buildNativeSystem(): Promise { + const pnpmEntry = process.env.npm_execpath + if (pnpmEntry === undefined || pnpmEntry === '') { + throw new Error(`desktop package: flock addon is not built; run ${BUILD_COMMAND_TEXT} or invoke this script through a pnpm package command`) + } + for (const args of BUILD_COMMANDS) { + const child = spawn(process.execPath, [pnpmEntry, ...args], { cwd: REPOSITORY_ROOT, stdio: 'inherit' }) + const [code, signal] = await once(child, 'close') as [number | null, NodeJS.Signals | null] + if (code !== 0) throw new Error(`desktop package: pnpm ${args.join(' ')} exited with ${String(code ?? signal)}`) + } +} + +/** + * Load `@deepseek-ai/node-addon-system/flock`, building the host addon and the entry's JavaScript through pnpm when either is missing. + * @returns The flock entry; rejects when a build fails or the script did not run under pnpm. + */ +export const loadFlockEntry = createFlockEntryLoader({ + importEntry: () => import('@deepseek-ai/node-addon-system/flock'), + hostAddonBuilt, + build: buildNativeSystem, +}) diff --git a/apps/desktop/scripts/installed-update-builder.ts b/apps/desktop/scripts/installed-update-builder.ts index c42ae2347b..2f0cbefe6c 100644 --- a/apps/desktop/scripts/installed-update-builder.ts +++ b/apps/desktop/scripts/installed-update-builder.ts @@ -23,7 +23,7 @@ export async function createInstalledUpdateBuilderConfig(manifest: string, versi const dsh = join(run.root, version, 'dsh') await verifyDesktopRuntime(dsh, version, { platform: 'win32', arch: 'x64' }) const config = createElectronBuilderConfig({ ...environment, DSH_DESKTOP_APP_ID: run.appId, - DSH_DESKTOP_TARGET_PLATFORM: 'win32', DSH_DESKTOP_TARGET_ARCH: 'x64', DSH_DESKTOP_UNSIGNED: '0' }, 'win32', 'x64', dsh) + DSH_DESKTOP_TARGET_PLATFORM: 'win32', DSH_DESKTOP_TARGET_ARCH: 'x64', DSH_DESKTOP_UNSIGNED: '0' }, 'win32', 'x64', dsh, version) return { ...config, productName: run.productName, directories: { ...config.directories, output: join(run.root, version, 'installer') }, diff --git a/apps/desktop/scripts/installer-directories.nsh b/apps/desktop/scripts/installer-directories.nsh index 881bdc8b20..49d4a2fb82 100644 --- a/apps/desktop/scripts/installer-directories.nsh +++ b/apps/desktop/scripts/installer-directories.nsh @@ -17,7 +17,11 @@ Var dshNewMoved ${If} $R0 != 0 DetailPrint $R1 Call dshRollbackDirectories - MessageBox MB_OK|MB_ICONEXCLAMATION "$(decompressionFailed)" /SD IDOK + !ifmacrodef customInstallerExtractFailed + !insertmacro customInstallerExtractFailed "${FILE}" + !else + MessageBox MB_OK|MB_ICONEXCLAMATION "$(decompressionFailed)" /SD IDOK + !endif SetErrorLevel 2 Quit ${EndIf} diff --git a/apps/desktop/scripts/installer.nsh b/apps/desktop/scripts/installer.nsh index fd3f1c4c36..0f4d859e78 100644 --- a/apps/desktop/scripts/installer.nsh +++ b/apps/desktop/scripts/installer.nsh @@ -1,19 +1,31 @@ !include "LogicLib.nsh" +!include "FileFunc.nsh" !define INSTALLER_SOURCE_DIR "${__FILEDIR__}\..\installer" !define /ifndef INSTALLER_BUILD_DIR "${__FILEDIR__}\..\.desktop-build\targets\win-x64\installer-ui" +ManifestDPIAware true !ifndef BUILD_UNINSTALLER - ManifestDPIAware true !define MUI_CUSTOMFUNCTION_GUIINIT InstallerGuiInit !endif !macro customHeader !define /ifndef INSTALLER_STRINGS_FILE "${INSTALLER_SOURCE_DIR}\strings.nsh" !include "${INSTALLER_STRINGS_FILE}" + !ifdef BUILD_UNINSTALLER + BrandingText " " + SetFont "Segoe UI" 9 + !ifdef LANG_SIMPCHINESE + SetFont /LANG=${LANG_SIMPCHINESE} "Microsoft YaHei UI" 9 + !endif + !include "${INSTALLER_SOURCE_DIR}\uninstall.nsh" + !endif !ifndef BUILD_UNINSTALLER !include "${INSTALLER_SOURCE_DIR}\theme.nsh" !include "${INSTALLER_SOURCE_DIR}\pages.nsh" !include "${INSTALLER_SOURCE_DIR}\lifecycle.nsh" + Function InstallerCheckAppRunning + !insertmacro customCheckAppRunning + FunctionEnd !endif !macroend @@ -54,6 +66,7 @@ File "/oname=$PLUGINSDIR\brand-dark.bmp" "${INSTALLER_BUILD_DIR}\brand-dark.bmp" File "/oname=$PLUGINSDIR\brand-dark-2x.bmp" "${INSTALLER_BUILD_DIR}\brand-dark-2x.bmp" File "/oname=$PLUGINSDIR\window-frame.dll" "${INSTALLER_BUILD_DIR}\window-frame.dll" + Call InstallerCheckAppRunning ${If} ${Silent} Call InstallerPreflight ${If} $InstallerError != "" @@ -74,6 +87,10 @@ Page custom InstallerWelcome InstallerWelcomeLeave !macroend +!macro customUnInstall + Call un.CleanData +!macroend + !macro customPageAfterChangeDir !define MUI_PAGE_CUSTOMFUNCTION_PRE InstallerBeforeInstall !define MUI_PAGE_CUSTOMFUNCTION_SHOW InstallerProgressShow @@ -108,16 +125,43 @@ System::Call /NOUNLOAD '$PLUGINSDIR\window-frame.dll::InstallerExtract(p $HWNDPARENT, w "$PLUGINSDIR\dsh-7za.exe", w "${Archive}", w "$INSTDIR", w "$PLUGINSDIR\extract.log") i.s ?c' System::Store "L" Pop $R0 + ; The failure report owns 7-Zip's UTF-8 output; the details view only needs the result code. StrCpy $R1 "$R0" - ${If} $R0 != 0 - Push $0 - FileOpen $0 "$PLUGINSDIR\extract.log" r - ${IfNot} ${Errors} - FileRead $0 $R1 - FileClose $0 - ${EndIf} - Pop $0 +!macroend + +; The report outlives $PLUGINSDIR so a user can send it; silent installs keep only the file. The updater cache +; directory is never an installation target and leaves with the application on uninstall. +; The directory smoke fixture predefines DSH_INSTALLER_LOG_DIR to keep reports inside its scratch tree. +!ifndef DSH_INSTALLER_LOG_DIR + !define DSH_INSTALLER_LOG_DIR "$LOCALAPPDATA\${DSH_UPDATER_CACHE_NAME}\installer-logs" +!endif + +!macro customInstallerExtractFailed Archive + Push $0 + Push $1 + Push $2 + Push $3 + Push $4 + Push $5 + Push $6 + ; GetTime yields zero-padded day, month, year, weekday, hour, minute, second. + ${GetTime} "" "L" $0 $1 $2 $3 $4 $5 $6 + StrCpy $0 "${DSH_INSTALLER_LOG_DIR}\extract-failure-$2$1$0-$4$5$6.log" + StrCpy $1 1 + ${If} ${Silent} + StrCpy $1 0 ${EndIf} + System::Call '$PLUGINSDIR\window-frame.dll::InstallerReportExtractFailure(p $HWNDPARENT, i R0, w "${Archive}", w "$dshNewDirectory", w "$PLUGINSDIR\extract.log", w r0, i r1, w "$(^SetupCaption)", w "$(INSTALLER_EXTRACT_FAILED)", w "$(INSTALLER_EXTRACT_HINT)", w "$(INSTALLER_EXTRACT_COPY)", w "$(INSTALLER_EXTRACT_EXPAND)", w "$(INSTALLER_EXTRACT_COLLAPSE)", w "$(INSTALLER_EXTRACT_SAVED)", w "$(INSTALLER_EXTRACT_UNSAVED)", w "$(INSTALLER_EXTRACT_COPIED)") i.r2 ?c' + ${If} $2 == 1 + DetailPrint $0 + ${EndIf} + Pop $6 + Pop $5 + Pop $4 + Pop $3 + Pop $2 + Pop $1 + Pop $0 !macroend !macro customCheckAppRunning @@ -163,6 +207,8 @@ ${EndIf} !insertmacro InstallerPublishStage 4 !insertmacro dshFinishDirectories + ; Standard uninstall-entry metadata read by inventory tools; the upstream template records it only under its private key. + WriteRegStr SHELL_CONTEXT "${UNINSTALL_REGISTRY_KEY}" InstallLocation "$INSTDIR" ${If} $0 == 1 SetErrors ${Else} diff --git a/apps/desktop/scripts/macos-notarization-proxy.ts b/apps/desktop/scripts/macos-notarization-proxy.ts index 11f19b4022..bd5900a758 100644 --- a/apps/desktop/scripts/macos-notarization-proxy.ts +++ b/apps/desktop/scripts/macos-notarization-proxy.ts @@ -5,7 +5,7 @@ import { closeSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } f import { connect } from 'node:net' import { homedir } from 'node:os' import { dirname, join, resolve } from 'node:path' -import { tryLockExclusive } from '@deepseek-ai/node-addon-system/flock' +import { loadFlockEntry } from './flock-entry.ts' interface ProxyState { readonly enabled: boolean @@ -50,6 +50,7 @@ const operations: ProxyOperations = { } async function withProxyLock(lock: string, action: () => Promise): Promise { + const { tryLockExclusive } = await loadFlockEntry() mkdirSync(dirname(lock), { recursive: true, mode: 0o700 }) // Keep this inode across transactions: unlinking it would allow two independent locks. const fd = openSync(`${lock}.flock`, 'a', 0o600) diff --git a/apps/desktop/scripts/macos-signing-keychain.d.mts b/apps/desktop/scripts/macos-signing-keychain.d.mts index 452878aad1..f95a370c98 100644 --- a/apps/desktop/scripts/macos-signing-keychain.d.mts +++ b/apps/desktop/scripts/macos-signing-keychain.d.mts @@ -3,14 +3,15 @@ /** * Import and authorize the required p12 before work; delete the owned keychain after work settles. * Children receive only its path, never the p12 password. Existing login keychains are not unlocked. + * The owned keychain is prepended to the user search list for the run and the previous list is restored. * Abrupt process termination requires the CI runner to clean its temporary directory. * @param environment Validated platform configuration with local CSC_LINK and CSC_KEY_PASSWORD. * @param action All signing work, settled before cleanup. - * @param run Apple command executor. + * @param run Apple command executor returning command standard output. * @returns Resolves after work and cleanup; rejects on setup, work, or cleanup failure. */ export function withMacOSSigningKeychain( environment: NodeJS.ProcessEnv, action: (environment: NodeJS.ProcessEnv) => Promise, - run?: (command: string, args: string[]) => void, + run?: (command: string, args: string[]) => string, ): Promise diff --git a/apps/desktop/scripts/macos-signing-keychain.mjs b/apps/desktop/scripts/macos-signing-keychain.mjs index e71d469395..425eb6a96c 100644 --- a/apps/desktop/scripts/macos-signing-keychain.mjs +++ b/apps/desktop/scripts/macos-signing-keychain.mjs @@ -9,23 +9,40 @@ import { join } from 'node:path' * Execute a credential-bearing Apple command without exposing arguments or tool output on failure. * @param {string} command Absolute executable path. * @param {string[]} args Command arguments, potentially containing secrets. - * @returns {void} + * @returns {string} Command standard output, needed when the caller reads a security query. */ function execute(command, args) { - try { execFileSync(command, args, { stdio: 'pipe', timeout: 120_000 }) } + try { + return execFileSync(command, args, { stdio: 'pipe', timeout: 120_000, encoding: 'utf8' }) + } catch (error) { // execFile errors contain the command line, including private-key passwords. throw new Error(`desktop macOS signing: ${command} ${args[0]} failed; check certificate, password, and signing access`) } } +/** + * Read the user's keychain search list so this invocation can extend and later restore it. + * @param {(command: string, args: string[]) => string} run Apple command executor. + * @returns {string[]} Keychain paths on the user search list. + */ +function readUserKeychains(run) { + const output = run('/usr/bin/security', ['list-keychains', '-d', 'user']) + if (typeof output !== 'string') { + throw new Error('desktop macOS signing: the user keychain search list could not be read') + } + return output.split('\n').map(line => line.trim().replace(/^"|"$/gu, '')).filter(path => path !== '') +} + /** * Import and authorize the required p12 before work; delete the owned keychain after work settles. * Children receive only its path, never the p12 password. Existing login keychains are not unlocked. + * codesign resolves an identity only from a keychain on the user search list, so the owned keychain + * is prepended during the run and the previous list is restored before deletion. * Abrupt process termination requires the CI runner to clean its temporary directory. * @param {NodeJS.ProcessEnv} environment Validated platform configuration with local CSC_LINK and CSC_KEY_PASSWORD. * @param {(environment: NodeJS.ProcessEnv) => Promise} action All signing work, settled before cleanup. - * @param {(command: string, args: string[]) => void} run Apple command executor. + * @param {(command: string, args: string[]) => string} run Apple command executor. * @returns {Promise} Resolves after work and cleanup; rejects on setup, work, or cleanup failure. */ export async function withMacOSSigningKeychain(environment, action, run = execute) { @@ -38,6 +55,7 @@ export async function withMacOSSigningKeychain(environment, action, run = execut /** @param {string[]} args Security command arguments. */ const security = args => run('/usr/bin/security', args) let created = false + let previousKeychains try { security(['create-keychain', '-p', password, keychain]) created = true @@ -45,6 +63,10 @@ export async function withMacOSSigningKeychain(environment, action, run = execut security(['set-keychain-settings', keychain]) security(['import', certificate, '-k', keychain, '-P', exportPassword, '-T', '/usr/bin/codesign', '-T', '/usr/bin/productbuild']) security(['set-key-partition-list', '-S', 'apple-tool:,apple:', '-s', '-k', password, keychain]) + // codesign ignores --keychain unless that file is also on the search list + // (https://github.com/electron-userland/electron-builder/issues/1457). + previousKeychains = readUserKeychains(run) + security(['list-keychains', '-d', 'user', '-s', keychain, ...previousKeychains]) const probe = join(directory, 'probe') run('/bin/cp', ['/usr/bin/true', probe]) run('/usr/bin/codesign', ['--force', '--sign', `Developer ID Application: ${environment.DSH_DESKTOP_MACOS_SIGNING_IDENTITY}`, '--keychain', keychain, '--timestamp', '--options', 'runtime', probe]) @@ -54,7 +76,10 @@ export async function withMacOSSigningKeychain(environment, action, run = execut delete childEnvironment.CSC_KEY_PASSWORD await action(childEnvironment) } finally { - try { if (created) security(['delete-keychain', keychain]) } + try { + if (previousKeychains !== undefined) security(['list-keychains', '-d', 'user', '-s', ...previousKeychains]) + if (created) security(['delete-keychain', keychain]) + } finally { rmSync(directory, { recursive: true, force: true }) } } } diff --git a/apps/desktop/scripts/package-target.ts b/apps/desktop/scripts/package-target.ts index 6909fc4371..62f4fcbbb2 100644 --- a/apps/desktop/scripts/package-target.ts +++ b/apps/desktop/scripts/package-target.ts @@ -1,6 +1,6 @@ /** Build one release target with matching Electron and dsh architecture. */ -import { execFileSync, spawn } from 'node:child_process' +import { spawn } from 'node:child_process' import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs' import { parseArgs } from 'node:util' import { join, resolve } from 'node:path' @@ -19,6 +19,10 @@ import { macOSDownloadEnvironment, resolveMacOSPackageSettings } from './macos-p import { packagingErrorDetails, packagingStep } from './packaging-step.mjs' import { notarizeMacOS } from './notarize-macos.mjs' import { resolveMacOSNotarizationEnvironment } from './desktop-release-environment.mjs' +import { DESKTOP_BUILD_VERSION_ENV, resolveDesktopBuildVersion, validateDesktopBuildVersion } from './desktop-build-version.mjs' +import { suggestDesktopBuildVersion } from './desktop-build-version-discovery.ts' +import { desktopBuildCommitEnvironment, readDesktopBuildCommit, resolveDesktopBuildCommit } from './desktop-build-commit.mjs' +import { requireDesktopToolchain } from './desktop-toolchain-preflight.ts' import { withMacOSNotarizationProxy } from './macos-notarization-proxy.ts' const APP_ROOT = resolve(import.meta.dirname, '..') @@ -39,6 +43,9 @@ const DESKTOP_UPLOAD_CREDENTIAL_ENV_NAMES = new Set([ 'DOWNLOAD_PROD_COS_SECRET_KEY', ]) +/** `--build-version` value that numbers a build after the ones already taken. */ +const AUTOMATIC_BUILD_VERSION = 'auto' + /** Fixed platform and architecture identifiers exposed by package scripts. */ export type DesktopPackageTargetName = 'mac-arm64' | 'mac-x64' | 'win-x64' @@ -136,15 +143,19 @@ function writeReleaseRecord( if (desktopVersion !== dshVersion) { throw new Error(`desktop package: desktop version ${desktopVersion} does not match dsh version ${dshVersion}`) } + const buildVersion = resolveDesktopBuildVersion(environment, dshVersion) + const packaged = resolveDesktopBuildCommit(environment) const update = resolveDesktopAutoUpdateConfig(environment, target.platform, target.arch) const recordPath = join(artifactsRoot, desktopBuildRecordFilename(target.name)) const temporaryPath = `${recordPath}.tmp` writeFileSync(temporaryPath, `${JSON.stringify({ schemaVersion: 1, target: target.name, - version: dshVersion, + version: buildVersion, environment: update.environment, publicUrl: update.publicUrl, + // Upload reads this to tag the commit a production release was packaged from. + ...packaged === undefined ? {} : { commit: packaged.commit, dirty: packaged.dirty }, }, null, 2)}\n`) renameSync(temporaryPath, recordPath) } @@ -186,6 +197,8 @@ interface DesktopPackageInvocation { readonly prepareOnly: boolean readonly unsigned: boolean readonly check: boolean + /** Build identifier to publish under, when this build does not publish the product version. */ + readonly requestedBuildVersion: string | undefined } function hostTargetName(platform: NodeJS.Platform, arch: string): DesktopPackageTargetName { @@ -207,25 +220,33 @@ export function parseDesktopPackageInvocation( hostArch: string = process.arch, ): DesktopPackageInvocation { const { values, positionals } = parseArgs({ - args: [...argv], + // `pnpm run