Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d0ba785
fix(sessions): reliably deliver startup user messages via event queue
jh0904 Jul 30, 2026
2151cf7
test(sessions): keep core startup delivery coverage only
jh0904 Jul 30, 2026
4029d81
refactor(sessions): simplify startup queue handoff after review
jh0904 Jul 30, 2026
112e515
fix(sessions): only queue startup messages for cloud environments
jh0904 Jul 30, 2026
6c573f5
fix(db): use stable tenant UUIDs for startup queue
jh0904 Jul 31, 2026
73b2d10
fix(sessions): reinject session history when activating code sessions
jh0904 Jul 31, 2026
7eef483
refactor(db): rename session startup window helper
arthur-zhang Jul 31, 2026
c79cbf5
refactor(db): simplify session event queue existence check
arthur-zhang Jul 31, 2026
2c06800
refactor(db): simplify listSessionEventQueueIdentityRows
arthur-zhang Jul 31, 2026
ee3eee6
refactor(db): simplify ListSessionEventQueueItems event lookup
arthur-zhang Jul 31, 2026
e05b3c7
refactor(db): simplify delete session event queue query
arthur-zhang Jul 31, 2026
f55b5ab
Merge origin/main into codex/fix-session-event-reliable-delivery
jh0904 Jul 31, 2026
88033f5
fix(db): renumber session event queue migration after main UUID series
jh0904 Jul 31, 2026
3609ca3
revert: drop unrelated merge fixes from startup-delivery branch
jh0904 Jul 31, 2026
c5a63a4
refactor(db): simplify startup queue SQL and drop cloud filter docs
jh0904 Jul 31, 2026
1ccc6fc
Merge remote-tracking branch 'origin/main' into codex/fix-session-eve…
jh0904 Jul 31, 2026
080310a
refactor(sessions): move activation tx orchestration out of DB
jh0904 Jul 31, 2026
56afac5
refactor(sessions): clarify startup queue delivery and activation han…
jh0904 Aug 1, 2026
3fa3cbe
fix(sessions): atomically replay activation history
jh0904 Aug 1, 2026
c8c0ed0
Migrate session activation SQL to generated yourbatis mappers
arthur-zhang Aug 3, 2026
5ea177c
refactor(db): migrate single code-session event append to yourbatis
jh0904 Aug 3, 2026
d3e211a
fix(ci): generate yourbatis mappers before Go typecheck
jh0904 Aug 3, 2026
f65c999
merge origin/main into session startup delivery branch
jh0904 Aug 3, 2026
c7c4c90
merge origin/main into session startup delivery branch
jh0904 Aug 4, 2026
8819d86
fix: reliably deliver startup session events
jh0904 Aug 4, 2026
4d76cdb
refactor(db): clarify code-session append naming and comments
jh0904 Aug 4, 2026
2b1bdee
fix(sessions): harden activation replay
jh0904 Aug 4, 2026
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: 3 additions & 1 deletion .github/workflows/complexity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- '.golangci-complexity.yml'
- 'go.mod'
- 'go.sum'
- 'scripts/generate-go.sh'
- 'scripts/go-complexity.sh'
- 'web/**'
- '.github/workflows/complexity.yml'
Expand All @@ -20,6 +21,7 @@ on:
- '.golangci-complexity.yml'
- 'go.mod'
- 'go.sum'
- 'scripts/generate-go.sh'
- 'scripts/go-complexity.sh'
- 'web/**'
- '.github/workflows/complexity.yml'
Expand Down Expand Up @@ -47,7 +49,7 @@ jobs:
cache: true

- name: Generate Go sources
run: go generate ./...
run: ./scripts/generate-go.sh

- name: Check Go complexity
uses: golangci/golangci-lint-action@v9
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/dead-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- '.golangci-dead-code.yml'
- 'go.mod'
- 'go.sum'
- 'scripts/generate-go.sh'
- 'scripts/go-dead-code.sh'
- '.github/workflows/dead-code.yml'
push:
Expand All @@ -19,6 +20,7 @@ on:
- '.golangci-dead-code.yml'
- 'go.mod'
- 'go.sum'
- 'scripts/generate-go.sh'
- 'scripts/go-dead-code.sh'
- '.github/workflows/dead-code.yml'

