@saluzi/saluzi-edu 0.2.52 → 0.2.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -199,7 +199,7 @@
199
199
  },
200
200
  {
201
201
  "name": "sa",
202
- "description": "Toggle SA mode — minimal prompt, 4 core tools, zero background tasks",
202
+ "description": "Toggle SA mode, or switch tool level: /sa [minimal|extended]. Minimal = 4 tools (Read/Edit/Write/Bash), extended = 7 (+Glob/Grep/WebFetch).",
203
203
  "descriptionZh": "切换 SA 极简模式:4 工具 + 20 行 prompt + 零后台任务"
204
204
  },
205
205
  {
@@ -677,7 +677,7 @@
677
677
  },
678
678
  "sa": {
679
679
  "name": "sa",
680
- "description": "Toggle SA mode — minimal prompt, 4 core tools, zero background tasks",
680
+ "description": "Toggle SA mode, or switch tool level: /sa [minimal|extended]. Minimal = 4 tools (Read/Edit/Write/Bash), extended = 7 (+Glob/Grep/WebFetch).",
681
681
  "descriptionZh": "切换 SA 极简模式:4 工具 + 20 行 prompt + 零后台任务"
682
682
  },
683
683
  "skill-learning": {
@@ -3123,7 +3123,7 @@
3123
3123
  "省 token"
3124
3124
  ]
3125
3125
  },
