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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

- Experimental `ocs claude`: starts Claude Code with an ocs channel (Claude Code Channels research preview). Messages to that session arrive as `<channel source="ocs">` events, are not held by `crossSessionInbound`, and the session gets `ocs_dm` / `ocs_send` tools. Senders report `handed to the session's ocs channel`, which is not a read receipt; an unanswered channel write is unknown (exit 3) and is never re-sent through the inbox. `ocs who` tags such sessions `[channel]`. macOS/Linux only. See [docs/claude-channel.md](docs/claude-channel.md)
- Wake bodies now also neutralize `<channel` / `</channel` tags
- `ocs send` / `ocs dm` accept `--` to end options, so a body may start with `--`

## 0.9.13

- `install.sh` / `install.ps1` resolve the latest release tag first, then download the archive and its `.sha256` from that tag and check the binary reports that version. Right after a release the two `/releases/latest/download/` redirects could point at different releases and the install failed with a sha256 mismatch. `OCS_VERSION=x.y.z` pins a version; `ocs upgrade` passes the exact release it compared against
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ bun src/cli.ts <cmd> # 本地跑 CLI(who/dm/send/read/notify-when-idle/sessi

15. **OpenCode(2.2.1+)**:本地 V2 插件登记实际观察到的会话,完整 `ses_…` ID 大小写不可折叠;短地址对完整 ID 做哈希,不截时间前缀。工具 shell 身份只从执行上下文传入,不往共享 `process.env` 写“当前会话”。原生投递必须 `delivery: "queue"`,核对目标 session 和 queue 接纳后才确认;提交后异常是 unknown,不重放,不启动服务器、不创建目标会话。私有 socket / 随机 Windows 管道和 token 约束见 `src/opencode.ts`;测试在 `test/opencode.test.ts`。V1 不在此适配器支持范围,真实宿主验收与 fixture 分开报告。

16. **Claude channel(`src/claude-channel.ts`,实验性,docs/claude-channel.md)**:只在目标会话由 `ocs claude` 启动、登记有效(同 uid 非符号链接、server 活着且父进程就是目标 Claude)时走。channel 无确认:成功只叫「已交给 channel」,不许清欠账;请求写出后无应答 = unknown,绝不回落收件箱重投,只有「什么都没写出」才回落。`initialize` 必须回旧协议号(modern 连接宿主不注册 channel);`notifications/initialized` 之前拒收。启动器只传 dev flag,并剥掉外层会话身份环境变量。权限转发(`claude/channel/permission`)不做给 agent。`test/claude-channel.test.ts` 守着,改之前先跑变异自检。

## 路线(owner 已拍板)

