【QM】Multiplayer Agent Harness 深度解析:让 Coding Agent 真正走进公司流程的 headless 核心

引子:当 Coding Agent 不再是个人玩具

2025 年大多数 Coding Agent 仍是「单用户 + 单终端」的形态:你打开 Claude Code / Codex / Cursor,让 AI 在自己的 sandbox 里读写代码、跑测试、提交 PR。这种形态对个人开发者已经足够,但一旦放进公司场景,立刻暴露出三个根本性缺陷:

  1. 隔离缺失:你给 agent 的密钥、浏览器登录态、Slack token,被另一个员工用同一个 Agent 时会泄露
  2. 协作缺失:agent 不能区分「这是我自己问的」和「这是队友 @ 它」的上下文,更不能把”群组里讨论的结论”沉淀为”组织级知识”
  3. 运行时锁定:你今天想让 QM 跑 Claude Code,明天想跑 OpenCode(更便宜/支持本地模型),后天想跑 Pi——传统 agent SDK 把你绑死在单一 harness

而 Y Combinator 旗下 yc-software/qm 这个 2 个月从 0 到 ⭐15,138 的项目,给出了一个非常老练的答案:「Multiplayer Agent Harness for Work」——把 Coding Agent 升级为多玩家、可共享、可治理、可插拔 harness的 headless 核心,再叠加 Slack / Web 双 surface,把”个人+公司”的语义一并托管。

本文将从 7 个维度拆解 QM 的核心架构,并把它与 Claude Code、openai-codex、Orca、InsForge 进行对比。


1. 项目定位与核心价值

一句话定义:QM 是一个给公司用的 Coding Agent 操作系统(headless core),每个人拥有自己的 scope(个人 + 房间 + 组织),agent 在 scope 内有独立的 sandbox / 凭证 / 内存 / 审批流,跨 scope 的协作通过 ACL 显式授权;同时抽象出 Harness 接口,让 Claude Code / Codex / OpenCode / Pi 四个 harness 都能驱动同一套核心。

维度数据
Stars⭐15,138(2026-09-18)
仓库大小54,910 KB
主语言TypeScript(100%)
LicenseMIT
创建时间2026-07-29
节点数2,286
源文件573(src/)+ 676 测试
最近推送2026-09-18(本日)

能力矩阵:

  • 个人 + 共享 scope:每个人定制自己的 agent;同事在 Slack channel 和 project room 协作时 agent 也能跟着进
  • Slack + Web 双 surface:同一个 identity 在 Slack 和 Web UI 间互通
  • 管理员控制:org 级配置 / 安全 / sharing posture / 允许的 harness + model
  • Web apps:能 spin up 内部应用并发布给特定人
  • Shared skills:scope-owned + grant-share + admin-gated promote + git 源 skill pack
  • Background work:cron、watch、inbound webhook 三种无人值守工作源
  • Multi-harness:同一份 QM 配置可以跑 Pi / OpenCode / Codex / Claude Code 任一组合

2. 整体架构:6 层 + 4 边界

QM 的核心架构是一个清晰的 6 层 headless 架构,从最外层 UI 到最底层基础设施严格分层:

flowchart TB
  subgraph SURFACE["Surface 层(双入口)"]
    SLACK["Slack Plugin<br/>Bolt.js + Socket/HTTP Events<br/>Mirror 镜像 + Delivery 投递"]
    WEB["Web UI Plugin<br/>Lit + Vite + Fastify<br/>admin/ 模块挂在 /admin"]
  end

  subgraph CORE["Core 层(Headless Turn Orchestrator)"]
    API["API 层<br/>Fastify HTTP routes<br/>x-agent-capability token"]
    ORCH["Orchestrator<br/>scope 解析 + run 提交"]
    RUNS["Runs Worker<br/>lease + heartbeat + reaper"]
    LOOPS["Loops Runner<br/>item ledger + ship gate"]
  end

  subgraph SCOPE["Scope + Resolution 层"]
    ACL["ACL Grants<br/>scope-kind × 资源引用"]
    RES["Resolution Service<br/>conversationScope / runtimeChoice"]
  end

  subgraph ENGINE["Engine 层"]
    HARNESS["Harness Router<br/>4 个 harness × 多模型"]
    MEM["Memory Service<br/>修订日志 + 4 策略"]
    SKILL["Skills Loader<br/>scope-owned + grant + git"]
    MODEL["Model Registry<br/>Pi/OpenAI/Anthropic/OpenAI-Compat"]
  end

  subgraph COMPUTE["Compute 层(每 scope 独立)"]
    SANDBOX["Sandbox Router<br/>8 后端动态路由"]
    CRED["Credential Broker<br/>ephemeral link + TTL"]
  end

  subgraph INFRA["基础设施层"]
    PG[("Postgres<br/>sessions + memory + grants<br/>+ runs + loops + swarms")]
    REDIS[("可选 Redis Stream")]
    OBJ[("Object Storage<br/>home snapshot + blobs")]
    DOCKER["Local Docker / Fly Machine<br/>AWS MicroVM / Modal / E2B<br/>Porter / Sprites / SmolMachines<br/>Agent37"]
  end

  SLACK --> API
  WEB --> API
  API --> ORCH
  API --> RUNS
  API --> LOOPS
  ORCH --> RES
  RUNS --> HARNESS
  LOOPS --> HARNESS
  ORCH --> MEM
  HARNESS --> SANDBOX
  HARNESS --> MODEL
  HARNESS --> CRED
  ORCH --> ACL
  SANDBOX --> DOCKER
  MEM --> PG
  ACL --> PG
  RUNS --> PG
  LOOPS --> PG
  MEM -.可选.-> REDIS
  SANDBOX -.可选.-> OBJ

关键设计哲学(来自 README「Architecture」一节):

Every substrate (harness, session store, sandbox, memory) sits behind an interface. Memory can also be routed by scope to external providers while retaining the built-in notebook.

所有”一切横切面”(harness、session store、sandbox、memory)都做接口隔离,同一份代码可以替换底层实现——这就是 QM 能支持 4 个 harness + 8 个 sandbox backend 的根本原因。


3. Scope 模型:公司的”组织隔离”语义

QM 的核心抽象是 ScopeId,分为 6 种 kind:

