merge: fix opencode model switching runtime refresh
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
2026-08-21 01:48:01 +08:00
15 changed files with 258 additions and 96 deletions

View File

@@ -13,7 +13,7 @@
| 运营发布 | Works Square 审核与交付 | 公共 `play_url` | 客户端只消费服务端发布结果;真实合同校验 → OSS/CDN 生产链仍待整链验收 |
| 可信发布门禁 | source+built+artifact contract | 服务端逐字节重算/合同校验 → 不可变 Release 固化 → 人工审核 | 服务端不运行项目 Vite仍是不可绕过权威未来 runtime 强门禁需可信 verifier |
| AI 编程 Session 执行提交 | ChatPanel 当前 Agent / Session | Renderer per-Session run state → deferred provider sync → Host API → Main manager FIFO → per-project Agent FIFO → runtime/config + Agent readiness → OpenCode message / command / summarize | 新建 session 已知为空时不等待空历史;普通 prompt、项目命令和压缩不重发模型 override不同 Session 没有 reply-duration 全局锁。provider/runtime 或 Agent 未应用时以 typed `409``promptSent:false` 在执行前终止且不自动重启、reload 或 dispose 共享 runtime |
| AI 编程 Session 模型切换 | 页面模型选择器或 `/models` / `/model` | Renderer Session model action → Host API → Main OpenCode client → `POST /api/session/{sessionID}/model` | 只改变现有 OpenCode Session 的后续 turn 模型并从 Session metadata 恢复不写伙伴默认模型、provider 配置或 runtime freshness也不要求 runtime restart |
| AI 编程 Session 模型切换 | 页面模型选择器或 `/models` / `/model` | Renderer Session model action → Host API → Main OpenCode client → `POST /api/session/{sessionID}/model` | Main client 将内部 `modelID` 映射为 OpenCode 1.18.9 wire `ModelRef.id`只改变现有 Session 的后续 turn 模型并从 Session metadata 恢复不写伙伴默认模型、provider 配置或 runtime freshness也不要求 runtime restart |
| AI 编程启动确认 | Host POST、OpenCode HTTP polling 与 Session events | Renderer `{sessionId, runToken}` watchdog → `sessionRunStates[sessionId]` | Host 接受与 user transcript 不算 ACKbusy/retry、assistant、question、permission 或 typed terminal event 才确认/结束启动。10 秒无确认只终止该 Session 为 `SESSION_START_UNCONFIRMED`,清理其队列且不自动重放 |
| AI 编程上下文压缩 | OpenCode compaction Part / `session.compacted` / `/compact` | Renderer Store → per-session transcript state → Chat mixed timeline | 手动请求先创建 immutable UI eventnative identity 负责归并与回放去重;完成只更新压缩项,真实 idle 才结束 run 和释放队列 |
| AI 编程模型代理错误 | OpenCode provider 请求 | Main Host AI proxy → Works 模型上游 | 配额耗尽保持独立终止态;只有明确的分组上游饱和才投影为 OpenCode 终止状态,通用限速仍保留 `429` |
@@ -48,7 +48,7 @@
- AI 编程 Store 的 `sessionMessagesBySessionId` own-key 是加载状态契约:键缺失表示历史未知,存在且值为 `[]` 表示已知为空。只有后者可使用不读取历史的快速选择;普通历史会话选择继续刷新消息。
- AI 编程 Store 的运行态、错误、startup deadline 和内部 pending queue 按 Session 持有top-level error 只表示真实全局错误。任何不确定的远程失败只清理对应 Session 的旧队列,手动重试不得复活失败前已排队的 prompt。
- OpenCode Session 持有现有会话的 active provider/model伙伴 `model` 只在创建新 Session 时提供默认值。页面与 slash command 通过同一 native Session endpoint 切换,后续执行和压缩不得用伙伴默认值覆盖该选择。
- Main 以 manager identity + runtime generation provenance 持有 runtime-config latch并按 Agent id 持有 desired/applied 内容状态与本 generation 已知 id。owned `fresh` generation 中首次出现的唯一 id 可在 live `/agent` 暴露后应用,且不会阻断已有未变 Agent同 id 编辑、删除后重建、live id 缺失以及 attached/unknown generation 均不能越过门禁。
- Main 以 manager identity + runtime generation provenance 持有 runtime-config latch并按 Agent id 持有 desired/applied 内容状态与本 generation 已知 id。owned `fresh` generation 中首次出现的唯一 id 可在 live `/agent` 暴露后应用,且不会阻断已有未变 Agent同 id 编辑、删除后重建、live id 缺失以及 attached/unknown generation 均不能越过门禁。托管本地 AI proxy 的 fresh runtime config 直接使用当前进程 Host API token把该已生效 token 写回 provider storage 不构成 runtime freshness 变化attached/unknown generation 或 timeout/partial persistence 仍保持 sticky pending。
- AI 编程压缩状态由 transcript 中的 `compactionsById` / `compactionOrder` 单一持有。运行中 hydration 保留 manual pending identity 并用 native Part 合并completed 不得回退为 running。`session.compacted` 不是 run idle不能据此释放 queued prompt失败、中止或 runtime generation 变化只清理对应未完成事件。
- Main Host AI proxy 可为固定 OpenCode 重试契约做窄化的内部状态投影:配额耗尽投影为 `402`,明确上游分组饱和的 `429` 投影为终止 `400`,其他 `429` 原样保留;升级 OpenCode 时必须重新验证该契约。
- Main AppUpdater 持有 feed、原始异常、下载和安装状态设置页只消费状态投影。一次 electron-updater `error` 事件覆盖的并发检查不会在 Renderer 重复发错,但独立的后续检查仍有自己的报告生命周期。
@@ -70,4 +70,4 @@
## Last Updated
2026-08-20
2026-08-21

