Repository navigation
feat(plugins): load directory plugins that generate, present, search, and change their own Artifacts - #2616
feat(plugins): load directory plugins that generate, present, search, and change their own Artifacts#2616jasper-yl wants to merge 20 commits into
Conversation
Zxf-xufeng
left a comment
There was a problem hiding this comment.
Static review of the plugin host/writer implementation at c408880f. I found two concurrency issues to address. I have not run the test suite.
[P1] Preserve request preconditions through the actual write transaction
In builtin/plugins/host.py, _change() checks the requested revision/governance version using _precheck(), then creates a separate writer for the plugin call (lines 345–388). UserChange retains the operation and IDs but not those expected versions. In writer.py, set_lifecycle() and merge() reread the current records and build their commit plans from those newer records.
For example, a forget request for revision 1 can pass its precheck, another request can publish revision 2, and the original forget can then deprecate revision 2 successfully instead of rejecting the stale request. The documented forget_artifact implementation follows this path. Merge inputs have the same gap: the versions used by the transaction can differ from those supplied by the caller.
Please carry the caller's expected revisions and governance versions into the writer and enforce them inside the write transaction. The framework should enforce the preconditions independently of whether the plugin repeats the checks. Add a regression test that pauses after the initial precheck, commits a competing update, and verifies that the stale forget/merge fails without changing the newer state.
[P2] Return the revision committed by this invocation, not a subsequent head
After the plugin commits, _change() reads the latest artifact again and returns that snapshot (host.py, lines 388–398). If request A commits revision 2 and request B commits revision 3 before A's final read, A can receive B's content/revision as the result of its own operation. Its response can also combine that later snapshot with A's lifecycle outcome.
Please retain the exact committed snapshot/result in the writer and use it for the response. Add an interleaving test that commits a second update between the first commit and response construction; each response should identify its own committed revision.
…he interfaces each plugin implements Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hidb4ai
left a comment
There was a problem hiding this comment.
Requesting changes for two reproduced correctness failures: plugin API writers can bypass Artifact write authorization, and supported v2 Artifact archives cannot be restored. The generated integration contracts also need to be synchronized. The existing concurrency findings are not duplicated here.
| owner = self._owner | ||
| await owner.artifacts.lock_heads( | ||
| self._connection, | ||
| owner.scope_id, | ||
| ((await self._require(artifact_id)).artifact.as_ref(),), |
There was a problem hiding this comment.
[P1] Enforce Artifact write authorization for direct writer mutations
A plugin route with access="scope.contribute" can obtain ctx.writer(scope_id) and modify another user's Artifact through set_tags() or the direct set_lifecycle(..., "retired") branch at lines 478–487. These paths write through repositories without calling the shared Artifact authorization service; _require() also reads with a None context. Reproduced with a real Server, spawned plugin host, and the built-in multi-Bearer identities: Alice creates a note; Bob has only scope.contributor. Bob's generic tag/content updates and plugin tx.put() return 403, but plugin tx.set_tags() and tx.set_lifecycle(..., "retired") both return 200 and persist the changes. Please enforce the caller's artifact.write permission inside the transaction for every direct mutation, including tags and lifecycle transitions, with a regression covering a non-owner contributor.
There was a problem hiding this comment.
Reproduced with the same setup (owner plus a scope.contributor acting through a plugin route) and fixed in de8c50a: set_tags, bind_handle, and the direct lifecycle transition now run the shared artifact.write check before writing; Worker writers are unchanged. tests/e2e/test_plugin_access.py uses a multi-Bearer identity map: a non-owner contributor gets 403 for tag, retire, and bind through a plugin route (204 before the fix), and the owner succeeds.
| payload = {name: payload.get(name) for name in spec.payload} | ||
| record = _Record(record_type=typed, identity=identity, payload=payload, digest=digest) |
There was a problem hiding this comment.
[P1] Preserve legacy record identity through staging and restore
This normalizes a v2 payload by adding metadata=None but retains its v2 digest. _record_document() subsequently emits that record with schema version 3, so the restore staging index contains a document whose digest no longer matches its version/payload. A three-record archive exported by base 1cb0e75d restores successfully on that base; on this head, validate() reports compatible=True, then restore() fails with BundleFormatError: record digest does not match content and inserts nothing. The same logical revision already present with null metadata is also incorrectly classified as an immutable conflict because target digests use v3. Please retain the verified original identity or normalize it consistently across staging and target comparisons, and test actual v2 restore plus repeat/identical-target restore.
There was a problem hiding this comment.
Reproduced: an archive exported by the released v1.5.1 failed restore with "record digest does not match content". Fixed in a9e1ff3: a verified schema-2 artifact_revision record is re-identified at the current schema for staging and target comparison, while the bundle total digest still covers the archived digests. The regression restores a real v1.5.1 archive (checked in as tests/fixtures/portable_archives/v1_5_1_profile_history.pcb), restores it again (0 inserted, 14 already present), and re-exports and restores the current-schema archive into the same target. The archive docs now describe schema 3 and the accepted schema 2.
| description: >- | ||
| Lists the plugins checked at Server startup, whether each loaded or failed, and what each one declares. | ||
| Plugins have no persisted lifecycle state; change the plugin directory and restart Server. | ||
| operationId: list_plugins |
There was a problem hiding this comment.
[P2] Regenerate all shipped consumers of the changed API contract
The OpenAPI changes are only partially propagated. In CI run 43340872845, rebuilding DSH, OpenCode, and Cindy changes their committed bundles, Dify reports stale tool declarations/contract.json, and all four Desktop jobs fail export_ipc --check. I also reproduced the Dify generator failure on this head. For example, wire.rs removes the old plugin schema fields while desktop/ui/src/generated/ipc.ts still declares them; the shipped JS bundles retain the removed admin operations and omit the new ones. Please regenerate and commit the built bundles, Dify contracts, and Desktop IPC types together with the source contract so the shipped clients and contract checks agree.
There was a problem hiding this comment.
This spans more than the OpenAPI document on c408880f: three shipped consumers still carry the removed endpoints.
e2e/bub/tests/test_access_audit.py:263-269still listslist_artifact_plugins,get_artifact_plugin,enable_all_artifact_plugins,reload_artifact_plugin,enable_artifact_plugin,disable_artifact_plugin,unload_artifact_pluginin_SERVER_WIDE_WITHOUT_CONTENT, whileopenapi/powercontext.yamlhas zeroartifact-pluginsmatches.test_every_server_wide_operation_is_classified_by_whether_it_returns_scope_contenttherefore fails at line 295, andharness-validation / validateends2 failed, 304 passedonmake harness-test(job 131643284529).integrations/opencode/plugins/powercontext/lib/index.js:239-305andintegrations/dsh/plugins/powercontext/lib/index.js:339-405still declare the seven/v1/admin/artifact-plugins…operations, while theirsrc/operations.generated.tsno longer contain any of them. The committed bundles are stale relative to their own sources, soopencode-packageanddsh-packagefail on the bundle-consistency step (jobs 131643275397 and 131643275520).
There was a problem hiding this comment.
Regenerated and committed (28e50ab, also covering @AlexStocks' points): the Dify tool declarations and contract.json, the Desktop IPC types, and the Bub access audit (the old *_artifact_plugin* operations removed; list_plugins, get_plugin, and list_plugin_scopes classified as returning no Scope content). The DSH, OpenCode, and Cindy bundles were rebuilt in d4b0f1b.
Regenerating Dify exposed a contract bug: adding the plugin branch (content: any object) to ReplaceArtifactRequest's oneOf made every built-in request match two branches, so JSON Schema validators such as the Dify tools rejected built-in edits. The create and replace bodies now use anyOf (effee7c), since the Server selects the branch by family name; the generated Python models are unchanged and the Desktop contract is requalified. I also relocked e2e/bub and evaluation for the new tzlocal dependency and aligned the Bub harness assembly check with the client omitting an unused per-section search.
At 8fe6162: make check, harness-test, dify-test, contract-test, the Desktop IPC check, the JS/OpenCode/Cindy builds, docs-test, and the full pytest suite pass.
| scope_id = request.scope_id | ||
| if scope_id is None: | ||
| raise ValueError("rendering needs a Scope") # noqa: TRY003 | ||
| await self.require("scope.read", scope_id) |
There was a problem hiding this comment.
[P2] 读路径的 Scope 授权原本有 e2e 断言,本 PR 删除后替代套件没有恢复
触发条件:本 PR 之后任何对插件家族读授权的回归,即 host.py 里这几处 require("scope.read", ...) 的语义被改(_render 252 行、_search 275 行、_CheckedReads 531 行、_allowed 625 行)。
实际影响:本 PR 删掉了 tests/e2e/test_artifact_authorization_startup.py(-501 行),其中 test_custom_artifact_authorization_survives_server_restart 断言的正是这条不变量:
denied = await client.get(f"/v1/scopes/{scopes[1]}/artifacts/{family}/report")
assert denied.status_code == 403, denied.text并且验证了重启后授权仍然生效。替代它的两个套件里没有任何授权断言:tests/e2e/test_plugins.py(3 个用例:插件加载与 on_sources 派发、通用合并路由、通用来源 API)与 tests/builtin/plugins/test_plugin_writer.py(5 个用例:写入游标、生命周期/合并、陈旧读冲突、用户变更、生命周期目标)对 contribute|forbidden|AccessDenied|denied|authoriz 的命中数都是 0。
于是"插件家族产物只对在该 Scope 有授权的调用方可见"这条安全性不变量,现在只剩 host 里的实现,没有任何端到端断言。本 PR 同一区域已经出现过一次同类的授权缺口(builtin/plugins/writer.py:585 的 P1:ctx.writer() 绕过 contribute 授权),说明这条边界确实会被改动穿过——而这次删除恰好移掉了能拦住它的测试。
证据:deleted hunk tests/e2e/test_artifact_authorization_startup.py 中的 assert denied.status_code == 403;两个新测试文件的关键字命中数均为 0(grep -ci 'contribute|forbidden|AccessDenied|denied|authoriz');src/powercontext/builtin/plugins/host.py 的四处授权点。
修复方向:在 tests/e2e/test_plugins.py 补一个用例——建两个 Scope,只对第一个授予 viewer,断言对第二个 Scope 的 /v1/scopes/{id}/artifacts/{family}/...(含搜索路由)返回 403,并覆盖重启后仍生效。这样删掉旧套件才是净等价替换,而不是覆盖面减少。
There was a problem hiding this comment.
同意,读路径的授权断言需要恢复(de8c50a86)。tests/e2e/test_plugin_access.py 新增的用例中,只在一个 Scope 有 viewer 权限的调用者访问另一个 Scope 的插件 Artifact、rendering、列表、search 以及 Prepared Context 的插件 section,都返回 403;在有权限的 Scope 中返回 200。同一个用例也覆盖了 hidb4ai 指出的写路径越权。
"重启后仍生效"这部分没有加:旧测试防的是 #2246 框架把插件授权配置持久化后、重启时丢失的问题;新框架每次请求都通过统一的访问服务授权,插件不保存任何授权状态,重启不会引入需要验证的新状态。如果你认为仍有必要,我再补上。
|
@Zxf-xufeng Thanks — both reproduced and fixed in 6966dd1. [P1] Preconditions through the write transaction. [P2] Return the committed revision. The writer keeps the snapshot and records its agreed write committed; the host returns them and the Server builds the replace, create, forget, and merge responses from them instead of rereading the head. Regression: |
… and change their own Artifacts Implements the Artifact plugin part of RFC oceanbase#2549 and the plugin APIs and Dashboard pages of RFC oceanbase#2579, and replaces the managed Artifact plugin framework (oceanbase#2246). - A plugin is a directory with plugin.py assigning an ArtifactPlugin. Server checks it in a child process; generate runs in Supervisor Workers, and render_artifact, search_artifacts, user changes, Source writes of the plugin's own types, and plugin routes run in a per-plugin host process. - Plugins present (GET .../rendering) and search their own families: generic search passes the complete request to search_artifacts, and Prepared Context sections may list plugin families. Core builds no index for them. - Revision v1_5_0_005 removes the previous framework's storage only when it is empty, adds pc_artifacts.metadata (plugin name and code digest), and accepts plugin: bindings; frozen revision resources are unchanged. Portable revision records move to schema version 3 and still restore version 2. - Plugin APIs mount at /v1/plugins/{name}/api with a configurable body ceiling, and plugin pages run in a sandboxed Dashboard frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cancel only timed-out calls - Each plugin keeps one host process and starts more while every process is full and requests queue, up to `host_processes` (overridable per plugin with POWERCONTEXT_SERVER_PLUGIN_HOST_PROCESSES, capped in total by POWERCONTEXT_SERVER_PLUGIN_TOTAL_HOST_PROCESSES); extra processes stop after 5 idle minutes. - A call past its time limit is cancelled in the host and returns 504 `plugin_timeout`; other calls continue. A host whose heartbeat stops for 5 seconds is terminated, its other calls return 503, and it restarts after a backoff. - Prepared Context leaves out a plugin section whose plugin is unavailable and records the failed search in the receipt. - `Cron` without a time zone uses the machine's time zone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n tables Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d lifecycle writes Plugin route writers act as the caller, but set_tags, bind_handle, and the direct lifecycle transition (retirement, deprecation with a replacement) wrote through repositories without the shared Artifact access check, so a Scope contributor could retag or retire another user's Artifact through a plugin route. These writes now run the same artifact.write check as content changes; Worker writers keep their unchanged security. The new acceptance test also restores the read-side assertions removed with the old plugin authorization suite: a viewer of one Scope gets 403 for plugin Artifacts, renderings, search, and Prepared Context sections of another. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and report the committed revision The host checked expected revisions and governance versions before calling the plugin, but the writer then decided forget, merge, and update against the rows it reread, so a write committed after the precheck could be forgotten or merged by a stale request. User changes now carry the caller's expectations into the transaction, which rejects a changed target with 409 (412 for updates) whether or not the plugin repeats the check. Responses no longer reread the head after the plugin returns: the writer keeps the snapshot and records its agreed write committed, and the Server builds replace, create, forget, and merge responses from them, so a later write cannot appear as the result of an earlier request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on plugin writes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schema version 2 artifact_revision records were verified against their archived digest and then staged as version 3 with that same digest, so restoring any archive exported by v1.4.1 through v1.5.1 failed with "record digest does not match content", and the restored revisions would have been reported as immutable conflicts on a repeat restore. A verified earlier-schema record is now re-identified at the current schema for staging and target comparison, while the bundle total digest still covers the archived digests. The regression test restores, re-restores, and re-exports an archive produced by the released v1.5.1 runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it for the plugin API contract The OpenAPI changes removed the managed plugin operations and custom family schemas and opened Artifact families to loaded plugins, but the Dify tool declarations and contract, the Desktop IPC types, and the Bub access audit still described the old contract. Regenerate them, and classify the new plugin listing, detail, and Scope progress operations as returning no Scope content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plugin family's create and replace bodies accept any content object, so a built-in family's request also matched the plugin branch and failed oneOf's exactly-one rule in JSON Schema validators; the Dify tools rejected every built-in Artifact edit as an invalid request. The Server selects the branch by family name, which anyOf states accurately. Generated Python models keep the same unions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ct request bodies Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host process groups added tzlocal to the builtin extras without relocking the projects that depend on powercontext by path, so their --locked checks failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…odies Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ness and fix new test typing The Prepared Context client drops an unused per-section search so an older Server accepts the request; the Bub compatibility check still compared the payload with the full model dump. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n runtime and its Dashboard titles Point plugin imports at powercontext.builtin.authz, pass the section to the Prepared Context budget evaluation's section builder, and title plugin pages through the Dashboard's document_title. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e after rebasing onto master Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion tables Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in their own section Rebasing onto master's grouped navigation dropped the closing tag of the group loop, so every Dashboard page with plugin pages failed to render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8fe6162 to
34ffdae
Compare
Which issue or RFC does this PR close?
Implements RFC #2549 (plugin interfaces) and the plugin APIs and Dashboard pages of RFC #2579 (plugin hot reload, APIs, and pages), and replaces the managed Artifact plugin framework added in #2246. Hot reload, plugin data upgrades, and MCP tools from RFC #2579 are follow-ups; standalone Source plugins and collection are proposed in RFC #2617.
Rationale for this change
RFC #2549 replaces plugin frameworks that make PC Server manage plugin lifecycle with a few interfaces PC Server calls when it needs them. RFC #2579 lets plugins add their own operations and pages, which domain plugins such as document knowledge bases need. Keeping the #2246 framework alongside would mean two plugin models, two sets of admin APIs, and readiness checks that reject the new plugins' bindings, so this PR removes it in the same change.
What changes are included in this PR?
A plugin is a directory with
plugin.pyunderPOWERCONTEXT_SERVER_PLUGINS_DIR;plugin.pyassigns anArtifactPlugindeclaration toplugin.builtin/plugins/manifest.py,server/plugins.py): Server imports each plugin in a time-limited child process, validates the declaration, records a content fingerprint, and registers the family, the plugin's own Source types, and a Supervisor binding when the plugin has a schedule oron_sources. A plugin that fails to load, or whosesetupresult lacks a required method, isfailedinGET /v1/pluginswithout affecting Server.builtin/plugins/executor.py,writer.py):generate(request, writer)runs in Supervisor Workers; writes inasync with writer.transaction()commit together with the Source cursor.generateis required only with a schedule oron_sources.render_artifact(required) servesGET /v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}/rendering. Optionalsearch_artifactsreceives the complete generic search request; core keeps only current, active revisions the caller may read, in the plugin's order.POST /v1/context/preparesections may list plugin families, with an optional per-sectionsearchpassed through. Core builds no index for plugin families.builtin/plugins/host.py,server/plugin_hosts.py): generic create, replace, merge, forget, and restoration on plugin families run the plugin's methods in a per-plugin host process that authorizes as the caller, checks revisions, and records lineage-only provenance. Unimplemented methods return 422.POST /v1/scopes/{scope_id}/sourceswithsource_typeor by plugin routes withctx.sources.submit./v1/plugins/{name}/api{path}underPOWERCONTEXT_SERVER_PLUGIN_API_MAX_BODY_BYTES(default 32 MiB); static pages run in a sandboxed Dashboard frame that forwards only the plugin's own routes with the session.server/plugin_hosts.py): each plugin has its own group of host processes. One always runs; another starts while every process is busy and requests queue, up to the declaredhost_processes(default 1, overridable per plugin withPOWERCONTEXT_SERVER_PLUGIN_HOST_PROCESSES, capped for all plugins withPOWERCONTEXT_SERVER_PLUGIN_TOTAL_HOST_PROCESSES); extra processes stop after 5 idle minutes. A call past its time limit is cancelled in the host and returns 504plugin_timeoutwhile other calls continue; a host whose heartbeat stops for 5 seconds is terminated, its other calls return 503, and it restarts after a backoff. Prepared Context leaves out a plugin section whose plugin is unavailable and records the failed search in the receipt.Cronwithout a time zone uses the machine's.ctx.storageis a dedicated SQLite file per plugin;ctx.modelsexposes the Server's embedding and generation models without their credentials./v1/admin/artifact-plugins/*,powercontext.artifact_plugins, declarative family schemas and indexes, custom family searchers, thePOWERCONTEXT_SERVER_ARTIFACT_PLUGINS_DIRsetting, and its examples and tests.v1_5_0_005(SQLite and MySQL) drops the feat(artifact): add managed plugin framework and some example #2246 tables and registration columns only when they hold no data (the domain preflight otherwise refuses withunsupported_plugin_data), adds nullablepc_artifacts.metadatawith the plugin name and code digest of plugin-written revisions, and acceptsplugin:bindings in its own readiness step. Frozen revision resources are unchanged. Revision reads omitmetadataso domain steps at earlier revisions still run. Portableartifact_revisionrecords move to schema version 3 and still restore version 2.docs/{en,zh}/docs/develop/plugins.md; the OceanBase restore table lists no longer name removed tables.A complete knowledge base (RAG) example plugin built on this branch is at https://github.com/jasper-yl/powercontext/tree/feat/knowledge-base-plugin-v2/examples/plugins/knowledge_base and is not part of this PR.
Are there any user-facing changes?
Yes.
powercontext.artifact_plugins, andPOWERCONTEXT_SERVER_ARTIFACT_PLUGINS_DIRare removed. Upgrading a database whose feat(artifact): add managed plugin framework and some example #2246 plugins still hold data is refused until the deployer exports and removes them on the previous version.ArtifactRecord.schema_version,ArtifactCollectionItem.schema_version, andCapabilities.custom_artifact_schemasare removed.POWERCONTEXT_SERVER_PLUGINS_DIR,POWERCONTEXT_SERVER_PLUGIN_API_MAX_BODY_BYTES;GET /v1/plugins,GET /v1/plugins/{name},GET /v1/plugins/{name}/scopes; the rendering endpoint; governance, merge, forget, and restoration routes for every registered family; plugin routes and Dashboard plugin pages.CreateArtifactRequestbecomes a choice betweenCreateBuiltinArtifactRequestandCreatePluginArtifactRequest;ArtifactCreated.family,CreateSourceRequest.source_type,TaggableArtifactFamily,ContextAssemblySection.family, andPreparedContextFamilybecome strings;ContextAssemblyFamilyis removed. Wire formats of existing requests are unchanged.release/upgrade-support.jsonrecords the persistence review forv1_5_0_005.sqlite-plugin-interfaces-e0501451-v1).How was this change tested?
Full suite on a Linux VM (Ubuntu 24.04, 4 cores) at
e0501451:uv run pytest -n 4— 10387 passed, 1013 skipped, 1 failed:test_bundled_desktop_contract_has_a_qualified_business_profile, which the Desktop qualification commit in this PR resolves (tests/test_desktop_contract.pythen passes locally, 4 passed).make docs-teston the same VM: verified 1108 public pages and their internal links.make checkhooks (uv run prek run --all-files) anduv run ty checkpass locally.Desktop qualification
sqlite-plugin-interfaces-e0501451-v1(desktop/tests/real_server.py, product mode): anonymous loopback, static Bearer, HTTPS with CA/base path, and enforced Provider modes passed against the wheel built frome0501451;bundled_product_qualification_covers_every_desktop_operationpassed.Migration:
tests/builtin/persistence/test_plugin_storage_migration.pyupgrades a SQLite database atv1_5_0_004, is refused whilepc_artifact_family_schemasholds a row, and after the row is removed drops the old tables and columns, addspc_artifacts.metadata, and keeps existing data; the registered SQLite fingerprints of every earlier revision are unchanged. The MySQL resources forv1_5_0_005pass the shared layout validation in the suite; they were not run against a native OceanBase or seekDB.Plugin end-to-end tests run a real Server process:
tests/e2e/test_plugins.py(loading, generation, rendering, passthrough search with an unsupported field rejected, Prepared Context with a plugin section, user changes, tags, merges) andtests/e2e/test_plugin_pages.py.The knowledge base example on the fork branch passes its end-to-end test against this branch, including rendering, generic search with
mode, and Prepared Context with a per-sectionsearch.tests/e2e/test_plugin_hosts.pyruns a real Server: a timed-out call returns 504 while another on the same host completes; Prepared Context with a timing-out plugin section still returns and its receipt records the failed search; blocking the event loop gives the timed-out call 504 and the other 503, then a new host serves; sixteen long calls start a second host. Hooks andty checkpass locally, and the related plugin and Prepared Context tests pass (123).AI usage statement
Implemented with Claude Code (Claude Opus 5.5) under the author's direction; the author reviewed the design decisions. Claude Code ran the tests and manual checks listed above.
🤖 Generated with Claude Code