Finch SkillMemory ExtractorBase Mainnet · MIT · FreeRUNTIME v0.3.0
Runs offline
Offline · Zero dependencies · Deterministic

A memory store that forgets

Turns a conversation into an auditable long-term memory store: every memory carries a type, a value score, the quote it came from, and an explicit verdict on whether to add, refine, merge, skip or forget it. No network, no API keys.

Real verdicts from case 03
KEEPI am a backend engineer on the payments team, based in Berlin.
KEEPSecrets must never be committed to the repository.
KEEPAll new services standardise on PostgreSQL 16.
COMPRESSI prefer TypeScript for backend work.
DELETECanary releases start at 5% of traffic before widening.
DELETEMove the settlement job to the new cluster next week.
3 kept · 1 compressed · 2 deleted
3
Modes: extract / consolidate / forget
4
Verdicts: ADD / UPDATE / MERGE / SKIP
0
Third-party dependencies (stdlib only)
100%
Reproducible (byte-identical output)

Edition 2 update · v0.3.0

  • Passwords, API keys, access tokens, identity numbers and payment-card values are blocked. Matched values appear as [REDACTED] in output; forget forces DELETE for a stored secret.
  • memory_roles = ["user"] by default: only user turns are memory sources, so assistant restatements are skipped.
  • Short Chinese statements are counted by Han characters; the six-character Chinese sentence for “I am allergic to peanuts.” is no longer rejected as a short fragment.
  • A changed number in the same fact produces UPDATE with a conflict object; consolidate returns data.conflicts for review.
  • Without now, the latest created_at / last_used_at in input memories is used. Only absent timestamps trigger the fixed fallback. data.now_source reports the source.
01

Differences

Common approach (illustrative comparison)

  • The whole transcript goes into a vector store, small talk included.
  • The same fact reappears in new wording and is stored again.
  • No way to say why a memory was kept, or where it came from.
  • No policy for what to drop, so the store only ever grows.
  • The same input produces different results on a second run.

After (actual run)

  • The one-off lunch event is SKIP; this input produces 10 candidates: 6 ADD and 4 SKIP.
  • Case 02 returns all four operations; the value change from 100 to 200 is explicitly flagged as a conflict.
  • Each candidate includes evidence.quote and scores; evidence containing a sensitive value is redacted too.
  • Case 03 returns 3 KEEP, 1 COMPRESS and 2 DELETE from 6 memories.
  • Each case was run 5 times with one identical stdout hash per input.
02

Capabilities

Split and screen

Splits the input into sentence-level spans and skips greetings, transient state, questions and fragments too short to be durable.

Six memory types

fact / preference / decision / constraint / identity / todo, with separate trigger rules for English and Chinese.

Four-part value rubric

Specificity, durability, reuse likelihood and actor relevance are all returned with the output, not collapsed into one opaque number.

Four-way dedupe

Screen roles, sensitive values and one-off events first. Numeric conflicts take priority as UPDATE; ordinary dedupe then uses similarity and salience.

Mandatory provenance

Every candidate must carry an evidence.quote from the source text, so a memory cannot be invented.

Explainable forgetting

Recency decays on a 45-day half-life, identity and constraint have sticky floors, and an explicit TTL overrides both.

03

Cases

All three cases were rerun with v0.3.0. Tables summarize the output; expand each panel for complete input and stdout.

CASE 01Extract durable memories from a conversation

Greetings, assistant acknowledgements and “Today I had noodles for lunch” are skipped. The CI example uses Jenkins.

