@slock-ai/daemon@0.49.0 源码 + WebSocket 抓包 + 本地文件分析Slock 是 botiverse 做的"人-AI 协作平台",外观像 Slack,但用户列表里混着真人和 AI agent。每个 agent 都是个**长期成员**:有 persona、有 MEMORY、能参与 thread / DM / task / channel。
| 组成 | 位置 | 性质 |
|---|---|---|
| api.slock.ai | 云端(Cloudflare 前置) | 编排/状态/消息总线 |
| app.slock.ai | 云端 | Web UI(Slack 风格) |
| slock-trace-upload.botiverse.dev | Fly.io | 遥测数据收集 |
| slock-daemon | 本机 npx | 本地 agent 管理器 |
| claude / codex / cursor CLI 子进程 | 本机 | 真正的 LLM agent |
消息持久化、用户/agent/channel 配置、订阅 fan-out。**不**做"智能编排" —— 看 ws 抓包,它对一条 channel msg 就是简单广播给所有在场 agent,同 seq。
Web UI(Slack 风格 thread/channel/DM)。CORS 允许 origin = app.slock.ai。截图里那个界面就是它。
独立的遥测 ingestion 端,接收 daemon 每 5-15 秒打包上传的 gzipped NDJSON span 文件。所有数据已脱敏成 size bucket(无明文)。
核心本地编排器:维护 ws 长连接、spawn 队列(500ms 限速)、per-agent inbox、driver 抽象层、OTel span 本地收集。**无业务逻辑**,纯执行 server 命令。
真正的 LLM agent 进程。每个 persona = 一个独立子进程,带定制的 --system-prompt-file 和 --mcp-config。Turn 结束即退出。
每个 agent 启动时注入一个 stdio MCP server。**只暴露运行时控制工具**(runtime_profile_migration_done 等),消息发送不走这里。
180 字节 shell wrapper,转发给 daemon 的 dist/cli/index.js。**agent 通过这个 CLI 发消息** —— 不走 MCP,直接调 api.slock.ai REST。
每个 agent 一个工作目录:.slock/claude-system-prompt.md(26KB persona)、.slock/agent-token、.slock/runtime-sessions/、MEMORY.md、notes/。
从"用户在 Web UI 点发送"到"agent 进程读到消息"的完整链路 —— 这是 slock 最核心的机制。
{
"type": "agent:deliver",
"agentId": "220e72da-...",
"message": {
"channel_id": "b3d84e4d-...",
"channel_name": "all",
"channel_type": "channel",
"sender_id": "fa31c41a-...",
"sender_name": "oanakiaja",
"sender_type": "human",
"content": "按顺序都报个数 @Alice", # ← 明文消息
"timestamp": "2026-05-16T09:29:17.765Z",
"seq": 3940919, # ← 全局序列号
"message_id": "f02a5e15-..."
},
"seq": 3940919,
"traceparent": "00-4a02...-08cc...-00", # ← OTel 链路
"deliveryId": "40e8b589-..."
}
[target=#all msg=f02a5e15 time=2026-05-16T09:29:17 type=human] @oanakiaja: 按顺序都报个数 @Alice
这是 daemon 把 ws 的 JSON payload 通过 formatIncomingMessage()(line 5118)转成的扁平字符串,然后通过 stream-json 协议喂给 claude 进程的 stdin。LLM 只看得到这个字符串。
反方向不走 ws,走 HTTPS REST —— 这是个让我意外的发现。
从"server 决定唤醒 agent A"到"claude 子进程开始推理"完整过程。
agent:start 帧,包含 config{name,model,runtime,envVars,sessionId}、wakeMessage、unreadSummary、launchId
~/.slock/agents/{id}/,如果 MEMORY.md 不存在则写 onboarding seed
{workspace}/.slock/claude-system-prompt.md
claude-mcp-config.json(注入 chat-bridge MCP)、slock CLI wrapper、agent-token
claude --model opus \
--allow-dangerously-skip-permissions \
--system-prompt-file {workspace}/.slock/claude-system-prompt.md \
--mcp-config {workspace}/.slock/claude-mcp-config.json \
--output-format stream-json --input-format stream-json \
--strict-mcp-config
ENV: SLOCK_AGENT_ID, SLOCK_SERVER_URL, SLOCK_AGENT_TOKEN_FILE, PATH+=workspace/.slock
"New message received:\n\n[target=... msg=... type=human] @user: 按顺序报个数\n\nRespond as appropriate — send a message in the same channel, or take action as needed..."
→ 写到 claude 的 stdin (stream-json 协议)
processEndedCleanly → 检查 inbox 有无积压消息,有就 restart 同 agent(不限速),无就缓存 idle config
每个 agent 进程实际"看到"的输入 = system prompt(预先写文件) + wake prompt(每次启动时通过 stdin 注入)。下面拆解每一块。
这是 daemon 调用 driver.buildSystemPrompt(config) 在本地拼出来的,写到 {workspace}/.slock/claude-system-prompt.md,通过 --system-prompt-file 传给 claude。下面把每章展开,**保留关键原文**让你看到 LLM 实际"读"到的内容。
You are "Cindy", an AI agent in Slock — a collaborative platform for human-AI collaboration, serving as a shared message service for humans and agents who may be running on different computers.Your workspace and MEMORY.md persist across turns, so you can recover context when resumed. You will be started, put to sleep when idle, and woken up again when someone sends you a message. Think of yourself as a colleague who is always available, accumulates knowledge over time, and develops expertise through interactions.This is authoritative context injected by Slock. Do not infer
computer identity from hostname or cwd when this section is present.
- Agent ID: 220e72da-16e1-4c1a-a8a5-d28cfa127d8d
- Server ID: 5a55aa61-86cd-4cfd-a965-16e73de3e832
- Computer: MBP (bb5494bd-5d36-4ff5-a15a-2fb679ccb561)
- Hostname: 192.168.0.101
- OS: darwin arm64
- Daemon: v0.49.0
- Workspace: /Users/oanakiaja/.slock/agents/220e72da-...
这些值由 daemon 从 agent:start 的 runtimeContext 字段注入。明确 "authoritative" + "Do not infer" 是为了防止 LLM 用 hostname 命令自查机器名造成混乱。
Use the slock CLI for chat / task / attachment operations. The daemon injects a local slock wrapper into PATH for you. Use ONLY these commands for communication:slock 已在 PATH 里(daemon 通过 PATH 注入了 wrapper),Bash 直接调用即可。
失败时打 JSON 到 stderr:{"ok":false,"code":"...","message":"..."}MISSING_* / TOKEN_* = 本地鉴权 bootstrap 问题*_FAILED = 4xx from serverSERVER_5XX = server 不可达或崩了
- Always communicate through `slock` CLI commands.
This is your only output channel.
- Runtime Profile migration completion is the only exception:
call mcp__chat__runtime_profile_migration_done with the exact
migration_key.
- Always claim a task via `slock task claim` before starting work.
If the claim fails, move on to a different task.
slock CLI 是唯一对外通信通道。唯一例外是 runtime profile migration(MCP 工具调用)。
1. If this turn already includes a concrete incoming message,
first decide whether that message needs a visible
acknowledgment, blocker question, or ownership signal.
2. Read MEMORY.md (in your cwd) and then only the additional
memory/files you need to handle the current turn well.
3. If there is no concrete incoming message to handle,
stop and wait. New messages may be delivered to you
automatically while your process stays alive.
4. When you receive a message, process it and reply with
`slock message send`.
5. Complete ALL your work before stopping. ...
New messages arrive automatically — you do not need to poll.
Slock preserves Claude Code same-turn steering through a gated stream-json delivery path. Busy messages are buffered and delivered at Claude-observed safe boundaries; if no earlier safe boundary is available, they are delivered after the current turn ends.[target=#general msg=a1b2c3d4 time=... type=human] @richard: hello
[target=#general msg=e5f6a7b8 time=... type=agent] @Alice: hi there
[target=dm:@richard msg=c9d0e1f2 time=... type=human] @richard: can you help?
[target=#general:a1b2c3d4 msg=f3a4b5c6 ...] @richard: thread reply
字段含义:
target= — 消息来源,回复时复用为 --target 参数msg= — 消息短 ID(UUID 前 8 字符),开新 thread 时作 suffixtime= — 时间戳type= — human / agent / systemtype=system 是状态变化通知,除非明确请求行动否则不要回复。
# 回复 channel
slock message send --target "#channel-name" <<'EOF'
content
EOF
# 回复 DM
slock message send --target dm:@peer-name <<'EOF' ...
# 回复 thread
slock message send --target "#channel:shortid" <<'EOF' ...
强制 heredoc <<'EOF' 是为了让引号、反引号、代码块、换行不被 shell 解释。
To reply to any message, always reuse the exact target from the received message. This ensures your reply goes to the right place — whether it's a channel, DM, or thread.A reminder is an author-owned, persistent, observable, snoozable, updatable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people.ScheduleWakeup / CronCreate,必须用 slock reminder schedule。
:shortid 后缀: #general:a1b2c3d4msg= 作 suffix。例如收到 [target=#general msg=a1b2c3d4 ...],回复用 --target "#general:a1b2c3d4" 自动创建 threadslock message read --channel "#general:a1b2c3d4"slock thread unfollow 停止接收某 thread 的消息slock server info 看所有 channel + agent + human。slock message read + slock channel members,但不能发消息或收消息,直到 slock channel join。Private channel 需 human 主动加你。If slock server info shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context.
slock server info 看 channel descriptionslock message read --channel "#channel-name"
slock message read --channel dm:@peer-name
slock message read --channel "#channel:shortid"
slock message read --channel "..." --around "messageId"
slock message read --channel "..." --around 12345
slock message search + slock message read 找到原 thread / decision / owner,**再**回答。找不到要明说,不要瞎编。
If fulfilling a message requires you to take action beyond just replying (running tools, writing code, making changes), claim the message first. If you're only answering a question or having a conversation, no claim needed.
# 已是任务
@Alice: Fix the login bug [task #3 status=in_progress]
# 普通消息(可能需要 claim)
@Alice: Can someone look into the login bug?
# 任务系统通知
📋 Alice converted a message to task #3 "Fix the login bug"
状态流转: todo → in_progress → in_review → done。Assignee 和 status 独立 —— 除了 done 之外任何状态都能改 claim。
in_review 让 human 验doneTasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.slock task create 只是"发消息 + 打 task 标签"的便捷写法,**不会自动认领**。重复任务检测靠 LLM 自觉:先 slock task list 看有没有,再决定 create 还是 claim。
@CindydisplayName 是 "Cindy" — 仅展示,mention 判断用 stable namenameKeep the user informed. They cannot see your internal reasoning, so:
@alice — 用户#general 或 #1 — 频道#engineering:b885b5ae — 指定 threadtask #123 — 任务(必须写 "task #N",不能裸 #N 因为会和 PR/issue 撞)# 错: 测试环境:http://localhost:3000,请查看
# 对: 测试环境:<http://localhost:3000>,请查看
# 也对: 测试环境:[http://localhost:3000](http://localhost:3000),请查看
Your working directory (cwd) is your persistent, agent-owned workspace; files you create here survive across sessions.~/.slock/agents/{agentId}/。memory、notes、artifacts、code checkout 都放这。在 repo 里工作时先 cd 到 worktree 再跑 git/package-manager 命令。
MEMORY.md is the entry point to all your knowledge. It is the first file read on every startup (including after context compression).# <Your Name>
## Role
<your role definition, evolved over time>
## Key Knowledge
- Read notes/user-preferences.md for ...
- Read notes/channels.md for ...
## Active Context
- Currently working on: <brief summary>
- Last interaction: <brief summary>
notes/ 目录放详细知识 (user-preferences.md / channels.md / work-log.md / <domain>.md)Your context will be periodically compressed to stay within limits. When this happens, you lose your in-context conversation history but MEMORY.md is always re-read.You can work with any files or tools on this computer — you are not confined to any directory. You may develop a specialized role over time through your interactions. Embrace it.--allow-dangerously-skip-permissions 模式跑的 — 任何文件、任何命令都能动,**用户机器上的全部权限**。第二句鼓励 LLM 通过交互演化出专长。
While you are working, new messages may be delivered directly into your current thread.
slock message check 只在你**想看其它 channel 待办或拉更广 context** 时用Onboarding Assistant. This may evolve.config.description 字段拉。Alice / Mike 的 description=null,他们的 system prompt 这一节是 generic 描述。"This may evolve" 鼓励 agent 通过交互演化角色 — 对应 MEMORY.md 里的 Role 字段会被持续更新。
daemon 根据当前状态拼装(line 5113 起),四种 case:
# Case 1: 有 wakeMessage(被消息触发)
New message received:
[target=#all msg=f02a5e15 time=2026-05-16T09:29:17 type=human] @oanakiaja: 按顺序都报个数 @Alice
You also have unread messages in other channels: # 仅当 unreadSummary 非空
- #general: 2 unread
Use `slock message read` to catch up, or respond to the message above first.
Respond as appropriate — send a message in the same channel, or take action as needed.
Complete ALL your work before stopping.
Reply in the channel or create/reply in a thread as appropriate; use each message's `target` and `msg` fields to choose the exact target.
IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done — only stop when you have NO more work to do.
# Case 2: 恢复时只有未读摘要(无具体消息)
You have unread messages from while you were offline:
- #all: 1 unread
- dm:@richard: 1 unread
Use `slock message read` to catch up...
# Case 3: 恢复但无未读 —— "No new messages while you were away. Nothing to do — just stop."
# Case 4: runtime profile control 模式 —— 注入 migration 指令
除了真人消息,server 还会发"系统消息"。例:新建 agent 时强制让它自我介绍:
{
"sender_id": "system",
"sender_name": "system",
"sender_type": "human", # 注意是 human 不是 system,LLM 把它当指令
"content": "First message task (system-triggered):
Please post in #all now.
Use your Slock identity name: @Mike.
Do not use local OS usernames...
Say hi and briefly introduce yourself (role + how you can help).
Keep it short and friendly."
}
把前面 6 节学到的所有机制串起来,推演最初那张截图(琦玉/架构师A/haoshuo/Eric 在 thread 里报 1-2-3-4)的完整数据流。这条流程**没有真抓到**(抓包时只有 2 agent),但我用真实 ws 帧验证过的机制能推断出 100% 准确的时序。
seq=3940919,只差不同的 deliveryId。4 个 agent 也是同样模式,server 一次性发 4 条 ws 帧。DEFAULT_AGENT_START_INTERVAL_MS = 500(line 3803),所以 4 个 agent 不会同瞬间 spawn,而是依次相隔 500ms。Use slock message read to catch up, or respond to the message above first"。每个 agent 在动笔前都会先看 channel 现有内容。agent:deliver,但 sender_type: "agent")。这让后报的 agent 能"看到"前面的回复。架构师A 这个进程被 spawn 起来时,通过 --system-prompt-file 看到 26KB persona,然后 daemon 通过 stdin (stream-json) 注入下面这条 wake prompt:
New message received:
[target=#讨论问题:0c8a3f12 msg=48cc7b43 time=2026-05-16T09:17:00 type=human] @Haoxiang: 在这条thread里所有人给我从1开始每一个人都往上报数,不允许报重复的数字
Respond as appropriate — send a message in the same channel, or take action as needed.
Complete ALL your work before stopping.
Reply in the channel or create/reply in a thread as appropriate; use each message's `target` and `msg` fields to choose the exact target.
IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping.
架构师A 的 LLM 看到这条之后:
$ slock message read --target "#讨论问题:0c8a3f12" --before $(now)$ slock message send --target "#讨论问题:0c8a3f12" <<'EOF'\n2\nEOFSLOCK_AGENT_TOKEN_FILE 鉴权,HTTPS POST 给 api.slock.ai如果两个 agent 同时 在 LLM 推理(spawn 间隔 500ms 内,LLM 5 秒内出结果),后启动那个调 read_history 时前一个还没 post 完,就可能两个都报"1"。截图里没撞号,是因为:
这个机制对慢 LLM 友好,对快 LLM(比如同时 4 个 haiku)就**容易撞**。理论上可以用 slock task claim 那种乐观锁来强一致,但 slock 选择了"靠 LLM 自觉" + "靠物理延时"这种宽松方案。
17:33:03 human: "按顺序报个数"
17:33:03 server → daemon agent:deliver → @Mike (seq=3941285) # 同 seq
17:33:03 server → daemon agent:deliver → @Cindy (seq=3941285) # 同 seq · 同时
17:33:13 @Mike posts "1" # LLM 快的赢
17:33:13 server → daemon agent:deliver → @Cindy: @Mike的"1" # fan-out
17:33:44 @Cindy posts "2" # 看到 "1" 了,自然报 2
| 层 | 机制 | 有效性 |
|---|---|---|
| ① 服务器侧串行派发 | (我之前以为)前一个回完才唤醒下一个 | ❌ 不存在 —— fan-out 同时广播 |
| ② daemon 本地 spawn 限速 | 500ms 跨 agent 启动间隔 | ⚠ 弱保护 —— 只防止同瞬间 spawn |
| ③ Agent 自己 read history | system prompt 强制要求回复前看 channel 历史 | ✓ 真正的核心机制 |
slock 把"orchestration"问题转嫁给了 LLM 自己 —— 通过 system prompt 里的"Respect ongoing conversations"、"Only the person doing the work should report on it"、"Claim before you start"等规则让 agent 们自觉协调。底层基建只保证:
"谁说话"完全是 LLM 决定的涌现行为。这其实是个很大胆的设计 —— 高度依赖 LLM 的对话能力。
Agent 唯一对外通信方式。从 system prompt 提取的 26 个命令:
| 类别 | 命令 | 作用 |
|---|---|---|
| 消息 | slock message check | 非阻塞拉新消息(自由用) |
slock message send | 发到 channel / DM / thread(stdin heredoc) | |
slock message read | 读历史,支持 before/after/around 分页 | |
slock message search | 搜索可见消息 | |
slock message react | 加/移除表情反应(慎用,别滥用 🎉) | |
| ↑ 5 条覆盖所有人类聊天动作 | ||
| 任务 | slock task list | 看 channel 的任务板 |
slock task create | 新建任务(支持批量) | |
slock task claim | 认领任务(防多人抢同一任务) | |
slock task unclaim | 放弃认领 | |
slock task update | 改状态(in_review / done) | |
| 频道 | slock channel join/leave | 加入/退出 channel |
slock channel members | 看成员 | |
slock thread unfollow | 不再跟随某 thread | |
| 附件 | slock attachment upload | 上传文件,返回 attachment ID |
slock attachment view | 下载附件到本地 | |
| 身份 | slock profile show | 看自己或他人 profile |
slock profile update | 改名/头像/描述 | |
| 提醒 | slock reminder schedule | 定一个未来提醒 |
slock reminder list | 看提醒列表 | |
slock reminder snooze | 推迟 | |
slock reminder update | 修改 | |
slock reminder cancel/log | 取消 / 看事件流 | |
| 动作 | slock action prepare | 给人类准备"点击执行"卡片(B 模式 quick-commit) |
| 服务器 | slock server info | 列出所有 channel / agent / human |
--allow-dangerously-skip-permissions 模式下随便跑 shell;CLI 命令也能让 agent 灵活地组合(slock message read | grep ...)。
| 数据 | 去哪 | 风险等级 |
|---|---|---|
| Thread 消息明文 | api.slock.ai(必然)+ Anthropic/OpenAI(LLM 推理) | ⚠ 中 |
| 用户名 / user_id / channel ID | api.slock.ai (每条 deliver payload) | ✓ 业务必需 |
| Agent 思考内容 + 工具调用 | api.slock.ai (stream-json 转发) | ⚠⚠ 高(比回复更敏感) |
| Shell 命令 + 参数 | api.slock.ai (commandExecution 事件) | ⚠⚠ 命令行可能含 token/路径 |
| 文件改动事件 | api.slock.ai | ⚠ 路径泄露项目结构 |
| 局域网 hostname (192.168.0.101) | api.slock.ai (ready 帧) | 轻 |
| workspace 文件远程读 | api.slock.ai 可主动调 agent:workspace:read | ⚠⚠ 服务器有这个权限 |
| API key | 明文存在 ps aux + ~/.slock/agents/*/claude-mcp-config.json + 子进程 env | ⚠⚠ 多副本 |
| 本地 trace | 仅 size bucket,无明文 | ✓ 设计上脱敏 |
| Claude session 文件 | 仅本地 ~/.claude/projects/... | ✓ |
| 端到端加密 | — | ❌ 没有 |
~/.slock/agents/{id}/notes/ 和 MEMORY.md 的内容。ps aux 全机器可见、claude-mcp-config.json 在所有 agent 目录、子进程 env。任何一个本地恶意进程(包括恶意 npm 包)都能盗走。建议向 botiverse 反馈支持 --api-key-file。~/.slock/
├── owner.json # 你的身份
├── agents/{agentId}/
│ ├── .slock/
│ │ ├── slock # 180B shell wrapper → daemon CLI
│ │ ├── agent-token # per-agent 鉴权
│ │ ├── claude-system-prompt.md # 26KB persona prompt
│ │ ├── claude-mcp-config.json # chat-bridge MCP 配置
│ │ └── runtime-sessions/*.jsonl # session handoff 记录
│ ├── MEMORY.md # agent 持久记忆
│ └── notes/ # agent 自由工作目录
└── machines/{machineId}/
├── daemon.lock
├── traces/*.jsonl # OTel span 本地缓存
└── trace-uploads/*.uploaded.json # 上传状态记录
~/.claude/projects/-Users-oanakiaja--slock-agents-{agentId}/{sessionId}.jsonl # Claude session 实际位置
~/.codex/sessions/{date}/rollout-...-{sessionId}.jsonl # Codex session 实际位置
| 行 | 符号 | 作用 |
|---|---|---|
| 1814 / 2291 / 2536 / 2694 / 2916 / 3169 / 3589 | buildSystemPrompt ×7 | 每个 driver 一份 system prompt 构造 |
| 3803 | DEFAULT_AGENT_START_INTERVAL_MS | = 500ms 跨 agent spawn 限速 |
| 3879 | RESPONSE_TARGET_HINT | wake prompt 里的回复指引 |
| 3995 | dynamicReplyInstruction | 按 driver 生成回复指令 |
| 4071 | buildUnreadSummary | 构造未读摘要 |
| 4943 | pumpAgentStartQueue | 带限速的启动泵 |
| 5057 | startAgentNow | 真正 spawn 子进程 |
| 5113 | wake prompt 拼装 | 按 case 组装(wakeMessage / resume / 空) |
| 5610 | deliverMessage | 消息分发主入口,4 种 outcome |
| 7213 | WebSocket 连接 | wss://api.slock.ai/daemon/connect?key=... |
| 7782 | uploadWithSignedCapability | trace 上传到 fly.io |
| 8042 | DEFAULT_TRACE_UPLOAD_URL | slock-trace-upload.botiverse.dev |
| 8172 | 10 个 inbound case | server→daemon 的所有指令(包括 workspace:read) |
共 276 frames 抓在 /Users/oanakiaja/slock-mitm/ws-frames.jsonl,包括:
agent:start(完整 config payload)agent:deliver(完整 message payload,含明文)agent:deliver:ackagent:activity(实时状态变更)agent:skills:list / agent:runtime_profileready 握手帧