W
AI-Wiki
CONCEPT

UserPromptSubmit

定义

UserPromptSubmitclaude-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 之前:

  1. SessionStart:打开或恢复会话时注入历史上下文;
  2. UserPromptSubmit:用户提交 prompt 时建立/续接会话并记录该轮用户输入;
  3. PostToolUse:Claude 调用工具后,把工具观察发送给异步处理链路;
  4. Stop:停止提问时生成会话摘要;
  5. 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 没有像 SessionStartPostToolUse 那样依赖 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 返回 23,依次递增。

这个编号是后续保存 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

请求体包含:

  • project
  • userPrompt
  • promptNumber

这一步正对应表格里写的 init worker。因此,UserPromptSubmit 的目的不只是落库,还要把当前 prompt 对应的会话状态告诉 worker,使后续处理链条能够围绕正确的会话与 prompt 序号运转。

输出

原文给出的输出为:

{ "continue": true, "suppressOutput": true }

这说明该 hook 在正常路径下不会向用户显示额外输出,而是让流程继续进行。

与其他阶段的关系

SessionStart 的区别

SessionStart 处理的是打开或恢复会话时的上下文注入,对应 context-hook.js;而 UserPromptSubmit 处理的是用户真正提交 prompt 的时刻,对应 new-hook.js。前者重点是拿上下文,后者重点是建会话、记 prompt、起 worker 初始化。