Skip to content

Add package services runtime - #264

Merged
kentcdodds merged 39 commits into
mainfrom
cursor/package-services-ea4e
Apr 25, 2026
Merged

kentcdodds merged 39 commits into
mainfrom
cursor/package-services-ea4e

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Apr 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add first-class kody.services package manifests, MCP service controls, and package-app service introspection
  • evolve package services toward gateway-style runtimes with background-managed runs, service runtime helpers, richer service status, auto-start alarms, and configurable executor timeouts
  • add an official Discord gateway package guide so the new native package-service architecture is discoverable and directly usable

Testing

  • ✅ npm run typecheck
  • ✅ npm run test -- packages/worker/src/package-registry/manifest.node.test.ts packages/worker/src/mcp/capabilities/services/services-domain.node.test.ts packages/worker/src/mcp/run-codemode-registry.node.test.ts packages/worker/src/package-runtime/package-service.node.test.ts packages/worker/src/package-runtime/module-graph.node.test.ts packages/worker/src/mcp/tools/search.node.test.ts packages/worker/src/repo/checks.node.test.ts packages/worker/src/oauth-handlers.workers.test.ts
  • ✅ npm run test -- packages/worker/src/mcp/capabilities/coding/kody-official-guide.node.test.ts packages/worker/src/package-registry/manifest.node.test.ts packages/worker/src/mcp/capabilities/services/services-domain.node.test.ts packages/worker/src/mcp/run-codemode-registry.node.test.ts packages/worker/src/package-runtime/package-service.node.test.ts packages/worker/src/package-runtime/module-graph.node.test.ts packages/worker/src/mcp/tools/search.node.test.ts packages/worker/src/repo/checks.node.test.ts packages/worker/src/oauth-handlers.workers.test.ts
Open in Web Open in Cursor 

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced package services for running long-lived background tasks within packages
    • Added service management capabilities: list, retrieve status, start, and stop services
    • Services support auto-start configuration and adjustable timeout settings
    • Services can publish to package app realtime sessions and access dedicated storage
  • Documentation

    • Added comprehensive documentation on package service architecture and runtime lifecycle
    • Added guide for implementing service patterns in packages
    • Updated execution and package documentation with service runtime details

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Apr 24, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces package services—a new capability for saved packages to declare and manage long-lived background runtimes via package.json#kody.services, including MCP capabilities for lifecycle control, a Durable Object implementation for persistence and scheduling, runtime bridges exposing service status and alarm APIs, and comprehensive documentation and guides.

Changes

Cohort / File(s) Summary
Documentation & Guides
docs/contributing/architecture/request-lifecycle.md, docs/use/execute.md, docs/use/packages.md, docs/guides/README.md, docs/guides/package-service-pattern.md
Introduces package service conceptual model, runtime exports (serviceContext, service), MCP capabilities, lifecycle semantics, and comprehensive guide for implementing long-lived background services.
Schema & Environment Configuration
packages/worker/src/env-schema.ts, packages/worker/src/package-registry/types.ts, packages/worker/worker-configuration.d.ts, packages/worker/wrangler.jsonc
Adds PACKAGE_SERVICE_INSTANCE Durable Object binding to environment schema and configuration; introduces packageServiceDefinitionSchema and PackageServiceDefinition type for validating service entries and optional metadata.
MCP Capabilities Domain
packages/worker/src/mcp/capabilities/domain-metadata.ts, packages/worker/src/mcp/capabilities/builtin-domains.ts, packages/worker/src/mcp/capabilities/services/domain.ts, packages/worker/src/mcp/capabilities/services/shared.ts, packages/worker/src/mcp/capabilities/services/service-list.ts, packages/worker/src/mcp/capabilities/services/service-get.ts, packages/worker/src/mcp/capabilities/services/service-start.ts, packages/worker/src/mcp/capabilities/services/service-stop.ts
Defines services capability domain with four capabilities (service_list, service_get, service_start, service_stop) and shared helpers for service context resolution, status normalization, and package service RPC wrapping.
Package Service Runtime (Durable Object)
packages/worker/src/package-runtime/package-service.ts, packages/worker/src/package-runtime/package-service.node.test.ts
Implements PackageServiceInstance Durable Object managing per-service state (binding, runtime metadata, alarms, status). Exposes POST endpoints for start/status/stop; loads saved package manifest/entry, builds execution bundles, manages background lifecycle with error/completion handling, and exports helper functions for storage ID generation, RPC wrapping, and service enumeration.
Package Runtime Bridges & Module Graph
packages/worker/src/package-runtime/package-app.ts, packages/worker/src/package-runtime/module-graph.ts, packages/worker/src/package-runtime/module-graph.node.test.ts
Wires service proxy into package app runtime exposing services.list/get/start/stop MCP operations; extends module source to export serviceContext and service helpers; adds optional serviceContext field to published bundle artifacts.
Published Artifacts & Manifest
packages/worker/src/package-runtime/published-bundle-artifacts.ts, packages/worker/src/package-runtime/published-runtime-artifacts.ts, packages/worker/src/package-registry/manifest.ts, packages/worker/src/package-registry/manifest.node.test.ts
Materializes service-kind bundle artifacts during rebuild; extends PublishedBundleArtifact with serviceContext; adds helpers to resolve service entries and list configured services with normalized metadata; indexes services in package search projection and documents.
MCP Execution & Codemode Runtime
packages/worker/src/mcp/executor.ts, packages/worker/src/mcp/run-codemode-registry.ts, packages/worker/src/mcp/run-codemode-registry.node.test.ts
Extends executor to accept optional timeoutMs parameter; adds service tool helpers (service_get_status, service_should_stop, service_set_alarm, service_clear_alarm) to codemode builder functions; injects serviceContext and service reference into runtime globals during bundled execution.
Search & Repository Checks
packages/worker/src/mcp/tools/search.ts, packages/worker/src/mcp/tools/search.node.test.ts, packages/worker/src/repo/checks.ts
Extends package search fallback projection to include services: []; updates typecheck target collection to include service entry paths with storage enabled; adds ambient type declarations for service context and runtime interface.
Official Guide Catalog
packages/worker/src/mcp/capabilities/coding/kody-official-guide.ts, packages/worker/src/mcp/capabilities/coding/kody-official-guide.node.test.ts
Adds package_service_pattern guide entry to catalog; derives guide IDs from catalog keys for dynamic zod validation; updates capability discovery keywords to include service-operation and realtime-connection terms.
Worker Entry Point & Tests
packages/worker/src/index.ts, packages/worker/src/mcp/capabilities/services/services-domain.node.test.ts, packages/worker/src/oauth-handlers.workers.test.ts, packages/worker/src/mcp/server-instructions.ts
Re-exports PackageServiceInstance from worker entrypoint; adds test suite for services domain capabilities validating advertisement, listing, status retrieval, and RPC delegation; extends oauth handler test environment with DO binding; updates server instructions to include service management guidance.
Dev Server Infrastructure
tools/mcp-test-support.ts
Implements bounded retry loop with exponential backoff for dev server startup, handling port-in-use conditions across mock Cloudflare and main server initialization.

Sequence Diagram

