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
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,24 @@
All notable changes to Braid are recorded here. The project follows Semantic
Versioning once release artifacts are published.

## [0.3.1] - unreleased
## [0.3.2] - 2026-09-19

### Changed

- 统一 Issue/PR 的逻辑生命周期驱动,将物理进程与连接的所有权留在 provider adapter。
Codex 内部复用 app-server,Pi 每个会话独立持有进程;恢复失效会话不重建健康会话。
- 将 mention 权限分类、调度推进与 GitHub outbox 拆为独立循环,避免网络权限查询拖延调度和写入。

### Fixed

- Provider 配置与持久化类型沿实际 Profile 解析,避免默认 PR 参数覆盖其他 Profile 或将 Pi 记录为 Codex。
- Context replacement 前释放旧句柄;Unknown 不生成失败 reaction,并能推进正在等待终态的 Context reset。
- 调度只领取具有可用句柄的会话;健康状态汇总避免一个 driver 的成功覆盖另一个 driver 的失败。
- 修正 Slice 3 验收中的公网就绪、Unknown 恢复及快速终态采样,保留完整的验收证据。

本版本不新增数据库 migration,配置与数据库 schema 均保持 v2。

## [0.3.1] - 2026-09-03

### Added

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "braid"
version = "0.3.1"
version = "0.3.2"
edition = "2024"
rust-version = "1.93"
description = "GitHub working memory for local coding agents"
Expand Down
31 changes: 15 additions & 16 deletions docs/20-product-tdd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,26 +98,25 @@ crates. Modules are deep and align with authority boundaries:
| `context` | Canonical snapshot model, HTML-comment removal, deterministic Markdown rendering, budget, and revision. |
| `events` | Canonical diff classification and compact Event Reference rendering. |
| `store` | One dedicated SQLite actor, transactions, migrations, leases, ledgers, sessions, batches, and outbox. |
| `scheduler` | Quiet/count/urgent coalescing and single-flight group turn claims. |
| `producer` | Webhook/GraphQL ingress → canonical diff → classified events (`ingress`, `reconcile`). |
| `queue` | Per-work-item per-agent-group quiet window, batch emission, claim decisions, context-pressure policy, and store-side reset fencing (`scheduler`). Never touches provider sessions or connections. |
| `outbox` | Drain the GitHub write outbox (reactions, comments, statuses) with uncertain-write recovery. Leaf over `store` + `github`, called by ingress and the runtime drain loop. |
| `group` | Agent Group workers that own every provider connection epoch (connect, resume, drive, reconnect), the dispatch/materialization half that executes queue decisions against `AgentSession`s, provider supervision/prompts/attribution, and the per-epoch in-process `SessionManager` (`issue_agent`, `pr_agent`, `dispatch`, `provider`, `session_manager`). |
| `agent_session` | Core `AgentSession` trait and event stream (`TurnStarted`, `TurnTerminal`, `Failed`). Core callers operate sessions only through `send_user_msg`; the event stream is the single authority for lifecycle facts. |
| `provider::session` | `ProviderAgentSession` adapter that maps `AgentSession` to `AgentProvider` primitives and translates provider notifications into `SessionEvent`s, deduplicating the provider's response-side and notification-side observation of the same fact. |
| `session_manager` | In-process `SessionManager` keyed by provider thread id; start/resume/get. Ephemeral per connection epoch: it is rebuilt from the durable store on every (re)connect because sessions bind the epoch's provider handle. |
| `provider` | Provider-neutral capability contract and Codex NDJSON implementation. |
| `producer` | Webhook/GraphQL observation、canonical diff 与事件分类;独立的 `mentions` 循环补全 GitHub 权限事实并保留失败 backoff。 |
| `queue` | 独立推进 Quiet Window、count、urgent 与批次状态;通过 store 领取可运行 turn,不执行网络权限查询或持有会话事件 receiver。 |
| `outbox` | 独立收敛 GitHub reactions、comments、statuses 及 uncertain writes;runtime 关闭时执行最终 drain。 |
| `group` | `GroupDriver` 统一 Issue/PR 的逻辑生命周期、dispatch、Context replacement 和恢复;领域模块保留 Profile、prompt、worktree 与兼容性规则。`RunningAgentTurn` 保存当前 claim 和事件 receiver。 |
| `agent_session` | Core 定义的 `SessionFactory` 创建/恢复契约与 `AgentSession` 行为、事件、失效观察和释放契约。 |
| `group::session_manager` | 按 opaque provider session ID 索引中立句柄;只恢复缺失或失效的句柄,并释放不再使用的句柄。持久化绑定仍由 store 权威保存。 |
| `provider` | 实现 core 会话契约,拥有物理进程、连接、会话寻址和通知翻译。Codex factory 内部缓存共享 app-server;Pi factory 为每个物理会话创建独立资源。 |
| `worktree` | Validate a Profile source checkout, resolve the bound ref (PR head, sole Development branch, or default origin branch), provision one generation-scoped worktree per Agent Group, and expose recovery diagnostics; no Git-operation sandbox. |
| `writer` | `braid gh`, attribution, reaction/status desired state, and write-outbox convergence. |
| `telemetry` | Trace/metric/log creation, payload events, sampling configuration, and OTLP export. |
| `tunnel` | Wrangler Quick Tunnel supervision and webhook URL handoff. |
| `runtime` | Owner lease, worker supervision, boot-time configuration gates, shutdown ordering, health, and public operator state. Never touches provider connections or sessions directly. |
| `cli` | `serve`, `config`, `doctor`, `profile`, `gh`, `status`, and migration/version surfaces. |