3126
- "content": "\n## 为什么需要 Token 节约模式\n\nSaluzi 默认开启大量「让 AI 更聪明」的后台机制:自动记忆提取、提示建议、会话记忆整合、验证 Agent、相关记忆预取、Agent 摘要……这些机制各自只消耗少量 token,但叠在一起会让每次对话的「隐性开销」相当可观。\n\n当你处于以下场景时,这些开销就是纯浪费:\n\n- **API Key 按量计费**,想压到最低\n- **弱模型 / 小上下文窗口**,每一段 prompt 都很贵\n- **一次性脚本任务**,不需要长期记忆\n- **清晰的单文件修改**,不需要多角度验证\n\n两种节约模式就是为这些场景设计的开关,按「砍多少」分档:\n\n| 模式 | 砍掉的范围 | 工具集 | System Prompt | 适合 |\n|------|-----------|--------|--------------|------|\n| **Poor** | 5 项后台副作用 | 全部保留 | 完整 | 日常开发省 token |\n| **SA** | 全部后台 + 工具收敛 | 仅 4 核心 | ~20 行极简 | 极限省 token / 弱模型 |\n\nSA 是 Poor 的**严格超集**——Poor 关掉的一切,SA 也关掉;SA 还额外砍掉工具和 prompt。两者的关系类似「省电模式」与「飞行模式」。\n\n## Poor 节约模式\n\n### 是什么\n\n`/poor` 切换 Poor 模式。开启后 Saluzi 跳过 5 类后台副作用任务,并对若干辅助模型调用降级。**主对话的工具集与 system prompt 完全不变**——你只是少了一些「后台 whisper」。\n\n### 关闭了什么\n\n| 被关闭的子系统 | 位置 | 原本做什么 |\n|---------------|------|-----------|\n| extract_memories | Stop hook | 每轮结束后从对话提取记忆写入 MEMORY.md |\n| prompt_suggestion | Stop hook | 给用户猜测「下一步可能想问什么」 |\n| autoDream | Stop hook | 后台整合/蒸馏历史记忆 |\n| AgentSummary | 后台 | 给子 Agent 生成摘要 |\n| 验证 Agent | System prompt | 非平凡改动后强制独立审查 |\n\n同时这些**辅助模型调用降级**(不影响主模型):\n\n| 调用点 | 原模型 | Poor 后 |\n|--------|--------|---------|\n| autoMode critique | MainLoop | SmallFast |\n| permission explainer | MainLoop | SmallFast |\n| universal classifier | MainLoop | DefaultPro |\n| yolo classifier | MainLoop | DefaultPro |\n\n降级意味着权限分类、自动模式批评等「辅助判断」用更便宜的模型跑,质量略降但成本显著下降。主对话模型不变。\n\n### 优势\n\n- **工具集完整**:Read/Edit/Write/Bash/Glob/Grep/WebFetch/Task/MCP……全部照常\n- **System prompt 完整**:项目记忆、CodeGraph、技能、SALUZI.md 仍然注入\n- **可逆且持久**:`/poor` 再按一次关闭;状态写入 `settings.json` 的 `poorMode` 字段,重启后保留\n- **粒度温和**:只砍「后台 whisper」,主循环质量基本无损\n\n### 劣势\n\n- **不再自动积累记忆**:MEMORY.md 不会自动更新,需要手动 `/memory` 编辑\n- **无提示建议**:终端不再显示「你可能想问……」的快捷建议\n- **无验证 Agent**:复杂改动后没有独立审查兜底,需要自己跑测试\n- **辅助分类降级**:权限判断、自动模式批评的精度略降\n\n## SA 极简模式\n\n### 是什么\n\n`/sa` 切换 SA 模式(极简风格 minimal harness)。开启后 Saluzi 收敛到「最小可用 harness」:\n\n- **System prompt 收缩到 ~20 行**:只有角色定义 + 4 个工具说明 + 基本准则 + cwd + 日期。没有动态段落、没有项目记忆、没有 CodeGraph、没有 MCP 说明、没有会话指导。\n- **工具收敛到 4 个核心**:Read / Edit / Write / Bash。其余工具(Glob、Grep、WebFetch、Task、TodoWrite、MCP、Skill……)全部移除。\n- **所有后台副作用归零**:Poor 关掉的 5 项 + 会话记忆 + 相关记忆预取,全部跳过。\n\n这是「把 Saluzi 当成一个极简的 Read/Edit/Write/Bash 编码助手」的开关——类似极简 harness 的体验。\n\n### SA 的 System Prompt 长什么样\n\n完整内容就这些:\n\n```\nYou are an expert coding assistant operating inside Saluzi, a coding agent harness.\nYou help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n- Read: Read file contents from the filesystem\n- Edit: Make targeted edits to existing files\n- Write: Create new files or overwrite existing files\n- Bash: Execute shell commands\n\nGuidelines:\n- Be concise in your responses\n- Show file paths clearly when working with files\n- Use file_path:line_number format when referencing code locations\n- Do not use a colon before tool calls\n- Ask the user before making significant architectural decisions\n\nCurrent working directory: <cwd>\nDate: <date>\n```\n\n对比默认 prompt 的数千字(项目记忆 + CodeGraph + 技能 + 会话指导 + 工具说明 + 安全规则……),SA prompt 几乎是零开销。\n\n### 关闭了什么\n\nSA 关闭 = Poor 的全部 + 以下额外项:\n\n| 额外关闭项 | 位置 | 原本做什么 |\n|-----------|------|-----------|\n| 会话记忆提取 | sessionMemory | 跨会话蒸馏项目级记忆 |\n| 相关记忆预取 | attachments | 每轮用户消息后 side-query 检索相关记忆 |\n| 全部工具(除 4 核心) | tools | Glob/Grep/Task/WebFetch/MCP/Skill 等 |\n| 完整 system prompt | prompts | 替换为 20 行极简版 |\n\n### 优势\n\n- **极限省 token**:prompt 从数千字降到 ~20 行,每轮省下大量输入 token\n- **弱模型友好**:小上下文窗口 / 弱模型不会被巨型 prompt 挤占空间\n- **响应快**:没有后台 side-query 拖慢首字节\n- **行为可预测**:AI 只有 4 个工具,不会偷偷跑 WebFetch / Task / MCP,调试简单\n- **可逆且持久**:`/sa` 再按一次关闭;状态写入 `settings.json` 的 `saMode` 字段\n\n### 劣势\n\n- **没有 Glob/Grep**:AI 找文件只能用 `Bash(ls/find)` + `Read`,搜索效率低\n- **没有 Task/TodoWrite**:无法拆分多步任务、不能 spawn 子 Agent\n- **没有 WebFetch/WebSearch**:不能查文档、不能上网\n- **没有 MCP/Skill**:所有 MCP 服务器工具、自定义技能全部不可用\n- **没有项目记忆**:SALUZI.md、MEMORY.md、CodeGraph 知识都不注入,AI 对项目「无记忆」\n- **没有验证 Agent**:同 Poor\n- **AI 不知道自己的完整能力**:极简 prompt 不说明权限模式、安全规则、提交规范等,复杂工作流需要你手动提示\n\n> 简言之:SA 模式适合「我就让 AI 改这一个文件」「跑个脚本看看」这类窄任务,不适合复杂架构重构或需要项目上下文的工作。\n\n## 两种模式的完整对比\n\n| 维度 | 默认 | Poor | SA |\n|------|------|------|-----|\n| System prompt | 数千字(完整动态) | 数千字(完整动态) | ~20 行(极简) |\n| 工具集 | 全部 | 全部 | 4 核心(Read/Edit/Write/Bash) |\n| extract_memories | ✅ | ❌ | ❌ |\n| prompt_suggestion | ✅ | ❌ | ❌ |\n| autoDream | ✅ | ❌ | ❌ |\n| AgentSummary | ✅ | ❌ | ❌ |\n| 验证 Agent | ✅ | ❌ | ❌ |\n| 会话记忆提取 | ✅ | ✅ | ❌ |\n| 相关记忆预取 | ✅ | ✅ | ❌ |\n| 项目记忆注入 (SALUZI.md) | ✅ | ✅ | ❌ |\n| CodeGraph | ✅ | ✅ | ❌ |\n| MCP 工具 | ✅ | ✅ | ❌ |\n| Skill 工具 | ✅ | ✅ | ❌ |\n| Glob/Grep/WebFetch/Task | ✅ | ✅ | ❌ |\n| 辅助分类模型 | MainLoop | Pro/SmallFast | Pro/SmallFast |\n| 主对话模型 | 不变 | 不变 | 不变 |\n| 输入 token / 轮 | 基准 | 略低(少后台 side-query) | 显著低(prompt 极小) |\n| 适合场景 | 日常全功能 | 日常省 token | 极限省 / 弱模型 / 窄任务 |\n\n## 与其他功能的兼容性\n\n这是最关键的部分——开了节约模式后,你常用的那些 Saluzi 特色还能不能用。\n\n### MOM 混合模型\n\n| 模式 | 兼容性 | 说明 |\n|------|--------|------|\n| Poor | ✅ 完全兼容 | Poor 不触碰 MOM 配置,顾问与主机按 `/mom` 设置正常运行 |\n| SA | ⚠️ 受限兼容 | MOM 仍可运行,但**主机只有 4 个工具**(Read/Edit/Write/Bash);顾问本就是 text-only 不受影响 |\n\n> 如果在 SA 模式下用 mom-stair,弱模型顾问(text-only)正常工作;升级到主机后主机也只能用 4 工具执行——不能 Glob/Grep/WebFetch。建议 SA + MOM 时把主机任务限定在文件读写 + 命令执行。\n\n### CodeGraph 代码智能\n\n| 模式 | 兼容性 | 说明 |\n|------|--------|------|\n| Poor | ✅ 完全兼容 | CodeGraph 工具与 prompt 段落照常注入 |\n| SA | ❌ 不可用 | 工具被收敛到 4 核心(无 CodeGraph 工具),prompt 也不含 CodeGraph 段落 |\n\n### 记忆系统 (Memory V2 / SALUZI.md)\n\n| 模式 | 兼容性 | 说明 |\n|------|--------|------|\n| Poor | ⚠️ 只读不写 | 已有记忆仍注入 prompt;但 extract_memories/autoDream 关闭,不再自动积累新记忆 |\n| SA | ❌ 完全隔离 | 既不注入(极简 prompt 无记忆段),也不提取(session memory 关闭) |\n\n### 验证 Agent (Verification Agent)\n\n| 模式 | 兼容性 |\n|------|--------|\n| Poor | ❌ 跳过 |\n| SA | ❌ 跳过 |\n\n两种模式都跳过验证 Agent。需要独立审查时请关闭节约模式后再跑,或手动 spawn 子 Agent(SA 模式下无 Task 工具,需先关 SA)。\n\n### Plan 模式 / Sandbox / Hooks\n\n| 模式 | Plan | Sandbox | Hooks |\n|------|------|---------|-------|\n| Poor | ✅ | ✅ | ✅ |\n| SA | ✅ | ✅ | ✅ |\n\n权限模式、沙箱、hooks 都是工具执行层机制,与节约模式正交,完全不受影响。\n\n### MCP 服务器 / Skill\n\n| 模式 | MCP | Skill |\n|------|-----|-------|\n| Poor | ✅ | ✅ |\n| SA | ❌ 工具被移除 | ❌ 工具被移除 |\n\nSA 模式下 MCP 服务器仍可连接(连接层不变),但 MCP 提供的工具不在 4 核心之列,不会被注入工具列表。\n\n### Auto Mode (/auto)\n\n| 模式 | 兼容性 | 说明 |\n|------|--------|------|\n| Poor | ⚠️ 降级 | autoMode 仍运行,但 critique 步骤用 SmallFast 模型 |\n| SA | ⚠️ 降级 | 同 Poor;且主机工具受限 |\n\n## 权限模式与安全风险\n\n> 用户最关心的问题:**开了节约模式,AI 会不会偷偷执行 `rm -rf /`、`drop database` 这类删库命令?**\n>\n> 简短回答:**节约模式本身不改变权限策略,不会让危险命令「更容易跑」**。真正的风险来自你选的**权限模式**(`default` / `acceptEdits` / `bypassPermissions` / `auto`),而不是 Poor/SA。但 Poor/SA 会把权限分类器降级到更便宜的模型,在 `bypassPermissions`/`auto` 模式下**理论上**误判概率略升。下面逐层拆解。\n\n### 权限模式与节约模式正交\n\n权限模式由 `/permissions` 或 `settings.json` 的 `permissions.defaultMode` 控制,**与 Poor/SA 完全独立**。开 Poor 或 SA 不会切换你的权限模式:\n\n| 权限模式 | Bash 危险命令会怎样 | Poor/SA 影响 |\n|---------|---------------------|-------------|\n| `default` | 每条命令弹确认,`rm -rf` 类必问你 | 无——该问还是问 |\n| `acceptEdits` | 只自动放行文件编辑,**Bash 仍逐条问** | 无——Bash 仍问 |\n| `plan` | 只读,不能执行任何写操作 | 无——根本跑不了 |\n| `bypassPermissions` | 分类器自动判断,不问你 | ⚠️ 分类器降级(见下) |\n| `auto` | autoMode 分类器自动判断 | ⚠️ 分类器降级(见下) |\n\n**结论**:在 `default` / `acceptEdits` / `plan` 这三种模式下,无论开不开 Poor/SA,危险命令都必须经过你手动确认——**不存在「开了 SA 就自动删库」的情况**。\n\n### SA 模式仍执行 deny 规则\n\nSA 模式收敛工具集时调用的是 `filterToolsByDenyRules`——也就是说你在 `settings.json` 里配的 `permissions.deny` 规则**在 SA 模式下照常生效**:\n\n```json\n{\n \"permissions\": {\n \"deny\": [\n \"Bash(rm -rf:*)\",\n \"Bash(rm -rf /*:*)\",\n \"Bash(drop database:*)\",\n \"Bash(git push --force:*)\",\n \"Bash(:(){ :|:& };:)\",\n \"Bash(mkfs*:*)\"\n ]\n }\n}\n```\n\n这些规则会在工具列表阶段就**直接屏蔽**匹配的命令——AI 根本看不到这些工具能跑这类命令,分类器也不会被调用。这是比分类器更硬的防线,且**不受 Poor/SA 影响**。\n\n> 强烈建议:无论用不用节约模式,都把 `rm -rf /`、`drop database`、`mkfs`、fork 炸弹等不可逆命令写进 `deny`。这是「物理隔离」级别的一刀切。\n\n### 分类器降级:风险有多大\n\n在 `bypassPermissions` 或 `auto` 模式下,命令是否自动放行由**分类器**(yoloClassifier / universalClassifier)决定。Saluzi 的分类器 prompt 明确把以下列为 **Irreversible Local Destruction(不可逆本地破坏)**:\n\n- `rm -rf` 递归强制删除非平凡路径\n- `Remove-Item -Recurse -Force`(PowerShell 等价物)\n- `> file` 截断已有文件为空\n- `drop database` 删库\n- `git push --force`(视上下文)\n\nPoor/SA 模式下分类器模型降级:\n\n| 模式 | 分类器模型 | 影响 |\n|------|-----------|------|\n| 默认 | MainLoop(如 Max/Pro 主模型) | 精度最高 |\n| Poor/SA | DefaultPro | 略低,但仍是 Pro 级模型 |\n\n**关键点**:\n\n- 分类器**仍然运行**——Poor/SA 没有关闭分类器,只是让它用更便宜的模型跑。\n- Pro 级模型对 `rm -rf /` 这种明显的破坏性命令识别能力依然很强——这不是「盲放」。\n- 降级影响的是**边界模糊**的命令(如 `rm -rf ./build` 这种有歧义的)的判断精度,不是放行 `rm -rf /`。\n\n**所以「删库」风险的真实情况**:\n\n1. `default` / `acceptEdits` / `plan` 模式:**零风险**,必须你确认。\n2. `bypassPermissions` / `auto` + 默认:**低风险**,分类器(MainLoop)会拦。\n3. `bypassPermissions` / `auto` + Poor/SA:**略升的边界风险**,分类器(Pro)对明显破坏命令仍拦,但对模糊命令误判概率略高。\n4. 任何模式 + 配了 `deny` 规则:**零风险**,物理屏蔽。\n\n### SA 模式的特殊风险面\n\nSA 模式只保留 4 个工具(Read/Edit/Write/Bash),这反而**缩小**了攻击面:\n\n- **没有 WebFetch/WebSearch**:AI 不能下载并执行远程脚本。\n- **没有 Task**:不能 spawn 子 Agent 绕过主循环审查。\n- **没有 MCP 工具**:外部 MCP 服务器不能注入未审查的工具。\n- **没有 Skill**:自定义技能不会被执行。\n\n但 **Bash 仍在**——这是 SA 模式下唯一的「能干坏事」的入口。只要 Bash 的权限策略到位(`default` 模式或 `deny` 规则),SA 模式的整体风险面比默认模式**更小**,不是更大。\n\n### 安全使用建议\n\n| 建议 | 原因 |\n|------|------|\n| 默认用 `default` 或 `acceptEdits` 权限模式 | Bash 逐条确认,物理安全 |\n| 在 `settings.json` 配 `permissions.deny` 屏蔽 `rm -rf /`、`drop database`、`mkfs`、fork 炸弹 | 一刀切物理隔离,不受任何模式影响 |\n| 开启沙箱(`sandbox.enabled: true`)| 限制写入路径与网络访问,`rm -rf /` 直接失败 |\n| 避免 `bypassPermissions` + Poor/SA 组合 | 分类器降级 + 不问你 = 信任分类器,边界命令风险略升 |\n| SA 模式下尤其盯紧 Bash 输出 | 没有验证 Agent 兜底,需要你自己看命令 |\n| 用 `/permissions` 临时切 `plan` 模式做危险探索 | 只读,根本不能执行写操作 |\n\n### 一句话总结\n\n**Poor/SA 不会让 Saluzi「更敢删库」**——它们只砍后台副作用和工具数量,不动权限策略与 deny 规则。真正的删库风险来自 `bypassPermissions`/`auto` 权限模式本身;要彻底消除风险,配 `deny` 规则 + 开沙箱 + 用 `default` 模式,这三道防线与节约模式正交,同时开启完全安全。\n\n## 如何开启 / 关闭\n\n### 命令切换\n\n```\n> /poor # 切换 Poor 模式(开↔关)\n> /sa # 切换 SA 模式(开↔关)\n```\n\n输出会确认当前状态,例如:\n\n```\nSA mode ON — minimal prompt, 4 tools (Read/Edit/Write/Bash), all background tasks disabled\n```\n\n### 配置面板\n\n`/config` → 找到 `SA mode (minimal harness)` 与 `poor mode` 开关,`Enter` 切换。\n\n### settings.json\n\n直接编辑 `~/.saluzi-edu/settings.json`:\n\n```json\n{\n \"poorMode\": true,\n \"saMode\": true\n}\n```\n\n两个字段都可选,`true` 开启、省略或 `false` 关闭。重启 Saluzi 后生效。\n\n> **不要同时开 Poor 和 SA**。SA 是 Poor 的严格超集,同时开启只是冗余——工具集与 prompt 都按 SA 走,Poor 的标志位不起额外作用。需要极限省 token 就直接开 SA;需要保留全部工具就开 Poor。\n\n## 何时该用哪个\n\n| 你的情况 | 推荐 |\n|---------|------|\n| 日常开发,想省点 token,但还要用 Glob/Grep/MOM/CodeGraph | **Poor** |\n| API Key 按量计费,想压到最低 | **Poor**(日常)或 **SA**(窄任务) |\n| 用弱模型 / 小上下文窗口 | **SA** |\n| 一次性脚本:改一个文件、跑个命令 | **SA** |\n| 复杂架构重构、需要项目记忆与多步任务 | **关闭**(用默认) |\n| 需要 MOM 多顾问交叉验证 | **Poor**(保留工具)或 **关闭** |\n| 调试时想要「AI 不要偷偷干别的」 | **SA** |\n| 长期挂着的会话,不想后台烧 token | **Poor** |\n\n## 节约效果怎么看\n\n开启后用 `/cost` 观察单轮 token:\n\n```\n> /cost\n```\n\n重点看**输入 token**——SA 模式下每轮输入 token 会显著下降(因为 prompt 从数千字缩到 ~20 行)。后台 side-query 的输出 token 也归零。多轮对比即可看到差异。\n\n## 下一步\n\n- [MOM 混合模型](./mom-mixed-models) — 节约模式下 MOM 如何运行\n- [模型选择与切换](./model-selection) — `/poor` 命令的简述与模型降级\n- [查看消耗](./cost-usage) — `/cost` 监控节约效果\n- [记忆系统](./memory-system) — Poor/SA 对记忆的影响\n"
3126
+ "content": "\n## 为什么需要 Token 节约模式\n\nSaluzi 默认开启大量「让 AI 更聪明」的后台机制:自动记忆提取、提示建议、会话记忆整合、验证 Agent、相关记忆预取、Agent 摘要……这些机制各自只消耗少量 token,但叠在一起会让每次对话的「隐性开销」相当可观。\n\n当你处于以下场景时,这些开销就是纯浪费:\n\n- **API Key 按量计费**,想压到最低\n- **弱模型 / 小上下文窗口**,每一段 prompt 都很贵\n- **一次性脚本任务**,不需要长期记忆\n- **清晰的单文件修改**,不需要多角度验证\n\n两种节约模式就是为这些场景设计的开关,按「砍多少」分档:\n\n| 模式 | 砍掉的范围 | 工具集 | System Prompt | 适合 |\n|------|-----------|--------|--------------|------|\n| **Poor** | 5 项后台副作用 | 全部保留 | 完整 | 日常开发省 token |\n| **SA** | 全部后台 + 工具收敛 | 仅 4 核心 | ~20 行极简 | 极限省 token / 弱模型 |\n\nSA 是 Poor 的**严格超集**——Poor 关掉的一切,SA 也关掉;SA 还额外砍掉工具和 prompt。两者的关系类似「省电模式」与「飞行模式」。\n\n## Poor 节约模式\n\n### 是什么\n\n`/poor` 切换 Poor 模式。开启后 Saluzi 跳过 5 类后台副作用任务,并对若干辅助模型调用降级。**主对话的工具集与 system prompt 完全不变**——你只是少了一些「后台 whisper」。\n\n### 关闭了什么\n\n| 被关闭的子系统 | 位置 | 原本做什么 |\n|---------------|------|-----------|\n| extract_memories | Stop hook | 每轮结束后从对话提取记忆写入 MEMORY.md |\n| prompt_suggestion | Stop hook | 给用户猜测「下一步可能想问什么」 |\n| autoDream | Stop hook | 后台整合/蒸馏历史记忆 |\n| AgentSummary | 后台 | 给子 Agent 生成摘要 |\n| 验证 Agent | System prompt | 非平凡改动后强制独立审查 |\n\n同时这些**辅助模型调用降级**(不影响主模型):\n\n| 调用点 | 原模型 | Poor 后 |\n|--------|--------|---------|\n| autoMode critique | MainLoop | SmallFast |\n| permission explainer | MainLoop | SmallFast |\n| universal classifier | MainLoop | DefaultPro |\n| yolo classifier | MainLoop | DefaultPro |\n\n降级意味着权限分类、自动模式批评等「辅助判断」用更便宜的模型跑,质量略降但成本显著下降。主对话模型不变。\n\n### 优势\n\n- **工具集完整**:Read/Edit/Write/Bash/Glob/Grep/WebFetch/Task/MCP……全部照常\n- **System prompt 完整**:项目记忆、CodeGraph、技能、SALUZI.md 仍然注入\n- **可逆且持久**:`/poor` 再按一次关闭;状态写入 `settings.json` 的 `poorMode` 字段,重启后保留\n- **粒度温和**:只砍「后台 whisper」,主循环质量基本无损\n\n### 劣势\n\n- **不再自动积累记忆**:MEMORY.md 不会自动更新,需要手动 `/memory` 编辑\n- **无提示建议**:终端不再显示「你可能想问……」的快捷建议\n- **无验证 Agent**:复杂改动后没有独立审查兜底,需要自己跑测试\n- **辅助分类降级**:权限判断、自动模式批评的精度略降\n\n## SA 极简模式\n\n### 是什么\n\n`/sa` 切换 SA 模式(极简风格 minimal harness)。开启后 Saluzi 收敛到「最小可用 harness」:\n\n- **System prompt 收缩到 ~20 行**:只有角色定义 + 工具说明 + 基本准则 + cwd + 日期。没有动态段落、没有项目记忆、没有 CodeGraph、没有 MCP 说明、没有会话指导。\n- **工具收敛到核心集**:`minimal` 档 4 个(Read / Edit / Write / Bash),`extended` 档 7 个(再加 Glob / Grep / WebFetch)。其余工具(Task、TodoWrite、MCP、Skill……)全部移除。\n- **所有后台副作用归零**:Poor 关掉的 5 项 + 会话记忆 + 相关记忆预取,全部跳过。\n\n这是「把 Saluzi 当成一个极简的编码助手」的开关——类似极简 harness 的体验。工具集分两档,按需选择。\n\n### SA 的 System Prompt 长什么样\n\n完整内容就这些:\n\n```\nYou are an expert coding assistant operating inside Saluzi, a coding agent harness.\nYou help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n- Read: Read file contents from the filesystem\n- Edit: Make targeted edits to existing files\n- Write: Create new files or overwrite existing files\n- Bash: Execute shell commands\n\nGuidelines:\n- Be concise in your responses\n- Show file paths clearly when working with files\n- Use file_path:line_number format when referencing code locations\n- Do not use a colon before tool calls\n- Ask the user before making significant architectural decisions\n\nCurrent working directory: <cwd>\nDate: <date>\n```\n\n以上是 `minimal` 档的 prompt。`extended` 档的 `Available tools` 部分会多列 3 行(Glob / Grep / WebFetch),其余不变。\n\n对比默认 prompt 的数千字(项目记忆 + CodeGraph + 技能 + 会话指导 + 工具说明 + 安全规则……),SA prompt 几乎是零开销。\n\n### 关闭了什么\n\nSA 关闭 = Poor 的全部 + 以下额外项:\n\n| 额外关闭项 | 位置 | 原本做什么 |\n|-----------|------|-----------|\n| 会话记忆提取 | sessionMemory | 跨会话蒸馏项目级记忆 |\n| 相关记忆预取 | attachments | 每轮用户消息后 side-query 检索相关记忆 |\n| 全部工具(除核心集) | tools | Task/TodoWrite/MCP/Skill 等(minimal 档还去掉 Glob/Grep/WebFetch) |\n| 完整 system prompt | prompts | 替换为 20 行极简版 |\n\n### 优势\n\n- **极限省 token**:prompt 从数千字降到 ~20 行,每轮省下大量输入 token\n- **弱模型友好**:小上下文窗口 / 弱模型不会被巨型 prompt 挤占空间\n- **响应快**:没有后台 side-query 拖慢首字节\n- **行为可预测**:AI 只有核心工具,不会偷偷跑 Task / MCP,调试简单\n- **可逆且持久**:`/sa` 再按一次关闭;状态写入 `settings.json` 的 `saMode` 字段\n\n### 劣势\n\n- **minimal 档没有 Glob/Grep/WebFetch**:AI 找文件只能用 `Bash(ls/find)` + `Read`,搜索效率低。切到 `extended` 档可恢复这三个工具\n- **没有 Task/TodoWrite**:无法拆分多步任务、不能 spawn 子 Agent\n- **没有 WebSearch**:minimal/extended 都不含 WebSearch,不能搜索网络(WebFetch 在 extended 档可用)\n- **没有 MCP/Skill**:所有 MCP 服务器工具、自定义技能全部不可用\n- **没有项目记忆**:SALUZI.md、MEMORY.md、CodeGraph 知识都不注入,AI 对项目「无记忆」\n- **没有验证 Agent**:同 Poor\n- **AI 不知道自己的完整能力**:极简 prompt 不说明权限模式、安全规则、提交规范等,复杂工作流需要你手动提示\n\n> 简言之:SA 模式适合「我就让 AI 改这一个文件」「跑个脚本看看」这类窄任务,不适合复杂架构重构或需要项目上下文的工作。\n\n## 两种模式的完整对比\n\n| 维度 | 默认 | Poor | SA |\n|------|------|------|-----|\n| System prompt | 数千字(完整动态) | 数千字(完整动态) | ~20 行(极简) |\n| 工具集 | 全部 | 全部 | 核心集(minimal 4 / extended 7) |\n| extract_memories | ✅ | ❌ | ❌ |\n| prompt_suggestion | ✅ | ❌ | ❌ |\n| autoDream | ✅ | ❌ | ❌ |\n| AgentSummary | ✅ | ❌ | ❌ |\n| 验证 Agent | ✅ | ❌ | ❌ |\n| 会话记忆提取 | ✅ | ✅ | ❌ |\n| 相关记忆预取 | ✅ | ✅ | ❌ |\n| 项目记忆注入 (SALUZI.md) | ✅ | ✅ | ❌ |\n| CodeGraph | ✅ | ✅ | ❌ |\n| MCP 工具 | ✅ | ✅ | ❌ |\n| Skill 工具 | ✅ | ✅ | ❌ |\n| Glob/Grep/WebFetch/Task | ✅ | ✅ | ❌ |\n| 辅助分类模型 | MainLoop | Pro/SmallFast | Pro/SmallFast |\n| 主对话模型 | 不变 | 不变 | 不变 |\n| 输入 token / 轮 | 基准 | 略低(少后台 side-query) | 显著低(prompt 极小) |\n| 适合场景 | 日常全功能 | 日常省 token | 极限省 / 弱模型 / 窄任务 |\n\n## 与其他功能的兼容性\n\n这是最关键的部分——开了节约模式后,你常用的那些 Saluzi 特色还能不能用。\n\n### MOM 混合模型\n\n| 模式 | 兼容性 | 说明 |\n|------|--------|------|\n| Poor | ✅ 完全兼容 | Poor 不触碰 MOM 配置,顾问与主机按 `/mom` 设置正常运行 |\n| SA | ⚠️ 受限兼容 | MOM 仍可运行,但**主机只有核心工具**(minimal 4 / extended 7);顾问本就是 text-only 不受影响 |\n\n> 如果在 SA 模式下用 mom-stair,弱模型顾问(text-only)正常工作;升级到主机后主机也只能用核心工具执行——minimal 档不能 Glob/Grep/WebFetch,extended 档可以。建议 SA + MOM 时按需选档位。\n\n### CodeGraph 代码智能\n\n| 模式 | 兼容性 | 说明 |\n|------|--------|------|\n| Poor | ✅ 完全兼容 | CodeGraph 工具与 prompt 段落照常注入 |\n| SA | ❌ 不可用 | 工具被收敛到 4 核心(无 CodeGraph 工具),prompt 也不含 CodeGraph 段落 |\n\n### 记忆系统 (Memory V2 / SALUZI.md)\n\n| 模式 | 兼容性 | 说明 |\n|------|--------|------|\n| Poor | ⚠️ 只读不写 | 已有记忆仍注入 prompt;但 extract_memories/autoDream 关闭,不再自动积累新记忆 |\n| SA | ❌ 完全隔离 | 既不注入(极简 prompt 无记忆段),也不提取(session memory 关闭) |\n\n### 验证 Agent (Verification Agent)\n\n| 模式 | 兼容性 |\n|------|--------|\n| Poor | ❌ 跳过 |\n| SA | ❌ 跳过 |\n\n两种模式都跳过验证 Agent。需要独立审查时请关闭节约模式后再跑,或手动 spawn 子 Agent(SA 模式下无 Task 工具,需先关 SA)。\n\n### Plan 模式 / Sandbox / Hooks\n\n| 模式 | Plan | Sandbox | Hooks |\n|------|------|---------|-------|\n| Poor | ✅ | ✅ | ✅ |\n| SA | ✅ | ✅ | ✅ |\n\n权限模式、沙箱、hooks 都是工具执行层机制,与节约模式正交,完全不受影响。\n\n### MCP 服务器 / Skill\n\n| 模式 | MCP | Skill |\n|------|-----|-------|\n| Poor | ✅ | ✅ |\n| SA | ❌ 工具被移除 | ❌ 工具被移除 |\n\nSA 模式下 MCP 服务器仍可连接(连接层不变),但 MCP 提供的工具不在 4 核心之列,不会被注入工具列表。\n\n### Auto Mode (/auto)\n\n| 模式 | 兼容性 | 说明 |\n|------|--------|------|\n| Poor | ⚠️ 降级 | autoMode 仍运行,但 critique 步骤用 SmallFast 模型 |\n| SA | ⚠️ 降级 | 同 Poor;且主机工具受限 |\n\n## 权限模式与安全风险\n\n> 用户最关心的问题:**开了节约模式,AI 会不会偷偷执行 `rm -rf /`、`drop database` 这类删库命令?**\n>\n> 简短回答:**节约模式本身不改变权限策略,不会让危险命令「更容易跑」**。真正的风险来自你选的**权限模式**(`default` / `acceptEdits` / `bypassPermissions` / `auto`),而不是 Poor/SA。但 Poor/SA 会把权限分类器降级到更便宜的模型,在 `bypassPermissions`/`auto` 模式下**理论上**误判概率略升。下面逐层拆解。\n\n### 权限模式与节约模式正交\n\n权限模式由 `/permissions` 或 `settings.json` 的 `permissions.defaultMode` 控制,**与 Poor/SA 完全独立**。开 Poor 或 SA 不会切换你的权限模式:\n\n| 权限模式 | Bash 危险命令会怎样 | Poor/SA 影响 |\n|---------|---------------------|-------------|\n| `default` | 每条命令弹确认,`rm -rf` 类必问你 | 无——该问还是问 |\n| `acceptEdits` | 只自动放行文件编辑,**Bash 仍逐条问** | 无——Bash 仍问 |\n| `plan` | 只读,不能执行任何写操作 | 无——根本跑不了 |\n| `bypassPermissions` | 分类器自动判断,不问你 | ⚠️ 分类器降级(见下) |\n| `auto` | autoMode 分类器自动判断 | ⚠️ 分类器降级(见下) |\n\n**结论**:在 `default` / `acceptEdits` / `plan` 这三种模式下,无论开不开 Poor/SA,危险命令都必须经过你手动确认——**不存在「开了 SA 就自动删库」的情况**。\n\n### SA 模式仍执行 deny 规则\n\nSA 模式收敛工具集时调用的是 `filterToolsByDenyRules`——也就是说你在 `settings.json` 里配的 `permissions.deny` 规则**在 SA 模式下照常生效**:\n\n```json\n{\n \"permissions\": {\n \"deny\": [\n \"Bash(rm -rf:*)\",\n \"Bash(rm -rf /*:*)\",\n \"Bash(drop database:*)\",\n \"Bash(git push --force:*)\",\n \"Bash(:(){ :|:& };:)\",\n \"Bash(mkfs*:*)\"\n ]\n }\n}\n```\n\n这些规则会在工具列表阶段就**直接屏蔽**匹配的命令——AI 根本看不到这些工具能跑这类命令,分类器也不会被调用。这是比分类器更硬的防线,且**不受 Poor/SA 影响**。\n\n> 强烈建议:无论用不用节约模式,都把 `rm -rf /`、`drop database`、`mkfs`、fork 炸弹等不可逆命令写进 `deny`。这是「物理隔离」级别的一刀切。\n\n### 分类器降级:风险有多大\n\n在 `bypassPermissions` 或 `auto` 模式下,命令是否自动放行由**分类器**(yoloClassifier / universalClassifier)决定。Saluzi 的分类器 prompt 明确把以下列为 **Irreversible Local Destruction(不可逆本地破坏)**:\n\n- `rm -rf` 递归强制删除非平凡路径\n- `Remove-Item -Recurse -Force`(PowerShell 等价物)\n- `> file` 截断已有文件为空\n- `drop database` 删库\n- `git push --force`(视上下文)\n\nPoor/SA 模式下分类器模型降级:\n\n| 模式 | 分类器模型 | 影响 |\n|------|-----------|------|\n| 默认 | MainLoop(如 Max/Pro 主模型) | 精度最高 |\n| Poor/SA | DefaultPro | 略低,但仍是 Pro 级模型 |\n\n**关键点**:\n\n- 分类器**仍然运行**——Poor/SA 没有关闭分类器,只是让它用更便宜的模型跑。\n- Pro 级模型对 `rm -rf /` 这种明显的破坏性命令识别能力依然很强——这不是「盲放」。\n- 降级影响的是**边界模糊**的命令(如 `rm -rf ./build` 这种有歧义的)的判断精度,不是放行 `rm -rf /`。\n\n**所以「删库」风险的真实情况**:\n\n1. `default` / `acceptEdits` / `plan` 模式:**零风险**,必须你确认。\n2. `bypassPermissions` / `auto` + 默认:**低风险**,分类器(MainLoop)会拦。\n3. `bypassPermissions` / `auto` + Poor/SA:**略升的边界风险**,分类器(Pro)对明显破坏命令仍拦,但对模糊命令误判概率略高。\n4. 任何模式 + 配了 `deny` 规则:**零风险**,物理屏蔽。\n\n### SA 模式的特殊风险面\n\nSA 模式只保留核心工具(minimal 4 / extended 7),这反而**缩小**了攻击面:\n\n- **minimal 档没有 WebFetch/WebSearch**:AI 不能下载并执行远程脚本。extended 档恢复 WebFetch,但仍无 WebSearch。\n- **没有 Task**:不能 spawn 子 Agent 绕过主循环审查。\n- **没有 MCP 工具**:外部 MCP 服务器不能注入未审查的工具。\n- **没有 Skill**:自定义技能不会被执行。\n\n但 **Bash 仍在**——这是 SA 模式下唯一的「能干坏事」的入口。只要 Bash 的权限策略到位(`default` 模式或 `deny` 规则),SA 模式的整体风险面比默认模式**更小**,不是更大。\n\n### 安全使用建议\n\n| 建议 | 原因 |\n|------|------|\n| 默认用 `default` 或 `acceptEdits` 权限模式 | Bash 逐条确认,物理安全 |\n| 在 `settings.json` 配 `permissions.deny` 屏蔽 `rm -rf /`、`drop database`、`mkfs`、fork 炸弹 | 一刀切物理隔离,不受任何模式影响 |\n| 开启沙箱(`sandbox.enabled: true`)| 限制写入路径与网络访问,`rm -rf /` 直接失败 |\n| 避免 `bypassPermissions` + Poor/SA 组合 | 分类器降级 + 不问你 = 信任分类器,边界命令风险略升 |\n| SA 模式下尤其盯紧 Bash 输出 | 没有验证 Agent 兜底,需要你自己看命令 |\n| 用 `/permissions` 临时切 `plan` 模式做危险探索 | 只读,根本不能执行写操作 |\n\n### 一句话总结\n\n**Poor/SA 不会让 Saluzi「更敢删库」**——它们只砍后台副作用和工具数量,不动权限策略与 deny 规则。真正的删库风险来自 `bypassPermissions`/`auto` 权限模式本身;要彻底消除风险,配 `deny` 规则 + 开沙箱 + 用 `default` 模式,这三道防线与节约模式正交,同时开启完全安全。\n\n## 如何开启 / 关闭\n\n### 命令切换\n\n```\n> /poor # 切换 Poor 模式(开↔关)\n> /sa # 切换 SA 模式(开↔关,保留当前档位)\n> /sa minimal # 切到 minimal 档(4 工具:Read/Edit/Write/Bash)\n> /sa extended # 切到 extended 档(7 工具:+Glob/Grep/WebFetch)\n```\n\n输出会确认当前状态,例如:\n\n```\nSA mode ON — minimal prompt, 4 tools (Read/Edit/Write/Bash), all background tasks disabled\nSA tool level set to extended (7 tools when SA is active)\n```\n\n### 配置面板\n\n`/config` → 找到 `Token saving mode`(normal / poor / sa)。选 `sa` 后会多出一个 `SA tool level` 子项(minimal / extended),`Enter` 切换。\n\n### settings.json\n\n直接编辑 `~/.saluzi-edu/settings.json`:\n\n```json\n{\n \"poorMode\": true,\n \"saMode\": {\n \"enabled\": true,\n \"tools\": \"minimal\"\n }\n}\n```\n\n`poorMode` 是布尔,`true` 开启、省略或 `false` 关闭。`saMode` 是对象:`enabled` 控制开关,`tools` 取 `minimal` 或 `extended`。旧版本写 `\"saMode\": true` 仍兼容,等同于 `{ \"enabled\": true, \"tools\": \"minimal\" }`。重启 Saluzi 后生效。\n\n> **不要同时开 Poor 和 SA**。SA 是 Poor 的严格超集,同时开启只是冗余——工具集与 prompt 都按 SA 走,Poor 的标志位不起额外作用。需要极限省 token 就直接开 SA;需要保留全部工具就开 Poor。\n\n## 何时该用哪个\n\n| 你的情况 | 推荐 |\n|---------|------|\n| 日常开发,想省点 token,但还要用 Glob/Grep/MOM/CodeGraph | **Poor** |\n| API Key 按量计费,想压到最低 | **Poor**(日常)或 **SA**(窄任务) |\n| 用弱模型 / 小上下文窗口 | **SA** |\n| 一次性脚本:改一个文件、跑个命令 | **SA** |\n| 复杂架构重构、需要项目记忆与多步任务 | **关闭**(用默认) |\n| 需要 MOM 多顾问交叉验证 | **Poor**(保留工具)或 **关闭** |\n| 调试时想要「AI 不要偷偷干别的」 | **SA** |\n| 长期挂着的会话,不想后台烧 token | **Poor** |\n\n## 节约效果怎么看\n\n开启后用 `/cost` 观察单轮 token:\n\n```\n> /cost\n```\n\n重点看**输入 token**——SA 模式下每轮输入 token 会显著下降(因为 prompt 从数千字缩到 ~20 行)。后台 side-query 的输出 token 也归零。多轮对比即可看到差异。\n\n## 下一步\n\n- [MOM 混合模型](./mom-mixed-models) — 节约模式下 MOM 如何运行\n- [模型选择与切换](./model-selection) — `/poor` 命令的简述与模型降级\n- [查看消耗](./cost-usage) — `/cost` 监控节约效果\n- [记忆系统](./memory-system) — Poor/SA 对记忆的影响\n"
3127
3127
  },
