From 44f4361acc2c04471006b43645fe08786b570cea Mon Sep 17 00:00:00 2001 From: Willi Budzinski Date: Fri, 19 Jun 2026 19:07:15 +0200 Subject: [PATCH 1/5] fix: preserve memory project scope through state serialization --- .../arena-synthesis.md | 55 +++++ .../plan.md | 60 +++++ .../todo.md | 87 +++++++ src/state/kv.ts | 183 ++++++++++++++- src/state/schema.ts | 1 + src/types.ts | 6 + ...emory-project-engine-serialization.test.ts | 134 +++++++++++ test/state-kv-memory-projects.test.ts | 213 ++++++++++++++++++ 8 files changed, 730 insertions(+), 9 deletions(-) create mode 100644 docs/todos/2026-06-19-issue-303-memory-save-project-coverage/arena-synthesis.md create mode 100644 docs/todos/2026-06-19-issue-303-memory-save-project-coverage/plan.md create mode 100644 docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md create mode 100644 test/memory-project-engine-serialization.test.ts create mode 100644 test/state-kv-memory-projects.test.ts diff --git a/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/arena-synthesis.md b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/arena-synthesis.md new file mode 100644 index 000000000..e2a61cf84 --- /dev/null +++ b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/arena-synthesis.md @@ -0,0 +1,55 @@ +# Arena Synthesis + +## Rubric + +| Criterion | Candidate A | Candidate B | Candidate C | +| --- | --- | --- | --- | +| Proves current bug from repo evidence | Strong | Strong | Medium | +| Fixes engine-backed persistence without broad API/schema drift | Medium | Medium | Low | +| Adds meaningful current-failing regression | Strong | Strong | Medium | +| Practical verification under pnpm hardening | Medium | Medium | Medium | +| Maintains future project-scoped memory behavior | Strong | Strong | Low | + +## Pick + +Base: candidate B. + +Candidate B gives the clearest implementation boundary: maintain an internal `mem:memory-projects` sidecar at `StateKV`, hydrate `Memory.project` centrally on `KV.memories` reads/lists, and use a fake SDK that intentionally strips `project` from memory rows to produce a focused red test. Candidate A converges on the same architecture and adds useful details around list hydration and legacy unscoped behavior. Candidate C is useful as a simpler alternative but is not a safe base: pinned `iii-sdk@0.11.2` serializes trigger messages with `JSON.stringify`, so a `project: undefined` own property is not a reliable wire/storage-shape stabilizer. + +The cross-judge agreed with this pick. Judge scores were A 21.0/25, B 24.0/25, and C 13.0/25. The judge recommended using B as the base, grafting A's lifecycle/schema guardrails, and grafting C's unscoped-first integration scenario only. + +## Grafts + +- From candidate A: keep direct `memory.project` authoritative when present, and hydrate from the sidecar only when the row lacks a usable project. +- From candidate A: include delete/stale-sidecar cleanup and legacy unscoped-memory tests. +- From candidate B: prefer a primitive JSON string sidecar value if the implementation proceeds, because the reported engine behavior specifically drops unknown object fields in the memory record. +- From candidate B: add a `mem::remember` + diagnostics regression using a fake `StateKV`/SDK that simulates the engine dropping `Memory.project`. + +## Rejections + +- Reject candidate C's primary fix for now. Adding `project` as an own property with value `undefined` in `remember.ts` would fail to survive the SDK's JSON wire encoding and therefore may only improve mock-object tests. +- Reject patching `memory-project-coverage` to ignore missing project values. That would hide the isolation failure instead of fixing readback. +- Reject per-endpoint hydration only in diagnostics or `/memories`. That leaves search, smart search, migration, mesh/export, lineage, and enrichment inconsistent. +- Reject dependency or iii-engine upgrades as the first move; those are broader dependency/runtime boundary changes. + +## Cross-Judge + +Report: `/tmp/arena-303-memory-project/judge/report.md`. + +The judge confirmed the candidate convergence/divergence pattern: + +- A/B converge on central `StateKV` sidecar hydration. +- C diverges with a `project: undefined` row-shape fix, which the judge rejected as unreliable because undefined values do not survive normal JSON wire encoding. +- All candidates agree MCP/REST/function contracts should stay unchanged and existing mock-KV tests are insufficient. + +## Boundary Result + +The winning design changes internal persistence/schema behavior by adding a KV sidecar scope and central hydration. That falls under the delegated Human Checkpoint list for persistence/schema changes. Implementation should stop until current-turn approval is given for this narrow internal persistence-sidecar change. + +## Verification If Approved + +- Red/green focused test for `StateKV` hydration under a fake engine that strips `project` from `KV.memories`. +- Focused function/API tests covering `mem::remember`, diagnostics `memory-project-coverage`, `/agentmemory/memories` project filtering, and `inferMemoryProjects`. +- `corepack pnpm test` when dependencies are materialized; if blocked by pnpm hardening, run `corepack pnpm install --frozen-lockfile --ignore-scripts` first per repo instructions. +- `git diff --check`. +- Because persistence/schema code is touched, run Semgrep default scan and staged Gitleaks before commit. diff --git a/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/plan.md b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/plan.md new file mode 100644 index 000000000..3ec0bf377 --- /dev/null +++ b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/plan.md @@ -0,0 +1,60 @@ +# Implementation Plan + +## Source + +- User request: implement the arena-selected solution for GitHub issue #303 using the GitHub feature loop. +- Arena result: use candidate B as the base, grafting candidate A's lifecycle guardrails and candidate C's unscoped-first integration scenario. +- Approved boundary: narrow internal persistence/schema sidecar for `KV.memories`; no public MCP/REST API changes, no dependency changes, no upstream iii-engine changes. + +## Goal + +Make `Memory.project` survive the iii-engine state boundary for memories saved through `memory_save`/`mem::remember`, so project-scoped memories read back as scoped and `memory-project-coverage` can pass for newly scoped data. + +## Files + +- `src/state/schema.ts`: add an internal `KV.memoryProjects` sidecar scope. +- `src/types.ts`: add the corresponding `MemoryProjectRecord` state type. +- `src/state/kv.ts`: hydrate `KV.memories` reads/lists from the sidecar and maintain sidecar lifecycle on set/delete. +- `test/state-kv-memory-projects.test.ts`: add the focused fake-engine regression that strips `project` from memory rows. +- Existing tests to run around `remember`, diagnostics, and migration/project inference. + +## Implementation Steps + +1. Add a red regression test for `StateKV` using a fake SDK whose `state::set` drops `project` only for `KV.memories`. +2. Cover: + - scoped memory set/get/list rehydrates `project`, + - unscoped overwrite clears stale sidecar, + - memory delete clears sidecar, + - malformed sidecar data is ignored, + - direct `project` on the memory row remains authoritative. +3. Implement the internal sidecar: + - `KV.memoryProjects = "mem:memory-projects"`, + - store sidecar values as primitive JSON strings keyed by memory id, + - sidecar payload: `{ memoryId, project, updatedAt }`, + - parse sidecars defensively and ignore malformed values, + - only hydrate existing memory rows; orphan sidecars never create memories. +4. Keep public contracts unchanged: `Memory.project?: string` stays the public field, MCP/REST payloads remain as-is. +5. Run focused tests, then broader repo-native checks available in this worktree. +6. Run required post-change review and security gates before any commit or remote action. + +## Verification Plan + +- Red: `corepack pnpm exec vitest run test/state-kv-memory-projects.test.ts` fails before `src/state/kv.ts` is fixed. +- Green targeted: + - `corepack pnpm exec vitest run test/state-kv-memory-projects.test.ts` + - `corepack pnpm exec vitest run test/remember-project-scope.test.ts test/diagnostics.test.ts test/infer-memory-projects.test.ts` +- Repo-native: + - `corepack pnpm test` + - `corepack pnpm run lint` + - `corepack pnpm run build` +- Hygiene/security: + - `git diff --check` + - `gitleaks protect --staged --redact` after staging intended files + - `semgrep scan --config p/default --error --metrics=off .` unless a repo-native equivalent is identified + +## Stop Conditions + +- The fake-engine regression cannot be made red before implementation. +- The sidecar requires changing public API behavior, auth/security behavior, dependencies, upstream iii-engine, or an automatic data migration. +- Verification is blocked or red after one focused debug pass without a clear next fix. +- Security gates fail or required tools are unavailable without explicit user acceptance. diff --git a/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md new file mode 100644 index 000000000..502524699 --- /dev/null +++ b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md @@ -0,0 +1,87 @@ +# Issue 303 Memory Save Project Coverage + +## Scope + +- Repository: `/Users/A1538552/.codex/worktrees/e321/agentmemory` +- Branch: `issue/303-memory-save-project-coverage` +- Issue: GitHub #303, `memory_save` accepts `project` but real iii-engine persistence drops it. +- Parent batch task record: `docs/todos/2026-06-19-issue-triage-batch-288-312/todo.md` was requested but is not present in this checkout. + +## Sprint Contract + +- Goal: make project-scoped memories saved through `memory_save` survive the real persistence boundary so `memory-project-coverage` can pass for newly scoped memories. +- Scope: validate the issue, identify the persistence seam, add a regression that would catch dropped `Memory.project`, implement the smallest repo-local fix if it does not require a prohibited boundary expansion, and verify with focused and repo-native checks. +- Non-goals: add or remove MCP tools, add REST endpoints, change auth, add dependencies, migrate production data automatically, alter iii-engine outside this repo, or broaden remote/project policy. +- Acceptance criteria: + - Current issue is validated or stopped as invalid/stale with evidence. + - A project value accepted by `memory_save`/`mem::remember` is read back as `memory.project` after the targeted persistence seam. + - Existing unscoped-memory backward compatibility remains intact. + - `memory-project-coverage` behavior remains based on latest memories missing `project`, not on hidden fallback fields. +- Intended verification: + - Red/green focused test for the persistence seam. + - Existing project-scope tests around `mem::remember`, MCP payload shaping, search/recall filtering, diagnostics, and migration as touched. + - `corepack pnpm test` or closest blocked-safe substitute per repo policy. + - `git diff --check`. + - Required security gates if implementation changes persistence/schema/tooling-sensitive surfaces. +- Known boundaries: + - Persistence/schema behavior is a Human Checkpoint boundary under the delegated instructions. If the root-cause fix requires changing persisted schema, dependency versions, iii-engine behavior, or an externally consumed API, stop before implementation. + - Remote writes are authorized only after green verification and only to `origin`; push/PR/merge remains subject to the workflow gates. +- Stop conditions: + - Issue is invalid, stale, duplicate, already fixed, or not reproducible with current repo evidence. + - Fix requires upstream iii-engine changes, dependency changes, data migration, or persisted schema changes without explicit current-turn approval. + - Verification is blocked, failing, flaky, or cannot cover the changed surface. + - Arena conclusions diverge on a boundary-sensitive design. + +## Feature / Verification Matrix + +| Change | Verification method | Status | Evidence | +| --- | --- | --- | --- | +| Issue validation | GitHub issue read plus source/test inspection | Complete | `wbugitlab1/agentmemory#303` is open; local mirror ties the bug title to upstream `rohitg00#751`; current source forwards and stores `project` in mocked KV. | +| Persistence regression | Focused red/green test at the state serialization seam | Complete | `test/state-kv-memory-projects.test.ts` failed before implementation because `KV.memoryProjects` and sidecar writes were absent; it now passes. | +| Minimal fix | Focused implementation preserving existing APIs | Complete | Added internal `KV.memoryProjects` sidecar/hydration in `StateKV`; MCP/REST `Memory.project` contract is unchanged. | +| Diagnostics coverage | Existing diagnostics or targeted test | Complete | `test/memory-project-engine-serialization.test.ts` covers `mem::remember` with dropped row project and `mem::diagnose` `memory-project-coverage=pass`. | +| PR readiness | GitHub feature loop and push-prepare gates | Local verification complete | Branch remains local; remote push/PR boundary not crossed yet. | + +## Issue Evidence + +- `gh issue view 303 --repo wbugitlab1/agentmemory --json ...` returned open issue #303, created 2026-06-14 and updated 2026-06-15. +- `src/mcp/server.ts` handles `memory_save`, trims `project`, and forwards it to `mem::remember`. +- `src/functions/remember.ts` normalizes `data.project` and writes `...(project !== undefined && { project })` into the `Memory` object before `kv.set(KV.memories, ...)`. +- `src/types.ts` defines `Memory.project?: string`. +- `test/remember-project-scope.test.ts` verifies project stamping through a mocked KV store, which does not exercise real iii-engine state serialization. +- `src/functions/diagnostics.ts` implements `memory-project-coverage` by counting latest memories without `project`. + +## Subagent Ledger + +| Workstream | Scope | Edits allowed | Expected output | Result | Residual risk | +| --- | --- | --- | --- | --- | --- | +| Arena candidate A | Read-only repo analysis, output `/tmp/arena-303-memory-project/candidate-a/plan.md` | No repo edits | Fix strategy and rationale | Complete | Proposed `StateKV` sidecar/hydration with object sidecar records. | +| Arena candidate B | Read-only repo analysis, output `/tmp/arena-303-memory-project/candidate-b/plan.md` | No repo edits | Fix strategy and rationale | Complete | Selected base; proposed `StateKV` sidecar/hydration with primitive JSON string sidecar values. | +| Arena candidate C | Read-only repo analysis, output `/tmp/arena-303-memory-project/candidate-c/plan.md` | No repo edits | Fix strategy and rationale | Complete | Proposed `project: undefined` row-shape stabilization; rejected as base but integration scenario is useful. | +| Arena cross-judge | Read-only review of all candidates, output `/tmp/arena-303-memory-project/judge/report.md` | No repo edits | Scored recommendation and graft list | Complete | Judge agrees with candidate B base; implementation remains checkpoint-blocked. | + +## Progress Notes + +- 2026-06-19: Worktree started clean and detached at `499b53fc4a0f58d6f7b2daf674a7943de023d75a`, equal to local `origin/main`. +- 2026-06-19: Local and remote issue branch did not exist; created `issue/303-memory-save-project-coverage` from `origin/main`. +- 2026-06-19: Required `arena` and `github-feature-loop` skills loaded. Support skills loaded for planning, TDD, review, verification, simple-code, and push preparation. +- 2026-06-19: Initial boundary assessment: likely persistence/schema-sensitive. Hold implementation until root cause and Human Checkpoint classification are clear. +- 2026-06-19: Arena completed. Candidates A and B converged on a central `StateKV` sidecar/hydration fix; candidate C proposed a row-shape fix. Cross-judge scored B highest and recommended grafting A's lifecycle guardrails plus C's unscoped-first integration scenario. The winning approach adds an internal `KV.memoryProjects` persistence sidecar, so implementation is blocked pending Human Checkpoint approval for a narrow persistence/schema change. +- 2026-06-19: User approved implementing the arena-selected solution through the GitHub feature loop. Added `plan.md`; proceeding with TDD against the `StateKV` persistence seam. +- 2026-06-19: Added red `StateKV` fake-engine regression. Initial run failed as expected: schema sidecar scope was missing and no sidecar string was written. +- 2026-06-19: Implemented `KV.memoryProjects` sidecar, `MemoryProjectRecord`, and `StateKV` hydration/maintenance for memory `get`, `list`, `set`, `delete`, and explicit `project` updates. +- 2026-06-19: Added `mem::remember` + `mem::diagnose` regression proving `memory-project-coverage` passes when the fake engine drops `project` from the memory row. +- 2026-06-19: Review result: no findings. Checked scope, malformed sidecars, stale sidecar cleanup, delete cleanup, direct-row precedence, explicit `project` update lifecycle, and diagnostic integration. Independent subagent review was not used because the available subagent tool policy requires an explicit user subagent request; performed a separate adversarial self-pass instead. +- 2026-06-19: Verification evidence: + - `corepack pnpm install --frozen-lockfile --ignore-scripts` succeeded to materialize missing `node_modules`. + - `corepack pnpm exec vitest run test/state-kv-memory-projects.test.ts` red before implementation, then green. + - `corepack pnpm exec vitest run test/state-kv-memory-projects.test.ts test/memory-project-engine-serialization.test.ts test/remember-project-scope.test.ts test/diagnostics.test.ts test/infer-memory-projects.test.ts` passed: 5 files, 70 tests. + - `corepack pnpm test` passed after final code changes: 209 files, 2835 tests. An earlier pre-final full run showed timing failures in `test/codex-sdk-provider.test.ts` and `test/hooks-plaintext-http.test.ts`; those files passed in isolation and the final full rerun passed. + - `corepack pnpm run lint` passed. + - `corepack pnpm run skills:check` passed. + - `corepack pnpm run build` passed. + - `corepack pnpm run coverage` passed: 209 files, 2835 tests; statements 77.79%, branches 70.19%, functions 80.87%, lines 79.70%. + - `git diff --check` passed. + - `semgrep scan --config p/default --error --metrics=off .` passed with 0 findings. + - `gitleaks protect --staged --redact` passed with no leaks found after staging task-owned files. + - OSV not run because no dependency, lockfile, container, vendored, or third-party package surface changed. diff --git a/src/state/kv.ts b/src/state/kv.ts index 0c0e6ccc1..ddce018d9 100644 --- a/src/state/kv.ts +++ b/src/state/kv.ts @@ -1,47 +1,212 @@ import type { ISdk } from 'iii-sdk' +import type { MemoryProjectRecord } from '../types.js' +import { KV } from './schema.js' + +type UpdateOperation = { type: string; path: string; value?: unknown } +type MemoryProjectUpdate = + | { changed: false } + | { changed: true; project?: string } + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function normalizeProject(value: unknown): string | undefined { + if (typeof value !== 'string') return undefined + const project = value.trim() + return project.length > 0 ? project : undefined +} + +function memoryIdFrom(value: unknown, fallback?: string): string | undefined { + if (isRecord(value)) { + const id = normalizeProject(value.id) + if (id !== undefined) return id + } + return fallback +} + +function readMemoryProject(value: unknown): string | undefined { + return isRecord(value) ? normalizeProject(value.project) : undefined +} + +function isProjectUpdatePath(path: string): boolean { + return path === 'project' || path === '/project' +} + +function readMemoryProjectUpdate(ops: UpdateOperation[]): MemoryProjectUpdate { + for (const op of ops.toReversed()) { + if (!isProjectUpdatePath(op.path)) continue + if (op.type === 'remove') return { changed: true } + const project = normalizeProject(op.value) + return project === undefined ? { changed: true } : { changed: true, project } + } + return { changed: false } +} + +function parseMemoryProjectRecord(value: unknown): MemoryProjectRecord | null { + if (typeof value !== 'string') return null + + try { + const parsed = JSON.parse(value) as unknown + if (!isRecord(parsed)) return null + const memoryId = normalizeProject(parsed.memoryId) + const project = normalizeProject(parsed.project) + const updatedAt = typeof parsed.updatedAt === 'string' ? parsed.updatedAt : undefined + if (memoryId === undefined || project === undefined || updatedAt === undefined) { + return null + } + return { memoryId, project, updatedAt } + } catch { + return null + } +} export class StateKV { constructor(private sdk: ISdk) {} async get(scope: string, key: string): Promise { + const value = await this.rawGet(scope, key) + return this.hydrateMemoryValue(scope, key, value) + } + + async set(scope: string, key: string, value: T): Promise { + if (scope === KV.memories) { + const project = readMemoryProject(value) + if (project !== undefined) { + await this.setMemoryProject(key, project) + } + + const stored = await this.rawSet(scope, key, value) + if (project === undefined) { + await this.rawDelete(KV.memoryProjects, key) + } + return (await this.hydrateMemoryValue(scope, key, stored)) as T + } + + return this.rawSet(scope, key, value) + } + + async update( + scope: string, + key: string, + ops: UpdateOperation[], + ): Promise { + const projectUpdate = scope === KV.memories ? readMemoryProjectUpdate(ops) : { changed: false } + const value = await this.rawUpdate(scope, key, ops) + if (projectUpdate.changed) { + if (projectUpdate.project !== undefined) { + await this.setMemoryProject(key, projectUpdate.project) + } else { + await this.rawDelete(KV.memoryProjects, key) + } + } + return (await this.hydrateMemoryValue(scope, key, value)) as T + } + + async delete(scope: string, key: string): Promise { + await this.rawDelete(scope, key) + if (scope === KV.memories) { + await this.rawDelete(KV.memoryProjects, key) + } + } + + async list(scope: string): Promise { + const values = await this.rawList(scope) + if (scope !== KV.memories) return values + + const projectByMemoryId = await this.listMemoryProjects() + return values.map((value) => this.hydrateMemoryListValue(value, projectByMemoryId)) + } + + private async rawGet(scope: string, key: string): Promise { return this.sdk.trigger<{ scope: string; key: string }, T | null>({ function_id: 'state::get', payload: { scope, key }, }) } - async set(scope: string, key: string, value: T): Promise { + private async rawSet(scope: string, key: string, value: T): Promise { return this.sdk.trigger<{ scope: string; key: string; value: T }, T>({ function_id: 'state::set', payload: { scope, key, value }, }) } - async update( + private async rawUpdate( scope: string, key: string, - ops: Array<{ type: string; path: string; value?: unknown }>, + ops: UpdateOperation[], ): Promise { - return this.sdk.trigger< - { scope: string; key: string; ops: Array<{ type: string; path: string; value?: unknown }> }, - T - >({ + return this.sdk.trigger<{ scope: string; key: string; ops: UpdateOperation[] }, T>({ function_id: 'state::update', payload: { scope, key, ops }, }) } - async delete(scope: string, key: string): Promise { + private async rawDelete(scope: string, key: string): Promise { return this.sdk.trigger<{ scope: string; key: string }, void>({ function_id: 'state::delete', payload: { scope, key }, }) } - async list(scope: string): Promise { + private async rawList(scope: string): Promise { return this.sdk.trigger<{ scope: string }, T[]>({ function_id: 'state::list', payload: { scope }, }) } + + private async setMemoryProject(memoryId: string, project: string): Promise { + const record: MemoryProjectRecord = { + memoryId, + project, + updatedAt: new Date().toISOString(), + } + await this.rawSet(KV.memoryProjects, memoryId, JSON.stringify(record)) + } + + private async listMemoryProjects(): Promise> { + const values = await this.rawList(KV.memoryProjects) + const projectByMemoryId = new Map() + + for (const value of values) { + const record = parseMemoryProjectRecord(value) + if (record !== null) { + projectByMemoryId.set(record.memoryId, record.project) + } + } + + return projectByMemoryId + } + + private async hydrateMemoryValue( + scope: string, + key: string, + value: T | null, + ): Promise { + if (scope !== KV.memories || value === null || !isRecord(value)) return value + if (readMemoryProject(value) !== undefined) return value + + const sidecar = await this.rawGet(KV.memoryProjects, key) + const record = parseMemoryProjectRecord(sidecar) + const memoryId = memoryIdFrom(value, key) + if (record === null || memoryId === undefined || record.memoryId !== memoryId) { + return value + } + + return { ...value, project: record.project } as T + } + + private hydrateMemoryListValue(value: T, projectByMemoryId: Map): T { + if (!isRecord(value) || readMemoryProject(value) !== undefined) return value + + const memoryId = memoryIdFrom(value) + if (memoryId === undefined) return value + + const project = projectByMemoryId.get(memoryId) + if (project === undefined) return value + + return { ...value, project } as T + } } diff --git a/src/state/schema.ts b/src/state/schema.ts index 4b2ddb506..fe023b764 100644 --- a/src/state/schema.ts +++ b/src/state/schema.ts @@ -4,6 +4,7 @@ export const KV = { sessions: "mem:sessions", observations: (sessionId: string) => `mem:obs:${sessionId}`, memories: "mem:memories", + memoryProjects: "mem:memory-projects", summaries: "mem:summaries", config: "mem:config", metrics: "mem:metrics", diff --git a/src/types.ts b/src/types.ts index 5054ecbfc..cb238d830 100644 --- a/src/types.ts +++ b/src/types.ts @@ -143,6 +143,12 @@ export interface Memory { metadata?: SessionMetadata; } +export interface MemoryProjectRecord { + memoryId: string; + project: string; + updatedAt: string; +} + export type MemoryPolicyMemoryType = Memory["type"]; export interface MemoryWritePolicy { diff --git a/test/memory-project-engine-serialization.test.ts b/test/memory-project-engine-serialization.test.ts new file mode 100644 index 000000000..8996cd578 --- /dev/null +++ b/test/memory-project-engine-serialization.test.ts @@ -0,0 +1,134 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../src/logger.js", () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); + +vi.mock("../src/state/keyed-mutex.js", () => ({ + withKeyedLock: (_key: string, fn: () => Promise) => fn(), +})); + +vi.mock("iii-sdk", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + TriggerAction: { + ...actual.TriggerAction, + Void: vi.fn(() => ({ type: "void" })), + }, + }; +}); + +vi.mock("../src/config.js", () => ({ + getAgentId: vi.fn(() => undefined), + isGraphExtractionEnabled: vi.fn(() => false), +})); + +import { registerDiagnosticsFunction } from "../src/functions/diagnostics.js"; +import { registerRememberFunction } from "../src/functions/remember.js"; +import { getSearchIndex, setIndexPersistence } from "../src/functions/search.js"; +import { StateKV } from "../src/state/kv.js"; +import { KV } from "../src/state/schema.js"; +import type { DiagnosticCheck, Memory } from "../src/types.js"; + +type TriggerInput = string | { function_id: string; payload?: unknown }; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function cloneForEngine(scope: string, value: unknown): unknown { + if (scope !== KV.memories || !isRecord(value)) { + return value; + } + + const { project: _dropped, ...rest } = value; + return rest; +} + +function createProjectDroppingSdk() { + const functions = new Map(); + const store = new Map>(); + + const trigger = vi.fn(async (idOrInput: TriggerInput, data?: unknown) => { + const id = typeof idOrInput === "string" ? idOrInput : idOrInput.function_id; + const payload = typeof idOrInput === "string" ? data : idOrInput.payload; + + if (id === "state::set") { + const { scope, key, value } = payload as { scope: string; key: string; value: unknown }; + if (!store.has(scope)) store.set(scope, new Map()); + const stored = cloneForEngine(scope, value); + store.get(scope)!.set(key, stored); + return stored; + } + + if (id === "state::get") { + const { scope, key } = payload as { scope: string; key: string }; + return store.get(scope)?.get(key) ?? null; + } + + if (id === "state::delete") { + const { scope, key } = payload as { scope: string; key: string }; + store.get(scope)?.delete(key); + return undefined; + } + + if (id === "state::list") { + const { scope } = payload as { scope: string }; + return Array.from(store.get(scope)?.values() ?? []); + } + + const fn = functions.get(id); + if (!fn) return {}; + return fn(payload); + }); + + return { + registerFunction: (idOrOpts: string | { id: string }, handler: Function) => { + const id = typeof idOrOpts === "string" ? idOrOpts : idOrOpts.id; + functions.set(id, handler); + }, + registerTrigger: () => {}, + trigger, + rawGet: (scope: string, key: string) => store.get(scope)?.get(key) ?? null, + }; +} + +beforeEach(() => { + getSearchIndex().clear(); + setIndexPersistence(null); +}); + +afterEach(() => { + setIndexPersistence(null); +}); + +describe("memory project persistence across engine serialization", () => { + it("keeps a memory_save project visible to diagnostics after the memory row drops project", async () => { + const sdk = createProjectDroppingSdk(); + const kv = new StateKV(sdk as never); + registerRememberFunction(sdk as never, kv); + registerDiagnosticsFunction(sdk as never, kv); + + const saved = (await sdk.trigger({ + function_id: "mem::remember", + payload: { + content: "API project memories must remain scoped after persistence", + project: "api", + }, + })) as { success: boolean; memory: Memory }; + + expect(saved.success).toBe(true); + expect(sdk.rawGet(KV.memories, saved.memory.id)).not.toHaveProperty("project"); + + const stored = await kv.get(KV.memories, saved.memory.id); + expect(stored?.project).toBe("api"); + + const result = (await sdk.trigger("mem::diagnose", { + categories: ["memories"], + })) as { checks: DiagnosticCheck[] }; + const coverage = result.checks.find((check) => check.name === "memory-project-coverage"); + + expect(coverage?.status).toBe("pass"); + }); +}); diff --git a/test/state-kv-memory-projects.test.ts b/test/state-kv-memory-projects.test.ts new file mode 100644 index 000000000..c27d7fcb4 --- /dev/null +++ b/test/state-kv-memory-projects.test.ts @@ -0,0 +1,213 @@ +import { describe, expect, it, vi } from "vitest"; +import { StateKV } from "../src/state/kv.js"; +import { KV } from "../src/state/schema.js"; +import type { Memory } from "../src/types.js"; + +const MEMORY_PROJECTS_SCOPE = "mem:memory-projects"; + +type TriggerInput = { + function_id: string; + payload: { + scope: string; + key?: string; + value?: unknown; + ops?: Array<{ type: string; path: string; value?: unknown }>; + }; +}; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function cloneForEngine(scope: string, value: unknown): unknown { + if (scope !== KV.memories || !isRecord(value)) { + return value; + } + + const { project: _dropped, ...rest } = value; + return rest; +} + +function applyUpdate(value: unknown, ops: Array<{ type: string; path: string; value?: unknown }>): unknown { + if (!isRecord(value)) return value; + const next = { ...value }; + + for (const op of ops) { + const path = op.path.startsWith("/") ? op.path.slice(1) : op.path; + if (op.type === "remove") { + delete next[path]; + } else { + next[path] = op.value; + } + } + + return next; +} + +function createProjectDroppingSdk() { + const store = new Map>(); + + const trigger = vi.fn(async (input: TriggerInput) => { + const { scope, key, value } = input.payload; + + if (input.function_id === "state::set") { + if (key === undefined) throw new Error("state::set requires key"); + if (!store.has(scope)) store.set(scope, new Map()); + const stored = cloneForEngine(scope, value); + store.get(scope)!.set(key, stored); + return stored; + } + + if (input.function_id === "state::get") { + if (key === undefined) throw new Error("state::get requires key"); + return store.get(scope)?.get(key) ?? null; + } + + if (input.function_id === "state::update") { + if (key === undefined) throw new Error("state::update requires key"); + const current = store.get(scope)?.get(key) ?? null; + const stored = cloneForEngine(scope, applyUpdate(current, input.payload.ops ?? [])); + if (!store.has(scope)) store.set(scope, new Map()); + store.get(scope)!.set(key, stored); + return stored; + } + + if (input.function_id === "state::delete") { + if (key === undefined) throw new Error("state::delete requires key"); + store.get(scope)?.delete(key); + return undefined; + } + + if (input.function_id === "state::list") { + return Array.from(store.get(scope)?.values() ?? []); + } + + throw new Error(`Unexpected function: ${input.function_id}`); + }); + + return { + trigger, + rawGet: (scope: string, key: string) => store.get(scope)?.get(key) ?? null, + seed: (scope: string, key: string, value: unknown) => { + if (!store.has(scope)) store.set(scope, new Map()); + store.get(scope)!.set(key, value); + }, + }; +} + +function makeMemory(id: string, project?: string): Memory { + const now = "2026-06-19T12:00:00.000Z"; + return { + id, + createdAt: now, + updatedAt: now, + type: "fact", + title: `Memory ${id}`, + content: `Content for ${id}`, + concepts: [], + files: [], + sessionIds: [], + strength: 5, + version: 1, + isLatest: true, + ...(project !== undefined && { project }), + }; +} + +describe("StateKV memory project sidecar", () => { + it("exposes the internal memory project scope in the schema", () => { + expect(KV).toHaveProperty("memoryProjects", MEMORY_PROJECTS_SCOPE); + }); + + it("hydrates scoped memories when the engine drops project from the memory row", async () => { + const sdk = createProjectDroppingSdk(); + const kv = new StateKV(sdk as never); + const memory = makeMemory("mem_scoped", "api"); + + await kv.set(KV.memories, memory.id, memory); + + expect(sdk.rawGet(KV.memories, memory.id)).not.toHaveProperty("project"); + expect(sdk.rawGet(MEMORY_PROJECTS_SCOPE, memory.id)).toEqual(expect.any(String)); + + const stored = await kv.get(KV.memories, memory.id); + expect(stored?.project).toBe("api"); + + const listed = await kv.list(KV.memories); + expect(listed).toHaveLength(1); + expect(listed[0]?.project).toBe("api"); + }); + + it("clears stale project sidecars when a memory is overwritten without a project", async () => { + const sdk = createProjectDroppingSdk(); + const kv = new StateKV(sdk as never); + + await kv.set(KV.memories, "mem_unscoped", makeMemory("mem_unscoped", "api")); + await kv.set(KV.memories, "mem_unscoped", makeMemory("mem_unscoped")); + + expect(sdk.rawGet(MEMORY_PROJECTS_SCOPE, "mem_unscoped")).toBeNull(); + const stored = await kv.get(KV.memories, "mem_unscoped"); + expect(stored?.project).toBeUndefined(); + }); + + it("deletes the project sidecar when a memory is deleted", async () => { + const sdk = createProjectDroppingSdk(); + const kv = new StateKV(sdk as never); + + await kv.set(KV.memories, "mem_deleted", makeMemory("mem_deleted", "api")); + await kv.delete(KV.memories, "mem_deleted"); + + expect(await kv.get(KV.memories, "mem_deleted")).toBeNull(); + expect(sdk.rawGet(MEMORY_PROJECTS_SCOPE, "mem_deleted")).toBeNull(); + }); + + it("updates the project sidecar when a memory update changes project", async () => { + const sdk = createProjectDroppingSdk(); + const kv = new StateKV(sdk as never); + + await kv.set(KV.memories, "mem_updated", makeMemory("mem_updated", "api")); + await kv.update(KV.memories, "mem_updated", [ + { type: "set", path: "project", value: "web" }, + ]); + + let stored = await kv.get(KV.memories, "mem_updated"); + expect(stored?.project).toBe("web"); + + await kv.update(KV.memories, "mem_updated", [ + { type: "remove", path: "project" }, + ]); + + stored = await kv.get(KV.memories, "mem_updated"); + expect(stored?.project).toBeUndefined(); + expect(sdk.rawGet(MEMORY_PROJECTS_SCOPE, "mem_updated")).toBeNull(); + }); + + it("ignores malformed sidecars instead of inventing a project", async () => { + const sdk = createProjectDroppingSdk(); + sdk.seed(KV.memories, "mem_malformed", makeMemory("mem_malformed")); + sdk.seed(MEMORY_PROJECTS_SCOPE, "mem_malformed", "{not json"); + + const kv = new StateKV(sdk as never); + const stored = await kv.get(KV.memories, "mem_malformed"); + + expect(stored?.project).toBeUndefined(); + }); + + it("keeps a direct memory project authoritative over a stale sidecar", async () => { + const sdk = createProjectDroppingSdk(); + sdk.seed(KV.memories, "mem_direct", makeMemory("mem_direct", "row-project")); + sdk.seed( + MEMORY_PROJECTS_SCOPE, + "mem_direct", + JSON.stringify({ + memoryId: "mem_direct", + project: "sidecar-project", + updatedAt: "2026-06-19T12:00:00.000Z", + }), + ); + + const kv = new StateKV(sdk as never); + const stored = await kv.get(KV.memories, "mem_direct"); + + expect(stored?.project).toBe("row-project"); + }); +}); From 064682bc9db5da9ff2fbd28b78170232110978e4 Mon Sep 17 00:00:00 2001 From: Willi Budzinski Date: Fri, 19 Jun 2026 19:21:32 +0200 Subject: [PATCH 2/5] docs: record issue 303 final verification --- .../todo.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md index 502524699..ee02c7e4f 100644 --- a/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md +++ b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md @@ -40,7 +40,7 @@ | Persistence regression | Focused red/green test at the state serialization seam | Complete | `test/state-kv-memory-projects.test.ts` failed before implementation because `KV.memoryProjects` and sidecar writes were absent; it now passes. | | Minimal fix | Focused implementation preserving existing APIs | Complete | Added internal `KV.memoryProjects` sidecar/hydration in `StateKV`; MCP/REST `Memory.project` contract is unchanged. | | Diagnostics coverage | Existing diagnostics or targeted test | Complete | `test/memory-project-engine-serialization.test.ts` covers `mem::remember` with dropped row project and `mem::diagnose` `memory-project-coverage=pass`. | -| PR readiness | GitHub feature loop and push-prepare gates | Local verification complete | Branch remains local; remote push/PR boundary not crossed yet. | +| PR readiness | GitHub feature loop and push-prepare gates | Local verification complete | Branch remains local; remote push/PR boundary not crossed yet. Final local checks passed after merging current `origin/main`. | ## Issue Evidence @@ -85,3 +85,17 @@ - `semgrep scan --config p/default --error --metrics=off .` passed with 0 findings. - `gitleaks protect --staged --redact` passed with no leaks found after staging task-owned files. - OSV not run because no dependency, lockfile, container, vendored, or third-party package surface changed. +- 2026-06-19: Fetched current `origin/main` at `51f926fe` and merged it into the issue branch. The merge required a sandbox escalation only because Git needed to write `ORIG_HEAD` under the worktree metadata directory. Final diff against `origin/main` remained limited to Issue 303 files. +- 2026-06-19: Final post-merge verification evidence: + - `corepack pnpm exec vitest run test/state-kv-memory-projects.test.ts test/memory-project-engine-serialization.test.ts test/remember-project-scope.test.ts test/diagnostics.test.ts test/infer-memory-projects.test.ts` passed: 5 files, 75 tests. + - `corepack pnpm test` passed: 213 files, 2927 tests. + - `corepack pnpm run lint` passed. + - `corepack pnpm run skills:check` passed: 15 skills checked. + - `corepack pnpm run build` passed. + - `corepack pnpm run coverage` passed: 213 files, 2927 tests; statements 77.72%, branches 70.16%, functions 81.13%, lines 79.67%. + - `git diff --check` passed. + - `semgrep scan --config p/default --error --metrics=off .` passed with 0 findings. + - `gitleaks detect --source . --redact --log-opts "origin/main..HEAD"` passed with no leaks found for branch commits. + - `gitleaks detect --source . --redact --no-git` passed with no leaks found in the current worktree. + - `gitleaks protect --staged --redact` passed with no leaks found. + - A full-history `gitleaks detect --source . --redact` reported 14 historical findings under old `.pnpm-store/...` paths at commit `6849579677ce`; none were on this branch's commits and the current worktree scan was clean. From 814eafc258678c86fd4698edb3be74c96832028f Mon Sep 17 00:00:00 2001 From: Willi Budzinski Date: Sat, 20 Jun 2026 04:10:56 +0200 Subject: [PATCH 3/5] docs: record issue 303 remote checkpoint --- .../todo.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md index ee02c7e4f..80fb4ecf2 100644 --- a/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md +++ b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md @@ -99,3 +99,13 @@ - `gitleaks detect --source . --redact --no-git` passed with no leaks found in the current worktree. - `gitleaks protect --staged --redact` passed with no leaks found. - A full-history `gitleaks detect --source . --redact` reported 14 historical findings under old `.pnpm-store/...` paths at commit `6849579677ce`; none were on this branch's commits and the current worktree scan was clean. +- 2026-06-20: User gave explicit current-turn approval for `push`, PR creation, and merge. Push preparation fetched `origin/main` at `a461f1e5`, merged it into the branch as `06da1caf`, and confirmed the branch diff still only touched Issue 303 files. +- 2026-06-20: Post-merge verification update before remote writes: + - `corepack pnpm exec vitest run test/state-kv-memory-projects.test.ts test/memory-project-engine-serialization.test.ts test/remember-project-scope.test.ts test/diagnostics.test.ts test/infer-memory-projects.test.ts` passed: 5 files, 75 tests. + - `corepack pnpm test` failed twice in `test/codex-sdk-provider.test.ts` with 2-second fake Codex child-process timeouts. The same file passed in isolation: 1 file, 3 tests. + - `corepack pnpm exec vitest run --exclude test/integration.test.ts --no-file-parallelism` passed: 213 files, 2931 tests. This serial run was used to avoid the observed parallel child-process timeout flake. + - `corepack pnpm run lint`, `corepack pnpm run skills:check`, and `corepack pnpm run build` passed. + - `corepack pnpm exec vitest run --coverage --exclude test/integration.test.ts --no-file-parallelism` failed with timing failures in `test/codex-sdk-provider.test.ts` and `test/context-injection.test.ts` under coverage instrumentation. Both files passed together without coverage: 2 files, 9 tests. + - `git diff --check`, `semgrep scan --config p/default --error --metrics=off .`, `gitleaks detect --source . --redact --no-git`, and `gitleaks detect --source . --redact --log-opts "origin/main..HEAD"` passed. + - While verifying, `origin/main` advanced again; the branch was observed as `ahead 5, behind 11`. +- 2026-06-20: Stopped before push/PR/merge for Human Checkpoint because formal coverage verification is failing/flaky and the PR base moved again during verification. From a6c2a927abf6ac2197efe8b8f04afaa1f6d13a50 Mon Sep 17 00:00:00 2001 From: Willi Budzinski Date: Sat, 20 Jun 2026 04:28:50 +0200 Subject: [PATCH 4/5] test: stabilize subprocess timeout checks --- .../todo.md | 8 +++++++- test/codex-sdk-provider.test.ts | 15 +++++++++++++-- test/context-injection.test.ts | 3 ++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md index 80fb4ecf2..e3afcb605 100644 --- a/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md +++ b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md @@ -40,7 +40,8 @@ | Persistence regression | Focused red/green test at the state serialization seam | Complete | `test/state-kv-memory-projects.test.ts` failed before implementation because `KV.memoryProjects` and sidecar writes were absent; it now passes. | | Minimal fix | Focused implementation preserving existing APIs | Complete | Added internal `KV.memoryProjects` sidecar/hydration in `StateKV`; MCP/REST `Memory.project` contract is unchanged. | | Diagnostics coverage | Existing diagnostics or targeted test | Complete | `test/memory-project-engine-serialization.test.ts` covers `mem::remember` with dropped row project and `mem::diagnose` `memory-project-coverage=pass`. | -| PR readiness | GitHub feature loop and push-prepare gates | Local verification complete | Branch remains local; remote push/PR boundary not crossed yet. Final local checks passed after merging current `origin/main`. | +| Test stabilization | Re-run the previously flaky parallel and coverage paths | Complete | Raised only test-local subprocess watchdogs; both normal parallel `corepack pnpm test` and serial coverage passed with 213 files, 2931 tests. | +| PR readiness | GitHub feature loop and push-prepare gates | Blocked before remote write | Branch remains local; remote push/PR boundary not crossed yet. Formal test stability is now restored, but `origin/main` had moved during the blocked verification window and must be re-synced before any later push/PR/merge. | ## Issue Evidence @@ -109,3 +110,8 @@ - `git diff --check`, `semgrep scan --config p/default --error --metrics=off .`, `gitleaks detect --source . --redact --no-git`, and `gitleaks detect --source . --redact --log-opts "origin/main..HEAD"` passed. - While verifying, `origin/main` advanced again; the branch was observed as `ahead 5, behind 11`. - 2026-06-20: Stopped before push/PR/merge for Human Checkpoint because formal coverage verification is failing/flaky and the PR base moved again during verification. +- 2026-06-20: Stabilized the flaky verification tests by increasing test-only subprocess watchdogs in `test/codex-sdk-provider.test.ts` and `test/context-injection.test.ts`. This does not change runtime/product timeouts. Verification after the stabilization: + - `corepack pnpm exec vitest run --coverage --exclude test/integration.test.ts --no-file-parallelism` passed: 213 files, 2931 tests; statements 77.73%, branches 70.18%, functions 81.16%, lines 79.68%. + - `corepack pnpm test` passed in the normal parallel mode that previously exposed the Codex fake-subprocess timeout: 213 files, 2931 tests. + - `corepack pnpm run lint` passed. + - `git diff --check` passed. diff --git a/test/codex-sdk-provider.test.ts b/test/codex-sdk-provider.test.ts index c8102c47f..38c013302 100644 --- a/test/codex-sdk-provider.test.ts +++ b/test/codex-sdk-provider.test.ts @@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { CodexSDKProvider } from "../src/providers/codex-sdk.js"; let tempDirs: string[] = []; +const FAKE_CODEX_TIMEOUT_MS = 10_000; function makeFakeCodex(body: string): string { const dir = mkdtempSync(join(tmpdir(), "agentmemory-codex-")); @@ -55,7 +56,12 @@ process.stdin.on("end", () => { `); process.env.OPENAI_API_KEY = "sk-test-secret"; - const provider = new CodexSDKProvider("codex-default", 128, command, 2_000); + const provider = new CodexSDKProvider( + "codex-default", + 128, + command, + FAKE_CODEX_TIMEOUT_MS, + ); await expect(provider.compress("system text", "user text")).resolves.toBe("ok"); }); @@ -80,7 +86,12 @@ process.stdin.on("end", () => { process.exit(4); }); `); - const provider = new CodexSDKProvider("codex-default", 128, command, 2_000); + const provider = new CodexSDKProvider( + "codex-default", + 128, + command, + FAKE_CODEX_TIMEOUT_MS, + ); await expect(provider.compress("system", "user")).rejects.toThrow( /Codex CLI exited with code 4: boom/, diff --git a/test/context-injection.test.ts b/test/context-injection.test.ts index af65f0494..d67ace64f 100644 --- a/test/context-injection.test.ts +++ b/test/context-injection.test.ts @@ -6,6 +6,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; const HOOKS_DIR = join(import.meta.dirname, "..", "plugin", "scripts"); +const HOOK_PROCESS_TIMEOUT_MS = 15_000; // Spawns a compiled plugin hook as a subprocess, feeds it JSON on stdin, // and returns { stdout, stderr, exitCode, tookMs }. The test is about @@ -119,7 +120,7 @@ describe("pre-tool-use hook — context injection gate (#143)", () => { "pre-tool-use.mjs", "", { AGENTMEMORY_URL: `http://127.0.0.1:${address.port}` }, - { endStdin: false, timeoutMs: 5000 }, + { endStdin: false, timeoutMs: HOOK_PROCESS_TIMEOUT_MS }, ); expect(result.exitCode).toBe(0); expect(result.stdout).toBe(""); From 65f05e87cee4501e84e125ee93cf494cce86d513 Mon Sep 17 00:00:00 2001 From: Willi Budzinski Date: Sat, 20 Jun 2026 04:45:41 +0200 Subject: [PATCH 5/5] docs: record issue 303 final verification --- .../todo.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md index e3afcb605..4f183e8b5 100644 --- a/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md +++ b/docs/todos/2026-06-19-issue-303-memory-save-project-coverage/todo.md @@ -41,7 +41,7 @@ | Minimal fix | Focused implementation preserving existing APIs | Complete | Added internal `KV.memoryProjects` sidecar/hydration in `StateKV`; MCP/REST `Memory.project` contract is unchanged. | | Diagnostics coverage | Existing diagnostics or targeted test | Complete | `test/memory-project-engine-serialization.test.ts` covers `mem::remember` with dropped row project and `mem::diagnose` `memory-project-coverage=pass`. | | Test stabilization | Re-run the previously flaky parallel and coverage paths | Complete | Raised only test-local subprocess watchdogs; both normal parallel `corepack pnpm test` and serial coverage passed with 213 files, 2931 tests. | -| PR readiness | GitHub feature loop and push-prepare gates | Blocked before remote write | Branch remains local; remote push/PR boundary not crossed yet. Formal test stability is now restored, but `origin/main` had moved during the blocked verification window and must be re-synced before any later push/PR/merge. | +| PR readiness | GitHub feature loop and push-prepare gates | Ready for remote write after final freshness check | Branch remains local as of this note; post-merge verification passed after syncing `origin/main` at `aec42790`. Run one final fetch/freshness check immediately before push/PR/merge. | ## Issue Evidence @@ -115,3 +115,15 @@ - `corepack pnpm test` passed in the normal parallel mode that previously exposed the Codex fake-subprocess timeout: 213 files, 2931 tests. - `corepack pnpm run lint` passed. - `git diff --check` passed. +- 2026-06-20: User instructed to do what is necessary. Fetched `origin`, merged current `origin/main` at `aec42790` into the issue branch as merge commit `c812a24f`, and confirmed the branch diff against `origin/main` remains limited to Issue 303 files. +- 2026-06-20: Post-merge verification after `origin/main@aec42790`: + - `corepack pnpm exec vitest run test/state-kv-memory-projects.test.ts test/memory-project-engine-serialization.test.ts test/remember-project-scope.test.ts test/diagnostics.test.ts test/infer-memory-projects.test.ts test/context-injection.test.ts test/codex-sdk-provider.test.ts` passed: 7 files, 84 tests. + - `corepack pnpm test` passed: 220 files, 3005 tests. + - `corepack pnpm exec vitest run --coverage --exclude test/integration.test.ts --no-file-parallelism` passed: 220 files, 3005 tests; statements 77.99%, branches 70.30%, functions 81.72%, lines 79.92%. + - `corepack pnpm run lint` passed. + - `corepack pnpm run skills:check` passed: 15 skills checked. + - `corepack pnpm run build` passed. + - `git diff --check origin/main..HEAD` passed. + - `semgrep scan --config p/default --error --metrics=off .` passed with 0 findings. + - `gitleaks detect --source . --redact --no-git` passed with no leaks found. + - `gitleaks detect --source . --redact --log-opts origin/main..HEAD` passed with no leaks found.