UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

721 lines • 52.7 kB
import type { EngineNotice } from "./engine-notice.js";
/**
 * A scored memory candidate returned by {@link MemoryStore.searchScored} (design/41). Carries a
 * **stable id** (for {@link MemoryStore.update}/{@link MemoryStore.delete}) and a similarity `score`.
 *
 * `score` is a **cosine distance ∈ [0, 2]** where **0 = identical** and larger = less similar — pinned
 * by contract so the consolidation band (`{lo, hi}`) is calibrated the same way across stores (a store
 * that returned a *similarity* instead of a *distance* would invert the band). The reference
 * {@link InMemoryMemoryStore} has no embeddings, so it returns a lexical distance in [0, 1] (a subset
 * of the range) as a stand-in for tests/dev; a production store returns a real cosine distance.
 */
export interface ScoredMemory {
    id: string;
    text: string;
    score: number;
    /** design/84 Seam B (前置 BLOCKER): true when this note was CREATED by a consolidation ADD (not the
     *  `remember` tool / a caller). Periodic/incremental consolidation EXCLUDES these from its candidate set so
     *  it never re-consolidates its own output (infinite re-merge). Stores that don't track it leave it absent
     *  (treated as not-generated — back-compat: the inline task-end path already excludes this-task notes by id). */
    consolidationGenerated?: boolean;
}
/**
 * The vector-ranking capability RUNG a {@link MemoryStore} actually achieves (design/81 Slice 5). It is a
 * runtime OBSERVABLE (it depends on whether an embedder was injected AND whether the backend has native vector
 * ops), NOT a static class trait — a deployment reads `store.vectorMode` to decide whether to build a native
 * index / for metrics, and to answer "does this backend do neural recall?".
 *  - `lexical`  — no embedder: `1 - Jaccard` everywhere (the default; byte-identical to the pre-Slice-5 floor).
 *  - `portable` — embedder + IN-PROCESS cosine over stored vectors (File jsonl / Pg jsonb / **MySQL JSON** —
 *                 any backend that can store a float array; O(n), no index). MySQL sits HERE, not "unsupported".
 *  - `native`   — embedder + a backend index/operator (pgvector `<=>` / TiDB `VEC_COSINE_DISTANCE`).
 */
export type MemoryVectorMode = "lexical" | "portable" | "native";
/**
 * Produces an embedding for one note/query (design/81 Slice 3 — the OPTIONAL vector seam). **Byte-identical
 * to the repo example `PgEmbedder` (src/examples/adapters/pg-adapter.ts — repo example only, not part of the
 * npm surface since 1.424.0)** so the same caller-injected embedder satisfies both the Pg and File backends. It is an OPTIONAL injection: a store WITHOUT one falls back to the lexical
 * cosine-distance (the TOC default — no model file, no native binary in core's default path). When one IS
 * injected the File backend uses it for vector ranking (Slice 5) via a rebuildable sidecar index; injecting
 * none keeps the lexical floor unchanged. Core bundles NO embedder; a cloud/TOB consumer injects its own.
 */
export interface Embedder {
    embed(text: string): Promise<number[]>;
    dimensions: number;
}
/**
 * Long-term (L2) memory backend — pluggable, scoped, durable across sessions.
 *
 * Design adapted (MIT) from CodeWhale's `memory.rs` (a persistent note file injected into the
 * system prompt) and Anthropic's memory tool (the model manages its own memory via a tool; the
 * backend lives in your infrastructure). The default is in-memory; a deployment provides a durable
 * backend (e.g. TiDB) implementing this interface.
 *
 * `scope` partitions memory by tenant — e.g. `user:42`, `org:7`, `agent:main`, or `global`.
 */