Expand All @@ -45,7 +47,7 @@ jobs:
cache: true

- name: Generate Go sources
run: go generate ./...
run: ./scripts/generate-go.sh

- name: Detect unreachable Go declarations
uses: golangci/golangci-lint-action@v9
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
cache: true

- name: Generate Go sources
run: go generate ./...
run: ./scripts/generate-go.sh

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
Expand Down
14 changes: 7 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ repos:
hooks:
- id: go-generate
name: Generate ignored Go sources
entry: go generate ./...
language: system
files: ^(?:.*\.go|internal/db/.*_mapper\.xml|go\.(?:mod|sum))$
entry: scripts/generate-go.sh
language: script
files: ^(?:.*\.go|internal/db/.*_mapper\.xml|go\.(?:mod|sum)|scripts/generate-go\.sh)$
pass_filenames: false
require_serial: true

Expand All @@ -34,25 +34,25 @@ repos:
types: [go]

- id: golangci-lint
name: Lint packages containing staged Go files
name: Lint packages containing staged Go inputs
entry: scripts/pre-commit-go-lint.sh
language: script
types: [go]
files: ^(?:.*\.go|internal/db/.*_mapper\.xml|go\.(?:mod|sum)|scripts/generate-go\.sh)$
require_serial: true

- id: go-dead-code
name: Detect unreachable Go declarations
entry: scripts/go-dead-code.sh
language: script
files: ^(?:.*\.go|\.golangci-dead-code\.yml|go\.(?:mod|sum)|scripts/go-dead-code\.sh)$
files: ^(?:.*\.go|internal/db/.*_mapper\.xml|\.golangci-dead-code\.yml|go\.(?:mod|sum)|scripts/(?:generate-go|go-dead-code)\.sh)$
pass_filenames: false
require_serial: true

- id: go-complexity
name: Enforce Go cyclomatic complexity
entry: scripts/go-complexity.sh
language: script
files: ^(?:.*\.go|\.golangci-complexity\.yml|go\.(?:mod|sum)|scripts/go-complexity\.sh)$
files: ^(?:.*\.go|internal/db/.*_mapper\.xml|\.golangci-complexity\.yml|go\.(?:mod|sum)|scripts/(?:generate-go|go-complexity)\.sh)$
pass_filenames: false
require_serial: true

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
## 测试要求

- 测试组织顺序应先写失败场景,再写成功场景。
- `*.gen.go` 不纳入版本控制;干净 checkout 在直接运行 Go 编译、测试或静态分析前先执行 `go generate ./...`。仓库标准 `just` 命令会自动完成生成。
- `*.gen.go` 不纳入版本控制;干净 checkout 在直接运行 Go 编译、测试或静态分析前先执行 `./scripts/generate-go.sh`(内部为 `go generate ./internal/db`)。仓库标准 `just` 命令会自动完成生成。
- 修改 `web/` 下的文件后,运行 `just web-format-check`,确保 Prettier 格式门禁通过。
- 修改 Go 代码后,运行 `just lint`;该命令使用仓库根目录的 `.golangci.yml` 执行与 CI 相同的静态分析和格式检查。
- 修改 schema 或 handler 后,运行 `just test`(等价于先生成 Go 源码,再运行 `go test ./... -count=1`)。
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go generate ./...
RUN ./scripts/generate-go.sh
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /oma-server .

