GoodMemory 0.6 to 0.7 Migration Guide

August 13, 2026 ยท View on GitHub

GoodMemory 0.7 is a clean breaking replacement of the former partial language adapter with one end-to-end LanguagePack contract. It does not provide an adapter compatibility layer, dual-write mode, or feature flag. Plan the upgrade as a coordinated code, configuration, and derived-projection change.

The 0.6 release remains the current rollback artifact until 0.7 passes its release gate. Its benchmark reports are historical 0.6 evidence; they do not establish 0.7 correctness or performance.

Before upgrading

  1. Pin the exact 0.6 application and GoodMemory versions.
  2. Stop GoodMemory writers for the scopes being upgraded, or use an application maintenance window that prevents a 0.6 and 0.7 process from sharing one store.
  3. Back up the canonical store and installed-host configuration. A completed 0.7 migration deletes old per-scope derived projections, so a downgrade after cutover requires this snapshot.
  4. Record custom language adapter/detector behavior and every locale passed by the application or installed host.
  5. Verify enough free storage for a second derived projection generation while a scope is being rebuilt.

Do not point 0.6 and 0.7 runtimes at the same writable store. There is no mixed version compatibility contract.

Replace language configuration

Remove the 0.6 language-adapter configuration and any adapterId assumptions. Configure the default and optional custom packs through language:

import { createGoodMemory } from "goodmemory";

const memory = createGoodMemory({
  language: {
    defaultLocale: "zh-TW",
    detection: "auto",
    packs: [productLanguagePack],
  },
});

LanguageConfig has this 0.7 shape:

interface LanguageConfig {
  defaultLocale?: string;
  detection?: "auto" | "default_only";
  detector?: LocaleDetector;
  detectorVersion?: string;
  packs?: readonly LanguagePack[];
}

The default locale is en-US. Explicit per-operation locale takes precedence. If the host knows the language, pass it rather than asking detection to guess:

await memory.remember({
  locale: "ja-JP",
  scope,
  messages,
});

await memory.recall({
  locale: "ja-JP",
  scope,
  query,
});

Installed Codex/Claude hosts should set the same default during setup or installation with --default-locale <locale>. Existing managed host config can be regenerated by the 0.7 installer; do not hand-merge obsolete language adapter fields into the generated block.

Port a custom language implementation

A custom pack must implement the whole LanguagePack interface, not only normalization or labels. Move all locale-specific detection, lexicons, tokenization, query/content signals, time expressions, entity behavior, candidate extraction, and rendering into the pack.

Registration rules are strict:

  • reuse a built-in id and the same locale claims to replace that built-in;
  • use a new id only for locales not already claimed by another effective pack;
  • declare non-empty, canonical defaultLocale, locales, apiVersion, analyzerVersion, and compatibilityGroup identity;
  • keep callbacks deterministic and free of mutable external state;
  • version a custom automatic detector with detectorVersion, otherwise the analyzer manifest is non-persistable and cannot prove a complete projection.

Any change to normalization, tokenization, search terms, detection, sentence boundaries, interrogative or behavioral-directive admission, temporal interpretation, or entity canonicalization requires an analyzerVersion bump and projection rebuild. LanguageContentAnalysis.behavioralDirective is an optional "none" | "one_off" | "durable" signal: all built-ins declare it, while an older custom pack may leave it undefined without an API-version shim.

The xx-Test conformance/integration pack is the reference acceptance shape: registering the pack alone must drive remember, projection, recall, and buildContext without adding locale branches to business modules.

Built-in language behavior changes

