From 40e1912f2f4709b7d208ad719d855208ea565dd8 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 22:26:20 +0800 Subject: [PATCH] fix(plugins): preserve frozen hosted workers on stale library --- ...901-web-search-stale-resources-8b4e2c71.md | 152 ++++++++++++++++++ electron/coding-plugins/effective-resolver.ts | 21 ++- electron/coding-plugins/registry.ts | 2 +- tests/unit/coding-capability-registry.test.ts | 7 +- .../coding-plugin-effective-resolver.test.ts | 38 +++++ 5 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260901-web-search-stale-resources-8b4e2c71.md diff --git a/.project-docs/30-worklog/tasks/20260901-web-search-stale-resources-8b4e2c71.md b/.project-docs/30-worklog/tasks/20260901-web-search-stale-resources-8b4e2c71.md new file mode 100644 index 0000000..cb85e64 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-web-search-stale-resources-8b4e2c71.md @@ -0,0 +1,152 @@ +# Task: Diagnose Web Search stale worker resources + +## Identity + +- Task ID: 20260901-web-search-stale-resources-8b4e2c71 +- Mode: Feature +- Branch: codex/20260901-web-search-stale-resources-8b4e2c71 +- Worktree: D:\Datas\OthersProjects\makelore-web-search-stale-resources-8b4e2c71 +- Base commit: 697c68974de8bf43c7094e67372a03123c449343 +- Owner: codex-root +- Status: Ready for Integration + +## Scope + +- Diagnose the reported `409 Web Search worker resources are stale` from the actual + installed-client Conversation, establish a deterministic regression at the owning + client seam, and implement only the smallest lifecycle/materialization correction + supported by evidence. +- Own only the client Plugin/Pi resource-generation paths and focused tests proven to + cause the failure. Do not change Works Square, pricing, Provider, Marketplace + publication, packaged trust, or unrelated Agent behavior. + +## Intent And Constraints + +- Preserve the product invariant that one parent Pi worker uses a frozen Plugin + snapshot for its lifetime. Account/project/Release/policy or worker-generation + changes must continue to invalidate future actions; the fix must not globally relax + fail-closed stale-resource checks. +- Use the live user-data files only as read-only diagnostic evidence and redact Account + tokens/credentials. Do not restart or kill the running MakeLore client without user + authorization. +- Follow the `diagnosing-bugs` loop: first establish a tight red-capable feedback loop, + then state ranked falsifiable hypotheses, add a regression before changing product + code, and verify the same loop after the fix. +- Preserve the two unrelated untracked package task records in the user root worktree; + all task writes stay in this isolated worktree. No push, package, publish, deploy, or + paid Provider call is authorized. + +## Project Context Loaded + +Task context: +- Task ID: `20260901-web-search-stale-resources-8b4e2c71` +- Mode: Feature +- Branch: `codex/20260901-web-search-stale-resources-8b4e2c71` +- Worktree: `D:\Datas\OthersProjects\makelore-web-search-stale-resources-8b4e2c71` +- Base commit: `697c68974de8bf43c7094e67372a03123c449343` +- Other active local tasks: the old Web Search coordinator + `20260831-web-search-client-5b9d2e63` remains registered at its pre-integration + frontier; several historical Marketplace reviewers remain registered in Planning. + The user root contains two unowned package task records. +- Overlap or semantic-conflict assessment: no current product writer owns the exact + main frontier or this diagnosis. The old coordinator and reviewers are historical or + read-only at earlier bases. The two root records remain unknown/unowned and untouched; + isolation prevents overlap. + +Read: +- `.project-docs/05-agent-entry/memory-index.md` +- `.project-docs/05-agent-entry/read-before-coding.md` +- `.project-docs/05-agent-entry/planning-gate.md` +- `.project-docs/00-brief/project-positioning.md` +- `.project-docs/30-worklog/current-state.md` +- `.project-docs/10-decisions/decision-index.md` +- `.project-docs/20-architecture/system-overview.md` +- `.project-docs/20-architecture/module-map.md` +- `.project-docs/20-architecture/data-flow.md` +- `.project-docs/40-domain/business-rules.md` +- `.project-docs/00-brief/success-criteria.md` +- `.project-docs/40-domain/glossary.md` +- `.project-docs/50-evidence/evidence-index.md` +- `.project-docs/60-reflection/reflection-index.md` +- `.project-docs/80-commitments/commitments.md` +- `.project-docs/90-maintenance/stale-items.md` +- peer record `20260831-web-search-client-5b9d2e63.md` + +Relevant understanding: +- Project goal: MakeLore exposes curated, Main-owned Plugin capabilities to eligible Pi + parent workers without leaking Account, Provider, trust, or billing authority. +- Current integrated focus: the Web Search package/runtime and assignment catalog fix + are on local `main`; the screenshot proves the new Conversation now loads and invokes + `makelore_web_search` but fails at invocation-time resource validation. +- Active task scope: diagnose and correct only the self-invalidating stale-resource path. +- Active constraints: frozen workers must remain frozen; child workers remain empty; + official bundled acquisition, project enablement, Agent assignment, current Admission, + confirmation, and billing remain separate gates. +- Decisions affecting this task: effective Plugin materialization is a single frozen + snapshot; supported lifecycle changes invalidate future actions rather than hot-swap + resources; Electron Main is the only hosted transport authority. +- Evidence, reflections, or commitments affecting this task: existing workers are + intentionally frozen, and the current installed binary may predate source changes; + live PostgreSQL/OpenAI/production activation holds are unrelated to this client-side + `409` and must not be presented as verified. +- Files or modules likely involved: actual Pi prompt/extension-context/session evidence, + then `electron/coding-plugins/registry.ts`, effective resolver/policy admission, Pi + materialization/lifecycle seams, and their focused tests if the evidence points there. +- Unknowns, stale docs, or conflicts: a paid live Provider call remains intentionally + unexecuted. The client-side failure is independently reproducible at the frozen + snapshot boundary and does not require spending Token Points to diagnose. + +Gate result: +- Passed. Concurrent ownership and Planning context are resolved; diagnostic work may + proceed in this isolated worktree. + +## Outcome + +- Live evidence from the reported Conversation proves that the parent worker contains + the `makelore-web-search` Skill, the `makelore_web_search` tool, the official bundled + Release `00000000-0000-4000-8000-000000000204`, account generation 1, and current + runtime policy. The confirmed invocation alone returned + `409 plugin_runtime_stale`; no resource-rebuild event occurred before it. +- Ranked hypotheses were checked against the persisted Pi manifest/session, project + selection, Agent assignment, session timestamps, extension-host context, and Main + logs. Wrong child role, wrong Conversation/generation, an account epoch change, and a + project/Release mutation were not supported. The remaining reachable path was the + registry's invocation-time call to the full worker materializer. +- Root cause: `CodingCapabilityRegistryImpl.invoke()` re-ran + `EffectivePluginResolver.resolve()`. That path intentionally rejects a hosted Plugin + whenever a subsequent Account Library request returns a trusted but stale snapshot, + so one transient Library refresh invalidated a worker that had just been materialized. + This happened before the hosted adapter could obtain the server-owned immutable + Admission that actually gates account, runtime status, and exact Release dispatch. +- Added a separate `resolveForInvocation()` contract. New workers still fail closed on + stale hosted Library data. An already frozen worker may retain the last verified + Library projection long enough to reach the typed hosted Admission; missing Library, + account change, project disable, removed acquisition, suspension, incompatible or + changed Release, unsupported policy, billing unavailability, and child-worker gates + remain unchanged. +- TDD evidence: the new resolver and registry regressions first failed because + `resolveForInvocation` did not exist and the registry still called `resolve`; after + the focused correction both pass. + +## Verification + +- `pnpm exec vitest run tests/unit/coding-plugin-effective-resolver.test.ts tests/unit/coding-capability-registry.test.ts`: 2 files / 19 tests passed. +- `pnpm exec vitest run tests/unit/coding-plugin-effective-resolver.test.ts tests/unit/coding-capability-registry.test.ts tests/unit/hosted-admission.test.ts tests/unit/web-search-plugin-adapter.test.ts tests/unit/coding-plugin-composition.test.ts tests/unit/coding-product-services.test.ts`: 6 files / 55 tests passed. +- `pnpm run typecheck`: passed. +- Scoped ESLint for the two product and two regression-test files: passed. +- `pnpm test`: 215 regular files / 1,786 passed / 2 skipped, followed by the + single-worker pressure suite 1/1 passed. The first diagnostic run encountered two + transient `spawn EBUSY` failures while the offline Electron dependency was settling; + that exact real-process file then passed 3/3, and the clean full rerun passed. +- `git diff --check`, `check_project_docs.py`, and task-aware + `check_doc_drift.py`: passed. +- A paid Web Search execution was not run; this task does not claim Provider, Token + Point settlement, deployment, packaging, or production activation evidence. + +## Follow-ups + +- None recorded. + +## Promotion Candidates + +- None recorded. diff --git a/electron/coding-plugins/effective-resolver.ts b/electron/coding-plugins/effective-resolver.ts index 6c25f9b..24c96ef 100644 --- a/electron/coding-plugins/effective-resolver.ts +++ b/electron/coding-plugins/effective-resolver.ts @@ -251,6 +251,23 @@ export class EffectivePluginResolver { constructor(private readonly options: EffectivePluginResolverOptions) {} async resolve(input: EffectivePluginResolverInput): Promise { + return this.resolveSnapshot(input, false); + } + + /** + * Revalidate an already frozen worker before invoking one of its tools. + * A stale Library snapshot is still the last verified account projection; + * the hosted adapter resolves a fresh server Admission for the exact frozen + * Release before dispatch. New workers continue to require a current Library. + */ + async resolveForInvocation(input: EffectivePluginResolverInput): Promise { + return this.resolveSnapshot(input, true); + } + + private async resolveSnapshot( + input: EffectivePluginResolverInput, + allowStaleHostedLibrary: boolean, + ): Promise { const definitions = await this.definitionRecords(); const blockedMarketplacePlugins = marketplaceSkillConflicts(definitions); const assigned = normalizeIds(input.assignedSkillIds); @@ -342,7 +359,9 @@ export class EffectivePluginResolver { unavailableReasons.push(unavailable(definition.id, 'account_required', 'Marketplace account is required')); continue; } - if (!library || (library.stale && definition.runtimeKind !== 'skill_only')) { + if (!library || (library.stale + && definition.runtimeKind !== 'skill_only' + && !allowStaleHostedLibrary)) { unavailableReasons.push(unavailable(definition.id, 'library_unavailable', 'Marketplace Library is unavailable')); continue; } diff --git a/electron/coding-plugins/registry.ts b/electron/coding-plugins/registry.ts index 014f89a..34321ab 100644 --- a/electron/coding-plugins/registry.ts +++ b/electron/coding-plugins/registry.ts @@ -560,7 +560,7 @@ export class CodingCapabilityRegistryImpl implements CodingCapabilityRegistryPor && candidate.capabilityId === tool.capabilityId && candidate.operation === tool.operation )); - const currentSnapshot = await this.options.effectiveResolver.resolve({ + const currentSnapshot = await this.options.effectiveResolver.resolveForInvocation({ projectId: input.context.projectId, projectPath: input.context.projectPath, assignedSkillIds: input.context.effectiveSnapshot.effectiveSkillIds, diff --git a/tests/unit/coding-capability-registry.test.ts b/tests/unit/coding-capability-registry.test.ts index 194a45a..fa63a1e 100644 --- a/tests/unit/coding-capability-registry.test.ts +++ b/tests/unit/coding-capability-registry.test.ts @@ -273,6 +273,7 @@ describe('CodingCapabilityRegistry', () => { const resolve = vi.fn(async () => frozenSnapshot); const effectiveResolver = { resolve, + resolveForInvocation: resolve, getSkillSources: vi.fn(async () => []), getPolicyState: vi.fn(() => hostedPolicy), getInstalledDefinition: vi.fn(async () => hostedDefinition), @@ -400,8 +401,10 @@ describe('CodingCapabilityRegistry', () => { const resolve = vi.fn(async ({ role }: { role: 'parent' | 'child' }) => ( role === 'child' ? { ...disabled, unavailableReasons: [] } : current )); + const resolveForInvocation = vi.fn(async () => current); const effectiveResolver = { resolve, + resolveForInvocation, getSkillSources: vi.fn(async () => [{ id: 'web-search', pluginId: definition.id, packageRoot: 'C:/packages/web-search', directory: 'C:/packages/web-search/skills/web-search', entryPath: 'SKILL.md', @@ -461,6 +464,7 @@ describe('CodingCapabilityRegistry', () => { capability_id: 'web-search.search', operation: 'search', payload_schema: 'web-search.v1', billing: { status: 'settled', actual_points: '1.00' }, }); + expect(resolveForInvocation).toHaveBeenCalledOnce(); current = disabled; const stale = await capabilityRegistry.invoke({ @@ -497,6 +501,7 @@ describe('CodingCapabilityRegistry', () => { }; const effectiveResolver = { resolve: vi.fn(async () => currentSnapshot), + resolveForInvocation: vi.fn(async () => currentSnapshot), getSkillSources: vi.fn(async () => []), getPolicyState: vi.fn(() => policy), } as unknown as EffectivePluginResolver; @@ -509,7 +514,7 @@ describe('CodingCapabilityRegistry', () => { }); expect(result.details).toMatchObject({ success: false, code: 'plugin_not_enabled' }); - expect(effectiveResolver.resolve).toHaveBeenCalledWith(expect.objectContaining({ + expect(effectiveResolver.resolveForInvocation).toHaveBeenCalledWith(expect.objectContaining({ projectId: context.projectId, projectPath: context.projectPath, role: 'parent', diff --git a/tests/unit/coding-plugin-effective-resolver.test.ts b/tests/unit/coding-plugin-effective-resolver.test.ts index 43f2933..6b4942b 100644 --- a/tests/unit/coding-plugin-effective-resolver.test.ts +++ b/tests/unit/coding-plugin-effective-resolver.test.ts @@ -322,6 +322,44 @@ describe('effective plugin resolver', () => { }); }); + it('keeps a frozen hosted worker usable through a transient stale Library refresh', async () => { + const currentHostedLibrary = { + ...library(), + items: [{ ...library().items[0], pluginId: serverDefinition.id }], + }; + const getLibrary = vi.fn() + .mockResolvedValueOnce(currentHostedLibrary) + .mockResolvedValue({ ...currentHostedLibrary, stale: true }); + const hosted = createEffectivePluginResolver({ + definitions: [serverDefinition], + getAccountBinding: () => binding, + getLibrary, + getInstalled: vi.fn(async () => installed(serverDefinition)), + getEnabledPluginIds: vi.fn(async () => [serverDefinition.id]), + policyClient: { getState: currentPolicy, refresh: vi.fn() }, + }); + const input = { + projectId: 'project-a', projectPath: 'C:/project-a', + assignedSkillIds: ['remote'], role: 'parent' as const, + }; + + const frozen = await hosted.resolve(input); + expect(frozen.toolDefinitions).toEqual([expect.objectContaining({ name: 'remote_read' })]); + await expect(hosted.resolveForInvocation(input)).resolves.toMatchObject({ + accountSessionId: frozen.accountSessionId, + pluginReleaseIds: frozen.pluginReleaseIds, + toolDefinitions: [{ name: 'remote_read' }], + runtimePolicies: [{ pluginId: serverDefinition.id, operation: 'read' }], + unavailableReasons: [], + }); + + await expect(hosted.resolve(input)).resolves.toMatchObject({ + toolDefinitions: [], + runtimePolicies: [], + unavailableReasons: [expect.objectContaining({ code: 'library_unavailable' })], + }); + }); + it('never exposes plugin resources to a child worker', async () => { const result = await resolver().resolve({ projectId: 'project-a', projectPath: 'C:/project-a', assignedSkillIds: ['notes'], role: 'child',