# ---- 前端构建 (Bun) ---------------------------------------------------------
Expand Down
8 changes: 8 additions & 0 deletions docs/design/be/ccrv2/ccr-v2-epoch-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,14 @@ update code_sessions sequence_num
commit()
```

该事务由共享的 `yourbatis.DB.Transaction` 创建;回调中的同一个事务 `Executor`
同时构造 `CodeSessionMapper`、`CodeSessionInboundEventMapper` 和
`CodeSessionOutboundEventMapper`。`CodeSessionMapper.LockCodeSessionByExternalID`
负责日常 append 的行锁,不附加 `initializing` 状态限制;启动激活仍使用独立的
`LockInitializingCodeSession`。方向对应的 event mapper 在锁内完成幂等单条查询和
`INSERT ... RETURNING`,随后由 `CodeSessionMapper` 更新对应方向的 sequence。
激活使用的 inbound 批量 insert 保持独立,不经过单条 append。

这样线性化语义才成立:

- 如果旧 worker 先拿到锁并写完,再发生 register,则这次写入发生在抢占之前,可以接受。
Expand Down
86 changes: 86 additions & 0 deletions docs/design/be/session-startup-message-delivery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Session 启动期消息可靠投递

## 问题

Runner 过去在 prepare 阶段读取一次 `session_events` 快照,随后才创建 sandbox 和 Code
Session。快照之后、Code Session 创建之前发送的消息虽然已经写入 `session_events`,但不会
进入 runtime 消费的 `code_session_inbound_events`。

## 设计

`session_events` 是启动输入的唯一事实源,不增加临时 queue、watermark 或公开状态。

Send Events 与 Code Session activation 都先锁同一条 Session 行:

- Send 通过 `DB.AppendSessionEvents` 锁 Session 并提交公开事件;
- activation 锁 Session 后读取完整公开历史,在同一事务中写 inbound 并将 Code Session 从
`initializing` 切为 `active`。

因此只可能有两种顺序:

1. Send 先提交,activation 随后读取到该事件并写入 inbound;
2. activation 先提交,Send 随后由现有 active realtime 路径投递当前 batch。

```mermaid
sequenceDiagram
participant Client
participant Send as Send transaction
participant Session as sessions row
participant Events as session_events
participant Activate as Activation transaction
participant CS as code_sessions
participant Inbound as code_session_inbound_events

