Skip to content

feat(mecatui): emit agent lifecycle hooks to host editors - #1697

Merged
aponcedeleonch merged 3 commits into
mainfrom
i-would-like-to-integrate-meca
Sep 22, 2026
Merged

aponcedeleonch merged 3 commits into
mainfrom
i-would-like-to-integrate-meca

Conversation

@aponcedeleonch

Copy link
Copy Markdown
Member

What

mecatui now reports its session lifecycle to a host editor's agent lifecycle hook, so an editor hosting the TUI can notify you when the agent needs an approval and when a run ends.

Zero configuration, and inert unless a supported host is detected.

Why the payload follows a cross-vendor schema

The hook contract here is not one host's convention. It is a de-facto standard:

Anthropic Claude Code OpenAI Codex
Field hook_event_name hook_event_name (identical)
Delivery one JSON object on stdin one JSON object on stdin
Common fields session_id, transcript_path, cwd session_id, transcript_path, cwd
Events SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, Stop, StopFailure, SessionEnd, SubagentStart/Stop same vocabulary
Config event -> matcher group -> handler (hooks.json) same three-level nesting

Codex even exports CLAUDE_PLUGIN_ROOT/CLAUDE_PLUGIN_DATA "for compatibility with existing plugin hooks", so OpenAI is explicitly targeting Anthropic's contract.

Sources: Claude Code hooks reference, Codex hooks.

Consequently the package is named for the mechanism (agenthook), not for a host, and it emits the canonical event names (UserPromptSubmit / Stop / StopFailure / PermissionRequest). A host that normalizes those names understands them, whereas a host-specific alias would not travel to another host.

Why mecatui emits this itself

mecatl has no hook surface that carries session lifecycle. Its only hooks are the per-tool-call PreToolUse/PostToolUse permission gate (engine/governance), so neither integration shape a host can write into applies:

  • no lifecycle hook config file to write into, and
  • no plugin API for a host to load a listener into.

mecatui already consumes the session event stream to render, so it emits the lifecycle directly.

Where it hooks in

Three reducer points, each nil-safe:

Lifecycle Reducer site Emitted event
Agent began work ui/update.go beginTurnEvent UserPromptSubmit (busy)
Blocked on a human ui/approval.go applyPermissionAsk PermissionRequest
Run reached a terminal ui/update.go applyResult + 3 transport-death paths Stop / StopFailure