Complete input JSON
{
  "mode": "extract",
  "now": "2026-03-14T09:00:00Z",
  "messages": [
    {
      "role": "user",
      "content": "Good morning. We decided all new services standardise on PostgreSQL 16 instead of MySQL. I prefer TypeScript for backend work. Secrets must never be committed to the repository. One sec, let me check the dashboard."
    },
    {
      "role": "assistant",
      "content": "Got it."
    },
    {
      "role": "user",
      "content": "By the way, I am a backend engineer on the payments team, based in Berlin. Next week we need to move the settlement job to the new cluster, remember to follow up."
    },
    {
      "role": "user",
      "content": "Today I had noodles for lunch. The team runs CI on Jenkins."
    }
  ]
}
Result of this run
VerdictTypeScoreMemoryReason
SKIPfact0.000Good morning.ephemeral: greeting / acknowledgement
ADDdecision0.860We decided all new services standardise on PostgreSQL 16 instead of MySQL.new durable decision (best existing similarity 0.00 < 0.55)
ADDpreference0.767I prefer TypeScript for backend work.new durable preference (best existing similarity 0.09 < 0.55)
ADDconstraint0.650Secrets must never be committed to the repository.new durable constraint (best existing similarity 0.23 < 0.55)
SKIPfact0.000One sec, let me check the dashboard.ephemeral: transient state, not durable
SKIPfact0.000Got it.not a memory source: 'assistant' turn (memory_roles = user)
ADDidentity0.833By the way, I am a backend engineer on the payments team, based in Berlin.new durable identity (best existing similarity 0.21 < 0.55)
ADDtodo0.500Next week we need to move the settlement job to the new cluster, remember to follow up.new durable todo (best existing similarity 0.21 < 0.55); explicitly asked to remember
SKIPfact0.000Today I had noodles for lunch.ephemeral: a one-off event, not a durable fact
ADDfact0.660The team runs CI on Jenkins.new durable fact (best existing similarity 0.19 < 0.55)
Summary · returned fields
{
  "candidates": 10,
  "added": 6,
  "updated": 0,
  "merged": 0,
  "skipped": 4,
  "conflicts": 0,
  "sensitive_blocked": 0
}
Complete stdout · v0.3.0
{
  "ok": true,
  "skill": "memory-extractor",
  "version": "0.3.0",
  "mode": "extract",
  "data": {
    "candidates": [
      {
        "id": "m_a8cbec7254",
        "text": "Good morning.",
        "type": "fact",
        "salience": 0.0,
        "salience_breakdown": {},
        "evidence": {
          "message_index": 0,
          "role": "user",
          "quote": "Good morning."
        },
        "operation": "SKIP",
        "reason": "ephemeral: greeting / acknowledgement"
      },
      {
        "id": "m_e005fbc601",
        "text": "We decided all new services standardise on PostgreSQL 16 instead of MySQL.",
        "type": "decision",
        "salience": 0.86,
        "salience_breakdown": {
          "specificity": 1.0,
          "durability": 0.85,
          "reuse_likelihood": 0.65,
          "actor_relevance": 1.0
        },
        "evidence": {
          "message_index": 0,
          "role": "user",
          "quote": "We decided all new services standardise on PostgreSQL 16 instead of MySQL."
        },
        "operation": "ADD",
        "reason": "new durable decision (best existing similarity 0.00 < 0.55)",
        "created_at": "2026-03-14T09:00:00Z"
      },
      {
        "id": "m_c26a9acf34",
        "text": "I prefer TypeScript for backend work.",
        "type": "preference",
        "salience": 0.767,
        "salience_breakdown": {
          "specificity": 0.5,
          "durability": 0.8,
          "reuse_likelihood": 0.85,
          "actor_relevance": 1.0
        },
        "evidence": {
          "message_index": 0,
          "role": "user",
          "quote": "I prefer TypeScript for backend work."
        },
        "operation": "ADD",
        "reason": "new durable preference (best existing similarity 0.09 < 0.55)",
        "created_at": "2026-03-14T09:00:00Z"
      },
      {
        "id": "m_e1c67c7ef5",
        "text": "Secrets must never be committed to the repository.",
        "type": "constraint",
        "salience": 0.65,
        "salience_breakdown": {
          "specificity": 0.25,
          "durability": 0.9,
          "reuse_likelihood": 0.85,
          "actor_relevance": 0.4
        },
        "evidence": {
          "message_index": 0,
          "role": "user",
          "quote": "Secrets must never be committed to the repository."
        },
        "operation": "ADD",
        "reason": "new durable constraint (best existing similarity 0.23 < 0.55)",
        "created_at": "2026-03-14T09:00:00Z"
      },
      {
        "id": "m_d694d29aa5",
        "text": "One sec, let me check the dashboard.",
        "type": "fact",
        "salience": 0.0,
        "salience_breakdown": {},
        "evidence": {
          "message_index": 0,
          "role": "user",
          "quote": "One sec, let me check the dashboard."
        },
        "operation": "SKIP",
        "reason": "ephemeral: transient state, not durable"
      },
      {
        "id": "m_816ade6559",
        "text": "Got it.",
        "type": "fact",
        "salience": 0.0,
        "salience_breakdown": {},
        "evidence": {
          "message_index": 1,
          "role": "assistant",
          "quote": "Got it."
        },
        "operation": "SKIP",
        "reason": "not a memory source: 'assistant' turn (memory_roles = user)"
      },
      {
        "id": "m_a4529d885f",
        "text": "By the way, I am a backend engineer on the payments team, based in Berlin.",
        "type": "identity",
        "salience": 0.833,
        "salience_breakdown": {
          "specificity": 0.75,
          "durability": 0.95,
          "reuse_likelihood": 0.65,
          "actor_relevance": 1.0
        },
        "evidence": {
          "message_index": 2,
          "role": "user",
          "quote": "By the way, I am a backend engineer on the payments team, based in Berlin."
        },
        "operation": "ADD",
        "reason": "new durable identity (best existing similarity 0.21 < 0.55)",
        "created_at": "2026-03-14T09:00:00Z"
      },
      {
        "id": "m_13886ff97e",
        "text": "Next week we need to move the settlement job to the new cluster, remember to follow up.",
        "type": "todo",
        "salience": 0.5,
        "salience_breakdown": {
          "specificity": 0.5,
          "durability": 0.4,
          "reuse_likelihood": 0.3,
          "actor_relevance": 1.0,
          "explicit_request": true
        },
        "evidence": {
          "message_index": 2,
          "role": "user",
          "quote": "Next week we need to move the settlement job to the new cluster, remember to follow up."
        },
        "operation": "ADD",
        "reason": "new durable todo (best existing similarity 0.21 < 0.55); explicitly asked to remember",
        "created_at": "2026-03-14T09:00:00Z"
      },
      {
        "id": "m_1cf11e9ab1",
        "text": "Today I had noodles for lunch.",
        "type": "fact",
        "salience": 0.0,
        "salience_breakdown": {},
        "evidence": {
          "message_index": 3,
          "role": "user",
          "quote": "Today I had noodles for lunch."
        },
        "operation": "SKIP",
        "reason": "ephemeral: a one-off event, not a durable fact"
      },
      {
        "id": "m_ced192ed00",
        "text": "The team runs CI on Jenkins.",
        "type": "fact",
        "salience": 0.66,
        "salience_breakdown": {
          "specificity": 0.5,
          "durability": 0.6,
          "reuse_likelihood": 0.7,
          "actor_relevance": 1.0
        },
        "evidence": {
          "message_index": 3,
          "role": "user",
          "quote": "The team runs CI on Jenkins."
        },
        "operation": "ADD",
        "reason": "new durable fact (best existing similarity 0.19 < 0.55)",
        "created_at": "2026-03-14T09:00:00Z"
      }
    ],
    "summary": {
      "candidates": 10,
      "added": 6,
      "updated": 0,
      "merged": 0,
      "skipped": 4,
      "conflicts": 0,
      "sensitive_blocked": 0
    },
    "now": "2026-03-14T09:00:00Z",
    "now_source": "input",
    "existing_count": 0
  }
}