3128
3128
  "docs/guide/assistant-proactive": {
3129
3129
  "frontmatter": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saluzi/saluzi-edu",
3
- "version": "0.2.52",
3
+ "version": "0.2.53",
4
4
  "description": "Saluzi CLI - interactive AI coding assistant in the terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -160,6 +160,53 @@ describe('getDb', () => {
160
160
  }
161
161
  expect(result.journal_mode).toBe('wal')
162
162
  })
163
+
164
+ test('db.transaction() is available and rolls back on error', () => {
165
+ // Regression guard: the Node.js compat layer (NodeSqliteCompat for
166
+ // node:sqlite, LibsqlCompat for libsql) must expose transaction().
167
+ // Without this, /web/auth/join throws TypeError under Node runtime
168
+ // (slz rcs → nodeServer path) and returns 500 "Failed to process
169
+ // invitation". bun:sqlite has transaction() natively, so this test
170
+ // only fails under Node — CI runs under Bun, so this is a guard for
171
+ // local/production Node deployments.
172
+ expect(initDatabase).toBeDefined()
173
+ const db = initDatabase!(':memory:')
174
+ expect(typeof db.transaction).toBe('function')
175
+
176
+ db.exec('CREATE TABLE tx_test (id INTEGER PRIMARY KEY, name TEXT)')
177
+ db.exec('CREATE TABLE tx_counter (n INTEGER)')
178
+ db.exec('INSERT INTO tx_counter VALUES (0)')
179
+
180
+ // Successful transaction commits both writes
181
+ const ok = db.transaction((name: string) => {
182
+ db.query('INSERT INTO tx_test (name) VALUES ($name)').run({ $name: name })
183
+ db.query('UPDATE tx_counter SET n = n + 1').run()
184
+ })
185
+ ok('alice')
186
+ expect(
187
+ (db.query('SELECT name FROM tx_test').get() as { name: string }).name,
188
+ ).toBe('alice')
189
+ expect(
190
+ (db.query('SELECT n FROM tx_counter').get() as { n: number }).n,
191
+ ).toBe(1)
192
+
193
+ // Failed transaction rolls back both writes
194
+ const fail = db.transaction(() => {
195
+ db.query('INSERT INTO tx_test (name) VALUES ($name)').run({
196
+ $name: 'bob',
197
+ })
198
+ db.query('UPDATE tx_counter SET n = n + 1').run()
199
+ throw new Error('intentional')
200
+ })
201
+ expect(() => fail()).toThrow('intentional')
202
+ // bob was rolled back; only alice remains
203
+ expect(
204
+ (db.query('SELECT name FROM tx_test').get() as { name: string }).name,
205
+ ).toBe('alice')
206
+ expect(
207
+ (db.query('SELECT n FROM tx_counter').get() as { n: number }).n,
208
+ ).toBe(1)
209
+ })
163
210
  })
