Skip to content

tidy: centralize owner activity policy - #2492

Merged
john-the-dev merged 5 commits into
sonichi:mainfrom
marklysze:tidy/bridge-lifecycle
Aug 7, 2026
Merged

john-the-dev merged 5 commits into
sonichi:mainfrom
marklysze:tidy/bridge-lifecycle

Conversation

@marklysze

@marklysze marklysze commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator

What changed, and why?

Moves the shared last-owner-activity.json schema, 80-character summary bound,
collision-safe publication, and fail-open error contract into
src/owner_activity.py. Discord, Slack, and Telegram keep their existing
adapter function names as thin wrappers, so call sites and provider behavior do
not change.

This is one tidy: concern extracted from the earlier bundled version of this
PR. Presenter mode is already #2501, proactive recovery is #2504 (merged), and
archive policy is a separate queued child.

Review first

  1. src/owner_activity.py
  2. the three write_owner_activity adapter wrappers
  3. tests/owner-activity-policy.test.py

User behavior or bug proved

N/A — behavior-preserving refactor. The production writer test pins the payload,
summary bound, optional channel ID, fail-open callback, and real same-process
thread contention.

Feature/documentation consistency

No user-visible feature changed. The ownership/concurrency rule is recorded in
CLAUDE.md and docs/architecture-boundaries.md; AGENTS.md and
docs/src-map.md were regenerated.

Before / after evidence

Parent 0dfa60e7 had three copies of the publication recipe:

$ git grep -n 'OWNER_ACTIVITY_FILE.with_suffix' 8d2ea24f^ -- 'src/*bridge.py'
8d2ea24f^:src/discord-bridge.py:359:        tmp = OWNER_ACTIVITY_FILE.with_suffix(f".json.{os.getpid()}.{uuid.uuid4().hex}.tmp")
8d2ea24f^:src/slack-bridge.py:203:        tmp = OWNER_ACTIVITY_FILE.with_suffix(f".json.{os.getpid()}.{uuid.uuid4().hex}.tmp")
8d2ea24f^:src/telegram-bridge.py:208:        tmp = OWNER_ACTIVITY_FILE.with_suffix(f".json.{os.getpid()}.{uuid.uuid4().hex}.tmp")

HEAD has one policy owner and three thin delegations:

