From 0a86ec825a5803bf7e037d3b23c39be23238c43d Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Mon, 31 Aug 2026 16:16:47 +0800 Subject: [PATCH 01/47] docs(plugin): record web search client seams --- ...e-web-search-client-inspection-6b4d2e81.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .project-docs/30-worklog/tasks/20260831-makelore-web-search-client-inspection-6b4d2e81.md diff --git a/.project-docs/30-worklog/tasks/20260831-makelore-web-search-client-inspection-6b4d2e81.md b/.project-docs/30-worklog/tasks/20260831-makelore-web-search-client-inspection-6b4d2e81.md new file mode 100644 index 0000000..124d468 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260831-makelore-web-search-client-inspection-6b4d2e81.md @@ -0,0 +1,70 @@ +# Task: Inspect MakeLore Web Search Plugin seams + +## Identity + +- Task ID: 20260831-makelore-web-search-client-inspection-6b4d2e81 +- Mode: Feature +- Branch: main +- Worktree: D:\Datas\OthersProjects\makelore +- Base commit: bf32fecd6013101485434b1977a73beb8b6a1fe3 +- Owner: codex-root +- Status: Ready for Integration + +## Scope + +- Inspect the exact MakeLore Main, Marketplace Package Store, effective plugin resolver, + capability registry, Pi worker, conversation-envelope, and Renderer seams needed by a + future `makelore.web-search` hosted Plugin. +- Record client ownership and acceptance requirements in the server-side task-owned Web + Search design/spec. Do not implement product code in this client root. + +## Intent And Constraints + +- The installed schema-2 package remains declarative; only a code-owned Main adapter may + call the fixed Works Square typed route. Pi and Renderer never receive Provider secrets + or arbitrary endpoints. +- A tool is materialized only for an acquired, installed, compatible, project-enabled, + Agent-assigned, admitted and currently supported Release, parent worker only. Existing + account/project/logout/worker invalidation and stable logical-operation identity apply. +- Preserve the generic Marketplace/My Plugins surfaces and + `makelore-capability.v1` conversation parser. P0 needs no plugin-specific Renderer panel. +- Concurrent Task Gate: PASS. This clean root was claimed at exact base + `bf32fecd6013101485434b1977a73beb8b6a1fe3`; no other active writer owns this semantic + slice and only this task record is changed. +- Planning Gate: PASS after loading AGENTS, project memory, current plugin source and + tests. The project-positioning document is still a template and is therefore recorded + as stale context; AGENTS, current-state, exact source, and tests are the implementation + authority for this inspection. + +## Outcome + +- Completed a read-only client seam inspection and fed exact ownership/acceptance + requirements into the server task's detailed design and implementation Spec. +- Confirmed Web Search can remain a signed schema-2 `platform_hosted` package with one + code-owned Main adapter; no static Pi allowlist, third-party executable, Provider + secret/model, generic invoke route, or Plugin-specific results page is required. +- Identified one necessary generic UI change: the conversation timeline may render the + closed billing union, while `web-search.v1` answer/sources remain tool payload used by + the Agent and preserved by conversation details. +- No client product file was changed. + +## Verification + +- Inspected current Package Store, Marketplace client, effective resolver, capability + registry, Game Resource hosted adapter, Pi worker materialization, conversation parser, + timeline, composition and their focused tests at exact base + `bf32fecd6013101485434b1977a73beb8b6a1fe3`. +- Confirmed `CapabilityBillingReceiptV1` currently has no Main transport-ambiguity + variant and the generic timeline currently shows only operation/outcome; both changes + are explicitly owned by MLW-02/MLW-03 rather than silently assumed. +- Final `check_project_docs` and task-aware `check_doc_drift` pass. + +## Follow-ups + +- Start client implementation only after the server WSW-03 typed DTO is frozen, then + follow MLW-01 -> MLW-02 -> MLW-03 and fixed-range review from the current integrated + client frontier. + +## Promotion Candidates + +- None recorded. From c4db68767fcd1916e4441ab0e2c2504210f75e83 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Mon, 31 Aug 2026 19:39:56 +0800 Subject: [PATCH 02/47] docs(web-search): open client integration ledger --- ...-web-search-client-integration-7d2f5b94.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md diff --git a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md new file mode 100644 index 0000000..e9e1e0f --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md @@ -0,0 +1,44 @@ +# Task: Coordinate MakeLore native Web Search client integration + +## Identity + +- Task ID: 20260831-web-search-client-integration-7d2f5b94 +- Mode: Integration +- Branch: codex/20260831-web-search-client-integration-7d2f5b94-web-search-client-integration +- Worktree: D:\Datas\OthersProjects\makelore-web-search-client-integration-7d2f5b94 +- Base commit: 0a86ec825a5803bf7e037d3b23c39be23238c43d +- Owner: web-search-client-integrator +- Status: In Progress + +## Scope + +- Serialize repository-local integration of MLW-01 through MLW-03 after the Server WSW-03 typed DTO freezes. +- Validate each isolated source ticket's exact parent, exclusive ownership, single source commit, clean state, and verification before cherry-picking only its final commit. +- Maintain the client integration ledger, fixed review range, review/remediation frontier, package proof, and fake-provider XWS-01 evidence without changing the user root worktree. + +## Intent And Constraints + +- Project Context Loaded: client `AGENTS.md`, complete `maintain-project-docs` skill, project-memory entry points/relevant architecture-domain-evidence records, complete reviewed Web Search design/spec, MLW-00 bootstrap record, and relevant Marketplace/Game Resource task history were read before planning. +- Concurrent Task Gate: Passed. Task-context owner fields exactly match this Integration task/worktree/branch/base and this task holds the repository integration lock. The MLW-00 feature bootstrap remains read-only at its isolated checkpoint; no active client owner has an evidenced overlapping Web Search scope. +- Planning Gate: Passed. Exact integration base is clean `0a86ec825a5803bf7e037d3b23c39be23238c43d`; MLW-00 focused baseline passed `7 files / 84 tests` across Marketplace client, effective resolver, Game Resource client/adapter, capability registry, Pi resource loader, and artifact proof. +- Reuse the existing signed Package Store, effective parent snapshot, hosted admission, Game Resource client/adapter, capability registry, Pi envelope/lifecycle, conversation timeline, and artifact proof; do not create a parallel hosted runtime. +- MLW-01 -> MLW-02 -> MLW-03, but MLW-01 remains blocked until WSW-03 freezes the Server DTO. Renderer owns no account/auth/provider/URL/credential/admission/trust authority. +- Preserve `submission_unknown` as result-less/non-retryable, result-bearing `pending_review`, and Main-only `receipt_unavailable`; no Web Search-specific settings page, `pi-web-search`, arbitrary Pi extension, generic invoke, push, PR, deploy, publish, or real paid OpenAI call. + +## Outcome + +- Coordinator gate and MLW-00 adoption complete. Current frontier is exact `0a86ec825a5803bf7e037d3b23c39be23238c43d`; MLW-01 is correctly blocked on WSW-03 DTO freeze. + +## Verification + +- Task-context status: exact task, Integration mode, owner, worktree, branch, base, and integration lock matched. +- Adopted MLW-00 baseline: `7 files / 84 tests passed` after reusing same-clean-HEAD dependencies through an ignored worktree-local junction; lockfile and source were unchanged. + +## Follow-ups + +- Start MLW-01 only after the Server coordinator returns the WSW-03 exact DTO/fault handoff. +- Keep the live OpenAI XWS-01 group and production activation closed pending explicit external inputs and user authorization. + +## Promotion Candidates + +- None recorded. From 1edd75e2465ae8bfbba87d8e077365daee144920 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 01:11:53 +0800 Subject: [PATCH 03/47] feat(plugins): share hosted admission resolution --- ...901-web-search-mlw01-admission-b7d5f3a2.md | 113 +++++++++++ .../coding-plugins/adapters/game-resource.ts | 41 ++-- electron/coding-plugins/hosted-admission.ts | 184 ++++++++++++++++++ .../unit/game-resource-plugin-adapter.test.ts | 20 +- tests/unit/hosted-admission.test.ts | 138 +++++++++++++ 5 files changed, 474 insertions(+), 22 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260901-web-search-mlw01-admission-b7d5f3a2.md create mode 100644 electron/coding-plugins/hosted-admission.ts create mode 100644 tests/unit/hosted-admission.test.ts diff --git a/.project-docs/30-worklog/tasks/20260901-web-search-mlw01-admission-b7d5f3a2.md b/.project-docs/30-worklog/tasks/20260901-web-search-mlw01-admission-b7d5f3a2.md new file mode 100644 index 0000000..cf1f226 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-web-search-mlw01-admission-b7d5f3a2.md @@ -0,0 +1,113 @@ +# Task: Implement MLW-01 shared hosted admission resolver + +## Identity + +- Task ID: 20260901-web-search-mlw01-admission-b7d5f3a2 +- Mode: Feature +- Branch: codex/20260901-web-search-mlw01-admission-b7d5f3a2-web-search-mlw01-admission +- Worktree: D:\Datas\OthersProjects\makelore-web-search-mlw01-admission-b7d5f3a2 +- Base commit: c4db68767fcd1916e4441ab0e2c2504210f75e83 +- Owner: web-search-mlw01-implementer +- Status: Ready for Integration + +## Scope + +- Implement MLW-01 shared hosted admission resolution from the exact client + frontier `c4db68767fcd1916e4441ab0e2c2504210f75e83`. +- Add `electron/coding-plugins/hosted-admission.ts` as the single shared, + provider-neutral resolver. It validates a trusted worker snapshot against + the Package Store, MakeLore version, Marketplace channel resolution and the + current account admission, returning only exact `releaseId` and + `releaseAdmissionId` or a typed failure. +- Refactor only the admission portion of + `electron/coding-plugins/adapters/game-resource.ts` to consume the helper; + preserve its existing provider, payload, confirmation, replay, and result + behavior. +- Add seam tests in `tests/unit/hosted-admission.test.ts` and the relevant + admission regression coverage in + `tests/unit/game-resource-plugin-adapter.test.ts`. +- Do not implement Web Search client/adapter, composition, Renderer, Pi + worker, Package Store/effective resolver, Server, or MLW-02/MLW-03 work. + +## Intent And Constraints + +- Project Context Loaded: client AGENTS.md, the complete + `maintain-project-docs` skill, TDD and implement-spec instructions, required + project-memory entry points, the MLW-01 implementation ticket, and the + reviewed Web Search design/implementation sections were read before coding. +- Concurrent Task Gate: Passed. Task-context owner, isolated worktree, branch, + owner, mode, and exact base match; the client coordinator and user root + worktrees remain untouched. +- Planning Gate: Passed. The prior broad Web Search feature/coordinator + records were inspected read-only. Their old `0a86ec8...` base and broad + coordination scope overlap by topic, but the current coordinator explicitly + owns this isolated MLW-01 ticket from the frozen DTO frontier; no unresolved + semantic conflict remains. +- The helper accepts only code-owned plugin identity, trusted worker snapshot, + Package Store, Marketplace client, and MakeLore version. It does not accept + provider/config, payload, billing, or HTTP-route inputs and sends no business + request of its own. +- Use TDD red -> green vertical slices and public seams. Every source change + stays within the five product/test paths above plus this task record. One + source commit with this task record included, clean worktree, and + `READY_FOR_INTEGRATION` handoff are required. No push, PR, deploy, publish, + or real Provider call. +- Project Context Loaded: + - Task ID: `20260901-web-search-mlw01-admission-b7d5f3a2` + - Mode: Feature + - Branch: `codex/20260901-web-search-mlw01-admission-b7d5f3a2-web-search-mlw01-admission` + - Worktree: `D:\Datas\OthersProjects\makelore-web-search-mlw01-admission-b7d5f3a2` + - Base commit: `c4db68767fcd1916e4441ab0e2c2504210f75e83` + - Other active local tasks: old Web Search feature/coordinator records and + unrelated historical review records; no other task owns the files in this + ticket. + - Overlap assessment: topic overlap with the old Web Search coordinator is + resolved by current ticket ownership and exact-base isolation; no code-path + or semantic overlap is left within this ticket's owned files. +- Planned slices: (1) resolver contract and mismatch tests; (2) minimal + resolver implementation; (3) Game Resource seam migration and regression; + (4) focused/adjacent/typecheck/lint/doc gates; (5) one source commit and + clean handoff. + +## Outcome + +- Implemented the provider-neutral `MarketplaceHostedAdmissionResolver` and + migrated Game Resource's admission seam to use it. A worker must carry a + frozen release; the resolver reads that exact installed release, resolves the + current account admission through the selected channel, and returns only the + exact release/admission IDs. Release, channel, action, digest, size, request, + and account mismatches fail with typed bounded errors. No current-release + fallback, provider/config, payload, billing, HTTP, Web Search, or renderer + behavior was added. +- The Game Resource adapter preserves its existing client and tool behavior, + while missing or stale hosted admission now fails closed through the shared + resolver. The only product/test paths changed are the five paths listed in + Scope; this task record is the sole project-doc change. + +## Verification + +- TDD red: the new resolver suite initially collected zero tests because the + new module was absent (expected module-not-found); no product module existed + before the red checkpoint. +- TDD green: hosted-admission and Game Resource admission suites passed 12/12. +- `pnpm run typecheck` passed. +- Adjacent coding-plugin/Game Resource suite passed 74/74 across 6 files. +- Full `pnpm test` passed: 212 files, 1,728 tests, 2 skipped; pressure suite + passed 1/1. `pnpm run lint:check` passed with 0 errors and 5 pre-existing + warnings outside this task (`Home` hook dependency and `Makelore` fast-refresh + exports). Owned-file ESLint and compile checks passed. +- Worktree diff/doc gates are being finalized before the single source commit; + no real provider call, publish, deployment, or production activation was + attempted. + +## Follow-ups + +- Coordinator must cherry-pick the single source commit from this exact base, + then run its repository-local integration and fixed-range review gates. +- Real PostgreSQL, live OpenAI, official signing-key, provider-secret, and + production-activation gates remain outside this client ticket and HOLD as + specified by the parent Web Search rollout. + +## Promotion Candidates + +- None recorded. diff --git a/electron/coding-plugins/adapters/game-resource.ts b/electron/coding-plugins/adapters/game-resource.ts index 983cabf..fcc8f17 100644 --- a/electron/coding-plugins/adapters/game-resource.ts +++ b/electron/coding-plugins/adapters/game-resource.ts @@ -4,6 +4,10 @@ import path from 'node:path'; import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins'; import { PiGameAssetTools } from '../../coding-runtime/pi/extensions/game-assets'; import type { MarketplacePackageClientPort, PluginPackageStore } from '../package-store'; +import { + MarketplaceHostedAdmissionError, + MarketplaceHostedAdmissionResolver, +} from '../hosted-admission'; import { GameResourceClient, GameResourceClientError, @@ -28,6 +32,7 @@ export interface GameResourcePluginAdapterOptions { readonly marketplace: MarketplacePackageClientPort; readonly packageStore: Pick; readonly makeloreVersion: string; + readonly admissionResolver?: MarketplaceHostedAdmissionResolver; readonly gameAssets?: PiGameAssetTools; } @@ -86,6 +91,9 @@ function clientFailure(error: unknown): AdapterInvocationResult { if (error instanceof GameResourceClientError) { return failure(error.code, error.message, error.status, error.retryable); } + if (error instanceof MarketplaceHostedAdmissionError) { + return failure(error.code, error.message, error.status, error.retryable); + } return failure( 'plugin_backend_unavailable', 'Hosted game-resource service is temporarily unavailable', @@ -159,9 +167,15 @@ function templateConfig(value: unknown): Readonly> { export class GameResourcePluginAdapter implements CodingPluginAdapter { readonly pluginId = PLUGIN_ID; private readonly gameAssets: PiGameAssetTools; + private readonly admissionResolver: MarketplaceHostedAdmissionResolver; constructor(private readonly options: GameResourcePluginAdapterOptions) { this.gameAssets = options.gameAssets ?? new PiGameAssetTools(); + this.admissionResolver = options.admissionResolver ?? new MarketplaceHostedAdmissionResolver({ + marketplace: options.marketplace, + packageStore: options.packageStore, + makeloreVersion: options.makeloreVersion, + }); } async inspect(): Promise { @@ -287,28 +301,13 @@ export class GameResourcePluginAdapter implements CodingPluginAdapter { releaseId: string; releaseAdmissionId: string; }> { - const installed = context.pluginReleaseId - ? await this.options.packageStore.getInstalledRelease(PLUGIN_ID, context.pluginReleaseId) - : await this.options.packageStore.getInstalled(PLUGIN_ID); - if (!installed || !installed.channel) { - throw new GameResourceClientError('plugin_release_unavailable', 409, false, 'Installed game-resource Release is unavailable'); - } - const resolved = await this.options.marketplace.resolve({ - resolveRequestId: context.requestId, - makeloreVersion: this.options.makeloreVersion, - channel: installed.channel, - installed: [{ - pluginId: installed.pluginId, - releaseId: installed.releaseId, - sha256: installed.sha256, - }], + return this.admissionResolver.resolve({ + pluginId: PLUGIN_ID, + workerSnapshot: { + requestId: context.requestId, + ...(context.pluginReleaseId === undefined ? {} : { pluginReleaseId: context.pluginReleaseId }), + }, }); - const item = resolved.items.find(({ pluginId }) => pluginId === PLUGIN_ID); - if (!item?.releaseId || !item.releaseAdmissionId || item.releaseId !== installed.releaseId - || (item.action !== 'keep' && item.action !== 'install')) { - throw new GameResourceClientError('plugin_runtime_stale', 409, false, 'Game-resource worker Release is stale'); - } - return { releaseId: item.releaseId, releaseAdmissionId: item.releaseAdmissionId }; } } diff --git a/electron/coding-plugins/hosted-admission.ts b/electron/coding-plugins/hosted-admission.ts new file mode 100644 index 0000000..83c2845 --- /dev/null +++ b/electron/coding-plugins/hosted-admission.ts @@ -0,0 +1,184 @@ +import type { MarketplaceResolveItem } from './account-plugin-cache'; +import type { MarketplacePackageClientPort, PluginPackageStore } from './package-store'; + +const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,127}$/u; +const RELEASE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u; +const REQUEST_ID_PATTERN = /^[\x21-\x7e]{1,128}$/u; +const SHA256_PATTERN = /^[a-f0-9]{64}$/u; + +export interface HostedAdmissionWorkerSnapshot { + /** Main-issued identity for the logical worker operation. */ + readonly requestId: string; + /** Release frozen into this parent worker's effective snapshot. */ + readonly pluginReleaseId?: string; +} + +export interface MarketplaceHostedAdmissionInput { + readonly pluginId: string; + readonly workerSnapshot: HostedAdmissionWorkerSnapshot; +} + +export interface MarketplaceHostedAdmission { + readonly releaseId: string; + readonly releaseAdmissionId: string; +} + +export type MarketplaceHostedAdmissionErrorCode = + | 'plugin_release_unavailable' + | 'plugin_runtime_stale' + | 'plugin_account_changed' + | 'plugin_backend_unavailable'; + +export class MarketplaceHostedAdmissionError extends Error { + constructor( + readonly code: MarketplaceHostedAdmissionErrorCode, + readonly status: number, + readonly retryable: boolean, + message: string, + ) { + super(message); + this.name = 'MarketplaceHostedAdmissionError'; + } +} + +export interface MarketplaceHostedAdmissionResolverOptions { + readonly packageStore: Pick; + readonly marketplace: Pick; + readonly makeloreVersion: string; +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function errorCode(value: unknown): unknown { + return isRecord(value) ? value.code : undefined; +} + +function releaseId(value: unknown): string | null { + return typeof value === 'string' && RELEASE_ID_PATTERN.test(value) ? value : null; +} + +function pluginId(value: unknown): string | null { + return typeof value === 'string' && PLUGIN_ID_PATTERN.test(value) ? value : null; +} + +function requestId(value: unknown): string | null { + return typeof value === 'string' && REQUEST_ID_PATTERN.test(value) ? value : null; +} + +function stale(message: string): MarketplaceHostedAdmissionError { + return new MarketplaceHostedAdmissionError('plugin_runtime_stale', 409, false, message); +} + +function unavailable(message: string): MarketplaceHostedAdmissionError { + return new MarketplaceHostedAdmissionError('plugin_release_unavailable', 409, false, message); +} + +function backendUnavailable(): MarketplaceHostedAdmissionError { + return new MarketplaceHostedAdmissionError( + 'plugin_backend_unavailable', + 503, + true, + 'Hosted Plugin admission service is temporarily unavailable', + ); +} + +function accountChanged(): MarketplaceHostedAdmissionError { + return new MarketplaceHostedAdmissionError( + 'plugin_account_changed', + 409, + false, + 'Marketplace account changed while resolving hosted Plugin admission', + ); +} + +function matchesFrozenRelease( + item: MarketplaceResolveItem, + installed: Awaited>, + plugin: string, +): item is MarketplaceResolveItem & { + readonly releaseId: string; + readonly releaseAdmissionId: string; +} { + if (!installed) return false; + if (item.pluginId !== plugin || (item.action !== 'keep' && item.action !== 'install')) return false; + if (releaseId(item.releaseId) !== installed.releaseId + || releaseId(item.releaseAdmissionId) === null + || item.releaseAdmissionId === undefined + || item.releaseAdmissionId === null) return false; + if (item.version !== installed.version || item.sha256 !== installed.sha256) return false; + if (item.sizeBytes !== installed.sizeBytes || item.channel !== installed.channel) return false; + return item.sha256 !== null && item.sha256 !== undefined && SHA256_PATTERN.test(item.sha256); +} + +/** + * Resolves the current account admission for the exact Release frozen into a + * parent worker. Marketplace identity and Release freshness are the only + * concerns here; provider, payload, billing, and transport routes remain + * outside this boundary. + */ +export class MarketplaceHostedAdmissionResolver { + constructor(private readonly options: MarketplaceHostedAdmissionResolverOptions) {} + + async resolve(input: MarketplaceHostedAdmissionInput): Promise { + const plugin = pluginId(input?.pluginId); + const worker = input?.workerSnapshot; + const request = requestId(worker?.requestId); + const frozenRelease = releaseId(worker?.pluginReleaseId); + if (!plugin || !request) throw unavailable('Hosted Plugin worker identity is unavailable'); + if (!frozenRelease) throw unavailable('Hosted Plugin worker Release is unavailable'); + + let installed: Awaited>; + try { + installed = await this.options.packageStore.getInstalledRelease(plugin, frozenRelease); + } catch (error) { + throw this.mapError(error); + } + if (!installed || installed.pluginId !== plugin || installed.releaseId !== frozenRelease + || installed.unavailableReason || !installed.channel) { + throw unavailable('Installed hosted Plugin Release is unavailable'); + } + + let resolved; + try { + resolved = await this.options.marketplace.resolve({ + resolveRequestId: request, + makeloreVersion: this.options.makeloreVersion, + channel: installed.channel, + installed: [{ + pluginId: installed.pluginId, + releaseId: installed.releaseId, + sha256: installed.sha256, + }], + }); + } catch (error) { + throw this.mapError(error); + } + + if (resolved.stale || resolved.resolveRequestId !== request) { + throw stale('Hosted Plugin worker admission is stale'); + } + const item = resolved.items.find(({ pluginId: candidate }) => candidate === plugin); + if (!item || !matchesFrozenRelease(item, installed, plugin)) { + throw stale('Hosted Plugin worker Release admission is stale'); + } + return Object.freeze({ + releaseId: item.releaseId, + releaseAdmissionId: item.releaseAdmissionId, + }); + } + + private mapError(error: unknown): MarketplaceHostedAdmissionError { + if (error instanceof MarketplaceHostedAdmissionError) return error; + const code = errorCode(error); + if (code === 'marketplace_account_changed' || code === 'plugin_account_changed') return accountChanged(); + return backendUnavailable(); + } +} + +export function createMarketplaceHostedAdmissionResolver( + options: MarketplaceHostedAdmissionResolverOptions, +): MarketplaceHostedAdmissionResolver { + return new MarketplaceHostedAdmissionResolver(options); +} diff --git a/tests/unit/game-resource-plugin-adapter.test.ts b/tests/unit/game-resource-plugin-adapter.test.ts index fe0a7cf..eeca4e6 100644 --- a/tests/unit/game-resource-plugin-adapter.test.ts +++ b/tests/unit/game-resource-plugin-adapter.test.ts @@ -94,7 +94,7 @@ async function fixture() { })), getInstalledRelease: vi.fn(async () => ({ pluginId: 'makelore.game-resource', releaseId: RELEASE_ID, version: '1.0.0', - sha256: 'a'.repeat(64), channel: 'stable', + sha256: 'a'.repeat(64), sizeBytes: 1, channel: 'stable', })), } as unknown as Pick, makeloreVersion: '2.0.0', @@ -181,4 +181,22 @@ describe('GameResourcePluginAdapter', () => { releaseId: RELEASE_ID, releaseAdmissionId: 'admission-a', kind: 'pixel', }); }); + + it('fails closed when a worker has no frozen release instead of using the current package', async () => { + const { adapter, context, client, resolve } = await fixture(); + + await expect(adapter.invoke( + { ...context, pluginReleaseId: undefined }, + tool('game_resource_templates'), + { kind: 'pixel' }, + )).resolves.toMatchObject({ + success: false, + status: 409, + code: 'plugin_release_unavailable', + retryable: false, + }); + + expect(resolve).not.toHaveBeenCalled(); + expect(client.templates).not.toHaveBeenCalled(); + }); }); diff --git a/tests/unit/hosted-admission.test.ts b/tests/unit/hosted-admission.test.ts new file mode 100644 index 0000000..6154def --- /dev/null +++ b/tests/unit/hosted-admission.test.ts @@ -0,0 +1,138 @@ +// @vitest-environment node + +import { describe, expect, it, vi } from 'vitest'; +import { + MarketplaceHostedAdmissionError, + MarketplaceHostedAdmissionResolver, +} from '../../electron/coding-plugins/hosted-admission'; +import type { MarketplacePackageClientPort, PluginPackageStore } from '../../electron/coding-plugins/package-store'; + +const PLUGIN_ID = 'makelore.game-resource'; +const RELEASE_ID = 'release-game-1'; +const REQUEST_ID = 'pi:run-a:resource-a'; +const DIGEST = 'a'.repeat(64); + +function installed() { + return { + pluginId: PLUGIN_ID, + releaseId: RELEASE_ID, + version: '1.0.0', + packageSchemaVersion: 2, + contractVersion: 1, + runtimeKind: 'platform_hosted' as const, + sha256: DIGEST, + sizeBytes: 1, + installedAt: '2026-09-01T00:00:00.000Z', + channel: 'stable' as const, + }; +} + +function resolved(overrides: Record = {}) { + return { + resolveRequestId: REQUEST_ID, + resolveRequestDigest: DIGEST, + items: [{ + pluginId: PLUGIN_ID, + action: 'keep' as const, + releaseId: RELEASE_ID, + version: '1.0.0', + sha256: DIGEST, + sizeBytes: 1, + releaseAdmissionId: 'admission-game-1', + expiresAt: '2100-01-01T00:00:00Z', + channel: 'stable' as const, + reason: null, + ...overrides, + }], + catalogGeneration: 1, + etag: '"plugins-1-tp-free"', + stale: false, + }; +} + +function resolverFixture() { + const getInstalledRelease = vi.fn(async () => installed()); + const resolve = vi.fn(async () => resolved()); + const resolver = new MarketplaceHostedAdmissionResolver({ + packageStore: { getInstalledRelease } as unknown as Pick, + marketplace: { resolve } as unknown as Pick, + makeloreVersion: '2.0.0', + }); + return { resolver, getInstalledRelease, resolve }; +} + +describe('MarketplaceHostedAdmissionResolver', () => { + it('resolves the frozen package release through the selected channel and current account admission', async () => { + const { resolver, getInstalledRelease, resolve } = resolverFixture(); + + await expect(resolver.resolve({ + pluginId: PLUGIN_ID, + workerSnapshot: { requestId: REQUEST_ID, pluginReleaseId: RELEASE_ID }, + })).resolves.toEqual({ + releaseId: RELEASE_ID, + releaseAdmissionId: 'admission-game-1', + }); + + expect(getInstalledRelease).toHaveBeenCalledWith(PLUGIN_ID, RELEASE_ID); + expect(resolve).toHaveBeenCalledWith({ + resolveRequestId: REQUEST_ID, + makeloreVersion: '2.0.0', + channel: 'stable', + installed: [{ pluginId: PLUGIN_ID, releaseId: RELEASE_ID, sha256: DIGEST }], + }); + }); + + it('does not substitute the current package when the worker has no frozen release', async () => { + const { resolver, getInstalledRelease, resolve } = resolverFixture(); + + await expect(resolver.resolve({ + pluginId: PLUGIN_ID, + workerSnapshot: { requestId: REQUEST_ID }, + })).rejects.toMatchObject({ + code: 'plugin_release_unavailable', + status: 409, + retryable: false, + }); + + expect(getInstalledRelease).not.toHaveBeenCalled(); + expect(resolve).not.toHaveBeenCalled(); + }); + + it.each([ + ['release mismatch', { releaseId: 'release-other' }], + ['channel mismatch', { channel: 'beta' }], + ['action mismatch', { action: 'update' }], + ['digest mismatch', { sha256: 'b'.repeat(64) }], + ])('returns one typed stale failure for %s', async (_label, item) => { + const marketplace = { + resolve: vi.fn(async () => resolved(item)), + } as unknown as Pick; + const staleResolver = new MarketplaceHostedAdmissionResolver({ + packageStore: { getInstalledRelease: vi.fn(async () => installed()) } as unknown as Pick, + marketplace, + makeloreVersion: '2.0.0', + }); + + const error = await staleResolver.resolve({ + pluginId: PLUGIN_ID, + workerSnapshot: { requestId: REQUEST_ID, pluginReleaseId: RELEASE_ID }, + }).catch((value: unknown) => value); + + expect(error).toBeInstanceOf(MarketplaceHostedAdmissionError); + expect(error).toMatchObject({ code: 'plugin_runtime_stale', status: 409, retryable: false }); + }); + + it('projects an account change as a typed non-retryable worker failure', async () => { + const { resolver, resolve } = resolverFixture(); + resolve.mockRejectedValueOnce({ code: 'marketplace_account_changed' }); + + await expect(resolver.resolve({ + pluginId: PLUGIN_ID, + workerSnapshot: { requestId: REQUEST_ID, pluginReleaseId: RELEASE_ID }, + })).rejects.toMatchObject({ + code: 'plugin_account_changed', + status: 409, + retryable: false, + }); + }); +}); From f3874e90706692094f0f22fca465923143e23c7e Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 01:15:05 +0800 Subject: [PATCH 04/47] docs(web-search): record MLW-01 integration --- ...-web-search-client-integration-7d2f5b94.md | 12 +- ...901-web-search-mlw01-admission-b7d5f3a2.md | 113 ------------------ 2 files changed, 10 insertions(+), 115 deletions(-) delete mode 100644 .project-docs/30-worklog/tasks/20260901-web-search-mlw01-admission-b7d5f3a2.md diff --git a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md index e9e1e0f..dcc0a04 100644 --- a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md +++ b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md @@ -27,16 +27,24 @@ ## Outcome -- Coordinator gate and MLW-00 adoption complete. Current frontier is exact `0a86ec825a5803bf7e037d3b23c39be23238c43d`; MLW-01 is correctly blocked on WSW-03 DTO freeze. +- Coordinator gate and MLW-00 adoption complete. The coordinator ledger checkpoint advanced the implementation frontier from base `0a86ec825a5803bf7e037d3b23c39be23238c43d` to `c4db68767fcd1916e4441ab0e2c2504210f75e83` without product changes. +- MLW-01 source task `20260901-web-search-mlw01-admission-b7d5f3a2` was integrated from exact frontier `c4db68767fcd1916e4441ab0e2c2504210f75e83` after the Server DTO/fault freeze at `a49c696ebc4213e3d62ece780961efbe17576f8e`. + - Source commit: `fbeaa8ee8b0a19f240469a289449253034ee3ec3` (sole parent `c4db68767fcd1916e4441ab0e2c2504210f75e83`). + - Product commit: `1edd75e2465ae8bfbba87d8e077365daee144920`; source and product trees are exact-equal at `b17a16ff1c46706980bd162af3678176b3a93e70` before the repository-local Integration Documentation Gate removes the duplicate foreign task record. + - Delivered the provider-neutral `MarketplaceHostedAdmissionResolver` and an admission-only Game Resource refactor. The helper resolves exact installed release/admission identity from the trusted frozen worker snapshot, Package Store, selected channel, Marketplace resolve, current account, and MakeLore version; it owns no Provider, payload, billing, route, Web Search client, composition, Renderer, or Pi behavior. + - The clean source branch retains its complete task record. This coordinator checkpoint removes only the duplicate cherry-picked copy and records integration evidence here. ## Verification - Task-context status: exact task, Integration mode, owner, worktree, branch, base, and integration lock matched. - Adopted MLW-00 baseline: `7 files / 84 tests passed` after reusing same-clean-HEAD dependencies through an ignored worktree-local junction; lockfile and source were unchanged. +- MLW-01 focused admission/Game Resource suites passed `12/12`; adjacent six-file coding-plugin/Game Resource regression passed `74/74`. +- MLW-01 full unit suite passed `212 files / 1728 tests`, with two staged-runtime skips; the pressure suite passed `1/1`. Typecheck passed. Full lint passed with zero errors and the five pre-existing Home/Makelore warnings; owned-file lint/compile, diff, project-docs, and document-drift gates passed. +- The coordinator's optional duplicate focused run stopped before collection because this isolated worktree has no executable `vitest`; no test or product failure occurred and no dependency/source file changed. The Integration Gate instead adopts the clean source's exact-tree test evidence above rather than performing an unrelated dependency installation. ## Follow-ups -- Start MLW-01 only after the Server coordinator returns the WSW-03 exact DTO/fault handoff. +- Start MLW-02 from the clean post-MLW-01 coordinator frontier. Keep MLW-03 composition, Renderer, Pi worker, and package-proof ownership closed until MLW-02 is integrated. - Keep the live OpenAI XWS-01 group and production activation closed pending explicit external inputs and user authorization. ## Promotion Candidates diff --git a/.project-docs/30-worklog/tasks/20260901-web-search-mlw01-admission-b7d5f3a2.md b/.project-docs/30-worklog/tasks/20260901-web-search-mlw01-admission-b7d5f3a2.md deleted file mode 100644 index cf1f226..0000000 --- a/.project-docs/30-worklog/tasks/20260901-web-search-mlw01-admission-b7d5f3a2.md +++ /dev/null @@ -1,113 +0,0 @@ -# Task: Implement MLW-01 shared hosted admission resolver - -## Identity - -- Task ID: 20260901-web-search-mlw01-admission-b7d5f3a2 -- Mode: Feature -- Branch: codex/20260901-web-search-mlw01-admission-b7d5f3a2-web-search-mlw01-admission -- Worktree: D:\Datas\OthersProjects\makelore-web-search-mlw01-admission-b7d5f3a2 -- Base commit: c4db68767fcd1916e4441ab0e2c2504210f75e83 -- Owner: web-search-mlw01-implementer -- Status: Ready for Integration - -## Scope - -- Implement MLW-01 shared hosted admission resolution from the exact client - frontier `c4db68767fcd1916e4441ab0e2c2504210f75e83`. -- Add `electron/coding-plugins/hosted-admission.ts` as the single shared, - provider-neutral resolver. It validates a trusted worker snapshot against - the Package Store, MakeLore version, Marketplace channel resolution and the - current account admission, returning only exact `releaseId` and - `releaseAdmissionId` or a typed failure. -- Refactor only the admission portion of - `electron/coding-plugins/adapters/game-resource.ts` to consume the helper; - preserve its existing provider, payload, confirmation, replay, and result - behavior. -- Add seam tests in `tests/unit/hosted-admission.test.ts` and the relevant - admission regression coverage in - `tests/unit/game-resource-plugin-adapter.test.ts`. -- Do not implement Web Search client/adapter, composition, Renderer, Pi - worker, Package Store/effective resolver, Server, or MLW-02/MLW-03 work. - -## Intent And Constraints - -- Project Context Loaded: client AGENTS.md, the complete - `maintain-project-docs` skill, TDD and implement-spec instructions, required - project-memory entry points, the MLW-01 implementation ticket, and the - reviewed Web Search design/implementation sections were read before coding. -- Concurrent Task Gate: Passed. Task-context owner, isolated worktree, branch, - owner, mode, and exact base match; the client coordinator and user root - worktrees remain untouched. -- Planning Gate: Passed. The prior broad Web Search feature/coordinator - records were inspected read-only. Their old `0a86ec8...` base and broad - coordination scope overlap by topic, but the current coordinator explicitly - owns this isolated MLW-01 ticket from the frozen DTO frontier; no unresolved - semantic conflict remains. -- The helper accepts only code-owned plugin identity, trusted worker snapshot, - Package Store, Marketplace client, and MakeLore version. It does not accept - provider/config, payload, billing, or HTTP-route inputs and sends no business - request of its own. -- Use TDD red -> green vertical slices and public seams. Every source change - stays within the five product/test paths above plus this task record. One - source commit with this task record included, clean worktree, and - `READY_FOR_INTEGRATION` handoff are required. No push, PR, deploy, publish, - or real Provider call. -- Project Context Loaded: - - Task ID: `20260901-web-search-mlw01-admission-b7d5f3a2` - - Mode: Feature - - Branch: `codex/20260901-web-search-mlw01-admission-b7d5f3a2-web-search-mlw01-admission` - - Worktree: `D:\Datas\OthersProjects\makelore-web-search-mlw01-admission-b7d5f3a2` - - Base commit: `c4db68767fcd1916e4441ab0e2c2504210f75e83` - - Other active local tasks: old Web Search feature/coordinator records and - unrelated historical review records; no other task owns the files in this - ticket. - - Overlap assessment: topic overlap with the old Web Search coordinator is - resolved by current ticket ownership and exact-base isolation; no code-path - or semantic overlap is left within this ticket's owned files. -- Planned slices: (1) resolver contract and mismatch tests; (2) minimal - resolver implementation; (3) Game Resource seam migration and regression; - (4) focused/adjacent/typecheck/lint/doc gates; (5) one source commit and - clean handoff. - -## Outcome - -- Implemented the provider-neutral `MarketplaceHostedAdmissionResolver` and - migrated Game Resource's admission seam to use it. A worker must carry a - frozen release; the resolver reads that exact installed release, resolves the - current account admission through the selected channel, and returns only the - exact release/admission IDs. Release, channel, action, digest, size, request, - and account mismatches fail with typed bounded errors. No current-release - fallback, provider/config, payload, billing, HTTP, Web Search, or renderer - behavior was added. -- The Game Resource adapter preserves its existing client and tool behavior, - while missing or stale hosted admission now fails closed through the shared - resolver. The only product/test paths changed are the five paths listed in - Scope; this task record is the sole project-doc change. - -## Verification - -- TDD red: the new resolver suite initially collected zero tests because the - new module was absent (expected module-not-found); no product module existed - before the red checkpoint. -- TDD green: hosted-admission and Game Resource admission suites passed 12/12. -- `pnpm run typecheck` passed. -- Adjacent coding-plugin/Game Resource suite passed 74/74 across 6 files. -- Full `pnpm test` passed: 212 files, 1,728 tests, 2 skipped; pressure suite - passed 1/1. `pnpm run lint:check` passed with 0 errors and 5 pre-existing - warnings outside this task (`Home` hook dependency and `Makelore` fast-refresh - exports). Owned-file ESLint and compile checks passed. -- Worktree diff/doc gates are being finalized before the single source commit; - no real provider call, publish, deployment, or production activation was - attempted. - -## Follow-ups - -- Coordinator must cherry-pick the single source commit from this exact base, - then run its repository-local integration and fixed-range review gates. -- Real PostgreSQL, live OpenAI, official signing-key, provider-secret, and - production-activation gates remain outside this client ticket and HOLD as - specified by the parent Web Search rollout. - -## Promotion Candidates - -- None recorded. From fc68cf295131d8f73556bef59fdeae61239a649e Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 02:03:17 +0800 Subject: [PATCH 05/47] feat(plugins): add hosted web search client --- ...260901-web-search-mlw02-client-c8e4a2d1.md | 123 +++++ .../coding-plugins/adapters/web-search.ts | 296 ++++++++++ electron/coding-plugins/registry.ts | 3 + electron/services/web-search-client.ts | 521 ++++++++++++++++++ ...ding-conversation-product-tool-protocol.ts | 5 + shared/data-service.ts | 5 + .../coding-conversation-contracts.test.ts | 27 + tests/unit/web-search-client.test.ts | 272 +++++++++ tests/unit/web-search-plugin-adapter.test.ts | 306 ++++++++++ 9 files changed, 1558 insertions(+) create mode 100644 .project-docs/30-worklog/tasks/20260901-web-search-mlw02-client-c8e4a2d1.md create mode 100644 electron/coding-plugins/adapters/web-search.ts create mode 100644 electron/services/web-search-client.ts create mode 100644 tests/unit/web-search-client.test.ts create mode 100644 tests/unit/web-search-plugin-adapter.test.ts diff --git a/.project-docs/30-worklog/tasks/20260901-web-search-mlw02-client-c8e4a2d1.md b/.project-docs/30-worklog/tasks/20260901-web-search-mlw02-client-c8e4a2d1.md new file mode 100644 index 0000000..13750c0 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-web-search-mlw02-client-c8e4a2d1.md @@ -0,0 +1,123 @@ +# Task: Implement MLW-02 Web Search Main client and adapter + +## Identity + +- Task ID: 20260901-web-search-mlw02-client-c8e4a2d1 +- Mode: Feature +- Branch: codex/20260901-web-search-mlw02-client-c8e4a2d1-web-search-mlw02-client +- Worktree: D:\Datas\OthersProjects\makelore-web-search-mlw02-client-c8e4a2d1 +- Base commit: f3874e90706692094f0f22fca465923143e23c7e +- Owner: web-search-mlw02-implementer +- Status: Ready for Integration + +## Scope + +- Implement MLW-02 from exact client coordinator frontier + `f3874e90706692094f0f22fca465923143e23c7e` and frozen server DTO/context + frontier `a49c696ebc4213e3d62ece780961efbe17576f8e`. +- Add the Main-only `electron/services/web-search-client.ts` with closed input, + fixed route, Works authentication/one-refresh behavior, bounded DTO parsing, + same-operation reconciliation, and honest billing receipt projection. +- Add `electron/coding-plugins/adapters/web-search.ts` as the code-owned hosted + adapter. It validates closed input and confirmation, invokes the shared MLW-01 + admission resolver, supplies trusted project/request identity, and maps server + outcomes without exposing Provider details or calculating Token Points. +- Make only the minimal shared contract changes required by the new + `platform_metered/not_started` and Main-only `receipt_unavailable` billing + statuses: `shared/data-service.ts`, + `shared/coding-conversation-product-tool-protocol.ts`, and the Registry billing + validator in `electron/coding-plugins/registry.ts`. +- Add focused Web Search, billing/parser, adapter, and conversation contract + tests within the ticket's owned test paths. +- Do not modify Renderer, composition, Pi worker/resource, Package Store, + effective resolver, server, MLW-03 paths, or any user/coordinator worktree. + +## Intent And Constraints + +- Project Context Loaded: client `AGENTS.md`, the complete + `maintain-project-docs` skill and document-system reference, TDD and + implement-spec instructions, required project-memory entry points, peer task + records, and Web Search design/implementation spec §12 were read before + planning. +- Concurrent Task Gate: Passed. `task_context.py start` created this isolated + feature task from the exact coordinator frontier; status identity matches the + task ID, owner, mode, branch, absolute worktree, and base. The user root and + client coordinator worktrees remain untouched. +- Planning Gate: Passed. The older broad Web Search coordinator/integration and + completed MLW-01 records were inspected read-only. MLW-01 is the only direct + dependency and its exact source commit is integrated at this task's base; + this task owns different files. No unresolved semantic conflict remains. +- Frozen server context is the typed Web Search route/receipt contract at + `a49c696ebc4213e3d62ece780961efbe17576f8e`; client code must not invent + Provider/model/key/URL/payload authority or a generic invoke surface. +- Main retains Works credentials, route, logical IDs, reconciliation and raw + server receipt projection. The client never calculates points, exposes query + beyond the typed request boundary, or forwards Provider response/header/raw + error data to Pi/Renderer. +- Transport uncertainty and `reserved`/`dispatched` responses reuse the same + logical operation and exact request body for one bounded window of at most + 155 seconds. A 429 is non-retryable and preserves only bounded + `Retry-After`; a new user confirmation must create a new logical operation. +- TDD vertical slices: closed parser/fixed request, auth refresh, reconciliation + and receipt states, adapter mappings, then shared billing/protocol validator + changes and regression gates. One source commit with this task record, + clean worktree, and `READY_FOR_INTEGRATION` handoff are required. No push, + PR, deploy, publish, or real OpenAI call. + +## Outcome + +- Added the Main-only `WebSearchClient` with the exact typed hosted route, + closed request/response parsing, one Works-token refresh, whole-response + bounds, bounded same-operation reconciliation, and honest projection of the + server-owned receipt. Transport ambiguity and in-progress receipts reuse the + same logical operation/body for at most 155 seconds; an exhausted window + returns only `plugin_receipt_unavailable` and never invents an amount. +- Added the code-owned Web Search Plugin adapter. It validates the exact + `query`/`confirmed` input, resolves the frozen Release admission through the + MLW-01 helper, and supplies only trusted durable project/request identity to + Main. Complete `succeeded` and result-bearing `pending_review` responses are + usable; result-less `submission_unknown`, 429, admission/auth/input failures, + and receipt uncertainty retain their bounded business code and billing state. +- Extended the shared billing union, Registry validator, and conversation + parser only for Main-owned `platform_metered/receipt_unavailable` while + retaining the existing `platform_metered/not_started` state for every + pre-transaction failure. +- Final review corrected two frozen-contract mismatches: source title/URL + bounds now exactly match Server `240/4096`, and the adapter preserves + MLW-01 `plugin_release_unavailable` / `plugin_account_changed` admission + failures instead of collapsing them into a generic backend error. +- No composition, Renderer, Pi worker/resource, Package Store, effective + resolver, Server, Provider/model/key/URL authority, generic invoke, deploy, + publish, or live Provider call was added. + +## Verification + +- Concurrent/Planning Gate identity remained exact after recovery: same task, + owner, worktree, branch, and base `f3874e90706692094f0f22fca465923143e23c7e`; + the coordinator and user root worktrees were not modified. +- The original delegated turn was interrupted after preserving the complete + owned test/product diff but before returning its red/green ledger. A second + delegated recovery also stalled without changing files. The parent + coordinator then became the sole writer in this same owned worktree; no + task release, rebuild, reset, stash, clean, or concurrent edit occurred. +- First observable recovered focused run: 3 files / 32 tests passed. Contract + review regressions for exact source bounds and both admission failure codes + brought the final focused result to 3 files / 35 tests passed. +- Adjacent hosted admission, Game Resource, Registry, conversation, + Marketplace-client and timeline regression: 11 files / 114 tests passed. +- `pnpm run typecheck` passed. Owned-file ESLint passed. Full + `pnpm run lint:check` passed with zero errors and the unchanged five warnings + in Home/Makelore outside this task. +- Full unit suite passed: 214 files / 1748 tests, 2 staged-runtime skips; the + pressure suite passed 1/1. `pnpm run build:vite` passed Renderer, Main, + Preload, and utility builds with only existing chunk/dynamic-import notices. +- Final diff, project-docs, document-drift, task-context completion, sole-parent, + and clean-worktree facts are recorded by the source commit handoff. + +## Follow-ups + +- None recorded. + +## Promotion Candidates + +- None recorded. diff --git a/electron/coding-plugins/adapters/web-search.ts b/electron/coding-plugins/adapters/web-search.ts new file mode 100644 index 0000000..6bae1dc --- /dev/null +++ b/electron/coding-plugins/adapters/web-search.ts @@ -0,0 +1,296 @@ +import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins'; +import type { CapabilityBillingReceiptV1 } from '../../../shared/data-service'; +import { + MarketplaceHostedAdmissionError, + MarketplaceHostedAdmissionResolver, +} from '../hosted-admission'; +import type { MarketplacePackageClientPort, PluginPackageStore } from '../package-store'; +import type { + AdapterInvocationResult, + CodingPluginAdapter, + PluginBackendProjection, + TrustedCodingCapabilityContext, +} from '../registry'; +import { + WebSearchClientError, + type WebSearchClient, + type WebSearchRead, +} from '../../services/web-search-client'; + +const PLUGIN_ID = 'makelore.web-search'; +const TOOL_NAME = 'makelore_web_search'; +const CAPABILITY_ID = 'web-search.search'; +const OPERATION = 'search'; +const MAX_QUERY_LENGTH = 2_000; +const MAX_RETRY_AFTER_SECONDS = 86_400; +const SAFE_ERROR_CODES = new Set([ + 'authentication_required', + 'confirmation_required', + 'plugin_backend_invalid', + 'plugin_backend_unavailable', + 'plugin_backend_response_too_large', + 'plugin_billing_unavailable', + 'plugin_execution_unavailable', + 'plugin_operation_conflict', + 'plugin_account_changed', + 'plugin_provider_unavailable', + 'plugin_receipt_unavailable', + 'plugin_release_admission_required', + 'plugin_release_unavailable', + 'plugin_reservation_expired', + 'plugin_reservation_unavailable', + 'plugin_runtime_stale', + 'token_point_balance_exhausted', + 'web_search_provider_rejected', + 'web_search_rate_limited', + 'web_search_request_invalid', + 'web_search_result_invalid', + 'web_search_submission_unknown', +]); + +type Input = Record; +type MeteredBilling = Extract; + +const NOT_STARTED: MeteredBilling = { mode: 'platform_metered', status: 'not_started' }; +const RECEIPT_UNAVAILABLE: MeteredBilling = { + mode: 'platform_metered', + status: 'receipt_unavailable', +}; + +export interface WebSearchPluginAdapterOptions { + readonly client: WebSearchClient; + readonly marketplace?: MarketplacePackageClientPort; + readonly packageStore?: Pick; + readonly makeloreVersion?: string; + readonly admissionResolver?: MarketplaceHostedAdmissionResolver; +} + +function isRecord(value: unknown): value is Input { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function exactInput(value: Input): boolean { + const keys = Object.keys(value); + return keys.length === 2 && keys.includes('query') && keys.includes('confirmed'); +} + +function validRetryAfter(value: unknown): value is number { + return Number.isSafeInteger(value) && (value as number) >= 0 + && (value as number) <= MAX_RETRY_AFTER_SECONDS; +} + +function safeErrorMessage(code: string, status: number): string { + switch (code) { + case 'confirmation_required': return 'Explicit Token Point Web Search confirmation is required'; + case 'plugin_input_invalid': return 'Web Search input is invalid'; + case 'plugin_release_admission_required': return 'Web Search Release admission is required'; + case 'plugin_release_unavailable': return 'Web Search Release is unavailable'; + case 'plugin_account_changed': return 'Marketplace account changed while resolving Web Search admission'; + case 'plugin_runtime_stale': return 'Web Search worker resources are stale'; + case 'plugin_operation_conflict': return 'Web Search operation conflicts with an existing request'; + case 'token_point_balance_exhausted': return 'Token Point balance is insufficient'; + case 'plugin_provider_unavailable': return 'Web Search Provider is unavailable'; + case 'web_search_provider_rejected': return 'Web Search Provider rejected the request'; + case 'web_search_rate_limited': return 'Web Search is rate limited; confirm a new search later'; + case 'web_search_submission_unknown': return 'Web Search submission status is unknown; do not retry automatically'; + case 'plugin_receipt_unavailable': return 'Web Search billing status could not be synchronized; do not retry automatically'; + case 'plugin_backend_invalid': return 'Web Search service returned an invalid response'; + case 'plugin_backend_response_too_large': return 'Web Search service returned an oversized response'; + case 'plugin_billing_unavailable': return 'Web Search billing is unavailable'; + case 'authentication_required': return 'Works Square sign-in is required'; + default: return status === 401 ? 'Works Square sign-in is required' : 'Web Search service is temporarily unavailable'; + } +} + +function safeErrorCode(code: unknown, fallback = 'plugin_backend_unavailable'): string { + return typeof code === 'string' && SAFE_ERROR_CODES.has(code) ? code : fallback; +} + +function failure( + code: string, + error: string, + status: number, + retryable: boolean, + billing: MeteredBilling = NOT_STARTED, + retryAfterSeconds?: number, +): AdapterInvocationResult { + return { + success: false, + status, + code, + error, + retryable, + ...(validRetryAfter(retryAfterSeconds) ? { retry_after_seconds: retryAfterSeconds } : {}), + payload_schema: 'web-search.v1', + data: null, + billing, + }; +} + +function resultData(result: WebSearchRead): { + answer: string; + sources: readonly { title: string; url: string }[]; + searchQueries: readonly string[]; +} | null { + if (typeof result.answer !== 'string' || result.answer.length === 0 + || result.answer.length > 16_000 || !Array.isArray(result.sources) + || result.sources.length > 20 || !Array.isArray(result.searchQueries) + || result.searchQueries.length > 8) return null; + return { + answer: result.answer, + sources: result.sources, + searchQueries: result.searchQueries, + }; +} + +function project(result: WebSearchRead): AdapterInvocationResult { + if (result.status === 'succeeded' || result.status === 'pending_review') { + const data = resultData(result); + if (!data) { + return failure('plugin_backend_invalid', 'Web Search result is incomplete', 502, false); + } + return { + success: true, + status: result.status === 'pending_review' ? 202 : 200, + code: null, + error: null, + retryable: false, + payload_schema: 'web-search.v1', + data, + billing: result.billing, + }; + } + if (result.status === 'submission_unknown') { + return failure( + safeErrorCode(result.errorCode, 'web_search_submission_unknown'), + 'Web Search submission status is unknown; do not retry automatically', + 503, + false, + result.billing, + ); + } + if (result.status === 'failed') { + const rateLimited = result.errorCode === 'web_search_rate_limited'; + return failure( + safeErrorCode(result.errorCode, 'web_search_request_invalid'), + rateLimited + ? 'Web Search is rate limited; confirm a new search later' + : 'Web Search request was rejected', + rateLimited ? 429 : 422, + false, + result.billing, + rateLimited ? result.retryAfterSeconds : undefined, + ); + } + return failure( + 'plugin_receipt_unavailable', + 'Web Search billing status could not be synchronized; do not retry automatically', + 503, + false, + RECEIPT_UNAVAILABLE, + ); +} + +function clientFailure(error: unknown): AdapterInvocationResult { + if (error instanceof MarketplaceHostedAdmissionError) { + const code = safeErrorCode(error.code); + return failure(code, safeErrorMessage(code, error.status), error.status, error.retryable); + } + if (error instanceof WebSearchClientError) { + const code = safeErrorCode(error.code); + if (code === 'plugin_receipt_unavailable') { + return failure(code, safeErrorMessage(code, 503), 503, false, RECEIPT_UNAVAILABLE); + } + return failure( + code, + safeErrorMessage(code, error.status), + error.status, + error.retryable, + NOT_STARTED, + error.status === 429 ? error.retryAfterSeconds : undefined, + ); + } + return failure( + 'plugin_backend_unavailable', + 'Web Search service is temporarily unavailable', + 503, + true, + ); +} + +export class WebSearchPluginAdapter implements CodingPluginAdapter { + readonly pluginId = PLUGIN_ID; + private readonly admissionResolver: MarketplaceHostedAdmissionResolver; + + constructor(private readonly options: WebSearchPluginAdapterOptions) { + if (options.admissionResolver) { + this.admissionResolver = options.admissionResolver; + } else if (options.marketplace && options.packageStore && options.makeloreVersion) { + this.admissionResolver = new MarketplaceHostedAdmissionResolver({ + marketplace: options.marketplace, + packageStore: options.packageStore, + makeloreVersion: options.makeloreVersion, + }); + } else { + throw new TypeError('Web Search adapter requires Marketplace admission dependencies'); + } + } + + async inspect(_projectPath: string): Promise { + const installed = await this.options.packageStore?.getInstalled(PLUGIN_ID).catch(() => null); + return installed ? { status: 'ready' } : { status: 'unconfigured' }; + } + + async invoke( + context: TrustedCodingCapabilityContext, + tool: CodingPluginToolDefinition, + input: unknown, + ): Promise { + if (tool.name !== TOOL_NAME || tool.capabilityId !== CAPABILITY_ID || tool.operation !== OPERATION) { + return failure('plugin_contract_unsupported', 'Web Search operation is unavailable', 503, true); + } + if (!isRecord(input) || !exactInput(input) + || typeof input.query !== 'string' || !input.query.trim() + || input.query.trim().length > MAX_QUERY_LENGTH) { + return failure('plugin_input_invalid', 'Web Search input is invalid', 422, false); + } + if (input.confirmed !== true) { + return failure( + 'confirmation_required', + 'Explicit Token Point Web Search confirmation is required', + 400, + false, + ); + } + let admission: { releaseId: string; releaseAdmissionId: string }; + try { + admission = await this.admissionResolver.resolve({ + pluginId: PLUGIN_ID, + workerSnapshot: { + requestId: context.requestId, + ...(context.pluginReleaseId === undefined ? {} : { pluginReleaseId: context.pluginReleaseId }), + }, + }); + } catch (error) { + return clientFailure(error); + } + try { + return project(await this.options.client.search({ + releaseId: admission.releaseId, + releaseAdmissionId: admission.releaseAdmissionId, + projectId: context.durableProjectId, + logicalOperationId: context.requestId, + query: input.query.trim(), + confirmed: true, + })); + } catch (error) { + return clientFailure(error); + } + } +} + +export function createWebSearchPluginAdapter( + options: WebSearchPluginAdapterOptions, +): WebSearchPluginAdapter { + return new WebSearchPluginAdapter(options); +} diff --git a/electron/coding-plugins/registry.ts b/electron/coding-plugins/registry.ts index ab28b47..014f89a 100644 --- a/electron/coding-plugins/registry.ts +++ b/electron/coding-plugins/registry.ts @@ -208,6 +208,9 @@ function validBillingReceipt(value: unknown): value is CapabilityBillingReceiptV } if (value.mode === 'included' && value.status === 'included') return keys.size === 2; if (value.mode === 'external_account' && value.status === 'external') return keys.size === 2; + if (value.mode === 'platform_metered' && value.status === 'receipt_unavailable') { + return keys.size === 2; + } if (value.mode !== 'platform_metered' || !validDecimal(value.reserved_points)) return false; const common = new Set(['mode', 'status', 'reserved_points', 'actual_points', 'usage_amount', 'unit']); if (keys.size !== [...keys].filter((key) => common.has(key)).length) return false; diff --git a/electron/services/web-search-client.ts b/electron/services/web-search-client.ts new file mode 100644 index 0000000..36a4981 --- /dev/null +++ b/electron/services/web-search-client.ts @@ -0,0 +1,521 @@ +import { Buffer } from 'node:buffer'; +import type { CapabilityBillingReceiptV1 } from '../../shared/data-service'; +import { WORKS_SQUARE_CONFIG } from '../api/works-config'; +import { proxyAwareFetch } from '../utils/proxy-fetch'; +import { getValidWorksSquareAccessToken } from './works-square-session'; + +export const WEB_SEARCH_ROUTE = '/api/plugins/v1/hosted/web-search/searches'; +export const WEB_SEARCH_RECONCILIATION_WINDOW_MS = 155_000; + +const MAX_JSON_BYTES = 1_048_576; +const MAX_REQUEST_BYTES = 98_304; +const MAX_QUERY_LENGTH = 2_000; +const MAX_ANSWER_LENGTH = 16_000; +const MAX_SOURCE_TITLE_LENGTH = 240; +const MAX_SOURCE_URL_LENGTH = 4_096; +const MAX_SEARCH_QUERY_LENGTH = 500; +const MAX_RETRY_AFTER_SECONDS = 86_400; +const DECIMAL = /^(?:0|[1-9]\d*)\.\d{2}$/u; +const SAFE_ID = /^[\x21-\x7e]{1,128}$/u; +const KNOWN_ERROR_CODES = new Set([ + 'authentication_required', + 'confirmation_required', + 'plugin_backend_unavailable', + 'plugin_billing_unavailable', + 'plugin_execution_unavailable', + 'plugin_operation_conflict', + 'plugin_provider_unavailable', + 'plugin_release_admission_required', + 'plugin_reservation_expired', + 'plugin_reservation_unavailable', + 'plugin_runtime_stale', + 'token_point_balance_exhausted', + 'web_search_provider_rejected', + 'web_search_rate_limited', + 'web_search_request_invalid', + 'web_search_result_invalid', + 'web_search_submission_unknown', +]); + +type FetchImplementation = typeof fetch; +type AccessTokenGetter = typeof getValidWorksSquareAccessToken; +type JsonRecord = Record; +type ServerBillingStatus = + | 'reserved' + | 'dispatched' + | 'settled' + | 'released' + | 'expired' + | 'pending_review' + | 'refunded'; + +export interface WebSearchRequest { + readonly releaseId: string; + readonly releaseAdmissionId: string; + readonly projectId: string; + readonly logicalOperationId: string; + readonly query: string; + readonly confirmed: true; +} + +export type WebSearchInput = WebSearchRequest; + +export interface WebSearchSource { + readonly title: string; + readonly url: string; +} + +export interface WebSearchServerBillingReceipt { + readonly mode: 'platform_metered'; + readonly status: ServerBillingStatus; + readonly reserved_points: string; + readonly actual_points?: string; + readonly usage_amount?: number; + readonly unit: 'search_request'; +} + +export interface WebSearchRead { + readonly executionId: string; + readonly releaseId: string; + readonly logicalOperationId: string; + readonly status: 'reserved' | 'dispatched' | 'succeeded' | 'failed' | 'submission_unknown' | 'pending_review'; + readonly answer: string | null; + readonly sources: readonly WebSearchSource[]; + readonly searchQueries: readonly string[]; + readonly errorCode: string | null; + readonly retryAfterSeconds?: number; + readonly billing: WebSearchServerBillingReceipt; +} + +export type WebSearchResult = WebSearchRead; + +export class WebSearchClientError extends Error { + constructor( + readonly code: string, + readonly status: number, + readonly retryable: boolean, + message: string, + readonly retryAfterSeconds?: number, + ) { + super(message); + this.name = 'WebSearchClientError'; + } +} + +export interface WebSearchClientOptions { + readonly fetchImpl?: FetchImplementation; + readonly getAccessToken?: AccessTokenGetter; + readonly apiBaseUrl?: string; + readonly now?: () => number; + readonly sleep?: (milliseconds: number) => Promise; +} + +class WebSearchTransportError extends Error {} + +function isRecord(value: unknown): value is JsonRecord { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function exactKeys(value: JsonRecord, required: readonly string[], optional: readonly string[] = []): boolean { + const allowed = new Set([...required, ...optional]); + return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) + && Object.keys(value).every((key) => allowed.has(key)); +} + +function boundedText(value: unknown, maximum: number): string | null { + return typeof value === 'string' && value.length > 0 && value.length <= maximum ? value : null; +} + +function boundedIdentifier(value: unknown, maximum: number): string | null { + return typeof value === 'string' && value.length > 0 && value.length <= maximum && SAFE_ID.test(value) + ? value + : null; +} + +function nonNegativeInteger(value: unknown, maximum = Number.MAX_SAFE_INTEGER): number | null { + return Number.isSafeInteger(value) && (value as number) >= 0 && (value as number) <= maximum + ? value as number + : null; +} + +function errorCode(value: unknown): string | null { + return typeof value === 'string' && KNOWN_ERROR_CODES.has(value) ? value : null; +} + +function billing(value: unknown): WebSearchServerBillingReceipt { + if (!isRecord(value) || !exactKeys( + value, + ['mode', 'status', 'reserved_points', 'unit'], + ['actual_points', 'usage_amount'], + )) { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search billing receipt is invalid'); + } + const status = value.status; + const reservedPoints = value.reserved_points; + const actualPoints = value.actual_points === null || value.actual_points === undefined + ? undefined + : boundedText(value.actual_points, 32) ?? undefined; + const usageAmount = value.usage_amount === null || value.usage_amount === undefined + ? undefined + : nonNegativeInteger(value.usage_amount) ?? undefined; + if (value.mode !== 'platform_metered' + || typeof status !== 'string' + || !['reserved', 'dispatched', 'settled', 'released', 'expired', 'pending_review', 'refunded'].includes(status) + || typeof reservedPoints !== 'string' + || !DECIMAL.test(reservedPoints) + || (value.actual_points !== null && value.actual_points !== undefined && actualPoints === undefined) + || (value.usage_amount !== null && value.usage_amount !== undefined && usageAmount === undefined) + || (['settled', 'refunded'].includes(status) && actualPoints === undefined) + || value.unit !== 'search_request') { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search billing receipt is invalid'); + } + return { + mode: 'platform_metered', + status: status as ServerBillingStatus, + reserved_points: reservedPoints, + ...(actualPoints === undefined ? {} : { actual_points: actualPoints }), + ...(usageAmount === undefined ? {} : { usage_amount: usageAmount }), + unit: 'search_request', + }; +} + +function source(value: unknown): WebSearchSource { + if (!isRecord(value) || !exactKeys(value, ['title', 'url'])) { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search source is invalid'); + } + const title = boundedText(value.title, MAX_SOURCE_TITLE_LENGTH); + const url = boundedText(value.url, MAX_SOURCE_URL_LENGTH); + if (!title || !url) { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search source is invalid'); + } + return { title, url }; +} + +function read(value: unknown, request: WebSearchRequest): WebSearchRead { + if (!isRecord(value) || !exactKeys( + value, + [ + 'schema_version', 'plugin_id', 'execution_id', 'release_id', 'logical_operation_id', + 'status', 'billing', + ], + ['answer', 'sources', 'search_queries', 'error_code', 'retry_after_seconds'], + ) || value.schema_version !== 1 || value.plugin_id !== 'makelore.web-search') { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search response is invalid'); + } + const executionId = boundedIdentifier(value.execution_id, 36); + const releaseId = boundedIdentifier(value.release_id, 36); + const logicalOperationId = boundedIdentifier(value.logical_operation_id, 128); + const statuses = ['reserved', 'dispatched', 'succeeded', 'failed', 'submission_unknown', 'pending_review']; + const status = typeof value.status === 'string' && statuses.includes(value.status) ? value.status : null; + const answer = value.answer === undefined || value.answer === null + ? null + : boundedText(value.answer, MAX_ANSWER_LENGTH); + const rawSources = value.sources === undefined ? [] : value.sources; + const rawSearchQueries = value.search_queries === undefined ? [] : value.search_queries; + const retryAfter = value.retry_after_seconds === null || value.retry_after_seconds === undefined + ? undefined + : nonNegativeInteger(value.retry_after_seconds, MAX_RETRY_AFTER_SECONDS) ?? undefined; + const parsedErrorCode = value.error_code === undefined || value.error_code === null + ? null + : errorCode(value.error_code); + if (!executionId || !releaseId || !logicalOperationId || !status + || releaseId !== request.releaseId || logicalOperationId !== request.logicalOperationId + || (value.answer !== undefined && value.answer !== null && answer === null) + || !Array.isArray(rawSources) || rawSources.length > 20 + || !Array.isArray(rawSearchQueries) || rawSearchQueries.length > 8 + || (value.error_code !== undefined && value.error_code !== null && parsedErrorCode === null) + || (value.retry_after_seconds !== null && value.retry_after_seconds !== undefined && retryAfter === undefined) + || (retryAfter !== undefined && (status !== 'failed' || parsedErrorCode !== 'web_search_rate_limited'))) { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search response is invalid'); + } + const sources = rawSources.map(source); + const searchQueries = rawSearchQueries.map((item) => boundedText(item, MAX_SEARCH_QUERY_LENGTH)); + if (searchQueries.some((item): item is null => item === null)) { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search response is invalid'); + } + if (status === 'succeeded' || status === 'pending_review') { + if (!answer || (status === 'pending_review' && parsedErrorCode !== null)) { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search result is incomplete'); + } + } else if (answer !== null || sources.length > 0 || searchQueries.length > 0) { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search response contains an unexpected result'); + } + if ((status === 'succeeded' || status === 'reserved' || status === 'dispatched' || status === 'pending_review') + && parsedErrorCode !== null) { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search response contains an unexpected error'); + } + return { + executionId, + releaseId, + logicalOperationId, + status: status as WebSearchRead['status'], + answer, + sources, + searchQueries: searchQueries as string[], + errorCode: parsedErrorCode, + ...(retryAfter === undefined ? {} : { retryAfterSeconds: retryAfter }), + billing: billing(value.billing), + }; +} + +function retryAfterHeader(response: Response): number | undefined { + const raw = response.headers.get('retry-after'); + if (!raw || !/^\d+$/u.test(raw)) return undefined; + return nonNegativeInteger(Number(raw), MAX_RETRY_AFTER_SECONDS) ?? undefined; +} + +async function readBoundedJson(response: Response): Promise { + const declared = response.headers.get('content-length'); + if (declared && /^\d+$/u.test(declared) && Number(declared) > MAX_JSON_BYTES) { + await response.body?.cancel().catch(() => undefined); + throw new WebSearchClientError('plugin_backend_response_too_large', 502, false, 'Web Search response exceeds its bound'); + } + const bytes = new Uint8Array(await response.arrayBuffer()); + if (bytes.byteLength > MAX_JSON_BYTES) { + throw new WebSearchClientError('plugin_backend_response_too_large', 502, false, 'Web Search response exceeds its bound'); + } + if (bytes.byteLength === 0) return null; + try { + return JSON.parse(Buffer.from(bytes).toString('utf8')) as unknown; + } catch { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search response is invalid'); + } +} + +function requestBody(input: WebSearchRequest): string { + const normalized = { + release_id: input.releaseId, + release_admission_id: input.releaseAdmissionId, + project_id: input.projectId, + logical_operation_id: input.logicalOperationId, + query: input.query.trim(), + confirmed: true, + }; + if (!normalized.query || normalized.query.length > MAX_QUERY_LENGTH + || Buffer.byteLength(normalized.query, 'utf8') > MAX_REQUEST_BYTES) { + throw new WebSearchClientError('plugin_input_invalid', 422, false, 'Web Search query is invalid'); + } + const encoded = JSON.stringify(normalized); + if (Buffer.byteLength(encoded, 'utf8') > MAX_REQUEST_BYTES) { + throw new WebSearchClientError('plugin_input_invalid', 422, false, 'Web Search request is too large'); + } + return encoded; +} + +function validateInput(input: WebSearchRequest): WebSearchRequest { + const value = input as unknown as JsonRecord; + if (!isRecord(value) || !exactKeys(value, [ + 'releaseId', 'releaseAdmissionId', 'projectId', 'logicalOperationId', 'query', 'confirmed', + ])) { + throw new WebSearchClientError('plugin_input_invalid', 422, false, 'Web Search input is invalid'); + } + for (const [key, maximum] of [ + ['releaseId', 36], + ['releaseAdmissionId', 36], + ['projectId', 36], + ['logicalOperationId', 128], + ] as const) { + if (!boundedIdentifier(value[key], maximum)) { + throw new WebSearchClientError('plugin_input_invalid', 422, false, 'Web Search input is invalid'); + } + } + if (typeof value.query !== 'string' || !value.query.trim() || value.query.trim().length > MAX_QUERY_LENGTH + || value.confirmed !== true) { + throw new WebSearchClientError( + value.confirmed === false ? 'confirmation_required' : 'plugin_input_invalid', + value.confirmed === false ? 400 : 422, + false, + value.confirmed === false ? 'Explicit Web Search confirmation is required' : 'Web Search input is invalid', + ); + } + return { + releaseId: value.releaseId as string, + releaseAdmissionId: value.releaseAdmissionId as string, + projectId: value.projectId as string, + logicalOperationId: value.logicalOperationId as string, + query: value.query as string, + confirmed: true, + }; +} + +function domainMessage(code: string, status: number): string { + switch (code) { + case 'confirmation_required': return 'Explicit Web Search confirmation is required'; + case 'web_search_request_invalid': return 'Web Search request is invalid'; + case 'plugin_release_admission_required': return 'Web Search Release admission is required'; + case 'plugin_runtime_stale': return 'Web Search worker resources are stale'; + case 'plugin_operation_conflict': return 'Web Search operation conflicts with an existing request'; + case 'token_point_balance_exhausted': return 'Token Point balance is insufficient'; + case 'plugin_provider_unavailable': return 'Web Search Provider is unavailable'; + case 'web_search_provider_rejected': return 'Web Search Provider rejected the request'; + case 'web_search_rate_limited': return 'Web Search is rate limited; confirm a new search later'; + case 'plugin_billing_unavailable': return 'Web Search billing is unavailable'; + default: + return status === 401 ? 'Works Square sign-in is required' : 'Web Search service is unavailable'; + } +} + +async function domainError(response: Response): Promise { + let payload: unknown = null; + try { + payload = await readBoundedJson(response); + } catch (error) { + if (error instanceof WebSearchClientError) return error; + } + const detail = isRecord(payload) && isRecord(payload.detail) ? payload.detail : null; + const code = errorCode(detail?.error_code) ?? (response.status === 429 ? 'web_search_rate_limited' : 'plugin_backend_unavailable'); + const retryAfter = response.status === 429 ? retryAfterHeader(response) : undefined; + const retryable = response.status >= 500 && response.status !== 401; + return new WebSearchClientError( + code, + response.status, + retryable, + domainMessage(code, response.status), + retryAfter, + ); +} + +function receiptUnavailable(): WebSearchClientError { + return new WebSearchClientError( + 'plugin_receipt_unavailable', + 503, + false, + 'Web Search billing status could not be synchronized; do not retry automatically', + ); +} + +function delayMilliseconds(response: Response): number { + const seconds = retryAfterHeader(response); + return (seconds === undefined ? 1 : seconds) * 1_000; +} + +export class WebSearchClient { + private readonly fetchImpl: FetchImplementation; + private readonly getAccessToken: AccessTokenGetter; + private readonly apiBaseUrl: string; + private readonly now: () => number; + private readonly sleep: (milliseconds: number) => Promise; + + constructor(options: WebSearchClientOptions = {}) { + this.fetchImpl = options.fetchImpl ?? proxyAwareFetch; + this.getAccessToken = options.getAccessToken ?? getValidWorksSquareAccessToken; + this.apiBaseUrl = (options.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/u, ''); + this.now = options.now ?? (() => Date.now()); + this.sleep = options.sleep ?? (async (milliseconds) => { + await new Promise((resolve) => setTimeout(resolve, milliseconds)); + }); + } + + async search(input: WebSearchRequest): Promise { + const valid = validateInput(input); + const encoded = requestBody(valid); + let token: string | null; + try { + token = await this.getAccessToken({ fetchImpl: this.fetchImpl }); + } catch { + token = null; + } + if (!token) throw new WebSearchClientError('authentication_required', 401, false, 'Works Square sign-in is required'); + + let refreshAttempted = false; + const request = async (): Promise => { + const send = async (accessToken: string): Promise => await this.fetchImpl( + `${this.apiBaseUrl}${WEB_SEARCH_ROUTE}`, + { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: encoded, + redirect: 'manual', + signal: AbortSignal.timeout(35_000), + }, + ); + let response: Response; + try { + response = await send(token as string); + } catch { + throw new WebSearchTransportError('Web Search request transport failed'); + } + if (response.status !== 401) return response; + await response.body?.cancel().catch(() => undefined); + if (refreshAttempted) { + throw new WebSearchClientError('authentication_required', 401, false, 'Works Square sign-in is required'); + } + refreshAttempted = true; + let refreshed: string | null; + try { + refreshed = await this.getAccessToken({ fetchImpl: this.fetchImpl, forceRefresh: true }); + } catch { + refreshed = null; + } + if (!refreshed) throw new WebSearchClientError('authentication_required', 401, false, 'Works Square sign-in is required'); + token = refreshed; + try { + response = await send(refreshed); + } catch { + throw new WebSearchTransportError('Web Search request transport failed'); + } + if (response.status === 401) { + await response.body?.cancel().catch(() => undefined); + throw new WebSearchClientError('authentication_required', 401, false, 'Works Square sign-in is required'); + } + return response; + }; + + const deadline = this.now() + WEB_SEARCH_RECONCILIATION_WINDOW_MS; + while (true) { + let response: Response; + try { + response = await request(); + } catch (error) { + if (error instanceof WebSearchClientError) throw error; + if (this.now() >= deadline) throw receiptUnavailable(); + const remaining = deadline - this.now(); + await this.sleep(Math.min(1_000, remaining)); + continue; + } + if (response.status === 429) throw await domainError(response); + if (response.ok) { + let payload: unknown; + try { + payload = await readBoundedJson(response); + } catch (error) { + if (error instanceof WebSearchClientError) throw error; + if (this.now() >= deadline) throw receiptUnavailable(); + const remaining = deadline - this.now(); + await this.sleep(Math.min(1_000, remaining)); + continue; + } + const result = read(payload, valid); + const shouldBeAccepted = result.status === 'reserved' + || result.status === 'dispatched' + || result.status === 'submission_unknown' + || result.status === 'pending_review'; + if ((shouldBeAccepted && response.status !== 202) + || (!shouldBeAccepted && response.status !== 200)) { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search response status is invalid'); + } + if (result.status !== 'reserved' && result.status !== 'dispatched') return result; + if (this.now() >= deadline) throw receiptUnavailable(); + const remaining = deadline - this.now(); + await this.sleep(Math.min(delayMilliseconds(response), remaining)); + continue; + } + throw await domainError(response); + } + } +} + +export function isWebSearchServerBillingReceipt(value: unknown): value is WebSearchServerBillingReceipt { + try { + billing(value); + return true; + } catch { + return false; + } +} + +export type WebSearchCapabilityBilling = Extract; diff --git a/shared/coding-conversation-product-tool-protocol.ts b/shared/coding-conversation-product-tool-protocol.ts index 14c6905..0d4634d 100644 --- a/shared/coding-conversation-product-tool-protocol.ts +++ b/shared/coding-conversation-product-tool-protocol.ts @@ -192,6 +192,11 @@ function capabilityBilling(value: unknown): CapabilityBillingReceiptV1 | null { ? { mode: 'external_account', status: 'external' } : null; } + if (billing.mode === 'platform_metered' && billing.status === 'receipt_unavailable') { + return exactKeys(billing, ['mode', 'status']) + ? { mode: 'platform_metered', status: 'receipt_unavailable' } + : null; + } if (billing.mode !== 'platform_metered' || typeof billing.reserved_points !== 'string' || !CAPABILITY_DECIMAL_PATTERN.test(billing.reserved_points) || billing.reserved_points.length > 32) return null; diff --git a/shared/data-service.ts b/shared/data-service.ts index ae0b059..e521b7e 100644 --- a/shared/data-service.ts +++ b/shared/data-service.ts @@ -100,6 +100,11 @@ export type CapabilityBillingReceiptV1 = actual_points: string; usage_amount?: number; unit?: string; + } + | { + /** Main-only projection used when a possibly-dispatched receipt cannot be read. */ + mode: 'platform_metered'; + status: 'receipt_unavailable'; }; export interface CapabilityResultV1 { diff --git a/tests/unit/coding-conversation-contracts.test.ts b/tests/unit/coding-conversation-contracts.test.ts index 5596b5e..6885255 100644 --- a/tests/unit/coding-conversation-contracts.test.ts +++ b/tests/unit/coding-conversation-contracts.test.ts @@ -106,6 +106,33 @@ describe('Conversation product contracts', () => { })).toBeNull(); }); + it('round-trips the Main-only receipt-unavailable billing state without an amount', () => { + const envelope = { + schema: 'makelore-capability.v1', + plugin_id: 'makelore.web-search', + plugin_version: '1.0.0', + capability_id: 'web-search.search', + operation: 'search', + request_id: 'pi:run-a:resource-a', + success: false, + status: 503, + code: 'plugin_receipt_unavailable', + error: 'Web Search billing status could not be synchronized; do not retry automatically', + retryable: false, + billing: { mode: 'platform_metered', status: 'receipt_unavailable' }, + payload_schema: 'web-search.v1', + data: null, + }; + expect(productToolDetails(envelope)).toMatchObject({ + plugin_id: 'makelore.web-search', + billing: { mode: 'platform_metered', status: 'receipt_unavailable' }, + data: null, + }); + expect(productToolDetails({ ...envelope, billing: { + mode: 'platform_metered', status: 'receipt_unavailable', reserved_points: '0.00', + } })).toBeNull(); + }); + it('accepts schema v1 snapshots and fail-closes unknown schemas until replacement', () => { const snapshot = createProductSnapshot(); expect(isConversationSnapshot(snapshot)).toBe(true); diff --git a/tests/unit/web-search-client.test.ts b/tests/unit/web-search-client.test.ts new file mode 100644 index 0000000..89a1722 --- /dev/null +++ b/tests/unit/web-search-client.test.ts @@ -0,0 +1,272 @@ +// @vitest-environment node + +import { describe, expect, it, vi } from 'vitest'; +import { + WebSearchClient, + WebSearchClientError, +} from '../../electron/services/web-search-client'; + +const RELEASE_ID = '22222222-2222-4222-8222-222222222222'; +const ADMISSION_ID = 'admission-a'; +const PROJECT_ID = '33333333-3333-4333-8333-333333333333'; +const LOGICAL_OPERATION_ID = 'pi:run-a:resource-a'; + +function search(overrides: Record = {}) { + return { + schema_version: 1, + plugin_id: 'makelore.web-search', + execution_id: '11111111-1111-4111-8111-111111111111', + release_id: RELEASE_ID, + logical_operation_id: LOGICAL_OPERATION_ID, + status: 'succeeded', + answer: 'The answer', + sources: [{ title: 'Primary source', url: 'https://example.test/source' }], + search_queries: ['latest release'], + error_code: null, + retry_after_seconds: null, + billing: { + mode: 'platform_metered', + status: 'settled', + reserved_points: '1.00', + actual_points: '1.00', + usage_amount: 1, + unit: 'search_request', + }, + ...overrides, + }; +} + +function jsonResponse(value: unknown, status = 200, headers: Record = {}): Response { + return new Response(JSON.stringify(value), { + status, + headers: { 'content-type': 'application/json', ...headers }, + }); +} + +function input(query = 'latest release') { + return { + releaseId: RELEASE_ID, + releaseAdmissionId: ADMISSION_ID, + projectId: PROJECT_ID, + logicalOperationId: LOGICAL_OPERATION_ID, + query, + confirmed: true as const, + }; +} + +describe('WebSearchClient', () => { + it('posts the closed request, refreshes one 401, and projects the server receipt', async () => { + const fetchImpl = vi.fn() + .mockResolvedValueOnce(new Response(null, { status: 401 })) + .mockResolvedValueOnce(jsonResponse(search(), 200)); + const getAccessToken = vi.fn(async (options?: { forceRefresh?: boolean }) => ( + options?.forceRefresh ? 'fresh-token' : 'stale-token' + )); + const client = new WebSearchClient({ + apiBaseUrl: 'https://works.example/', + fetchImpl, + getAccessToken: getAccessToken as never, + }); + + await expect(client.search(input())).resolves.toEqual({ + executionId: '11111111-1111-4111-8111-111111111111', + releaseId: RELEASE_ID, + logicalOperationId: LOGICAL_OPERATION_ID, + status: 'succeeded', + answer: 'The answer', + sources: [{ title: 'Primary source', url: 'https://example.test/source' }], + searchQueries: ['latest release'], + errorCode: null, + billing: { + mode: 'platform_metered', + status: 'settled', + reserved_points: '1.00', + actual_points: '1.00', + usage_amount: 1, + unit: 'search_request', + }, + }); + expect(fetchImpl).toHaveBeenCalledTimes(2); + expect(fetchImpl.mock.calls[0]?.[0]).toBe( + 'https://works.example/api/plugins/v1/hosted/web-search/searches', + ); + expect(fetchImpl.mock.calls[0]?.[1]).toMatchObject({ + method: 'POST', + redirect: 'manual', + headers: { Authorization: 'Bearer stale-token', 'Content-Type': 'application/json' }, + }); + expect(JSON.parse(String(fetchImpl.mock.calls[0]?.[1]?.body))).toEqual({ + release_id: RELEASE_ID, + release_admission_id: ADMISSION_ID, + project_id: PROJECT_ID, + logical_operation_id: LOGICAL_OPERATION_ID, + query: 'latest release', + confirmed: true, + }); + expect(fetchImpl.mock.calls[1]?.[1]).toMatchObject({ + headers: { Authorization: 'Bearer fresh-token' }, + }); + expect(getAccessToken).toHaveBeenNthCalledWith(2, { fetchImpl, forceRefresh: true }); + }); + + it('rejects oversized input and unknown response fields without a request', async () => { + const fetchImpl = vi.fn().mockResolvedValue(jsonResponse(search({ extra: true }))); + const client = new WebSearchClient({ + fetchImpl, + getAccessToken: vi.fn(async () => 'token') as never, + }); + + await expect(client.search(input('x'.repeat(2_001)))).rejects.toMatchObject({ + code: 'plugin_input_invalid', status: 422, retryable: false, + }); + expect(fetchImpl).not.toHaveBeenCalled(); + await expect(client.search(input())).rejects.toMatchObject({ + code: 'plugin_backend_invalid', status: 502, retryable: false, + }); + }); + + it('matches the frozen server source title and URL bounds exactly', async () => { + const maximumUrl = `https://example.test/${'x'.repeat(4_096 - 'https://example.test/'.length)}`; + await expect(new WebSearchClient({ + fetchImpl: vi.fn().mockResolvedValue(jsonResponse(search({ + sources: [{ title: 't'.repeat(240), url: maximumUrl }], + }))), + getAccessToken: vi.fn(async () => 'token') as never, + }).search(input())).resolves.toMatchObject({ + sources: [{ title: 't'.repeat(240), url: maximumUrl }], + }); + + await expect(new WebSearchClient({ + fetchImpl: vi.fn().mockResolvedValue(jsonResponse(search({ + sources: [{ title: 't'.repeat(241), url: 'https://example.test/source' }], + }))), + getAccessToken: vi.fn(async () => 'token') as never, + }).search(input())).rejects.toMatchObject({ + code: 'plugin_backend_invalid', status: 502, retryable: false, + }); + }); + + it('preserves only a bounded Retry-After for a known rate limit', async () => { + const fetchImpl = vi.fn().mockResolvedValue(jsonResponse( + { detail: { error_code: 'web_search_rate_limited', message: 'try later' } }, + 429, + { 'retry-after': '30' }, + )); + const client = new WebSearchClient({ + fetchImpl, + getAccessToken: vi.fn(async () => 'token') as never, + }); + + await expect(client.search(input())).rejects.toMatchObject({ + code: 'web_search_rate_limited', status: 429, retryable: false, retryAfterSeconds: 30, + }); + }); + + it('projects a server-side rate-limit result and does not trust Retry-After on other statuses', async () => { + const rateLimited = vi.fn().mockResolvedValue(jsonResponse(search({ + status: 'failed', + answer: null, + sources: [], + search_queries: [], + error_code: 'web_search_rate_limited', + retry_after_seconds: 30, + billing: { mode: 'platform_metered', status: 'released', reserved_points: '1.00', unit: 'search_request' }, + }))); + const client = new WebSearchClient({ + fetchImpl: rateLimited, + getAccessToken: vi.fn(async () => 'token') as never, + }); + await expect(client.search(input())).resolves.toMatchObject({ + status: 'failed', errorCode: 'web_search_rate_limited', retryAfterSeconds: 30, + billing: { status: 'released' }, + }); + + const unavailable = vi.fn().mockResolvedValue(jsonResponse( + { detail: { error_code: 'plugin_provider_unavailable', message: 'private provider text' } }, + 503, + { 'retry-after': '30' }, + )); + const unavailableClient = new WebSearchClient({ + fetchImpl: unavailable, + getAccessToken: vi.fn(async () => 'token') as never, + }); + await expect(unavailableClient.search(input())).rejects.toMatchObject({ + code: 'plugin_provider_unavailable', status: 503, retryable: true, + retryAfterSeconds: undefined, + }); + }); + + it('fails closed on a result-bearing status with a mismatched HTTP status', async () => { + const client = new WebSearchClient({ + fetchImpl: vi.fn().mockResolvedValue(jsonResponse(search(), 202)), + getAccessToken: vi.fn(async () => 'token') as never, + }); + + await expect(client.search(input())).rejects.toMatchObject({ + code: 'plugin_backend_invalid', status: 502, retryable: false, + }); + }); + + it('accepts omitted nullable/default response fields while keeping the object closed', async () => { + const payload = search(); + delete payload.answer; + delete payload.sources; + delete payload.search_queries; + delete payload.error_code; + delete payload.retry_after_seconds; + payload.status = 'failed'; + payload.billing = { + mode: 'platform_metered', status: 'released', reserved_points: '1.00', unit: 'search_request', + }; + await expect(new WebSearchClient({ + fetchImpl: vi.fn().mockResolvedValue(jsonResponse(payload)), + getAccessToken: vi.fn(async () => 'token') as never, + }).search(input())).resolves.toMatchObject({ + status: 'failed', answer: null, sources: [], searchQueries: [], errorCode: null, + }); + }); + + it('reconciles the same operation after in-progress and transport responses', async () => { + let clock = 0; + const fetchImpl = vi.fn() + .mockResolvedValueOnce(jsonResponse(search({ + status: 'dispatched', + answer: null, + sources: [], + search_queries: [], + billing: { + mode: 'platform_metered', status: 'dispatched', reserved_points: '1.00', + usage_amount: 1, unit: 'search_request', + }, + }), 202, { 'retry-after': '1' })) + .mockRejectedValueOnce(new TypeError('connection lost')) + .mockResolvedValueOnce(jsonResponse(search())); + const client = new WebSearchClient({ + fetchImpl, + getAccessToken: vi.fn(async () => 'token') as never, + now: () => clock, + sleep: async (milliseconds) => { clock += milliseconds; }, + }); + + await expect(client.search(input())).resolves.toMatchObject({ status: 'succeeded' }); + expect(fetchImpl).toHaveBeenCalledTimes(3); + expect(String(fetchImpl.mock.calls[0]?.[1]?.body)).toBe(String(fetchImpl.mock.calls[1]?.[1]?.body)); + expect(String(fetchImpl.mock.calls[1]?.[1]?.body)).toBe(String(fetchImpl.mock.calls[2]?.[1]?.body)); + }); + + it('returns receipt_unavailable when the bounded reconciliation window is exhausted', async () => { + let clock = 0; + const fetchImpl = vi.fn().mockRejectedValue(new TypeError('offline')); + const client = new WebSearchClient({ + fetchImpl, + getAccessToken: vi.fn(async () => 'token') as never, + now: () => clock, + sleep: async () => { clock = 155_000; }, + }); + + await expect(client.search(input())).rejects.toMatchObject({ + code: 'plugin_receipt_unavailable', status: 503, retryable: false, + }); + expect(fetchImpl).toHaveBeenCalledTimes(2); + }); +}); diff --git a/tests/unit/web-search-plugin-adapter.test.ts b/tests/unit/web-search-plugin-adapter.test.ts new file mode 100644 index 0000000..4172a4e --- /dev/null +++ b/tests/unit/web-search-plugin-adapter.test.ts @@ -0,0 +1,306 @@ +// @vitest-environment node + +import { describe, expect, it, vi } from 'vitest'; +import type { CodingPluginToolDefinition } from '../../shared/coding-plugins'; +import { + WebSearchPluginAdapter, +} from '../../electron/coding-plugins/adapters/web-search'; +import { + MarketplaceHostedAdmissionError, +} from '../../electron/coding-plugins/hosted-admission'; +import type { MarketplaceHostedAdmissionResolver } from '../../electron/coding-plugins/hosted-admission'; +import type { TrustedCodingCapabilityContext } from '../../electron/coding-plugins/registry'; +import { + WebSearchClientError, + type WebSearchClient, + type WebSearchRead, +} from '../../electron/services/web-search-client'; + +const RELEASE_ID = '22222222-2222-4222-8222-222222222222'; +const PROJECT_ID = '33333333-3333-4333-8333-333333333333'; +const REQUEST_ID = 'pi:run-a:resource-a'; + +const TOOL: CodingPluginToolDefinition = { + name: 'makelore_web_search', + label: 'Search the web', + description: 'Search the web', + capabilityId: 'web-search.search', + operation: 'search', + roles: ['parent'], + mutation: 'read', + projectWriteLease: false, + permissions: ['hosted.web-search.search'], + inputSchema: { + type: 'object', + additionalProperties: false, + required: ['query', 'confirmed'], + properties: { + query: { type: 'string', minLength: 1, maxLength: 2_000 }, + confirmed: { type: 'boolean' }, + }, + }, + executionMode: 'synchronous', +}; + +const BILLING = { + mode: 'platform_metered' as const, + status: 'settled' as const, + reserved_points: '1.00', + actual_points: '1.00', + usage_amount: 1, + unit: 'search_request' as const, +}; + +function response(overrides: Partial = {}): WebSearchRead { + return { + executionId: '11111111-1111-4111-8111-111111111111', + releaseId: RELEASE_ID, + logicalOperationId: REQUEST_ID, + status: 'succeeded', + answer: 'The answer', + sources: [{ title: 'Primary source', url: 'https://example.test/source' }], + searchQueries: ['latest release'], + errorCode: null, + billing: BILLING, + ...overrides, + }; +} + +function toolContext(): TrustedCodingCapabilityContext { + return { + conversationId: 'conversation-a', + runId: 'run-a', + resourceId: 'resource-a', + requestId: REQUEST_ID, + localProjectId: 'local-project', + projectPath: 'C:/project', + durableProjectId: PROJECT_ID, + workerRole: 'parent', + effectiveSkillIds: ['makelore-web-search'], + pluginReleaseId: RELEASE_ID, + }; +} + +function fixture(search: ReturnType = vi.fn(async () => response())) { + const resolve = vi.fn(async () => ({ + releaseId: RELEASE_ID, + releaseAdmissionId: 'admission-a', + })); + const admissionResolver = { resolve } as unknown as MarketplaceHostedAdmissionResolver; + const client = { search } as unknown as WebSearchClient; + const adapter = new WebSearchPluginAdapter({ client, admissionResolver }); + return { adapter, client, resolve }; +} + +describe('WebSearchPluginAdapter', () => { + it('uses trusted project/request/admission context and projects a successful result', async () => { + const search = vi.fn(async () => response()); + const { adapter, client, resolve } = fixture(search); + + const result = await adapter.invoke(toolContext(), TOOL, { + query: 'latest release', + confirmed: true, + }); + + expect(resolve).toHaveBeenCalledTimes(1); + expect(search).toHaveBeenCalledWith({ + releaseId: RELEASE_ID, + releaseAdmissionId: 'admission-a', + projectId: PROJECT_ID, + logicalOperationId: REQUEST_ID, + query: 'latest release', + confirmed: true, + }); + expect(result).toMatchObject({ + success: true, + status: 200, + payload_schema: 'web-search.v1', + billing: BILLING, + data: { + answer: 'The answer', + sources: [{ title: 'Primary source', url: 'https://example.test/source' }], + searchQueries: ['latest release'], + }, + }); + expect(JSON.stringify(result)).not.toContain('provider'); + expect(client).toBeDefined(); + }); + + it('keeps a complete pending-review result usable without claiming settlement', async () => { + const { adapter } = fixture(vi.fn(async () => response({ + status: 'pending_review', + billing: { mode: 'platform_metered', status: 'pending_review', reserved_points: '1.00', usage_amount: 1, unit: 'search_request' }, + }))); + + await expect(adapter.invoke(toolContext(), TOOL, { + query: 'latest release', confirmed: true, + })).resolves.toMatchObject({ + success: true, + status: 202, + billing: { mode: 'platform_metered', status: 'pending_review' }, + data: { answer: 'The answer' }, + }); + }); + + it('does not resolve admission or call the client before confirmation', async () => { + const search = vi.fn(async () => response()); + const { adapter, client, resolve } = fixture(search); + + await expect(adapter.invoke(toolContext(), TOOL, { + query: 'latest release', confirmed: false, + })).resolves.toMatchObject({ + success: false, + status: 400, + code: 'confirmation_required', + billing: { mode: 'platform_metered', status: 'not_started' }, + data: null, + }); + expect(resolve).not.toHaveBeenCalled(); + expect(search).not.toHaveBeenCalled(); + expect(client).toBeDefined(); + }); + + it('preserves admission failures as not-started business failures', async () => { + const admissionResolver = { + resolve: vi.fn(async () => { + throw new MarketplaceHostedAdmissionError('plugin_runtime_stale', 409, false, 'stale'); + }), + } as unknown as MarketplaceHostedAdmissionResolver; + const search = vi.fn(async () => response()); + const adapter = new WebSearchPluginAdapter({ + client: { search } as unknown as WebSearchClient, + admissionResolver, + }); + + await expect(adapter.invoke(toolContext(), TOOL, { + query: 'latest release', confirmed: true, + })).resolves.toMatchObject({ + success: false, + status: 409, + code: 'plugin_runtime_stale', + billing: { mode: 'platform_metered', status: 'not_started' }, + }); + expect(search).not.toHaveBeenCalled(); + }); + + it.each([ + ['plugin_release_unavailable', 409], + ['plugin_account_changed', 409], + ] as const)('preserves the typed %s admission failure', async (code, status) => { + const admissionResolver = { + resolve: vi.fn(async () => { + throw new MarketplaceHostedAdmissionError(code, status, false, 'private admission detail'); + }), + } as unknown as MarketplaceHostedAdmissionResolver; + const search = vi.fn(async () => response()); + const adapter = new WebSearchPluginAdapter({ + client: { search } as unknown as WebSearchClient, + admissionResolver, + }); + + await expect(adapter.invoke(toolContext(), TOOL, { + query: 'latest release', confirmed: true, + })).resolves.toMatchObject({ + success: false, + status, + code, + billing: { mode: 'platform_metered', status: 'not_started' }, + }); + expect(search).not.toHaveBeenCalled(); + }); + + it('maps result-less submission unknown to a non-retryable null payload', async () => { + const { adapter } = fixture(vi.fn(async () => response({ + status: 'submission_unknown', + answer: null, + sources: [], + searchQueries: [], + errorCode: 'web_search_submission_unknown', + billing: { mode: 'platform_metered', status: 'pending_review', reserved_points: '1.00', usage_amount: 1, unit: 'search_request' }, + }))); + + await expect(adapter.invoke(toolContext(), TOOL, { + query: 'latest release', confirmed: true, + })).resolves.toMatchObject({ + success: false, + status: 503, + code: 'web_search_submission_unknown', + retryable: false, + billing: { mode: 'platform_metered', status: 'pending_review' }, + data: null, + }); + }); + + it('maps rate limiting to 429 while preserving only the bounded Retry-After', async () => { + const { adapter } = fixture(vi.fn(async () => response({ + status: 'failed', + answer: null, + sources: [], + searchQueries: [], + errorCode: 'web_search_rate_limited', + retryAfterSeconds: 30, + billing: { mode: 'platform_metered', status: 'released', reserved_points: '1.00', unit: 'search_request' }, + }))); + + await expect(adapter.invoke(toolContext(), TOOL, { + query: 'latest release', confirmed: true, + })).resolves.toMatchObject({ + success: false, + status: 429, + code: 'web_search_rate_limited', + retryable: false, + retry_after_seconds: 30, + billing: { mode: 'platform_metered', status: 'released' }, + }); + }); + + it('projects receipt-unavailable without inventing an amount', async () => { + const search = vi.fn(async () => { + throw new WebSearchClientError( + 'plugin_receipt_unavailable', + 503, + false, + 'Web Search billing status could not be synchronized; do not retry automatically', + ); + }); + const { adapter } = fixture(search); + + await expect(adapter.invoke(toolContext(), TOOL, { + query: 'latest release', confirmed: true, + })).resolves.toMatchObject({ + success: false, + status: 503, + code: 'plugin_receipt_unavailable', + billing: { mode: 'platform_metered', status: 'receipt_unavailable' }, + }); + }); + + it('does not project provider-shaped error text or client-supplied authority fields', async () => { + const search = vi.fn(async () => { + throw new WebSearchClientError( + 'plugin_provider_unavailable', + 503, + true, + 'https://provider.example model=secret-model key=secret-key', + ); + }); + const { adapter } = fixture(search); + const result = await adapter.invoke(toolContext(), TOOL, { + query: 'latest release', confirmed: true, + projectId: 'attacker-project', logicalOperationId: 'attacker-operation', + }); + expect(result).toMatchObject({ success: false, code: 'plugin_input_invalid', status: 422 }); + expect(JSON.stringify(result)).not.toContain('provider.example'); + expect(JSON.stringify(result)).not.toContain('secret-key'); + + const normal = await adapter.invoke(toolContext(), TOOL, { + query: 'latest release', confirmed: true, + }); + expect(normal).toMatchObject({ + success: false, + code: 'plugin_provider_unavailable', + status: 503, + error: 'Web Search Provider is unavailable', + }); + }); +}); From 68cb2e73beb17d5041198d020aaa7b2884124950 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 02:05:09 +0800 Subject: [PATCH 06/47] docs(web-search): record MLW-02 integration --- ...-web-search-client-integration-7d2f5b94.md | 10 +- ...260901-web-search-mlw02-client-c8e4a2d1.md | 123 ------------------ 2 files changed, 9 insertions(+), 124 deletions(-) delete mode 100644 .project-docs/30-worklog/tasks/20260901-web-search-mlw02-client-c8e4a2d1.md diff --git a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md index dcc0a04..741abcf 100644 --- a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md +++ b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md @@ -33,6 +33,11 @@ - Product commit: `1edd75e2465ae8bfbba87d8e077365daee144920`; source and product trees are exact-equal at `b17a16ff1c46706980bd162af3678176b3a93e70` before the repository-local Integration Documentation Gate removes the duplicate foreign task record. - Delivered the provider-neutral `MarketplaceHostedAdmissionResolver` and an admission-only Game Resource refactor. The helper resolves exact installed release/admission identity from the trusted frozen worker snapshot, Package Store, selected channel, Marketplace resolve, current account, and MakeLore version; it owns no Provider, payload, billing, route, Web Search client, composition, Renderer, or Pi behavior. - The clean source branch retains its complete task record. This coordinator checkpoint removes only the duplicate cherry-picked copy and records integration evidence here. +- MLW-02 source task `20260901-web-search-mlw02-client-c8e4a2d1` was integrated from exact frontier `f3874e90706692094f0f22fca465923143e23c7e`. + - Source commit: `94f98e03b9e4e59eb2de07aa9d0e68ec0c74de8e` (sole parent `f3874e90706692094f0f22fca465923143e23c7e`). + - Product commit: `fc68cf295131d8f73556bef59fdeae61239a649e`; source and product trees are exact-equal at `5c14e92224e8441d18a983dd7099cd6d76f83e85` before this Integration Documentation Gate removes the duplicate foreign task record. + - Delivered the Main-only closed Web Search client, bounded same-operation reconciliation, code-owned adapter, and the minimal generic billing/conversation/Registry support for `not_started` and Main-only `receipt_unavailable`. The adapter uses the MLW-01 admission helper and trusted project/request identity; no composition, Renderer, Pi, Package Store/effective resolver, Provider authority, or server path changed. + - Final source review aligned source title/URL bounds with the frozen Server DTO and preserved both MLW-01 typed admission failures. The clean source branch retains its complete task record; integration evidence is consolidated here. ## Verification @@ -41,10 +46,13 @@ - MLW-01 focused admission/Game Resource suites passed `12/12`; adjacent six-file coding-plugin/Game Resource regression passed `74/74`. - MLW-01 full unit suite passed `212 files / 1728 tests`, with two staged-runtime skips; the pressure suite passed `1/1`. Typecheck passed. Full lint passed with zero errors and the five pre-existing Home/Makelore warnings; owned-file lint/compile, diff, project-docs, and document-drift gates passed. - The coordinator's optional duplicate focused run stopped before collection because this isolated worktree has no executable `vitest`; no test or product failure occurred and no dependency/source file changed. The Integration Gate instead adopts the clean source's exact-tree test evidence above rather than performing an unrelated dependency installation. +- MLW-02 final focused suite passed `3 files / 35 tests`; adjacent hosted admission, Game Resource, Registry, conversation, Marketplace-client, and timeline regression passed `11 files / 114 tests`. +- MLW-02 full unit suite passed `214 files / 1748 tests`, with two staged-runtime skips; pressure passed `1/1`. Typecheck, owned lint, full lint (zero errors and the same five existing warnings), and Vite Renderer/Main/Preload/utility builds passed. +- MLW-02 diff, project-docs, document-drift, sole-parent, clean-worktree, and `READY_FOR_INTEGRATION` gates passed. No live Provider call or production activation occurred. ## Follow-ups -- Start MLW-02 from the clean post-MLW-01 coordinator frontier. Keep MLW-03 composition, Renderer, Pi worker, and package-proof ownership closed until MLW-02 is integrated. +- Start MLW-03 only from the clean post-MLW-02 coordinator frontier. MLW-03 owns composition, the generic timeline billing projection, dynamic tool/package proof, README, and necessary focused/E2E evidence; it must not reopen MLW-01/02 authority. - Keep the live OpenAI XWS-01 group and production activation closed pending explicit external inputs and user authorization. ## Promotion Candidates diff --git a/.project-docs/30-worklog/tasks/20260901-web-search-mlw02-client-c8e4a2d1.md b/.project-docs/30-worklog/tasks/20260901-web-search-mlw02-client-c8e4a2d1.md deleted file mode 100644 index 13750c0..0000000 --- a/.project-docs/30-worklog/tasks/20260901-web-search-mlw02-client-c8e4a2d1.md +++ /dev/null @@ -1,123 +0,0 @@ -# Task: Implement MLW-02 Web Search Main client and adapter - -## Identity - -- Task ID: 20260901-web-search-mlw02-client-c8e4a2d1 -- Mode: Feature -- Branch: codex/20260901-web-search-mlw02-client-c8e4a2d1-web-search-mlw02-client -- Worktree: D:\Datas\OthersProjects\makelore-web-search-mlw02-client-c8e4a2d1 -- Base commit: f3874e90706692094f0f22fca465923143e23c7e -- Owner: web-search-mlw02-implementer -- Status: Ready for Integration - -## Scope - -- Implement MLW-02 from exact client coordinator frontier - `f3874e90706692094f0f22fca465923143e23c7e` and frozen server DTO/context - frontier `a49c696ebc4213e3d62ece780961efbe17576f8e`. -- Add the Main-only `electron/services/web-search-client.ts` with closed input, - fixed route, Works authentication/one-refresh behavior, bounded DTO parsing, - same-operation reconciliation, and honest billing receipt projection. -- Add `electron/coding-plugins/adapters/web-search.ts` as the code-owned hosted - adapter. It validates closed input and confirmation, invokes the shared MLW-01 - admission resolver, supplies trusted project/request identity, and maps server - outcomes without exposing Provider details or calculating Token Points. -- Make only the minimal shared contract changes required by the new - `platform_metered/not_started` and Main-only `receipt_unavailable` billing - statuses: `shared/data-service.ts`, - `shared/coding-conversation-product-tool-protocol.ts`, and the Registry billing - validator in `electron/coding-plugins/registry.ts`. -- Add focused Web Search, billing/parser, adapter, and conversation contract - tests within the ticket's owned test paths. -- Do not modify Renderer, composition, Pi worker/resource, Package Store, - effective resolver, server, MLW-03 paths, or any user/coordinator worktree. - -## Intent And Constraints - -- Project Context Loaded: client `AGENTS.md`, the complete - `maintain-project-docs` skill and document-system reference, TDD and - implement-spec instructions, required project-memory entry points, peer task - records, and Web Search design/implementation spec §12 were read before - planning. -- Concurrent Task Gate: Passed. `task_context.py start` created this isolated - feature task from the exact coordinator frontier; status identity matches the - task ID, owner, mode, branch, absolute worktree, and base. The user root and - client coordinator worktrees remain untouched. -- Planning Gate: Passed. The older broad Web Search coordinator/integration and - completed MLW-01 records were inspected read-only. MLW-01 is the only direct - dependency and its exact source commit is integrated at this task's base; - this task owns different files. No unresolved semantic conflict remains. -- Frozen server context is the typed Web Search route/receipt contract at - `a49c696ebc4213e3d62ece780961efbe17576f8e`; client code must not invent - Provider/model/key/URL/payload authority or a generic invoke surface. -- Main retains Works credentials, route, logical IDs, reconciliation and raw - server receipt projection. The client never calculates points, exposes query - beyond the typed request boundary, or forwards Provider response/header/raw - error data to Pi/Renderer. -- Transport uncertainty and `reserved`/`dispatched` responses reuse the same - logical operation and exact request body for one bounded window of at most - 155 seconds. A 429 is non-retryable and preserves only bounded - `Retry-After`; a new user confirmation must create a new logical operation. -- TDD vertical slices: closed parser/fixed request, auth refresh, reconciliation - and receipt states, adapter mappings, then shared billing/protocol validator - changes and regression gates. One source commit with this task record, - clean worktree, and `READY_FOR_INTEGRATION` handoff are required. No push, - PR, deploy, publish, or real OpenAI call. - -## Outcome - -- Added the Main-only `WebSearchClient` with the exact typed hosted route, - closed request/response parsing, one Works-token refresh, whole-response - bounds, bounded same-operation reconciliation, and honest projection of the - server-owned receipt. Transport ambiguity and in-progress receipts reuse the - same logical operation/body for at most 155 seconds; an exhausted window - returns only `plugin_receipt_unavailable` and never invents an amount. -- Added the code-owned Web Search Plugin adapter. It validates the exact - `query`/`confirmed` input, resolves the frozen Release admission through the - MLW-01 helper, and supplies only trusted durable project/request identity to - Main. Complete `succeeded` and result-bearing `pending_review` responses are - usable; result-less `submission_unknown`, 429, admission/auth/input failures, - and receipt uncertainty retain their bounded business code and billing state. -- Extended the shared billing union, Registry validator, and conversation - parser only for Main-owned `platform_metered/receipt_unavailable` while - retaining the existing `platform_metered/not_started` state for every - pre-transaction failure. -- Final review corrected two frozen-contract mismatches: source title/URL - bounds now exactly match Server `240/4096`, and the adapter preserves - MLW-01 `plugin_release_unavailable` / `plugin_account_changed` admission - failures instead of collapsing them into a generic backend error. -- No composition, Renderer, Pi worker/resource, Package Store, effective - resolver, Server, Provider/model/key/URL authority, generic invoke, deploy, - publish, or live Provider call was added. - -## Verification - -- Concurrent/Planning Gate identity remained exact after recovery: same task, - owner, worktree, branch, and base `f3874e90706692094f0f22fca465923143e23c7e`; - the coordinator and user root worktrees were not modified. -- The original delegated turn was interrupted after preserving the complete - owned test/product diff but before returning its red/green ledger. A second - delegated recovery also stalled without changing files. The parent - coordinator then became the sole writer in this same owned worktree; no - task release, rebuild, reset, stash, clean, or concurrent edit occurred. -- First observable recovered focused run: 3 files / 32 tests passed. Contract - review regressions for exact source bounds and both admission failure codes - brought the final focused result to 3 files / 35 tests passed. -- Adjacent hosted admission, Game Resource, Registry, conversation, - Marketplace-client and timeline regression: 11 files / 114 tests passed. -- `pnpm run typecheck` passed. Owned-file ESLint passed. Full - `pnpm run lint:check` passed with zero errors and the unchanged five warnings - in Home/Makelore outside this task. -- Full unit suite passed: 214 files / 1748 tests, 2 staged-runtime skips; the - pressure suite passed 1/1. `pnpm run build:vite` passed Renderer, Main, - Preload, and utility builds with only existing chunk/dynamic-import notices. -- Final diff, project-docs, document-drift, task-context completion, sole-parent, - and clean-worktree facts are recorded by the source commit handoff. - -## Follow-ups - -- None recorded. - -## Promotion Candidates - -- None recorded. From 60e6a8eeb35431b0f1fb569af4d1502683f16317 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 02:39:16 +0800 Subject: [PATCH 07/47] feat(web-search): wire hosted plugin composition --- ...1-web-search-mlw03-composition-d9f5b3e2.md | 64 ++++++++ README.md | 2 + electron/api/coding-composition.ts | 12 +- scripts/lib/pi-product-artifact.mjs | 15 +- src/pages/Chat/CodingConversationTimeline.tsx | 18 +++ tests/unit/coding-capability-registry.test.ts | 142 ++++++++++++++++++ .../coding-conversation-timeline.test.tsx | 54 +++++++ tests/unit/coding-plugin-composition.test.ts | 48 ++++++ tests/unit/pi-product-artifact.test.ts | 12 +- 9 files changed, 361 insertions(+), 6 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260901-web-search-mlw03-composition-d9f5b3e2.md diff --git a/.project-docs/30-worklog/tasks/20260901-web-search-mlw03-composition-d9f5b3e2.md b/.project-docs/30-worklog/tasks/20260901-web-search-mlw03-composition-d9f5b3e2.md new file mode 100644 index 0000000..7bc0264 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-web-search-mlw03-composition-d9f5b3e2.md @@ -0,0 +1,64 @@ +# Task: Implement MLW-03 composition and packaged proof + +## Identity + +- Task ID: 20260901-web-search-mlw03-composition-d9f5b3e2 +- Mode: Feature +- Branch: codex/20260901-web-search-mlw03-composition-d9f5b3e2-web-search-mlw03-composition +- Worktree: D:\Datas\OthersProjects\makelore-web-search-mlw03-composition-d9f5b3e2 +- Base commit: 68cb2e73beb17d5041198d020aaa7b2884124950 +- Owner: web-search-mlw03-implementer +- Status: Ready for Integration + +## Scope + +- Wire the code-owned Web Search adapter into `electron/api/coding-composition.ts` so the existing effective-plugin snapshot is the only parent-worker materialization and invocation authority. +- Add the Web Search parent/child/current-authority scenario to `tests/unit/coding-capability-registry.test.ts` and the composition registration proof to `tests/unit/coding-plugin-composition.test.ts`. +- Add only a generic closed billing-state projection to `src/pages/Chat/CodingConversationTimeline.tsx` with focused coverage in `tests/unit/coding-conversation-timeline.test.tsx`; do not parse `web-search.v1` or add Web Search-specific Renderer state. +- Extend the packaged main-reachable proof in `scripts/lib/pi-product-artifact.mjs` and `tests/unit/pi-product-artifact.test.ts` for the fixed hosted Web Search route, receipt-unavailable parser, and absence of provider authority. The verifier script is a necessary adjacent proof file required by the MLW-03 acceptance criteria and has no competing ticket owner. +- Update `README.md` to describe the shipped native hosted Web Search capability and its production activation holds. +- Do not modify MLW-01 admission ownership, MLW-02 client/adapter/envelope ownership, project selection services, Marketplace pages, Provider/Server code, or Release B surfaces. + +## Intent And Constraints + +- Exact source base is `68cb2e73beb17d5041198d020aaa7b2884124950`; server DTO authority is frozen at `a49c696ebc4213e3d62ece780961efbe17576f8e`. +- Concurrent Task Gate and Planning Gate passed after verifying task identity, branch/worktree/base, clean product state, coordinator/MLW-01/MLW-02 peers, canonical implementation spec/design, and project memory. The older broad Web Search client task remains at its original base with no product diff and does not conflict with this exact-base ticket. +- Use test-first natural boundaries. Parent workers may receive one dynamically installed Web Search Skill/tool; child workers receive none. Disabled, uninstalled, stale, account-switched, project-switched, or logged-out state must affect only the next frozen worker snapshot. +- Keep provider key, model choice, provider URL, signed URL, admission authority, and account authority out of Renderer/package artifacts. No static tool allowlist, generic invoke route, `pi-web-search`, or third-party Pi extension. +- Real PostgreSQL, live paid OpenAI, production signing key, production OpenAI key/model/price/privacy copy, push, PR, deployment, and publication remain out of scope or explicitly held. + +## Plan + +1. Add focused failing tests for composition registration, dynamic parent/child/current invocation, generic billing projection, and packaged reachability/provider-authority exclusions. +2. Implement the smallest composition/timeline/artifact/README changes that turn those tests green. +3. Run focused and adjacent regressions, typecheck, lint, Vite/Electron/package proof as applicable, full unit pressure, documentation gates, and produce one clean source commit. + +## Outcome + +- Registered the code-owned Web Search adapter in both production capability execution and project-plugin inspection/deactivation paths. Marketplace-installed schema-v2 definitions remain the only Skill/tool source; no Web Search definition or tool allowlist was added to composition. +- Added an end-to-end registry proof for the frozen Web Search parent snapshot: one parent Skill/tool, no child inheritance, exact Main request/admission/envelope mapping, and refusal of a subsequent old-worker action after the project is disabled. +- Added a generic closed billing-state renderer for all `makelore-capability.v1` tools. `receipt_unavailable` is displayed as `收费状态未同步,请勿重复发起`; the timeline does not inspect `web-search.v1` answer/source payloads or add Web Search-specific state. +- Extended the package-main-reachable artifact proof to require the Web Search plugin ID, fixed Works route, and receipt-unavailable parser while rejecting embedded OpenAI key/origin authority. The built app remains fail-closed for the absent official Marketplace signing key. +- Updated README current-product documentation for native hosted Web Search, dynamic Marketplace Skill materialization, explicit Token Point confirmation, and production activation holds. + +## Verification + +- TDD boundary: after dependency materialization, the first focused run failed exactly four assertions (missing composition registration, generic receipt projection, Web Search artifact result field, and provider-authority rejection). The implementation turned the same boundary green: 4 files / 40 tests. +- Focused plus adjacent Web Search/admission/composition/lifecycle/Pi/timeline/artifact suite: 10 files / 82 tests passed. +- Full unit suite: 214 files / 1,751 tests passed / 2 staged-runtime tests skipped; isolated pressure suite: 1/1 passed. +- `pnpm run typecheck`: passed. Scoped ESLint: passed. Full `pnpm run lint:check`: 0 errors and the unchanged 5 warnings in Home/Makelore. +- `pnpm run build:vite`: passed for Renderer, Main, Preload, and utility worker. Electron Windows Vitest: 2 files / 6 tests passed. Marketplace, Project Plugins, and Skill configuration Playwright targets: 4/4 passed. +- `pnpm run package:win`: passed and produced the unpacked x64 app plus NSIS installer. The first installer was 208,252,283 bytes with SHA-256 `E6ABFA36C219F71FDBCAE811BA01DAEFFFAA3625FCCA17FE40C12CE507D0808E`; a clean-source-HEAD package/verification is required immediately after the sole commit before handoff. +- `pnpm run verify:artifact:pi`: passed against the real built `app.asar`; Marketplace proof includes the Web Search ID, fixed route, receipt parser, absent Web Search provider authority, and official-key-absent fail-closed trust. Existing Pi runtime result remains the inherited `partial-pass` for deferred cross-platform/real-provider evidence. +- `pnpm run verify:publish-runtime`: passed with npm 11.6.2. The pre-commit `verify:artifact:win` correctly refused to claim a clean artifact while the source diff was uncommitted; it is not a product failure and must be rerun on the source commit. +- `git diff --check`: passed. + +## Follow-ups + +- Real PostgreSQL lifecycle remains an external HOLD because no local service, Docker runtime, or `TEST_POSTGRESQL_DATABASE_URL` is available; SQLite is not accepted as a substitute. +- Live OpenAI acceptance remains HOLD because paid calls were not authorized. Production activation also remains HOLD for the official Ed25519 public key, `WEB_SEARCH_OPENAI_API_KEY`, approved model, price, and privacy copy. +- XWS-01 and the fixed-range Standards/Spec review remain coordinator-owned after MLW-03 integration. + +## Promotion Candidates + +- None recorded. diff --git a/README.md b/README.md index c30e2fd..80fbc77 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Makelore 是一个面向软件、视觉创作、互动学习与智能机器人 - AI 学习:主区展示服务端分页项目卡片,详情页用安全 Markdown 渲染 README;原始 HTML 被禁用,Markdown 图片节点直接加载服务端校验后的无凭据 HTTPS URL,包括 SVG 和 Electron 支持的其他图片格式,不经过服务端下载、识别、转码或镜像。下载按钮打开系统保存对话框,Main 不按 `Content-Length`、声明字节数或客户端上限阻断下载,流式校验 SHA-256 与 ZIP 签名后原子保存。客户端不提供课程生成、课程播放器、本地课程库、Agent、ASR 或课堂 runtime。运营管理与接口字段见 [`docs/learning-project-catalog-server-contract.md`](docs/learning-project-catalog-server-contract.md)。 - 插件市场 Release A:插件中心(Marketplace)提供运营精选的免费 `skill_only` 插件;“免费获取”只写入账号 Library,“下载/更新”才写入本机 Package Store,“启用到项目”和“分配给伙伴”仍是 Project Plugins 中彼此独立的动作。我的插件(My Plugins)展示账号获取状态、本机安装/更新/删除设备包、移除后的 tombstone 和 bounded unavailable reason;Project Plugins 继续只修改项目选择,不会因获取或下载自动启用或分配。 - 插件运行架构:Renderer 只调用 Main-owned Marketplace facade;Main 负责账号、请求 deadline、签名/摘要校验、不可变 Release、current selection 与原子回滚。下一代 Pi worker 使用同一个 effective snapshot,将每个有效 Skill 与已验证 Package Store root 成对传给 resource loader、Extension Host 和 CLI;`skill_only` 不依赖运行时 Policy,也不执行分发包中的任意代码。正式激活仍等待官方 Ed25519 公钥(production key activation HOLD);生产私钥只能来自部署 secret,测试使用注入的临时密钥。 +- 原生 Web Search 是 `platform_hosted` 精选插件:用户仍需依次免费获取、下载、启用到项目并分配给伙伴,每次搜索还必须显式确认 Token Point 消耗。Renderer 与插件包不持有 Provider 密钥、模型或 URL;Main 只调用固定 Works Square typed route,并把收费状态作为 closed receipt 展示。正式激活仍等待官方签名公钥、`WEB_SEARCH_OPENAI_API_KEY`、模型、价格与隐私文案;开发和验收不得把 fake Provider 结果当作生产 OpenAI 通过。 - 提示词博物馆:只陈列经过审核的作品预览、Prompt、分类以及作者/来源/许可证信息,支持搜索、使用场景/风格/主体筛选和详情抽屉;“使用此 Prompt”只把原文带回当前 Canvas 对话草稿,不自动发送、不构成社区。列表和详情数据由服务端提供,客户端不打包数据集;服务端字段契约见 [`docs/prompt-museum-server-contract.md`](docs/prompt-museum-server-contract.md)。 - 视觉系统:单一浅色主题,品牌蓝 `#3A5578`、星火橙 `#F26A3D`、白色画布与低饱和蓝灰层级。 - 字体系统:Renderer UI 内嵌 Inter Variable 与经过字符子集化的 Source Han Sans SC WOFF2,按字符范围统一中英文并保留系统中文字体 fallback;代码、路径和日志使用独立等宽字体。 @@ -112,6 +113,7 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p ### 项目内置编码技能 - 产品内置四个核心编码技能根:`agent-browser`(开发浏览器)、`frontend-slides`(项目演示)、`grilling`(方案质询)和 `planning-with-files`(项目规划),统一从 vendor-neutral 的 `resources/coding-skills/` 打包。`data-service`(开发数据)由固定的 `resources/coding-plugins/data-service/` 插件包持有,不在核心技能根中复制路径或定义。 +- Marketplace 插件 Skill(包括 Web Search)从已签名 Package Store Release 动态物化,不进入核心技能根,也不靠静态工具白名单;只有下一代 parent worker 的冻结 effective snapshot 同时满足安装、账号 Library、项目启用与伙伴分配时才生效,child worker 不继承 hosted tool。 - 项目插件“启用”和把插件技能分配给智能体是两个独立动作:只有项目已启用 Data Service 时,未分配的 `data-service` 才可供新选择;禁用后,已有分配仍会显示并继续保存在智能体的 `skillIds`,但处于不可用且不生效的状态,重新启用后恢复生效。未启用的技能不进入该智能体的有效 Pi 资源集合。 - `data-service` 只在用户显式请求后触发:先检查并说明最小集合,用户确认后配置一次、复制 SDK 资产,再用本地预览执行 put/read-back;它不用于已发布作品。 - 创建项目智能体时,`agent-browser`、`grilling` 与 `planning-with-files` 默认启用;`frontend-slides` 作为专项能力可手动启用。用户可以在创建或编辑智能体时调整选择。最终选择写入项目智能体的 `skillIds`。 diff --git a/electron/api/coding-composition.ts b/electron/api/coding-composition.ts index 542414c..1210947 100644 --- a/electron/api/coding-composition.ts +++ b/electron/api/coding-composition.ts @@ -46,6 +46,8 @@ import { import { createDataServicePluginAdapter } from '../coding-plugins/adapters/data-service'; import { createGameResourcePluginAdapter } from '../coding-plugins/adapters/game-resource'; import { GameResourceClient } from '../services/game-resource-client'; +import { createWebSearchPluginAdapter } from '../coding-plugins/adapters/web-search'; +import { WebSearchClient } from '../services/web-search-client'; import { AccountPluginCache } from '../coding-plugins/account-plugin-cache'; import { createMarketplaceClient, @@ -288,6 +290,12 @@ export function createCodingComposition( packageStore, makeloreVersion: options.clientVersion ?? '2.0.0', }); + const webSearchAdapter = createWebSearchPluginAdapter({ + client: new WebSearchClient(), + marketplace: marketplaceClient, + packageStore, + makeloreVersion: options.clientVersion ?? '2.0.0', + }); const policyClient = options.policyClient ?? new PluginPolicyClient(); const knownPluginIds = new Set(pluginDefinitions.map(({ id }) => id)); // Existing user Releases are discovered from the device index at startup; @@ -323,7 +331,7 @@ export function createCodingComposition( const capabilityRegistry = createCodingCapabilityRegistry({ policyClient, projectPlugins, - adapters: [dataServiceAdapter, gameResourceAdapter], + adapters: [dataServiceAdapter, gameResourceAdapter, webSearchAdapter], definitions: pluginDefinitions, effectiveResolver, getDurableProjectId: async (projectPath, localProjectId) => { @@ -339,7 +347,7 @@ export function createCodingComposition( projects, projectPlugins, policyClient, - adapters: [dataServiceAdapter, gameResourceAdapter], + adapters: [dataServiceAdapter, gameResourceAdapter, webSearchAdapter], definitions: pluginDefinitions, effectiveResolver, getDefinitions: async () => { diff --git a/scripts/lib/pi-product-artifact.mjs b/scripts/lib/pi-product-artifact.mjs index 627494f..d180b71 100644 --- a/scripts/lib/pi-product-artifact.mjs +++ b/scripts/lib/pi-product-artifact.mjs @@ -45,6 +45,12 @@ const MARKETPLACE_ARTIFACT_MARKERS = Object.freeze({ 'makelore.game-resource', '/api/plugins/v1/hosted/game-resource/generations', ]), + webSearchRuntime: Object.freeze([ + 'makelore.web-search', + '/api/plugins/v1/hosted/web-search/searches', + 'plugin_receipt_unavailable', + 'receipt_unavailable', + ]), mainRoutes: Object.freeze([ '/api/coding/plugin-marketplace', 'plugin-marketplace\\/install\\/', @@ -61,12 +67,14 @@ const MARKETPLACE_ARTIFACT_MARKERS = Object.freeze({ '我的插件', ]), }); -const FORBIDDEN_MEOWA_ARTIFACT_MARKERS = Object.freeze([ +const FORBIDDEN_PROVIDER_AUTHORITY_MARKERS = Object.freeze([ 'MEOWA_API_KEY', 'MEOWA_API_URL', 'MEOWA_GAME_ASSETS_SHARED_SECRET', '/api/coding/meowa-game-assets', 'https://api.meowa.ai', + 'WEB_SEARCH_OPENAI_API_KEY', + 'https://api.openai.com/v1/responses', ]); const CODE_OWNED_PLUGIN_SIGNING_KEYS_SOURCE_MARKER = 'makelore.plugin-trust.code-owned.v1'; const PI_AI_PROVIDER_PREFIX = 'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/'; @@ -309,11 +317,11 @@ export function verifyMarketplaceClientArtifact(appAsarContents, productionTrust if (missing.length > 0) { throw new Error(`Packaged app.asar does not contain Marketplace contract markers: ${missing.join(', ')}`); } - const forbidden = FORBIDDEN_MEOWA_ARTIFACT_MARKERS.filter((marker) => ( + const forbidden = FORBIDDEN_PROVIDER_AUTHORITY_MARKERS.filter((marker) => ( appAsarContents.includes(Buffer.from(marker)) )); if (forbidden.length > 0) { - throw new Error(`Packaged app.asar still contains legacy Meowa client authority: ${forbidden.join(', ')}`); + throw new Error(`Packaged app.asar still contains provider authority: ${forbidden.join(', ')}`); } const hasEmptyCodeOwnedTrust = /(?:CODE_OWNED_PLUGIN_SIGNING_KEYS\s*=\s*)?Object\.freeze\(\s*\{\}\s*(?:as\s+[^)]*)?\)/u.test(productionTrustSource); if (!hasEmptyCodeOwnedTrust @@ -327,6 +335,7 @@ export function verifyMarketplaceClientArtifact(appAsarContents, productionTrust schema2SkillOnly: true, schema2PlatformHosted: true, legacyMeowaClientAuthorityAbsent: true, + webSearchProviderAuthorityAbsent: true, productionTrust: 'official-key-absent-fail-closed', libraryInstallAndEffectiveRoutes: true, rendererAssets: true, diff --git a/src/pages/Chat/CodingConversationTimeline.tsx b/src/pages/Chat/CodingConversationTimeline.tsx index 2a82f2c..4cd34be 100644 --- a/src/pages/Chat/CodingConversationTimeline.tsx +++ b/src/pages/Chat/CodingConversationTimeline.tsx @@ -87,6 +87,23 @@ const NODE_STATUS_LABELS: Record = { type TextualBlock = Exclude; type ThinkingBlock = Extract; +type CapabilityToolDetails = Extract; + +function capabilityBillingLabel(billing: CapabilityToolDetails['billing']): string { + switch (billing.status) { + case 'not_started': return '尚未开始计费'; + case 'included': return '已包含'; + case 'external': return '由外部账户结算'; + case 'reserved': return `已预留 ${billing.reserved_points} Token Point`; + case 'dispatched': return `处理中 · 已预留 ${billing.reserved_points} Token Point`; + case 'released': return '预留已释放,未收费'; + case 'pending_review': return '账单待审核'; + case 'expired': return '预留已过期'; + case 'settled': return `已结算 ${billing.actual_points} Token Point`; + case 'refunded': return `已退款 ${billing.actual_points} Token Point`; + case 'receipt_unavailable': return '收费状态未同步,请勿重复发起'; + } +} type ProcessItem = | { kind: 'thinking'; id: string; block: ThinkingBlock } @@ -608,6 +625,7 @@ const ToolDetails = memo(function ToolDetails({ details }: { details: KnownToolD

{details.success ? '成功' : details.error ?? '请求失败'} · HTTP {details.status}

+

{capabilityBillingLabel(details.billing)}

); } diff --git a/tests/unit/coding-capability-registry.test.ts b/tests/unit/coding-capability-registry.test.ts index 9b2274e..194a45a 100644 --- a/tests/unit/coding-capability-registry.test.ts +++ b/tests/unit/coding-capability-registry.test.ts @@ -7,6 +7,7 @@ import { } from '../../electron/coding-plugins/registry'; import type { CodingPluginAdapter } from '../../electron/coding-plugins/registry'; import { createDataServicePluginAdapter } from '../../electron/coding-plugins/adapters/data-service'; +import { createWebSearchPluginAdapter } from '../../electron/coding-plugins/adapters/web-search'; import { createDataServiceOperations, DataServiceCloudClient, @@ -331,6 +332,147 @@ describe('CodingCapabilityRegistry', () => { expect(invoke).toHaveBeenCalledTimes(1); }); + it('materializes Web Search only for the parent snapshot and rejects the old worker after disable', async () => { + const definition: CodingPluginDefinition = { + id: 'makelore.web-search', version: '1.0.0', contractVersion: 1, + displayName: 'Web Search', description: 'Hosted web search', + runtimeKind: 'platform_hosted', acquisitionMode: 'user_acquired', + releaseId: 'release-web-search-1', + provenance: { source: 'marketplace', packageRoot: 'C:/packages/web-search' }, + scope: 'project', adapterId: 'makelore.web-search', requiresBackend: true, + skills: [{ + id: 'web-search', entryPath: 'skills/web-search/SKILL.md', grants: ['web-search.search'], + }], + tools: [{ + name: 'makelore_web_search', label: 'Web Search', description: 'Search the web', + capabilityId: 'web-search.search', operation: 'search', roles: ['parent'], + mutation: 'read', projectWriteLease: false, + permissions: ['hosted.web-search.search'], executionMode: 'synchronous', + inputSchema: { + type: 'object', additionalProperties: false, required: ['query', 'confirmed'], + properties: { query: { type: 'string' }, confirmed: { type: 'boolean' } }, + }, + }], + operations: [{ + capabilityId: 'web-search.search', operation: 'search', toolName: 'makelore_web_search', + }], + surfaces: {}, + }; + const billing = { + mode: 'platform_metered' as const, entitlement_scope: 'plugin_usage', notice: 'Metered', + unit_name: 'search_request', unit_size: 1, rate_points: '1.00', + minimum_charge_points: '1.00', rounding_mode: 'ceil' as const, + }; + const webSearchPolicy: PluginPolicyClientState = { + status: 'current', revision: 10, lastVerifiedAt: 1, + catalog: { + schema_version: 1, catalog_version: 'web-search-1', pricing_version: 'pricing-1', + plugins: [{ + plugin_id: definition.id, supported_contract_versions: [1], status: 'active', + capabilities: [{ + capability_id: 'web-search.search', operations: [{ operation: 'search', billing }], + }], + }], + }, + }; + const frozen: EffectivePluginSnapshot = { + accountSessionId: 'account-a\u00001', projectId: context.projectId, + pluginReleaseIds: ['release-web-search-1'], effectiveSkillIds: ['web-search'], + skillEntries: [{ + id: 'web-search', entryPath: 'skills/web-search/SKILL.md', packageRoot: 'C:/packages/web-search', + }], + toolDefinitions: definition.tools, + runtimePolicies: [{ + pluginId: definition.id, pluginVersion: definition.version, + releaseId: definition.releaseId, contractVersion: 1, + capabilityId: 'web-search.search', operation: 'search', billing, + }], + unavailableReasons: [], + }; + const disabled: EffectivePluginSnapshot = { + ...frozen, pluginReleaseIds: [], effectiveSkillIds: [], skillEntries: [], + toolDefinitions: [], runtimePolicies: [], + unavailableReasons: [{ + pluginId: definition.id, code: 'project_disabled', message: 'Plugin is not enabled', + }], + }; + let current = frozen; + const resolve = vi.fn(async ({ role }: { role: 'parent' | 'child' }) => ( + role === 'child' ? { ...disabled, unavailableReasons: [] } : current + )); + const effectiveResolver = { + resolve, + 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', + }]), + getPolicyState: vi.fn(() => webSearchPolicy), + getInstalledDefinition: vi.fn(async () => definition), + } as unknown as EffectivePluginResolver; + const search = vi.fn().mockResolvedValue({ + executionId: 'execution-web-1', releaseId: definition.releaseId, + logicalOperationId: 'pi:run-a:resource-a', status: 'succeeded', + answer: 'A bounded answer', sources: [{ title: 'Source', url: 'https://example.com' }], + searchQueries: ['Makelore'], errorCode: null, + billing: { + mode: 'platform_metered', status: 'settled', reserved_points: '1.00', + actual_points: '1.00', usage_amount: 1, unit: 'search_request', + }, + }); + const adapter = createWebSearchPluginAdapter({ + client: { search } as never, + admissionResolver: { + resolve: vi.fn().mockResolvedValue({ + releaseId: definition.releaseId, releaseAdmissionId: 'admission-web-1', + }), + } as never, + }); + const capabilityRegistry = registry({ + definitions: [], effectiveResolver, adapters: [adapter], + policyClient: { getState: () => webSearchPolicy, refresh: vi.fn() }, + getEnabledPluginIds: async () => [definition.id], + }); + + const parent = await capabilityRegistry.resolveWorkerResources({ + projectId: context.projectId, projectPath: context.projectPath, + assignedSkillIds: ['web-search'], role: 'parent', + }); + const child = await capabilityRegistry.resolveWorkerResources({ + projectId: context.projectId, projectPath: context.projectPath, + assignedSkillIds: ['web-search'], role: 'child', + }); + expect(parent.tools.map(({ name }) => name)).toEqual(['makelore_web_search']); + expect(parent.effectiveSkillIds).toEqual(['web-search']); + expect(child.tools).toEqual([]); + expect(child.effectiveSkillIds).toEqual([]); + + const result = await capabilityRegistry.invoke({ + toolName: 'makelore_web_search', + context: { ...context, skillIds: ['web-search'], effectiveSnapshot: frozen }, + workerRole: 'parent', effectiveSkillIds: ['web-search'], + value: { query: ' Makelore ', confirmed: true }, + }); + expect(search).toHaveBeenCalledWith(expect.objectContaining({ + releaseId: 'release-web-search-1', releaseAdmissionId: 'admission-web-1', + logicalOperationId: 'pi:run-a:resource-a', query: 'Makelore', confirmed: true, + })); + expect(result.details).toMatchObject({ + schema: 'makelore-capability.v1', plugin_id: 'makelore.web-search', + capability_id: 'web-search.search', operation: 'search', + payload_schema: 'web-search.v1', billing: { status: 'settled', actual_points: '1.00' }, + }); + + current = disabled; + const stale = await capabilityRegistry.invoke({ + toolName: 'makelore_web_search', + context: { ...context, skillIds: ['web-search'], effectiveSnapshot: frozen }, + workerRole: 'parent', effectiveSkillIds: ['web-search'], + value: { query: 'Makelore', confirmed: true }, + }); + expect(stale.details).toMatchObject({ success: false, code: 'plugin_not_enabled' }); + expect(search).toHaveBeenCalledOnce(); + }); + it('refuses a new plugin action from an old worker after lifecycle invalidation', async () => { const frozenSnapshot: EffectivePluginSnapshot = { accountSessionId: 'account-a\u00001', diff --git a/tests/unit/coding-conversation-timeline.test.tsx b/tests/unit/coding-conversation-timeline.test.tsx index 6bef893..8fc4302 100644 --- a/tests/unit/coding-conversation-timeline.test.tsx +++ b/tests/unit/coding-conversation-timeline.test.tsx @@ -880,4 +880,58 @@ describe('CodingConversationTimeline', () => { expect(document.querySelector('[data-node-id="tool-changed-file-feature"]')).toBeNull(); expect(screen.queryByText(/回滚|revert/i)).not.toBeInTheDocument(); }); + + it('renders receipt-unavailable as a generic billing state without parsing Web Search payloads', async () => { + const { codingConversationStore } = await import('@/stores/coding-conversations'); + const { CodingConversationTimeline } = await import( + '@/pages/Chat/CodingConversationTimeline' + ); + const base = createProductSnapshot('conversation-web-search-billing', 1); + const snapshot = { + ...base, + nodes: [{ + kind: 'tool' as const, + id: 'tool-web-search-billing', + toolCallId: 'call-web-search-billing', + toolName: 'makelore_web_search', + title: '搜索网络', + inputText: '{"query":"Makelore"}', + status: 'error' as const, + output: [], + details: { + schema: 'makelore-capability.v1' as const, + plugin_id: 'makelore.web-search', + plugin_version: '1.0.0', + capability_id: 'web-search.search', + operation: 'search', + request_id: 'pi:run-a:resource-a', + success: false, + status: 503, + code: 'plugin_receipt_unavailable', + error: 'Web Search billing status could not be synchronized', + retryable: false, + billing: { mode: 'platform_metered' as const, status: 'receipt_unavailable' as const }, + payload_schema: 'web-search.v1', + data: null, + }, + }], + }; + codingConversationStore.getState().applySnapshotEvent({ + type: 'snapshot', + conversationId: 'conversation-web-search-billing', + workerGeneration: 1, + seq: snapshot.cursor.seq, + snapshot, + }); + + render(); + + const process = screen.getByTestId('coding-process-group'); + fireEvent.click(process.querySelector('summary')!); + const tool = document.querySelector('[data-node-id="tool-web-search-billing"]')!; + fireEvent.click(tool.querySelector('summary')!); + const details = within(tool as HTMLElement).getByTestId('tool-details'); + expect(within(details).getByText('收费状态未同步,请勿重复发起')).toBeVisible(); + expect(within(details).queryByText(/来源|搜索结果|Makelore\.com/u)).not.toBeInTheDocument(); + }); }); diff --git a/tests/unit/coding-plugin-composition.test.ts b/tests/unit/coding-plugin-composition.test.ts index e68a3dc..3a04de8 100644 --- a/tests/unit/coding-plugin-composition.test.ts +++ b/tests/unit/coding-plugin-composition.test.ts @@ -4,21 +4,69 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { AgentBrowserModule } from '../../electron/agent-browser'; +import { createCodingComposition } from '../../electron/api/coding-composition'; import { createCodingProjectPluginService, } from '../../electron/api/coding-product-services'; import { createCodingProjectMetadata, createCodingProjectAgent } from '../../electron/coding-projects/project-config'; import type { CodingPluginAdapter } from '../../electron/coding-plugins/registry'; import { createProjectPluginService } from '../../electron/coding-plugins/project-service'; +import { createMemoryCodingProjectStorage } from '../../electron/coding-projects/project-store'; import { DATA_SERVICE_PLUGIN_DEFINITION } from '../../shared/coding-plugins'; +const webSearchAdapterMock = vi.hoisted(() => ({ + create: vi.fn(), + deactivate: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock('../../electron/coding-plugins/adapters/web-search', () => ({ + createWebSearchPluginAdapter: webSearchAdapterMock.create.mockImplementation(() => ({ + pluginId: 'makelore.web-search', + inspect: vi.fn().mockResolvedValue({ status: 'ready' }), + invoke: vi.fn(), + deactivate: webSearchAdapterMock.deactivate, + })), +})); + const roots: string[] = []; afterEach(async () => { await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); + webSearchAdapterMock.create.mockClear(); + webSearchAdapterMock.deactivate.mockClear(); }); describe('coding plugin bounded product service', () => { + it('registers the code-owned Web Search adapter in the production composition', async () => { + const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-web-search-composition-project-')); + const userDataDir = await mkdtemp(path.join(tmpdir(), 'makelore-web-search-composition-user-')); + roots.push(projectPath, userDataDir); + const composition = createCodingComposition({ + storage: createMemoryCodingProjectStorage(), + browser: { close: vi.fn().mockResolvedValue(undefined) } as unknown as AgentBrowserModule, + paths: { + executablePath: process.execPath, + cliPath: path.join(projectPath, 'unused-cli.js'), + serverPath: path.join(projectPath, 'unused-server.mjs'), + userDataDir, + bundledSkillsDir: path.resolve('resources/coding-skills'), + }, + }); + + await composition.plugins.deactivate(projectPath, 'makelore.web-search'); + + expect(webSearchAdapterMock.create).toHaveBeenCalledOnce(); + expect(webSearchAdapterMock.create).toHaveBeenCalledWith(expect.objectContaining({ + client: expect.any(Object), + marketplace: expect.any(Object), + packageStore: expect.any(Object), + makeloreVersion: '2.0.0', + })); + expect(webSearchAdapterMock.deactivate).toHaveBeenCalledWith(projectPath); + await composition.shutdown(); + }); + it('joins package and policy exactly while isolating adapter inspection failure', async () => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-product-')); roots.push(root); diff --git a/tests/unit/pi-product-artifact.test.ts b/tests/unit/pi-product-artifact.test.ts index c1a16ed..621c03e 100644 --- a/tests/unit/pi-product-artifact.test.ts +++ b/tests/unit/pi-product-artifact.test.ts @@ -29,6 +29,8 @@ const MARKETPLACE_ARTIFACT_TEXT = [ 'makelore-plugin-release.v1', 'skill_only', 'platform_hosted', 'plugin_signature_invalid', 'signing key is not trusted', 'makelore.game-resource', '/api/plugins/v1/hosted/game-resource/generations', + 'makelore.web-search', '/api/plugins/v1/hosted/web-search/searches', + 'plugin_receipt_unavailable', 'receipt_unavailable', '/api/coding/plugin-marketplace', 'plugin-marketplace\\/install\\/', 'plugin-marketplace\\/update\\/', 'effectiveSkillIds', 'pluginReleaseIds', @@ -215,6 +217,7 @@ describe('final Pi product artifact verification', () => { schema2SkillOnly: true, schema2PlatformHosted: true, legacyMeowaClientAuthorityAbsent: true, + webSearchProviderAuthorityAbsent: true, productionTrust: 'official-key-absent-fail-closed', libraryInstallAndEffectiveRoutes: true, rendererAssets: true, @@ -238,7 +241,12 @@ describe('final Pi product artifact verification', () => { expect(() => verifyMarketplaceClientArtifact( Buffer.from(`${MARKETPLACE_ARTIFACT_TEXT}\nMEOWA_API_KEY`), `${emptyTrust}\nmakelore.plugin-trust.code-owned.v1`, - )).toThrow('legacy Meowa client authority'); + )).toThrow('provider authority'); + + expect(() => verifyMarketplaceClientArtifact( + Buffer.from(`${MARKETPLACE_ARTIFACT_TEXT}\nWEB_SEARCH_OPENAI_API_KEY`), + `${emptyTrust}\nmakelore.plugin-trust.code-owned.v1`, + )).toThrow('provider authority'); }); it('proves Marketplace trust from the packaged app.asar rather than checkout source', async () => { @@ -320,6 +328,8 @@ describe('final Pi product artifact verification', () => { await writeFile(path.join(source, 'dist', 'assets', 'plugin-marketplace.js'), [ 'makelore-plugin-release.v1 skill_only platform_hosted plugin_signature_invalid signing key is not trusted', 'makelore.game-resource /api/plugins/v1/hosted/game-resource/generations', + 'makelore.web-search /api/plugins/v1/hosted/web-search/searches', + 'plugin_receipt_unavailable receipt_unavailable', '/api/coding/plugin-marketplace plugin-marketplace\\/install\\/ plugin-marketplace\\/update\\/', 'effectiveSkillIds pluginReleaseIds', '/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 我的插件', From 4a1e5d31213191a0102eab9278dd9887ba8738f4 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 02:44:41 +0800 Subject: [PATCH 08/47] docs(web-search): record client integration --- .../20-architecture/system-overview.md | 5 +- .project-docs/30-worklog/current-state.md | 12 ++++ ...-web-search-client-integration-7d2f5b94.md | 12 +++- ...1-web-search-mlw03-composition-d9f5b3e2.md | 64 ------------------- .project-docs/40-domain/business-rules.md | 8 +++ 5 files changed, 33 insertions(+), 68 deletions(-) delete mode 100644 .project-docs/30-worklog/tasks/20260901-web-search-mlw03-composition-d9f5b3e2.md diff --git a/.project-docs/20-architecture/system-overview.md b/.project-docs/20-architecture/system-overview.md index 5e2bd26..be429f7 100644 --- a/.project-docs/20-architecture/system-overview.md +++ b/.project-docs/20-architecture/system-overview.md @@ -23,6 +23,7 @@ Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展 | Pi Conversation Runtime | 一个长驻 Pi `0.84.2` Agent Server 承载每条 active/warm Conversation 的隔离逻辑 Runtime/Session/JSONL channel | 严格 LF JSONL RPC、generation recovery、Snapshot hydration;top-level 逻辑 turn 并发 4、warm idle LRU 8;Server 退出统一使旧 channel 失效并按需单实例重启 | | Pi Provider & Managed Resources | Provider catalog、thread-local secret projection、model/resource revision、Prompt/Skill/extension materialization | 父凭据只进入选中逻辑线程的内存 credential store,child 凭据只进入该短命进程;不扫描项目或用户的 `.pi/.agents/.codex`,不把 secret 放进 argv、catalog 或 Renderer | | Pi Extension, Subagents & Lifecycle | 唯一显式 Makelore extension、UI interaction、ephemeral child、write lease 与 background run lease | child 并发 4、单次最多 8、禁止递归;active/uncertain run 不因页面隐藏或 confirmation timeout 被停止,replacement/stop 必须可解释并清理所有 ownership | +| Native Web Search Plugin | Signed Marketplace Package → effective parent snapshot → code-owned Main adapter → fixed Works Square Web Search route | Renderer/Package/Pi 不持有 Provider key、model 或 URL;每次搜索要求显式确认,closed receipt 与 result-less uncertain state 由 Main 投影,child 不继承 hosted tool | | AI Design Workspace & Living Form | 一个 Workspace 的当前 Direction、Current Specification、持久 Agent Session、conversation timeline、Tasks 与 Assets | Living Form 是服务端 Current Specification 的投影;Renderer 只持有草稿和已接受投影 | | AI Design Input & Reconciliation | Chat、字段/集合编辑、decision、proposal、lock、Asset binding 与 restore | 全部进入同一 `design.input.apply` reducer;稳定 command/operation ID 支持 unknown-result 重放,revision conflict 刷新权威状态 | | AI Design Gateway Routing | Main-owned Works Square V2 adapter 与 Direction event stream | Main 持有 Works Token、stream ticket、WebSocket、重试分类和错误脱敏;事件顺序与 Task progress 不构成 Specification 真值 | @@ -87,8 +88,8 @@ Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展 ## Related Decisions -- 当前长期边界记录于 README、ADR-002 至 ADR-007(ADR-001 已由 ADR-007 supersede)及各 Integration Gate;后续如改变唯一入口、凭据所有权、构建执行边界、Living Form/Specification 状态归属、Pi 单 runtime/worker/lease 边界、Learning/Marketplace 分发边界、Robot 配网边界或重新引入客户端部署协调器,应新增 ADR。 +- 当前长期边界记录于 README、ADR-002 至 ADR-007(ADR-001 已由 ADR-007 supersede)及各 Integration Gate;后续如改变唯一入口、凭据所有权、构建执行边界、Living Form/Specification 状态归属、Pi 单 runtime/worker/lease 边界、Learning/Marketplace/Web Search 分发边界、Robot 配网边界或重新引入客户端部署协调器,应新增 ADR。 ## Last Updated -2026-08-31 +2026-09-01 diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index 8199081..b6135c4 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -4,6 +4,18 @@ This file is the integrated default-branch snapshot. Feature tasks record progre ## Integrated Through +- Native Web Search client tickets MLW-01 through MLW-03 are integrated by task + `20260831-web-search-client-integration-7d2f5b94` through product commit + `60e6a8eeb35431b0f1fb569af4d1502683f16317`, against frozen Works Square DTO + frontier `a49c696ebc4213e3d62ece780961efbe17576f8e`. MakeLore now reuses the shared + hosted Admission resolver, calls the single fixed Web Search typed route from + Electron Main, materializes the signed Marketplace Skill/tool only in an eligible + frozen parent Pi snapshot, and renders the closed billing receipt without parsing + Web Search payloads in Renderer. The packaged Windows app proves that the route + and receipt parser are main-reachable while OpenAI Provider authority is absent. + Real PostgreSQL and paid OpenAI acceptance remain external HOLDs; production + activation still requires the official Ed25519 key, OpenAI key/model, price, and + privacy copy. - Human-authorized takeover task `20260831-promote-main-merge-5e9c7a31` completed the already-started local `main` merge as `03a9e866d4366e0a1cb416e26b424fe981071310`, recorded the transfer in diff --git a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md index 741abcf..980e090 100644 --- a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md +++ b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md @@ -38,6 +38,11 @@ - Product commit: `fc68cf295131d8f73556bef59fdeae61239a649e`; source and product trees are exact-equal at `5c14e92224e8441d18a983dd7099cd6d76f83e85` before this Integration Documentation Gate removes the duplicate foreign task record. - Delivered the Main-only closed Web Search client, bounded same-operation reconciliation, code-owned adapter, and the minimal generic billing/conversation/Registry support for `not_started` and Main-only `receipt_unavailable`. The adapter uses the MLW-01 admission helper and trusted project/request identity; no composition, Renderer, Pi, Package Store/effective resolver, Provider authority, or server path changed. - Final source review aligned source title/URL bounds with the frozen Server DTO and preserved both MLW-01 typed admission failures. The clean source branch retains its complete task record; integration evidence is consolidated here. +- MLW-03 source task `20260901-web-search-mlw03-composition-d9f5b3e2` was integrated from exact frontier `68cb2e73beb17d5041198d020aaa7b2884124950`. + - Source commit: `52f6a0b148403c822a09880467f81416a181b259` (sole parent `68cb2e73beb17d5041198d020aaa7b2884124950`). + - Product commit: `60e6a8eeb35431b0f1fb569af4d1502683f16317`; source and product trees are exact-equal at `d570c40f2ad315c8a314f831fb4337300ae28fc2` before this Integration Documentation Gate removes the duplicate foreign task record. + - Delivered production composition registration, the dynamic Web Search parent/child/current-authority proof, a generic closed billing-status timeline projection, package-main-reachable route/receipt proof with Provider-authority exclusion, and current README documentation. No Web Search payload-specific Renderer or static tool allowlist was added. + - The clean source branch retains its complete task record. This coordinator checkpoint removes only the duplicate cherry-picked copy and records the accepted facts in canonical project memory. ## Verification @@ -49,11 +54,14 @@ - MLW-02 final focused suite passed `3 files / 35 tests`; adjacent hosted admission, Game Resource, Registry, conversation, Marketplace-client, and timeline regression passed `11 files / 114 tests`. - MLW-02 full unit suite passed `214 files / 1748 tests`, with two staged-runtime skips; pressure passed `1/1`. Typecheck, owned lint, full lint (zero errors and the same five existing warnings), and Vite Renderer/Main/Preload/utility builds passed. - MLW-02 diff, project-docs, document-drift, sole-parent, clean-worktree, and `READY_FOR_INTEGRATION` gates passed. No live Provider call or production activation occurred. +- MLW-03 TDD started with four exact focused failures and finished with 4 files / 40 tests passed; the adjacent Web Search/admission/composition/lifecycle/Pi/timeline/artifact suite passed 10 files / 82 tests. +- MLW-03 full unit suite passed 214 files / 1,751 tests with two staged-runtime skips; pressure passed 1/1. Typecheck, scoped lint, full lint (zero errors and the unchanged five warnings), Vite builds, Electron Windows 6/6, and target Marketplace/Project Plugins/Skill E2E 4/4 passed. +- Clean source HEAD Windows packaging and verification passed. `verify:artifact:win` embedded exact commit `52f6a0b148403c822a09880467f81416a181b259`; installer size was 208,252,221 bytes with SHA-256 `2D027DB5BE00336F1EB45D882519F971EB791014A1930C5FBED93FE6B15B6303`. `verify:artifact:pi` passed the real app.asar Web Search route/receipt/provider-authority proof; inherited Pi cross-platform/real-provider waivers remain unchanged. ## Follow-ups -- Start MLW-03 only from the clean post-MLW-02 coordinator frontier. MLW-03 owns composition, the generic timeline billing projection, dynamic tool/package proof, README, and necessary focused/E2E evidence; it must not reopen MLW-01/02 authority. -- Keep the live OpenAI XWS-01 group and production activation closed pending explicit external inputs and user authorization. +- Run fixed-range Standards and Spec review from client base `0a86ec825a5803bf7e037d3b23c39be23238c43d` through the clean post-MLW-03 documentation frontier; use one remediation owner only if either axis reports an actionable finding. +- Keep real PostgreSQL, the live paid OpenAI XWS-01 group, and production activation closed pending their explicit external inputs and user authorization. ## Promotion Candidates diff --git a/.project-docs/30-worklog/tasks/20260901-web-search-mlw03-composition-d9f5b3e2.md b/.project-docs/30-worklog/tasks/20260901-web-search-mlw03-composition-d9f5b3e2.md deleted file mode 100644 index 7bc0264..0000000 --- a/.project-docs/30-worklog/tasks/20260901-web-search-mlw03-composition-d9f5b3e2.md +++ /dev/null @@ -1,64 +0,0 @@ -# Task: Implement MLW-03 composition and packaged proof - -## Identity - -- Task ID: 20260901-web-search-mlw03-composition-d9f5b3e2 -- Mode: Feature -- Branch: codex/20260901-web-search-mlw03-composition-d9f5b3e2-web-search-mlw03-composition -- Worktree: D:\Datas\OthersProjects\makelore-web-search-mlw03-composition-d9f5b3e2 -- Base commit: 68cb2e73beb17d5041198d020aaa7b2884124950 -- Owner: web-search-mlw03-implementer -- Status: Ready for Integration - -## Scope - -- Wire the code-owned Web Search adapter into `electron/api/coding-composition.ts` so the existing effective-plugin snapshot is the only parent-worker materialization and invocation authority. -- Add the Web Search parent/child/current-authority scenario to `tests/unit/coding-capability-registry.test.ts` and the composition registration proof to `tests/unit/coding-plugin-composition.test.ts`. -- Add only a generic closed billing-state projection to `src/pages/Chat/CodingConversationTimeline.tsx` with focused coverage in `tests/unit/coding-conversation-timeline.test.tsx`; do not parse `web-search.v1` or add Web Search-specific Renderer state. -- Extend the packaged main-reachable proof in `scripts/lib/pi-product-artifact.mjs` and `tests/unit/pi-product-artifact.test.ts` for the fixed hosted Web Search route, receipt-unavailable parser, and absence of provider authority. The verifier script is a necessary adjacent proof file required by the MLW-03 acceptance criteria and has no competing ticket owner. -- Update `README.md` to describe the shipped native hosted Web Search capability and its production activation holds. -- Do not modify MLW-01 admission ownership, MLW-02 client/adapter/envelope ownership, project selection services, Marketplace pages, Provider/Server code, or Release B surfaces. - -## Intent And Constraints - -- Exact source base is `68cb2e73beb17d5041198d020aaa7b2884124950`; server DTO authority is frozen at `a49c696ebc4213e3d62ece780961efbe17576f8e`. -- Concurrent Task Gate and Planning Gate passed after verifying task identity, branch/worktree/base, clean product state, coordinator/MLW-01/MLW-02 peers, canonical implementation spec/design, and project memory. The older broad Web Search client task remains at its original base with no product diff and does not conflict with this exact-base ticket. -- Use test-first natural boundaries. Parent workers may receive one dynamically installed Web Search Skill/tool; child workers receive none. Disabled, uninstalled, stale, account-switched, project-switched, or logged-out state must affect only the next frozen worker snapshot. -- Keep provider key, model choice, provider URL, signed URL, admission authority, and account authority out of Renderer/package artifacts. No static tool allowlist, generic invoke route, `pi-web-search`, or third-party Pi extension. -- Real PostgreSQL, live paid OpenAI, production signing key, production OpenAI key/model/price/privacy copy, push, PR, deployment, and publication remain out of scope or explicitly held. - -## Plan - -1. Add focused failing tests for composition registration, dynamic parent/child/current invocation, generic billing projection, and packaged reachability/provider-authority exclusions. -2. Implement the smallest composition/timeline/artifact/README changes that turn those tests green. -3. Run focused and adjacent regressions, typecheck, lint, Vite/Electron/package proof as applicable, full unit pressure, documentation gates, and produce one clean source commit. - -## Outcome - -- Registered the code-owned Web Search adapter in both production capability execution and project-plugin inspection/deactivation paths. Marketplace-installed schema-v2 definitions remain the only Skill/tool source; no Web Search definition or tool allowlist was added to composition. -- Added an end-to-end registry proof for the frozen Web Search parent snapshot: one parent Skill/tool, no child inheritance, exact Main request/admission/envelope mapping, and refusal of a subsequent old-worker action after the project is disabled. -- Added a generic closed billing-state renderer for all `makelore-capability.v1` tools. `receipt_unavailable` is displayed as `收费状态未同步,请勿重复发起`; the timeline does not inspect `web-search.v1` answer/source payloads or add Web Search-specific state. -- Extended the package-main-reachable artifact proof to require the Web Search plugin ID, fixed Works route, and receipt-unavailable parser while rejecting embedded OpenAI key/origin authority. The built app remains fail-closed for the absent official Marketplace signing key. -- Updated README current-product documentation for native hosted Web Search, dynamic Marketplace Skill materialization, explicit Token Point confirmation, and production activation holds. - -## Verification - -- TDD boundary: after dependency materialization, the first focused run failed exactly four assertions (missing composition registration, generic receipt projection, Web Search artifact result field, and provider-authority rejection). The implementation turned the same boundary green: 4 files / 40 tests. -- Focused plus adjacent Web Search/admission/composition/lifecycle/Pi/timeline/artifact suite: 10 files / 82 tests passed. -- Full unit suite: 214 files / 1,751 tests passed / 2 staged-runtime tests skipped; isolated pressure suite: 1/1 passed. -- `pnpm run typecheck`: passed. Scoped ESLint: passed. Full `pnpm run lint:check`: 0 errors and the unchanged 5 warnings in Home/Makelore. -- `pnpm run build:vite`: passed for Renderer, Main, Preload, and utility worker. Electron Windows Vitest: 2 files / 6 tests passed. Marketplace, Project Plugins, and Skill configuration Playwright targets: 4/4 passed. -- `pnpm run package:win`: passed and produced the unpacked x64 app plus NSIS installer. The first installer was 208,252,283 bytes with SHA-256 `E6ABFA36C219F71FDBCAE811BA01DAEFFFAA3625FCCA17FE40C12CE507D0808E`; a clean-source-HEAD package/verification is required immediately after the sole commit before handoff. -- `pnpm run verify:artifact:pi`: passed against the real built `app.asar`; Marketplace proof includes the Web Search ID, fixed route, receipt parser, absent Web Search provider authority, and official-key-absent fail-closed trust. Existing Pi runtime result remains the inherited `partial-pass` for deferred cross-platform/real-provider evidence. -- `pnpm run verify:publish-runtime`: passed with npm 11.6.2. The pre-commit `verify:artifact:win` correctly refused to claim a clean artifact while the source diff was uncommitted; it is not a product failure and must be rerun on the source commit. -- `git diff --check`: passed. - -## Follow-ups - -- Real PostgreSQL lifecycle remains an external HOLD because no local service, Docker runtime, or `TEST_POSTGRESQL_DATABASE_URL` is available; SQLite is not accepted as a substitute. -- Live OpenAI acceptance remains HOLD because paid calls were not authorized. Production activation also remains HOLD for the official Ed25519 public key, `WEB_SEARCH_OPENAI_API_KEY`, approved model, price, and privacy copy. -- XWS-01 and the fixed-range Standards/Spec review remain coordinator-owned after MLW-03 integration. - -## Promotion Candidates - -- None recorded. diff --git a/.project-docs/40-domain/business-rules.md b/.project-docs/40-domain/business-rules.md index b9630a2..491b9b7 100644 --- a/.project-docs/40-domain/business-rules.md +++ b/.project-docs/40-domain/business-rules.md @@ -38,6 +38,14 @@ not expose Provider credentials, URLs, credit balances, raw responses, or Provider job IDs. Saving a hosted result must use bounded project-relative paths and the existing project write lease. +- Native Web Search is a user-acquired `platform_hosted` Marketplace Plugin. Free + acquisition, device installation, project enablement, Agent assignment, current + Admission, and explicit per-search Token Point confirmation are separate gates. + Only the frozen parent worker receives `makelore_web_search`; child workers receive + no hosted tool. `submission_unknown` is result-less and must not auto-retry, while + `receipt_unavailable` is a Main-only closed billing projection that tells the user + not to repeat the search. Provider key, model, URL, price authority, and raw response + never enter Renderer, the Package, Pi arguments, or project metadata. - 面向用户的 AI 编程新建流程必须在 `mini_game`、`mini_program`、`custom` 中选择;`ProjectType` 是产品类型,创建后不能通过 UI 或 Host API 修改,未传类型的兼容 API 调用按 `custom` 处理。 - 新建小游戏和小程序会生成平台固定版本的受控 Vite 发布模板,并可使用项目配置中的单一“提交审核”入口;`custom` 和缺少类型字段的旧项目不提供一键发布。 - `ProjectType` 不等于 `BuildPreset`:第一期两个可发布产品类型都映射到内部受控 Vite preset;本地 `projectType` 不是授权边界,Main-owned 安全打包、Host API 和服务端包体校验仍必须执行。 From 49de82c4860fd0b377070279b6411237dc6b9564 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 03:49:52 +0800 Subject: [PATCH 09/47] fix(web-search): harden client response boundary --- ...earch-rev01-client-remediation-e4a7c9b2.md | 105 ++++++++++++ electron/services/web-search-client.ts | 114 +++++++++++-- tests/unit/web-search-client.test.ts | 152 +++++++++++++++++- 3 files changed, 355 insertions(+), 16 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260901-web-search-rev01-client-remediation-e4a7c9b2.md diff --git a/.project-docs/30-worklog/tasks/20260901-web-search-rev01-client-remediation-e4a7c9b2.md b/.project-docs/30-worklog/tasks/20260901-web-search-rev01-client-remediation-e4a7c9b2.md new file mode 100644 index 0000000..f6de240 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-web-search-rev01-client-remediation-e4a7c9b2.md @@ -0,0 +1,105 @@ +# Task: REV-01 Client WebSearchClient remediation + +## Identity + +- Task ID: 20260901-web-search-rev01-client-remediation-e4a7c9b2 +- Mode: Feature +- Branch: codex/20260901-web-search-rev01-client-remediation-e4a7c9b2-web-search-rev01-client-remediation +- Worktree: D:\Datas\OthersProjects\makelore-web-search-rev01-client-remediation-e4a7c9b2 +- Base commit: 4a1e5d31213191a0102eab9278dd9887ba8738f4 +- Owner: web-search-rev01-client-remediation +- Status: Ready for Integration + +## Scope + +- Remediate the four accepted REV-01 Client Spec-axis findings from the exact + coordinator frontier `4a1e5d31213191a0102eab9278dd9887ba8738f4`. +- Ownership is limited to `electron/services/web-search-client.ts`, its + focused Web Search client tests, and this task record. No Server, + composition, Pi, Package Store, Renderer, or shared product-scope changes. +- Preserve the fixed typed route, trusted admission, same logical-operation + replay, closed capability envelope, and external PostgreSQL/live OpenAI/ + production-activation holds. + +## Intent And Constraints + +- Project Context Loaded: + - Task ID: `20260901-web-search-rev01-client-remediation-e4a7c9b2` + - Mode: Feature + - Branch: `codex/20260901-web-search-rev01-client-remediation-e4a7c9b2-web-search-rev01-client-remediation` + - Worktree: `D:\Datas\OthersProjects\makelore-web-search-rev01-client-remediation-e4a7c9b2` + - Base commit: `4a1e5d31213191a0102eab9278dd9887ba8738f4` + - Other active local tasks: client coordinator, MLW-01/02/03 source tasks, + and the read-only REV-01 client/server review tasks. Their records were + inspected for scope; no product writer overlaps this remediation. + - Overlap/semantic assessment: the four findings are confined to the + WebSearchClient response boundary and have one authorized owner. No + semantic conflict with server DTO, composition, or Package Store scope. +- Read: client `AGENTS.md`; complete `maintain-project-docs`, `implement-spec`, + and `tdd` skills; project entry files; client architecture/domain/decision, + evidence/reflection/commitment/stale indexes; the Web Search design and + implementation Spec; REV-01 client Spec and Standards task records. +- Concurrent Task Gate: Passed. `check_project_docs.py` passed; task_context + created the isolated worktree and `status --json` matches this task ID, + owner, worktree, branch, and exact base. +- Planning Gate: Passed. The fixed client review identified exactly four + actionable response-boundary findings; the current plan remains within the + accepted ownership and does not alter the frozen contract. +- Implementation plan: + 1. Add public-seam tests that fail for status/body precedence, streamed + response bounds, closed status/receipt/result pairings, and source URL + validation. + 2. Make the smallest WebSearchClient changes to pass each red test: status + first with bounded Retry-After, chunked streaming cap, closed receipt and + result invariants, and absolute HTTP(S) URL validation without userinfo. + 3. Run focused and adjacent tests, typecheck, lint, diff/doc gates; then + update this record and create one source commit only after a clean handoff. + +## Outcome + +- Implemented the four accepted WebSearchClient response-boundary fixes: + HTTP-status-first 429 handling with bounded Retry-After, a true streamed + response cap, closed search/billing/error pairings, and safe bounded source + URLs. No file outside the owned client service/test/task-record scope was + changed. + +## Verification + +- Concurrent/Planning gates passed. +- TDD RED: the new focused suite exposed 8 failures in 16 tests for unsafe + URLs, 429 body precedence, chunked buffering, malformed receipts/pairings, + and the now-invalid failed-without-error fixture. +- TDD GREEN: `pnpm exec vitest run tests/unit/web-search-client.test.ts` + passed 16/16. +- Adjacent: `pnpm exec vitest run tests/unit/web-search-client.test.ts + tests/unit/web-search-plugin-adapter.test.ts` passed 27/27. +- `pnpm run typecheck` passed; owned-file ESLint passed with zero errors. +- Full `pnpm test` passed: 214 files, 1,759 tests passed, 2 skipped; the + pressure suite passed 1/1. Full `pnpm run lint:check` passed with zero + errors and five pre-existing warnings outside this task's ownership. +- Task-aware doc drift and source diff checks passed; the final source commit + and clean `task_context` handoff are recorded below. + +## Handoff + +- Source commit: the final task `HEAD` handed off to the coordinator; its + sole parent is the exact coordinator frontier + `4a1e5d31213191a0102eab9278dd9887ba8738f4`. +- `task_context complete` passed with `READY_FOR_INTEGRATION`; this source + commit is the sole integration candidate. + +## Follow-ups + +- Coordinator must integrate only the final source commit and trigger a fresh + fixed-range Standards/Spec review. + +## Promotion Candidates + +- Target: client integration coordinator and fresh REV-01 review checkpoint. + Proposal: preserve HTTP-status-first semantics, enforce a real 1 MiB stream + cap, reject incoherent closed receipts/results, and reject unsafe source URLs. + Evidence: REV-01 Client Spec findings and new public-seam regressions. + Future impact: prevents malformed responses from overriding server status, + unbounded chunked buffering, ungrounded success results, or provider URL + leakage. Semantic conflicts: none; human confirmation: not required for this + in-scope remediation. diff --git a/electron/services/web-search-client.ts b/electron/services/web-search-client.ts index 36a4981..83da079 100644 --- a/electron/services/web-search-client.ts +++ b/electron/services/web-search-client.ts @@ -12,7 +12,7 @@ const MAX_REQUEST_BYTES = 98_304; const MAX_QUERY_LENGTH = 2_000; const MAX_ANSWER_LENGTH = 16_000; const MAX_SOURCE_TITLE_LENGTH = 240; -const MAX_SOURCE_URL_LENGTH = 4_096; +const MAX_SOURCE_URL_LENGTH = 2_048; const MAX_SEARCH_QUERY_LENGTH = 500; const MAX_RETRY_AFTER_SECONDS = 86_400; const DECIMAL = /^(?:0|[1-9]\d*)\.\d{2}$/u; @@ -36,6 +36,17 @@ const KNOWN_ERROR_CODES = new Set([ 'web_search_result_invalid', 'web_search_submission_unknown', ]); +const FAILED_ERROR_CODES = new Set([ + 'plugin_reservation_expired', + 'plugin_reservation_unavailable', + 'web_search_provider_rejected', + 'web_search_rate_limited', + 'web_search_request_invalid', +]); +const SUBMISSION_UNKNOWN_ERROR_CODES = new Set([ + 'web_search_result_invalid', + 'web_search_submission_unknown', +]); type FetchImplementation = typeof fetch; type AccessTokenGetter = typeof getValidWorksSquareAccessToken; @@ -163,9 +174,12 @@ function billing(value: unknown): WebSearchServerBillingReceipt { || !['reserved', 'dispatched', 'settled', 'released', 'expired', 'pending_review', 'refunded'].includes(status) || typeof reservedPoints !== 'string' || !DECIMAL.test(reservedPoints) - || (value.actual_points !== null && value.actual_points !== undefined && actualPoints === undefined) + || (value.actual_points !== null && value.actual_points !== undefined + && (actualPoints === undefined || !DECIMAL.test(actualPoints))) || (value.usage_amount !== null && value.usage_amount !== undefined && usageAmount === undefined) + || (usageAmount !== undefined && usageAmount !== 1) || (['settled', 'refunded'].includes(status) && actualPoints === undefined) + || (!['settled', 'refunded'].includes(status) && actualPoints !== undefined) || value.unit !== 'search_request') { throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search billing receipt is invalid'); } @@ -185,12 +199,35 @@ function source(value: unknown): WebSearchSource { } const title = boundedText(value.title, MAX_SOURCE_TITLE_LENGTH); const url = boundedText(value.url, MAX_SOURCE_URL_LENGTH); - if (!title || !url) { + if (!title || !url || !safeSourceUrl(url)) { throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search source is invalid'); } return { title, url }; } +function safeSourceUrl(value: string): boolean { + if (!/^https?:\/\//iu.test(value)) return false; + let parsed: URL; + try { + parsed = new URL(value); + } catch { + return false; + } + if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') + || !parsed.hostname + || parsed.username.length > 0 + || parsed.password.length > 0) { + return false; + } + const schemeSeparator = value.indexOf('//'); + const authorityEnd = value.slice(schemeSeparator + 2).search(/[/?#]/u); + const authority = value.slice( + schemeSeparator + 2, + authorityEnd < 0 ? value.length : schemeSeparator + 2 + authorityEnd, + ); + return !authority.includes('@'); +} + function read(value: unknown, request: WebSearchRequest): WebSearchRead { if (!isRecord(value) || !exactKeys( value, @@ -218,6 +255,7 @@ function read(value: unknown, request: WebSearchRequest): WebSearchRead { const parsedErrorCode = value.error_code === undefined || value.error_code === null ? null : errorCode(value.error_code); + const parsedBilling = billing(value.billing); if (!executionId || !releaseId || !logicalOperationId || !status || releaseId !== request.releaseId || logicalOperationId !== request.logicalOperationId || (value.answer !== undefined && value.answer !== null && answer === null) @@ -225,7 +263,17 @@ function read(value: unknown, request: WebSearchRequest): WebSearchRead { || !Array.isArray(rawSearchQueries) || rawSearchQueries.length > 8 || (value.error_code !== undefined && value.error_code !== null && parsedErrorCode === null) || (value.retry_after_seconds !== null && value.retry_after_seconds !== undefined && retryAfter === undefined) - || (retryAfter !== undefined && (status !== 'failed' || parsedErrorCode !== 'web_search_rate_limited'))) { + || (retryAfter !== undefined && (status !== 'failed' || parsedErrorCode !== 'web_search_rate_limited')) + || ((status === 'reserved' && parsedBilling.status !== 'reserved') + || (status === 'dispatched' && parsedBilling.status !== 'dispatched') + || (status === 'succeeded' && !['settled', 'refunded'].includes(parsedBilling.status)) + || (status === 'failed' && !['released', 'expired'].includes(parsedBilling.status)) + || (status === 'submission_unknown' && parsedBilling.status !== 'pending_review') + || (status === 'pending_review' && parsedBilling.status !== 'pending_review')) + || ((status === 'succeeded' || status === 'pending_review') && parsedBilling.usage_amount !== 1) + || (status === 'failed' && (parsedErrorCode === null || !FAILED_ERROR_CODES.has(parsedErrorCode))) + || (status === 'submission_unknown' + && (parsedErrorCode === null || !SUBMISSION_UNKNOWN_ERROR_CODES.has(parsedErrorCode)))) { throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search response is invalid'); } const sources = rawSources.map(source); @@ -234,7 +282,7 @@ function read(value: unknown, request: WebSearchRequest): WebSearchRead { throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search response is invalid'); } if (status === 'succeeded' || status === 'pending_review') { - if (!answer || (status === 'pending_review' && parsedErrorCode !== null)) { + if (!answer || sources.length === 0 || parsedErrorCode !== null) { throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search result is incomplete'); } } else if (answer !== null || sources.length > 0 || searchQueries.length > 0) { @@ -254,7 +302,7 @@ function read(value: unknown, request: WebSearchRequest): WebSearchRead { searchQueries: searchQueries as string[], errorCode: parsedErrorCode, ...(retryAfter === undefined ? {} : { retryAfterSeconds: retryAfter }), - billing: billing(value.billing), + billing: parsedBilling, }; } @@ -270,11 +318,39 @@ async function readBoundedJson(response: Response): Promise { await response.body?.cancel().catch(() => undefined); throw new WebSearchClientError('plugin_backend_response_too_large', 502, false, 'Web Search response exceeds its bound'); } - const bytes = new Uint8Array(await response.arrayBuffer()); - if (bytes.byteLength > MAX_JSON_BYTES) { - throw new WebSearchClientError('plugin_backend_response_too_large', 502, false, 'Web Search response exceeds its bound'); + if (!response.body) return null; + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let length = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (!(value instanceof Uint8Array)) { + throw new WebSearchClientError('plugin_backend_invalid', 502, false, 'Web Search response is invalid'); + } + length += value.byteLength; + if (length > MAX_JSON_BYTES) { + await reader.cancel().catch(() => undefined); + throw new WebSearchClientError( + 'plugin_backend_response_too_large', + 502, + false, + 'Web Search response exceeds its bound', + ); + } + chunks.push(new Uint8Array(value)); + } + } finally { + reader.releaseLock(); + } + if (length === 0) return null; + const bytes = new Uint8Array(length); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; } - if (bytes.byteLength === 0) return null; try { return JSON.parse(Buffer.from(bytes).toString('utf8')) as unknown; } catch { @@ -356,15 +432,25 @@ function domainMessage(code: string, status: number): string { } async function domainError(response: Response): Promise { + const retryAfter = response.status === 429 ? retryAfterHeader(response) : undefined; + if (response.status === 429) { + await response.body?.cancel().catch(() => undefined); + return new WebSearchClientError( + 'web_search_rate_limited', + 429, + false, + domainMessage('web_search_rate_limited', 429), + retryAfter, + ); + } let payload: unknown = null; try { payload = await readBoundedJson(response); - } catch (error) { - if (error instanceof WebSearchClientError) return error; + } catch { + // Preserve the HTTP-status fallback when the bounded error body is unusable. } const detail = isRecord(payload) && isRecord(payload.detail) ? payload.detail : null; - const code = errorCode(detail?.error_code) ?? (response.status === 429 ? 'web_search_rate_limited' : 'plugin_backend_unavailable'); - const retryAfter = response.status === 429 ? retryAfterHeader(response) : undefined; + const code = errorCode(detail?.error_code) ?? 'plugin_backend_unavailable'; const retryable = response.status >= 500 && response.status !== 401; return new WebSearchClientError( code, diff --git a/tests/unit/web-search-client.test.ts b/tests/unit/web-search-client.test.ts index 89a1722..1a6bbfe 100644 --- a/tests/unit/web-search-client.test.ts +++ b/tests/unit/web-search-client.test.ts @@ -10,6 +10,7 @@ const RELEASE_ID = '22222222-2222-4222-8222-222222222222'; const ADMISSION_ID = 'admission-a'; const PROJECT_ID = '33333333-3333-4333-8333-333333333333'; const LOGICAL_OPERATION_ID = 'pi:run-a:resource-a'; +const MAX_JSON_BYTES = 1_048_576; function search(overrides: Record = {}) { return { @@ -126,7 +127,7 @@ describe('WebSearchClient', () => { }); it('matches the frozen server source title and URL bounds exactly', async () => { - const maximumUrl = `https://example.test/${'x'.repeat(4_096 - 'https://example.test/'.length)}`; + const maximumUrl = `https://example.test/${'x'.repeat(2_048 - 'https://example.test/'.length)}`; await expect(new WebSearchClient({ fetchImpl: vi.fn().mockResolvedValue(jsonResponse(search({ sources: [{ title: 't'.repeat(240), url: maximumUrl }], @@ -146,6 +147,25 @@ describe('WebSearchClient', () => { }); }); + it.each([ + 'http://user:password@example.test/source', + 'javascript:alert(1)', + '//example.test/source', + 'https:example.test/source', + 'https://example.test/' + 'x'.repeat(2_049 - 'https://example.test/'.length), + ])('rejects an unsafe or overlong source URL: %s', async (url) => { + const client = new WebSearchClient({ + fetchImpl: vi.fn().mockResolvedValue(jsonResponse(search({ + sources: [{ title: 'Source', url }], + }))), + getAccessToken: vi.fn(async () => 'token') as never, + }); + + await expect(client.search(input())).rejects.toMatchObject({ + code: 'plugin_backend_invalid', status: 502, retryable: false, + }); + }); + it('preserves only a bounded Retry-After for a known rate limit', async () => { const fetchImpl = vi.fn().mockResolvedValue(jsonResponse( { detail: { error_code: 'web_search_rate_limited', message: 'try later' } }, @@ -162,6 +182,38 @@ describe('WebSearchClient', () => { }); }); + it('uses HTTP 429 as the authoritative non-retryable rate-limit result', async () => { + const responses = [ + new Response('{malformed', { + status: 429, + headers: { 'retry-after': '31' }, + }), + new Response('x'.repeat(MAX_JSON_BYTES + 1), { + status: 429, + headers: { 'retry-after': '31' }, + }), + jsonResponse( + { detail: { error_code: 'plugin_provider_unavailable', message: 'private' } }, + 429, + { 'retry-after': '31' }, + ), + ]; + + for (const response of responses) { + const client = new WebSearchClient({ + fetchImpl: vi.fn().mockResolvedValue(response), + getAccessToken: vi.fn(async () => 'token') as never, + }); + + await expect(client.search(input())).rejects.toMatchObject({ + code: 'web_search_rate_limited', + status: 429, + retryable: false, + retryAfterSeconds: 31, + }); + } + }); + it('projects a server-side rate-limit result and does not trust Retry-After on other statuses', async () => { const rateLimited = vi.fn().mockResolvedValue(jsonResponse(search({ status: 'failed', @@ -207,6 +259,101 @@ describe('WebSearchClient', () => { }); }); + it('bounds a chunked response before buffering the complete body', async () => { + let cancelled = false; + let chunks = 0; + const stream = new ReadableStream({ + pull(controller) { + controller.enqueue(new Uint8Array(chunks++ === 0 ? MAX_JSON_BYTES : 1)); + }, + cancel() { + cancelled = true; + }, + }); + const response = new Response(stream, { status: 200 }); + const arrayBuffer = vi.spyOn(response, 'arrayBuffer'); + const client = new WebSearchClient({ + fetchImpl: vi.fn().mockResolvedValue(response), + getAccessToken: vi.fn(async () => 'token') as never, + }); + + await expect(client.search(input())).rejects.toMatchObject({ + code: 'plugin_backend_response_too_large', status: 502, retryable: false, + }); + expect(arrayBuffer).not.toHaveBeenCalled(); + expect(cancelled).toBe(true); + }); + + it('rejects incoherent search, billing, and error pairings', async () => { + const invalidPayloads = [ + search({ + billing: { + mode: 'platform_metered', status: 'settled', reserved_points: '1.00', + actual_points: '1', usage_amount: 1, unit: 'search_request', + }, + }), + search({ + billing: { + mode: 'platform_metered', status: 'settled', reserved_points: '1.00', + actual_points: '1.000', usage_amount: 1, unit: 'search_request', + }, + }), + search({ + billing: { + mode: 'platform_metered', status: 'settled', reserved_points: '1.00', + actual_points: '1.00', usage_amount: 2, unit: 'search_request', + }, + }), + search({ + billing: { + mode: 'platform_metered', status: 'settled', reserved_points: '1.00', + actual_points: '1.00', unit: 'search_request', + }, + }), + search({ sources: [] }), + search({ + status: 'pending_review', + billing: { + mode: 'platform_metered', status: 'pending_review', reserved_points: '1.00', + usage_amount: 1, unit: 'search_request', + }, + sources: [], + }), + search({ + billing: { + mode: 'platform_metered', status: 'released', reserved_points: '1.00', + unit: 'search_request', + }, + }), + search({ + status: 'failed', answer: null, sources: [], search_queries: [], error_code: null, + billing: { + mode: 'platform_metered', status: 'released', reserved_points: '1.00', + usage_amount: 1, unit: 'search_request', + }, + }), + search({ + status: 'submission_unknown', answer: null, sources: [], search_queries: [], + error_code: 'web_search_rate_limited', + billing: { + mode: 'platform_metered', status: 'pending_review', reserved_points: '1.00', + usage_amount: 1, unit: 'search_request', + }, + }), + ]; + + for (const payload of invalidPayloads) { + const client = new WebSearchClient({ + fetchImpl: vi.fn().mockResolvedValue(jsonResponse(payload)), + getAccessToken: vi.fn(async () => 'token') as never, + }); + + await expect(client.search(input())).rejects.toMatchObject({ + code: 'plugin_backend_invalid', status: 502, retryable: false, + }); + } + }); + it('accepts omitted nullable/default response fields while keeping the object closed', async () => { const payload = search(); delete payload.answer; @@ -215,6 +362,7 @@ describe('WebSearchClient', () => { delete payload.error_code; delete payload.retry_after_seconds; payload.status = 'failed'; + payload.error_code = 'web_search_request_invalid'; payload.billing = { mode: 'platform_metered', status: 'released', reserved_points: '1.00', unit: 'search_request', }; @@ -222,7 +370,7 @@ describe('WebSearchClient', () => { fetchImpl: vi.fn().mockResolvedValue(jsonResponse(payload)), getAccessToken: vi.fn(async () => 'token') as never, }).search(input())).resolves.toMatchObject({ - status: 'failed', answer: null, sources: [], searchQueries: [], errorCode: null, + status: 'failed', answer: null, sources: [], searchQueries: [], errorCode: 'web_search_request_invalid', }); }); From 4de3feefe451dc34dc46b323e2ea5e0b4e4840e8 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 03:55:34 +0800 Subject: [PATCH 10/47] docs(web-search): record client remediation integration --- ...-web-search-client-integration-7d2f5b94.md | 11 +- ...earch-rev01-client-remediation-e4a7c9b2.md | 105 ------------------ 2 files changed, 10 insertions(+), 106 deletions(-) delete mode 100644 .project-docs/30-worklog/tasks/20260901-web-search-rev01-client-remediation-e4a7c9b2.md diff --git a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md index 980e090..94d1110 100644 --- a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md +++ b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md @@ -43,6 +43,14 @@ - Product commit: `60e6a8eeb35431b0f1fb569af4d1502683f16317`; source and product trees are exact-equal at `d570c40f2ad315c8a314f831fb4337300ae28fc2` before this Integration Documentation Gate removes the duplicate foreign task record. - Delivered production composition registration, the dynamic Web Search parent/child/current-authority proof, a generic closed billing-status timeline projection, package-main-reachable route/receipt proof with Provider-authority exclusion, and current README documentation. No Web Search payload-specific Renderer or static tool allowlist was added. - The clean source branch retains its complete task record. This coordinator checkpoint removes only the duplicate cherry-picked copy and records the accepted facts in canonical project memory. +- REV-01 fixed-range review over `0a86ec825a5803bf7e037d3b23c39be23238c43d...4a1e5d31213191a0102eab9278dd9887ba8738f4` completed with Standards PASS and four actionable Spec findings confined to the `WebSearchClient` response boundary. + - Standards task `20260901-web-search-rev01-client-standards-c3e9a6f4` reported zero documented-standard or Fowler-smell findings. + - Spec task `20260901-web-search-rev01-client-spec-d4f1b7a5` identified status-first 429 handling, streamed response bounding, closed response-state/billing invariants, and absolute HTTP(S) no-userinfo source URL validation. +- The sole client remediation task `20260901-web-search-rev01-client-remediation-e4a7c9b2` was integrated from exact frontier `4a1e5d31213191a0102eab9278dd9887ba8738f4`. + - Source commit: `7dcfc9b0a951fd96c3d283c9cb0f2118f922c41a` (sole parent `4a1e5d31213191a0102eab9278dd9887ba8738f4`). + - Product commit: `49de82c4860fd0b377070279b6411237dc6b9564`; source and product trees are exact-equal at `d40e90619406b822485ef2729e373fc4587a42d1` before this Integration Documentation Gate removes the duplicate foreign task record. + - The remediation is limited to `electron/services/web-search-client.ts` and its focused tests. It makes HTTP 429 non-retryable from status before bounded body parsing, enforces a true streamed 1 MiB bound, closes result/billing/error combinations, and accepts only bounded absolute HTTP(S) source URLs without userinfo. + - The clean source branch retains its complete task record. This coordinator checkpoint removes only its cherry-picked duplicate and advances the frontier for fresh fixed-range review. ## Verification @@ -57,10 +65,11 @@ - MLW-03 TDD started with four exact focused failures and finished with 4 files / 40 tests passed; the adjacent Web Search/admission/composition/lifecycle/Pi/timeline/artifact suite passed 10 files / 82 tests. - MLW-03 full unit suite passed 214 files / 1,751 tests with two staged-runtime skips; pressure passed 1/1. Typecheck, scoped lint, full lint (zero errors and the unchanged five warnings), Vite builds, Electron Windows 6/6, and target Marketplace/Project Plugins/Skill E2E 4/4 passed. - Clean source HEAD Windows packaging and verification passed. `verify:artifact:win` embedded exact commit `52f6a0b148403c822a09880467f81416a181b259`; installer size was 208,252,221 bytes with SHA-256 `2D027DB5BE00336F1EB45D882519F971EB791014A1930C5FBED93FE6B15B6303`. `verify:artifact:pi` passed the real app.asar Web Search route/receipt/provider-authority proof; inherited Pi cross-platform/real-provider waivers remain unchanged. +- Client remediation TDD recorded eight exact red cases and finished with `16/16` focused tests; adjacent response/admission tests passed `27/27`. The full unit suite passed `1,759` tests with two staged-runtime skips and pressure passed `1/1`; typecheck passed and lint reported zero errors plus the unchanged five warnings. Source diff, documentation, sole-parent, clean-worktree, and `READY_FOR_INTEGRATION` gates passed. ## Follow-ups -- Run fixed-range Standards and Spec review from client base `0a86ec825a5803bf7e037d3b23c39be23238c43d` through the clean post-MLW-03 documentation frontier; use one remediation owner only if either axis reports an actionable finding. +- Run fresh fixed-range Standards and Spec review from client base `0a86ec825a5803bf7e037d3b23c39be23238c43d` through the clean post-remediation documentation frontier. XWS-01 remains closed until both fresh axes and both repositories pass. - Keep real PostgreSQL, the live paid OpenAI XWS-01 group, and production activation closed pending their explicit external inputs and user authorization. ## Promotion Candidates diff --git a/.project-docs/30-worklog/tasks/20260901-web-search-rev01-client-remediation-e4a7c9b2.md b/.project-docs/30-worklog/tasks/20260901-web-search-rev01-client-remediation-e4a7c9b2.md deleted file mode 100644 index f6de240..0000000 --- a/.project-docs/30-worklog/tasks/20260901-web-search-rev01-client-remediation-e4a7c9b2.md +++ /dev/null @@ -1,105 +0,0 @@ -# Task: REV-01 Client WebSearchClient remediation - -## Identity - -- Task ID: 20260901-web-search-rev01-client-remediation-e4a7c9b2 -- Mode: Feature -- Branch: codex/20260901-web-search-rev01-client-remediation-e4a7c9b2-web-search-rev01-client-remediation -- Worktree: D:\Datas\OthersProjects\makelore-web-search-rev01-client-remediation-e4a7c9b2 -- Base commit: 4a1e5d31213191a0102eab9278dd9887ba8738f4 -- Owner: web-search-rev01-client-remediation -- Status: Ready for Integration - -## Scope - -- Remediate the four accepted REV-01 Client Spec-axis findings from the exact - coordinator frontier `4a1e5d31213191a0102eab9278dd9887ba8738f4`. -- Ownership is limited to `electron/services/web-search-client.ts`, its - focused Web Search client tests, and this task record. No Server, - composition, Pi, Package Store, Renderer, or shared product-scope changes. -- Preserve the fixed typed route, trusted admission, same logical-operation - replay, closed capability envelope, and external PostgreSQL/live OpenAI/ - production-activation holds. - -## Intent And Constraints - -- Project Context Loaded: - - Task ID: `20260901-web-search-rev01-client-remediation-e4a7c9b2` - - Mode: Feature - - Branch: `codex/20260901-web-search-rev01-client-remediation-e4a7c9b2-web-search-rev01-client-remediation` - - Worktree: `D:\Datas\OthersProjects\makelore-web-search-rev01-client-remediation-e4a7c9b2` - - Base commit: `4a1e5d31213191a0102eab9278dd9887ba8738f4` - - Other active local tasks: client coordinator, MLW-01/02/03 source tasks, - and the read-only REV-01 client/server review tasks. Their records were - inspected for scope; no product writer overlaps this remediation. - - Overlap/semantic assessment: the four findings are confined to the - WebSearchClient response boundary and have one authorized owner. No - semantic conflict with server DTO, composition, or Package Store scope. -- Read: client `AGENTS.md`; complete `maintain-project-docs`, `implement-spec`, - and `tdd` skills; project entry files; client architecture/domain/decision, - evidence/reflection/commitment/stale indexes; the Web Search design and - implementation Spec; REV-01 client Spec and Standards task records. -- Concurrent Task Gate: Passed. `check_project_docs.py` passed; task_context - created the isolated worktree and `status --json` matches this task ID, - owner, worktree, branch, and exact base. -- Planning Gate: Passed. The fixed client review identified exactly four - actionable response-boundary findings; the current plan remains within the - accepted ownership and does not alter the frozen contract. -- Implementation plan: - 1. Add public-seam tests that fail for status/body precedence, streamed - response bounds, closed status/receipt/result pairings, and source URL - validation. - 2. Make the smallest WebSearchClient changes to pass each red test: status - first with bounded Retry-After, chunked streaming cap, closed receipt and - result invariants, and absolute HTTP(S) URL validation without userinfo. - 3. Run focused and adjacent tests, typecheck, lint, diff/doc gates; then - update this record and create one source commit only after a clean handoff. - -## Outcome - -- Implemented the four accepted WebSearchClient response-boundary fixes: - HTTP-status-first 429 handling with bounded Retry-After, a true streamed - response cap, closed search/billing/error pairings, and safe bounded source - URLs. No file outside the owned client service/test/task-record scope was - changed. - -## Verification - -- Concurrent/Planning gates passed. -- TDD RED: the new focused suite exposed 8 failures in 16 tests for unsafe - URLs, 429 body precedence, chunked buffering, malformed receipts/pairings, - and the now-invalid failed-without-error fixture. -- TDD GREEN: `pnpm exec vitest run tests/unit/web-search-client.test.ts` - passed 16/16. -- Adjacent: `pnpm exec vitest run tests/unit/web-search-client.test.ts - tests/unit/web-search-plugin-adapter.test.ts` passed 27/27. -- `pnpm run typecheck` passed; owned-file ESLint passed with zero errors. -- Full `pnpm test` passed: 214 files, 1,759 tests passed, 2 skipped; the - pressure suite passed 1/1. Full `pnpm run lint:check` passed with zero - errors and five pre-existing warnings outside this task's ownership. -- Task-aware doc drift and source diff checks passed; the final source commit - and clean `task_context` handoff are recorded below. - -## Handoff - -- Source commit: the final task `HEAD` handed off to the coordinator; its - sole parent is the exact coordinator frontier - `4a1e5d31213191a0102eab9278dd9887ba8738f4`. -- `task_context complete` passed with `READY_FOR_INTEGRATION`; this source - commit is the sole integration candidate. - -## Follow-ups - -- Coordinator must integrate only the final source commit and trigger a fresh - fixed-range Standards/Spec review. - -## Promotion Candidates - -- Target: client integration coordinator and fresh REV-01 review checkpoint. - Proposal: preserve HTTP-status-first semantics, enforce a real 1 MiB stream - cap, reject incoherent closed receipts/results, and reject unsafe source URLs. - Evidence: REV-01 Client Spec findings and new public-seam regressions. - Future impact: prevents malformed responses from overriding server status, - unbounded chunked buffering, ungrounded success results, or provider URL - leakage. Semantic conflicts: none; human confirmation: not required for this - in-scope remediation. From b9a1441cc8b0e4016f6cec75de78adbdecbed6eb Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 05:16:09 +0800 Subject: [PATCH 11/47] docs(web-search): record client review and artifact proof --- .../20260831-web-search-client-integration-7d2f5b94.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md index 94d1110..2bb9094 100644 --- a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md +++ b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md @@ -51,6 +51,10 @@ - Product commit: `49de82c4860fd0b377070279b6411237dc6b9564`; source and product trees are exact-equal at `d40e90619406b822485ef2729e373fc4587a42d1` before this Integration Documentation Gate removes the duplicate foreign task record. - The remediation is limited to `electron/services/web-search-client.ts` and its focused tests. It makes HTTP 429 non-retryable from status before bounded body parsing, enforces a true streamed 1 MiB bound, closes result/billing/error combinations, and accepts only bounded absolute HTTP(S) source URLs without userinfo. - The clean source branch retains its complete task record. This coordinator checkpoint removes only its cherry-picked duplicate and advances the frontier for fresh fixed-range review. +- Fresh R2 review over `0a86ec825a5803bf7e037d3b23c39be23238c43d...4de3feefe451dc34dc46b323e2ea5e0b4e4840e8` completed with both axes PASS and zero actionable findings. + - Standards task `20260901-web-search-rev01-client-standards-r2-7a2e4c91` reviewed the full range and remediation boundary with zero Standards/Fowler findings. + - Spec task `20260901-web-search-rev01-client-spec-r2-6b7e4a2c` confirmed all four response-boundary findings closed and rechecked admission, reconciliation, trusted Main identity, envelope, parent-only materialization, composition, package proof, and exclusions. +- The exact reviewed head `4de3feefe451dc34dc46b323e2ea5e0b4e4840e8` was packaged locally for Windows without publish. Artifact verification embedded the same exact commit and retained fail-closed production trust; no live Provider or production credential was used. ## Verification @@ -66,10 +70,11 @@ - MLW-03 full unit suite passed 214 files / 1,751 tests with two staged-runtime skips; pressure passed 1/1. Typecheck, scoped lint, full lint (zero errors and the unchanged five warnings), Vite builds, Electron Windows 6/6, and target Marketplace/Project Plugins/Skill E2E 4/4 passed. - Clean source HEAD Windows packaging and verification passed. `verify:artifact:win` embedded exact commit `52f6a0b148403c822a09880467f81416a181b259`; installer size was 208,252,221 bytes with SHA-256 `2D027DB5BE00336F1EB45D882519F971EB791014A1930C5FBED93FE6B15B6303`. `verify:artifact:pi` passed the real app.asar Web Search route/receipt/provider-authority proof; inherited Pi cross-platform/real-provider waivers remain unchanged. - Client remediation TDD recorded eight exact red cases and finished with `16/16` focused tests; adjacent response/admission tests passed `27/27`. The full unit suite passed `1,759` tests with two staged-runtime skips and pressure passed `1/1`; typecheck passed and lint reported zero errors plus the unchanged five warnings. Source diff, documentation, sole-parent, clean-worktree, and `READY_FOR_INTEGRATION` gates passed. +- Fresh Client R2 Standards and Spec reviews both passed with zero findings. The exact-head Windows build/package passed; `verify:artifact:win`, `verify:artifact:pi`, and `verify:publish-runtime` passed. The installer is 208,252,520 bytes with SHA-256 `2492F88BB6F813834ECD24B392EB8337220E131E746547851393E4885C4B5BEF`; `app.asar` is 131,258,191 bytes with SHA-256 `27EFABBB741F0A62CB58452801DD1D8893B8E5131EA1D30F74D610DEAD3F7A1D`. The inherited Pi cross-platform/real-provider evidence remains an explicit partial-pass waiver, not a Web Search failure. ## Follow-ups -- Run fresh fixed-range Standards and Spec review from client base `0a86ec825a5803bf7e037d3b23c39be23238c43d` through the clean post-remediation documentation frontier. XWS-01 remains closed until both fresh axes and both repositories pass. +- Client implementation and fixed-range review are complete. XWS-01 remains externally blocked because its twelve live groups require disposable PostgreSQL 16 and an exact packaged signed-in environment; do not substitute SQLite. The paid OpenAI group and production activation remain separate explicit HOLDs. - Keep real PostgreSQL, the live paid OpenAI XWS-01 group, and production activation closed pending their explicit external inputs and user authorization. ## Promotion Candidates From ae81949a49d7df3be4859e6c111235a991a504e4 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 08:32:19 +0800 Subject: [PATCH 12/47] docs(web-search): close client integration ledger --- .project-docs/30-worklog/current-state.md | 11 ++++++++--- ...20260831-web-search-client-integration-7d2f5b94.md | 8 +++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index b6135c4..e654991 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -6,16 +6,21 @@ This file is the integrated default-branch snapshot. Feature tasks record progre - Native Web Search client tickets MLW-01 through MLW-03 are integrated by task `20260831-web-search-client-integration-7d2f5b94` through product commit - `60e6a8eeb35431b0f1fb569af4d1502683f16317`, against frozen Works Square DTO - frontier `a49c696ebc4213e3d62ece780961efbe17576f8e`. MakeLore now reuses the shared + `49de82c4860fd0b377070279b6411237dc6b9564`; the final fixed-range Standards and + Spec reviews passed at `4de3feefe451dc34dc46b323e2ea5e0b4e4840e8` with zero + actionable findings, against frozen Works Square DTO frontier + `a49c696ebc4213e3d62ece780961efbe17576f8e`. MakeLore now reuses the shared hosted Admission resolver, calls the single fixed Web Search typed route from Electron Main, materializes the signed Marketplace Skill/tool only in an eligible frozen parent Pi snapshot, and renders the closed billing receipt without parsing Web Search payloads in Renderer. The packaged Windows app proves that the route and receipt parser are main-reachable while OpenAI Provider authority is absent. + The exact reviewed Windows package passed artifact verification; installer SHA-256 + is `2492F88BB6F813834ECD24B392EB8337220E131E746547851393E4885C4B5BEF` and + `app.asar` SHA-256 is `27EFABBB741F0A62CB58452801DD1D8893B8E5131EA1D30F74D610DEAD3F7A1D`. Real PostgreSQL and paid OpenAI acceptance remain external HOLDs; production activation still requires the official Ed25519 key, OpenAI key/model, price, and - privacy copy. + privacy copy. No deployment, publication, push, or PR occurred. - Human-authorized takeover task `20260831-promote-main-merge-5e9c7a31` completed the already-started local `main` merge as `03a9e866d4366e0a1cb416e26b424fe981071310`, recorded the transfer in diff --git a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md index 2bb9094..2672dd5 100644 --- a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md +++ b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md @@ -8,7 +8,7 @@ - Worktree: D:\Datas\OthersProjects\makelore-web-search-client-integration-7d2f5b94 - Base commit: 0a86ec825a5803bf7e037d3b23c39be23238c43d - Owner: web-search-client-integrator -- Status: In Progress +- Status: Ready for Integration ## Scope @@ -27,6 +27,12 @@ ## Outcome +- Final coordinator closure: product frontier `49de82c4860fd0b377070279b6411237dc6b9564`, + reviewed frontier `4de3feefe451dc34dc46b323e2ea5e0b4e4840e8`, and pre-closure + documentation frontier `b9a1441cc8b0e4016f6cec75de78adbdecbed6eb` are reconciled in + canonical current state. Both review axes passed with zero findings; exact-head + package verification passed and the external PostgreSQL, paid OpenAI, and + production-activation HOLDs remain explicit. - Coordinator gate and MLW-00 adoption complete. The coordinator ledger checkpoint advanced the implementation frontier from base `0a86ec825a5803bf7e037d3b23c39be23238c43d` to `c4db68767fcd1916e4441ab0e2c2504210f75e83` without product changes. - MLW-01 source task `20260901-web-search-mlw01-admission-b7d5f3a2` was integrated from exact frontier `c4db68767fcd1916e4441ab0e2c2504210f75e83` after the Server DTO/fault freeze at `a49c696ebc4213e3d62ece780961efbe17576f8e`. - Source commit: `fbeaa8ee8b0a19f240469a289449253034ee3ec3` (sole parent `c4db68767fcd1916e4441ab0e2c2504210f75e83`). From b2c1ad9dc149d7f44bebef618f35f87f22e5ddfb Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 08:36:54 +0800 Subject: [PATCH 13/47] docs(web-search): record client main promotion --- .project-docs/30-worklog/current-state.md | 6 +- ...-web-search-client-integration-7d2f5b94.md | 88 ------------------- ...1-web-search-client-main-merge-5a9d3b82.md | 60 +++++++++++++ 3 files changed, 64 insertions(+), 90 deletions(-) delete mode 100644 .project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md create mode 100644 .project-docs/30-worklog/tasks/20260901-web-search-client-main-merge-5a9d3b82.md diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index e654991..9582dc6 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -4,8 +4,10 @@ This file is the integrated default-branch snapshot. Feature tasks record progre ## Integrated Through -- Native Web Search client tickets MLW-01 through MLW-03 are integrated by task - `20260831-web-search-client-integration-7d2f5b94` through product commit +- Native Web Search client tickets MLW-01 through MLW-03 are integrated by source + coordinator `20260831-web-search-client-integration-7d2f5b94` and promoted to local + `main` by task `20260901-web-search-client-main-merge-5a9d3b82` from reviewed + coordinator closure `ae81949a49d7df3be4859e6c111235a991a504e4` through product commit `49de82c4860fd0b377070279b6411237dc6b9564`; the final fixed-range Standards and Spec reviews passed at `4de3feefe451dc34dc46b323e2ea5e0b4e4840e8` with zero actionable findings, against frozen Works Square DTO frontier diff --git a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md b/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md deleted file mode 100644 index 2672dd5..0000000 --- a/.project-docs/30-worklog/tasks/20260831-web-search-client-integration-7d2f5b94.md +++ /dev/null @@ -1,88 +0,0 @@ -# Task: Coordinate MakeLore native Web Search client integration - -## Identity - -- Task ID: 20260831-web-search-client-integration-7d2f5b94 -- Mode: Integration -- Branch: codex/20260831-web-search-client-integration-7d2f5b94-web-search-client-integration -- Worktree: D:\Datas\OthersProjects\makelore-web-search-client-integration-7d2f5b94 -- Base commit: 0a86ec825a5803bf7e037d3b23c39be23238c43d -- Owner: web-search-client-integrator -- Status: Ready for Integration - -## Scope - -- Serialize repository-local integration of MLW-01 through MLW-03 after the Server WSW-03 typed DTO freezes. -- Validate each isolated source ticket's exact parent, exclusive ownership, single source commit, clean state, and verification before cherry-picking only its final commit. -- Maintain the client integration ledger, fixed review range, review/remediation frontier, package proof, and fake-provider XWS-01 evidence without changing the user root worktree. - -## Intent And Constraints - -- Project Context Loaded: client `AGENTS.md`, complete `maintain-project-docs` skill, project-memory entry points/relevant architecture-domain-evidence records, complete reviewed Web Search design/spec, MLW-00 bootstrap record, and relevant Marketplace/Game Resource task history were read before planning. -- Concurrent Task Gate: Passed. Task-context owner fields exactly match this Integration task/worktree/branch/base and this task holds the repository integration lock. The MLW-00 feature bootstrap remains read-only at its isolated checkpoint; no active client owner has an evidenced overlapping Web Search scope. -- Planning Gate: Passed. Exact integration base is clean `0a86ec825a5803bf7e037d3b23c39be23238c43d`; MLW-00 focused baseline passed `7 files / 84 tests` across Marketplace client, effective resolver, Game Resource client/adapter, capability registry, Pi resource loader, and artifact proof. -- Reuse the existing signed Package Store, effective parent snapshot, hosted admission, Game Resource client/adapter, capability registry, Pi envelope/lifecycle, conversation timeline, and artifact proof; do not create a parallel hosted runtime. -- MLW-01 -> MLW-02 -> MLW-03, but MLW-01 remains blocked until WSW-03 freezes the Server DTO. Renderer owns no account/auth/provider/URL/credential/admission/trust authority. -- Preserve `submission_unknown` as result-less/non-retryable, result-bearing `pending_review`, and Main-only `receipt_unavailable`; no Web Search-specific settings page, `pi-web-search`, arbitrary Pi extension, generic invoke, push, PR, deploy, publish, or real paid OpenAI call. - -## Outcome - -- Final coordinator closure: product frontier `49de82c4860fd0b377070279b6411237dc6b9564`, - reviewed frontier `4de3feefe451dc34dc46b323e2ea5e0b4e4840e8`, and pre-closure - documentation frontier `b9a1441cc8b0e4016f6cec75de78adbdecbed6eb` are reconciled in - canonical current state. Both review axes passed with zero findings; exact-head - package verification passed and the external PostgreSQL, paid OpenAI, and - production-activation HOLDs remain explicit. -- Coordinator gate and MLW-00 adoption complete. The coordinator ledger checkpoint advanced the implementation frontier from base `0a86ec825a5803bf7e037d3b23c39be23238c43d` to `c4db68767fcd1916e4441ab0e2c2504210f75e83` without product changes. -- MLW-01 source task `20260901-web-search-mlw01-admission-b7d5f3a2` was integrated from exact frontier `c4db68767fcd1916e4441ab0e2c2504210f75e83` after the Server DTO/fault freeze at `a49c696ebc4213e3d62ece780961efbe17576f8e`. - - Source commit: `fbeaa8ee8b0a19f240469a289449253034ee3ec3` (sole parent `c4db68767fcd1916e4441ab0e2c2504210f75e83`). - - Product commit: `1edd75e2465ae8bfbba87d8e077365daee144920`; source and product trees are exact-equal at `b17a16ff1c46706980bd162af3678176b3a93e70` before the repository-local Integration Documentation Gate removes the duplicate foreign task record. - - Delivered the provider-neutral `MarketplaceHostedAdmissionResolver` and an admission-only Game Resource refactor. The helper resolves exact installed release/admission identity from the trusted frozen worker snapshot, Package Store, selected channel, Marketplace resolve, current account, and MakeLore version; it owns no Provider, payload, billing, route, Web Search client, composition, Renderer, or Pi behavior. - - The clean source branch retains its complete task record. This coordinator checkpoint removes only the duplicate cherry-picked copy and records integration evidence here. -- MLW-02 source task `20260901-web-search-mlw02-client-c8e4a2d1` was integrated from exact frontier `f3874e90706692094f0f22fca465923143e23c7e`. - - Source commit: `94f98e03b9e4e59eb2de07aa9d0e68ec0c74de8e` (sole parent `f3874e90706692094f0f22fca465923143e23c7e`). - - Product commit: `fc68cf295131d8f73556bef59fdeae61239a649e`; source and product trees are exact-equal at `5c14e92224e8441d18a983dd7099cd6d76f83e85` before this Integration Documentation Gate removes the duplicate foreign task record. - - Delivered the Main-only closed Web Search client, bounded same-operation reconciliation, code-owned adapter, and the minimal generic billing/conversation/Registry support for `not_started` and Main-only `receipt_unavailable`. The adapter uses the MLW-01 admission helper and trusted project/request identity; no composition, Renderer, Pi, Package Store/effective resolver, Provider authority, or server path changed. - - Final source review aligned source title/URL bounds with the frozen Server DTO and preserved both MLW-01 typed admission failures. The clean source branch retains its complete task record; integration evidence is consolidated here. -- MLW-03 source task `20260901-web-search-mlw03-composition-d9f5b3e2` was integrated from exact frontier `68cb2e73beb17d5041198d020aaa7b2884124950`. - - Source commit: `52f6a0b148403c822a09880467f81416a181b259` (sole parent `68cb2e73beb17d5041198d020aaa7b2884124950`). - - Product commit: `60e6a8eeb35431b0f1fb569af4d1502683f16317`; source and product trees are exact-equal at `d570c40f2ad315c8a314f831fb4337300ae28fc2` before this Integration Documentation Gate removes the duplicate foreign task record. - - Delivered production composition registration, the dynamic Web Search parent/child/current-authority proof, a generic closed billing-status timeline projection, package-main-reachable route/receipt proof with Provider-authority exclusion, and current README documentation. No Web Search payload-specific Renderer or static tool allowlist was added. - - The clean source branch retains its complete task record. This coordinator checkpoint removes only the duplicate cherry-picked copy and records the accepted facts in canonical project memory. -- REV-01 fixed-range review over `0a86ec825a5803bf7e037d3b23c39be23238c43d...4a1e5d31213191a0102eab9278dd9887ba8738f4` completed with Standards PASS and four actionable Spec findings confined to the `WebSearchClient` response boundary. - - Standards task `20260901-web-search-rev01-client-standards-c3e9a6f4` reported zero documented-standard or Fowler-smell findings. - - Spec task `20260901-web-search-rev01-client-spec-d4f1b7a5` identified status-first 429 handling, streamed response bounding, closed response-state/billing invariants, and absolute HTTP(S) no-userinfo source URL validation. -- The sole client remediation task `20260901-web-search-rev01-client-remediation-e4a7c9b2` was integrated from exact frontier `4a1e5d31213191a0102eab9278dd9887ba8738f4`. - - Source commit: `7dcfc9b0a951fd96c3d283c9cb0f2118f922c41a` (sole parent `4a1e5d31213191a0102eab9278dd9887ba8738f4`). - - Product commit: `49de82c4860fd0b377070279b6411237dc6b9564`; source and product trees are exact-equal at `d40e90619406b822485ef2729e373fc4587a42d1` before this Integration Documentation Gate removes the duplicate foreign task record. - - The remediation is limited to `electron/services/web-search-client.ts` and its focused tests. It makes HTTP 429 non-retryable from status before bounded body parsing, enforces a true streamed 1 MiB bound, closes result/billing/error combinations, and accepts only bounded absolute HTTP(S) source URLs without userinfo. - - The clean source branch retains its complete task record. This coordinator checkpoint removes only its cherry-picked duplicate and advances the frontier for fresh fixed-range review. -- Fresh R2 review over `0a86ec825a5803bf7e037d3b23c39be23238c43d...4de3feefe451dc34dc46b323e2ea5e0b4e4840e8` completed with both axes PASS and zero actionable findings. - - Standards task `20260901-web-search-rev01-client-standards-r2-7a2e4c91` reviewed the full range and remediation boundary with zero Standards/Fowler findings. - - Spec task `20260901-web-search-rev01-client-spec-r2-6b7e4a2c` confirmed all four response-boundary findings closed and rechecked admission, reconciliation, trusted Main identity, envelope, parent-only materialization, composition, package proof, and exclusions. -- The exact reviewed head `4de3feefe451dc34dc46b323e2ea5e0b4e4840e8` was packaged locally for Windows without publish. Artifact verification embedded the same exact commit and retained fail-closed production trust; no live Provider or production credential was used. - -## Verification - -- Task-context status: exact task, Integration mode, owner, worktree, branch, base, and integration lock matched. -- Adopted MLW-00 baseline: `7 files / 84 tests passed` after reusing same-clean-HEAD dependencies through an ignored worktree-local junction; lockfile and source were unchanged. -- MLW-01 focused admission/Game Resource suites passed `12/12`; adjacent six-file coding-plugin/Game Resource regression passed `74/74`. -- MLW-01 full unit suite passed `212 files / 1728 tests`, with two staged-runtime skips; the pressure suite passed `1/1`. Typecheck passed. Full lint passed with zero errors and the five pre-existing Home/Makelore warnings; owned-file lint/compile, diff, project-docs, and document-drift gates passed. -- The coordinator's optional duplicate focused run stopped before collection because this isolated worktree has no executable `vitest`; no test or product failure occurred and no dependency/source file changed. The Integration Gate instead adopts the clean source's exact-tree test evidence above rather than performing an unrelated dependency installation. -- MLW-02 final focused suite passed `3 files / 35 tests`; adjacent hosted admission, Game Resource, Registry, conversation, Marketplace-client, and timeline regression passed `11 files / 114 tests`. -- MLW-02 full unit suite passed `214 files / 1748 tests`, with two staged-runtime skips; pressure passed `1/1`. Typecheck, owned lint, full lint (zero errors and the same five existing warnings), and Vite Renderer/Main/Preload/utility builds passed. -- MLW-02 diff, project-docs, document-drift, sole-parent, clean-worktree, and `READY_FOR_INTEGRATION` gates passed. No live Provider call or production activation occurred. -- MLW-03 TDD started with four exact focused failures and finished with 4 files / 40 tests passed; the adjacent Web Search/admission/composition/lifecycle/Pi/timeline/artifact suite passed 10 files / 82 tests. -- MLW-03 full unit suite passed 214 files / 1,751 tests with two staged-runtime skips; pressure passed 1/1. Typecheck, scoped lint, full lint (zero errors and the unchanged five warnings), Vite builds, Electron Windows 6/6, and target Marketplace/Project Plugins/Skill E2E 4/4 passed. -- Clean source HEAD Windows packaging and verification passed. `verify:artifact:win` embedded exact commit `52f6a0b148403c822a09880467f81416a181b259`; installer size was 208,252,221 bytes with SHA-256 `2D027DB5BE00336F1EB45D882519F971EB791014A1930C5FBED93FE6B15B6303`. `verify:artifact:pi` passed the real app.asar Web Search route/receipt/provider-authority proof; inherited Pi cross-platform/real-provider waivers remain unchanged. -- Client remediation TDD recorded eight exact red cases and finished with `16/16` focused tests; adjacent response/admission tests passed `27/27`. The full unit suite passed `1,759` tests with two staged-runtime skips and pressure passed `1/1`; typecheck passed and lint reported zero errors plus the unchanged five warnings. Source diff, documentation, sole-parent, clean-worktree, and `READY_FOR_INTEGRATION` gates passed. -- Fresh Client R2 Standards and Spec reviews both passed with zero findings. The exact-head Windows build/package passed; `verify:artifact:win`, `verify:artifact:pi`, and `verify:publish-runtime` passed. The installer is 208,252,520 bytes with SHA-256 `2492F88BB6F813834ECD24B392EB8337220E131E746547851393E4885C4B5BEF`; `app.asar` is 131,258,191 bytes with SHA-256 `27EFABBB741F0A62CB58452801DD1D8893B8E5131EA1D30F74D610DEAD3F7A1D`. The inherited Pi cross-platform/real-provider evidence remains an explicit partial-pass waiver, not a Web Search failure. - -## Follow-ups - -- Client implementation and fixed-range review are complete. XWS-01 remains externally blocked because its twelve live groups require disposable PostgreSQL 16 and an exact packaged signed-in environment; do not substitute SQLite. The paid OpenAI group and production activation remain separate explicit HOLDs. -- Keep real PostgreSQL, the live paid OpenAI XWS-01 group, and production activation closed pending their explicit external inputs and user authorization. - -## Promotion Candidates - -- None recorded. diff --git a/.project-docs/30-worklog/tasks/20260901-web-search-client-main-merge-5a9d3b82.md b/.project-docs/30-worklog/tasks/20260901-web-search-client-main-merge-5a9d3b82.md new file mode 100644 index 0000000..4d6979e --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-web-search-client-main-merge-5a9d3b82.md @@ -0,0 +1,60 @@ +# Task: Merge reviewed Web Search client to main + +## Identity + +- Task ID: 20260901-web-search-client-main-merge-5a9d3b82 +- Mode: Integration +- Branch: codex/20260901-web-search-client-main-merge-5a9d3b82-web-search-client-main-merge +- Worktree: D:\Datas\OthersProjects\makelore-web-search-main-merge-5a9d3b82 +- Base commit: 0a86ec825a5803bf7e037d3b23c39be23238c43d +- Owner: web-search-client-main-merger +- Status: In Progress + +## Scope + +- Promote the complete reviewed native Web Search client chain from released coordinator + closure `ae81949a49d7df3be4859e6c111235a991a504e4` onto local `main` from exact clean + base `0a86ec825a5803bf7e037d3b23c39be23238c43d`. +- Preserve product-tree equality with the reviewed coordinator, replace only the + coordinator-owned ledger copy with this main-merge audit record, and retain package + evidence plus all XWS, paid OpenAI, PostgreSQL and production-activation HOLDs. + +## Intent And Constraints + +- Concurrent Task Gate: Passed. This integration task owns its isolated worktree, + branch, exact base and the repository integration lock. No other active integration + writer exists; the completed root inspection and review/source tasks are read-only. +- Planning Gate: Passed after reading client AGENTS, the maintain-project-docs workflow, + project agent-entry/startup memory, architecture/decisions, the released Web Search + coordinator outcome, and the user-authorized main-merge boundary. +- Integration plan: verify ancestry and root cleanliness; fast-forward only to the + released coordinator closure; remove only the duplicated released coordinator task + record while preserving its source branch; record the main landing here/current-state; + verify product-tree equality, docs drift, diff, clean status and reviewed client tree; + then fast-forward local `main`, complete and release this task. +- Never reset, stash, clean, push, create a PR, deploy, publish, run paid OpenAI, or + represent the external PostgreSQL/XWS groups as passed. + +## Outcome + +- The isolated merge branch fast-forwarded from exact root base + `0a86ec825a5803bf7e037d3b23c39be23238c43d` to released coordinator closure + `ae81949a49d7df3be4859e6c111235a991a504e4` with no conflict or semantic replay. +- The duplicate coordinator task record is removed only from this promotion branch; + its released source branch remains intact. Canonical current state now attributes the + local `main` landing to this integration task while retaining source/product/review, + exact package evidence, and external HOLD facts. + +## Verification + +- Root `main` was clean at `0a86ec825a5803bf7e037d3b23c39be23238c43d`; + that commit is an ancestor of coordinator closure `ae81949a49d7df3be4859e6c111235a991a504e4`. +- The merge branch reached the exact coordinator closure by `git merge --ff-only`. + +## Follow-ups + +- None recorded. + +## Promotion Candidates + +- None recorded. From 7f0e9310a7f394249fab3cadc5c82d70f91cbfeb Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 08:38:00 +0800 Subject: [PATCH 14/47] docs(web-search): finalize client main merge gate --- ...0260901-web-search-client-main-merge-5a9d3b82.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.project-docs/30-worklog/tasks/20260901-web-search-client-main-merge-5a9d3b82.md b/.project-docs/30-worklog/tasks/20260901-web-search-client-main-merge-5a9d3b82.md index 4d6979e..87b095d 100644 --- a/.project-docs/30-worklog/tasks/20260901-web-search-client-main-merge-5a9d3b82.md +++ b/.project-docs/30-worklog/tasks/20260901-web-search-client-main-merge-5a9d3b82.md @@ -8,7 +8,7 @@ - Worktree: D:\Datas\OthersProjects\makelore-web-search-main-merge-5a9d3b82 - Base commit: 0a86ec825a5803bf7e037d3b23c39be23238c43d - Owner: web-search-client-main-merger -- Status: In Progress +- Status: Ready for Integration ## Scope @@ -50,10 +50,19 @@ - Root `main` was clean at `0a86ec825a5803bf7e037d3b23c39be23238c43d`; that commit is an ancestor of coordinator closure `ae81949a49d7df3be4859e6c111235a991a504e4`. - The merge branch reached the exact coordinator closure by `git merge --ff-only`. +- All non-`.project-docs` paths are exact-equal to released coordinator closure + `ae81949a49d7df3be4859e6c111235a991a504e4`; only canonical/task ledger paths differ. +- `check_project_docs`, task-aware `check_doc_drift`, fixed-range `git diff --check`, + and clean worktree checks passed. Exact product-tree equality adopts the reviewed + `1,759 passed / 2 skipped`, pressure `1/1`, typecheck, lint, Windows package and + artifact verification evidence without rebuilding an unchanged product tree. +- No live PostgreSQL or OpenAI command was run. ## Follow-ups -- None recorded. +- Complete the final root `main` fast-forward only if it is still clean at the exact + recorded base, then complete and release this main-merge task. Keep XWS/PostgreSQL, + paid OpenAI and production activation on HOLD. ## Promotion Candidates From 7df245af5a04f62be48980831ff41987ba686009 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 11:11:00 +0800 Subject: [PATCH 15/47] fix(coding): resolve packaged Pi Agent Server dependencies --- ...0901-local-runtime-unavailable-8b42c7f1.md | 67 +++++++++++++++++++ .../coding-runtime/pi/agent-server-process.ts | 8 ++- .../unit/pi-agent-server-process-real.test.ts | 50 +++++++++++++- 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260901-local-runtime-unavailable-8b42c7f1.md diff --git a/.project-docs/30-worklog/tasks/20260901-local-runtime-unavailable-8b42c7f1.md b/.project-docs/30-worklog/tasks/20260901-local-runtime-unavailable-8b42c7f1.md new file mode 100644 index 0000000..c0a9347 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-local-runtime-unavailable-8b42c7f1.md @@ -0,0 +1,67 @@ +# Task: Diagnose local programming runtime unavailable + +## Identity + +- Task ID: 20260901-local-runtime-unavailable-8b42c7f1 +- Mode: Feature +- Branch: codex/20260901-local-runtime-unavailable-8b42c7f1-local-runtime-unavailable-8b42c7f1 +- Worktree: D:\Datas\OthersProjects\makelore-worktrees\local-runtime-unavailable-8b42c7f1 +- Base commit: 7f0e9310a7f394249fab3cadc5c82d70f91cbfeb +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Reproduce the current Makelore Code state that renders `本地编程运行时暂时不可用。` and identify the first failing Renderer, Host API, Pi runtime, packaging, or Provider boundary. +- Build a deterministic, agent-runnable feedback loop for the exact visible symptom before ranking or testing root-cause hypotheses. +- Correlate current source and focused tests with privacy-safe local runtime evidence where available. +- Implement the user-authorized minimal product fix for the confirmed packaged-layout Pi AI ESM resolution failure. +- Add a focused regression at the installed sibling-resource-layout seam, then verify the original process path plus the repository's required checks. + +## Intent And Constraints + +- Treat the visible sentence as a generic safe projection, not as proof of a Pi process failure. +- Preserve ADR-006: Pi `0.84.2` remains the sole production runtime; do not add an OpenCode fallback, dual runtime, compatibility layer, or automatic replay. +- Keep Renderer access behind `/api/coding/*`; runtime, Provider credentials, recovery, and raw diagnostics remain Electron Main-owned. +- Redact credentials, headers, prompts, session content, account identifiers, and complete user project paths from reported evidence. +- Distinguish confirmed facts, evidence-supported inferences, and unverified assumptions. The historical Host-token startup defect is a regression lead, not an assumed cause. +- Keep package resolution anchored to the explicit Main-owned `runtimeRoot`; development and packaged layouts must both continue to work without hard-coded layout branches or a compatibility layer. +- Work only in this isolated task worktree and do not modify peer worktrees or the occupied local `main` worktree. + +## Outcome + +- Confirmed that the installed Makelore application reaches the generic `CODING_RUNTIME_UNAVAILABLE` projection only after the Pi worker fails during `open`. +- The installed Agent Server imports `@earendil-works/pi-ai` with `import.meta.resolve(specifier, runtimePackageUrl)`. In the embedded Node.js `v24.18.1` process, the second `parentURL` argument is not honored unless `--experimental-import-meta-resolve` is enabled, so resolution starts beside `resources/resources/pi-agent-server.mjs` instead of the sibling `resources/pi-runtime/node_modules` tree. +- The dependency is present and healthy: the installed Agent Server matches repository source byte-for-byte, both `@earendil-works/pi-coding-agent` and `@earendil-works/pi-ai` are version `0.84.2`, and the expected ESM export exists. +- Reproduced the installed startup failure twice with the exact executable, script, runtime root, working directory, and environment shape. Both attempts exited with code `1` and `ERR_MODULE_NOT_FOUND` before producing protocol output. +- A one-variable differential adding `--experimental-import-meta-resolve` made the same process exit cleanly without the module error. Importing the verified absolute `pi-runtime/node_modules/@earendil-works/pi-ai/dist/index.js` URL also succeeded without the experimental flag. +- Added `--experimental-import-meta-resolve` only to the Main-owned Agent Server child process. This activates the existing `runtimePackageUrl` parent argument and preserves standard ESM package resolution across both the development layout and packaged `pi-runtime` closure. +- Added a real-process regression that places `pi-agent-server.mjs` under the installed `resources/resources` sibling layout, exposes the real Pi runtime and Pi AI package through a temporary production-shaped root, and starts it with the project's Electron executable. +- The regression failed before the fix with the same `ERR_MODULE_NOT_FOUND` / `PI_RPC_EXITED` chain and passed after the fix. No runtime bundle layout, dependency version, Provider behavior, Renderer contract, installed application file, user data, or peer task content was changed. + +## Verification + +- Current startup-log signal loop: found the missing `@earendil-works/pi-ai` module plus two `PI_WORKER_STOPPED` / `open_failure` events; verdict `RED` as expected for the reported failure. +- Exact installed-process loop: two independent launches reproduced `ExitCode=1`, `ModuleNotFound=true`, `MissingPiAi=true`, and zero protocol bytes within five seconds. +- Resolver differential: the same installed process with only `--experimental-import-meta-resolve` added produced no resolver error. +- Absolute-path differential: importing the fixed runtime-root ESM entry through `pathToFileURL(...)` succeeded in the same embedded Node.js process without the experimental flag. +- Artifact comparison: installed `pi-agent-server.mjs` equals repository source; installed Pi package versions and exports match the expected `0.84.2` runtime closure. +- The existing real-process unit test was inspected and does not cover this boundary because it runs the server from the repository layout, where ancestor lookup can reach the root `node_modules`; the installed sibling `resources/resources` and `resources/pi-runtime` layout is not exercised. +- Red regression: `pnpm exec vitest run tests/unit/pi-agent-server-process-real.test.ts -t "boots from the packaged sibling resource layout" --maxWorkers=1` failed at `pi-agent-server.mjs:22` with `ERR_MODULE_NOT_FOUND` and `PI_RPC_EXITED` before the product change. +- Green regression: the same command passed against Electron Node after the Agent Server launch flag was added. +- Focused verification: 4 relevant test files, 24 tests passed, covering Agent Server behavior, composition paths, Pi runtime bundling, and product artifact rules. +- Existing real-process behavior: both tests in `pi-agent-server-process-real.test.ts` passed, including two logical Conversation threads, process reuse, invalidation, and restart. +- Full unit suite: 215 test files passed; 1,761 tests passed and 2 existing conditional tests were skipped. +- `pnpm run typecheck`: passed. Final changed-file ESLint: passed. +- `pnpm run lint:check`: passed with 0 errors and 5 pre-existing unrelated React warnings. +- `pnpm run build:vite`: passed for Renderer, Electron Main, Preload, and utility worker; only existing bundle/chunk warnings were emitted. +- `git diff --check`: passed; no debug instrumentation or generated artifact is tracked. + +## Follow-ups + +- The currently installed application predates this source change. After integration, rebuild the Windows artifact, run `verify:artifact:win`, `verify:artifact:pi`, and the final packaged Pi proof, then install it before performing the first-conversation UI acceptance. +- When the pinned Electron/Node runtime is upgraded, retain the packaged-layout regression; remove the launch flag only after the new embedded Node accepts the `import.meta.resolve` parent argument without it. + +## Promotion Candidates + +- Target: `current-state` and the Pi release evidence index. Proposal: require an Electron Node Agent Server initialization proof from the installed sibling resource layout, not only source-tree process tests. Evidence: the new layout test reproduced the exact installed `ERR_MODULE_NOT_FOUND` before the fix and passed after enabling the resolver parent URL; 24 focused tests, the full unit suite, typecheck, lint, and production build passed. Future impact: prevents source-root ancestor `node_modules` from masking final-artifact ESM resolution regressions. Semantic conflicts: none with ADR-006 or the existing Pi closure requirement. Human confirmation: not required to integrate the evidence rule; final package installation/release remains an operator gate. diff --git a/electron/coding-runtime/pi/agent-server-process.ts b/electron/coding-runtime/pi/agent-server-process.ts index 2304a3d..15c6075 100644 --- a/electron/coding-runtime/pi/agent-server-process.ts +++ b/electron/coding-runtime/pi/agent-server-process.ts @@ -514,7 +514,13 @@ export class PiAgentServerProcess { const generation = ++this.generation; const child = spawn( this.options.executablePath, - [this.options.serverPath, '--runtime-root', this.options.runtimeRoot], + [ + // Enables the parent URL used to resolve dependencies from the packaged Pi runtime root. + '--experimental-import-meta-resolve', + this.options.serverPath, + '--runtime-root', + this.options.runtimeRoot, + ], { cwd: this.options.runtimeRoot, env: buildPiWorkerEnvironment(this.options.configDir), diff --git a/tests/unit/pi-agent-server-process-real.test.ts b/tests/unit/pi-agent-server-process-real.test.ts index e5accb8..f835674 100644 --- a/tests/unit/pi-agent-server-process-real.test.ts +++ b/tests/unit/pi-agent-server-process-real.test.ts @@ -1,7 +1,8 @@ // @vitest-environment node -import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { copyFile, mkdtemp, mkdir, realpath, rm, symlink, writeFile } from 'node:fs/promises'; import { createServer, type ServerResponse } from 'node:http'; +import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; @@ -11,6 +12,36 @@ import { MAKELORE_DEFAULT_LANGUAGE_PROMPT } from '../../electron/coding-runtime/ import type { PiWorkerProcessOptions } from '../../electron/coding-runtime/pi/worker-process'; const roots: string[] = []; +const electronExecutable = createRequire(path.resolve('package.json'))('electron') as string; + +async function materializePackagedAgentServerLayout(root: string): Promise<{ + configDir: string; + runtimeRoot: string; + serverPath: string; +}> { + const resourcesPath = path.join(root, 'artifact', 'resources'); + const serverPath = path.join(resourcesPath, 'resources', 'pi-agent-server.mjs'); + const runtimeRoot = path.join(resourcesPath, 'pi-runtime'); + const configDir = path.join(root, 'config'); + const piAiRoot = path.join(runtimeRoot, 'node_modules', '@earendil-works', 'pi-ai'); + const [sourceRuntimeRoot, sourcePiAiRoot] = await Promise.all([ + realpath(path.resolve('node_modules/@earendil-works/pi-coding-agent')), + realpath(path.resolve('node_modules/@earendil-works/pi-ai')), + ]); + await Promise.all([ + mkdir(path.dirname(serverPath), { recursive: true }), + mkdir(path.dirname(piAiRoot), { recursive: true }), + mkdir(configDir, { recursive: true }), + ]); + const directoryLinkType = process.platform === 'win32' ? 'junction' : 'dir'; + await Promise.all([ + copyFile(path.resolve('resources/pi-agent-server.mjs'), serverPath), + copyFile(path.join(sourceRuntimeRoot, 'package.json'), path.join(runtimeRoot, 'package.json')), + symlink(path.join(sourceRuntimeRoot, 'dist'), path.join(runtimeRoot, 'dist'), directoryLinkType), + symlink(sourcePiAiRoot, piAiRoot, directoryLinkType), + ]); + return { configDir, runtimeRoot, serverPath }; +} async function startHeldProvider(): Promise<{ baseUrl: string; @@ -85,6 +116,23 @@ afterEach(async () => { }); describe('Pi Agent Server real process', () => { + it('boots from the packaged sibling resource layout', async () => { + const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-layout-')); + roots.push(root); + const layout = await materializePackagedAgentServerLayout(root); + const server = new PiAgentServerProcess({ + executablePath: electronExecutable, + ...layout, + }); + + try { + await expect(server.start()).resolves.toBeUndefined(); + expect(server.processId).toBeTypeOf('number'); + } finally { + await server.stop(); + } + }, 10_000); + it('hosts isolated Conversation threads in one long-lived process', async () => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-')); roots.push(root); From 42ea83c0cbad52432eca99e4161e0360bef3e219 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 11:23:54 +0800 Subject: [PATCH 16/47] docs(coding): integrate packaged Pi runtime fix --- .project-docs/30-worklog/current-state.md | 12 ++++ ...01-integrate-local-runtime-fix-9c4e2a71.md | 51 ++++++++++++++ ...0901-local-runtime-unavailable-8b42c7f1.md | 67 ------------------- .project-docs/50-evidence/evidence-index.md | 1 + .project-docs/80-commitments/commitments.md | 2 +- 5 files changed, 65 insertions(+), 68 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260901-integrate-local-runtime-fix-9c4e2a71.md delete mode 100644 .project-docs/30-worklog/tasks/20260901-local-runtime-unavailable-8b42c7f1.md diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index 9582dc6..f0dd3c2 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -4,6 +4,18 @@ This file is the integrated default-branch snapshot. Feature tasks record progre ## Integrated Through +- Packaged Pi Agent Server resolver fix source `7df245af5a04f62be48980831ff41987ba686009` + from task `20260901-local-runtime-unavailable-8b42c7f1` is integrated by task + `20260901-integrate-local-runtime-fix-9c4e2a71` through merge + `96402551f46d875ba3db0a2f625397aba2f332fb`. Electron Main now starts only the + shared Agent Server with Node's `import.meta.resolve` parent-URL capability enabled, + so the unchanged server script resolves `@earendil-works/pi-ai` from the explicit + packaged `pi-runtime` root instead of the sibling `resources/resources` directory. + A production-shaped sibling-layout test reproduces the installed failure before the + fix and initializes successfully through Electron Node after it; Pi remains pinned at + `0.84.2`, and no bundle layout, Provider, Renderer, or recovery contract changed. + The currently installed application predates this source integration; a rebuilt and + verified Windows artifact plus first-Conversation acceptance remain pending. - Native Web Search client tickets MLW-01 through MLW-03 are integrated by source coordinator `20260831-web-search-client-integration-7d2f5b94` and promoted to local `main` by task `20260901-web-search-client-main-merge-5a9d3b82` from reviewed diff --git a/.project-docs/30-worklog/tasks/20260901-integrate-local-runtime-fix-9c4e2a71.md b/.project-docs/30-worklog/tasks/20260901-integrate-local-runtime-fix-9c4e2a71.md new file mode 100644 index 0000000..50e81eb --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-integrate-local-runtime-fix-9c4e2a71.md @@ -0,0 +1,51 @@ +# Task: Integrate local runtime unavailable fix + +## Identity + +- Task ID: 20260901-integrate-local-runtime-fix-9c4e2a71 +- Mode: Integration +- Branch: codex/20260901-integrate-local-runtime-fix-9c4e2a71-integrate-local-runtime-fix +- Worktree: D:\Datas\OthersProjects\makelore-worktrees\integrate-local-runtime-fix-9c4e2a71 +- Base commit: 7f0e9310a7f394249fab3cadc5c82d70f91cbfeb +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Integrate verified source commit `7df245a` for the packaged Pi Agent Server resolver failure onto the exact local `main` base. +- Promote the accepted source evidence into canonical current state, evidence, and the existing Windows Pi artifact commitment without changing ADR-006. +- Re-run the relevant product checks on the merged tree, commit the integration result, and fast-forward local `main` only when its worktree ownership is released. + +## Intent And Constraints + +- Preserve Pi `0.84.2` as the sole production runtime and keep Agent Server/runtime ownership in Electron Main. +- Do not change the Pi runtime bundle layout, dependency versions, Renderer/Provider contracts, recovery behavior, or add a compatibility fallback. +- Treat the currently installed application as pre-fix until a rebuilt artifact passes the existing publish/runtime/artifact/final proof and installed first-Conversation acceptance. +- Do not modify or release the separately owned `main` worktree while task `20260831-makelore-web-search-client-inspection-6b4d2e81` retains ownership. + +## Outcome + +- Verified source commit `7df245af5a04f62be48980831ff41987ba686009` contains only the packaged Agent Server launch fix, the production-shaped real-process regression, and its feature task record. +- Merged the source branch without conflicts as `96402551f46d875ba3db0a2f625397aba2f332fb` on the isolated integration branch. +- Omitted the duplicate source-owned task record from the promotion tip while preserving it unchanged on source commit `7df245af5a04f62be48980831ff41987ba686009`; the promoted tree therefore contains only the product fix, regression test, canonical memory, and this integration task's own record relative to `main`. +- Promoted the accepted evidence into `current-state.md`, `evidence-index.md`, and the existing Windows Pi artifact commitment. ADR-006 remains unchanged because runtime ownership, topology, version, and public contracts did not change. +- Prepared a verified fast-forward candidate for local `main`. Actual `main` promotion remains a separate ownership-gated step because task `20260831-makelore-web-search-client-inspection-6b4d2e81` still owns that worktree. + +## Verification + +- Source range inspection: exactly one source commit and three expected files relative to `main`; no unrelated source or project-memory change was present. +- Merge result: Git `ort` merge completed without conflicts. +- Integration focused tests: 4 files / 24 tests passed, including the production-shaped sibling-layout Agent Server startup through Electron Node. +- `pnpm run typecheck`: passed. +- `pnpm run lint:check`: passed with 0 errors and 5 pre-existing unrelated React warnings. +- `pnpm run build:vite`: passed for Renderer, Electron Main, Preload, and utility worker with existing bundle warnings only. +- The source task's full-suite evidence remains 215 files / 1,761 tests passed and 2 conditional skips; integration changed only canonical Markdown after the conflict-free source merge, so the full suite was not redundantly rerun. + +## Follow-ups + +- Release the completed `main` worktree owner only through the non-force task-context command, claim `main` with a separate integration task, and fast-forward it to this verified integration tip. +- Rebuild and install the Windows artifact, run the publish/runtime/artifact/final packaged proof, and confirm a first Coding Conversation no longer shows the runtime-unavailable projection. + +## Promotion Candidates + +- None. The source candidate was accepted and promoted by this integration task without semantic conflict. diff --git a/.project-docs/30-worklog/tasks/20260901-local-runtime-unavailable-8b42c7f1.md b/.project-docs/30-worklog/tasks/20260901-local-runtime-unavailable-8b42c7f1.md deleted file mode 100644 index c0a9347..0000000 --- a/.project-docs/30-worklog/tasks/20260901-local-runtime-unavailable-8b42c7f1.md +++ /dev/null @@ -1,67 +0,0 @@ -# Task: Diagnose local programming runtime unavailable - -## Identity - -- Task ID: 20260901-local-runtime-unavailable-8b42c7f1 -- Mode: Feature -- Branch: codex/20260901-local-runtime-unavailable-8b42c7f1-local-runtime-unavailable-8b42c7f1 -- Worktree: D:\Datas\OthersProjects\makelore-worktrees\local-runtime-unavailable-8b42c7f1 -- Base commit: 7f0e9310a7f394249fab3cadc5c82d70f91cbfeb -- Owner: codex -- Status: Ready for Integration - -## Scope - -- Reproduce the current Makelore Code state that renders `本地编程运行时暂时不可用。` and identify the first failing Renderer, Host API, Pi runtime, packaging, or Provider boundary. -- Build a deterministic, agent-runnable feedback loop for the exact visible symptom before ranking or testing root-cause hypotheses. -- Correlate current source and focused tests with privacy-safe local runtime evidence where available. -- Implement the user-authorized minimal product fix for the confirmed packaged-layout Pi AI ESM resolution failure. -- Add a focused regression at the installed sibling-resource-layout seam, then verify the original process path plus the repository's required checks. - -## Intent And Constraints - -- Treat the visible sentence as a generic safe projection, not as proof of a Pi process failure. -- Preserve ADR-006: Pi `0.84.2` remains the sole production runtime; do not add an OpenCode fallback, dual runtime, compatibility layer, or automatic replay. -- Keep Renderer access behind `/api/coding/*`; runtime, Provider credentials, recovery, and raw diagnostics remain Electron Main-owned. -- Redact credentials, headers, prompts, session content, account identifiers, and complete user project paths from reported evidence. -- Distinguish confirmed facts, evidence-supported inferences, and unverified assumptions. The historical Host-token startup defect is a regression lead, not an assumed cause. -- Keep package resolution anchored to the explicit Main-owned `runtimeRoot`; development and packaged layouts must both continue to work without hard-coded layout branches or a compatibility layer. -- Work only in this isolated task worktree and do not modify peer worktrees or the occupied local `main` worktree. - -## Outcome - -- Confirmed that the installed Makelore application reaches the generic `CODING_RUNTIME_UNAVAILABLE` projection only after the Pi worker fails during `open`. -- The installed Agent Server imports `@earendil-works/pi-ai` with `import.meta.resolve(specifier, runtimePackageUrl)`. In the embedded Node.js `v24.18.1` process, the second `parentURL` argument is not honored unless `--experimental-import-meta-resolve` is enabled, so resolution starts beside `resources/resources/pi-agent-server.mjs` instead of the sibling `resources/pi-runtime/node_modules` tree. -- The dependency is present and healthy: the installed Agent Server matches repository source byte-for-byte, both `@earendil-works/pi-coding-agent` and `@earendil-works/pi-ai` are version `0.84.2`, and the expected ESM export exists. -- Reproduced the installed startup failure twice with the exact executable, script, runtime root, working directory, and environment shape. Both attempts exited with code `1` and `ERR_MODULE_NOT_FOUND` before producing protocol output. -- A one-variable differential adding `--experimental-import-meta-resolve` made the same process exit cleanly without the module error. Importing the verified absolute `pi-runtime/node_modules/@earendil-works/pi-ai/dist/index.js` URL also succeeded without the experimental flag. -- Added `--experimental-import-meta-resolve` only to the Main-owned Agent Server child process. This activates the existing `runtimePackageUrl` parent argument and preserves standard ESM package resolution across both the development layout and packaged `pi-runtime` closure. -- Added a real-process regression that places `pi-agent-server.mjs` under the installed `resources/resources` sibling layout, exposes the real Pi runtime and Pi AI package through a temporary production-shaped root, and starts it with the project's Electron executable. -- The regression failed before the fix with the same `ERR_MODULE_NOT_FOUND` / `PI_RPC_EXITED` chain and passed after the fix. No runtime bundle layout, dependency version, Provider behavior, Renderer contract, installed application file, user data, or peer task content was changed. - -## Verification - -- Current startup-log signal loop: found the missing `@earendil-works/pi-ai` module plus two `PI_WORKER_STOPPED` / `open_failure` events; verdict `RED` as expected for the reported failure. -- Exact installed-process loop: two independent launches reproduced `ExitCode=1`, `ModuleNotFound=true`, `MissingPiAi=true`, and zero protocol bytes within five seconds. -- Resolver differential: the same installed process with only `--experimental-import-meta-resolve` added produced no resolver error. -- Absolute-path differential: importing the fixed runtime-root ESM entry through `pathToFileURL(...)` succeeded in the same embedded Node.js process without the experimental flag. -- Artifact comparison: installed `pi-agent-server.mjs` equals repository source; installed Pi package versions and exports match the expected `0.84.2` runtime closure. -- The existing real-process unit test was inspected and does not cover this boundary because it runs the server from the repository layout, where ancestor lookup can reach the root `node_modules`; the installed sibling `resources/resources` and `resources/pi-runtime` layout is not exercised. -- Red regression: `pnpm exec vitest run tests/unit/pi-agent-server-process-real.test.ts -t "boots from the packaged sibling resource layout" --maxWorkers=1` failed at `pi-agent-server.mjs:22` with `ERR_MODULE_NOT_FOUND` and `PI_RPC_EXITED` before the product change. -- Green regression: the same command passed against Electron Node after the Agent Server launch flag was added. -- Focused verification: 4 relevant test files, 24 tests passed, covering Agent Server behavior, composition paths, Pi runtime bundling, and product artifact rules. -- Existing real-process behavior: both tests in `pi-agent-server-process-real.test.ts` passed, including two logical Conversation threads, process reuse, invalidation, and restart. -- Full unit suite: 215 test files passed; 1,761 tests passed and 2 existing conditional tests were skipped. -- `pnpm run typecheck`: passed. Final changed-file ESLint: passed. -- `pnpm run lint:check`: passed with 0 errors and 5 pre-existing unrelated React warnings. -- `pnpm run build:vite`: passed for Renderer, Electron Main, Preload, and utility worker; only existing bundle/chunk warnings were emitted. -- `git diff --check`: passed; no debug instrumentation or generated artifact is tracked. - -## Follow-ups - -- The currently installed application predates this source change. After integration, rebuild the Windows artifact, run `verify:artifact:win`, `verify:artifact:pi`, and the final packaged Pi proof, then install it before performing the first-conversation UI acceptance. -- When the pinned Electron/Node runtime is upgraded, retain the packaged-layout regression; remove the launch flag only after the new embedded Node accepts the `import.meta.resolve` parent argument without it. - -## Promotion Candidates - -- Target: `current-state` and the Pi release evidence index. Proposal: require an Electron Node Agent Server initialization proof from the installed sibling resource layout, not only source-tree process tests. Evidence: the new layout test reproduced the exact installed `ERR_MODULE_NOT_FOUND` before the fix and passed after enabling the resolver parent URL; 24 focused tests, the full unit suite, typecheck, lint, and production build passed. Future impact: prevents source-root ancestor `node_modules` from masking final-artifact ESM resolution regressions. Semantic conflicts: none with ADR-006 or the existing Pi closure requirement. Human confirmation: not required to integrate the evidence rule; final package installation/release remains an operator gate. diff --git a/.project-docs/50-evidence/evidence-index.md b/.project-docs/50-evidence/evidence-index.md index 0d52f15..4de42ae 100644 --- a/.project-docs/50-evidence/evidence-index.md +++ b/.project-docs/50-evidence/evidence-index.md @@ -4,6 +4,7 @@ Use this index for searchable, traceable evidence records. | Date | Topic | Status | Source | Detail | |---|---|---|---|---| +| 2026-09-01 | Makelore Code packaged Agent Server Pi AI resolver fix | Integrated locally; rebuilt Windows artifact and installed-client acceptance pending | Source `7df245a`, source task `20260901-local-runtime-unavailable-8b42c7f1`, merge `9640255`, integration task `20260901-integrate-local-runtime-fix-9c4e2a71` | The installed Electron Node process reproduced `ERR_MODULE_NOT_FOUND` for the present `@earendil-works/pi-ai@0.84.2` because the optional `import.meta.resolve` parent URL was inactive and lookup began beside `resources/resources/pi-agent-server.mjs`. Enabling that capability only for the Main-owned Agent Server made the exact differential pass without changing the runtime closure. A production-shaped sibling-layout Electron test went red before and green after; 24 focused tests, 1,761 full unit tests / 2 conditional skips, typecheck, lint with 0 errors, and Renderer/Main/Preload/utility build passed. This does not claim that the currently installed old package is fixed or that a rebuilt final artifact has passed release proof. | | 2026-08-30 | MakeLore AI Design Living Form V2 hard cutover | Integrated locally with matching server source; production data cutover and Provider activation pending | Client source `b0b5a602b501308a23eb27e2f51a5169b9e46b1e`, server source `b5351d54f595ce8eb873593e462e4a556bea0b05`, integration task `20260830-integrate-marketplace-design-client-main-9d5f3b82`, ADR-007 | Replaces nested V1 Conversations, Briefs, editable provider Prompt, mutable Quote PATCH, and local semantic fallback with one current Direction, one Living Form/Specification authority, stable operations, immutable Quote confirmation, and Main-owned V2 transport. Client source passed typecheck, lint with zero errors, 1,425 unit tests, Vite/Electron build, and 2 Electron E2E tests. Server source passed 2,626 tests / 31 skipped under UTF-8 mode. No production database cutover, real paid Provider request, deployment, or publication occurred. | | 2026-08-30 | MakeLore curated Plugin Marketplace Release A | Integrated locally; XMA-01 live acceptance PASS; production trust activation HOLD | Source `40df677a31ff7651f962151eb84b925987781c03`, source task `20260828-plugin-marketplace-client-5f8b3d72`, integration task `20260830-integrate-plugin-marketplace-client-6e3b9d82` | R7 Standards/Spec passed with zero findings. XMA-01 passed 12/12 using real PostgreSQL, signed-in packaged MakeLore, Operations-published temporary-key packages, A/B account isolation, immutable install/update/freeze/rollback, project/Agent/Pi materialization, lifecycle invalidation, pricing-only ETag change, Data Service 10 tools/14 operations and zero Token Point transactions. Final Windows installer was 208,235,670 bytes with SHA-256 `738A2F6573C502281787F418EA666C6E62C287CA0EC13C6EB22369DAB0B3FFFA`. The official Ed25519 public key was not supplied, so the production trust store correctly remains fail closed; no deployment or production publication occurred. | | 2026-08-26 | Makelore Code Pi hard cutover、后台运行 ownership 与 Works user-context Provider 修复 | 本地 `main` 已集成;Windows 最终安装包与 packaged proof Pass;真实 Provider/macOS/native Linux 非 Pass | 集成交付 `48a9189`、实现 `a098266`、任务 `20260826-fix-pi-model-provider-8d4c2a71` / `20260826-integrate-pi-provider-fix-6e4c2a91`、`docs/pi-runtime-release-runbook.md` | OpenCode production runtime/package/plugin/fallback 已删除,Pi `0.84.2` 是唯一 runtime。未解析 Conversation 现在 validate→persist resolved model→prepare;exact Works `works square AI gateway did not return one-api user context` 会失效缓存 credential、以非重试 Provider-auth failure 结束且不自动 replay,`agent_end willRetry:false` 不再被 `agent_settled` 覆盖成 completed。聚焦 47/47、related 54/54、projector 12/12、181 files / 1541 full unit + pressure、typecheck、lint、Vite build、Windows Electron 4/4、publish/runtime/artifact/Pi/subagent/final packaged proof 通过。NSIS 208,162,564 bytes,SHA-256 `A02F79FCD3273FB0B013450492DC788B6E5ED7BBC1007468FB83BE949B8CB30D`,未签名;final app.asar SHA-256 `A8394D0F7324B27D2C95469ABD4E2A39C739EABCC6D8B41E1D65BA4B41FB9D39`。真实 Provider 是 Explicitly Waived / Accepted Risk 且 `realTurnVerified=false`;macOS x64/arm64 与 native non-WSL Linux 未验收。 | diff --git a/.project-docs/80-commitments/commitments.md b/.project-docs/80-commitments/commitments.md index f78b90b..fc361f3 100644 --- a/.project-docs/80-commitments/commitments.md +++ b/.project-docs/80-commitments/commitments.md @@ -13,7 +13,7 @@ Track future-facing memory: promised follow-ups, unfinished loops, timed checks, | 2026-08-16 | 验收 Prompt Museum 与 Canvas 删除/重报价的真实服务端链路 | 发布包含 `26b52d7` Canvas 行为的安装包前 | 客户端/服务端集成 | Pending | 使用真实 Works 账号核对 Museum 列表/详情/分页/筛选/署名/CDN/Prompt 回填,并确认固定相对媒体路径可经 Main Bearer 代理、401 刷新、10 MiB/可信 raster MIME 边界后在 Renderer 展示;核对最终 Prompt/options 重报价和确认设计点;删除 Workspace 后确认软删除可见性、未提交任务取消/预留积分释放、已运行任务结算。保留 Main 错误脱敏和严格 DTO/HTTPS 投影,不以客户端回归替代服务端验收 | | 2026-08-16 | 验收 default-on Robot Guided Hotspot Binding 的 Windows/macOS 真实设备链路 | 下一份包含页面内热点连接行为的安装包发布前 | 客户端/硬件/服务端集成 | Pending | 核对精确出货固件与固定 Portal、六位码发行/消费语义;执行 Windows Robot 真机扫描/连接、签名 macOS x64/arm64 CoreLocation/CoreWLAN/worker/ASAR/Koffi smoke,以及真实 Host/native Electron 端到端配网+Binding;保留 `NIANCODE_AI_HARDWARE_GUIDED_HOTSPOT_BINDING=0` 回滚并记录支持矩阵 | | 2026-08-10 | 完成客户端提交到 App `play_url` 播放的真实生产整链验收 | source+built+contract 服务端协议、OSS immutable Release、CDN/Edge 与 App 消费链成组集成后 | 客户端/服务端集成 | Pending | 使用真实账号执行小游戏和小程序创建、客户端本地构建与同字节预检、双归档提交、服务端逐字节校验/不可变 Release 固化、运营批准、CDN 发布、App 播放与监控核对;如需不可绕过 runtime gate,另行设计可信 verifier | -| 2026-08-12 | Windows 发布流水线保留固定 npm/Pi 运行时产物门禁 | 每次生成正式 Windows 安装器时 | 客户端发布 | Pending | 运行 `pnpm verify:publish-runtime`、`pnpm verify:artifact:win`、`pnpm verify:artifact:pi` 与 final packaged proof。当前 208,162,564-byte / SHA-256 `A02F79FCD3273FB0B013450492DC788B6E5ED7BBC1007468FB83BE949B8CB30D` 安装器为本地未签名证据,尚未 push、发布或安装。 | +| 2026-08-12 | Windows 发布流水线保留固定 npm/Pi 运行时产物门禁 | 每次生成正式 Windows 安装器时 | 客户端发布 | Pending | 运行 `pnpm verify:publish-runtime`、`pnpm verify:artifact:win`、`pnpm verify:artifact:pi` 与 final packaged proof;启动最终 `resources/resources/pi-agent-server.mjs` 并确认它从相邻 `pi-runtime` 初始化,再用安装后的首个 Conversation 确认不出现 runtime-unavailable 投影。当前 208,162,564-byte / SHA-256 `A02F79FCD3273FB0B013450492DC788B6E5ED7BBC1007468FB83BE949B8CB30D` 安装器早于 `7df245a` 修复,为本地未签名历史证据,尚未 push、发布或安装。 | | 2026-08-10 | 删除客户端 `runtime_url` 兼容回退 | 一个客户端兼容版本结束,且服务端与存量数据稳定提供 `play_url` | 客户端 | Pending | 删除类型字段、读取分支和对应回归测试 | | 2026-08-11 | 部署并验收 AI 绘画多 Conversation 服务端契约 | 发布包含多会话客户端之前 | 客户端/服务端集成 | Pending | 确认迁移 `0033`、Conversation list/create/read/turn/confirm API、持久 Agent Session 与事件字段已上线 | | 2026-08-12 | 复核 AI 编程 Provider 错误与重试投影 | 升级 Pi、Works gateway 或上游 Provider 错误格式时 | 客户端/Pi/provider 集成 | Pending | 重新验证确定性 Works user-context 缺失仍会 expire credential、非重试 fail fast、固定脱敏为 `CODING_PROVIDER_AUTH_REQUIRED`,并且不会被后续 `agent_settled` 改写为 completed;不得把其他 401/429/5xx 扩大归类。 | From e1e4b570f98a5c159a01fda740222ce3cd1d755e Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 11:26:41 +0800 Subject: [PATCH 17/47] docs(coding): record packaged runtime main promotion --- .project-docs/30-worklog/current-state.md | 5 +- ...01-integrate-local-runtime-fix-9c4e2a71.md | 51 -------------- ...901-promote-local-runtime-main-a7c4e291.md | 68 +++++++++++++++++++ .project-docs/50-evidence/evidence-index.md | 2 +- 4 files changed, 72 insertions(+), 54 deletions(-) delete mode 100644 .project-docs/30-worklog/tasks/20260901-integrate-local-runtime-fix-9c4e2a71.md create mode 100644 .project-docs/30-worklog/tasks/20260901-promote-local-runtime-main-a7c4e291.md diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index f0dd3c2..7e4b493 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -5,8 +5,9 @@ This file is the integrated default-branch snapshot. Feature tasks record progre ## Integrated Through - Packaged Pi Agent Server resolver fix source `7df245af5a04f62be48980831ff41987ba686009` - from task `20260901-local-runtime-unavailable-8b42c7f1` is integrated by task - `20260901-integrate-local-runtime-fix-9c4e2a71` through merge + from task `20260901-local-runtime-unavailable-8b42c7f1` is promoted to local + `main` by task `20260901-promote-local-runtime-main-a7c4e291` through verified + integration candidate `42ea83c0cbad52432eca99e4161e0360bef3e219` and source merge `96402551f46d875ba3db0a2f625397aba2f332fb`. Electron Main now starts only the shared Agent Server with Node's `import.meta.resolve` parent-URL capability enabled, so the unchanged server script resolves `@earendil-works/pi-ai` from the explicit diff --git a/.project-docs/30-worklog/tasks/20260901-integrate-local-runtime-fix-9c4e2a71.md b/.project-docs/30-worklog/tasks/20260901-integrate-local-runtime-fix-9c4e2a71.md deleted file mode 100644 index 50e81eb..0000000 --- a/.project-docs/30-worklog/tasks/20260901-integrate-local-runtime-fix-9c4e2a71.md +++ /dev/null @@ -1,51 +0,0 @@ -# Task: Integrate local runtime unavailable fix - -## Identity - -- Task ID: 20260901-integrate-local-runtime-fix-9c4e2a71 -- Mode: Integration -- Branch: codex/20260901-integrate-local-runtime-fix-9c4e2a71-integrate-local-runtime-fix -- Worktree: D:\Datas\OthersProjects\makelore-worktrees\integrate-local-runtime-fix-9c4e2a71 -- Base commit: 7f0e9310a7f394249fab3cadc5c82d70f91cbfeb -- Owner: codex -- Status: Ready for Integration - -## Scope - -- Integrate verified source commit `7df245a` for the packaged Pi Agent Server resolver failure onto the exact local `main` base. -- Promote the accepted source evidence into canonical current state, evidence, and the existing Windows Pi artifact commitment without changing ADR-006. -- Re-run the relevant product checks on the merged tree, commit the integration result, and fast-forward local `main` only when its worktree ownership is released. - -## Intent And Constraints - -- Preserve Pi `0.84.2` as the sole production runtime and keep Agent Server/runtime ownership in Electron Main. -- Do not change the Pi runtime bundle layout, dependency versions, Renderer/Provider contracts, recovery behavior, or add a compatibility fallback. -- Treat the currently installed application as pre-fix until a rebuilt artifact passes the existing publish/runtime/artifact/final proof and installed first-Conversation acceptance. -- Do not modify or release the separately owned `main` worktree while task `20260831-makelore-web-search-client-inspection-6b4d2e81` retains ownership. - -## Outcome - -- Verified source commit `7df245af5a04f62be48980831ff41987ba686009` contains only the packaged Agent Server launch fix, the production-shaped real-process regression, and its feature task record. -- Merged the source branch without conflicts as `96402551f46d875ba3db0a2f625397aba2f332fb` on the isolated integration branch. -- Omitted the duplicate source-owned task record from the promotion tip while preserving it unchanged on source commit `7df245af5a04f62be48980831ff41987ba686009`; the promoted tree therefore contains only the product fix, regression test, canonical memory, and this integration task's own record relative to `main`. -- Promoted the accepted evidence into `current-state.md`, `evidence-index.md`, and the existing Windows Pi artifact commitment. ADR-006 remains unchanged because runtime ownership, topology, version, and public contracts did not change. -- Prepared a verified fast-forward candidate for local `main`. Actual `main` promotion remains a separate ownership-gated step because task `20260831-makelore-web-search-client-inspection-6b4d2e81` still owns that worktree. - -## Verification - -- Source range inspection: exactly one source commit and three expected files relative to `main`; no unrelated source or project-memory change was present. -- Merge result: Git `ort` merge completed without conflicts. -- Integration focused tests: 4 files / 24 tests passed, including the production-shaped sibling-layout Agent Server startup through Electron Node. -- `pnpm run typecheck`: passed. -- `pnpm run lint:check`: passed with 0 errors and 5 pre-existing unrelated React warnings. -- `pnpm run build:vite`: passed for Renderer, Electron Main, Preload, and utility worker with existing bundle warnings only. -- The source task's full-suite evidence remains 215 files / 1,761 tests passed and 2 conditional skips; integration changed only canonical Markdown after the conflict-free source merge, so the full suite was not redundantly rerun. - -## Follow-ups - -- Release the completed `main` worktree owner only through the non-force task-context command, claim `main` with a separate integration task, and fast-forward it to this verified integration tip. -- Rebuild and install the Windows artifact, run the publish/runtime/artifact/final packaged proof, and confirm a first Coding Conversation no longer shows the runtime-unavailable projection. - -## Promotion Candidates - -- None. The source candidate was accepted and promoted by this integration task without semantic conflict. diff --git a/.project-docs/30-worklog/tasks/20260901-promote-local-runtime-main-a7c4e291.md b/.project-docs/30-worklog/tasks/20260901-promote-local-runtime-main-a7c4e291.md new file mode 100644 index 0000000..e1a730e --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-promote-local-runtime-main-a7c4e291.md @@ -0,0 +1,68 @@ +# Task: Promote packaged Pi runtime fix to main + +## Identity + +- Task ID: 20260901-promote-local-runtime-main-a7c4e291 +- Mode: Integration +- Branch: main +- Worktree: D:\Datas\OthersProjects\makelore +- Base commit: 7f0e9310a7f394249fab3cadc5c82d70f91cbfeb +- Owner: codex +- Status: In Progress + +## Scope + +- Fast-forward the exact verified packaged Pi Agent Server resolver candidate + `42ea83c0cbad52432eca99e4161e0360bef3e219` from local `main` base + `7f0e9310a7f394249fab3cadc5c82d70f91cbfeb`. +- Preserve the candidate's product tree and accepted canonical facts while keeping + source and intermediate integration task records on their owning branches. +- Complete the task-aware documentation gate and release `main` ownership from a clean + worktree; do not push, publish, package, or deploy. + +## Intent And Constraints + +- Pi `0.84.2` remains the only production Coding runtime; Electron Main remains the + Agent Server owner, and the bundle layout, Provider/Renderer contracts, recovery + behavior, and public APIs remain unchanged. +- The currently installed client predates this fix. Local source integration must not + be represented as a rebuilt or installed-package acceptance result. +- Concurrent Task Gate passed after the previous clean, ready feature owner of `main` + was released non-forcibly. This task exclusively owns `main` and the Integration Lock. +- Planning and Integration Gates passed using the source outcome, verified candidate, + current-state, ADR-006, evidence index, commitment ledger, and exact Git ancestry; + no semantic conflict or human product-direction decision was present. + +## Outcome + +- Fast-forwarded local `main` from `7f0e9310a7f394249fab3cadc5c82d70f91cbfeb` + to verified integration candidate `42ea83c0cbad52432eca99e4161e0360bef3e219` + with no merge conflict or replay. +- Promoted the packaged Pi Agent Server resolver fix and production-shaped regression + to local `main`; canonical current state and evidence now identify this promotion task. +- Omitted the duplicate intermediate integration task record from the promotion tip; + it remains recoverable unchanged at candidate `42ea83c0cbad52432eca99e4161e0360bef3e219`, + while the source task record remains at source `7df245af5a04f62be48980831ff41987ba686009`. + +## Verification + +- Pre-promotion ancestry check passed: local `main` was an ancestor of the candidate, + with `0` main-only and `3` candidate-only commits. +- `git merge --ff-only` succeeded and placed `main` at exact candidate `42ea83c` before + this promotion ledger commit. +- Candidate verification carried forward unchanged: 4 focused files / 24 tests passed; + typecheck passed; lint passed with 0 errors and 5 pre-existing warnings; Vite/Electron + Main/Preload/utility build passed. Source full suite passed 215 files / 1,761 tests with + 2 conditional skips. +- Final product-tree equality, task-aware drift, clean status, and exact `main` tip are + pending the promotion ledger commit. + +## Follow-ups + +- Rebuild and install the Windows artifact, run the publish/runtime/artifact/final + packaged proof, and confirm the first Coding Conversation no longer reports local + runtime unavailable. + +## Promotion Candidates + +- None recorded. diff --git a/.project-docs/50-evidence/evidence-index.md b/.project-docs/50-evidence/evidence-index.md index 4de42ae..c7a3d16 100644 --- a/.project-docs/50-evidence/evidence-index.md +++ b/.project-docs/50-evidence/evidence-index.md @@ -4,7 +4,7 @@ Use this index for searchable, traceable evidence records. | Date | Topic | Status | Source | Detail | |---|---|---|---|---| -| 2026-09-01 | Makelore Code packaged Agent Server Pi AI resolver fix | Integrated locally; rebuilt Windows artifact and installed-client acceptance pending | Source `7df245a`, source task `20260901-local-runtime-unavailable-8b42c7f1`, merge `9640255`, integration task `20260901-integrate-local-runtime-fix-9c4e2a71` | The installed Electron Node process reproduced `ERR_MODULE_NOT_FOUND` for the present `@earendil-works/pi-ai@0.84.2` because the optional `import.meta.resolve` parent URL was inactive and lookup began beside `resources/resources/pi-agent-server.mjs`. Enabling that capability only for the Main-owned Agent Server made the exact differential pass without changing the runtime closure. A production-shaped sibling-layout Electron test went red before and green after; 24 focused tests, 1,761 full unit tests / 2 conditional skips, typecheck, lint with 0 errors, and Renderer/Main/Preload/utility build passed. This does not claim that the currently installed old package is fixed or that a rebuilt final artifact has passed release proof. | +| 2026-09-01 | Makelore Code packaged Agent Server Pi AI resolver fix | Integrated on local `main`; rebuilt Windows artifact and installed-client acceptance pending | Source `7df245a`, source task `20260901-local-runtime-unavailable-8b42c7f1`, merge `9640255`, verified candidate `42ea83c`, main promotion task `20260901-promote-local-runtime-main-a7c4e291` | The installed Electron Node process reproduced `ERR_MODULE_NOT_FOUND` for the present `@earendil-works/pi-ai@0.84.2` because the optional `import.meta.resolve` parent URL was inactive and lookup began beside `resources/resources/pi-agent-server.mjs`. Enabling that capability only for the Main-owned Agent Server made the exact differential pass without changing the runtime closure. A production-shaped sibling-layout Electron test went red before and green after; 24 focused tests, 1,761 full unit tests / 2 conditional skips, typecheck, lint with 0 errors, and Renderer/Main/Preload/utility build passed. This does not claim that the currently installed old package is fixed or that a rebuilt final artifact has passed release proof. | | 2026-08-30 | MakeLore AI Design Living Form V2 hard cutover | Integrated locally with matching server source; production data cutover and Provider activation pending | Client source `b0b5a602b501308a23eb27e2f51a5169b9e46b1e`, server source `b5351d54f595ce8eb873593e462e4a556bea0b05`, integration task `20260830-integrate-marketplace-design-client-main-9d5f3b82`, ADR-007 | Replaces nested V1 Conversations, Briefs, editable provider Prompt, mutable Quote PATCH, and local semantic fallback with one current Direction, one Living Form/Specification authority, stable operations, immutable Quote confirmation, and Main-owned V2 transport. Client source passed typecheck, lint with zero errors, 1,425 unit tests, Vite/Electron build, and 2 Electron E2E tests. Server source passed 2,626 tests / 31 skipped under UTF-8 mode. No production database cutover, real paid Provider request, deployment, or publication occurred. | | 2026-08-30 | MakeLore curated Plugin Marketplace Release A | Integrated locally; XMA-01 live acceptance PASS; production trust activation HOLD | Source `40df677a31ff7651f962151eb84b925987781c03`, source task `20260828-plugin-marketplace-client-5f8b3d72`, integration task `20260830-integrate-plugin-marketplace-client-6e3b9d82` | R7 Standards/Spec passed with zero findings. XMA-01 passed 12/12 using real PostgreSQL, signed-in packaged MakeLore, Operations-published temporary-key packages, A/B account isolation, immutable install/update/freeze/rollback, project/Agent/Pi materialization, lifecycle invalidation, pricing-only ETag change, Data Service 10 tools/14 operations and zero Token Point transactions. Final Windows installer was 208,235,670 bytes with SHA-256 `738A2F6573C502281787F418EA666C6E62C287CA0EC13C6EB22369DAB0B3FFFA`. The official Ed25519 public key was not supplied, so the production trust store correctly remains fail closed; no deployment or production publication occurred. | | 2026-08-26 | Makelore Code Pi hard cutover、后台运行 ownership 与 Works user-context Provider 修复 | 本地 `main` 已集成;Windows 最终安装包与 packaged proof Pass;真实 Provider/macOS/native Linux 非 Pass | 集成交付 `48a9189`、实现 `a098266`、任务 `20260826-fix-pi-model-provider-8d4c2a71` / `20260826-integrate-pi-provider-fix-6e4c2a91`、`docs/pi-runtime-release-runbook.md` | OpenCode production runtime/package/plugin/fallback 已删除,Pi `0.84.2` 是唯一 runtime。未解析 Conversation 现在 validate→persist resolved model→prepare;exact Works `works square AI gateway did not return one-api user context` 会失效缓存 credential、以非重试 Provider-auth failure 结束且不自动 replay,`agent_end willRetry:false` 不再被 `agent_settled` 覆盖成 completed。聚焦 47/47、related 54/54、projector 12/12、181 files / 1541 full unit + pressure、typecheck、lint、Vite build、Windows Electron 4/4、publish/runtime/artifact/Pi/subagent/final packaged proof 通过。NSIS 208,162,564 bytes,SHA-256 `A02F79FCD3273FB0B013450492DC788B6E5ED7BBC1007468FB83BE949B8CB30D`,未签名;final app.asar SHA-256 `A8394D0F7324B27D2C95469ABD4E2A39C739EABCC6D8B41E1D65BA4B41FB9D39`。真实 Provider 是 Explicitly Waived / Accepted Risk 且 `realTurnVerified=false`;macOS x64/arm64 与 native non-WSL Linux 未验收。 | From 5e8b7266c2a8297c9d307b576256886659d44780 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 11:27:18 +0800 Subject: [PATCH 18/47] docs(coding): close packaged runtime main promotion --- .../20260901-promote-local-runtime-main-a7c4e291.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.project-docs/30-worklog/tasks/20260901-promote-local-runtime-main-a7c4e291.md b/.project-docs/30-worklog/tasks/20260901-promote-local-runtime-main-a7c4e291.md index e1a730e..7c60021 100644 --- a/.project-docs/30-worklog/tasks/20260901-promote-local-runtime-main-a7c4e291.md +++ b/.project-docs/30-worklog/tasks/20260901-promote-local-runtime-main-a7c4e291.md @@ -8,7 +8,7 @@ - Worktree: D:\Datas\OthersProjects\makelore - Base commit: 7f0e9310a7f394249fab3cadc5c82d70f91cbfeb - Owner: codex -- Status: In Progress +- Status: Ready for Integration ## Scope @@ -54,8 +54,12 @@ typecheck passed; lint passed with 0 errors and 5 pre-existing warnings; Vite/Electron Main/Preload/utility build passed. Source full suite passed 215 files / 1,761 tests with 2 conditional skips. -- Final product-tree equality, task-aware drift, clean status, and exact `main` tip are - pending the promotion ledger commit. +- At promotion ledger `e1e4b570f98a5c159a01fda740222ce3cd1d755e`, the product tree + exactly matched verified candidate `42ea83c`, the candidate remained an ancestor, + task-aware drift and project-docs structure checks passed, the net task diff contained + only the two product files plus canonical/own-task memory, and `git status` was clean. +- This closure changes only this task's status and verification record; the same drift, + structure, ancestry, product-tree, and clean-status checks are rerun before release. ## Follow-ups From 38f2358db3288c81462b2ac7f049ab737a29515d Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 12:17:31 +0800 Subject: [PATCH 19/47] fix(plugins): accept weak catalog etags --- ...901-plugin-catalog-load-client-7d4a8c21.md | 59 +++++++++++++++++++ electron/coding-plugins/marketplace-client.ts | 2 +- .../coding-plugin-marketplace-client.test.ts | 28 +++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 .project-docs/30-worklog/tasks/20260901-plugin-catalog-load-client-7d4a8c21.md diff --git a/.project-docs/30-worklog/tasks/20260901-plugin-catalog-load-client-7d4a8c21.md b/.project-docs/30-worklog/tasks/20260901-plugin-catalog-load-client-7d4a8c21.md new file mode 100644 index 0000000..ab5ea4d --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-plugin-catalog-load-client-7d4a8c21.md @@ -0,0 +1,59 @@ +# Task: Diagnose MakeLore Marketplace catalog loading + +## Identity + +- Task ID: 20260901-plugin-catalog-load-client-7d4a8c21 +- Mode: Feature +- Branch: codex/20260901-plugin-catalog-load-client-7d4a8c21-plugin-catalog-load-client +- Worktree: D:\Datas\OthersProjects\makelore-plugin-catalog-load-client-7d4a8c21 +- Base commit: 5e8b7266c2a8297c9d307b576256886659d44780 +- Owner: codex-root +- Status: Ready for Integration + +## Scope + +- Reproduce the installed MakeLore Marketplace catalog failure at the Renderer → IPC dispatcher → Main Marketplace client boundary. +- Verify the installed 1.2.0 artifact contains the current Marketplace composition and compare its parser/network behavior with the live catalog. +- If a client defect is confirmed, change only the smallest Marketplace client/Host API/error-projection seam and focused tests. + +## Intent And Constraints + +- Preserve the occupied client root worktree and its untracked packaging record; all product work stays in this isolated worktree. +- Do not expose Works credentials, read real `.env` files, alter Package Store/Provider/billing state, or publish a package. +- Treat local-package inference as forbidden: a failed server catalog must remain fail closed. +- The tight loop must reproduce the exact initial catalog failure or prove each boundary healthy before any implementation change. + +## Plan + +1. Verify the live catalog parses through the exact client DTO parser. +2. Inspect the running installed Main process, local Host API, logs, embedded bundle, and effective API origin. +3. Reproduce the failing boundary with a focused test/probe, rank falsifiable causes, and implement only the confirmed fix. +4. Run focused regressions and task documentation gates; do not rebuild/publish unless the fix affects packaged behavior and needs an artifact proof. + +## Outcome + +- Completed. The Marketplace client now accepts both strong and standards-compliant weak composite ETags, while preserving the exact received validator for the next `If-None-Match` request and retaining all generation/pricing identity checks. +- Root cause: the production gateway emits `W/"plugins-..."` for the compressed catalog response. The previous strong-only regular expression rejected that otherwise valid HTTP 200 response, and the local Host API surfaced the rejection as `plugin_backend_unavailable`. +- No fallback catalog, server mutation, proxy workaround, or billing/Provider behavior was added. + +## Verification + +- Live catalog parsed through `parseMarketplaceCatalogPage`: HTTP 200, total 3, exact IDs `makelore.data-service`, `makelore.game-resource`, `makelore.web-search`. +- Installed MakeLore is version 1.2.0 and its `app.asar` contains the Marketplace route, service composition, and production origin `https://square.nianxx.cn`. +- Running local Host API is on `127.0.0.1:13210`; an unauthenticated shell probe correctly returns 401 and therefore cannot yet distinguish dispatcher/service from upstream failure. +- Installed-session Electron probes returned HTTP 200 and the weak composite ETag; the exact source Marketplace client failed before the fix with `marketplace_response_invalid: invalid composite Marketplace ETag` and returned all three official Plugins after the fix. +- TDD regression: the new weak-ETag/304-reuse case failed before the product change and passed after it. +- `pnpm exec vitest run tests/unit/coding-plugin-marketplace-client.test.ts tests/unit/plugin-marketplace-routes.test.ts tests/unit/plugin-marketplace-store.test.ts tests/unit/plugin-marketplace-pages.test.tsx`: 4 files, 68 tests passed. +- `pnpm test`: 214 files, 1761 tests passed, 2 expected skips; pressure test 1/1 passed. +- `pnpm run typecheck`: passed. +- `pnpm run lint:check`: 0 errors and 5 unchanged warnings outside this task. +- `pnpm run build:vite`: Renderer, Main, Preload, and utility builds passed. +- Scoped ESLint and `git diff --check`: passed. + +## Follow-ups + +- Integrate this task into the client main branch, rebuild the Windows artifact, and install/restart it. The currently installed MakeLore 1.2.0 artifact cannot pick up this source-only fix dynamically. + +## Promotion Candidates + +- None recorded. diff --git a/electron/coding-plugins/marketplace-client.ts b/electron/coding-plugins/marketplace-client.ts index d9bf018..547f8fc 100644 --- a/electron/coding-plugins/marketplace-client.ts +++ b/electron/coding-plugins/marketplace-client.ts @@ -44,7 +44,7 @@ const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,127}$/u; const SHA256_PATTERN = /^[a-f0-9]{64}$/u; const RELEASE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u; const REQUEST_ID_PATTERN = /^[\x21-\x7e]{1,128}$/u; -const ETAG_PATTERN = /^"plugins-(\d+)-tp-([A-Za-z0-9._-]{1,128})"$/u; +const ETAG_PATTERN = /^(?:W\/)?"plugins-(\d+)-tp-([A-Za-z0-9._-]{1,128})"$/u; type UnknownRecord = Record; type FetchImplementation = (input: string | URL, init?: RequestInit) => Promise; diff --git a/tests/unit/coding-plugin-marketplace-client.test.ts b/tests/unit/coding-plugin-marketplace-client.test.ts index 39d21e9..75931bc 100644 --- a/tests/unit/coding-plugin-marketplace-client.test.ts +++ b/tests/unit/coding-plugin-marketplace-client.test.ts @@ -350,6 +350,34 @@ describe('Marketplace client and account cache', () => { await expect(client.readCatalog({ limit: 10 })).resolves.toMatchObject({ stale: true, total: 1 }); }); + it('accepts a weak composite ETag from compressed Marketplace responses and reuses it verbatim', async () => { + const pricingVersionId = '00000000-0000-0000-0000-000000000420'; + const etag = `W/"plugins-2-tp-${pricingVersionId}"`; + const fetcher = vi.fn() + .mockResolvedValueOnce(response({ ...catalogPage, catalog_generation: 2 }, {}, { + ETag: etag, + 'X-Plugin-Catalog-Generation': '2', + 'X-Token-Point-Pricing-Version': pricingVersionId, + })) + .mockResolvedValueOnce(new Response(null, { status: 304 })); + const client = createMarketplaceClient({ + fetchImpl: fetcher, + apiBaseUrl: 'https://square.example', + getAccessToken: async () => null, + subscribeSession: () => () => undefined, + }); + + await expect(client.readCatalog({ limit: 10 })).resolves.toMatchObject({ + total: 1, + etag, + generation: 2, + pricingVersionId, + stale: false, + }); + await expect(client.readCatalog({ limit: 10 })).resolves.toMatchObject({ etag, stale: false }); + expect(fetcher.mock.calls[1]?.[1]?.headers).toMatchObject({ 'If-None-Match': etag }); + }); + it('refreshes download authentication at most once before accepting the artifact', async () => { const archive = Buffer.from('signed-artifact'); const { grant } = signedGrant(archive); From 6083de6aee8942a78191ed18a00bfd9f4ba0902d Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 12:25:47 +0800 Subject: [PATCH 20/47] docs(plugins): integrate catalog load fix --- .project-docs/30-worklog/current-state.md | 11 ++++ ...901-plugin-catalog-load-client-7d4a8c21.md | 59 ------------------- ...lugin-catalog-main-integration-8e5c2a91.md | 48 +++++++++++++++ 3 files changed, 59 insertions(+), 59 deletions(-) delete mode 100644 .project-docs/30-worklog/tasks/20260901-plugin-catalog-load-client-7d4a8c21.md create mode 100644 .project-docs/30-worklog/tasks/20260901-plugin-catalog-main-integration-8e5c2a91.md diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index 7e4b493..09e9d65 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -4,6 +4,17 @@ This file is the integrated default-branch snapshot. Feature tasks record progre ## Integrated Through +- Marketplace weak-ETag interoperability fix source + `b3cfe7e1ceb7da65ccc99214db09102b4fc1cace` from task + `20260901-plugin-catalog-load-client-7d4a8c21` is promoted to local `main` by + task `20260901-plugin-catalog-main-integration-8e5c2a91` through product commit + `38f2358`. The Main-owned Marketplace client now accepts both strong and valid + weak composite catalog ETags, preserves the exact received validator for the next + `If-None-Match`, and retains catalog-generation and Token Point pricing identity + checks. This closes the installed-client failure where the production gateway's + compressed `W/"plugins-..."` response was incorrectly surfaced as + `plugin_backend_unavailable`; it does not add a local fallback catalog or change + server, Provider, trust, pricing, or billing authority. - Packaged Pi Agent Server resolver fix source `7df245af5a04f62be48980831ff41987ba686009` from task `20260901-local-runtime-unavailable-8b42c7f1` is promoted to local `main` by task `20260901-promote-local-runtime-main-a7c4e291` through verified diff --git a/.project-docs/30-worklog/tasks/20260901-plugin-catalog-load-client-7d4a8c21.md b/.project-docs/30-worklog/tasks/20260901-plugin-catalog-load-client-7d4a8c21.md deleted file mode 100644 index ab5ea4d..0000000 --- a/.project-docs/30-worklog/tasks/20260901-plugin-catalog-load-client-7d4a8c21.md +++ /dev/null @@ -1,59 +0,0 @@ -# Task: Diagnose MakeLore Marketplace catalog loading - -## Identity - -- Task ID: 20260901-plugin-catalog-load-client-7d4a8c21 -- Mode: Feature -- Branch: codex/20260901-plugin-catalog-load-client-7d4a8c21-plugin-catalog-load-client -- Worktree: D:\Datas\OthersProjects\makelore-plugin-catalog-load-client-7d4a8c21 -- Base commit: 5e8b7266c2a8297c9d307b576256886659d44780 -- Owner: codex-root -- Status: Ready for Integration - -## Scope - -- Reproduce the installed MakeLore Marketplace catalog failure at the Renderer → IPC dispatcher → Main Marketplace client boundary. -- Verify the installed 1.2.0 artifact contains the current Marketplace composition and compare its parser/network behavior with the live catalog. -- If a client defect is confirmed, change only the smallest Marketplace client/Host API/error-projection seam and focused tests. - -## Intent And Constraints - -- Preserve the occupied client root worktree and its untracked packaging record; all product work stays in this isolated worktree. -- Do not expose Works credentials, read real `.env` files, alter Package Store/Provider/billing state, or publish a package. -- Treat local-package inference as forbidden: a failed server catalog must remain fail closed. -- The tight loop must reproduce the exact initial catalog failure or prove each boundary healthy before any implementation change. - -## Plan - -1. Verify the live catalog parses through the exact client DTO parser. -2. Inspect the running installed Main process, local Host API, logs, embedded bundle, and effective API origin. -3. Reproduce the failing boundary with a focused test/probe, rank falsifiable causes, and implement only the confirmed fix. -4. Run focused regressions and task documentation gates; do not rebuild/publish unless the fix affects packaged behavior and needs an artifact proof. - -## Outcome - -- Completed. The Marketplace client now accepts both strong and standards-compliant weak composite ETags, while preserving the exact received validator for the next `If-None-Match` request and retaining all generation/pricing identity checks. -- Root cause: the production gateway emits `W/"plugins-..."` for the compressed catalog response. The previous strong-only regular expression rejected that otherwise valid HTTP 200 response, and the local Host API surfaced the rejection as `plugin_backend_unavailable`. -- No fallback catalog, server mutation, proxy workaround, or billing/Provider behavior was added. - -## Verification - -- Live catalog parsed through `parseMarketplaceCatalogPage`: HTTP 200, total 3, exact IDs `makelore.data-service`, `makelore.game-resource`, `makelore.web-search`. -- Installed MakeLore is version 1.2.0 and its `app.asar` contains the Marketplace route, service composition, and production origin `https://square.nianxx.cn`. -- Running local Host API is on `127.0.0.1:13210`; an unauthenticated shell probe correctly returns 401 and therefore cannot yet distinguish dispatcher/service from upstream failure. -- Installed-session Electron probes returned HTTP 200 and the weak composite ETag; the exact source Marketplace client failed before the fix with `marketplace_response_invalid: invalid composite Marketplace ETag` and returned all three official Plugins after the fix. -- TDD regression: the new weak-ETag/304-reuse case failed before the product change and passed after it. -- `pnpm exec vitest run tests/unit/coding-plugin-marketplace-client.test.ts tests/unit/plugin-marketplace-routes.test.ts tests/unit/plugin-marketplace-store.test.ts tests/unit/plugin-marketplace-pages.test.tsx`: 4 files, 68 tests passed. -- `pnpm test`: 214 files, 1761 tests passed, 2 expected skips; pressure test 1/1 passed. -- `pnpm run typecheck`: passed. -- `pnpm run lint:check`: 0 errors and 5 unchanged warnings outside this task. -- `pnpm run build:vite`: Renderer, Main, Preload, and utility builds passed. -- Scoped ESLint and `git diff --check`: passed. - -## Follow-ups - -- Integrate this task into the client main branch, rebuild the Windows artifact, and install/restart it. The currently installed MakeLore 1.2.0 artifact cannot pick up this source-only fix dynamically. - -## Promotion Candidates - -- None recorded. diff --git a/.project-docs/30-worklog/tasks/20260901-plugin-catalog-main-integration-8e5c2a91.md b/.project-docs/30-worklog/tasks/20260901-plugin-catalog-main-integration-8e5c2a91.md new file mode 100644 index 0000000..a5b328a --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-plugin-catalog-main-integration-8e5c2a91.md @@ -0,0 +1,48 @@ +# Task: Integrate and package Marketplace weak-ETag fix + +## Identity + +- Task ID: 20260901-plugin-catalog-main-integration-8e5c2a91 +- Mode: Integration +- Branch: main +- Worktree: D:\Datas\OthersProjects\makelore +- Base commit: 5e8b7266c2a8297c9d307b576256886659d44780 +- Owner: codex-root +- Status: In Progress + +## Scope + +- Integrate the completed weak-ETag Marketplace client fix from source commit `b3cfe7e1ceb7da65ccc99214db09102b4fc1cace` into local `main`. +- Reconcile the accepted fact into current project state without changing Marketplace architecture or authority boundaries. +- Rebuild and verify the Windows 1.2.0 artifact, then replace the currently installed client and verify the Plugin Center against the deployed catalog. + +## Intent And Constraints + +- The user explicitly authorized release/takeover of completed root task `20260901-package-120-4c7e`, integration, rebuild, and installation. +- Preserve the released task's untracked record `.project-docs/30-worklog/tasks/20260901-package-120-4c7e.md` byte-for-byte; to satisfy the Integration Gate it was moved without modification to `C:\Users\7brot\AppData\Local\Temp\codex-task-recovery-20260901-package-120-4c7e\20260901-package-120-4c7e.md` and remains outside this task's commit. +- Source task promotion candidates are empty and its sole parent is the exact current `main` base, so no semantic conflict or merge choice exists. +- Do not push, publish, deploy, mutate the Works Square catalog, or weaken trust/Provider/billing fail-closed behavior. +- Installation may stop the running MakeLore process, but must target only the exact generated MakeLore installer/application. + +## Plan + +1. Cherry-pick the exact source fix and remove only the duplicate source task record from the integration history. +2. Update canonical current state and run focused/full source verification from the integrated tree. +3. Build and verify the Windows artifact, stop MakeLore, install the exact artifact, and smoke the Plugin Center. +4. Record artifact/install evidence, complete project-document gates, and leave `main` clean except the preserved foreign task record. + +## Outcome + +- Product source commit `b3cfe7e1ceb7da65ccc99214db09102b4fc1cace` was integrated as local `main` commit `38f2358`; packaging and installed-client verification remain in progress. + +## Verification + +- Source commit sole parent equals integration base `5e8b7266c2a8297c9d307b576256886659d44780`; cherry-pick completed without conflict. + +## Follow-ups + +- None recorded. + +## Promotion Candidates + +- None recorded. From 850947c092892cb647c4191b6d8bbf37a763e1ad Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 12:38:41 +0800 Subject: [PATCH 21/47] docs(plugins): record installed catalog fix --- .project-docs/30-worklog/current-state.md | 10 +++++++++- ...plugin-catalog-main-integration-8e5c2a91.md | 18 +++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index 09e9d65..ffaa20c 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -14,7 +14,15 @@ This file is the integrated default-branch snapshot. Feature tasks record progre checks. This closes the installed-client failure where the production gateway's compressed `W/"plugins-..."` response was incorrectly surfaced as `plugin_backend_unavailable`; it does not add a local fallback catalog or change - server, Provider, trust, pricing, or billing authority. + server, Provider, trust, pricing, or billing authority. The rebuilt Windows 1.2.0 + installer embeds verification head `6083de6aee8942a78191ed18a00bfd9f4ba0902d`, + is 294,864,542 bytes with SHA-256 + `1301AE189BCBD44AA0E373982981E80B324EC45CDDF8F30415C2810F68319C06`, + and was installed over the prior 1.2.0 at the existing user-selected location. + The signed-in installed client then loaded the deployed generation-2 catalog and + rendered exactly Data Service, Game Resource, and Web Search without the unavailable + catalog error. Game Resource and Web Search still honestly show no stable Release; + production signing/publication activation remains a separate gate. - Packaged Pi Agent Server resolver fix source `7df245af5a04f62be48980831ff41987ba686009` from task `20260901-local-runtime-unavailable-8b42c7f1` is promoted to local `main` by task `20260901-promote-local-runtime-main-a7c4e291` through verified diff --git a/.project-docs/30-worklog/tasks/20260901-plugin-catalog-main-integration-8e5c2a91.md b/.project-docs/30-worklog/tasks/20260901-plugin-catalog-main-integration-8e5c2a91.md index a5b328a..66a656b 100644 --- a/.project-docs/30-worklog/tasks/20260901-plugin-catalog-main-integration-8e5c2a91.md +++ b/.project-docs/30-worklog/tasks/20260901-plugin-catalog-main-integration-8e5c2a91.md @@ -8,7 +8,7 @@ - Worktree: D:\Datas\OthersProjects\makelore - Base commit: 5e8b7266c2a8297c9d307b576256886659d44780 - Owner: codex-root -- Status: In Progress +- Status: Ready for Integration ## Scope @@ -33,15 +33,27 @@ ## Outcome -- Product source commit `b3cfe7e1ceb7da65ccc99214db09102b4fc1cace` was integrated as local `main` commit `38f2358`; packaging and installed-client verification remain in progress. +- Product source commit `b3cfe7e1ceb7da65ccc99214db09102b4fc1cace` was integrated as local `main` commit `38f2358`; canonical integration checkpoint is `6083de6aee8942a78191ed18a00bfd9f4ba0902d`. +- A Windows 1.2.0 artifact was rebuilt from that exact checkpoint, verified, installed over the prior 1.2.0 at `D:\Tools\泥土\niancode\Makelore`, and restarted with the existing signed-in session. +- The installed Plugin Center now renders exactly three deployed official Plugins—Data Service, Game Resource, and Web Search—and no longer shows `plugin_backend_unavailable`. +- Game Resource and Web Search are listed but still show no stable Release. That is the existing production signing/publication activation hold, not a catalog-loading regression; this task did not bypass it. ## Verification - Source commit sole parent equals integration base `5e8b7266c2a8297c9d307b576256886659d44780`; cherry-pick completed without conflict. +- Integrated focused Marketplace suite: 4 files / 68 tests passed; `pnpm run typecheck` passed. +- `pnpm run package:stage:win-x64` and electron-builder with `--config.extraMetadata.version=1.2.0` passed. The first aggregate download attempt stopped before build on an external GitHub `ECONNRESET`; staging reused the previous verified uv 0.10.0 binary (SHA-256 `5E559E322AD2F2E25E7D9C3CB51E3891AB0676A7E7B59EA250A021E4CB2F6E31`) and did not change tracked source. +- `pnpm run verify:publish-runtime`: passed with npm 11.6.2. +- `pnpm run verify:artifact:win -- --installer .\release\Makelore-1.2.0-win-x64.exe`: passed with embedded `gitCommit`/`verificationHead` `6083de6aee8942a78191ed18a00bfd9f4ba0902d`, Electron 43.4.0, Node 24.18.1, Python, uv, npm, native modules, and Unicode proof. +- `pnpm run verify:artifact:pi -- --app-exe .\release\win-unpacked\Makelore.exe --samples 2`: passed; Pi 0.84.2 closure, Marketplace markers, Web Search route/receipt authority, and bundled Data Service ten-tool catalog were present. +- Final installer: 294,864,542 bytes; SHA-256 `1301AE189BCBD44AA0E373982981E80B324EC45CDDF8F30415C2810F68319C06`. +- Final `app.asar`: 136,144,898 bytes; SHA-256 `5024322B112DC1839E306EB45953D216A4622149341A89E6BEE35018EDABEB1B`; the active Main bundle contains the optional weak `W/` composite ETag prefix and is selected by `dist-electron/main/index.js`. +- Silent installer exit code was 0. Installed `Makelore.exe` and `app.asar` hashes exactly matched `release/win-unpacked`; installed version remained 1.2.0. +- Signed-in installed-client UI smoke opened Code → Plugin Center and displayed `3 个插件`: Data Service, 游戏资源生成, and 联网搜索, with no catalog-unavailable error. Screenshot: `C:\Users\7brot\AppData\Local\Temp\makelore-plugin-center-installed-fixed.png`. ## Follow-ups -- None recorded. +- If Game Resource and Web Search must become acquirable rather than merely listed, complete the separate official signing key, signed stable Release, and production publication activation gate. Do not infer that authority from this client transport fix. ## Promotion Candidates From ae7936174208a1d13cdfd260d5c6f2b70b450b60 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 14:16:18 +0800 Subject: [PATCH 22/47] feat: consume server model reasoning capabilities --- ...0901-server-model-capabilities-9e31b6c4.md | 84 +++++++ electron/api/routes/coding-conversations.ts | 4 +- electron/api/routes/providers.ts | 16 +- electron/coding-projects/project-config.ts | 1 + electron/coding-runtime/pi/provider-config.ts | 33 ++- electron/coding-runtime/pi/runtime.ts | 1 + .../services/providers/provider-service.ts | 7 + electron/shared/providers/types.ts | 3 + shared/coding-conversation-contracts.ts | 2 +- shared/coding-conversation-reducer.ts | 2 +- shared/imported-model-profile.ts | 29 ++- shared/user-model-config.ts | 38 ++++ src/lib/providers.ts | 3 + .../Chat/CodingComposerRuntimeControls.tsx | 1 + .../coding-conversation-contracts.test.ts | 14 ++ tests/unit/coding-feature-ui.test.tsx | 55 +++++ tests/unit/coding-projects-schema-v2.test.ts | 13 ++ tests/unit/imported-model-profile.test.ts | 69 ++++++ tests/unit/pi-provider-config.test.ts | 210 +++++++++++++++++- tests/unit/provider-routes.test.ts | 146 +++++++++++- 20 files changed, 711 insertions(+), 20 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260901-server-model-capabilities-9e31b6c4.md diff --git a/.project-docs/30-worklog/tasks/20260901-server-model-capabilities-9e31b6c4.md b/.project-docs/30-worklog/tasks/20260901-server-model-capabilities-9e31b6c4.md new file mode 100644 index 0000000..1e0162b --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-server-model-capabilities-9e31b6c4.md @@ -0,0 +1,84 @@ +# Task: Consume server model reasoning capabilities + +## Identity + +- Task ID: 20260901-server-model-capabilities-9e31b6c4 +- Mode: Feature +- Branch: codex/20260901-server-model-capabilities-9e31b6c4-server-model-capabilities +- Worktree: D:\w\makelore-model-capabilities-9e31b6c4 +- Base commit: 850947c092892cb647c4191b6d8bbf37a763e1ad +- Owner: codex +- Status: Ready for integration + +## Scope + +- Consume per-model reasoning capabilities from Works Square model config and persist + them on the imported Main-owned Provider account. +- Make server metadata override the local Pi reasoning map for server-provisioned + models while retaining the local verified profile as an old-server/direct-provider + fallback. +- Add native `max` to the product-neutral thinking-level contract and composer menu so + DeepSeek exposes off/low/high/max without relabeling provider values. +- Keep one-api and Pi `0.84.2` unchanged. + +## Intent And Constraints + +- Electron Main remains the sole Works/model-config, Provider, credential, and Pi-wire + owner. Renderer receives only safe product-neutral levels. +- Strictly normalize the trusted response shape; a missing capability field means an + older server and falls back to the local profile. +- Preserve persisted user choices and all non-Works Provider behavior. +- Do not restore OpenCode, add a second runtime, or expose Provider credentials or raw + response data. +- Feature mode may update only product code, focused tests, and this task record. + +## Plan + +1. Add failing tests for Works capability import, Provider metadata persistence, + server-over-local Pi mapping, and native `max` menu/contract acceptance. +2. Implement the typed response parser, Provider metadata projection, capability-map + precedence, local DeepSeek fallback, and minimal `max` propagation through existing + product validators/UI. +3. Run focused unit tests, typecheck, scoped lint, Vite/Main build, and the project-docs + completion gate. + +## Outcome + +- Parsed and normalized Works Square `model_capabilities`, stored the safe result on + the managed Provider account, removed stale overrides when the optional server field + disappears, and included the metadata in Provider runtime-shape invalidation. +- Server-provided efforts now override the local Pi reasoning/thinking-level map while + preserving verified local modalities, token limits, and wire-format compatibility. + A capability for a future model without a local profile enables standard + `reasoning_effort` serialization automatically. +- Updated the verified DeepSeek fallback to expose only off/low/high/max. Pi sends + `thinking.type=disabled` with no `reasoning_effort` for off, and sends the exact + low/high/max effort for enabled requests. +- Added native `max` to the product contract, snapshot validation, project persistence, + Host route, Pi runtime, and composer UI where it is labelled `最高`. +- Kept Pi at 0.84.2 and made no one-api, dependency, README, or second-runtime change. + +## Verification + +- Focused final Vitest combination: 8 files and 102/102 tests passed, including actual + Pi `streamSimple` payload checks for off and each of low/high/max. +- `pnpm run typecheck`: passed. +- `pnpm run lint:check`: passed with the repository's existing five warnings and no + errors; a separate scoped ESLint run over every changed TypeScript/TSX file passed. +- `pnpm run build:vite`: passed; only existing Browserslist, dynamic-import, and chunk + size warnings were emitted. +- `git diff --check`: passed. + +## Follow-ups + +- Integrate with the paired Works Square API change. Deployment order is tolerant: + a client talking to an older server uses the corrected local DeepSeek profile, and + older clients ignore the new server response member. +- Verify future server model entries against their provider's exact effort vocabulary + before publishing them; no one-api change is required while it remains transparent. + +## Promotion Candidates + +- Promote the Main-owned capability normalization/precedence rule and the safe + server-to-Provider metadata contract into canonical architecture documentation after + the paired server and client branches are integrated. diff --git a/electron/api/routes/coding-conversations.ts b/electron/api/routes/coding-conversations.ts index 499a904..8666664 100644 --- a/electron/api/routes/coding-conversations.ts +++ b/electron/api/routes/coding-conversations.ts @@ -12,7 +12,7 @@ import { } from '../route-utils'; import { decodeRouteId, sendCodingRouteError } from './coding-route-errors'; -const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high']); +const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'max']); function invalidRequest(message: string): never { throw new CodingConversationServiceError(400, 'CODING_CONVERSATION_REQUEST_INVALID', message); @@ -187,7 +187,7 @@ export async function handleCodingConversationRoutes( sendJson(res, 200, { model: await service.setThinking( conversationId, - body.thinkingLevel as 'off' | 'minimal' | 'low' | 'medium' | 'high', + body.thinkingLevel as 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'max', ), }); return true; diff --git a/electron/api/routes/providers.ts b/electron/api/routes/providers.ts index 62cec1d..e5d5050 100644 --- a/electron/api/routes/providers.ts +++ b/electron/api/routes/providers.ts @@ -20,8 +20,10 @@ import { seedWorksSquareAIGatewayCredential } from '../../services/works-square- import { NIANCODE_USER_MODEL_ACCOUNT_ID, NIANCODE_USER_MODEL_ACCOUNT_LABEL, + normalizeImportedModelCapabilities, normalizeImportedUserModelId, } from '../../../shared/user-model-config'; +import type { ImportedModelCapabilities } from '../../../shared/imported-model-profile'; const legacyProviderRoutesWarned = new Set(); @@ -141,6 +143,7 @@ type ImportedUserModelConfig = { credentialMode: string; apiKeyExpiresIn: number | null; models: string[]; + modelCapabilities?: ImportedModelCapabilities; }; const WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE = 'works_square_ai_gateway'; @@ -157,7 +160,7 @@ class WorksSquareModelConfigError extends Error { } } -function normalizeImportedUserModelConfig(payload: unknown): ImportedUserModelConfig { +export function normalizeImportedUserModelConfig(payload: unknown): ImportedUserModelConfig { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw new Error('Works Square model config response is invalid'); } @@ -181,6 +184,7 @@ function normalizeImportedUserModelConfig(payload: unknown): ImportedUserModelCo const label = typeof record.label === 'string' && record.label.trim() ? record.label.trim() : NIANCODE_USER_MODEL_ACCOUNT_LABEL; + const modelCapabilities = normalizeImportedModelCapabilities(record.model_capabilities, models); return { label, @@ -189,6 +193,7 @@ function normalizeImportedUserModelConfig(payload: unknown): ImportedUserModelCo credentialMode: credentialMode || 'api_key', apiKeyExpiresIn, models, + ...(modelCapabilities ? { modelCapabilities } : {}), }; } @@ -211,9 +216,13 @@ function importedUserModelMetadata( if (useLocalAiProxy) { const metadata = { ...(existing?.metadata ?? {}) }; delete metadata.worksSquareCredentialExpiresAt; + delete metadata.worksSquareModelCapabilities; return { ...metadata, customModels: modelConfig.models, + ...(modelConfig.modelCapabilities + ? { worksSquareModelCapabilities: modelConfig.modelCapabilities } + : {}), worksSquareCredentialMode: WORKS_SQUARE_AI_GATEWAY_PROXY_CREDENTIAL_MODE, worksSquareOneApiBaseUrl: modelConfig.baseUrl, }; @@ -222,12 +231,16 @@ function importedUserModelMetadata( const metadata = { ...(existing?.metadata ?? {}) }; delete metadata.worksSquareCredentialExpiresAt; delete metadata.worksSquareOneApiBaseUrl; + delete metadata.worksSquareModelCapabilities; const credentialExpiresAt = modelConfig.apiKeyExpiresIn === null ? undefined : new Date(nowMs + modelConfig.apiKeyExpiresIn * 1000).toISOString(); return { ...metadata, customModels: modelConfig.models, + ...(modelConfig.modelCapabilities + ? { worksSquareModelCapabilities: modelConfig.modelCapabilities } + : {}), worksSquareCredentialMode: modelConfig.credentialMode, ...(credentialExpiresAt ? { worksSquareCredentialExpiresAt: credentialExpiresAt } : {}), }; @@ -295,6 +308,7 @@ function providerAccountRuntimeShape(account: ProviderAccount): unknown { isDefault: account.isDefault, metadata: { customModels: account.metadata?.customModels, + worksSquareModelCapabilities: account.metadata?.worksSquareModelCapabilities, worksSquareCredentialMode: account.metadata?.worksSquareCredentialMode, worksSquareOneApiBaseUrl: account.metadata?.worksSquareOneApiBaseUrl, }, diff --git a/electron/coding-projects/project-config.ts b/electron/coding-projects/project-config.ts index 0172f94..c53de72 100644 --- a/electron/coding-projects/project-config.ts +++ b/electron/coding-projects/project-config.ts @@ -72,6 +72,7 @@ const THINKING_LEVELS = new Set([ 'low', 'medium', 'high', + 'max', ]); function projectConfigPath(projectPath: string): string { diff --git a/electron/coding-runtime/pi/provider-config.ts b/electron/coding-runtime/pi/provider-config.ts index ad6d374..a3ac93f 100644 --- a/electron/coding-runtime/pi/provider-config.ts +++ b/electron/coding-runtime/pi/provider-config.ts @@ -17,7 +17,10 @@ import { normalizeImportedUserModelId, selectUserModelRuntimeAccounts, } from '../../../shared/user-model-config'; -import { getImportedModelProfile } from '../../../shared/imported-model-profile'; +import { + getImportedModelProfile, + thinkingLevelMapForImportedModelCapability, +} from '../../../shared/imported-model-profile'; const PI_ENV_PREFIX = 'MAKELORE_PI'; const WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE = 'works_square_ai_gateway'; @@ -302,6 +305,7 @@ function modelDescriptor( )); const backend = backendModels.get(modelId); const profile = getImportedModelProfile(modelId); + const serverCapability = account.metadata?.worksSquareModelCapabilities?.[modelId]; const backendInput = Array.isArray(backend?.input) ? backend.input.filter((input): input is 'text' | 'image' => input === 'text' || input === 'image') : []; @@ -319,17 +323,28 @@ function modelDescriptor( id: modelId, name: summary?.name || (typeof backend?.name === 'string' && backend.name.trim()) || modelId, input: supportsImage ? ['text', 'image'] : ['text'], - reasoning: summary?.supportsReasoning === true - || profile?.pi?.reasoning === true - || backend?.reasoning === true, + reasoning: serverCapability + ? serverCapability.reasoningEfforts.length > 0 + : summary?.supportsReasoning === true + || profile?.pi?.reasoning === true + || backend?.reasoning === true, ...(contextWindow ? { contextWindow } : {}), ...(maxOutputTokens ? { maxOutputTokens } : {}), - ...(compat || profile?.pi?.compat || enforcedCompat - ? { compat: { ...compat, ...profile?.pi?.compat, ...enforcedCompat } } - : {}), - ...(profile?.pi?.thinkingLevelMap - ? { thinkingLevelMap: { ...profile.pi.thinkingLevelMap } } + ...(compat || profile?.pi?.compat || enforcedCompat || serverCapability + ? { + compat: { + ...compat, + ...profile?.pi?.compat, + ...enforcedCompat, + ...(serverCapability ? { supportsReasoningEffort: true } : {}), + }, + } : {}), + ...(serverCapability + ? { thinkingLevelMap: thinkingLevelMapForImportedModelCapability(serverCapability) } + : profile?.pi?.thinkingLevelMap + ? { thinkingLevelMap: { ...profile.pi.thinkingLevelMap } } + : {}), }; } diff --git a/electron/coding-runtime/pi/runtime.ts b/electron/coding-runtime/pi/runtime.ts index 56bc7f9..acc69b7 100644 --- a/electron/coding-runtime/pi/runtime.ts +++ b/electron/coding-runtime/pi/runtime.ts @@ -459,6 +459,7 @@ const PRODUCT_THINKING_LEVELS = new Set([ 'low', 'medium', 'high', + 'max', ]); function productThinkingLevel(value: unknown): ProductModelRef['thinkingLevel'] | null { diff --git a/electron/services/providers/provider-service.ts b/electron/services/providers/provider-service.ts index 1aa53c9..dec19e2 100644 --- a/electron/services/providers/provider-service.ts +++ b/electron/services/providers/provider-service.ts @@ -29,6 +29,7 @@ import { } from '../../utils/secure-storage'; import type { ProviderWithKeyInfo } from '../../shared/providers/types'; import { logger } from '../../utils/logger'; +import { normalizeImportedModelCapabilities } from '../../../shared/user-model-config'; function maskApiKey(apiKey: string | null): string | null { if (!apiKey) return null; @@ -69,6 +70,12 @@ function normalizeSyncedMetadata(metadata: Record): ProviderAcc result.customModels = customModels; } } + const worksSquareModelCapabilities = normalizeImportedModelCapabilities( + metadata.worksSquareModelCapabilities, + ); + if (worksSquareModelCapabilities) { + result.worksSquareModelCapabilities = worksSquareModelCapabilities; + } return result; } diff --git a/electron/shared/providers/types.ts b/electron/shared/providers/types.ts index 6f196e0..6994fbc 100644 --- a/electron/shared/providers/types.ts +++ b/electron/shared/providers/types.ts @@ -1,3 +1,5 @@ +import type { ImportedModelCapabilities } from '../../../shared/imported-model-profile'; + export const PROVIDER_TYPES = [ 'anthropic', 'openai', @@ -133,6 +135,7 @@ export interface ProviderAccount { email?: string; resourceUrl?: string; customModels?: string[]; + worksSquareModelCapabilities?: ImportedModelCapabilities; worksSquareCredentialMode?: string; worksSquareCredentialExpiresAt?: string; worksSquareOneApiBaseUrl?: string; diff --git a/shared/coding-conversation-contracts.ts b/shared/coding-conversation-contracts.ts index 111e0a9..76d8f28 100644 --- a/shared/coding-conversation-contracts.ts +++ b/shared/coding-conversation-contracts.ts @@ -2,7 +2,7 @@ import type { CapabilityResultV1 } from './data-service'; export type { CapabilityBillingReceiptV1, CapabilityResultV1 } from './data-service'; -export type ConversationThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high'; +export type ConversationThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'max'; export interface ProductModelRef { accountId: string; diff --git a/shared/coding-conversation-reducer.ts b/shared/coding-conversation-reducer.ts index 985ba23..efd53f3 100644 --- a/shared/coding-conversation-reducer.ts +++ b/shared/coding-conversation-reducer.ts @@ -54,7 +54,7 @@ const RUN_STATUSES = new Set([ ]); const WORKER_STATUSES = new Set(['stopped', 'starting', 'ready', 'recovering', 'error']); -const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high']); +const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'max']); const ERROR_CODES = new Set([ 'CODING_RUNTIME_START_FAILED', 'CODING_RUNTIME_READY_TIMEOUT', diff --git a/shared/imported-model-profile.ts b/shared/imported-model-profile.ts index f74892d..ce31fa6 100644 --- a/shared/imported-model-profile.ts +++ b/shared/imported-model-profile.ts @@ -1,6 +1,28 @@ export type ImportedModelModality = 'text' | 'audio' | 'image' | 'pdf'; export type ImportedVisionTokenEstimator = 'qwen-32px-grid'; -export type ImportedThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high'; +export type ImportedThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'max'; +export const IMPORTED_REASONING_EFFORTS = ['low', 'high', 'max'] as const; +export type ImportedReasoningEffort = (typeof IMPORTED_REASONING_EFFORTS)[number]; + +export interface ImportedModelCapability { + reasoningEfforts: ImportedReasoningEffort[]; + reasoningCanDisable: boolean; +} + +export type ImportedModelCapabilities = Record; + +export function thinkingLevelMapForImportedModelCapability( + capability: ImportedModelCapability, +): Partial> { + return { + ...(capability.reasoningCanDisable ? {} : { off: null }), + minimal: null, + low: capability.reasoningEfforts.includes('low') ? 'low' : null, + medium: null, + high: capability.reasoningEfforts.includes('high') ? 'high' : null, + max: capability.reasoningEfforts.includes('max') ? 'max' : null, + }; +} export interface ImportedPiModelProfile { reasoning: boolean; @@ -62,14 +84,15 @@ export function getImportedModelProfile(rawModelId: string): ImportedModelProfil pi: { reasoning: true, thinkingLevelMap: { - off: null, minimal: null, - low: null, + low: 'low', medium: null, high: 'high', + max: 'max', }, compat: { thinkingFormat: 'deepseek', + supportsReasoningEffort: true, requiresReasoningContentOnAssistantMessages: true, }, }, diff --git a/shared/user-model-config.ts b/shared/user-model-config.ts index 9707b81..dbd4fac 100644 --- a/shared/user-model-config.ts +++ b/shared/user-model-config.ts @@ -1,3 +1,10 @@ +import { + IMPORTED_REASONING_EFFORTS, + type ImportedModelCapabilities, + type ImportedModelCapability, + type ImportedReasoningEffort, +} from './imported-model-profile'; + export const NIANCODE_USER_MODEL_ACCOUNT_ID = 'niancode-user-models'; export const NIANCODE_USER_MODEL_ACCOUNT_LABEL = 'Makelore Models'; @@ -23,6 +30,37 @@ export function normalizeImportedUserModelId(rawModel: string): string { : trimmed; } +function normalizeImportedModelCapability(value: unknown): ImportedModelCapability | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + const record = value as Record; + const rawEfforts = record.reasoning_efforts ?? record.reasoningEfforts; + const reasoningCanDisable = record.reasoning_can_disable ?? record.reasoningCanDisable; + if (!Array.isArray(rawEfforts) || typeof reasoningCanDisable !== 'boolean') return null; + const reasoningEfforts = IMPORTED_REASONING_EFFORTS.filter((effort) => ( + rawEfforts.some((candidate) => candidate === effort) + )) as ImportedReasoningEffort[]; + if (reasoningEfforts.length === 0 && rawEfforts.length > 0) return null; + return { reasoningEfforts, reasoningCanDisable }; +} + +export function normalizeImportedModelCapabilities( + value: unknown, + modelIds?: readonly string[], +): ImportedModelCapabilities | undefined { + if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined; + const allowedModelIds = modelIds + ? new Set(modelIds.map(normalizeImportedUserModelId)) + : null; + const result: ImportedModelCapabilities = {}; + for (const [rawModelId, rawCapability] of Object.entries(value as Record)) { + const modelId = normalizeImportedUserModelId(rawModelId); + if (!modelId || (allowedModelIds && !allowedModelIds.has(modelId))) continue; + const capability = normalizeImportedModelCapability(rawCapability); + if (capability && !result[modelId]) result[modelId] = capability; + } + return Object.keys(result).length > 0 ? result : undefined; +} + export function selectUserModelRuntimeAccounts( accounts: T[], ): T[] { diff --git a/src/lib/providers.ts b/src/lib/providers.ts index ab7b81a..1b9be96 100644 --- a/src/lib/providers.ts +++ b/src/lib/providers.ts @@ -6,6 +6,8 @@ * layer so TypeScript project boundaries remain stable during the migration. */ +import type { ImportedModelCapabilities } from '../../shared/imported-model-profile'; + export const PROVIDER_TYPES = [ 'anthropic', 'openai', @@ -125,6 +127,7 @@ export interface ProviderAccount { email?: string; resourceUrl?: string; customModels?: string[]; + worksSquareModelCapabilities?: ImportedModelCapabilities; worksSquareCredentialMode?: string; worksSquareCredentialExpiresAt?: string; }; diff --git a/src/pages/Chat/CodingComposerRuntimeControls.tsx b/src/pages/Chat/CodingComposerRuntimeControls.tsx index e051b5b..f582905 100644 --- a/src/pages/Chat/CodingComposerRuntimeControls.tsx +++ b/src/pages/Chat/CodingComposerRuntimeControls.tsx @@ -30,6 +30,7 @@ const THINKING_OPTIONS: Array<{ value: ConversationThinkingLevel; label: string { value: 'low', label: '低' }, { value: 'medium', label: '中等' }, { value: 'high', label: '高' }, + { value: 'max', label: '最高' }, ]; function thinkingLabel(level: ConversationThinkingLevel): string { diff --git a/tests/unit/coding-conversation-contracts.test.ts b/tests/unit/coding-conversation-contracts.test.ts index 6885255..5af968e 100644 --- a/tests/unit/coding-conversation-contracts.test.ts +++ b/tests/unit/coding-conversation-contracts.test.ts @@ -50,6 +50,20 @@ function envelope( } describe('Conversation product contracts', () => { + it('accepts max in the persisted model and available thinking-level contract', () => { + const snapshot = createProductSnapshot(); + snapshot.conversation.model = { + model: { + accountId: 'account-max', + modelId: 'deepseek-v4-pro', + thinkingLevel: 'max', + }, + modelResolution: 'resolved', + availableThinkingLevels: ['off', 'low', 'high', 'max'], + }; + expect(isConversationSnapshot(snapshot)).toBe(true); + }); + it('accepts bounded capability envelopes for every Data Service operation', () => { const control = new Set(['configure', 'inspect', 'list_projects', 'remove_collection', 'reset', 'remove_project']); const operations = [ diff --git a/tests/unit/coding-feature-ui.test.tsx b/tests/unit/coding-feature-ui.test.tsx index 729a93c..fbe9e6c 100644 --- a/tests/unit/coding-feature-ui.test.tsx +++ b/tests/unit/coding-feature-ui.test.tsx @@ -528,6 +528,61 @@ describe('PI-130 feature-complete Coding UI', () => { expect(screen.queryByRole('menu', { name: '选择推理强度' })).not.toBeInTheDocument(); }); + it('shows the native max thinking level as 最高', async () => { + const { CodingComposerRuntimeControls } = await import('@/pages/Chat/CodingComposerRuntimeControls'); + const conversation = { + id: 'conversation-max-thinking', + agentId: 'agent-1', + title: 'Max thinking controls', + archivedAt: null, + unread: false, + createdAt: '2026-08-28T00:00:00.000Z', + updatedAt: '2026-08-28T00:00:00.000Z', + model: { accountId: 'account-1', modelId: 'deepseek-v4-pro', thinkingLevel: 'max' as const }, + modelResolution: 'resolved' as const, + }; + const snapshot: ConversationSnapshot = { + schemaVersion: 1, + conversation: { + id: conversation.id, + projectId: 'project-1', + agentId: conversation.agentId, + title: conversation.title, + model: { + model: conversation.model, + modelResolution: 'resolved', + availableThinkingLevels: ['off', 'low', 'high', 'max'], + }, + }, + nodes: [], + run: { status: 'idle' }, + queue: { items: [] }, + context: { usedTokens: 0, contextWindow: 0, compaction: 'idle' }, + pendingInteractions: [], + worker: { status: 'ready', generation: 1 }, + cursor: { workerGeneration: 1, seq: 0 }, + }; + + render( + undefined)} + />, + ); + + const settingsTrigger = screen.getByRole('button', { + name: '模型与思考设置:deepseek-v4-pro,最高', + }); + expect(settingsTrigger).toHaveTextContent('最高'); + fireEvent.pointerDown(settingsTrigger, { button: 0, ctrlKey: false }); + const thinkingRow = await screen.findByRole('menuitem', { name: '推理强度 最高' }); + fireEvent.click(thinkingRow); + expect(screen.getByRole('menuitemradio', { name: '最高' })).toBeInTheDocument(); + expect(screen.queryByRole('menuitemradio', { name: '极简' })).not.toBeInTheDocument(); + expect(screen.queryByRole('menuitemradio', { name: '中等' })).not.toBeInTheDocument(); + }); + it('blocks overlapping mutations but keeps abort and recover available while confirmation is uncertain', async () => { interactionApi.abort.mockResolvedValue(undefined); const recover = vi.fn(async () => undefined); diff --git a/tests/unit/coding-projects-schema-v2.test.ts b/tests/unit/coding-projects-schema-v2.test.ts index fa634ee..f6b1a9f 100644 --- a/tests/unit/coding-projects-schema-v2.test.ts +++ b/tests/unit/coding-projects-schema-v2.test.ts @@ -13,6 +13,7 @@ import { atomicWriteJson } from '../../electron/coding-projects/atomic-json'; import { createCodingProjectAgent, createCodingProjectConfigV2, + normalizeProductModelRef, readCodingProjectConfigV2, } from '../../electron/coding-projects/project-config'; import { @@ -54,6 +55,18 @@ afterEach(async () => { }); describe('coding project schema v2', () => { + it('accepts the native max thinking level in a project model reference', () => { + expect(normalizeProductModelRef({ + accountId: 'account-local', + modelId: 'deepseek-v4-pro', + thinkingLevel: 'max', + })).toEqual({ + accountId: 'account-local', + modelId: 'deepseek-v4-pro', + thinkingLevel: 'max', + }); + }); + it('creates project, Agent, and empty Conversation metadata without a runtime child', async () => { const projectPath = await makeProjectPath(); const storage = createMemoryCodingProjectStorage(); diff --git a/tests/unit/imported-model-profile.test.ts b/tests/unit/imported-model-profile.test.ts index 2bccc9b..cbcd129 100644 --- a/tests/unit/imported-model-profile.test.ts +++ b/tests/unit/imported-model-profile.test.ts @@ -3,6 +3,7 @@ import { estimateImportedModelVisionTokens, getImportedModelProfile, } from '../../shared/imported-model-profile'; +import { normalizeImportedModelCapabilities } from '../../shared/user-model-config'; import { getKnownModelCapabilityKind, getModelCapabilityLabel, @@ -10,6 +11,74 @@ import { } from '../../shared/model-capabilities'; describe('getImportedModelProfile', () => { + it('returns native DeepSeek V4 Pro thinking levels without generic placeholders', () => { + expect(getImportedModelProfile('deepseek-v4-pro')).toMatchObject({ + modalities: { + input: ['text'], + output: ['text'], + }, + limit: { + context: 1_000_000, + output: 384_000, + }, + pi: { + reasoning: true, + thinkingLevelMap: { + minimal: null, + low: 'low', + medium: null, + high: 'high', + max: 'max', + }, + compat: { + thinkingFormat: 'deepseek', + supportsReasoningEffort: true, + requiresReasoningContentOnAssistantMessages: true, + }, + }, + }); + }); + + it('normalizes Works model capabilities by normalized model id and supported effort', () => { + expect(normalizeImportedModelCapabilities({ + 'deepseek/deepseek-v4-pro': { + reasoning_efforts: ['max', 'low', 'low', 'medium', 'unsupported'], + reasoning_can_disable: true, + }, + 'qwen3.8-max': { + reasoning_efforts: ['high'], + reasoning_can_disable: false, + }, + 'unknown-model': { + reasoning_efforts: ['low'], + reasoning_can_disable: 'yes', + }, + }, ['deepseek-v4-pro', 'qwen3.8-max'])).toEqual({ + 'deepseek-v4-pro': { + reasoningEfforts: ['low', 'max'], + reasoningCanDisable: true, + }, + 'qwen3.8-max': { + reasoningEfforts: ['high'], + reasoningCanDisable: false, + }, + }); + }); + + it('preserves an explicit empty effort list so the server can disable local reasoning metadata', () => { + expect(normalizeImportedModelCapabilities({ + 'deepseek-v4-pro': { + reasoning_efforts: [], + reasoning_can_disable: false, + }, + }, ['deepseek-v4-pro'])).toEqual({ + 'deepseek-v4-pro': { + reasoningEfforts: [], + reasoningCanDisable: false, + }, + }); + }); + it.each([ 'qwen3.6-plus', 'qwen3.6-plus-2026-04-02', diff --git a/tests/unit/pi-provider-config.test.ts b/tests/unit/pi-provider-config.test.ts index c3714f1..b21dc42 100644 --- a/tests/unit/pi-provider-config.test.ts +++ b/tests/unit/pi-provider-config.test.ts @@ -2,6 +2,8 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { streamSimple } from '@earendil-works/pi-ai/api/openai-completions'; +import type { Model } from '@earendil-works/pi-ai'; import type { ProviderAccount } from '@electron/shared/providers/types'; import { buildPiProviderCatalog, @@ -269,11 +271,11 @@ describe('Pi Provider catalog', () => { requiresReasoningContentOnAssistantMessages: true, }, thinkingLevelMap: { - off: null, minimal: null, - low: null, + low: 'low', medium: null, high: 'high', + max: 'max', }, }); expect(written).toMatchObject({ @@ -283,6 +285,210 @@ describe('Pi Provider catalog', () => { }); }); + it('uses server reasoning capabilities while retaining local model metadata', () => { + const provider = account({ + id: 'niancode-user-models', + vendorId: 'custom', + apiProtocol: 'openai-completions', + baseUrl: 'http://127.0.0.1:54321/api/ai-proxy/v1', + model: 'qwen3.8-max', + metadata: { + worksSquareCredentialMode: 'works_square_ai_gateway_proxy', + customModels: ['qwen3.8-max'], + worksSquareModelCapabilities: { + 'qwen3.8-max': { + reasoningEfforts: ['low'], + reasoningCanDisable: false, + }, + }, + }, + }); + + const descriptor = buildPiProviderCatalog({ accounts: [provider] }).descriptors[0]!.models[0]!; + + expect(descriptor).toMatchObject({ + id: 'qwen3.8-max', + input: ['text', 'image'], + reasoning: true, + contextWindow: 1_000_000, + maxOutputTokens: 131_072, + thinkingLevelMap: { + off: null, + minimal: null, + low: 'low', + medium: null, + high: null, + max: null, + }, + compat: { + thinkingFormat: 'qwen', + supportsDeveloperRole: false, + supportsReasoningEffort: true, + supportsStore: false, + }, + }); + }); + + it('enables reasoning-effort serialization for a server model without a local profile', () => { + const provider = account({ + id: 'niancode-user-models', + vendorId: 'custom', + apiProtocol: 'openai-completions', + baseUrl: 'http://127.0.0.1:54321/api/ai-proxy/v1', + model: 'future-reasoning-model', + metadata: { + worksSquareCredentialMode: 'works_square_ai_gateway_proxy', + customModels: ['future-reasoning-model'], + worksSquareModelCapabilities: { + 'future-reasoning-model': { + reasoningEfforts: ['low', 'high', 'max'], + reasoningCanDisable: true, + }, + }, + }, + }); + + const descriptor = buildPiProviderCatalog({ accounts: [provider] }).descriptors[0]!.models[0]!; + + expect(descriptor).toMatchObject({ + id: 'future-reasoning-model', + reasoning: true, + compat: { + supportsDeveloperRole: false, + supportsReasoningEffort: true, + }, + thinkingLevelMap: { + minimal: null, + low: 'low', + medium: null, + high: 'high', + max: 'max', + }, + }); + expect(descriptor.thinkingLevelMap).not.toHaveProperty('off'); + }); + + it('lets an explicit empty server effort list override a locally reasoning model', () => { + const provider = account({ + id: 'niancode-user-models', + vendorId: 'custom', + apiProtocol: 'openai-completions', + baseUrl: 'http://127.0.0.1:54321/api/ai-proxy/v1', + model: 'deepseek-v4-pro', + metadata: { + worksSquareCredentialMode: 'works_square_ai_gateway_proxy', + customModels: ['deepseek-v4-pro'], + worksSquareModelCapabilities: { + 'deepseek-v4-pro': { + reasoningEfforts: [], + reasoningCanDisable: false, + }, + }, + }, + }); + + const descriptor = buildPiProviderCatalog({ accounts: [provider] }).descriptors[0]!.models[0]!; + + expect(descriptor.reasoning).toBe(false); + expect(descriptor.thinkingLevelMap).toEqual({ + off: null, + minimal: null, + low: null, + medium: null, + high: null, + max: null, + }); + expect(descriptor.compat).toMatchObject({ + thinkingFormat: 'deepseek', + supportsReasoningEffort: true, + requiresReasoningContentOnAssistantMessages: true, + }); + }); + + it('serializes DeepSeek off without sending a reasoning effort', async () => { + const payloads: unknown[] = []; + const model: Model<'openai-completions'> = { + id: 'deepseek-v4-pro', + name: 'DeepSeek V4 Pro', + api: 'openai-completions', + provider: 'deepseek', + baseUrl: 'https://gateway.test/v1', + reasoning: true, + input: ['text'], + contextWindow: 1_000_000, + maxTokens: 384_000, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + thinkingLevelMap: { + minimal: null, + low: 'low', + medium: null, + high: 'high', + max: 'max', + }, + compat: { + thinkingFormat: 'deepseek', + supportsReasoningEffort: true, + }, + }; + + const stream = streamSimple(model, { messages: [] }, { + apiKey: 'test-key', + reasoning: 'off', + onPayload(payload) { + payloads.push(payload); + throw new Error('stop before network'); + }, + }); + const result = await stream.result(); + + expect(result.stopReason).toBe('error'); + expect(payloads[0]).toMatchObject({ thinking: { type: 'disabled' } }); + expect(payloads[0]).not.toHaveProperty('reasoning_effort'); + }); + + it.each(['low', 'high', 'max'] as const)('serializes DeepSeek %s with its reasoning effort', async (level) => { + const payloads: unknown[] = []; + const model: Model<'openai-completions'> = { + id: 'deepseek-v4-pro', + name: 'DeepSeek V4 Pro', + api: 'openai-completions', + provider: 'deepseek', + baseUrl: 'https://gateway.test/v1', + reasoning: true, + input: ['text'], + contextWindow: 1_000_000, + maxTokens: 384_000, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + thinkingLevelMap: { + minimal: null, + low: 'low', + medium: null, + high: 'high', + max: 'max', + }, + compat: { + thinkingFormat: 'deepseek', + supportsReasoningEffort: true, + }, + }; + + const stream = streamSimple(model, { messages: [] }, { + apiKey: 'test-key', + reasoning: level, + onPayload(payload) { + payloads.push(payload); + throw new Error('stop before network'); + }, + }); + const result = await stream.result(); + + expect(result.stopReason).toBe('error'); + expect(payloads[0]).toMatchObject({ + thinking: { type: 'enabled' }, + reasoning_effort: level, + }); + }); + it('does not replace an existing catalog when model selection is unavailable', async () => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-provider-')); temporaryRoots.push(root); diff --git a/tests/unit/provider-routes.test.ts b/tests/unit/provider-routes.test.ts index 7164a04..12ba377 100644 --- a/tests/unit/provider-routes.test.ts +++ b/tests/unit/provider-routes.test.ts @@ -1,20 +1,30 @@ import { EventEmitter } from 'node:events'; import type { IncomingMessage, ServerResponse } from 'node:http'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { handleProviderRoutes } from '@electron/api/routes/providers'; +import { + handleProviderRoutes, + importCurrentUserModelConfig, + normalizeImportedUserModelConfig, +} from '@electron/api/routes/providers'; import type { ProviderAccount } from '@electron/shared/providers/types'; const providerServiceMock = vi.hoisted(() => ({ getAccount: vi.fn(), + getAccountApiKey: vi.fn(), getDefaultAccountId: vi.fn(), updateAccount: vi.fn(), setDefaultAccount: vi.fn(), })); +const proxyAwareFetchMock = vi.hoisted(() => vi.fn()); vi.mock('@electron/services/providers/provider-service', () => ({ getProviderService: () => providerServiceMock, })); +vi.mock('@electron/utils/proxy-fetch', () => ({ + proxyAwareFetch: (...args: unknown[]) => proxyAwareFetchMock(...args), +})); + function createRequest(method: string, body?: unknown): IncomingMessage { const request = new EventEmitter(); Object.assign(request, { @@ -66,6 +76,7 @@ describe('provider host api routes', () => { beforeEach(() => { vi.clearAllMocks(); providerServiceMock.getAccount.mockResolvedValue(account()); + providerServiceMock.getAccountApiKey.mockResolvedValue('stored-key'); providerServiceMock.updateAccount.mockResolvedValue(account({ updatedAt: '2026-08-24T01:00:00.000Z', })); @@ -73,6 +84,139 @@ describe('provider host api routes', () => { providerServiceMock.setDefaultAccount.mockResolvedValue(undefined); }); + it('normalizes optional Works model capabilities before storing the account', () => { + expect(normalizeImportedUserModelConfig({ + label: 'Makelore Models', + base_url: 'https://gateway.test/v1', + api_key: 'gateway-key', + credential_mode: 'works_square_ai_gateway', + models: ['deepseek/deepseek-v4-pro', 'qwen3.8-max'], + model_capabilities: { + 'deepseek/deepseek-v4-pro': { + reasoning_efforts: ['max', 'low', 'medium', 'low'], + reasoning_can_disable: true, + }, + 'qwen3.8-max': { + reasoning_efforts: ['high'], + reasoning_can_disable: false, + }, + 'not-provisioned': { + reasoning_efforts: ['low'], + reasoning_can_disable: true, + }, + }, + })).toMatchObject({ + models: ['deepseek-v4-pro', 'qwen3.8-max'], + modelCapabilities: { + 'deepseek-v4-pro': { + reasoningEfforts: ['low', 'max'], + reasoningCanDisable: true, + }, + 'qwen3.8-max': { + reasoningEfforts: ['high'], + reasoningCanDisable: false, + }, + }, + }); + }); + + it('persists server capabilities and invalidates the runtime when they change', async () => { + const existing = account({ + id: 'niancode-user-models', + vendorId: 'custom', + model: 'deepseek-v4-pro', + metadata: { + customModels: ['deepseek-v4-pro'], + worksSquareModelCapabilities: { + 'deepseek-v4-pro': { + reasoningEfforts: ['high'], + reasoningCanDisable: false, + }, + }, + }, + }); + providerServiceMock.getAccount.mockResolvedValue(existing); + providerServiceMock.updateAccount.mockImplementation(async (_accountId: string, patch: Partial) => ({ + ...existing, + ...patch, + })); + proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({ + label: 'Makelore Models', + base_url: 'https://gateway.test/v1', + api_key: 'gateway-key', + credential_mode: 'api_key', + models: ['deepseek/deepseek-v4-pro'], + model_capabilities: { + 'deepseek/deepseek-v4-pro': { + reasoning_efforts: ['low', 'high', 'max'], + reasoning_can_disable: true, + }, + }, + }), { status: 200, headers: { 'content-type': 'application/json' } })); + + const imported = await importCurrentUserModelConfig(context, 'access-token'); + + expect(imported.account.metadata?.worksSquareModelCapabilities).toEqual({ + 'deepseek-v4-pro': { + reasoningEfforts: ['low', 'high', 'max'], + reasoningCanDisable: true, + }, + }); + expect(providerServiceMock.updateAccount).toHaveBeenCalledWith( + 'niancode-user-models', + expect.objectContaining({ + metadata: expect.objectContaining({ + worksSquareModelCapabilities: { + 'deepseek-v4-pro': { + reasoningEfforts: ['low', 'high', 'max'], + reasoningCanDisable: true, + }, + }, + }), + }), + 'gateway-key', + ); + expect(markProviderStale).toHaveBeenCalledTimes(1); + }); + + it('clears a previous server capability override when the optional field is absent', async () => { + const existing = account({ + id: 'niancode-user-models', + vendorId: 'custom', + model: 'deepseek-v4-pro', + metadata: { + customModels: ['deepseek-v4-pro'], + worksSquareModelCapabilities: { + 'deepseek-v4-pro': { + reasoningEfforts: ['max'], + reasoningCanDisable: true, + }, + }, + }, + }); + providerServiceMock.getAccount.mockResolvedValue(existing); + providerServiceMock.updateAccount.mockImplementation(async (_accountId: string, patch: Partial) => ({ + ...existing, + ...patch, + })); + proxyAwareFetchMock.mockResolvedValue(new Response(JSON.stringify({ + base_url: 'https://gateway.test/v1', + api_key: 'gateway-key', + models: ['deepseek/deepseek-v4-pro'], + }), { status: 200, headers: { 'content-type': 'application/json' } })); + + const imported = await importCurrentUserModelConfig(context, 'access-token'); + + expect(imported.account.metadata).not.toHaveProperty('worksSquareModelCapabilities'); + expect(providerServiceMock.updateAccount).toHaveBeenCalledWith( + 'niancode-user-models', + expect.objectContaining({ + metadata: expect.not.objectContaining({ worksSquareModelCapabilities: expect.anything() }), + }), + 'gateway-key', + ); + }); + it('marks Pi provider input stale after an account credential update', async () => { const result = createResponse(); const handled = await handleProviderRoutes( From a30053c30e48a0da8639f13e117168f5afd7532e Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 14:16:54 +0800 Subject: [PATCH 23/47] docs: record reasoning capability diagnosis --- ...soning-effort-client-diagnosis-b6c4e1a2.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .project-docs/30-worklog/tasks/20260901-reasoning-effort-client-diagnosis-b6c4e1a2.md diff --git a/.project-docs/30-worklog/tasks/20260901-reasoning-effort-client-diagnosis-b6c4e1a2.md b/.project-docs/30-worklog/tasks/20260901-reasoning-effort-client-diagnosis-b6c4e1a2.md new file mode 100644 index 0000000..36a18b6 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-reasoning-effort-client-diagnosis-b6c4e1a2.md @@ -0,0 +1,90 @@ +# Task: Diagnose reasoning-effort client capability + +## Identity + +- Task ID: 20260901-reasoning-effort-client-diagnosis-b6c4e1a2 +- Mode: Feature +- Branch: main +- Worktree: D:\Datas\OthersProjects\makelore +- Base commit: 850947c092892cb647c4191b6d8bbf37a763e1ad +- Owner: codex +- Status: Ready for integration + +## Scope + +- Diagnose why the current AI Programming model menu exposes only one reasoning + strength for a model that is expected to support multiple strengths. +- Trace the committed Makelore chain from model/provider capability metadata through + Conversation state, Pi runtime mapping, Snapshot projection, and Renderer options. +- Build and run a deterministic focused reproduction for the exact menu symptom. +- Report the verified boundary and the smallest coordinated repair scope; do not + modify product behavior unless the user separately asks for a fix. + +## Intent And Constraints + +- Treat the current Pi 0.84.2 hard-cutover architecture and Main-owned Provider + boundary as authoritative; do not restore OpenCode or add a compatibility layer. +- Distinguish committed source behavior, deployed/model-provider behavior, and + unverified assumptions. The screenshot proves the visible symptom only. +- Keep credentials, real user data, and upstream payloads out of diagnostics. +- Preserve concurrent task ownership. The potentially related historical model-parity + peer has no defined scope, so this task remains read-only outside its own record. +- The one-api repository is separately gated; do not inspect its occupied worktree or + initialize missing project-doc assets without authorization. + +## Outcome + +- Confirmed the direct cause in `shared/imported-model-profile.ts`: the + `deepseek-v4-pro` profile marks `off`, `minimal`, `low`, and `medium` as + unsupported (`null`) and maps only product `high` to provider `high`. +- Confirmed Pi 0.84.2 filters every `null` level and exposes `xhigh`/`max` only when + those keys exist. The committed profile therefore deterministically projects the + single level `['high']`. +- Confirmed the Main/runtime and Renderer chain is behaving as designed: provider + config copies the profile map, the Pi session reports its supported levels, and + the composer renders only levels in that runtime list. The Renderer is not + dropping additional options. +- Confirmed the local AI proxy forwards the original JSON request body to one-api; + it does not remove `reasoning_effort`. +- A one-variable diagnostic map exposed `off`, `low`, `medium`, and `high`, and the + real Pi serializer emitted provider values `low`, `high`, and `max` for product + `low`, `medium`, and `high` respectively. This proves the repair seam without + changing production behavior. +- The product contract currently has only `off|minimal|low|medium|high`; literal + `xhigh`/`max` menu labels require a separate contract/UI expansion. Mapping the + existing product `high` to provider `max` avoids that expansion if those product + semantics are accepted. +- No product code or canonical project documentation was changed. Temporary + diagnostic tests were removed after use. + +## Verification + +- Deterministic red reproduction, run three times: + `pnpm exec vitest run tests/unit/diagnostic-deepseek-v4-reasoning.test.ts --maxWorkers=1` + -> each run failed with expected `['off','low','medium','high']` versus actual + `['high']`. +- One-variable causal test using only a replacement level map -> passed and returned + `['off','low','medium','high']`. +- Real Pi payload serialization diagnostic -> `3 passed`; product + `low -> low`, `medium -> high`, and `high -> max`, with thinking enabled. +- Focused proxy diagnostic with `reasoning_effort: 'max'` -> passed; the temporary + assertion was reverted. +- Final existing-suite check: + `pnpm exec vitest run tests/unit/imported-model-profile.test.ts tests/unit/pi-provider-config.test.ts tests/unit/coding-feature-ui.test.tsx tests/unit/ai-proxy-routes.test.ts --maxWorkers=1` + -> `4` files and `50` tests passed. + +## Follow-ups + +- Update the `deepseek-v4-pro` profile and its focused tests after confirming the + desired product labels. The smallest compatible map is expected to expose product + low/medium/high while translating to provider low/high/max. +- If the UI must display literal `max` (or `xhigh`) rather than translating the + existing `high`, extend `ThinkingLevel`, IPC/runtime contracts, labels, and tests + as one coordinated change. +- A server-owned capability descriptor can reduce future profile staleness, but is + a larger ownership change and is not needed for the immediate fix. + +## Promotion Candidates + +- None. Promote a durable capability-ownership rule only with an implemented repair + or accepted server/client contract design. From 738eda14300b18beff826d87e0e043317ec5bb7d Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 14:34:56 +0800 Subject: [PATCH 24/47] docs: record model capability integration --- .../20-architecture/system-overview.md | 7 +- .project-docs/30-worklog/current-state.md | 17 +++- ...1-integrate-model-capabilities-9b3e7c1a.md | 84 +++++++++++++++++++ ...0901-server-model-capabilities-9e31b6c4.md | 84 ------------------- 4 files changed, 106 insertions(+), 86 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260901-integrate-model-capabilities-9b3e7c1a.md delete mode 100644 .project-docs/30-worklog/tasks/20260901-server-model-capabilities-9e31b6c4.md diff --git a/.project-docs/20-architecture/system-overview.md b/.project-docs/20-architecture/system-overview.md index be429f7..3afdca5 100644 --- a/.project-docs/20-architecture/system-overview.md +++ b/.project-docs/20-architecture/system-overview.md @@ -21,7 +21,7 @@ Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展 | Makelore Code Workspace | project/Agent/Conversation schema v2、产品中立 Snapshot/Patch、Composer 与时间线 | Conversation 本地创建且不等待 worker;旧 OpenCode 会话备份后不再继续,Renderer 不导入 Pi RPC/event 类型 | | Coding Host API & Composition | 唯一 `/api/coding/*` composition、202 acceptance/dedupe、SSE、附件/文件/交互/诊断 | Electron Main 拥有 project/Conversation 服务、选中目标、认证和错误脱敏;SSE 公开面只有 Snapshot 与 `patch-batch` | | Pi Conversation Runtime | 一个长驻 Pi `0.84.2` Agent Server 承载每条 active/warm Conversation 的隔离逻辑 Runtime/Session/JSONL channel | 严格 LF JSONL RPC、generation recovery、Snapshot hydration;top-level 逻辑 turn 并发 4、warm idle LRU 8;Server 退出统一使旧 channel 失效并按需单实例重启 | -| Pi Provider & Managed Resources | Provider catalog、thread-local secret projection、model/resource revision、Prompt/Skill/extension materialization | 父凭据只进入选中逻辑线程的内存 credential store,child 凭据只进入该短命进程;不扫描项目或用户的 `.pi/.agents/.codex`,不把 secret 放进 argv、catalog 或 Renderer | +| Pi Provider & Managed Resources | Provider catalog、thread-local secret projection、model/resource revision、Prompt/Skill/extension materialization | 父凭据只进入选中逻辑线程的内存 credential store,child 凭据只进入该短命进程;Works `model_capabilities` 由 Main 严格归一化并作为安全 Provider metadata 持久化,服务端 reasoning levels 优先于本地 profile,缺字段则清理 override 并回退;不扫描项目或用户的 `.pi/.agents/.codex`,不把 secret 或原始响应放进 argv、catalog 或 Renderer | | Pi Extension, Subagents & Lifecycle | 唯一显式 Makelore extension、UI interaction、ephemeral child、write lease 与 background run lease | child 并发 4、单次最多 8、禁止递归;active/uncertain run 不因页面隐藏或 confirmation timeout 被停止,replacement/stop 必须可解释并清理所有 ownership | | Native Web Search Plugin | Signed Marketplace Package → effective parent snapshot → code-owned Main adapter → fixed Works Square Web Search route | Renderer/Package/Pi 不持有 Provider key、model 或 URL;每次搜索要求显式确认,closed receipt 与 result-less uncertain state 由 Main 投影,child 不继承 hosted tool | | AI Design Workspace & Living Form | 一个 Workspace 的当前 Direction、Current Specification、持久 Agent Session、conversation timeline、Tasks 与 Assets | Living Form 是服务端 Current Specification 的投影;Renderer 只持有草稿和已接受投影 | @@ -57,6 +57,11 @@ Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展 - Renderer 只消费共享的产品中立 project/Conversation/Snapshot/Patch DTO,并经 `src/lib/host-api.ts` 或 `src/lib/api-client.ts` 访问 Main。Pi RPC、event、Provider credential 和本地 runtime 路径保持 Main-private。 - accepted/uncertain Coding mutation 不得自动重放;RPC confirmation timeout 后仍保留 target run permit、process ownership 和 background lease,直到迟到 success/failure/exit/abort 权威收敛。其他 Conversation 必须继续可用。 - selected Provider credential 只投影到目标父逻辑线程的内存 credential store 或目标 child 进程;跨账号模型变化必须重建目标逻辑线程。确定性 Works user-context 缺失是 Provider-auth failure:失效缓存 credential、fail fast、固定脱敏提示,不得归类为 Pi crash。 +- Works 下发的 per-model reasoning capability 是可选 Main-owned metadata,不是 + Renderer 或 one-api 的权威。存在时只接受受支持的安全形状并覆盖目标模型的 + 本地 effort map;缺失时移除旧 override 并使用已验证本地 profile。`off` 在 Pi + wire 上表示 `thinking.type=disabled` 且不发送 `reasoning_effort`,启用档位保持 + provider 原生值,当前 DeepSeek 产品投影为 `off`/`low`/`high`/`max`。 - Guided Hotspot Binding is implemented behind a Main-owned capability that is true by default; exact `NIANCODE_AI_HARDWARE_GUIDED_HOTSPOT_BINDING=0` disables it, while capability-read failure falls back to direct six-digit Binding. The guided state is process-local, opener failures expose only the same fixed address for manual copy, and Binding conflicts refresh the safe account overview. - Robot hotspot scanning and connection are local Main operations that return before Works credentials/upstream access. Renderer may submit only an opaque candidate ID from the latest bounded scan; Main alone filters open printable `Xiaozhi-*` SSIDs, performs platform association, and verifies the exact current SSID. - Hotspot discovery and connection do not authenticate a Robot. BSSID, interface/profile details, native diagnostics, location data, and Wi-Fi credentials never cross the Main boundary; permission or platform failure keeps the system-settings/manual path available. diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index ffaa20c..b14ecc0 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -4,6 +4,21 @@ This file is the integrated default-branch snapshot. Feature tasks record progre ## Integrated Through +- Works-provisioned model-reasoning capability source + `ae7936174208a1d13cdfd260d5c6f2b70b450b60` from task + `20260901-server-model-capabilities-9e31b6c4` is merged into local `main` as + `6a8ebe1b671ca24150c2226b9111b9d07174e37b` by task + `20260901-integrate-model-capabilities-9b3e7c1a`, paired with Works Square source + `9e1b6886b360175f1ca1596fb07f71e3bf86c894`. Electron Main now strictly + normalizes and persists the optional safe `model_capabilities` metadata, removes a + stale override when the field is absent, and includes it in Provider runtime-shape + invalidation. Server levels override the verified local reasoning map; old servers + and direct Providers retain the local fallback. DeepSeek exposes + `off`/`low`/`high`/`max`: Pi disables thinking without `reasoning_effort` for + `off`, and sends the exact enabled effort otherwise. Native `max` reaches project + persistence, Snapshot/Patch, Host API, runtime, and the composer label `最高`. + Pi remains `0.84.2`; no one-api, dependency, package, deployment, or real Provider + acceptance is claimed. - Marketplace weak-ETag interoperability fix source `b3cfe7e1ceb7da65ccc99214db09102b4fc1cace` from task `20260901-plugin-catalog-load-client-7d4a8c21` is promoted to local `main` by @@ -359,4 +374,4 @@ Robot 绑定设备默认先显示“引导配网 / 已有激活码”路径选 ## Last Updated -2026-08-31 +2026-09-01 diff --git a/.project-docs/30-worklog/tasks/20260901-integrate-model-capabilities-9b3e7c1a.md b/.project-docs/30-worklog/tasks/20260901-integrate-model-capabilities-9b3e7c1a.md new file mode 100644 index 0000000..a5e8ca6 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-integrate-model-capabilities-9b3e7c1a.md @@ -0,0 +1,84 @@ +# Task: Integrate model reasoning capabilities + +## Identity + +- Task ID: 20260901-integrate-model-capabilities-9b3e7c1a +- Mode: Integration +- Branch: main +- Worktree: D:\Datas\OthersProjects\makelore +- Base commit: a30053c30e48a0da8639f13e117168f5afd7532e +- Owner: codex +- Status: Completed + +## Scope + +- Merge exact source commit `ae7936174208a1d13cdfd260d5c6f2b70b450b60` + into local `main` without changing the reviewed product behavior. +- Promote the paired Works-to-Main capability normalization and Pi precedence + contract into canonical architecture/current state memory. +- Do not package, deploy, push, change Pi/one-api dependencies, or add a second + runtime or compatibility layer. + +## Intent And Constraints + +- Electron Main remains the sole owner of Works model config, Provider metadata, + credentials, and Pi wire values; Renderer receives only product-neutral levels. +- Server capability metadata overrides the verified local reasoning map when present; + an older server falls back locally, and persisted user choice remains intact. +- Integrate only the reviewed source commit. Any merge conflict or semantic conflict + with current `main` stops integration for explicit reconciliation. +- Pair the client merge with Works Square source + `9e1b6886b360175f1ca1596fb07f71e3bf86c894`; no one-api change is required. + +## Plan + +1. Verify source identity, worktree cleanliness, and a conflict-free merge tree. +2. Merge the exact source branch into `main` with a merge commit. +3. Promote the Main-owned capability normalization/precedence boundary into canonical + project memory. +4. Run focused serializer/UI/contract tests, typecheck, lint, Vite build, diff checks, + and documentation drift checks. +5. Commit the integration record, complete the task, and release ownership only from + a clean `main` worktree. + +## Outcome + +- Merged exact reviewed source `ae7936174208a1d13cdfd260d5c6f2b70b450b60` + into local `main` as merge commit + `6a8ebe1b671ca24150c2226b9111b9d07174e37b`; the merge was conflict-free. +- Promoted the Main-owned Works capability normalization/persistence/precedence and + Pi wire boundary into canonical current-state and architecture memory. +- Confirmed paired Works Square source + `9e1b6886b360175f1ca1596fb07f71e3bf86c894` is merged into local server `master` as + `ef73b9c97997942996ddd3ed3cd3bf4e29120bd0`. +- Kept the reviewed scope: Pi remains `0.84.2`; no one-api, dependency, package, + deployment, push, second runtime, or compatibility layer was added. + +## Verification + +- `git merge-tree --write-tree` returned conflict-free tree + `94c573f77dd11931750a255d537c800c6f5d09c9` before merge. +- Focused merged-tree Vitest run: 8 files / 111 tests passed, covering Works + capability import and cleanup, model/profile precedence, project/Snapshot/route/UI + `max` propagation, Pi runtime behavior, and actual off/low/high/max serializer output. +- `pnpm run typecheck`: passed. +- `pnpm run lint:check`: passed with zero errors and the repository's existing five + warnings. +- `pnpm run build:vite`: passed for Renderer, Electron Main, Preload, and utility + output; existing Browserslist, dynamic-import, and chunk-size warnings remain. +- `git diff --check`: passed. +- Paired server merged-tree verification passed 11 focused tests and focused Ruff. + +## Follow-ups + +- Release the paired Works Square API and Makelore client when release owners are + ready. Deployment order remains tolerant: an older server uses the local DeepSeek + profile and an older client ignores the optional server field. Real Provider and + final installed-package acceptance remain separate release evidence. + +## Promotion Candidates + +- Resolved: promoted the safe Works-to-Provider metadata contract, missing-field + cleanup/fallback, server-over-local precedence, and Pi `off`/enabled wire behavior + into `.project-docs/20-architecture/system-overview.md`; recorded the integration + in `.project-docs/30-worklog/current-state.md`. diff --git a/.project-docs/30-worklog/tasks/20260901-server-model-capabilities-9e31b6c4.md b/.project-docs/30-worklog/tasks/20260901-server-model-capabilities-9e31b6c4.md deleted file mode 100644 index 1e0162b..0000000 --- a/.project-docs/30-worklog/tasks/20260901-server-model-capabilities-9e31b6c4.md +++ /dev/null @@ -1,84 +0,0 @@ -# Task: Consume server model reasoning capabilities - -## Identity - -- Task ID: 20260901-server-model-capabilities-9e31b6c4 -- Mode: Feature -- Branch: codex/20260901-server-model-capabilities-9e31b6c4-server-model-capabilities -- Worktree: D:\w\makelore-model-capabilities-9e31b6c4 -- Base commit: 850947c092892cb647c4191b6d8bbf37a763e1ad -- Owner: codex -- Status: Ready for integration - -## Scope - -- Consume per-model reasoning capabilities from Works Square model config and persist - them on the imported Main-owned Provider account. -- Make server metadata override the local Pi reasoning map for server-provisioned - models while retaining the local verified profile as an old-server/direct-provider - fallback. -- Add native `max` to the product-neutral thinking-level contract and composer menu so - DeepSeek exposes off/low/high/max without relabeling provider values. -- Keep one-api and Pi `0.84.2` unchanged. - -## Intent And Constraints - -- Electron Main remains the sole Works/model-config, Provider, credential, and Pi-wire - owner. Renderer receives only safe product-neutral levels. -- Strictly normalize the trusted response shape; a missing capability field means an - older server and falls back to the local profile. -- Preserve persisted user choices and all non-Works Provider behavior. -- Do not restore OpenCode, add a second runtime, or expose Provider credentials or raw - response data. -- Feature mode may update only product code, focused tests, and this task record. - -## Plan - -1. Add failing tests for Works capability import, Provider metadata persistence, - server-over-local Pi mapping, and native `max` menu/contract acceptance. -2. Implement the typed response parser, Provider metadata projection, capability-map - precedence, local DeepSeek fallback, and minimal `max` propagation through existing - product validators/UI. -3. Run focused unit tests, typecheck, scoped lint, Vite/Main build, and the project-docs - completion gate. - -## Outcome - -- Parsed and normalized Works Square `model_capabilities`, stored the safe result on - the managed Provider account, removed stale overrides when the optional server field - disappears, and included the metadata in Provider runtime-shape invalidation. -- Server-provided efforts now override the local Pi reasoning/thinking-level map while - preserving verified local modalities, token limits, and wire-format compatibility. - A capability for a future model without a local profile enables standard - `reasoning_effort` serialization automatically. -- Updated the verified DeepSeek fallback to expose only off/low/high/max. Pi sends - `thinking.type=disabled` with no `reasoning_effort` for off, and sends the exact - low/high/max effort for enabled requests. -- Added native `max` to the product contract, snapshot validation, project persistence, - Host route, Pi runtime, and composer UI where it is labelled `最高`. -- Kept Pi at 0.84.2 and made no one-api, dependency, README, or second-runtime change. - -## Verification - -- Focused final Vitest combination: 8 files and 102/102 tests passed, including actual - Pi `streamSimple` payload checks for off and each of low/high/max. -- `pnpm run typecheck`: passed. -- `pnpm run lint:check`: passed with the repository's existing five warnings and no - errors; a separate scoped ESLint run over every changed TypeScript/TSX file passed. -- `pnpm run build:vite`: passed; only existing Browserslist, dynamic-import, and chunk - size warnings were emitted. -- `git diff --check`: passed. - -## Follow-ups - -- Integrate with the paired Works Square API change. Deployment order is tolerant: - a client talking to an older server uses the corrected local DeepSeek profile, and - older clients ignore the new server response member. -- Verify future server model entries against their provider's exact effort vocabulary - before publishing them; no one-api change is required while it remains transparent. - -## Promotion Candidates - -- Promote the Main-owned capability normalization/precedence rule and the safe - server-to-Provider metadata contract into canonical architecture documentation after - the paired server and client branches are integrated. From 12d7588b3ebd4d192c2e14ae285d4f6ddebeeb42 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 14:36:18 +0800 Subject: [PATCH 25/47] fix(coding): serialize agent server sleep restart --- .../20260901-agent-received-stall-8b6d4c21.md | 53 ++++++++++++++++ electron/api/coding-composition.ts | 1 + .../coding-runtime/pi/agent-server-process.ts | 1 + ...oding-composition-background-sleep.test.ts | 62 +++++++++++++++++++ .../unit/pi-agent-server-process-real.test.ts | 24 +++++++ 5 files changed, 141 insertions(+) create mode 100644 .project-docs/30-worklog/tasks/20260901-agent-received-stall-8b6d4c21.md create mode 100644 tests/unit/coding-composition-background-sleep.test.ts diff --git a/.project-docs/30-worklog/tasks/20260901-agent-received-stall-8b6d4c21.md b/.project-docs/30-worklog/tasks/20260901-agent-received-stall-8b6d4c21.md new file mode 100644 index 0000000..eae4691 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-agent-received-stall-8b6d4c21.md @@ -0,0 +1,53 @@ +# Task: Diagnose local Agent received-message stall + +## Identity + +- Task ID: 20260901-agent-received-stall-8b6d4c21 +- Mode: Feature +- Branch: codex/20260901-agent-received-stall-8b6d4c21-agent-received-stall +- Worktree: D:\Datas\OthersProjects\makelore-worktrees\agent-received-stall-8b6d4c21 +- Base commit: 850947c092892cb647c4191b6d8bbf37a763e1ad +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Diagnose the installed-app state where a prompt is acknowledged locally but remains optimistic indefinitely. +- Fix the background-sleep versus Agent Server start race without replaying accepted prompts. +- Add focused regression coverage for both lifecycle race windows. + +## Intent And Constraints + +- Preserve the single shared Pi Agent Server and per-conversation logical thread architecture. +- Background sleep must not stop work that begins while idle worker cleanup is in flight. +- A start racing an already-started stop must wait for the stop and then create a fresh server process. +- Do not auto-replay an accepted or uncertain prompt. + +## Outcome + +- Confirmed installed-state evidence: the accepted UI state was newer than the latest Pi session write, and no Agent Server process remained live, so the prompt had not reached Provider generation. +- Confirmed regression: `PiAgentServerProcess.start()` returned immediately when the old child still existed during an in-flight stop; after that stop completed, no server remained. The real-process race test failed before the fix and passed afterward. +- Confirmed regression: composition checked for active work only before asynchronous background cleanup, then stopped the shared Agent Server even when a new run began during cleanup. The composition race test failed before the fix and passed afterward. +- Fixed both lifecycle windows: background sleep rechecks active work after worker cleanup, and a start racing a stop waits for that stop before creating a fresh server. +- The installed-app symptom maps directly to these two confirmed races, although the exact renderer/Main interleaving of the reported occurrence was not captured live. +- Accepted and uncertain prompts are still never auto-replayed. + +## Verification + +- Installed-state red loop: accepted screenshot timestamp is newer than the latest Pi session write while no Agent Server process is live. +- Red phase: the two focused race tests both failed against the original implementation. +- `pnpm exec vitest run tests/unit/pi-agent-server-process-real.test.ts tests/unit/coding-composition-background-sleep.test.ts --maxWorkers=1` — 2 files, 4 tests passed. +- `pnpm exec vitest run tests/unit/background-lifecycle.test.ts tests/unit/pi-background-lifecycle.test.ts tests/unit/pi-agent-server-process-real.test.ts tests/unit/coding-composition-background-sleep.test.ts --maxWorkers=1` — 4 files, 9 tests passed. +- `pnpm run typecheck` — passed. +- `pnpm run lint:check` — passed with 5 pre-existing warnings and no errors. +- `pnpm test` — 216 files passed; 1,764 tests passed and 2 skipped. +- `pnpm run build:vite` — Renderer, Main, Preload, and utility production builds passed. +- Existing Electron E2E fixtures mock prompt acceptance and do not exercise the real Main-owned Agent Server lifecycle; the real-process regression test is the relevant product-path coverage. + +## Follow-ups + +- Integrate the feature commit into `main`, then produce/reinstall a Windows package before validating the original installed-app reproduction; the currently installed binary does not contain this source fix. + +## Promotion Candidates + +- None. The change enforces existing background-lease and single-Agent-Server architecture rather than changing canonical product behavior. diff --git a/electron/api/coding-composition.ts b/electron/api/coding-composition.ts index 1210947..8d55ac3 100644 --- a/electron/api/coding-composition.ts +++ b/electron/api/coding-composition.ts @@ -498,6 +498,7 @@ export function createCodingComposition( await Promise.allSettled(conversationIds.map((conversationId) => ( runtime.dispose(conversationId, reason) ))); + if (reason === 'background_sleep' && runtime.hasActiveWork()) return; await agentServer.stop(); }, async shutdown() { diff --git a/electron/coding-runtime/pi/agent-server-process.ts b/electron/coding-runtime/pi/agent-server-process.ts index 15c6075..41ed00b 100644 --- a/electron/coding-runtime/pi/agent-server-process.ts +++ b/electron/coding-runtime/pi/agent-server-process.ts @@ -490,6 +490,7 @@ export class PiAgentServerProcess { } start(): Promise { + if (this.stopFlight) return this.stopFlight.then(() => this.start()); if (this.startFlight) return this.startFlight; if (this.child && !this.failure) return Promise.resolve(); this.startFlight = this.startServer().finally(() => { diff --git a/tests/unit/coding-composition-background-sleep.test.ts b/tests/unit/coding-composition-background-sleep.test.ts new file mode 100644 index 0000000..d969cd3 --- /dev/null +++ b/tests/unit/coding-composition-background-sleep.test.ts @@ -0,0 +1,62 @@ +// @vitest-environment node + +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { AgentBrowserModule } from '../../electron/agent-browser'; +import { createCodingComposition } from '../../electron/api/coding-composition'; +import { PiAgentServerProcess } from '../../electron/coding-runtime/pi/agent-server-process'; +import { createMemoryCodingProjectStorage } from '../../electron/coding-projects/project-store'; + +const roots: string[] = []; + +afterEach(async () => { + vi.restoreAllMocks(); + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); +}); + +describe('coding composition background sleep', () => { + it('does not stop the shared Agent Server when work starts during worker cleanup', async () => { + const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-sleep-race-project-')); + const userDataDir = await mkdtemp(path.join(tmpdir(), 'makelore-sleep-race-user-')); + roots.push(projectPath, userDataDir); + const composition = createCodingComposition({ + storage: createMemoryCodingProjectStorage(), + browser: { close: vi.fn(async () => undefined) } as unknown as AgentBrowserModule, + paths: { + executablePath: process.execPath, + cliPath: path.join(projectPath, 'unused-cli.js'), + serverPath: path.join(projectPath, 'unused-server.mjs'), + userDataDir, + bundledSkillsDir: path.resolve('resources/coding-skills'), + }, + }); + const stopAgentServer = vi.spyOn(PiAgentServerProcess.prototype, 'stop') + .mockResolvedValue(undefined); + let activeWork = false; + const hasActiveWork = vi.spyOn(composition.runtime, 'hasActiveWork') + .mockImplementation(() => activeWork); + const getDiagnostics = vi.spyOn(composition.runtime, 'getDiagnostics') + .mockReturnValue({ + workers: [{ conversationId: 'conversation-a' }], + } as ReturnType); + const dispose = vi.spyOn(composition.runtime, 'dispose').mockImplementation(async () => { + activeWork = true; + }); + + try { + await composition.sleep('background_sleep'); + + expect(dispose).toHaveBeenCalledWith('conversation-a', 'background_sleep'); + expect(hasActiveWork).toHaveBeenCalledTimes(2); + expect(stopAgentServer).not.toHaveBeenCalled(); + } finally { + dispose.mockRestore(); + getDiagnostics.mockRestore(); + hasActiveWork.mockRestore(); + stopAgentServer.mockRestore(); + await composition.shutdown(); + } + }); +}); diff --git a/tests/unit/pi-agent-server-process-real.test.ts b/tests/unit/pi-agent-server-process-real.test.ts index f835674..17a95f6 100644 --- a/tests/unit/pi-agent-server-process-real.test.ts +++ b/tests/unit/pi-agent-server-process-real.test.ts @@ -133,6 +133,30 @@ describe('Pi Agent Server real process', () => { } }, 10_000); + it('restarts when start races with a graceful background stop', async () => { + const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-restart-')); + roots.push(root); + const layout = await materializePackagedAgentServerLayout(root); + const server = new PiAgentServerProcess({ + executablePath: electronExecutable, + ...layout, + }); + + try { + await server.start(); + const firstProcessId = server.processId; + expect(firstProcessId).toBeTypeOf('number'); + + const stopping = server.stop(); + const restarting = server.start(); + await Promise.all([stopping, restarting]); + expect(server.processId).toBeTypeOf('number'); + expect(server.processId).not.toBe(firstProcessId); + } finally { + await server.stop(); + } + }, 20_000); + it('hosts isolated Conversation threads in one long-lived process', async () => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-')); roots.push(root); From 143aaec3d6770dd263bf1e418e89bf722989caef Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 14:42:39 +0800 Subject: [PATCH 26/47] docs: record agent server lifecycle integration --- .project-docs/30-worklog/current-state.md | 13 +++++ .../20260901-agent-received-stall-8b6d4c21.md | 53 ----------------- ...20260901-integrate-agent-stall-4f2c8a91.md | 58 +++++++++++++++++++ 3 files changed, 71 insertions(+), 53 deletions(-) delete mode 100644 .project-docs/30-worklog/tasks/20260901-agent-received-stall-8b6d4c21.md create mode 100644 .project-docs/30-worklog/tasks/20260901-integrate-agent-stall-4f2c8a91.md diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index b14ecc0..02e9aef 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -4,6 +4,19 @@ This file is the integrated default-branch snapshot. Feature tasks record progre ## Integrated Through +- Agent Server background-sleep race fix source + `12d7588b3ebd4d192c2e14ae285d4f6ddebeeb42` from task + `20260901-agent-received-stall-8b6d4c21` is merged into local `main` as + `c2137c9f3f2e05441064cc90bfeed115363ac04e` by task + `20260901-integrate-agent-stall-4f2c8a91`. Background sleep now rechecks + active Coding work after asynchronous worker cleanup before stopping the shared + Agent Server, and a server start racing an in-flight stop waits for that stop and + creates one fresh process. This closes the confirmed lifecycle windows that can + leave a locally accepted prompt optimistic with no live Agent Server or Pi session + write; accepted/uncertain prompts are still never auto-replayed. Source tests and + a real Electron-Node Agent Server race test pass. The currently installed binary + predates this integration; a rebuilt/reinstalled Windows package and repetition of + the reported interaction remain pending release evidence. - Works-provisioned model-reasoning capability source `ae7936174208a1d13cdfd260d5c6f2b70b450b60` from task `20260901-server-model-capabilities-9e31b6c4` is merged into local `main` as diff --git a/.project-docs/30-worklog/tasks/20260901-agent-received-stall-8b6d4c21.md b/.project-docs/30-worklog/tasks/20260901-agent-received-stall-8b6d4c21.md deleted file mode 100644 index eae4691..0000000 --- a/.project-docs/30-worklog/tasks/20260901-agent-received-stall-8b6d4c21.md +++ /dev/null @@ -1,53 +0,0 @@ -# Task: Diagnose local Agent received-message stall - -## Identity - -- Task ID: 20260901-agent-received-stall-8b6d4c21 -- Mode: Feature -- Branch: codex/20260901-agent-received-stall-8b6d4c21-agent-received-stall -- Worktree: D:\Datas\OthersProjects\makelore-worktrees\agent-received-stall-8b6d4c21 -- Base commit: 850947c092892cb647c4191b6d8bbf37a763e1ad -- Owner: codex -- Status: Ready for Integration - -## Scope - -- Diagnose the installed-app state where a prompt is acknowledged locally but remains optimistic indefinitely. -- Fix the background-sleep versus Agent Server start race without replaying accepted prompts. -- Add focused regression coverage for both lifecycle race windows. - -## Intent And Constraints - -- Preserve the single shared Pi Agent Server and per-conversation logical thread architecture. -- Background sleep must not stop work that begins while idle worker cleanup is in flight. -- A start racing an already-started stop must wait for the stop and then create a fresh server process. -- Do not auto-replay an accepted or uncertain prompt. - -## Outcome - -- Confirmed installed-state evidence: the accepted UI state was newer than the latest Pi session write, and no Agent Server process remained live, so the prompt had not reached Provider generation. -- Confirmed regression: `PiAgentServerProcess.start()` returned immediately when the old child still existed during an in-flight stop; after that stop completed, no server remained. The real-process race test failed before the fix and passed afterward. -- Confirmed regression: composition checked for active work only before asynchronous background cleanup, then stopped the shared Agent Server even when a new run began during cleanup. The composition race test failed before the fix and passed afterward. -- Fixed both lifecycle windows: background sleep rechecks active work after worker cleanup, and a start racing a stop waits for that stop before creating a fresh server. -- The installed-app symptom maps directly to these two confirmed races, although the exact renderer/Main interleaving of the reported occurrence was not captured live. -- Accepted and uncertain prompts are still never auto-replayed. - -## Verification - -- Installed-state red loop: accepted screenshot timestamp is newer than the latest Pi session write while no Agent Server process is live. -- Red phase: the two focused race tests both failed against the original implementation. -- `pnpm exec vitest run tests/unit/pi-agent-server-process-real.test.ts tests/unit/coding-composition-background-sleep.test.ts --maxWorkers=1` — 2 files, 4 tests passed. -- `pnpm exec vitest run tests/unit/background-lifecycle.test.ts tests/unit/pi-background-lifecycle.test.ts tests/unit/pi-agent-server-process-real.test.ts tests/unit/coding-composition-background-sleep.test.ts --maxWorkers=1` — 4 files, 9 tests passed. -- `pnpm run typecheck` — passed. -- `pnpm run lint:check` — passed with 5 pre-existing warnings and no errors. -- `pnpm test` — 216 files passed; 1,764 tests passed and 2 skipped. -- `pnpm run build:vite` — Renderer, Main, Preload, and utility production builds passed. -- Existing Electron E2E fixtures mock prompt acceptance and do not exercise the real Main-owned Agent Server lifecycle; the real-process regression test is the relevant product-path coverage. - -## Follow-ups - -- Integrate the feature commit into `main`, then produce/reinstall a Windows package before validating the original installed-app reproduction; the currently installed binary does not contain this source fix. - -## Promotion Candidates - -- None. The change enforces existing background-lease and single-Agent-Server architecture rather than changing canonical product behavior. diff --git a/.project-docs/30-worklog/tasks/20260901-integrate-agent-stall-4f2c8a91.md b/.project-docs/30-worklog/tasks/20260901-integrate-agent-stall-4f2c8a91.md new file mode 100644 index 0000000..b4c2018 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-integrate-agent-stall-4f2c8a91.md @@ -0,0 +1,58 @@ +# Task: Integrate Agent Server sleep restart fix + +## Identity + +- Task ID: 20260901-integrate-agent-stall-4f2c8a91 +- Mode: Integration +- Branch: main +- Worktree: D:\Datas\OthersProjects\makelore +- Base commit: 738eda14300b18beff826d87e0e043317ec5bb7d +- Owner: codex +- Status: Completed + +## Scope + +- Merge exact feature commit `12d7588c` into local `main`. +- Preserve the already-integrated model-reasoning capability behavior. +- Record the integration in canonical current state without changing Pi, Provider, packaging, or replay contracts. + +## Intent And Constraints + +- Pi `0.84.2` remains the sole Coding runtime and one Main-owned Agent Server remains the parent topology. +- Background sleep must not stop work that begins during cleanup, and start racing stop must create one fresh server after stop settles. +- Accepted and uncertain prompts must never be replayed automatically. +- Stop on any merge conflict or semantic conflict; do not choose by merge order. +- Do not package, install, deploy, push, or change dependencies in this integration task. + +## Plan + +1. Verify the exact source commit and a conflict-free merge tree against current `main`. +2. Merge the source branch with an explicit merge commit. +3. Record the integrated behavior and remaining installed-package gate in canonical current state. +4. Run focused lifecycle tests, typecheck, lint, production build, and documentation drift checks. +5. Commit integration memory, complete the task, and release ownership from a clean worktree. + +## Outcome + +- Verified source commit `12d7588b3ebd4d192c2e14ae285d4f6ddebeeb42` and conflict-free merge tree `63750df00cda0da00304725b6f0a8c17dd5e09b0` against current `main`. +- Merged the exact source into local `main` as `c2137c9f3f2e05441064cc90bfeed115363ac04e` without conflict. +- Preserved the already-integrated model-reasoning capability changes and the Pi `0.84.2` single-Agent-Server architecture. +- Recorded the background-sleep recheck and serialized stop/start behavior in canonical current state. +- No package, installation, deployment, push, dependency, Provider, or replay-contract change was performed. + +## Verification + +- `git merge-tree --write-tree HEAD 12d7588` returned conflict-free tree `63750df00cda0da00304725b6f0a8c17dd5e09b0` before merge. +- Merged-main focused lifecycle run: 4 files / 9 tests passed. +- `pnpm run typecheck`: passed. +- `pnpm run lint:check`: zero errors and the repository's existing five warnings. +- `pnpm test`: 216 files passed; 1,780 tests passed and 2 skipped. +- `pnpm run build:vite`: Renderer, Electron Main, Preload, and utility production builds passed with existing build warnings only. + +## Follow-ups + +- Build, verify, and reinstall a Windows package from integrated `main`, then repeat the reported prompt-after-idle interaction. The currently installed application still contains the old lifecycle code. + +## Promotion Candidates + +- Resolved: recorded the exact source and merge commits, lifecycle behavior, no-replay invariant, and remaining installed-package gate in `.project-docs/30-worklog/current-state.md`. No ADR or architecture rewrite was needed because the fix enforces ADR-006. From d2ef37bc4d7d3609cec0a55c4ae8ffb2734696d5 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 1 Sep 2026 15:17:45 +0800 Subject: [PATCH 27/47] fix(coding): reconcile snapshot after abort --- ...60901-conversation-abort-stall-6f4c2a91.md | 54 +++++++++++++++++++ src/pages/Chat/CodingChatPanel.tsx | 23 +++++--- src/pages/Chat/CodingConversationHeader.tsx | 5 +- tests/e2e/pi-coding-first-chat.spec.ts | 53 ++++++++++++++++-- tests/unit/coding-chat-panel.test.tsx | 39 ++++++++++++++ tests/unit/coding-feature-ui.test.tsx | 4 ++ 6 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260901-conversation-abort-stall-6f4c2a91.md diff --git a/.project-docs/30-worklog/tasks/20260901-conversation-abort-stall-6f4c2a91.md b/.project-docs/30-worklog/tasks/20260901-conversation-abort-stall-6f4c2a91.md new file mode 100644 index 0000000..af1b238 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-conversation-abort-stall-6f4c2a91.md @@ -0,0 +1,54 @@ +# Task: Fix running conversation abort stall + +## Identity + +- Task ID: 20260901-conversation-abort-stall-6f4c2a91 +- Mode: Feature +- Branch: codex/20260901-conversation-abort-stall-6f4c2a91-conversation-abort-stall +- Worktree: D:\Datas\OthersProjects\makelore-conversation-abort-stall-6f4c2a91 +- Base commit: 143aaec3d6770dd263bf1e418e89bf722989caef +- Owner: codex-root +- Status: Completed + +## Scope + +- Diagnose the installed 1.2.1 state where a Conversation continued to render as running and neither visible abort entry point unlocked the UI. +- Correlate the screenshot with privacy-safe Main lifecycle and Pi Session metadata, then reproduce the stale-Renderer state at the public Chat seam. +- Make both the Header and Composer abort actions reconcile the authoritative Conversation Snapshot after the abort request completes. +- Add focused Renderer regression coverage and extend the existing Electron E2E fixture for a missed terminal SSE patch. + +## Intent And Constraints + +- Preserve ADR-006: Pi `0.84.2` remains the sole production runtime and Electron Main remains the only Pi/Host API authority. +- Do not replay accepted or uncertain work, change the abort RPC/Host API contract, add polling, or introduce a fallback/compatibility path. +- Treat the visible running state as evidence to investigate, not proof that the bash subprocess or Pi turn is still active. +- Keep the change at the existing Renderer Host API boundary and use the existing target-only Snapshot recovery semantics. +- Work only in the isolated feature worktree; do not modify the occupied `main` worktree or the completed 1.2.1 packaging task. + +## Outcome + +- Confirmed the screenshot was from installed Makelore 1.2.1. The affected Pi JSONL Session recorded three bash tool results and a final assistant `stop` by 14:55:35, while the 14:59 screenshot still rendered the earlier bash call as executing. Main later stopped the already-idle logical thread through background sleep, so the supported incident was stale Renderer state rather than an indefinitely running curl process. +- Reproduced the defect with a focused Chat test: the UI held a running Snapshot, the abort request succeeded against an already-terminal authority, but `getCodingConversationSnapshot` remained at one call and the UI stayed on `中止生成`. +- Added one shared `abortConversation` path in `CodingChatPanel`: after the existing POST abort completes, it silently reloads that Conversation's authoritative Snapshot. Both the Header `中止` button and Composer `中止生成` button now use this path. +- Kept error ownership in the existing controls: Header action failures remain local to Header, while Composer failures remain scoped to the originating draft/Conversation. +- Updated the Electron E2E host fixture so a deliberately missed terminal SSE patch becomes an authoritative aborted Snapshot only after the user clicks abort; the test now proves both abort controls disappear and runtime settings unlock after reconciliation. + +## Verification + +- Red regression before the fix: `tests/unit/coding-chat-panel.test.tsx` failed because the Snapshot API was called once instead of twice after abort. +- `pnpm exec vitest run tests/unit/coding-chat-panel.test.tsx tests/unit/coding-feature-ui.test.tsx tests/unit/coding-conversations-facade.test.ts tests/unit/pi-conversation-runtime.test.ts tests/unit/pi-worker-pool-process-integration.test.ts` passed: 5 files, 35 tests. +- `pnpm run typecheck` passed. +- Scoped ESLint over the two product files and three changed test files passed with no findings. +- `pnpm run build:vite` passed for Renderer, Electron Main, Preload, and release utility bundles; only existing bundle-size/dynamic-import warnings were reported. +- `pnpm exec playwright test tests/e2e/pi-coding-first-chat.spec.ts --grep "PI feature UI"` passed: 1/1 Electron E2E. +- `pnpm test` passed: 215 files / 1780 tests, 2 skipped, plus the isolated pressure test 1/1. +- `pnpm run lint:check` completed with 0 errors and 5 existing warnings in `src/pages/Home/index.tsx` and `src/pages/Makelore/index.tsx`; no warning is in a changed file. +- `git diff --check` passed. + +## Follow-ups + +- A new Windows installer must be built and installed before claiming the user's installed app contains this fix. Do not overwrite or relabel the already-built 1.2.1 artifact with changed source under the same version. + +## Promotion Candidates + +- None. This fix enforces the existing abort/terminal convergence and Snapshot recovery contracts without changing architecture or product direction. diff --git a/src/pages/Chat/CodingChatPanel.tsx b/src/pages/Chat/CodingChatPanel.tsx index 0e46119..1770f85 100644 --- a/src/pages/Chat/CodingChatPanel.tsx +++ b/src/pages/Chat/CodingChatPanel.tsx @@ -170,6 +170,10 @@ export function CodingChatPanel({ ? `new:${activeProject.id}:${selectedAgent.id}` : null; const draftKey = targetConversationId ?? provisionalDraftKey; + const abortConversation = useCallback(async (conversationId: string) => { + await abortCodingConversation(conversationId); + await loadConversationSnapshot(conversationId, 'silent'); + }, [loadConversationSnapshot]); const promptMode = draftKey ? modesByDraftKey[draftKey] ?? 'prompt' : 'prompt'; const provisionalDraft = provisionalDraftKey ? provisionalDrafts[provisionalDraftKey] ?? '' : ''; const submissionError = draftKey ? submissionErrors[draftKey] ?? null : null; @@ -598,6 +602,9 @@ export function CodingChatPanel({ onRename={async (title) => { if (targetConversationId) await patchConversation(targetConversationId, { title }); }} + onAbort={async () => { + if (targetConversationId) await abortConversation(targetConversationId); + }} onRecover={async () => { if (targetConversationId) await recoverConversation(targetConversationId); }} @@ -698,13 +705,15 @@ export function CodingChatPanel({ onSubmit={handleSubmit} onAbort={() => { if (!targetConversationId) return; - void abortCodingConversation(targetConversationId).catch((error) => { - if (!draftKey) return; - setSubmissionErrors((current) => ({ - ...current, - [draftKey]: localSubmissionError(error), - })); - }); + const conversationId = targetConversationId; + void abortConversation(conversationId) + .catch((error) => { + if (!draftKey) return; + setSubmissionErrors((current) => ({ + ...current, + [draftKey]: localSubmissionError(error), + })); + }); }} onRecover={() => { if (targetConversationId) { diff --git a/src/pages/Chat/CodingConversationHeader.tsx b/src/pages/Chat/CodingConversationHeader.tsx index 77f5336..522d399 100644 --- a/src/pages/Chat/CodingConversationHeader.tsx +++ b/src/pages/Chat/CodingConversationHeader.tsx @@ -16,7 +16,6 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; -import { abortCodingConversation } from '@/lib/coding-conversations'; import { cn } from '@/lib/utils'; import { useSettingsStore } from '@/stores/settings'; import type { ConversationSnapshot } from '@/types/coding-conversation'; @@ -54,11 +53,13 @@ export function CodingConversationHeader({ conversation, snapshot, onRename, + onAbort, onRecover, }: { conversation: CodingConversationMetadata | null; snapshot: ConversationSnapshot | null; onRename(title: string): Promise; + onAbort(): Promise; onRecover(): Promise; }) { const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed); @@ -128,7 +129,7 @@ export function CodingConversationHeader({ disabled={Boolean(busyAction) || !conversation} aria-label="中止" title="中止" - onClick={() => perform('abort', async () => abortCodingConversation(conversation!.id))} + onClick={() => perform('abort', onAbort)} > {busyAction === 'abort' ?