exit 0 · v0.3.0 · Tables are derived from this run; expand to copy the complete JSON.

information-en-01
Actual execution screenshot · information-en-01
CASE 02Compare with stored memories and flag changed values

The four-operation example also changes the API rate limit from 100 to 200, producing UPDATE with conflict.

Complete input JSON
{
  "mode": "extract",
  "now": "2026-03-14T09:00:00Z",
  "existing_memories": [
    {
      "id": "m_en0001",
      "text": "I prefer TypeScript for backend work.",
      "type": "preference"
    },
    {
      "id": "m_en0002",
      "text": "All new services standardise on PostgreSQL 16.",
      "type": "decision"
    },
    {
      "id": "m_en0003",
      "text": "Production databases need regular backups.",
      "type": "constraint"
    },
    {
      "id": "m_rate",
      "type": "fact",
      "text": "The staging API rate limit is 100 req/s."
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": "I prefer TypeScript for all backend work. All new services standardise on PostgreSQL 16. Production databases must have regular backups and restore checks. Canary releases start at 5% of traffic before widening."
    },
    {
      "role": "user",
      "content": "The staging API rate limit is 200 req/s."
    }
  ]
}
Result of this run
VerdictTypeScoreMemoryReason
UPDATEpreference0.767I prefer TypeScript for all backend work.refines m_en0001 (similarity 0.88 in [0.72, 0.9), same type)
SKIPdecision0.708All new services standardise on PostgreSQL 16.duplicate of m_en0002 (similarity 1.00 >= 0.9)
MERGEconstraint0.650Production databases must have regular backups and restore checks.related to m_en0003 (similarity 0.66 in [0.55, 0.72), same type)
ADDfact0.520Canary releases start at 5% of traffic before widening.new durable fact (best existing similarity 0.23 < 0.55)
UPDATEfact0.695The staging API rate limit is 200 req/s.conflicts with m_rate: same statement, different value(s) 100, 200 -- newer statement supersedes, review the change
Summary · returned fields
{
  "candidates": 5,
  "added": 1,
  "updated": 2,
  "merged": 1,
  "skipped": 1,
  "conflicts": 1,
  "sensitive_blocked": 0
}
Complete stdout · v0.3.0
{
  "ok": true,
  "skill": "memory-extractor",
  "version": "0.3.0",
  "mode": "extract",
  "data": {
    "candidates": [
      {
        "id": "m_0268e04c6c",
        "text": "I prefer TypeScript for all backend work.",
        "type": "preference",
        "salience": 0.767,
        "salience_breakdown": {
          "specificity": 0.5,
          "durability": 0.8,
          "reuse_likelihood": 0.85,
          "actor_relevance": 1.0
        },
        "evidence": {
          "message_index": 0,
          "role": "user",
          "quote": "I prefer TypeScript for all backend work."
        },
        "operation": "UPDATE",
        "reason": "refines m_en0001 (similarity 0.88 in [0.72, 0.9), same type)",
        "created_at": "2026-03-14T09:00:00Z",
        "target_id": "m_en0001"
      },
      {
        "id": "m_93031c738a",
        "text": "All new services standardise on PostgreSQL 16.",
        "type": "decision",
        "salience": 0.708,
        "salience_breakdown": {
          "specificity": 0.75,
          "durability": 0.85,
          "reuse_likelihood": 0.65,
          "actor_relevance": 0.4
        },
        "evidence": {
          "message_index": 0,
          "role": "user",
          "quote": "All new services standardise on PostgreSQL 16."
        },
        "operation": "SKIP",
        "reason": "duplicate of m_en0002 (similarity 1.00 >= 0.9)",
        "created_at": "2026-03-14T09:00:00Z",
        "target_id": "m_en0002"
      },
      {
        "id": "m_20673c0425",
        "text": "Production databases must have regular backups and restore checks.",
        "type": "constraint",
        "salience": 0.65,
        "salience_breakdown": {
          "specificity": 0.25,
          "durability": 0.9,
          "reuse_likelihood": 0.85,
          "actor_relevance": 0.4
        },
        "evidence": {
          "message_index": 0,
          "role": "user",
          "quote": "Production databases must have regular backups and restore checks."
        },
        "operation": "MERGE",
        "reason": "related to m_en0003 (similarity 0.66 in [0.55, 0.72), same type)",
        "created_at": "2026-03-14T09:00:00Z",
        "target_id": "m_en0003"
      },
      {
        "id": "m_fb19c9e360",
        "text": "Canary releases start at 5% of traffic before widening.",
        "type": "fact",
        "salience": 0.52,
        "salience_breakdown": {
          "specificity": 0.5,
          "durability": 0.6,
          "reuse_likelihood": 0.5,
          "actor_relevance": 0.4
        },
        "evidence": {
          "message_index": 0,
          "role": "user",
          "quote": "Canary releases start at 5% of traffic before widening."
        },
        "operation": "ADD",
        "reason": "new durable fact (best existing similarity 0.23 < 0.55)",
        "created_at": "2026-03-14T09:00:00Z"
      },
      {
        "id": "m_d5ca16271e",
        "text": "The staging API rate limit is 200 req/s.",
        "type": "fact",
        "salience": 0.695,
        "salience_breakdown": {
          "specificity": 1.0,
          "durability": 0.6,
          "reuse_likelihood": 0.7,
          "actor_relevance": 0.4
        },
        "evidence": {
          "message_index": 1,
          "role": "user",
          "quote": "The staging API rate limit is 200 req/s."
        },
        "operation": "UPDATE",
        "reason": "conflicts with m_rate: same statement, different value(s) 100, 200 -- newer statement supersedes, review the change",
        "created_at": "2026-03-14T09:00:00Z",
        "target_id": "m_rate",
        "conflict": {
          "with": "m_rate",
          "differing_values": [
            "100",
            "200"
          ]
        }
      }
    ],
    "summary": {
      "candidates": 5,
      "added": 1,
      "updated": 2,
      "merged": 1,
      "skipped": 1,
      "conflicts": 1,
      "sensitive_blocked": 0
    },
    "now": "2026-03-14T09:00:00Z",
    "now_source": "input",
    "existing_count": 4
  }
}

