跳转到内容

Private & Derived Objects

0. 规范语言

本文中的规范关键字(MUST / SHOULD / MAY 等)按 conformance/normative-language.md 解释;仅大写形式具规范约束力。

1. 目标

本文集中定义 Arkret 协作图中的派生 / actor-private 对象:

  • Read Cursor:actor 私有的已读位置状态。
  • Notification:从 Event / Strand / Message / Relation 派生的 inbox projection。

这些对象不是 canonical truth——它们由 client / SDK 从 Event 集合本地计算;schema 仅用于 wire 表示。它们不进入协作图归约,不向其他 actor 广播持久化共享对象。

公共字段、lifecycle、reducer 总则见 common-fields.md。

2. Read Cursor

2.1 概念

read_cursor 是 actor-private 状态。它 SHOULD 存在于私有 account data 或 ephemeral sync channel 中,而不是作为公共 durable Event 高频写入。

完整 read receipt / read cursor 同步规则、ak.receipt.read 的 disclosure 选项和高频更新策略见 ../discovery/read-receipts.md。

2.2 Schema 与字段

Schema id: ak.schema.read_cursor.v1

字段必填类型约束说明
schemayesak.schema.read_cursor.v1Schema ID。
actor_idyesdid_core_id只对该 actor 生效。读取主体。
device_idyesid:deviceak:device:<uuidv7>。多设备收敛 tiebreaker。来源设备。
realm_idyesid:realmRealm。
read_scopeyesobject{kind, container_ref?, track_name?}。kind ∈ enum(realm, circle, space, strand, thread)。container_ref 必填规则:kind=realm 时 MUST 省略(范围即本对象 realm_id);kind=circle 时 MUST 是 id:circle;kind=space 时 MUST 是 id:space;kind=strand 时 MUST 是 id:strand;kind=thread 时 MUST 是 Thread 根消息的 id:message(Thread 是 root message 回复子时间线的投影选择器,不是一等协议对象,已读隔离语义见 ../discovery/read-receipts.md §5)。track_name 仅在 kind=strand 时 MAY 出现(限定到该 Strand 的某个 track 时间线,省略表示整个 Strand);其余 kind MUST 省略 track_name。Read Receipt 的 read_scope 使用 object_ref,与本字段共享同一 discriminator 族,但两者的 kind 取值集合并不相交一致:Read Cursor 支持 realm / circle / space / strand / thread,Read Receipt 另支持 view / message / morph 但不支持 circle / space。因此 SDK / 实现 MUST 按各自 schema 分别校验 read_scope.kind,MUST NOT 共用单一 enum 类型(共用会让 circle 误用于 Receipt、或 message 误用于 Cursor 等错配静默通过);以各自 schema 为字段形状权威。跨对象 kind 可引用类别的机读真相源是 id-kind-registry.json 的 referenceability:本字段使用 read_cursor_scope_kind、read_cursor_scope_ref 与 read_cursor_thread_root 类别。已读范围。
positionyesobject{event_id, hlc}。已读位置。