sequenceDiagram
    participant Client
    participant MCP as MCP Server
    participant DO as PackageServiceInstance<br/>(Durable Object)
    participant Manifest as Package Manifest
    participant Executor as Bundled Executor
    participant Storage as Durable Storage

    Client->>MCP: service_start(service_name, package_id)
    MCP->>MCP: requirePackageServiceContext()
    MCP->>DO: POST /service/start
    DO->>Storage: Load persisted state
    alt State is running
        DO->>MCP: {ok: false, already_running: true}
    else State is idle
        DO->>DO: Transition to running<br/>Record runId & timestamps
        DO->>Storage: Persist state
        DO->>Manifest: Load service entry
        DO->>Executor: waitUntil(execute bundled<br/>service module)
        DO->>MCP: {ok: true, run_id, status: running}
    end
    
    rect rgba(100, 150, 200, 0.5)
        Note over Executor,Storage: Background Execution (parallel)
        Executor->>Executor: Load saved package source
        Executor->>Executor: Build published bundle
        Executor->>Executor: Create MCP context
        Executor->>Executor: Execute service module<br/>with service tools
        Executor->>Storage: Persist lastResult/<br/>lastError & timestamps
        Executor->>Storage: Clear alarms on stop
    end
    
    Client->>MCP: service_get(service_name, package_id)
    MCP->>DO: POST /service/status
    DO->>Storage: Load current status
    DO->>MCP: {status, active_run_id, ...metadata}
    MCP->>Client: {status, ...}
    
    Client->>MCP: service_stop(service_name, package_id)
    MCP->>DO: POST /service/stop
    DO->>Storage: Load state
    DO->>Storage: Set stopRequested=true<br/>Update status to stopping
    DO->>DO: Clear alarms
    DO->>MCP: {ok: true}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hop, hop, services spring to life!
Long-lived tasks without the strife,
Durable Objects stand so tall,
Background work, we manage it all,
Alarms and state, forever at rest,
Package services—put them to the test! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add package services runtime' is concise, clear, and directly reflects the primary objective of the changeset: introducing first-class package services with runtime infrastructure, MCP controls, and durable object support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/package-services-ea4e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kentcdodds
kentcdodds marked this pull request as ready for review April 24, 2026 22:40
Comment thread packages/worker/wrangler.jsonc
Comment thread packages/worker/src/mcp/capabilities/services/service-stop.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/worker/src/package-runtime/published-runtime-artifacts.ts (1)

52-70: ⚠️ Potential issue | 🟡 Minor

Legacy artifacts will deserialize with serviceContext: undefined, not null.

bundleArtifactVersion remains at 1, and readPublishedBundleArtifact spreads the stored artifact without normalizing missing fields. Any artifact written before this PR will return with serviceContext absent, resulting in undefined, while the type declares { serviceName: string } | null. This violates the type contract and can cause downstream checks or destructuring with default null values to behave inconsistently.

Normalize on read to ensure the field is always null when absent, matching the pattern used in module-graph.ts:

Suggested fix
 	return {
 		...artifact,
+		serviceContext: artifact.serviceContext ?? null,
 		modules: deserializeWorkerLoaderModules(artifact.modules),
 	} satisfies PublishedBundleArtifact
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/package-runtime/published-runtime-artifacts.ts` around
lines 52 - 70, Legacy artifacts may omit serviceContext and deserialize it as
undefined, violating the PublishedBundleArtifact type; update
readPublishedBundleArtifact to normalize missing or undefined serviceContext to
null (matching the module-graph.ts pattern) so that returned objects always have
serviceContext: { serviceName: string } | null; ensure other optional fields
follow the same normalization and keep bundleArtifactVersion and
PublishedBundleArtifact type unchanged.
packages/worker/src/repo/checks.ts (1)

403-405: ⚠️ Potential issue | 🟡 Minor

Bundle-check message is stale — services are now bundled too.

This message still says "no app entry, exports, or jobs to bundle." Services are now collected as additional bundle targets (lines 257-265), so the message should mention them for accuracy when a manifest with only services would otherwise look empty.

📝 Proposed wording
-		return 'Package defines no app entry, exports, or jobs to bundle.'
+		return 'Package defines no app entry, exports, jobs, or services to bundle.'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/repo/checks.ts` around lines 403 - 405, The bundle-empty
message is outdated: when input.targetCount === 0 the returned string only
mentions "app entry, exports, or jobs" but services are now included as bundle
targets (see the services collection around the service-target logic), so update
the return value in checks.ts to include services (e.g., mention "services"
alongside app entry, exports, and jobs) so the message accurately reflects all
possible bundle targets; locate the check using input.targetCount and replace
the stale message string accordingly.
🧹 Nitpick comments (7)
packages/worker/src/package-runtime/package-service.node.test.ts (1)

1-11: LGTM — consider extending coverage for collision-prone characters.

The current cases cover the happy path and whitespace encoding. Optional: add a case for a serviceName containing : or / to lock in that the encoded id remains unambiguous against the service:<packageId>:<serviceName> separator (since packageId is concatenated unencoded and could otherwise collide).

♻️ Optional extra assertion
 	expect(buildPackageServiceStorageId('package-1', 'guild sync')).toBe(
 		'service:package-1:guild%20sync',
 	)
+	expect(buildPackageServiceStorageId('package-1', 'a:b/c')).toBe(
+		'service:package-1:a%3Ab%2Fc',
+	)
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/package-runtime/package-service.node.test.ts` around
lines 1 - 11, Add tests to assert that buildPackageServiceStorageId handles
collision-prone characters by percent-encoding service names containing
separators like ':' and '/', e.g., call buildPackageServiceStorageId with
serviceName values that include ':' and '/' and assert the returned ID is
'service:<packageId>:<percent-encoded-serviceName>' (verify ':' becomes %3A and
'/' becomes %2F) so the concatenated form remains unambiguous; update the test
file to include these extra expect(...) assertions referencing
buildPackageServiceStorageId.
packages/worker/src/package-runtime/published-bundle-artifacts.ts (1)

43-54: Remove the unused serviceContext field from the persisted artifact shape.

The serviceContext field hardcoded to null (line 174) is never read by consumers. Both package-service.ts (lines 219–221) and job execution construct serviceContext fresh at runtime from binding or manifest data, then pass it directly to runBundledModuleWithRegistry. The persisted field is dead weight and should be removed to avoid schema confusion.

Either:

  • Delete serviceContext: null from the persisted shape and the type field.
  • Or, if intended for future use, add it to PersistPublishedBundleArtifactInput and thread it through the service-iteration loop (lines 299–318) so the field carries actual data.
Option 1: Remove the field entirely
 type PublishedBundleArtifact = {
 	env: Env
 	userId: string
 	source: EntitySourceRow
 	kind: BundleArtifactKind
 	artifactName?: string | null
 	entryPoint: string
 	mainModule: string
 	modules: WorkerLoaderModules
 	dependencies: Array<BundleArtifactDependency>
 	packageContext: PublishedBundleArtifactPackageContext | null
-	serviceContext: PublishedBundleArtifactServiceContext | null
 	createdAt: string
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/package-runtime/published-bundle-artifacts.ts` around
lines 43 - 54, Persisted artifact shape includes an unused serviceContext field;
remove it by deleting serviceContext from the
PersistPublishedBundleArtifactInput type and removing the hardcoded
serviceContext: null assignment where the persisted object is created (reference
symbol: PersistPublishedBundleArtifactInput and the persisted artifact
construction that sets serviceContext: null), and also remove any code in the
service-iteration loop that expects or threads this persisted field (references:
the service-iteration loop that later reconstructs serviceContext and the
consumer call runBundledModuleWithRegistry), so serviceContext is only created
at runtime and not stored in the persisted shape.
packages/worker/src/repo/checks.ts (1)

