The session model¶
A Sesja is one play session: a dated, narrated block of fiction plus the structured metadata the tool needs to award PU, move currency, and route notifications. Sessions are the core write target of Nerthus.Core — almost every mutation the daemon performs is anchored to a session block. This page defines the header key's role, authoring and editing, entity-driven distribution, and the integrity and participation machinery that keeps duplicated copies honest. For the plain-language introduction, start at the sessions overview; the exact grammar — header parsing, every metadata key, custom narrative tags, and the legacy generations — is the session metadata reference.
Concepts¶
- Session header —
### YYYY-MM-DD, Title, Narrator: the universal key for deduplication, content-hash integrity, the participation graph, and distribution. Comparison is case-insensitive. The header is canonical and immutable — the tool never rewrites it; a malformed header date is rescued by an in-body@Dataoverride while the key stays untouched (session metadata reference). - Narrator — the last header field, kept as a raw string. Narrator
comparison uses the effective parsed narrator — the header value unless
an in-body
@Narratoroverride replaces it. The participation graph tracks participant entities only, so the narrator is matched by exact text. - Participant entity — any entity named in a session's structured metadata: the location(s) it took place in, the player-characters who earned PU, and (forward-designed) groups and NPCs. Distribution copies the block to each participant's file.
- Current (Aktualny) body — the canonical
@-tag session body the tool authors; the three older shapes parse as-is (legacy generations). - Session-bearing roots — the daemon scans
*.mdunder exactly four directories:Wątki/,Organizacje/,Postaci/,Świat gry/. A session block anywhere else is invisible — to dedup, integrity, distribution, and the monthly PU batch alike. @plik— the entity attribute naming an entity's hand-authored source file. Distribution resolves where a session block must be written by reading each participant's@plik(entity model).
The canonical example this page reuses:
### 2026-07-01, Eraster rozmawia z Tussalem, Anward
Eraster spotkał się z Tussalem w jego willi w Thuzal. Wypili herbatę i zjedli
obiad. ... (prose body) ...
- @Lokacje:
- Thuzal
- Thuzal/Rezydencja Tussal
- @Logi:
- https://nerthus.pl/logizsesjierasteraztussalem
- @PU:
- Eraster: 0,2
- Lord Tussal: 0,2
- @Transfer:
- 10 Korony, Eraster -> Lord Tussal
- @Uczestnicy:
- Opat Perrin
- @Pliki: (auto)
- Postaci/Gracze/Eraster.md
- Postaci/Gracze/Lord Tussal.md
- Świat gry/Thuzal/Sesje lokalne.md
Authoring & editing¶
Authoring (Add-NerthusSession; New-NerthusSession is a synonym)
requires an explicit repo-relative -File along with -Date, -Title,
and -Narrator. The daemon renders the block in the current @-tag form and
inserts it at its chronological position in the target file — before the
first later-dated session, else appended. Open-NerthusSession authors a
draft (header + prose only) the same way.
Editing (Set-NerthusSession) splices only the managed sections the
caller passes — @Lokacje, @Logi, @PU, @Transfer, @Intel,
@Uczestnicy. The mechanism: each managed section is the - @Tag: bullet
plus its indented children, located by exact label; a passed section is
replaced in place (appended at the end of the block when absent, removed when
passed empty). Every other line — prose wherever it sits, custom narrative
bullets like - Efekty:, unknown tags — is preserved byte for byte. The
header, @Narrator, and @Data are not editable this way — the header is the
key (see Concepts).
An edit rewrites only the session's primary copy — the first copy the
scanner encounters (scan order: Wątki, Organizacje, Postaci,
Świat gry), which is the model's authoritative block. Other distributed
copies drift until the next distribution converges them.
Neither authoring nor editing distributes the block (see Distribution). Both
honor -WhatIf dry-run previews, and both pass the write gate
(Assert-NerthusWriteAllowed) like every mutating route — see the
API reference.
Generic entity-driven distribution¶
A session is authored once — into one file under a session-bearing root — and distributed to the files of all participant entities. Distribution is generic and entity-driven:
- Extract participants from the block's structured metadata: characters
from
@PUand@Uczestnicy, plus the interior part of each@Lokacjereference (the text after the first/, or the whole token when there is none). SoThuzal/Rezydencja Tussalalone distributes to the Rezydencja Tussal entity, not to Thuzal — list the city standalone too (as the canonical example does) to reach itsSesje lokalne.md. - Resolve each name → its entity → its file through the
name resolver (fuzzy matching disabled), reading
the entity's
@plik. For the example:Eraster⇒Postaci/Gracze/Eraster.md,Thuzal⇒Świat gry/Thuzal/Sesje lokalne.md. - Write the block into each target file at its chronological position,
and record the full target set in the block's
@Pliki: (auto)section. - Dedup on the header — the source block is authoritative. The block is
copied verbatim (prose and custom narrative tags preserved); only
@Plikiis refreshed to the resolved target set. When the same header already exists in a target file the copy is replaced — every copy converges to the source content and re-distribution is idempotent.
@Pliki is machine-authored and rewritten whole on every distribution, so it carries
the (auto) marker that says exactly that (tag schema).
The section holds no value of its own, so the marker sits on the section line where a
value would be. The section-matching regex is not end-anchored, so an unmarked @Pliki
written before the marker existed is found and replaced in place — the next
distribution upgrades it, and never doubles it.
Invariant
Distribution never invents a target. A name that does not resolve, an
entity with no @plik, or a path resolving outside the repo root is
skipped and reported in the response's Skipped list — the one signal for
skipped targets. A Gracz carries no @plik, so a Gracz named in the
metadata is always skipped (players model).
Distribution runs only when explicitly invoked —
Invoke-NerthusSessionDistribution, or as part of Close-NerthusSession.
Adding or editing a session never auto-distributes; the
AfterSessionDistributed hook fires after a distribution run (an audit line
plus a session:distributed event). The settlement workflow re-distributes
only sessions that already carry @Pliki and have drifted, never a session
awaiting its first distribution (settlement model).
Closing: the two acts, and what each one is¶
The word "closing" covers one act only, and it is worth stating what it is not.
Committing a session to the repository is the Narrator's delivery: the block is final, the header is frozen as an identifier, and the work is handed over. It is a git operation and the tool has no part in it. A committed session that was never closed is inert — nothing is distributed, no coin moves, no player hears anything, and no monthly sweep will pick it up, because settlement only ever repairs what was distributed at least once.
Closing (Close-NerthusSession) is the explicit instruction that makes a session
take effect. It applies, in order:
- distribution — the fan-out above, firing
AfterSessionDistributed; @Transfer—Invoke-NerthusSessionTransfersService, dedup-guarded by header, firingAfterTransferon first application (currency model);@Intel—Invoke-NerthusSessionIntelService, dedup-guarded by header viastate/intel-ledger.json(logs model §4);- the hash re-baseline below.
Steps 2 and 3 are best-effort: each is wrapped, and a throw is warned and swallowed, because distribution has already landed and must not be reported as failed. Both are idempotent, so re-closing moves no coin and sends no message twice.
PU is the one exception, and the reason the monthly batch exists: the cap is computed per month across all of a character's sessions, so a single session's award is not derivable in isolation (PU model).
Closing spans capabilities its route does not name
POST /workflows/close-session requires session.write alone, yet closing now
writes currency and sends Discord messages. This is deliberate: a Narrator's
authority to move the coin comes from having run the session that records it, and
the @Transfer line in their own committed block is the authorization. It is a real
widening — before, only a workflow.settle caller (or the CI pipeline, behind an
MR review) could enact a Narrator's transfer directives
(settlement model, permissions).
Integrity — content hashing¶
Because one session lives in many files, the tool guards against silent edits
with SHA-256 content hashing (Nerthus.ContentHasher):
- The hash input is the full header line + body of the primary copy, with all whitespace stripped — reflows, blank-line churn, and CRLF/LF conversion never trip a false positive, while genuine content or PU changes always do. Values are 64-char lowercase hex.
- Hashes live in one store file,
.nerthus/cache/session-hashes.json, keyed by the header text without the###prefix. Test-NerthusSessionIntegritycompares current content to stored hashes and reports findings in three tiers:- Critical —
PUSessionModified(a modified session carries@PUdata) andDoublePUMarker(one block carries two@PUmarkers): PU tampering. - High —
ContentChanged(a modified non-PU session),Deleted(a hashed header no longer appears in the live model). - Medium —
Unhashed(no stored hash yet),MalformedDate(the effective date — header or@Data— is not a real calendar date),FutureDated.
- Critical —
Set-NerthusSessionHashre-baselines the entire store from the current session model — a global operation that clears every pending finding, not a per-session one.Close-NerthusSessionends with that same global re-baseline (see Closing above) — reviewTest-NerthusSessionIntegrityfirst if other pending drift matters.
Participation graph¶
The participation graph answers "which entities were involved in which sessions" with tiers of decreasing confidence, merged per session (lowest tier number wins):
| Tier | Signal | Status |
|---|---|---|
| 0 — Filesystem | the block sits in the entity's file (matched through @plik) |
implemented |
| 1 — Structured tags | entity named in @PU, @Uczestnicy, or a @Transfer endpoint |
implemented |
| 2 — Body mentions | name found in the prose by the Polish-aware resolver | deferred (needs calibrated mention confidence) |
Tier-1 names run through the resolver (fuzzy matching disabled); an unresolved
token is kept verbatim so the graph still surfaces it. The graph is computed
on demand from the in-memory session model (Get-NerthusSessionGraph);
Set-NerthusSessionGraph forces a model rebuild first, and
Compare-NerthusSessionParticipation reports per-entity session sets plus
their intersection over the same graph.
The per-entity and per-narrator profile lookups
(Get-NerthusEntitySessionProfile, Get-NerthusNarratorSessionProfile) are
simpler: exact-match scans over parsed metadata (@PU names, locations,
@Uczestnicy; narrator string equality against the effective parsed narrator
— the header value unless a @Narrator override replaces it).
Find-NerthusSession's narrator filter compares the same effective value.
They consult neither the graph nor the resolver — use the graph cmdlets when
canonical resolution matters.
Cmdlet surface¶
- Read / search:
Get-NerthusSession(one session by header),Find-NerthusSession(filter by narrator, date range, entity),Get-NerthusSessionLog - Author / edit:
Add-NerthusSession,New-NerthusSession,Set-NerthusSession,Open-NerthusSession,Close-NerthusSession - Distribution & transfers:
Invoke-NerthusSessionDistribution,Invoke-NerthusSessionTransfers - Integrity:
Test-NerthusSessionIntegrity,Set-NerthusSessionHash - Participation:
Get-NerthusSessionGraph,Set-NerthusSessionGraph,Compare-NerthusSessionParticipation,Get-NerthusEntitySessionProfile,Get-NerthusNarratorSessionProfile
Routes, parameters, envelopes, and capabilities: API reference.
Examples¶
Author into an explicit file, then distribute:
Add-NerthusSession `
-File 'Wątki/Intrygi w Thuzal.md' `
-Date '2026-07-01' `
-Title 'Eraster rozmawia z Tussalem' `
-Narrator 'Anward' `
-Locations 'Thuzal','Thuzal/Rezydencja Tussal' `
-Logs 'https://nerthus.pl/logizsesjierasteraztussalem' `
-PU @{ Eraster = '0,2'; 'Lord Tussal' = '0,2' } `
-Transfers '10 Korony, Eraster -> Lord Tussal'
Invoke-NerthusSessionDistribution -Header '2026-07-01, Eraster rozmawia z Tussalem, Anward'
# → written to Postaci/Gracze/Eraster.md, Postaci/Gracze/Lord Tussal.md, and Świat gry/Thuzal/Sesje lokalne.md; @Pliki populated
Correct one section without disturbing the rest of the block:
Set-NerthusSession -Header '2026-07-01, Eraster rozmawia z Tussalem, Anward' `
-PU @{ Eraster = '0,3'; 'Lord Tussal' = '0,2' }
# → only the - @PU: section is respliced; prose and '- Efekty:' stay byte-identical
Design decisions¶
- Groups & NPCs in distribution — extraction stays explicit (via
@Uczestnicy); auto-extracting mentions is deferred until Tier-2 mention confidence is calibrated. Decision: keep extraction explicit. @Uczestnicyvs@PU—@PUremains authoritative for character participation;@Uczestnicyrecords attendance without a grant; the two are unioned for distribution.- Multi-day end-date validation — the
/DD·/MM-DDsuffix is composed textually, never range-checked against the start. Decision: leave unvalidated until a real corpus case demands it.
See also¶
- Sessions overview — the operator introduction to this topic
- Record a session — the step-by-step recipe
- Session metadata reference — exact grammar per key
- Architecture — the daemon/client contract every page assumes
- API reference — routes, cmdlet mapping, envelopes, capabilities
- Entity model —
@plik, entity tags, the 7 types - PU model — how
@PUbecomes the monthly batch - Name resolution — participant and mention resolution
- Players model — Gracz/Postać participant model
- Locations model —
@Lokacjepaths and->routes - Logs model —
@Logifetch/parse,@Inteldelivery - Currency model —
@Transferand the apply-transfers workflow - Adoption — how the import reads the legacy shapes as-is