Skip to content

fix(cli): unblock prebuilt release entrypoint (SyntaxError + init deadlock) - #8

Closed
continued-agent wants to merge 1 commit into
mainfrom
fix/cli-release-entrypoint-circular-deps
Closed

fix(cli): unblock prebuilt release entrypoint (SyntaxError + init deadlock)#8
continued-agent wants to merge 1 commit into
mainfrom
fix/cli-release-entrypoint-circular-deps

Conversation

@continued-agent

Copy link
Copy Markdown
Owner

Problem

The prebuilt CLI bundle produced by npm run build crashes at module load with:

SyntaxError: Unexpected reserved word
    at compileSourceTextModule (node:internal/modules/esm/utils:346:16)

The Release CLI workflow (triggered by cli-v* tag pushes) was building the bundle successfully but failing at the Verify release entrypoint step, so cli-v0.1.0 could not be released.

Root cause

Circular static imports between three modules caused esbuild to emit a __esm factory for src/stream/streamChatResponse.autoCompaction.ts that contained a top-level await without the async keyword:

  • src/compaction.ts <-> src/stream/streamChatResponse.ts (the former called streamChatResponse for streaming the summary; the latter used pruneLastMessage from the former).
  • src/subagent/executor.ts static-imported streamChatResponse, adding a second cycle through the tools/subagent init chain.

The earlier minifySyntax: false fix only stopped esbuild from mangling the keyword; it did not remove the cycle. With the cycle removed, a second symptom surfaced in the form of a module-init deadlock (init_streamChatResponse waiting on init_subagent/executor waiting on init_streamChatResponse) that hung the bundled CLI indefinitely.

Fix

Break both cycles with the minimum number of files:

  1. Extract pruneLastMessage into src/util/chatHistoryPrune.ts so the stream layer and the compaction helpers no longer share a static edge through compaction.ts.
  2. Move compactChatHistory (and the related COMPACTION_PROMPT / Compaction* types) into a new src/stream/streamChatResponse.compactChatHistory.ts so compaction.ts becomes a leaf with no dependency on the stream layer, and streamChatResponse.autoCompaction.ts no longer reaches streamChatResponse.ts through compaction.ts.
  3. Resolve streamChatResponse at call time through a late-bound streamChatResponseRef (new src/stream/streamChatResponse.lateRef.ts) from both compactChatHistory.ts and subagent/executor.ts, so neither module has a static import back into streamChatResponse.ts.

streamChatResponse.ts registers itself in the ref at the end of its module factory, so consumers can call the function at runtime without creating a circular import.

Release validation

The release workflow now validates the exact artifact that will be published by copying dist/index.js, dist/cn.js, dist/xhr-sync-worker.js and a minimal {"type":"module"} package.json into a clean directory and running both node cn.js --help and node cn.js --version from there. The Create GitHub release step is only reached when both commands succeed and produce non-empty output. This catches parse errors, top-level await cycles, and missing-module issues before any release is published.

Tests

  • node smoke-test.mjs (10/10 pass) — invokes the bundled dist/cn.js --help/--version and other entrypoint checks.
  • npx vitest run — 1697/1697 tests pass, 29 skipped.
  • npx tsc --noEmit — clean.
  • npx eslint . — clean.
  • Manual packaged-artifact test: copying the build output plus package.json to a clean directory and running node cn.js --help and node cn.js --version succeeds.

Files

  • extensions/cli/src/util/chatHistoryPrune.ts (new) — leaf module for pruneLastMessage.
  • extensions/cli/src/stream/streamChatResponse.compactChatHistory.ts (new) — compactChatHistory and types.
  • extensions/cli/src/stream/streamChatResponse.lateRef.ts (new) — late-bound streamChatResponseRef.
  • extensions/cli/src/compaction.ts — re-exports pruneLastMessage for back-compat; no longer imports streamChatResponse.
  • extensions/cli/src/stream/streamChatResponse.ts — registers itself in the ref.
  • extensions/cli/src/stream/streamChatResponse.autoCompaction.ts, extensions/cli/src/subagent/executor.ts, extensions/cli/src/ui/hooks/useChat.compaction.ts, extensions/cli/src/commands/chat.ts — updated imports.
  • Tests updated to mock the new file paths and register the ref in beforeAll where needed.
  • .github/workflows/release-cli.yml — combined verify/package step tests the exact published artifact (--help and --version) in a clean directory; release is gated on success.
  • extensions/cli/README.md, extensions/cli/CHANGELOG.md — document the packaging/validation process.

…dlock)

The bundled CLI produced by esbuild crashed at module load with
`SyntaxError: Unexpected reserved word` because circular static imports
between `src/compaction.ts` and `src/stream/streamChatResponse.ts`
(plus `src/subagent/executor.ts`) caused esbuild to emit a `__esm`
factory that contained a top-level `await` without the `async`
keyword. The previous fix (minifySyntax:false) only changed syntax
mangling; the underlying cycle remained and, once the SyntaxError was
removed, surfaced as a second bug: a deadlock in the module-init
promise chain (`init_streamChatResponse` <-> `init_subagent/executor`)
that hung the bundled CLI indefinitely.

Break the cycles by:
- extracting `pruneLastMessage` to `src/util/chatHistoryPrune.ts` so
  the stream layer and compaction helpers no longer share a static
  edge through the compaction module,
- moving `compactChatHistory` (and its `COMPACTION_PROMPT` /
  `Compaction*` types) into a dedicated
  `src/stream/streamChatResponse.compactChatHistory.ts` module so
  `compaction.ts` becomes a leaf and `streamChatResponse.ts` is no
  longer reachable from `autoCompaction.ts` through the compaction
  module,
- resolving `streamChatResponse` at call time through a late-bound
  `streamChatResponseRef` (`src/stream/streamChatResponse.lateRef.ts`)
  from `compactChatHistory.ts` and `subagent/executor.ts`, so neither
  module has a static import back into `streamChatResponse.ts`.

The release workflow () now validates
the exact artifact that will be published by copying `dist/index.js`,
`dist/cn.js`, `dist/xhr-sync-worker.js` and a minimal
`{"type":"module"}` `package.json` into a clean directory and
running `node cn.js --help` and `node cn.js --version` from there.
The GitHub release is only created when both commands succeed and
produce non-empty output. The README documents the packaging/validation
process and how to reproduce it locally.

All 1697 unit tests pass; the `node smoke-test.mjs` suite (which
invokes the bundled `dist/cn.js --help/--version`) passes; tsc and
eslint are clean.

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@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.

1 participant