feat(slack): adopt existing threads before readiness - #3816
Conversation
|
@Yeachan-Heo — Maintainer review requested for exact head
Fork workflows require maintainer approval:
The PR-author token cannot use the formal requested-reviewers endpoint (404), so this single comment is the review notification. No duplicate mutation was opened. |
5771660 to
c0af9cd
Compare
|
Rebased onto current Exact-head validation, run in a disposable worktree checked out at exactly GitHub CI has not run on this PR. The workflow runs exist but are held for maintainer approval because this is a fork PR — Ready for review. |
c0af9cd to
0fdceb5
Compare
|
The only conflict was in Re-running the generator against the committed result produces no diff, so the file matches what CI would generate. Exact-head validation in a disposable worktree checked out at
|
Yeachan-Heo
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — exact-head hostile review
Reviewed exact head 0fdceb5e317f75c524dfedfab5f7fe766602b9ed against exact base/current dev 732856b3ccb3fade6e9fbc17908a4fbca5a7682f. GitHub reports MERGEABLE / UNSTABLE.
Blocking findings
-
HIGH — a reported cancellation can still commit later (
chat-daemon-command-channel.ts:435-436, 626-641). The timeout path returnscancelledafter winning the response claim, but request deletion suppresses every error and the response claim is then also removed. If request unlink fails while response unlink succeeds, the still-live request can be served later and commit after the CLI was told no mapping changed. Cancellation must remain fenced when cleanup is uncertain. -
HIGH — prepared sessions still accept mutating controls before activation (
sdk/host/host.ts:225-232, 350-367). Deferred mode withholds onlysession_ready; everycontrol_request, including prompt controls, still reaches the application control dispatcher without checkingprepared/ready. The new pre-activation interval therefore makes activation optional for input admission. -
HIGH — binding/activation authority has restart and endpoint-swap ABA gaps (
existing-thread-readiness.ts:60-71, 108-132;slack-thread-binding.ts:140-166;chat-daemon-runtime.ts:447-461, 522-528). Host generations restart from 1 in a new process. Activation proves endpoint A, rereads and connects to whatever endpoint exists, then authorizes only by session ID plus numeric generation. Binding also ignores the indexeddefaultversuschatscope,endpointMtimeMs, and restart-stable incarnation, while the runtime compares only generation to its captured attachment. A replacement process can repeat the generation and consume an old binding; valid.gjc/state/chat/sdksessions can also be rejected or checked against the wrong endpoint. -
HIGH — malformed preparation input fails open (
coordinator-mcp/server.ts:4184-4199, 5078-5081, 5124-5130). Runtime dispatch does not validate the advertised schema.prepare_existing_thread: "true"is silently treated as false, so a supplied initial prompt starts an ordinary ready session instead of being rejected before mutation. -
HIGH — failed defensive cleanup is swallowed and terminally cached (
coordinator-mcp/server.ts:4236-4248). If the broker returns a live non-prepared session, the compensatingsession.closefailure is ignored; the created session identity is discarded and the start failure is sealed by idempotency. A ready, untracked session can remain alive while exact retries only replay the cached error. -
MEDIUM — shutdown does not fence in-flight activation (
sdk/host/host.ts:273-319).stop()leaves#startedtrue while awaiting broker unregister. An already-running activation gate can resolve during that await and publishsession_readyafter shutdown has begun.
Dependency and CI
- This exact head adds mandatory
conversations.repliesroot verification but does not contain #3753's form-encoding fix. The contributor's live evidence shows the current JSON path returninginvalid_arguments; #3753 must land first or be folded into this branch before the feature is deployable. - Exact-head
Dev CIrun30936007622andPublic site syncrun30936006416are bothaction_required; the commit has 0 check runs and 0 statuses. No CI was approved, rerun, or controlled in this review.
Local evidence
bun --cwd=packages/coding-agent run check— passed.- Five focused feature files —
126 pass / 0 fail / 787 assertionswith a 20s test timeout after one default-timeout-only rerun; the isolated activation subset was14/14green. This local evidence does not replace exact-head CI.
Contributor ledger
nahyeongjin1 remains the owner of this #3816 feature lane. The same contributor's #3753 is the canonical form-encoding prerequisite; #3824 was self-closed as its duplicate, so no replacement/duplicate implementation lane is needed.
gajae.pr-review-verdict.v1 REQUEST_CHANGES head:0fdceb5e317f75c524dfedfab5f7fe766602b9ed base:732856b3ccb3fade6e9fbc17908a4fbca5a7682f reviewer:Yeachan-Heo
Signed: GJC / Yeachan-Heo via GitHub CLI
Yeachan-Heo
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — exact-head independent re-review (glm-deepseek lane)
Fresh hostile review at exact head 0fdceb5e317f75c524dfedfab5f7fe766602b9ed against exact base/current dev 732856b3ccb3fade6e9fbc17908a4fbca5a7682f. GitHub reports MERGEABLE / UNSTABLE. This re-confirms the prior REQUEST_CHANGES disposition under a new lane; each blocking finding was independently re-derived against the exact head, with one refinement on Finding 3.
Blocking findings (all re-confirmed)
1. HIGH — a reported cancellation can still commit later. submitAgainstScope claims the response entry and returns cancelled (chat-daemon-command-channel.ts:614), then the finally unlinks the request before the response (:640-641). Both unlinkScopedEntry calls suppress errors (:435-436). If the request unlink fails while the response unlink succeeds, the orphaned request remains servable: a later serveChatDaemonCommandsAgainstScope sees no response entry (:735), re-claims it, and the daemon's commitAuthority re-reads the still-present request (:810) and commits. The submitter was already told cancelled — no mapping changed. Cancellation must remain fenced when cleanup is uncertain.
2. HIGH — prepared sessions still accept mutating controls before activation. host.ts #onFrame dispatches control_request straight to this.#options.control with no prepared/ready gate (:353-367). Deferred readiness withholds only session_ready (:225-231); it does not withhold control_request, query_request, or register_provider. The pre-activation interval therefore makes activation optional for input admission, and no test covers a control_request arriving on a prepared-but-not-ready session. Confirmed untested: sdk-session-readiness-lifecycle.test.ts has zero control_request assertions.
3. HIGH — binding/activation authority has restart and endpoint-swap ABA gaps. Refined from the prior review: the daemon generation is a static constant (slack: 25, chat-daemon-control.ts:50-53), not a per-process counter, and incarnation is OS-proven process-start provenance (process-incarnation.ts:108-121, Linux /proc/<pid>/stat start ticks — monotonic, restart-unique). So the owner tuple {ownerId, pid, incarnation, generation} is restart-distinct. The genuine ABA surface is the endpoint generation, which is the SDK session host's event-ring generation and is not globally unique across distinct sessions: activation proves endpoint A, activatePreparedSession re-resolves and connects to whatever endpoint the index currently reports, then authorizes by session ID + numeric generation (existing-thread-readiness.ts:104-136; slack-thread-binding.ts:140-166). The binding fence also does not consult endpointMtimeMs or the default/chat scope that attach() uses to reject stale/foreign attachments (chat-daemon-runtime.ts:447-461). A replacement process that repeats the endpoint generation, or a valid .gjc/state/chat/sdk session checked against the wrong scope, can consume or reject an old binding.
4. HIGH — malformed preparation input fails open. coordinator-mcp/server.ts:4184 uses args.prepare_existing_thread === true. The MCP dispatcher passes params.arguments verbatim with no schema validation (:5124-5130), so a client sending prepare_existing_thread: "true" (string) evaluates to false: the session starts as an ordinary ready session and accepts the initial prompt, instead of being rejected before mutation as the schema implies.
5. HIGH — failed defensive cleanup is swallowed and terminally cached. When the broker returns a live non-prepared session, the compensating session.close failure is swallowed (.catch(() => undefined), server.ts:4239-4244), the session identity is discarded, and the thrown SdkClientError is caught by withToolIdempotency's .catch(error => sdkError(error)) (:2690) and sealed as completed (:2692-2697, since isNonterminal defaults to () => false). A ready, untracked session remains alive while exact retries replay the cached error.
6. MEDIUM — shutdown does not fence in-flight activation. #stopStartedHost awaits broker.unregister while #started is still true (host.ts:313-319); #started is set false only after that await. An activation gate resolving during that await passes the post-gate #started re-check (:290) and publishes session_ready after shutdown has begun.
Dependency and CI
- This exact head adds mandatory
conversations.repliesroot verification (findMessageByTimestamp) but does not contain #3753's form-encoding fix — at this head#requeststill serializes JSON. The contributor's live evidence shows that path returninginvalid_arguments; #3753 must land first or be folded into this branch before the feature is deployable. - Exact-head
Dev CIrun30936007622andPublic site syncrun30936006416are bothaction_required(fork PR). Head has 0 check runs and 0 statuses; commit statepending. No CI was approved, rerun, or controlled.
Exact-head local evidence (independently reproduced)
Detached worktree at 0fdceb5e, natives rebuilt from source:
bun --cwd=packages/coding-agent run check— pass (Biome 2524 files clean +tsc --noEmitexit 0).- Five focused feature suites — 126 pass / 0 fail / 787 assertions:
sdk-session-readiness-lifecycle(9/29),sdk-slack-thread-binding+sdk-chat-daemon-control-frames+notify-thread-commands(51/429),coordinator-mcp-server(66/329). This local evidence does not substitute for exact-head CI.
Contributor ledger
nahyeongjin1 remains the owner of this #3816 feature lane. The same contributor's #3753 is the canonical form-encoding prerequisite; #3824 was self-closed as its duplicate, so no replacement/duplicate implementation lane is needed.
gajae.pr-review-verdict.v1 REQUEST_CHANGES head:0fdceb5e317f75c524dfedfab5f7fe766602b9ed base:732856b3ccb3fade6e9fbc17908a4fbca5a7682f reviewer:Yeachan-Heo lane:glm-deepseek
Signed: GJC / Yeachan-Heo via GitHub CLI
Add an opt-in prepare, bind, and activate lifecycle for daemon-owned Slack thread adoption. Fence binding and activation on exact live authority, corroborate durable mappings, and drop control frames before chat mutation. Co-authored-by: gaebal-gajae <gaebal-gajae@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
Owner review of Yeachan-Heo#3816 found six blocking gaps where a failure or an unvalidated input was allowed to fall through instead of stopping. Cancellation: the cleanup unlinked the request and the response claim while suppressing both errors. If only the request unlink failed the orphaned request stayed servable, so a submission already answered `cancelled` could be re-claimed and committed. The claim is now released only once the request is proven gone; a leaked response object is inert, releasing it is not. Activation: deferred readiness withheld `session_ready` but left the control dispatcher reachable, making activation optional for input admission. `control_request` is now refused while prepared. `session_activate` is a separate frame type, so activation itself is unaffected. Binding authority: the proof was built from one endpoint read and the connection made from a second, and neither derived the state-root scope nor compared `endpointMtimeMs`. A replacement process repeating the endpoint generation could be connected to, and a `.gjc/state/chat/sdk` session was read at the wrong scope. Authority now derives scope exactly as the runtime's attach() fence does, verifies the endpoint mtime the index observed, and carries the proven endpoint so activation never re-resolves. Preparation input: `prepare_existing_thread` was compared with `=== true` against unvalidated dispatch arguments, so the string "true" silently became false and started an ordinary session that accepted the prompt. A non-boolean is now rejected before any mutation. Compensation: a failed defensive `session.close` was swallowed and the resulting error sealed under the idempotency key, leaving a live untracked session while retries replayed the cached failure. The failure now names the session and is marked nonterminal so the key stays open. Shutdown: `#started` stayed true across the unregister await, so an activation resolving during teardown could publish readiness afterwards. Teardown now fences before its first await. Lore-id: 4b91c7d2 Constraint: activation must remain reachable while controls are withheld Constraint: a cancelled submission must never commit later Rejected: gate query_request and register_provider too | read-only and setup paths, blocking them risks breaking legitimate pre-activation clients without closing an admission gap Rejected: keep the second endpoint read and compare pid again | the generation and pid can both repeat across a restart; only the mtime-proven endpoint is unambiguous Confidence: high Scope-risk: moderate Reversibility: easy Tested: bun test sdk-session-readiness-lifecycle sdk-slack-thread-binding sdk-chat-daemon-control-frames notify-thread-commands coordinator-mcp-server (128 pass, 0 fail) Tested: bun --cwd=packages/coding-agent run check Not-tested: prepare_existing_thread rejection and the compensation nonterminal path have no dedicated case; the coordinator harness needs setup this change did not add Co-authored-by: Claude <noreply@anthropic.com>
0fdceb5 to
5a17d79
Compare
|
All six blocking findings addressed. New exact head Thank you for the Finding 3 refinement — the owner tuple really is restart-distinct, and pointing at the endpoint generation instead sent me to the actual window. 1 — cancellation fencing. 2 — controls before activation. 3 — binding/activation authority. Two defects, one fix. 4 — malformed preparation input. A non-boolean 5 — swallowed compensation. The compensating 6 — shutdown fencing. Verification at
|
Summary
prepare -> bind -> activatelifecycle so a live GJC session can adopt an existing Slack thread before publishing readinessbind-threadthrough the running Slack daemon's exact owner/generation command channel; the CLI never writes conversation mappings directlySafety invariants
unknown, not a definitive rejectionalreadyfrom stale/terminal durable stateVerification
bun test packages/coding-agent/test/sdk-session-readiness-lifecycle.test.ts packages/coding-agent/test/sdk-slack-thread-binding.test.ts packages/coding-agent/test/sdk-chat-daemon-control-frames.test.ts packages/coding-agent/test/notify-thread-commands.test.ts— 60 pass / 0 fail / 458 assertionsbun --cwd=packages/coding-agent run check:types— passgit diff --check— passcargo fmt --all -- --check— pass on the frozen Rust bytes before the final TypeScript-only remediation roundsThe local root
check:tsaggregate was not claimed green: its long SDK adapter-disposition matrix exhausted the Hermes gateway's file-descriptor budget and was terminated after its emitted receipts remained green through the observed range. Focused/package checks above completed normally; PR CI should run the canonical aggregate in a clean runner.Scope and dependency
This is one atomic opt-in delivery contract: lifecycle preparation without daemon-owned adoption is unusable, and adoption without activation/control-frame containment is unsafe. Same-UID kernel peer authentication is intentionally a separate platform-hardening concern.
Live Slack verification is intentionally not included. The installed v0.12.7 runtime still hits the
conversations.repliesrequest-encoding failure addressed by #3753; merge/install/live smoke remain separate from this source PR.