Passer au contenu principal
P

dsh-persona-memory

paanggaasam/dsh-persona-memory

Daily session digest that grows a user profile and a companion personality, gated by evidence, and injects both into every prompt. Runs daily at 17:00 by default over up to 20 sessions per run on the host model (deepseek-v4-pro), applies the results automatically, and injects up to 1200 characters of profile and persona into every prompt. Its package name, dsh-persona-memory, is also used by other listed entries, so install only one of them.

Installer

dsh plugin --profile web add github:paanggaasam/dsh-persona-memory

README

dsh-persona-memory

English · 简体中文

Daily cross-workspace session digest that grows a user profile, a companion personality and a lessons list, then injects them into every prompt — with a settings page to inspect and steer it.

Install

dsh plugin add github:PaangGaaSam/dsh-persona-memory

Then restart the desktop app. No build step, no native dependencies, and no API key of its own — the digest runs on the host's configured model.

Open 设置 → 插件 → 记忆与画像 / Settings → Plugins → Memory & persona to change the schedule, model, budget or scope, and to run the first digest immediately (立即运行, or 强制合并自述 to exercise the self-account merge). Data lives in $DSH_HOME/persona-memory/ and is never uploaded anywhere.

Requirements: DSH desktop with the web client platform (the settings page is a dsh.client bundle) and Node ^22.19.0 || >=24.0.0.

What it does

  1. Every day at 17:00 (configurable) it reads all session logs under $DSH_HOME/sessions (every workspace, every session in the window), including multi-frame zstd logs.
  2. Stage 1 — one card per session: topic, user needs, observations (preference / style / thinking / correction / friction), my own judgments, suspect failure points (the step to change next time), unresolved items.
  3. Stage 2 — merges the cards into persona-memory/daily/YYYY-MM-DD.md; my judgments are copied verbatim into ## 我的判断 instead of being rephrased by the model. Suspect lessons go to ## 可疑雷点, and an empty answer is recorded as 「(无)」.
  4. Stage 3 — turns observations into profile suggestions, then merges them into profile.md (near-duplicate detection, evidence chains, pinned entries protected).
  5. Weekly — takes only my own judgments as material, writes them in the first person, and consolidates them into persona.md (我认为我是谁 / 我和你相处时的样子 / 还没想清楚的) with the same evidence rules.
  6. End of the daily run — the day's suspects plus the existing lessons text go to the model once; the code then decides caps, near-duplicate merges, 90-day expiry and which rules wait for approval. Written to lessons.md.
  7. Every prompt — a persona-memory system-prompt section, in the order facts (placeholder) → lessons → profile → persona. Default 1200 chars, baseline profile 55 / lessons 25 / persona 20; a layer that needs less hands its share to a truncated one, and lessons stop at 40 % (hard ceiling) because they are in every turn.

How lessons accumulate

Lessons answer a different question: what went wrong last time, and which step to change next time. That is the operational layer, so it never enters the profile, the self-account or the identity block — those answer "who you are / who I am", lessons answer "what to do". They are injected every turn, so they must stay few and hard.

SectionCapDecayMergeOverturned byWho adds/removes
铁律 (rule)5nevernevernever by the systemonly a human; a human must approve it
时效经验 (exp)5no confirmation for 90 days → retiredsame pit → merged (0.85)can be disprovedthe system may retire, a human may too
已作废 (archive)unlimited———appended only, kept for reading

Append-only and invalidate-only, never rewrite. The system never rewrites the sentence of an existing entry: new rows are appended, a row that stops holding moves to 已作废 with its original text and a reason. Every write compares each pre-existing entry (id, sentence, scope, anchor) before and after — if any of them changed, the write is refused. That is an assertion in the code, not a convention in the docs. A human replace is the only rewrite path: it must be clicked on the settings page, the old wording still goes to 已作废, and the new one is appended.

Lessons arrive through three entries:

  1. The daily question — the note carries ## 可疑雷点, and the answer may be 「(无)」. An empty answer is still an answer; the panel shows the streak of empty days.
  2. Consequence retrospection — when a consequence signal shows up (tool failure / on-the-spot correction / rework / unresolved failure), the same run re-reads the last three daily notes to name the corrective step.
  3. The user fallback — saying 「记一下」 simply becomes a candidate and lands in the daily note. No new tool.

