Skip to content

feat(events): render native hooks through warm runtime - #180

Merged
ScriptedAlchemy merged 1 commit into
mainfrom
wave4/97-event-routes-transport
Sep 1, 2026
Merged

feat(events): render native hooks through warm runtime#180
ScriptedAlchemy merged 1 commit into
mainfrom
wave4/97-event-routes-transport

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Summary

  • compile semantic event routes into target-native hook clients that validate bounded envelopes and call the generated MCP entry over local IPC
  • bind discovery and requests to the artifact epoch, enforce deadlines, secure per-user Unix sockets (or Windows named pipes), and fail closed on mismatch, timeout, malformed messages, or runtime failures
  • keep in-process route execution explicit through runtime: 'standalone' or fallback: 'standalone', with compile-time rejection when shared mode has no generated runtime host
  • project Agent.Context and route result decisions into Claude/Codex and Cursor native response envelopes

Evidence table additions

Runtime coordination

  • No @agent-bundle/runtime source changes in this PR; the shared-package seam remains limited to the Agent.Context vocabulary addition landed earlier.

Test plan

  • pnpm --filter @agent-bundle/runtime build && pnpm --filter agent-bundle build
  • pnpm exec rstest run packages/agent-bundle/tests/event-ipc.test.ts packages/agent-bundle/tests/event-project.test.ts packages/agent-bundle/tests/entry-shell.test.ts packages/agent-bundle/tests/route-graph.test.ts packages/agent-bundle/tests/target-hook-contract.test.ts packages/agent-bundle/tests/generated-route-server.test.ts packages/agent-bundle/tests/hook-playground-service.test.ts (65 passed)
  • pnpm typecheck
  • pnpm lint

Compile semantic event routes into epoch-bound thin clients so native hooks reuse the generated MCP runtime, while preserving explicit standalone execution and fail-closed transport behavior.
@changeset-bot

changeset-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e8e2f81

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ScriptedAlchemy
ScriptedAlchemy merged commit 363890b into main Sep 1, 2026
4 of 9 checks passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T19:39:34.576792Z e8e2f81 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8e2f81742

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