164
211
 
165
212
  // ===========================================================================
@@ -1903,31 +1903,6 @@ describe('POST /web/auth/join (Phase 2 team association)', () => {
1903
1903
  }
1904
1904
  })
1905
1905
 
1906
- test('team_members insert failure → user still created (graceful degradation)', async () => {
1907
- // Simulate a schema/SQL error on team_members INSERT by dropping the table.
1908
- // The /join handler should catch the error, still create the user, consume
1909
- // the invitation, and return 200 — instead of 500 "Failed to process
1910
- // invitation". The admin can add the user to the team manually later.
1911
- db.exec('DROP TABLE team_members')
1912
-
1913
- const res = await app.request('/web/auth/join', {
1914
- method: 'POST',
1915
- headers: { 'Content-Type': 'application/json' },
1916
- body: JSON.stringify({
1917
- inviteToken,
1918
- username: 'resilientjoiner',
1919
- password: 'resilient-password-123',
1920
- }),
1921
- })
1922
- expect(res.status).toBe(200)
1923
-
1924
- // User was still created despite team_members failure
1925
- const userRow = db
1926
- .query("SELECT id FROM users WHERE username = 'resilientjoiner'")
1927
- .get() as { id: string } | null
1928
- expect(userRow).not.toBeNull()
1929
- })
1930
-
1931
1906
  test('expired invitation → 400', async () => {
1932
1907
  const expiredToken = `inv_expired_${randomUUID().replace(/-/g, '')}`
1933
1908
  const tokenHash = createHash('sha256').update(expiredToken).digest('hex')
@@ -25,7 +25,11 @@ const ResolvedDatabase: DatabaseCtor = (() => {
25
25
  try {
26
26
  const { DatabaseSync } = require('node:sqlite')
27
27
  if (DatabaseSync) {
28
- // node:sqlite has prepare() but not query() — add bun:sqlite compat.
28
+ // node:sqlite has prepare()/exec() but not query() or transaction() —
29
+ // add bun:sqlite compat for both. transaction() is implemented via
30
+ // manual BEGIN/COMMIT/ROLLBACK because DatabaseSync has no native
31
+ // transaction support. Without this, /web/auth/join (the only caller
32
+ // of db.transaction()) throws TypeError under Node.js runtime.
29
33
  return class NodeSqliteCompat extends DatabaseSync {
30
34
  query(sql: string) {
31
35
  const stmt = this.prepare(sql)
@@ -35,6 +39,26 @@ const ResolvedDatabase: DatabaseCtor = (() => {
35
39
  run: (...params: unknown[]) => stmt.run(...params),
36
40
  }
37
41
  }
42
+ transaction<TArgs extends unknown[], TResult>(
43
+ fn: (...args: TArgs) => TResult,
44
+ ): (...args: TArgs) => TResult {
45
+ return (...args: TArgs) => {
46
+ this.exec('BEGIN')
47
+ try {
48
+ const result = fn(...args)
49
+ this.exec('COMMIT')
50
+ return result
51
+ } catch (err) {
52
+ try {
53
+ this.exec('ROLLBACK')
54
+ } catch {
55
+ // ROLLBACK may fail if the connection is already broken;
56
+ // the original error is more important to surface.
57
+ }
58
+ throw err
59
+ }
60
+ }
61
+ }
38
62
  } as unknown as DatabaseCtor
39
63
  }
40
64
  } catch {
@@ -78,6 +102,9 @@ const ResolvedDatabase: DatabaseCtor = (() => {
78
102
  }
79
103
  exec(sql: string): void
80
104
  close(): void
105
+ transaction<TArgs extends unknown[], TResult>(
106
+ fn: (...args: TArgs) => TResult,
107
+ ): (...args: TArgs) => TResult
81
108
  }
82
109
  constructor(path: string) {
83
110
  this._db = new libsql(path)
@@ -93,6 +120,14 @@ const ResolvedDatabase: DatabaseCtor = (() => {
93
120
  run: (...params: unknown[]) => stmt.run(...convertParams(params)),
94
121
  }
95
122
  }
123
+ // libsql (better-sqlite3 lineage) has transaction() natively with
124
+ // the same API as bun:sqlite — delegate to it. Without this, the
125
+ // /web/auth/join flow throws TypeError under Node + libsql backend.
126
+ transaction<TArgs extends unknown[], TResult>(
127
+ fn: (...args: TArgs) => TResult,
128
+ ): (...args: TArgs) => TResult {
129
+ return this._db.transaction(fn)
130
+ }
96
131
  close(): void {
97
132
  this._db.close()
98
133
  }
@@ -511,9 +511,6 @@ app.post('/join', async c => {
511
511
  const now = new Date().toISOString()
512
512
 
513
513
  try {
514
- // Hash password inside the try/catch so argon2 failures return the
515
- // specific 500 error message instead of propagating as an unhandled
516
- // Hono error with a generic body.
517
514
  const passwordHash = await hashPasswordAsync(password)
518
515
 
519
516
  db.transaction(() => {
@@ -566,29 +563,21 @@ app.post('/join', async c => {
566
563
  }
567
564
 
568
565
  // Phase 2: If invitation is linked to a team, auto-add user to team.
569
- // Wrapped in try/catch — INSERT OR IGNORE swallows UNIQUE/CHECK/NOT NULL
570
- // but NOT FK violations or SQL errors (e.g. missing column on upgraded
571
- // DBs). If this fails, the user is still created and the invitation
572
- // consumed; the admin can add the user manually. Failing the whole
573
- // transaction here would leave the user unable to log in at all.
566
+ // Inside the transaction — if this fails, the whole transaction rolls
567
+ // back (user creation + invitation consumption included), preserving
568
+ // atomicity. team_id and role come from a validated invitation row, so
569
+ // FK/CHECK failures should not occur in normal operation.
574
570
  if (invitation.team_id) {
575
- try {
576
- db.query(
577
- `INSERT OR IGNORE INTO team_members (team_id, user_id, role, added_at, added_by)
578
- VALUES ($teamId, $userId, $role, $now, $addedBy)`,
579
- ).run({
580
- $teamId: invitation.team_id,
581
- $userId: userId,
582
- $role: invitation.role,
583
- $now: now,
584
- $addedBy: userId,
585
- })
586
- } catch (memberErr) {
587
- console.warn(
588
- '[/join] team_members insert failed (user still created):',
589
- memberErr instanceof Error ? memberErr.message : memberErr,
590
- )
591
- }
571
+ db.query(
572
+ `INSERT OR IGNORE INTO team_members (team_id, user_id, role, added_at, added_by)
573
+ VALUES ($teamId, $userId, $role, $now, $addedBy)`,
574
+ ).run({
575
+ $teamId: invitation.team_id,
576
+ $userId: userId,
577
+ $role: invitation.role,
578
+ $now: now,
579
+ $addedBy: userId,
580
+ })
592
581
  }
593
582
  })()
594
583
  } catch (err) {