exit 0 · v0.3.0 · Tables are derived from this run; expand to copy the complete JSON.

information-en-02
Actual execution screenshot · information-en-02
CASE 03Decide what to forget

The full input contains text, timestamps, salience, usage and TTL, so every action can be checked.

Complete input JSON
{
  "mode": "forget",
  "now": "2026-03-14T09:00:00Z",
  "memories": [
    {
      "id": "m_identity",
      "text": "I am a backend engineer on the payments team, based in Berlin.",
      "type": "identity",
      "salience": 0.92,
      "created_at": "2025-02-01",
      "last_used_at": "2025-02-01",
      "use_count": 3
    },
    {
      "id": "m_constraint",
      "text": "Secrets must never be committed to the repository.",
      "type": "constraint",
      "salience": 0.9,
      "created_at": "2025-06-02",
      "last_used_at": "2025-06-02",
      "use_count": 0
    },
    {
      "id": "m_decision",
      "text": "All new services standardise on PostgreSQL 16.",
      "type": "decision",
      "salience": 0.86,
      "created_at": "2026-03-01",
      "last_used_at": "2026-03-01",
      "use_count": 9
    },
    {
      "id": "m_preference",
      "text": "I prefer TypeScript for backend work.",
      "type": "preference",
      "salience": 0.71,
      "created_at": "2025-11-20",
      "last_used_at": "2025-11-20",
      "use_count": 1
    },
    {
      "id": "m_fact",
      "text": "Canary releases start at 5% of traffic before widening.",
      "type": "fact",
      "salience": 0.66,
      "created_at": "2025-04-10",
      "last_used_at": "2025-04-10",
      "use_count": 0
    },
    {
      "id": "m_todo",
      "text": "Move the settlement job to the new cluster next week.",
      "type": "todo",
      "salience": 0.44,
      "created_at": "2025-09-01",
      "last_used_at": "2025-09-01",
      "use_count": 2,
      "ttl_days": 30
    }
  ]
}
Result of this run
ActionScoreInput memory (matched by id)Reasons
KEEP0.600I am a backend engineer on the payments team, based in Berlin.value=0.920 (salience) · recency=0.100 (406d since last_used_at, half-life 45d) · frequency=0.500 (3 use(s)) · type floor: 'identity' is sticky, score raised to 0.60 · score 0.600 >= 0.55 -> KEEP
KEEP0.600Secrets must never be committed to the repository.value=0.900 (salience) · recency=0.136 (285d since last_used_at, half-life 45d) · frequency=0.000 (0 use(s)) · type floor: 'constraint' is sticky, score raised to 0.60 · score 0.600 >= 0.55 -> KEEP
KEEP0.796All new services standardise on PostgreSQL 16.value=0.860 (salience) · recency=0.771 (13d since last_used_at, half-life 45d) · frequency=0.750 (9 use(s)) · score 0.796 >= 0.55 -> KEEP
COMPRESS0.422I prefer TypeScript for backend work.value=0.710 (salience) · recency=0.282 (114d since last_used_at, half-life 45d) · frequency=0.250 (1 use(s)) · score 0.422 in [0.3, 0.55) -> COMPRESS
DELETE0.272Canary releases start at 5% of traffic before widening.value=0.660 (salience) · recency=0.117 (338d since last_used_at, half-life 45d) · frequency=0.000 (0 use(s)) · score 0.272 < 0.3 -> DELETE
DELETE0.340Move the settlement job to the new cluster next week.value=0.440 (salience) · recency=0.188 (194d since last_used_at, half-life 45d) · frequency=0.400 (2 use(s)) · stale todo: 194d > 30d without use -> DELETE · explicit ttl_days=30 exceeded (age 194d) -> DELETE, overrides type floor
Summary · returned fields
{
  "total": 6,
  "keep": 3,
  "compress": 1,
  "delete": 2,
  "bytes_reclaimed_estimate": 108
}
Complete stdout · v0.3.0
{
  "ok": true,
  "skill": "memory-extractor",
  "version": "0.3.0",
  "mode": "forget",
  "data": {
    "memories": [
      {
        "id": "m_identity",
        "type": "identity",
        "action": "KEEP",
        "score": 0.6,
        "reasons": [
          "value=0.920 (salience)",
          "recency=0.100 (406d since last_used_at, half-life 45d)",
          "frequency=0.500 (3 use(s))",
          "type floor: 'identity' is sticky, score raised to 0.60",
          "score 0.600 >= 0.55 -> KEEP"
        ]
      },
      {
        "id": "m_constraint",
        "type": "constraint",
        "action": "KEEP",
        "score": 0.6,
        "reasons": [
          "value=0.900 (salience)",
          "recency=0.136 (285d since last_used_at, half-life 45d)",
          "frequency=0.000 (0 use(s))",
          "type floor: 'constraint' is sticky, score raised to 0.60",
          "score 0.600 >= 0.55 -> KEEP"
        ]
      },
      {
        "id": "m_decision",
        "type": "decision",
        "action": "KEEP",
        "score": 0.796,
        "reasons": [
          "value=0.860 (salience)",
          "recency=0.771 (13d since last_used_at, half-life 45d)",
          "frequency=0.750 (9 use(s))",
          "score 0.796 >= 0.55 -> KEEP"
        ]
      },
      {
        "id": "m_preference",
        "type": "preference",
        "action": "COMPRESS",
        "score": 0.422,
        "reasons": [
          "value=0.710 (salience)",
          "recency=0.282 (114d since last_used_at, half-life 45d)",
          "frequency=0.250 (1 use(s))",
          "score 0.422 in [0.3, 0.55) -> COMPRESS"
        ],
        "compressed_text": "I prefer TypeScript for backend work."
      },
      {
        "id": "m_fact",
        "type": "fact",
        "action": "DELETE",
        "score": 0.272,
        "reasons": [
          "value=0.660 (salience)",
          "recency=0.117 (338d since last_used_at, half-life 45d)",
          "frequency=0.000 (0 use(s))",
          "score 0.272 < 0.3 -> DELETE"
        ]
      },
      {
        "id": "m_todo",
        "type": "todo",
        "action": "DELETE",
        "score": 0.34,
        "reasons": [
          "value=0.440 (salience)",
          "recency=0.188 (194d since last_used_at, half-life 45d)",
          "frequency=0.400 (2 use(s))",
          "stale todo: 194d > 30d without use -> DELETE",
          "explicit ttl_days=30 exceeded (age 194d) -> DELETE, overrides type floor"
        ]
      }
    ],
    "summary": {
      "total": 6,
      "keep": 3,
      "compress": 1,
      "delete": 2,
      "bytes_reclaimed_estimate": 108
    },
    "now": "2026-03-14T09:00:00Z",
    "now_source": "input"
  }
}