View File

@@ -18,7 +18,9 @@
| `src/pages/Chat/OpencodeChatPanel.tsx` | AI 编程 Agent 选择、首次 session 创建、per-Session 模型选择、prompt 提交与消息/压缩混合时间线 | 页面模型选择器与 `/models` / `/model` 共用 OpenCode Session 切换动作;伙伴模型只作为新 Session 默认值,选中 Session 只投影自己的 run error |
| `src/lib/opencode-session-state.ts` | OpenCode 会话消息与上下文压缩时间线的规范化、hydration 和事件归并 | 压缩 UI identity 不变native Part/event identity 用于回放去重completed 状态不可降级 |
| `src/stores/opencode.ts` | OpenCode runtime、session、消息缓存、per-Session 模型/提交/启动确认与压缩生命周期 | Session 列表 hydration 恢复当前模型;每个 Session 独立持有 model/run/error/queue10 秒 run-token watchdog 只终止对应 Session 且不自动重放;`session.compacted` 只完成压缩项,真实 idle 才释放 run 队列 |
| `electron/api/routes/opencode.ts` | Main-owned OpenCode Session 模型切换、execution acceptance、provider/runtime freshness 与项目 Agent 门禁 | Session 模型切换调用 OpenCode native endpoint不写 provider/伙伴配置message/command/summarize 共用 bounded manager FIFO普通执行路径不自动 restart/reload/dispose |
| `electron/opencode/client.ts` | Makelore 内部 OpenCode DTO 与固定 runtime HTTP wire contract | 内部 `{ providerID, modelID }` Session 模型切换边界映射为 OpenCode 1.18.9 `ModelRef``{ providerID, id }`runtime 升级时复核 schema |
| `electron/opencode/provider-config.ts` | Main-owned OpenCode provider/env 配置构建 | 托管本地 AI proxy 在 owned fresh runtime 启动时使用当前进程 Host API token持久化旧 token 不得覆盖本次 runtime 已注入值 |
| `electron/api/routes/opencode.ts` | Main-owned OpenCode Session 模型切换、execution acceptance、provider/runtime freshness 与项目 Agent 门禁 | Session 模型切换调用 OpenCode native endpoint不写 provider/伙伴配置fresh runtime 已生效 proxy token 的持久化不标记 staleattached/unknown 与不确定写入仍 fail closedmessage/command/summarize 共用 bounded manager FIFO普通执行路径不自动 restart/reload/dispose |
| `electron/opencode/project-agent-runtime.ts` | 项目 Agent 逐 id desired/applied 内容状态、runtime generation provenance、live registry acceptance 与配置 mutation 串行化 | owned fresh generation 中首次出现的唯一 id 可在 live `/agent` 暴露后应用;同 id 编辑、删除后重建及 attached/unknown generation 仍 fail closed用户修改的 retired Agent 文件不得被删除 |
| `electron/opencode/runtime-config-readiness.ts` | manager + generation scoped provider/runtime stale latch | timeout/partial persistence 保持 sticky uncertain 状态;只有成功显式 apply 后的合格 fresh generation 可解除,迟到 lease 不能改写 readiness |
| `electron/api/routes/ai-proxy.ts` | Main-owned 模型代理、凭据边界与上游响应投影 | 仅对明确上游分组饱和做终止态兼容投影,通用限速保持 `429` |
@@ -79,4 +81,4 @@
## Last Updated
2026-08-20
2026-08-21

View File