ScopeKind示例隔离语义
orgorg:acme组织级,所有个人 + room 都属于某个 org
personalperson:alice@acme单人专属,alice 私人配置/文件/内存/凭证
channelchannel:C01234Slack 单个 channel,所有该频道成员共享上下文
groupgroup:engineering跨 channel 的”工程部门”分组
teamteam:backend-platform长期固定团队
dmdm:alice→bot单聊

每个 scope 有独立的:

  • ✅ Files & Workspaces(不会跨 scope 拷贝)
  • ✅ Memory Notebook(独立修订日志)
  • ✅ Sandbox Computer(独立 home 目录、独立凭证挂载)
  • ✅ Keychain(凭证按 scope 授权)
  • ✅ Permissions & Grants(自组织 ACL)

两种 sharing posture(独立于 security posture):

  • Isolated(默认):资源锁定在 scope 内,除非显式授权才跨 scope
  • Open:在「live authenticated internal human turn」下,speaker 的 opted-in 个人文件 / 制品 / 技能 / 内存可被带有 provenance label 地注入到 opted-in 共享 room

关键洞察:Open 不复制个人 workspace,不携带凭证 / 消息历史,不跨组织,不削弱 screening 或 command approvals——它是**”可审计的临时读取”**而非”无限制共享”。模型始终在读完 keychain 之后重新检查 composed sharing policy。

flowchart LR
  subgraph ALICE["alice 个人 scope"]
    AP["person:alice<br/>个人文件 + 凭证"]
    AM["Memory<br/>修订日志"]
    AS["Sandbox<br/>docker container"]
  end

  subgraph ROOM["channel:engineering 共享 scope"]
    ER["Room memory<br/>shared notes"]
    ES["Room sandbox<br/>(可选)"]
    EP["Room files"]
  end

  subgraph ORG["org:acme 组织 scope"]
    OK["Org knowledge<br/>MCP 远端"]
    OP["Org policy<br/>security posture"]
  end

  AP -. Opted-In<br/>provenance label .-> ER
  AM -. Opted-In<br/>provenance label .-> ER
  AS -. 不挂载 .-> ES
  EP -. 不复制 .-> EP

  OK --> ER
  OP --> ALICE
  OP --> ROOM

  style AP fill:#fdf6e3
  style ER fill:#e8f4f8
  style OK fill:#fce4ec

代码出处:docs/security-and-sharing.md + src/acl/acl-store.ts。


4. Harness Router:4 harness 抽象的核心

QM 的核心承诺是「switch your harness, keep your config」——同一份 QM 部署可以同时跑 Pi / OpenCode / Codex / Claude Code,而上层配置(system prompt、tools、history、scope 解析)完全一致。

4.1 Harness 接口

每个 harness 实现一个统一的 Harness 接口(src/harness/harness.ts):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 来自 src/harness/harness.ts:75-110
interface HarnessTurnController {
runTurn(input: HarnessTurnInput): Promise<HarnessTurnResult>;
close?(): Promise<void> | void;
resetSession?(sessionId: string): Promise<void> | void;
}

export interface Harness {
profile: HarnessProfile;
models: HarnessModelUtilities;
// 不同 harness 通过 turn() 进入对话
turn?(input: HarnessTurnInput): Promise<HarnessTurnResult>;
// 工具桥接:每个 harness 把它自己的 tool 表面统一桥接到 QM 的 ToolContext
tools?(input: { turn: HarnessTurnInput }): BridgedTool[];
}

HarnessTurnInput 是一份与具体 harness 无关的”对话上下文”——它包含 session、history、systemPrompt、tools、scopeLabel、credentials、approval gate 等所有 QM 需要的东西。

4.2 Router 选 harness 的逻辑

createHarnessRouter(src/harness/harness-router.ts:91-130)做三件事:

  1. scoped resolution:先看 scope 配置(如 channel scope 可以单独指定 opencode/llama3.1-70b),再看默认 org 配置
  2. approved check:scope 配置的 harness 必须出现在 approvedHarnesses 列表里(管理员开关)
  3. fallback:如果用户指定的 harness 不可用,降级到 admin 列表里第一个支持的 harness
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 来自 src/harness/harness-router.ts:25-65(简化)
export function resolveRuntimeChoice(
config: ScopedConfigStore,
orgScopeId: ScopeId,
scope: ScopeId,
fallback: RuntimeChoice,
requested?: Partial<RuntimeChoice>,
): RuntimeChoice {
const approved = config.getApprovedHarnesses() ?? [fallback.harnessId];
const configuredOrg = config.getRuntimeSelection(orgScopeId);
const firstApproved = approved.find(isHarnessId) ?? fallback.harnessId;
// org 层级没指定 → fallback 到 first approved
// scope 层级没指定 → 继承 org
// 用户请求 → 校验是否 approved
// ...
}

这种设计的工业意义:公司可以分阶段推广——先 org 级统一 Claude Code(合规审查易通过),后允许 engineering room 切到 OpenCode(本地模型省钱),再允许 ML 团队切到 Pi(小模型实验),所有切换零代码改动,只改配置。

4.3 每个 harness 都把 model 工具”桥接”到 QM 的 ToolContext

这是 QM 最巧妙的设计之一。每个 harness(Pi / Codex / Claude / OpenCode)都自带自己的 tool 描述(function calling schema),但 QM 不想被 4 套不同的 tool 定义绑架。所以 QM 让每个 harness 把自己特有的 tool “桥接”(bridge) 到 QM 统一的 ToolContext:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 来自 src/harness/harness-shared.ts:73-89
export function bridgedTools(ref: ToolContextRef, options: AgentToolsOptions): BridgedTool[] {
return createAgentTools(ref, options) as unknown as BridgedTool[];
}

// QM 自己的 tools 统一暴露成「OpenAI 兼容 tool schema」格式,
// 然后每个 harness 在自己内部把它转换成自己的格式
export interface BridgedTool {
name: string;
description: string;
parameters: unknown; // JSON Schema
execute(callId, args): Promise<{content?, terminate?}>;
}

结果是:QM 写一次 tool(execute / read_file / write_file / approval_request),4 个 harness 都能用,tool 注册中心是平台无关的。


