Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ TARGET_ARCH ?= $(shell $(GO) env GOARCH)
CLI_BIN ?= $(BIN_DIR)/csgclaw-cli

IMAGE ?= opencsg-registry.cn-beijing.cr.aliyuncs.com/opencsghq/picoclaw
TAG ?= 2026.4.27.0
TAG ?= 2026.5.27
LOCAL_IMAGE ?= picoclaw:local

.DEFAULT_GOAL := build-all
Expand Down
27 changes: 22 additions & 5 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1035,13 +1035,14 @@ Example single event:
```text
id: msg-1
event: message
data: {"message_id":"msg-1","room_id":"room-1","sender_id":"u-admin","text":"hello","thread_root_id":"msg-root","thread_context":{"root_message_id":"msg-root","context":[{"id":"msg-root","sender_id":"u-admin","content":"root text"}],"summary":{"root_excerpt":"root text","message_count":1,"before_count":0,"after_count":0}}}
data: {"message_id":"msg-1","room_id":"room-1","channel":"csgclaw","chat_id":"room-1","sender_id":"u-admin","text":"hello","thread_root_id":"msg-root","context":{"channel":"csgclaw","chat_id":"room-1","chat_type":"direct","topic_id":"msg-root","sender_id":"u-admin","message_id":"msg-1"},"thread_context":{"root_message_id":"msg-root","context":[{"id":"msg-root","sender_id":"u-admin","content":"root text"}],"summary":{"root_excerpt":"root text","message_count":1,"before_count":0,"after_count":0}}}
```

For thread replies, `thread_root_id` is the root message ID and
`thread_context` carries the deterministic hidden context captured when the
thread was started. Bot/LLM bridges use it as prompt context; it is not a list
of thread replies.
of thread replies. PicoClaw-native clients can use `context.topic_id` as the
same thread/session identifier.

### `POST /api/bots/{id}/messages/send`

Expand All @@ -1057,9 +1058,25 @@ Example request body:
}
```

`thread_root_id` is optional. When present, the bot response is sent as a reply
inside that IM thread. The detailed response behavior depends on the
compatibility bridge implementation.
`thread_root_id`, `topic_id`, and `context.topic_id` are optional thread/topic
identifiers. When one is present, the bot response is sent as a reply inside
that IM thread. When all are omitted, the response is sent as a top-level room/DM
message; the server does not infer a thread from the bot's most recent room
event.

PicoClaw outbound message shape is also accepted:

```json
{
"chat_id": "room-1",
"content": "hello",
"context": {
"channel": "csgclaw",
"chat_id": "room-1",
"topic_id": "msg-root"
}
}
```

### `GET /api/bots/{id}/llm/models`

Expand Down
24 changes: 21 additions & 3 deletions docs/api.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -1029,12 +1029,13 @@ Bot 和 Codex bridge 使用的 thread/session 隔离规则见
```text
id: msg-1
event: message
data: {"message_id":"msg-1","room_id":"room-1","sender_id":"u-admin","text":"hello","thread_root_id":"msg-root","thread_context":{"root_message_id":"msg-root","context":[{"id":"msg-root","sender_id":"u-admin","content":"root text"}],"summary":{"root_excerpt":"root text","message_count":1,"before_count":0,"after_count":0}}}
data: {"message_id":"msg-1","room_id":"room-1","channel":"csgclaw","chat_id":"room-1","sender_id":"u-admin","text":"hello","thread_root_id":"msg-root","context":{"channel":"csgclaw","chat_id":"room-1","chat_type":"direct","topic_id":"msg-root","sender_id":"u-admin","message_id":"msg-1"},"thread_context":{"root_message_id":"msg-root","context":[{"id":"msg-root","sender_id":"u-admin","content":"root text"}],"summary":{"root_excerpt":"root text","message_count":1,"before_count":0,"after_count":0}}}
```

对于 thread replies,`thread_root_id` 是 root message ID,`thread_context`
携带 thread 开启时记录的确定性隐藏上下文。Bot/LLM bridge 会把它作为
prompt context 使用;它不是 thread reply 列表。
prompt context 使用;它不是 thread reply 列表。PicoClaw 原生 client 可以把
`context.topic_id` 当作同一个 thread/session 标识。

### `POST /api/bots/{id}/messages/send`

Expand All @@ -1050,7 +1051,24 @@ prompt context 使用;它不是 thread reply 列表。
}
```