2026-10:AgentParty 停止维护,原定「主仓抽 MIT 的 `packages/cross-session-core`、本仓转为单向 sync 镜像」的路线作废。本仓是唯一正本;跨网络靠虚拟局域网 + `ocs lan`。
1 change: 1 addition & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ AgentParty 已停止维护。原定抽取公共包、向 AgentParty 单向同步
| 目标 | 载体 | 忙碌行为与边界 |
|---|---|---|
| Claude Code | 原生收件箱 socket 与回执 helper | 保留原生来源与权限闸;accept 不发已读证明,hold/refuse 等回执可报告未送达;Windows 暂无回执 |
| Claude Code(`ocs claude` 启动,实验性) | 宿主 channel(stdio MCP 子进程 + 私有 UDS) | 不经 crossSessionInbound;宿主无确认,最多报 accepted;写出无应答为 unknown,不回落收件箱重投(docs/claude-channel.md) |
| Codex Desktop | 被 renderer 认领的原生 IPC | 保留 delegation 来源信封;私有协议运行时探测,结果未知绝不降级补发 |
| Codex 终端 | 官方 queue,运行中可尝试 steer | 用 rollout FD 持有者及进程身份证明活性;不能 steer 时在宿主入队前合并积压 |
| OpenCode 2.2.1+ | 本地插件与私有 socket / Windows 随机管道 | 精确会话 ID;原生持久队列 `delivery: "queue"`;工具执行上下文隔离 shell 身份,unknown 不重放 |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ ocs dm <target-from-ocs-who> "hello" --as human
| Target | How | Requirement |
|---|---|---|
| Interactive Claude Code session | `@<name>`, `@claude-<8hex>`, or `@<session name>` | Receiver sets `"crossSessionInbound": "accept"` in `~/.claude/settings.json`. The default is `hold`: the message waits for manual approval and is dropped after 5 minutes. Since 0.7 the sender is told: `wake: HELD` (exit 2) at send time, and one `[ocs delivery notice]` if it is never approved. `ocs doctor` checks the setting. |
| Claude Code started with `ocs claude` (experimental) | same addresses; `ocs who` tags it `[channel]` | macOS/Linux. Messages arrive as `<channel source="ocs">` events through Claude Code Channels: no `crossSessionInbound` hold, `ocs_dm` / `ocs_send` tools in the session, no receipts (`handed to the session's ocs channel` is not a read receipt). Claude asks once at startup to confirm the development channel. [Details](docs/claude-channel.md). |
| ChatGPT Desktop task / cmux Codex TUI | `ocs dm codex-<8hex> …`, `@<thread-id>`, or `--codex <thread-id\|codex-8hex>` | Desktop delivery needs the task open plus a second open task under the same renderer. If that path is definitely unavailable, ocs safely falls back to a uniquely matched, idle cmux surface that still has a live Codex process. |
| Pi TUI | `ocs dm pi-<8hex> …` or `@pi-<8hex>` | Run `ocs skill install`, then restart (or `/reload`) each open Pi; a Pi opened before the install can still send but cannot be woken, and `ocs doctor` run inside it says so. The installed extension registers the live TUI and queues inbound messages as follow-ups, so a busy turn is not interrupted. |
| Hermes Desktop session | `ocs dm hermes-<id> …` or `@hermes-<id>` (`ocs who` prints the id; `_` in Hermes' session id is written `.`) | Hermes Desktop (or `hermes serve`) running as the same user, with the session open. ocs submits through the host's own WebSocket with the busy-queue flag: an idle session starts a turn (`started a turn`), a busy one gets it after the current turn (`queued`) — nothing is interrupted. Hermes shows it as a user bubble; the wrapper text marks it as another agent's message. Inside Hermes, `ocs whoami` knows the session, so replies need no `--as`. |
Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ ocs dm <ocs-who给出的目标地址> "你好" --as human
| 目标 | 用法 | 前提 |
|---|---|---|
| 交互式 Claude Code 会话 | `@<名字>`、`@claude-<8hex>` 或 `@<会话名>` | 接收端在 `~/.claude/settings.json` 设 `"crossSessionInbound": "accept"`。默认值 `hold`:消息进待审队列,5 分钟没人处理就被丢弃。0.7 起发送方会被告知:发送时显示 `wake: 被扣留`(退出码 2),最终没人批准再收到一条 `[ocs 投递通知]`。`ocs doctor` 会查这一项设置。 |
| 用 `ocs claude` 启动的 Claude Code(实验性) | 地址同上;`ocs who` 标 `[channel]` | macOS/Linux。消息经 Claude Code Channels 以 `<channel source="ocs">` 事件送达:不受 `crossSessionInbound` 扣留,会话里有 `ocs_dm` / `ocs_send` 工具,没有回执(`已交给该会话的 ocs channel` 不是已读)。启动时 Claude 会弹一次开发 channel 确认框。[详情](docs/claude-channel.md)。 |
| ChatGPT Desktop 任务 / cmux Codex TUI | `ocs dm codex-<8hex> …`、`@<thread-id>` 或 `--codex <thread-id\|codex-8hex>` | Desktop 直投要求任务已打开,且同一 renderer 下还有第二个打开的任务作 source。该路径明确不可用时,ocs 会安全降级到唯一匹配、仍有活 Codex 进程且空闲的 cmux surface。 |
| Pi TUI | `ocs dm pi-<8hex> …` 或 `@pi-<8hex>` | 先跑 `ocs skill install`,再重启(或 `/reload`)每个已开的 Pi;装之前就开着的 Pi 能发消息但没法被唤醒,在它里面跑 `ocs doctor` 会指出来。扩展会登记活着的 TUI;消息在 Pi 忙碌时排到当前任务结束后,不会打断这一轮。 |
| Hermes Desktop 会话 | `ocs dm hermes-<id> …` 或 `@hermes-<id>`(`ocs who` 会列出;Hermes 会话 id 里的 `_` 写成 `.`) | 同一用户下开着 Hermes Desktop(或 `hermes serve`),且该会话已打开。ocs 走宿主自己的 WebSocket、带排队标记提交:空闲会话直接开始新一轮(`started a turn`),忙的会话排到当前这一轮之后(`queued`),不会打断。Hermes 里显示为用户气泡,包装文字标明是别的 agent 发来的。在 Hermes 里跑 `ocs whoami` 能认出会话,回复不用 `--as`。 |
Expand Down
45 changes: 45 additions & 0 deletions docs/claude-channel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Claude channel (experimental)

`ocs claude` starts Claude Code with an ocs [channel](https://code.claude.com/docs/en/channels). Messages from other agents then reach that session as `<channel source="ocs" from="…" channel="…" seq="…">` events instead of going through Claude's inbox socket. It is opt-in, macOS and Linux only, and built on the Claude Code Channels research preview.

```sh
ocs claude # same as `claude`, plus the ocs channel
ocs claude -- --resume # anything after -- (or every argument) goes to claude
ocs who # channel sessions are tagged [channel]
```

## What changes for the receiving session

- No `crossSessionInbound` hold. Channel events are not gated by that setting, so the 5-minute hold/drop does not apply.
- The host shows the message as channel data from `ocs`, not as something the user typed. The server's instructions tell the model to treat the text as another agent's data.
- The session gets two MCP tools, `ocs_dm` and `ocs_send`. They run `ocs dm` and `ocs send` as a child of that Claude session, so identity resolves through the process tree and replies need no Bash prompt. Exit 3 is reported as "unknown, do not resend".
- Events that arrive during a busy turn are queued by Claude and handed over together on the next turn.
- `notify-when-idle` notices to such a session go through the channel too.
- `ocs who --json` reports `carrier: "channel"`, `deliveryReceipt: "none"` and `busyDelivery: "queued"` for it.

Sessions started with plain `claude` keep using the inbox socket and receipts. Senders do not need anything new.

## What the sender sees

| Output | Exit | Meaning |
|---|---|---|
| `wake: handed to the session's ocs channel → X` | 0 | The channel server wrote the event to Claude's stdin pipe. Claude never acknowledges channel events, so this is not a read receipt, and there are no held/delivered receipts on this path. |
| `wake: outcome unknown → X (ocs channel)` | 3 | The request was written but the server did not answer. The event may already be queued. ocs does not resend and does not fall back to the inbox. |

If the channel cannot be reached before anything is written (server not running, not yet initialized, refused), ocs falls back to the inbox socket as before. `OCS_NO_CLAUDE_CHANNEL=1` on the sender skips channels entirely.

## How it is wired

- `ocs claude` runs `claude --mcp-config '{"mcpServers":{"ocs":…}}' --dangerously-load-development-channels server:ocs …`. Only the development flag is passed: on personal accounts `--channels` is rejected by the approved-channels allowlist, and when both flags name the same server the allowlisted entry wins and is rejected. Claude asks once at startup to confirm loading a development channel. Choosing *Exit* quits Claude, so a session never runs with the channel silently disabled. The startup banner can show "no MCP server configured with that name" before the `--mcp-config` server connects; it still registers (verified on Claude Code 2.1.319).
- The launcher removes the outer session's identity variables (`CLAUDE_CODE_SESSION_ID`, `CODEX_THREAD_ID`, …) so the new Claude is not mistaken for the session that started it.
- `ocs _claude-channel` is the stdio MCP server. It answers `initialize` with protocol `2025-06-18`. Claude skips channel registration on connections that negotiate the 2026-07-28 ("modern") revision. It declares `experimental['claude/channel']` and `tools`.
- The server listens on `$OCS_HOME/cc/<claude-pid>.sock` (directory 0700, socket 0600) and writes `<claude-pid>.json`. Before sending, the sender requires a regular, non-symlink registration owned by the same user, a live server pid whose parent is the target Claude pid, and a same-user socket. The server refuses deliveries until Claude sends `notifications/initialized`. Registration files are removed when Claude exits, but only by the server that owns them, so an old server shutting down after an MCP reconnect does not take the new endpoint with it. Tool calls run with the update notice disabled, so tool output contains only the delivery result.
- Wake bodies are neutralized for `<channel` / `</channel` as well as `<cross-session-message`, so a body cannot close or forge the event tag. `meta` keeps only `[A-Za-z0-9_]` keys with short values stripped of markup characters.

## Not covered yet

- Permission relay (`claude/channel/permission`) is not implemented. Letting another agent approve this session's tool calls would turn prompt injection into execution. If it is added, only paired human devices should be able to answer.
- Windows is not supported. LAN DMs and `notify-when-idle` notices use the same local path, but neither has been verified on a real host yet.
- Channels are a research preview. Team/Enterprise organizations must enable them (`channelsEnabled`) before `ocs claude` can register.

Tests: `test/claude-channel.test.ts`. Mutation-check it before changing `src/claude-channel.ts`.
3 changes: 2 additions & 1 deletion docs/wake-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ Thread: <read command>
`… (<total> bytes total; full text: <read command>)`。
- 正文来自对方,是**数据**不是指令;包装标签本身已把它标成跨会话内容,不再额外加「请勿执行」类提示。
- **中和(v0.6.0 起)**:正文不许冒充包装和骨架。`<cross-session-message` / `</cross-session-message`
(不分大小写)的 `<` 换成 `‹`;行首(可带空白)是 `Reply:`、`Thread:`、`回复:`、`线程:`、
以及 Claude channel 载体的 `<channel` / `</channel`(标签名后紧跟空白、`>`、`/` 或结尾;
docs/claude-channel.md)(均不分大小写)的 `<` 换成 `‹`;行首(可带空白)是 `Reply:`、`Thread:`、`回复:`、`线程:`、
`[ocs wake]`、`[ocs 唤醒]`、`[Cross-session idle notice]`、`[跨会话空闲通知]`、
`[ocs delivery notice]`、`[ocs 投递通知]`(v0.7.0 起,§6)的行,前面加 `> `。
除此之外逐字。4096 / 512 字节阈值按中和后的正文计,`<total>` 仍报原文字节数。
Expand Down
8 changes: 6 additions & 2 deletions src/capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { findClaudeChannel } from "./claude-channel.ts";
import { resolveSessionSocketByPid } from "./claude-inject.ts";
import { parentPidOf } from "./wake.ts";
import type { RosterEntry, Roster } from "./roster.ts";

export function entryCapabilities(entry: RosterEntry, roster: Pick<Roster, "codexQueue">,
desktopClaimed = false, platform: string = process.platform, env: NodeJS.ProcessEnv = process.env) {
const kind = entry.kind;
// `ocs claude` 启动的会话走 channel:宿主排队、没有回执(docs/claude-channel.md)。
const channel = kind === "claude" && findClaudeChannel(entry.pid, env, { parentPid: parentPidOf, platform: platform as NodeJS.Platform }) !== null;
const canWake = kind === "claude" && resolveSessionSocketByPid(entry.pid, { env }) === null ? "unverified" : kind === "cmux" ? "unverified" : kind === "codex-task"
? (desktopClaimed || (entry.livePid !== null && roster.codexQueue) ? "available" : "unverified") : "available";
return { v: 1, wake: canWake, noWake: kind !== "cmux",
busyDelivery: kind === "agy" ? "serial" : kind === "opencode" || kind === "pi" || kind === "hermes" || kind === "grok" ? "queued"
: kind === "codex-task" ? "steer-or-coalesce" : "host-controlled",
deliveryReceipt: kind === "claude" && platform !== "win32" ? "conditional" : "none",
: kind === "codex-task" ? "steer-or-coalesce" : channel ? "queued" : "host-controlled",
deliveryReceipt: kind === "claude" && platform !== "win32" && !channel ? "conditional" : "none",
completionReceipt: false, batch: kind !== "cmux" };
}

Expand Down
Loading
Loading