Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ credentials*

# Local tool state
.codex-auto-memory.local.json
*.local.*
.codex-auto-memory/
.claude/

Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ If any step is skipped, explain why in the final handoff.
- Milestone 20 is complete: startup-loaded `MEMORY.md` index files now stay separate from on-demand topic refs in the reviewer surface, topic-entry metadata parsing skips invalid shapes safely, and continuity evidence no longer treats in-progress command output as a failed command.
- Milestone 21 is complete: partial-success durable sync and continuity saves now write explicit recovery markers, reviewer JSON/text surfaces expose pending recovery state additively, and `processedRolloutEntries` bounded compaction remains intentionally deferred.
- Post-alpha.21 review fixes are complete: wrapper post-run persistence now attempts continuity auto-save even if durable sync sidecars fail, recovery markers are cleared only for the same rollout/session identity, startup-loaded index files only count truly quoted startup content, and continuity parsing drops the known `Process running with session ID ...` pseudo-failure pattern from persisted reviewer state.
- Milestone 22 is complete: durable sync audit entries now expose explicit `isRecovery` provenance, Chinese next-step extraction has extra short-capture and connector guards, and official Codex/Claude doc references were re-checked before widening companion-first wording.
- Post-alpha.22 review hardening is complete: mixed `PASS`/`FAIL` command output now classifies as failure, corrupted durable-sync processed state falls back to an empty state instead of blocking sync, tiny startup/continuity budgets no longer emit partial blocks, stale local continuity goals no longer override fresh shared goals, and recovery/guardrail coverage is broader.
- `cam memory --json` now also exposes `startupBudget` and `refCountsByScope` for reviewer tooling.
- Native Codex `memories` and `codex_hooks` still remain outside the trusted implementation path until `cam doctor --json` and public docs both show stable support.

Expand All @@ -85,6 +87,7 @@ Short-term priorities:
- keep recovery markers separate from both durable sync audit history and continuity audit history
- keep durable sync audit explicit and typed without turning it into a manual-edit history browser
- keep structured processed-rollout identity and actual-vs-configured extractor audit stable without adding migration-heavy state machinery
- keep corrupted processed-state files and tiny line-budget edge cases on the resilient path instead of letting them block normal reviewer workflows
- keep bounded `processedRolloutEntries` compaction deferred unless the repository explicitly accepts replay-triggered re-sync of evicted old rollouts
- keep in-progress command output out of continuity failure buckets unless the rollout later records an explicit failure
- keep public wording precise whenever `cam memory` exposes edit paths rather than richer in-command editing
Expand Down
41 changes: 40 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,46 @@ The format is intentionally simple and reviewer-friendly: each entry maps to a c

## Unreleased

No unreleased changes yet.
### Added

- Added regression coverage for mixed `PASS`/`FAIL` command output, corrupted durable-sync processed state, tiny startup/continuity line budgets, stale local-goal clearing, and richer recovery-marker JSON assertions.
- Added guardrail coverage for `runSession` invalid-scope, missing-rollout, malformed-rollout, and `clear --json` command paths.

### Changed

- Durable startup and continuity compilation now honor very small line budgets without emitting partial scope or section headers.
- Heuristic continuity no longer carries a stale local goal forward when the latest goal belongs to the shared project layer.
- Slower audit, project-context, and session-command integration tests now use 30-second per-test timeouts instead of tighter 15-second caps.

### Fixed

- Mixed command output that contains both success and failure markers now classifies as failure instead of success.
- Corrupted durable-sync `state.json` no longer blocks future sync attempts; the sync path now degrades to an empty processed-state view and rebuilds forward.
- Matching sync recovery markers can now self-clear even when the rollout is already marked processed, preserving reviewer recovery semantics after transient cleanup failures.

## 0.1.0-alpha.22 - 2026-03-18

### Added

- Added `isRecovery` provenance to durable sync audit entries so reviewer surfaces can distinguish normal sync events from recovery follow-through.
- Added extra Chinese next-step guards that skip very short captures and common narrative connector fragments before they become continuity evidence.
- Added official-doc review notes to the Claude and native migration docs so broken public links are easier to re-verify during future review passes.

