Repository navigation
Expose the resolved plugin root / durable-state anchor on the request context (and to providers) instead of making plugins re-derive AGENT_BUNDLE_PLUGIN_ROOT #468
Description
Symptom in movie-library
movie-library (feat/react-app-shell @ d6a247b) keeps an 80-line resolver, scripts/state-root.ts, whose only job is to answer "where is this plugin installed / where does its durable state live" — the same question every generated agent-bundle worker already answers privately:
scripts/state-root.ts:55-61re-readsAGENT_BUNDLE_PLUGIN_ROOT, guards against a literal unexpanded${...}token, then falls back to source-layout / artifact-layout heuristics /process.cwd().src/providers/tracker-session.ts:78— the request-context provider exposesstateRoot: resolveStateRoot(env)so layouts, thesession/startevent route (src/events/session/start.tsx:47) and the CLI can print where state lands.agent-bundle.config.ts:23-26documents the arrangement: "every adapter injects the well-known AGENT_BUNDLE_PLUGIN_ROOT anchor into generated stdio entries, and scripts/state-root.ts resolves it".- PORT-NOTES.md § Durable state: "The kernel anchors on
AGENT_BUNDLE_PLUGIN_ROOT… and falls back to the artifact root, which is different from the plugin's own.runtimeroot (scripts/state-root.ts…)". Two anchors for one install, because the plugin cannot read the framework's. - PR build(deps): bump @mantine/core from 9.5.1 to 9.5.2 #8 framework-mode audit, finding 7: "
scripts/run-stdio.sh+scripts/state-root.tshand-roll launcher/state-root resolution that overlaps the framework's stdio entry,output.distPath, anddefineStateroots" — category (b), left in place because nothing public replaces it.
Any plugin with pre-existing on-disk state that is not a defineState store (caches, token files, a Python sidecar's data dir) has to do the same.
Root cause in agent-bundle (main @ 284141958)
The request context has no plugin-root / durable-anchor axis; the anchor is computed inside each generated shell and never surfaced:
packages/rsc-runtime/src/agent-request.ts:199-220—AgentRequestContextexposesinvocation,host,session,actor,workspace,lineage,progress,signal,providers,state.workspace(:83-85) is{ root: string }and is populated with the caller's cwd (packages/agent-bundle/src/build/entry-shell.ts:331,:528—workspace: available({ root: cwd }, 'derived')), i.e. the user's project, not the plugin install.packages/agent-bundle/src/build/entry-shell.ts:188,:712,:987— three separateconst durableAnchor = process.env.AGENT_BUNDLE_PLUGIN_ROOT ?? <fallback>computations (state kernel, notice ledger, lineage registry), each private to the emitted source.packages/agent-bundle/src/routes/public.ts:96-102—AgentProviderContext = { invocation, signal }; a provider cannot read the anchor either (Pass request identity, lineage, and read-only state/notices handles to context providers #459 widens providers to identity/lineage/state, but does not add a plugin-root/anchor field).packages/agent-bundle/src/core/types.ts:792/adapters/types.ts:117-122—pluginRootEnvAnchoris a public env contract; there is no public runtime accessor, andagent-bundle/meta(packages/agent-bundle/src/meta.ts) carries only name/version.- None of the framework's own anchor computations apply movie-library's
${guard (state-root.ts:57-60): a host that passes manifest env through literally would give the kerneljoin('${CLAUDE_PLUGIN_ROOT}', 'state')under cwd.
Workaround in movie-library
scripts/state-root.ts (resolvePluginRoot / resolveStateRoot, five-tier fallback), consumed by the provider, the layouts' footer, the event route, and every legacy domain module.
Proposed fix
Expose the resolved anchor once and let routes, layouts, providers, and event routes read it:
interface AgentRequestContext {
// new
readonly plugin: Observed<{
readonly root: string; // resolved AGENT_BUNDLE_PLUGIN_ROOT, or the fallback the shell used
readonly stateRoot: string; // join(root, 'state') — where defineState / notices / lineage live
}>;
}source: 'native'whenAGENT_BUNDLE_PLUGIN_ROOTwas set and expanded,'derived'for the artifact/cwd fallback;unavailablewith a typed reason for an unexpanded${...}token (and have the kernel treat that token as unset instead of creating a literal${...}directory).- Compute it in one place in the emitted shells (state kernel, notice ledger, lineage registry, and the request context all read the same value) instead of three inline expressions.
- Add the same field to
AgentProviderContext(fits naturally alongside Pass request identity, lineage, and read-only state/notices handles to context providers #459). - Document in
docs/entry-conventions.md(state section) and the runtime reference.
Acceptance
(await agent()).plugin.root/.stateRootareavailablein a generated MCP tool route, a rendered CLI command, a hook-backed event route, and the route-unit harness (fixture seam for tests), on portable/claude/codex/cursor artifacts and undermcp run.- A provider receives the same field.
entry-shell.test.tsproves the kernel, notice ledger, lineage registry, andplugin.stateRootagree on one path, and that an unexpanded${...}anchor is reportedunavailablerather than joined into a path.- movie-library can delete
scripts/state-root.ts's anchor tier and readplugin.rootinsrc/providers/tracker-session.ts.
Activity
feat(runtime): expose the resolved plugin root as request.plugin and …
feat(runtime): expose the resolved plugin root as request.plugin and …
feat(runtime): expose the resolved plugin root as request.plugin and …
feat(runtime): expose the resolved plugin root as request.plugin and …