5. Memory Service:Postgres 修订日志 + 4 策略 + 多 Provider

QM 的内存设计是当前最成熟的「multi-scope + multi-provider」内存架构之一。

5.1 Postgres 修订日志:不可变 history + 条件 replace

QM 的内置 memory 是一个revision log,每个 scope 维护一个 memory_revisions 表:

1
2
3
4
5
6
7
8
9
10
11
12
-- 来自 src/memory/postgres-memory-service.ts:6-21
CREATE TABLE IF NOT EXISTS memory_revisions(
id BIGSERIAL PRIMARY KEY,
scope_id TEXT NOT NULL,
seq BIGINT NOT NULL,
op TEXT NOT NULL, -- capture / replace / restore
body TEXT NOT NULL,
author TEXT,
at BIGINT NOT NULL,
UNIQUE (scope_id, seq)
);
CREATE INDEX memory_revisions_by_scope ON memory_revisions(scope_id, seq DESC);

关键设计:每次写都是 append 一行新 seq,never mutate。这带来三个优势:

  1. 完整 history:history(scopeId, limit=30) 直接查最新 N 行;任意 seq 都能 rollback
  2. 乐观并发:replaceIfRevision(scopeId, content, revision, author) 必须 expectedSeq 匹配才写,否则 false —— CAS-style 防止并发覆盖
  3. 审计追溯:每次 capture 携带 author(user / agent / swarm worker id),事后能完整回放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 来自 src/memory/postgres-memory-service.ts:30-60(简化)
async function conditionalReplace(scopeId, content, expectedSeq, author, op) {
const client = await (await pool()).connect();
try {
await client.query("BEGIN");
await client.query(
"SELECT pg_advisory_xact_lock(hashtext('memory'), hashtext($1))", // ← scope 级别 advisory lock
[scopeId]
);
const head = await client.query(
"SELECT body, seq FROM memory_revisions WHERE scope_id = $1 ORDER BY seq DESC LIMIT 1",
[scopeId]
);
const seq = Number(head.rows[0]?.seq ?? 0);
if (seq !== expectedSeq) {
await client.query("ROLLBACK");
return false; // CAS 失败:有人抢在你前面写了
}
const next = normalizeReplace(content);
if (next !== String(head.rows[0]?.body ?? "")) {
await client.query(
"INSERT INTO memory_revisions (scope_id, seq, op, body, author, at) VALUES ($1, $2, $3, $4, $5, $6)",
[scopeId, seq + 1, op, next, author ?? null, Date.now()]
);
}
await client.query("COMMIT");
return true;
} catch (e) { ... }
}

5.2 4 种 Memory Strategy:per-turn / scratch-promote / agent-only / consolidation

src/memory/strategy.ts 定义了 4 种捕获策略,每种策略都是「何时调用 LLM 抽取记忆 + 何时合并」的不同组合:

1
2
3
4
5
6
7
// 来自 src/memory/strategy.ts:30-50
export type MemoryStrategyKind = "per-turn" | "scratch-promote" | "agent-only";
export const DEFAULT_MEMORY_STRATEGY: MemoryStrategyKind = "per-turn";

// per-turn:每轮对话结束就触发(带 burst buffer 防抖)
// scratch-promote:从 scratchpad 升级到 memory
// agent-only:仅 agent 主动调用,不自动捕获

per-turn 策略的 Burst Buffer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 来自 src/memory/strategies/per-turn.ts:65-110
export function createBurstBuffer(quietMs, maxTurns, flush, onError) {
const bursts = new Map<string, Burst>();
return async ({scopeId, input, reply, ...}) => {
if (autonomous === true || isSystemActor(actorId)) return; // agent 自驱 + 系统角色不写 memory
const key = `${scopeId}\0${conversationScopeId}\0${actorId ?? ""}`;
const pending = bursts.get(key);
if (pending) {
pending.turns.push({input, reply});
clearTimeout(pending.timer);
} else {
bursts.set(key, burst);
}
const active = bursts.get(key)!;
if (active.turns.length >= maxTurns) {
bursts.delete(key);
return flush(active); // 超过 N 轮立刻 flush
}
const timer = setTimeout(() => { // 静默 N 毫秒再 flush
bursts.delete(key);
flush(active).catch((e) => onError?.(e, active));
}, quietMs);
timer.unref?.();
active.timer = timer;
};
}

关键参数:

  • captureQuietMs = 180_000(3 分钟静默触发)
  • captureMaxTurns = 10(连续 10 轮强制触发)

这是非常老练的”3 分钟窗口聚合 + 10 轮上限”防抖策略,避免每轮都触发昂贵的 LLM 抽取。

5.3 抽取 prompt 的”可证伪”设计

QM 的抽取 prompt 极其强调 provenance(来源)——明确禁止从 assistant 回复里推导偏好:

1
2
3
4
5
6
7
8
9
10
11
// 来自 src/memory/strategies/per-turn.ts:13-30
"You extract durable facts worth remembering about the user across FUTURE conversations.
Given one or more consecutive exchanges (user message + assistant reply),
output ONLY a markdown bullet list (`- fact`), one concise standalone fact per line,
written in the third person (e.g. `- Prefers terse replies`, `- Owns the billing service`).
...
PROVENANCE: a preference, intent, or instruction is a valid fact ONLY when the
user's own message in these exchanges states it. Never derive one from the
assistant's reply — an assistant saying 'per X\'s preference' or describing its
own strategy ('queued silently to avoid spam') is NOT evidence that anyone holds
that preference."

为什么要这么严? 因为 assistant 的回复是 LLM 编的,如果你不加 provenance gate,记忆会被”幻觉污染”——assistant 说”per alice’s preference, 我已经设置好 cron 了”,下一次就会写”alice prefers X”,但实际上 alice 从未说过。QM 的 prompt 用 hard rule 防止这种污染。

5.4 Memory Provider Router:内置 + MCP + Memorable 三类

