Repository navigation
fix(sdk): stop Slack/Discord chat daemons from publishing lean finals twice - #6305
Conversation
… twice The host emits each turn frame on a positioned leg and a raw native leg, excluding from the raw leg only connections that negotiate positioned_notification_effects_v1. The SessionRouter client used by the Slack/Discord ChatDaemonRuntime never advertised it, so it received both copies; a lean final has no messageRef, so the raw copy (no publicationId) became a second provider post next to the Router-positioned one. SessionRouter gains an opt-in positionedNotificationEffects option that ChatDaemonRuntime sets. The capability constant moves to sdk/host/host so the router does not import from telegram-daemon. Lore-id: 7c41e2a9 Constraint: plain SDK observers (sdk session tail) must keep both public surfaces (Yeachan-Heo#4570) Rejected: add messageRef to lean finals | changes threaded-render edit-in-place/rich promotion Rejected: dedupe by body hash | collapses legitimately repeated answers Confidence: high Scope-risk: narrow Reversibility: clean Directive: notification-effect publishers built on SessionRouter must opt into positionedNotificationEffects Tested: production host -> Slack/Discord ChatDaemonRuntime lean final posts once (RED 2 -> GREEN 1) Tested: real host negotiation: plain observer router lacks capability, opted-in router advertises it Tested: chat-daemon worker/reconnect, turn-ordering, session-router, slack-daemon, host-wiring suites Not-tested: live Slack/Discord network delivery Co-authored-by: Claude <noreply@anthropic.com>
probepark
left a comment
There was a problem hiding this comment.
리뷰 (head 3a03f30, gajae-reviewer가 probepark 대신 작성)
CI: PR 원인 1개 — Telegram daemon generation guard 실패 (https://github.com/Yeachan-Heo/gajae-code/actions/runs/37174073883/job/111353012945). gjc-state-gates / native addon, Affected path validation / native-build는 아직 대기 중입니다.
범위: +200 / -7, 9 files — packages/coding-agent/src/sdk/{router,bus,host}, tests 3개, changelog.d fragment
규약: changelog fragment 있음(packages/coding-agent/changelog.d/lean-final-answer-publication-dedupe.md), generated 파일 없음, 라벨 없음
주목할 곳:
packages/coding-agent/src/sdk/router/session-router.ts:1546-1552—SessionRouter.#createAttachedClient본문이 바뀌었습니다. 이 선언은scripts/telegram-daemon-generation-manifest.json:436(discord:)와:521(slack:)에 digest가 고정돼 있습니다. 가드 로그:semantic manifest declaration digests do not byte-match the current tree (discord:…SessionRouter.#createAttachedClient: manifest=76a3018a… actual=f7d9879a…, slack:… 동일). 이 PR 원인임을 확인했습니다: base61f2e17은 dev tip(behind_by=0)이고, manifest는 diff에 없으며, 가드의 canonical 형식으로 계산한 선언 길이가 base 1765자 → head 1905자로 달라졌습니다.- 위 실패는 가드의 1단계(digest)입니다. manifest만 다시 만들면 2단계에서
CHAT_DAEMON_GENERATIONS상향을 요구합니다. head와 dev 모두packages/coding-agent/src/sdk/bus/chat-daemon-control.ts:143-146이discord: 82, slack: 89로 같습니다. 이 변경은 실행 중인 Slack/Discord 데몬의 capability 협상을 바꿉니다. 그래서 상향하지 않으면 업그레이드 후에도 옛 데몬이 유지돼 중복 게시가 계속됩니다. 해결: 로컬에서bun run fix:daemon-generations 61f2e17243af522e1572caf8d82f8f0e6a696684(digest 재생성과 discord/slack 상향을 한 번에 적용)을 실행하고 결과를 커밋하세요. dev가 그 사이에 움직였으면 새 base sha로 다시 실행하세요. - 로직 자체(opt-in
positionedNotificationEffects,ChatDaemonRuntime만 켜는 것, 상수를sdk/host/host.ts로 옮기고telegram-daemon.ts에서 다시 export하는 것)에서는 결함을 찾지 못했습니다.host/host.ts는bus/telegram-daemon을 import하지 않으므로 새 순환 import도 없습니다.sdk-host-wiring.test.ts의spyOn은finally에서mockRestore()로 정리됩니다.
blocking: 1번 (generation guard: manifest digest와 discord/slack generation 상향 누락)
Verdict: gajae.pr-review-verdict.v1 needs-human sha256:7a1da23a3aebb5608a24b6614888bc79337110f1f52f5e5cd9c39aa3fbfd6d2b reviewer:critic reviewer-id:gajae-reviewer evidence:generation-guard-red-pr-caused;createAttachedClient-digest-moved;chat-daemon-generations-unbumped
PR body verdict line count=0, not updated. Suggested verdict line: gajae.pr-review-verdict.v1 needs-human sha256:7a1da23a3aebb5608a24b6614888bc79337110f1f52f5e5cd9c39aa3fbfd6d2b reviewer:critic reviewer-id:gajae-reviewer evidence:generation-guard-red-pr-caused;createAttachedClient-digest-moved;chat-daemon-generations-unbumped
…ability change SessionRouter.#createAttachedClient is a protected chat-daemon lifecycle declaration. Advertising positioned_notification_effects_v1 from it changes what a running Slack/Discord daemon negotiates, so already-running owners must be replaced rather than keep receiving both delivery legs. Applied with `bun scripts/telegram-daemon-generation-guard.ts --fix-generations 61f2e17` (discord 82->83, slack 89->90) and the regenerated semantic manifest. Lore-id: 5e0b7d21 Constraint: protected chat-daemon lifecycle changes require a strictly higher CHAT_DAEMON_GENERATIONS entry Confidence: high Scope-risk: narrow Reversibility: clean Tested: telegram-daemon-generation-guard passes against base 61f2e17 Co-authored-by: Codex <noreply@openai.com>
|
@probepark 3a03f30c6에 대한 리뷰의 blocking 1건(generation guard)을 조치 (
증거 (exact head
|
probepark
left a comment
There was a problem hiding this comment.
리뷰 (head ca91875, gajae-reviewer가 probepark 대신 작성)
CI: green — Telegram daemon generation guard pass (https://github.com/Yeachan-Heo/gajae-code/actions/runs/37176110362/job/111359046507). check:@gajae-code/coding-agent, test:@gajae-code/coding-agent:shard-1-of-8, sdk-chat-daemon-worker, sdk-host-wiring, notifications-turn-ordering, Windows Telegram daemon safety, Virtual integration validation, gjc-state-gates가 모두 pass입니다. approve 게이트 ALLOW(대기 0, 실패 0).
범위: +206 / -13, 11 files — packages/coding-agent/src/sdk/{router,bus,host}, tests 3개, changelog.d fragment, scripts/telegram-daemon-generation-manifest.json
규약: changelog fragment 있음(packages/coding-agent/changelog.d/lean-final-answer-publication-dedupe.md, ### Fixed), 손편집 generated 파일 없음(manifest는 guard --fix-generations 출력), 라벨 없음
주목할 곳:
- 이전 blocking(3a03f30 리뷰 5404134013) 해소:
3a03f30..ca91875diff는 2 files, +6/-6뿐입니다.packages/coding-agent/src/sdk/bus/chat-daemon-control.ts:144-145가discord: 83, slack: 90으로 올랐고, manifest의SessionRouter.#createAttachedClientdigest(discord/slack)가 가드 로그의 actual 값f7d9879a…로,CHAT_DAEMON_GENERATIONS.{discord,slack}digest가 갱신됐습니다. 그 밖의 코드는 3a03f30와 같습니다. packages/coding-agent/src/sdk/router/session-router.ts:1546-1552— capability 배열은observer와positionedNotificationEffects를 각각 독립적으로 더합니다. 두 옵션이 모두 꺼져 있으면 예전처럼capabilities를 넘기지 않습니다.chat-daemon-runtime.ts:321-323만 opt-in하므로 일반 SDK observer(gjc sdk session tail)의 동작은 바뀌지 않습니다.- base
61f2e17이후 dev는 2 commits 앞서 있습니다(config-cli.ts및 테스트, changelog). 이 PR과 겹치는 파일은 없습니다.
blocking: 없음
Verdict: gajae.pr-review-verdict.v1 merge-approved sha256:3d6fe80401ec3fcb71b5a1f762509ad44c1aae04e2a7b32156aa3d09db146078 reviewer:human reviewer-id:probepark evidence:ci-green;generation-guard-green;daemon-generations-bumped;manifest-regenerated;capability-opt-in-scoped
PR body verdict line count=0, not updated. Suggested verdict line: gajae.pr-review-verdict.v1 merge-approved sha256:3d6fe80401ec3fcb71b5a1f762509ad44c1aae04e2a7b32156aa3d09db146078 reviewer:human reviewer-id:probepark evidence:ci-green;generation-guard-green;daemon-generations-bumped;manifest-regenerated;capability-opt-in-scoped
|
Merged into dev as
Thanks @nahyeongjin1 — two good fixes in one morning. — |
What
Slack/Discord 채팅 데몬이 lean 모드의 최종 답변을 매번 두 번 게시하던 문제를 고칩니다.
SessionRouterOptions에 opt-in 옵션positionedNotificationEffects를 추가했습니다. 켜면 Router의 기본 SDK 연결이positioned_notification_effects_v1capability도 알립니다.ChatDaemonRuntime만 켭니다.gjc sdk session tail같은 일반 SDK observer는 지금처럼 두 공개 경로를 모두 받습니다(fix(sdk): broadcast positioned session events live to attached subscribers #4570).sdk/host/host.ts로 옮겼습니다. 공개 경로@gajae-code/coding-agent/sdk/bus/telegram-daemon에서도 같은 이름으로 계속 export됩니다.Why
turn_stream을 두 경로로 보냅니다(sdk/bus/index.tspushSessionFrame).eventenvelopepositioned_notification_effects_v1을 협상한 연결만 대상에서 제외합니다.ChatDaemonRuntime이 쓰는SessionRouter의 기본 연결은 observer/turn_streamcapability만 알려서 두 경로를 모두 받았습니다.messageRef도 없어서 두 사본을 하나로 묶을 식별자가 없고, 원시 사본이 두 번째 게시가 됐습니다.<sessionId>:<generation>:<seq>)에서 나온 결정적 clientMsgId였고, 다른 쪽은 publicationId가 없는 경로의 무작위 ID였습니다. 한 쌍은 해시를 역산해 좌표:1:128까지 확인했습니다.검토 후 채택하지 않은 대안
messageRef부여: threaded-render의 같은 메시지 수정(edit-in-place)과 rich 승격 동작이 바뀝니다.Testing
RED → GREEN
sdk-chat-daemon-worker.test.ts: "posts a lean final answer from the production host to {Slack,Discord} exactly once"ChatDaemonRuntime을 띄워 agent_start → message_end → turn_end → agent_end를 진행합니다.Expected: 1, Received: 2(Slack, Discord 모두)였고, 수정 후 GREEN입니다.sdk-host-wiring.test.ts: "only a positioned-effects SessionRouter negotiates positioned-only notification delivery"observer: truerouter는 capability를 알리지 않고 opt-in한 router만 알리는지 검증합니다.관련 테스트 묶음 (모두 exit 0, 실패 0)
3a03f30c6)에서 별도로 다시 돌린sdk-chat-daemon-worker+sdk-host-wiring+sdk-slack-daemon→ 248 pass / 0 fail기타
bun --cwd=packages/coding-agent run check→ exit 0 (경고 2·info 1건은 이 변경과 무관한 파일)bunx biome check→ exit 0sdk/bus/telegram-daemon)와 새 경로(sdk/host/host)에서 import한 상수가 같은 값인지 확인코드 읽기로만 확인한 동작 (전용 테스트 없음)
messageRef기반 같은 메시지 수정은 그대로입니다.Daemon generation bump (
ca9187525)SessionRouter.#createAttachedClient는 Slack/Discord 채팅 데몬 lifecycle의 보호 선언이라telegram-daemon-generation-guard가 generation 상향을 요구했습니다.bun scripts/telegram-daemon-generation-guard.ts --fix-generations 61f2e17243af522e1572caf8d82f8f0e6a696684로CHAT_DAEMON_GENERATIONS를 discord 82→83, slack 89→90으로 올리고 semantic manifest를 재생성했습니다. 이미 실행 중인 데몬은 교체되어야 새 capability를 협상하므로 의도한 효과입니다. 로컬 guard는required generation bump verified(exit 0)였고,sdk-chat-daemon-control-frames+sdk-daemon-cli-e2e85건 pass / 0 fail입니다.Approval
Merges to
devrequire one approving GitHub review from a write-access maintainer on the current head. Agent reviews (architect/critic) are advisory comments.Risk classification: medium / narrow — 알림 데몬의 SDK 연결 capability 협상을 바꿉니다. 일반 SDK observer와 Telegram 경로는 바뀌지 않습니다.
devbun checkpasses (bun --cwd=packages/coding-agent run check)packages/<pkg>/changelog.d/(if user-facing)