Lark event inbox¶
LoopX can consume Lark feedback without keeping an agent process alive. The integration deliberately separates collection from interpretation:
Lark event stream
-> host-managed collector
-> .loopx/inbox/<channel>/*.json
-> loopx lark-inbox drain
-> domain agent writes a todo, vision correction, artifact update, or rationale
-> direct bot question: loopx lark-inbox reply (optional, configured sender only)
-> loopx lark-inbox ack --message-id ... --execute
The collector is host infrastructure. LoopX can validate a local-private
collector config, preview or explicitly install a macOS launchd / Linux
systemd user service, and report supervisor plus event-bus health. The
installed service runs a small LoopX collector runtime around
lark-cli --profile <configured-profile> event consume with a bounded timeout,
so stdin EOF under a supervisor cannot terminate an otherwise unbounded
consumer. When the official npm package exposes a Node wrapper, LoopX records
absolute paths for both Node and the wrapper so launchd does not depend on an
interactive-shell PATH. It filters before persistence and writes one compact
event per Lark event_id/message_id. Direct mentions are persisted
immediately. For a message without a direct mention, the runtime reads back the
current message and its direct parent, then marks it actionable only when the
parent sender is the app id of the configured profile. A reply to a person,
another app, or an unverifiable parent remains captured but does not wake the
agent. The agent does not need to keep a websocket open.
Use addressed_only only when direct bot mentions are the entire feedback
contract. A review or collaboration inbox that accepts non-mention replies to
bot messages should use configured_chat_all: the host collector filters by
its local-private chat id, persists every message from that chat, and verifies
the reply relation through message readback before scheduling a reply. Full-chat
capture is not full-chat activation; unrelated conversation remains available
to domain interpretation without being treated as addressed to the bot.
Activate the provider¶
Install and explicitly activate the bundled provider once in the LoopX runtime used by the project:
Configured lark-inbox commands fail closed when loopx-lark is absent,
disabled, or no longer matches its doctor-verified revision. Each operation
also requires its manifest permission: inbox read/write, reply send, or
collector management. extension upgrade and extension rollback probe the
candidate revision before switching it. A goal with no Lark inbox pointer still
returns the existing quiet disabled drain projection and does not require the
extension, so projects that do not use Lark remain unaffected.
Quota and Turn planning also resolve lark.inbox.read before the extension
opens the local-private profile/chat config. A missing, disabled, or stale
extension yields an unavailable urgency projection, does not read that config,
and cannot schedule a Lark reply lane. The compatibility CLI performs this
composition internally; agents do not pass a provider, profile, or alias.
The collector service is a separate host lifecycle. Disabling or switching the
extension blocks its next collector-run start but does not signal a process
that is already consuming events. Stop or restart the configured launchd or
systemd user service when disabling, upgrading, or rolling back the provider.
Local-private configuration¶
The inbox is opt-in. Create a local-private generic Lark inbox config:
{
"schema_version": "lark_event_inbox_config_v0",
"enabled": true,
"inbox_dir": ".loopx/inbox/team-feedback",
"capture_scope": "configured_chat_all"
}
inbox_dir must stay under .loopx/inbox. Destination ids, member ids,
profile names, raw provider payloads, and credentials stay in local-private
configuration or host state and must not enter public LoopX packets.
capture_scope defaults to addressed_only for compatibility. Drain output
reports thread_complete=false and a coverage warning for that mode. For
configured_chat_all, the collector's jq filter should select the configured
chat only; do not add a content-level @bot predicate.
Optional source-thread replies are a separate, default-off boundary. Enable
them only for a configured_chat_all inbox and bind an explicit non-default
bot profile to the same local-private chat:
{
"schema_version": "lark_event_inbox_config_v0",
"enabled": true,
"inbox_dir": ".loopx/inbox/team-feedback",
"capture_scope": "configured_chat_all",
"reply": {
"enabled": true,
"sender_profile": "project-review-bot",
"sender_identity": "bot",
"bot_display_name": "Project Review Bot",
"chat_id": "oc_<local-private-chat-id>"
}
}
The reply path never uses the machine default profile. Before any send it
verifies that the named profile resolves to the expected bot and that the bot
can read the configured chat. A profile/app mismatch fails with
lark_inbox_reply_sender_identity_mismatch; a profile that cannot access the
configured chat fails with
lark_inbox_reply_sender_not_in_configured_chat. Neither failure falls back
to another app. Public results contain only compact status/receipt fields, not
the profile, chat id, message id, reply text, or provider payload.
Host collector lifecycle¶
Keep the collector config ignored and untracked. It references the generic inbox config but owns host-only details such as the chat id and supervisor:
{
"schema_version": "lark_event_collector_config_v0",
"enabled": true,
"service_name": "loopx-lark-feedback",
"event_key": "im.message.receive_v1",
"identity": "bot",
"profile": "project-review-bot",
"supervisor": "launchd",
"chat_id": "oc_<local-private-chat-id>",
"consume_timeout": "30m",
"lark_cli_bin": "lark-cli",
"event_inbox_config": ".loopx/config/lark/event-inbox.json"
}
The packaged v0 lifecycle accepts only im.message.receive_v1, bot identity,
an isolated loopx- service name, and configured_chat_all. These constraints
match the canonical inbox schema, avoid collisions with unrelated user services,
and make thread completeness explicit instead of pretending that an
addressed_only consumer sees later unaddressed replies. Plan and install
output never returns the chat id, local paths, generated jq, or credentials.
An enabled collector must bind an explicit non-default Lark CLI profile. When
profile is omitted, LoopX may reuse the enabled inbox reply
sender_profile; when both are present they must match. The generated service
passes the profile-bound collector config to the LoopX runtime, which places
--profile before both event consume and message readback calls. Collection,
reply-target verification, and optional replies therefore cannot silently use
different app identities. Public plan/status packets expose only whether a
profile is bound and where the binding came from, never its value.
When the CLI uses a custom --runtime-root, the generated service records that
same root before lark-inbox collector-run; a supervisor restart therefore
resolves the same extension activation state that was validated at install.
loopx lark-inbox collector-plan \
--project . \
--config .loopx/config/lark/collector.json
# Preview first; this writes nothing and starts no process.
loopx lark-inbox collector-install \
--project . \
--config .loopx/config/lark/collector.json
# Explicitly write the user service and start/restart it.
loopx lark-inbox collector-install \
--project . \
--config .loopx/config/lark/collector.json \
--execute
# Read-only supervisor, event-bus, and real-event evidence check.
loopx lark-inbox collector-status \
--project . \
--config .loopx/config/lark/collector.json \
--probe-event-bus
Missing lark-cli produces a non-blocking install hint. Reply-target
verification also requires the configured bot to read messages in the selected
chat. Missing scopes or bot configuration still belong to lark-cli; LoopX
does not authenticate a bot,
copy app credentials, or silently install packages. Service installation is a
local host write and therefore requires explicit --execute. Status separates
healthy from real_event_evidence_present: a running subscriber can be
healthy before the first message, while acceptance of a real integration still
requires one post-install event to appear in the inbox.
Register the generic inbox pointer once at the goal boundary:
loopx configure-goal \
--goal-id <goal-id> \
--lark-event-inbox-config .loopx/config/lark/event-inbox.json
# Review the preview, then apply explicitly.
loopx configure-goal \
--goal-id <goal-id> \
--lark-event-inbox-config .loopx/config/lark/event-inbox.json \
--execute
The configuration catalog exposes this optional capability on demand. Quota
projects enabled, config_pointer_registered, a local control command, and a
content-free urgency summary; it never projects the private path, message ids,
senders, or message bodies. The summary includes
pending/direct-question/direct-mention/verified-bot-reply counts and the oldest
pending age. A configured direct mention or verified reply to a message authored
by the configured bot becomes a high-priority lark_event_inbox work lane
before ordinary monitor or advancement work. Generated heartbeat bodies run the
actual goal-boundary drain_command;
loopx --registry <invoked-registry> lark-inbox drain --goal-id <goal-id>
follows a shared registry's source_registry to the canonical project before
resolving the ignored config. It therefore remains correct from linked or
independent worktrees without binding control state to --project .. A disabled or empty inbox
is a quiet zero-spend path, so projects without Lark keep the default behavior.
Drain and acknowledge¶
loopx lark-inbox drain \
--project . \
--config .loopx/config/lark/event-inbox.json
loopx lark-inbox ack \
--project . \
--config .loopx/config/lark/event-inbox.json \
--message-id om_xxx \
--execute
Drain is read-only and returns bounded local-private message content. A message
must be acknowledged only after its effect is written back. Duplicate event
files collapse by message_id; repeated acknowledgement is idempotent.
Urgency classification stays local: explicit @ mentions of the configured
bot/LoopX, bounded question signals on addressed events, and provider-verified
direct replies to the configured bot produce counts only. A question elsewhere in the
group or a reply to a human does not become reply_due. The agent still drains
and interprets the source event before deciding the durable effect or reply; the
summary is a scheduling signal, not semantic authority.
For a direct question, explicit bot mention, or verified reply to the configured bot, write the requested durable effect first, preview one concise source-thread reply, execute it, require readback, and only then ACK:
loopx lark-inbox reply \
--project . \
--config .loopx/config/lark/event-inbox.json \
--message-id om_xxx \
--text '已记录并修正。'
loopx lark-inbox reply \
--project . \
--config .loopx/config/lark/event-inbox.json \
--message-id om_xxx \
--text '已记录并修正。' \
--execute
The command uses an idempotency key derived from the source message and reply
text, sends with --reply-in-thread, and reads the created message back through
the same configured profile. Ordinary chatter remains a no-reply path;
enabling this capability does not grant reviewer-notification or other
outbound authority.
For text replies containing Lark <at user_id="...">...</at> mentions, provider
readback may replace the markup with tokens such as @_user_1. Verification
therefore compares the normalized visible-text template and requires every
mention token to resolve to the identity requested at send time. A missing,
extra, or differently resolved mention remains sent_unverified; display-name
or raw-markup similarity alone is not accepted.
Bounded history reconciliation¶
Real-time event subscriptions do not backfill messages sent before a collector
started, and an earlier addressed_only collector will already have omitted
unaddressed replies. Fetch the bounded source conversation with the Lark CLI,
project each message into lark_event_inbox_event_v0, then pipe the JSON array
or NDJSON into the generic importer:
<bounded-lark-message-export> \
| loopx lark-inbox ingest \
--project . \
--config .loopx/config/lark/event-inbox.json \
--execute
Ingest validates ids and schema, deduplicates by message_id, writes only to
the configured local-private inbox, and returns counts rather than message
content. It does not acknowledge imported messages; the domain agent must still
write each actionable effect before ACK.
Reviewer notification dedupe uses durable lifecycle receipts first, then exact
PR-link evidence in the persisted configured_chat_all inbox, and finally a
bounded user-identity search of the configured chat. Missing search:message
permission degrades to the two persisted sources and does not create a user
gate; other provider read failures remain blockers because absence cannot be
established safely.
Domain bindings¶
The inbox itself does not know why a message matters. A domain capability binds the generic event stream to its own interpretation and writeback rules. For example, issue-fix can turn reviewer-group messages into PR-description updates, Kanban context, vision corrections, or explicit no-follow-up rationale. Other domains can consume the same inbox without adopting any issue-fix schema or lifecycle.
For issue-fix, outbound GitHub reviewer requests and outbound Lark notifications remain independent obligations. The Lark inbox is only the inbound feedback path.