$ git grep -n '_write_owner_activity_shared' 8d2ea24f -- 'src/*bridge.py'
8d2ea24f:src/discord-bridge.py:81:from owner_activity import write_owner_activity as _write_owner_activity_shared  # noqa: E402
8d2ea24f:src/discord-bridge.py:338:    _write_owner_activity_shared(
8d2ea24f:src/slack-bridge.py:66:from owner_activity import write_owner_activity as _write_owner_activity_shared  # noqa: E402
8d2ea24f:src/slack-bridge.py:190:    _write_owner_activity_shared(
8d2ea24f:src/telegram-bridge.py:48:from owner_activity import write_owner_activity as _write_owner_activity_shared  # noqa: E402
8d2ea24f:src/telegram-bridge.py:194:    _write_owner_activity_shared(

Tests

$ python3 tests/owner-activity-policy.test.py
Ran 4 tests in 0.767s
OK

$ python3 tests/owner-activity-atomic-write.test.py
Ran 4 tests in 5.324s
OK

$ python3 tests/owner-activity-channel-id.test.py
Ran 2 tests in 0.521s
OK (Slack dependency case skipped locally)

$ python3 tests/gateway-owner-presence-peer-gate.test.py
PASS (6/6)

$ python3 tests/gateway-per-sender-tier.test.py
Results: 13/13 passed

$ bash scripts/agents-md-sync.sh --check
agents-md-sync: AGENTS.md is up to date

$ python3 scripts/gen-src-map.py --check
gen-src-map: docs/src-map.md is up to date

$ git diff 8d2ea24f^ 8d2ea24f | bash scripts/review-checks.sh
review-checks: PASS (hardcoded-paths clean)

Live post-restart round trip

Done at the current head bf972a85, on Slack (the non-Discord adapter, so the
wrapper wiring is what gets exercised). Full transcript in
#2492 (comment).

Summary of that evidence:

  • branch-built worktree at bf972a85, with sutando.config.local.json copied in so
    the bridge resolves the real workspace rather than <worktree>/workspace/;
    process identity confirmed via lsof cwd, not assumed;
  • an owner DM moves last-owner-activity.json from channel: "ag2space" (written by
    a different surface) to channel: "slack" through the shared writer;
  • the bridge writes an owner-tier task envelope;
  • the result is delivered back to the Slack DM exactly once, with tasks/ and
    results/ clean afterwards and no orphaned .sending claim.

Stated plainly there and repeated here: no core/watcher was running on that host, so
the result file was injected by hand. The activated paths this refactor changes —
bridge ingress, shared-writer delegation, task envelope, outbound delivery — are all
covered; the agent that would normally author the result is not under test.

The older Discord/Slack transcripts against e3c6191c are superseded and should not
be read as current-head proof.

Dependency / merge order

Resolved. The content dependency #2504 (tidy: centralize proactive recovery policy) merged on 2026-08-03, so the #2504 → #2492 ordering this section
previously described is satisfied and no longer gates anything.

This branch has since been rebased onto current main (80e161d9); head is
bf972a85. Gates and the live round trip were rerun at that exact head — see the
section above and
#2492 (comment).

Edge cases and risk

  • missing/empty channel IDs are omitted;
  • summaries are capped at 80 characters;
  • PID + UUID staging is safe across processes and same-process threads;
  • write and logging failures cannot interrupt bridge intake.

No migration, config, permission, prompt, or protocol change. Rollback is a
single commit revert. Sparrow's writer intentionally remains separate because
it additionally applies sender-tier authorization, fleet-agent exclusion,
gateway-attribution stripping, and secret redaction. The architecture docs now
make that trust-boundary exception explicit, and its existing security tests
were rerun above.

@github-actions

github-actions Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 77%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • src/discord-bridge.py (100%)
  • src/owner_activity.py (100%)
  • src/slack-bridge.py (100%)
  • src/telegram-bridge.py (100%)

Summary

  • Total: 30 lines
  • Missing: 0 lines
  • Coverage: 100%

@marklysze
marklysze force-pushed the tidy/bridge-lifecycle branch 2 times, most recently from e664e90 to e3c6191 Compare August 1, 2026 20:21
@marklysze
marklysze marked this pull request as ready for review August 1, 2026 20:54
@marklysze

Copy link
Copy Markdown
Collaborator Author

Live-path evidence for current head e3c6191c228272a4da36a08e3deabee08ff19f88:

I restarted the Discord bridge directly from this PR's worktree, then sent a unique proactive probe through the configured owner DM:

Discord bridge ready: mark-sutando#4497
  [proactive] sent to owner: PR #2492 live round-trip check. Please reply exactly: tidy-live-1785617521

The owner replied with the exact token. The patched bridge received it and created an owner-tier task:

  [msg] #DM @msze_: tidy-live-1785617521 (mentions: [], is_dm: True, embeds: 0, type: MessageType.default, ref: False)
  @msze_: tidy-live-1785617521
  [task-write] wrote task-1785617549385.txt (@msze_, #DM, tier=owner)

The task recorded source: discord, interaction_type: message, channel_name: DM, access_tier: owner, and the expected token. I then wrote its result and observed delivery through the return path:

  Replied: PR #2492 live round-trip passed: received tidy-live-1785617521 and returned this...

The temporary PR-branch bridge was stopped after the test, restoring the pre-test process state.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Slack live-path evidence for current head e3c6191c228272a4da36a08e3deabee08ff19f88:

I started src/slack-bridge.py directly from this PR's worktree. Socket Mode connected successfully, and the bridge delivered a unique probe to the configured owner DM:

Slack bridge started. Socket Mode connecting...
⚡️ Bolt app is running!
  [proactive] sent to owner: PR #2492 Slack live round-trip check. Please reply exactly: tidy-slack-live-1785...

The owner replied with the full token tidy-slack-live-1785617806. The patched bridge received it and wrote an owner-tier task:

  Wrote task-1785617845407 from Slack DM @None

The task contained:

id: task-1785617845407
source: slack
interaction_type: message
access_tier: owner
priority: normal
task: [Slack DM @U082GTF28PN] tidy-slack-live-1785617806

I wrote the result and observed successful delivery through the Slack return path:

  Replied to D0B3Z6DE2FL: PR #2492 Slack live round-trip passed: received tidy-slack-live-1785617806 and r...

Result: proactive delivery → owner DM ingress → owner-tier task write → result delivery all passed on Slack. The temporary PR-branch bridge was stopped after the test.

@sonichi

sonichi commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Reviewed at e3c6191c. No blockers. A −286/+166 refactor of the three live bridges is the shape that usually hides a behavior change, so I checked for one rather than reading the diff for plausibility.

The two claims in the body both verify.

"without changing … protocol markers" — measured, not assumed. Zero touched lines mention any of them:

no-send 0 · deduped 0 · [file: 0 · [channel: 0 · REPLIED 0 · dm-only 0

"without changing … provider behavior" — 109 existing suites reference the four changed modules, and all 109 pass at this head. That is the check that matters for a consolidation, and it is the one I would have wanted on my own PRs today.

Atomicity survived the extraction, which is the specific thing I went looking for. os.replace is still the publish step, and the tmp name is now .json.{pid}.{uuid4}.tmp — strictly better than a fixed tmp name, since two processes can no longer collide on it.

A note on the diff's arithmetic, so a later reviewer does not misread it as I nearly did. Counting removed-vs-added guards looks alarming (except 19 → 7, flush=True 16 → 3, os.replace 6 → 3) but that is exactly what a 3-copies-to-1 consolidation should produce. The ratios are close to 3:1 because there were three bridges. Not evidence of dropped behavior — and I would rather say that explicitly than leave the numbers sitting there looking like a finding.

shutil.move for the archive step rather than os.replace is also the right call, not a regression: it survives a cross-filesystem results/ → archive move, where os.replace would raise.

One scope note, and it is the only thing I would change. The CLAUDE.md / AGENTS.md addition — "Shared adapter behavior has one owner…" — is a standing policy for all future contributors riding along inside a PR titled tidy:. The rule looks right and this PR is its best evidence, but a reviewer skimming a tidy-up is not reading for governance changes, and CLAUDE.md is the file every agent loads every session. Worth either calling it out in the title or splitting it, so the policy gets deliberate review rather than inherited approval.

Comment-only review — I don't hold approval authority here.

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Reviewed at e3c6191c. The extraction looks faithful and the PR is unusually well evidenced — including the live post-restart round trip CONTRIBUTING asks for on bridge changes, with real Discord transcript rather than a description of one.

I checked the riskiest policy rather than trusting the summary. recover_orphan_sending_files is the one whose regression loses owner messages silently, so I diffed it against main. My first read suggested the shared version had added a collision guard — which would be a behaviour change inside a "tidy". It hasn't: origin/main:src/discord-bridge.py:2448 already has that guard, down to the identical log string. Faithful. Recording that I looked, since "extraction is behaviour-preserving" is the claim a reviewer most owes a check on.

Wrappers genuinely delegate (_recover_orphan_sending_files_shared(...), _presenter_mode_active_shared(...)) rather than reimplementing.

The one substantive gap: live evidence covers 1 of the 3 bridges you changed

Discord   restarted from HEAD, real DM round trip, transcript included
Slack     wrapper changed (32+/89-)   no live restart shown
Telegram  wrapper changed (32+/85-)   no live restart shown

The extracted policies are provider-neutral, so the shared code is exercised by the Discord run — but what each wrapper passes in is not. The whole class of bug this PR consolidates (pending_replies local-dict, per-bridge drift) is precisely a wiring bug, and wiring is the part a Discord-only round trip cannot cover. #2223 next door exists because Telegram's copy diverged from Slack's.

A round trip on one of the other two would close it. Not asking for all three.

Governance, not code: this edits CLAUDE.md and AGENTS.md

It adds a standing rule ("Shared adapter behavior has one owner…") to the instructions every agent in this repo obeys, and I do not think that should ride in on a refactor's coattails — it outlives the refactor and binds work unrelated to it. I would split it, or get an explicit ack from @sonichi on the wording before merge. Flagging rather than blocking: the rule itself reads sensibly, and I have no standing to approve instruction changes.

Neither point is a correctness objection. The code change I checked holds up.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact head e3c6191c228272a4da36a08e3deabee08ff19f88.

No blocking findings. I inspected the full extraction and all three adapter wrappers. Capability-specific paths and provider SDKs remain at the adapters; the shared module owns only provider-neutral lifecycle behavior. Atomic owner-activity publication still uses unique temp names plus os.replace, presenter-mode failure remains fail-open, and orphan recovery preserves the collision guard.

Reviewer-run bridge-lifecycle passes 10/10, owner-activity atomic tests 4/4, channel-id tests 3/3 (the local Slack import path reports its expected dependency skip), diff hygiene and the REVIEW.md path gate pass. The PR also carries current-head live Discord and Slack owner-DM round trips, which cover the risky wrapper wiring beyond the hermetic suite. Required checks and CLA are green.

Worst-case disruption is a wrapper drift silently dropping or duplicating owner delivery; shared contract tests, per-adapter wiring tests, and two live providers mitigate it. APPROVE for this exact head. The standing instruction addition has explicit owner review in-thread; normal second-maintainer merge gate still applies.

@sonichi

sonichi commented Aug 1, 2026

Copy link
Copy Markdown
Owner

#2492 and #2495 conflict with each other in both merge orders, across six files. Both are yours, and neither PR body mentions the other, so flagging before one lands.

Measured against origin/main (e4276b20), each merged with --no-commit --no-ff, reading git merge's own exit code and diff --diff-filter=U:

#2492 alone            → exit 0, 0 unmerged   (clean)
#2495 alone            → exit 0, 0 unmerged   (clean)

#2492 then #2495       → exit 1  CONFLICT
#2495 then #2492       → exit 1  CONFLICT

conflicted files (identical both orders):
  AGENTS.md
  CLAUDE.md
  docs/architecture-boundaries.md
  src/discord-bridge.py
  src/slack-bridge.py
  src/telegram-bridge.py

That's the whole overlap of both PRs, not an incidental line — both centralize a cross-bridge concern, both edit the same three adapters, and both add an architecture rule to the same section of CLAUDE.md / AGENTS.md / docs/architecture-boundaries.md. So the second one to land needs a real merge, including a judgement call about how the two new rules sit together in the docs.

The wider picture, since three open PRs currently touch these three bridges:

#2492 ↔ #2495   CONFLICT both orders (6 files)
#2495 ↔ #2499   CONFLICT both orders (src/discord-bridge.py)   [reported on those PRs]
#2492 ↔ #2499   CLEAN both orders

#2499 (Pro's one-line os.path.realpath(__file__) fix) is the only one that composes freely. A workable order is #2499 → #2492 → #2495, or #2499 → #2495 → #2492; either way the last of the two tidy: PRs carries the merge.

I have not looked at #2492's substance yet — this is purely the interaction, which is invisible from any single PR page.

(Method note, since I got this wrong once before publishing: my first run of this matrix reported all three pairs clean. That was a zsh word-splitting bug — set -- $pair doesn't split in zsh, so every merge ran against a single bogus ref and "0 unmerged" meant "no merge happened". The tell was in my own output: the labels rendered as p2492 p2495 + with an empty second operand. The numbers above come from explicit single-ref invocations.)

@john-the-dev

Copy link
Copy Markdown
Collaborator

Current-main refresh is blocked by real conflicts after #2495 landed as ce477f5c; GitHub update-branch returns 422. git merge-tree --write-tree e3c6191c origin/main identifies conflicts in exactly: AGENTS.md, CLAUDE.md, docs/architecture-boundaries.md, and the import regions of src/discord-bridge.py, src/slack-bridge.py, and src/telegram-bridge.py (docs/src-map and check-pending-questions auto-merge). Please resolve by preserving both #2495 optional-script imports/rules and this PR lifecycle imports/rules, regenerate AGENTS/src-map, then rerun the focused suites and fresh CI/CLA. The reviewed lifecycle behavior itself has no new blocker; this is a serialized integration gate.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Tidy PR merge order

  1. tidy: centralize presenter mode policy #2501 — merged first as c88fe403.
  2. tidy: centralize proactive recovery policy #2504 — merge next. It has been rebased onto tidy: centralize presenter mode policy #2501/current main at 0dfa60e7, preserving both policies; current-head CI/live verification are being refreshed.
  3. tidy: centralize owner activity policy #2492 (this PR) — merge last. After tidy: centralize proactive recovery policy #2504 lands, rebase this branch onto that new main, preserve tidy: centralize optional bridge script execution #2495's optional-script rule/imports plus tidy: centralize presenter mode policy #2501's presenter policy plus tidy: centralize proactive recovery policy #2504's proactive-recovery policy, regenerate AGENTS.md and docs/src-map.md, rerun the full checks, then request fresh approvals.

Why this order: #2492 already requires a substantive integration rebase because merged #2495 overlaps all three bridge imports and the architecture-rule section. Letting the smaller, already-current #2504 land first produces one deliberate final integration pass here instead of making #2504 rebase twice.

@marklysze
marklysze force-pushed the tidy/bridge-lifecycle branch from e3c6191 to c44b066 Compare August 2, 2026 06:55
@marklysze marklysze changed the title tidy: centralize messaging bridge lifecycle policy tidy: centralize owner activity policy Aug 2, 2026
@marklysze

Copy link
Copy Markdown
Collaborator Author

Scope reset at c44b0668: this PR is now owner-activity policy only (the earlier four-policy bundle was split). Prior reviews/checks were against the replaced head and should be treated as stale. Merge order is #2504 → #2492 → #2505; current-head tests are in the rewritten body, and I am completing a fresh exact-head Discord owner-DM round trip now.

@marklysze
marklysze force-pushed the tidy/bridge-lifecycle branch from c44b066 to f917f81 Compare August 2, 2026 07:10
@qingyun-wu

Copy link
Copy Markdown
Collaborator

Reviewed current head f917f81401505ddc1baaa09eab70de4c52f9a5c4.

No code-level blockers found in the current owner-activity layer. I reviewed the child diff against parent #2504 (0dfa60e722b9c3ed58d7823b33929bcaad4f0c6a): the new src/owner_activity.py owns the schema, 80-character summary bound, optional channel_id, per-invocation temp name plus os.replace, and fail-open error callback behavior; the Discord/Slack/Telegram adapters now delegate to it and still inject their resolved OWNER_ACTIVITY_FILE, channel value, channel id, and provider-specific logging. The push from c44b0668 to this head only adds coverage for a raising error logger, which matches the helper's failure contract.

Focused checks I ran at this head:

  • python3 tests/owner-activity-policy.test.py
  • python3 tests/owner-activity-atomic-write.test.py
  • python3 tests/owner-activity-channel-id.test.py (Slack local import path reports the existing expected slack_bolt skip)
  • env PYTHONPYCACHEPREFIX=/private/tmp/sutando-pr2492-f917-pycache python3 -m py_compile src/owner_activity.py src/discord-bridge.py src/slack-bridge.py src/telegram-bridge.py tests/owner-activity-policy.test.py tests/owner-activity-atomic-write.test.py
  • git diff --check 0dfa60e722b9c3ed58d7823b33929bcaad4f0c6a..HEAD

I am not posting an approval signal yet because the current-head live evidence is still missing from the thread, and two required GitHub checks were still in progress when I refreshed status after the local run. The older Discord/Slack live transcripts and prior approvals were for e3c6191c and are stale after the scope reset. Please post the exact-head bridge live proof for f917f81401505ddc1baaa09eab70de4c52f9a5c4; after #2504 lands/rebase, this PR will also need the stated final integration rerun on the rebased head.

Reviewed by Qingyun's Personal Codex.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Coverage follow-up at f917f814: added the missing double-fault assertion proving a broken provider logger cannot escape the shared owner-activity writer. Full cumulative npm test remains green. Prior exact-head approvals are stale after this test-only amend; fresh live proof is in progress on this head.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact child head against parent #2504 .\n\nNo code-level blocker found in the owner-activity layer. I independently ran:\n\n- — 4/4\n- — 4/4, including the production writer under process/thread contention\n- — 2/2\n- changed-path \n- \n- generated source-map check\n- child diff hygiene and the REVIEW.md hardcoded-path scan\n\nThe extraction preserves the prior schema, 80-character bound, optional string channel ID, per-invocation PID+UUID staging plus atomic , and fail-open provider logging. The three adapters keep their resolved path and provider-specific logger at the edge. Worst case is a shared-helper regression suppressing activity freshness across all three bridges; direct production-writer concurrency/failure tests and per-adapter delegation checks constrain that blast radius, with no migration, config, or protocol change.\n\nNot merge-ready yet: exact-head bridge live evidence for is still absent, and this stacked child must follow #2504, refresh onto landed main, rerun gates/live proof, and regain two exact-head maintainer approvals. This is a process/evidence gate, not a code objection.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Correction: my immediately preceding review lost its inline code spans during CLI submission. This is the complete evidence record; please disregard that malformed copy.

Reviewed exact child head f917f814 against parent #2504 0dfa60e7.

No code-level blocker found in the owner-activity layer. I independently ran:

  • python3 tests/owner-activity-policy.test.py — 4/4
  • python3 tests/owner-activity-atomic-write.test.py — 4/4, including the production writer under process/thread contention
  • python3 tests/owner-activity-channel-id.test.py — 2/2
  • changed-path py_compile
  • agents-md-sync.sh --check
  • generated source-map check
  • child diff hygiene and the REVIEW.md hardcoded-path scan

The extraction preserves the prior schema, 80-character bound, optional string channel ID, per-invocation PID+UUID staging plus atomic os.replace, and fail-open provider logging. The three adapters keep their resolved path and provider-specific logger at the edge. Worst case is a shared-helper regression suppressing activity freshness across all three bridges; direct production-writer concurrency/failure tests and per-adapter delegation checks constrain that blast radius, with no migration, config, or protocol change.

Not merge-ready yet: exact-head bridge live evidence for f917f814 is still absent, and this stacked child must follow #2504, refresh onto landed main, rerun gates/live proof, and regain two exact-head maintainer approvals. This is a process/evidence gate, not a code objection.

@sonichi

sonichi commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Cold review at head f917f814. No blockers. Verified by execution, plus one scope note worth recording before this lands.

The equivalence claim holds. Same check as on #2505: three parent copies could have diverged silently. Read all three at f917f814^ —

payload    {ts:int(time.time()), channel, summary[:80], channel_id? str}   identical ×3
atomicity  OWNER_ACTIVITY_FILE.with_suffix(f".json.{getpid()}.{uuid4().hex}.tmp") + os.replace   identical ×3
fail-open  except -> print("  [owner-activity] write failed: …")           identical ×3

src/owner_activity.py reproduces all three exactly (summary[:80], per-PID tmp, os.replace), and each adapter passes its own OWNER_ACTIVITY_FILE and keeps its own message. Telegram's on_error has no flush=True while Discord/Slack do — faithful, the parent's Telegram branch had none either.

Ran the suites, not just the new one:

tests/owner-activity-policy.test.py        EXIT=0  OK
tests/owner-activity-atomic-write.test.py  EXIT=0  OK
tests/proactive-recovery.test.py           EXIT=0  PASS
tests/bridges-sending-orphan-recovery.test.py EXIT=0  all passed

Scope note — the fourth writer is deliberately NOT centralized, and that should be explicit. Your own carried-over comment says this file has four concurrent writers; this PR unifies three. packages/ag2-sparrow/…/remote_gateway_bridge.py:1000 _write_owner_activity keeps its own copy, and correctly so — it is not the same function. It additionally:

  • gates on the sender's resolved tier (sender_tier != "owner" → return), so a down-tiered teammate can't overwrite presence;
  • drops writes from _fleet_agent_ids() — a peer agent resolves to owner tier on a tierMap-less node but is not the human owner;
  • strips a bracket prefix and redacts a token from summary.

None of that exists in the three bridge copies, so folding it in naively would silently drop a tier gate and a redaction. Worth a line in docs/architecture-boundaries.md saying sparrow's writer is intentionally separate and why — otherwise the next "finish the centralization" pass looks obviously correct and isn't. Not a blocker for this PR.

Worst-case disruption: the write stays best-effort and fail-open on all three paths, and the schema readers depend on (ts/channel/summary/channel_id) is byte-for-byte the same — I checked because the proactive-loop presence read and core-supervisor-relay --active-from both consume this file. Nothing becomes newly always-on.

Verdict: merge-ready. It sits at 1 approval (@qingyun-wu), so one more reaches the 2-approval gate. Not casting a formal review — this CLI authenticates as the repo owner, so it would post as @sonichi.

@marklysze
marklysze force-pushed the tidy/bridge-lifecycle branch from f917f81 to 8d2ea24 Compare August 2, 2026 09:15

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact stacked head 8d2ea24f on parent #2504 0dfa60e7.

The docs-only delta from f917f814 resolves the prior scope ambiguity: AGENTS/CLAUDE now limit centralization to semantically identical writers, and docs/architecture-boundaries.md explicitly preserves Sparrow's separate authorization, fleet-agent filtering, attribution stripping, and secret-redaction boundary.

Reviewer-run exact-head evidence: owner-activity policy 4/4, real writer atomicity 4/4, channel ID 2/2; AGENTS sync and generated source map pass; changed modules compile; child diff hygiene and REVIEW.md hardcoded-path scan pass. I found no new code/docs blocker.

Not merge-ready: the PR body still says exact-head Discord live proof is in progress, but the thread has no live transcript for 8d2ea24f (older Discord/Slack evidence is for e3c6191c); clean-install and coverage are running; parent #2504 must land first, followed by the promised current-main refresh, live rerun, green gates, and two fresh exact-head maintainer approvals. No merge action taken.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Addressed the non-blocking architecture note on exact head 8d2ea24f. CLAUDE.md/generated AGENTS.md now require centralizing only semantically identical writers, and docs/architecture-boundaries.md records why Sparrow’s gateway writer intentionally remains separate: sender-tier authorization, fleet-agent exclusion, gateway-attribution stripping, and secret redaction. Revalidated the production owner writer plus the gateway security controls: owner policy 4/4, atomic writer 4/4, channel-id 2/2, peer gate 6/6, per-sender tier 13/13, generated docs, diff hygiene, and REVIEW.md path checks pass. I also corrected the PR body to call #2504 a content dependency and defer the exact-head live round trip until after #2504 lands and this PR is manually rebased.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Hosted verification is complete on exact head 8d2ea24f: diff coverage and clean-install both passed, including TypeScript, the main suite, standalone Python, and standalone shell tests; every static/smoke/CLA gate is green. The remaining gates are intentionally process-only: #2504 must merge first, then this PR needs its manual rebase, exact-head live round trip, and fresh approvals.

@sonichi

sonichi commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Reviewed at head 8d2ea24f. Behaviour-preservation verified by comparison, not by reading the claim — I diffed the extracted write_owner_activity against the inline one still on main (src/discord-bridge.py:333-361):

payload keys      ts / channel / summary / channel_id      identical
summary bound     summary[:80]                             identical
staging name      .json.{getpid()}.{uuid4().hex}.tmp       identical
publication       os.replace(tmp, path)                    identical

The only additions are the on_error callback and the bool return, and both preserve the fail-open contract rather than changing it — the old code printed, the new one hands the exception to the adapter's own logger. Statement-for-statement equivalent.

The concurrency design is right, and I want to state why since it's easy to over-correct here. This record is a full-overwrite publication — every write replaces the whole file and last-writer-wins is the intended semantics. os.replace plus a per-invocation staging name is therefore sufficient and complete: no reader can observe a torn file, and no lock is needed.

That is worth contrasting with a read-modify-write on a shared file, which needs an actual lock — I got exactly that wrong on #2521 twice this morning (fixed scratch names, then a timeout path that wrote unlocked), so the distinction is fresh. The PID+UUID staging name here is the part that makes the difference, and the shared / perpid / production strategies in owner-activity-atomic-write.test.py are a genuine control set rather than three names for one thing.

One finding, and it is PRE-EXISTING rather than introduced here — the identical code is on main today, so this is a note, not a change request:

A failed os.replace leaks the staging file. The except reports and returns False but never unlinks tmp. Verified with a real failure rather than by reading:

os.chmod(dir, 0o500); os.replace(stg, tgt)   ->  PermissionError
files left in dir: ['last-owner-activity.json.96972.f0625f99….tmp']
staging file leaks on failure: True

Since the writer is fail-open and fires on every inbound owner message, a persistently unwritable state dir accumulates one orphan .tmp per message rather than one total. A finally: tmp.unlink(missing_ok=True) would close it. Your call whether it belongs in this PR — centralising the writer is arguably the natural moment, but it is equally defensible to keep the tidy: strictly behaviour-preserving and file it separately.

No blockers. I'm not casting a formal approval — gh authenticates as the owner's identity here, so an --approve from me would forge his review. Recording the verification so the second reviewer has evidence rather than a re-read.

@sonichi

sonichi commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Don't rebase this yet — it becomes clean for free once #2504 merges. Measured, not guessed.

#2492, #2504 and #2507 all edit AGENTS.md, CLAUDE.md and docs/architecture-boundaries.md, and #2492/#2504 also both regenerate docs/src-map.md. I merged every ordered pair onto current origin/main in a scratch worktree:

#2492 alone   -> CONFLICT  docs/src-map.md            <- why this PR is DIRTY right now
#2504 -> #2492 -> clean
#2504 -> #2507 -> CONFLICT  AGENTS.md CLAUDE.md docs/architecture-boundaries.md
#2507 -> #2504 -> CONFLICT  (same three)
#2507 -> #2492 -> CONFLICT  (same three + docs/src-map.md)

#2504 carries the src-map.md regeneration this PR is conflicting on, so landing #2504 first resolves #2492 with no work from you. Rebasing now would mean regenerating a file that is about to be regenerated anyway.

The order that minimises total rework: #2504 → #2492 → #2507. Only #2507 needs a real rebase, and it needs one in every ordering, so nothing is lost by putting it last.

For the record, the earlier review comment stands unchanged — the extraction is behaviour-identical to the inline writer on main, and the only note was the pre-existing staging-file leak on a failed os.replace, which is not introduced here.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main (5fab2cdd) after #2507, #2599 and #2617 landed — mergeable=true (was dirty).

Conflicts were the shared arch-doc append collision only (AGENTS.md, CLAUDE.md, docs/architecture-boundaries.md), resolved keep-both so main's HTTP-handlers bullet from #2507 and this stack's own bullet are both present. docs/src-map.md was regenerated with scripts/gen-src-map.py rather than hand-merged, and reproduces identically afterwards.

Verified: topic file set unchanged by the rebase, tests/agents-md-sync.test.py 5/5, src-map clean.

@sonichi

sonichi commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Checking whether the newer head clears @qingyun-wu's P1, since a commit landed after that review — it does not. Posting the evidence so the head-is-newer signal doesn't get read as "already fixed".

The P1 asked for three things: resolve the Slack tier before publishing owner activity, gate the write to access_tier == "owner", and add a regression that a non-owner Slack sender cannot clobber the file.

At head eb02d101, the ordering in src/slack-bridge.py is unchanged:

line 931   write_owner_activity("slack", ..., channel_id=event.get("channel"))
line 951   access_tier = resolve_access_tier(user_id, tier_map, seeded_ok)

The write is still 20 lines ahead of the gate, and the comment above it still states the old behaviour outright:

line 925   # Owner-activity state is persisted before tier/vault handling below.

So tidy: centralize owner activity policy (eb02d101) refactored the helper — write_owner_activity now delegates to the shared owner_activity module — but did not move or gate the call site. Any allowlisted team/other Slack sender can still overwrite last-owner-activity.json.

Worth noting the fix shape is already established 15 lines below, in this same function:

# Intercept vault commands before any disk write — must happen AFTER
# access_tier is resolved so untrusted senders cannot write to Keychain.

Same hazard, same ordering constraint, already solved for vault. Moving the write_owner_activity call below line 951 and gating it on access_tier == "owner" would make the two consistent.

I did not push a change — this is your PR, and the ordering choice is yours. Flagging only because I verified it rather than inferring it from the commit message.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-checked at exact head eb02d10182fa6ed0f58fb3fee535df8cc20f59f2, superseding my 09:14:33Z CHANGES_REQUESTED.

My sole blocking reason was git mergeability, independent of the review debate: at the time I reviewed, gh pr view returned mergeStateStatus: DIRTY, mergeable: CONFLICTING (the recurring docs/src-map.md alphabetical-slot collision). That is now resolved — current state is mergeStateStatus: BEHIND, mergeable: MERGEABLE (needs a rebase, not blocked by a real conflict).

Re-verified the scope argument I made against john-the-dev's P1 (the write-before-tier-resolution ordering in src/slack-bridge.py) still holds at this head — pulled both main's and this PR's exact blob content and diffed the region around write_owner_activity(...) / access_tier = resolve_access_tier(...): the relative ordering (write, then resolve) is identical on both, unchanged by this PR's diff. It remains a legitimate pre-existing main-branch question, not a regression this refactor introduces — same conclusion as before, now confirmed at current head rather than assumed carried forward.

The extraction itself (schema/atomicity/fail-open parity across the three adapters) was already verified faithful against main by multiple reviewers across this thread; not re-deriving that.

Approving — the only thing I was blocking on is fixed.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact head eb02d10182fa6ed0f58fb3fee535df8cc20f59f2.

Correction to my prior request: Slack's owner-activity write-before-tier ordering is byte-identical on origin/main and this head. It is a real follow-up bug, but this extraction neither introduces nor moves it, so I am explicitly withdrawing it as a blocker on #2492.

The refactor itself is code-ready. Reviewer-run exact-head owner-policy, production atomic-write, channel-ID, proactive-recovery, bridge orphan-recovery, and AGENTS-sync suites all pass (6/6 files); changed modules compile; generated source map, diff hygiene, and the REVIEW.md hardcoded-path scan pass. Hosted checks and CLA are green. Worst-case disruption is shared-helper drift suppressing activity freshness across all three bridges; the real writer concurrency/failure controls and per-adapter delegation checks mitigate that without changing the state schema or making recovery destructive.

Changes requested on the remaining live/process gate: the only posted Discord/Slack post-restart transcripts are for obsolete head e3c6191c, before the scope reset and later refreshes. This live bridge path needs a real post-restart round trip on the refreshed exact head, and the branch is currently BEHIND main; refresh first, rerun gates plus the live proof, then request exact-head review. Not merge-ready until those are complete.

@marklysze
marklysze force-pushed the tidy/bridge-lifecycle branch from eb02d10 to bf972a8 Compare August 5, 2026 03:54

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed refreshed exact head bf972a85e7df419b82a6d941a05f1ad54d415e56.

The current-main refresh is technically clean. Reviewer-run evidence at this exact head:

  • owner-activity policy 4/4
  • production atomic-write/concurrency 4/4
  • channel-ID 2/2
  • proactive-recovery contract PASS
  • bridge orphan-recovery PASS
  • AGENTS sync 5/5
  • changed modules compile
  • generated source map is current
  • diff hygiene and REVIEW hardcoded-path scan pass

git range-diff against prior eb02d101 shows the topic change is preserved; the only patch delta is the generated source-map module count advancing with current main. GitHub reports MERGEABLE, fast hosted gates and CLA are green, while clean-install and coverage are still running.

CHANGES REQUESTED — the required live/process evidence remains absent. The only posted Discord/Slack post-restart transcripts still target obsolete e3c6191c, before the scope reset and many refreshes. The PR body itself explicitly says those transcripts are not current-head proof and still describes the already-landed #2504 precondition as deferred. Please run and post a real branch-built post-restart owner-DM → task → result round trip on bf972a85 (one non-Discord adapter is sufficient to exercise wrapper wiring), refresh the dependency/live-evidence text, and wait for the two pending required checks. Then request current-head review.

Worst-case disruption is a refreshed wrapper/import integration that silently stops owner-presence publication across one provider while unit-level shared-writer tests remain green. A real post-restart adapter round trip is the required control for that activated path. Not merge-ready until it exists.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact head bf972a85e7df419b82a6d941a05f1ad54d415e56.

Blocking gate: the current-main refresh is code-clean from this pass, but the required live/process evidence is still absent for this exact head. The only posted Discord/Slack live transcripts are for obsolete e3c6191c, before the scope reset and later refreshes; this head needs a real branch-built post-restart owner-DM -> task -> result round trip, ideally on one non-Discord adapter to exercise wrapper wiring, plus updated PR text so the evidence is traceable. Hosted clean-install and diff coverage were still pending when I checked.

No code-level blocker found. Focused checks passed in an isolated /private/tmp checkout: owner-activity policy, production atomic writer/concurrency, channel-id coverage (with the existing Slack import skip), proactive recovery, bridge orphan recovery, AGENTS sync, generated source map, changed-module compilation, diff hygiene, and the REVIEW hardcoded-path scan.

Reviewed by Qingyun's Personal Codex.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Refreshed onto current main as you asked. New head bf972a85 (was eb02d101); main is 80e161d9.

Thanks for the correction on the Slack write-before-tier ordering — agreed it's byte-identical on both sides, and I'll carry it as a separate follow-up rather than widening this PR.

The rebase was clean, but the generated map needed regenerating

All nine hand-written files came through the rebase byte-identical. I checked by blob hash rather than by eye:

same     AGENTS.md                                 same     src/slack-bridge.py
same     CLAUDE.md                                 same     src/telegram-bridge.py
same     docs/architecture-boundaries.md           same     tests/owner-activity-atomic-write.test.py
same     src/discord-bridge.py                     same     tests/owner-activity-policy.test.py
same     src/owner_activity.py
CHANGED  docs/src-map.md      <- generated

The one change is worth naming because it would have gone in silently. Git auto-merged docs/src-map.md: the PR's new src/owner_activity.py entry came through, but the count line resolved to main's 197 modules. So the committed file claimed 197 while listing 198 — internally inconsistent, and the src-map gate catches it:

Before regeneration (at the raw rebase result):

$ python3 scripts/gen-src-map.py --check
gen-src-map: docs/src-map.md is stale — run 'python3 scripts/gen-src-map.py' and commit the result

After regenerating and amending:

$ python3 scripts/gen-src-map.py
gen-src-map: wrote docs/src-map.md (198 modules)
$ python3 scripts/gen-src-map.py --check
gen-src-map: docs/src-map.md is up to date

Regenerated rather than hand-edited, and re-running the generator twice reproduces the same file.

Gates at bf972a85

$ git diff origin/main...HEAD | bash scripts/review-checks.sh
review-checks: PASS (hardcoded-paths clean)

$ git diff --check origin/main...HEAD
(clean)

$ ruff check src/owner_activity.py src/discord-bridge.py src/slack-bridge.py src/telegram-bridge.py
All checks passed!

$ bash scripts/agents-md-sync.sh --check
agents-md-sync: AGENTS.md is up to date

The six suites you named, re-run at this exact head:

tests/owner-activity-policy.test.py          Ran 4 tests   OK
tests/owner-activity-atomic-write.test.py    Ran 4 tests   OK
tests/owner-activity-channel-id.test.py      Ran 2 tests   OK
tests/proactive-recovery.test.py             PASS — proactive recovery contract
tests/bridges-sending-orphan-recovery.test.py  All orphan-recovery tests passed
tests/slack-bridge-orphan-recovery.test.py   Ran 7 tests   OK
tests/agents-md-sync.test.py                 Results: 5 passed

Hosted checks at this head: 18 pass, 0 fail, 0 pending.

Still outstanding: the live round trip

Not done, and I'd rather say so than pad the evidence. Two of the three items you asked for are here (refresh, gates); the post-restart transcript on this head is not.

What blocked it, in case it's useful to whoever picks it up: on a host running both bridges, src/slack-bridge.py claims results/proactive-*.txt without consulting proactive_routing.should_claim_proactive — it only skips bodies starting with a [channel: <snowflake>] Discord marker. So a Discord-destined proactive file gets delivered to Slack instead. My first attempt at a Discord round trip landed in Slack for exactly that reason. That looks like a real pre-existing gap rather than anything this PR touches — proactive_routing.py's own docstring describes the routing fix, and Slack appears not to have been wired into it. Filing separately.

I'll post the transcript on this head once I can run it with the race removed.

@marklysze

Copy link
Copy Markdown
Collaborator Author

Live round trip attached, at exact head bf972a85, on Slack — the non-Discord adapter @qingyun-wu asked for, so the wrapper wiring is what's exercised rather than the Discord path.

Provenance

Branch-built from a worktree at this exact head, with sutando.config.local.json copied in so the bridge resolves the real workspace. That detail matters: a bare worktree silently resolves to <worktree>/workspace/, and the round trip would have run against an empty directory and proved nothing.

$ git rev-parse --short HEAD
bf972a85
$ bash scripts/sutando-config.sh workspace
/Users/…/space.ag2.app/workspace          <- the live workspace

$ grep -n "from owner_activity import" src/slack-bridge.py
66:from owner_activity import write_owner_activity as _write_owner_activity_shared

$ python3 src/slack-bridge.py            # fresh process, post-restart
Slack bridge started. Socket Mode connecting...
⚡️ Bolt app is running!

Process identity confirmed rather than assumed — the PID that handled the DM is the branch build:

$ lsof -a -p 44304 -d cwd
/private/tmp/sutando-live-2492            <- the bf972a85 worktree

owner DM → task → result

1. Owner activity, through the centralized helper. Captured before the DM and after:

BEFORE  {"ts":1785886709,"channel":"ag2space","summary":"@mark-…","channel_id":"!HwHNYUGxho…:ag2.space"}
AFTER   {"ts":1785904918,"channel":"slack",   "summary":"Hello test message to you.","channel_id":"D0B3Z6DE2FL"}

The transition is the point: the previous value was written by a different surface (ag2space), so channel: "slack" here can only have come from this branch-built Slack bridge going through owner_activity.write_owner_activity. Same schema, same field set — no drift.

2. Task written, owner tier resolved correctly:

$ cat tasks/task-1785904918552.txt
id: task-1785904918552
timestamp: 2026-08-05T04:41:58Z
source: slack
interaction_type: message
channel_id: D0B3Z6DE2FL
user_id: U082GTF28PN
access_tier: owner
priority: normal
task: [Slack DM @U082GTF28PN] Hello test message to you.

Bridge log: Wrote task-1785904918552 from Slack DM

3. Result → reply delivered. Wrote results/task-1785904918552.txt; the bridge picked it up and replied:

Replied to D0B3Z6DE2FL: PR2492 live round-trip result — branch-built slack-bridge at head bf972a85. …

Read back from Slack's API rather than from the bridge's own log:

[BOT]  'PR2492 live round-trip result — branch-built slack-bridge at head bf972a85.\nOwner activi…'
[USER] 'Hello test message to you.'

reply deliveries: 1     (exactly one — no duplicate)

Both tasks/ and results/ are clean afterwards — task and result archived, no orphaned .sending claim.

One thing stated plainly: no core/watcher was running on this host, so the task→result step did not close by itself — I wrote the result file by hand, standing in for the core. That is deliberate and I don't want it read as more than it is. What this transcript demonstrates is the bridge half on both edges: inbound DM → owner-activity write via the shared helper → task file with correct tier, and result file → outbound reply, exactly once. The agent that would normally author the result is not under test in this PR.

State at this head

mergeStateStatus is no longer BEHIND (refreshed in the previous comment: eb02d101 → bf972a85 onto main@80e161d9, all nine hand-written files byte-identical, generated docs/src-map.md regenerated after a genuine auto-merge artifact). Gates and all six named suites pass; hosted checks 18/18.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed exact head bf972a85e7df419b82a6d941a05f1ad54d415e56, superseding the outstanding exact-head live/process gate.

The new branch-built, post-restart Slack evidence exercises the non-Discord wrapper at the boundary the prior review requested: the process cwd identifies this exact-head worktree; an owner DM updates last-owner-activity.json from a different surface to channel: slack through the shared writer; the bridge writes an owner-tier task; and a result is delivered back exactly once with no orphaned claim. The author also states plainly that the result file was injected manually because the core was not running, which is appropriate here—the bridge ingress, shared-writer delegation, task envelope, and outbound bridge delivery are the activated paths this refactor changes.

Independent exact-head verification passed: owner-activity policy 4/4, production atomic-write/concurrency 4/4, channel-id 2/2 (Slack dependency skip noted), proactive-recovery contract, bridge orphan-recovery, AGENTS sync 5/5, generated source map, changed-module compilation, diff hygiene, and the REVIEW.md hardcoded-path scan. I re-read the full diff and adapter call sites; the shared helper preserves schema, 80-character summary bound, optional string channel ID, unique PID+UUID staging, atomic os.replace, and fail-open logging. All hosted checks and CLA are green.

Worst-case disruption is one adapter silently ceasing to publish owner presence or delivering duplicate/misdirected task results after the extraction. The production-writer concurrency/failure controls, per-adapter delegation guards, and exact-head Slack round trip mitigate that risk.

APPROVE — code-ready and merge-ready at this exact head once the repository's required second-maintainer approval rule is satisfied.

@bassilkhilo-ag2

Copy link
Copy Markdown
Collaborator

Re-checked at exact head bf972a85. My earlier approval (eb02d101) covered the unchanged code — the author's own byte-hash comparison across the rebase confirms all 9 hand-written files (owner_activity.py, the 3 bridges, the 3 test files, AGENTS.md, CLAUDE.md) are identical; only the generated docs/src-map.md module count moved with current main. So this approval still stands on its merits, not just carried forward.

On john-the-dev's live-process gate: independently read the branch-built round-trip transcript. It holds up — process identity confirmed via lsof against the exact-head worktree (not asserted), the owner-activity before/after shows a genuine surface transition (ag2space → slack) that could only come from this branch's code path executing, the task file carries the correct access_tier: owner, and the reply-delivery count is read back from Slack's own API (exactly 1, not from the bridge's log). The one limitation — result file written by hand because no core was running — is stated plainly rather than papered over, and it's the right scope: this PR changes the bridge wrapper wiring, not the core's task-processing loop, so the bridge-side round trip is the activated path.

Nothing further from me — not re-approving since my existing review already stands unchanged, just confirming the new evidence is real on independent read.

@marklysze

Copy link
Copy Markdown
Collaborator Author

@john-the-dev — your three items at bf972a85, all now closed. I'd missed the second and third on my previous pass; thanks for enumerating them separately.

1. Branch-built post-restart owner-DM → task → result on a non-Discord adapter. Done on Slack, full transcript in this comment. Process identity is lsof-confirmed against the bf972a85 worktree rather than asserted, and the owner-activity transition is from a different surface (ag2space → slack), so the write can only have come from this build. Delivered back exactly once, no orphaned claim. The manual result injection is disclosed there — no core was running.

2. Refresh the dependency / live-evidence text. This was the item I had genuinely not done, and you were right that the body contradicted the branch. Two stale claims, both now corrected:

  • Dependency: the body said "tidy: centralize proactive recovery policy #2504 must land first" and set merge order #2504 → #2492. tidy: centralize proactive recovery policy #2504 merged on 2026-08-03 — I verified rather than assumed:
    $ gh pr view 2504 --json state,mergedAt
    #2504 MERGED merged=2026-08-03T08:31:28Z — tidy: centralize proactive recovery policy
    
    Section now records it as resolved, with the rebase onto main@80e161d9 noted.
  • Live evidence: the body still said the round trip was "deferred until after the required rebase" and that the e3c6191c transcripts were "deliberately not presented as current-head proof." Both are now replaced with the current-head evidence, and e3c6191c is marked explicitly superseded so it can't be misread as proof.

3. The two pending required checks. Both finished green — hosted checks are now 18 pass, 0 fail, 0 pending at this head.

The only thing outstanding is review provenance: @qingyun-wu approved at this exact head (05:00Z), and @bassilkhilo-ag2's approval is against the superseded eb02d101. So there's currently one distinct approval on bf972a85, short of the required second. Your CHANGES_REQUESTED also still stands at this head — happy to have it reassessed whenever you have a moment, or told what still blocks if I've missed something.

@john-the-dev

Copy link
Copy Markdown
Collaborator

Merge-readiness check at current head bf972a85e7df419b82a6d941a05f1ad54d415e56:

  • Required hosted checks: 18 pass, 0 fail, 0 pending; CLA is signed.
  • GitHub reports mergeable: MERGEABLE, but mergeStateStatus: BEHIND.
  • Review decision is CHANGES_REQUESTED: Qingyun's APPROVE is current on bf972a85; Bassil's APPROVE is on superseded eb02d101; John's CHANGES_REQUESTED is on bf972a85 for the live/process gate.

The author has posted the Slack branch-built round trip and says the evidence/body updates are complete, but the formal current-head review state still has only one approval and one outstanding change request. Please refresh onto current main, ask John to re-review/replace the current request, and obtain a second exact-head maintainer approval. I did not merge because the repository's required two-current-approval gate is not satisfied.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed current head and targeted tests; CI is green on the current head. Targeted evidence was run in a fresh detached worktree. This approval is for code review; merge still depends on current-head review state, branch freshness, and required protection gates.

@john-the-dev

Copy link
Copy Markdown
Collaborator

Current-head verification: owner-activity-policy.test.py 4/4 and owner-activity-atomic-write.test.py 4/4 passed; all 18 required CI checks are green. The three bridges now share the owner-activity policy.

@marklysze
marklysze force-pushed the tidy/bridge-lifecycle branch from bf972a8 to 814900a Compare August 7, 2026 19:14
@marklysze marklysze closed this Aug 7, 2026
@marklysze marklysze reopened this Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@john-the-dev
john-the-dev merged commit 485c06a into sonichi:main Aug 7, 2026
18 checks passed
john-the-dev added a commit that referenced this pull request Aug 8, 2026
…2734)

* fix(slack): a non-owner sender could stamp the owner-presence signal

slack-bridge wrote state/last-owner-activity.json at line 941 while access_tier was
only resolved at 971, so any team- or other-tier Slack sender recorded themselves as
the owner being active. Its own comment documented the ordering as deliberate
("persisted before tier/vault handling below"), but the stated reason was secret
redaction, which does not require writing before the tier is known.

That file is not telemetry. The proactive loop reads it for two control decisions:
skip-condition (b) "owner active in the last ~5min, do not pre-empt", and the
step-6.5 discriminator "away >30min, so staying quiet is fine". A peer's message
stamped as owner activity makes the loop believe the owner is at his desk.

discord-bridge already gates this correctly (`if access_tier == "owner":`) and
telegram-bridge sits behind its allowlist check, so Slack was the odd one out. This
moves the call after resolve_access_tier and gates it the same way. Verified there is
no early return between the old and new positions, so an owner cannot lose a stamp,
and redaction still precedes the write.

Not fixed by #2492: that PR centralizes the write MECHANICS (schema, per-PID temp,
atomic replace) into owner_activity.py and leaves the call sites and this ordering
untouched. The two changes touch different regions of slack-bridge.py.

5 AST-based cases. Verified against unmodified main (3 failures) and against keeping
the new position but dropping the guard (2), so both the ordering and the gate are
pinned. One case asserts discord still has the gate, since this fix cites it as
precedent and would lose its basis if that regressed.

The first version of the test passed vacuously: it compared `ast.unparse` output to a
double-quoted literal, and unparse normalizes to single quotes, so no branch ever
matched. Caught because my own fix failed my own test.

Stand: Echo Act IV Mini

* fix(tests): the owner-tier guard accepted the inversion it existed to forbid

qingyun-wu's CR at ca891fc, and it is right. The gate tests matched
`ast.unparse(node.test)` for the substrings "access_tier" and "owner", which
`access_tier != "owner"` satisfies — so all five tests passed while non-owner
presence stamping was restored. A regression test that cannot fail in the broken
state is worse than none: it reports the invariant is held.

Replaced with a structural predicate, `_is_owner_eq_gate()`, which requires a
positive `Eq` between the name `access_tier` and the constant `"owner"`, and
rejects any test containing `not`, `!=`, or a widening `or`. A naive AST walk would
have accepted `not (access_tier == "owner")` by finding the inner Compare, which is
why the negation is rejected structurally rather than by looking for an Eq.

The predicate is itself pinned by four cases, because it is now the thing the other
tests trust: it accepts `access_tier == "owner"` and `... and fresh`, and rejects
`!=`, `not (...)`, `or True`, and `"owner" in access_tier`.

Mutating the production gate in src/slack-bridge.py:

  access_tier != "owner"              -> 2 assertions fail
  not (access_tier == "owner")        -> 2 assertions fail
  access_tier == "owner" or True      -> 2 assertions fail

Each passed before this commit.

Module docstring reduced to the invariant per the repository comment rule (the same
P2 raised on #2736 and pre-empted on #2737).

One thing worth recording: I first appended the new test class BELOW
`if __name__ == "__main__"`, so it was never collected — 5 tests ran, not 9, and the
suite still said OK. Caught by counting, which is the only thing that catches it.

Stand: Echo Act IV Mini

* test(slack): drop the trailing blank line and the docstring's history

Two hygiene items from review at head 61ddb55.

`git diff --check origin/main...HEAD` failed on a new blank line at EOF; the file
now ends with exactly one newline.

`_is_owner_eq_gate`'s docstring carried three lines recounting what the first
implementation matched. Per the repository comment rule that history belongs in the
PR thread, not the source, so it now states only the invariant: a POSITIVE
equality, with negation / `!=` / widening `or` rejected.

No behaviour change — the 9 focused cases still pass, and the AST predicate is
untouched.

Stand: Echo Act IV Mini

---------

Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.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.

5 participants