Module dependencies point one way only: `runtime` → `group` → `queue`, and
`runtime` → `producer` → `outbox`/`health`; `queue`, `outbox`, and `health`
sit above the leaf modules (`store`, `context`, `github`, `config`,
`provider`, `worktree`, `telemetry`) and no lower layer imports an upper one.
`runtime` 装配 `group` 与 provider 实现。`group` 和 `provider` 都依赖
core 的 `agent_session` 契约;adapter 不导入 Group、queue 或 store。
Group 使用 store/context/github/worktree 编排产品状态,不操纵连接 epoch。
Producer、queue、outbox 各自通过 store 收敛其负责的状态;只有需要平台
事实或写入的 producer/outbox 依赖 GitHub 网络操作。

### Internal Event Model

Expand All @@ -140,9 +139,9 @@ best-effort one-way projection from it. Nonessential state is not persisted.
| --- | --- | --- |
| Work Items, assignments, turns, context ledger/resets, queue/batches, outbox, owner lease | Durable store (SQLite) | In-memory `RunningAgentTurn` (claim cache for the in-flight turn), health snapshot |
| GitHub canonical state | GitHub | `canonical_objects` / `sync_cursors` snapshots for diffing |
| Physical session identity (`provider_session_id`) | Durable store (`provider_sessions`) | `SessionManager` map key + adapter `thread_id` (both ephemeral, rebuilt per epoch) |
| Current provider turn | Provider process | `SessionEvent` stream (exactly one `TurnStarted`/`TurnTerminal` per turn; receiver handed off with the turn, never re-subscribed) → durable store; resume fencing as the cross-epoch backstop |
| Provider connectivity | Provider connection | `AgentProvider::closed()` futureworker epoch loop → health snapshot + blocked-session records |
| Physical session identity (`provider_session_id`) | Durable store (`provider_sessions`) | `SessionManager` 的 opaque key adapter 寻址;句柄可替换,assignment 与 worktree 连续性不依赖连接 |
| Current provider turn | Provider process | `SessionEvent` stream (exactly one `TurnStarted`/`TurnTerminal` per turn; receiver handed off with the turn, never re-subscribed) → durable store; 恢复时 fencing 遗留的 starting/running turn |
| Provider connectivity | Provider connection | adapter 内部观察连接退出受影响句柄的 latched availability;runtime 汇总各 driver 的恢复结果,成功不能覆盖另一方的失败 |
| Worktree presence | Filesystem + git | `worktrees` table (refreshed by inspection at prepare time) |

Selected dependency baseline, verified against crates.io on 2026-08-13:
Expand Down
28 changes: 17 additions & 11 deletions docs/20-product-tdd/app-server.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
# Provider Contract and Codex app-server Mapping

Braid owns a provider-neutral logical session contract while MVP implements
Codex app-server only. Pi and Claude Code remain future adapters; their
different compaction/profile/resource semantics cannot leak into the core state
machine.
Braid 的 core 会话契约与 provider 的物理拓扑分离。Codex 与 Pi 都实现同一契约;
Group 不根据 backend 决定连接数量或故障范围。

## Provider-Neutral Interface

The core runtime uses the `AgentSession` trait and `SessionManager` rather than
calling provider primitives directly. The adapter (`ProviderAgentSession`)
implements `AgentSession` over the lower-level `AgentProvider` contract and
translates provider notifications into `SessionEvent`s:
`agent_session` 定义 `SessionFactory` 和 `AgentSession`。Runtime 按实际 Profile
选择并注入 factory;Group 提供已选择的 Profile、instructions、完整 Context 和工作目录。
创建结果包含 opaque provider session ID 与中立句柄,store 保存它与 Agent/assignment
的绑定。Resume 返回同一持久化身份的新句柄,不改变 assignment 或 worktree。

| Core method | Adapter behavior |
| --- | --- |
| `send_user_msg(msg, steering)` | If idle, start a new turn with `msg`; if running and `steering`, forward the steer to the active turn; if running and not steering, drop the message (the event queue owns redelivery). Returns `Started` or `Acknowledged`; lifecycle facts arrive only via events. |
| `interrupt()` | Best-effort termination of the observed in-flight turn (Codex `turn/interrupt`, Pi `abort`); idempotent at the state-machine boundary, terminal still arrives via the event stream. Used by hard invalidation after the DB fence. |
| `events()` | Emits exactly one `TurnStarted` per turn, then exactly one `TurnTerminal` (carrying the provider error when the outcome is `Failed`/`Unknown`), translated and deduplicated from provider notifications. The receiver created before dispatch is handed to the consumer with the turn — never re-subscribed. Connection death is observed via `AgentProvider::closed()`, not this stream. |
| `SessionFactory::check()` | 检查 adapter 的启动前置条件;共享运行资源由 adapter 自己维护。 |
| `SessionFactory::start/resume` | 创建或恢复会话并返回中立句柄。Codex 内部共享 app-server,Pi 每个会话持有独立进程;上层接口相同。 |
| `send_user_msg(msg, steering)` | Idle 时启动 turn;running 且 steering 时发送 steer;否则返回 Acknowledged,由 queue 保留后续输入。 |
| `interrupt()` | 尝试停止已观察到的 active turn;terminal 仍通过事件流返回。 |
| `events()` | 将 provider 的响应与通知去重为 TurnStarted / TurnTerminal;dispatch 前订阅,同一 receiver 随 RunningAgentTurn 交给 driver。失效的 active handle 合成 Unknown,不能伪造失败。 |
| `is_unavailable()` | 句柄失效后永久返回 true;idle 或晚订阅也可观察。恢复创建新句柄,不复活旧句柄。 |
| `close()` | 停止使用该句柄,尝试 interrupt,取消监听并释放资源;不能影响其他会话。 |

具体 `AgentProvider` 接口只在 adapter 内使用。共享 Codex 进程退出会使其所有
句柄失效;独立 Pi 进程退出只影响所属句柄。释放旧句柄会取消旧监听任务,
防止它继续消费通知;turn ID 去重防止旧 terminal 结算新的 turn。