The built-ins are en, zh-Hans, zh-Hant, ja, ko, fr, and es.

  • zh-CN and zh-SG resolve to Simplified Chinese.
  • zh-TW, zh-HK, and zh-MO resolve to Traditional Chinese.
  • bare zh uses a configured Chinese default when present, otherwise zh-Hans.
  • Kana is a distinctive Japanese signal.
  • Hangul is a distinctive Korean signal.
  • French and Spanish auto-detection requires language-specific grammatical, diacritic, or punctuation evidence; unmarked Latin text uses the configured default.
  • unambiguous Hans/Hant characters may select the matching Chinese pack.
  • ambiguous Han-only text uses the configured default instead of guessing.
  • an unsupported explicit locale uses the neutral Unicode pack and does not inherit English query/content semantics.
  • ordinary questions and one-off behavioral directives abstain before every automatic producer; politeness or a pack-recognized imperative construction is not a durable cue;
  • automatic feedback requires both a behavioral directive and a pack-owned standing cue. Public feedback(), explicit memory directives, quoted/structured literals, and confirmed remember: "always" input retain their authority semantics.

The 0.7 guarantee is script-local: Simplified query to Simplified source and Traditional query to Traditional source. Simplified-to-Traditional and Traditional-to-Simplified lexical recall are explicitly not guaranteed. There is no OpenCC conversion, no handwritten conversion table, and no transformed canonical text. If cross-script retrieval is required, use an embedding channel or a future versioned buildSearchTerms implementation and rebuild the derived projections.

Migrate derived projections

0.7 uses a new projection generation:

Projection0.7 collection generation
recall documentsv4
entities / adjacencyv2
claims and claim statusv2
scope catalogv2

Each derived record carries searchText, searchLocale, languagePackId, searchAnalyzerVersion, and searchSchemaVersion. The scope catalog binds the projection/search versions, active analyzer manifest, and canonical source-generation proof.

Migration runs lazily on first recall or explicitly through maintenance:

await memory.runMaintenance({
  scope,
  jobs: ["projectionMigration"],
});

For each scope, 0.7 acquires the migration lock, rebuilds from canonical memory, validates source/claim/status/evidence/entity coverage, and atomically marks the new catalog complete. Until completion, recall does not read a partial new generation; it uses the canonical repository fallback. A failed or interrupted run is safe to repeat. After successful cutover, old projections and stale FTS rows for that scope are deleted.

Canonical memory and raw user text are not rewritten. Missing old locale provenance is repaired only for Kana or an unambiguous Hans/Hant signal; ambiguous Han-only records keep their existing/default locale identity.

Run the explicit job scope by scope before opening traffic when predictable first-request latency matters. A lazy migration is correct but may be slower than a warm, pre-cutover rebuild.

Application and output changes

  • Read locale, localeSource, languagePackId, and analyzer/language pack version provenance instead of language adapterId.
  • Keep JSON keys stable; only human-readable context, evidence, journal, fact/reference/episode, and installed-host text is localized.
  • Do not persist searchText as canonical content or build application logic around its exact token form. It is a versioned derived index value.
  • Do not copy language rules into recall, storage, policy, or host modules. Extend the pack and bump its analyzer identity.
  • Do not use routine remember/recall experiences as operation telemetry in 0.7.4. Read operation counts from the operation result or the optional redacted trace sink. Existing routine records are retained as audit history but are not raw behavioral exemplars.
  • Treat raw behavioral carryover as a typed tool_outcome path: it requires a safer alternative and an explicit stored retrieval profile equal to the current call. Missing or mismatched profiles fail closed, and an accepted carryover reports the exact experience through RuntimeKit recordRefs.
  • Treat a behavioral traceId as immutable provenance. Retrying the same payload is idempotent; reusing the id for a different payload fails with an identity conflict and does not replace evidence or experience records.
  • Validate every behavioral-outcome field as storage-safe before recording. Evidence and experience are committed as one outcome aggregate; a storage failure rejects the call instead of reporting recorded: true. Custom document stores that enable the behavioral-outcome recorder must implement the versioned ProjectionCapableDocumentStore atomic batch contract; an unversioned same-named method is rejected before recording.
  • policy.redact keeps its existing MemoryCandidate callback signature, but the runtime only applies content, explicitness, kindHint, and metadata. Candidate identity, source ownership, disposition, and durable-target admission remain owned by the remember pipeline.
  • Custom extractors should construct DurableOptOutTargetSelector.identities[] when one opt-out clause targets one or more typed fields. The singular identity field remains accepted for API-v1 custom packs but is deprecated; new code should use identities, or an empty array for exact-text fallback. Typed technical assignment values keep case, while natural profile/preference slots compare case-insensitively.
  • Treat LanguageService.getAnalyzerManifest().persistable === false as a prohibition on durable completeness proof, not as permission to trust the existing index.

