UserPromptSubmit
定义
UserPromptSubmit 是 claude-mem 钩子生命周期中的第 2 阶段。原文明确写为 Stage 2 is UserPromptSubmit,并在 5 阶段表格中给出其对应关系:
- Hook:
new-hook.js - Trigger:用户提交一个 prompt
- Purpose:创建或获取会话、保存 prompt、初始化 worker
它不是会话启动阶段,也不是工具调用后的保存阶段,而是用户把一条新提示词提交进当前会话时触发的入口阶段。
在本文档中的语境
该概念来自 Hook Lifecycle - Claude-Mem 摘要 对 Claude-Mem 五阶段 hook system 的说明。这个系统用于跨 Claude Code 会话捕获开发工作,并采用扩展进程与 worker 进程分离的两进程架构。
在这个语境里,UserPromptSubmit 的位置介于 SessionStart 之后、PostToolUse 之前:
- SessionStart:打开或恢复会话时注入历史上下文;
- UserPromptSubmit:用户提交 prompt 时建立/续接会话并记录该轮用户输入;
- PostToolUse:Claude 调用工具后,把工具观察发送给异步处理链路;
- Stop:停止提问时生成会话摘要;
- SessionEnd:会话关闭时标记完成。
因此,UserPromptSubmit 是后续观察归属、prompt 编号递增、worker 初始化等动作的会话级前置步骤。
触发时机
原文给出的触发时机非常直接:When user submits any prompt in a session。也就是只要用户在某个会话里提交任意 prompt,就会进入这一阶段。
这里的边界有两个重点:
- 触发条件是“提交任意 prompt”,不是仅首次提问才触发;
- 该阶段既覆盖新会话中的第一条 prompt,也覆盖同一会话里的后续 continuation prompt。
这也是为什么原文特别强调同一个 session_id 会贯穿整段对话中的所有 hooks,而本阶段的会话创建逻辑是幂等的。
对应 Hook 与配置
这一阶段对应的脚本是 new-hook.js。
在 hooks.json 中,UserPromptSubmit 被配置为执行以下命令:
node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js
并且该命令的超时配置为:
timeout: 120
从配置角度看,UserPromptSubmit 没有像 SessionStart 或 PostToolUse 那样依赖 matcher 条件文本;它就是在该生命周期事件到来时运行对应命令。
输入与核心处理机制
原文给出了这一阶段通过标准输入接收的数据结构,包含以下字段:
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"prompt": "User's actual prompt text"
}
围绕这份输入,UserPromptSubmit 的处理步骤可以拆成几个关键机制。
1. 从工作目录提取项目名
第一步会从 cwd 中提取项目名,原文示意为:
project = path.basename(cwd)
这说明该阶段并不是让用户显式传一个项目名,而是根据当前工作目录推导项目归属。
2. 创建或获取数据库中的会话
核心调用是:
sessionDbId = db.createSDKSession(session_id, project, prompt)
原文特别注明这里采用 INSERT OR IGNORE 模式,含义是:
- 如果这是该
session_id的第一条 prompt,则创建新会话记录; - 如果这是延续同一会话的后续 prompt,则忽略重复插入,并返回已存在的会话。
这就是它的幂等性来源。文档把这一点总结为:同一个 session_id 总是映射到同一个 sessionDbId,从而支持 conversation continuations。
换句话说,UserPromptSubmit 不把“续聊”当成新会话,而是保证整段会话在数据库层面持续落到同一个会话实体上。
3. 递增 prompt 计数器
在获得 sessionDbId 之后,会执行:
promptNumber = db.incrementPromptCounter(sessionDbId)
原文给出的返回语义非常明确:
- 首条 prompt 返回
1; - 后续 continuation prompt 返回
2、3,依次递增。
这个编号是后续保存 prompt、初始化 worker、关联工具观察时的重要顺序信息。
4. 去除隐私与上下文标签
在保存之前,hook 会先清理 prompt:
cleanedPrompt = stripMemoryTags(prompt)
原文明确指出这里会移除两类标签内容:
<private>...</private><claude-mem-context>...</claude-mem-context>
这意味着存入数据库以及传给后续 worker 初始化的内容,并不直接等于用户原始输入,而是经过隐私/内部上下文标记剥离后的版本。
5. 全私有内容时直接跳过
清理之后还有一个明确分支:
if (!cleanedPrompt || cleanedPrompt.trim() === '') {
return // Don't save, don't call worker
}
这是 UserPromptSubmit 的重要边界条件:
- 如果 prompt 去掉私有标签后为空;
- 或只剩空白字符;
那么这一轮不会保存 prompt,也不会调用 worker。
也就是说,阶段会被触发,但有效业务处理会提前终止。文档明确写了这两个后果:Don't save, don't call worker。
6. 保存清洗后的用户 prompt
如果不是全私有内容,就会执行保存:
db.saveUserPrompt(session_id, promptNumber, cleanedPrompt)
这里保存的是:
- 当前
session_id下的用户输入; - 与刚刚递增得到的
promptNumber绑定; - 内容是已经去掉私有与上下文标签的
cleanedPrompt。
7. 通过 HTTP 初始化会话 worker
保存之后,本阶段还会调用 worker 初始化接口:
POST http://127.0.0.1:<worker-port>/sessions/{sessionDbId}/init
请求体包含:
projectuserPromptpromptNumber
这一步正对应表格里写的 init worker。因此,UserPromptSubmit 的目的不只是落库,还要把当前 prompt 对应的会话状态告诉 worker,使后续处理链条能够围绕正确的会话与 prompt 序号运转。
输出
原文给出的输出为:
{ "continue": true, "suppressOutput": true }
这说明该 hook 在正常路径下不会向用户显示额外输出,而是让流程继续进行。
与其他阶段的关系
与 SessionStart 的区别
SessionStart 处理的是打开或恢复会话时的上下文注入,对应 context-hook.js;而 UserPromptSubmit 处理的是用户真正提交 prompt 的时刻,对应 new-hook.js。前者重点是拿上下文,后者重点是建会话、记 prompt、起 worker 初始化。