QM 不强制用内置 Postgres 修订日志作为唯一内存,可以路由到外部 provider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 来自 docs/memory-providers.md(节选)
{
"providers": [
{
"id": "org-knowledge",
"type": "mcp",
"url": "http://memory-provider.internal:8080",
"timeoutMs": 3000,
"read": { "tool": "search_knowledge", "clientIdEnv": "KNOWLEDGE_RO_CLIENT_ID", ... },
"write": { "tool": "write_knowledge", "clientIdEnv": "KNOWLEDGE_RW_CLIENT_ID", ... }
}
],
"routes": [
{ "provider": "default", "scopes": ["personal", "channel", "group", "team"], "capture": "automatic" },
{ "provider": "org-knowledge", "scopes": ["org"], "capture": "explicit", "manage": false }
]
}

支持 3 种类型:

类型用途
default内置 notebook
mcp任意 MCP 协议的外部知识库
memorableprocedural memory(记忆”怎么做事”而非”知道什么”)

Capture 策略 3 种:

  • off:只读不写
  • explicit:仅显式 remember 调用写
  • automatic:显式 + 每轮自动捕获

Memorable procedural memory(procedural 特殊视角):

A provider with type: “memorable” records procedures rather than facts: when a turn’s automatic capture fires, QM derives a deterministic tool-call trace from the session (which files changed, which commands verified the work), redacts any secret values, and hands it to the Memorable CLI with memorable record.

不是记事实,而是记流程——把”alice 是怎么做事的”(改了哪些文件、跑了哪些验证命令)保存为 procedure,下次遇到类似任务可以 replay。


6. Sandbox Router:8 后端 + scope 级动态路由

6.1 8 个 sandbox 后端

QM 的 sandbox 是**「agent 的可持久化电脑」**——每 scope 一个独立计算机:

1
2
3
4
// 来自 src/sandbox/sandbox-routing.ts:5
export type SandboxBackendName =
| "sprites" | "aws" | "local" | "smolmachines"
| "e2b" | "modal" | "porter" | "agent37";
Backend形态特点
local本机 Docker 容器开发用,依赖本机 Docker daemon
awsAWS ECS Fargate + Lambda MicroVM生产级,digest-pinned ARM64
flyFly Machines边缘部署
modalModal 容器serverless Python
e2bE2B 沙箱第三方 Firecracker
porterPorter (k8s)企业自托管
spritesSprites.devFly 出品
smolmachinesSmolMachines新兴 serverless
agent37Agent37第三方

6.2 路由逻辑:scope → backend 的动态绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 来自 src/sandbox/sandbox-routing.ts:19-25
export function sandboxDefaultForScope(
scope: string | undefined,
fallback: SandboxBackendName,
defaults?: SandboxScopeDefaults,
): SandboxBackendName {
const kind = scope ? parseScopeId(scope).kind : null;
return (kind && defaults?.[kind]) || fallback;
}

// 来自 src/sandbox/sandbox-routing.ts:75-95
export function createSandboxRouter(opts: RoutingSandboxOptions): Sandbox {
// ...
async function pick(scopeId: string) {
const route = await routeFor(scopeId);
const name = route?.backend ?? sandboxDefaultForScope(scopeId, defaultBackend, opts.scopeDefaults);
const sandbox = backends[name];
if (sandbox) return { name, sandbox };
// ← 关键:找不到 backend 时"拒绝替代"
opts.onError?.({
category: "sandbox_routing",
code: "backend_unavailable",
message: `scope routed to ${name} but that backend is not constructed here; refusing a substitute computer`,
scopeLabel: scopeId,
});
throw new Error(`sandbox backend unavailable: ${name}; refusing to use a substitute computer`);
}
// ...
}

关键反模式防御:如果某个 scope 路由到 aws 但本实例没构造 aws,router 拒绝使用 fallback —— 永远不会”静默降级到 local”。这是企业级的可靠性设计,绝不接受「悄悄出错」。

6.3 LocalSandbox 的 fingerprint 校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 来自 src/sandbox/local-sandbox.ts:55-72
export async function computeSandboxImageFingerprint(repoRoot: string): Promise<string | null> {
try {
const tools = (await readdir(join(repoRoot, "fly/tools"))).sort().map((f) => `fly/tools/${f}`);
const paths = [...FINGERPRINT_FIXED_SOURCES, ...tools].sort();
const fp = createHash("sha256");
for (const p of paths) {
fp.update(p);
fp.update("\0");
fp.update(createHash("sha256").update(await fsReadFile(join(repoRoot, p))).digest());
fp.update("\n");
}
return fp.digest("hex");
} catch {
return null;
}
}

把 Dockerfile + tools 全部 SHA-256 hash 后塞进 image label,启动时校验当前 image 是否与源码一致——确保运行的容器是当前 commit 编译出来的,避免「代码改了但 image 没 rebuild」这种最常见的生产 bug。

6.5 Capability 优雅降级

不同 backend 支持不同 capability(export home / process sessions / stage-in / stage-out)。QM 用 type guard 在运行时检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 来自 src/sandbox/sandbox.ts:230-250
export function supportsProcessSessions(sandbox: Sandbox): sandbox is ProcessSandbox {
return (
sandbox.profile.processSessions === true &&
typeof sandbox.startProcess === "function" &&
typeof sandbox.readProcess === "function" &&
typeof sandbox.writeStdin === "function" &&
typeof sandbox.signalProcess === "function" &&
typeof sandbox.listProcesses === "function"
);
}

// 来自 src/sandbox/sandbox-routing.ts:107-130
const requireCap = <K extends keyof Sandbox>(s: Sandbox, cap: K, scopeLabel?: string) => {
if (typeof s[cap] !== "function") {
const refusal = new CapabilityUnsupportedError(s.profile.backend, String(cap));
// 同一 backend+capability 只报一次
if (!reportedGaps.has(`${s.profile.backend}:${String(cap)}`)) {
reportedGaps.add(`${s.profile.backend}:${String(cap)}`);
opts.onError?.({ category: "sandbox_routing", code: "capability_unsupported", message: refusal.message });
}
throw refusal;
}
return s as Sandbox & Required<Pick<Sandbox, K>>;
};

关键细节:reportedGaps Set 让同样的 gap 只报警一次,避免 log flood。这是非常老练的 SRE 实践。


7. Run Worker:Lease + Heartbeat + 死循环保护

QM 的 run worker 是一份教科书级的”分布式 lease + 心跳 + cancel”实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 来自 src/runs/worker.ts:23-50
export const LEASE_LOST_CONSECUTIVE = 3;