2.3 行为规则

  • Read Cursor 不是可原地更新的对象:它没有 revision / compare-and-set(对照 account-data.md §5 的可变 typed current result),一次「更新」就是 author 并提交一条新的 ak.read_cursor.advance。因此本对象 MUST NOT 携带 updated_at:该次更新的时间就是那条 Event 信封的 created_at,在 payload 里重述一遍只会制造第二份同源时间且不受签名约束。需要更新时间的派生视图(read_marker_outcome、跨设备 actor-private read cursor 更新)MUST 取胜出 advance 的信封 created_at。
  • 本对象没有 typed ID。身份是 (actor_id, realm_id, read_scope) 三元组;ak.read_cursor.advance 在 event-kind-registry 中登记为 id_source=not_an_object_id,id-kind-registry 中不存在 read_cursor typed ID kind,也不存在任何按 id 寻址的读面(../authz/resource-selector-grammar.md §3.1 的 selector 只接受 read_cursor:<realm>:*)。携带 id 的 payload MUST 以 schema_violation 拒绝。
  • 跨设备下发的 ak.read_cursor.update device message 的 content 是 ak.schema.read_cursor_update.v1(device-message.schema.json#/$defs/read_cursor_update_content):它是当前胜出 advance 的派生投影,携带派生的 updated_at,MUST NOT 声明自己是 ak.schema.read_cursor.v1。
  • Read marker MUST NOT 作为持久化共享对象写入 Event 链;它属于 ephemeral / actor-private 范畴(详见 strand-and-message.md §9.6)。
  • 多设备更新同一 (actor_id, realm_id, read_scope) 时,接收方 MUST 按 ../discovery/read-receipts.md §6.5 的支配优先规则 收敛:同一 commit stream 上 stream_position 更大者胜出;仅当两者互不支配时才取 HLC 较大者,HLC 全等 才按 device_id 作 actor 域内确定性 tiebreaker;不持有 position 已提交坐标时零写入拒绝, 不得持久化猜测的 winner。
  • Strand 时间线与父 Realm 在 read receipt policy 上需要分离时,整个 Strand 通过 Strand.scope_circle_id 落在一个 Circle(参见 strand-and-message.md §5);effective policy 由 Circle 自身策略与父 Realm ak.realm.read_receipt_policy 取更严格者。Track 级别 override 不在 v1 范围内。

3. Notification

3.1 概念

notification SHOULD 是从 Event / Strand / Message / Relation 派生的 inbox projection,不是 canonical truth。它面向单个 actor 的 inbox / push pipeline,不参与协作图归约。

inbox state 的跨设备真源(normative):notification 对象本身不被持久化为共享 canonical event,但其可变 inbox state(unread / read / dismissed / archived)的跨设备收敛真源是 actor-private account data:read 由 read cursor(../discovery/read-receipts.md)派生;dismissed / archived 由 actor-private account-data key 承载(key 规则见 ../discovery/client-preferences.md),并按 account-data.md §5 的 compare-and-set 契约跨设备收敛:服务端只比较 expected_server_revision,HLC + tie-break 的领域规则由客户端在解密明文上执行。客户端 MUST 从该真源重算 inbox state,MUST NOT 把某设备本地的 dismissed / archived 当作不可同步的纯本地状态而在其它设备丢失。账号同步通道交付的普通当前行不携带 state,服务端也不得代填:state 只由上述两个 actor-private 真源在客户端重算,不存在第二份 inbox 状态。

完整推送规则、push gateway、E2EE 脱敏推送策略见 ../discovery/push-notifications.md。

3.2 Schema 与字段

Schema id: ak.schema.notification.v1

字段必填类型约束说明
idyesid:notification_projection | id:notification普通源 Event 分支使用 ak:notification_projection:<44-char-suite-tagged-full-digest-token>;仅 Agent approval 专用分支使用 ak:notification:<uuidv7>。通知 ID;两个分支的身份权威与校验规则不得混用。
schemayesak.schema.notification.v1Schema ID。
actor_idyesActorId普通源 Event 分支必须是接收者的完整 account ActorId;Agent approval 分支按其专用 controller 绑定。通知主体;不得只保留 principal 分量或从当前 Station 补齐 AccountId。
realm_idnoid:realm来源 Realm。
source_event_idconditionalid:event与 source_account_artifact 恰好一个出现。durable Realm Event 来源。
source_account_artifactconditional{kind, id}与 source_event_id 恰好一个出现;v1 闭合分支仅 {kind="agent_runtime_approval", id=<approval_request_id>}。非 Event 的 account-private 短期 artifact 来源。不得伪造 Event id。
source_refnoid:(message|strand|morph|relation|view|blob)只允许在 source_event_id 分支出现。取值形如 ak:(message|strand|morph|relation|view|blob):…,union 枚举即此 6 类。render-only hint;reducer MUST 以 source_event_id 为权威。子集差异(informative):本字段允许的 kind 子集与 Relation 端点(relation.md §1,端点另允许 realm / space / actor_profile / event / DID)、Read Cursor read_scope(§2.2,scope 限 realm / circle / space / strand / thread)各自不同;差异由各自语义决定(Notification 渲染目标 = 可被通知指向的内容对象;Relation 端点 = 可连边的图节点;Read Cursor scope = 可定位已读位置的时间线容器)。三处实现 MUST 按各自 schema 校验字段形状,同时以 id-kind-registry.json 的 referenceability 作为 kind 子集的机读真相源;本字段对应 notification_source 类别。可选 canonical 对象引用,供客户端直接渲染通知目标。
strand_idnoid:strand可选 Strand 上下文,用于路由通知。
track_namenostring^[a-z][a-z0-9_]{0,63}$。可选,来源 Strand 上的 track key。
notification_kindyesenum(message, mention, reply, assignment, schedule, invite, reaction, policy, call, applet, agent, moderation, system)通知类型。
priorityyesenum(low, normal, high, urgent)优先级。
stateyesenum(unread, read, dismissed, archived)Notification projection-state 例外;表示 inbox/read 状态,不表示 canonical object 物理 lifecycle。通知状态。
previewnoobjectE2EE 场景必须脱敏。展示摘要。
created_atyestimestamp创建时间。
updated_atnotimestamp更新时间。

3.3 行为规则

  • Notification 是派生 projection,不是 canonical truth,MUST NOT 被持久化为 durable canonical event。普通源 Event 分支的当前行由接收者自己的 Station 物化,并且只经 account subscribe 的 notifications.items 普通分支交付(../sync/client-sync.md §3.1.2);客户端信任自己 Station 的当前判定,但仍 MUST 用完整 recipient AccountId 重算 ak:notification_projection:* 身份(../discovery/read-receipts.md §6.3),按当前访问权与 actor-private preferences 决定是否展示,并在该通道不可用时从已验证的原始 source Event 与适用 accepted evidence 本地重建。服务端 MAY 从同一事实计算粗粒度 push wakeup,但 MUST NOT 改写 source Event、伪造 recipient-authored Event,MUST NOT 把 Notification 放入 account_data.events[],也 MUST NOT 另建第二套通知列表 operation。
  • 每条 Notification 必须恰好选择 source_event_id 或 source_account_artifact;Agent runtime approval 使用后者、notification_kind="agent",且不得携带 realm_id、source_ref、strand_id 或 track_name。source_account_artifact.id 是 profile-local 短期 id,不是 durable protocol object ref;Notification 终止后 durable 真相只有 accepted ak.agent.key.authorize / lifecycle state。
  • E2EE Realm 中 preview 必须由发送者客户端脱敏后置入推送 envelope;服务端不得用明文重新生成 preview。
  • notification_kind=message 表示普通 ak.message.create 在接收者 effective watch / push rule 允许普通消息提醒时产生的 inbox / push 提醒;默认 mentions_only 不得为非定向普通消息产生该类型。当同一 source event 对同一 actor 同时命中 mention、reply、assignment 等更具体原因时,dispatcher MUST NOT 额外产生重复的 message notification。
  • notification_kind=assignment 表示当前 actor 被新增为某 Strand 的 assigned_to target;它不是普通 message 的别名。
  • notification_kind=schedule 表示该 actor 需要知晓的 Strand due date 或 Calendar schedule 变更;它覆盖 metadata.fields.due_at 与 calendar-event.md §2 schedule fields。
  • notification_kind=applet / agent / policy / moderation 等扩展类型分别沿用对应 Applet、Agent、policy 与 moderation 业务对象的可见性边界;参见 ../extensions/applet-integration.md 与 ../governance/content-moderation.md。

3.4 Mention notification 派生

notification_kind=mention 覆盖普通 direct mention 与 audience mention(例如 @all / @here)。派生器 MUST 遵守 strand-and-message.md §9.4:

  • Direct mention 以结构化节点的 subject_account_id(完整 AccountId)为目标,逐字节比较两个分量;audience mention 先按 source Event 的 committed stream position、Message effective scope、Realm / Circle policy 与可见性规则展开 receiver set。strand_watchers / strand_engaged audience 的 watcher 命中由完整 effective watch level 计算,但只作为 receiver-side fanout 条件。
  • 对同一 (actor_id, source_event_id, notification_kind) MUST 去重。一个 Message 中重复 direct mention、direct mention 与 audience mention 同时命中、或 watch / reply / assignment 叠加命中,都不得在同一 push delivery window 内产生多次 wakeup。
  • actor_id MUST 是接收 notification 的 actor,而不是发送者。默认发送者自 mention 不产生 notification,除非该 actor 的私有 push rule 显式 opt-in。
  • Station 派生器 MUST 在生成 notification projection 或 wakeup 前执行 access check、history visibility 与服务端可见 level=muted gate;失败时不得留下可查询 stub。个人 blocklist、DND 与完整 push-rule chain 只由持钥 receiver 客户端在用户可感知展示前执行,详见 push-notifications.md §2.1 / §3.3 / §4.3;这些加密偏好不抑制服务端 blind wakeup。
  • 派生器、delivery response、inbox projection 与 push payload MUST NOT 暴露 audience 展开结果、recipient count、watcher 列表、watch level 或命中原因;sender 不得区分某 receiver 是因历史参与、watch 还是 direct mention 命中。
  • preview 在 E2EE / redaction / history-limited 场景下 MUST 为空或使用已授权的脱敏摘要;不得因为 notification projection 需要展示而扩大源 Message 的明文可见性。

3.5 Assignment notification 派生

当一个 accepted ak.relation.create 满足下列条件时,notification dispatcher MUST 为被分配 actor 派生 notification_kind=assignment:

  • relation_kind="assigned_to"。
  • from_ref 是 active Strand id,to_ref 是完整 ActorId object。
  • Relation create 不是现有 active (realm_id, relation_kind, from_ref, to_ref) assignment tuple 的 no-op 重放;同一 (actor_id, source_event_id, notification_kind=assignment) 最多生成一个 notification。
  • 接收 actor 对该 Strand 的 effective Realm / Circle scope 有读取权,且未被服务端可见 level=muted gate 抑制;客户端展示前另执行私有 blocklist、DND 与完整 push rule。

派生 notification 的 actor_id MUST 是 to_ref,realm_id MUST 是 Relation 所属 Realm,source_event_id MUST 是产生该 Relation create 的 canonical Event id,source_ref SHOULD 是新增的 Relation id,strand_id MUST 是 from_ref。缺少可验证 canonical Event id 时不得生成普通 notification projection;OperationId 不是 EventId,也不得作为本地或 wire source_event_id 替代品。

解除 assignment(Relation tombstone)默认不产生 assignment notification;需要审计或流程提示的产品 MAY 在本地 UI 活动流展示,但不得把 tombstone 当作新的 assignment。发送者自分配默认不通知自己,除非 receiver 私有 push rule 显式 opt-in。

Assignment 只影响通知订阅与 inbox 派生,不扩大 Strand 访问权;无访问权时 dispatcher MUST 不留下可查询 notification stub,也不得通过 push 发送 wakeup。

3.6 Schedule notification 派生

core notification 只把 accepted ak.strand.update 对 metadata.fields.due_at 的改变视为 schedule-relevant change。扩展 profile 的额外 schedule 字段由该 profile 自己登记;core 实现不得因本节而被迫识别 calendar 字段。

Schedule notification 的 receiver set 是下列集合的并集,并在服务端生成前按 access check、history visibility 与服务端可见 level=muted gate 过滤:

  • 该 Strand 当前 active assigned_to Relation 的 to_ref actors。
  • 对该 Strand 显式选择 watch=all 的 watchers。

默认 mentions_only / 隐含 participating 不因普通 schedule field 变更自动通知;但 actor 同时处于上述 receiver set(例如 assignee)时,dispatcher SHOULD 将 push-rule EventContext 标记为 target-directed,以避免被普通消息规则错误过滤。发送者默认不通知自己,除非私有 push rule 显式 opt-in。

派生 notification 的 notification_kind MUST 是 schedule,source_event_id MUST 是该 ak.strand.update 的 Event id,source_ref SHOULD 是被更新的 Strand id,strand_id MUST 是被更新的 Strand id。对同一 (actor_id, source_event_id, notification_kind=schedule) MUST 去重;一次 patch 同时改 due date 和 calendar fields 也只生成一条 schedule notification。

E2EE / plaintext policy 不允许服务端读取 schedule fields 时,服务端不得为了通知而解密或扩展明文可见性;实现 MAY 发送不含 preview 的 blind wakeup,或让客户端在本地解密后根据同一规则完成 inbox 派生。

服务端可验证的 access / history / mute 过滤发生在生成之前,失败时 MUST NOT 产生 notification、stub 或 wakeup。持钥客户端 MUST 在任何用户可感知 inbox、OS 通知、声音或高亮展示前应用个人 blocklist、DND 与完整 push rule;私有规则可阻止展示而不阻止 blind wakeup。服务端不得索取私有密文规则的明文来完成派生。

Calendar 扩展在此基础上收窄两点,规则正文见 calendar-event.md §10:一是 receiver 候选集扩展为 pre_state.attendees ∪ post_state.attendees ∪ assigned_to ∪ watch_all,使被移除但仍有 scope 读取权的 attendee 也能获知变更;二是 calendar 字段的 dispatcher 职责属于独立的 server profile ak.profile.calendar_notification_dispatch.v1,client 侧 ak.profile.calendar_event.v1 不承担该职责,core 实现也不因此被迫识别 calendar 字段。

4. 与 Account Data 的关系

Read marker 与个人通知偏好、saved view personalization、列宽 / 折叠等本地状态都属于 actor-private account data 类别。完整 account data 模型、私有标签、个人 blocklist 见 ../discovery/client-preferences.md。

4.1 Agent draft、Sidecar view 与 participation account data

三个 Agent actor-private Event 的 owner、request/draft/rejection 唯一键、workflow CAS、exact retry 与 原子拒绝合同见 actor-private-effects.md §3.2。

两类 controller-owned encrypted account data 类型在 ak.agent.* 命名空间下:

  • ak.agent.draft.v1:ak.agent.draft.propose 接受后只创建结构化 Station-private pending intent,并通过 controller device HPKE handoff 交付候选内容;它不写 account data。controller holder 解密、校验 content digest 后,使用 account secret 构造 encrypted value,再以唯一 ak.account_data.set CAS 创建 ak.agent.draft.v1:<agent_id_sha256_b64u43>:<draft_id_sha256_b64u43> revision 1,并在同一事务消费 pending intent。两段 key component 是不同注册 domain 下完整 SHA-256 的 43 字符 unpadded base64url;Station 只从 source pending row 重算比较,不反解、不接受调用方 selector。Station 不持 holder secret、不生成密文,pending intent 也不得冒充 account-data current value;exact owner/key/value、到期、消费和失败恢复见 actor-private-effects.md §3.2。Draft MUST NOT 作为 ak.message.create / ak.strand.create 或任何 wire_scope=durable_event 进入目标 Realm 共享历史。Draft 引用目标 realm_id / strand_id / message_id 不授予目标 Realm 成员读取 draft 内容的权利。
  • ak.agent.sidecar_view_state.v1:controller-private context view state,使用 ak.schema.agent_sidecar_view_state.v1 plaintext。令 controller_account_key = derive_account_data_key(RFC8785_JCS(controller_account_id)),其中派生 primitive 严格复用 account-data.md §2;Key pattern 为 ak.agent.sidecar_view_state.v1:<controller_account_key>:<target_realm_id>:<target_strand_id>。不得把结构化 AccountId 的 JSON 直接插入冒号分隔 key;同 principal、异 Station 必须产生不同 controller_account_key。该 value 只保存 pin/局部折叠与跨设备 HLC;原 Strand 与 Sidecar 固定合并展示,不保存或接受 display_mode 字段。它引用 sidecar_id,但不得产生 shared Strand durable 写入;局部折叠不得成为阅读自己的私密消息必须切换模式或执行 publish 的条件(sidecar.md §8)。

上述两类 key 前缀不同、key 第二段语义不同(draft 为 agent_id,Sidecar view 为 controller_account_id),不会在 ak.agent.* 命名空间下冲突。注册时 MUST 在 account-data-key-registry.json 显式声明 key pattern、plaintext schema 与 holder principal,reducer/client 据此做归属、key/content binding 与 closed-schema 校验。

ak.vector.sidecar.view_state_closed.v1 的 sidecar-view-state-schema-fixture.json 只验证该 view-state plaintext 的封闭 shape(不含 display_mode,拒绝两个旧模式字段);不证明加密传输、权限、MLS 或 主人实时阅读。固定合并展示与具名生产验收遵循 sidecar.md §8.2。

Agent participation selection 不是 Account Data(normative):逐 scope 的 Agent participation selection 由 Account Authority 持有,寻址键是 (agent_id, target_scope),读写入口只有 ak.self.agent.participation.resource.get.v1 / .replace(见 ../sync/service-http-binding.md §5 与 ../authz/capabilities.md §11)。它 MUST NOT 登记 account-data key,也 MUST NOT 经 ak.account_data.set 写入:那会给同一记录造出第二套 CAS 计数器 (account-data 的 expected_server_revision 与本记录的 expected_version),两者无法互相推导。记录内容是 closed {target_scope, selection, version},selection 五位恰为 {reply_message, reaction_add, reaction_remove, accept_third_party_mention, act_on_behalf}, target_scope 只允许 realm / circle / strand 的 closed XOR。首次写 expected_version=0,每次接受严格 +1。 它不授予 capability、不写 Realm Event、不复制 ceiling/effective;target 在实际动作时将当前 selection 与 本地 current ceiling、普通 capability 和 lifecycle 求交。

配置 UI MUST 将 selection 偏好与已接纳 grant、当前有效动作分别展示;只有明确确认的完整 回复配置入口才可协调独立 grant authoring,不能让本 replace 悄悄写 Realm grant。入口、就地 结果与重置后验收见 agent-interaction §4.1。

主人可明确 author 自有受限 Grant,selection 仍不能替代该原签授权、主人 current 上界或管理 Policy;未知 current 不能被当作无禁令。管理员收紧不修改 controller-private selection,也不因此读取它。

ak.schema.agent_sidecar_exchange_projection.v1 不属于本节 Account Data:它只是 controller 设备从 Sidecar private Event history 生成的本地可删除 cache/SDK DTO,不注册 account-data key,不进入 account stream,也不跨设备合并。真相源是 Sidecar private Event history 本身(sidecar.md §8);fold 与 cache 恢复的判据由 ../conformance/conformance-vectors.md §11.10.4 固化。

4.2 隐私边界(normative)

agent_interaction 是 Realm-scoped、controller 自著 ak.agent.interaction.set 的治理 current, 不是本节的 controller-private participation selection,也不是 Account Data;机器 value 与披露边界见 agent-interaction.md。其记录或模式变化 MUST NOT 修改 Sidecar 派生成员, 公开参与也不得自动公开 private prompt、工具结果或 Sidecar history。

针对上述 agent-attributed private state:

  • 存储 MUST 使用 wire_scope=actor_private_event 通道(encrypted account data 或 actor-private stream);不得进入 shared Realm data-plane history 或 control-plane RealmCommit history。
  • 目标 Realm 的 ak.self.committed_event.stream.subscribe.v1 / ak.self.committed_event.read.scan.v1 / shared reducer / Realm search index / notification fanout / push preview MUST NOT 返回 draft、Sidecar view state 或本地 exchange cache 内容。
  • ak.self.account.stream.subscribe.v1 只能把 controller-owned approval draft / Sidecar view state 返回给 controller principal 的授权 session。Agent runtime MUST NOT 接收上述 controller-owned encrypted account data:其 value 以 controller account secret 派生密钥加密(account-data.md §3),不同 principal 的 account secret 强制隔离,不存在也不得新增向 Agent runtime 分发该 secret 的机制。Agent runtime 所需的 Sidecar exchange identity 只经 Event 内的 exchange binding 传递(runtime 从 role=request binding 取得 exchange_id,不存在 Account Data projection 读写路径),判据见 ../conformance/conformance-vectors.md §11.10.3。
  • 若服务端存储明文,该 deployment MUST 把”明文可见服务”写入 profile / policy 并向 controller 披露;默认语义 SHOULD 是服务端只保存 encrypted account data。
  • Draft 发布到目标 Strand 时,shared event MAY 通过 semantic_refs[].role="draft_source" 携带 opaque digest,但明文 draft id、private metadata、scratchpad、private prompt 或历史版本 MUST NOT 泄露到共享历史。
  • Sidecar 发布到目标 Strand 时,MUST NOT 泄露 sidecar_id、private Event id、MLS material、private messages、scratchpad 或 draft history。

5. 规范性引用