The core never assumes a provider can rewrite arbitrary history or accept a
custom compaction result. Context replacement is therefore orchestrated by the
Expand Down
39 changes: 17 additions & 22 deletions docs/20-product-tdd/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,12 @@ replacement comment.

## Provider and Transport Unknown

Connection loss is not a provider terminal. While a turn outcome is unknown,
Braid does not start a parallel turn, apply a terminal reaction, or retry Agent
side effects. It reconnects/resumes the same physical session when compatible;
if the provider proves it unavailable, the group becomes `blocked` and Braid
updates Operational Status. Context replacement may create a fresh session
only after the old turn is terminal or fenced so its later output is ignored.
连接丢失只证明旧执行结果未知,不能据此生成成功/失败 reaction。Core 将旧 turn
记为 Unknown 并 fence 旧会话,发布 Operational Status;随后物化完整当前 Context,
保留 assignment 和 worktree,将已接收输入重新送入调度。它不会盲目重发未知结果的
provider RPC,也不承诺任意外部副作用 exactly-once。历史 Unknown 记录不因恢复成功消失。
若断连发生在已经 interrupting 的 Context reset 中,Unknown 同样使 reset 进入
materializing,继续既有 continuation 规则,不能让 reset 永久等待。

## AgentSession Event Stream

Expand All @@ -166,13 +166,12 @@ Delivery semantics are part of the contract:
- **No subscription-timing gap.** The dispatcher subscribes *before* sending
and hands the receiver to the drive loop inside `RunningAgentTurn`; the
consumer never re-subscribes mid-turn.
- **Connection death is connection-scoped**, observed through
`AgentProvider::closed()` — a future, not a channel — so it cannot be lost
while idle. The worker marks any in-flight turn `unknown` and starts a new
epoch.
- **Cross-epoch backstop.** On every (re)connect, resume fencing marks
orphaned `starting`/`running` turns `unknown`. If in-epoch delivery ever
failed, the store still converges at the next epoch boundary.
- **失效范围由 adapter 决定。** `AgentProvider::closed()` 留在 adapter 内部。
Core 使用句柄的 latched `is_unavailable()`,包括 idle 与晚订阅情形。
一个句柄失效不触发全局 epoch,也不重建健康的同类会话。
- **持久化恢复兜底。** 恢复缺失/失效句柄之前,Group 将遗留的 starting/running
turn 记为 Unknown。只向具备可用句柄的 opaque session ID 领取 runnable turn;
不可用会话的输入保留待处理,不占住其他可用会话。

Responsibilities do not overlap:

Expand All @@ -185,12 +184,8 @@ Responsibilities do not overlap:
the control-plane sibling of steering — an immediate operation on the
observed in-flight turn that carries termination rather than input; the
terminal still arrives via the event stream.
- The **group layer** (`SessionManager`) owns the physical session lifecycle
for one connection epoch: start/resume keyed by the adapter-created thread
id, rebuilt from the durable store on every reconnect. There is no in-place
replacement; context replacement fences the old turn in the store and then
starts a fresh session with the materialized context.
- The **adapter** (`ProviderAgentSession`) owns the mechanism only: mapping
the contract onto `AgentProvider` RPCs and translating provider
notifications into exactly-once `SessionEvent`s. It holds no durable state
and makes no scheduling decisions.
- **Group** 决定 Work Item 的物化、恢复、Context replacement、睡眠和退役,
`SessionManager` 只索引中立句柄。替换完整 Context 不等于创建新的逻辑 Agent;
adapter 的物理 ID 是可替换的执行绑定。
- **Adapter** 管理进程、连接、物理会话和通知翻译,不读取 GitHub、不操作业务 store。
Group 释放旧句柄时,adapter 清理其资源;Codex 共享连接和 Pi 独立进程都服从同一契约。
Loading
Loading