Skip to content

feat(messaging): Runtime v2 — 3채널 동시 실행·내구성·릴리스 무결성 (#332) - #342

Merged
lidge-jun merged 29 commits into
mainfrom
dev
Aug 14, 2026
Merged

feat(messaging): Runtime v2 — 3채널 동시 실행·내구성·릴리스 무결성 (#332)#342
lidge-jun merged 29 commits into
mainfrom
dev

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Messaging Runtime v2

Closes #332, #333, #334, #335, #336, #337, #338, #339.

What shipped

M Issue What
M0 #333 publish dispatch-only + expected-sha + promote-to-main.sh
M1 #334 concurrent 3-channel gateway, settings v4
M2 #335 ChannelAdapter contract + conformance suite + capability matrix gate
M3 #336 durable ingress journal + 3-channel admit/settle + DLQ/replay CLI + effect_claims FSM + outbound_attempts outbox
M4-A #337 /stop /queue /approve /deny on 3 channels + native Approve/Deny buttons
M4-B #338 session generation on ingress; stale_generation refuse
M5 #339 log.event + ALS trace context + metrics registry + health snapshot + 22nd gate (messaging-conformance) + functional-certified artifact + restart/429/unique-race tests

Release gate status

  • gate:all 22/22 (including the new messaging-conformance gate)
  • Certification: (live canary absent → not )
  • Unit: 7551 pass / 1 fail (pre-existing FC-007 timing flake, isolated 4/4)
  • Integration: 41 pass / 2 fail (pre-existing concurrency tests)

Not included (deferred)

  • Wiring the 6 production effects through effect_claims (effect substrate is landed)

  • Routing production sends through the outbound outbox (outbox substrate is landed)

  • Usage:
    jaw messaging ingress list [options] List journaled inbound events
    jaw messaging ingress show Show one event
    jaw messaging ingress replay --reason
    jaw messaging ingress audit [--limit 20] Show replay history
    jaw messaging doctor [--json] Journal counts + this-process events

    List options:
    --channel telegram|discord|slack
    --state received|processing|completed|dead_letter
    --older-than 30m|24h|7d
    --limit 50
    --json

    Replay options:
    --reason Required. Recorded in the audit trail.
    --force Replay a completed event. Refused by default: its effects
    already happened, and running them twice is worse than not
    replaying at all.

    A successful replay only marks the journal row received. Nothing inside this
    process re-runs the handler. The next vendor redelivery is what actually
    executes it — Telegram's offset, Slack's retry, Discord's resume. and
    Usage:
    jaw messaging ingress list [options] List journaled inbound events
    jaw messaging ingress show Show one event
    jaw messaging ingress replay --reason
    jaw messaging ingress audit [--limit 20] Show replay history
    jaw messaging doctor [--json] Journal counts + this-process events

    List options:
    --channel telegram|discord|slack
    --state received|processing|completed|dead_letter
    --older-than 30m|24h|7d
    --limit 50
    --json

    Replay options:
    --reason Required. Recorded in the audit trail.
    --force Replay a completed event. Refused by default: its effects
    already happened, and running them twice is worse than not
    replaying at all.

    A successful replay only marks the journal row received. Nothing inside this
    process re-runs the handler. The next vendor redelivery is what actually
    executes it — Telegram's offset, Slack's retry, Discord's resume. operator CLIs

  • Reconcilers for expired claims

  • Live 3-channel canary

  • Chaos matrix (12 cases from 060 §5)

…equired, promote-to-main.sh, aggregate tests job

- .github/workflows/publish.yml: remove push triggers; add required expected-sha
  input; verify dispatched SHA matches HEAD; require successful test.yml run
  for this commit; require platform checks when installer surface changed.
- .github/workflows/test.yml: run on preview/main only; add changes job and
  aggregate tests job so docs-only PRs satisfy required checks.
- scripts/promote-to-main.sh: preview-to-main stable promotion with evidence,
  worktree isolation, PR merge, CI wait, and publish dispatch.
- scripts/release.sh: removed. Caller tests and docs updated.
- scripts/release-preview.sh: dispatch publish.yml after push with expected-sha.
- scripts/require-release-evidence.mjs: add --changed-files-stdin mode; update
  sensitive path list for promote-to-main.sh.
- tests/unit/release-scripts-contract.test.ts: branch policy assertions updated.
- tests/unit/electron-version-sync.test.ts: promote-to-main.sh + release-preview.sh.
- tests/unit/safe-install.test.ts: release/promote assertions updated.
- README.md, structure/infra.md: release script references updated.

Refs: #333
Settings schema v4 replaces the single `channel` field with
`messaging.enabledChannels[]` + `messaging.homeChannel`, so Telegram,
Discord and Slack can take inbound traffic at the same time instead of
one at a time.

- config: v3 -> v4 migration folds the old `channel` into a one-element
  enabled list and the same home channel. Each migration marker now keys
  off the version that introduced it (MULTI_SESSION_DEFAULT_SCHEMA_VERSION)
  rather than the current schema, so a later bump no longer re-asks a
  question the user already answered.
- runtime: per-channel registry with running/error state, start/stop of a
  single transport, and a restart that touches only the channels whose
  enablement actually moved. TransportFns.init returns whether it started.
- send: reply routing resolves target -> explicit channel -> home channel,
  so a reply lands where the message came from.
- health: `activeInboundChannels[]` added alongside the existing
  `activeInbound` scalar, which keeps pointing at the home channel. Old
  parsers keep working.
- surfaces: Settings API keeps a deprecated `channel` alias (with a
  Deprecation header on writes), `jaw init` gains --channels/--home-channel
  with --channel as a deprecated alias, manager UI gets a per-channel
  enablement control.

Two integration tests that grepped runtime.ts for `channelSwitched` and
`prevChannel !== nextChannel` are replaced with behavioural checks: those
identifiers died with the single-channel model, but the contract they
protected did not.

Refs #334
…sed capability set

Two lies removed before the ChannelAdapter contract is built on top of them.

Transport start no longer returns a fabricated boolean. All three registrars
hardcoded `return true`, while the vendor inits returned early — and silently —
on real failures: Telegram when getMe gave no identity, Slack when auth.test
failed or the instance was not the attach instance, Discord on lock contention.
Every one of those reported "started" and landed in runningTransports.

`TransportStartOutcome` replaces the boolean and separates the twelve early
returns into what they actually are. Only `failed` is a fault; `not_configured`,
`outbound_only`, `not_attach_instance` and `superseded` are states an operator
chose or a concurrent call owns. `startMessagingTransport` records a transport
error only for `failed`, and the boot log in server.ts stops printing
"init failed" at an outbound-only Slack install on every start.

`started: true` deliberately means accepted, not receiving. Telegram's poller and
Discord's gateway both become ready after init returns — Discord's supervisor
even swallows a login failure into a reconnect schedule — so readiness belongs to
channel health, not to this value. The comment at each success return says so.

Two supporting fixes fall out. Slack's queued follow-up init discarded its own
result, so the retry that actually opened the socket returned into a void; it now
hands its outcome back to the caller. And Slack refuses to open the socket when
the workspace id cannot be resolved, because `slackEventKey` degrades an empty
one to the literal 'unknown' and collapses separate workspaces into one dedup
namespace. That mirrors Telegram refusing to poll without a bot identity.

The capability declaration was also wrong in three places beyond the known
`durableOffset`. Discord declared editText and interactiveActions true with no
edit call and no component handler; Slack declared interactiveActions true while
routing interactive events to a log. The set is now closed and named for
behaviour rather than for Telegram's implementation: sendText, editText,
deleteMessage, reaction, typing, fileUpload, voice, threads, interactiveActions,
durableIngress, replayableTransport, maxMessageChars.

`maxMessageChars` was decorative — the real limits lived as separate literals in
three chunkers. The chunkers now export named constants and a test binds the
declaration to them, so the two cannot drift apart while still reading as
verified.

Refs #335
…very receipt

The closed port every transport is reached through, plus the two value types that
cross its boundary, plus a suite that makes the capability declaration falsifiable.

ChannelAdapter lists its entire callable surface. A method whose capability is
false still exists and answers with an `unsupported` receipt, so a caller can
never probe `if (adapter.editText)` and get an answer the declaration disagrees
with. Adapters are registered as factories, not instances, because a disabled
channel must not import its vendor SDK — discord.js alone costs ~48MB RSS. The
factory takes optional dependencies so the conformance suite can run real adapter
code against fixtures instead of the network.

InboundEnvelope is where vendor Context, Message and SlackEnvelope stop. It
requires accountId and a canonical target: without the account an ingress journal
cannot namespace an event key, and without the target the core would re-derive
per vendor where a reply goes. Three pure normalizers build it, each returning
null rather than inventing a value it cannot determine. They deliberately avoid
importing the bot modules, which run singletons and open SQLite at import time.

DeliveryReceipt replaces per-transport result shapes. `platformMessageId` is
nullable rather than optional so "no id issued" cannot be confused with "field
omitted", and `unsupported` is modelled as a refusal, not a failure: nothing was
dispatched, so `ambiguous` is false and no DeliveryFailure is attached.

Slack's silent keyboard downgrade is gone. Sending a keyboard to Slack used to
deliver the text with the actions dropped and report plain success, so a caller
could not tell fidelity had been lost. It now returns 501 unless the caller opts
in with `interactiveFallback: 'text'`, in which case the text is sent and the
downgrade is recorded on the result. The request normalizer is an allowlist, so
the opt-in is threaded through it explicitly — otherwise every HTTP keyboard send
would have become a refusal.

The capability matrix in structure/CAPABILITY_TRUTH_TABLE.md is now generated
from the declaration and checked byte-for-byte inside gate:truth-table-fresh. The
mirror rule that ties the table to the agbrowse repo is narrowed to the browser
section, so a messaging capability change no longer implies an edit there.

Refs #335
One persistent record of every inbound event, so "already handled" survives a
restart the way "still to handle" already does. The three channels had arrived at
three different answers: Telegram an offset frontier, Slack a ten-minute dedupe
table, Discord a set in memory that a restart forgets.

Nothing calls it yet. The channel migrations are separate units because the Slack
one reorders a vendor protocol handshake, and that risk should not ride along with
a schema.

Four decisions worth recording, three of which came out of auditing the plan
against the actual database rather than against its description.

The connection is injected, never imported. `src/core/db.ts` opens the real
database and runs its DDL as an import side effect, so a module that reaches for
that singleton cannot be tested against a temporary one. `TelegramUpdateOffsetStore`
already takes its connection as an argument; this follows it, and server bootstrap
hands the singleton in explicitly.

`append` accepts only a validated `InboundEnvelope`. Binding a raw vendor id would
be a silent corruption: a numeric Telegram `update_id` stores in a TEXT primary key
as "12345.0" while the same id as a string stores as "12345", so one logical event
becomes two rows and dedupe stops working without any error.

The journal mints its own trace id. This tree has no correlation-id producer — the
single `traceId` declaration in it has no consumers — so a NOT NULL column with no
upstream source would have pushed the obligation onto every future caller.

The retention sweeper is built so it cannot quietly rot. With no child tables yet,
a plain `DELETE ... WHERE tombstone_until <= now` would pass every test here and
keep passing, then start deleting parents of live outbound attempts the day an
outbox lands — a regression authored by a different milestone, which no test in
this one could catch. So children register a predicate saying when their rows are
finished, the sweep deletes child-before-parent because foreign keys are on, and a
boot guard walks `sqlite_master` and refuses to start if any table references the
journal without registering one. A test fires that guard against a synthetic child.

Completion nulls the payload and sets a tombstone in one transaction instead of
deleting the row: the payload is dead weight, but the fact that the event was
handled is the only thing a redelivery needs to read. A dead letter keeps its
payload, because replay needs the input.

Refs #336
First production caller of an M2 normalizer, and the first channel whose
restart behaviour comes from the shared journal rather than its own mechanism.

The ordering is the whole change. The poller advances its offset only after
`handleUpdateThroughFinalDelivery` resolves, so the update is appended and marked
processing before the handler runs and completed before the call returns. An
append that throws leaves the offset where it was and Telegram redelivers — which
is why the journal write goes first rather than alongside.

A redelivery that finds its row already present returns without re-running the
handler. That is the case the offset alone could never cover: it lives in the same
database write that the crash interrupted, so after a restart the offset says
"unhandled" while the journal knows better.

A handler failure marks the row back to received rather than dead-lettered.
Telegram is about to redeliver the update anyway, so the redelivery is the retry;
dead-lettering here would invent a manual recovery step for something the
transport already handles.

Updates the journal cannot identify — kinds carrying no chat or no sender — are
still handled as before. They have no conversation to dedupe against, and refusing
them would trade working behaviour for a guarantee they cannot use.

The poller sees a raw `Update`, not the grammY `Context` that `buildTelegramTarget`
consumes, so the routing fields are extracted from the update shapes that actually
arrive: message, edited message, channel post, and the message carried by a
callback query.

Tests assert the ordering through the poller rather than by reading the source: an
update runs once and advances the offset, a redelivery does not run twice, a
handler failure leaves the frontier untouched, and a numeric update id produces the
same journal key as its string form — bound raw it would land in the TEXT key as
"12345.0" and quietly become a second row.

Refs #336
Auditing M3b turned up a hole that would have lost messages.

`admitTelegramUpdate` treated any already-present journal row as a duplicate and
returned without handling the update. But a row sits in `processing` precisely
because an earlier run died between claiming the event and finishing it — and the
offset never advanced, so Telegram redelivers. That redelivery was the message's
last chance, and the old code dropped it while letting the offset move past it.

The append/claim ordering now lives in `durable-ingress` as `admitIngress` and
`settleIngress`, shared by every transport instead of rewritten per channel. Only a
`completed` row is a duplicate; anything else is admitted and re-claimed.
`markProcessing` accepts `processing` as a source state for the same reason.

The test previously built its own copy of the three-call ordering, so it would have
kept passing against a parallel implementation while production diverged. It now
calls the shipped functions, and the crash-recovery case fails without this fix.

Refs #336
Socket Mode acked first, before any work. That is fast and it is what the 3s
deadline pushes you toward, but it means an envelope whose record never reached
disk is one Slack considers delivered: it will not send it again, and a crash in
that window loses the message with nothing to show for it.

The order is now gate, durable append, ack, dispatch. A preflight runs before the
ack and must reach storage; throwing withholds the ack and recycles the socket, so
Slack redelivers on the new connection instead. The 3s budget is still met — the
preflight is one local SQLite insert — and the guard that leaves frames un-acked
during a reconnect window is untouched.

Three cases keep their old shape deliberately. An envelope type we never act on is
acked immediately without journaling, because Slack should stop retrying a payload
that has nowhere to go and the table should not fill with noise. A connection-local
duplicate is acked and dropped, since the work was done and only our ack failed to
land. And a caller that supplies no preflight — CLI paths, tests — keeps the
previous behaviour rather than silently gaining a dependency on the journal.

The preflight runs the same gate the dispatch path runs. Slack sends a `message`
copy and an `app_mention` copy of one mention under a shared ts; journaling the
copy the gate drops would claim the key and suppress the canonical delivery.

`IngressAckPolicy` gains `after-durable-append`. M2 pinned Slack to
`transport-first` with a comment saying the ack precedes any work, and that stopped
being true here. The policy describes what the transport does, so it moved when the
transport did.

Tests assert the ordering rather than the end state, because the failure this
guards against is invisible in the end state: ack-before-append and
append-before-ack look identical once both succeed.

Refs #336
…uthfully

Discord was the one channel whose memory of what it had handled died with the
process. A TTL set filtered duplicates inside one run; a restart forgot everything,
so a gateway Resume could replay a message that had already been answered.

Messages now go through the shared journal after the gates and the seen-set. The
seen-set stays as the hot in-process filter — it is cheaper than a query and it
still does its job — and the journal is what survives a restart.

Discord reads its account id differently from the other two, and that difference is
load-bearing. `client.user.id` is null before READY and can change across
reconnects, so it is read per message rather than captured at startup. When it
cannot be resolved the message is not admitted at all: handling it would run work
whose durability record could not be written, and dropping it quietly is the exact
failure this milestone exists to remove. The gateway still holds it and redelivers
on the next generation.

`discord.durableIngress` moves from false to true. That flag is checked by
`gate:truth-table-fresh`, so the generated capability matrix moves with it, and a
test asserts the claim against the behaviour rather than against itself: it handles
a message, restarts the journal, and requires the replay to be ignored.

`InteractionCreate` stays unjournaled. It has no normalizer yet and its consumer is
the approval binding in M4, so it belongs there rather than half-built here.

Refs #336
Three channels now write every inbound event to one SQLite table, and until
now nothing could read it. A dead letter was invisible. A stuck row had no
recovery path except hoping the vendor would send the same payload again.

jaw messaging ingress lists, shows, and replays those rows. Replay is
deliberately small: it marks the row received under a state CAS, records
who asked and why in an append-only JSONL, and then stops. Nothing in this
process re-runs the handler. The next vendor redelivery is what actually
executes it, through the same admitIngress path a crash recovery already
uses. Saying "queued" would have implied a worker that does not exist.

A completed event is refused unless forced, and --force still fails once
completion has dropped the payload — there is nothing left to run. A
processing row is refused rather than stolen from a live handler. The CAS
exists because a read-then-update here would reset a row another handler
had just claimed, and that is a worse incident than a refused replay.

Refs #336
…mand substrate

/new /stop /approve cannot share a meaning across three channels until the
identity they act on exists in one place. That identity is not the
process-local spawn owner token in session-persistence.ts — that dies with
the process. It is an integer on chat_sessions that survives a restart.

The column is additive. Existing databases get generation 0 through the
same PRAGMA table_info path every other chat_sessions column already uses.
Rebinding a conversation onto a session has to delete a leftover UNIQUE
row in the same transaction; otherwise two remotes would share one session
or the upsert would throw.

access-policy and RemoteCommandContext have no production caller yet. They
are the same kind of substrate the ingress journal was in M3a: tested,
default-deny, unused, so the first transport that needs them does not
invent a third shape.

Refs #337
… gate

The six names are now on the three catalogs. Only the new privileged four
go through RemoteCommandContext and access-policy. Putting default-deny
in front of executeCommand would have taken /status and /help to zero on
every live bot; those keep the channel allowlists they already had.

/stop interrupts the current conversation scope, not the process.
/queue list|drop reads that same scope. /deny is the public name for the
existing cancel transition and still needs a digest. Slack cannot ship
/status in its app manifest — Slack reserved the word — so the starter
set adds the other five and leaves /status to the shared parser.

Refs #337
…st on the wire

A button cannot ship jti+digest: Telegram and Discord will truncate it, and
the digest would sit in every transcript. The store now issues an opaque id
bound to actor, conversation, session generation, and approve|deny. The
text commands still require a digest. Restart still voids the in-memory
store the same way it always did.

Native keyboards are not routed yet. Slack interactive remains logged and
dropped; this commit only makes a later renderer have somewhere honest to
point.

Refs #337
The previous commit shipped the store API without the SoT sentence.
Native UI is still not routed.

Refs #337
The pending-dispatch DM was a wall of jti+digest and a request to type it
back. Telegram already had a callback router for elicitation, so the same
operator now gets Approve/Deny buttons whose callback_data is only an
opaque id. The digest never leaves the store. Slack and Discord stay text:
their send helpers cannot carry actions yet, and pretending they can would
be the silent downgrade we already banned.

The delivery test still sees one Telegram DM. The keyboard rides on that
same sendMessage.

Refs #337
Telegram already had Approve/Deny. Discord operator DMs were still a
wall of digest text because sendDiscordDm only posted content. The same
opaque appr:/aprd: ids now ride on that one DM as an action row, and
InteractionCreate routes the tap through handleApprovalCallback. Slash
commands are unchanged. Slack stays text until Block Kit send exists.

Refs #337
Telegram and Discord already had Approve/Deny. Slack still posted a wall
of digest text because sendSlackText could not carry blocks, and because
String(user) on an interactive payload is "[object Object]". The operator
DM now attaches the same opaque appr:/aprd: ids as Block Kit actions.
Events API text approve still works with a string user id. Generic Slack
keyboard send stays refused — that capability is still false.

Refs #337
The journal remembered events, not which conversation generation they
belonged to. After /new or /reset the vendor can still redeliver, and
admitIngress would have claimed the old row again. Rows now stamp
session_generation. A redelivery whose generation no longer matches is
refused and not marked processing. A crash mid-flight with the same
generation is still admitted — that is still the message's last chance.

The lookup goes through buildRemoteBindingKey, not envelope.conversationKey.
Those strings are not the same thing. effect-once.ts is still not a file.

Refs #338
The journal already minted trace_id. A second 128-bit id at admit time
would have given operators two names for the same delivery. log.event
now writes one JSON line onto the existing ring, and admitIngress enters
ALS with the stored journal identity so a later event in that turn can
stamp it. Metrics and a 22nd gate are still later work.

Refs #339
Operators had transport chips and no journal numbers. /api/health now
carries additive ingress counts and this-process metrics. Labels are
only channel, state, and result — actor and event ids stay on the
trace. jaw messaging doctor reads the same SQLite file locally; it
cannot see the server ring, and pretending otherwise would be a lie.

Refs #339
The three channels already had contract tests. Nothing in gate:all
required them, so a deleted fixture would still publish. The 22nd gate
re-runs those suites and writes a functional-certified artifact. It
refuses exactly-once wording and refuses to call this release-certified.
The twelve chaos cases are still not here.

Refs #339
In-memory unit tests never closed the database. After a real process
death the next boot opens the same jaw.db path. Completed stays
already_handled. Mid-flight keeps the stored trace_id. A later
generation is stale and is not claimed.

Refs #339
append looked, then inserted. A second connection that lost the race
used to throw SQLITE_CONSTRAINT and look like a broken delivery. The
primary-key clash is now the same duplicate the first connection would
have returned. A locked or full database still throws, so Slack can
refuse ACK instead of pretending the event landed.

Refs #339
Telegram already waited and retried the same send. Slack returned
ratelimited immediately, so a later operator retry could post chunks
that had already landed. A short Retry-After now waits and posts that
chunk once more. A long pause and not_in_channel still fail after one
call. The chunk loop does not start over.

Refs #339
admitIngress entered the messaging trace and nothing downstream read it,
so an operator could see an inbound event and its outbound reply as two
unrelated lines. sendChannelOutput now stamps outbound.send with the
same traceId. Outside a trace the field is simply absent — a send-side
id would be a second name for one delivery.

Refs #339
At-least-once ingress means a redelivery can reach a reset, an approval,
or an employee dispatch a second time. effect_claims is where the second
arrival learns the first already happened: a claim is owned by an id and
a token, and completing it requires both.

An expired lease is deliberately not an outcome. The owner may have died
just before or just after the external write, so expiry only lets a new
owner take the claim; deciding what happened belongs to a per-effect
reconciler, and manual is the honest terminal state until one does.

Non-terminal claims block the ingress sweep, so an unresolved effect
cannot lose the event that caused it. Wiring the six production effects
is the next unit.

Refs #336
Reserve-before-send means the outbound row exists before the vendor
call, so a crash between dispatch and receipt is a row in ambiguous
instead of a gap. ambiguous is terminal for the automatic path —
nothing in-process retries it, because retrying is exactly the
duplicate the state exists to prevent.

Refs #336
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 123 files, which is 23 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a9659c7-ecb4-4269-b55a-e26d242992f5

📥 Commits

Reviewing files that changed from the base of the PR and between 55ea58c and d82e932.

📒 Files selected for processing (123)
  • .github/workflows/postinstall-platform.yml
  • .github/workflows/publish.yml
  • .github/workflows/test.yml
  • AGENTS.md
  • CLAUDE.md
  • README.md
  • bin/cli-jaw.ts
  • bin/commands/doctor.ts
  • bin/commands/init.ts
  • bin/commands/messaging.ts
  • devlog
  • officecli
  • package.json
  • public/js/features/settings-channel.ts
  • public/js/features/settings-types.ts
  • public/js/features/transport-status-row.ts
  • public/manager/src/settings/pages/ChannelsDiscord.tsx
  • public/manager/src/settings/pages/ChannelsSlack.tsx
  • public/manager/src/settings/pages/ChannelsTelegram.tsx
  • public/manager/src/settings/pages/components/ChannelEnablementControl.tsx
  • public/manager/src/settings/pages/components/TransportStatusChips.tsx
  • scripts/docs/extract-commands.mts
  • scripts/generate-channel-capability-table.mts
  • scripts/promote-to-main.sh
  • scripts/release-gates.mjs
  • scripts/release-preview.sh
  • scripts/release.sh
  • scripts/require-release-evidence.mjs
  • scripts/validate-messaging-certification.mts
  • scripts/write-messaging-certification.mts
  • server.ts
  • src/cli/commands.ts
  • src/cli/handlers/remote-session-commands.ts
  • src/command-contract/catalog.ts
  • src/core/chat-sessions.ts
  • src/core/config.ts
  • src/core/db.ts
  • src/core/dispatch-approval-ingress.ts
  • src/core/dispatch-approval.ts
  • src/core/logger.ts
  • src/core/session-generation.ts
  • src/discord/bot.ts
  • src/discord/commands.ts
  • src/discord/forwarder.ts
  • src/discord/send-only-client.ts
  • src/messaging/access-policy.ts
  • src/messaging/approval-presentation.ts
  • src/messaging/channel-adapter.ts
  • src/messaging/channel-capabilities.ts
  • src/messaging/channel-health.ts
  • src/messaging/delivery-outcome.ts
  • src/messaging/durable-ingress.ts
  • src/messaging/effect-once.ts
  • src/messaging/inbound-envelope.ts
  • src/messaging/ingress-audit.ts
  • src/messaging/ingress-generation.ts
  • src/messaging/metrics.ts
  • src/messaging/outbound-outbox.ts
  • src/messaging/remote-command-context.ts
  • src/messaging/retry.ts
  • src/messaging/runtime.ts
  • src/messaging/send.ts
  • src/messaging/trace-context.ts
  • src/messaging/types.ts
  • src/routes/orchestrate.ts
  • src/routes/settings.ts
  • src/slack/bot.ts
  • src/slack/commands.ts
  • src/slack/format.ts
  • src/slack/manifest.ts
  • src/slack/send-handler.ts
  • src/slack/send-only-client.ts
  • src/slack/socket.ts
  • src/telegram/bot.ts
  • structure/AGENTS.md
  • structure/CAPABILITY_TRUTH_TABLE.md
  • structure/INDEX.md
  • structure/commands.md
  • structure/infra.md
  • structure/str_func.md
  • structure/telegram.md
  • tests/integration/messaging-ingress-restart.test.ts
  • tests/integration/settings-channel-switch.test.ts
  • tests/unit/approval-presentation.test.ts
  • tests/unit/channel-contract-conformance.test.ts
  • tests/unit/channel-delivery-guards.test.ts
  • tests/unit/channel-file-delivery-prompt.test.ts
  • tests/unit/channel-inbound-envelope.test.ts
  • tests/unit/default-runtime-migration.test.ts
  • tests/unit/delivery-outcome.test.ts
  • tests/unit/discord-dispatch-approval-delivery.test.ts
  • tests/unit/discord-ingress-journal.test.ts
  • tests/unit/dispatch-approval-callback.test.ts
  • tests/unit/docs-extract-commands.test.ts
  • tests/unit/electron-version-sync.test.ts
  • tests/unit/grammy-409-defense.test.ts
  • tests/unit/ingress-generation.test.ts
  • tests/unit/messaging-access-policy.test.ts
  • tests/unit/messaging-certification.test.ts
  • tests/unit/messaging-command-parity.test.ts
  • tests/unit/messaging-durable-ingress.test.ts
  • tests/unit/messaging-effect-once.test.ts
  • tests/unit/messaging-ingress-cli.test.ts
  • tests/unit/messaging-ingress-operations.test.ts
  • tests/unit/messaging-metrics.test.ts
  • tests/unit/messaging-outbound-outbox.test.ts
  • tests/unit/messaging-runtime.test.ts
  • tests/unit/messaging-trace-context.test.ts
  • tests/unit/release-gates.test.ts
  • tests/unit/release-scripts-contract.test.ts
  • tests/unit/remote-command-context.test.ts
  • tests/unit/safe-install.test.ts
  • tests/unit/send-validation.test.ts
  • tests/unit/service-reboot-hardening.test.ts
  • tests/unit/session-generation.test.ts
  • tests/unit/settings-runtime-gate-persistence.test.ts
  • tests/unit/settings-watch.test.ts
  • tests/unit/slack-ack-ordering.test.ts
  • tests/unit/slack-init-behavior.test.ts
  • tests/unit/slack-operator-tooling.test.ts
  • tests/unit/slack-outbound.test.ts
  • tests/unit/slack-setup.test.ts
  • tests/unit/telegram-ingress-journal.test.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lidge-jun
lidge-jun marked this pull request as ready for review August 14, 2026 00:13
@lidge-jun
lidge-jun merged commit 02ad4e1 into main Aug 14, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Epic] Messaging Runtime v2 — 3채널 동시 실행·내구성·릴리스 무결성

1 participant