From 8a67e8bd47d814b533476a6e36421f7a5d983a27 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Mon, 14 Sep 2026 20:57:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E4=BA=91=E6=99=BA=E8=83=BD=E4=BD=93):=20?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E8=BD=BB=E5=BE=AE=E6=97=B6=E5=B7=AE=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E8=BF=9E=E6=8E=A5=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260914-cloud-reconnect-5ad71e6c.md | 44 +++++++++++++++++++ electron/services/cloud-agents.ts | 10 +++-- tests/unit/cloud-agents-main.test.ts | 29 ++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260914-cloud-reconnect-5ad71e6c.md diff --git a/.project-docs/30-worklog/tasks/20260914-cloud-reconnect-5ad71e6c.md b/.project-docs/30-worklog/tasks/20260914-cloud-reconnect-5ad71e6c.md new file mode 100644 index 0000000..848e65a --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260914-cloud-reconnect-5ad71e6c.md @@ -0,0 +1,44 @@ +# Task: 诊断云智能体客户端频繁不可用 + +## Identity + +- Task ID: 20260914-cloud-reconnect-5ad71e6c +- Mode: Feature +- Branch: codex/20260914-cloud-reconnect-5ad71e6c-cloud-reconnect +- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260914-cloud-reconnect-5ad71e6c +- Base commit: 1d00ba39bd598e2f2ea26cc867d82afc2bdba3ee +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Diagnose frequent unavailable errors in the installed cloud Agent/channel pages. Repair the reproduced Main session time-validation defect, keeping channel routing, credentials, request identity and mutation recovery unchanged. + +## Intent And Constraints + +- Concurrent Gate passed through official check/start/status; exact identity, isolated feature worktree and main base match above. Planning Gate passed after required entries, own task, positioning/current state, decisions/architecture, ADR-004, personal cloud Agent ADR, domain and evidence/reflection/commitment context. Peer Scope/Intent/Promotion context was assessed read-only; historical placeholders are unknown. Related channel/Agent diagnostics and UX discussion are read-only, completed, or on earlier fixed bases, with no concrete semantic conflict affecting this narrow Main repair. +- Credentials/network remain Main-owned. The user subsequently authorized committing the verified repair and merging it into main. No production model request, mutation replay, reconnect, account change, subagent, push, package, installation or deployment is included. +- Merge preparation resumed this same task through official start/status; Concurrent and Planning Gates passed using the previously loaded context and refreshed changed UX discussion record. The separate integration start was blocked by main's previous integration owner, 20260914-merge-ppt-makelore-2e8ec5dc; transferring that registration requires explicit confirmation. Main and its three pre-existing untracked documents remain untouched. +- Diagnosis uses actual CloudAgentsModule code with controlled session responses. Valid upstream 200 is insufficient: the asserted signal includes whether Main emits the next Yuxi read. + +## Outcome + +- Confirmed defect: with the issuer one second ahead, a normal 300-second session is rejected by the client upper-bound comparison against local Date.now; the client emits cloud_service_unavailable and never calls /api/makelore/agents. Equal clocks pass the identical harness. +- Supplied production API logs contain only successful internal session issuance every approximately 30 seconds; they do not contain the failing client reads. Local installed 1.5.2 Main logs do not record these errors. Anonymous Works HTTP Date probes show subsecond offsets but do not prove the deployed Yuxi session claims or this incident's sole cause. Public Yuxi URL requested for the remaining network check. +- Fixed Main by capping local session reuse at 300 seconds instead of rejecting normal issuer expiry for clock skew. Existing expiry, finite integer timestamp, account, scope, bearer and HTTPS checks remain. No mutation retry or new channel behavior was added. +- Read-only inspection of the running installed 1.5.2 app.asar confirms the same faulty expires_at > Date.now() + 300000 clause is present. The installation was not altered or restarted. This record accompanies the verified source repair for integration; the installed app remains unchanged. + +## Verification + +- Read-only stdin Node/TypeScript harness ran the actual Main module: clock offset 0 => PASS, session + agents requested; offset +1000 ms => FAIL, cloud_service_unavailable, only session requested. No production credentials used. +- Formal regression was added before the fix: 1 failing skew test / 3 passing expired-invalid timestamp cases. Failure stack identifies CloudAgentsModule.session; with the fix all 33 Main cloud Agent tests pass, including cache reuse and renewal at the existing five-second margin. +- Pinned pnpm 10.33.4 frozen offline dependency install completed. Typecheck, scoped ESLint, production Renderer/Main/Preload/utility build, and git diff --check passed. Build emits existing Browserslist-age/dynamic import notices. No full suite, new installer, live authenticated end-to-end check or production operation is claimed; no visible UI flow changed, so the actual Main session test is the relevant regression seam. + +## Follow-ups + +- Correlate the deployed public Yuxi path and failed Main response; production incident attribution remains provisional until that evidence exists. +- Rebuilt desktop installation required to deliver any Main fix. Do not change cloud configuration or bindings based only on this repro. + +## Promotion Candidates + +- Target: current-state.md / Integrated Through. Record this narrow Main credential clock-skew repair and its source commit after merge. Evidence: actual Main reproduction, 33 passing tests, typecheck, scoped lint and production build. Future impact: correctly issued credentials can be accepted across slight client/issuer clock differences; local reuse stays capped. No conflict with ADR-004 or personal cloud Agent ownership. Main integration is authorized; registration transfer requires confirmation. No broader production root-cause claim or architecture change is proposed. diff --git a/electron/services/cloud-agents.ts b/electron/services/cloud-agents.ts index e2e85b5..dc91943 100644 --- a/electron/services/cloud-agents.ts +++ b/electron/services/cloud-agents.ts @@ -608,13 +608,17 @@ export class CloudAgentsModule { if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname))) || url.username || url.password || url.search || url.hash || value.scope !== 'makelore-agents' || value.token_type !== 'bearer' - || typeof value.expires_at !== 'number' || value.expires_at * 1000 <= Date.now() - || value.expires_at * 1000 > Date.now() + 300000) { + || typeof value.expires_at !== 'number' || !Number.isSafeInteger(value.expires_at) + || value.expires_at * 1000 <= Date.now()) { throw new CloudAgentsError(502, 'cloud_service_unavailable'); } this.cached = { binding, accessToken: textField(value.access_token, 8192), - apiBaseUrl: url.toString().replace(/\/+$/, ''), expiresAt: value.expires_at * 1000, + apiBaseUrl: url.toString().replace(/\/+$/, ''), + // The issuer and this computer have independent clocks. Cap local reuse + // instead of rejecting a valid five-minute credential for slight skew. + // Yuxi remains responsible for enforcing the credential's actual expiry. + expiresAt: Math.min(value.expires_at * 1000, Date.now() + 300000), }; return this.cached; } diff --git a/tests/unit/cloud-agents-main.test.ts b/tests/unit/cloud-agents-main.test.ts index be965dd..b84a1f7 100644 --- a/tests/unit/cloud-agents-main.test.ts +++ b/tests/unit/cloud-agents-main.test.ts @@ -44,6 +44,35 @@ beforeEach(() => { vi.clearAllMocks(); journalRecords.clear(); clearWorksSquareS afterEach(() => { instances.splice(0).forEach((module) => module.dispose()); clearWorksSquareSession(); vi.useRealTimers(); }); describe('Main cloud Agents boundary', () => { + it('accepts a five-minute session from a clock one second ahead and caps local caching', async () => { + vi.useFakeTimers(); + const now = Math.floor(Date.now() / 1000) * 1000; + vi.setSystemTime(now); + const transport = vi.fn(async (url: string) => url.endsWith('/session') + ? json({ access_token: 'yuxi-secret', token_type: 'bearer', scope: 'makelore-agents', + expires_at: Math.floor(Date.now() / 1000) + 301, api_base_url: 'https://agents.example.test' }) + : json({ agents: [], next_cursor: null })); + const module = moduleFor(transport); + expect(await module.list()).toEqual({ agents: [], next_cursor: null }); + expect(transport.mock.calls.map(([url]) => new URL(url).pathname)) + .toEqual(['/api/cloud-agents/session', '/api/makelore/agents']); + vi.setSystemTime(now + 294000); + await module.list(); + expect(transport.mock.calls.filter(([url]) => url.endsWith('/session'))).toHaveLength(1); + vi.setSystemTime(now + 295000); + await module.list(); + expect(transport.mock.calls.filter(([url]) => url.endsWith('/session'))).toHaveLength(2); + }); + + it.each([0, -1, 1e308])('rejects an expired or invalid session timestamp %s before calling Yuxi', async (expiresAt) => { + const transport = vi.fn().mockResolvedValue(json({ + access_token: 'yuxi-secret', token_type: 'bearer', scope: 'makelore-agents', + expires_at: expiresAt, api_base_url: 'https://agents.example.test', + })); + await expect(moduleFor(transport).list()).rejects.toMatchObject({ code: 'cloud_service_unavailable' }); + expect(transport).toHaveBeenCalledTimes(1); + }); + it('persists recovery in the real electron-store file and reloads it after replacing Main', async () => { const directory = await mkdtemp(join(tmpdir(), 'makelore-cloud-journal-')); try {