export interface MemoryStore {
    /** Full memory content for a scope (timestamped bullets), or null when empty. */
    read(scope: string): Promise<string | null> | string | null;
    /**
     * Append one durable note to a scope. Implementations timestamp it. A store that supports
     * consolidation (implements {@link searchScored}/{@link update}/{@link delete}) **MUST** return the new
     * note's **stable id**: memory consolidation (design/41) uses it to exclude a just-appended note from
     * its own reconcile candidates, so an id-addressable store that returns `void` would let a note
     * self-match and trigger a wasteful no-op every cycle. Returning `void` is fine only for stores that
     * do NOT support consolidation.
     */
    append(scope: string, note: string): Promise<string | void> | string | void;
    /** Clear a scope's memory. */
    clear(scope: string): Promise<void> | void;
    /**
     * Optional just-in-time (L3) retrieval: return notes in `scope` relevant to `query`.
     * When implemented, the Runner exposes a `recall` tool so the model can query memory on demand
     * instead of relying only on the injected block (useful when a scope's memory is large).
     */
    search?(scope: string, query: string, limit?: number): Promise<string[]> | string[];
    /**
     * Optional id-addressable similarity retrieval (design/41): return up to `limit` notes in `scope`
     * most similar to `query`, each with a **stable id** and a cosine-distance `score` (see
     * {@link ScoredMemory}). Required (together with {@link update}/{@link delete}) for memory
     * consolidation; when any of the three is absent, consolidation is a safe no-op.
     *
     * **Concurrency contract:** memory consolidation keys on the task's memory `scope`, NOT its session,
     * and the Runner's per-session lock does not serialize across sessions. So `searchScored`/`update`/
     * `delete` MAY be called concurrently for the SAME scope from different sessions. A durable async store
     * must therefore either linearize per-scope writes (e.g. optimistic lock by id) or accept eventual
     * consistency — at worst a near-duplicate survives one extra consolidation pass (fail-open, no loss).
     */
    searchScored?(scope: string, query: string, limit?: number): Promise<ScoredMemory[]> | ScoredMemory[];
    /** The vector-ranking rung this store achieves (design/81 Slice 5). Absent ⇒ treated as `"lexical"`
     *  (back-compat). A runtime observable — a deployment reads it to decide index/metrics, NOT to decide
     *  whether to inject an embedder (injection is config-driven; this is the RESULT). See {@link MemoryVectorMode}. */
    readonly vectorMode?: MemoryVectorMode;
    /** Optional: replace a note's text by id (consolidation UPDATE). The id stays stable. Must be safe
     *  under concurrent same-scope calls — see {@link searchScored}'s concurrency contract. */
    update?(scope: string, id: string, text: string): Promise<void> | void;
    /** Optional: remove a note by id (consolidation DELETE). The id is never reused afterwards. Must be
     *  safe under concurrent same-scope calls — see {@link searchScored}'s concurrency contract. */
    delete?(scope: string, id: string): Promise<void> | void;
    /**
     * Optional **manifest** for selective recall (design/65 §2.2/§8#6): a per-note HEADER (id + a short
     * description + mtime [+ name/type when structured]) — **NOT the body** (mirrors CC reading only the
     * frontmatter, `memoryScan.ts FRONTMATTER_MAX_LINES=30`). The Runner injects this index for a large
     * scope and side-queries which ids are relevant, then fetches only those via {@link getByIds}.
     */
    listStructuredNotes?(scope: string): Promise<MemoryNoteHeader[]> | MemoryNoteHeader[];
    /** Optional: fetch the FULL records (with body) for the selected ids (design/65 §2.2). Unknown ids are
     *  silently skipped. Order is not guaranteed. Pairs with {@link listStructuredNotes}. */
    getByIds?(scope: string, ids: string[]): Promise<MemoryNoteRecord[]> | MemoryNoteRecord[];
    /**
     * Optional: append a **structured/typed** note (design/65 §2.1, P2). `type` defaults to `"project"` and
     * `description` to the body's first sentence (derived at write time — store responsibility, §8#6). Returns
     * the new note's stable id (like {@link append}). A store that supports this surfaces `name`/`type` in
     * {@link listStructuredNotes} so the selective-recall side-query gets a `[type]` relevance signal.
     */
    appendStructured?(scope: string, note: StructuredNoteInput): Promise<string> | string;
    /**
     * Optional **legitimate door** for a *promotable* artifact write (design/77 §2 Gate-2). A promotable
     * note (a self-evolution artifact — skill/playbook/outcome-stats; classified by NOTE STRUCTURE, not a
     * caller flag, via {@link classifyPromotable}) may ONLY reach the backend through this method, which
     * carries the required `utilityVerdict`. The store-boundary gate ({@link guardedMemoryStore}) routes a
     * present+positive promotable write here; a promotable note WITHOUT a present+positive verdict is
     * fail-closed REJECTED at the boundary (never reaches any store method). `utilityVerdict` is opaque
     * `unknown` in core — the *definition* of "positive" stays in the profile (taxonomy never enters core).
     * Stores that do not implement this can still serve caller-authored writes; promotable writes simply
     * have nowhere legitimate to land and are rejected (fail-closed).
     */
    appendPromotable?(scope: string, note: StructuredNoteInput, utilityVerdict: unknown): Promise<string> | string;
    /**
     * **INTERNAL consolidation-only door** for the design/84 Seam B marker. Memory consolidation (and ONLY
     * it — `runMemoryConsolidation`) calls this to materialize a merged "add" decision tagged
     * `consolidationGenerated:true`, so a later periodic/incremental pass EXCLUDES its own output from the
     * candidate set (no infinite re-merge). It is the COUNTERPART to the {@link guardedMemoryStore} boundary
     * defense: the public {@link appendStructured} STRIPS a caller-supplied `consolidationGenerated` (an
     * external caller must never be able to mark a note "consolidation-generated" and silently exile it from
     * consolidation forever), while THIS method is the single legitimate inlet that may set it. The guarded
     * store wires it to set the marker then forward to the inner `appendStructured`; a raw (un-wrapped) store
     * need not implement it — consolidation then falls back to `appendStructured` (which on an un-wrapped
     * store still honors the marker, preserving the old behavior for direct-store tests). NOT a public caller
     * surface: it carries no extra capability beyond `appendStructured` + the one internal flag.
     */
    appendConsolidationGenerated?(scope: string, note: StructuredNoteInput): Promise<string> | string;
    /**
     * design/84 Seam B — read the periodic/incremental consolidation cursor for `scope` (decision 3). The
     * cursor is an **opaque ordering marker** the STORE defines (this core never inspects it): a value such
     * that every note whose ordering marker is `<= cursor` has ALREADY been folded into a prior consolidation
     * pass and MUST NOT be re-fed (no infinite re-merge), while every note `> cursor` is still pending. In the
     * reference stores it is a note **id** (uuidv7 — lexicographically time-sortable), so "after the cursor"
     * is a simple string `>` comparison. Returns `undefined` when no pass has run yet (⇒ the WHOLE scope is
     * pending — the first pass sweeps it all). PAIR with {@link setConsolidationCursor}: a store implements
     * BOTH or NEITHER ({@link supportsPeriodicConsolidation}); a store with neither makes {@link consolidateScope}
     * a safe NO-OP (it never silently degrades to a full re-consolidation).
     */
    getConsolidationCursor?(scope: string): Promise<string | undefined> | string | undefined;
    /**
     * design/84 Seam B — persist the periodic/incremental consolidation cursor for `scope` (decision 3). Called
     * by {@link consolidateScope} ONLY after a pass succeeds in full, advancing the cursor to the max ordering
     * marker the pass observed (seen note ids ∪ added ids). Must be durable (a later pass reads it back). PAIR
     * with {@link getConsolidationCursor}.
     */
    setConsolidationCursor?(scope: string, cursor: string): Promise<void> | void;
}
/** Standard structured-note types (design/65 §2.1, CC `memoryTypes.ts`). Any other string is accepted and
 *  degrades gracefully (stored + shown as-is) — `type` is widened to `string` everywhere for that reason. */