`thread_root_id` 可选;传入时 bot 响应会发送到该 IM thread 中。具体响应由兼容桥实现决定。
`thread_root_id`、`topic_id` 和 `context.topic_id` 都是可选的 thread/topic
标识;传入任一字段时 bot 响应会发送到该 IM thread 中。全部省略时,
响应会作为 room/DM 顶层消息发送;服务端不会根据 bot 在房间中最近收到的
事件推断 thread。

也接受 PicoClaw outbound message 形态:

```json
{
"chat_id": "room-1",
"content": "hello",
"context": {
"channel": "csgclaw",
"chat_id": "room-1",
"topic_id": "msg-root"
}
}
```

### `GET /api/bots/{id}/llm/models`

Expand Down
14 changes: 11 additions & 3 deletions docs/im-threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,23 @@ Thread-aware bot events may include:

- `thread_root_id`: root message ID when the event is inside a thread.
- `thread_context`: hidden context snapshot and summary for the thread root.
- `context.topic_id`: PicoClaw-native topic/session ID. For CSGClaw IM
threads this is the same value as `thread_root_id`.

`thread_context` is prompt context, not visible thread history.

Bot sends may include `thread_root_id`. When present, the message is sent as a
reply in that thread.
Bot sends may include either CSGClaw fields (`room_id`, `text`,
`thread_root_id`) or PicoClaw outbound fields (`chat_id`, `content`,
`context.topic_id`). When a thread root/topic is present, the message is sent as
a reply in that thread. Bot sends that omit `thread_root_id`, `topic_id`, and
`context.topic_id` are treated as top-level room/DM messages; CSGClaw does not
infer a thread from the bot's most recent room event.

This maps to PicoClaw/topic isolation requirements: a runtime should treat
`room_id` as the normal conversation key and `room_id:thread_root_id` as the
thread conversation key.
thread conversation key. Generated PicoClaw configs set session dimensions to
`["chat", "topic"]` so `context.topic_id` creates a separate PicoClaw session
for every CSGClaw IM thread.

## Codex Bridge Session Isolation

Expand Down
14 changes: 11 additions & 3 deletions docs/im-threads.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,22 @@ Thread-aware bot event 可能包含:

- `thread_root_id`:事件位于 thread 内时的 root message ID。
- `thread_context`:该 thread root 的隐藏上下文快照和 summary。
- `context.topic_id`:PicoClaw 原生 topic/session ID。对 CSGClaw IM
threads 来说,它与 `thread_root_id` 相同。

`thread_context` 是 prompt context,不是可见 thread 历史。

Bot send 可以传入 `thread_root_id`。传入后,消息会作为该 thread 内的 reply
发送。
Bot send 可以传入 CSGClaw 字段(`room_id`、`text`、`thread_root_id`),
也可以传入 PicoClaw outbound 字段(`chat_id`、`content`、
`context.topic_id`)。存在 thread root/topic 时,消息会作为该 thread 内的
reply 发送。如果 bot send 同时省略 `thread_root_id`、`topic_id` 和
`context.topic_id`,CSGClaw 会按 room/DM 顶层消息处理,不会根据该 bot 在
房间中最近收到的事件推断 thread。