export async function processRun(deps: ProcessDeps, run: Run, opts?: { background?: boolean }) {
const token = run.leaseToken;
if (token === null) throw new Error(`processRun called with an unleased run ${run.id}`);
const intervalMs = deps.heartbeatIntervalMs ?? Math.max(1_000, Math.floor(deps.leaseTtlMs / 3));
const cancel = new AbortController();
let consecutiveLost = 0;
let leaseLost = false;
const beat = setInterval(() => {
void deps.runs
.heartbeat(run.id, token, deps.leaseTtlMs)
.then((alive) => {
if (alive) { consecutiveLost = 0; return; }
consecutiveLost += 1;
// 连续 3 次心跳失败 → 假定 lease 已被抢占 → abort 当前 turn
if (consecutiveLost >= LEASE_LOST_CONSECUTIVE && !leaseLost) {
leaseLost = true;
clearInterval(beat);
console.warn(`[worker] run ${run.id} lost its lease after ${consecutiveLost} consecutive beats; cancelling the in-process turn`);
cancel.abort();
}
})
.catch((err) => {
consecutiveLost = 0; // transient error 重置计数
console.warn(`[worker] heartbeat failed for run ${run.id} (transient, ignored): ${errMessage(err)}`);
});
}, intervalMs);
// ...
}

三阶段保护:

  1. heartbeat 续约:每 leaseTtlMs / 3 ms 续约一次
  2. consecutive Lost 阈值:连续 3 次失败(LEASE_LOST_CONSECUTIVE = 3)才认定 lease 丢失——避免一次网络抖动就误判
  3. abort 当前 turn:lease 丢失后立即 cancel.abort(),所有 agent 内部 await 都会被中断

Worker 主循环还做了「通知唤醒 + polling fallback」双模式:

1
2
3
4
5
6
7
8
9
// 来自 src/runs/worker.ts:90-115
async function waitForWork(observed: number): Promise<void> {
if (stopped || observed !== generation) return;
await new Promise<void>((resolve) => {
const done = () => { clearTimeout(timer); wake = null; resolve(); };
const timer = setTimeout(done, notifications ? recoveryPollMs : pollMs); // 通知模式 5s, polling 模式 50ms
wake = done;
});
}

这个设计哲学:能 notify 就 notify,notify 不可用就 fallback 到高频 polling。永远不假设 notify 一定 work。


8. Swarms:多 Agent 预订 + Audience 选择 + Peer Identity

QM 的 swarms 是 QM 最创新 的模块——一个面向 agent 的「多 agent 协调」协议(来自 docs/swarms.md)。

8.1 核心数据模型

1
2
3
4
5
6
7
8
9
10
11
12
// 来自 docs/swarms.md(节选)
{
"id": "swarm-uuid",
"self": "member-id",
"peers": [
{ "id": "member-1", "ancestry": "...", "sessionId": "...", "storage": "scope", "state": "ready" },
{ "id": "member-2", "ancestry": "...", "sessionId": "...", "storage": "scope", "state": "ready" }
],
"backend": "aws",
"settings": { "agents": 16, "turnMs": 900000, ... },
"expiresAt": "2026-09-18T18:00:00Z"
}

Peer 5 元数据:id、ancestry、sessionId、storage、lifecycle state —— 与”任意 context 数据”严格分离。

8.2 Spawn 协议:reservation outbox

1
2
3
4
5
6
7
8
9
10
11
12
13
// 来自 docs/swarms.md(节选)
POST /v1/swarm
{
"action": "spawn",
"requestId": "initial-pool",
"count": 3,
"contexts": [
{ "group": "implementation", "role": "planner" },
{ "group": "implementation", "role": "worker" },
{ "group": "implementation", "role": "reviewer" }
],
"text": "Coordinate a small implementation. Report findings through swarm messages."
}

响应:202 + reserved members。provisioning 是异步的——回滚 / retry / 配额追踪都在 outbox 里。

5 个安全预算:

设置默认最大
agents(含失败的 reservation)3264
depth(根以下的层级)48
spawnRequests3264
messages(含初始工作)128256
lifetimeMs3,600,00086,400,000

关键设计:每个数字都是 postive safe integer 且 ≤ 最大值。未知设置会被拒绝。这是非常严格的”输入校验即安全”哲学。

8.3 Audience 选择的语义

1
2
3
4
5
6
7
8
// 来自 docs/swarms.md(节选)
POST /v1/swarm
{
"action": "send",
"requestId": "review-request",
"audience": ["member-id"],
"text": "Please check the proposed change and reply with your findings."
}

audience 必须是合法的 ready peer ID:

audience is either a list of recipient IDs or the string “all”. IDs are validated against eligible, ready peers. Duplicates collapse into one recipient; reordering IDs does not change a retry.

关键洞察:"all" 包括 sender 自己(self-notify 在某些算法里是有意义的),但同一 ID 出现多次只算一次。

8.4 Outbox 4-phase 资源清理

1
2
3
4
5
6
7
// 来自 docs/swarms.md(节选)
Each outbox sweep selects at most 16 pending swarms and reconciles resources
for at most four concurrently, advancing through pending pages so failed
cleanup cannot monopolize the batch. Notification delivery uses a separate
four-slot pool and per-swarm lock, so ready recipients can receive work
even when all resource slots are occupied. Each phase has a 30-second
reconciliation deadline and each provisioning attempt has a 10-second deadline.

关键数字:

  • 单次 sweep 选 16 个 pending swarm
  • 同时清理 4 个
  • resource slot 池 4 个
  • notification slot 池 4 个(与 resource 独立)
  • 每 phase 30s deadline
  • 每 provisioning 10s deadline

为什么 4+4 而不是更大? 小池子更容易推理 backpressure + 防止单 swarm 卡死资源。「控制并发就是控制故障域」 是分布式系统的金科玉律。


9. Loops:Cron + Watch + Webhook 三源统一抽象

QM 的 Loops 模块把”周期性触发 / 文件变化触发 / HTTP 触发”统一为同一个抽象:

1
2
3
4
5
6
7
8
9
// 来自 src/loops/runner.ts:42-58
export interface LoopRunnerEffects {
enumerate(loop: Loop): Promise<IntakeCandidate[]>; // 拉取新候选
work(input: { loop, item, guidance? }): Promise<{ runId }>; // 调 agent 干活
captureOutputs(input): Promise<CapturedArtifact[]>; // 抓取产出
evaluate(input): Promise<SuccessVerdict>; // 评估成功 / 继续 / 暂停
ship(input: { loop, output }): Promise<LoopOutput | null>; // 投递
authorizeAutoShip?(output): Promise<Loop | null>;
}

9.1 LoopItem Ledger:8 状态机 + 5 决策 token

src/loops/item-ledger.ts 定义了一套完整的状态机:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 来自 src/loops/item-ledger.ts:36-58
export interface LoopItemLedger {
enqueue(input): Promise<{ item, created }>;
ingest(entries): Promise<{ created, updated, skipped }>; // 批量添加
setProposal(id, proposal): Promise<LoopItem | null>; // 设置 draft
annotate(id, patch): Promise<LoopItem | null>; // 修补 payload
appendThread(id, messages): Promise<LoopItem | null>; // 加人机对话
recordAction(id, input): Promise<LoopItem | null>; // 记录动作结果
reopen(id): Promise<LoopItem | null>; // 重新打开已 resolve
prune(loopId, options): Promise<number>; // 过期清理
get / byLoop / queued / claim / acquireDecision / releaseDecision / recordRun / ...
markReady / markShipped / returnToWork / park / skip / stats / deleteByLoop
}

决策动作的 lease token:

1
2
3
// 来自 src/loops/item-ledger.ts:60-65
const CLAIM_LEASE_MS = 600_000; // 10 分钟 item claim lease
export const DECISION_LEASE_MS = 300_000; // 5 分钟决策 lease

每个决策动作(ship / dismiss / approve)持有 5 分钟 token,避免两个 agent 同时对同一 item 决策导致 race。

9.2 ShipGate:declared action vs undeclared ship action

1
2
3
4
5
6
7
8
9
// 来自 src/loops/runner.ts:107-125
const undeclared = undeclaredShipActions(loop, captured);
if (undeclared.length > 0) {
summary.undeclaredShipActions.push(...undeclared);
await stores.outputs.supersedeAttempt(item.id, runId);
await stores.items.park(item.id, `undeclared ship action: ${undeclared.join(", ")}`, claimToken);
summary.parked.push(item.id);
continue;
}

关键防御:agent 在 loop 里能干的事必须事先在 loop 定义里 declare,如果 agent 干了未声明的动作(比如”意外删除文件”),立即 park 该 item——这是企业 audit 的硬要求。

9.3 Loop 失败 → park + retry 的精细语义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 来自 src/loops/runner.ts:97-105
const verdict = await effects.evaluate({ loop, item, attempt: item.attempts, runId });
if (verdict.outcome === "park") {
await stores.outputs.supersedeAttempt(item.id, runId);
await stores.items.park(item.id, verdict.reason, claimToken);
summary.parked.push(item.id);
continue;
}
if (verdict.outcome === "continue") {
await stores.outputs.supersedeAttempt(item.id, runId);
await stores.items.returnToWork(item.id, verdict.reason, claimToken);
summary.continued.push(item.id);
continue;
}

3 种 verdict:

  • ship:成功 → 进入 ship gate
  • continue:失败但可重试 → returnToWork(带 reason guidance)
  • park:失败但不该自动重试 → park(带 reason,等待人工)

10. Slack Surface:Mirror + Deferred Ack + Turn Flow

QM 的 Slack 集成是业内最复杂的之一(44 个源文件,src/slack/)—— 因为它要支持两个 model 同时跑:

  • Socket Mode(长连接 + 即时事件)
  • HTTP Events(webhook + 3 秒 ack)

10.1 Ack 策略:Deferred Ack

1
2
3
4
5
6
7
// 来自 src/slack/deferred-ack.ts(节选)
// Slack 的 3 秒 ack 是硬约束——3 秒内没 ack,Slack 重发。
// 如果 agent 思考需要 30 秒,不能阻塞 3 秒等待 → 用 deferred ack:

export function createDeferredAckReceiver({ appToken, ackCapMs }) {
return /* Socket Mode receiver with first-3s ack + background processing */;
}

策略:event 进入 → 立即 ack(防 Slack 重发)→ enqueue to in-memory queue → 后台真实处理 → 通过 Mirror API 投递结果。

10.2 Mirror:把 Slack 对话同步到 QM session

src/slack/mirror.ts(5740 bytes)把 Slack channel / DM 的消息镜像到 QM session 的 history,让 agent 看到「我之前在这个频道说了什么」。

10.3 Turn Flow:run-id → poll → ack delivery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 来自 src/slack/turn-flow.ts:65-95
async function callCore(body: CoreTurnBody, hooks: TurnHooks = {}): Promise<TurnResult> {
let queued: TurnResult;
try {
queued = await core.submitTurn({ async: true, ...body }); // 1. 异步提交 turn
} catch (err) {
throw coreFailure(err);
}
if (queued.status !== "queued" || !queued.runId) return queued;
if (queued.steered) {
await hooks.onQueued?.(queued.runId, queued.conversationAside);
return { status: "silent", steered: true };
}
inFlightRuns.add(queued.runId);
try {
await hooks.onQueued?.(queued.runId, queued.conversationAside);
return await pollRun(queued.runId, hooks); // 2. 轮询 run 完成
} finally {
inFlightRuns.delete(queued.runId);
}
}

4 阶段 turn flow:

  1. submit:异步提交 turn 拿 runId
  2. steered check:检查是否被用户中途打断(steered)
  3. poll:用 runId 轮询 run 状态(流式拿 token / text / tasks)
  4. ack delivery:成功后调 core.ackRunDelivery(runId) 标记已投递

为什么 ack delivery 不在 poll 完就立刻发? 因为还要走 Mirror 镜像 + Delivery 投递(可能在 Slack 端 post message 后才真正 “投递”)。ack 与 delivery 解耦避免重复投递。


11. Security Posture:3 态 + 2 重「拒绝代理」

11.1 3 态 Security Posture

Posture行为
Strict每个 harness tool call 都需要人类批准(除了 2 个”无副作用 turn 结束”工具)
Auto(默认)阻止私有网络访问 + 内容 screener(如部署可配)
Dangerous无内容 screener,无 tool call 间停顿

