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
71 changes: 71 additions & 0 deletions compat/documentation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,77 @@ func TestPublicDocsUseOutcomeReportingSemantics(t *testing.T) {
}
}

func TestStateClosureDocumentationContract(t *testing.T) {
required := map[string][]string{
"../docs/protocol-v1.md": {
"Actor Goals | 32",
"Actor detailed Memories | 128",
"Actor Beliefs / BeliefSets | 256",
"Recall counts saturate at 1,000,000",
"Retained Proposal and Arbitration tick fields are not upper-bounded",
"Imported historical Receipt revisions become",
"provide a permanent Event ID index",
},
"../docs/protocol-v1.zh-CN.md": {
"Actor Goals | 32",
"Actor 详细 Memories | 128",
"Actor Beliefs / BeliefSets | 256",
"RecallCount 在 1,000,000 饱和",
"State 中保留的 Proposal 与 Arbitration tick 不受",
"导入的历史 Receipt revision 会在",
"尚未提供超出这些投影的永久 Event ID",
},
"../docs/architecture.md": {
"reducer or candidate-validation failure",
"Policy calls receive isolated copies",
"Receipt revisions are set to zero",
},
"../docs/architecture.zh-CN.md": {
"reducer 或候选校验失败",
"Policy 调用收到 State、Actor 和请求的隔离副本",
"历史 Receipt revision 设为 0",
},
}
for path, fragments := range required {
payload, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
for _, fragment := range fragments {
if !strings.Contains(string(payload), fragment) {
t.Errorf("%s is missing state-closure rule %q", path, fragment)
}
}
}

prohibited := map[string][]string{
"../docs/architecture.md": {
"A failed transition therefore leaves both the event log",
"Receipt revision metadata is rebased",
},
"../docs/architecture.zh-CN.md": {
"失败的转换既不会改变事件日志",
},
"../docs/protocol-v1.md": {
"persistent idempotency index described in the migration roadmap",
},
"../docs/protocol-v1.zh-CN.md": {
"迁移路线中的持久幂等索引",
},
}
for path, fragments := range prohibited {
payload, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
for _, fragment := range fragments {
if strings.Contains(string(payload), fragment) {
t.Errorf("%s retains obsolete state-closure wording %q", path, fragment)
}
}
}
}