### Changed

- Recovery follow-through stayed additive: sync and continuity recovery markers still remain separate from the JSONL audit streams and clear only on matching logical identity.
- Companion-first migration wording was re-checked against the current official Codex and Claude public docs before widening any product claims.

### Fixed

- Reviewer surfaces now preserve recovery provenance more explicitly instead of requiring inference from surrounding audit state.
- Chinese next-step extraction avoids several false-positive narrative fragments that previously looked like actionable follow-up items.

### Review focus

- Confirm that recovery provenance stays additive in reviewer JSON and text surfaces.
- Confirm that Chinese next-step extraction remains conservative without dropping genuine actionable items.
- Confirm that public Codex and Claude doc references remain companion-first and supportable.

## 0.1.0-alpha.21 - 2026-03-18

Expand Down
2 changes: 2 additions & 0 deletions docs/claude-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,5 @@ Claude `/memory` 是完整的交互入口;`codex-auto-memory` 当前更接近
- Claude settings docs: <https://code.claude.com/docs/en/settings>
- Claude subagents docs: <https://code.claude.com/docs/en/sub-agents>
- Claude docs index: <https://code.claude.com/docs/llms.txt>

<!-- Note: canonical Claude Code docs are at https://docs.anthropic.com — verify these URLs if links appear broken -->
3 changes: 2 additions & 1 deletion docs/native-migration.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,5 @@ Migration becomes reasonable only when all of the following are true:
- Codex CLI overview: <https://developers.openai.com/codex/cli>
- Codex feature maturity: <https://developers.openai.com/codex/feature-maturity>
- Codex changelog: <https://developers.openai.com/codex/changelog>
- Codex config docs: <https://developers.openai.com/codex/cli/config>
- Codex config basics: <https://developers.openai.com/codex/config-basic>
- Codex config reference: <https://developers.openai.com/codex/config-reference>
5 changes: 4 additions & 1 deletion docs/native-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,7 @@ Codex 的官方公开资料已经能确认一些对本项目有价值的基础
- Codex CLI overview: <https://developers.openai.com/codex/cli>
- Codex feature maturity: <https://developers.openai.com/codex/feature-maturity>
- Codex changelog: <https://developers.openai.com/codex/changelog>
- Codex config docs: <https://developers.openai.com/codex/cli/config>
- Codex config basics: <https://developers.openai.com/codex/config-basic>
- Codex config reference: <https://developers.openai.com/codex/config-reference>

<!-- Last verified: 2026-03. Standard OpenAI docs are at https://platform.openai.com/docs — verify if broken -->
4 changes: 2 additions & 2 deletions docs/next-phase-brief.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Next Phase Brief

This brief prepares the next implementation window after `0.1.0-alpha.21`.
This brief prepares the next implementation window after `0.1.0-alpha.22`.

## Milestone 22 focus
## Milestone 23 focus

The next phase should continue to stay compact and reviewer-oriented:

Expand Down
22 changes: 18 additions & 4 deletions docs/progress-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This document tracks implementation progress in a format that is easy to consume

- Approximate overall progress toward a strong Claude-style alpha: `99%`
- Approximate progress toward a working local MVP: `99%`
- Current phase: `Phase 21 - failure-path reviewer contract`
- Current phase: `Phase 22 - recovery follow-through and review hardening`

## Completed milestones

Expand Down Expand Up @@ -204,7 +204,7 @@ This document tracks implementation progress in a format that is easy to consume
- Legacy path-only processed state remains readable, but it is no longer treated as authoritative for skip decisions; the new logic accepts a conservative one-time re-sync to avoid false skips.
- Durable sync audit now records both configured and actual extractor metadata, while the compatibility `extractorMode` / `extractorName` fields now reflect the actual extractor that produced the saved updates.
- Added regression coverage for rewritten same-path rollouts, legacy processed-state compatibility, and Codex-configured fallback-to-heuristic audit truth.
- Added explicit timeout headroom for the slower `audit` and `project-context` tests so reviewer validation is more stable on typical local machines.
- Set explicit 30-second per-test timeouts for the slower `audit`, `project-context`, and reviewer-heavy session command tests so CI keeps useful headroom without diverging from the suite default.