257-265: Redundant entry-path lookup.

listPackageServices(manifest) already returns entry normalized via normalizePackageWorkspacePath (per packages/worker/src/package-registry/manifest.ts:130-138), so calling getPackageServiceEntryPath({ manifest, serviceName }) here just re-derives the same value. You can use service.entry directly and drop the getPackageServiceEntryPath import (and the if guard, since listPackageServices only emits services with a defined entry per the schema).

♻️ Proposed simplification
 	for (const service of listPackageServices(manifest)) {
-		const serviceEntryPath = getPackageServiceEntryPath({
-			manifest,
-			serviceName: service.name,
-		})
-		if (serviceEntryPath) {
-			remember(serviceEntryPath, true)
-		}
+		remember(service.entry, true)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/repo/checks.ts` around lines 257 - 265, Replace the
redundant lookup: use the entry already provided by listPackageServices by
calling remember(service.entry, true) for each service (iterate over
listPackageServices(manifest) and reference service.entry), remove the
getPackageServiceEntryPath call and its conditional guard, and delete the
now-unused getPackageServiceEntryPath import; keep manifest and remember as-is.
packages/worker/src/mcp/capabilities/services/services-domain.node.test.ts (1)

76-102: Mock returns a rpc field that isn't part of the real API.

listSavedPackageServices (in packages/worker/src/package-runtime/package-service.ts) returns only { savedPackage, services }. The rpc(...) builder is added by listPackageServicesForContext in shared.ts. Including it on the mocked listSavedPackageServices value misrepresents the contract and is dead code in the test (the real packageServiceRpc mock is what gets exercised).

Consider trimming the mock to match the real return shape so the test stays aligned with the source:

 mockModule.listSavedPackageServices.mockResolvedValue({
   savedPackage: {
     id: 'package-123',
     kodyId: 'example',
   },
   services: [
     {
       name: 'discord-gateway',
       entry: 'services/discord-gateway.ts',
       autoStart: true,
     },
   ],
-  rpc: () => ({
-    status: async () => ({ ... }),
-  }),
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/capabilities/services/services-domain.node.test.ts`
around lines 76 - 102, The mock for listSavedPackageServices is returning an
extra rpc builder that doesn't exist on the real API; remove the rpc field from
the mocked resolved value so it matches the actual return shape ({ savedPackage,
services }) and let listPackageServicesForContext / packageServiceRpc provide
any RPC behavior in other mocks; update the
mockModule.listSavedPackageServices.mockResolvedValue call to only include
savedPackage and services (e.g., id/kodyId and the services array with
name/entry/autoStart) to keep the test aligned with the real package-service.ts
contract.
packages/worker/src/package-runtime/package-service.ts (1)

197-231: Minor: extract storageId to avoid recomputation.

buildPackageServiceStorageId(binding.packageId, binding.serviceName) is computed twice in runService (lines 200-204 and 222-227). Easy to drift if one site is updated. Pull it into a local once:

+const storageId = buildPackageServiceStorageId(
+  binding.packageId,
+  binding.serviceName,
+)
 const callerContext = createMcpCallerContext({
   baseUrl: binding.baseUrl,
   user: { ... },
   storageContext: {
     sessionId: null,
     appId: binding.packageId,
-    storageId: buildPackageServiceStorageId(
-      binding.packageId,
-      binding.serviceName,
-    ),
+    storageId,
   },
 })
 ...
   storageTools: {
     userId: binding.userId,
-    storageId: buildPackageServiceStorageId(
-      binding.packageId,
-      binding.serviceName,
-    ),
+    storageId,
     writable: true,
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/package-runtime/package-service.ts` around lines 197 -
231, The code calls buildPackageServiceStorageId(binding.packageId,
binding.serviceName) twice in runService; extract it to a local const (e.g.,
const storageId = buildPackageServiceStorageId(binding.packageId,
binding.serviceName)) and replace both uses: the storageContext.storageId and
the storageTools.storageId passed into runBundledModuleWithRegistry; ensure the
same storageId variable is used in both places so the value isn’t recomputed or
drift between sites.
packages/worker/src/mcp/capabilities/services/service-get.ts (1)

19-35: Misleading "not found" error and minor consistency note.

The service field on requirePackageServiceContext's return value is null only when serviceName.trim() is empty. Since z.string().min(1) accepts whitespace-only strings (it checks length, not trimmed length), a value like ' ' will pass schema validation, reach line 31, and surface as Package service " " was not found., even though the real cause is an empty name and not a missing manifest entry.

Two small improvements worth considering:

  1. Tighten the input schema (or reuse requirePackageServiceContext's trim) to reject whitespace-only names up front.
  2. For symmetry with service-list which runs normalizePackageServiceStatus(...), consider validating the raw .status() payload through the same helper before returning so output validation isn't solely dependent on the framework using outputSchema.
Possible diff
+import { normalizePackageServiceStatus, packageServiceStatusSchema, requirePackageServiceContext } from './shared.ts'
@@
 		inputSchema: z.object({
-			service_name: z.string().min(1),
+			service_name: z.string().trim().min(1),
 			package_id: z.string().min(1).optional(),
 		}),
@@
-			return await serviceContext.service.status()
+			return normalizePackageServiceStatus(await serviceContext.service.status())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/capabilities/services/service-get.ts` around lines 19
- 35, Tighten validation and normalize output: change the inputSchema for this
handler to trim and reject whitespace-only names (e.g., transform/trim then
refine non-empty) so requirePackageServiceContext never receives blank
serviceName, and in the handler use the same normalization used by service-list
(normalizePackageServiceStatus) to validate/normalize the raw service.status()
payload before returning (ensuring output conforms to
packageServiceStatusSchema); reference inputSchema, handler,
requirePackageServiceContext, normalizePackageServiceStatus, and
packageServiceStatusSchema when making these changes.
packages/worker/src/mcp/capabilities/services/shared.ts (1)

142-146: buildPackageServiceBinding is a no-op wrapper.

This function just returns input.binding unchanged — no validation, no transformation, no branded type. As written it's pure indirection and doesn't justify its export surface. Either delete it (callers can pass the binding object directly) or have it actually validate / freeze / brand the binding to add value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/capabilities/services/shared.ts` around lines 142 -
146, buildPackageServiceBinding is currently a no-op that just returns
input.binding (PackageServiceBindingState); remove the useless wrapper or make
it meaningful. Option A: delete the exported function and update all callers to
use the PackageServiceBindingState object directly. Option B: implement actual
validation/branding in buildPackageServiceBinding: validate required properties
on input.binding, coerce/normalize values as needed, freeze the object
(Object.freeze) and return it as a branded type (e.g., cast/return as
PackageServiceBinding) so callers get runtime guarantees; then keep the export
and adjust types where necessary. Ensure you update every caller of
buildPackageServiceBinding (or their imports) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/mcp/capabilities/services/service-list.ts`:
- Around line 38-48: The current service listing uses Promise.all over
listed.services.map and will reject if any listed.rpc(service.name).status()
throws; change this to tolerate per-service failures by using Promise.allSettled
(or wrapping each status call in try/catch) and, for failed entries, set the
returned object's status field (the one currently assigned from
normalizePackageServiceStatus(...).status) to a fallback like 'unknown' or the
error message; update the mapping that builds { name, entry, auto_start, status
} so healthy services still appear even if one status call fails.

In `@packages/worker/src/mcp/capabilities/services/service-start.ts`:
- Around line 38-43: service_stop currently uses optional chaining
(serviceContext.service?.stop()) and hides "service not found" cases, while
service_start throws when serviceContext.service is null; make them symmetric by
changing service_stop to explicitly check serviceContext.service and throw the
same Error used in service_start when null (use the same message that references
args.service_name), otherwise call and await serviceContext.service.stop() and
return its result—update service_stop function to mirror service_start's
missing-service handling (referencing service_stop, service_start, and
serviceContext.service).

In `@packages/worker/src/package-runtime/package-app.ts`:
- Around line 704-716: The runtime serviceList() currently returns services from
listSavedPackageServices without the live status field, causing a mismatch with
MCP's service_list; update serviceList (in package-app.ts) to iterate over
result.services and for each service call rpc(service.name).status() (or await
this.rpc(service.name).status()) to populate a status property before returning,
ensuring the returned object includes package_id, kody_id and services with
name, entry, autoStart and status; alternatively, if you choose not to enrich,
add clear documentation in runtime.services.list() noting the intentional
absence of live status.

In `@packages/worker/src/package-runtime/package-service.ts`:
- Around line 293-315: handleStatusRequest is reloading the package manifest on
every call via loadSavedPackageService to read autoStart and also contains a
dead null fallback because initializeBinding guarantees
this.stateSnapshot.binding is set; fix by caching/resolving the
serviceDefinition.autoStart into this.stateSnapshot (or another cached field)
during initializeBinding or on first start so handleStatusRequest reads
autoStart from this.stateSnapshot instead of calling loadSavedPackageService,
remove the redundant ternary null branch that checks this.stateSnapshot.binding
after initializeBinding, and ensure handleStatusRequest returns status even if
manifest lookup fails so it degrades gracefully when the package source is
temporarily unavailable.
- Around line 248-291: handleStartRequest currently awaits runService with no
cancellation and overwrites a user-initiated stop; add cooperative cancellation
and a stop flag: modify handleStopRequest to set stateSnapshot.stopRequested =
true (persist) and call alarm delete as today, then thread an AbortSignal into
runService and into runBundledModuleWithRegistry so runService accepts an
AbortSignal param (or an options object) and cancels work when signaled (use
AbortController in the stop handler to signal the running start). At the end of
handleStartRequest, after runService resolves or rejects, check
stateSnapshot.stopRequested and, if true, ensure the final persisted status
reflects that an explicit stop was requested (do not overwrite it with a
natural-success result), clear stopRequested as appropriate, and persist state;
ensure names to change/inspect include handleStartRequest, handleStopRequest,
runService, runBundledModuleWithRegistry, and stateSnapshot.stopRequested.
- Around line 85-90: The storage id assembly in buildPackageServiceStorageId is
asymmetric: packageId is used raw while serviceName is
encodeURIComponent-encoded, which allows ambiguous ids when packageId contains
delimiters; update buildPackageServiceStorageId to encode both packageId and
serviceName (e.g., call encodeURIComponent on packageId as well) so the returned
string `service:${encodedPackageId}:${encodedServiceName}` is unambiguous.

In `@packages/worker/wrangler.jsonc`:
- Around line 119-122: The preview and test environment blocks are missing the
Durable Object binding for PackageServiceInstance (PACKAGE_SERVICE_INSTANCE),
causing runtime failures; add an entry identical to the production binding
(class_name "PackageServiceInstance", name "PACKAGE_SERVICE_INSTANCE") into both
the preview and test environment "bindings" arrays so the Durable Object is
available in all environments and matches the v14 migration and tests that
reference PACKAGE_SERVICE_INSTANCE.

---

Outside diff comments:
In `@packages/worker/src/package-runtime/published-runtime-artifacts.ts`:
- Around line 52-70: Legacy artifacts may omit serviceContext and deserialize it
as undefined, violating the PublishedBundleArtifact type; update
readPublishedBundleArtifact to normalize missing or undefined serviceContext to
null (matching the module-graph.ts pattern) so that returned objects always have
serviceContext: { serviceName: string } | null; ensure other optional fields
follow the same normalization and keep bundleArtifactVersion and
PublishedBundleArtifact type unchanged.

In `@packages/worker/src/repo/checks.ts`:
- Around line 403-405: The bundle-empty message is outdated: when
input.targetCount === 0 the returned string only mentions "app entry, exports,
or jobs" but services are now included as bundle targets (see the services
collection around the service-target logic), so update the return value in
checks.ts to include services (e.g., mention "services" alongside app entry,
exports, and jobs) so the message accurately reflects all possible bundle
targets; locate the check using input.targetCount and replace the stale message
string accordingly.