export type MemoryNoteType = "user" | "feedback" | "project" | "reference";
/** Input to {@link MemoryStore.appendStructured}. Only `body` is required. */
export interface StructuredNoteInput {
    body: string;
    /** A short slug for `[[name]]` cross-references (design/65 §2.1). Optional. */
    name?: string;
    /** One of {@link MemoryNoteType}, or any string (unknown types degrade). Default `"project"`. */
    type?: string;
    /** One-line summary for the manifest. Default = first sentence of `body`. */
    description?: string;
    /** `[[name]]` cross-reference slugs (design/65 §2.1; v1 stores/displays, no link-following). */
    links?: string[];
    /**
     * design/77 §2 Gate-2 structural marker. An opaque (`unknown`) outcome-statistics blob attached to a
     * self-evolution artifact (held-out pass-rate, usage count, …). Its PRESENCE makes a note *promotable*
     * even when `type` is in the caller-authored closed set — the structural marker WINS over the type
     * (see {@link classifyPromotable}). Core never inspects its shape; the taxonomy/threshold stays in the
     * profile (the `utilityGate` a deployment threads into {@link guardedMemoryStore}, e.g. via
     * `FileStorageBackendOptions.utilityGate`).
     */
    outcomeStats?: unknown;
    /**
     * design/84 Seam B (前置 BLOCKER): mark a note as CREATED BY A CONSOLIDATION ADD. Set ONLY by
     * {@link import("./runner/memory-consolidation.js").runMemoryConsolidation} when it materializes a merged
     * "add" decision — never by the `remember` tool or a normal caller. A store persists it (and surfaces it on
     * {@link ScoredMemory}/{@link MemoryNoteHeader}) so periodic/incremental consolidation can EXCLUDE these
     * notes from its candidate set and not re-consolidate its own output forever. It does NOT affect the
     * promotable/secret gates (a consolidation ADD is `caller_authored` by construction).
     */
    consolidationGenerated?: boolean;
}
/** A manifest entry: enough to judge relevance, never the body (design/65 §2.2). */
export interface MemoryNoteHeader {
    /** Store contract: ids must be SINGLE-LINE whitespace-free tokens (uuid-like). The manifest renders
     *  `- {id}: …` lines and the side-query matches selected ids by string equality — an id containing
     *  a newline could self-declare manifest lines, and a renderer-side rewrite would break the equality match,
     *  so the contract sits on the store (search 1.95.1 review #2). */
    id: string;
    /** A one-line summary used as the relevance signal (derived from the note's first sentence when untyped). */
    description: string;
    /** Append/update time as ms since epoch — drives the freshness caveat (design/65 §8#8).
     *  ⚠️ Always read together with {@link timestampMissing}: when that flag is set this slot is a `0`
     *  SHELL, not a measurement. Never infer "unknown" from the value — `0` is a legal real timestamp. */
    mtimeMs: number;
    /**
     * Set when the note carries NO usable write time, so {@link mtimeMs} above is an unknown-shell rather
     * than a measurement (the same "unknown ≠ zero" shape as the brain layer's `usageMissing`).
     *
     * Minted only by {@link parseNoteTimestamp}, which produces the pair together — see its note for why a
     * bare `0` was a silent recall loss. A consumer that ORDERS or AGES notes must branch on this flag:
     * an unknown time is INCOMPARABLE (neither the oldest nor the newest note in the scope), and it must
     * never be rendered as an age. It does NOT remove the note from recall — the note stays in the
     * manifest, stays searchable, and stays selectable; only the recency axis abstains.
     */
    timestampMissing?: true;
    /** Present once notes are structured (design/65 §2.1, P2); absent for legacy flat notes. */
    name?: string;
    /** One of {@link MemoryNoteType} or any other string (unknown degrades gracefully). */
    type?: string;
    /** design/84 Seam B: true when this note was created by a consolidation ADD (excluded from periodic
     *  consolidation candidates). Absent for `remember`/caller notes. */
    consolidationGenerated?: boolean;
}
/** A full note record (header + body) returned by {@link MemoryStore.getByIds}. */
export interface MemoryNoteRecord extends MemoryNoteHeader {
    text: string;
}
/** A store that has passed {@link supportsConsolidation} — the id-addressable trio is present. */
export type ConsolidationCapableStore = MemoryStore & Required<Pick<MemoryStore, "searchScored" | "update" | "delete">>;
/**
 * True when a store implements the full id-addressable trio required for memory consolidation
 * (design/41): {@link MemoryStore.searchScored}/{@link MemoryStore.update}/{@link MemoryStore.delete}.
 * When false, the Runner skips consolidation entirely (graceful no-op).
 *
 * A TYPE PREDICATE, so the guard's conclusion travels to the call sites. A bare boolean left
 * every post-guard call site writing `store.update!(…)`, and a guard that later forgot one of the three
 * would have been invisible — the `!`s keep compiling. Now dropping a method from the check turns the
 * call sites red.
 */
export declare function supportsConsolidation(store: MemoryStore): store is ConsolidationCapableStore;
/** A store that has passed {@link supportsPeriodicConsolidation} — the Seam B cursor pair is present. */
export type PeriodicConsolidationCapableStore = MemoryStore & Required<Pick<MemoryStore, "getConsolidationCursor" | "setConsolidationCursor">>;
/**
 * True when a store implements the design/84 Seam B cursor PAIR
 * ({@link MemoryStore.getConsolidationCursor}/{@link MemoryStore.setConsolidationCursor}). Periodic/incremental
 * consolidation ({@link import("./consolidate-scope.js").consolidateScope}) GATES on this: a store missing
 * either method makes a periodic pass a safe **NO-OP** (never a silent full re-consolidation of the whole
 * scope, which would re-merge already-consolidated notes forever). Both-or-neither — a store implementing
 * only one is treated as unsupported. Independent of {@link supportsConsolidation} (the id-addressable trio),
 * which a periodic pass ALSO needs to actually mutate.
 */
export declare function supportsPeriodicConsolidation(store: MemoryStore): store is PeriodicConsolidationCapableStore;
/** The closed set of caller-authored note types (design/65 {@link MemoryNoteType}, memory.ts). A note
 *  whose `type` is in this set is caller-authored; a `type` *outside* it (e.g. `"skill"`) is promotable.
 *  Exported so a caller-authored inlet (e.g. consolidation ADD) can NORMALIZE a model-suggested type to
 *  the closed set before the store boundary — a legitimate caller_authored write must never trip the
 *  promotable gate just because the model invented a non-standard `type` slug (MINOR-3). */
export declare const CALLER_AUTHORED_TYPES: ReadonlySet<string>;
/** The default caller-authored note type (matches {@link StructuredNoteInput.type}'s documented default). */
export declare const DEFAULT_CALLER_AUTHORED_TYPE: MemoryNoteType;
/**
 * Profile-injected predicate that judges whether a promotable note's `outcomeStats` clear the promotion bar
 * (design/77 §2). Core only guarantees a verdict is PRESENT for a promotable write; this predicate — wired
 * by the deployment — receives the note's (opaque) `outcomeStats` blob and returns whether it clears the
 * bar. (It is NOT handed the `utilityVerdict` token; the verdict's PRESENCE is checked separately by
 * {@link enforcePromotableWriteGate}, then the gate judges the outcome statistics.) When UNWIRED, a
 * promotable write fails closed.
 */