### Milestone 20: Reviewer truth tightening and markdown hardening

Expand All @@ -230,6 +230,20 @@ This document tracks implementation progress in a format that is easy to consume
- Continuity parsing and summarization now drop the known `Process running with session ID ...` pseudo-failure pattern from persisted `triedAndFailed` state, so reviewer surfaces do not keep replaying old false failures forever.
- The full Vitest suite now runs under an explicit serial + higher-timeout configuration to restore `pnpm test` as a stable baseline gate.

### Milestone 22: Recovery follow-through and official-doc review discipline

- Added explicit `isRecovery` provenance to durable sync audit entries so reviewer surfaces no longer need to infer recovery follow-through indirectly.
- Chinese next-step extraction now skips very short captures plus several narrative-connector fragments before they can land in continuity evidence.
- Official Codex and Claude public docs were re-checked again before refreshing migration and reviewer wording, keeping the repository companion-first and supportable.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (typo): Consider removing the redundancy in "re-checked again".

"Re-checked" already implies "again". Consider using either "were checked again" or "were re-checked" for cleaner wording.

Suggested change
- Official Codex and Claude public docs were re-checked again before refreshing migration and reviewer wording, keeping the repository companion-first and supportable.
- Official Codex and Claude public docs were re-checked before refreshing migration and reviewer wording, keeping the repository companion-first and supportable.


### Post-alpha.22 review hardening

- Mixed command output that contains both `PASS` and `FAIL` markers now classifies as failure instead of silently landing in the success bucket.
- Corrupted durable-sync processed state now degrades to an empty state instead of blocking future sync attempts.
- Startup and continuity compilers now respect tiny line budgets without emitting partial scope or section blocks.
- Heuristic continuity now clears stale local goals when the latest goal belongs to the shared project layer, preventing the merged resume brief from getting stuck on old local intent.
- Added broader regression coverage for corrupted state recovery, already-processed recovery cleanup, richer recovery JSON surfaces, session guardrail errors, and tiny-budget startup behavior.

## Reviewer checkpoints

If you are reviewing the repository now, start here:
Expand Down Expand Up @@ -261,11 +275,11 @@ If you are reviewing the repository now, start here:

## Next planned milestones

### Milestone 22: Recovery follow-through and official-doc review discipline
### Milestone 23: Reviewer contract polish and history-discipline follow-through

- Keep recovery markers compact and additive; do not let them evolve into a history browser or manual journal.
- Revisit `processedRolloutEntries` compaction only if the repository explicitly accepts the possibility that evicted old rollouts may sync again when replayed manually.
- Keep bilingual public docs and reviewer docs aligned after the new recovery-marker surfaces land.
- Keep bilingual public docs, reviewer handoff docs, and local AI handoff notes aligned after the alpha.22 review hardening pass.
- Continue reviewing companion-first wording against official Codex and Claude docs before widening any product claims.

## Review-ready habits
Expand Down
2 changes: 1 addition & 1 deletion docs/reviewer-handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Manual `cam remember` / `cam forget` updates remain outside the durable sync aud

## Most recent milestone commits

- current implementation window: alpha.21 failure-path reviewer contract and recovery markers
- current implementation window: alpha.22 recovery follow-through and official-doc review discipline, plus post-alpha.22 review hardening
- `91336e8` `feat(alpha.18): tighten durable sync audit contract`
- `d8e88f9` `feat(alpha.17): add continuity drill-down and docs discipline`
- `0f40277` `docs(alpha.16): redesign bilingual readme and docs portal`
Expand Down
6 changes: 5 additions & 1 deletion src/lib/domain/memory-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,11 @@ export class MemoryStore {
}