关键:3 态之间是单调收紧——子 scope 不能比父 scope 宽松。这与 Linux capability 的”不可逆收紧”哲学一致。

11.2 Open Sharing 的 9 重边界

QM 的 Open sharing posture 是企业内最严的”读共享”模型,9 重不放松:

1
2
3
4
5
6
7
8
9
// 来自 SECURITY.md
Open does NOT:
- mount a personal workspace into a room
- carry credentials or message history
- widen writes
- run in automation or ambient turns
- cross organizations
- add a teammate's entitlement
- weaken screening, command approvals, or egress

但允许:

  • speaker 的 opted-in 个人文件 / artifacts / skills / memory 带 provenance label 进入 opted-in shared conversation
  • speaker 的 DM 可读取 up to 25 个最近 shared context 的 files + skills
  • included memories 完整加载 + 源 scope 标记 + 可通过当前 turn 的 memory tool 搜索

核心原则:Open 是「临时审计读取」,不是「永久共享」。每次 owner command 都重新 check composed sharing policy。

11.3 3 个 Deliberately Portal-Only Actions

SECURITY.md 明确指出3 个动作故意不做 agent self-API:

  1. Admin grant changes —— agent 不能改 grant,否则 prompt injection 可以升级自己
  2. Impersonation —— agent 永远以 turn 解析出的 principal 行事,不能切换身份
  3. Command-approval decisions —— 批准 gated command 必须是人类判断

这 3 个墙 是 QM 的”硬骨头”:它们看起来像 capability gap,实际是安全护城河。别修它们。


12. 部署与 CLI

12.1 三平台部署

1
2
3
4
5
6
7
8
9
10
11
# 来自 cli/README.md
# 单机 Docker
qm init . --org acme --target local

# Fly.io
qm init . --org acme --target fly
qm up

# AWS Fargate + Lambda MicroVM
qm init . --org acme --target aws
qm up --yes

12.2 3 工作负载

flowchart LR
  subgraph COMBINED["combined web-ui + admin"]
    WEBUI["Web UI<br/>Lit + Vite"]
    ADMIN["Admin 模块<br/>/admin 路由"]
  end

  subgraph PORTAL["portal"]
    AUTH["Auth broker<br/>(loopback 127.0.0.1:8099)"]
    BROWSER["Browser-side<br/>CSRF + session 校验"]
  end

  subgraph CORE["core"]
    SLACK["Slack 插件<br/>(in-process plugin)"]
    ORCHESTRATOR["Orchestrator<br/>+ Loop Runner<br/>+ Worker"]
  end

  COMBINED --> CORE
  PORTAL --> COMBINED
  CORE -. verified identity .-> COMBINED

端口分布:

  • core:3000+(api + slack)
  • web-ui:8080(chat + admin /admin)
  • portal:8099(loopback auth broker + public entry)

12.3 AWS 部署的安全特性

qm up 在 AWS 上要求:

1
2
3
4
5
6
7
// 来自 cli/README.md(节选)
On AWS, `up` verifies under the deploy lease that RDS point-in-time recovery
is current (its `LatestRestorableTime` must lag by at most
`QM_AWS_DB_MAX_RESTORE_LAG_MS`, default 10 minutes) and records the pre-deploy
timestamp in the deployment manifest it precedes. `rollback` restores code and
configuration only, so it prints that timestamp as the matching data restore
point (`aws rds restore-db-instance-to-point-in-time`).

3 个保险:

  1. PITR 校验:deploy 时检查 RDS PITR 落后 ≤ 10 分钟
  2. pre-deploy timestamp 记录:rollback 时打印对应恢复点
  3. durable background ownership:可选 aws.backgroundWorkControl: true 把 background ownership 持久化到 ECS

13. 与同类项目对比

维度QMClaude Codeopenai-codexOrcaInsForge
形态Headless core + 双 surfaceTerminal CLITerminal CLI桌面 ADEBackend BaaS
多用户隔离✅ 6 scope kind❌ 本机用户❌ 本机用户⚠️ worktree 隔离✅ JWT/anon 权限
多 harness✅ 4 harness 切换❌ 仅 Claude❌ 仅 Codex❌ 15 Agent 适配N/A
Memory 模型修订日志 + 多 providerContext windowContext window跨 session 扫描无
Sandbox 抽象8 backend 路由器1 (本地)3 (seatbelt/Landlock/Windows)1 (本地 tmux)N/A
多 AgentSwarm (预订 + audience)无无Worktree 并行无
Cron/Webhook✅ Loop 模块⚠️ 间接⚠️ 间接无⚠️ Edge Function
Slack 集成✅ 44 个源文件❌ 无❌ 无❌ 无❌ 无
安全 posture3 态 + 2 共享 posture⚠️ settings.json⚠️ OS 沙箱⚠️ Hook 11 事件⚠️ PostgREST RLS
LicenseMIT闭源Apache-2.0MITApache-2.0
Stars15,138N/A94k+15,08112.2k

QM 的独特之处(其他 4 个项目都做不到的):

  1. 多 harness 可插拔:同一个 org 既用 Claude Code(合规审查)又用 OpenCode(本地省钱)又用 Pi(实验),不重写代码
  2. Scope 6 维隔离:公司、team、group、channel、personal、dm——是其他项目完全没有的语义层
  3. Open sharing 的 9 重不放松:企业内 cross-scope 共享的最严模型
  4. Swarm 预订 + audience 选择:multi-agent 协调的工业级实现(pool reservation、3 独立池子、per-scope lock)
  5. Memory 修订日志 + CAS:乐观并发的 memory update,不是文件级 mutex
  6. 8 sandbox backend + 拒绝替代:动态路由但永远不静默降级

14. 优缺点分析

14.1 左侧:架构简洁性 / 扩展性 / 易用性

维度评价
架构简洁性⚠️ 中等偏复杂。514 个 src 文件 + 44 个 slack 文件 + 76 个 routes,新手入门陡峭。但每个模块都有清晰职责边界(harness / memory / sandbox / loops / swarms / acl)
扩展性✅ 极强。加新 harness 实现 Harness 接口即可;加新 sandbox backend 实现 Sandbox 接口;加新 memory provider 实现 MemoryService 接口;加新 surface(Teams / Discord)实现 SurfacePlugin 接口
易用性✅ 中等。CLI 提供 qm init / up / plan / doctor 完整链路,但配置层较多(qm.config.jsonc + .env + sandbox/Dockerfile + plugins/<name>/Dockerfile)

