8000 fix(collector): mark agents dispatched outside Paseo with dispatched-by for 0.12.7 by InfinityPacer · Pull Request #56 · InfinityPacer/meterleaf · GitHub
[go: up one dir, main page]

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
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## [未发布]

## [0.12.6] - 2026-10-25
## [0.12.7] - 2026-10-25

- `scripts/claude-resume-visibility.ts` 不再等 Paseo 代理结束才处理,在 Paseo 里仍开着的主会话也会出现在 `claude --resume` 的列表里。想在终端里接着用时,先在 Paseo 里归档它,避免两边的对话分叉。
- 只需更新这个脚本,采集器和服务端都不用替换。数据库结构未变,升级无需先备份。
- 本机采集器可以登记额外任务:`meterleaf-collector tasks add` 登记一条本机命令,由采集器的后台任务按间隔运行,有限时,失败只记日志,不影响同步,`status` 和 `tasks` 显示最近一次结果。用法见[本机采集器](docs/collector.md#额外任务可选)。
- 新增可选脚本 `scripts/claude-resume-visibility.ts`,登记为额外任务后,你在 Paseo 里开的 Claude Code 主会话会出现在 `claude --resume` 的会话列表里,仍开着的也会列出。其他代理派出的子代理不列出,即使之后你直接和它对话过;在 Paseo 之外用 `paseo run` 派出时加上 `--label dispatched-by=<派出者的会话 ID>` 即可被识别。改动可以按记录改回。
- 版本与发布说明补充了发版时机和只保留最新版本的做法,见[版本与发布](docs/releasing.md)。
- 从 0.12.3 升级需要重新构建并替换本机采集器,服务端不用升级。数据库结构未变,升级无需先备份。
2 changes: 1 addition & 1 deletion docs/collector.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ Paseo 以无界面模式启动 Claude Code,这样产生的会话不会出现
仓库里的 `scripts/claude-resume-visibility.ts` 把你在 Paseo 里开的主会话改成按普通终端会话列出。它只处理同时满足下面两点的会话。

- 属于某个 Paseo 代理,也就是 Paseo 恢复这个代理时用的那个 Claude 会话。
- 这个代理是你开的,不是其他代理派出的。Paseo 内派出的代理带有父代理标签,第一条消息也是上级代理转来的任务,这样的子代理即使之后你直接和它对话过,也不会被列出。在 Paseo 之外,例如普通终端里的 Claude Code 用 `paseo run` 派出的代理,Paseo 不记录派出者,派出时加上 `--label paseo.parent-agent-id=<派出者>` 就会被识别为子代理。
- 这个代理是你开的,不是其他代理派出的。Paseo 内派出的代理带有父代理标签,第一条消息也是上级代理转来的任务,这样的子代理即使之后你直接和它对话过,也不会被列出。在 Paseo 之外,例如普通终端里的 Claude Code 用 `paseo run` 派出的代理,Paseo 不记录派出者,看起来和你开的一样。派出时加上 `--label dispatched-by=<派出者的会话 ID>` 就会被识别为子代理,在终端的 Claude Code 里可以用 `--label "dispatched-by=$CLAUDE_CODE_SESSION_ID"`。不要用 `paseo.parent-agent-id` 代替,Paseo 会把带这个标签的代理当成那个上级代理的子代理,上级不在 Paseo 里时,不会自动为它打开标签页,工作区的运行状态也不计入它。

在 Paseo 里仍开着的代理也会列出。想在终端里接着用时,先在 Paseo 里归档它。Paseo 那边的进程不知道终端里新增的对话,之后在 Paseo 里再发消息,会从它自己的最后一条接着写,两边的对话就分叉了。

Expand Down
11 changes: 11 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
3. 运行 `bun test tests`、`bun run build`,提交版本与更新说明,例如 `chore(release): prepare 0.1.1`。
4. 推送到 `main`。Release 工作流自动校验、测试和构建,创建版本标签,将 `linux/amd64`、`linux/arm64` 镜像同时发布到 GHCR 和 Docker Hub,并创建 GitHub Release。

## 什么时候发版

合并到 `main` 的提交只要改了 `package.json` 的版本,Release 工作流就会发布一个新版本。所以一件事讨论中的后续修正先留在同一个分支上,规则定下来后一次合并、一次发版,不要每改一轮就升一次版本。只改仓库里的可选脚本或文档、不影响镜像和采集器时,可以不升版本,等下一次正式发版一起写进更新说明。

## 只保留最新版本

仓库只保留最新的一个版本。新版本发布成功后,删除旧版本的 GitHub Release 和 Git 标签、GHCR 中旧版本的镜像,以及 Docker Hub 上除 `latest` 和最新版本以外的标签;`CHANGELOG.md` 也只保留最新版本的说明。

- GHCR 里没有标签的条目大多是带标签版本实际指向的各架构镜像,只删除不属于保留版本的那些,否则保留的版本也拉不下来。
- 删除前确认 NAS 等部署没有固定使用要删的版本号;已在运行的容器不受影响,但之后重新拉取会失败。

版本未变时跳过自动发布。需要覆盖已有版本时,在 Actions 的 Release 工作流点击「Run workflow」,选择 `main`;这会按该提交重新构建,替换对应 Git 标签、同版本镜像及 Release 说明。

首次运行仍需按[部署指南](deployment.md)准备数据目录权限和来源配置。不执行删除数据卷的清理命令。
Expand Down
2 changes: 1 addition & 1 deletion package.json
8000
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "meterleaf",
"version": "0.12.6",
"version": "0.12.7",
"private": true,
"type": "module",
"license": "Apache-2.0",
Expand Down
12 changes: 10 additions & 2 deletions scripts/claude-resume-visibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ import { join } from "node:path";
/** Claude Code 读取会话列表摘要时读入的文件开头字节数。 */
const HEAD_BYTES = 65536;
const PROGRAMMATIC = ["sdk-cli", "sdk-ts", "sdk-py"];
/** Paseo 给由其他代理派出的代理加的标签;`paseo run --label` 也可以手动带上。 */
/** Paseo 给在 Paseo 内由其他代理派出的代理加的标签,指向上级代理。 */
const PARENT_LABEL = "paseo.parent-agent-id";
/**
* 在 Paseo 之外用 `paseo run --label dispatched-by=<会话 ID>` 派出时带上的标签。
* 不能借用 PARENT_LABEL:Paseo 会把带它的代理当成该上级代理的子代理,
* 上级不是 Paseo 代理时,不会自动为它打开标签页,工作区的运行状态也不计入它。
*/
const DISPATCHED_LABEL = "dispatched-by";

type Env = Record<string, string | undefined>;

Expand Down Expand Up @@ -100,7 +106,8 @@ function readJson(path: string): unknown {
/**
* Paseo 每个代理一个 JSON;persistence.sessionId 是它恢复的会话。值为 true 表示
* 记录显示这个代理由其他代理派出:Paseo 内派出的代理带 paseo.parent-agent-id
* 标签;从未收到过你在 Paseo 里发的消息时 lastUserMessageAt 为 null。
* 标签,Paseo 之外派出时可以带 dispatched-by 标签;从未收到过你在 Paseo 里
* 发的消息时 lastUserMessageAt 为 null。
* 同一会话被多个代理引用时,任何一个显示是派出的就按派出处理。
*/
function paseoClaudeSessions(paseoHome: string): Map<string, boolean> {
Expand All @@ -126,6 +133,7 @@ function paseoClaudeSessions(paseoHome: string): Map<string, boolean> {
continue;
const dispatched =
Boolean(record?.labels?.[PARENT_LABEL]) ||
Boolean(record?.labels?.[DISPATCHED_LABEL]) ||
record?.lastUserMessageAt === null;
sessions.set(sessionId, (sessions.get(sessionId) ?? false) || dispatched);
}
Expand Down
19 changes: 16 additions & 3 deletions tests/claude-resume-visibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const ids = {
live: "44444444-4444-4444-8444-444444444444",
labeled: "55555555-5555-4555-8555-555555555555",
silent: "66666666-6666-4666-8666-666666666666",
outside: "77777777-7777-4777-8777-777777777777",
};

function line(record: Record<string, unknown>): string {
Expand Down Expand Up @@ -77,6 +78,7 @@ function setup() {
live: join(projects, `${ids.live}.jsonl`),
labeled: join(projects, `${ids.labeled}.jsonl`),
silent: join(projects, `${ids.silent}.jsonl`),
outside: join(projects, `${ids.outside}.jsonl`),
};

// 主会话:开头是注入的上下文和一段转义文本,随后是你的提问;后半部分超过 64 KiB。
Expand Down Expand Up @@ -121,6 +123,7 @@ function setup() {
// 这两个会话的第一条消息看起来像人写的,但代理记录显示它们是派出的。
writeFileSync(files.labeled, user(ids.labeled, "按计划实现第 2 项"));
writeFileSync(files.silent, user(ids.silent, "Read and carry out the plan"));
writeFileSync(files.outside, user(ids.outside, "Read and carry out step 3"));

const agent = (id: string, extra: Record<string, unknown> = {}) =>
writeFileSync(
Expand All @@ -141,6 +144,10 @@ function setup() {
labels: { "paseo.parent-agent-id": "agent-11111111" },
});
agent(ids.silent, { lastUserMessageAt: null });
// 终端里的 Claude Code 用 paseo run 派出,带上自己的会话 ID。
agent(ids.outside, {
labels: { "dispatched-by": "aaaaaaaa-0000-4000-8000-000000000000" },
});
writeFileSync(
join(paseo, "codex.json"),
JSON.stringify({
Expand Down Expand Up @@ -171,7 +178,7 @@ describe("让 Paseo 会话出现在 resume 列表", () => {
const before = statSync(files.main);
const report = reveal(env);
expect(report.revealed.sort()).toEqual([ids.main, ids.live].sort());
expect(report.notEligible).toBe(3);
expect(report.notEligible).toBe(4);

const after = statSync(files.main);
expect(after.ino).toBe(before.ino);
Expand All @@ -195,7 +202,13 @@ describe("让 Paseo 会话出现在 resume 列表", () => {
expect(text).toContain('\\"entrypoint\\":\\"sdk-cli\\"');

expect(listedEntrypoint(files.live)).toBe("cli");
for (const key of ["child", "solo", "labeled", "silent"] as const)
for (const key of [
"child",
"solo",
"labeled",
"silent",
"outside",
] as const)
expect(readFileSync(files[key]).equals(original[key])).toBe(true);
});

Expand All @@ -204,7 +217,7 @@ describe("让 Paseo 会话出现在 resume 列表", () => {
reveal(env);
const second = reveal(env);
expect(second.revealed).toEqual([]);
expect(second.notEligible).toBe(3);
expect(second.notEligible).toBe(4);
const checked = JSON.parse(
readFileSync(join(env.METERLEAF_TASK_STATE_DIR, "checked.json"), "utf8"),
) as Record<string, unknown>;
Expand Down
Loading
0