Design decisions worth reviewing

  • Host discovery is isolated. superset_host.go holds the only host-specific code (env detection plus the host's env var). The schema and delivery core are vendor-neutral, so a second host is a sibling detect function rather than a new package.
  • Inert by default. With no supported host, New returns nil, every method is a no-op, and behavior is byte-identical to before. Composition returns an honestly-nil interface to avoid the typed-nil trap.
  • Subagent asks are filtered (isChildAsk). A host drives terminal-level status from the main loop, so delegated work must not relabel the session.
  • Delivery is ordered through one worker. A goroutine per event lost ordering and could land the terminal before the busy signal. Caught by a test.
  • Deliveries are cancel-detached and separately bounded. The terminal event fires exactly as the run's context is torn down, so honoring that context would abort the hook before it could report completion. Same rationale as the server's cancel-detached durable append. The timeout still prevents a wedged hook from wedging the worker.
  • A full queue drops the oldest event instead of blocking. A stalled hook must never back-pressure the agent loop.
  • encoding/json marshals the payload. It carries model- and tool-influenced text, so hand-escaping would be a field-forgery hole. Pinned by a hostile-input test.
  • The auto-retry branch emits no terminal, because the run continues. Emitting there would produce a spurious completion notification mid-run.

Verification

  • task lint: 0 issues across all 7 modules.
  • go test -race ./cmd/mecatui/...: all green (36 new tests).
  • task docs: 431 documents, 3922 references, 0 broken links/anchors/orphans.
  • task site:build: succeeds.
  • Live end-to-end against a real host notify.sh: the full UserPromptSubmit -> PermissionRequest -> Stop sequence delivered in order, each accepted 200, with the agent identity intact. Outside a host terminal the notifier is nil.

Tests stay offline through an injected runner seam. A separate group exercises the real shell-out (payload on stdin, host env, best-effort on a failing script, context bound) against a local recording script. TestEventNamesAreCanonicalSchemaNames and TestPayloadUsesSchemaFieldNames pin the wire contract so a rename fails CI.

Docs

user-docs/mecatui/using-the-tui.md gains a short section. Per user-docs/_README.md, mecatui/ owns terminal-client behavior and this extends the existing page rather than adding one.

Scope and follow-up

This is the mecatl half. Making it fire under a specific host also needs that host to register mecatl as a known agent so its wrapper exports the agent identity the hook script cross-checks. For Superset that is five touch points in its own repo (agent-setup-targets.ts, agent-setup.ts, builtin-terminal-agents.ts, the wrapper, and the hook-matrix CI script). Until then this code is dormant and harmless.

Two unrelated pre-existing failures were observed while verifying and are not from this change (both reproduce with the change stashed):

  • deploy/helm/mecak8s TestMecak8sHelmChart_DeployCheckProductionFixtureRuntimeAndSpread resolves --session-lease-k8s-namespace from the ambient kubeconfig (toolhive-system) instead of default.
  • internal/app flaked once under full-suite load with a TLS handshake error, and passes in isolation with -race.

@JAORMX JAORMX 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.

Panel review — PR #1697

Reviewed: bd7d1e5d5c136f28c9f6eb4e9aa98cf58ab85d7b against the merge base with d1b659c1fe99bb9d5b5d8486c04b00d8edfe7b5a: 10 files, +1,282/−0, one commit.

Request changes for two lifecycle defects described inline: missing terminal transitions leave the notifier stuck busy, and detached workers can reorder lifecycle events across /connect restarts. The other findings are non-blocking. Superset-side registration is an explicitly accepted follow-up, not a merge blocker.

Spec

Source: this PR's description and commit contract.

  • Blocker — incomplete transport-terminal handling. Active StreamClosedMsg (including the clear handoff) and authentication recovery call endRun without notifying the hook. This violates the promised terminal notifications and suppresses the next busy signal because Notifier.running stays true. See the inline comment on ui/update.go.
  • Important — documentation overstates availability. user-docs/mecatui/using-the-tui.md:104–115 promises automatic Superset notifications, while this PR explicitly says the required host registration is deferred and the feature remains dormant until then. State that prerequisite; the deferral itself is fine.

Standards

Checked applicable AGENTS.md, test-isolation guidance, TUI architecture and lifecycle inventory conventions; skipped tooling-enforced formatting/lint rules.

  • Important — incomplete subprocess containment. agenthook/hook.go:252–258 kills only the direct command on timeout, unlike the existing process-group containment convention documented in ADR 0027 and implemented by procgroup.Configure. Descendants can survive the claimed bound.
  • Advisory — resource inventory. The worker/queue introduced at agenthook/hook.go:209–243 has no close/join owner and no ADR 0027 List 1 entry. Document ownership, cleanup, and reset behavior alongside the lifecycle fix.

Independent test adequacy

  • Important — transport terminals. ui/agenthook_notify_test.go:138–150 exercises an error ResultMsg, not transport termination. Cover ordinary stream errors, active EOF, clear handoff, and authentication recovery; then start a new run and assert a fresh busy signal.
  • Important — retry boundary. Existing semantic-retry tests do not attach a notifier. Exercise retry-eligible result → retry turn-start → final result and prove there is no intermediate terminal or duplicate busy signal.
  • Important — worker boundary contract. Current recorder tests and the direct runner deadline test do not prove queue saturation/oldest-drop, nonblocking producers, or the worker's independently bounded detached contexts. Use a controlled blocking runner and canceled caller context to assert those properties.

Domain

Panel: security, software architecture, library reuse, and duplication; disputed findings received a separate validation pass.

  • Blocker / High — missing terminal transitions. Cross-confirmed by architecture and library-reuse reviewers.
  • Blocker / High — cross-restart ordering. A prior notifier's delayed Stop can arrive after the replacement notifier's UserPromptSubmit; workers need an owned, bounded shutdown boundary before restart.
  • Important / Medium — subprocess descendants outlive timeout. Cross-confirmed by security and library-reuse reviewers; reuse the existing process-group facility.
  • Important / Medium — promoted approvals are silent. A main ask queued behind a background-child ask never emits PermissionRequest when it becomes visible.
  • Advisory / Info — standard-library reuse. agenthook/hook.go:300–306: replace the custom utf8Start predicate with unicode/utf8.RuneStart.

The validation pass rejected the claimed Medium credential-boundary defect: this is an operator-installed host hook, not an agent-facing shell. It also rejected speculative Superset-only restructuring, removal of intentionally ignored interface contexts, and consolidation of focused tests. No upheld duplication finding.

Verification and totals

All applicable checks on the reviewed revision are green; live e2e jobs are skipped. No local tests or linters were run. No implementation changes made.

Counts preserve the independent axes: a root cause reported by Spec and Domain counts in both; duplicates within Domain count once. Thus the three blocker findings below represent two distinct blocking defects.

PANEL: ship_blockers=3 important=7 advisory=2 reviewer_failures=0

Comment thread cmd/mecatui/ui/update.go
Comment thread cmd/mecatui/agenthook/hook.go Outdated
Comment thread cmd/mecatui/agenthook/hook.go
Comment thread cmd/mecatui/ui/approval.go Outdated
mecatui now reports its session lifecycle to a host editor's agent lifecycle
hook, so an editor hosting the TUI can raise a notification when the agent needs
an approval and when a run ends.

The emitted payload follows the cross-vendor agent-hook schema rather than any
single host's convention: one JSON object on stdin carrying `hook_event_name`
plus the schema's common fields (`session_id`). Anthropic's Claude Code
originated that contract and OpenAI's Codex adopted it field-for-field, down to
exporting CLAUDE_PLUGIN_ROOT for plugin-hook compatibility, so the package is
named for the mechanism (agenthook) and emits the canonical event names
(UserPromptSubmit / Stop / StopFailure / PermissionRequest). A host that
normalizes those names understands them; a host-specific alias would not travel.

mecatl has no hook surface of its own that carries session lifecycle. Its only
hooks are the per-tool-call PreToolUse/PostToolUse permission gate, so neither
of the integration shapes a host can write into (a hook config file, or a plugin
the agent loads) applies. mecatui already consumes the session event stream to
render, so it emits the lifecycle directly from three reducer points: turn start
becomes the busy signal, a main-session permission ask becomes
PermissionRequest, and the terminal result becomes Stop or StopFailure.

Design notes:

- Host discovery is the one host-specific half and lives in superset_host.go;
  the schema and delivery core are vendor-neutral. A second supported host is a
  sibling detect function, not a new package.
- Inert by default: with no supported host detected, New returns nil, every
  method is a no-op, and behavior is byte-identical to before.
- Subagent asks are filtered (isChildAsk). A host drives terminal-level status
  from the main loop, so delegated work must not relabel the session.
- Delivery is ordered through one worker. A goroutine per event lost ordering,
  which could land the terminal before the busy signal.
- Each delivery is detached from the run context and separately bounded. The
  terminal event fires as the run's context is torn down, so honoring that
  context would abort the hook before it could report completion.
- A full queue drops the oldest event instead of blocking. A stalled hook must
  never back-pressure the agent loop.
- The payload is marshalled with encoding/json. It carries model- and
  tool-influenced text, so hand-escaping would be a field-forgery hole.
- The auto-retry branch deliberately emits no terminal, since the run continues.

Tests are offline via an injected runner seam, and cover the real shell-out
(payload on stdin, host env, best-effort on failure, context bound) against a
local recording script. TestEventNamesAreCanonicalSchemaNames and
TestPayloadUsesSchemaFieldNames pin the wire contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Panel review on #1697 found two lifecycle defects plus three smaller ones.

Missing terminal transitions. The notifier closes a busy period only when it
sees a terminal, and Start dedupes while running stays true. Three run-ending
paths called endRun without telling it: StreamClosedMsg for an active run,
the same message under the /clear handoff, and reduceLiveAuthRecovery. After
any of them the host stayed busy forever, because the NEXT run's Start was
silently deduped. All three now report a failed terminal. The genuine
non-terminals keep their exemption: an automatic failed-step retry continues
the same run, and a parked authorization (phaseAuthorizing) is suspended
rather than over, so neither emits.

Cross-generation reordering. A /connect restart re-enters runWithOptions and
builds a fresh notifier, but the old worker had no owner: its channel was
never closed and nothing joined it. With a slow hook command the retired
generation could deliver its queued Stop after the successor's
UserPromptSubmit, marking the host idle during a live run. Notifier.Close
closes the queue and joins the worker within a bound, then abandons; past
that boundary enqueue drops instead of resurrecting a worker. main settles it
inside runCleanup, before any restart can build a successor.

Writing the concurrency test for that found a real bug in the first version
of Close: enqueue captured the channel under the mutex and sent after
releasing it, so a concurrent Close could close the channel under an in-flight
send. Both the send and the close now happen under the lock, which is sound
because every send arm is non-blocking and the worker never takes the lock.

Descendant containment. exec.CommandContext kills the script, not helpers it
spawned, so a hook that backgrounds a curl or a sleep outlived the bound this
package advertises and repeated deliveries accumulated orphans. Reuse the
convention already in hookexec and statusline: procgroup.Configure plus a
nonzero WaitDelay backstop.

Silent promoted approvals. A main ask arriving behind an open background-child
card was queued with no notification, and promoting it only re-rendered, so
the host never learned the main session wanted attention. Both entry points
now route through one emitter keyed on the visible-head transition, so the
child filter cannot drift between them.

Docs and inventory. The user-docs page promised automatic notifications while
the required Superset-side registration is still deferred; it now states that
prerequisite. The worker and its queue get ADR 0027 List 1 row 75 with a
re-audit recording why they take no List 2 row.

Tests. Reducer coverage for all four transport terminals, each followed by a
second run so a stuck-busy notifier fails; the retry boundary proving one busy
signal and exactly one terminal at the end; both promotion cases. In the
package: nonblocking producers against a stalled worker, oldest-drop
saturation, cancel-detached but independently bounded deliveries, the drain
and its bound, and the close boundary. Every new test was mutation-checked
against the reverted fix. The containment test withholds the kill until the
descendant provably exists — an earlier fixed-timeout version passed with
procgroup removed, because the shell had not forked yet, and asserted nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aponcedeleonch

Copy link
Copy Markdown
Member Author

Thanks Ozz, this was a good catch list. Both blockers are fixed, and I took every other finding too. Inline replies cover the four threads; the rest:

  • Docs overstate availability. Fixed. The page now says the reports reach Superset only once it registers mecatl as a hook-emitting agent, and that nothing appears until then.
  • Resource inventory. Added as ADR 0027 List 1 row 75, with a re-audit paragraph recording why it takes no List 2 row (display-only payloads, nothing a restart couldn't re-derive) and why the row exists anyway: ordering across the generation boundary is a correctness property, not a best-effort one.
  • utf8Start → utf8.RuneStart. Done.
  • Test adequacy, all three. Transport terminals and the retry boundary are covered in the reducer; the worker boundary contract (nonblocking producers against a stalled worker, oldest-drop saturation, cancel-detached but independently bounded deliveries) is covered in the package with a controlled blocking runner.

Two things I'd rather you know than not:

The concurrency test I wrote for the shutdown found a real bug in my first Close: enqueue sent on the queue after releasing the mutex, so a concurrent Close could close it under an in-flight send. Send and close now both happen under the lock.

My first descendant-containment test was vacuous. It used a fixed 200ms timeout and passed with procgroup removed, because the shell hadn't forked the descendant yet. The shipped version waits for a started marker before cancelling, and I verified it fails without the fix. I mutation-checked every new test the same way.

Gates: task lint, task test (full -race), task docs, task site:build, and mecademo all green locally.

@JAORMX

JAORMX commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Follow-up review

Reviewed 57e9353ccdf455fb13d82c4ac7145e2dc0965a88. Most of the previous comments are addressed, but I’m keeping the existing request for changes because the cross-restart ordering blocker remains.

Addressed

  • The reported active EOF, clear-stream EOF, and authentication-recovery paths now settle the notifier.
  • Subprocess containment uses procgroup.Configure and a nonzero WaitDelay, with a marker-synchronized descendant test.
  • Promoted main approvals now emit PermissionRequest, while child approvals remain filtered.
  • The user docs state the pending Superset registration prerequisite.
  • The worker has an owner and lifecycle inventory entry, and the custom UTF-8 predicate is replaced with utf8.RuneStart.
  • Transport-terminal, retry, queue, and context tests have been added.

Blocker: shutdown still allows old notifications into the successor run

At cmd/mecatui/agenthook/hook.go:252–305, Close closes the buffered channel and waits for either the worker or its own deadline. When that deadline expires, it does not cancel the active delivery or discard buffered events. The worker’s for d := range q continues draining those events afterward.

Main allows three seconds for shutdown (cmd/mecatui/main.go:694–708), while each delivery gets five seconds. An old Stop can therefore still arrive after the successor’s UserPromptSubmit. Closing the channel prevents new enqueues; it does not prevent already-queued deliveries. This is the original cross-generation ordering issue, not a new requirement.

Please give shutdown an owned cancellation path, discard remaining work when the drain deadline expires, and ensure the worker has joined before starting the successor. The inventory’s guarantee that retired generations cannot affect successors should match that behavior.

The regression test also needs to cross this actual boundary. TestNoDeliveryCrossesTheCloseBoundary (cmd/mecatui/agenthook/worker_test.go:216–235) drains a fast recorder successfully and then checks rejection of new enqueues. It does not exercise an in-flight or queued old terminal surviving a timed-out close. Hold an old delivery, queue a terminal, expire shutdown, and attempt the successor transition; prove no predecessor notification can arrive afterward.

Important, non-blocking: two other terminal paths remain

  • Successful /clear response before the old stream terminal: cmd/mecatui/ui/update.go:790–800 calls endRun and invalidates pending old-stream messages without settling the hook. The Clear RPC response and stream terminal arrive through independent commands, so server-side completion does not establish their reducer order. Settle the still-unsettled source before binding the successor, and test the Clear response arriving first.
  • MCP authorization continuation transport failure: cmd/mecatui/ui/mcp_authorization.go:374–382 calls endRun without settling the hook when the authorization control stream owns an active run. Cover continuation turn-start → control-stream error → next run.

Both can leave Notifier.running true and suppress the next busy signal.

Optional test strengthening

  • Assert the exact surviving FIFO suffix in the oldest-drop test (cmd/mecatui/agenthook/worker_test.go:93–130).
  • Assert the second delivery gets a fresh, live context rather than only observing that its runner starts (cmd/mecatui/agenthook/worker_test.go:155–174).

The reducer and notifier tests are otherwise reasonable at their separate layers; I’m not asking for redundant full-composition tests for retry or every transport terminal. Superset-side registration remains an accepted follow-up, not a blocker.

Applicable CI checks are green. No local tests or linters were run, and no implementation changes were made.

Panel totals retain independent axes: the production shutdown blocker also appears as a test-adequacy blocker; the inaccurate lifetime guarantee is a Standards finding. Thus the two blocker entries represent one production defect plus its missing regression coverage.

PANEL: ship_blockers=2 important=3 advisory=2 reviewer_failures=0

Close treated "closed the queue" as "no more deliveries", but those are
different facts. Closing the queue only stops new enqueues; the worker's
`for d := range q` goes on draining everything already buffered, each event
getting a fresh full 5s timeout. So when the 3s drain bound expired, Close
returned while the retired worker was still delivering. main then built the
successor notifier, and a queued generation-N Stop could land after
generation N+1's UserPromptSubmit, marking the host idle during a live run.
That is the cross-generation reordering the boundary exists to prevent, and
the inventory claimed it could not happen.

Close now REVOKES on the timeout path: a new abandon channel cancels the
context every delivery descends from, so the invocation in flight is torn
down and the worker discards the rest of the backlog rather than delivering
it, and Close then joins. The guarantee is now stated exactly, in the doc
comment and in ADR 0027 row 75 -- once Close returns no delivery from that
generation can START; on the revoked path the only residual is the single
already-cancelled invocation.

Two more active-run terminals were also reaching endRun without settling the
hook, each leaving Notifier.running true and silently deduping the next run's
busy signal:

- The /clear RPC response winning the race against its own source stream
  terminal. The two arrive on independent commands, so server-side completion
  does not fix their reducer order, and the StreamClosedMsg branch that
  settles the hook never runs when the response lands first.
- An MCP authorization continuation whose control stream dies while it owns
  the active run. No ResultMsg follows, so that is a genuine transport
  terminal.

Tests. TestRevokedGenerationCannotDeliverIntoItsSuccessor crosses the
timed-out shutdown specifically, with both generations sharing one runner so
the recorded order is what the host would observe; reverting the revocation
reproduces the exact reported sequence (gen-1 start, gen-2 start, gen-1
StopFailure). TestCloseJoinsTheWorkerOnTheRevokedPath pins the join. Both
new reducer terminals get a start-failed stop-start test. Every new test was
mutation-checked against the reverted fix.

Also tightened two existing assertions the review called out: the oldest-drop
test now parks the worker first so the surviving set is deterministic and
asserts the exact contiguous FIFO suffix, and the independently-bounded test
now proves the second delivery gets a fresh live context with its own
deadline, sampled at entry so it cannot race the bound.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aponcedeleonch

Copy link
Copy Markdown
Member Author

Good catch, and you're right that it's the original issue rather than a new requirement. I'd conflated "closed the queue" with "no more deliveries", and those aren't the same fact.

The blocker

Fixed in ec394e9. Closing the queue only stopped new enqueues; the worker's range kept handing over the backlog, each event with a fresh full 5s timeout. So the 3s drain bound expiring meant Close returned while the retired worker was still delivering.

Close now revokes on the timeout path. A new abandon channel cancels the context every delivery descends from, so the invocation in flight is torn down and the worker discards the rest of the backlog instead of delivering it, and only then does Close join.

I've also stated the guarantee exactly rather than gesturing at it, in both the doc comment and ADR 0027 row 75: once Close returns, no delivery from that generation can start. On the revoked path the only residual is the single already-cancelled invocation, which the bounded join normally collects too. The old row claimed something stronger than the code delivered, which is the mismatch you flagged.

You were also right that the old regression test proved nothing about this. TestRevokedGenerationCannotDeliverIntoItsSuccessor now holds a delivery, queues a terminal behind it, expires shutdown, starts the successor, then releases. Both generations share one runner, so the recorded order is what the host would actually observe. Reverting the revocation reproduces your sequence exactly:

gen-1 UserPromptSubmit, gen-2 UserPromptSubmit, gen-1 StopFailure

TestCloseJoinsTheWorkerOnTheRevokedPath pins the join separately, since "cannot start a new delivery" and "has exited" are different claims and I only want to make the ones that hold.

The two other terminal paths

Both fixed, each with a start -> failed stop -> start reducer test.

The /clear one is the more interesting: the response and the stream terminal arrive on independent commands, so server-side completion doesn't establish their reducer order, and when the response wins the StreamClosedMsg branch that settles the hook never runs at all. I'd only covered the other ordering.

The MCP authorization continuation now settles before endRun when the control stream owns the active run.

Test strengthening

Took both. The oldest-drop test parks the worker before saturating, which makes the surviving set deterministic instead of racing the worker's first receive, so it can assert the exact contiguous FIFO suffix rather than loose bounds. The independently-bounded test now proves the second delivery gets a fresh live context carrying its own deadline, sampled at entry so the assertion can't race the 60ms bound.

Every new test was mutation-checked against the reverted fix.

Gates: task lint, task test (full -race), task docs, and mecademo all green locally.

@JAORMX

JAORMX commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Follow-up review — approved

Reviewed ec394e9c99777c623101b10c965d99132ade72d3. The fixes requested in my last review are addressed, including the original cross-restart ordering blocker. Approving with two non-blocking follow-ups.

Addressed

  • Timed-out shutdown now revokes the active delivery and discards queued notifications. With the production runner's process-group termination and WaitDelay, this resolves the old worker's queued terminal crossing into a successor run.
  • A successful /clear response arriving before the old stream terminal now settles the notifier before binding the successor.
  • An active MCP authorization continuation control-stream error now settles the notifier before ending the run.
  • The regression tests now exercise a held predecessor delivery, queued terminal, expired shutdown, and successor; actual worker joining; the exact surviving FIFO suffix; and a fresh live context for the second delivery.

Important, non-blocking: handle premature MCP control-stream EOF too

At cmd/mecatui/ui/mcp_authorization.go:273–296, the sibling mcpAuthorizationStreamClosedMsg branch clears active ownership and moves to idle without notifyHookFailed or endRun. If an owned active continuation closes before delivering a ResultMsg, the notifier remains busy and suppresses the next start notification.

Please apply the same owned-active terminal treatment as the newly fixed stream-error branch, preserving harmless EOF after a result and ordinary authorization-polling closure. Add an EOF-before-result regression followed by another run. This has the same non-blocking severity as the additional MCP error-terminal omission in the previous review.

Advisory: qualify the lifecycle inventory wording

docs/adr/0027-cloud-native.md:984 should distinguish preventing new/queued dispatches from the explicitly admitted cancelled, in-flight residual. The claim that no dispatch can start does not by itself prove that no already-started invocation can have a later external effect. This is wording precision, not a remaining production shutdown blocker.

Verification

All applicable CI checks on this revision are green: 29 successful checks, none pending or failing. No local tests or linters were run, and no implementation changes were made.

Spec and requested regression coverage pass. Standards retains the inventory wording advisory; Domain retains the premature-EOF finding, independently identified by two reviewers. A disputed shutdown blocker based on a context-ignoring runner was rejected after validating the production runner and restart ownership.

PANEL: ship_blockers=0 important=1 advisory=1 reviewer_failures=0

@JAORMX JAORMX 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.

Approved at ec394e9. The requested fixes and original cross-restart ordering blocker are addressed. All applicable CI checks are green. Two non-blocking follow-ups (premature MCP control-stream EOF and inventory wording) are detailed in #1697 (comment).

@aponcedeleonch
aponcedeleonch merged commit c006c99 into main Sep 22, 2026
35 checks passed
@aponcedeleonch
aponcedeleonch deleted the i-would-like-to-integrate-meca branch September 22, 2026 11:09
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