@@ -4,6 +4,15 @@ This file is the integrated default-branch snapshot. Feature tasks record progre
## Integrated Through
- OpenCode model-switch runtime correction source commit `cce7722` from feature
task `20260821-model-switch-runtime-fix-a83d6c91` is integrated on local
`main` by task `20260821-integrate-model-switch-fix-8f2d6c41`. The Main-owned
client now maps Makelore's internal `modelID` to OpenCode 1.18.9's wire field
`id`. An owned fresh runtime receives the current per-process Host API token
when its local-proxy provider config is built, so persisting that already
active token no longer creates a false manual-restart requirement;
attached/unknown generations and timeout/partial persistence remain
fail-closed. A rebuilt-client real local smoke is still pending.
- Login-layout source commit `4d512b1` from feature task
`20260821-remember-below-login-4a7c91d2` was merged into local `main` as
`22f4bbc` by integration task `20260820-integrate-remember-password-5d7e3a1c`.
@@ -122,7 +131,7 @@ Makelore 在会话恢复、登录和刷新后由 Electron Main 请求 Works `/ap
AI 学习现在是已启用的运营精选项目目录,并继续受登录和 `module_access.learning` 控制。Renderer 通过 Main-owned Host API 获取分页项目卡片和 README 详情Markdown 支持 GFM、禁用原始 HTML。服务端发布时只校验图片 URL 为无凭据、默认端口、无 fragment 且当前 DNS 结果全部为公网地址的 HTTPS URL保留地址而不下载、识别格式、转码或镜像客户端仅为 README 图片节点启用直连,因此 SVG 和 Electron 支持的其他格式可直接显示单图失败不阻断详情。封面和历史发布媒体继续走受控路径。详情页的下载按钮打开系统保存对话框Main 将 ZIP 流式写入临时文件,只允许最多五跳同 Works origin 重定向,不校验 `Content-Length``archiveBytes`、实际流字节数或客户端大小上限,校验 SHA-256 和 ZIP 签名后原子保存Renderer 只接收 `saved``cancelled`。课程生成、进度、本地课程库、OpenMAIC player、Agent、ASR、课堂 runtime、Learning IPC 和 player artifact 打包已删除且没有兼容读取路径;历史课程数据保留但不再读取。服务端和客户端源码契约已完成,不代表生产部署或真实账号安装包联调已经完成。
AI 编程首次发送在新建 OpenCode session 已知为空时不再等待冗余历史读取prompt 可直接进入 Host API普通历史会话仍刷新消息。伙伴模型只作为新 Session 默认值,现有 Session 的 active model 由 OpenCode 持有;页面选择器和 `/models` / `/model` 走同一个 native Session 切换动作,后续 prompt、项目命令和上下文压缩不再用伙伴默认值覆盖选择。每个 Session 独立持有 model、run、loading、error 与内部队列;一个 Session 的回复或终止不会锁住、改写或重放另一个 Session。Host 接受或用户消息落库不算启动确认,只有明确 busy/retry、assistant、question、permission 或 typed terminal event 才确认/终结启动;独立的 10 秒 watchdog 到期时仅以 `SESSION_START_UNCONFIRMED` 终止该 Session。Main 按 Agent id 维护 desired/applied 内容状态与 runtime generation provenanceowned fresh generation 中首次出现的唯一 id 可在 live registry 暴露后启用且不阻断已有 Agent同 id 编辑、删除后重建及 attached/unknown generation 仍保持 pending。provider/runtime 或 Agent 未应用时在执行前返回 typed `409``promptSent:false`。普通 message、command、summarize 和后台 provider sync 不自动重启、reload 或 dispose 共享 runtime只有显式用户 apply/lifecycle 操作可以推进运行时刷新。Main AI proxy 只把明确的上游分组饱和投影为当前 OpenCode 的终止状态,配额耗尽保持独立终止态,通用限速继续保留 `429`。上下文压缩以每个 Session 的持久时间线事件呈现:自动与手动压缩使用不同文案,运行态原位弱化显示,完成后静态保留并可从历史恢复;`session.compacted` 只完成对应事件,只有真实 idle 才结束 run 和释放排队消息。
AI 编程首次发送在新建 OpenCode session 已知为空时不再等待冗余历史读取prompt 可直接进入 Host API普通历史会话仍刷新消息。伙伴模型只作为新 Session 默认值,现有 Session 的 active model 由 OpenCode 持有;页面选择器和 `/models` / `/model` 走同一个 native Session 切换动作,Main 在 OpenCode client 边界把内部 `modelID` 映射为 1.18.9 `ModelRef.id`后续 prompt、项目命令和上下文压缩不再用伙伴默认值覆盖选择。每个 Session 独立持有 model、run、loading、error 与内部队列;一个 Session 的回复或终止不会锁住、改写或重放另一个 Session。Host 接受或用户消息落库不算启动确认,只有明确 busy/retry、assistant、question、permission 或 typed terminal event 才确认/终结启动;独立的 10 秒 watchdog 到期时仅以 `SESSION_START_UNCONFIRMED` 终止该 Session。Main 按 Agent id 维护 desired/applied 内容状态与 runtime generation provenanceowned fresh generation 中首次出现的唯一 id 可在 live registry 暴露后启用且不阻断已有 Agent同 id 编辑、删除后重建及 attached/unknown generation 仍保持 pending。fresh runtime 的本地 AI proxy provider 在构建时使用当前 Main Host API token后续将该已生效 token 持久化不构成 provider/runtime 变更attached/unknown generation 或不确定写入仍保持 pending。provider/runtime 或 Agent 未应用时在执行前返回 typed `409``promptSent:false`。普通 message、command、summarize 和后台 provider sync 不自动重启、reload 或 dispose 共享 runtime只有显式用户 apply/lifecycle 操作可以推进运行时刷新。Main AI proxy 只把明确的上游分组饱和投影为当前 OpenCode 的终止状态,配额耗尽保持独立终止态,通用限速继续保留 `429`。上下文压缩以每个 Session 的持久时间线事件呈现:自动与手动压缩使用不同文案,运行态原位弱化显示,完成后静态保留并可从历史恢复;`session.compacted` 只完成对应事件,只有真实 idle 才结束 run 和释放排队消息。
Updater 仍由 Electron Main 选择目标 feed、记录原始诊断并保持失败语义。正式稳定源缺少对应平台 manifest 时设置页只显示一条简洁中文提示并允许重试不把缺包误报为已是最新版也不向普通界面暴露堆栈、URL、路径或错误码签名产物发布和真实升级安装仍属于外部 Release Gate。
@@ -197,10 +206,10 @@ Robot 绑定设备默认先显示“引导配网 / 已有激活码”路径选
- AI proxy 的明确饱和 `429 → 400` 是与当前 OpenCode 重试契约匹配的内部投影;升级 OpenCode 或上游饱和错误文案变化时必须复核。不得把通用 `429` 或仅含 `rate_limit_exceeded` 的响应误分类为终止饱和。
- AI 编程压缩事件必须按 Session、run token、runtime generation 与 native Part/event identity 关联;`session.compacted` 不等于 `session.idle`,历史 hydration 不得把 completed 降回 running失败或中止必须清理本 run 的未完成事件。
- AI 编程多 Session 隔离只证明应用提交、状态、错误、超时和队列互不污染,不证明 bundled OpenCode 或真实 provider 会并行执行两个模型 turn。任何产品或性能结论都必须保留上游可能串行、限流或拒绝的边界。
- 项目 Agent 与 provider/runtime freshness 必须在 Main 的 bounded acceptance 临界区内验证。新增唯一 id 的 live discovery 只在 owned fresh generation 可接受;同 id 不能证明同内容已经加载,删除后重建及 attached/unknown runtime generation 必须 fail closed。普通 message、command、summarize、Session 模型切换和后台同步不得自动重启、reload 或 dispose 共享 runtime。
- 项目 Agent 与 provider/runtime freshness 必须在 Main 的 bounded acceptance 临界区内验证。新增唯一 id 的 live discovery 只在 owned fresh generation 可接受;同 id 不能证明同内容已经加载,删除后重建及 attached/unknown runtime generation 必须 fail closed。普通 message、command、summarize、Session 模型切换和后台同步不得自动重启、reload 或 dispose 共享 runtime。升级 OpenCode 时必须复核 native Session model wire schema不得把已注入 fresh generation 的 Main 临时 proxy token 误判为上游 provider credential rotation。
- AI 绘画单图选择器仍精确匹配 quick reply `从作品列表选择图片`,用途再从当前 Brief medium 推断;本地化或协议文案变化可能破坏入口。扩展更多素材输入前应升级为结构化 action/purpose。
- Updater 源码错误提示不能代替发布正式签名产物;稳定 feed 缺 manifest 必须保持失败Renderer 不得展示原始堆栈、URL、路径或错误码。升级链只有在旧版本完成发现、下载、重启和安装 smoke 后才可视为生产就绪。
## Last Updated
2026-08-20
2026-08-21

View File

@@ -0,0 +1,99 @@
# Task: Integrate model switch runtime fix into main
## Identity
- Task ID: 20260821-integrate-model-switch-fix-8f2d6c41
- Mode: Integration
- Branch: main
- Worktree: D:\Datas\OthersProjects\makelore
- Base commit: 605fef417fe2e7a5d23d4994fbd109cdfb38efcb
- Owner: codex
- Status: Blocked
## Scope
- Merge verified source commit `cce7722` into local `main` with normal
no-fast-forward history.
- Integrate the OpenCode Session model wire correction and fresh-runtime local
proxy Host API token binding without changing partner Agent hot-add behavior.
- Reconcile the source task's accepted promotion candidate into canonical
current state, architecture/data flow, evidence, and the pending real-client
smoke commitment.
- Preserve the separately owned untracked packaging task record unchanged.
## Intent And Constraints
- Keep page selection and `/models` / `/model` on OpenCode's native per-Session
mutation and prevent false runtime-refresh requirements for an owned fresh
runtime that already uses the current Main-owned proxy token.
- Preserve attached/unknown fail-closed behavior, sticky timeout/partial-write
uncertainty, true provider/model shape refreshes, and existing Agent runtime
acceptance rules.
- Treat the source feature task record as read-only source history. Do not edit
it; keep integration notes in this task record and canonical documents.
- Do not modify or commit
`.project-docs/30-worklog/tasks/20260819-package-learning-off-115-9c4d.md`.
- Merge only into local `main`; do not push, package, or claim a rebuilt-client
smoke that has not been run.
## Outcome
- With explicit user authorization, force-released the blocked legacy
integration ownership `20260820-integrate-remember-password-5d7e3a1c` and
adopted the known dirty `main` worktree without modifying its separately
owned untracked packaging task record.
- Merged source commit `cce7722` into the local `main` integration tree with
normal no-fast-forward history and no code conflicts. The source task record
remains read-only source history and was not added to the canonical tree.
- Integrated the OpenCode 1.18.9 Session model `id` wire mapping, current Host
API token injection for a fresh managed local-proxy runtime, and the
fresh/attached runtime acceptance distinction. Partner Agent hot-add and true
provider/runtime refresh semantics are unchanged.
- Promoted the accepted source candidate into current state, module ownership,
data flow, evidence, and a pending rebuilt-client local smoke commitment.
- Kept the merge local. No push, package, paid-provider request, or claim of a
real rebuilt-client smoke was made.
## Verification
- Integrated focused OpenCode client/provider config/provider route/OpenCode
route suite — 4 files, 171 tests passed.
- `corepack pnpm run typecheck` — passed.
- `corepack pnpm run lint:check` — passed with 0 errors and 6 existing warnings
in unrelated Renderer files.
- The first parallel full-test/build attempt exhausted worker/V8 process
resources: Vitest reported one unexpected worker exit after 176 files / 2047
tests passed, and the concurrent Vite process terminated with a V8 fatal
check. Rerunning the commands independently resolved the execution failure.
- Sequential `corepack pnpm test` — 177 files, 2074 tests passed.
- Sequential `corepack pnpm run build:vite` — passed for
Renderer/Main/Preload/utility; only existing dynamic-import and large-chunk
advisory warnings were reported.
- `node ./node_modules/@playwright/test/cli.js test
tests/e2e/opencode-multichat-runtime.spec.ts` — 1 Electron E2E passed. An
earlier `pnpm exec playwright` attempt selected the separate `playwright` CLI
and failed during collection because the fixture used `@playwright/test`; no
test body ran in that attempt.
- `git diff --check` — passed with configured LF-to-CRLF notices only.
- Task-aware documentation drift remains blocked solely because the explicitly
adopted, pre-existing untracked
`.project-docs/30-worklog/tasks/20260819-package-learning-off-115-9c4d.md`
is another task's owned record. It was not modified, staged, or committed.
The checker also lists the expected nine merged source code/test files as
non-document changes.
## Follow-ups
- Build and relaunch a desktop application containing source `cce7722`, then
repeat page-selector and `/model` switching against the real local OpenCode
runtime without a manual runtime restart. This is recorded in canonical
commitments and remains outside this source-only integration.
- Resolve the project-docs drift-check handling for the explicitly adopted
pre-existing foreign task record before marking this integration complete or
releasing its ownership. Do not delete, edit, move, or commit that record as
part of this integration.
## Promotion Candidates
- Applied the source task's Session model wire and fresh-runtime local-proxy
token candidate; none remain pending for this integration.

View File

@@ -4,6 +4,7 @@ Use this index for searchable, traceable evidence records.
| Date | Topic | Status | Source | Detail |
|---|---|---|---|---|
| 2026-08-21 | AI 编程模型切换真实 OpenCode wire 与 fresh runtime proxy token 修复 | 客户端实现及自动化验证通过;新构建真实本地 smoke 待执行 | 源任务 `20260821-model-switch-runtime-fix-a83d6c91`、源提交 `cce7722`、集成任务 `20260821-integrate-model-switch-fix-8f2d6c41` | 本地 1.1.8 / OpenCode 1.18.9 日志暴露旧请求缺少 `model.id`,且每进程 Host API token 被误判为 provider credential rotation。Main client 现将内部 `modelID` 映射为 wire `id`owned fresh runtime 构建时注入当前 proxy token持久化该已生效值不再要求 restartattached/unknown 与 timeout/partial persistence 仍 fail closed。4 files / 171 focused、177 files / 2074 full unit、typecheck、lint0 errors / 6 existing warnings、production build 与 Electron E2E 1/1 通过。 |
| 2026-08-20 | AI 编程 Session 模型切换与新增伙伴 hot-add | 客户端实现及自动化验证通过;真实 provider 多 Session smoke 仍沿用既有待验收项 | 源任务 `20260820-session-model-agent-hotfix-6e4c9a2f`、源提交 `c0163bc`、集成任务 `20260820-integrate-session-model-hotfix-7b3e91c4` | 页面选择器与 `/models` / `/model` 通过 OpenCode native Session endpoint 切换模型,普通执行不再覆盖选择或要求 runtime restartAgent readiness 改为逐 id新增唯一 id 在 owned fresh generation 被 live registry 发现后可用,同 id 编辑与删后重建仍 fail closed。405 focused、176 files / 2068 full unit、typecheck、lint0 errors / 6 existing warnings、Vite build 与 Electron E2E 1/1 通过。 |
| 2026-08-20 | Learning 项目下载移除大小校验 | 客户端实现与自动化验证通过;真实账号安装包 smoke 待验收 | 源任务 `20260820-remove-download-size-check-4f8a2c1d`、源提交 `8509084`、集成任务 `20260820-integrate-download-size-6e3a91c2` | 聚焦 Learning 2 files / 17 tests、176 files / 2061 full unit、typecheck、lint0 errors / 6 existing warnings及 Renderer/Main/Preload/utility production build 通过。覆盖缺失或错误 `Content-Length``archiveBytes` 超过旧 512 MiB 上限时继续下载SHA-256、ZIP 签名、同源重定向、临时文件清理和原子落盘仍保留。未执行真实账号、安装包或磁盘空间压力 smoke。 |
| 2026-08-19 | AI Design 请求冻结与 mutation 隐式重放修复 | 客户端实现与自动化验证通过;安装包真实账号 smoke 待验收 | 源任务 `20260819-design-freeze-live-6e2c`、源提交 `87e4140`、集成任务 `20260819-integrate-design-freeze-3f8b7c` | 源分支通过 59 focused、2190 full unit、typecheck、lint0 errors / 7 existing warnings、Renderer/Main/Preload production build 与 package verifier集成树另行通过 7 files / 134 focused、184 files / 2190 full、typecheck、同样的 lint 结果和 Renderer/Main/Preload/utility production build。覆盖 30 秒完整 Workspace JSON deadline、忽略 abort 时确定性 reject、shared refresh coalescing/释放/后续重试,以及只对 `GET`/`HEAD`/`OPTIONS` 保留 Electron→Node fallbackPATCH/POST transport failure 不再隐式二次发送。原生密码/SMS 登录与临时 `disable-http2` 诊断 bootstrap 同包保留;不据此宣称现场冻结已消失或 HTTP/2 是根因。 |

View File

@@ -4,6 +4,7 @@ Track future-facing memory: promised follow-ups, unfinished loops, timed checks,
| Date | Commitment | Trigger / Due | Owner | Status | Next Action |
|---|---|---|---|---|---|
| 2026-08-21 | 验收修复后的真实本地 OpenCode Session 模型切换 | 构建并启动包含源 `cce7722` 的 Makelore 后 | 客户端/OpenCode 集成 | Pending | 重启桌面应用以加载新 Main 代码,在无需手动重启 runtime 的前提下分别从页面选择器和 `/model` 切换模型并发送后续 prompt核对运行日志不再出现 `Missing key at ["model"]["id"]` 或因当前进程 Host API token 持久化产生的 false runtime-refresh pending。attached/unknown runtime 的真实 token mismatch 仍应 fail closed。 |
| 2026-08-19 | 验收 AI Design 有限请求生命周期并决定正式 HTTP/2 策略 | 发布包含源 `87e4140` 的正式安装包前 | 客户端/Works 服务端/发布集成 | Pending | 使用真实账号连续执行 Quote PATCH、重试报价与继续调整制造或等待慢响应确认 30 秒内稳定返回、其他请求不被共同挂住、服务端无隐式重复 mutation随后恢复 HTTP/2 重新打包做同场景对照。只有证据确认后才能移除临时 `disable-http2` 或把协议策略固化为正式决定。 |
| 2026-08-17 | 验收 AI 编程真实双 Session runtime/provider 行为与同 id Agent 内容刷新边界 | 升级 bundled OpenCode、改变 provider 并发策略,或对外宣称双会话真实并发/即时应用同 id Agent 编辑前 | 客户端/OpenCode/provider 集成 | Pending | 使用明确配置的测试 provider 运行 opt-in bundled OpenCode 双 Session smoke区分应用隔离与上游并发/串行/限流。owned fresh generation 已允许 live registry 新发现的唯一 id hot-add若要求运行期即时应用同 id 编辑或删除后重建,仍须先提供 directory-scoped authoritative invalidation 或 whole-instance quiescence oracle不从 `/session/status` 推断 dispose/reload 安全。 |
| 2026-08-17 | 验收用户级四模块入口策略的真实发布链 | 发布包含源 tip `3b799af` 的 Makelore 安装包前 | Works 服务端/客户端/发布集成 | Pending | 部署 `module_access` migration 和 `/api/auth/me` API重新打包并安装 Makelore使用真实账号依次关闭 Code、Canvas、Learning、Robot重启后验证卡片置灰/不可点、根/深层/别名路由阻断、`/settings` 仍可访问和 terminal `401` 退出;另行以直接 API 请求确认服务端授权,不以 UI 置灰代替 |

View File

@@ -511,6 +511,7 @@ function expectedUserModelProxyConfig(
async function rebindLocalProxyHostApiTokenIfNeeded(
expectedBaseUrl: string,
markPending: () => void,
runtimeAlreadyUsesCurrentHostApiToken: boolean,
signal?: AbortSignal,
): Promise<boolean> {
const providerService = getProviderService();
@@ -532,14 +533,31 @@ async function rebindLocalProxyHostApiTokenIfNeeded(
return false;
}
markPending();
await providerService.updateAccount(
NIANCODE_USER_MODEL_ACCOUNT_ID,
account,
currentHostApiToken,
);
signal?.throwIfAborted();
return true;
if (!runtimeAlreadyUsesCurrentHostApiToken) {
markPending();
}
const markPendingOnAbort = runtimeAlreadyUsesCurrentHostApiToken
? () => markPending()
: undefined;
if (markPendingOnAbort) {
signal?.addEventListener('abort', markPendingOnAbort, { once: true });
}
try {
await providerService.updateAccount(
NIANCODE_USER_MODEL_ACCOUNT_ID,
account,
currentHostApiToken,
);
signal?.throwIfAborted();
} catch (error) {
markPending();
throw error;
} finally {
if (markPendingOnAbort) {
signal?.removeEventListener('abort', markPendingOnAbort);
}
}
return !runtimeAlreadyUsesCurrentHostApiToken;
}
async function runtimeUsesExpectedUserModelProxy(
@@ -607,7 +625,12 @@ async function ensureRuntimeUserModelProxyConfig(
try {
if (
expectedConfig.usesLocalProxy
&& await rebindLocalProxyHostApiTokenIfNeeded(expectedConfig.baseUrl, markPending, signal)
&& await rebindLocalProxyHostApiTokenIfNeeded(
expectedConfig.baseUrl,
markPending,
ctx.opencodeManager.getRuntimeGenerationProvenance() === 'fresh',
signal,
)
) {
if (!allowRestart) {
markPending();

View File

@@ -361,10 +361,13 @@ export async function importCurrentUserModelConfig(
createdAt: existing?.createdAt ?? now,
updatedAt: now,
};
const shouldRestartRuntime = importedProviderRuntimeShapeChanged(existing, account)
|| await importedProviderApiKeyChanged(providerService, existing, accountApiKey);
signal.throwIfAborted();
const runtimeIsActive = ctx.opencodeManager.getStatus().state !== 'stopped';
const runtimeAlreadyUsesLocalProxyApiKey = useLocalAiProxy
&& ctx.opencodeManager.getRuntimeGenerationProvenance?.() === 'fresh';
const shouldRestartRuntime = importedProviderRuntimeShapeChanged(existing, account)
|| (!runtimeAlreadyUsesLocalProxyApiKey
&& await importedProviderApiKeyChanged(providerService, existing, accountApiKey));
signal.throwIfAborted();
const refreshAlreadyPending = lease.isRefreshPending();
const runtimeRefreshRequired = runtimeIsActive
&& (shouldRestartRuntime || refreshAlreadyPending);

View File

@@ -108,6 +108,7 @@ const useSecureWorksSquareSessionPersistence = shouldUseSecureWorksSquareSession
async function buildMakeloreOpencodeRuntimeConfig() {
return await buildOpencodeRuntimeConfigFromNianCodeProviders({
localProxyApiKey: getHostApiToken() || undefined,
mcpServers: {
[PLAYWRIGHT_MCP_SERVER_ID]: resolvePlaywrightMcpServer(),
},

View File

@@ -278,7 +278,12 @@ export function createOpencodeClient(options: OpencodeClientOptions) {
): Promise<void> =>
request<void>(`/api/session/${encodeURIComponent(sessionID)}/model`, {
method: 'POST',
body: JSON.stringify({ model }),
body: JSON.stringify({
model: {
providerID: model.providerID,
id: model.modelID,
},
}),
signal: options?.signal,
}),
executeSessionCommand: (

View File

@@ -23,6 +23,7 @@ const OPENAI_COMPATIBLE_PACKAGE = '@ai-sdk/openai-compatible';
const OPENAI_PACKAGE = '@ai-sdk/openai';
const ANTHROPIC_PACKAGE = '@ai-sdk/anthropic';
const WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE = 'works_square_ai_gateway';
const WORKS_SQUARE_AI_GATEWAY_PROXY_CREDENTIAL_MODE = 'works_square_ai_gateway_proxy';
const OPENCODE_BUILTIN_PROVIDER_IDS = new Set([
'anthropic',
@@ -120,6 +121,7 @@ export interface BuildOpencodeRuntimeConfigOptions {
accounts: ProviderAccount[];
defaultAccountId?: string | null;
resolveApiKey: ResolveOpencodeProviderApiKey;
localProxyApiKey?: string;
mcpServers?: Record<string, OpencodeMcpServerEntry>;
}
@@ -303,7 +305,11 @@ export async function buildOpencodeRuntimeConfig(
continue;
}
const apiKey = await options.resolveApiKey(account, opencodeProviderId);
const localProxyApiKey = account.id === NIANCODE_USER_MODEL_ACCOUNT_ID
&& account.metadata?.worksSquareCredentialMode === WORKS_SQUARE_AI_GATEWAY_PROXY_CREDENTIAL_MODE
? options.localProxyApiKey
: undefined;
const apiKey = localProxyApiKey ?? await options.resolveApiKey(account, opencodeProviderId);
if (!apiKey && accountNeedsApiKey(account)) {
continue;
}
@@ -367,6 +373,7 @@ async function resolveNianCodeApiKey(
}
export interface BuildOpencodeRuntimeConfigFromNianCodeProvidersOptions {
localProxyApiKey?: string;
mcpServers?: Record<string, OpencodeMcpServerEntry>;
}
@@ -383,6 +390,7 @@ export async function buildOpencodeRuntimeConfigFromNianCodeProviders(
accounts,
defaultAccountId,
resolveApiKey: resolveNianCodeApiKey,
localProxyApiKey: options.localProxyApiKey,
mcpServers: options.mcpServers,
});
}

View File

@@ -190,7 +190,7 @@ describe('opencode client', () => {
body: JSON.stringify({
model: {
providerID: 'niancode-user-models',
modelID: 'qwen3.7-plus',
id: 'qwen3.7-plus',
},
}),
signal: controller.signal,

View File

@@ -341,6 +341,7 @@ describe('buildOpencodeRuntimeConfig', () => {
});
it('routes imported Works Square models through the local AI proxy without gateway-token headers', async () => {
const resolveApiKey = vi.fn().mockResolvedValue('stale-host-api-token');
const result = await buildOpencodeRuntimeConfig({
accounts: [
createAccount({
@@ -358,7 +359,8 @@ describe('buildOpencodeRuntimeConfig', () => {
}),
],
defaultAccountId: 'niancode-user-models',
resolveApiKey: vi.fn().mockResolvedValue('local-host-api-token'),
resolveApiKey,
localProxyApiKey: 'current-host-api-token',
});
expect(result.config.provider['niancode-user-models'].options).toEqual({
@@ -368,8 +370,9 @@ describe('buildOpencodeRuntimeConfig', () => {
expect(JSON.stringify(result.config)).not.toContain('X-Works-Square-AI-Token');
expect(JSON.stringify(result.config)).not.toContain('https://open-api.example.com');
expect(result.env).toEqual({
NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY: 'local-host-api-token',
NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY: 'current-host-api-token',
});
expect(resolveApiKey).not.toHaveBeenCalled();
});
it('strips DeepSeek routing prefixes from imported Works Square runtime models', async () => {

View File

@@ -3588,6 +3588,7 @@ description: Browser debugging.
{
opencodeManager: {
getStatus: () => runtimeStatus,
getRuntimeGenerationProvenance: () => 'fresh',
restart,
},
opencodeProjectStore: {
@@ -4029,13 +4030,15 @@ description: Browser debugging.
updatedAt: '2026-07-06T00:00:00.000Z',
};
providerServiceMock.getAccount.mockResolvedValue(existingAccount);
providerServiceMock.getAccountApiKey.mockResolvedValue('old-host-api-token');
let storedApiKey = 'old-host-api-token';
providerServiceMock.getAccountApiKey.mockImplementation(async () => storedApiKey);
let releaseRebind!: () => void;
const rebindGate = new Promise<void>((resolve) => {
releaseRebind = resolve;
});
providerServiceMock.updateAccount.mockImplementationOnce(async () => {
providerServiceMock.updateAccount.mockImplementationOnce(async (_accountId, _account, apiKey) => {
await rebindGate;
storedApiKey = apiKey;
return existingAccount;
});
buildConfigSummaryMock.mockResolvedValue({
@@ -4109,18 +4112,13 @@ description: Browser debugging.
'current-host-api-token',
);
expect(restart).not.toHaveBeenCalled();
expect(promptSessionAsync).not.toHaveBeenCalled();
expect(response.statusCode).toBe(409);
expect(response.json()).toMatchObject({
code: 'OPENCODE_RUNTIME_CONFIG_PENDING',
promptSent: false,
});
expect(sameGenerationResponse.statusCode).toBe(409);
expect(promptSessionAsync).not.toHaveBeenCalled();
providerServiceMock.getAccountApiKey.mockResolvedValue('current-host-api-token');
expect(promptSessionAsync).toHaveBeenCalledTimes(2);
expect(response.statusCode).toBe(202);
expect(sameGenerationResponse.statusCode).toBe(202);
runtimeGeneration = 2;
provenance = 'attached';
storedApiKey = 'old-host-api-token';
const attachedResponse = createResponse();
await handleOpencodeRoutes(
createRequest('POST', { text: 'Attached is not authoritative' }),
@@ -4129,10 +4127,11 @@ description: Browser debugging.
context,
);
expect(attachedResponse.statusCode).toBe(409);
expect(promptSessionAsync).not.toHaveBeenCalled();
expect(promptSessionAsync).toHaveBeenCalledTimes(2);
runtimeGeneration = 3;
provenance = 'fresh';
storedApiKey = 'current-host-api-token';
const freshResponse = createResponse();
await handleOpencodeRoutes(
createRequest('POST', { text: 'Fresh generation' }),
@@ -4141,7 +4140,7 @@ description: Browser debugging.
context,
);
expect(freshResponse.statusCode).toBe(202);
expect(promptSessionAsync).toHaveBeenCalledOnce();
expect(promptSessionAsync).toHaveBeenCalledTimes(3);
});
it('fails closed when rebinding the local proxy Host API token is rejected', async () => {

View File

@@ -412,68 +412,76 @@ describe('provider host api routes', () => {
);
});
it('restarts a running runtime when the local proxy Host API token changed', async () => {
const existingAccount = createProviderAccount({
id: 'niancode-user-models',
vendorId: 'custom',
label: 'Makelore Models',
authMode: 'api_key',
baseUrl: 'http://127.0.0.1:13210/api/ai-proxy/v1',
apiProtocol: 'openai-completions',
model: 'gpt-4.1-mini',
fallbackModels: ['gpt-4o-mini'],
enabled: true,
isDefault: true,
metadata: {
customModels: ['gpt-4.1-mini', 'gpt-4o-mini'],
worksSquareCredentialMode: 'works_square_ai_gateway_proxy',
worksSquareOneApiBaseUrl: 'https://one-api.example.com/v1',
},
});
providerServiceMock.getAccount.mockResolvedValueOnce(existingAccount);
providerServiceMock.getAccountApiKey.mockResolvedValueOnce('old-host-api-token');
getHostApiTokenMock.mockReturnValueOnce('new-host-api-token');
providerServiceMock.updateAccount.mockResolvedValueOnce(existingAccount);
const fetchMock = vi.fn().mockResolvedValueOnce(
new Response(JSON.stringify({
provider_type: 'openai-compatible',
it.each([
['fresh', false],
['attached', true],
] as const)(
'%s runtime handles a changed local proxy Host API token without losing freshness guarantees',
async (provenance, runtimeRefreshRequired) => {
const existingAccount = createProviderAccount({
id: 'niancode-user-models',
vendorId: 'custom',
label: 'Makelore Models',
base_url: 'https://one-api.example.com/v1',
api_key: 'fresh-ws-ai-token',
credential_mode: 'works_square_ai_gateway',
api_key_expires_in: 3600,
models: ['gpt-4.1-mini', 'gpt-4o-mini'],
}), { status: 200 }),
);
vi.stubGlobal('fetch', fetchMock);
const response = createResponse();
const restart = vi.fn(async () => ({ state: 'running', port: 4096, pid: 4242 }));
await handleProviderRoutes(
createRequest('POST', { accessToken: 'access-token' }),
response.res,
new URL('http://127.0.0.1/api/provider-accounts/import-user-model-config'),
{
opencodeManager: {
getStatus: () => ({ state: 'running', port: 4096 }),
restart,
},
} as never,
);
expect(response.statusCode).toBe(200);
expect(providerServiceMock.updateAccount).toHaveBeenCalledWith(
'niancode-user-models',
expect.objectContaining({
authMode: 'api_key',
baseUrl: 'http://127.0.0.1:13210/api/ai-proxy/v1',
metadata: expect.objectContaining({
apiProtocol: 'openai-completions',
model: 'gpt-4.1-mini',
fallbackModels: ['gpt-4o-mini'],
enabled: true,
isDefault: true,
metadata: {
customModels: ['gpt-4.1-mini', 'gpt-4o-mini'],
worksSquareCredentialMode: 'works_square_ai_gateway_proxy',
worksSquareOneApiBaseUrl: 'https://one-api.example.com/v1',
},
});
providerServiceMock.getAccount.mockResolvedValueOnce(existingAccount);
providerServiceMock.getAccountApiKey.mockResolvedValueOnce('old-host-api-token');
getHostApiTokenMock.mockReturnValueOnce('new-host-api-token');
providerServiceMock.updateAccount.mockResolvedValueOnce(existingAccount);
const fetchMock = vi.fn().mockResolvedValueOnce(
new Response(JSON.stringify({
provider_type: 'openai-compatible',
label: 'Makelore Models',
base_url: 'https://one-api.example.com/v1',
api_key: 'fresh-ws-ai-token',
credential_mode: 'works_square_ai_gateway',
api_key_expires_in: 3600,
models: ['gpt-4.1-mini', 'gpt-4o-mini'],
}), { status: 200 }),
);
vi.stubGlobal('fetch', fetchMock);
const response = createResponse();
const restart = vi.fn(async () => ({ state: 'running', port: 4096, pid: 4242 }));
await handleProviderRoutes(
createRequest('POST', { accessToken: 'access-token' }),
response.res,
new URL('http://127.0.0.1/api/provider-accounts/import-user-model-config'),
{
opencodeManager: {
getStatus: () => ({ state: 'running', port: 4096 }),
getRuntimeGenerationProvenance: () => provenance,
restart,
},
} as never,
);
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({ runtimeRefreshRequired });
expect(providerServiceMock.updateAccount).toHaveBeenCalledWith(
'niancode-user-models',
expect.objectContaining({
baseUrl: 'http://127.0.0.1:13210/api/ai-proxy/v1',
metadata: expect.objectContaining({
worksSquareCredentialMode: 'works_square_ai_gateway_proxy',
}),
}),
}),
'new-host-api-token',
);
expect(restart).toHaveBeenCalledOnce();
});
'new-host-api-token',
);
expect(restart).toHaveBeenCalledTimes(runtimeRefreshRequired ? 1 : 0);
},
);
it('defers a direct API-key rotation without restarting and reports the runtime refresh requirement', async () => {
const existingAccount = createProviderAccount({