---

Nitpick comments:
In `@packages/worker/src/mcp/capabilities/services/service-get.ts`:
- Around line 19-35: Tighten validation and normalize output: change the
inputSchema for this handler to trim and reject whitespace-only names (e.g.,
transform/trim then refine non-empty) so requirePackageServiceContext never
receives blank serviceName, and in the handler use the same normalization used
by service-list (normalizePackageServiceStatus) to validate/normalize the raw
service.status() payload before returning (ensuring output conforms to
packageServiceStatusSchema); reference inputSchema, handler,
requirePackageServiceContext, normalizePackageServiceStatus, and
packageServiceStatusSchema when making these changes.

In `@packages/worker/src/mcp/capabilities/services/services-domain.node.test.ts`:
- Around line 76-102: The mock for listSavedPackageServices is returning an
extra rpc builder that doesn't exist on the real API; remove the rpc field from
the mocked resolved value so it matches the actual return shape ({ savedPackage,
services }) and let listPackageServicesForContext / packageServiceRpc provide
any RPC behavior in other mocks; update the
mockModule.listSavedPackageServices.mockResolvedValue call to only include
savedPackage and services (e.g., id/kodyId and the services array with
name/entry/autoStart) to keep the test aligned with the real package-service.ts
contract.

In `@packages/worker/src/mcp/capabilities/services/shared.ts`:
- Around line 142-146: buildPackageServiceBinding is currently a no-op that just
returns input.binding (PackageServiceBindingState); remove the useless wrapper
or make it meaningful. Option A: delete the exported function and update all
callers to use the PackageServiceBindingState object directly. Option B:
implement actual validation/branding in buildPackageServiceBinding: validate
required properties on input.binding, coerce/normalize values as needed, freeze
the object (Object.freeze) and return it as a branded type (e.g., cast/return as
PackageServiceBinding) so callers get runtime guarantees; then keep the export
and adjust types where necessary. Ensure you update every caller of
buildPackageServiceBinding (or their imports) accordingly.