alt Send 先锁 Session
Send->>Session: SELECT FOR UPDATE
Activate->>Session: wait
Send->>Events: INSERT current batch
Send->>Send: COMMIT
Activate->>Session: acquire lock
Activate->>Events: read complete history
Activate->>Inbound: append forwardable events
Activate->>CS: initializing → active
Activate->>Activate: COMMIT
else Activate 先锁 Session
Activate->>Session: SELECT FOR UPDATE
Send->>Session: wait
Activate->>Events: read complete history
Activate->>Inbound: append forwardable events
Activate->>CS: initializing → active
Activate->>Activate: COMMIT
Send->>Session: acquire lock
Send->>Events: INSERT current batch
Send->>Inbound: active realtime delivery
end
```

## 激活流程

`Service.CreateManagedAgentCodeSession`:

1. 创建 `initializing` Code Session;
2. 写入 `initialize` inbound;
3. 调用 `ActivateManagedAgentCodeSession`;
4. activation 事务锁定 Session 和 initializing Code Session;
5. 按 `created_at ASC, id ASC` 读取完整 `session_events`;
6. 过滤并转换可转发事件,幂等写入 inbound;
7. 将 Code Session 切为 `active` 并提交。

任一历史转换、inbound 写入或状态更新失败时,activation 事务整体回滚,Code Session 保持
`initializing`。Deployment initial events 已经属于 `session_events`,无需单独交接路径。

## Realtime cutover

Send 提交公开事件后始终调用 `Service.QueuePublicSessionEvents`。该方法重新读取最新 Code
Session,只有 `status == active` 时才写 inbound;不存在或仍为 `initializing` 时直接返回。

如果 activation 恰好在公开事件提交后、realtime 检查前完成,同一事件可能同时出现在
activation 历史和 realtime 尝试中;现有 inbound idempotency key 会保留一份,不会重复投递。

## 验收

- Runner prepare 后、Code Session 创建前接受的消息最终进入 inbound;
- 启动期接受多条用户消息,activation 按公开历史顺序全部重放;
- activation 失败时不留下部分 inbound,也不切换为 active;
- activation 后的新 batch 只通过 realtime 路径追加;
- Deployment initial user messages 在 `initialize` 后按输入顺序进入 inbound。
7 changes: 4 additions & 3 deletions docs/design/be/yourbatis-admin-api-keys-prototype.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ Yourbatis runtime 与生成器均由 `go.mod` 固定到已发布的
后续升级只需更新 `go.mod` 中的模块版本,运行时和生成器会保持一致。

生成的 `*.gen.go` 不纳入版本控制,由 `.gitignore` 排除。干净 checkout 必须先执行
`go generate ./...`;`just server`、`just test`、Go lint/死代码/复杂度门禁、pre-commit、
GitHub Actions 和 Docker 构建都在消费生成代码前自动执行该命令。XML 和生成器版本因此成为唯一
受版本控制的 Mapper 代码来源,避免生成输出与声明发生漂移。
`./scripts/generate-go.sh`(内部为 `go generate ./internal/db`);`just server`、`just test`、Go
lint/死代码/复杂度门禁、pre-commit、GitHub Actions 和 Docker 构建都在消费生成代码前自动
执行该入口。XML 和生成器版本因此成为唯一受版本控制的 Mapper 代码来源,避免生成输出与声明
发生漂移。

## 验证

Expand Down
3 changes: 2 additions & 1 deletion docs/design/development-quality-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

## 配置与执行

- yourbatis 根据 `internal/db/*.xml` 生成的 `*.sqlmap.gen.go` 不提交;`scripts/generate-go.sh` 是统一生成入口。Go lint、死代码、复杂度、测试、开发启动和 Docker 构建在类型检查或编译前调用该入口,避免本地残留的 ignored 文件掩盖干净 checkout 中的缺失生成代码。
- `.golangci.yml` 是常规 Go lint 规则来源;复杂度和死代码等需要不同扫描范围的专项门禁使用独立的固定配置。
- `just lint` 在本地对所有 Go package(包括测试)运行相同配置。
- `.golangci-dead-code.yml` 单独启用 golangci-lint 的 `unused` 分析器并覆盖测试代码;`just dead-code` 通过 `scripts/go-dead-code.sh` 枚举当前 Go module 的仓库 package,避免本地前端依赖中的第三方 Go 示例污染结果。
Expand All @@ -36,5 +37,5 @@ just large-files
just lint
just dead-code
just duplicates
go test ./... -count=1
just test
```
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,3 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect
google.golang.org/grpc v1.80.0 // indirect
)

replace ybatis => /Users/arthur/GolandProjects/ybatis
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/superduck-ai/e2b-go-sdk v0.0.1 h1:yW23X/fEQvaPdJHhDnp8PgnsaIixn+UTVMt94X4Kr0s=
github.com/superduck-ai/e2b-go-sdk v0.0.1/go.mod h1:dWlhv18vJamYp5gQ3ktmjjrX7tUaMe2OyTJXUo8EeXY=
github.com/superduck-ai/yourbatis v0.1.0 h1:U49je+0PhoR6PVJKfR3mbNo31rr+2SzkWwBi28wndwE=
github.com/superduck-ai/yourbatis v0.1.0/go.mod h1:BlCyyT1yfU2Zxya89rDf89keXqsdcwP6Q3PFscIjvig=
github.com/superduck-ai/yourbatis v0.1.1 h1:iAEi8Hrx+p6MjS8ciI2kL8ueQ7FOrFDEphnN1NP3GSc=
github.com/superduck-ai/yourbatis v0.1.1/go.mod h1:BlCyyT1yfU2Zxya89rDf89keXqsdcwP6Q3PFscIjvig=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
Expand Down
75 changes: 72 additions & 3 deletions internal/codesessions/managed_agent_code_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ type ManagedAgentCreateInput struct {
PermissionMode string
DangerouslySkipPermissions bool
Config json.RawMessage
InitialEvents []json.RawMessage
}

// ManagedAgentCreateResult 只在创建链路内短暂携带两份明文凭证,调用方应立即交给
Expand Down Expand Up @@ -66,7 +65,7 @@ func (s *Service) CreateManagedAgentCodeSession(ctx context.Context, input Manag
WorkDir: strings.TrimSpace(input.WorkDir),
PermissionMode: strings.TrimSpace(input.PermissionMode),
Model: strings.TrimSpace(input.Model),
Status: "active",
Status: "initializing",
Comment thread
jh0904 marked this conversation as resolved.
Metadata: metadata,
// OAuth-compatible token 只落 SHA-256 hash;明文仅存在于当前返回值中。
OAuthAccessTokenHash: auth.HashAPIKey(oauthAccessToken),
Expand Down Expand Up @@ -99,7 +98,7 @@ func (s *Service) CreateManagedAgentCodeSession(ctx context.Context, input Manag
if err := s.queueInitialize(ctx, record, input.Config, now); err != nil {
return ManagedAgentCreateResult{}, err
}
if err := s.queueInitialPublicSessionEvents(ctx, record, input.InitialEvents, now); err != nil {
if err := s.ActivateManagedAgentCodeSession(ctx, record); err != nil {
return ManagedAgentCreateResult{}, err
}
credentialContext, err := s.db.GetCodeSessionCredentialContextForIssue(
Expand All @@ -126,6 +125,76 @@ func (s *Service) CreateManagedAgentCodeSession(ctx context.Context, input Manag
}, nil
}

// ActivateManagedAgentCodeSession locks the owning Session, replays complete
// public history in stable order, and activates the Code Session atomically.
func (s *Service) ActivateManagedAgentCodeSession(
ctx context.Context,
codeSession db.CodeSession,
) error {
if s == nil || s.db == nil {
return db.ErrNotFound
}
return s.db.WithManagedAgentActivationTx(ctx, func(tx db.ManagedAgentActivationTx) error {
// lock session by session external id
lockedSession, err := tx.LockSessionForEvents(
ctx,
codeSession.WorkspaceUUID,
codeSession.SessionExternalID,
)
if err != nil {
return err
}
// lock code_session by code session id
lockedCodeSession, err := tx.LockInitializingCodeSession(
ctx,
codeSession.WorkspaceUUID,
codeSession.UUID,
)
if err != nil {
return err
}
sessionEvents, err := tx.ListSessionEventsForActivation(ctx, lockedSession)
if err != nil {
return err
}
inboundInputs := make([]db.AppendCodeSessionEventInput, 0, len(sessionEvents))
for _, event := range sessionEvents {
if !shouldForwardPublicEventToWorker(event.EventType) {
continue
}
inbound, err := s.convertSessionEventToInbound(lockedCodeSession.ExternalID, event)
if err != nil {
return err
}
inboundInputs = append(inboundInputs, inbound)
}
if err := tx.AppendCodeSessionInboundEvents(ctx, lockedCodeSession, inboundInputs); err != nil {
return err
}
activated, err := tx.ActivateCodeSession(ctx, lockedCodeSession.UUID, time.Now().UTC())
if err != nil {
return err
}
if !activated {
return db.ErrInvalidState
}
return nil
})
}

// convertSessionEventToInbound maps one public session event payload into a
// Code Session inbound append input.
func (s *Service) convertSessionEventToInbound(
codeSessionID string,
event db.SessionEvent,
) (db.AppendCodeSessionEventInput, error) {
payload, err := workerPayloadForPublicEvent(codeSessionID, event.Payload, event.ProcessedAt)
if err != nil {
return db.AppendCodeSessionEventInput{}, err
}
return newInboundEventInput(codeSessionID, payload, "public-session")
}

// TerminateManagedAgentCodeSession revokes a Code Session created for a
// sandbox launch that failed before the runtime became usable.
func (s *Service) TerminateManagedAgentCodeSession(
Expand Down
Loading
Loading