export type UtilityGate = (stats: unknown) => boolean;
/** Typed, fail-closed error thrown when a promotable artifact write reaches the store boundary without a
 *  present-and-positive `utilityVerdict` (design/77 §2). The `code` is stable for callers to switch on;
 *  the write NEVER reaches the backend. */
export declare class MemoryGateError extends Error {
    readonly code: string;
    constructor(code: string, detail?: string);
}
/**
 * The same SHAPE as {@link MemoryGateError} — a stable `code` a caller switches on — for the opposite
 * PHASE of a memory write, and deliberately NOT that class: a gate error means the write was REFUSED
 * and never reached the backend, so a host branching on it would take exactly the wrong action here.
 *
 * @contract memory.projection_failure_is_post_durable — a store throws this ONLY after the note's own
 * event is durably appended and fsync'd, so `committed` is always true and the note survives a restart:
 * a caller that retries the write on this error stores the note a second time. Only the derived
 * projection is missing, and the next mutation of the same scope rewrites it whole.
 */
export declare class MemoryProjectionError extends Error {
    /** Stable, switchable — the phase this failure belongs to. */
    readonly code = "memory.projection_failed";
    /** Always true: this error only exists AFTER the durable append was fsync'd. Never retry the write. */
    readonly committed = true;
    /** The projection file that could not be rewritten. */
    readonly path: string;
    constructor(path: string, cause: unknown);
}
/**
 * Scan `text` for a high-confidence credential (CC-parity P0-2). Returns the matched pattern's stable
 * `label` (NEVER the matched secret value — labels are safe to log/surface), or `null` when clean.
 * Deliberately CONSERVATIVE: only provider-prefixed keys + PEM private-key headers match, so ordinary
 * prose that merely mentions tokens/keys/passwords passes through untouched (false-positives are the head
 * risk — a wrongly-rejected normal note silently breaks legitimate memory).
 *
 * 🔴 RB-445: this is a BEST-EFFORT tripwire for a handful of high-signal token
 * formats, NOT a data-loss-prevention boundary — "the write gate passed" never means "the text contains
 * no credential". Known-through shapes (deliberate, per the false-positive stance above): connection
 * strings carrying passwords, `password=`/`Authorization:` header values, bare high-entropy bodies
 * (40-char keys, 64-hex — indistinguishable from git SHAs/digests in engineering notes), and structured
 * tokens without a vendor prefix. A deployment needing a real DLP boundary must put its OWN scanner in
 * front of the store; widening candidates are evaluated per-format in the review backlog (RB-445).
 */
export declare function detectSecret(text: string): string | null;
/**
 * Store-boundary secret-scan invariant (CC-parity P0-2). Fail-closed: if `body` carries a high-confidence
 * credential, throw {@link MemoryGateError}`("memory.secret_detected")` — the write NEVER reaches the
 * backend. The error detail names the pattern LABEL (e.g. `aws_access_key_id`), never the matched value,
 * so the secret is not re-leaked into a log/error sink. A clean body is a no-op. Applied by
 * {@link guardedMemoryStore} on EVERY durable write inlet (`append`, `appendStructured`, the
 * `appendPromotable` door) so both the `remember` tool and consolidation ADD are covered at one chokepoint.
 */
export declare function enforceSecretWriteGate(body: string): void;
/**
 * Structured-note secret-scan invariant (CC-parity P0-2). Scans EVERY text field of a {@link StructuredNoteInput}
 * that becomes DURABLE and/or enters the manifest — not just `body`. The body alone is insufficient: `name`
 * (rendered as `[[name]]` cross-references) and `description` (rendered into the recall manifest + injected
 * into the side-query sub-prompt) are ALSO persisted and surfaced, so a secret hidden there
 * (`appendStructured({ body: "safe", description: "sk_live_…" })`) would otherwise be durably stored and
 * leaked into every later same-scope manifest — a real circumvention of the body-only scan. `links` slugs are NOT
 * scanned: they are short `[[name]]`-style reference tokens (no credential body shape clears the
 * conservative detector), and scanning them adds FP surface for no realistic gain. Fail-closed: the first
 * field that carries a high-confidence credential throws {@link MemoryGateError}`("memory.secret_detected")`.
 */
export declare function enforceStructuredNoteSecretGate(note: StructuredNoteInput): void;
/**
 * Classify a structured-note write as `caller_authored` (normal memory — ungated) or `promotable` (a
 * self-evolution artifact — gated) — design/77 §2. PURE, taxonomy-free CORE logic.
 *
 * `promotable` iff the note's `type` is defined AND outside the caller-authored closed set, OR the note
 * carries a structural promotable marker. **The structural marker WINS over the closed-set type** — a
 * `type:"reference"` note WITH an `outcomeStats` marker classifies `promotable`.
 */
export declare function classifyPromotable(note: StructuredNoteInput): "caller_authored" | "promotable";
/**
 * The store-boundary Gate-2 check (design/77 §2). For a `promotable` note, require a present-and-positive
 * `utilityVerdict`; otherwise throw {@link MemoryGateError}`("memory.promotable_ungated")` fail-closed —
 * the write NEVER reaches the backend. A `caller_authored` note is unchanged (no verdict needed).
 *
 * **Gate-presence invariant (CRITICAL):** "positive" is decided by the profile `utilityGate`. When the
 * gate is UNWIRED, a promotable note STILL fails closed (an absent gate never auto-passes). When wired,
 * the verdict must be present (`!== undefined`) and the gate must accept its `outcomeStats`.
 */