In `@packages/worker/src/package-runtime/package-service.node.test.ts`:
- Around line 1-11: Add tests to assert that buildPackageServiceStorageId
handles collision-prone characters by percent-encoding service names containing
separators like ':' and '/', e.g., call buildPackageServiceStorageId with
serviceName values that include ':' and '/' and assert the returned ID is
'service:<packageId>:<percent-encoded-serviceName>' (verify ':' becomes %3A and
'/' becomes %2F) so the concatenated form remains unambiguous; update the test
file to include these extra expect(...) assertions referencing
buildPackageServiceStorageId.

In `@packages/worker/src/package-runtime/package-service.ts`:
- Around line 197-231: The code calls
buildPackageServiceStorageId(binding.packageId, binding.serviceName) twice in
runService; extract it to a local const (e.g., const storageId =
buildPackageServiceStorageId(binding.packageId, binding.serviceName)) and
replace both uses: the storageContext.storageId and the storageTools.storageId
passed into runBundledModuleWithRegistry; ensure the same storageId variable is
used in both places so the value isn’t recomputed or drift between sites.

In `@packages/worker/src/package-runtime/published-bundle-artifacts.ts`:
- Around line 43-54: Persisted artifact shape includes an unused serviceContext
field; remove it by deleting serviceContext from the
PersistPublishedBundleArtifactInput type and removing the hardcoded
serviceContext: null assignment where the persisted object is created (reference
symbol: PersistPublishedBundleArtifactInput and the persisted artifact
construction that sets serviceContext: null), and also remove any code in the
service-iteration loop that expects or threads this persisted field (references:
the service-iteration loop that later reconstructs serviceContext and the
consumer call runBundledModuleWithRegistry), so serviceContext is only created
at runtime and not stored in the persisted shape.

In `@packages/worker/src/repo/checks.ts`:
- Around line 257-265: Replace the redundant lookup: use the entry already
provided by listPackageServices by calling remember(service.entry, true) for
each service (iterate over listPackageServices(manifest) and reference
service.entry), remove the getPackageServiceEntryPath call and its conditional
guard, and delete the now-unused getPackageServiceEntryPath import; keep
manifest and remember as-is.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7070ed73-afb6-4e07-a485-5dc8e756e7f7

📥 Commits

Reviewing files that changed from the base of the PR and between 4d44980 and 4b8c2a7.

📒 Files selected for processing (30)
  • docs/contributing/architecture/request-lifecycle.md
  • docs/use/execute.md
  • docs/use/packages.md
  • packages/worker/src/env-schema.ts
  • packages/worker/src/index.ts
  • packages/worker/src/jobs/service.ts
  • packages/worker/src/mcp/capabilities/builtin-domains.ts
  • packages/worker/src/mcp/capabilities/domain-metadata.ts
  • packages/worker/src/mcp/capabilities/services/domain.ts
  • packages/worker/src/mcp/capabilities/services/service-get.ts
  • packages/worker/src/mcp/capabilities/services/service-list.ts
  • packages/worker/src/mcp/capabilities/services/service-start.ts
  • packages/worker/src/mcp/capabilities/services/service-stop.ts
  • packages/worker/src/mcp/capabilities/services/services-domain.node.test.ts
  • packages/worker/src/mcp/capabilities/services/shared.ts
  • packages/worker/src/mcp/run-codemode-registry.ts
  • packages/worker/src/mcp/server-instructions.ts
  • packages/worker/src/mcp/tools/search.ts
  • packages/worker/src/package-registry/manifest.node.test.ts
  • packages/worker/src/package-registry/manifest.ts
  • packages/worker/src/package-registry/types.ts
  • packages/worker/src/package-runtime/module-graph.ts
  • packages/worker/src/package-runtime/package-app.ts
  • packages/worker/src/package-runtime/package-service.node.test.ts
  • packages/worker/src/package-runtime/package-service.ts
  • packages/worker/src/package-runtime/published-bundle-artifacts.ts
  • packages/worker/src/package-runtime/published-runtime-artifacts.ts
  • packages/worker/src/repo/checks.ts
  • packages/worker/worker-configuration.d.ts
  • packages/worker/wrangler.jsonc

Comment thread packages/worker/src/mcp/capabilities/services/service-list.ts
Comment thread packages/worker/src/mcp/capabilities/services/service-start.ts
Comment thread packages/worker/src/package-runtime/package-app.ts
Comment thread packages/worker/src/package-runtime/package-service.ts
Comment thread packages/worker/src/package-runtime/package-service.ts
Comment thread packages/worker/src/package-runtime/package-service.ts
Comment thread packages/worker/wrangler.jsonc
@github-actions

github-actions Bot commented Apr 24, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-264.kentcdodds.workers.dev

Worker: kody-pr-264
D1: kody-pr-264-db
KV: kody-pr-264-oauth-kv

Mocks:

Comment thread packages/worker/src/mcp/capabilities/services/shared.ts Outdated
Comment thread packages/worker/src/package-runtime/module-graph.ts
cursoragent and others added 2 commits April 24, 2026 22:57
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts
Comment thread packages/worker/src/package-runtime/package-app.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/mcp/capabilities/services/service-start.ts Outdated
Comment thread packages/worker/src/mcp/capabilities/services/shared.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts Outdated
Comment thread packages/worker/src/package-runtime/module-graph.ts
Comment thread packages/worker/src/mcp/run-codemode-registry.ts

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/worker/src/package-runtime/package-service.ts (2)

256-265: ⚠️ Potential issue | 🟠 Major

Dead ternary — both branches set status to 'stopped'.

this.stateSnapshot.status = this.stateSnapshot.stopRequested
    ? 'stopped'
    : 'stopped'

A natural completion of runService is currently indistinguishable from a user-initiated stop, and (per the prior review on stop semantics) a late-finishing run will overwrite a real stopped state with another stopped plus a fresh lastResult/lastRunFinishedAt, hiding that an explicit stop ever happened.

Pick distinct semantics for the two cases — e.g., 'stopped' only when stopRequested, otherwise something like 'idle' (or a new 'completed') — and skip overwriting lastResult/lastRunFinishedAt when a stop has already finalized state for this run.