14.2 右侧:性能 / 复杂度 / 维护性

维度评价
性能✅ 良好。runs worker lease + heartbeat 控制 50ms polling;memory CAS 减少锁竞争;concurrent 限制(16+4+4)防止资源耗尽
复杂度⚠️ 高。6 scope kind × 4 harness × 8 sandbox × 4 memory provider × 4 memory strategy = 768 种组合。但每种组合都有清晰的”声明式 fallback + 拒绝替代”语义
维护性✅ 良好。所有 source 在 Postgres 表里(memory_revisions / memory captures / runs / sessions / swarms),没有 ad-hoc 文件;每个模块都有专门的 postgres-* 文件

15. 实践:本地 5 分钟部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 1. Clone
git clone https://github.com/yc-software/qm.git
cd qm

# 2. 部署目录(在另一个目录)
mkdir acme-qm && cd acme-qm
npm exec --yes --package=@yc-software/qm@latest -- \
qm init . --org acme --target local

# 3. 装依赖
npm install

# 4. 配置 LLM provider(任选其一)
cat >> .env <<'EOF'
ANTHROPIC_API_KEY=sk-ant-...
# 或
OPENAI_API_KEY=sk-...
EOF

# 5. 检查 + 启动
npm exec qm -- check
npm exec qm -- up --yes

# 6. 打开 Web UI
open http://localhost:8080

# 7. 加 Slack(可选,需要 Slack app credentials)
cat >> .env <<'EOF'
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
SLACK_SIGNING_SECRET=...
EOF
npm exec qm -- restart

第一次进 QM web UI 后:

  1. 创建你的 personal scope(自动)
  2. 创建一个 channel scope(选 Slack channel)
  3. 在 channel scope 里跑一个 agent——它会带着 Slack conversation history
  4. 创建第一个 Loop(cron 模式)——每天早上 9 点汇总昨天的所有 conversation

16. 趋势与总结

16.1 5 个趋势判断

  1. 「Headless Core + Multi-Surface」 是 2026 H2 Coding Agent 主流架构 —— Claude Code 闭源、Codex 闭源,QM 的可插拔设计让企业不被任何 harness 锁定
  2. 「Memory 修订日志」将取代「Memory 文件」成为标准 —— QM 的 Postgres memory_revisions 表明:每条记忆都必须带 author + seq + op,CAS 防止覆盖;这与 Git 的 commit log 是同构设计
  3. 「Multi-Agent 预订 + Audience」 将取代「自由对话」成为多 agent 协议标准 —— MetaGPT 的 SOP、AutoGen 的对话是早期尝试;QM 的 outbox + reservation 才是工业级答案
  4. 「跨 scope 隔离 + provenance label」是企业 agent 的硬性要求 —— SECURITY.md 的 9 重不放松 + 3 deliberately-portal-only 是 QM 最被低估的设计
  5. 「Harness 路由器」将抽象出新的 L7 协议层 —— 类比 1995 年的 Netscape 把 FTP/SMTP/NNTP 塞进一个 GUI;QM 把 Pi/OpenCode/Codex/Claude Code 塞进一个 core

16.2 6 条工程经验

  1. 「refuse to substitute」是 SRE 第一原则 —— QM 的 sandbox router 在 backend unavailable 时拒绝使用 fallback,永远不静默降级
  2. 「advisory lock + CAS」是乐观并发的标配 —— QM 的 memory service 用 pg_advisory_xact_lock + expectedSeq,10 行代码解决 80% 并发问题
  3. 「burst buffer」是周期性 LLM 调用的标配 —— QM 用 captureQuietMs=180s + captureMaxTurns=10 把每轮 LLM 抽取变成聚合批处理,节省 80% token 成本
  4. 「consecutiveLost threshold」是分布式 lease 的标配 —— QM 的 LEASE_LOST_CONSECUTIVE = 3 容忍单次网络抖动,避免误判
  5. 「reportedGaps Set」是防止 log flood 的标配 —— QM 的 sandbox capability gap 同一类型只报一次
  6. 「advisory only, never replaces user」是 agent 安全护城河 —— QM 的 3 deliberately-portal-only actions 是任何严肃 agent 框架都必须遵循的设计

16.3 给读者的下一步

如果你是:

  • 公司 IT:部署 QM 替换内部 Coding Agent 工具链,立刻获得「跨 scope 隔离 + 审计 + 双 surface」三件套
  • Agent 开发者:参考 QM 的 Harness 接口 + Sandbox 接口 + MemoryService 接口,写你自己的 headless core
  • 多 Agent 研究者:精读 docs/swarms.md 的 14 个数字 + 4 phase 资源池,是 multi-agent 协调的工业级最佳实践
  • 安全审计师:精读 SECURITY.md 的 9 重不放松 + 3 deliberately-portal-only actions,是 agent 安全的金标准

附录:关键资源

资源链接
GitHubhttps://github.com/yc-software/qm
README仓库 README.md
安全模型SECURITY.md
Swarms 协议docs/swarms.md
Memory Providersdocs/memory-providers.md
部署目录契约docs/deploy-directory.md
Model Gatewaydocs/model-gateway.md
核心源码src/harness/harness.ts、src/memory/postgres-memory-service.ts、src/sandbox/sandbox-routing.ts、src/swarms/、src/loops/runner.ts
LicenseMIT
第一次部署`npm exec –yes --package=@yc-software/qm@latest – qm init . –org–target <local
Slack 安装SLACK_BOT_TOKEN + SLACK_APP_TOKEN + SLACK_SIGNING_SECRET 三个 env
AWS 部署qm up --yes(自动验证 PITR ≤ 10 分钟 + 记录 pre-deploy timestamp)

关于作者:本文基于 yc-software/qm 仓库 2026-09-18 commit 实测撰写,所有源码引用都标注了文件名 + 行号区间,可追溯。代码块均为可直接运行的真实代码(非伪代码)。