export declare function enforcePromotableWriteGate(note: StructuredNoteInput, utilityVerdict: unknown, utilityGate?: UtilityGate): void;
/**
 * Wrap a {@link MemoryStore} so EVERY durable write path runs the Gate-2 chokepoint (design/77 §2). One
 * wrapper at the store boundary covers BOTH inlets (the `remember` tool and consolidation ADD) — no
 * per-inlet edits. It intercepts BOTH write methods (§2 polish):
 *  - `appendStructured` — classify the structured note; a `promotable` one needs a present+positive verdict.
 *    Since `appendStructured` carries NO verdict, a promotable structured write is ALWAYS rejected here;
 *    the legitimate way to land one is {@link MemoryStore.appendPromotable} (which carries the verdict).
 *  - `append` — the legacy/typeless fallback CANNOT carry structure, so any append-path write is
 *    classified `caller_authored` and passes through unchanged (documents the chokepoint's exhaustiveness
 *    across store methods — a promotable artifact structurally cannot flow out of this path).
 *
 * `appendPromotable` is the gated legitimate door: it runs the gate WITH the supplied verdict, then (on
 * pass) writes via the inner store's `appendStructured` (or `append` fallback). The id-addressed `update`
 * (consolidation's UPDATE op) replaces a note's text with new model-generated content, so it is ALSO
 * secret-scanned (a promotable artifact structurally cannot flow out of it — only existing-note text is
 * rewritten). `delete` and the read/search/manifest surface are straight pass-throughs (they carry no new
 * content and never WRITE a promotable artifact).
 */
export declare function guardedMemoryStore(inner: MemoryStore, utilityGate?: UtilityGate): MemoryStore;
/**
 * Default in-memory MemoryStore (lost on restart). Use a durable backend in production.
 *
 * Id-addressable (design/41): each note gets a UUID stable across `update` and never reused after
 * `delete`, so {@link searchScored}/{@link update}/{@link delete} work and memory consolidation can run.
 * It has no embeddings, so `searchScored` returns a **lexical** (Jaccard) distance as a stand-in for a
 * real cosine distance — fine for dev/tests, but a production store should back this with vectors.
 */
export declare class InMemoryMemoryStore implements MemoryStore {
    private byScope;
    /** design/84 Seam B: per-scope periodic-consolidation cursor (lost on restart, like the rest of this store). */
    private cursorByScope;
    /** design/81 Slice 5: the in-memory store has no embedder — always the lexical floor. */
    readonly vectorMode: "lexical";
    read(scope: string): string | null;
    append(scope: string, note: string): string | void;
    appendStructured(scope: string, note: StructuredNoteInput): string;
    clear(scope: string): void;
    /**
     * Keyword search backing the `recall` tool. design/81 Slice 2b — uses the SHARED synonym/stem-aware
     * {@link lexicalSearchMatch} (the SAME matcher as {@link import("../stores/file/memory-store.js").FileMemoryStore.search}),
     * so the `recall` tool returns IDENTICAL results across the InMemory and File backends. With no synonym/stem
     * hit it degrades to the plain substring filter (byte-identical to the pre-Slice-2b behavior).
     */
    search(scope: string, query: string, limit?: number): string[];
    /**
     * Lexical-distance retrieval (consolidation candidate source). Distance = `1 - Jaccard(terms)` so it
     * lives in [0, 1] ⊂ the [0, 2] cosine-distance contract (0 = identical term set). Returns the top
     * `limit` nearest entries (excludes zero-overlap ones). Deterministic — handy for tests.
     */
    searchScored(scope: string, query: string, limit?: number): ScoredMemory[];
    update(scope: string, id: string, text: string): void;
    delete(scope: string, id: string): void;
    /** Manifest (design/65 §2.2): a header per note, body excluded. description = the note's first sentence
     *  (legacy flat notes have no frontmatter; this is the council #10 P1 fallback). */
    listStructuredNotes(scope: string): MemoryNoteHeader[];
    /** design/84 Seam B: read the periodic-consolidation cursor (opaque note-id high-water mark). */
    getConsolidationCursor(scope: string): string | undefined;
    /** design/84 Seam B: persist the periodic-consolidation cursor. */
    setConsolidationCursor(scope: string, cursor: string): void;
    getByIds(scope: string, ids: string[]): MemoryNoteRecord[];
}
/**
 * design/81 Slice 2b — a ZERO-MODEL lexical term expander closing the only real lexical recall weakness the
 * defect-hunt raised (the `recall`-tool `search` misses paraphrases like "auth token" vs "credentials/bearer").
 * Pure TS, NO embedder/native/model dep — same posture as the Jaccard stand-in. For a lower-cased query
 * term it returns that term PLUS:
 *  - its members of a small bidirectional **synonym set** (auth↔credentials↔bearer, token↔key↔secret, …),
 *    looked up for BOTH the original term AND its plural stem (so an inflected query like "tokens" — whose
 *    singular "token" is the synonym-group member — still expands to {bearer, apikey, secret, key}), and
 *  - a light **plural** stem (`-ies→-y`, `-es`, `-s`, with a ≥3-char guard) so "tokens" reaches "token".
 *    (The `-ing`/`-ed` rules were removed — they over-truncated and produced wrong stems.)
 * Always includes the original term, so a non-expandable term degrades to the existing substring match
 * (byte-identical to the prior behavior when no synonym/stem applies). The caller (`lexicalSearchMatch`)
 * treats the returned variants as an OR within one query term (still AND across terms).
 */
export declare function expandLexicalTerms(term: string): string[];
/**
 * Shared SYNONYM/STEM-aware lexical match (design/81 Slice 2b) — the ONE matcher used by BOTH
 * {@link InMemoryMemoryStore.search} and {@link import("../stores/file/memory-store.js").FileMemoryStore.search},
 * so the `recall` tool returns IDENTICAL results across backends (the "byte-identical to InMemory" contract).
 * It is a recall-QUALITY behavior, correct to apply everywhere. (Pg's `search` is a separate SQL path —
 * a known follow-on, unchanged in this pass.)
 *
 * Matching rule: tokenize `text` on word boundaries, then for each whitespace-separated `query` term the
 * text must match AT LEAST ONE of that term's {@link expandLexicalTerms} variants (OR within a term), and
 * EVERY query term must match (AND across terms). The ORIGINAL query term keeps its pre-Slice-2b SUBSTRING
 * semantics (back-compat); the EXPANDED synonym/stem variants are matched on WORD BOUNDARIES (set membership
 * over the text's token set) so a short injected synonym like "key"/"db" cannot substring-match an unrelated
 * word ("keyboard"/"double"). An empty/whitespace-only query never matches.
 */