yield* liftPromise(async () => {
await mkdir(dirname(endpoint), { mode: 0o700, recursive: true });
await chmod(dirname(endpoint), 0o700);
await rm(endpoint, { force: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid unlinking another live runtime socket

When two host instances use the same artifact and target, they derive the same endpoint ID, so starting the second generated MCP runtime unconditionally unlinks the first runtime's still-live Unix socket and binds a replacement. Either runtime's later cleanup also removes that shared pathname, potentially making the other live runtime unreachable; concurrent desktop sessions can therefore route hooks to the wrong process or begin failing with runtime-unavailable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in #209 (merged as c69c6b5). Startup no longer unconditionally unlinks the endpoint: it probes the existing socket first — a live server fails the second startup with a typed runtime-failed error (leaving the owner untouched), and only a stale ECONNREFUSED file is removed. Shutdown removes the endpoint only when this instance still owns it (device/inode identity check), so a later replacement's socket is never deleted. Regression tests cover live-server rejection (owner still answers afterwards) and stale-file replacement.

Comment on lines +180 to +186
const handled = yield* liftPromise(() => options.handle({
artifactEpoch: parsed.data.artifactEpoch,
event: parsed.data.event,
hostContractRevision: parsed.data.hostContractRevision,
native: parsed.data.native,
target: parsed.data.target,
})).pipe(Effect.exit);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Propagate hook timeouts into the server render

When the client deadline expires or its signal is aborted after the request has been parsed, the client socket is destroyed but options.handle continues without any cancellation signal. The generated handler creates its own AbortController, so the corresponding dispatcher.dispatch can keep running and perform side effects after the hook has failed; repeatedly timing out a hung event route also leaves outstanding work in the warm worker.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in #209 (merged as c69c6b5). The per-connection handler now receives an AbortSignal wired to socket teardown (close/end/error) and the connection fiber is interrupted on disconnect, so an expired client deadline or abort cancels the in-flight render — mcp-server-runtime threads that signal into dispatcher.dispatch instead of a dangling controller. Regression test: destroying the client socket mid-render aborts the handler.

Comment on lines +444 to +447
const timeoutMs = route.config['timeoutMs'];
const timeout = typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0
? Math.ceil(timeoutMs / 1_000)
: undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve timeoutMs instead of rounding it up

For event routes whose configured deadline is not an exact multiple of one second, this converts the millisecond budget into a rounded-up host timeout and then the generated wrapper converts that value back to milliseconds for IPC. For example, timeoutMs: 1001 becomes a 2000 ms runtime deadline, allowing the route to run almost a second beyond the documented budget; retain the exact millisecond value separately while rounding only the native manifest's seconds field.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in #209 (merged as c69c6b5). The canonical hook model now carries timeoutMs end-to-end — event-route timeoutMs is no longer rounded to seconds at normalization. Conversion to the hosts' seconds unit (ceil) happens only at native document projection, and the generated wrapper consumes the exact milliseconds. Fixture test pins 1250 ms surviving in the model while the host document projects 2 s.

Comment on lines +117 to +119
const onData = (chunk: Buffer): void => {
raw += chunk.toString('utf8');
if (Buffer.byteLength(raw) > MAX_EVENT_MESSAGE_BYTES) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Decode UTF-8 across socket chunk boundaries

When a socket data boundary splits a multibyte UTF-8 character, decoding each Buffer independently inserts replacement characters before concatenation. Native payloads commonly contain Unicode paths or messages, so requests or projected responses can be silently corrupted even though the resulting JSON still parses; accumulate buffers or use StringDecoder so partial code points carry across chunks.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in #209 (merged as c69c6b5). Request decoding uses a per-connection StringDecoder with byte-accurate limit accounting (and newline-delimited framing), so multibyte UTF-8 code points split across chunk boundaries decode correctly. Regression test splits a request mid-🚀 across two socket writes and asserts a clean round-trip.

server.generatedRoutes !== undefined && server.targets.includes(target));
if (runtimeHost) continue;
diagnostics.push({
code: 'AB4816',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Assign a distinct diagnostic code to missing event runtimes

This reuses AB4816, which is already emitted by validateConventionalCliRoutes for unsupported rendered CLI commands. Consumers that key remediation, suppression, or documentation on diagnostic codes can no longer determine which unrelated condition occurred, so the new missing-runtime condition should receive its own stable code.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in #209 (merged as c69c6b5). The missing-shared-runtime condition now emits fresh code AB4817; AB4816 stays retired per the no-reuse rule. docs/diagnostics.md registry and range header updated, tests moved to the new code.

ScriptedAlchemy added a commit that referenced this pull request Sep 1, 2026
ScriptedAlchemy added a commit that referenced this pull request Sep 1, 2026
…e example's semantic event route (#182)

Both features merged while main CI was already red, so their outdated
sibling tests were never caught: normalizeHooks now orders hooks by
stable id, the rsc-agent-runtime example declares one tool/after event
route instead of per-host prebuilt hooks, and the workbench therefore
exposes the simulatable Hooks capability page.
ScriptedAlchemy added a commit that referenced this pull request Sep 1, 2026
… can go green (#197)

* test: align hook expectations with sorted normalization (#180) and the example's semantic event route (#182)

Both features merged while main CI was already red, so their outdated
sibling tests were never caught: normalizeHooks now orders hooks by
stable id, the rsc-agent-runtime example declares one tool/after event
route instead of per-host prebuilt hooks, and the workbench therefore
exposes the simulatable Hooks capability page.

* fix(ci): keep route-unit proofs out of the example plain test pool

* fix(ci): defer to the mainline Skill IR digest canonicalization (#191)
@ScriptedAlchemy
ScriptedAlchemy deleted the wave4/97-event-routes-transport branch September 3, 2026 05:30
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.

1 participant