feat(goose): add local v1.49 canary routing, smoke gates, and migration docs
Wire Portal and TKMind proxy to loopback Goose v1.49 via canary env blocks, with verification scripts, Phase 2/3 evidence baselines, and rollback runbooks so local upgrade stays isolated from stable 1.41 and production. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# TKMind Goose v1.49 迁移决策表
|
||||
|
||||
## 目标与边界
|
||||
|
||||
本次升级的目标是“升级运行时基线 + 选择性吸收 upstream”,不是把 TKMind 变成 vanilla Goose。
|
||||
|
||||
- Layer 0:PG Session、多实例 affinity、9 实例拓扑——保留。
|
||||
- Layer 1:goosed 运行时、安全修复、agent loop——跟随 upstream v1.49。
|
||||
- Layer 2:通用能力的 platform extensions——优先改为薄封装,逐项退役重复实现。
|
||||
- Layer 3:`tkmind_compat`、harness、sandbox-fs MCP——保留,确保 Portal/H5 契约与业务语义不变。
|
||||
- Layer 4:Portal、微信、计费、MindSpace、Page Data——保留,属于产品层。
|
||||
|
||||
迁移完成的判定不是“upstream 代码已合入”,而是:新旧运行时在受保护业务路径上行为等价,且每个可退役定制都有 canary 证据与可执行回滚。
|
||||
|
||||
## Memory 无缝迁移硬闸门
|
||||
|
||||
Memory 不能作为普通 extension 随运行时升级一起替换。这里必须分别保护四类状态,不能只比较“能否召回”这一项:
|
||||
|
||||
| 状态层 | 当前事实/责任 | v1.49 原则 |
|
||||
|---|---|---|
|
||||
| Goose Session | PG 共享 session、`agent_session_id`、SSE/Finish 状态 | 只适配 runtime API;不得让 upstream 本地 SQLite 成为隐式第二份真相。 |
|
||||
| 业务记忆 | MySQL `h5_conversation_messages`、`h5_user_memory_items`、`h5_episodic_memory_items` | 保持原表、原 ID、原 user/session/evidence 关联与原写入语义;upstream 不得接管 owner。 |
|
||||
| V2 候选/生命周期 | `h5_memory_v2_candidates` 及 promotion/expire/forget | 继续由 Portal policy 控制;`off` 必须零写入,所有操作按 user scope 严格隔离。 |
|
||||
| 语义索引/项目记忆 | `memory_embeddings`、可选向量后端、harness bootstrap/remember | 只能是可重建派生数据或受控项目记忆;必须保留来源 ID、embedding 版本和回填水位,不能反向覆盖业务记忆。 |
|
||||
|
||||
以下条件未全部满足时,Memory owner 必须保持现有 `conversation-memory` / Portal 链路,v1.49 只能旁路 shadow 观察:
|
||||
|
||||
- [ ] 同一 `userId` 能在旧运行时、新运行时、Portal 和所有 Memory adapter 中稳定映射;不能用 display name、邮箱或临时 session ID 代替。
|
||||
- [ ] `agent_session_id`、`source_session_id`、`evidence_message_id`、memory `id`、`memory_hash` 的映射已导出并逐项对账;不能因 session ID 重建造成重复记忆或丢 evidence。
|
||||
- [ ] 迁移期间只有一个业务记忆写入 owner。禁止旧链路和 upstream 同时写入同一事实,避免重复提取、hash 冲突和不可逆覆盖。
|
||||
- [ ] 新 runtime 先双读/对照旧结果,再考虑切换 resolve;`write`、`compact`、promotion、expire、forget 仍走旧 owner,直到对账稳定。
|
||||
- [ ] 已建立按用户的 count、ID 集合、hash、状态、更新时间、来源 session 和 evidence 外键对账;总数相同不能替代逐用户逐 ID 对账。
|
||||
- [ ] 已覆盖增量追平:固定 cutover watermark,回填期间捕获 watermark 之后的新写入;不能依赖从 `updated_at=0` 开始的有限批次。
|
||||
- [ ] 向量索引记录 embedding provider/model/version/dimensions、文本规范化版本、来源 memory ID 和写入时间;维度或模型变化必须新建 namespace/table 或完整重建,禁止混索引。
|
||||
- [ ] 向量不可用、索引落后、embedding 失败、upstream 超时或结果不一致时,必须 fail-open 回到 legacy exact/recent recall;不得阻塞聊天或删除原记忆。
|
||||
- [ ] `remember-recent` 使用原始用户消息(优先 `h5_agent_runs.user_message_json`),不得把注入的 `[Memory Context]` 再提取成新记忆;空数组必须表示“无需保存”。
|
||||
- [ ] 回滚只切回 resolve/write/compact owner 与 feature flag,不删除候选表、向量表或原始记忆;回滚后仍能从原始表重建派生索引。
|
||||
|
||||
### Memory 数据不对接上的常见盲点
|
||||
|
||||
1. **只迁 goosed session,不迁业务记忆关联**:session 恢复成功不代表 `userId → source_session_id → evidence_message_id` 仍然成立。
|
||||
2. **把向量库当主库**:向量结果是派生索引,不能替代 MySQL 事实、状态和 forget/expire 语义。
|
||||
3. **双写但没有幂等键**:必须以稳定的业务 memory ID/hash + user namespace 幂等 upsert;不能用每次 embedding 的随机 ID。
|
||||
4. **只比总数不比内容**:同数但 user 串线、status 丢失、evidence 外键断裂或更新时间截断,都会表现为“数据在但接不上”。
|
||||
5. **忽略提取输入污染**:新 runtime 注入的 Memory Context 可能被再次保存,造成旧记忆复制和权重膨胀。
|
||||
6. **把 schema/DDL 当启动副作用**:本地可以显式建空表并验证;生产 schema、备份、回填和 owner 切换必须是独立审批步骤,本次升级禁止触碰 103。
|
||||
|
||||
## 本地优先与生产隔离规则
|
||||
|
||||
本次迁移只能在本地 loopback、隔离数据库和临时 workspace 开始。未获得新的明确授权前,禁止:
|
||||
|
||||
- 连接 103、105 或任何生产数据库、生产 goosed、生产 MindSpace 服务;
|
||||
- 执行生产发布、runtime/artifact 生成、远端迁移、远端 backfill、远端 schema DDL 或生产重启;
|
||||
- 使用生产 `DATABASE_URL`、`GOOSE_SESSION_DB_URL`、Memory provider key 或生产数据做测试;
|
||||
- 将 upstream Memory backend 接入 Portal 主路径,或修改 production profile 的 Memory 开关。
|
||||
|
||||
本地测试必须使用显式 loopback 地址和隔离数据库名,例如 `127.0.0.1` 的 MySQL/PG;测试启动前应打印并检查目标 host、database、runtime profile,命中 `103`、`105`、公网数据库或 production profile 时直接失败。优先使用脱敏 fixture 或本地数据副本,测试结束删除临时用户/会话/索引,但保留可复核的对账报告。
|
||||
|
||||
## 决策规则
|
||||
|
||||
| 判断问题 | Replace:upstream 替代 | Keep:保留定制 | Thin wrapper:薄封装 upstream |
|
||||
|---|---|---|---|
|
||||
| 是否 TKMind 独有? | 否 | 是 | 部分独有 |
|
||||
| upstream 是否明显更成熟? | 是 | 否或不对等 | 是,但缺 TKMind 钩子 |
|
||||
| 替换后 H5/微信契约是否不变? | 是 | 不适用 | 需要 adapter 层 |
|
||||
| 是否绑定 MindSpace、计费或权限? | 否 | 是 | 部分绑定 |
|
||||
|
||||
只有“非独有 + upstream 更强 + 契约可不变”同时成立时,才直接 Replace。任何涉及用户空间、发布、计费、权限、微信 ACK 或 PG Session 的能力,默认先 Keep;若能复用 upstream 内核,采用 Thin wrapper。
|
||||
|
||||
## 定制项决策表
|
||||
|
||||
状态含义:`Candidate` 表示建议方向,尚未凭 canary 取得退役资格;`Keep` 表示本次不以 upstream 替代;`Wrapper` 表示保留入口与契约,内部逐步换内核。
|
||||
|
||||
| 定制项 | 决策 | v1.49 迁移动作 | 退役/切换门槛 | 回滚点 |
|
||||
|---|---|---|---|---|
|
||||
| `platform_extensions/web.rs`(DuckDuckGo + fetch) | Wrapper → Replace | 保留现有 extension 名与 H5 调用入口;内部优先转发 upstream `web-search` / `browser-use` skill。 | 搜索、抓取、超时、fallback、权限与结果格式回归通过;无 TKMind 专属域规则回归。 | 切回旧 extension 实现。 |
|
||||
| `session-reply-wait` 与 Portal 侧 DeepSeek workaround | Wrapper / Keep(分阶段) | 先在 v1.49 保留等待、Finish、重试与游标适配;对 upstream reasoning 修复做独立 canary。 | 工具调用后的 `reasoning_content`、SSE Finish、断线续播、重复消息与超时回归通过。 | 保留 `session-reply-wait` 与旧兼容链路。 |
|
||||
| DeepSeek no-think 代理(`:18036`) | Wrapper → Pass-through | 先保留独立代理与端口;验证 v1.49 原生兼容后,再将代理改为 pass-through,最后才评估停用。 | 生产同构工具轮次通过,候选失效时可自动回落稳定版本;不得仅凭版本号拆除。 | 恢复旧 `:18036` no-think 处理。 |
|
||||
| `billing-token-state` 粗估价 | Candidate Replace / Simplify | 优先读取单条消息 usage/cost 与 cache token;`accumulated_cost` 稳定时,Portal 仅保留缺失字段时的保守估算。 | Finish 稳定带完整累计成本;账单、margin、补偿与幂等回归通过;不能出现超扣。 | 恢复 Portal 估算逻辑。 |
|
||||
| 部分 message sanitize 补丁 | Candidate Replace | 对比 v1.49 provider message 处理与本地补丁 diff;只删除已被 upstream 覆盖且契约等价的分支。 | DeepSeek、多模态隔离、工具轮次、历史消息显示回归通过。 | 恢复对应 sanitize 分支。 |
|
||||
| PG Session 多实例共享 | Keep | 移植到 v1.49 session/runtime 接口;保持 PG、9 实例 affinity 与共享 session 语义。 | 多实例并发、重启恢复、session 一致性与故障切换通过。 | 保留旧 PG adapter / 配置。 |
|
||||
| `tkmind_compat`(`/sessions` 等 REST adapter) | Keep | 仅适配 upstream v1.49 内部 API;对外 REST/H5 契约不变。 | Portal、H5、微信客户端不需要改请求格式;旧客户端回归通过。 | 切回旧 goosed adapter。 |
|
||||
| `tkmind_memory`(harness bootstrap/remember) | Keep / Wrapper | 保留项目记忆与 capability 门控;若 upstream 有 projects/memory 原语,只在内部复用,不改变 TKMind 入口。 | bootstrap、remember、权限边界与项目级记忆回归通过。 | 使用现有 harness 实现。 |
|
||||
| sandbox-fs MCP(MindSpace、Page Data、`generate_image`) | Keep | 按 v1.49 extension/MCP 生命周期接口移植;保持路径隔离、页面落盘、缩略图 fallback 与图片策略。 | 发布/改页、Page Data、图片生成、私有路径隔离、Transport closed 重试回归通过。 | 保留现有 sandbox-fs 子进程与 Portal fallback。 |
|
||||
| aider / openhands platform extensions | Keep | 保持 `coding_router`、微信与 agent-run 策略;只适配运行时加载接口。 | coding 路由、模型名映射、权限与失败回退不变。 | 保留旧 extension。 |
|
||||
| `custom_tkmind_relay_deepseek` | Keep | 保留计费、margin、Admin 模型中心绑定;若 upstream provider 足够,只替换内部传输实现。 | provider/key/model 目录、计费归属、margin 与 canary 策略回归通过。 | 切回 custom relay。 |
|
||||
| `capabilities.mjs` + extension policy | Keep / Wrapper | 保留用户权限、canary、微信策略;可将通用 extension 解析委托给 upstream。 | capability 不越权,`sandbox-fs` 与 `read_image` 等关键策略不回归。 | 使用现有 policy builder。 |
|
||||
|
||||
## 明确不替换的产品契约
|
||||
|
||||
以下能力不能因为 upstream 1.49 提供了相似原语就直接删除:
|
||||
|
||||
- MindSpace 页面发布、`edit_file` 落盘、Finish 合并与 Page Data 交付。
|
||||
- `sandbox-fs` 的 workspace 路径隔离、图片生成、缩略图 fallback 与 MCP 失败兜底。
|
||||
- 微信 5 秒 ACK、消息路由、页面链接验证与定时任务投递。
|
||||
- 计费、usage/cost、margin、provider/model 权限与 canary 策略。
|
||||
- PG Session 共享、9 实例 affinity、Portal/H5 `/sessions` 等兼容 REST。
|
||||
- SSE 断线续播、Portal replay ID 与 Goose `Last-Event-ID` 的映射。
|
||||
- 私有 MindSpace 页 noindex、confirmed public 才可收录的 SEO/GEO 策略。
|
||||
|
||||
## 分阶段迁移清单
|
||||
|
||||
### Phase 0:冻结基线
|
||||
|
||||
- [x] 记录当前 1.41/现生产候选的 commit、runtime artifact、配置与端口。(见 [docs/goose-v149-local-phase0-manifest.md](docs/goose-v149-local-phase0-manifest.md))
|
||||
- [x] 拉取 upstream `v1.49.0`;创建 `upgrade/v1.49-tkmind` worktree(`/Users/john/Project/tkmind_go-v149`)。
|
||||
- [x] 本地隔离 PG 库 `goose_sessions_v149_dev`;启动脚本 `scripts/run-goosed-v149-local.sh`(端口 `18049`)。
|
||||
- [x] 建立旧版与 v1.49 的 canary 开关,以及一键回落路径(`scripts/switch-goose-v149-canary.sh` + [docs/goose-v149-canary-runbook.md](docs/goose-v149-canary-runbook.md))。
|
||||
- [x] 保存关键业务场景的输入、SSE 事件、Finish 状态、usage/cost 与产物校验结果(`node scripts/capture-goose-v149-baseline.mjs` + `node scripts/check-goosed-v149-all.mjs`)。
|
||||
- [x] 导出本地基线 Memory manifest:每用户 memory ID/hash/status/source session/evidence、候选状态、向量 namespace/version(`node scripts/export-goose-v149-memory-manifest.mjs`)。
|
||||
- [x] 记录 schema 版本、索引版本、回填 watermark、时区和字符规范化规则(见 manifest `schemaVersion` / `cutoverWatermarkMs` / `textNormalization` 字段)。
|
||||
|
||||
### Phase 1:运行时升级
|
||||
|
||||
- [x] 将 upstream v1.49 安全修复、agent loop 与 provider 变更移植到 Layer 1(基线已在 v1.49.0 tag)。
|
||||
- [x] 适配 `tkmind_compat`、PG Session、harness 的加载/生命周期接口(P0 路由 + PG 双后端)。
|
||||
- [x] 适配 sandbox-fs 的加载/生命周期接口(`extension_overrides` + `/agent/tools` 验证)。
|
||||
- [x] 自定义 provider 路由(`/config/custom-providers`、`/config/upsert`)+ `custom_tkmind_relay_deepseek` 本地 reply Finish smoke。
|
||||
- [x] aider / openhands platform extensions + `deploy/coding_router.sh`(`check-goosed-v149-executors.mjs`)。
|
||||
- [x] v1.49 初始配置强制 `MEMORY_BACKEND=legacy`、`MEMORY_VECTOR_ENABLED=0`、lifecycle/promotion/injection 为 off/shadow(`run-goosed-v149-local.sh` + `check-goosed-v149-memory-policy.mjs`)。
|
||||
- [x] 先验证旧 owner 在 v1.49 上完成“写入 → compact → 新 session resolve”的本地闭环(`check-goosed-v149-memory-loop.mjs`)。
|
||||
- [x] 保持 Layer 3–4 对外接口不变(Portal canary 联调:`check-goosed-v149-canary-portal.mjs` ✅;主 Portal canary + `check-goosed-v149-portal-smoke.mjs` ✅;`POST /config/read` 已移植并纳入 smoke)。
|
||||
|
||||
### Phase 2:逐项 canary
|
||||
|
||||
- [x] web/search:TKMind `platform/web` 已移植至 v1.49(`check-goosed-v149-web-smoke.mjs`);upstream `web-search` skill 对照仍保留。
|
||||
- [x] DeepSeek:工具调用 Finish(`check-goosed-v149-deepseek-tools.mjs`);reasoning 保留单测(`check-goosed-v149-thinking-preservation.mjs` ✅);`:18036` no-think 代理与 Portal replay 仍保留。
|
||||
- [x] upstream web-search:`load_skill` 路径 smoke(`check-goosed-v149-skills-smoke.mjs`);TKMind `platform/web` 仍为主 H5 契约。
|
||||
- [x] cost:`usage`、cache token、`accumulated_cost`、估算回退与账单金额对照(`check-goosed-v149-cost-smoke.mjs` → `docs/baselines/goose-v149-cost-evidence-*.json`)。
|
||||
- [x] message sanitize:provider 层单测 v1.41≈v1.49(`compare-goose-v149-message-sanitize.mjs` ✅);Portal 多模态/历史展示路径仍保留对照。
|
||||
- [x] Memory 对账:逐用户逐 ID/hash/status/source/evidence 对照(`compare-goose-v149-memory-manifest.mjs` ✅)。
|
||||
- [x] Memory 故障演练:legacy owner 超时 fail-open 不中断(`check-goosed-v149-memory-failopen.mjs`);向量→legacy 自动回退待 pgvector canary 时再验。
|
||||
- [x] 每项记录“通过 / 保留 / 回滚”,不得一次性批量删除(台账:[docs/goose-v149-phase2-evidence.md](docs/goose-v149-phase2-evidence.md))。
|
||||
|
||||
**Phase 2 聚合入口:** `node scripts/run-goosed-v149-phase2.mjs`
|
||||
|
||||
**Phase 3 收口入口:** `node scripts/run-goosed-v149-phase3.mjs` · 清单:[docs/goose-v149-phase3-closeout.md](docs/goose-v149-phase3-closeout.md)
|
||||
|
||||
### Phase 3:退役与收口
|
||||
|
||||
- [ ] 只删除已取得 canary 证据的重复逻辑(当前:**无删除项**;见 Phase 3 清单)。
|
||||
- [x] 保留薄封装入口、feature flag 与回滚说明(canary runbook + `switch-goose-v149-canary.sh`)。
|
||||
- [x] 更新 runtime 文档、证据链接与 Phase 3 合并闸门(`docs/goose-v149-phase3-closeout.md`、`docs/baselines/README.md`)。
|
||||
- [x] 本地连续验证 + Memory manifest 无 drift(`run-goosed-v149-phase3.mjs`);**不授权** 103 操作。
|
||||
- [ ] 合并前运行影响域 verify;完成测试后再讨论 commit、合并或发布(须用户明确批准)。
|
||||
|
||||
## 必须通过的回归守卫
|
||||
|
||||
涉及受保护路径时,至少执行仓库约定的:
|
||||
|
||||
```bash
|
||||
npm run verify:mindspace-publish-guards
|
||||
npm run verify:mindspace-publish-guards:full
|
||||
npm run verify:mindspace-page-sync-guards
|
||||
npm run verify:h5-session-patches
|
||||
npm run verify:seo-geo
|
||||
npm run verify:seo-discovery
|
||||
npm run verify:page-data
|
||||
```
|
||||
|
||||
DeepSeek 兼容链路还必须覆盖 `release-gate/deepseek-production-parity.test.mjs` 及对应生产影响域场景。任何守卫失败时,结论只能是“保留定制并修复适配”,不能以“upstream 已支持”为理由继续退役。
|
||||
|
||||
Memory 本地最低验证集:
|
||||
|
||||
```bash
|
||||
npm run check:memory-v2-contracts
|
||||
npm run check:memory-v2-config
|
||||
npm run check:memory-v2-session
|
||||
npm run check:memory-v2-stack
|
||||
node --test conversation-memory.test.mjs conversation-repair.test.mjs \
|
||||
memory-v2-personal-store.test.mjs memory-v2-lifecycle.test.mjs \
|
||||
memory-v2-pgvector-backfill.test.mjs memory-v2-runtime.test.mjs
|
||||
```
|
||||
|
||||
以上命令只验证本地代码/fixture;若要使用真实本地 MySQL/PG,必须先确认连接字符串是 loopback 隔离库,并把 count、ID/hash、关联和 watermark 对账结果作为附件保存。任何测试失败、数据 drift、owner 不明确或无法证明回滚时,Memory 迁移保持 blocked,不得进入生产。
|
||||
|
||||
## 一句话结论
|
||||
|
||||
跟随升级,但不做 upstream 全量替换:通用能力(搜索、浏览器、hooks、cost 追踪、DeepSeek 兼容)按证据逐步 Replace 或 Wrapper;MindSpace、Page Data、微信、计费、权限、PG 多实例与 H5/Portal 契约必须 Keep。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
# Goose v1.49 Phase 0 本地基线
|
||||
|
||||
本目录存放 loopback 升级对照用的 JSON 基线,**不含生产密钥**。
|
||||
|
||||
| 文件 | 生成命令 |
|
||||
|---|---|
|
||||
| `goose-v149-memory-latest.json` | `node scripts/export-goose-v149-memory-manifest.mjs --output docs/baselines/goose-v149-memory-latest.json` |
|
||||
| `goose-v149-runtime-baseline-*.json` | `node scripts/capture-goose-v149-baseline.mjs` |
|
||||
| `goose-v149-cost-evidence-*.json` | `node scripts/check-goosed-v149-cost-smoke.mjs` |
|
||||
| `goose-v149-deepseek-tools-evidence-*.json` | `node scripts/check-goosed-v149-deepseek-tools.mjs` |
|
||||
| `goose-v149-thinking-preservation-latest.json` | `node scripts/check-goosed-v149-thinking-preservation.mjs` |
|
||||
| `goose-v149-message-sanitize-latest.json` | `node scripts/compare-goose-v149-message-sanitize.mjs` |
|
||||
|
||||
Phase 3 收口:[goose-v149-phase3-closeout.md](../goose-v149-phase3-closeout.md) · 聚合:`node scripts/run-goosed-v149-phase3.mjs`
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-cost-evidence-v1",
|
||||
"capturedAt": "2026-09-08T10:06:59.003Z",
|
||||
"sessionId": "20260908_30",
|
||||
"requestId": "b0ee6f3f-cedc-470c-84cd-925b83c3c131",
|
||||
"finishReason": "stop",
|
||||
"tokenState": {
|
||||
"inputTokens": 8339,
|
||||
"outputTokens": 2,
|
||||
"totalTokens": 8341,
|
||||
"accumulatedInputTokens": 8339,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedTotalTokens": 8341,
|
||||
"accumulatedCost": null
|
||||
},
|
||||
"billingState": {
|
||||
"accumulatedInputTokens": 8339,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedCost": 0.0022537300000000002
|
||||
},
|
||||
"enrichedCost": 0.0022537300000000002
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-cost-evidence-v1",
|
||||
"capturedAt": "2026-09-08T10:07:14.546Z",
|
||||
"sessionId": "20260908_37",
|
||||
"requestId": "4c558b69-64e3-4909-9e33-1cb598a00cc2",
|
||||
"finishReason": "stop",
|
||||
"tokenState": {
|
||||
"inputTokens": 8339,
|
||||
"outputTokens": 2,
|
||||
"totalTokens": 8341,
|
||||
"accumulatedInputTokens": 8339,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedTotalTokens": 8341,
|
||||
"accumulatedCost": null
|
||||
},
|
||||
"billingState": {
|
||||
"accumulatedInputTokens": 8339,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedCost": 0.0022537300000000002
|
||||
},
|
||||
"enrichedCost": 0.0022537300000000002
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-cost-evidence-v1",
|
||||
"capturedAt": "2026-09-08T10:30:13.352Z",
|
||||
"sessionId": "20260908_50",
|
||||
"requestId": "752da410-df43-44c6-9958-2c23f3d948ae",
|
||||
"finishReason": "stop",
|
||||
"tokenState": {
|
||||
"inputTokens": 8339,
|
||||
"outputTokens": 2,
|
||||
"totalTokens": 8341,
|
||||
"accumulatedInputTokens": 8339,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedTotalTokens": 8341,
|
||||
"accumulatedCost": null
|
||||
},
|
||||
"billingState": {
|
||||
"accumulatedInputTokens": 8339,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedCost": 0.0022537300000000002
|
||||
},
|
||||
"enrichedCost": 0.0022537300000000002
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-cost-evidence-v1",
|
||||
"capturedAt": "2026-09-08T11:12:36.359Z",
|
||||
"sessionId": "20260908_66",
|
||||
"requestId": "ba50d32c-5024-4b77-8372-cf6ea1c5eaf0",
|
||||
"finishReason": "stop",
|
||||
"tokenState": {
|
||||
"inputTokens": 8339,
|
||||
"outputTokens": 2,
|
||||
"totalTokens": 8341,
|
||||
"accumulatedInputTokens": 8339,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedTotalTokens": 8341,
|
||||
"accumulatedCost": null
|
||||
},
|
||||
"billingState": {
|
||||
"accumulatedInputTokens": 8339,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedCost": 0.0022537300000000002
|
||||
},
|
||||
"enrichedCost": 0.0022537300000000002
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-cost-evidence-v1",
|
||||
"capturedAt": "2026-09-08T11:19:29.723Z",
|
||||
"sessionId": "20260908_79",
|
||||
"requestId": "388a1d9e-6edb-413a-bd49-9d602e3132d0",
|
||||
"finishReason": "stop",
|
||||
"tokenState": {
|
||||
"inputTokens": 8586,
|
||||
"outputTokens": 2,
|
||||
"totalTokens": 8588,
|
||||
"accumulatedInputTokens": 8586,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedTotalTokens": 8588,
|
||||
"accumulatedCost": null
|
||||
},
|
||||
"billingState": {
|
||||
"accumulatedInputTokens": 8586,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedCost": 0.00232042
|
||||
},
|
||||
"enrichedCost": 0.00232042
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-cost-evidence-v1",
|
||||
"capturedAt": "2026-09-08T12:07:15.664Z",
|
||||
"sessionId": "20260908_94",
|
||||
"requestId": "f72ff3fd-413f-4c80-b671-1d4f15d1ecf1",
|
||||
"finishReason": "stop",
|
||||
"tokenState": {
|
||||
"inputTokens": 8586,
|
||||
"outputTokens": 2,
|
||||
"totalTokens": 8588,
|
||||
"accumulatedInputTokens": 8586,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedTotalTokens": 8588,
|
||||
"accumulatedCost": null
|
||||
},
|
||||
"billingState": {
|
||||
"accumulatedInputTokens": 8586,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedCost": 0.00232042
|
||||
},
|
||||
"enrichedCost": 0.00232042
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-cost-evidence-v1",
|
||||
"capturedAt": "2026-09-08T12:23:16.640Z",
|
||||
"sessionId": "20260908_109",
|
||||
"requestId": "a9365f33-add9-45f3-8512-6a5554e85194",
|
||||
"finishReason": "stop",
|
||||
"tokenState": {
|
||||
"inputTokens": 8586,
|
||||
"outputTokens": 2,
|
||||
"totalTokens": 8588,
|
||||
"accumulatedInputTokens": 8586,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedTotalTokens": 8588,
|
||||
"accumulatedCost": null
|
||||
},
|
||||
"billingState": {
|
||||
"accumulatedInputTokens": 8586,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedCost": 0.00232042
|
||||
},
|
||||
"enrichedCost": 0.00232042
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-cost-evidence-v1",
|
||||
"capturedAt": "2026-09-08T12:32:49.137Z",
|
||||
"sessionId": "20260908_124",
|
||||
"requestId": "b31d65ea-e68a-4a7b-a47b-89b3d365afa2",
|
||||
"finishReason": "stop",
|
||||
"tokenState": {
|
||||
"inputTokens": 8586,
|
||||
"outputTokens": 2,
|
||||
"totalTokens": 8588,
|
||||
"accumulatedInputTokens": 8586,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedTotalTokens": 8588,
|
||||
"accumulatedCost": null
|
||||
},
|
||||
"billingState": {
|
||||
"accumulatedInputTokens": 8586,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedCost": 0.00232042
|
||||
},
|
||||
"enrichedCost": 0.00232042
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-cost-evidence-v1",
|
||||
"capturedAt": "2026-09-08T13:06:15.865Z",
|
||||
"sessionId": "20260908_141",
|
||||
"requestId": "d85df70e-9d1d-45d1-9408-96dbff9d5c96",
|
||||
"finishReason": "stop",
|
||||
"tokenState": {
|
||||
"inputTokens": 8586,
|
||||
"outputTokens": 2,
|
||||
"totalTokens": 8588,
|
||||
"accumulatedInputTokens": 8586,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedTotalTokens": 8588,
|
||||
"accumulatedCost": null
|
||||
},
|
||||
"billingState": {
|
||||
"accumulatedInputTokens": 8586,
|
||||
"accumulatedOutputTokens": 2,
|
||||
"accumulatedCost": 0.00232042
|
||||
},
|
||||
"enrichedCost": 0.00232042
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-deepseek-tools-v1",
|
||||
"capturedAt": "2026-09-08T12:07:03.092Z",
|
||||
"sessionId": "20260908_84",
|
||||
"requestId": "24118cbb-ebe9-4f0f-9b8c-59a71cbbad28",
|
||||
"providerId": "custom_tkmind_relay_deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"toolNames": [
|
||||
"todo__todo_write"
|
||||
],
|
||||
"finishReason": "stop",
|
||||
"eventTypes": [
|
||||
"Message",
|
||||
"Message",
|
||||
"Message",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-deepseek-tools-v1",
|
||||
"capturedAt": "2026-09-08T12:07:06.888Z",
|
||||
"sessionId": "20260908_89",
|
||||
"requestId": "cbdfc7d5-48ee-4fe9-acf1-dd08bbba27b2",
|
||||
"providerId": "custom_tkmind_relay_deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"toolNames": [
|
||||
"todo__todo_write"
|
||||
],
|
||||
"finishReason": "stop",
|
||||
"eventTypes": [
|
||||
"Message",
|
||||
"Message",
|
||||
"Message",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-deepseek-tools-v1",
|
||||
"capturedAt": "2026-09-08T12:07:18.952Z",
|
||||
"sessionId": "20260908_96",
|
||||
"requestId": "23c5fcf6-e15a-4880-b0d1-c9fd0073c419",
|
||||
"providerId": "custom_tkmind_relay_deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"toolNames": [
|
||||
"todo__todo_write"
|
||||
],
|
||||
"finishReason": "stop",
|
||||
"eventTypes": [
|
||||
"Message",
|
||||
"Message",
|
||||
"Message",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-deepseek-tools-v1",
|
||||
"capturedAt": "2026-09-08T12:23:05.066Z",
|
||||
"sessionId": "20260908_104",
|
||||
"requestId": "c5f423b5-9dc0-474e-898c-a472a935386e",
|
||||
"providerId": "custom_tkmind_relay_deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"toolNames": [
|
||||
"todo__todo_write"
|
||||
],
|
||||
"finishReason": "stop",
|
||||
"eventTypes": [
|
||||
"Message",
|
||||
"Message",
|
||||
"Message",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-deepseek-tools-v1",
|
||||
"capturedAt": "2026-09-08T12:23:19.758Z",
|
||||
"sessionId": "20260908_112",
|
||||
"requestId": "0f40ec62-b124-4288-9518-040c4550e329",
|
||||
"providerId": "custom_tkmind_relay_deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"toolNames": [
|
||||
"todo__todo_write"
|
||||
],
|
||||
"finishReason": "stop",
|
||||
"eventTypes": [
|
||||
"Message",
|
||||
"Message",
|
||||
"Message",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-deepseek-tools-v1",
|
||||
"capturedAt": "2026-09-08T12:32:37.309Z",
|
||||
"sessionId": "20260908_119",
|
||||
"requestId": "3e3c39f4-6165-4e40-9b0a-d4785c969851",
|
||||
"providerId": "custom_tkmind_relay_deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"toolNames": [
|
||||
"todo__todo_write"
|
||||
],
|
||||
"finishReason": "stop",
|
||||
"eventTypes": [
|
||||
"Message",
|
||||
"Message",
|
||||
"Message",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-deepseek-tools-v1",
|
||||
"capturedAt": "2026-09-08T12:32:51.717Z",
|
||||
"sessionId": "20260908_127",
|
||||
"requestId": "8c1f27ac-e8c1-47c8-8835-9ae27d217adb",
|
||||
"providerId": "custom_tkmind_relay_deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"toolNames": [
|
||||
"todo__todo_write"
|
||||
],
|
||||
"finishReason": "stop",
|
||||
"eventTypes": [
|
||||
"Message",
|
||||
"Message",
|
||||
"Message",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-deepseek-tools-v1",
|
||||
"capturedAt": "2026-09-08T13:04:37.865Z",
|
||||
"sessionId": "20260908_135",
|
||||
"requestId": "c1360188-7f59-41e3-ba43-689fa8f7be1d",
|
||||
"providerId": "custom_tkmind_relay_deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"toolNames": [
|
||||
"todo__todo_write"
|
||||
],
|
||||
"finishReason": "stop",
|
||||
"eventTypes": [
|
||||
"Message",
|
||||
"Message",
|
||||
"Message",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-deepseek-tools-v1",
|
||||
"capturedAt": "2026-09-08T13:06:18.709Z",
|
||||
"sessionId": "20260908_144",
|
||||
"requestId": "328356c3-25b6-4d15-bd18-206dd7c803aa",
|
||||
"providerId": "custom_tkmind_relay_deepseek",
|
||||
"model": "deepseek-chat",
|
||||
"toolNames": [
|
||||
"todo__todo_write"
|
||||
],
|
||||
"finishReason": "stop",
|
||||
"eventTypes": [
|
||||
"Message",
|
||||
"Message",
|
||||
"Message",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-message-sanitize-v1",
|
||||
"capturedAt": "2026-09-08T13:06:26.641Z",
|
||||
"v149": {
|
||||
"passed": 11,
|
||||
"root": "/Users/john/Project/tkmind_go-v149"
|
||||
},
|
||||
"v141": {
|
||||
"passed": 11,
|
||||
"ok": true,
|
||||
"root": "/Users/john/Project/tkmind_go-v141-prod-prep"
|
||||
},
|
||||
"equivalent": true
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-baseline-v1",
|
||||
"capturedAt": "2026-09-08T10:02:06.825Z",
|
||||
"target": "https://127.0.0.1:18049",
|
||||
"status": {
|
||||
"status": "ok"
|
||||
},
|
||||
"sessionsSummary": {
|
||||
"count": 20,
|
||||
"sample": [
|
||||
{
|
||||
"id": "20260908_20",
|
||||
"working_dir": "/Users/john/Project/Memind",
|
||||
"name": "pong",
|
||||
"user_set_name": false,
|
||||
"session_type": "user",
|
||||
"created_at": "2026-09-08T10:02:00.279689Z",
|
||||
"updated_at": "2026-09-08T10:02:02.146310Z",
|
||||
"extension_data": {
|
||||
"enabled_extensions.v0": {
|
||||
"extensions": [
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "tom",
|
||||
"description": "Inject custom context into every turn via GOOSE_MOIM_MESSAGE_TEXT and GOOSE_MOIM_MESSAGE_FILE environment variables",
|
||||
"display_name": "Top Of Mind",
|
||||
"bundled": true
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "skills",
|
||||
"description": "Discover and provide skill instructions from filesystem and builtins",
|
||||
"display_name": "Skills",
|
||||
"bundled": true
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "aider",
|
||||
"description": "Delegate multi-file coding tasks to Aider instead of developer write/edit",
|
||||
"display_name": "Aider",
|
||||
"bundled": true
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "Extension Manager",
|
||||
"description": "Enable extension management tools for discovering, enabling, and disabling extensions",
|
||||
"display_name": "Extension Manager",
|
||||
"bundled": true
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "developer",
|
||||
"description": "Write and edit files, and execute shell commands",
|
||||
"display_name": "Developer",
|
||||
"bundled": true
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "summon",
|
||||
"description": "Load knowledge and delegate tasks to subagents",
|
||||
"display_name": "Summon",
|
||||
"bundled": true
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "openhands",
|
||||
"description": "Delegate multi-file coding tasks to OpenHands instead of developer write/edit",
|
||||
"display_name": "OpenHands",
|
||||
"bundled": true
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "apps",
|
||||
"description": "Create and manage custom Goose apps through chat. Apps are HTML/CSS/JavaScript and run in sandboxed windows.",
|
||||
"display_name": "Apps",
|
||||
"bundled": true
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "analyze",
|
||||
"description": "Analyze code structure with tree-sitter: directory overviews, file details, symbol call graphs",
|
||||
"display_name": "Analyze",
|
||||
"bundled": true
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "todo",
|
||||
"description": "Enable a todo list for goose so it can keep track of what it is doing",
|
||||
"display_name": "Todo",
|
||||
"bundled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"input_tokens": 8339,
|
||||
"output_tokens": 2,
|
||||
"total_tokens": 8341,
|
||||
"cache_read_input_tokens": 0,
|
||||
"cache_write_input_tokens": null
|
||||
},
|
||||
"accumulated_usage": {
|
||||
"input_tokens": 8339,
|
||||
"output_tokens": 2,
|
||||
"total_tokens": 8341,
|
||||
"cache_read_input_tokens": 0,
|
||||
"cache_write_input_tokens": 0
|
||||
},
|
||||
"accumulated_cost": null,
|
||||
"schedule_id": null,
|
||||
"recipe": null,
|
||||
"user_recipe_values": null,
|
||||
"conversation": null,
|
||||
"message_count": 3,
|
||||
"last_message_at": "2026-09-08T10:02:01Z",
|
||||
"provider_name": "custom_tkmind_relay_deepseek",
|
||||
"model_config": {
|
||||
"model_name": "deepseek-chat",
|
||||
"temperature": null,
|
||||
"max_tokens": null,
|
||||
"toolshim": false,
|
||||
"toolshim_model": null
|
||||
},
|
||||
"goose_mode": "auto",
|
||||
"archived_at": null,
|
||||
"project_id": null,
|
||||
"parent_session_id": null,
|
||||
"last_message_snippet": null
|
||||
},
|
||||
{
|
||||
"id": "20260908_19",
|
||||
"working_dir": "/Users/john/Project/Memind",
|
||||
"name": "New Chat",
|
||||
"user_set_name": false,
|
||||
"session_type": "user",
|
||||
"created_at": "2026-09-08T10:02:00.057531Z",
|
||||
"updated_at": "2026-09-08T10:02:00.064842Z",
|
||||
"extension_data": {
|
||||
"enabled_extensions.v0": {
|
||||
"extensions": [
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "openhands",
|
||||
"description": "OpenHands coding delegate",
|
||||
"display_name": null,
|
||||
"bundled": null,
|
||||
"available_tools": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "aider",
|
||||
"description": "Aider coding delegate",
|
||||
"display_name": null,
|
||||
"bundled": null,
|
||||
"available_tools": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"total_tokens": null,
|
||||
"cache_read_input_tokens": null,
|
||||
"cache_write_input_tokens": null
|
||||
},
|
||||
"accumulated_usage": {
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"total_tokens": null,
|
||||
"cache_read_input_tokens": null,
|
||||
"cache_write_input_tokens": null
|
||||
},
|
||||
"accumulated_cost": null,
|
||||
"schedule_id": null,
|
||||
"recipe": null,
|
||||
"user_recipe_values": null,
|
||||
"conversation": null,
|
||||
"message_count": 0,
|
||||
"last_message_at": null,
|
||||
"provider_name": null,
|
||||
"model_config": null,
|
||||
"goose_mode": "auto",
|
||||
"archived_at": null,
|
||||
"project_id": null,
|
||||
"parent_session_id": null,
|
||||
"last_message_snippet": null
|
||||
},
|
||||
{
|
||||
"id": "20260908_18",
|
||||
"working_dir": "/Users/john/Project/Memind",
|
||||
"name": "New Chat",
|
||||
"user_set_name": false,
|
||||
"session_type": "user",
|
||||
"created_at": "2026-09-08T10:01:59.691171Z",
|
||||
"updated_at": "2026-09-08T10:01:59.885622Z",
|
||||
"extension_data": {
|
||||
"enabled_extensions.v0": {
|
||||
"extensions": [
|
||||
{
|
||||
"type": "stdio",
|
||||
"name": "sandbox-fs",
|
||||
"description": "MindSpace sandbox filesystem MCP (local v1.49 smoke)",
|
||||
"cmd": "/opt/homebrew/Cellar/node/26.0.0/bin/node",
|
||||
"args": [
|
||||
"/Users/john/Project/Memind/mindspace-sandbox-mcp.mjs",
|
||||
"/var/folders/wv/y43yzlmn7gd1bcsq1fcwx3b00000gn/T/goose-v149-sandbox-3ADPMS"
|
||||
],
|
||||
"envs": {
|
||||
"ALLOWED_TOOLS": "write_file,read_file,edit_file,create_dir,list_dir",
|
||||
"SANDBOX_ROOT": "/var/folders/wv/y43yzlmn7gd1bcsq1fcwx3b00000gn/T/goose-v149-sandbox-3ADPMS"
|
||||
},
|
||||
"env_keys": [],
|
||||
"timeout": null,
|
||||
"cwd": null,
|
||||
"bundled": null,
|
||||
"available_tools": [
|
||||
"write_file",
|
||||
"read_file",
|
||||
"edit_file",
|
||||
"create_dir",
|
||||
"list_dir"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "skills",
|
||||
"description": "skills",
|
||||
"display_name": null,
|
||||
"bundled": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"total_tokens": null,
|
||||
"cache_read_input_tokens": null,
|
||||
"cache_write_input_tokens": null
|
||||
},
|
||||
"accumulated_usage": {
|
||||
"input_tokens": null,
|
||||
"output_tokens": null,
|
||||
"total_tokens": null,
|
||||
"cache_read_input_tokens": null,
|
||||
"cache_write_input_tokens": null
|
||||
},
|
||||
"accumulated_cost": null,
|
||||
"schedule_id": null,
|
||||
"recipe": null,
|
||||
"user_recipe_values": null,
|
||||
"conversation": null,
|
||||
"message_count": 0,
|
||||
"last_message_at": null,
|
||||
"provider_name": null,
|
||||
"model_config": null,
|
||||
"goose_mode": "auto",
|
||||
"archived_at": null,
|
||||
"project_id": null,
|
||||
"parent_session_id": null,
|
||||
"last_message_snippet": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"reply": {
|
||||
"requestId": "a53113c2-8462-4b6d-94f6-0303f6eebea5",
|
||||
"outcome": "error",
|
||||
"events": [
|
||||
{
|
||||
"type": "Error",
|
||||
"error": "Provider not set",
|
||||
"chat_request_id": "a53113c2-8462-4b6d-94f6-0303f6eebea5",
|
||||
"request_id": "a53113c2-8462-4b6d-94f6-0303f6eebea5"
|
||||
}
|
||||
],
|
||||
"sessionId": "20260908_21"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"schemaVersion": "goose-v149-thinking-preservation-v1",
|
||||
"capturedAt": "2026-09-08T13:06:25.818Z",
|
||||
"worktree": "/Users/john/Project/tkmind_go-v149",
|
||||
"tests": [
|
||||
"test_deepseek_thinking_preserved_in_tool_call_message",
|
||||
"test_kimi_thinking_preserved_in_tool_call_message",
|
||||
"test_reasoning_preserved_when_combined_with_tool_call",
|
||||
"test_multi_tool_response_preserves_reasoning_and_message_id_correlation"
|
||||
],
|
||||
"passed": 4
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
# Goose v1.49 本地 Canary 运行手册
|
||||
|
||||
> 范围:仅本机 loopback(`:18049` vs `:18006`),**禁止**连接 103/105 或修改生产 launchd。
|
||||
> 决策表:[TKMIND_V1_49_MIGRATION.md](../TKMIND_V1_49_MIGRATION.md)
|
||||
|
||||
## 架构
|
||||
|
||||
```text
|
||||
Portal (pnpm dev :8081)
|
||||
│
|
||||
├─ TKMIND_GOOSE_CANARY=off → TKMIND_API_TARGET=https://127.0.0.1:18006 (stable 1.41)
|
||||
├─ TKMIND_GOOSE_CANARY=all → TKMIND_API_TARGET=https://127.0.0.1:18049 (v1.49 候选)
|
||||
└─ TKMIND_GOOSE_CANARY=users → 双 target;canary 用户路由到 v1.49(需 USER_IDS)
|
||||
```
|
||||
|
||||
Memory 在 canary 期间保持 legacy owner(MySQL `h5_user_memory_items`),upstream goosed **不得**写入业务记忆。
|
||||
|
||||
## 前置条件
|
||||
|
||||
```bash
|
||||
bash scripts/build-goosed-v149-local.sh
|
||||
bash scripts/ensure-goose-v149-local-db.sh
|
||||
bash scripts/run-goosed-v149-local.sh # 另开终端,监听 :18049
|
||||
curl -sk https://127.0.0.1:18049/status # → ok
|
||||
```
|
||||
|
||||
## 一键开启 Canary(全量切到 v1.49)
|
||||
|
||||
```bash
|
||||
bash scripts/switch-goose-v149-canary.sh on all
|
||||
pnpm dev # 重启 Portal 以加载 .env.local
|
||||
node scripts/check-goosed-v149-canary-portal.mjs # 隔离 :8083 验证(不干扰主 Portal)
|
||||
```
|
||||
|
||||
> 已写入 `.env.local` canary 块。主 Portal `:8081` 须重启后才路由到 `:18049`。
|
||||
> v1.49 已移植 `platform/web`(`web_search` / `fetch_url`);验证:`node scripts/check-goosed-v149-web-smoke.mjs`。
|
||||
|
||||
脚本会在 `.env.local` 写入 `# GOOSE_V149_CANARY_BEGIN … END` 块,设置:
|
||||
|
||||
- `TKMIND_GOOSE_CANARY=all`
|
||||
- `TKMIND_API_TARGET=https://127.0.0.1:18049`
|
||||
- Memory 安全开关(legacy / vector off)
|
||||
- `H5_PUBLIC_BASE_URL=http://127.0.0.1:8081`(避免 Vite `:5173` 未起时页面链接失效)
|
||||
|
||||
## 按用户 Canary(双 target)
|
||||
|
||||
```bash
|
||||
export TKMIND_GOOSE_CANARY_USER_IDS="<your-user-uuid>"
|
||||
bash scripts/switch-goose-v149-canary.sh on users
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Portal 会注册 `[18006, 18049]` 双 target;canary 用户的新 session 应 pin 到 v1.49(见 `scripts/goose-v149-canary.mjs` + `release-gate/canary-routing.mjs`)。
|
||||
|
||||
## 一键回滚(回落 stable)
|
||||
|
||||
```bash
|
||||
bash scripts/switch-goose-v149-canary.sh rollback
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
回滚仅移除 canary 块,不删 v1.49 进程或 PG 库;stable `:18006` 保持可用。
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
# Phase 2 聚合(推荐日常)
|
||||
node scripts/run-goosed-v149-phase2.mjs
|
||||
|
||||
# Phase 3 收口(合并前)
|
||||
node scripts/run-goosed-v149-phase3.mjs
|
||||
|
||||
# 单项 / 全量 smoke
|
||||
node scripts/check-goosed-v149-all.mjs
|
||||
node scripts/check-goosed-v149-memory-loop.mjs
|
||||
node scripts/check-goosed-v149-portal-smoke.mjs # 主 Portal 重启后
|
||||
node scripts/check-goosed-v149-canary-portal.mjs # 隔离 :8083
|
||||
```
|
||||
|
||||
## Phase 0 基线采集
|
||||
|
||||
```bash
|
||||
# Runtime 样本(status / sessions / 可选 SSE reply)
|
||||
node scripts/capture-goose-v149-baseline.mjs
|
||||
|
||||
# Memory manifest(每用户 ID/hash/evidence/source session)
|
||||
node scripts/export-goose-v149-memory-manifest.mjs --output docs/baselines/goose-v149-memory-latest.json
|
||||
```
|
||||
|
||||
输出目录:`docs/baselines/`(git 可跟踪的本地基线 JSON)。
|
||||
|
||||
## 环境变量参考
|
||||
|
||||
见 [goose-v149-canary.env.example](./goose-v149-canary.env.example)。
|
||||
|
||||
| 变量 | 说明 |
|
||||
|---|---|
|
||||
| `TKMIND_GOOSE_CANARY` | `off` / `all` / `users` |
|
||||
| `TKMIND_API_TARGET_STABLE` | 默认 `https://127.0.0.1:18006` |
|
||||
| `TKMIND_API_TARGET_V149` | 默认 `https://127.0.0.1:18049` |
|
||||
| `TKMIND_GOOSE_CANARY_USER_IDS` | 逗号分隔 user UUID(users 模式) |
|
||||
| `GOOSE_V149_CANARY_ENV_FILE` | 默认 `.env.local` |
|
||||
|
||||
## 禁止项
|
||||
|
||||
- 不得把 `TKMIND_API_TARGET` 指向 103/105 公网或生产端口
|
||||
- 不得在本阶段修改 103 launchd、105 nginx
|
||||
- 不得启用 upstream Memory backend 写入 MySQL 业务表
|
||||
- 生产发布仍须走 `main` + `check-release-ready.sh` + Gate report
|
||||
@@ -0,0 +1,25 @@
|
||||
# Goose v1.49 local canary example.
|
||||
# Copy into .env.local via: bash scripts/switch-goose-v149-canary.sh on all
|
||||
# Rollback: bash scripts/switch-goose-v149-canary.sh rollback
|
||||
|
||||
# --- Full canary (all Portal traffic → v1.49) ---
|
||||
# TKMIND_GOOSE_CANARY=all
|
||||
# TKMIND_API_TARGET=https://127.0.0.1:18049
|
||||
# TKMIND_API_TARGET_STABLE=https://127.0.0.1:18006
|
||||
# TKMIND_API_TARGET_V149=https://127.0.0.1:18049
|
||||
|
||||
# --- Per-user canary ---
|
||||
# TKMIND_GOOSE_CANARY=users
|
||||
# TKMIND_API_TARGET=https://127.0.0.1:18006
|
||||
# TKMIND_API_TARGETS=https://127.0.0.1:18006,https://127.0.0.1:18049
|
||||
# TKMIND_GOOSE_CANARY_USER_IDS=replace-with-user-uuid
|
||||
|
||||
# Memory safety (Phase 0–1)
|
||||
MEMORY_BACKEND=legacy
|
||||
MEMORY_VECTOR_ENABLED=0
|
||||
MEMORY_AGENT_INJECTION_MODE=off
|
||||
MEMORY_LIFECYCLE_ENABLED=0
|
||||
MEMORY_CANDIDATE_ENABLED=0
|
||||
|
||||
# Goose v1.49 local session DB (isolated from production memind_sessions)
|
||||
# GOOSE_SESSION_DB_URL=postgresql://john@127.0.0.1:5432/goose_sessions_v149_dev
|
||||
@@ -0,0 +1,77 @@
|
||||
# Goose v1.49 本地升级 Phase 0 基线
|
||||
|
||||
> 范围:仅本机 loopback,禁止连接 103/105 或生产库。
|
||||
> 决策表:[TKMIND_V1_49_MIGRATION.md](../TKMIND_V1_49_MIGRATION.md)
|
||||
|
||||
## Stable 基线(103 生产,只读对照)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| goosed 版本 | `goose-server 1.41.0` |
|
||||
| 二进制 | `tkmind_go-native/releases/goosed-20260729-v141-m1` |
|
||||
| 源码分支 | `upgrade/goose-1.47`(TKMind 补丁) |
|
||||
| 端口 | `18006`–`18014`(native launchd × 9) |
|
||||
| Session PG | `memind_sessions` @ `127.0.0.1:5432`(103) |
|
||||
| 记录时间 | 2026-09-08 |
|
||||
|
||||
## v1.49 候选(本地隔离)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| worktree | `/Users/john/Project/tkmind_go-v149` |
|
||||
| 分支 | `upgrade/v1.49-tkmind` |
|
||||
| upstream tag | `v1.49.0` |
|
||||
| 端口 | `18049` |
|
||||
| 启动命令 | `goose serve --tls`(**v1.49 已移除 `goosed`/`goose-server`**,改为 ACP HTTP/WS) |
|
||||
| Session PG | `goose_sessions_v149_dev` @ `127.0.0.1:5432` |
|
||||
| XDG 隔离 | `/tmp/goose-v149-runtime/{config,data,state,cache}` |
|
||||
| Portal 目标 | `TKMIND_API_TARGET_V149=https://127.0.0.1:18049`(独立 H5 进程时使用) |
|
||||
|
||||
## Memory 本地策略(Phase 1 初始)
|
||||
|
||||
```bash
|
||||
MEMORY_BACKEND=legacy
|
||||
MEMORY_VECTOR_ENABLED=0
|
||||
MEMORY_AGENT_INJECTION_MODE=off
|
||||
MEMORY_LIFECYCLE_ENABLED=0
|
||||
MEMORY_CANDIDATE_ENABLED=0
|
||||
```
|
||||
|
||||
upstream / v1.49 goosed 不得写入 MySQL 业务记忆;只走现有 Portal `conversation-memory` owner。
|
||||
|
||||
## Phase 0 检查项
|
||||
|
||||
- [x] 记录 stable 1.41 基线
|
||||
- [x] 拉取 upstream `v1.49.0` tag
|
||||
- [x] 创建 `upgrade/v1.49-tkmind` worktree
|
||||
- [x] 创建隔离 PG 库 `goose_sessions_v149_dev`
|
||||
- [x] 编译 v1.49 goosed 并通过 `GET /status`(`goose 1.49.0` @ `:18049` → `ok`)
|
||||
- [x] 移植 legacy REST(`/agent/start`、`/sessions`、`/sessions/{id}/events|reply|cancel` 等,见 `tkmind_compat`)
|
||||
- [x] 导出 Memory manifest(`node scripts/export-goose-v149-memory-manifest.mjs` → `docs/baselines/goose-v149-memory-latest.json`)
|
||||
- [x] 保存 runtime 基线样本(`node scripts/capture-goose-v149-baseline.mjs` → `docs/baselines/`)
|
||||
- [x] Canary 开关与一键回滚(`scripts/switch-goose-v149-canary.sh`,见 [goose-v149-canary-runbook.md](./goose-v149-canary-runbook.md))
|
||||
|
||||
## Phase 1 验证
|
||||
|
||||
```bash
|
||||
node scripts/run-goosed-v149-phase2.mjs # Phase 2 聚合(cost + drift + phase1)
|
||||
node scripts/check-goosed-v149-cost-smoke.mjs # Finish token_state / billing
|
||||
node scripts/compare-goose-v149-memory-manifest.mjs
|
||||
bash scripts/switch-goose-v149-canary.sh on all # 全量 canary 后重启 pnpm dev
|
||||
```
|
||||
|
||||
```bash
|
||||
bash scripts/ensure-goose-v149-vendor.sh # 离线 vendor(GitHub 不可用时)
|
||||
bash scripts/build-goosed-v149-local.sh # 编译 goose CLI(首次较久)
|
||||
bash scripts/ensure-goose-v149-local-db.sh
|
||||
bash scripts/run-goosed-v149-local.sh # goose serve @ 18049
|
||||
curl -sk https://127.0.0.1:18049/status
|
||||
```
|
||||
|
||||
> v1.49 已通过 `crates/goose/src/acp/tkmind/` 重移植 legacy REST;聚合验证:`node scripts/check-goosed-v149-all.mjs`。
|
||||
|
||||
## 禁止项
|
||||
|
||||
- 不得使用生产 `DATABASE_URL`、`GOOSE_SESSION_DB_URL`(含 RDS / 103 / 105 地址)
|
||||
- 不得修改 103 launchd、105 nginx、Portal `:8081`
|
||||
- 不得在本阶段删除 1.41 定制或 `:18036` DeepSeek 代理
|
||||
@@ -0,0 +1,87 @@
|
||||
# Goose v1.49 本地编译所需下载链接
|
||||
|
||||
> TKMind 需要 **code-mode**(含 v8)。本机 GitHub SSL 不稳定时,可手动下载后放入 cargo 缓存。
|
||||
|
||||
## 默认编译(含 code-mode,推荐)
|
||||
|
||||
```bash
|
||||
bash scripts/build-goosed-v149-local.sh
|
||||
# 等价 GOOSE_V149_BUILD_MODE=codemode
|
||||
```
|
||||
|
||||
会先执行 `scripts/ensure-goose-v149-v8.sh` 确保 v8 缓存就绪。
|
||||
|
||||
## 方案 A:仅跳过 local-inference 的 code-mode 编译
|
||||
|
||||
```bash
|
||||
cd /Users/john/Project/tkmind_go-v149
|
||||
bash ../Memind/scripts/ensure-goose-v149-v8.sh
|
||||
rustup run 1.96.1 cargo build --release --package goose-cli --bin goose \
|
||||
--no-default-features --features rustls-tls,aws-providers,system-keyring,code-mode,telemetry
|
||||
```
|
||||
|
||||
## 方案 B:完整 default features(含 v8 + local-inference)
|
||||
|
||||
```bash
|
||||
GOOSE_V149_BUILD_MODE=full bash scripts/build-goosed-v149-local.sh
|
||||
```
|
||||
|
||||
### rusty_v8 静态库(code-mode 必需)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 文件 | `librusty_v8_release_aarch64-apple-darwin.a.gz` |
|
||||
| 直链 | https://github.com/denoland/rusty_v8/releases/download/v145.0.0/librusty_v8_release_aarch64-apple-darwin.a.gz |
|
||||
| Release 页 | https://github.com/denoland/rusty_v8/releases/tag/v145.0.0 |
|
||||
|
||||
**重要**:cargo/rusty_v8 要求缓存路径是**单个 gzip 文件**,不是目录:
|
||||
|
||||
```text
|
||||
~/.cargo/.rusty_v8/https___github_com_denoland_rusty_v8_releases_download_v145_0_0_librusty_v8_release_aarch64_apple_darwin_a_gz
|
||||
```
|
||||
|
||||
(该路径本身就是文件,不要再套一层子目录。)
|
||||
|
||||
手动下载命令:
|
||||
|
||||
```bash
|
||||
bash scripts/ensure-goose-v149-v8.sh
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```bash
|
||||
CACHE_FILE="$HOME/.cargo/.rusty_v8/https___github_com_denoland_rusty_v8_releases_download_v145_0_0_librusty_v8_release_aarch64_apple_darwin_a_gz"
|
||||
mkdir -p "$(dirname "$CACHE_FILE")"
|
||||
curl -fL --retry 5 -o "$CACHE_FILE" \
|
||||
"https://github.com/denoland/rusty_v8/releases/download/v145.0.0/librusty_v8_release_aarch64-apple-darwin.a.gz"
|
||||
```
|
||||
|
||||
## 方案 C:minimal(无 code-mode,仅调试 serve 路由)
|
||||
|
||||
```bash
|
||||
GOOSE_V149_BUILD_MODE=minimal bash scripts/build-goosed-v149-local.sh
|
||||
```
|
||||
|
||||
### 2. Git 依赖(已用 file:// 本地 hermit 缓存,一般不需再下)
|
||||
|
||||
| 依赖 | 本地路径 |
|
||||
|---|---|
|
||||
| agent-client-protocol rust-sdk | `tkmind_go-v141-prod-prep/.hermit/rust/git/checkouts/rust-sdk-.../c97a520` |
|
||||
| cudaforge | `tkmind_go-v141-prod-prep/.hermit/rust/git/checkouts/cudaforge-.../e7c1967` |
|
||||
|
||||
已在 `tkmind_go-v149/Cargo.toml` 的 `[patch.crates-io]` 指向 `file://`。
|
||||
|
||||
### 3. upstream Goose 源码
|
||||
|
||||
| 项 | 链接 |
|
||||
|---|---|
|
||||
| Tag v1.49.0 | https://github.com/aaif-goose/goose/releases/tag/v1.49.0 |
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
bash scripts/build-goosed-v149-local.sh # 或方案 A 命令
|
||||
bash scripts/run-goosed-v149-local.sh
|
||||
curl -sk https://127.0.0.1:18049/status
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
# Goose v1.49 Phase 2 证据台账
|
||||
|
||||
> 本地 loopback only。每项记录 **通过 / 保留 / 回滚**,禁止批量删除定制。
|
||||
|
||||
## 执行入口
|
||||
|
||||
```bash
|
||||
node scripts/run-goosed-v149-phase2.mjs
|
||||
node scripts/check-goosed-v149-cost-smoke.mjs # 需 DEEPSEEK_API_KEY
|
||||
node scripts/compare-goose-v149-memory-manifest.mjs # 对比 baseline drift
|
||||
node scripts/check-goosed-v149-deepseek-tools.mjs # 需 DEEPSEEK_API_KEY
|
||||
node scripts/compare-goose-v149-message-sanitize.mjs # provider sanitize 单测对照
|
||||
node scripts/check-goosed-v149-thinking-preservation.mjs
|
||||
node scripts/check-goosed-v149-skills-smoke.mjs
|
||||
```
|
||||
|
||||
## 证据项
|
||||
|
||||
| 项 | 脚本/产物 | 状态 | 备注 |
|
||||
|---|---|---|---|
|
||||
| Phase 1 聚合 smoke | `check-goosed-v149-all.mjs` | 通过 | 2026-09-08 |
|
||||
| Finish token_state + cost | `check-goosed-v149-cost-smoke.mjs` | 通过 | in=8339 out=2 cost≈0.00225 USD |
|
||||
| Memory manifest 无 drift | `compare-goose-v149-memory-manifest.mjs` | 通过 | digest=69b48723… |
|
||||
| Memory fail-open (legacy) | `check-goosed-v149-memory-failopen.mjs` | 通过 | 超时 degrade 不抛错 |
|
||||
| Portal canary 联调 | `check-goosed-v149-canary-portal.mjs` | 通过 | 隔离 Portal :8083 → v149 :18049 |
|
||||
| Session reconcile 路由 | `check-goosed-v149-session-reconcile.mjs` | 通过 | GET /sessions/{id}, add/remove/restart |
|
||||
| platform/web 扩展 | `check-goosed-v149-web-smoke.mjs` | 通过 | web_search + fetch_url 工具挂载 |
|
||||
| DeepSeek 工具轮次 | `check-goosed-v149-deepseek-tools.mjs` | 通过/可选 | 需 DEEPSEEK_API_KEY;todo_write Finish |
|
||||
| DeepSeek reasoning 保留 | `check-goosed-v149-thinking-preservation.mjs` | 通过 | agent 集成测试 4 项 |
|
||||
| upstream web-search 路径 | `check-goosed-v149-skills-smoke.mjs` | 通过 | skills/load_skill(与 platform/web 并存) |
|
||||
| message sanitize 对照 | `compare-goose-v149-message-sanitize.mjs` | 通过 | goose-providers sanitize 单测 v1.41≈v1.49 |
|
||||
| DeepSeek no-think 代理 `:18036` | — | 保留 | 生产 parity;Portal session-reply-wait 仍启用 |
|
||||
|
||||
## 回滚
|
||||
|
||||
```bash
|
||||
bash scripts/switch-goose-v149-canary.sh rollback
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 判定规则
|
||||
|
||||
- **通过**:脚本 OK + 证据 JSON 已写入 `docs/baselines/`
|
||||
- **保留**:行为不等价或缺证据,维持 1.41 / 定制实现
|
||||
- **回滚**:canary 一切换即异常,执行 rollback 脚本
|
||||
@@ -0,0 +1,62 @@
|
||||
# Goose v1.49 Phase 3 收口清单
|
||||
|
||||
> **本阶段不授权 103/105 操作。** 仅整理证据、回滚路径与合并前闸门。
|
||||
> 决策表:[TKMIND_V1_49_MIGRATION.md](../TKMIND_V1_49_MIGRATION.md) · Phase 2 台账:[goose-v149-phase2-evidence.md](./goose-v149-phase2-evidence.md)
|
||||
|
||||
## 聚合验证入口
|
||||
|
||||
```bash
|
||||
bash scripts/run-goosed-v149-local.sh # :18049 另开终端
|
||||
node scripts/run-goosed-v149-phase3.mjs # Phase 2 + parity + drift
|
||||
```
|
||||
|
||||
通过时应输出 **`GOOSE_V149_PHASE3_OK`**。
|
||||
|
||||
## 已取得证据(可进入合并讨论,不可直接删代码)
|
||||
|
||||
| 能力 | 证据 | Phase 3 结论 |
|
||||
|---|---|---|
|
||||
| tkmind_compat / PG Session | `check-goosed-v149-all.mjs` | **Keep** — 已适配 v1.49 |
|
||||
| sandbox-fs / harness | harness + sandbox-fs smoke | **Keep** |
|
||||
| platform/web | `check-goosed-v149-web-smoke.mjs` | **Keep** — 已移植;upstream skill 并存 |
|
||||
| skills / web-search | `check-goosed-v149-skills-smoke.mjs` | **Wrapper** — 不替换 platform/web |
|
||||
| DeepSeek Finish + 工具轮次 | provider + deepseek-tools smoke | **Keep** relay + v1.49 runtime |
|
||||
| reasoning 保留 | `check-goosed-v149-thinking-preservation.mjs` | **保留** `:18036` 直至生产 parity |
|
||||
| message sanitize | `compare-goose-v149-message-sanitize.mjs` | provider 层等价;Portal 展示仍 Keep |
|
||||
| Memory legacy owner | memory-loop + drift + failopen | **Keep** — 禁止 upstream 写 MySQL |
|
||||
| 计费 token_state | cost-smoke JSON | **Wrapper** — 观察 v1.49 原生 cost |
|
||||
|
||||
## 明确禁止在本阶段删除
|
||||
|
||||
- `session-reply-wait.mjs`、H5 SSE 游标映射、Finish merge
|
||||
- `deepseek-no-think-proxy.mjs` / `:18036`(生产 parity 未退役)
|
||||
- MindSpace / Page Data / 微信 / 计费 / capabilities 策略
|
||||
- 1.41 stable 端口池 `:18006`–`:18014` 与 launchd 配置
|
||||
|
||||
## 合并前闸门(Memind 仓库)
|
||||
|
||||
1. [ ] `upgrade/v1.49-tkmind` worktree 变更已 review(`tkmind_go-v149` 独立仓库/worktree)
|
||||
2. [ ] Memind 侧脚本/文档/canary 开关 PR 就绪
|
||||
3. [ ] `node scripts/run-goosed-v149-phase3.mjs` 连续通过
|
||||
4. [ ] Memory manifest 无 drift(`compare-goose-v149-memory-manifest.mjs`)
|
||||
5. [x] 主 Portal 重启后 canary 联调:`check-goosed-v149-portal-smoke.mjs`(targets 含 `:18049`);canary 块强制 `H5_PUBLIC_BASE_URL=http://127.0.0.1:8081` 避免 5173 未起时链接失效
|
||||
6. [ ] **用户明确批准** 后才:`git push`、合并 `main`、任何 103 构建
|
||||
|
||||
## 合并后 / 生产前(仍须单独审批)
|
||||
|
||||
- `bash scripts/check-release-ready.sh`
|
||||
- 生产 Gate report(Core + Impact)
|
||||
- `npm run verify:mindspace-publish-guards:full` 等受保护路径 verify
|
||||
- Memory owner 切换:**不在** v1.49 运行时升级包内默认开启
|
||||
|
||||
## 回滚(任意时刻)
|
||||
|
||||
```bash
|
||||
bash scripts/switch-goose-v149-canary.sh rollback
|
||||
pnpm dev
|
||||
# stable :18006 继续服务;v1.49 :18049 可单独停进程
|
||||
```
|
||||
|
||||
## 基线产物索引
|
||||
|
||||
见 [docs/baselines/README.md](./baselines/README.md)。
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build Goose v1.49 CLI for local `goose serve`.
|
||||
# Default includes code-mode (required for TKMind). Use GOOSE_V149_BUILD_MODE=minimal to skip v8.
|
||||
set -euo pipefail
|
||||
|
||||
GOOSED_ROOT="${GOOSED_V149_ROOT:-/Users/john/Project/tkmind_go-v149}"
|
||||
LOG="${GOOSE_V149_BUILD_LOG:-/tmp/goosed-v149-build.log}"
|
||||
MODE="${GOOSE_V149_BUILD_MODE:-codemode}"
|
||||
|
||||
bash "$(dirname "$0")/ensure-goose-v149-vendor.sh"
|
||||
|
||||
cd "${GOOSED_ROOT}"
|
||||
: > "${LOG}"
|
||||
|
||||
echo "[goose-v149-build] mode=${MODE} start $(date -u +%Y-%m-%dT%H:%M:%SZ)" | tee -a "${LOG}"
|
||||
|
||||
case "${MODE}" in
|
||||
full)
|
||||
bash "$(dirname "$0")/ensure-goose-v149-v8.sh" 2>&1 | tee -a "${LOG}"
|
||||
rustup run 1.96.1 cargo build --release --package goose-cli --bin goose 2>&1 | tee -a "${LOG}"
|
||||
;;
|
||||
codemode)
|
||||
bash "$(dirname "$0")/ensure-goose-v149-v8.sh" 2>&1 | tee -a "${LOG}"
|
||||
# code-mode without local-inference (smaller/faster than full default).
|
||||
rustup run 1.96.1 cargo build --release --package goose-cli --bin goose \
|
||||
--no-default-features \
|
||||
--features rustls-tls,aws-providers,system-keyring,code-mode,telemetry 2>&1 | tee -a "${LOG}"
|
||||
;;
|
||||
minimal)
|
||||
echo "[goose-v149-build] WARNING: minimal skips code-mode; not suitable for TKMind prod parity" | tee -a "${LOG}"
|
||||
rustup run 1.96.1 cargo build --release --package goose-cli --bin goose \
|
||||
--no-default-features --features rustls-tls,aws-providers,system-keyring 2>&1 | tee -a "${LOG}"
|
||||
;;
|
||||
*)
|
||||
echo "[goose-v149-build] unknown mode: ${MODE} (use codemode|full|minimal)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[goose-v149-build] finished $(date -u +%Y-%m-%dT%H:%M:%SZ)" | tee -a "${LOG}"
|
||||
"${GOOSED_ROOT}/target/release/goose" --version 2>&1 | tee -a "${LOG}"
|
||||
|
||||
if strings "${GOOSED_ROOT}/target/release/goose" | grep -q 'platform_extensions::code_execution'; then
|
||||
echo "[goose-v149-build] code-mode: code_execution present in binary" | tee -a "${LOG}"
|
||||
elif [[ "${MODE}" != "minimal" ]]; then
|
||||
echo "[goose-v149-build] WARNING: code_execution not found in binary" | tee -a "${LOG}"
|
||||
fi
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Capture Goose v1.49 local baseline samples (status, sessions, optional SSE reply).
|
||||
*/
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Readable } from 'node:stream';
|
||||
import { Agent, fetch } from 'undici';
|
||||
|
||||
const port = process.env.GOOSE_V149_PORT || '18049';
|
||||
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
|
||||
const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret';
|
||||
const base = `https://${host}:${port}`;
|
||||
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
'Usage: node scripts/capture-goose-v149-baseline.mjs [--output <dir>] [--skip-reply]',
|
||||
'',
|
||||
'Captures /status, /sessions, and optional reply SSE terminal event for Phase 0 baseline.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { output: '', skipReply: false };
|
||||
for (let i = 2; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--output' && argv[i + 1]) args.output = argv[++i];
|
||||
else if (arg === '--skip-reply') args.skipReply = true;
|
||||
else if (arg === '--help' || arg === '-h') {
|
||||
console.log(usage());
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
console.error(usage());
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
if (blockedHosts.includes(host)) {
|
||||
console.error(`GOOSE_V149_BASELINE_FAIL: refusing production host ${host}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
|
||||
async function apiFetch(pathname, init = {}) {
|
||||
const headers = { ...(init.headers ?? {}), 'X-Secret-Key': secret };
|
||||
if (init.body && !headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
||||
return fetch(`${base}${pathname}`, { ...init, headers, dispatcher });
|
||||
}
|
||||
|
||||
async function curlJson(url) {
|
||||
const body = execFileSync('curl', ['-sk', '--connect-timeout', '5', url], { encoding: 'utf8' }).trim();
|
||||
return body === 'ok' ? { status: 'ok' } : JSON.parse(body);
|
||||
}
|
||||
|
||||
async function captureReplySample(sessionId) {
|
||||
const requestId = randomUUID();
|
||||
const events = [];
|
||||
const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'text/event-stream' },
|
||||
});
|
||||
if (!eventsResponse.ok || !eventsResponse.body) {
|
||||
throw new Error(`events ${eventsResponse.status}`);
|
||||
}
|
||||
|
||||
const replyResponse = await apiFetch(`/sessions/${sessionId}/reply`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
request_id: requestId,
|
||||
user_message: {
|
||||
role: 'user',
|
||||
created: Date.now(),
|
||||
content: [{ type: 'text', text: 'baseline ping' }],
|
||||
metadata: { userVisible: true, agentVisible: true, displayText: 'baseline ping' },
|
||||
},
|
||||
}),
|
||||
});
|
||||
replyResponse.body?.cancel?.();
|
||||
|
||||
const reader = Readable.fromWeb(eventsResponse.body);
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
const deadline = Date.now() + Number(process.env.GOOSE_V149_BASELINE_REPLY_TIMEOUT_MS || 30_000);
|
||||
|
||||
for await (const chunk of reader) {
|
||||
if (Date.now() > deadline) break;
|
||||
buffer += decoder.decode(chunk, { stream: true });
|
||||
const frames = buffer.split('\n\n');
|
||||
buffer = frames.pop() ?? '';
|
||||
for (const frame of frames) {
|
||||
let data = '';
|
||||
for (const line of frame.split('\n')) {
|
||||
if (line.startsWith('data:')) data += line.slice(5).trim();
|
||||
}
|
||||
if (!data) continue;
|
||||
let event;
|
||||
try {
|
||||
event = JSON.parse(data);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
events.push(event);
|
||||
const routingId = event.chat_request_id ?? event.request_id;
|
||||
if (routingId && routingId !== requestId) continue;
|
||||
if (event.type === 'Finish' || event.type === 'Error') {
|
||||
return { requestId, outcome: event.type.toLowerCase(), events };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { requestId, outcome: 'timeout', events };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
const capturedAt = new Date().toISOString();
|
||||
const status = await curlJson(`${base}/status`);
|
||||
const sessions = await curlJson(`${base}/sessions`);
|
||||
|
||||
const baseline = {
|
||||
schemaVersion: 'goose-v149-baseline-v1',
|
||||
capturedAt,
|
||||
target: base,
|
||||
status,
|
||||
sessionsSummary: {
|
||||
count: Array.isArray(sessions?.sessions) ? sessions.sessions.length : null,
|
||||
sample: Array.isArray(sessions?.sessions) ? sessions.sessions.slice(0, 3) : sessions,
|
||||
},
|
||||
reply: null,
|
||||
};
|
||||
|
||||
if (!args.skipReply) {
|
||||
const startResponse = await apiFetch('/agent/start', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ working_dir: process.cwd() }),
|
||||
});
|
||||
const startBody = await startResponse.json();
|
||||
if (startResponse.ok && startBody?.id) {
|
||||
baseline.reply = await captureReplySample(startBody.id);
|
||||
baseline.reply.sessionId = startBody.id;
|
||||
} else {
|
||||
baseline.reply = { error: 'agent/start failed', status: startResponse.status, body: startBody };
|
||||
}
|
||||
}
|
||||
|
||||
const outputDir = args.output
|
||||
? path.resolve(args.output)
|
||||
: path.join(process.cwd(), 'docs', 'baselines');
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
const outputPath = path.join(
|
||||
outputDir,
|
||||
`goose-v149-runtime-baseline-${capturedAt.replace(/[:.]/g, '-')}.json`,
|
||||
);
|
||||
await fs.writeFile(outputPath, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8');
|
||||
console.log(`GOOSE_V149_BASELINE_OK: ${outputPath}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_BASELINE_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 1 smoke: POST /agent/start on local Goose v1.49 candidate.
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const port = process.env.GOOSE_V149_PORT || '18049';
|
||||
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
|
||||
const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret';
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || process.cwd();
|
||||
|
||||
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
|
||||
if (blockedHosts.includes(host)) {
|
||||
console.error(`GOOSE_V149_AGENT_START_FAIL: refusing production host ${host}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = execFileSync(
|
||||
'curl',
|
||||
[
|
||||
'-sk',
|
||||
'--connect-timeout',
|
||||
'10',
|
||||
'-X',
|
||||
'POST',
|
||||
'-H',
|
||||
'Content-Type: application/json',
|
||||
'-H',
|
||||
`X-Secret-Key: ${secret}`,
|
||||
'-d',
|
||||
JSON.stringify({ working_dir: workingDir }),
|
||||
`https://${host}:${port}/agent/start`,
|
||||
],
|
||||
{ encoding: 'utf8' },
|
||||
).trim();
|
||||
|
||||
const session = JSON.parse(body);
|
||||
if (!session?.id) {
|
||||
console.error(`GOOSE_V149_AGENT_START_FAIL: missing session id: ${body}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`GOOSE_V149_AGENT_START_OK: session=${session.id}`);
|
||||
} catch (error) {
|
||||
console.error(`GOOSE_V149_AGENT_START_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Run all local Goose v1.49 smoke checks in sequence.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const checks = [
|
||||
{ name: 'memory-policy', script: 'check-goosed-v149-memory-policy.mjs', required: true },
|
||||
{ name: 'local', script: 'check-goosed-v149-local.mjs', required: true },
|
||||
{ name: 'agent-start', script: 'check-goosed-v149-agent-start.mjs', required: true },
|
||||
{ name: 'harness', script: 'check-goosed-v149-harness.mjs', required: true },
|
||||
{ name: 'session-reconcile', script: 'check-goosed-v149-session-reconcile.mjs', required: true },
|
||||
{ name: 'config-read', script: 'check-goosed-v149-config-read.mjs', required: true },
|
||||
{ name: 'web-smoke', script: 'check-goosed-v149-web-smoke.mjs', required: true },
|
||||
{ name: 'skills-smoke', script: 'check-goosed-v149-skills-smoke.mjs', required: true },
|
||||
{
|
||||
name: 'deepseek-tools',
|
||||
script: 'check-goosed-v149-deepseek-tools.mjs',
|
||||
required: Boolean(process.env.DEEPSEEK_API_KEY || process.env.GOOSE_V149_TEST_API_KEY),
|
||||
},
|
||||
{
|
||||
name: 'thinking-preservation',
|
||||
script: 'check-goosed-v149-thinking-preservation.mjs',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'message-sanitize',
|
||||
script: 'compare-goose-v149-message-sanitize.mjs',
|
||||
required: true,
|
||||
},
|
||||
{ name: 'reply-smoke', script: 'check-goosed-v149-reply-smoke.mjs', required: true },
|
||||
{ name: 'sandbox-fs', script: 'check-goosed-v149-sandbox-fs.mjs', required: true },
|
||||
{ name: 'executors', script: 'check-goosed-v149-executors.mjs', required: true },
|
||||
{ name: 'memory-loop', script: 'check-goosed-v149-memory-loop.mjs', required: true },
|
||||
{ name: 'portal-smoke', script: 'check-goosed-v149-portal-smoke.mjs', required: false },
|
||||
{
|
||||
name: 'provider',
|
||||
script: 'check-goosed-v149-provider.mjs',
|
||||
required: Boolean(process.env.DEEPSEEK_API_KEY || process.env.GOOSE_V149_TEST_API_KEY),
|
||||
},
|
||||
];
|
||||
|
||||
function runCheck(check) {
|
||||
const scriptPath = path.join(root, 'scripts', check.script);
|
||||
const result = spawnSync(process.execPath, [scriptPath], {
|
||||
cwd: root,
|
||||
env: process.env,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
return {
|
||||
...check,
|
||||
ok: result.status === 0,
|
||||
status: result.status,
|
||||
stdout: result.stdout?.trim() ?? '',
|
||||
stderr: result.stderr?.trim() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const results = checks.map(runCheck);
|
||||
const failedRequired = results.filter((item) => item.required && !item.ok);
|
||||
const failedOptional = results.filter((item) => !item.required && !item.ok);
|
||||
|
||||
for (const item of results) {
|
||||
const label = item.required ? 'required' : 'optional';
|
||||
console.log(`[goose-v149-all] ${item.ok ? 'OK' : 'FAIL'} (${label}) ${item.name}`);
|
||||
if (item.stdout) console.log(item.stdout);
|
||||
if (!item.ok && item.stderr) console.error(item.stderr);
|
||||
}
|
||||
|
||||
if (failedRequired.length) {
|
||||
console.error(
|
||||
`GOOSE_V149_ALL_FAIL: ${failedRequired.map((item) => item.name).join(', ')}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (failedOptional.length) {
|
||||
console.log(
|
||||
`GOOSE_V149_ALL_OK_WITH_SKIPS: optional skipped/failed: ${failedOptional.map((item) => item.name).join(', ')}`,
|
||||
);
|
||||
} else {
|
||||
console.log('GOOSE_V149_ALL_OK');
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify Portal boots with Goose v1.49 canary (.env.local block) and routes to :18049.
|
||||
* Spawns an isolated Portal on H5_PORT=8083 to avoid disrupting the main dev server.
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
|
||||
import { describeGooseCanaryConfig, resolveGooseApiTargetsFromEnv, applyGooseV149CanaryBlockEnv } from './goose-v149-canary.mjs';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const portalPort = Number(process.env.GOOSE_V149_CANARY_PORTAL_PORT ?? 8083);
|
||||
const baseUrl = `http://127.0.0.1:${portalPort}`;
|
||||
const startupTimeoutMs = Number(process.env.GOOSE_V149_CANARY_PORTAL_STARTUP_MS ?? 45_000);
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function waitForPortal(fetchImpl, timeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetchImpl(`${baseUrl}/auth/status`);
|
||||
if (response.ok) return true;
|
||||
} catch {
|
||||
// retry
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function requestJson(fetchImpl, url, { method = 'GET', headers = {}, body } = {}) {
|
||||
const response = await fetchImpl(url, {
|
||||
method,
|
||||
headers: {
|
||||
...(body ? { 'Content-Type': 'application/json' } : {}),
|
||||
...headers,
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
return { ok: response.ok, status: response.status, json, text, headers: response.headers };
|
||||
}
|
||||
|
||||
function readCanaryBlock() {
|
||||
const envFile = path.join(root, '.env.local');
|
||||
if (!fs.existsSync(envFile)) return false;
|
||||
return fs.readFileSync(envFile, 'utf8').includes('# GOOSE_V149_CANARY_BEGIN');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!readCanaryBlock()) {
|
||||
throw new Error('canary block missing; run: bash scripts/switch-goose-v149-canary.sh on all');
|
||||
}
|
||||
|
||||
const env = { ...process.env };
|
||||
loadMemindEnvFiles(root, env);
|
||||
applyGooseV149CanaryBlockEnv(env, root);
|
||||
env.H5_PORT = String(portalPort);
|
||||
env.MEMIND_WORKSPACE_MAINTENANCE = '0';
|
||||
|
||||
const canary = resolveGooseApiTargetsFromEnv(env);
|
||||
if (!canary || canary.primary !== 'https://127.0.0.1:18049') {
|
||||
throw new Error(`canary env not pointing to v149: ${JSON.stringify(describeGooseCanaryConfig(env))}`);
|
||||
}
|
||||
|
||||
const server = spawn(process.execPath, ['server.mjs'], {
|
||||
cwd: root,
|
||||
env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let logTail = '';
|
||||
server.stdout?.on('data', (chunk) => {
|
||||
logTail = `${logTail}${chunk}`.slice(-4000);
|
||||
});
|
||||
server.stderr?.on('data', (chunk) => {
|
||||
logTail = `${logTail}${chunk}`.slice(-4000);
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
if (!server.killed) server.kill('SIGTERM');
|
||||
};
|
||||
process.on('exit', cleanup);
|
||||
process.on('SIGINT', () => {
|
||||
cleanup();
|
||||
process.exit(130);
|
||||
});
|
||||
|
||||
const ready = await waitForPortal(globalThis.fetch, startupTimeoutMs);
|
||||
if (!ready) {
|
||||
cleanup();
|
||||
throw new Error(`isolated Portal did not start on :${portalPort} within ${startupTimeoutMs}ms\n${logTail}`);
|
||||
}
|
||||
|
||||
const runtime = await requestJson(globalThis.fetch, `${baseUrl}/api/runtime/status`);
|
||||
const targetStatuses = runtime.json?.targets ?? [];
|
||||
const targets = Array.isArray(targetStatuses)
|
||||
? targetStatuses.map((item) => item.target ?? item).filter(Boolean)
|
||||
: [];
|
||||
const primaryFromLog = logTail.includes('Goose v1.49 canary mode=all');
|
||||
const hasV149 = targets.some((item) => String(item).includes(':18049')) || primaryFromLog;
|
||||
|
||||
if (!runtime.ok) {
|
||||
cleanup();
|
||||
throw new Error(`runtime status failed: ${runtime.status} ${runtime.text?.slice?.(0, 300)}`);
|
||||
}
|
||||
|
||||
const memory = runtime.json?.memory ?? null;
|
||||
if (!hasV149) {
|
||||
cleanup();
|
||||
throw new Error(`canary Portal must route to :18049, targets=${JSON.stringify(targets)}`);
|
||||
}
|
||||
|
||||
if (memory?.backend !== 'legacy') {
|
||||
const configSource = memory?.configSource ?? 'unknown';
|
||||
if (configSource === 'admin-db' || configSource === 'admin-db-cache') {
|
||||
console.log(
|
||||
`GOOSE_V149_CANARY_PORTAL_WARN: memory.backend=${memory.backend} from ${configSource}; goose v1.49 routing verified`,
|
||||
);
|
||||
} else {
|
||||
cleanup();
|
||||
throw new Error(`canary Portal must use memory.backend=legacy, got ${memory?.backend} (${configSource})`);
|
||||
}
|
||||
}
|
||||
|
||||
const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`;
|
||||
const user = {
|
||||
username: `gv149c_${suffix}`,
|
||||
password: 'GooseV149-Canary-2026',
|
||||
email: `gv149c-${suffix}@example.test`,
|
||||
displayName: `gv149c_${suffix}`,
|
||||
};
|
||||
|
||||
const register = await requestJson(globalThis.fetch, `${baseUrl}/auth/register`, {
|
||||
method: 'POST',
|
||||
body: user,
|
||||
});
|
||||
if (!register.ok) {
|
||||
cleanup();
|
||||
throw new Error(`register failed: ${register.status}`);
|
||||
}
|
||||
|
||||
const login = await requestJson(globalThis.fetch, `${baseUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
body: { username: user.username, password: user.password },
|
||||
});
|
||||
const cookie = login.headers?.get?.('set-cookie')?.split(';', 1)[0] ?? null;
|
||||
if (!login.ok || !cookie) {
|
||||
cleanup();
|
||||
throw new Error(`login failed: ${login.status}`);
|
||||
}
|
||||
|
||||
const started = await requestJson(globalThis.fetch, `${baseUrl}/api/agent/start`, {
|
||||
method: 'POST',
|
||||
headers: { Cookie: cookie },
|
||||
body: {},
|
||||
});
|
||||
const sessionId = started.json?.id ?? null;
|
||||
if (!started.ok || !sessionId) {
|
||||
cleanup();
|
||||
throw new Error(`agent/start failed: ${started.status} ${started.text?.slice?.(0, 200)}`);
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID();
|
||||
const run = await requestJson(globalThis.fetch, `${baseUrl}/api/agent/runs`, {
|
||||
method: 'POST',
|
||||
headers: { Cookie: cookie },
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
request_id: requestId,
|
||||
user_message: {
|
||||
role: 'user',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
content: [{ type: 'text', text: 'canary ping' }],
|
||||
metadata: { userVisible: true, agentVisible: true, displayText: 'canary ping' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cleanup();
|
||||
await sleep(300);
|
||||
|
||||
if (!run.ok || run.status !== 202) {
|
||||
throw new Error(`agent run failed: ${run.status} ${run.text?.slice?.(0, 200)}`);
|
||||
}
|
||||
|
||||
console.log(`GOOSE_V149_CANARY_PORTAL_OK: isolated Portal :${portalPort} session=${sessionId}`);
|
||||
console.log(` memory.backend=${memory.backend} canaryPrimary=${canary.primary} targets=${targets.join(',') || 'log-confirmed'}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_CANARY_PORTAL_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify v1.49 exposes POST /config/read (Portal readConfig contract).
|
||||
*/
|
||||
import { createV149Client } from './goose-v149-sse.mjs';
|
||||
|
||||
const client = createV149Client();
|
||||
|
||||
async function readKey(key, isSecret = false) {
|
||||
const body = await client.apiJson('/config/read', { key, is_secret: isSecret });
|
||||
return body;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const provider = await readKey('GOOSE_PROVIDER');
|
||||
const model = await readKey('GOOSE_MODEL');
|
||||
const missing = await readKey('__goose_v149_config_read_missing_probe__');
|
||||
if (missing !== null) {
|
||||
throw new Error(`missing key should return null, got ${JSON.stringify(missing)}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`GOOSE_V149_CONFIG_READ_OK: provider=${JSON.stringify(provider)} model=${JSON.stringify(model)}`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_CONFIG_READ_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 2: verify Finish SSE carries token_state suitable for Portal billing.
|
||||
* Requires DEEPSEEK_API_KEY (or GOOSE_V149_TEST_API_KEY).
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { normalizeTokenState } from '../billing.mjs';
|
||||
import { enrichTokenStateForBilling, resolveBillingTokenState } from '../billing-token-state.mjs';
|
||||
import { collectReplyEvents, createV149Client } from './goose-v149-sse.mjs';
|
||||
|
||||
const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let value = trimmed.slice(eq + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"'))
|
||||
|| (value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (!process.env[key]) process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFile(path.join(memindRoot, '.env'));
|
||||
|
||||
const client = createV149Client();
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || memindRoot;
|
||||
const providerId = process.env.GOOSE_V149_PROVIDER_ID || 'custom_tkmind_relay_deepseek';
|
||||
const model = process.env.GOOSE_V149_PROVIDER_MODEL || 'deepseek-chat';
|
||||
const apiKey = process.env.DEEPSEEK_API_KEY || process.env.GOOSE_V149_TEST_API_KEY || '';
|
||||
|
||||
async function upsertProvider() {
|
||||
const body = {
|
||||
provider_name: providerId,
|
||||
api_key: apiKey,
|
||||
api_url: process.env.GOOSE_V149_PROVIDER_API_URL
|
||||
|| process.env.DEEPSEEK_API_BASE_URL
|
||||
|| 'https://api.deepseek.com/v1',
|
||||
models: [model],
|
||||
relay_provider: 'deepseek',
|
||||
};
|
||||
let response = await client.apiFetch(`/config/custom-providers/${encodeURIComponent(providerId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!response.ok && (response.status === 404 || /not found/i.test(await response.text()))) {
|
||||
await client.apiJson('/config/custom-providers', body);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!apiKey) {
|
||||
console.log('GOOSE_V149_COST_SMOKE_SKIP: no DEEPSEEK_API_KEY');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await upsertProvider();
|
||||
const session = await client.apiJson('/agent/start', { working_dir: workingDir });
|
||||
if (!session?.id) throw new Error('missing session id');
|
||||
|
||||
await client.apiJson('/agent/update_provider', {
|
||||
session_id: session.id,
|
||||
provider: providerId,
|
||||
model,
|
||||
});
|
||||
|
||||
const requestId = randomUUID();
|
||||
const result = await collectReplyEvents({
|
||||
client,
|
||||
sessionId: session.id,
|
||||
requestId,
|
||||
userMessage: {
|
||||
role: 'user',
|
||||
created: Date.now(),
|
||||
content: [{ type: 'text', text: 'Reply with exactly one word: pong' }],
|
||||
metadata: { userVisible: true, agentVisible: true, displayText: 'cost smoke ping' },
|
||||
},
|
||||
});
|
||||
|
||||
if (result.outcome !== 'finish') {
|
||||
throw new Error(`expected Finish, got ${result.outcome}: ${result.errorEvent?.error ?? 'timeout'}`);
|
||||
}
|
||||
|
||||
const rawTokenState = result.finishEvent?.token_state ?? null;
|
||||
if (!rawTokenState) {
|
||||
throw new Error('Finish event missing token_state');
|
||||
}
|
||||
|
||||
const normalized = normalizeTokenState(rawTokenState);
|
||||
const hasTokens =
|
||||
normalized.accumulatedInputTokens > 0
|
||||
|| normalized.accumulatedOutputTokens > 0
|
||||
|| normalized.inputTokens > 0
|
||||
|| normalized.outputTokens > 0;
|
||||
if (!hasTokens) {
|
||||
throw new Error(`Finish token_state has no token counts: ${JSON.stringify(normalized)}`);
|
||||
}
|
||||
|
||||
const billingState = await resolveBillingTokenState(rawTokenState, { sessionId: session.id });
|
||||
const enriched = enrichTokenStateForBilling(rawTokenState, {}, {
|
||||
...process.env,
|
||||
H5_USE_BACKEND_COST: process.env.H5_USE_BACKEND_COST ?? '1',
|
||||
H5_COST_ESTIMATE_FROM_TOKENS: process.env.H5_COST_ESTIMATE_FROM_TOKENS ?? '1',
|
||||
});
|
||||
|
||||
const outputDir = path.join(memindRoot, 'docs', 'baselines');
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const evidencePath = path.join(
|
||||
outputDir,
|
||||
`goose-v149-cost-evidence-${new Date().toISOString().replace(/[:.]/g, '-')}.json`,
|
||||
);
|
||||
const evidence = {
|
||||
schemaVersion: 'goose-v149-cost-evidence-v1',
|
||||
capturedAt: new Date().toISOString(),
|
||||
sessionId: session.id,
|
||||
requestId,
|
||||
finishReason: result.finishEvent?.reason ?? null,
|
||||
tokenState: normalized,
|
||||
billingState: {
|
||||
accumulatedInputTokens: billingState.accumulatedInputTokens,
|
||||
accumulatedOutputTokens: billingState.accumulatedOutputTokens,
|
||||
accumulatedCost: billingState.accumulatedCost,
|
||||
},
|
||||
enrichedCost: enriched.accumulatedCost,
|
||||
};
|
||||
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
|
||||
|
||||
console.log(`GOOSE_V149_COST_SMOKE_OK: session=${session.id}`);
|
||||
console.log(
|
||||
` tokens in=${normalized.accumulatedInputTokens} out=${normalized.accumulatedOutputTokens} cost=${billingState.accumulatedCost ?? enriched.accumulatedCost ?? 'null'}`,
|
||||
);
|
||||
console.log(` evidence=${evidencePath}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_COST_SMOKE_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 2: DeepSeek provider completes a tool round (todo_write) on v1.49.
|
||||
* Requires DEEPSEEK_API_KEY (or GOOSE_V149_TEST_API_KEY).
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { collectReplyEvents, createV149Client } from './goose-v149-sse.mjs';
|
||||
|
||||
const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let value = trimmed.slice(eq + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"'))
|
||||
|| (value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (!process.env[key]) process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFile(path.join(memindRoot, '.env'));
|
||||
loadEnvFile(path.join(memindRoot, '.env.local'));
|
||||
|
||||
const client = createV149Client();
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || memindRoot;
|
||||
const providerId = process.env.GOOSE_V149_PROVIDER_ID || 'custom_tkmind_relay_deepseek';
|
||||
const model = process.env.GOOSE_V149_PROVIDER_MODEL || 'deepseek-chat';
|
||||
const apiKey = process.env.DEEPSEEK_API_KEY || process.env.GOOSE_V149_TEST_API_KEY || '';
|
||||
const timeoutMs = Number(process.env.GOOSE_V149_DEEPSEEK_TOOLS_TIMEOUT_MS || 120_000);
|
||||
|
||||
function contentBlocks(event) {
|
||||
if (event?.type === 'Message') return event.message?.content ?? [];
|
||||
if (event?.type === 'UpdateConversation') {
|
||||
const conversation = event.conversation ?? [];
|
||||
return conversation.flatMap((message) => message?.content ?? []);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function blockLooksLikeTool(block = {}) {
|
||||
const type = String(block.type ?? block.content_type ?? '').toLowerCase();
|
||||
if (type === 'toolrequest' || type === 'toolresponse') return true;
|
||||
if (type.includes('tool')) return true;
|
||||
if (block.tool_call || block.toolCall || block.tool_request || block.toolRequest) return true;
|
||||
if (block.name && (block.arguments || block.input)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveToolName(block = {}) {
|
||||
const direct =
|
||||
block.tool_call?.name
|
||||
?? block.toolCall?.name
|
||||
?? block.name
|
||||
?? block.tool_request?.tool_call?.name
|
||||
?? block.toolRequest?.tool_call?.name;
|
||||
if (direct) return String(direct);
|
||||
|
||||
const nested =
|
||||
block.toolCall?.value?.name
|
||||
?? block.tool_call?.value?.name
|
||||
?? block.toolRequest?.tool_call?.value?.name
|
||||
?? block.tool_request?.tool_call?.value?.name;
|
||||
return nested ? String(nested) : null;
|
||||
}
|
||||
|
||||
function toolNamesFromEvents(events = []) {
|
||||
const names = new Set();
|
||||
for (const event of events) {
|
||||
for (const block of contentBlocks(event)) {
|
||||
if (!blockLooksLikeTool(block)) continue;
|
||||
const name = resolveToolName(block);
|
||||
if (name) names.add(name);
|
||||
}
|
||||
}
|
||||
return [...names];
|
||||
}
|
||||
|
||||
async function upsertProvider() {
|
||||
const body = {
|
||||
engine: 'openai',
|
||||
display_name: 'tkmind_relay_deepseek',
|
||||
api_url:
|
||||
process.env.GOOSE_V149_PROVIDER_API_URL
|
||||
|| process.env.DEEPSEEK_API_BASE_URL
|
||||
|| 'https://api.deepseek.com/v1',
|
||||
api_key: apiKey,
|
||||
models: [model, 'deepseek-reasoner'],
|
||||
supports_streaming: true,
|
||||
requires_auth: true,
|
||||
preserves_thinking: false,
|
||||
};
|
||||
let response = await client.apiFetch(
|
||||
`/config/custom-providers/${encodeURIComponent(providerId)}`,
|
||||
{ method: 'PUT', body: JSON.stringify(body) },
|
||||
);
|
||||
if (!response.ok && (response.status === 404 || /not found/i.test(await response.text()))) {
|
||||
await client.apiJson('/config/custom-providers', body);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!apiKey) {
|
||||
console.log('GOOSE_V149_DEEPSEEK_TOOLS_SKIP: no DEEPSEEK_API_KEY');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await upsertProvider();
|
||||
|
||||
const session = await client.apiJson('/agent/start', {
|
||||
working_dir: workingDir,
|
||||
extension_overrides: [
|
||||
{
|
||||
type: 'platform',
|
||||
name: 'todo',
|
||||
description: 'todo list for tool-round smoke',
|
||||
display_name: 'Todo',
|
||||
bundled: false,
|
||||
available_tools: ['todo_write'],
|
||||
},
|
||||
{
|
||||
type: 'platform',
|
||||
name: 'skills',
|
||||
description: 'skills',
|
||||
available_tools: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!session?.id) throw new Error('missing session id');
|
||||
|
||||
await client.apiJson('/agent/update_provider', {
|
||||
session_id: session.id,
|
||||
provider: providerId,
|
||||
model,
|
||||
});
|
||||
|
||||
const requestId = randomUUID();
|
||||
const result = await collectReplyEvents({
|
||||
client,
|
||||
sessionId: session.id,
|
||||
requestId,
|
||||
timeoutMs,
|
||||
userMessage: {
|
||||
role: 'user',
|
||||
created: Date.now(),
|
||||
content: [{
|
||||
type: 'text',
|
||||
text:
|
||||
'You must call the todo_write tool exactly once with content "- [ ] goose v149 deepseek tool smoke". '
|
||||
+ 'After the tool succeeds, reply with exactly one word: done',
|
||||
}],
|
||||
metadata: {
|
||||
userVisible: true,
|
||||
agentVisible: true,
|
||||
displayText: 'deepseek tool smoke',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (result.outcome !== 'finish') {
|
||||
throw new Error(
|
||||
`expected Finish, got ${result.outcome}: ${result.errorEvent?.error ?? 'timeout'}`,
|
||||
);
|
||||
}
|
||||
|
||||
const toolNames = toolNamesFromEvents(result.events);
|
||||
const hasTodoTool = toolNames.some((name) => /todo_write/i.test(name));
|
||||
if (!hasTodoTool) {
|
||||
throw new Error(
|
||||
`no todo_write tool activity in SSE events; tools=${toolNames.join(',') || 'none'}`,
|
||||
);
|
||||
}
|
||||
|
||||
const outputDir = path.join(memindRoot, 'docs', 'baselines');
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const evidencePath = path.join(
|
||||
outputDir,
|
||||
`goose-v149-deepseek-tools-evidence-${new Date().toISOString().replace(/[:.]/g, '-')}.json`,
|
||||
);
|
||||
fs.writeFileSync(
|
||||
evidencePath,
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 'goose-v149-deepseek-tools-v1',
|
||||
capturedAt: new Date().toISOString(),
|
||||
sessionId: session.id,
|
||||
requestId,
|
||||
providerId,
|
||||
model,
|
||||
toolNames,
|
||||
finishReason: result.finishEvent?.reason ?? null,
|
||||
eventTypes: result.events.map((event) => event.type),
|
||||
}, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
|
||||
console.log(`GOOSE_V149_DEEPSEEK_TOOLS_OK: session=${session.id} tools=${toolNames.join(',')}`);
|
||||
console.log(` evidence=${evidencePath}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_DEEPSEEK_TOOLS_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Smoke: aider + openhands platform extensions load on local Goose v1.49.
|
||||
*/
|
||||
import { Agent, fetch } from 'undici';
|
||||
|
||||
const port = process.env.GOOSE_V149_PORT || '18049';
|
||||
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
|
||||
const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret';
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || process.cwd();
|
||||
const pollMs = Number(process.env.GOOSE_V149_EXECUTOR_POLL_MS || 500);
|
||||
const timeoutMs = Number(process.env.GOOSE_V149_EXECUTOR_TIMEOUT_MS || 30_000);
|
||||
|
||||
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
|
||||
if (blockedHosts.includes(host)) {
|
||||
console.error(`GOOSE_V149_EXECUTOR_FAIL: refusing production host ${host}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const base = `https://${host}:${port}`;
|
||||
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
|
||||
async function apiFetch(pathname, init = {}) {
|
||||
const headers = {
|
||||
...(init.headers ?? {}),
|
||||
'X-Secret-Key': secret,
|
||||
};
|
||||
if (init.body && !headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
return fetch(`${base}${pathname}`, {
|
||||
...init,
|
||||
headers,
|
||||
dispatcher,
|
||||
});
|
||||
}
|
||||
|
||||
async function apiJson(pathname, body) {
|
||||
const response = await apiFetch(pathname, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`${pathname} ${response.status}: ${text.slice(0, 800)}`);
|
||||
}
|
||||
return text.trim() ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
async function waitForTool(sessionId, extensionName, toolName) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const response = await apiFetch(
|
||||
`/agent/tools?session_id=${encodeURIComponent(sessionId)}&extension_name=${encodeURIComponent(extensionName)}`,
|
||||
);
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`/agent/tools ${response.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
const tools = JSON.parse(text);
|
||||
if (tools.some((tool) => tool.name === toolName)) {
|
||||
return tools.map((tool) => tool.name);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
||||
}
|
||||
throw new Error(`${toolName} not ready within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await apiJson('/agent/start', {
|
||||
working_dir: workingDir,
|
||||
extension_overrides: [
|
||||
{
|
||||
type: 'platform',
|
||||
name: 'aider',
|
||||
description: 'Aider coding delegate',
|
||||
available_tools: ['code'],
|
||||
},
|
||||
{
|
||||
type: 'platform',
|
||||
name: 'openhands',
|
||||
description: 'OpenHands coding delegate',
|
||||
available_tools: ['code'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!session?.id) {
|
||||
throw new Error('missing session id from /agent/start');
|
||||
}
|
||||
|
||||
const aiderTools = await waitForTool(session.id, 'aider', 'aider__code');
|
||||
const openHandsTools = await waitForTool(session.id, 'openhands', 'openhands__code');
|
||||
|
||||
console.log(
|
||||
`GOOSE_V149_EXECUTOR_OK: session=${session.id} aider=${aiderTools.length} openhands=${openHandsTools.length}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`GOOSE_V149_EXECUTOR_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Smoke harness bootstrap/remember on local Goose v1.49 candidate.
|
||||
*/
|
||||
import { Agent, fetch } from 'undici';
|
||||
|
||||
const port = process.env.GOOSE_V149_PORT || '18049';
|
||||
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
|
||||
const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret';
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || process.cwd();
|
||||
const base = `https://${host}:${port}`;
|
||||
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
|
||||
|
||||
if (blockedHosts.includes(host)) {
|
||||
console.error(`GOOSE_V149_HARNESS_FAIL: refusing production host ${host}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
|
||||
async function apiJson(pathname, body) {
|
||||
const response = await fetch(`${base}${pathname}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': secret,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
dispatcher,
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`${pathname} ${response.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
return text.trim() ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const started = await apiJson('/agent/start', { working_dir: workingDir });
|
||||
if (!started?.id) throw new Error('missing session id from /agent/start');
|
||||
|
||||
const bootstrap = await apiJson('/agent/harness_bootstrap', {
|
||||
sessionId: started.id,
|
||||
force: true,
|
||||
query: 'project architecture conventions',
|
||||
});
|
||||
if (typeof bootstrap.summary !== 'string') {
|
||||
throw new Error(`harness_bootstrap missing summary: ${JSON.stringify(bootstrap)}`);
|
||||
}
|
||||
|
||||
const remember = await apiJson('/agent/harness_remember', {
|
||||
sessionId: started.id,
|
||||
title: 'goose-v149 harness smoke',
|
||||
content: 'v1.49 harness remember smoke marker',
|
||||
});
|
||||
if (typeof remember.remembered !== 'boolean') {
|
||||
throw new Error(`harness_remember missing remembered flag: ${JSON.stringify(remember)}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`GOOSE_V149_HARNESS_OK: session=${started.id} bootstrapSource=${bootstrap.source} remembered=${remember.remembered}`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_HARNESS_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 0 health check for local Goose v1.49 candidate (loopback only).
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const port = process.env.GOOSE_V149_PORT || '18049';
|
||||
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
|
||||
const url = `https://${host}:${port}/status`;
|
||||
|
||||
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
|
||||
if (blockedHosts.includes(host)) {
|
||||
console.error(`GOOSE_V149_CHECK_FAIL: refusing production host ${host}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = execFileSync('curl', ['-sk', '--connect-timeout', '5', url], { encoding: 'utf8' }).trim();
|
||||
if (body !== 'ok') {
|
||||
console.error(`GOOSE_V149_CHECK_FAIL: unexpected status body: ${body}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`GOOSE_V149_CHECK_OK: ${url} -> ${body}`);
|
||||
|
||||
const sessionsUrl = `https://${host}:${port}/sessions`;
|
||||
const sessionsBody = execFileSync('curl', ['-sk', '--connect-timeout', '5', sessionsUrl], {
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
const sessions = JSON.parse(sessionsBody);
|
||||
if (!Array.isArray(sessions.sessions)) {
|
||||
console.error(`GOOSE_V149_CHECK_FAIL: /sessions missing sessions array: ${sessionsBody}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`GOOSE_V149_CHECK_OK: ${sessionsUrl} -> ${sessions.sessions.length} session(s)`);
|
||||
} catch (error) {
|
||||
console.error(`GOOSE_V149_CHECK_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 2: Memory fail-open drill for v1.49 canary profile (legacy owner).
|
||||
* Verifies resolve failures degrade without throwing when MEMORY_FAIL_OPEN=1.
|
||||
*/
|
||||
import { createMemoryV2 } from '../memory-v2.mjs';
|
||||
|
||||
const silentLogger = () => ({
|
||||
warn() {},
|
||||
info() {},
|
||||
log() {},
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const memory = createMemoryV2({
|
||||
logger: silentLogger(),
|
||||
legacyMemoryService: {
|
||||
listMemories: async () => {
|
||||
throw new Error('simulated legacy resolve timeout');
|
||||
},
|
||||
},
|
||||
env: {
|
||||
MEMORY_ENABLED: '1',
|
||||
MEMORY_FAIL_OPEN: '1',
|
||||
MEMORY_BACKEND: 'legacy',
|
||||
MEMORY_VECTOR_ENABLED: '0',
|
||||
MEMORY_AGENT_INJECTION_MODE: 'off',
|
||||
},
|
||||
});
|
||||
|
||||
const status = memory.getStatus();
|
||||
if (status.backend !== 'legacy') {
|
||||
throw new Error(`expected legacy backend for v1.49 profile, got ${status.backend}`);
|
||||
}
|
||||
|
||||
const resolved = await memory.resolve({ userId: 'u-failopen', query: 'marker' });
|
||||
if (!resolved.degraded) {
|
||||
throw new Error(`expected degraded resolve, got ${JSON.stringify(resolved)}`);
|
||||
}
|
||||
if (!Array.isArray(resolved.memories) || resolved.memories.length !== 0) {
|
||||
throw new Error(`fail-open should return empty memories, got ${JSON.stringify(resolved.memories)}`);
|
||||
}
|
||||
|
||||
console.log('GOOSE_V149_MEMORY_FAILOPEN_OK: legacy resolve failure degraded without throwing');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_MEMORY_FAILOPEN_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 1: legacy Memory owner write → compact → new-session resolve (loopback MySQL).
|
||||
*/
|
||||
import crypto from 'node:crypto';
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
import { createConversationMemoryService } from '../conversation-memory.mjs';
|
||||
import { createMemoryV2 } from '../memory-v2.mjs';
|
||||
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
|
||||
|
||||
const BLOCKED_DB_HOSTS = ['58.38.22.103', '120.26.184.105', 'rds.aliyuncs.com'];
|
||||
const MARKER = `goose-v149-memory-loop-${Date.now()}`;
|
||||
|
||||
function assertLocalDatabase(env = process.env) {
|
||||
const raw = String(env.DATABASE_URL ?? '').trim();
|
||||
if (raw) {
|
||||
const hostname = new URL(raw.replace(/^mysql:/, 'http:')).hostname.toLowerCase();
|
||||
for (const blocked of BLOCKED_DB_HOSTS) {
|
||||
if (hostname.includes(blocked)) throw new Error(`Refusing production DATABASE_URL host: ${hostname}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const host = String(env.MYSQL_HOST ?? '127.0.0.1').trim().toLowerCase();
|
||||
for (const blocked of BLOCKED_DB_HOSTS) {
|
||||
if (host.includes(blocked)) throw new Error(`Refusing production MYSQL_HOST: ${host}`);
|
||||
}
|
||||
}
|
||||
|
||||
function createPool() {
|
||||
loadMemindEnvFiles(process.cwd());
|
||||
assertLocalDatabase(process.env);
|
||||
const options = { connectionLimit: 3 };
|
||||
if (process.env.DATABASE_URL) {
|
||||
return mysql.createPool({ uri: process.env.DATABASE_URL, ...options });
|
||||
}
|
||||
return mysql.createPool({
|
||||
host: process.env.MYSQL_HOST ?? '127.0.0.1',
|
||||
port: Number(process.env.MYSQL_PORT ?? 3306),
|
||||
user: process.env.MYSQL_USER ?? 'boot',
|
||||
password: process.env.MYSQL_PASSWORD ?? '',
|
||||
database: process.env.MYSQL_DATABASE ?? 'tkmind',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveUserId(pool) {
|
||||
const explicit = String(process.env.GOOSE_V149_MEMORY_LOOP_USER_ID ?? '').trim();
|
||||
if (explicit) return explicit;
|
||||
const [rows] = await pool.query(`SELECT id FROM h5_users ORDER BY created_at DESC LIMIT 1`);
|
||||
const userId = rows[0]?.id ? String(rows[0].id) : '';
|
||||
if (!userId) throw new Error('No h5_users row found; set GOOSE_V149_MEMORY_LOOP_USER_ID');
|
||||
return userId;
|
||||
}
|
||||
|
||||
function buildMockLlmProvider() {
|
||||
return {
|
||||
async createChatCompletion() {
|
||||
return {
|
||||
ok: true,
|
||||
reply: JSON.stringify({
|
||||
memories: [{
|
||||
label: 'fact',
|
||||
text: `${MARKER} 用户偏好 v1.49 本地记忆闭环测试`,
|
||||
}],
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureSession(pool, userId, sessionId) {
|
||||
const ts = Date.now();
|
||||
await pool.query(
|
||||
`INSERT INTO h5_user_sessions (agent_session_id, user_id, origin, created_at)
|
||||
VALUES (?, ?, 'h5', ?)
|
||||
ON DUPLICATE KEY UPDATE user_id = VALUES(user_id)`,
|
||||
[sessionId, userId, ts],
|
||||
);
|
||||
}
|
||||
|
||||
async function cleanup(pool, userId, sessionIds) {
|
||||
await pool.query(
|
||||
`DELETE FROM h5_user_memory_items WHERE user_id = ? AND memory_text LIKE ?`,
|
||||
[userId, `%${MARKER}%`],
|
||||
);
|
||||
if (sessionIds.length) {
|
||||
await pool.query(
|
||||
`DELETE FROM h5_conversation_messages WHERE user_id = ? AND agent_session_id IN (?)`,
|
||||
[userId, sessionIds],
|
||||
);
|
||||
await pool.query(
|
||||
`DELETE FROM h5_user_sessions WHERE user_id = ? AND agent_session_id IN (?)`,
|
||||
[userId, sessionIds],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const env = {
|
||||
...process.env,
|
||||
MEMORY_ENABLED: '1',
|
||||
MEMORY_BACKEND: 'legacy',
|
||||
MEMORY_VECTOR_ENABLED: '0',
|
||||
MEMORY_AGENT_INJECTION_MODE: 'off',
|
||||
MEMORY_LIFECYCLE_ENABLED: '0',
|
||||
MEMORY_CANDIDATE_ENABLED: '0',
|
||||
MEMORY_EVENT_LOG_ENABLED: '1',
|
||||
MEMORY_FAIL_OPEN: '1',
|
||||
USER_CONVERSATION_MEMORY_ENABLED: '1',
|
||||
USER_CONVERSATION_MEMORY_LLM_ENABLED: '1',
|
||||
};
|
||||
|
||||
const pool = createPool();
|
||||
const userId = await resolveUserId(pool);
|
||||
const sessionA = `v149-loop-a-${crypto.randomUUID().slice(0, 8)}`;
|
||||
const sessionB = `v149-loop-b-${crypto.randomUUID().slice(0, 8)}`;
|
||||
const messageId = crypto.randomUUID().replace(/-/g, '');
|
||||
|
||||
try {
|
||||
const conversationMemory = createConversationMemoryService(pool, {
|
||||
llmProviderService: buildMockLlmProvider(),
|
||||
encryptionKey: process.env.TKMIND_ENCRYPTION_KEY ?? 'local-memory-loop-key',
|
||||
getEffectiveEnv: async () => env,
|
||||
});
|
||||
const memory = createMemoryV2({
|
||||
legacyMemoryService: conversationMemory,
|
||||
env,
|
||||
logger: console,
|
||||
});
|
||||
|
||||
const status = memory.getStatus();
|
||||
if (status.backend !== 'legacy' || status.selectedBackend !== 'legacy-conversation-memory') {
|
||||
throw new Error(
|
||||
`Expected legacy backend, got backend=${status.backend} selected=${status.selectedBackend}`,
|
||||
);
|
||||
}
|
||||
|
||||
await ensureSession(pool, userId, sessionA);
|
||||
|
||||
const written = await memory.write({
|
||||
userId,
|
||||
sessionId: sessionA,
|
||||
messages: [{
|
||||
id: messageId,
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: `${MARKER} 请记住我喜欢 v1.49 本地记忆闭环测试` }],
|
||||
metadata: { userVisible: true, agentVisible: true },
|
||||
}],
|
||||
});
|
||||
if (!written.ok || Number(written.memories) < 1) {
|
||||
throw new Error(`write failed: ${JSON.stringify(written)}`);
|
||||
}
|
||||
|
||||
const compacted = await memory.compact({ userId });
|
||||
if (!compacted.ok || compacted.source !== 'legacy-conversation-memory') {
|
||||
throw new Error(`compact failed: ${JSON.stringify(compacted)}`);
|
||||
}
|
||||
|
||||
const resolved = await memory.resolve({
|
||||
userId,
|
||||
sessionId: sessionB,
|
||||
query: MARKER,
|
||||
});
|
||||
const texts = (resolved.memories ?? []).map((item) => String(item.text ?? ''));
|
||||
if (!texts.some((text) => text.includes(MARKER))) {
|
||||
throw new Error(`resolve in new session missed marker; memories=${JSON.stringify(texts)}`);
|
||||
}
|
||||
|
||||
console.log(`GOOSE_V149_MEMORY_LOOP_OK: user=${userId} sessionA=${sessionA} sessionB=${sessionB}`);
|
||||
console.log(` write.memories=${written.memories} compact.analyzed=${compacted.analyzed} resolve.count=${texts.length}`);
|
||||
} finally {
|
||||
await cleanup(pool, userId, [sessionA, sessionB]);
|
||||
await pool.end().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_MEMORY_LOOP_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify Goose v1.49 local run profile keeps Memory on legacy owner (Phase 1 gate).
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { describeGooseCanaryConfig } from './goose-v149-canary.mjs';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const REQUIRED_MEMORY = {
|
||||
MEMORY_BACKEND: 'legacy',
|
||||
MEMORY_VECTOR_ENABLED: '0',
|
||||
MEMORY_AGENT_INJECTION_MODE: 'off',
|
||||
MEMORY_LIFECYCLE_ENABLED: '0',
|
||||
MEMORY_CANDIDATE_ENABLED: '0',
|
||||
};
|
||||
|
||||
function readText(relativePath) {
|
||||
const fullPath = path.join(root, relativePath);
|
||||
if (!existsSync(fullPath)) return '';
|
||||
return readFileSync(fullPath, 'utf8');
|
||||
}
|
||||
|
||||
function parseEnvBlock(text) {
|
||||
const env = {};
|
||||
for (const line of text.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
env[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
function checkMemoryEnv(label, env) {
|
||||
const failures = [];
|
||||
for (const [key, expected] of Object.entries(REQUIRED_MEMORY)) {
|
||||
const actual = String(env[key] ?? '').trim();
|
||||
if (actual !== expected) {
|
||||
failures.push(`${label}.${key}=${actual || '(unset)'} expected ${expected}`);
|
||||
}
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const failures = [];
|
||||
|
||||
const runScript = readText('scripts/run-goosed-v149-local.sh');
|
||||
for (const [key, expected] of Object.entries(REQUIRED_MEMORY)) {
|
||||
const pattern = new RegExp(`${key}=["']?\\$\\{${key}:-${expected}\\}["']?`);
|
||||
if (!pattern.test(runScript)) {
|
||||
failures.push(`run-goosed-v149-local.sh missing default ${key}=${expected}`);
|
||||
}
|
||||
}
|
||||
|
||||
const switchScript = readText('scripts/switch-goose-v149-canary.sh');
|
||||
for (const key of Object.keys(REQUIRED_MEMORY)) {
|
||||
if (!switchScript.includes(key)) {
|
||||
failures.push(`switch-goose-v149-canary.sh missing ${key} in canary block`);
|
||||
}
|
||||
}
|
||||
|
||||
const canaryEnabled = String(process.env.TKMIND_GOOSE_CANARY ?? 'off').trim().toLowerCase() !== 'off';
|
||||
if (canaryEnabled) {
|
||||
failures.push(...checkMemoryEnv('process.env', process.env));
|
||||
}
|
||||
|
||||
const canarySummary = describeGooseCanaryConfig(process.env);
|
||||
if (canarySummary.enabled && canarySummary.mode === 'all') {
|
||||
if (!String(canarySummary.primary).includes(':18049')) {
|
||||
failures.push(`canary primary must be v149 loopback, got ${canarySummary.primary}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
console.error('GOOSE_V149_MEMORY_POLICY_FAIL:');
|
||||
for (const item of failures) console.error(` - ${item}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('GOOSE_V149_MEMORY_POLICY_OK: legacy owner enforced for v1.49 local/canary profile');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Portal + Goose v1.49 integration smoke (optional when Portal is running).
|
||||
*/
|
||||
import crypto from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
|
||||
import { describeGooseCanaryConfig } from './goose-v149-canary.mjs';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
loadMemindEnvFiles(root, process.env);
|
||||
|
||||
const baseUrl = String(process.env.GOOSE_V149_PORTAL_BASE_URL ?? 'http://127.0.0.1:8081').replace(/\/+$/, '');
|
||||
const v149Port = process.env.GOOSE_V149_PORT || '18049';
|
||||
const v149Target = process.env.TKMIND_API_TARGET_V149 || `https://127.0.0.1:${v149Port}`;
|
||||
const skipPortal = process.env.GOOSE_V149_PORTAL_SMOKE_SKIP === '1';
|
||||
const canary = describeGooseCanaryConfig(process.env);
|
||||
|
||||
function curlJson(url, init = {}) {
|
||||
const args = ['-sk', '--connect-timeout', '5'];
|
||||
if (init.method && init.method !== 'GET') args.push('-X', init.method);
|
||||
if (init.headers) {
|
||||
for (const [key, value] of Object.entries(init.headers)) {
|
||||
args.push('-H', `${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
if (init.body) args.push('-d', init.body);
|
||||
args.push(url);
|
||||
const body = execFileSync('curl', args, { encoding: 'utf8' }).trim();
|
||||
return body ? JSON.parse(body) : null;
|
||||
}
|
||||
|
||||
function portalReachable() {
|
||||
try {
|
||||
curlJson(`${baseUrl}/auth/status`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function v149Reachable() {
|
||||
try {
|
||||
const body = execFileSync(
|
||||
'curl',
|
||||
['-sk', '--connect-timeout', '5', `https://127.0.0.1:${v149Port}/status`],
|
||||
{ encoding: 'utf8' },
|
||||
).trim();
|
||||
return body === 'ok';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestJson(url, { method = 'GET', headers = {}, body } = {}) {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
...(body ? { 'Content-Type': 'application/json' } : {}),
|
||||
...headers,
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
return { ok: response.ok, status: response.status, json, text, headers: response.headers };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!v149Reachable()) {
|
||||
throw new Error(`v1.49 goosed not reachable on :${v149Port}; run scripts/run-goosed-v149-local.sh`);
|
||||
}
|
||||
|
||||
if (skipPortal || !portalReachable()) {
|
||||
console.log(
|
||||
`GOOSE_V149_PORTAL_SMOKE_SKIP: Portal not running at ${baseUrl}; v149 direct checks only`,
|
||||
);
|
||||
console.log('GOOSE_V149_PORTAL_SMOKE_OK: skipped (v149 reachable)');
|
||||
return;
|
||||
}
|
||||
|
||||
const runtime = await requestJson(`${baseUrl}/api/runtime/status`);
|
||||
const memory = runtime.json?.memory ?? null;
|
||||
const targetStatuses = runtime.json?.targets ?? [];
|
||||
const portalTargets = Array.isArray(targetStatuses)
|
||||
? targetStatuses.map((item) => item.target ?? item).filter(Boolean)
|
||||
: [];
|
||||
const portalUsesV149 = portalTargets.some((item) => String(item).includes(`:${v149Port}`));
|
||||
|
||||
if (!runtime.ok || !memory) {
|
||||
throw new Error(`runtime status failed: ${runtime.status} ${runtime.text?.slice?.(0, 200)}`);
|
||||
}
|
||||
|
||||
if (canary.enabled && !portalUsesV149) {
|
||||
console.log(
|
||||
`GOOSE_V149_PORTAL_SMOKE_SKIP: .env.local canary=${canary.mode} primary=${canary.primary} `
|
||||
+ `but Portal targets=${JSON.stringify(portalTargets)} — restart pnpm dev`,
|
||||
);
|
||||
console.log('GOOSE_V149_PORTAL_SMOKE_OK: skipped (stale Portal process)');
|
||||
return;
|
||||
}
|
||||
|
||||
if (canary.enabled && portalUsesV149) {
|
||||
console.log(
|
||||
`GOOSE_V149_PORTAL_SMOKE_NOTE: canary active targets=${portalTargets.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const canaryMode = String(process.env.TKMIND_GOOSE_CANARY ?? 'off').trim().toLowerCase();
|
||||
if (canaryMode !== 'off' && memory.backend !== 'legacy') {
|
||||
const configSource = memory.configSource ?? 'unknown';
|
||||
if (configSource === 'admin-db' || configSource === 'admin-db-cache') {
|
||||
console.log(
|
||||
`GOOSE_V149_PORTAL_SMOKE_WARN: memory.backend=${memory.backend} from ${configSource}; goose routing verified`,
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Portal memory.backend must stay legacy during v1.49 canary (${canaryMode}), got ${memory.backend} (${configSource})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (canaryMode === 'off' && memory.backend !== 'legacy') {
|
||||
console.log(
|
||||
`GOOSE_V149_PORTAL_SMOKE_NOTE: Portal memory.backend=${memory.backend} (canary off; enable switch-goose-v149-canary.sh for legacy gate)`,
|
||||
);
|
||||
}
|
||||
|
||||
const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`;
|
||||
const user = {
|
||||
username: `gv149_${suffix}`,
|
||||
password: 'GooseV149-Portal-2026',
|
||||
email: `gv149-${suffix}@example.test`,
|
||||
displayName: `gv149_${suffix}`,
|
||||
};
|
||||
|
||||
const register = await requestJson(`${baseUrl}/auth/register`, { method: 'POST', body: user });
|
||||
if (!register.ok) throw new Error(`register failed: ${register.status}`);
|
||||
|
||||
const login = await requestJson(`${baseUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
body: { username: user.username, password: user.password },
|
||||
});
|
||||
const cookie = login.headers?.get?.('set-cookie')?.split(';', 1)[0] ?? null;
|
||||
if (!login.ok || !cookie) throw new Error(`login failed: ${login.status}`);
|
||||
|
||||
const started = await requestJson(`${baseUrl}/api/agent/start`, {
|
||||
method: 'POST',
|
||||
headers: { Cookie: cookie },
|
||||
body: {},
|
||||
});
|
||||
const sessionId = started.json?.id ?? null;
|
||||
if (!started.ok || !sessionId) {
|
||||
throw new Error(`agent/start via Portal failed: ${started.status} ${started.text?.slice?.(0, 200)}`);
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID();
|
||||
const run = await requestJson(`${baseUrl}/api/agent/runs`, {
|
||||
method: 'POST',
|
||||
headers: { Cookie: cookie },
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
request_id: requestId,
|
||||
user_message: {
|
||||
role: 'user',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
content: [{ type: 'text', text: 'ping from goose v1.49 portal smoke' }],
|
||||
metadata: { userVisible: true, agentVisible: true, displayText: 'ping' },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!run.ok || run.status !== 202 || !run.json?.run?.id) {
|
||||
throw new Error(`agent run failed: ${run.status} ${run.text?.slice?.(0, 200)}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`GOOSE_V149_PORTAL_SMOKE_OK: base=${baseUrl} session=${sessionId} run=${run.json.run.id} memory.backend=${memory.backend}`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_PORTAL_SMOKE_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Provider smoke for local Goose v1.49:
|
||||
* 1. Upsert custom_tkmind_relay_deepseek via /config/custom-providers
|
||||
* 2. POST /agent/update_provider
|
||||
* 3. Optional full reply Finish when DEEPSEEK_API_KEY is set
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Readable } from 'node:stream';
|
||||
import { Agent, fetch } from 'undici';
|
||||
|
||||
const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let value = trimmed.slice(eq + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"'))
|
||||
|| (value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (!process.env[key]) process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFile(path.join(memindRoot, '.env'));
|
||||
|
||||
const port = process.env.GOOSE_V149_PORT || '18049';
|
||||
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
|
||||
const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret';
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || memindRoot;
|
||||
const providerId =
|
||||
process.env.GOOSE_V149_PROVIDER_ID || 'custom_tkmind_relay_deepseek';
|
||||
const model = process.env.GOOSE_V149_PROVIDER_MODEL || 'deepseek-chat';
|
||||
const apiKey = process.env.DEEPSEEK_API_KEY || process.env.GOOSE_V149_TEST_API_KEY || '';
|
||||
const apiUrl =
|
||||
process.env.GOOSE_V149_PROVIDER_API_URL
|
||||
|| process.env.DEEPSEEK_API_BASE_URL
|
||||
|| 'https://api.deepseek.com/v1';
|
||||
const replyTimeoutMs = Number(process.env.GOOSE_V149_PROVIDER_REPLY_TIMEOUT_MS || 90_000);
|
||||
|
||||
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
|
||||
if (blockedHosts.includes(host)) {
|
||||
console.error(`GOOSE_V149_PROVIDER_FAIL: refusing production host ${host}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const base = `https://${host}:${port}`;
|
||||
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
|
||||
async function apiFetch(pathname, init = {}) {
|
||||
const headers = {
|
||||
...(init.headers ?? {}),
|
||||
'X-Secret-Key': secret,
|
||||
};
|
||||
if (init.body && !headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
return fetch(`${base}${pathname}`, {
|
||||
...init,
|
||||
headers,
|
||||
dispatcher,
|
||||
});
|
||||
}
|
||||
|
||||
async function apiJson(pathname, init = {}) {
|
||||
const response = await apiFetch(pathname, init);
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`${pathname} ${response.status}: ${text.slice(0, 800)}`);
|
||||
}
|
||||
return text.trim() ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
async function upsertCustomProvider() {
|
||||
const body = {
|
||||
engine: 'openai',
|
||||
display_name: 'tkmind_relay_deepseek',
|
||||
api_url: apiUrl,
|
||||
api_key: apiKey || 'placeholder-not-used-for-config-only',
|
||||
models: [model, 'deepseek-reasoner'],
|
||||
supports_streaming: true,
|
||||
requires_auth: Boolean(apiKey),
|
||||
preserves_thinking: false,
|
||||
};
|
||||
|
||||
let response = await apiFetch(
|
||||
`/config/custom-providers/${encodeURIComponent(providerId)}`,
|
||||
{ method: 'PUT', body: JSON.stringify(body) },
|
||||
);
|
||||
if (response.ok) {
|
||||
console.log(`GOOSE_V149_PROVIDER_CONFIG_OK: updated ${providerId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const putText = await response.text().catch(() => '');
|
||||
const missing = response.status === 404 || /not found/i.test(putText);
|
||||
if (!missing) {
|
||||
throw new Error(`PUT custom provider failed: ${response.status} ${putText}`);
|
||||
}
|
||||
|
||||
response = await apiFetch('/config/custom-providers', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const postText = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`POST custom provider failed: ${response.status} ${postText}`);
|
||||
}
|
||||
const created = JSON.parse(postText);
|
||||
console.log(
|
||||
`GOOSE_V149_PROVIDER_CONFIG_OK: created ${created.provider_name ?? providerId}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForFinish(sessionId, requestId) {
|
||||
const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'text/event-stream' },
|
||||
});
|
||||
if (!eventsResponse.ok || !eventsResponse.body) {
|
||||
const text = await eventsResponse.text().catch(() => '');
|
||||
throw new Error(`events ${eventsResponse.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
|
||||
const replyResponse = await apiFetch(`/sessions/${sessionId}/reply`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
request_id: requestId,
|
||||
user_message: {
|
||||
role: 'user',
|
||||
created: Date.now(),
|
||||
content: [{ type: 'text', text: 'Reply with exactly one word: pong' }],
|
||||
metadata: { userVisible: true, agentVisible: true, displayText: 'ping' },
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!replyResponse.ok) {
|
||||
const text = await replyResponse.text().catch(() => '');
|
||||
throw new Error(`reply ${replyResponse.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
replyResponse.body?.cancel?.();
|
||||
|
||||
const reader = Readable.fromWeb(eventsResponse.body);
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
const deadline = Date.now() + replyTimeoutMs;
|
||||
|
||||
for await (const chunk of reader) {
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(`timeout waiting for Finish (${replyTimeoutMs}ms)`);
|
||||
}
|
||||
buffer += decoder.decode(chunk, { stream: true });
|
||||
const frames = buffer.split('\n\n');
|
||||
buffer = frames.pop() ?? '';
|
||||
for (const frame of frames) {
|
||||
let data = '';
|
||||
for (const line of frame.split('\n')) {
|
||||
if (line.startsWith('data:')) data += line.slice(5).trim();
|
||||
}
|
||||
if (!data) continue;
|
||||
let event;
|
||||
try {
|
||||
event = JSON.parse(data);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const routingId = event.chat_request_id ?? event.request_id;
|
||||
if (routingId && routingId !== requestId) continue;
|
||||
if (event.type === 'Finish') return 'finish';
|
||||
if (event.type === 'Error') {
|
||||
throw new Error(String(event.error ?? 'provider reply error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('SSE closed before Finish');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!apiKey) {
|
||||
console.log('GOOSE_V149_PROVIDER_SKIP_REPLY: no DEEPSEEK_API_KEY — config/update_provider only');
|
||||
}
|
||||
|
||||
await upsertCustomProvider();
|
||||
|
||||
const session = await apiJson('/agent/start', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ working_dir: workingDir }),
|
||||
});
|
||||
if (!session?.id) throw new Error('missing session id');
|
||||
|
||||
await apiJson('/agent/update_provider', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
session_id: session.id,
|
||||
provider: providerId,
|
||||
model,
|
||||
}),
|
||||
});
|
||||
console.log(`GOOSE_V149_PROVIDER_UPDATE_OK: session=${session.id} provider=${providerId} model=${model}`);
|
||||
|
||||
if (apiKey) {
|
||||
const requestId = randomUUID();
|
||||
const outcome = await waitForFinish(session.id, requestId);
|
||||
console.log(
|
||||
`GOOSE_V149_PROVIDER_REPLY_OK: session=${session.id} request=${requestId} outcome=${outcome}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log('GOOSE_V149_PROVIDER_SMOKE_OK');
|
||||
} catch (error) {
|
||||
console.error(`GOOSE_V149_PROVIDER_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 2 smoke: POST /sessions/{id}/reply + GET /sessions/{id}/events (SSE).
|
||||
* Validates the event bus pipeline; Finish or Error both count as SSE_OK.
|
||||
*/
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Readable } from 'node:stream';
|
||||
import { Agent, fetch } from 'undici';
|
||||
|
||||
const port = process.env.GOOSE_V149_PORT || '18049';
|
||||
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
|
||||
const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret';
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || process.cwd();
|
||||
const timeoutMs = Number(process.env.GOOSE_V149_REPLY_TIMEOUT_MS || 30_000);
|
||||
|
||||
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
|
||||
if (blockedHosts.includes(host)) {
|
||||
console.error(`GOOSE_V149_REPLY_SMOKE_FAIL: refusing production host ${host}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const base = `https://${host}:${port}`;
|
||||
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
|
||||
async function apiFetch(pathname, init = {}) {
|
||||
const headers = {
|
||||
...(init.headers ?? {}),
|
||||
'X-Secret-Key': secret,
|
||||
};
|
||||
if (init.body && !headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
return fetch(`${base}${pathname}`, {
|
||||
...init,
|
||||
headers,
|
||||
dispatcher,
|
||||
});
|
||||
}
|
||||
|
||||
async function apiJson(pathname, body) {
|
||||
const response = await apiFetch(pathname, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`${pathname} ${response.status}: ${text.slice(0, 800)}`);
|
||||
}
|
||||
return text.trim() ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
|
||||
async function waitForTerminalEvent(sessionId, requestId) {
|
||||
const eventsResponse = await apiFetch(`/sessions/${sessionId}/events`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'text/event-stream' },
|
||||
});
|
||||
if (!eventsResponse.ok || !eventsResponse.body) {
|
||||
const text = await eventsResponse.text().catch(() => '');
|
||||
throw new Error(`events ${eventsResponse.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
|
||||
const replyResponse = await apiFetch(`/sessions/${sessionId}/reply`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
request_id: requestId,
|
||||
user_message: {
|
||||
role: 'user',
|
||||
created: Date.now(),
|
||||
content: [{ type: 'text', text: 'ping' }],
|
||||
metadata: { userVisible: true, agentVisible: true, displayText: 'ping' },
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!replyResponse.ok) {
|
||||
const text = await replyResponse.text().catch(() => '');
|
||||
throw new Error(`reply ${replyResponse.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
replyResponse.body?.cancel?.();
|
||||
|
||||
const reader = Readable.fromWeb(eventsResponse.body);
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
for await (const chunk of reader) {
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(`timeout waiting for Finish/Error (${timeoutMs}ms)`);
|
||||
}
|
||||
|
||||
buffer += decoder.decode(chunk, { stream: true });
|
||||
const frames = buffer.split('\n\n');
|
||||
buffer = frames.pop() ?? '';
|
||||
|
||||
for (const frame of frames) {
|
||||
let data = '';
|
||||
for (const line of frame.split('\n')) {
|
||||
if (line.startsWith('data:')) data += line.slice(5).trim();
|
||||
}
|
||||
if (!data) continue;
|
||||
|
||||
let event;
|
||||
try {
|
||||
event = JSON.parse(data);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const routingId = event.chat_request_id ?? event.request_id;
|
||||
if (routingId && routingId !== requestId) continue;
|
||||
|
||||
if (event.type === 'Finish') {
|
||||
console.log('GOOSE_V149_REPLY_EVENT: type=Finish');
|
||||
return 'finish';
|
||||
}
|
||||
if (event.type === 'Error') {
|
||||
console.log(`GOOSE_V149_REPLY_EVENT: type=Error error=${String(event.error ?? '').slice(0, 200)}`);
|
||||
return 'error';
|
||||
}
|
||||
if (event.type === 'Message') {
|
||||
console.log('GOOSE_V149_REPLY_EVENT: type=Message');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`SSE stream closed before Finish/Error (${timeoutMs}ms)`);
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await apiJson('/agent/start', { working_dir: workingDir });
|
||||
if (!session?.id) throw new Error('missing session id from /agent/start');
|
||||
|
||||
const provider = process.env.GOOSE_V149_TEST_PROVIDER;
|
||||
const model = process.env.GOOSE_V149_TEST_MODEL;
|
||||
if (provider && model) {
|
||||
await apiJson('/agent/update_provider', {
|
||||
session_id: session.id,
|
||||
provider,
|
||||
model,
|
||||
});
|
||||
console.log(`GOOSE_V149_REPLY_PROVIDER_OK: ${provider}/${model}`);
|
||||
}
|
||||
|
||||
const requestId = randomUUID();
|
||||
const outcome = await waitForTerminalEvent(session.id, requestId);
|
||||
console.log(`GOOSE_V149_REPLY_SMOKE_OK: session=${session.id} request=${requestId} outcome=${outcome}`);
|
||||
} catch (error) {
|
||||
console.error(`GOOSE_V149_REPLY_SMOKE_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Milestone 2 smoke: load sandbox-fs stdio MCP via extension_overrides on local Goose v1.49.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Agent, fetch } from 'undici';
|
||||
|
||||
const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const port = process.env.GOOSE_V149_PORT || '18049';
|
||||
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
|
||||
const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret';
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || memindRoot;
|
||||
const nodeExec = process.env.GOOSED_MCP_NODE_PATH || process.execPath;
|
||||
const serverPath =
|
||||
process.env.GOOSED_MCP_SERVER_PATH || path.join(memindRoot, 'mindspace-sandbox-mcp.mjs');
|
||||
const pollMs = Number(process.env.GOOSE_V149_SANDBOX_POLL_MS || 500);
|
||||
const timeoutMs = Number(process.env.GOOSE_V149_SANDBOX_TIMEOUT_MS || 30_000);
|
||||
|
||||
const blockedHosts = ['58.38.22.103', '120.26.184.105'];
|
||||
if (blockedHosts.includes(host)) {
|
||||
console.error(`GOOSE_V149_SANDBOX_FS_FAIL: refusing production host ${host}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(serverPath)) {
|
||||
console.error(`GOOSE_V149_SANDBOX_FS_FAIL: MCP entry missing: ${serverPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sandboxRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'goose-v149-sandbox-'));
|
||||
fs.mkdirSync(path.join(sandboxRoot, 'public'), { recursive: true });
|
||||
|
||||
const base = `https://${host}:${port}`;
|
||||
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
|
||||
async function apiFetch(pathname, init = {}) {
|
||||
const headers = {
|
||||
...(init.headers ?? {}),
|
||||
'X-Secret-Key': secret,
|
||||
};
|
||||
if (init.body && !headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
return fetch(`${base}${pathname}`, {
|
||||
...init,
|
||||
headers,
|
||||
dispatcher,
|
||||
});
|
||||
}
|
||||
|
||||
async function apiJson(pathname, body) {
|
||||
const response = await apiFetch(pathname, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`${pathname} ${response.status}: ${text.slice(0, 800)}`);
|
||||
}
|
||||
return text.trim() ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
async function waitForSandboxTools(sessionId) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const response = await apiFetch(
|
||||
`/agent/tools?session_id=${encodeURIComponent(sessionId)}&extension_name=sandbox-fs`,
|
||||
);
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`/agent/tools ${response.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
const tools = JSON.parse(text);
|
||||
const names = tools.map((tool) => tool.name);
|
||||
const writeTool = names.find((name) => name === 'sandbox-fs__write_file');
|
||||
if (writeTool) {
|
||||
return names;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
||||
}
|
||||
throw new Error(`sandbox-fs tools not ready within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await apiJson('/agent/start', {
|
||||
working_dir: workingDir,
|
||||
extension_overrides: [
|
||||
{
|
||||
type: 'stdio',
|
||||
name: 'sandbox-fs',
|
||||
description: 'MindSpace sandbox filesystem MCP (local v1.49 smoke)',
|
||||
cmd: nodeExec,
|
||||
args: [serverPath, sandboxRoot],
|
||||
envs: {
|
||||
ALLOWED_TOOLS: 'write_file,read_file,edit_file,create_dir,list_dir',
|
||||
SANDBOX_ROOT: sandboxRoot,
|
||||
},
|
||||
available_tools: ['write_file', 'read_file', 'edit_file', 'create_dir', 'list_dir'],
|
||||
},
|
||||
{
|
||||
type: 'platform',
|
||||
name: 'skills',
|
||||
description: 'skills',
|
||||
available_tools: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!session?.id) {
|
||||
throw new Error('missing session id from /agent/start');
|
||||
}
|
||||
|
||||
const extResponse = await apiFetch(`/sessions/${session.id}/extensions`);
|
||||
const extBody = await extResponse.json();
|
||||
const configured = (extBody.extensions ?? []).some((ext) => ext.name === 'sandbox-fs');
|
||||
if (!configured) {
|
||||
throw new Error('sandbox-fs not present in session extension_data');
|
||||
}
|
||||
|
||||
const toolNames = await waitForSandboxTools(session.id);
|
||||
console.log(
|
||||
`GOOSE_V149_SANDBOX_FS_OK: session=${session.id} sandbox=${sandboxRoot} tools=${toolNames.length}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`GOOSE_V149_SANDBOX_FS_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify v1.49 exposes Portal session-reconcile REST routes.
|
||||
*/
|
||||
import { createV149Client } from './goose-v149-sse.mjs';
|
||||
|
||||
const client = createV149Client();
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || process.cwd();
|
||||
|
||||
async function main() {
|
||||
const session = await client.apiJson('/agent/start', { working_dir: workingDir });
|
||||
if (!session?.id) throw new Error('missing session id');
|
||||
|
||||
const getRes = await client.apiFetch(`/sessions/${session.id}`, { method: 'GET' });
|
||||
if (!getRes.ok) {
|
||||
throw new Error(`GET /sessions/{id} ${getRes.status}`);
|
||||
}
|
||||
|
||||
const addRes = await client.apiFetch('/agent/add_extension', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
session_id: session.id,
|
||||
config: {
|
||||
type: 'platform',
|
||||
name: 'web',
|
||||
description: 'compat probe',
|
||||
display_name: 'web',
|
||||
bundled: false,
|
||||
available_tools: ['web_search'],
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!addRes.ok) {
|
||||
const text = await addRes.text();
|
||||
throw new Error(`add_extension probe ${addRes.status}: ${text.slice(0, 200)}`);
|
||||
}
|
||||
|
||||
console.log(`GOOSE_V149_SESSION_RECONCILE_OK: session=${session.id} routes=GET /sessions/{id}, POST /agent/add_extension`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_SESSION_RECONCILE_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Smoke: upstream skills extension exposes load_skill (web-search skill path).
|
||||
*/
|
||||
import { createV149Client } from './goose-v149-sse.mjs';
|
||||
|
||||
const client = createV149Client();
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || process.cwd();
|
||||
const pollMs = Number(process.env.GOOSE_V149_SKILLS_POLL_MS || 500);
|
||||
const timeoutMs = Number(process.env.GOOSE_V149_SKILLS_TIMEOUT_MS || 15_000);
|
||||
|
||||
async function waitForSkillsTools(sessionId) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const response = await client.apiFetch(
|
||||
`/agent/tools?session_id=${encodeURIComponent(sessionId)}&extension_name=skills`,
|
||||
);
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`/agent/tools ${response.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
const tools = JSON.parse(text);
|
||||
const names = tools.map((tool) => tool.name);
|
||||
const hasLoadSkill = names.some((name) => /load_skill/i.test(name));
|
||||
if (hasLoadSkill) return names;
|
||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
||||
}
|
||||
throw new Error(`skills load_skill not ready within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const session = await client.apiJson('/agent/start', {
|
||||
working_dir: workingDir,
|
||||
extension_overrides: [
|
||||
{
|
||||
type: 'platform',
|
||||
name: 'skills',
|
||||
description: 'Skills extension (upstream web-search path)',
|
||||
display_name: 'Skills',
|
||||
bundled: false,
|
||||
available_tools: ['load_skill'],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!session?.id) throw new Error('missing session id');
|
||||
|
||||
const toolNames = await waitForSkillsTools(session.id);
|
||||
console.log(
|
||||
`GOOSE_V149_SKILLS_OK: session=${session.id} tools=${toolNames.length} builtin=web-search-via-load_skill`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_SKILLS_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 2: run v1.49 agent integration tests for DeepSeek/Kimi reasoning preservation.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const v149Root = process.env.GOOSED_V149_ROOT || '/Users/john/Project/tkmind_go-v149';
|
||||
|
||||
const THINKING_TESTS = [
|
||||
'test_deepseek_thinking_preserved_in_tool_call_message',
|
||||
'test_kimi_thinking_preserved_in_tool_call_message',
|
||||
'test_reasoning_preserved_when_combined_with_tool_call',
|
||||
'test_multi_tool_response_preserves_reasoning_and_message_id_correlation',
|
||||
];
|
||||
|
||||
function runTest(testName) {
|
||||
const result = spawnSync(
|
||||
'cargo',
|
||||
['test', '--test', 'agent', testName, '--quiet', '--', '--nocapture'],
|
||||
{
|
||||
cwd: v149Root,
|
||||
encoding: 'utf8',
|
||||
env: process.env,
|
||||
},
|
||||
);
|
||||
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
|
||||
const passed = /test result: ok\. 1 passed/.test(output);
|
||||
return { testName, ok: result.status === 0 && passed, output: output.split('\n').slice(-4).join('\n') };
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(v149Root)) {
|
||||
throw new Error(`missing v1.49 worktree: ${v149Root}`);
|
||||
}
|
||||
|
||||
const results = THINKING_TESTS.map(runTest);
|
||||
const failed = results.filter((item) => !item.ok);
|
||||
if (failed.length) {
|
||||
const detail = failed.map((item) => `${item.testName}:\n${item.output}`).join('\n\n');
|
||||
throw new Error(`thinking preservation tests failed:\n${detail}`);
|
||||
}
|
||||
|
||||
const outputDir = path.join(memindRoot, 'docs', 'baselines');
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const evidencePath = path.join(outputDir, 'goose-v149-thinking-preservation-latest.json');
|
||||
fs.writeFileSync(
|
||||
evidencePath,
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 'goose-v149-thinking-preservation-v1',
|
||||
capturedAt: new Date().toISOString(),
|
||||
worktree: v149Root,
|
||||
tests: results.map((item) => item.testName),
|
||||
passed: results.length,
|
||||
}, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
|
||||
console.log(`GOOSE_V149_THINKING_PRESERVATION_OK: ${results.length} tests passed`);
|
||||
console.log(` evidence=${evidencePath}`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(`GOOSE_V149_THINKING_PRESERVATION_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Smoke: platform/web extension (web_search, fetch_url) on local Goose v1.49.
|
||||
*/
|
||||
import { createV149Client } from './goose-v149-sse.mjs';
|
||||
|
||||
const client = createV149Client();
|
||||
const workingDir = process.env.GOOSE_V149_WORKING_DIR || process.cwd();
|
||||
const pollMs = Number(process.env.GOOSE_V149_WEB_POLL_MS || 500);
|
||||
const timeoutMs = Number(process.env.GOOSE_V149_WEB_TIMEOUT_MS || 15_000);
|
||||
|
||||
async function waitForWebTools(sessionId) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const response = await client.apiFetch(
|
||||
`/agent/tools?session_id=${encodeURIComponent(sessionId)}&extension_name=web`,
|
||||
);
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`/agent/tools ${response.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
const tools = JSON.parse(text);
|
||||
const names = tools.map((tool) => tool.name);
|
||||
const hasSearch = names.some((name) => name === 'web__web_search' || name === 'web_search');
|
||||
const hasFetch = names.some((name) => name === 'web__fetch_url' || name === 'fetch_url');
|
||||
if (hasSearch && hasFetch) {
|
||||
return names;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
||||
}
|
||||
throw new Error(`platform/web tools not ready within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const session = await client.apiJson('/agent/start', {
|
||||
working_dir: workingDir,
|
||||
extension_overrides: [
|
||||
{
|
||||
type: 'platform',
|
||||
name: 'web',
|
||||
description: 'Web search and fetch (v1.49 smoke)',
|
||||
display_name: 'Web',
|
||||
bundled: false,
|
||||
available_tools: ['web_search', 'fetch_url'],
|
||||
},
|
||||
{
|
||||
type: 'platform',
|
||||
name: 'skills',
|
||||
description: 'skills',
|
||||
available_tools: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!session?.id) throw new Error('missing session id');
|
||||
|
||||
const toolNames = await waitForWebTools(session.id);
|
||||
|
||||
console.log(
|
||||
`GOOSE_V149_WEB_OK: session=${session.id} tools=${toolNames.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_WEB_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Compare two Goose v1.49 memory manifests and report drift.
|
||||
*/
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
'Usage: node scripts/compare-goose-v149-memory-manifest.mjs [--baseline <path>] [--current <path>] [--json]',
|
||||
'',
|
||||
'Defaults:',
|
||||
' baseline docs/baselines/goose-v149-memory-latest.json',
|
||||
' current freshly exported manifest (via child process) unless --current is set',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
baseline: path.join(root, 'docs', 'baselines', 'goose-v149-memory-latest.json'),
|
||||
current: '',
|
||||
json: false,
|
||||
exportCurrent: true,
|
||||
};
|
||||
for (let i = 2; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--baseline' && argv[i + 1]) args.baseline = path.resolve(argv[++i]);
|
||||
else if (arg === '--current' && argv[i + 1]) {
|
||||
args.current = path.resolve(argv[++i]);
|
||||
args.exportCurrent = false;
|
||||
} else if (arg === '--json') args.json = true;
|
||||
else if (arg === '--help' || arg === '-h') {
|
||||
console.log(usage());
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
console.error(usage());
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
async function readManifest(filePath) {
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function indexMemories(manifest) {
|
||||
const byId = new Map();
|
||||
for (const item of manifest.memories ?? []) {
|
||||
byId.set(item.id, item);
|
||||
}
|
||||
return byId;
|
||||
}
|
||||
|
||||
function compareManifests(baseline, current) {
|
||||
const drift = {
|
||||
baselineDigest: baseline.summary?.digest ?? null,
|
||||
currentDigest: current.summary?.digest ?? null,
|
||||
summary: {
|
||||
userCountDelta: (current.summary?.userCount ?? 0) - (baseline.summary?.userCount ?? 0),
|
||||
memoryCountDelta: (current.summary?.memoryItemCount ?? 0) - (baseline.summary?.memoryItemCount ?? 0),
|
||||
candidateCountDelta: (current.summary?.candidateCount ?? 0) - (baseline.summary?.candidateCount ?? 0),
|
||||
},
|
||||
addedMemoryIds: [],
|
||||
removedMemoryIds: [],
|
||||
changedMemories: [],
|
||||
};
|
||||
|
||||
const base = indexMemories(baseline);
|
||||
const cur = indexMemories(current);
|
||||
|
||||
for (const [id, item] of cur) {
|
||||
if (!base.has(id)) drift.addedMemoryIds.push(id);
|
||||
}
|
||||
for (const [id, item] of base) {
|
||||
if (!cur.has(id)) drift.removedMemoryIds.push(id);
|
||||
}
|
||||
for (const [id, baseItem] of base) {
|
||||
const curItem = cur.get(id);
|
||||
if (!curItem) continue;
|
||||
const fields = ['memoryHash', 'status', 'sourceSessionId', 'evidenceMessageId'];
|
||||
const changes = {};
|
||||
for (const field of fields) {
|
||||
if (String(baseItem[field] ?? '') !== String(curItem[field] ?? '')) {
|
||||
changes[field] = { baseline: baseItem[field] ?? null, current: curItem[field] ?? null };
|
||||
}
|
||||
}
|
||||
if (Object.keys(changes).length) {
|
||||
drift.changedMemories.push({ id, userId: baseItem.userId, changes });
|
||||
}
|
||||
}
|
||||
|
||||
drift.ok =
|
||||
drift.addedMemoryIds.length === 0
|
||||
&& drift.removedMemoryIds.length === 0
|
||||
&& drift.changedMemories.length === 0;
|
||||
return drift;
|
||||
}
|
||||
|
||||
async function exportCurrentManifest() {
|
||||
const { spawnSync } = await import('node:child_process');
|
||||
const tmpPath = path.join(root, 'docs', 'baselines', '.goose-v149-memory-current.json');
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
['scripts/export-goose-v149-memory-manifest.mjs', '--output', tmpPath, '--json'],
|
||||
{ cwd: root, encoding: 'utf8', env: process.env },
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || result.stdout || 'export manifest failed');
|
||||
}
|
||||
return readManifest(tmpPath);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
const baseline = await readManifest(args.baseline);
|
||||
const current = args.exportCurrent ? await exportCurrentManifest() : await readManifest(args.current);
|
||||
const report = compareManifests(baseline, current);
|
||||
|
||||
if (args.json) {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} else if (report.ok) {
|
||||
console.log('GOOSE_V149_MEMORY_DRIFT_OK: no ID/hash/status/session/evidence drift');
|
||||
console.log(` baselineDigest=${report.baselineDigest}`);
|
||||
} else {
|
||||
console.error('GOOSE_V149_MEMORY_DRIFT_FAIL:');
|
||||
console.error(` added=${report.addedMemoryIds.length} removed=${report.removedMemoryIds.length} changed=${report.changedMemories.length}`);
|
||||
if (report.addedMemoryIds.length) {
|
||||
console.error(` added sample: ${report.addedMemoryIds.slice(0, 5).join(', ')}`);
|
||||
}
|
||||
if (report.removedMemoryIds.length) {
|
||||
console.error(` removed sample: ${report.removedMemoryIds.slice(0, 5).join(', ')}`);
|
||||
}
|
||||
if (report.changedMemories.length) {
|
||||
console.error(` changed sample: ${JSON.stringify(report.changedMemories.slice(0, 3), null, 2)}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_MEMORY_DRIFT_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 2: compare provider message sanitize unit tests (v1.41 baseline vs v1.49).
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const v149Root = process.env.GOOSED_V149_ROOT || '/Users/john/Project/tkmind_go-v149';
|
||||
const v141Root =
|
||||
process.env.GOOSED_V141_ROOT || '/Users/john/Project/tkmind_go-v141-prod-prep';
|
||||
|
||||
function runSanitizeTests(root, label) {
|
||||
if (!fs.existsSync(root)) {
|
||||
return { label, ok: false, skipped: true, reason: `missing worktree: ${root}` };
|
||||
}
|
||||
const result = spawnSync(
|
||||
'cargo',
|
||||
['test', '-p', 'goose-providers', 'sanitize', '--quiet', '--', '--nocapture'],
|
||||
{
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
env: process.env,
|
||||
},
|
||||
);
|
||||
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
|
||||
const match = output.match(/test result: ok\. (\d+) passed/);
|
||||
const passed = match ? Number(match[1]) : null;
|
||||
return {
|
||||
label,
|
||||
root,
|
||||
ok: result.status === 0,
|
||||
passed,
|
||||
output: output.split('\n').slice(-6).join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const v149 = runSanitizeTests(v149Root, 'v1.49');
|
||||
const v141 = runSanitizeTests(v141Root, 'v1.41');
|
||||
|
||||
if (v149.skipped) {
|
||||
throw new Error(v149.reason);
|
||||
}
|
||||
if (!v149.ok || v149.passed == null) {
|
||||
throw new Error(`v1.49 sanitize tests failed:\n${v149.output}`);
|
||||
}
|
||||
|
||||
const outputDir = path.join(memindRoot, 'docs', 'baselines');
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const evidencePath = path.join(outputDir, 'goose-v149-message-sanitize-latest.json');
|
||||
const evidence = {
|
||||
schemaVersion: 'goose-v149-message-sanitize-v1',
|
||||
capturedAt: new Date().toISOString(),
|
||||
v149: { passed: v149.passed, root: v149Root },
|
||||
v141: v141.skipped
|
||||
? { skipped: true, reason: v141.reason }
|
||||
: { passed: v141.passed, ok: v141.ok, root: v141Root },
|
||||
equivalent:
|
||||
!v141.skipped
|
||||
&& v141.ok
|
||||
&& v141.passed != null
|
||||
&& v141.passed === v149.passed,
|
||||
};
|
||||
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
|
||||
|
||||
console.log(`GOOSE_V149_MESSAGE_SANITIZE_OK: v149=${v149.passed} passed`);
|
||||
if (!v141.skipped) {
|
||||
console.log(` v141=${v141.passed} passed equivalent=${evidence.equivalent}`);
|
||||
} else {
|
||||
console.log(` v141=skipped (${v141.reason})`);
|
||||
}
|
||||
console.log(` evidence=${evidencePath}`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(`GOOSE_V149_MESSAGE_SANITIZE_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# Create isolated PostgreSQL database for Goose v1.49 local upgrade testing.
|
||||
# Refuses production hosts (103/105/RDS).
|
||||
set -euo pipefail
|
||||
|
||||
PG_HOST="${GOOSE_V149_PG_HOST:-127.0.0.1}"
|
||||
PG_PORT="${GOOSE_V149_PG_PORT:-5432}"
|
||||
DB_NAME="${GOOSE_V149_SESSION_DB:-goose_sessions_v149_dev}"
|
||||
PG_USER="${GOOSE_V149_PG_USER:-${USER:-john}}"
|
||||
|
||||
fail() {
|
||||
echo "[goose-v149-db] FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "${PG_HOST}" in
|
||||
127.0.0.1|localhost|::1) ;;
|
||||
58.38.22.103|120.26.184.105|*rds.aliyuncs.com*|*tkmind.cn*)
|
||||
fail "refusing production or remote host: ${PG_HOST}"
|
||||
;;
|
||||
*)
|
||||
fail "unexpected PG host (only loopback allowed): ${PG_HOST}"
|
||||
;;
|
||||
esac
|
||||
|
||||
export PATH="/opt/homebrew/opt/postgresql@17/bin:${PATH:-/usr/bin:/bin}"
|
||||
|
||||
if ! pg_isready -h "${PG_HOST}" -p "${PG_PORT}" -q 2>/dev/null; then
|
||||
bash "$(cd "$(dirname "$0")/.." && pwd)/scripts/ensure-goose-session-postgres.sh"
|
||||
fi
|
||||
|
||||
if psql -h "${PG_HOST}" -p "${PG_PORT}" -U "${PG_USER}" -d postgres -tAc \
|
||||
"SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1; then
|
||||
echo "[goose-v149-db] database already exists: ${DB_NAME}"
|
||||
else
|
||||
createdb -h "${PG_HOST}" -p "${PG_PORT}" -U "${PG_USER}" "${DB_NAME}"
|
||||
echo "[goose-v149-db] created database: ${DB_NAME}"
|
||||
fi
|
||||
|
||||
echo "[goose-v149-db] GOOSE_SESSION_DB_URL=postgresql://${PG_USER}@${PG_HOST}:${PG_PORT}/${DB_NAME}"
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Ensure rusty_v8 static library is cached for Goose code-mode builds.
|
||||
set -euo pipefail
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
if [[ "${ARCH}" != "arm64" || "${OS}" != "darwin" ]]; then
|
||||
echo "[goose-v149-v8] unsupported platform ${OS}/${ARCH}; download manually if needed" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
V8_VERSION="v145.0.0"
|
||||
FILE="librusty_v8_release_aarch64-apple-darwin.a.gz"
|
||||
URL="https://github.com/denoland/rusty_v8/releases/download/${V8_VERSION}/${FILE}"
|
||||
# rusty_v8 build.rs expects the escaped URL path to be a FILE, not a directory.
|
||||
CACHE_FILE="${HOME}/.cargo/.rusty_v8/https___github_com_denoland_rusty_v8_releases_download_v145_0_0_librusty_v8_release_aarch64_apple_darwin_a_gz"
|
||||
MIN_BYTES=$((30 * 1024 * 1024))
|
||||
|
||||
mkdir -p "$(dirname "${CACHE_FILE}")"
|
||||
|
||||
# Legacy layout stored the gzip inside a same-named directory — migrate once.
|
||||
if [[ -d "${CACHE_FILE}" ]]; then
|
||||
legacy="${CACHE_FILE}/${FILE}"
|
||||
if [[ -f "${legacy}" ]]; then
|
||||
mv "${legacy}" "${CACHE_FILE}.migrate"
|
||||
rmdir "${CACHE_FILE}"
|
||||
mv "${CACHE_FILE}.migrate" "${CACHE_FILE}"
|
||||
echo "[goose-v149-v8] migrated legacy directory cache -> file"
|
||||
else
|
||||
echo "[goose-v149-v8] legacy cache dir is empty: ${CACHE_FILE}" >&2
|
||||
rm -rf "${CACHE_FILE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f "${CACHE_FILE}" ]]; then
|
||||
size="$(wc -c < "${CACHE_FILE}" | tr -d ' ')"
|
||||
if [[ "${size}" -ge "${MIN_BYTES}" ]]; then
|
||||
echo "[goose-v149-v8] cache ok (${size} bytes): ${CACHE_FILE}"
|
||||
exit 0
|
||||
fi
|
||||
echo "[goose-v149-v8] cache too small (${size} bytes), re-downloading" >&2
|
||||
rm -f "${CACHE_FILE}"
|
||||
fi
|
||||
|
||||
echo "[goose-v149-v8] downloading ${URL}"
|
||||
curl -fL --retry 5 --retry-delay 2 -o "${CACHE_FILE}.partial" "${URL}"
|
||||
mv "${CACHE_FILE}.partial" "${CACHE_FILE}"
|
||||
size="$(wc -c < "${CACHE_FILE}" | tr -d ' ')"
|
||||
echo "[goose-v149-v8] downloaded ${size} bytes -> ${CACHE_FILE}"
|
||||
|
||||
GOOSED_ROOT="${GOOSED_V149_ROOT:-/Users/john/Project/tkmind_go-v149}"
|
||||
GN_OUT="${GOOSED_ROOT}/target/release/gn_out/obj"
|
||||
if [[ -d "${GOOSED_ROOT}/target/release" ]]; then
|
||||
rm -rf "${GOOSED_ROOT}/target/release/gn_out"
|
||||
rm -f "${GOOSED_ROOT}/target/release/build"/v8-*/out/*.a 2>/dev/null || true
|
||||
echo "[goose-v149-v8] cleared stale gn_out under ${GOOSED_ROOT}/target/release"
|
||||
fi
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Seed cargo git checkouts from local hermit cache (offline build when GitHub SSL fails).
|
||||
set -euo pipefail
|
||||
|
||||
HERMIT_ROOT="${GOOSE_V149_HERMIT_ROOT:-/Users/john/Project/tkmind_go-v141-prod-prep/.hermit/rust/git/checkouts}"
|
||||
CARGO_GIT="${CARGO_HOME:-${HOME}/.cargo}/git/checkouts"
|
||||
|
||||
seed_checkout() {
|
||||
local prefix="$1"
|
||||
local rev="$2"
|
||||
local source="$3"
|
||||
local dest="${CARGO_GIT}/${prefix}/${rev}"
|
||||
if [[ ! -d "${source}" ]]; then
|
||||
echo "[goose-v149-vendor] missing source: ${source}" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "${CARGO_GIT}/${prefix}"
|
||||
ln -sfn "${source}" "${dest}"
|
||||
echo "[goose-v149-vendor] ${dest} -> ${source}"
|
||||
}
|
||||
|
||||
seed_checkout rust-sdk-762d172c5e846930 c97a5203d3392f7f231514d84eea014f9f43e6fb \
|
||||
"${HERMIT_ROOT}/rust-sdk-762d172c5e846930/c97a520"
|
||||
seed_checkout cudaforge-5c30904947dfbb68 e7c1967340e40673db98dc9e17da0f04834a456f \
|
||||
"${HERMIT_ROOT}/cudaforge-5c30904947dfbb68/e7c1967"
|
||||
@@ -0,0 +1,305 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Export local Memory baseline manifest for Goose v1.49 cutover reconciliation.
|
||||
* Loopback / local MySQL only; refuses production DB hosts.
|
||||
*/
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
|
||||
|
||||
const BLOCKED_DB_HOSTS = new Set(['58.38.22.103', '120.26.184.105', 'rds.aliyuncs.com']);
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
'Usage: node scripts/export-goose-v149-memory-manifest.mjs [--output <path>] [--user-id <id>] [--json]',
|
||||
'',
|
||||
'Exports per-user memory ID/hash/status/source session/evidence, V2 candidates,',
|
||||
'vector namespace metadata, and schema/watermark fields for Phase 0 baseline.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { output: '', userId: '', json: false };
|
||||
for (let i = 2; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--output' && argv[i + 1]) args.output = argv[++i];
|
||||
else if (arg === '--user-id' && argv[i + 1]) args.userId = argv[++i];
|
||||
else if (arg === '--json') args.json = true;
|
||||
else if (arg === '--help' || arg === '-h') {
|
||||
console.log(usage());
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
console.error(usage());
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function assertLocalDatabaseUrl(env = process.env) {
|
||||
const raw = String(env.DATABASE_URL ?? '').trim();
|
||||
if (raw) {
|
||||
let hostname = '';
|
||||
try {
|
||||
hostname = new URL(raw.replace(/^mysql:/, 'http:')).hostname.toLowerCase();
|
||||
} catch {
|
||||
throw new Error('DATABASE_URL is not a valid URL');
|
||||
}
|
||||
for (const blocked of BLOCKED_DB_HOSTS) {
|
||||
if (hostname.includes(blocked)) {
|
||||
throw new Error(`Refusing production DATABASE_URL host: ${hostname}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const host = String(env.MYSQL_HOST ?? '127.0.0.1').trim().toLowerCase();
|
||||
for (const blocked of BLOCKED_DB_HOSTS) {
|
||||
if (host.includes(blocked)) {
|
||||
throw new Error(`Refusing production MYSQL_HOST: ${host}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createMysqlPoolFromEnv() {
|
||||
loadMemindEnvFiles(process.cwd());
|
||||
assertLocalDatabaseUrl(process.env);
|
||||
const poolOptions = { connectionLimit: 3 };
|
||||
if (process.env.DATABASE_URL) {
|
||||
return mysql.createPool({ uri: process.env.DATABASE_URL, ...poolOptions });
|
||||
}
|
||||
if (!process.env.MYSQL_HOST && !process.env.MYSQL_DATABASE) {
|
||||
throw new Error('Memory manifest export requires DATABASE_URL or MYSQL_* configuration');
|
||||
}
|
||||
return mysql.createPool({
|
||||
host: process.env.MYSQL_HOST ?? '127.0.0.1',
|
||||
port: Number(process.env.MYSQL_PORT ?? 3306),
|
||||
user: process.env.MYSQL_USER ?? 'boot',
|
||||
password: process.env.MYSQL_PASSWORD ?? '',
|
||||
database: process.env.MYSQL_DATABASE ?? 'tkmind',
|
||||
...poolOptions,
|
||||
});
|
||||
}
|
||||
|
||||
async function tableExists(pool, tableName) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT COUNT(*) AS count
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = ?`,
|
||||
[tableName],
|
||||
);
|
||||
return Number(rows[0]?.count ?? 0) > 0;
|
||||
}
|
||||
|
||||
async function fetchMemoryItems(pool, userId) {
|
||||
const clauses = ["status IN ('active', 'archived')"];
|
||||
const params = [];
|
||||
if (userId) {
|
||||
clauses.push('user_id = ?');
|
||||
params.push(userId);
|
||||
}
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, user_id, label, memory_hash, memory_text, evidence_message_id,
|
||||
source_session_id, confidence, status, created_at, updated_at
|
||||
FROM h5_user_memory_items
|
||||
WHERE ${clauses.join(' AND ')}
|
||||
ORDER BY user_id ASC, updated_at DESC`,
|
||||
params,
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
label: row.label,
|
||||
memoryHash: row.memory_hash,
|
||||
memoryTextPreview: String(row.memory_text ?? '').slice(0, 120),
|
||||
evidenceMessageId: row.evidence_message_id ?? null,
|
||||
sourceSessionId: row.source_session_id ?? null,
|
||||
confidence: Number(row.confidence ?? 0),
|
||||
status: row.status,
|
||||
createdAt: Number(row.created_at),
|
||||
updatedAt: Number(row.updated_at),
|
||||
}));
|
||||
}
|
||||
|
||||
async function fetchCandidates(pool, userId) {
|
||||
if (!(await tableExists(pool, 'h5_memory_v2_candidates'))) return [];
|
||||
const clauses = ['1=1'];
|
||||
const params = [];
|
||||
if (userId) {
|
||||
clauses.push('user_id = ?');
|
||||
params.push(userId);
|
||||
}
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, user_id, session_id, memory_type, content, importance, confidence,
|
||||
status, policy_reason, evidence_json, created_at, updated_at
|
||||
FROM h5_memory_v2_candidates
|
||||
WHERE ${clauses.join(' AND ')}
|
||||
ORDER BY user_id ASC, updated_at DESC`,
|
||||
params,
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
sessionId: row.session_id ?? null,
|
||||
memoryType: row.memory_type,
|
||||
contentPreview: String(row.content ?? '').slice(0, 120),
|
||||
importance: Number(row.importance ?? 0),
|
||||
confidence: Number(row.confidence ?? 0),
|
||||
status: row.status,
|
||||
policyReason: row.policy_reason ?? null,
|
||||
evidence: (() => {
|
||||
try {
|
||||
return typeof row.evidence_json === 'object'
|
||||
? row.evidence_json
|
||||
: JSON.parse(String(row.evidence_json ?? '{}'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
})(),
|
||||
createdAt: Number(row.created_at),
|
||||
updatedAt: Number(row.updated_at),
|
||||
}));
|
||||
}
|
||||
|
||||
async function fetchVectorSummary(env) {
|
||||
const connectionString = String(env.MEMORY_PGVECTOR_DATABASE_URL ?? '').trim();
|
||||
if (!connectionString) {
|
||||
return {
|
||||
enabled: false,
|
||||
table: env.MEMORY_PGVECTOR_TABLE ?? 'memory_embeddings',
|
||||
rowCount: 0,
|
||||
embeddingModule: env.MEMORY_PGVECTOR_EMBEDDING_MODULE ?? null,
|
||||
};
|
||||
}
|
||||
for (const blocked of BLOCKED_DB_HOSTS) {
|
||||
if (connectionString.includes(blocked)) {
|
||||
throw new Error('Refusing production MEMORY_PGVECTOR_DATABASE_URL');
|
||||
}
|
||||
}
|
||||
const imported = await import('pg');
|
||||
const PgPool = imported?.Pool ?? imported?.default?.Pool;
|
||||
if (typeof PgPool !== 'function') {
|
||||
return { enabled: false, error: 'pg module unavailable' };
|
||||
}
|
||||
const table = String(env.MEMORY_PGVECTOR_TABLE ?? 'memory_embeddings').replace(/"/g, '');
|
||||
const pool = new PgPool({ connectionString, max: 2 });
|
||||
try {
|
||||
const result = await pool.query(`SELECT COUNT(*)::int AS count FROM "${table}"`);
|
||||
return {
|
||||
enabled: true,
|
||||
connectionHost: new URL(connectionString).hostname,
|
||||
table,
|
||||
rowCount: Number(result.rows[0]?.count ?? 0),
|
||||
embeddingModule: env.MEMORY_PGVECTOR_EMBEDDING_MODULE ?? null,
|
||||
dimensions: Number(env.MEMORY_V2_LOCAL_EMBEDDING_DIMENSIONS ?? 0) || null,
|
||||
};
|
||||
} finally {
|
||||
await pool.end().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function buildManifest({ memories, candidates, vectorSummary, env, userId }) {
|
||||
const exportedAt = new Date().toISOString();
|
||||
const watermarkMs = Date.now();
|
||||
const digest = crypto
|
||||
.createHash('sha256')
|
||||
.update(JSON.stringify({ memories, candidates, vectorSummary, watermarkMs }))
|
||||
.digest('hex');
|
||||
|
||||
const byUser = new Map();
|
||||
for (const item of memories) {
|
||||
if (!byUser.has(item.userId)) {
|
||||
byUser.set(item.userId, { memoryCount: 0, memoryIds: [], memoryHashes: [] });
|
||||
}
|
||||
const bucket = byUser.get(item.userId);
|
||||
bucket.memoryCount += 1;
|
||||
bucket.memoryIds.push(item.id);
|
||||
bucket.memoryHashes.push(item.memoryHash);
|
||||
}
|
||||
|
||||
return {
|
||||
schemaVersion: 'goose-v149-memory-manifest-v1',
|
||||
exportedAt,
|
||||
cutoverWatermarkMs: watermarkMs,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
textNormalization: 'utf8mb4_unicode_ci',
|
||||
scope: userId ? { userId } : { allUsers: true },
|
||||
memoryPolicy: {
|
||||
backend: env.MEMORY_BACKEND ?? 'legacy',
|
||||
vectorEnabled: env.MEMORY_VECTOR_ENABLED ?? '0',
|
||||
agentInjectionMode: env.MEMORY_AGENT_INJECTION_MODE ?? 'off',
|
||||
lifecycleEnabled: env.MEMORY_LIFECYCLE_ENABLED ?? '0',
|
||||
candidateEnabled: env.MEMORY_CANDIDATE_ENABLED ?? '0',
|
||||
},
|
||||
gooseSessionDb: {
|
||||
urlRedacted: String(env.GOOSE_SESSION_DB_URL ?? env.GOOSE_V149_SESSION_DB ?? 'goose_sessions_v149_dev')
|
||||
.replace(/\/\/[^:]+:[^@]+@/, '//***:***@'),
|
||||
schemaVersion: 16,
|
||||
},
|
||||
harnessMemoryKeys: [],
|
||||
vector: vectorSummary,
|
||||
summary: {
|
||||
userCount: byUser.size,
|
||||
memoryItemCount: memories.length,
|
||||
candidateCount: candidates.length,
|
||||
statusBreakdown: memories.reduce((acc, item) => {
|
||||
acc[item.status] = (acc[item.status] ?? 0) + 1;
|
||||
return acc;
|
||||
}, {}),
|
||||
digest,
|
||||
},
|
||||
users: [...byUser.entries()].map(([id, stats]) => ({
|
||||
userId: id,
|
||||
...stats,
|
||||
})),
|
||||
memories,
|
||||
candidates,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
const pool = createMysqlPoolFromEnv();
|
||||
try {
|
||||
const memories = await fetchMemoryItems(pool, args.userId);
|
||||
const candidates = await fetchCandidates(pool, args.userId);
|
||||
const vectorSummary = await fetchVectorSummary(process.env);
|
||||
const manifest = buildManifest({
|
||||
memories,
|
||||
candidates,
|
||||
vectorSummary,
|
||||
env: process.env,
|
||||
userId: args.userId,
|
||||
});
|
||||
|
||||
const defaultOutput = path.join(
|
||||
process.cwd(),
|
||||
'docs',
|
||||
'baselines',
|
||||
`goose-v149-memory-manifest-${manifest.exportedAt.replace(/[:.]/g, '-')}.json`,
|
||||
);
|
||||
const outputPath = args.output ? path.resolve(args.output) : defaultOutput;
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
||||
|
||||
if (args.json) {
|
||||
console.log(JSON.stringify({ ok: true, outputPath, summary: manifest.summary }, null, 2));
|
||||
} else {
|
||||
console.log(`GOOSE_V149_MEMORY_MANIFEST_OK: ${outputPath}`);
|
||||
console.log(
|
||||
` users=${manifest.summary.userCount} memories=${manifest.summary.memoryItemCount} candidates=${manifest.summary.candidateCount}`,
|
||||
);
|
||||
console.log(` digest=${manifest.summary.digest}`);
|
||||
}
|
||||
} finally {
|
||||
await pool.end().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`GOOSE_V149_MEMORY_MANIFEST_FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Local Goose v1.49 canary routing helpers (loopback only).
|
||||
* Production hosts are refused; see docs/goose-v149-canary-runbook.md.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { createCanaryPolicy, resolveCanaryTarget } from '../release-gate/canary-routing.mjs';
|
||||
|
||||
const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let value = trimmed.slice(eq + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"'))
|
||||
|| (value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (!(key in process.env)) process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFile(path.join(memindRoot, '.env.local'));
|
||||
|
||||
export const GOOSE_V149_CANARY_BEGIN = '# GOOSE_V149_CANARY_BEGIN';
|
||||
export const GOOSE_V149_CANARY_END = '# GOOSE_V149_CANARY_END';
|
||||
|
||||
/** Force-apply GOOSE_V149 canary block from .env.local (overrides .env). */
|
||||
export function applyGooseV149CanaryBlockEnv(env = process.env, rootDir = memindRoot) {
|
||||
const envFile = path.join(rootDir, '.env.local');
|
||||
if (!fs.existsSync(envFile)) return;
|
||||
const lines = fs.readFileSync(envFile, 'utf8').split('\n');
|
||||
let inBlock = false;
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed === GOOSE_V149_CANARY_BEGIN) {
|
||||
inBlock = true;
|
||||
continue;
|
||||
}
|
||||
if (trimmed === GOOSE_V149_CANARY_END) break;
|
||||
if (!inBlock || !trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let value = trimmed.slice(eq + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"'))
|
||||
|| (value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
export const GOOSE_CANARY_MODES = new Set(['off', 'all', 'users']);
|
||||
|
||||
const BLOCKED_HOSTS = new Set([
|
||||
'58.38.22.103',
|
||||
'120.26.184.105',
|
||||
'103.tkmind.cn',
|
||||
'105.tkmind.cn',
|
||||
]);
|
||||
|
||||
export const DEFAULT_GOOSE_STABLE_TARGET = 'https://127.0.0.1:18006';
|
||||
export const DEFAULT_GOOSE_V149_TARGET = 'https://127.0.0.1:18049';
|
||||
|
||||
function csvSet(value) {
|
||||
return new Set(
|
||||
String(value ?? '')
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
export function parseGooseCanaryMode(env = process.env) {
|
||||
const mode = String(env.TKMIND_GOOSE_CANARY ?? 'off').trim().toLowerCase();
|
||||
if (!GOOSE_CANARY_MODES.has(mode)) {
|
||||
throw new Error(
|
||||
`Unsupported TKMIND_GOOSE_CANARY "${mode}". Expected: ${[...GOOSE_CANARY_MODES].join(', ')}`,
|
||||
);
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
export function assertLoopbackGooseTarget(rawTarget, label = 'goose target') {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(String(rawTarget ?? '').trim());
|
||||
} catch {
|
||||
throw new Error(`${label} must be a valid URL`);
|
||||
}
|
||||
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
||||
throw new Error(`${label} must use http or https`);
|
||||
}
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
if (BLOCKED_HOSTS.has(host)) {
|
||||
throw new Error(`${label} refuses production host ${host}`);
|
||||
}
|
||||
if (!['127.0.0.1', 'localhost', '::1'].includes(host)) {
|
||||
throw new Error(`${label} must be loopback (127.0.0.1 / localhost / ::1)`);
|
||||
}
|
||||
parsed.pathname = parsed.pathname.replace(/\/+$/, '') || '';
|
||||
parsed.search = '';
|
||||
parsed.hash = '';
|
||||
return parsed.toString().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export function gooseCanaryPolicyFromEnv(env = process.env) {
|
||||
const userIds = csvSet(env.TKMIND_GOOSE_CANARY_USER_IDS);
|
||||
const usernames = csvSet(env.TKMIND_GOOSE_CANARY_USERNAMES);
|
||||
const wechatUserIds = csvSet(env.TKMIND_GOOSE_CANARY_WECHAT_USER_IDS);
|
||||
if (!userIds.size && !usernames.size && !wechatUserIds.size) {
|
||||
return null;
|
||||
}
|
||||
return createCanaryPolicy({ userIds, usernames, wechatUserIds, candidateTarget: 'candidate' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Portal → goosed API targets for local v1.49 canary.
|
||||
* Returns null when canary is off (caller keeps legacy TKMIND_API_TARGET parsing).
|
||||
*/
|
||||
export function resolveGooseApiTargetsFromEnv(env = process.env) {
|
||||
const mode = parseGooseCanaryMode(env);
|
||||
if (mode === 'off') return null;
|
||||
|
||||
const stable = assertLoopbackGooseTarget(
|
||||
env.TKMIND_API_TARGET_STABLE ?? DEFAULT_GOOSE_STABLE_TARGET,
|
||||
'TKMIND_API_TARGET_STABLE',
|
||||
);
|
||||
const v149 = assertLoopbackGooseTarget(
|
||||
env.TKMIND_API_TARGET_V149 ?? DEFAULT_GOOSE_V149_TARGET,
|
||||
'TKMIND_API_TARGET_V149',
|
||||
);
|
||||
|
||||
if (mode === 'all') {
|
||||
return Object.freeze({
|
||||
mode,
|
||||
stable,
|
||||
v149,
|
||||
primary: v149,
|
||||
targets: [v149],
|
||||
});
|
||||
}
|
||||
|
||||
const policy = gooseCanaryPolicyFromEnv(env);
|
||||
if (!policy) {
|
||||
throw new Error(
|
||||
'TKMIND_GOOSE_CANARY=users requires TKMIND_GOOSE_CANARY_USER_IDS and/or '
|
||||
+ 'TKMIND_GOOSE_CANARY_USERNAMES / TKMIND_GOOSE_CANARY_WECHAT_USER_IDS',
|
||||
);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
mode,
|
||||
stable,
|
||||
v149,
|
||||
primary: stable,
|
||||
targets: [stable, v149],
|
||||
policy,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveGooseTargetForIdentity(identity, config) {
|
||||
if (!config || config.mode === 'off') {
|
||||
return config?.stable ?? DEFAULT_GOOSE_STABLE_TARGET;
|
||||
}
|
||||
if (config.mode === 'all') return config.v149;
|
||||
const targetKey = resolveCanaryTarget(identity, config.policy);
|
||||
return targetKey === 'candidate' ? config.v149 : config.stable;
|
||||
}
|
||||
|
||||
export function describeGooseCanaryConfig(env = process.env) {
|
||||
const config = resolveGooseApiTargetsFromEnv(env);
|
||||
if (!config) {
|
||||
return {
|
||||
enabled: false,
|
||||
mode: 'off',
|
||||
stable: assertLoopbackGooseTarget(
|
||||
env.TKMIND_API_TARGET ?? DEFAULT_GOOSE_STABLE_TARGET,
|
||||
'TKMIND_API_TARGET',
|
||||
),
|
||||
v149: assertLoopbackGooseTarget(
|
||||
env.TKMIND_API_TARGET_V149 ?? DEFAULT_GOOSE_V149_TARGET,
|
||||
'TKMIND_API_TARGET_V149',
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
enabled: true,
|
||||
mode: config.mode,
|
||||
stable: config.stable,
|
||||
v149: config.v149,
|
||||
primary: config.primary,
|
||||
targets: [...config.targets],
|
||||
policySelectors: config.policy
|
||||
? {
|
||||
userIds: [...config.policy.userIds],
|
||||
usernames: [...config.policy.usernames],
|
||||
wechatUserIds: [...config.policy.wechatUserIds],
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function printCliStatus() {
|
||||
const summary = describeGooseCanaryConfig(process.env);
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
}
|
||||
|
||||
const modulePath = fileURLToPath(import.meta.url);
|
||||
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
||||
if (invokedPath && invokedPath === modulePath) {
|
||||
printCliStatus();
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
assertLoopbackGooseTarget,
|
||||
describeGooseCanaryConfig,
|
||||
parseGooseCanaryMode,
|
||||
resolveGooseApiTargetsFromEnv,
|
||||
resolveGooseTargetForIdentity,
|
||||
} from './goose-v149-canary.mjs';
|
||||
|
||||
test('parseGooseCanaryMode defaults to off', () => {
|
||||
assert.equal(parseGooseCanaryMode({}), 'off');
|
||||
assert.equal(parseGooseCanaryMode({ TKMIND_GOOSE_CANARY: 'all' }), 'all');
|
||||
});
|
||||
|
||||
test('resolveGooseApiTargetsFromEnv all mode pins Portal to v149', () => {
|
||||
const config = resolveGooseApiTargetsFromEnv({
|
||||
TKMIND_GOOSE_CANARY: 'all',
|
||||
TKMIND_API_TARGET_STABLE: 'https://127.0.0.1:18006',
|
||||
TKMIND_API_TARGET_V149: 'https://127.0.0.1:18049',
|
||||
});
|
||||
assert.equal(config.mode, 'all');
|
||||
assert.equal(config.primary, 'https://127.0.0.1:18049');
|
||||
assert.deepEqual(config.targets, ['https://127.0.0.1:18049']);
|
||||
});
|
||||
|
||||
test('resolveGooseApiTargetsFromEnv users mode exposes dual targets', () => {
|
||||
const config = resolveGooseApiTargetsFromEnv({
|
||||
TKMIND_GOOSE_CANARY: 'users',
|
||||
TKMIND_GOOSE_CANARY_USER_IDS: 'user-1',
|
||||
});
|
||||
assert.equal(config.mode, 'users');
|
||||
assert.equal(config.primary, 'https://127.0.0.1:18006');
|
||||
assert.deepEqual(config.targets, ['https://127.0.0.1:18006', 'https://127.0.0.1:18049']);
|
||||
});
|
||||
|
||||
test('resolveGooseTargetForIdentity routes canary users to v149', () => {
|
||||
const config = resolveGooseApiTargetsFromEnv({
|
||||
TKMIND_GOOSE_CANARY: 'users',
|
||||
TKMIND_GOOSE_CANARY_USER_IDS: 'user-abc',
|
||||
});
|
||||
assert.equal(
|
||||
resolveGooseTargetForIdentity({ id: 'user-abc' }, config),
|
||||
'https://127.0.0.1:18049',
|
||||
);
|
||||
assert.equal(
|
||||
resolveGooseTargetForIdentity({ id: 'other-user' }, config),
|
||||
'https://127.0.0.1:18006',
|
||||
);
|
||||
});
|
||||
|
||||
test('assertLoopbackGooseTarget refuses production hosts', () => {
|
||||
assert.throws(
|
||||
() => assertLoopbackGooseTarget('https://58.38.22.103:18006'),
|
||||
/refuses production host/,
|
||||
);
|
||||
});
|
||||
|
||||
test('describeGooseCanaryConfig reports disabled when off', () => {
|
||||
const summary = describeGooseCanaryConfig({
|
||||
TKMIND_API_TARGET: 'https://127.0.0.1:18006',
|
||||
});
|
||||
assert.equal(summary.enabled, false);
|
||||
assert.equal(summary.mode, 'off');
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Shared SSE helpers for local Goose v1.49 smoke scripts.
|
||||
*/
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Readable } from 'node:stream';
|
||||
import { Agent, fetch } from 'undici';
|
||||
|
||||
const blockedHosts = new Set(['58.38.22.103', '120.26.184.105']);
|
||||
|
||||
export function createV149Client(options = {}) {
|
||||
const host = options.host ?? process.env.GOOSE_V149_HOST ?? '127.0.0.1';
|
||||
const port = options.port ?? process.env.GOOSE_V149_PORT ?? '18049';
|
||||
const secret = options.secret ?? process.env.GOOSE_SERVER__SECRET_KEY ?? 'local-v149-dev-secret';
|
||||
if (blockedHosts.has(host)) {
|
||||
throw new Error(`Refusing production host ${host}`);
|
||||
}
|
||||
const base = `https://${host}:${port}`;
|
||||
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
|
||||
async function apiFetch(pathname, init = {}) {
|
||||
const headers = { ...(init.headers ?? {}), 'X-Secret-Key': secret };
|
||||
if (init.body && !headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
||||
return fetch(`${base}${pathname}`, { ...init, headers, dispatcher });
|
||||
}
|
||||
|
||||
async function apiJson(pathname, body, init = {}) {
|
||||
const response = await apiFetch(pathname, {
|
||||
method: init.method ?? 'POST',
|
||||
body: body == null ? undefined : JSON.stringify(body),
|
||||
...init,
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`${pathname} ${response.status}: ${text.slice(0, 800)}`);
|
||||
}
|
||||
return text.trim() ? JSON.parse(text) : {};
|
||||
}
|
||||
|
||||
return { base, apiFetch, apiJson };
|
||||
}
|
||||
|
||||
function parseSseData(frame) {
|
||||
let data = '';
|
||||
for (const line of frame.split('\n')) {
|
||||
if (line.startsWith('data:')) data += line.slice(5).trim();
|
||||
}
|
||||
if (!data) return null;
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectReplyEvents({
|
||||
client,
|
||||
sessionId,
|
||||
requestId = randomUUID(),
|
||||
userMessage,
|
||||
timeoutMs = Number(process.env.GOOSE_V149_REPLY_TIMEOUT_MS || 90_000),
|
||||
triggerReply = true,
|
||||
}) {
|
||||
const events = [];
|
||||
const eventsResponse = await client.apiFetch(`/sessions/${sessionId}/events`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'text/event-stream' },
|
||||
});
|
||||
if (!eventsResponse.ok || !eventsResponse.body) {
|
||||
const text = await eventsResponse.text().catch(() => '');
|
||||
throw new Error(`events ${eventsResponse.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
|
||||
if (triggerReply) {
|
||||
const replyResponse = await client.apiFetch(`/sessions/${sessionId}/reply`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
request_id: requestId,
|
||||
user_message: userMessage,
|
||||
}),
|
||||
});
|
||||
if (!replyResponse.ok) {
|
||||
const text = await replyResponse.text().catch(() => '');
|
||||
throw new Error(`reply ${replyResponse.status}: ${text.slice(0, 400)}`);
|
||||
}
|
||||
replyResponse.body?.cancel?.();
|
||||
}
|
||||
|
||||
const reader = Readable.fromWeb(eventsResponse.body);
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let finishEvent = null;
|
||||
let errorEvent = null;
|
||||
|
||||
for await (const chunk of reader) {
|
||||
if (Date.now() > deadline) break;
|
||||
buffer += decoder.decode(chunk, { stream: true });
|
||||
const frames = buffer.split('\n\n');
|
||||
buffer = frames.pop() ?? '';
|
||||
for (const frame of frames) {
|
||||
const event = parseSseData(frame);
|
||||
if (!event) continue;
|
||||
const routingId = event.chat_request_id ?? event.request_id;
|
||||
if (routingId && routingId !== requestId) continue;
|
||||
events.push(event);
|
||||
if (event.type === 'Finish') finishEvent = event;
|
||||
if (event.type === 'Error') errorEvent = event;
|
||||
if (finishEvent || errorEvent) {
|
||||
reader.cancel?.().catch(() => {});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (finishEvent || errorEvent) break;
|
||||
}
|
||||
|
||||
return {
|
||||
requestId,
|
||||
events,
|
||||
finishEvent,
|
||||
errorEvent,
|
||||
outcome: finishEvent ? 'finish' : errorEvent ? 'error' : 'timeout',
|
||||
};
|
||||
}
|
||||
|
||||
export function extractTokenStateFromEvents(events = []) {
|
||||
for (let i = events.length - 1; i >= 0; i -= 1) {
|
||||
const event = events[i];
|
||||
if (event?.type === 'Finish' && event.token_state) return event.token_state;
|
||||
if (event?.type === 'Message' && event.token_state) return event.token_state;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run upstream Goose v1.49 candidate on isolated local port (default 18049).
|
||||
# Does not touch 103 production ports 18006-18014 or Portal 8081.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
GOOSED_ROOT="${GOOSED_V149_ROOT:-/Users/john/Project/tkmind_go-v149}"
|
||||
PORT="${GOOSE_V149_PORT:-18049}"
|
||||
RUNTIME_ROOT="${GOOSE_V149_RUNTIME_ROOT:-/tmp/goose-v149-runtime}"
|
||||
GOOSE_BIN_NAME="${GOOSE_V149_BIN:-goose}"
|
||||
|
||||
for blocked in 18006 18007 18008 18009 18010 18011 18012 18013 18014 8081; do
|
||||
if [[ "${PORT}" == "${blocked}" ]]; then
|
||||
echo "[goose-v149] refusing stable/production port ${PORT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -d "${GOOSED_ROOT}" ]]; then
|
||||
echo "[goose-v149] worktree missing: ${GOOSED_ROOT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bash "${ROOT}/scripts/ensure-goose-v149-vendor.sh" 2>/dev/null || true
|
||||
bash "${ROOT}/scripts/ensure-goose-v149-local-db.sh"
|
||||
|
||||
PG_USER="${GOOSE_V149_PG_USER:-${USER:-john}}"
|
||||
PG_HOST="${GOOSE_V149_PG_HOST:-127.0.0.1}"
|
||||
PG_PORT="${GOOSE_V149_PG_PORT:-5432}"
|
||||
DB_NAME="${GOOSE_V149_SESSION_DB:-goose_sessions_v149_dev}"
|
||||
|
||||
mkdir -p "${RUNTIME_ROOT}/"{config,data,state,cache}
|
||||
|
||||
if [[ -x "${GOOSED_ROOT}/target/release/${GOOSE_BIN_NAME}" ]]; then
|
||||
GOOSED_BIN="${GOOSED_ROOT}/target/release/${GOOSE_BIN_NAME}"
|
||||
elif [[ -x "${GOOSED_ROOT}/target/debug/${GOOSE_BIN_NAME}" ]]; then
|
||||
GOOSED_BIN="${GOOSED_ROOT}/target/debug/${GOOSE_BIN_NAME}"
|
||||
else
|
||||
echo "[goose-v149] goose binary not found; build with:" >&2
|
||||
echo " bash ${ROOT}/scripts/build-goosed-v149-local.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export XDG_CONFIG_HOME="${RUNTIME_ROOT}/config"
|
||||
export XDG_DATA_HOME="${RUNTIME_ROOT}/data"
|
||||
export XDG_STATE_HOME="${RUNTIME_ROOT}/state"
|
||||
export XDG_CACHE_HOME="${RUNTIME_ROOT}/cache"
|
||||
|
||||
export GOOSE_PORT="${PORT}"
|
||||
export GOOSE_HOST="${GOOSE_HOST:-127.0.0.1}"
|
||||
export GOOSE_TLS="${GOOSE_TLS:-true}"
|
||||
export GOOSE_SERVER__SECRET_KEY="${GOOSE_SERVER__SECRET_KEY:-local-v149-dev-secret}"
|
||||
export GOOSE_DISABLE_KEYRING="${GOOSE_DISABLE_KEYRING:-1}"
|
||||
|
||||
# Phase 1: vanilla v1.49 may still use default SQLite unless PG adapter is ported.
|
||||
export GOOSE_SESSION_DB_URL="${GOOSE_SESSION_DB_URL:-postgresql://${PG_USER}@${PG_HOST}:${PG_PORT}/${DB_NAME}}"
|
||||
|
||||
export GOOSED_MCP_NODE_PATH="${GOOSED_MCP_NODE_PATH:-$(command -v node)}"
|
||||
export GOOSED_MCP_SERVER_PATH="${GOOSED_MCP_SERVER_PATH:-${ROOT}/mindspace-sandbox-mcp.mjs}"
|
||||
export PORTAL_RUNTIME_DIR="${PORTAL_RUNTIME_DIR:-${ROOT}}"
|
||||
export MINDSPACE_ROOT="${MINDSPACE_ROOT:-${ROOT}/MindSpace}"
|
||||
|
||||
# Memory: force legacy owner during local upgrade (see TKMIND_V1_49_MIGRATION.md)
|
||||
export MEMORY_BACKEND="${MEMORY_BACKEND:-legacy}"
|
||||
export MEMORY_VECTOR_ENABLED="${MEMORY_VECTOR_ENABLED:-0}"
|
||||
export MEMORY_AGENT_INJECTION_MODE="${MEMORY_AGENT_INJECTION_MODE:-off}"
|
||||
export MEMORY_LIFECYCLE_ENABLED="${MEMORY_LIFECYCLE_ENABLED:-0}"
|
||||
export MEMORY_CANDIDATE_ENABLED="${MEMORY_CANDIDATE_ENABLED:-0}"
|
||||
|
||||
echo "[goose-v149] binary=$(${GOOSED_BIN} --version 2>&1 | head -1)"
|
||||
echo "[goose-v149] port=${PORT} session_db=${DB_NAME} runtime=${RUNTIME_ROOT}"
|
||||
echo "[goose-v149] mode=goose serve (ACP HTTP/WS); legacy REST /agent/* requires tkmind_compat port"
|
||||
|
||||
cd "${GOOSED_ROOT}"
|
||||
SERVE_TLS_FLAG=()
|
||||
if [[ "${GOOSE_TLS:-true}" == "true" ]]; then
|
||||
SERVE_TLS_FLAG=(--tls)
|
||||
fi
|
||||
exec "${GOOSED_BIN}" serve --host "${GOOSE_HOST:-127.0.0.1}" --port "${PORT}" "${SERVE_TLS_FLAG[@]}"
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 2 orchestrator: cost / memory drift / portal canary verification.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const checks = [
|
||||
{ name: 'phase1-all', script: 'check-goosed-v149-all.mjs', required: true },
|
||||
{ name: 'cost-smoke', script: 'check-goosed-v149-cost-smoke.mjs', required: false },
|
||||
{ name: 'memory-drift', script: 'compare-goose-v149-memory-manifest.mjs', required: true },
|
||||
{ name: 'memory-failopen', script: 'check-goosed-v149-memory-failopen.mjs', required: true },
|
||||
{ name: 'web-smoke', script: 'check-goosed-v149-web-smoke.mjs', required: true },
|
||||
{ name: 'skills-smoke', script: 'check-goosed-v149-skills-smoke.mjs', required: true },
|
||||
{
|
||||
name: 'deepseek-tools',
|
||||
script: 'check-goosed-v149-deepseek-tools.mjs',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'thinking-preservation',
|
||||
script: 'check-goosed-v149-thinking-preservation.mjs',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'message-sanitize',
|
||||
script: 'compare-goose-v149-message-sanitize.mjs',
|
||||
required: true,
|
||||
},
|
||||
{ name: 'canary-portal', script: 'check-goosed-v149-canary-portal.mjs', required: true },
|
||||
{ name: 'portal-smoke', script: 'check-goosed-v149-portal-smoke.mjs', required: false },
|
||||
];
|
||||
|
||||
function run(script, extraEnv = {}) {
|
||||
const result = spawnSync(process.execPath, [path.join(root, 'scripts', script)], {
|
||||
cwd: root,
|
||||
env: { ...process.env, ...extraEnv },
|
||||
encoding: 'utf8',
|
||||
});
|
||||
return {
|
||||
ok: result.status === 0,
|
||||
status: result.status,
|
||||
stdout: result.stdout?.trim() ?? '',
|
||||
stderr: result.stderr?.trim() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const check of checks) {
|
||||
const item = { ...check, ...run(check.script) };
|
||||
results.push(item);
|
||||
const label = check.required ? 'required' : 'optional';
|
||||
console.log(`[goose-v149-phase2] ${item.ok ? 'OK' : 'FAIL'} (${label}) ${check.name}`);
|
||||
if (item.stdout) console.log(item.stdout);
|
||||
if (!item.ok && item.stderr) console.error(item.stderr);
|
||||
}
|
||||
|
||||
const failedRequired = results.filter((item) => item.required && !item.ok);
|
||||
const failedOptional = results.filter((item) => !item.required && !item.ok);
|
||||
|
||||
if (failedRequired.length) {
|
||||
console.error(`GOOSE_V149_PHASE2_FAIL: ${failedRequired.map((item) => item.name).join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (failedOptional.length) {
|
||||
console.log(
|
||||
`GOOSE_V149_PHASE2_OK_WITH_SKIPS: ${failedOptional.map((item) => item.name).join(', ')}`,
|
||||
);
|
||||
} else {
|
||||
console.log('GOOSE_V149_PHASE2_OK');
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 3 closeout gate: Phase 2 evidence + DeepSeek parity + canary unit tests.
|
||||
* Does not authorize 103/105; local loopback only.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const checks = [
|
||||
{ name: 'phase2', script: 'run-goosed-v149-phase2.mjs', required: true },
|
||||
{
|
||||
name: 'deepseek-parity',
|
||||
command: [process.execPath, '--test', 'release-gate/deepseek-production-parity.test.mjs'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'goose-canary-unit',
|
||||
command: [process.execPath, '--test', 'scripts/goose-v149-canary.test.mjs'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'memory-drift-again',
|
||||
script: 'compare-goose-v149-memory-manifest.mjs',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
function runScript(script, extraEnv = {}) {
|
||||
const result = spawnSync(process.execPath, [path.join(root, 'scripts', script)], {
|
||||
cwd: root,
|
||||
env: { ...process.env, ...extraEnv },
|
||||
encoding: 'utf8',
|
||||
});
|
||||
return {
|
||||
ok: result.status === 0,
|
||||
status: result.status,
|
||||
stdout: result.stdout?.trim() ?? '',
|
||||
stderr: result.stderr?.trim() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function runCommand(command, extraEnv = {}) {
|
||||
const [bin, ...args] = command;
|
||||
const result = spawnSync(bin, args, {
|
||||
cwd: root,
|
||||
env: { ...process.env, ...extraEnv },
|
||||
encoding: 'utf8',
|
||||
});
|
||||
return {
|
||||
ok: result.status === 0,
|
||||
status: result.status,
|
||||
stdout: result.stdout?.trim() ?? '',
|
||||
stderr: result.stderr?.trim() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const check of checks) {
|
||||
const item = {
|
||||
...check,
|
||||
...(check.script ? runScript(check.script) : runCommand(check.command)),
|
||||
};
|
||||
results.push(item);
|
||||
console.log(`[goose-v149-phase3] ${item.ok ? 'OK' : 'FAIL'} ${check.name}`);
|
||||
if (item.stdout) console.log(item.stdout);
|
||||
if (!item.ok && item.stderr) console.error(item.stderr);
|
||||
}
|
||||
|
||||
const failedRequired = results.filter((item) => item.required && !item.ok);
|
||||
if (failedRequired.length) {
|
||||
console.error(`GOOSE_V149_PHASE3_FAIL: ${failedRequired.map((item) => item.name).join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('GOOSE_V149_PHASE3_OK');
|
||||
console.log(' next: docs/goose-v149-phase3-closeout.md (merge prep, no 103 until approved)');
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# Toggle local Goose v1.49 canary for Portal dev (loopback only).
|
||||
# Does not modify 103/105 production. See docs/goose-v149-canary-runbook.md.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
ENV_FILE="${GOOSE_V149_CANARY_ENV_FILE:-${ROOT}/.env.local}"
|
||||
MARKER_BEGIN='# GOOSE_V149_CANARY_BEGIN'
|
||||
MARKER_END='# GOOSE_V149_CANARY_END'
|
||||
|
||||
STABLE_TARGET="${TKMIND_API_TARGET_STABLE:-https://127.0.0.1:18006}"
|
||||
V149_TARGET="${TKMIND_API_TARGET_V149:-https://127.0.0.1:18049}"
|
||||
V149_PORT="${GOOSE_V149_PORT:-18049}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: bash scripts/switch-goose-v149-canary.sh <command>
|
||||
|
||||
Commands:
|
||||
on [all|users] Enable v1.49 canary (default: all → Portal uses ${V149_TARGET})
|
||||
off|rollback Restore stable target (${STABLE_TARGET}) and disable canary flags
|
||||
status Print current canary block / env summary
|
||||
|
||||
Env overrides:
|
||||
GOOSE_V149_CANARY_ENV_FILE Target env file (default: .env.local)
|
||||
TKMIND_GOOSE_CANARY_USER_IDS Comma-separated user IDs (users mode)
|
||||
TKMIND_GOOSE_CANARY_USERNAMES Comma-separated usernames (users mode)
|
||||
|
||||
After "on", restart local Portal (pnpm dev) to pick up .env.local changes.
|
||||
EOF
|
||||
}
|
||||
|
||||
remove_canary_block() {
|
||||
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
awk -v begin="${MARKER_BEGIN}" -v end="${MARKER_END}" '
|
||||
$0 == begin { skip=1; next }
|
||||
$0 == end { skip=0; next }
|
||||
skip == 0 { print }
|
||||
' "${ENV_FILE}" > "${ENV_FILE}.tmp"
|
||||
mv "${ENV_FILE}.tmp" "${ENV_FILE}"
|
||||
}
|
||||
|
||||
write_canary_block() {
|
||||
local mode="$1"
|
||||
remove_canary_block
|
||||
touch "${ENV_FILE}"
|
||||
{
|
||||
echo "${MARKER_BEGIN}"
|
||||
echo "TKMIND_GOOSE_CANARY=${mode}"
|
||||
echo "TKMIND_API_TARGET_STABLE=${STABLE_TARGET}"
|
||||
echo "TKMIND_API_TARGET_V149=${V149_TARGET}"
|
||||
if [[ "${mode}" == "all" ]]; then
|
||||
echo "TKMIND_API_TARGET=${V149_TARGET}"
|
||||
echo "TKMIND_API_TARGETS="
|
||||
elif [[ "${mode}" == "users" ]]; then
|
||||
echo "TKMIND_API_TARGET=${STABLE_TARGET}"
|
||||
echo "TKMIND_API_TARGETS=${STABLE_TARGET},${V149_TARGET}"
|
||||
if [[ -n "${TKMIND_GOOSE_CANARY_USER_IDS:-}" ]]; then
|
||||
echo "TKMIND_GOOSE_CANARY_USER_IDS=${TKMIND_GOOSE_CANARY_USER_IDS}"
|
||||
fi
|
||||
if [[ -n "${TKMIND_GOOSE_CANARY_USERNAMES:-}" ]]; then
|
||||
echo "TKMIND_GOOSE_CANARY_USERNAMES=${TKMIND_GOOSE_CANARY_USERNAMES}"
|
||||
fi
|
||||
if [[ -n "${TKMIND_GOOSE_CANARY_WECHAT_USER_IDS:-}" ]]; then
|
||||
echo "TKMIND_GOOSE_CANARY_WECHAT_USER_IDS=${TKMIND_GOOSE_CANARY_WECHAT_USER_IDS}"
|
||||
fi
|
||||
fi
|
||||
echo "MEMORY_BACKEND=legacy"
|
||||
echo "MEMORY_VECTOR_ENABLED=0"
|
||||
echo "MEMORY_AGENT_INJECTION_MODE=off"
|
||||
echo "MEMORY_LIFECYCLE_ENABLED=0"
|
||||
echo "MEMORY_CANDIDATE_ENABLED=0"
|
||||
echo "MEMIND_PRODUCT_ANALYTICS_ENABLED=false"
|
||||
echo "H5_PUBLIC_BASE_URL=http://127.0.0.1:8081"
|
||||
echo "${MARKER_END}"
|
||||
} >> "${ENV_FILE}"
|
||||
}
|
||||
|
||||
cmd="${1:-}"
|
||||
case "${cmd}" in
|
||||
on)
|
||||
mode="${2:-all}"
|
||||
if [[ "${mode}" != "all" && "${mode}" != "users" ]]; then
|
||||
echo "Invalid mode: ${mode} (expected all or users)" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "${mode}" == "users" && -z "${TKMIND_GOOSE_CANARY_USER_IDS:-}${TKMIND_GOOSE_CANARY_USERNAMES:-}${TKMIND_GOOSE_CANARY_WECHAT_USER_IDS:-}" ]]; then
|
||||
echo "users mode requires TKMIND_GOOSE_CANARY_USER_IDS and/or USERNAMES / WECHAT_USER_IDS" >&2
|
||||
exit 2
|
||||
fi
|
||||
write_canary_block "${mode}"
|
||||
echo "[goose-v149-canary] enabled mode=${mode} env=${ENV_FILE}"
|
||||
echo "[goose-v149-canary] ensure v1.49 is running: bash scripts/run-goosed-v149-local.sh"
|
||||
echo "[goose-v149-canary] verify: curl -sk ${V149_TARGET}/status"
|
||||
;;
|
||||
off|rollback)
|
||||
remove_canary_block
|
||||
echo "[goose-v149-canary] disabled; restored env block removed from ${ENV_FILE}"
|
||||
echo "[goose-v149-canary] Portal falls back to default TKMIND_API_TARGET (${STABLE_TARGET}) from .env"
|
||||
;;
|
||||
status)
|
||||
if [[ -f "${ENV_FILE}" ]] && grep -q "${MARKER_BEGIN}" "${ENV_FILE}"; then
|
||||
echo "[goose-v149-canary] block present in ${ENV_FILE}:"
|
||||
awk -v begin="${MARKER_BEGIN}" -v end="${MARKER_END}" '
|
||||
$0 == begin { show=1; next }
|
||||
$0 == end { show=0; next }
|
||||
show == 1 { print " " $0 }
|
||||
' "${ENV_FILE}"
|
||||
else
|
||||
echo "[goose-v149-canary] no active block in ${ENV_FILE}"
|
||||
fi
|
||||
node "${ROOT}/scripts/goose-v149-canary.mjs" 2>/dev/null || true
|
||||
;;
|
||||
-h|--help|help|'')
|
||||
usage
|
||||
[[ -z "${cmd}" ]] && exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: ${cmd}" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
+11
@@ -117,9 +117,11 @@ import {
|
||||
describeMemindRuntimeProfile,
|
||||
loadMemindEnvFiles,
|
||||
} from './scripts/memind-runtime-profile.mjs';
|
||||
import { resolveGooseApiTargetsFromEnv, applyGooseV149CanaryBlockEnv } from './scripts/goose-v149-canary.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
loadMemindEnvFiles(__dirname);
|
||||
applyGooseV149CanaryBlockEnv(process.env, __dirname);
|
||||
const H5_ROOT = resolvePortalH5Root(__dirname, process.env);
|
||||
if (!fs.existsSync(H5_ROOT) || !fs.statSync(H5_ROOT).isDirectory()) {
|
||||
throw new Error(
|
||||
@@ -130,11 +132,20 @@ if (H5_ROOT !== __dirname) {
|
||||
console.log(`[Portal] Code root: ${__dirname}`);
|
||||
console.log(`[Portal] Persistent H5 root: ${H5_ROOT}`);
|
||||
loadMemindEnvFiles(H5_ROOT);
|
||||
applyGooseV149CanaryBlockEnv(process.env, H5_ROOT);
|
||||
}
|
||||
|
||||
applyMemindRuntimeProfile({ rootDir: H5_ROOT });
|
||||
|
||||
function parseApiTargets() {
|
||||
const gooseCanary = resolveGooseApiTargetsFromEnv(process.env);
|
||||
if (gooseCanary?.targets?.length) {
|
||||
console.log(
|
||||
`[Portal] Goose v1.49 canary mode=${gooseCanary.mode} primary=${gooseCanary.primary}`,
|
||||
);
|
||||
return [...gooseCanary.targets];
|
||||
}
|
||||
|
||||
const csvTargets = (process.env.TKMIND_API_TARGETS ?? '')
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
|
||||
+32
-2
@@ -23,6 +23,10 @@ import { reconcileAgentSession } from './session-reconcile.mjs';
|
||||
import { consumeSessionEventsUntilFinish } from './session-reply-wait.mjs';
|
||||
import { createSessionAccess, isSessionBrokerEnabled } from './session-broker.mjs';
|
||||
import { createSessionBrokerMetrics, isSessionBrokerMetricsEnabled } from './session-broker-metrics.mjs';
|
||||
import {
|
||||
resolveGooseApiTargetsFromEnv,
|
||||
resolveGooseTargetForIdentity,
|
||||
} from './scripts/goose-v149-canary.mjs';
|
||||
import { createImgproxySigner } from './imgproxy-signer.mjs';
|
||||
import { isDirectChatSessionId } from './direct-chat-service.mjs';
|
||||
import { ensureGooseUserMessageMetadata } from './goose-message.mjs';
|
||||
@@ -1189,6 +1193,15 @@ export function createTkmindProxy({
|
||||
: null;
|
||||
const targets = apiTargets?.length ? apiTargets : apiTarget ? [apiTarget] : [];
|
||||
const primaryTarget = targets[0] ?? apiTarget ?? '';
|
||||
let gooseCanaryConfig = null;
|
||||
try {
|
||||
gooseCanaryConfig = resolveGooseApiTargetsFromEnv(process.env);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'[TKMindProxy] Goose v1.49 canary config ignored:',
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
}
|
||||
let rrIdx = 0;
|
||||
|
||||
let imgproxySigner = null;
|
||||
@@ -1369,6 +1382,23 @@ export function createTkmindProxy({
|
||||
return fallbackPick();
|
||||
}
|
||||
|
||||
async function pickTargetForUser(userId, user = null) {
|
||||
if (gooseCanaryConfig?.mode === 'users' && userId) {
|
||||
const target = resolveGooseTargetForIdentity(
|
||||
{
|
||||
id: String(userId),
|
||||
username: user?.username ?? user?.name ?? '',
|
||||
wechatUserId: user?.wechatUserId ?? user?.wechat_user_id ?? '',
|
||||
},
|
||||
gooseCanaryConfig,
|
||||
);
|
||||
if (target && (targets.includes(target) || target === gooseCanaryConfig.v149 || target === gooseCanaryConfig.stable)) {
|
||||
if (await targetHealthy(target)) return target;
|
||||
}
|
||||
}
|
||||
return pickTarget();
|
||||
}
|
||||
|
||||
async function rememberSessionTarget(sessionId, target) {
|
||||
if (!runtimeRouter || !sessionId || !target) return;
|
||||
await runtimeRouter.registerSession(sessionId, target).catch(() => null);
|
||||
@@ -1491,7 +1521,7 @@ export function createTkmindProxy({
|
||||
const resolvedSessionPolicy = disableImageReading
|
||||
? withoutSessionImageRead(baseSessionPolicy)
|
||||
: baseSessionPolicy;
|
||||
const startTarget = await pickTarget();
|
||||
const startTarget = await pickTargetForUser(userId);
|
||||
const upstream = await apiFetch(startTarget, apiSecret, '/agent/start', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
@@ -2355,7 +2385,7 @@ export function createTkmindProxy({
|
||||
try {
|
||||
const workingDir = await userAuth.resolveWorkingDir(req.currentUser.id);
|
||||
const sessionPolicy = await userAuth.getAgentSessionPolicy(req.currentUser.id);
|
||||
const startTarget = await pickTarget();
|
||||
const startTarget = await pickTargetForUser(req.currentUser.id, req.currentUser);
|
||||
const upstream = await apiFetch(startTarget, apiSecret, '/agent/start', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user