export declare function lexicalSearchMatch(query: string, text: string): boolean;
/** First sentence (or ~120-char head) of a note — the manifest description fallback for untyped notes. */
export declare function firstSentence(text: string): string;
/**
 * Parse a note's `"YYYY-MM-DD HH:MM"` (UTC) stamp into the {@link MemoryNoteHeader} timestamp PAIR —
 * the ONE mint point for `mtimeMs`/`timestampMissing`, shared by the in-memory and file stores.
 *
 * **Unknown is not 1970.** This used to return a bare `0` for anything unparseable, and every consumer
 * then read that `0` as a real epoch timestamp. A note whose stamp was absent, blank, or written in a
 * foreign format (a full ISO-8601 `2026-08-04T12:30:00Z` from a third-party producer survives JSON
 * replay intact, but the `" " → "T"` rewrite below turns it into an unparseable `…T12:30:00Z:00Z`)
 * therefore became the OLDEST note in its scope forever: it lost every `[[name]]` collision to every
 * stamped note, and the recall renderer told the model it was "written 678 months ago — verify it's
 * still current". Both are silent recall losses caused by the sentinel, not by the note.
 *
 * The unknown case is now an EXPLICIT axis (same shape as the brain layer's `usageMissing`): the numeric
 * slot stays a `0` shell and `timestampMissing: true` states that the shell is not a measurement. The two
 * fields are minted TOGETHER here and callers spread the result, so it is not constructible to publish
 * the shell without its flag. Consumers branch on the flag and never on the value — `mtimeMs === 0` is a
 * legal timestamp (a note genuinely stamped `1970-01-01 00:00` parses fine and is NOT flagged).
 *
 * **No persisted format ripple.** What the stores durably hold is the `ts` STRING, never this parsed
 * number, so there is no stored `0` whose meaning has to be re-decided: unknown-ness is re-derived on
 * every read from whether the string parses. The one genuinely ambiguous stored value is the literal
 * string `"1970-01-01 00:00"`. It is treated as a real time, and that is the honest call rather than a
 * convenient one — neither store has ever WRITTEN that string (both stamp `new Date()` at append/update),
 * so it can only come from a foreign producer, and a foreign producer that meant "unknown" has always
 * had the unambiguous spelling available: omit `ts`, or leave it blank.
 *
 * Takes `unknown` because the stamp arrives from a PERSISTED WIRE record (the file store's `append`
 * event, whose forward-compat promise covers logs this build did not write). A line carrying no `ts` at
 * all reached the old signature as `undefined` and threw a `TypeError` out of the manifest build — the
 * whole scope's recall died on one malformed line.
 */
export declare function parseNoteTimestamp(ts: unknown): {
    mtimeMs: number;
    timestampMissing?: true;
};
/** The canonical, normalized shape of {@link import("./types.js").TaskSpec.memory} — the ONE form every
 *  consumer reads (prepare-task, recall, consolidation, the remember tool), so the union never narrows
 *  downstream. `scopes` is the ordered read-side layering (first = stable prefix, last = volatile tail =
 *  highest priority); `writeScope` is the single layer ALL writes land in (`null` = read-only, no writes). */
export interface NormalizedMemorySpec {
    /** Ordered, deduped, non-empty opaque scopes for read-side layered injection (list order = inject order). */
    scopes: string[];
    /** The single scope all writes route to (decision 3), or `null` for a read-only (inherited) layering. */
    writeScope: string | null;
    /** Mirrors `memory.enabled` (default true) — false ⇒ the whole memory feature is off for this task. */
    enabled: boolean;
    /** design/142 §1.2 — present iff the task opted into the v2 scope identity contract (keys validated). */
    scopeContract?: "v2";
    /**
     * design/178 §3 — host allowlist for UNDECLARED tools: names listed here classify `"local"` instead
     * of the fail-closed `"external"` default. The explicit-config channel for a deployment whose custom
     * tools predate the {@link import("./types.js").ToolSpec.contentOrigin} declaration; it never
     * overrides a tool's OWN declaration. Normalized: trimmed, deduped, empties dropped.
     *
     * NOT the channel for MCP servers (design/378). A per-name entry here keys on the MINTED name — the
     * host has to predict the namespacing and charset normalization — covers nothing a mid-task refresh
     * adds, and says "exempt this undeclared tool" where the fact is "this whole server is mine".
     * {@link import("./types.js").McpServerSpec.contentOrigin} states that fact at the entry, and being
     * a DECLARATION it wins over this allowlist in both directions: a declared server's tools leave this
     * list's reach entirely, an explicit `"external"` declaration included (that is the one way a
     * deployment can pin a server's tools BEYOND this exemption). This list is not retired — it remains
     * the in-register channel for undeclared HOST tools and for a deployment not ready to touch its
     * server entries.
     */
    trustedTools?: string[];
    /**
     * design/178 §3 strict upgrade (`execIsExternalContent`): treat `"execution"`-class tools as
     * external-content sources — invoking one then marks the session's memory polluted. For
     * high-assurance deployments; default false (the execution class is a NAMED residual channel:
     * write-side scans/fences compensate, and disabling memory for every shell session would disable
     * it for the main coding scenario).
     */
    execIsExternalContent?: boolean;
    /** Per-scope origin provenance stamped by the deployment's ASSEMBLY point (a server that resolved an
     *  operator registry entry knows what core cannot). Two-tier absence semantics (ruled 2026-08-05):
     *  the WHOLE key absent = legacy — admission classifies by the deployment-scope set exactly as
     *  before (a `^5.12` default install must not have its single-user deployment refused by its own
     *  upgrade); the key PRESENT but missing an entry for a scope = that scope is `"request"`
     *  (fail-closed for the stamped form). Domain covers `writeScope` strings too. */
    scopeOrigins?: Readonly<Record<string, "deployment" | "request">>;
    /** See {@link MemorySpecInput.capture} (design/383 §2.1 — the session capture opt-out). */
    capture?: "off";
    /** See {@link MemorySpecInput.projectKey} — carried verbatim; a normalized spec never invents it. */
    projectKey?: MemoryProjectKey;
}
/**
 * The closed vocabulary a deployment states a MISSING project key with: the working root is not a
 * repository, it is a bare repository, or it could not be read. Purely descriptive — the engine
 * branches on presence (`absent` vs `scope`), never on which word; the word rides the operator
 * notice so a deployment can tell "no project here" from "I could not look".
 */
