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.
[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.conflict object; consolidate returns data.conflicts for review.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.Splits the input into sentence-level spans and skips greetings, transient state, questions and fragments too short to be durable.
fact / preference / decision / constraint / identity / todo, with separate trigger rules for English and Chinese.
Specificity, durability, reuse likelihood and actor relevance are all returned with the output, not collapsed into one opaque number.
Screen roles, sensitive values and one-off events first. Numeric conflicts take priority as UPDATE; ordinary dedupe then uses similarity and salience.
Every candidate must carry an evidence.quote from the source text, so a memory cannot be invented.
Recency decays on a 45-day half-life, identity and constraint have sticky floors, and an explicit TTL overrides both.
All three cases were rerun with v0.3.0. Tables summarize the output; expand each panel for complete input and stdout.
Greetings, assistant acknowledgements and “Today I had noodles for lunch” are skipped. The CI example uses Jenkins.
{
"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."
}
]
}| Verdict | Type | Score | Memory | Reason |
|---|---|---|---|---|
| SKIP | fact | 0.000 | Good morning. | ephemeral: greeting / acknowledgement |
| ADD | decision | 0.860 | We decided all new services standardise on PostgreSQL 16 instead of MySQL. | new durable decision (best existing similarity 0.00 < 0.55) |
| ADD | preference | 0.767 | I prefer TypeScript for backend work. | new durable preference (best existing similarity 0.09 < 0.55) |
| ADD | constraint | 0.650 | Secrets must never be committed to the repository. | new durable constraint (best existing similarity 0.23 < 0.55) |
| SKIP | fact | 0.000 | One sec, let me check the dashboard. | ephemeral: transient state, not durable |
| SKIP | fact | 0.000 | Got it. | not a memory source: 'assistant' turn (memory_roles = user) |
| ADD | identity | 0.833 | By the way, I am a backend engineer on the payments team, based in Berlin. | new durable identity (best existing similarity 0.21 < 0.55) |
| ADD | todo | 0.500 | Next 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 |
| SKIP | fact | 0.000 | Today I had noodles for lunch. | ephemeral: a one-off event, not a durable fact |
| ADD | fact | 0.660 | The team runs CI on Jenkins. | new durable fact (best existing similarity 0.19 < 0.55) |
{
"candidates": 10,
"added": 6,
"updated": 0,
"merged": 0,
"skipped": 4,
"conflicts": 0,
"sensitive_blocked": 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.

The four-operation example also changes the API rate limit from 100 to 200, producing UPDATE with conflict.
{
"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."
}
]
}| Verdict | Type | Score | Memory | Reason |
|---|---|---|---|---|
| UPDATE | preference | 0.767 | I prefer TypeScript for all backend work. | refines m_en0001 (similarity 0.88 in [0.72, 0.9), same type) |
| SKIP | decision | 0.708 | All new services standardise on PostgreSQL 16. | duplicate of m_en0002 (similarity 1.00 >= 0.9) |
| MERGE | constraint | 0.650 | Production databases must have regular backups and restore checks. | related to m_en0003 (similarity 0.66 in [0.55, 0.72), same type) |
| ADD | fact | 0.520 | Canary releases start at 5% of traffic before widening. | new durable fact (best existing similarity 0.23 < 0.55) |
| UPDATE | fact | 0.695 | The 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 |
{
"candidates": 5,
"added": 1,
"updated": 2,
"merged": 1,
"skipped": 1,
"conflicts": 1,
"sensitive_blocked": 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.

The full input contains text, timestamps, salience, usage and TTL, so every action can be checked.
{
"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
}
]
}| Action | Score | Input memory (matched by id) | Reasons |
|---|---|---|---|
| KEEP | 0.600 | I 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 |
| KEEP | 0.600 | Secrets 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 |
| KEEP | 0.796 | All 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 |
| COMPRESS | 0.422 | I 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 |
| DELETE | 0.272 | Canary 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 |
| DELETE | 0.340 | Move 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 |
{
"total": 6,
"keep": 3,
"compress": 1,
"delete": 2,
"bytes_reclaimed_estimate": 108
}{
"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.

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.
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.
| Action | Score | Input memory (matched by id) | Reasons |
|---|---|---|---|
| KEEP | 0.600 | I 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 |
| KEEP | 0.600 | Secrets 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 |
| KEEP | 0.796 | All 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 |
| COMPRESS | 0.422 | I 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 |
| DELETE | 0.272 | Canary 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 |
| DELETE | 0.340 | Move 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 |
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 into sentences, then mark greetings, transient state, questions and short fragments as ephemeral and SKIP them.
Assign one of six memory types and score the four-part rubric; anything below 0.35 is skipped.
Score similarity against every existing memory and route the result to ADD, UPDATE, MERGE or SKIP.
A single object is printed to stdout: ok=true on success, ok=false with an error code otherwise, with matching exit codes.
Adding long-term memory to a conversational agent and needing a write policy that can be explained and regression-tested.
An existing store has grown with near-duplicates that must be merged without losing any fact.
Every memory has to trace back to a quote in the source; invented memories are not acceptable.
Environments where outbound network calls and third-party API keys are not permitted.
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.
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.