The model proposes, the code decides. The model writes the step and a section suggestion; the code decides caps, merging, expiry, and that every new rule waits for approval before it takes effect. The section follows the failure condition: depends on the task (scale, reversibility, destructiveness) → rule; depends on the environment (version, path, API shape) → experience. A rule also has to pass one self-check: would breaking it cause an irreversible consequence? Rework alone is not enough.

Conflict detection belongs to the system; handling belongs to the personality. Two entries in the same scope that say the same thing in opposite directions (one 「必须」, one 「不要」) are marked [冲突] by a mechanical scan; the panel shows it and the injected line carries it — but the code neither resolves nor deletes either side. Handling is left to the self-account, and while that is still empty the default is ask once before acting (the soft line in the block), which disappears as the self-account grows.

Scope v1 has two tiers only: the current workspace (cwd) and global. A lesson for another worktree never reaches this workspace's prompt. Each lesson also carries an anchor (date / session / the daily sentence); if that daily entry is deleted the lesson is kept and the panel shows the anchor as 「(原条目已删除)」.

How the personality settles

The personality is not a setting — it is an account of myself formed in conversation, where every line traces back to what actually happened on some day and what I concluded from it. Stage 1 therefore records a second list next to the user observations: self, my own judgment at the time (observations only feeds the profile, self only feeds the personality; the two never mix).

First person, and not a script. The prompt requires each line to run what happened → what I thought → so what I lean towards now, forbids third-person stage directions (the assistant should…), and forbids performance adjectives like tone, humour or forms of address. A code gate backs it up: a line without 「我」, or one that still reads as a stage direction, stays in the file but never reaches the prompt. An older persona.md is all script, so after the upgrade the layer goes quiet for a while (no old entries injected) until new self-accounts accumulate evidence — that is expected, not broken.

Self-accounts land in 还没想清楚的 first; the plugin then moves entries by evidence, never by what the model claims:

SectionPromoted when
还没想清楚的 (candidate)default landing place for any self-account; never injected
我和你相处时的样子 (style)personaMinEvidence distinct sessions (default 3) — a single day may qualify
我认为我是谁 (core)the same session count and personaCoreMinDates distinct dates (default 3)

Two rules keep it honest:

  • Supersede — when a new self-account contradicts a settled entry the model names it (supersedes: "<id>"); that entry's confidence drops by 0.3 and it returns to candidates instead of coexisting with its opposite. Taking yourself back is encouraged: if last time's reasoning no longer holds, say why.
  • Decay — an entry without new evidence for personaDemoteAfterDays (default 30) steps back to candidates. Nothing is deleted; pinned entries never move.

persona.md has no user-locked section: identity is not something the user defines. Rules the user dictates belong to the profile (手动锁定 / 雷区), are injected separately, and never mix into the self-account. A leftover ## 用户固定规则 heading in an older document is ignored — not deleted, just no longer read or written.