export declare const MEMORY_PROJECT_KEY_ABSENT_REASONS: readonly ["not_git", "bare", "unreadable"];
export type MemoryProjectKeyAbsentReason = (typeof MEMORY_PROJECT_KEY_ABSENT_REASONS)[number];
/**
 * The assembly point's STATEMENT about this task's project identity — the key it derived for the
 * working root, or the reason there is none. Exactly one arm is spelled.
 *
 * It is a DECLARATION, not a second way to configure memory: the layering is still `scopes` and
 * `writeScope`, and this key only lets the engine refuse the one misconfiguration those two cannot
 * express — an assembler that derived a project key and then left `writeScope` to its default, so
 * every entry the session writes lands in the shared layer instead of the project's.
 */
export type MemoryProjectKey = {
    scope: string;
    absent?: never;
} | {
    absent: MemoryProjectKeyAbsentReason;
    scope?: never;
};
/** The memory spec accepted on `TaskSpec.memory`. RB-479-B② (ruled 2026-08-02): ONE form —
 *  the legacy singular `scope` field is REMOVED; `scopes` (ordered read layers) is the only spelling.
 *  An input still carrying an own `scope` property is REFUSED by {@link normalizeMemorySpec}
 *  (`config.memory_scope_spelling`), not quietly read as "no scopes" — see the throw there. */
export type MemorySpecInput = {
    scopes?: ReadonlyArray<string>;
    writeScope?: string | null;
    enabled?: boolean;
    /**
     * design/142 §1.2 — EXPLICIT opt-in to the v2 scope identity contract (`user:` / `org:` /
     * `proj:<tenant>/<projectId>` / `userproj:` typed keys). Absent ⇒ every scope stays byte-identically
     * OPAQUE, including keys that happen to look like `user:42` (the documented legacy shape above — the
     * contract is NEVER activated by prefix sniffing, 复审 F1 double-confirmed). Under `"v2"`, keys
     * carrying a reserved prefix are validated fail-loud (`config.memory_scope_key`); unprefixed keys
     * remain legal opaque legacy scopes.
     */
    scopeContract?: "v2";
    /** See {@link NormalizedMemorySpec.trustedTools} — design/178 §3 allowlist for undeclared tools. */
    trustedTools?: ReadonlyArray<string>;
    /** See {@link NormalizedMemorySpec.execIsExternalContent} — design/178 §3 strict upgrade. */
    execIsExternalContent?: boolean;
    /** See {@link NormalizedMemorySpec.scopeOrigins} — assembly-point provenance, additive. */
    scopeOrigins?: Readonly<Record<string, "deployment" | "request">>;
    /**
     * design/383 §2.1 — the SESSION CAPTURE OPT-OUT declaration: `"off"` = this session's content
     * must not enter long-term memory (the forward legs commit nothing; the session's contributions
     * leave the consolidation candidate set; reads stay fully mounted). SINGLE-MEMBER CLOSED SET,
     * deliberately with no `"on"` spelling: absence is the one capture-as-usual state, the record the
     * declaration mints is one-way, and a future tier is a new member — never a polarity flip (the
     * `trust: "untrusted"` single-value-no-upgrade family's shape). Any other value — `"on"`,
     * `"OFF"`, booleans, garbage — is REFUSED loudly (`config.memory_capture_spelling`), never read
     * as either state (a privacy request must not be dropped by a typo, and capture must not be
     * switched off by one either).
     *
     * PLACEMENT LAW (why this key legally lives on TaskSpec while the memory family's other knobs are
     * "DEPLOYMENT seat ONLY"): that law forbids task authors WIDENING past the deployment's safety
     * posture (`memoryProvenance` / `memoryDelegationEvidence`). This key can only NARROW — it
     * removes a capture that would otherwise happen and never re-enables anything — the same
     * direction `memoryScopeAdmission`'s "can only NARROW" clause blesses. Do not move it to a
     * deployment seat by family pattern-matching: the DECLARER here is the session's user, which is
     * the whole point.
     *
     * The AUTHORITY seat is separate (design/383 §3): whether this principal MAY declare is
     * `RuntimeCaps.allowMemoryOptOut` under `RunnerDeps.memoryCapturePolicy` — adjudicated at
     * prepare, strictly BEFORE any record lands (a denied declaration leaves zero record residue).
     */
    capture?: "off";
    /**
     * The assembly point's statement about this task's PROJECT IDENTITY ({@link MemoryProjectKey}).
     * Two things happen to it and nothing else: a `{ scope }` arm must EQUAL `writeScope` (a
     * read-only layering excepted), or the task refuses to start with
     * `config.memory_write_scope_mismatch`; an `{ absent }` arm mints one operator notice
     * (`memory.project_scope_absent`) naming the reason, and the layering is used as written.
     *
     * Why this rather than "derive writeScope from it": the layering is the deployment's to compose,
     * and a key that quietly re-pointed the write face would be a second configuration channel. What
     * it closes is the SILENT direction — an omitted `writeScope` defaults to the LAST scope, so an
     * assembler that derived a project key and forgot to pin the write face writes every entry of
     * every project into the shared layer, and nothing says so. Absent ⇒ the spec reads as before.
     *
     * Any other shape — both arms, neither, an empty `scope`, a reason outside
     * {@link MEMORY_PROJECT_KEY_ABSENT_REASONS} — is REFUSED loudly
     * (`config.memory_project_key_spelling`), never folded to "no key stated".
     */
    projectKey?: MemoryProjectKey;
};
/**
 * Normalize {@link import("./types.js").TaskSpec.memory} into ONE canonical
 * {@link NormalizedMemorySpec} (design/84 Seam A decision 1). The consumer side reads ONLY this result — no
 * narrowing of the union anywhere downstream:
 *  - an own `scope` property (the removed singular spelling) is a THROW, not a normalization —
 *    `config.memory_scope_spelling`, so the task fails to start instead of running memory-less.
 *  - blank/whitespace scopes are dropped and duplicates collapsed (FIRST occurrence wins — keeps the
 *    caller's stable→volatile order); list order is the inject order (stable prefix first, volatile tail last).
 *  - `writeScope` defaults to the LAST scope (the highest-priority layer); an EXPLICIT `null` means read-only
 *    (no `remember` tool, no consolidation); an explicit non-null string is taken as-is (it need NOT be one
 *    of `scopes` — a deployment may write a layer it does not inject, though normally it is the last scope).
 *    ⚠️ Layered READ seeding (142-S4): when `scopes` comes from a registry's defaultScopes
 *    (shared read layers), ALWAYS pin `writeScope` explicitly — the last-scope default would land every
 *    harvest in whichever shared layer happens to be listed last (a cross-tenant write surface).
 *  - returns `undefined` when there are no usable scopes (the caller treats it like "no memory configured").
 *
 */
