apache / apache/maka

RFC: Radically slim the Session Task Ledger

Open
#2,290 14 comments 0 reactions 1 assignee Claimed by @somewan820 View on GitHub
enhancement
Dominant language
TypeScript
Stars
5.4k
Forks
502
Avg merge
1d 2h
Merged PRs (30d)
715

Description

# RFC: Radically slim the Session Task Ledger

Status: Draft. Scope: task-ledger in `core` / `storage` / `runtime-host` / `runtime` + desktop panel + `docs/session-task-ledger-lifecycle.md`.

## TL;DR

The ledger exists for one reason: **the model's work plan lives in a context that is volatile, private, bounded, and expensive — so it must live outside the conversation, in a persistent, authority-owned, session-scoped state re-injected each turn at bounded cost.** That goal needs only three things: a persisted task list, `task_create`/`task_update`, and turn-tail injection. In one month the feature grew from 5 fields / 2 tools / one JSON file to 13 fields / 4 tools / event-sourced dual tables + a paginated query protocol. Proposal: **cut every element whose removal loses nothing irreversible** (code is in git, data stays in place), and keep an explicit restore-on-signal list for what we expect to miss.

## 1. First principles

Volatile/private/bounded context → plan must be external → session-scoped with stable identity → model needs create/update → transitions validated by a system authority → injection has a hard budget, scale derived from it → writes atomic.

**Not derivable from this goal:** event sourcing, task trees, subagent ownership, audit, time-based archiving, pagination, revisions, a 5-level trust taxonomy, free-text evidence. Each must justify itself independently.

## 2. Diagnosis (all verified on current `main`)

- **F1 — `resumeTrust` is dead.** Only production caller classifies with empty refs: 5 levels, 3 reachable; `untrusted` never occurs, so `filterModelVisibleTaskLedgerTasks` is the identity function. A corrupt ledger throws on read rather than being classified.
- **F2 — the "Goal trust gate" is a status gate.** `listActionableTaskKeys` filters on `pending`/`in_progress` only. The belief that trust had a consumer is why it survived.
- **F3 — the projections table is written, never read.** Every read replays all events; the write path commits events and projection in two transactions and signals before the projection is written.
- **F4 — `claimAvailable` is dead code** through 4 layers, zero callers.
- **F5 — `omittedCount` misreports**: counts policy-excluded terminal tasks as "omitted", so every turn the model is told "N omitted — use task_list" for tasks that weren't budget-omitted at all.
- **F6 — one rule, six authorities.** "Completion requires evidence" is enforced in zod, normalize, validate, replay, read-validation, and wire projection. This duplication, more than any field, explains 1092 lines.

## 3. The criterion

Code is reversible (git). Data is reversible (no DROP). Model contract is reversible (models re-adapt). **The only irreversible act is deleting user data.** Therefore: cut everything, keep old tables in place read-only.

## 4. Verdict

### Fields (13 → 3)

| Field | Verdict | Why |
|---|---|---|
| `id` | **keep** (uuid → compact random id) | foundation |
| `key` | **cut (merge into id)** | dual identity + dual allocation paths; key is just id's display form |
| `subject` | **keep** | the payload |
| `status` | **6 → 3** (pending/in_progress/completed) | blocked/failed/cancelled are execution details; top restore candidate |
| `createdAt` / `updatedAt` | **cut** | insertion order suffices |
| `endedAt` | **cut** | with archiving |
| `parentId` | **cut** | ~150 lines of core for a hierarchy the original design said was flat; the one real invariant (parent-before-child) can return standalone |
| `owner` | **cut** | subagent binding belongs to AgentRun, which has no such field — the product never asked for it |
| `blockedReason` / `failureReason` / `completionEvidence` | **cut** | model-authored text is a claim, not evidence (real evidence lives in tool results/files/git/tests) |
| `resumeTrust` | **cut** | F1 |

### Mechanisms