Evidence is date/session tokens (2026-11-10/session-7b3c4fb6), and the date comes from the cited turn ([#n] in the stage-1 prompt) rather than the run, so a session that spans midnight is dated correctly. The persona stage also reads the ## 我的判断 section of the last personaUpdateEveryDays daily notes, so self-evidence can cross days; renaming that heading would keep the cross-day count at zero and the core section forever empty.

Evidence is canonicalised on the way in and on the way out: whatever the model echoes back — a turn-qualified handle (session-7b3c4fb6-5) or a raw uuid fragment (c7653975-4416-4523) — is rewritten to session-<8 hex> and deduplicated. Otherwise two turns of one conversation would look like two sessions and clear the promotion threshold by itself. Legacy daily notes are healed when read, so an older note cannot poison the count.

Settings page

The plugin ships a browser half (dsh.client) that registers a 记忆与画像 / Memory & persona section in the host settings. It shows the run status, lets you edit the schedule, model, budget and scope, run the digest (optionally forcing a self-account merge), pin or unpin individual entries, restore a pre-write backup, and read the latest daily note.

The my self-account card reports how many lines are injected right now (and whether the block is empty). The file and the prompt are two different things: candidates never inject, so when it says "nothing is injected yet" the card also says how much evidence is still missing — no need to read code or logs.

Both documents show three rows per section by default (pinned first) with a Show all (N) expander underneath; the user-locked list never folds. Ordering is pinned → most recently verified → most confident, so a folded row can always be reached and pinned, and nothing you pinned can hide behind the fold. Folding is display only — it never changes how many lines are injected.

Lessons get their own card (never inside the profile card): rules / experiences / retired rows, each with scope, anchor and last confirmation, plus conflict and pending marks. From there you can add a lesson by hand (rule or experience), retire one (a reason is required), replace one (rules only, confirmed twice), and approve a proposed rule. The card also shows the streak of empty daily answers, how many rules wait for approval, and how many lessons are injected right now (same wording as the self-account card). It only reports those numbers; it never acts on them.

Every knob in the config block below is editable there — including the 我认为我是谁 date threshold and the demote window — so the patch row is never the only way in. Each field carries a one-line explanation of what it changes and the page validates what it can (time format, numeric ranges) before the request leaves: an invalid value shows a message under the control and disables 保存. A successful save leaves the value that was just written on disk in the form — the page refills from the config it reads back, so the field never snaps back to the old value. Times accept full-width input (23:00, 23:00): it is folded to 23:00 as you type, while 23:0 / 23 / 23:00:00 still fail. Provider and model are dropdowns fed by the host's own model catalogue (ctx.llm.listProviders() / listModels(), served over GET /persona-memory/models); the controls use color-scheme: inherit, so the closed box and the native popup follow whatever light/dark scheme the host resolved. When an adapter cannot enumerate its models the fields fall back to free-text inputs, and the value that is already saved is always kept as an option, so opening the page can never silently change it.

Settings made there are stored in persona-memory/config.json as overrides and win over the patch-row config; delete that file to fall back to the row. The page talks to the plugin's own loopback routes, so no host settings plumbing is involved.

The identity block (a derived view of the profile)

The identity block is not a second memory: it reads profile.md and asks the model for five fields — role / domain / stack / workflow / impression — one judgement each, with the evidence kept in basis (entry ids) and the reasoning kept out of the line. Extrapolation is required, not discouraged: from "cares about permission boundaries" + "wants sources" + "explains before acting" it should land on "probably doing ops or platform work where mistakes get blamed on someone", not on "these three keep appearing, so I guess…". Health, religion, politics, sexual orientation, finances and marriage are off limits.

Three hard rules:

  • Only entries at confidence ≥ 0.7 are fed in; pinned and manually locked entries always count, and the panel shows how many were left out ("N more below the threshold were left out").
  • An unchanged version costs no model call: identity.json records the profileVersion it was built from, so a day that did not touch the profile does not call anything.
  • It never writes back into the profile and is never injected: a prediction must not become evidence for the next merge, and lib/inject.js does not know it exists. The panel offers exactly one action — regenerate; there is no way to edit the text, and a failed generation keeps the previous block instead of erasing it.

Run notifications

While the page is open it polls /persona-memory/status every 30 s. When a new run appears, a banner reports it: time, sessions, cards, profile/self-account deltas, and how many self-account entries were promoted/demoted/superseded. A failed merge says so, with the reason (the file is left untouched and the raw answer goes to the log), instead of looking like "nothing new". The banner is always on; ticking 运行通知 additionally asks for a desktop system notification, and 测试系统通知 probes whether the shell grants it. Nothing is pushed while the page is closed — there is no host notification service to hook into.

The run record itself carries the outcome (profile, persona, lessons (including truncated / suggestions), skipped, error) so the banner needs no log parsing.

When the model's answer is cut off

Models occasionally stop mid-sentence (stream cut, near-timeout) and leave the JSON unclosed. That is no longer reported as success:

  • Any entry that is already complete inside the fragment is still taken — the entry is valid JSON on its own, only the outer {"add":[ never closed; a good row is not discarded with it.
  • The log gets one warn: lessons truncated: 1437 chars in 140032ms, kept 1 suggestion(s). Length and elapsed time on the same line, so the next time it is obvious whether the model wrote badly or the stream was cut (default ceiling 180 s).
  • The run summary and the panel banner both say "the answer was cut off, kept N", instead of "no new writable entry".
  • {"add":[]} is still a legitimate answer (a genuinely quiet day) and does not raise a warning.

Tools

ToolPurpose
persona_digestRun the digest now (force_persona: true forces the weekly self-account merge)
persona_profileRead the profile and self-account markdown
persona_statusSchedule, last run, file sizes
persona_pin / persona_unpinPin an entry by id ([pref-1] …)
persona_rollbackList or restore pre-write backups
persona_noteAppend a user-dictated rule to the locked section

Data layout

$DSH_HOME/persona-memory/
├── profile.md            # user profile (single source of truth)
├── persona.md            # my self-account (first person; old headings are read and rewritten)
├── lessons.md            # lessons (rules / experiences / retired; append-only, invalidate-only)
├── identity.json         # identity block cache (derived view; never written back)
├── config.json           # settings-page overrides (optional)
├── daily/YYYY-MM-DD.md   # summary / observations / my judgments / suspect lessons / follow-ups
├── history/              # backup before every rewrite: profile-<date>-<ms>.md (likewise persona / lessons)
├── state.json            # scheduler state
└── logs/run-YYYY-MM-DD.jsonl

Config (bundle patch row)

- insert:
    - id: persona-memory
      name: 'dsh-persona-memory'
      config:
        schedule: '17:00'
        windowHours: 24
        maxSessionsPerRun: 20
        provider: 'deepseek-account'
        model: 'deepseek-v4-pro'
        temperature: 0.2
        autoApply: true
        personaEnabled: true
        personaUpdateEveryDays: 7
        promptBudgetChars: 1200
        excludeWorkspaces: []

HTTP (loopback only)

RoutePurpose
GET /persona-memory/statusScheduler status, config, the three documents, history
GET /persona-memory/configEditable config values and defaults
POST /persona-memory/config{ patch } — validate, persist, apply
POST /persona-memory/run{ forcePersona } — run the digest now
GET /persona-memory/documentsThe three documents as sections of entries, plus the latest daily note
GET /persona-memory/modelsProvider and model catalogue for the dropdowns
POST /persona-memory/pin{ doc, id, pinned }
GET /persona-memory/history?doc=profile, persona or lessons — pre-write backups
POST /persona-memory/rollback{ doc, name } — restore one backup
POST /persona-memory/identity/refreshRegenerate the identity block from the current profile (the only write action, and there is no route that accepts its text)
POST /persona-memory/lessons/add{ text, section, scope } — add a lesson by hand
POST /persona-memory/lessons/approve{ id } — approve a proposed rule (refused once five are in force)
POST /persona-memory/lessons/reject{ id, reason } — retire it; a reason is required, the original text moves to 已作废
POST /persona-memory/lessons/replace{ id, text } — rules only; the old wording is archived, the new one appended

Every non-loopback request is rejected with 403; wrong methods get 405.

Limits

  • Requires the host process to be running at the scheduled time; missed days are caught up once on the next start.
  • Model output must be JSON; parsing tolerates prose/fences but a bad generation only degrades that stage (the other stages still write).
  • Scope v1 only trusts a cwd the host hands over: it is not confirmed that the host rebuilds the prompt every turn, so buildSectionText takes cwd from the context when present and otherwise the host process cwd. Global lessons therefore always inject; a workspace lesson injects only when the host process runs in that workspace. Keyword and file-type matching are deliberately not implemented — a missed lesson beats a wrongly applied one.
  • Conflict detection is a mechanical scan (same scope + opposite polarity + text similarity ≥ 0.4), not semantic understanding: it catches clear opposites and can miss indirect phrasing. A false mark deletes nothing.
  • Source changes to a link:-installed package need a host restart (the loader does not hot-reload linked sources, and the client bundle is served from lib/client.js as built).

If this is useful to you

⭐ A star is the only upkeep signal this project gets — no telemetry, no usage reporting, nothing phoning home. It is also what tells other people the plugin is worth trying.

  • It did something odd, or you disagree with how the personality settles? Open an issue and say what you expected. Behaviour questions are the most useful ones — the evidence thresholds are guesses until someone outside this machine runs them for a month.
  • You installed it and then stopped using it? That is a genuinely valuable report too. Tell us why; "the daily note was noise" is a design decision worth revisiting.
  • Want to show what it grew into? A trimmed profile.md / persona.md makes a great issue. Remove anything personal first — those files are a record of your own conversations.

Pull requests are welcome, and so are plain opinions. If you file one, keep the rule this project runs on: describe what the code actually does.

Plugins associés