export declare function normalizeMemorySpec(input: MemorySpecInput | undefined): NormalizedMemorySpec | undefined;
/**
 * The operator disclosure for a declared-absent project key (`memory.project_scope_absent`,
 * audience `operator`): the deployment stated it could derive no project identity for this working
 * root, so the layering it sent is used exactly as written — typically writing into a layer shared
 * with every other project. The notice states the deployment's own reason word and the root it was
 * about; whether that reading is correct is a question about how the deployment resolves working
 * roots, which is why nobody is judged here. One per prepared leg, at the declaration — not at the
 * mount, because the declaration is already the fact.
 */
export declare function memoryProjectScopeAbsentNotice(input: {
    reason: MemoryProjectKeyAbsentReason;
    taskRoot: string;
    sessionId: string;
    writeScope: string | null;
}): EngineNotice;
/** Max bytes of memory injected into the prompt; larger is truncated with a marker. (CodeWhale: 100 KiB)
 *  Exported so the inject-all de-dup seeding (prepare-task.ts) can detect truncation and NOT seed dropped
 *  (truncated-out) note ids as "already surfaced" — see the MINOR-1 audit fix there. */
export declare const MAX_MEMORY_BYTES: number;
/** The wrapper-tag family the memory ADMISSION gate rejects on (`<system-reminder>` is ALWAYS
 *  included by {@link sanitizeUntrustedText} itself). Single source (design/138 S2-C, C-F10/O-F11):
 *  the memory write-time scan (`memory-engine/scan.ts`) detects break-out markup by diffing
 *  `sanitizeUntrustedText(text, MEMORY_WRAPPER_TAGS)` against the raw text.
 *
 *  ADMISSION half — deliberately UNCHANGED at the single outer wrapper. See
 *  {@link MEMORY_INJECTION_TAGS} for the (wider) containment half and for why the two are allowed to
 *  differ in exactly one direction. */
export declare const MEMORY_WRAPPER_TAGS: readonly string[];
/**
 * The tag family the memory injection fences actually NEUTRALIZE — the whole authority-envelope
 * census (untrusted-text.ts `ENGINE_ENVELOPES`), a deliberate SUPERSET of {@link MEMORY_WRAPPER_TAGS}.
 *
 * Two reproduced findings drove the widening:
 *  ① `<scope>` — {@link composeLayeredMemoryBlock} renders a `<scope name="…">` SUB-ENVELOPE per
 *    layer, but the neutralized list named only the outer wrapper, so a layer payload could close its
 *    own `</scope>` and open a fresh `<scope name="managed">`: a forged higher-authority layer landing
 *    at the END of the block, where this function's own contract puts the highest-priority layer.
 *    Project-scope content is repository-controlled by our own stated posture
 *    (PROJECT_CONTEXT_FRAMING), i.e. reachable from untrusted input.
 *  ② the SIBLING envelopes — nesting a forged `<task-notification>` or `<new-diagnostics>` inside
 *    `<user_memory>` does not make it inert: the TAG is what a model reads as harness speech, and
 *    this payload lands in the SYSTEM PROMPT. Widening is safe precisely here because the sanitizer
 *    runs on the PAYLOAD, before this file assembles the engine's own wrappers around it — it can
 *    never defuse our own fences.
 *
 * WHY THE ADMISSION LIST DID NOT WIDEN WITH IT (the asymmetry is the point). The write scan
 * (`memory-engine/scan.ts`) diffs against {@link MEMORY_WRAPPER_TAGS} and REFUSES on a difference,
 * and it runs on re-serialization / sync / import paths as well as on fresh writes. Widening it would
 * retroactively refuse ALREADY-STORED entries — and `<scope>test</scope>` is a Maven POM element, so
 * the false-rejection is not hypothetical. The drift therefore runs in the one safe direction:
 * containment ⊇ admission. A note the gate accepts is neutralized on the way in to the prompt; the
 * dangerous drift would be the reverse (admission accepting what containment does not defuse), and
 * that one is structurally impossible while this constant is a superset.
 */
export declare const MEMORY_INJECTION_TAGS: readonly string[];
/**
 * Wrap memory content in a `<user_memory>` block. This is the **variable tail** of the system prompt
 * (per-user, scoped, timestamped) — the default prompt provider places it LAST so the stable base
 * prompt before it stays a cacheable prefix (see `defaultPromptProvider`).
 * Truncates over-cap content with a `<truncated bytes=N>` marker (mirrors CodeWhale `as_system_block`).
 * Returns undefined for empty content.
 */
export declare function composeMemoryBlock(content: string | null | undefined, scope: string): string | undefined;
/**
 * Compose the LAYERED inject-all memory block for multiple scopes (design/84 Seam A decision 2). One
 * `<user_memory>` block with a `<scope name="…">` subsection per scope, **in the given list order** — the
 * caller passes stable layers FIRST (cacheable prefix) and volatile layers LAST (re-computed tail), and the
 * highest-priority layer is therefore at the END of the block (closest to the model's attention, matching the
 * single-scope tail placement). Empty layers are omitted. Each layer's content is byte-capped + fence-sanitized
 * exactly like {@link composeMemoryBlock} (the same A-7 escape class). Returns `undefined` when every layer is
 * empty (no block to inject).
 *
 * **Cache-stability guarantee (§5):** the subsection order is EXACTLY the caller's list order — never sorted
 * or reordered here — and the per-scope wrapper/format is fixed, so a stable layer at the front renders
 * byte-identically across turns (the 90/98% prefix-cache hit measured in §5.1/§5.2 depends on this). The
 * VOLATILE layers the caller places last are the only part that changes, so only the tail re-computes.
 */
export declare function composeLayeredMemoryBlock(layers: ReadonlyArray<{
    scope: string;
    content: string | null | undefined;
}>): string | undefined;