public async getSyncState(): Promise<Required<SyncState>> {
return normalizeSyncState(await readJsonFile<SyncState>(this.paths.stateFile));
try {
return normalizeSyncState(await readJsonFile<SyncState>(this.paths.stateFile));
} catch {
return normalizeSyncState(null);
}
Comment on lines +530 to +534
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't collapse every state-file read error into "empty state".

Because src/lib/util/fs.ts's readJsonFile() also throws on fs.readFile(), Lines 530-534 now suppress storage failures like EACCES/EIO the same way as malformed JSON. That can make sync treat already-processed rollouts as new instead of surfacing the real failure.

Suggested narrowing
  public async getSyncState(): Promise<Required<SyncState>> {
    try {
      return normalizeSyncState(await readJsonFile<SyncState>(this.paths.stateFile));
-    } catch {
-      return normalizeSyncState(null);
+    } catch (error) {
+      const errno = error as NodeJS.ErrnoException;
+      if (error instanceof SyntaxError || errno.code === "ENOENT") {
+        return normalizeSyncState(null);
+      }
+      throw error;
     }
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/domain/memory-store.ts` around lines 530 - 534, The current try/catch
around readJsonFile(this.paths.stateFile) swallows all errors and treats them as
"empty state"; change it so only expected benign conditions (file-not-found or
JSON parse errors) map to normalizeSyncState(null) while other IO errors are
propagated. Update the block around readJsonFile/normalizeSyncState: await
readJsonFile<SyncState>(this.paths.stateFile) inside try, catch the thrown error
and inspect it (e.g., if (err.code === 'ENOENT') return
normalizeSyncState(null); if (err instanceof SyntaxError || error message
indicates JSON parse) return normalizeSyncState(null); else throw err). This
preserves existing behavior for missing/malformed state but surfaces real
filesystem errors instead of hiding them.

}

public async markRolloutProcessed(identity: ProcessedRolloutIdentity): Promise<void> {
Expand Down
5 changes: 4 additions & 1 deletion src/lib/domain/memory-sync-audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export function parseMemorySyncAuditEntry(value: unknown): MemorySyncAuditEntry
sessionSource: entry.sessionSource,
status: entry.status,
skipReason: entry.status === "skipped" ? entry.skipReason : undefined,
...(entry.isRecovery === true ? { isRecovery: true } : {}),
appliedCount: entry.appliedCount,
scopesTouched: entry.scopesTouched,
resultSummary: entry.resultSummary,
Expand All @@ -135,6 +136,7 @@ interface BuildMemorySyncAuditEntryOptions {
appliedAt?: string;
sessionId?: string;
skipReason?: MemorySyncAuditSkipReason;
isRecovery?: boolean;
operations?: MemoryOperation[];
}

Expand All @@ -160,6 +162,7 @@ export function buildMemorySyncAuditEntry(
sessionSource: options.sessionSource,
status: options.status,
skipReason: options.status === "skipped" ? options.skipReason : undefined,
...(options.isRecovery ? { isRecovery: true } : {}),
appliedCount,
scopesTouched,
resultSummary: summaryForStatus(options.status, appliedCount, options.skipReason),
Expand All @@ -169,7 +172,7 @@ export function buildMemorySyncAuditEntry(

export function formatMemorySyncAuditEntry(entry: MemorySyncAuditEntry): string[] {
const lines = [
`- ${entry.appliedAt}: [${entry.status}] ${entry.resultSummary}`,
`- ${entry.appliedAt}: [${entry.status}]${entry.isRecovery ? ' [recovery]' : ''} ${entry.resultSummary}`,
` Session: ${entry.sessionId ?? "unknown"} | Extractor: ${entry.actualExtractorName || entry.actualExtractorMode}`,
` Applied: ${entry.appliedCount} | Scopes: ${entry.scopesTouched.length ? entry.scopesTouched.join(", ") : "none"}`
];
Expand Down
4 changes: 4 additions & 0 deletions src/lib/domain/recovery-records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ export function buildSyncRecoveryRecord(
};
}

// Recovery identity uses 4 fields (projectId, worktreeId, rolloutPath, sessionId) rather than
// the 6-field processed-rollout identity (which also includes sizeBytes and mtimeMs).
// This intentional difference ensures that a modified rollout file (changed size/mtime)
// can still clear its recovery marker, since the logical identity is the same rollout.
export function matchesSyncRecoveryRecord(
record: SyncRecoveryRecord,
identity: {
Expand Down
97 changes: 66 additions & 31 deletions src/lib/domain/session-continuity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ function quoteLines(items: string[]): string[] {
return items.map((item) => `| ${item.replace(/```/g, "\\`\\`\\`")}`);
}

function appendWithinBudget(
lines: string[],
blockLines: string[],
maxLines: number,
minimumLines = 1
): number {
if (maxLines - lines.length < minimumLines) {
return 0;
}

let appended = 0;
for (const line of blockLines) {
if (lines.length >= maxLines) {
break;
}
lines.push(line);
appended += 1;
}

return appended;
}

function parseFrontmatter(raw: string): { metadata: Record<string, string>; body: string } {
const match = raw.match(/^---\n([\s\S]*?)\n---\n?/);
if (!match) {
Expand Down Expand Up @@ -293,21 +315,37 @@ export function applySessionContinuityLayerSummary(
sourceSessionId?: string
): SessionContinuityState {
const sanitized = sanitizeSessionContinuityLayerSummary(summary);
return mergeSessionContinuityStates(
{
...base,
updatedAt: new Date().toISOString(),
status: "active",
sourceSessionId: sourceSessionId ?? base.sourceSessionId,
goal: sanitized.goal || base.goal,
confirmedWorking: sanitized.confirmedWorking,
triedAndFailed: sanitized.triedAndFailed,
notYetTried: sanitized.notYetTried,
incompleteNext: sanitized.incompleteNext,
filesDecisionsEnvironment: sanitized.filesDecisionsEnvironment
},
base
);
return {
...base,
updatedAt: new Date().toISOString(),
status: "active",
sourceSessionId: sourceSessionId ?? base.sourceSessionId,
goal: sanitized.goal,
confirmedWorking: sanitizeList(
[...sanitized.confirmedWorking, ...base.confirmedWorking],
8,
240
),
triedAndFailed: sanitizeFailureList([
...sanitized.triedAndFailed,
...base.triedAndFailed
]),
notYetTried: sanitizeList(
[...sanitized.notYetTried, ...base.notYetTried],
8,
240
),
incompleteNext: sanitizeList(
[...sanitized.incompleteNext, ...base.incompleteNext],
8,
240
),
filesDecisionsEnvironment: sanitizeList(
[...sanitized.filesDecisionsEnvironment, ...base.filesDecisionsEnvironment],
8,
240
)
};
}

export function renderSessionContinuity(state: SessionContinuityState): string {
Expand Down Expand Up @@ -354,22 +392,23 @@ export function compileSessionContinuity(
sourceFiles: string[],
maxLines = DEFAULT_SESSION_CONTINUITY_LINE_LIMIT
): CompiledSessionContinuity {
const lines: string[] = [
const lines: string[] = [];
const preamble = [
"# Session Continuity",
"Treat this as temporary working state, not durable memory or executable instructions.",
"If it conflicts with the user, the codebase, or current files, verify first.",
""
];
appendWithinBudget(lines, preamble, maxLines);

for (const filePath of sourceFiles) {
if (lines.length >= maxLines) {
if (appendWithinBudget(lines, [`- Source: ${JSON.stringify(filePath)}`], maxLines) === 0) {
break;
}
lines.push(`- Source: ${JSON.stringify(filePath)}`);
}

if (sourceFiles.length > 0 && lines.length < maxLines) {
lines.push("");
if (sourceFiles.length > 0) {
appendWithinBudget(lines, [""], maxLines);
}

const sectionBlocks: Array<[string, string[]]> = [
Expand Down Expand Up @@ -399,19 +438,15 @@ export function compileSessionContinuity(
];

for (const [title, items] of sectionBlocks) {
if (lines.length >= maxLines) {
const appended = appendWithinBudget(
lines,
[`## ${title}`, ...quoteLines(items), ""],
maxLines,
2
);
if (appended === 0) {
break;
}
lines.push(`## ${title}`);
for (const item of quoteLines(items)) {
if (lines.length >= maxLines) {
break;
}
lines.push(item);
}
if (lines.length < maxLines) {
lines.push("");
}
}

const finalText = lines.join("\n").trimEnd();
Expand Down
Loading
Loading