这对应 PicoClaw/topic 隔离需求:runtime 应把 `room_id` 视为普通会话 key,
把 `room_id:thread_root_id` 视为 thread 会话 key。
把 `room_id:thread_root_id` 视为 thread 会话 key。生成的 PicoClaw config
会把 session dimensions 设为 `["chat", "topic"]`,因此
`context.topic_id` 会为每个 CSGClaw IM thread 建立独立的 PicoClaw session。

## Codex Bridge 会话隔离

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ go 1.26.2
require github.com/larksuite/oapi-sdk-go/v3 v3.5.3

require (
github.com/go-chi/chi/v5 v5.2.5
github.com/gin-gonic/gin v1.10.1
github.com/router-for-me/CLIProxyAPI/v6 v6.9.40
github.com/go-chi/chi/v5 v5.2.5
github.com/pelletier/go-toml/v2 v2.2.2
github.com/router-for-me/CLIProxyAPI/v6 v6.10.9
golang.org/x/term v0.37.0
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/router-for-me/CLIProxyAPI/v6 v6.9.40 h1:DM3Prm8+pxzMrh6UfHdbYvnEU/8ZOxDZOMQFooJw1Rw=
github.com/router-for-me/CLIProxyAPI/v6 v6.9.40/go.mod h1:P1jsIPFXorYGuS2N/3BlZYkpRKi/z7+oR3+1tdG0u4k=
github.com/router-for-me/CLIProxyAPI/v6 v6.10.9 h1:pGteumLPbwMXtruCTkLbsIgFa0WVlXsyDF7B8drAArs=
github.com/router-for-me/CLIProxyAPI/v6 v6.10.9/go.mod h1:P1jsIPFXorYGuS2N/3BlZYkpRKi/z7+oR3+1tdG0u4k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
Expand Down
35 changes: 35 additions & 0 deletions internal/agent/manager_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,41 @@ func TestRenderAgentPicoClawConfigUsesBridgeModelEndpoint(t *testing.T) {
if got, want := execTool["timeout_seconds"], float64(600); got != want {
t.Fatalf("tools.exec.timeout_seconds = %v, want %v", got, want)
}
session, ok := rendered["session"].(map[string]any)
if !ok {
t.Fatalf("renderAgentPicoClawConfig() missing session in:\n%s", text)
}
dimensions, ok := session["dimensions"].([]any)
if !ok {
t.Fatalf("session.dimensions = %T, want array in:\n%s", session["dimensions"], text)
}
if got, want := stringifyJSONList(dimensions), []string{"chat", "topic"}; !stringSlicesEqual(got, want) {
t.Fatalf("session.dimensions = %v, want %v", got, want)
}
}

func stringifyJSONList(values []any) []string {
result := make([]string, 0, len(values))
for _, value := range values {
text, ok := value.(string)
if !ok {
continue
}
result = append(result, text)
}
return result
}

func stringSlicesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

func TestPicoclawBridgeModelIDPrefixesOpenAIForSlashModelNames(t *testing.T) {
Expand Down
13 changes: 8 additions & 5 deletions internal/api/bot_compat.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,20 +357,23 @@ func (h *Handler) handleBotSendMessage(w http.ResponseWriter, r *http.Request, b
http.Error(w, fmt.Sprintf("decode request: %v", err), http.StatusBadRequest)
return
}
roomID := req.ResolvedRoomID()
text := req.ResolvedText()
threadRootID := req.ResolvedThreadRootID()

message, err := h.im.DeliverMessage(im.DeliverMessageRequest{
RoomID: req.RoomID,
RoomID: roomID,
SenderID: botID,
Content: req.Text,
Content: text,
MessageID: req.MessageID,
ThreadRootID: req.ThreadRootID,
ThreadRootID: threadRootID,
})
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
h.publishMessageCreated(req.RoomID, botID, message)
h.publishThreadUpdated(req.RoomID, message)
h.publishMessageCreated(roomID, botID, message)
h.publishThreadUpdated(roomID, message)
writeJSON(w, http.StatusOK, map[string]string{"message_id": message.ID})
}

Expand Down
Loading
Loading