| Mechanism | Verdict | Why |
|---|---|---|
| Event log + projection table | **cut → one normalized current-state table** | F3; copy becomes a snapshot; audit belongs to RuntimeEvent |
| Key allocation / backfill | cut | identity merged |
| 7-day archive | cut | cap never freed terminal slots; renderer selector already governs budget |
| Pagination / cursor / `revision_changed` / revision | cut | ≤2 pages under 200 cap, server does full list + slice anyway, Desktop never uses it |
| `claim` / `settleAgentOutcome` | cut | real regression (production callers exist) but reversible; second goal deserves its own system |
| `claimAvailable` | cut | F4 |
| Transition whitelist + `explicitReopen` + evidence rules | cut | validate enum membership only; system doesn't endorse self-reports either way |
| `task_list` / `task_get` | cut | cap drops to 20 active + 3 terminal, full ledger always fits the tail — "tail makes list redundant" becomes true again |
| Legacy aliases | cut | one-month-old interface |
| 200-task cap | tighten to 20+3 | derived from the 8000-char budget (open question 5) |
| Paginated `task.ledger.query` | simplify to one full-read query | shared by Desktop + TUI |
| turn-tail injection | **keep** | the feature itself |
| Redaction / tag stripping | **keep** | security boundary |
| Host single-writer + admission gate | **keep** | this *is* what authority means, not complexity |
| Desktop panel | keep, simplify fields | user-visible |

Tools: 4 → 2 (`task_create`, `task_update`).

## 5. Target shape

```ts
interface Task { id: string; subject: string; status: 'pending'|'in_progress'|'completed'; }
// session_tasks(session_id, task_id, subject, status, seq)
// tail each turn: 20 active + 3 recent terminal, ≤8000 chars, stable tail truncation
// old tables kept in place, no longer written
```

## 6. Restore list (what we expect to add back, and the signal)

| Cut element | Signal | Cost |
|---|---|---|
| `blocked` | models keep writing "waiting for user" into subjects | 1 enum + 1 render line — most likely |
| `completionEvidence` | users: "says done, no trace why" | restore as typed refs (runId/toolCallId), not text |
| `claim`/`settle` | the day AgentRun grows a `taskId` field | design as its own system |
| Event log | conversation copy needs precise turn history | zero (tables kept) |
| Revision | TUI stale-response bugs | one integer |

## 7. Migration

No DROP. Import existing tasks (id/subject/status) into the new table. Update `operational-state-backup.ts` manifest. Panel + lifecycle doc update in the same PRs.

## 8. Rollout

- **P0 — delete dead code, zero behavior change**: F1/F4/F5 fixes, pagination, `task_get`, legacy aliases, archive, `endedAt`. ~20% of surface, independently reviewable.
- **P1 — field convergence**: 3 statuses, id/key merge, cut parentId + evidence + owner.
- **P2 — storage simplification**: single table, copy → snapshot, backup manifest.
- **P3 — scale/tools**: cap 20+3, cut `task_list`, unpaginated query.

Each phase = one PR, own verification. Two independent external architecture reviews (one radical, one conservative) were run on this exact question; both confirmed over-design and verified F1–F6. Their conservative positions became §6.

## 9. Open questions

1. Cut `blocked`? (lean: yes — top of restore list, but the signal may come fast)
2. Accept losing subagent task-binding for now?
3. Is `failed` truly redundant once subagent outcomes stop auto-writing?
4. `completionEvidence`: cut entirely, or keep as typed references?
5. Is 20+3 the right cap? Derive from the 8000-char budget with real renders before merging.
6. Cap change and `task_list` removal must land atomically (one decision)?

简体中文要点版

# RFC:Session Task Ledger 激进瘦身

状态:Draft。范围:core/storage/runtime-host/runtime 的 task-ledger + desktop 面板 + 生命周期文档。

## TL;DR

