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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ LoopX 1.0 brings these long-horizon control states into the Personal Workspace.
- review protected changes through typed preview, explicit confirmation, and
receipts while LoopX state—not the browser—remains authoritative.

For Manager group conversations, LoopX keeps message visibility separate from
Turn authority; see the bilingual [Lark Manager context and authority
contract](docs/reference/protocols/lark-manager-context-authority-v0.md).

```bash
loopx dashboard
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@ const en = {
"lark.groupLoading": "Reading groups joined by this bot…",
"lark.groupSearch": "Search groups joined by this bot",
"lark.historyPermission": "Group-history permission (separate capability)",
"lark.health.contextCaptured": "Group context captured",
"lark.health.contextCapturedDetail": "The message is retained as non-authoritative context. It did not start or steer a Manager turn; send a direct @ mention or verified reply when action is required.",
"lark.health.eventProcessed": "{events} events processed, {replies} replies sent.",
"lark.health.eventUnverified": "Event subscription needs verification",
"lark.health.eventUnverifiedDetail": "The provider listener is ready, but no event has arrived. Enable im.message.receive_v1 and group mention permissions, publish a new version, then send a new @ mention inside this Agent Topic. Group-level messages fail closed when multiple Agent routes exist.",
Expand Down Expand Up @@ -1494,6 +1496,8 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"lark.groupLoading": "正在读取该机器人已加入的群…",
"lark.groupSearch": "搜索该机器人已加入的群",
"lark.historyPermission": "历史补读权限(独立能力)",
"lark.health.contextCaptured": "已捕获群聊上下文",
"lark.health.contextCapturedDetail": "该消息仅作为非权威上下文保留,不会启动或引导管家 Turn;需要执行时请直接 @ 机器人或回复机器人的消息。",
"lark.health.eventProcessed": "已处理 {events} 条事件,成功回复 {replies} 条。",
"lark.health.eventUnverified": "事件订阅待验证",
"lark.health.eventUnverifiedDetail": "Provider listener 已就绪,但尚未收到消息事件。请启用 im.message.receive_v1、开通群聊 @ 消息权限并发布新版,然后在这个 Agent Topic 内发送新的 @ 消息;存在多条 Agent 路由时,群顶层消息会 fail closed。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ function larkConnectionHealth(connection: LarkGoalConnection, t: WorkspaceTransl
state: "ready",
};
}
if (
connection.last_event_status === "context_only_captured"
|| connection.last_event_status === "context_only_already_captured"
) {
return {
label: t("lark.health.contextCaptured"),
detail: t("lark.health.contextCapturedDetail"),
state: "ready",
};
}
if (connection.last_event_status === "ignored" && connection.last_event_reason === "not_addressed") {
return {
label: t("lark.health.notAddressed"),
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ provider-specific routing data. This runtime contract is not itself a new
capability registry entry; providers advertise stable caller outcomes through
their existing extension and capability surfaces.

Manager group bindings additionally apply the bilingual
[context-capture and Turn-authority contract](protocols/lark-manager-context-authority-v0.md):
an unaddressed message may be retained as bounded non-authoritative context,
but only typed provider addressing may enqueue or steer a Turn.

For asynchronous sources, the same module provides an owner-local incremental
inbox runtime. A provider translates a bounded page into
`agent_external_connector_event_v0` envelopes and calls the capture operation
Expand Down
92 changes: 92 additions & 0 deletions docs/reference/protocols/lark-manager-context-authority-v0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Lark Manager Context and Turn Authority v0

## English

A LoopX Manager connection separates **message visibility** from **Turn
authority**. When exactly one enabled Manager binding owns a Lark App and group,
LoopX may retain compact non-self messages from that group as local-private
context. Retention does not start a model call, send a reply or reaction,
acknowledge the provider event, or authorize any Goal/Todo mutation.

This is an early Lark adapter slice for M3/A10 of the
[capable-manager semantic-handoff RFC](../../architecture/rfcs/capable-manager-semantic-handoff-v0.md).
A context-only item is an adapter-owned Observation/material, not a WorkRequest,
Assessment, Todo, or delivery receipt. The Lark inbox and material-review ledger
do not own generic collaboration lifecycle state and must later project the M2
request/result contract rather than becoming a second request database.

A Manager Turn is authorized only by a provider-native mention of the bound Bot,
a provider-verified reply to that Bot, or another existing typed authority
record. The next authorized Turn may receive up to eight recent context-only
messages with a 4,000-character total budget. Every item is labeled
`context-only`; the prompt explicitly states that these items are not commands,
authorization, or independent Todos.

Before an authorized Manager Turn reads that context, the existing bounded
turn-start history sync fills gaps left by the live event subscription. Items
recovered from history are always marked `context-only`, including old messages
that originally mentioned the Bot: catch-up never replays a missed Turn. This
recovery uses the same private cursor and inbox, performs no history-message
reaction or reply, and degrades without blocking the current authorized Turn if
the provider history read is unavailable.

Provider addressing is preserved as historical provenance while normalized live
attention/reply flags are cleared. The generic urgency projection and the Lark
material-settlement path therefore agree that a recovered mention is material,
not a delayed request.

After a successful authorized Turn and verified reply, consumed context items
are settled through the existing event-bound material-review ledger. Duplicate
delivery and restart recovery remain idempotent. Self messages, another chat,
invalid routing, and ambiguous Manager bindings remain closed and are not
captured.

The connection health projection distinguishes `context_only_captured` from
`replied_and_acknowledged`. CLI/managed Turn, frontend, and Lark must reuse this
single runtime inbox and receipt model; adapters must not invent a second
authority source.

The eight-item / 4,000-character limits bound one Turn projection, not durable
content retention. A separate adapter-owned retention/expiry/compaction change is
still required before this slice can claim long-running M3/A10 acceptance; any
discard must expose a reason and preserve duplicate/restart safety.

## 中文

LoopX 管家连接将**消息可见性**与 **Turn 权限**分开处理。当且仅当一个启用的
管家绑定唯一拥有某个 Lark App 与群聊时,LoopX 可以把该群中的非机器人消息以
紧凑、本地私有的上下文材料保留下来。仅保留消息不会调用模型、发送回复或
reaction、确认 provider event,也不会授权任何 Goal/Todo 修改。

这是[强能力管家与语义工作交接 RFC](../../architecture/rfcs/capable-manager-semantic-handoff-v0.zh-CN.md)
下 M3/A10 的早期 Lark adapter 切片。`context-only` 项是 adapter owner 管理的
Observation/材料,不是 WorkRequest、Assessment、Todo 或送达回执。Lark inbox 与
material-review ledger 不拥有通用协作生命周期;后续应投影 M2 的 request/result
契约,不能变成第二套请求数据库。

只有以下来源能够授权管家 Turn:provider 原生的目标机器人 mention、provider
验证过的对机器人回复,或其他既有 typed authority 记录。下一次获得授权的 Turn
最多读取最近八条、总计不超过 4,000 字符的仅上下文消息。每条材料都会标记为
`context-only`,prompt 也会明确说明这些内容不是指令、授权或独立 Todo。

在已授权的管家 Turn 读取上下文前,既有的有界 turn-start 历史同步会补齐实时
事件订阅遗漏的消息。所有历史补采项一律标记为 `context-only`;即使旧消息原本
真正 mention 了机器人,也不得借补采重放成一个 Turn。补采复用同一私有游标和
inbox,不给历史消息发送 reaction 或回复;provider 历史读取不可用时,会准确
降级但不阻塞当前已授权 Turn。

provider 的原始寻址信息作为历史 provenance 保留,但 live attention/reply 标志会被
清除,因此通用 urgency 投影与 Lark material settlement 对“历史 mention 只是材料”
得出同一个结论,不会把它恢复为延迟请求。

获得授权的 Turn 成功完成且回复验证通过后,已使用的上下文材料通过现有的、
绑定事件的 material-review ledger 结算。重复投递与重启恢复保持幂等。机器人
自身消息、其他群聊、无效路由以及多重歧义的管家绑定继续安全关闭且不采集。

连接健康投影会区分 `context_only_captured` 与
`replied_and_acknowledged`。CLI/managed Turn、frontend 与 Lark 必须复用同一份
运行时 inbox 和 receipt 模型;适配器不得另造权限来源。

八条/4,000 字符只约束单次 Turn 投影,不等于持久内容 retention 已有上限。要宣称
长期运行的 M3/A10 验收,仍需 adapter owner 交付 retention/expiry/compaction;任何
丢弃都必须显示原因,并保留重复投递与重启安全。
32 changes: 30 additions & 2 deletions loopx/extensions/lark/event_inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,13 @@ def _event_from_payload(
or (bot_display_name is None and payload.get("addressed_to_bot") is True)
)
event["addressed_to_bot"] = addressed_to_bot
historical_context_only = payload.get("historical_context_only") is True
if historical_context_only:
# History catch-up is evidence recovery, never delayed Turn authority.
# Preserve this provenance so a later authorized manager Turn may use
# the item as context even when the old message contained a real Bot
# mention. Live delivery still owns all execution authority.
event["historical_context_only"] = True

mentions = payload.get("mentions")
provider_mention_count = 0
Expand Down Expand Up @@ -386,7 +393,19 @@ def _event_from_payload(
addressing_source = stored_addressing_source or "legacy_text"
else:
addressing_source = ""
if addressing_source:
if historical_context_only:
# Preserve what the provider observed without allowing a recovered
# historical mention/reply to re-enter the live attention or reply
# authority lanes. Both the generic urgency projector and the Lark
# settlement adapter consume the normalized flags below.
event["historical_was_addressed_to_bot"] = addressed_to_bot
event["historical_was_reply_to_bot"] = event["reply_to_bot"]
if addressing_source:
event["historical_addressing_source"] = addressing_source
event["addressed_to_bot"] = False
event["reply_to_bot"] = False
event["reply_context_verified"] = False
elif addressing_source:
event["addressing_source"] = addressing_source
return event

Expand Down Expand Up @@ -443,6 +462,8 @@ def _event_attention_kind(
bot_display_name: str,
capture_scope: str,
) -> str | None:
if event.get("historical_context_only") is True:
return None
normalized = dict(event)
normalized["addressed_to_operator"] = bool(
event.get("addressed_to_bot") is True
Expand Down Expand Up @@ -615,7 +636,14 @@ def inspect_lark_event_inbox(
inbox = config["inbox_path"]
processed = _load_processed(inbox / "processed.json")
pending, captured_count, invalid_count = _pending_events(config)
bounded = pending[: max(1, min(int(limit), 100))]
requested_limit = int(limit)
# Internal retention/retry callers use zero to read the complete pending
# projection. Public callers retain the historical 1..100 bound.
bounded = (
pending
if requested_limit <= 0
else pending[: max(1, min(requested_limit, 100))]
)
return {
"ok": True,
"schema_version": "lark_event_inbox_projection_v0",
Expand Down
Loading