Declare custom-storage deletion semantics

deleteAllMemory() now fails before deleting anything when custom document, session, or vector adapters are configured without an explicit terminal deletion contract. Custom storage must provide the document, session, and vector adapters as one complete bundle; mixing shared and runtime-local stores cannot support cross-runtime deletion. The declaration is a caller assertion: every cooperating GoodMemory runtime must point each of its document, session, and vector adapters at the same corresponding shared backend, and the shared projection-capable document-store namespace must own the persistent mutation intents and deletion barriers:

const memory = createGoodMemory({
  adapters: {
    documentStore,
    sessionStore,
    vectorStore,
    terminalDeletionSemantics: "shared-coordinated-backends-v1",
  },
});

The declaration cannot prove remote adapter identity and does not make uncoordinated external writers safe. Do not set it when any cooperating runtime uses a process-local session/vector store, points an adapter at a different backend, or writes directly without entering the same GoodMemory mutation protocol. A false declaration can return success while leaving data behind.

Deletion ownership and mutation intents do not expire. An operation failure persists a failed journal and keeps the scope closed; a hard process exit leaves the journal in deleting state. After correcting the cause, stop every possible old writer and deleter, then explicitly resume the same request:

await memory.deleteAllMemory({
  scope,
  includeRuntime: true,
  resumeInterrupted: {
    confirmPriorRuntimesStopped: true,
  },
});

Recovery atomically verifies the canonical scope, deletion contract, includeRuntime mode, lock/barrier generation, and interrupted mutation intents before it starts the idempotent deletion again. Its deleted counts cover the recovery attempt; records removed before the interruption are already absent. Never set the confirmation while an old runtime may still be alive, and never clear or replace a persisted owner merely because a timestamp is old: without generation fencing in every document, session, and vector mutation, a paused old owner could resume after takeover.

Verification before cutover

Run the release commands from the 0.7 source identity you will publish:

bun test
bun run typecheck
bun run test:coverage

Also require the package/release/type-surface suites, packed-package consumer smokes under Bun and Node 20, and a tarball smaller than 4 MiB. For every enabled backend, test search/update/delete/conditional batch/repair/restart and an interrupted/repeated projection migration.

PostgreSQL support for any non-English built-in pack is not accepted unless the same candidate runs against a real GOODMEMORY_TEST_POSTGRES_URL, including functional migration, scale, and EXPLAIN proof that the query uses the version-matched searchText GIN index. A skipped PostgreSQL suite is a missing gate, not a pass.

Check the multilingual scale gate with all seven built-in packs represented and verify its declared p95/materialization/query-count/index-use thresholds. Do not infer these results from the 0.6 English/Simplified-Chinese benchmark artifacts.

Cutover and rollback

Cut over only after all scopes required for the deployment have complete, version-matching projection proof and the 0.7 release gates are recorded. Start only 0.7 writers after cutover.

If failure occurs before a scope is complete, stop 0.7, correct the issue, and repeat migration; canonical memory has not changed. If failure occurs after cutover, prefer a forward fix and rebuild. To run 0.6 again, stop every 0.7 process and restore the pre-upgrade store and host-configuration snapshot. Do not reconstruct a downgrade from leftover derived collections and do not introduce an adapter shim as an emergency compatibility path.

Evidence boundary

The published 0.6 benchmark declarations, coverage report, package checksum, and release workflow remain historical evidence for that exact version and source identity. They must retain their 0.6 labels. A 0.7 release statement requires fresh 0.7 package, runtime, storage, migration, scale, and consumer evidence; old benchmark scores may be cited only as historical context unless the full protocol is rerun and bound to the 0.7 artifact.

See ADR-008 and the LanguagePack extension guide for the architecture and pack authoring contracts.