🔧 Suggested direction
-			this.stateSnapshot.status = this.stateSnapshot.stopRequested
-				? 'stopped'
-				: 'stopped'
-			this.stateSnapshot.currentRunId = null
-			this.stateSnapshot.stopRequested = false
-			this.stateSnapshot.lastResult = result
-			this.stateSnapshot.lastRunFinishedAt = new Date().toISOString()
-			this.stateSnapshot.lastStoppedAt = this.stateSnapshot.lastRunFinishedAt
-			await this.persistState()
+			const wasStopRequested = this.stateSnapshot.stopRequested
+			this.stateSnapshot.status = wasStopRequested ? 'stopped' : 'idle'
+			this.stateSnapshot.currentRunId = null
+			this.stateSnapshot.stopRequested = false
+			this.stateSnapshot.lastResult = result
+			this.stateSnapshot.lastRunFinishedAt = new Date().toISOString()
+			if (wasStopRequested) {
+				this.stateSnapshot.lastStoppedAt = this.stateSnapshot.lastRunFinishedAt
+			}
+			await this.persistState()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/package-runtime/package-service.ts` around lines 256 -
265, The ternary setting this.stateSnapshot.status currently sets 'stopped' in
both branches; change it so when this.stateSnapshot.stopRequested is true the
status remains 'stopped' but when false set a distinct state such as 'idle' or
'completed' (choose one and apply consistently). Also, in the completion path
inside the same block (the code that sets this.stateSnapshot.lastResult and
lastRunFinishedAt and clears currentRunId/stopRequested), do not overwrite
lastResult/lastRunFinishedAt if stopRequested was true (i.e., only set
lastResult/lastRunFinishedAt when stopRequested is false), then call await
this.persistState(); keep references to this.stateSnapshot and the
completion/stop logic in the same method so the semantics are unambiguous.

382-415: ⚠️ Potential issue | 🟡 Minor

Stop still cannot interrupt an in-flight run.

handleStopRequest flips stopRequested and clears the alarm, but runService / runBundledModuleWithRegistry is not given an AbortSignal, so a long-lived service module continues executing until it cooperatively polls service.shouldStop(). For genuinely long-lived runtimes (the stated PR goal) this is acceptable only if shouldStop is documented as the cancellation contract; otherwise consider plumbing an AbortController through runBundledModuleWithRegistry.

At minimum, the service.shouldStop contract should be called out in the package authoring docs added in this PR so authors know they're responsible for honoring it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/package-runtime/package-service.ts` around lines 382 -
415, The stop flow doesn't actually abort the running module because
runServiceInBackground/runBundledModuleWithRegistry aren't given an AbortSignal
and rely only on service.shouldStop; fix by creating an AbortController when
starting a run (in handleStartRequest), store its signal on the instance (e.g.
this.activeRunController and this.activeRunPromise), and pass that AbortSignal
into runServiceInBackground and onward into runBundledModuleWithRegistry so the
bundled runtime can be force-aborted; then update handleStopRequest to call
this.activeRunController?.abort() in addition to flipping stopRequested/clearing
alarms. Alternatively, if you prefer cooperative cancellation only, add explicit
documentation for the service.shouldStop contract in package authoring docs, but
the code-level fix is to thread an AbortSignal through the symbols
handleStartRequest, handleStopRequest, runServiceInBackground, and
runBundledModuleWithRegistry.
🧹 Nitpick comments (7)
packages/worker/src/mcp/run-codemode-registry.node.test.ts (1)

926-939: Optional: prefer asserting on spy call args rather than expect inside mockImplementation.

Assertions inside mockImplementation fire as a side effect of the call; if the production code ever wraps createExecuteExecutor invocation in a try/catch, a failed expect could be swallowed. A more robust pattern is to let the mock return normally and then assert on createExecuteExecutorSpy.mock.calls[0][0].timeoutMs after the awaited call. Non-blocking.

♻️ Suggested pattern
-	const createExecuteExecutorSpy = vi
-		.spyOn(await import('#mcp/executor.ts'), 'createExecuteExecutor')
-		.mockImplementation((input) => {
-			expect(input.timeoutMs).toBe(300_000)
-			return {
-				async execute(source) {
-					wrapped = String(source)
-					return {
-						result: 'ok',
-						logs: [],
-					}
-				},
-			} as never
-		})
+	const createExecuteExecutorSpy = vi
+		.spyOn(await import('#mcp/executor.ts'), 'createExecuteExecutor')
+		.mockImplementation(() => ({
+			async execute(source) {
+				wrapped = String(source)
+				return { result: 'ok', logs: [] }
+			},
+		}) as never)

Then after await runBundledModuleWithRegistry(...):