exit 0 · v0.3.0 · Tables are derived from this run; expand to copy the complete JSON.

information-en-03
Actual execution screenshot · information-en-03
04

Pipeline

1Splitsentences2Screenephemeral3Classifysix types4Scorefour axes5Comparesimilarity6Decideone of four
05

Thresholds

Bands show ordinary dedupe thresholds; markers represent the first four statements in case 02. Numeric conflicts bypass these bands and produce UPDATE + conflict even at high similarity.

ADDMERGEUPDATESKIP00.550.720.910.881.000.660.23
06

Decay

Recency = 45 / (45 + days since last use). The weighted score is 0.35 × value + 0.35 × recency + 0.30 × frequency. Type floors, stale todos and TTL can change the final action. The table reports the current case 03 run.

ActionScoreInput memory (matched by id)Reasons
KEEP0.600I am a backend engineer on the payments team, based in Berlin.value=0.920 (salience) · recency=0.100 (406d since last_used_at, half-life 45d) · frequency=0.500 (3 use(s)) · type floor: 'identity' is sticky, score raised to 0.60 · score 0.600 >= 0.55 -> KEEP
KEEP0.600Secrets must never be committed to the repository.value=0.900 (salience) · recency=0.136 (285d since last_used_at, half-life 45d) · frequency=0.000 (0 use(s)) · type floor: 'constraint' is sticky, score raised to 0.60 · score 0.600 >= 0.55 -> KEEP
KEEP0.796All new services standardise on PostgreSQL 16.value=0.860 (salience) · recency=0.771 (13d since last_used_at, half-life 45d) · frequency=0.750 (9 use(s)) · score 0.796 >= 0.55 -> KEEP
COMPRESS0.422I prefer TypeScript for backend work.value=0.710 (salience) · recency=0.282 (114d since last_used_at, half-life 45d) · frequency=0.250 (1 use(s)) · score 0.422 in [0.3, 0.55) -> COMPRESS
DELETE0.272Canary releases start at 5% of traffic before widening.value=0.660 (salience) · recency=0.117 (338d since last_used_at, half-life 45d) · frequency=0.000 (0 use(s)) · score 0.272 < 0.3 -> DELETE
DELETE0.340Move the settlement job to the new cluster next week.value=0.440 (salience) · recency=0.188 (194d since last_used_at, half-life 45d) · frequency=0.400 (2 use(s)) · stale todo: 194d > 30d without use -> DELETE · explicit ttl_days=30 exceeded (age 194d) -> DELETE, overrides type floor
07