func TestPublicDocumentationLanguage(t *testing.T) {
required := map[string]string{
"../README.en.md": "> Game-native agent runtime.",
Expand Down
27 changes: 26 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@ produces a complete, verifiable snapshot without writing to the store.
`rin inspect` reuses both paths for machine-readable diagnostics; opening a
data directory still verifies the entire event hash chain.

### Mutation and state closure

Every event is first applied to an isolated candidate state. The reducer then
validates the complete `SessionState`, including feature-gated fields,
capacities, revision and tick bounds, actor references, and paired belief
projections. Only a valid candidate may be appended to the Store and published
as the live state. A reducer or candidate-validation failure therefore leaves
both the event log and the in-memory session unchanged. Store write failures
use the separate append-confirmation and reconciliation rules described by the
outcome protocol.

Policy calls receive isolated copies of the State, Actor, and request. Policy
code may inspect or mutate those values locally, but it cannot mutate the live
session outside an event. Runtime-owned collections also close their
references when bounded retention runs: memory compaction rewrites recalled
IDs to the replacement Summary, non-archive eviction removes those references,
and Belief/BeliefSet eviction is deterministic and paired.

### Store

File-store layout:
Expand Down Expand Up @@ -206,7 +224,9 @@ only scheduling time, never boundaries or the action allowlist.
## Save and rollback

- Game saves should store snapshots returned by Rin, not internal file paths.
- A snapshot carries the content-pack binding and state hash.
- A snapshot carries the content-pack binding and state hash. Rin validates a
cloned State before hashing or saving it, so every successfully returned
snapshot passes the same structural validation used by Restore.
- With `outcome-reporting-v1`, Restore retains pending proposals so a saved,
unhandled Proposal Attempt can resume, and so a game-save Outcome Outbox can
report actions already applied before the save. Restored proposals never
Expand All @@ -217,6 +237,11 @@ only scheduling time, never boundaries or the action allowlist.
proposals.
- Committed events, memories, facts, goal progress, and scheduling ticks are
restored.
- Restore starts a new local event-chain generation. Retained Proposal,
Memory, Belief, Activity, and Arbitration revision metadata is rebased to
that generation before the restored State is published. Imported historical
Receipt revisions are set to zero; the new Restore Receipt records the local
generation.
- A new data directory may import a snapshot; its local event chain then
begins with a restore event.
- When loading the same save repeatedly, callers should bind the restore
Expand Down
20 changes: 19 additions & 1 deletion docs/architecture.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ Ren'Py worker registry、Godot `HTTPRequest` 和 Unity coroutine 都只存在于

Timeline 只从事件 payload 提取 ID 和枚举状态,不返回玩家原话、剧情摘要、Commit outcome 或模型内容。Replay 则运行同一个 reducer 到指定 revision,生成完整且可验证的 Snapshot,不写回 Store。`rin inspect` 复用这两条路径输出机器可读诊断;打开数据目录时仍会验证全部事件 hash chain。

### Mutation 与状态闭包

每个事件都先应用到隔离的候选 State。Reducer 随后校验完整
`SessionState`,包括 Feature 门禁、容量、revision/tick 上界、Actor 引用和
成对的 Belief 投影;只有通过校验的候选状态才能追加到 Store 并发布为 live
State。因此 reducer 或候选校验失败既不会改变事件日志,也不会改变内存中的
Session。Store 写入失败则遵循 outcome 协议单独定义的 append 确认与对账规则。

Policy 调用收到 State、Actor 和请求的隔离副本。Policy 可以在本地读取或修改
这些值,但不能绕过事件直接改变 live Session。Runtime 的有界保留也会闭合
引用:Memory 归档会把 recalled ID 改写到替代 Summary,未启用归档时会移除
被淘汰的引用,Belief 与 BeliefSet 则按确定性顺序成对淘汰。

### 存储

文件存储结构:
Expand Down Expand Up @@ -113,14 +126,19 @@ rin-data/
## 存档与回滚

- 游戏存档应保存 Rin 返回的 Snapshot,而不是内部文件路径。
- Snapshot 带内容包 Binding 和状态哈希。
- Snapshot 带内容包 Binding 和状态哈希。Rin 在计算哈希或保存前先校验克隆的
State,因此每个成功返回的 Snapshot 都通过与 Restore 相同的结构校验。
- 启用 `outcome-reporting-v1` 后,Restore 会保留 pending Proposal,既让存档中
尚未处理的 Proposal Attempt 能恢复,也让 Outcome Outbox 能补报读档前已经
应用的动作。恢复出的 Proposal 不授权执行;游戏必须依赖持久化 Attempt 和
applied-operation marker 区分两种状态,重新校验尚未处理的动作,并且绝不
重做已经处理的动作。
- 未启用该 Feature 的 Session 保留旧版 Restore 行为并清空 Proposal。
- 已提交事件、记忆、事实、目标进度和调度 tick 会恢复。
- Restore 会开始一个新的本地事件链 generation;保留的 Proposal、Memory、
Belief、Activity 和 Arbitration revision 元数据会在发布恢复状态前重基到该
generation。导入的历史 Receipt revision 设为 0,本次 Restore Receipt 则记录
新的本地 generation。
- 新数据目录可以导入 Snapshot;此时本地事件链从一条 restore 事件开始。
- 重复载入同一存档时,调用方应让 restore request ID 同时绑定 Snapshot hash 与当前 Sidecar head,以区分网络重试和真正的再次回档。

Expand Down
55 changes: 54 additions & 1 deletion docs/protocol-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,45 @@ the entire batch without partial mutation.
Results are stably sorted by `next_think_tick` and actor ID for turn-based,
regional, and time-sliced games.

## State closure and bounded retention

Every successful mutation produces a complete State that passes the same
structural validation used by Snapshot and Restore. Reducers validate an
isolated candidate before Store append, so an invalid transition is rejected
without a partial in-memory or durable update. Dynamic references such as Fact
visibility must name actors in the Session. With `belief-conflicts-v1`,
`beliefs` and `belief_sets` have exactly the same keys and selected Fact.

Retained collections use these protocol bounds:

| Collection | Bound | Full-capacity behavior |
|---|---:|---|
| Actor Goals | 32, including distinct pending ProposedGoal reservations | Reject a new reservation; never silently drop a Goal |
| Actor detailed Memories | 128 | Archive into a Summary when `memory-archive-v1` is enabled; otherwise evict details and remove their recalled references |
| Actor Memory Summaries | 32 | Deterministically merge older summaries; level saturates at 16 |
| Actor Beliefs / BeliefSets | 256 keys | Deterministically evict the oldest projected key and its paired set |
| Actor RecentActions | 32 | Retain the latest game-occurrence outcomes |
| Session Proposals | 64 | Evict only resolved proposals; fail closed when all retained proposals are pending |
| Session Arbitrations | 32 | Retain the latest records |
| Session Receipts | 1024 | Retain the newest revision generation |

Recall counts saturate at 1,000,000. Memory compaction rewrites a Proposal or
RecentAction reference to the replacement Summary ID; non-archive eviction
removes the unavailable ID. Revisions, ticks, selected belief sources, Goal
status sources, and visibility actors retained by Memory, Summary, Belief,
Activity, Goal, and outcome metadata must remain inside the containing State.
Retained Proposal and Arbitration tick fields are not upper-bounded by
`state.tick` and may describe work ahead of it; live Propose and Arbitrate
requests still reject tick regression. `nil` and an empty Fact visibility list
are the same JSON contract value.

An `event_id` is rejected while it is discoverable from any retained Proposal,
RecentAction, Goal status, Memory, Summary, Belief claim, or observation
Receipt. The current v1 hot path does not scan the unbounded event log after
all bounded projections of that ID have been evicted; applications must still
use globally unique IDs. This retained-State implementation does not yet
provide a permanent Event ID index beyond those projections.

## Snapshot and restore

Snapshot and Session State requests use the same shape:
Expand All @@ -444,7 +483,21 @@ Restore:
```

Restore rejects snapshots with an invalid hash, different session ID, or
different binding. With `outcome-reporting-v1`, it retains pending proposals
different binding. Rin validates a cloned State before computing or saving a
Snapshot, so every successfully returned Snapshot immediately passes
`ValidateSnapshot` and can be imported into a fresh or non-exhausted matching
Session.

Restore writes a new local event-chain generation. Retained nested revision
metadata is rebased to the Restore event; a retained Proposal references the
preceding local revision and head hash. On a fresh import that base is revision
zero with an empty head hash. Imported historical Receipt revisions become
zero before the new Restore Receipt is inserted, so a full 1,024-entry map
cannot evict the operation that just succeeded. World revision advances
without wrapping; importing an already-maximal world revision keeps it
saturated, while later world mutations fail closed.

With `outcome-reporting-v1`, Restore retains pending proposals
for two durable recovery states: an unresolved Proposal Attempt received before
the game handled it, or an already-handled operation whose saved Outcome Outbox
still needs to report. A restored Proposal never authorizes execution. The game
Expand Down
49 changes: 48 additions & 1 deletion docs/protocol-v1.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,41 @@ reducer。超时或暂时错误只能使用相同 `request_id` 重报,不能

按 `next_think_tick` 和 actor ID 稳定排序,便于回合制、区域制和时间片游戏使用。

## 状态闭包与有界保留

每个成功 Mutation 都会产生通过 Snapshot 与 Restore 同一套结构校验的完整
State。Reducer 在 Store append 前校验隔离的候选状态,因此无效转换不会留下
部分内存更新或持久化更新。Fact visibility 等动态引用必须指向 Session 中的
Actor。启用 `belief-conflicts-v1` 后,`beliefs` 与 `belief_sets` 必须具有完全
相同的 key,并投影同一个 selected Fact。

保留集合遵循以下协议上限:

| 集合 | 上限 | 满容量行为 |
|---|---:|---|
| Actor Goals | 32,包含不同 pending ProposedGoal 预留 | 拒绝新预留,不静默删除 Goal |
| Actor 详细 Memories | 128 | 启用 `memory-archive-v1` 时归档为 Summary;否则淘汰明细并移除其 recalled 引用 |
| Actor Memory Summaries | 32 | 确定性合并较旧摘要,level 在 16 饱和 |
| Actor Beliefs / BeliefSets | 256 个 key | 确定性淘汰最旧投影 key 及其配对 Set |
| Actor RecentActions | 32 | 保留按游戏发生时间排序的最新 outcome |
| Session Proposals | 64 | 只淘汰 resolved Proposal;全部为 pending 时 fail closed |
| Session Arbitrations | 32 | 保留最新记录 |
| Session Receipts | 1024 | 保留最新 revision generation |

RecallCount 在 1,000,000 饱和。Memory 归档会把 Proposal 或 RecentAction 的
引用改写到替代 Summary ID;未启用归档时会移除不可用 ID。revision、tick、
selected belief source、Goal status source 与 visibility actor 等 Memory、
Summary、Belief、Activity、Goal 和 outcome 元数据都必须处在容器 State 的
有效范围内。State 中保留的 Proposal 与 Arbitration tick 不受 `state.tick`
上界限制,可以描述其后的工作;实时 Propose 与 Arbitrate 请求仍会拒绝 tick
倒退。Fact visibility 的 `null`/缺省与空数组属于相同 JSON 契约值。

只要 `event_id` 仍能从保留的 Proposal、RecentAction、Goal status、Memory、
Summary、Belief claim 或 observation Receipt 找到,就会被拒绝。当前 v1 热
路径不会在该 ID 的所有有界投影均被淘汰后扫描无界事件日志;应用仍必须生成
全局唯一 ID。当前 retained-State 实现尚未提供超出这些投影的永久 Event ID
索引。

## Snapshot 与 Restore

Snapshot 请求和 Session State 请求结构相同:
Expand All @@ -376,7 +411,19 @@ Restore:
}
```

Restore 拒绝 hash 错误、Session ID 不同或 Binding 不同的快照。启用
Restore 拒绝 hash 错误、Session ID 不同或 Binding 不同的快照。Rin 在计算或
保存 Snapshot 前校验克隆的 State,因此每个成功返回的 Snapshot 都会立即
通过 `ValidateSnapshot`,并可导入空 Session 或尚未耗尽 revision 的匹配
Session。

Restore 会写入新的本地事件链 generation。保留的嵌套 revision 元数据会重基
到 Restore 事件;保留 Proposal 引用前一个本地 revision 与 head hash。Fresh
import 的 base 是 revision 0 和空 head hash。导入的历史 Receipt revision 会在
插入本次 Restore Receipt 前改为 0,因此已经装满 1,024 项的 map 不会淘汰刚
成功的操作。World revision 只前进、不回绕;导入已经达到最大值的 world
revision 时保持饱和,后续 world mutation 则 fail closed。

启用
`outcome-reporting-v1` 时,它会为两种持久恢复状态保留 pending Proposal:
游戏处理前收到但尚未结算的 Proposal Attempt,以及动作已经处理、但存档中的
Outcome Outbox 仍待补报的 Operation。恢复 Proposal 绝不授权游戏执行它。
Expand Down
Loading
Loading