expect(createExecuteExecutorSpy).toHaveBeenCalledWith(
    expect.objectContaining({ timeoutMs: 300_000 }),
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/run-codemode-registry.node.test.ts` around lines 926
- 939, Remove the inline assertion from the mockImplementation of
createExecuteExecutor (created as createExecuteExecutorSpy) and have the mock
simply return the fake executor; after the test's awaited call to
runBundledModuleWithRegistry, assert on the spy's call arguments instead (e.g.,
expect(createExecuteExecutorSpy).toHaveBeenCalledWith(expect.objectContaining({
timeoutMs: 300_000 }))). This keeps the mock free of side-effect assertions and
uses createExecuteExecutorSpy.mock.calls (or Jest/Vi matcher) to validate the
timeoutMs passed into createExecuteExecutor.
docs/use/execute.md (2)

108-112: Consider reorganizing lifecycle details for clarity.

The explanation of service_start returning immediately and background execution semantics (lines 110-112) is technically accurate but feels organizationally disconnected from storage concepts. While the information is correct, readers might benefit from a clearer separation between storage scoping (line 108-109) and lifecycle/execution model (lines 110-112).

💡 Optional organizational improvement

Consider splitting this into two separate bullets or adding a subheading to distinguish storage semantics from execution lifecycle:

 - bound storage is execute-, app-, package-, or job-owned durable state
-- package service runs also get writable service-owned durable state scoped to
-  the declared service name
-- package service runs are background-managed by the service Durable Object, so
-  `service_start` returns immediately with a running state while the service code
-  continues in the background until it finishes, errors, or cooperatively stops
+- package service runs get writable service-owned durable state scoped to the
+  declared service name
+- package service execution is background-managed: `service_start` returns
+  immediately with a running state while the service code continues in the
+  background until it finishes, errors, or cooperatively stops
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/use/execute.md` around lines 108 - 112, Split the combined bullet into
two distinct parts: one bullet that explains storage scoping for "package
service runs" (service-owned durable state scoped to the declared service name)
and a separate bullet or subheading that describes the execution lifecycle
semantics for service_start (that service_start returns immediately and the
service runs in the background until finish/error/cooperative stop). Update the
wording under the "package service runs" statement to only cover storage
concerns and create a new bullet or a small subheading titled "Execution
lifecycle" (or similar) to host the service_start background/execution details
so storage and lifecycle are clearly separated.

27-32: Specify the parameter type for setAlarm.

The setAlarm method accepts a Date parameter, but this isn't documented. Based on the implementation, users might not know whether to pass a Date object, timestamp, or ISO string, which could lead to confusion or errors.

📝 Suggested enhancement
-  - `await service.setAlarm(runAt)` — schedule the next service wake-up
+  - `await service.setAlarm(runAt)` — schedule the next service wake-up (pass a Date)

Alternatively, for consistency with the brief style of this documentation:

-  - `await service.setAlarm(runAt)` — schedule the next service wake-up
+  - `await service.setAlarm(runAt: Date)` — schedule the next service wake-up

As per relevant code snippet from packages/worker/src/mcp/run-codemode-registry.ts:51-72, the ServiceToolOptions type signature shows setAlarm: (runAt: Date) => Promise<{ ok: true; scheduled_at: string }>.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/use/execute.md` around lines 27 - 32, Update the docs to specify that
service.setAlarm accepts a Date object: document the parameter as
setAlarm(runAt: Date) and note the Promise return shape per ServiceToolOptions
(Promise<{ ok: true; scheduled_at: string }>) so users know to pass a JavaScript
Date and what the method returns; reference the implementation signature in
ServiceToolOptions (see run-codemode-registry.ts) when editing the list item for
setAlarm.
packages/worker/src/package-runtime/package-service.ts (2)

463-480: alarm() is brittle and re-uses an HTTP-shaped helper internally.

Two concerns:

  1. loadSavedPackageService is called unconditionally; if the package source is briefly unavailable (KV/D1 hiccup), the alarm throws and Cloudflare may retry, but the auto-start intent is lost mid-window. Consider falling back to the cached this.stateSnapshot.autoStart on load failure.
  2. await this.handleStartRequest({ binding }) constructs and discards a Response.json(...). Extract the start logic into a private method that returns the PackageServiceRunResult (or void) and have both handleStartRequest and alarm() call it directly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/package-runtime/package-service.ts` around lines 463 -
480, alarm() currently always calls loadSavedPackageService and then calls
handleStartRequest which builds and discards an HTTP Response; change it so
alarm() catches failures from loadSavedPackageService and falls back to
this.stateSnapshot.autoStart (so auto-start intent isn't lost on transient KV/D1
errors), and refactor the start flow by extracting the core start logic from
handleStartRequest into a new private method (e.g., private
executeStart(binding): Promise<PackageServiceRunResult | void>) that returns the
PackageServiceRunResult or void; have handleStartRequest call executeStart and
build its Response.json around that result, and have alarm() call executeStart
directly (no Response construction), while preserving the existing
persistState() call and stopRequested checks.

439-461: fetch consumes the request body before route matching.

await request.json() is called for every request, including unsupported methods/paths that fall through to 404. This is harmless today but means any future GET endpoint (e.g., a health probe) added to the DO will fail since GET requests have no body. Consider matching the route first, then parsing the body only for handlers that need it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/package-runtime/package-service.ts` around lines 439 -
461, The fetch method currently always calls await request.json(), consuming the
body before route matching; change fetch to first inspect request.method and
url.pathname and only parse the JSON body for the POST routes that need it
(e.g., before calling handleStartRequest, handleStatusRequest,
handleStopRequest). Keep the early binding check logic but move the body parsing
into the branches that call
handleStartRequest/handleStatusRequest/handleStopRequest; for the
/service-status branch call buildServiceStatusResponse(binding) without parsing
body, and return 404 for unmatched routes without touching request.json().
packages/worker/src/mcp/capabilities/services/shared.ts (1)

91-126: Redundant package + source load on the service_list path.

listPackageServicesForContext first calls requirePackageServiceContext (which executes getSavedPackageById and, indirectly, the saved-package lookup), and then calls listSavedPackageServices which does another getSavedPackageById plus loadPackageSourceBySourceId for the same (userId, packageId). For an MCP service_list call this means duplicate D1/source-bundle loads on every request. Consider either:

  • Inlining the resolve+load steps here and skipping listSavedPackageServices, or
  • Having listSavedPackageServices accept a pre-resolved savedPackage so it only loads the source.
packages/worker/src/repo/checks.ts (1)

201-210: Ambient service types diverge from runtime contract.

The prelude declares setAlarm(runAt: string | Date) and getStatus()/setAlarm()/clearAlarm() returning Promise<unknown>, but the actual runtime injected from PackageServiceInstanceBase.runServiceInBackground (see packages/worker/src/package-runtime/package-service.ts lines 241-249) only accepts Date for setAlarm and returns concrete shapes ({ ok: true; scheduled_at: string }, etc.). Consider tightening these signatures (and exposing getStatus's return shape — e.g., the packageServiceStatusSchema from shared.ts) so package authors get accurate IntelliSense and catch misuse at typecheck time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/repo/checks.ts` around lines 201 - 210, The ambient
declaration for packageContext/serviceContext/service is too loose: update the
`service` type used in checks.ts to match the runtime contract provided by
`PackageServiceInstanceBase.runServiceInBackground` — make `setAlarm` accept
only `Date` (not string | Date) and tighten the return types of `getStatus`,
`setAlarm` and `clearAlarm` to the concrete shapes used at runtime (e.g., use
the `packageServiceStatusSchema` shape for `getStatus` and the actual `{ ok:
boolean; scheduled_at: string }`-like shape for `setAlarm`/`clearAlarm`), so
package authors get accurate IntelliSense and typechecking.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/package-runtime/package-service.ts`:
- Around line 188-197: scheduleAlarm currently assumes input.runAt is a Date and
calls input.runAt.toISOString(), which will throw if callers pass a string;
update scheduleAlarm (the method) to normalize runAt by checking typeof
input.runAt === 'string' and parsing it to a Date (or use new Date(input.runAt))
before calling toISOString(), then pass that Date to this.ctx.storage.setAlarm,
set this.stateSnapshot.nextAlarmAt to the ISO string, and await
this.persistState(); this keeps the declared service.setAlarm signature
(string|Date) working without runtime errors.

---

Duplicate comments:
In `@packages/worker/src/package-runtime/package-service.ts`:
- Around line 256-265: The ternary setting this.stateSnapshot.status currently
sets 'stopped' in both branches; change it so when
this.stateSnapshot.stopRequested is true the status remains 'stopped' but when
false set a distinct state such as 'idle' or 'completed' (choose one and apply
consistently). Also, in the completion path inside the same block (the code that
sets this.stateSnapshot.lastResult and lastRunFinishedAt and clears
currentRunId/stopRequested), do not overwrite lastResult/lastRunFinishedAt if
stopRequested was true (i.e., only set lastResult/lastRunFinishedAt when
stopRequested is false), then call await this.persistState(); keep references to
this.stateSnapshot and the completion/stop logic in the same method so the
semantics are unambiguous.
- Around line 382-415: The stop flow doesn't actually abort the running module
because runServiceInBackground/runBundledModuleWithRegistry aren't given an
AbortSignal and rely only on service.shouldStop; fix by creating an
AbortController when starting a run (in handleStartRequest), store its signal on
the instance (e.g. this.activeRunController and this.activeRunPromise), and pass
that AbortSignal into runServiceInBackground and onward into
runBundledModuleWithRegistry so the bundled runtime can be force-aborted; then
update handleStopRequest to call this.activeRunController?.abort() in addition
to flipping stopRequested/clearing alarms. Alternatively, if you prefer
cooperative cancellation only, add explicit documentation for the
service.shouldStop contract in package authoring docs, but the code-level fix is
to thread an AbortSignal through the symbols handleStartRequest,
handleStopRequest, runServiceInBackground, and runBundledModuleWithRegistry.

---

Nitpick comments:
In `@docs/use/execute.md`:
- Around line 108-112: Split the combined bullet into two distinct parts: one
bullet that explains storage scoping for "package service runs" (service-owned
durable state scoped to the declared service name) and a separate bullet or
subheading that describes the execution lifecycle semantics for service_start
(that service_start returns immediately and the service runs in the background
until finish/error/cooperative stop). Update the wording under the "package
service runs" statement to only cover storage concerns and create a new bullet
or a small subheading titled "Execution lifecycle" (or similar) to host the
service_start background/execution details so storage and lifecycle are clearly
separated.
- Around line 27-32: Update the docs to specify that service.setAlarm accepts a
Date object: document the parameter as setAlarm(runAt: Date) and note the
Promise return shape per ServiceToolOptions (Promise<{ ok: true; scheduled_at:
string }>) so users know to pass a JavaScript Date and what the method returns;
reference the implementation signature in ServiceToolOptions (see
run-codemode-registry.ts) when editing the list item for setAlarm.

In `@packages/worker/src/mcp/run-codemode-registry.node.test.ts`:
- Around line 926-939: Remove the inline assertion from the mockImplementation
of createExecuteExecutor (created as createExecuteExecutorSpy) and have the mock
simply return the fake executor; after the test's awaited call to
runBundledModuleWithRegistry, assert on the spy's call arguments instead (e.g.,
expect(createExecuteExecutorSpy).toHaveBeenCalledWith(expect.objectContaining({
timeoutMs: 300_000 }))). This keeps the mock free of side-effect assertions and
uses createExecuteExecutorSpy.mock.calls (or Jest/Vi matcher) to validate the
timeoutMs passed into createExecuteExecutor.

In `@packages/worker/src/package-runtime/package-service.ts`:
- Around line 463-480: alarm() currently always calls loadSavedPackageService
and then calls handleStartRequest which builds and discards an HTTP Response;
change it so alarm() catches failures from loadSavedPackageService and falls
back to this.stateSnapshot.autoStart (so auto-start intent isn't lost on
transient KV/D1 errors), and refactor the start flow by extracting the core
start logic from handleStartRequest into a new private method (e.g., private
executeStart(binding): Promise<PackageServiceRunResult | void>) that returns the
PackageServiceRunResult or void; have handleStartRequest call executeStart and
build its Response.json around that result, and have alarm() call executeStart
directly (no Response construction), while preserving the existing
persistState() call and stopRequested checks.
- Around line 439-461: The fetch method currently always calls await
request.json(), consuming the body before route matching; change fetch to first
inspect request.method and url.pathname and only parse the JSON body for the
POST routes that need it (e.g., before calling handleStartRequest,
handleStatusRequest, handleStopRequest). Keep the early binding check logic but
move the body parsing into the branches that call
handleStartRequest/handleStatusRequest/handleStopRequest; for the
/service-status branch call buildServiceStatusResponse(binding) without parsing
body, and return 404 for unmatched routes without touching request.json().

In `@packages/worker/src/repo/checks.ts`:
- Around line 201-210: The ambient declaration for
packageContext/serviceContext/service is too loose: update the `service` type
used in checks.ts to match the runtime contract provided by
`PackageServiceInstanceBase.runServiceInBackground` — make `setAlarm` accept
only `Date` (not string | Date) and tighten the return types of `getStatus`,
`setAlarm` and `clearAlarm` to the concrete shapes used at runtime (e.g., use
the `packageServiceStatusSchema` shape for `getStatus` and the actual `{ ok:
boolean; scheduled_at: string }`-like shape for `setAlarm`/`clearAlarm`), so
package authors get accurate IntelliSense and typechecking.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c1512569-4d25-40b3-82a9-ae73b2ede11b

📥 Commits

Reviewing files that changed from the base of the PR and between 8147b28 and b2c5bb3.

📒 Files selected for processing (19)
  • docs/use/execute.md
  • docs/use/packages.md
  • packages/worker/src/mcp/capabilities/services/service-get.ts
  • packages/worker/src/mcp/capabilities/services/service-list.ts
  • packages/worker/src/mcp/capabilities/services/service-start.ts
  • packages/worker/src/mcp/capabilities/services/service-stop.ts
  • packages/worker/src/mcp/capabilities/services/services-domain.node.test.ts
  • packages/worker/src/mcp/capabilities/services/shared.ts
  • packages/worker/src/mcp/executor.ts
  • packages/worker/src/mcp/run-codemode-registry.node.test.ts
  • packages/worker/src/mcp/run-codemode-registry.ts
  • packages/worker/src/oauth-handlers.workers.test.ts
  • packages/worker/src/package-registry/manifest.ts
  • packages/worker/src/package-runtime/module-graph.ts
  • packages/worker/src/package-runtime/package-app.ts
  • packages/worker/src/package-runtime/package-service.node.test.ts
  • packages/worker/src/package-runtime/package-service.ts
  • packages/worker/src/package-runtime/published-runtime-artifacts.ts
  • packages/worker/src/repo/checks.ts
✅ Files skipped from review due to trivial changes (2)
  • docs/use/packages.md
  • packages/worker/src/mcp/capabilities/services/services-domain.node.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/worker/src/package-runtime/package-service.node.test.ts
  • packages/worker/src/mcp/capabilities/services/service-get.ts
  • packages/worker/src/mcp/capabilities/services/service-list.ts
  • packages/worker/src/package-runtime/package-app.ts
  • packages/worker/src/mcp/run-codemode-registry.ts

Comment thread packages/worker/src/package-runtime/package-service.ts Outdated
cursoragent and others added 4 commits April 25, 2026 00:45
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts
Comment thread packages/worker/src/package-runtime/package-service.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts Outdated
Comment thread packages/worker/src/package-runtime/package-service.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-app.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts
Comment thread packages/worker/src/package-runtime/package-service.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-app.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-runtime/package-service.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
run_id: runId,
started_at: startedAt,
status: 'running',
} satisfies PackageServiceRunResult)

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.

Unnecessary alarm scheduled on first explicit start request

Low Severity

handleStartRequest calls initializeBinding with armAutoStart: true. On first binding when autoStart is true, initializeBinding schedules an immediate alarm (new Date()). Then handleStartRequest proceeds to start the service directly by setting currentRunId and calling runServiceInBackground. This means both an alarm AND a direct start are initiated. The alarm fires after the fetch returns, finds currentRunId set, and exits — but not before persisting a state write to clear alarm fields. This is a wasted DO storage write on every first start of an auto-start service.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cfe34b4. Configure here.

Comment thread packages/worker/src/package-runtime/package-service.ts

export type PackageServiceStatusRecord = z.infer<
typeof packageServiceStatusSchema
>

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.

Duplicate type export across two modules

Low Severity

PackageServiceStatusRecord is independently exported from both package-service.ts and shared.ts as identically-defined z.infer type aliases of the same schema. shared.ts already re-exports the schema itself, so the second type alias is redundant and could confuse consumers about the canonical import location.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cfe34b4. Configure here.

cursoragent and others added 2 commits April 25, 2026 05:25
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/package-registry/service.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 597effc. Configure here.

Comment thread packages/worker/src/package-runtime/package-app.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.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.

2 participants