Steps

Build one input object

Pick the mode and pass the conversation or the memory list inline as a single JSON object; set now to pin the reference instant.

Split and screen

Split into sentences, then mark greetings, transient state, questions and short fragments as ephemeral and SKIP them.

Classify and score

Assign one of six memory types and score the four-part rubric; anything below 0.35 is skipped.

Compare with what is stored

Score similarity against every existing memory and route the result to ADD, UPDATE, MERGE or SKIP.

Read one JSON object

A single object is printed to stdout: ok=true on success, ok=false with an error code otherwise, with matching exit codes.

08

Audience

Agent developers

Adding long-term memory to a conversational agent and needing a write policy that can be explained and regression-tested.

Memory store maintainers

An existing store has grown with near-duplicates that must be merged without losing any fact.

Teams with audit requirements

Every memory has to trace back to a quote in the source; invented memories are not acceptable.

Offline and air-gapped deployments

Environments where outbound network calls and third-party API keys are not permitted.

Start

Download the Skill from Finch, give the package or unpacked folder to your AI to install, then ask to extract, consolidate or forget memories. Requires Python 3.9+ with no third-party dependencies.

MITLicense
Base MainnetChain
FreePrice
Python 3.9+Runtime

Case tables are derived from real v0.3.0 output. The before/after comparison explains common workflows; the Skill returns decisions and does not modify your store itself.