这个特性存在的唯一理由:**模型的工作计划在易失、私有、昂贵的上下文里,必须外置成持久、权威、每轮受限预算注入的 session 级状态。** 这个目标只需要三样东西:持久化任务列表、`task_create`/`task_update`、turn-tail 注入。一个月里它从 5 字段/2 工具/单文件 JSON 长成 13 字段/4 工具/事件溯源双表+分页协议。提议:**凡是砍掉后没有不可逆损失的(代码在 git 里、数据原地保留)全部砍掉**,并维护"按信号加回"清单。

## 实测诊断(都在 main 上验证过)

- **F1 `resumeTrust` 是死的**:唯一调用点传空 refs,5 档只有 3 档可达,`untrusted` 永不产生,过滤函数是恒等函数;账本坏了是读不出来而不是被分类。
- **F2 "Goal 信任门"实际是状态门**:只按 pending/in_progress 过滤,trust 不参与。
- **F3 投影表只写不读**:每次读都全量重放事件;写入还是两个事务、signal 先于投影。
- **F4 `claimAvailable` 死代码**:穿透 4 层,零调用。
- **F5 `omittedCount` 恒定误报**:把策略性不选的终态也算省略,每轮怂恿模型调 task_list。
- **F6 一条规则六个权威**:"完成必须带证据"写在 6 处——这比任何字段都更能解释 1092 行。

## 判据

代码可逆(git)、数据可逆(不 DROP)、模型契约可逆。**唯一不可逆的是删用户数据**——所以全砍,旧表原地保留只读。

## 裁决

**字段 13→3**:保留 `id`(uuid 换紧凑随机)、`subject`、`status` 3 态。砍:`key`(与 id 合一)、`createdAt/updatedAt/endedAt`、`parentId`(~150 行逻辑买一个本应是扁平的设计)、`owner`(AgentRun 都没有这字段)、三个证据字段(模型写的文本是声明不是证据)、`resumeTrust`(F1)。

**机制**:砍事件日志+投影表(换一张规范化当前态表)、key 分配/backfill、7 天归档、分页/revision、`claim`/`settle`/`claimAvailable`、迁移白名单+证据规则、`task_list`/`task_get`、legacy 别名;200 上限收紧到 20+3。保留:tail 注入(特性本身)、脱敏(安全)、host 单一权威(权威的真正内容)、Desktop 面板(字段简化)。

**工具 4→2**:`task_create`、`task_update`。

## 目标形态

```ts
interface Task { id: string; subject: string; status: 'pending'|'in_progress'|'completed'; }
// session_tasks(session_id, task_id, subject, status, seq)
// 每轮 tail:20 非终态 + 3 终态,≤8000 字符;旧表保留不再写
```

## 加回清单(按信号)

| 元素 | 信号 | 成本 |
|---|---|---|
| `blocked` | 模型老把"等用户"写进 subject | 1 枚举+1 行渲染,最可能 |
| `completionEvidence` | 用户抱怨"说做完了没痕迹" | 以强类型引用回归,非文本 |
| `claim`/`settle` | AgentRun 长出 taskId 那天 | 作为独立系统 |
| 事件日志 | 会话分叉要精确历史 | 零(表没删) |
| revision | TUI 迟到响应 bug | 一个整数 |

## 落地

P0 删死代码零行为变化(F1/F4/F5、分页、task_get、别名、归档、endedAt)→ P1 字段收敛 → P2 存储简化(单表、copy 改快照)→ P3 规模/工具(20+3、砍 task_list、无分页查询)。每阶段独立 PR。

做过两次独立外部架构评审(一激进一保守),都确认过度设计并独立验证了 F1–F6;保守方的意见变成了加回清单。

## 开放问题

1. 砍 `blocked`?(倾向砍,但它是加回清单榜首)
2. 暂时接受失去子代理任务绑定?
3. 子代理不再自动写回后 `failed` 还必要吗?
4. `completionEvidence` 彻底砍还是留强类型引用?
5. 20+3 是正确上限吗(用真实渲染从 8000 字符反推验证)?
6. 上限调整与砍 `task_list` 必须原子落地?

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.