Skip to content

feat(slack): partner follow-ups — per-agent bots, per-channel consent, completion report-back (ent#222/#223/#224) - #1763

Merged
dolho merged 11 commits into
devfrom
feat/slack-partner-followups
Jul 24, 2026
Merged

feat(slack): partner follow-ups — per-agent bots, per-channel consent, completion report-back (ent#222/#223/#224)#1763
dolho merged 11 commits into
devfrom
feat/slack-partner-followups

Conversation

@dolho

@dolho dolho commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

The OSS half of the three Slack issues from the 2026-07-23 design-partner call. Consolidated into one PR because they are one body of work: they share a migration chain, ent#224 is gated by ent#223's consent flag, and all three touch the same Slack path.

Pairs with trinity-enterprise#232 (the private module: bindings, transport manager, cascade). Two PRs total — one per repo. The repo boundary is the only reason this isn't a single PR.

Supersedes #1761, #1762, #1758 (closed in favour of this).


ent#223 — per-channel proactive consent

Consent was per-recipient (agent_sharing.allow_proactive, keyed by verified email). In an open Slack workspace nobody authenticates, so there was no recipient to opt in — and channel posts had no consent gate at all.

  • slack_channel_agents.allow_proactive, dual-track (SQLite + Alembic 0030)
  • Split default so nothing silently flips: NEW bindings deny; EXISTING bindings backfilled to allow (they worked ungated before). Backfill runs only when this caller added the column, so a later explicit OFF survives re-runs.
  • Gate sits between the 404 (not bound) and 429 (rate capped) → three distinguishable reasons; returns named proactive_not_allowed
  • Owner-gated toggle endpoint + the switch in SlackChannelPanel.vue
  • MCP send_group_message now reports why: consent_required / rate_limited / not_bound

ent#224 — completion reported back to the originating channel/thread

The partner's bug: agent delegates a ~10-min job, it succeeds, nobody tells the user. Two things were missing (the issue called it "a join" — it wasn't):

  1. Channel context didn't survive delegation — the A→B path never forwarded source_channel*, so B had no destination. Added parent_execution_id + inheritance, and an optional execution_id on MCP chat_with_agent.
  2. Nothing reported at the terminal — new channel_completion_report, hooked at both CAS-won chokepoints beside spawn_task_terminal_event (feat: system-emitted agent.task.completed/failed events at execution terminal (async caller report-back) #1578). Success and failure.

⚠️ The load-bearing safety rule: a direct channel turn is synchronous and already replies inline (triggered_by == the channel name). Reporting those would duplicate every normal Slack reply in a customer workspace, so reporting is restricted to executions that inherited context. Consent-gated, at-most-once via effect_guard keyed on the resolved destination.

ent#222 — per-agent bot identities (OSS half)

  • adapters/per_agent_bot.pyinert-by-default hook registry; with no resolver registered every path is a no-op and routing is byte-for-byte unchanged. Both hooks fail open.
  • message_router resolves a per-agent bot over the channel binding; slack_adapter stamps the receiving bot identity
  • SlackAgentBotPanel.vue — entitlement-gated config UI (resolved identity shown; tokens write-only)

Verified on a live PostgreSQL stack (not just unit tests)

25 unit tests, plus a real deployment:

  • Migrations applied on real PG; single linear Alembic head (00300029_product_events)
  • ent#223: consent OFF → 403 · unbound → 404 · toggle ON → gate passes · toggle OFF → 403 returns
  • ent#224: delegated terminal posted into the originating thread; replay through the real effect_guard → no second send; inline turn → no send
  • ent#222: real Slack auth.test reached Slack and mapped to named invalid_bot_token; cascade proven by a real rename (binding followed the agent)

🔴 A boot-breaking bug this deployment caught

0029_slack_channel_allow_proactive collided with 0029_product_events (already on dev) — two Alembic heads → upgrade head fails → backend crash-loops. Reproduced (HTTP 000), then fixed by rebasing to 0030. No unit test could have found this. My eval PR #1752 had the identical defect; it's fixed there too (0031, chained off 0030) — merge #1752 after this.

Honest limits

  • No live Slack workspace — sends are mocked. Socket connect, inbound routing, and the actual post are unproven end-to-end; needs a real per-agent Slack app from the partner.
  • MCP TypeScript changes couldn't be compiled offline; the UI compiles under Vite but wasn't clicked through in a browser.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

@dolho

dolho commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

/review report — feat/slack-partner-followupsdev

Files: 25 (+1284/−12) · Merge-base: 49b235c7 · Scope: CLEAN (every file traces to ent#222/#223/#224; no drift)

Self-review caveat: I wrote this code, so I weighted findings toward "assume I got it wrong." Two criticals surfaced — both fixed in this PR, both invisible to the existing tests because those covered the db/service layer, not the principal or the egress.


🔴 C1 — An agent could grant ITSELF proactive consent (Confidence 9/10) — FIXED

File: src/backend/routers/slack.py (set_slack_channel_proactive)

if not db.can_user_share_agent(current_user.username, name):
    raise HTTPException(status_code=403, ...)

Issue: dependencies.py:423 states it plainly — "Agent-scoped keys resolve to the owner user on REST, which incidentally made sharing, permission grants, rename, and credential ops reachable by an agent." So this check passes for an agent's own key. An agent could therefore flip on the exact control ent#223 exists to enforce, making consent self-serve and the whole feature decorative.

Why it matters: same class as the retention-acknowledge trap (trinity-ops-agent#232), where require_admin alone was insufficient for the same reason.

Fix applied: reject_agent_principal(current_user) before the owner check. Granting consent is a human decision; sending under it stays agent-callable (unchanged).


🔴 C2 — Unsanitised failure text posted into a customer Slack channel (Confidence 9/10) — FIXED

File: src/backend/services/channel_completion_report.py (_summarize)

body = (summary_or_error or "").strip()
if len(body) > _MAX_REPORT_CHARS:
    body = body[:_MAX_REPORT_CHARS].rstrip() + "…"

Issue: the #1578 emit chokepoint credential-sanitises summary_or_error before egress precisely because a failure terminal can carry secrets (event_dispatch_service.py:311). This path truncated but never sanitised — so a token in an error message would be posted verbatim into a persistent, externally-hosted, human-visible surface. Strictly worse than the #1578 case it was modelled on.

Compounding it: the bare slice is the pattern #1578 explicitly warns against — cutting a secret in half can stop the redaction pattern matching, so it survives.

Fix applied: sanitize_text over a 2× window before truncating — same order as #1578. Regression test asserts a bot token in failure text never reaches the posted message.


🟡 Informational

  • I1 — 0030 backfill is intentional but load-bearing: it flips every existing binding to allow_proactive = 1. Correct (posts were ungated before, so denying would break live integrations) and guarded so a later explicit OFF survives re-runs — but it is a data-touching migration, worth a reviewer's eye.
  • I2 — #1649 stub was a false green: test_slack_router_does_not_persist_a_failed_send asserts pytest.raises(HTTPException), and the new consent 403 is one — so it passed without ever reaching the 502 path it exists to cover. CI could not flag this (it stayed green). Fixed by marking the stub consented.

✅ Clean (verified, not assumed)

  • SQL safety — no string interpolation introduced; all new queries use text() with bound params
  • Credential exposure in logs — reporter logs team_id/execution_id/channel only, never a token
  • CAS-won placement — both ent#224 hooks sit inside the CAS-won branches beside spawn_task_terminal_event, so a lost CAS reports nothing
  • Idempotencyeffect_guard keyed on the resolved destination, never the generated body; verified against the real table (replay → no second post)
  • The no-double-post rule — inline channel turns (triggered_by ∈ slack/telegram/whatsapp) never report; proven live and in tests

Summary

Critical: 2 — both fixed in this PR · Informational: 2 · Scope: clean

@dolho

dolho commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

/review — re-run after fixes

Both criticals are now verified present on the branch (2a0e8fdf), not just claimed:

Status Evidence on origin
C1 agent could self-grant consent ✅ Fixed reject_agent_principal(current_user) runs before the owner check in set_slack_channel_proactive
C2 unsanitised failure text → Slack ✅ Fixed sanitize_text(window) over a 2× window before truncation

Tests: 14 green, including two new regressions — a static guard that the toggle keeps its human-only check, and one asserting a bot token in failure text never reaches the posted message.

Three things the re-review caught that the first pass did not

1. My first "fixed and pushed" was false. The review worktree was in detached HEAD, so git push origin feat/... pushed a stale local branch while my commit went nowhere — and my success message came from an echo that runs unconditionally. The remote had zero of the fixes when I reported them as landed. Now verified by reading the pushed files back from origin rather than trusting the push command.

2. My own C2 fix had a latent flaw. The first version decided truncation with len(raw) > len(body) — but sanitize_text changes length, so a short redacted message would get a phantom "…" implying content was cut when it wasn't. Truncation is now decided on what was actually cut, with a dedicated test.

3. Push protection rejected the regression test — it hardcoded a realistic xoxb-… literal in a public repo, which is the "hardcoded credential in a test" this very checklist forbids. The token is now assembled at runtime, so no literal is committed while the sanitiser still sees a well-formed token.

Unchanged from the first pass

Informational: the 0030 backfill is intentional but data-touching; the #1649 stub was a false green (fixed). Clean: SQL safety, credential-free logging, CAS-won hook placement, effect_guard idempotency, and the no-double-post rule.

Critical: 0 outstanding · CI re-running on 2a0e8fdf.

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated via /validate-pr (paired with trinity-enterprise#232). Approving. Since dev's required checks run no unit tests, I ran all four touched test files locally on this branch: 41 passed (test_222_per_agent_bot_seam, test_223_slack_channel_proactive_consent, test_224_channel_completion_report, updated test_1649). Load-bearing integration points verified against the real code, not just the diff:

  • effect_guard usage matches the real #1084 API exactly (async CM, g.replay/g.snapshot, EffectInProgressError before yield); db.get_execution returns the ScheduleExecution model (attribute access safe, source_channel* fields present); send_message_detailed kwargs match.
  • Dual-track migration complete and correctly chained: schema.py + tables.py + SQLite migrations.py + Alembic 0030 revising 0029_product_events (the real dev head) — the schema-parity blind spot covered.
  • Consent posture is the right one and is proven: new-binding-denies + existing-backfilled-to-allow tested against a real legacy table, including idempotency (an operator's explicit OFF survives re-runs).
  • The human-only consent toggle (reject_agent_principal before the owner check — access-first, self-uniform per Invariant #8) plus its guard test and the learnings entry: exactly the right response to the self-grant class.
  • Sanitize-over-2x-window-before-truncate in the completion reporter matches the #1578 chokepoint discipline, with the phantom-ellipsis edge tested. The seam is inert-by-default and fail-open (failure-injection tested); router early-return conforms to _resolve_agent_and_token's Optional[Tuple[str, str]] contract. Three MCP surfaces updated in sync (Invariant #13).

Non-blocking items:

  1. learnings.md has an accidental duplicate line — the ent#183 tar Lesson is repeated verbatim (line ~132). Trivial; fix on the branch or in a follow-up.
  2. Manual status bump needed: all three issues are cross-tracker (ent#222/#223/#224), so no auto-promotion — set status-in-dev on them after merge, manual close at release.
  3. Known residual to name somewhere visible: with a dedicated bot AND the workspace bot in one channel, an un-@mentioned thread-reply message event is delivered to both apps and can produce two turns (the design's Phase-3 "shared-channel disambiguation tests" are deferred, but Phase 2 + wiring ship now, so the window is live for entitled installs).
  4. Behavioral caveat worth documenting: the completion report requires the executing agent (B) to be bound to the originating channel with consent — a non-channel-bound worker's completion still won't report. Defensible consent posture, but it means the classic router-agent topology stays silent.
  5. architecture.md's adapters catalog doesn't list the new adapters/per_agent_bot.py seam (one line), and the requirements area file wasn't touched for the new OSS-visible capability. Docs-only; fine as a follow-up given the enterprise-docs privacy split.

Sequencing: either order vs ent#232 is safe (both sides guard); the submodule pointer bump at the next release must carry both.

@dolho

dolho commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Enterprise submodule pointer bumped — ent#222 is now active in this PR

trinity-enterprise#232 merged to enterprise main (6f073c7), so this PR now carries the pointer bump that activates it:

-Subproject commit 594b0afd…
+Subproject commit 6f073c73…

Why it had to be in this PR: without it the OSS half here ships inertadapters/per_agent_bot.py has no resolver registered, so per-agent routing silently doesn't exist and ent#222 would look like it just didn't work (the ent#185 class). This was the third step I flagged in the merge plan; folding it in removes the chance of it being missed.

Verified 6f073c73 carries:

  • backend/slack_per_agent_bots/ — module, transport manager, cascade wiring
  • backend/migrations/versions/0012_slack_agent_bindings.py — the enterprise Alembic revision

It is a direct child of 594b0afd (which dev already had), so nothing else rides along.

Merge plan is now two steps, not three: merge this PR → then #1752 (its Alembic 0031 chains off 0030 here).

dolho and others added 11 commits July 24, 2026 13:53
Proactive consent was modeled per RECIPIENT (agent_sharing.allow_proactive, keyed
by verified email). In an open Slack workspace users never authenticate, so no
recipient record exists to opt in and a Slack-facing agent had no sanctioned path
to post unprompted — channel posts had no consent gate at all, only the owner
check + #1609 rate caps. For Slack the consent unit is the CHANNEL BINDING.

- slack_channel_agents gains allow_proactive; dual-track migration (SQLite
  db/migrations.py + Alembic 0029) plus schema.py DDL and tables.py MetaData.
- Default posture, deliberately split so nothing silently flips:
    NEW bindings      -> 0 (deny). Binding an agent is not itself consent.
    EXISTING bindings -> backfilled to 1 (allow), because channel posts worked
                         with no gate before this change.
  The backfill runs only when this caller added the column (_safe_add_column
  returns True), so an operator's later explicit OFF survives re-runs.
- Enforcement in send_agent_slack_channel_message sits between the 404 (not
  bound) and the 429 (rate capped) so the three refusal reasons stay
  distinguishable, and returns a NAMED, actionable code (proactive_not_allowed)
  the agent can relay instead of a generic failure. Rate caps still apply on top.
- New owner-gated PUT /api/agents/{name}/slack/channels/{channel_id}/proactive
  so the flag is toggleable rather than API-only.

8 unit tests: default-deny for new bindings, flag present on both read paths,
toggle on/off + scoping, unknown-binding miss, and the migration's no-silent-flip
backfill + post-migration default + idempotency.

Related to ent#223

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ason, docs (ent#223)

Completes the remaining ent#223 acceptance criteria on top of the backend gate:

- GET /api/agents/{name}/slack/channel now returns allow_proactive so the panel
  can render the current state (it was write-only before).
- SlackChannelPanel.vue gains an "Allow proactive messages" switch that calls
  PUT .../slack/channels/{channel_id}/proactive and re-reads the binding, so the
  control always reflects what the backend stored. This is what makes the flag
  toggleable rather than API-only.
- MCP send_group_message now reports WHY a send was refused: consent_required
  (with an actionable hint), rate_limited, or not_bound — the three causes the
  backend already keeps distinguishable (403 / 429 / 404). Previously a consent
  refusal surfaced as a generic error the agent could not act on.
- docs/memory/feature-flows/proactive-messaging.md records the two consent units:
  DM = per-recipient verified email, Slack channel = per-channel binding, plus
  the split default posture and the refusal semantics.

Related to ent#223
…t#223)

0029_slack_channel_allow_proactive declared down_revision = 0028_agent_reminders,
but dev ALREADY carries 0029_product_events off that same parent. Two revisions
sharing a parent are two Alembic heads, and `upgrade head` then fails with
"Multiple head revisions are present" — which aborts init_database and
CRASH-LOOPS THE BACKEND on boot. Caught by deploying the branch to a real
PostgreSQL stack; no unit test would have seen it.

Renamed to 0030_slack_channel_allow_proactive chained off 0029_product_events,
so the line is linear again. Verified on a live stack: backend boots, alembic
head = 0030_slack_channel_allow_proactive, and the allow_proactive column is
present on slack_channel_agents.

Related to ent#223
…hread (ent#224)

The reported failure: a user asks in Slack, the agent delegates a ~10-minute job
to another agent, the job succeeds — and nobody tells the user. The trigger side
worked; the completion side died silently.

Two things were actually missing, and both are fixed here:

1. Channel context did not survive delegation. The A->B path never forwarded
   source_channel*, so B's row had no destination and B's terminal had nowhere to
   report. ParallelTaskRequest gains parent_execution_id; chat_execution_service
   copies the parent's channel/chat/thread down to the child (fail-open — a miss
   changes nothing). MCP chat_with_agent gains an optional execution_id so a
   delegating agent can pass its own execution, mirroring the #1084 effect tools.

2. Nothing reported at the terminal. channel_completion_report posts the outcome
   to the originating channel/thread, hooked at both CAS-won terminal chokepoints
   beside spawn_task_terminal_event (#1578) — success AND failure, because a
   silent failure is the bug being closed.

The no-double-post rule is the load-bearing part: a direct channel turn is
synchronous and the adapter already replies inline, and those rows carry
triggered_by == the channel name. Reporting them would duplicate every normal
Slack reply in a customer workspace, so we report ONLY when the context was
INHERITED (triggered_by not in {slack, telegram, whatsapp}).

Gated by ent#223 per-channel consent, at-most-once via effect_guard (#1084) keyed
on the resolved destination (never the generated body), Slack-only in v1, and
fail-soft throughout — a reporting failure never disturbs an execution that
already completed and was billed.

12 unit tests: the double-post guard across all three channel triggers, delegated
reporting into the right thread, consent + unbound gates, no-context/non-Slack/
missing-row no-ops, failure terminals reporting, replay not re-posting, and the
never-raises guarantee.

Related to ent#224
The edition-agnostic seam that lets the enterprise slack_per_agent_bots module
(trinity-enterprise#222) route an inbound Slack event to the agent whose
DEDICATED bot received it, and reply through that bot's own token — coexisting
with the workspace-level single bot.

- adapters/per_agent_bot.py — inert-by-default hook registry (set_resolver /
  set_token_provider). With no resolver registered (OSS-only, or before the
  enterprise module loads) every path is a no-op and channel routing is
  byte-for-byte unchanged. Both hooks FAIL OPEN — an error falls back to normal
  routing, so a bug can never take Slack offline.
- message_router._resolve_agent_and_token — consult the per-agent resolver
  first; a match wins over the channel binding and uses the per-agent bot token.
  Falls through to channel routing when no binding / no token.
- slack_adapter.parse_message — stamp the RECEIVING bot identity
  (authorizations[0].user_id + api_app_id + team_id) into message.metadata so
  the resolver can key on it. Additive; inert for every non-Slack channel.

Mirrors the connector seam (#118): OSS owns the seam, the enterprise module owns
the policy (it registers the resolver + token provider at startup).

5 unit tests: inert-by-default, resolver reads recipient bot, non-Slack ignored,
both hooks fail-open, token provider. The end-to-end path (real Slack event ->
per-agent routing) needs a live per-agent Slack app to verify — the recipient-bot
extraction from authorizations is the live-verification point.

Related to trinity-enterprise#222

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SlackAgentBotPanel: configure an agent's dedicated Slack bot from the Sharing tab,
mounted beside the existing workspace SlackChannelPanel.

- Entitlement-gated on `slack_per_agent_bots` via the enterprise store, so it is
  hidden entirely in OSS / unentitled builds — never a blank or broken section.
- Paste bot (xoxb-) + app-level (xapp-) tokens; on save the backend validates
  them against Slack auth.test and the panel then shows the RESOLVED identity
  (bot name, bot_user_id, team) rather than echoing anything secret. Tokens are
  write-only — a read never returns them.
- Enable/disable without re-entering credentials, replace tokens, and remove.
- Refusals surface the backend's NAMED code (wrong token type, bot already bound
  to another agent, Slack unreachable) instead of a generic failure, so the
  operator knows what to fix.

Verified the component compiles under Vite (HMR, no build errors); the flows
themselves still need a live per-agent Slack app to exercise end to end.

Related to trinity-enterprise#222
…nt#224)

The e2e job failed the mcp-server image build:

  src/tools/chat.ts(296,9): error TS2353: Object literal may only specify known
  properties, and 'parent_execution_id' does not exist in type '{...}'

chat.ts passed parent_execution_id into apiClient.task(), but the client's
options type never declared it, so tsc rejected the call and the Docker build
exited 2.

Two changes, because the type fix alone is not enough: the request body in
client.ts is assembled field-by-field rather than spread, so the property would
have been silently DROPPED at runtime even once it compiled — the delegated task
would never have inherited the channel context and ent#224 would have been a
no-op in production. Declared it on the options type AND forwarded it into the
body.

Verified with the same toolchain CI uses: tsc --noEmit clean, npm run build
exit 0, 87/87 mcp-server tests pass.

Related to ent#224
The base-vs-head regression diff flagged two new failures:

  test_1649_group_message_history.TestRoutersPersist
    ::test_slack_router_persists_at_the_posted_ts
    ::test_slack_router_uses_the_parent_thread_when_replying

Both stub get_slack_channels_for_agent with a binding dict that has no
allow_proactive, so the ent#223 consent gate refuses the post with 403 and the
send — the thing these tests actually assert on — never happens. The tests cover
#1649 persistence, not consent, so the fixture should represent a CONSENTED
channel.

Also fixed the third stub in the same class, which the diff did NOT flag because
it was passing for the wrong reason: test_slack_router_does_not_persist_a_failed
_send asserts pytest.raises(HTTPException), and the consent 403 is an
HTTPException — so it short-circuited before ever reaching the 502 send-failure
path it exists to cover. A false green is worse than a red.

Related to ent#223
…#224)

Both findings in this PR were classes, not one-offs, and /autoplan reads this
file before planning:

* a capability-GRANTING endpoint must reject agent principals — ownership and
  role checks all pass for an agent's own key, because it resolves to the owner;
* a new egress sink inherits the payload's credential hazard but not the
  sanitisation the original sink applied.

Related to ent#223, ent#224
…re Slack (ent#223/#224)

Two critical findings from /review, both invisible to the existing tests, which
covered the db/service layer but neither the PRINCIPAL nor the EGRESS.

C1 — an agent could grant ITSELF proactive consent. set_slack_channel_proactive
gated only on can_user_share_agent, but an agent-scoped key resolves to the OWNER
on REST (dependencies.py:423), so that check passes for the agent's own key. The
agent could flip on the exact control ent#223 adds, making consent self-serve.
Added reject_agent_principal: granting consent is a human decision; SENDING under
it stays agent-callable. Same class as trinity-ops-agent#232.

C2 — unsanitised failure text was posted into a customer Slack channel. The #1578
emit chokepoint credential-sanitises summary_or_error before egress precisely
because a failure terminal can carry secrets (event_dispatch_service.py:311).
This sink reused that payload, truncated it, and never sanitised — so a token in
an error message would be posted verbatim to a persistent, externally hosted,
human-visible surface. Now sanitises over a 2x window BEFORE truncating, the same
order #1578 uses, because a bare slice can cut a secret so the pattern no longer
matches and it survives.

Truncation is decided on what was actually cut rather than by comparing against
the raw input: redaction changes length, so the naive comparison appends a
phantom ellipsis to text that was never truncated. Covered by its own test.

Related to ent#223, ent#224
…agent Slack bots (ent#222)

trinity-enterprise#232 merged to enterprise main (6f073c7), so dev's pointer at
594b0afd predates the private slack_per_agent_bots module. Without this bump the
OSS half in this PR ships INERT: adapters/per_agent_bot.py has no resolver
registered, so per-agent routing silently does not exist and ent#222 would look
like it simply did not work — the ent#185 class.

Pointer moves 594b0afd -> 6f073c73, which carries backend/slack_per_agent_bots/
(module + manager + cascade wiring) and the enterprise Alembic 0012
slack_agent_bindings revision. Also picks up nothing else: 6f073c7 is a direct
child of 594b0afd, which dev already had.

Related to ent#222
@dolho
dolho force-pushed the feat/slack-partner-followups branch from 9106830 to 70ce676 Compare July 24, 2026 10:54
@dolho
dolho merged commit bc546b6 into dev Jul 24, 2026
24 checks passed
vybe pushed a commit that referenced this pull request Jul 31, 2026
…t#224 debt (ent#265)

- requirements/public-access.md: NEW §15.1h "Channel Completion Report-Back
  (CHANNEL-REPORT — ent#224 Slack, ent#265 Telegram)" — generic mechanism
  (inherited-context-only, D0 row-creation persistence + provenance guard,
  binding-agent resolution, chokepoints incl. D3, effect-guard at-most-once,
  sanitize-before-truncate), per-channel consent units, the two-DM-consent-
  regimes rationale (F6), known v1 limits, the deliberate ungated
  proactive-send scope cut. (§15.1f was already taken by WHATSAPP-001 —
  plan's placement kept, id shifted to h.)
- feature-flows/channel-completion-report.md: NEW flow doc — entry points,
  D0/D3 fixes, chokepoint coverage table with the v1 boundaries
  (lease-reaper, bulk sweeps, pull sink, operator-terminate, restart
  mid-inline-turn, FAILED→SUCCESS resurrection, fan-out per-child,
  pre-migration NULL rows), destination/consent resolution per channel,
  D1 identity design with every rejected alternative, failure modes,
  Testing. Pays the #1763 flow-doc debt (ent#224 shipped undocumented).
- feature-flows.md: Recent Updates row + Collaboration-table entry.
- telegram-integration.md: ent#265 section (column + toggle + pointer) +
  revision row.
- task-completion-events.md: sibling-spawn paragraph (report rides beside
  the #1578 emit at the same CAS-won chokepoints incl. the failure applier).
- architecture.md: services-catalog entry for channel_completion_report.py,
  schedule_executions DDL line for source_channel_agent, telegram DB-module
  line mentions allow_proactive.
- learnings.md: the D0 class — a threaded parameter only one callee branch
  consumes is a severed wire; mock-row suites are blind to it AND to its
  facade-passthrough sibling (caught pre-merge by the row-read-back tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants