mirror of
https://github.com/AventurasTeam/Aventuras.git
synced 2026-10-19 09:34:41 +00:00
Design mentioned places for the periodic classifier
Resolves the mentioned-places half of the factions-and-mentioned-places followup: behind a story setting, off by default, a pass creates places the prose mentions without entering, matched by the per-turn sibling rule, and writes only when no turn is in flight on its branch, so the turn wins a race to create the same place. principles.md now records the 2026-10-12 decision that the periodic classifier sees every row whatever its injection mode. New: an implementation followup; the followup narrowed to factions, with the state-written-whole problem; a parked entry for placing a mentioned place later; a triage item on a pass outliving its story. Record: docs/explorations/2026-10-16-mentioned-places.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d9f5095840
commit
d1388fca2a
17 changed files with 530 additions and 124 deletions
|
|
@ -605,11 +605,11 @@ shape for templates to consume.
|
|||
Memory-state writes split across three time scales per the cadence
|
||||
stratification in [`memory/cadence.md`](./memory/cadence.md):
|
||||
|
||||
| Layer | Trigger | Scope |
|
||||
| -------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Piggyback** | Every AI reply, inline on the narrative call (capability-gated) | Scene-local fast-mutating state — `sceneEntities`, `currentLocationId`, `worldTime`, visual mutations, item transfers, and the new locations and items they name |
|
||||
| **Periodic classifier** | Background, configurable cadence (`stories.settings.classifierCadence`) | Multi-turn batch extractions — happenings, involvements, awareness, new characters, entity status flips, keyword appends, character relationships |
|
||||
| **Chapter-close pipeline** | Token threshold crossed OR user-triggered | 5-phase pipeline: catch-up classifier, boundary, metadata, lore-mgmt (5 sub-jobs), lifecycle review |
|
||||
| Layer | Trigger | Scope |
|
||||
| -------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Piggyback** | Every AI reply, inline on the narrative call (capability-gated) | Scene-local fast-mutating state — `sceneEntities`, `currentLocationId`, `worldTime`, visual mutations, item transfers, and the new locations and items they name |
|
||||
| **Periodic classifier** | Background, configurable cadence (`stories.settings.classifierCadence`) | Multi-turn batch extractions — happenings, involvements, awareness, new characters and (by story setting) mentioned places, entity status flips, keyword appends, character relationships |
|
||||
| **Chapter-close pipeline** | Token threshold crossed OR user-triggered | 5-phase pipeline: catch-up classifier, boundary, metadata, lore-mgmt (5 sub-jobs), lifecycle review |
|
||||
|
||||
The periodic classifier is itself a Pipeline (per
|
||||
[`generation-pipeline.md`](./generation-pipeline.md)) — same
|
||||
|
|
@ -634,8 +634,8 @@ Two agents from the prior design were collapsed:
|
|||
reliable at tagged blocks), its per-turn fallback classifier, and the
|
||||
periodic classifier (background; handles the larger multi-turn
|
||||
batch). Parity holds between **piggyback and its per-turn fallback**,
|
||||
which cover the same write set — the periodic classifier's set is
|
||||
disjoint from both and could never recover their fields, which is why
|
||||
which cover the same write set — the periodic classifier's set
|
||||
covers none of their fields and could never recover them, which is why
|
||||
a failed parse falls back to a per-turn pass rather than waiting for
|
||||
the background one (see
|
||||
[`memory/piggyback.md → Parse strategy and failure recovery`](./memory/piggyback.md#parse-strategy-and-failure-recovery)).
|
||||
|
|
|
|||
|
|
@ -774,10 +774,12 @@ rejection that did reach the orchestrator would fail the whole run
|
|||
[`generation-pipeline.md → Transaction lifecycle`](./generation-pipeline.md#transaction-lifecycle)),
|
||||
and neither the phase nor the LLM retry tier can recover it — the retry
|
||||
finishes before any write is emitted, and a phase never sees a write's
|
||||
result. The one machine path that writes `parent_location_id` is the
|
||||
per-turn writer creating a location, which on a broken chain creates
|
||||
it at the root
|
||||
([`piggyback.md → New locations and items`](./memory/piggyback.md#new-locations-and-items));
|
||||
result. The machine paths that write `parent_location_id` are the
|
||||
per-turn writer creating a location and the periodic classifier
|
||||
creating a mentioned place, which on a broken chain create it at the
|
||||
root
|
||||
([`piggyback.md → New locations and items`](./memory/piggyback.md#new-locations-and-items),
|
||||
[`classifier.md → Mentioned places`](./memory/classifier.md#mentioned-places));
|
||||
re-parenting an existing location stays user-only. User edits surface as a form-validation error in the World
|
||||
panel — user fixes or leaves it.
|
||||
|
||||
|
|
@ -813,9 +815,10 @@ Every write that could break it refuses, or redirects:
|
|||
retired one. The `createEntity` handler refuses such a create too, a
|
||||
backstop behind the writers' own checks: a refusal there fails a
|
||||
machine writer's whole run, so its planner redirects first;
|
||||
- a per-turn create naming a retired parent goes to the root, logged,
|
||||
as an unresolvable parent does
|
||||
([`piggyback.md → Creating a place or an item`](./memory/piggyback.md#creating-a-place-or-an-item));
|
||||
- a per-turn create or a pass's mentioned place naming a retired
|
||||
parent goes to the root, logged, as an unresolvable parent does
|
||||
([`piggyback.md → Creating a place or an item`](./memory/piggyback.md#creating-a-place-or-an-item),
|
||||
[`classifier.md → Mentioned places`](./memory/classifier.md#mentioned-places));
|
||||
- a location merge refuses a result that breaks it
|
||||
([`world.md → Merge`](./ui/screens/world/world.md#merge)).
|
||||
|
||||
|
|
@ -1144,21 +1147,21 @@ via dotted paths. The split:
|
|||
|
||||
Per-field "who writes / when":
|
||||
|
||||
| Field group | First write | Subsequent writes |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `description` (top-level) | Whoever spawns the entity | User-only in v1 |
|
||||
| `name_collision_flag`, `_partner_id`, `_reason` (top-level) | Classifier (Layer B) or the per-turn writer, at create | User-only: clearing, or a merge re-pointing the partner (with the deleted row's reason when it takes over the deleted row's partner); delta-logged |
|
||||
| `visual.*` | Classifier from prose, or user via form | Both — classifier evolves on observed prose change |
|
||||
| `traits`, `drives` | Classifier from prose, or user via form | Classifier (chapter-close lore-mgmt only) + user via form |
|
||||
| `voice` | Classifier from prose, or user via form | Both |
|
||||
| `current_location_id` | Classifier per-turn | Classifier per-turn primary; user can edit |
|
||||
| `equipped_items`, `inventory`, `stackables` | Classifier per-turn | Classifier per-turn primary; user can edit |
|
||||
| `faction_id` | Classifier or user | Both |
|
||||
| `lastSeenAt` | Classifier-only | Classifier-only |
|
||||
| `parent_location_id` | User, or the per-turn writer creating the location | User-only in v1 |
|
||||
| `condition` (Location/Item) | Classifier or user | Both |
|
||||
| `standing`, `agenda` (Faction) | Classifier or user | Classifier (chapter-close) + user |
|
||||
| `at_location_id` (Item) | Classifier per-turn | Classifier per-turn primary; user can edit |
|
||||
| Field group | First write | Subsequent writes |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `description` (top-level) | Whoever spawns the entity | User-only in v1 |
|
||||
| `name_collision_flag`, `_partner_id`, `_reason` (top-level) | Classifier (Layer B, or a mentioned place's sibling rule) or the per-turn writer, at create | User-only: clearing, or a merge re-pointing the partner (with the deleted row's reason when it takes over the deleted row's partner); delta-logged |
|
||||
| `visual.*` | Classifier from prose, or user via form | Both — classifier evolves on observed prose change |
|
||||
| `traits`, `drives` | Classifier from prose, or user via form | Classifier (chapter-close lore-mgmt only) + user via form |
|
||||
| `voice` | Classifier from prose, or user via form | Both |
|
||||
| `current_location_id` | Classifier per-turn | Classifier per-turn primary; user can edit |
|
||||
| `equipped_items`, `inventory`, `stackables` | Classifier per-turn | Classifier per-turn primary; user can edit |
|
||||
| `faction_id` | Classifier or user | Both |
|
||||
| `lastSeenAt` | Classifier-only | Classifier-only |
|
||||
| `parent_location_id` | User, the per-turn writer creating the location, or the periodic classifier creating a mentioned place | User-only in v1 |
|
||||
| `condition` (Location/Item) | Classifier or user | Both |
|
||||
| `standing`, `agenda` (Faction) | Classifier or user | Classifier (chapter-close) + user |
|
||||
| `at_location_id` (Item) | Classifier per-turn | Classifier per-turn primary; user can edit |
|
||||
|
||||
Manual user edit vs classifier overwrite policy is parked as an
|
||||
architecture concern. v1 lean: classifier writes from prose-evidenced
|
||||
|
|
@ -1324,6 +1327,7 @@ stories.settings: {
|
|||
classifierCadence: number // entries between periodic classifier runs in the background; checked after each completed turn, so a run can fire up to one entry past it. Cadence-vs-window overlap warning only fires in partial mode (full mode catches up unclassified entries at chapter close)
|
||||
classifierContextEntries: number // default 4, MINIMUM 2; trailing entries the per-turn fallback classifier sees. The floor is the fixed action-plus-reply pair it extracts from — see docs/memory/cadence.md → User-tunable knobs
|
||||
piggybackMode: 'on' | 'off' // capability-gated; on = narrative emits structured trailing block; off = separate per-turn classifier pass
|
||||
classifierMentionedPlaces: boolean // default false; the periodic classifier also creates places the prose mentions but the scene doesn't enter. Read once per pass. See docs/memory/classifier.md → Mentioned places
|
||||
embeddingBackend: 'provider' | 'local' // embedding runtime (provider endpoint OR bundled local ONNX); both produce identical retrieval algorithm
|
||||
embedding_model_id: string // canonical embedding model id; copied from app_settings.embedding_model_id at story creation. Locked thereafter unless the user explicitly re-indexes via the model swap UX. Different stories may carry different model ids; vec0 partitions per branch. See docs/memory/retrieval.md → Storage and Model swap UX
|
||||
embedding_swap_target?: string // model id of the in-flight re-index target. Non-null while a stage-then-flip swap is in progress; cleared atomically with the swap's Phase 2 commit. Crash recovery on story open surfaces a resume/cancel prompt when this is set. See docs/memory/retrieval.md → Model swap UX
|
||||
|
|
|
|||
171
docs/explorations/2026-10-16-mentioned-places.md
Normal file
171
docs/explorations/2026-10-16-mentioned-places.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# Mentioned places
|
||||
|
||||
Resolves the mentioned-places half of the entry in
|
||||
`followups.md → Data-model` on the periodic classifier creating
|
||||
factions and mentioned places. The factions half stays there. The
|
||||
commit that lands this record adds an implementation entry: the
|
||||
design is settled, the code is not.
|
||||
|
||||
The substrate is the per-turn writer's place creates and matcher
|
||||
([`piggyback.md → New locations and items`](../memory/piggyback.md#new-locations-and-items)),
|
||||
built by #604 and narrowed for retired and disabled places by #617.
|
||||
The spec now lives in
|
||||
[`classifier.md → Mentioned places`](../memory/classifier.md#mentioned-places)
|
||||
(the setting, what the pass sees and emits, the row and its parent, the
|
||||
matcher),
|
||||
[`generation-pipeline.md → No-gate write phase`](../generation-pipeline.md#no-gate-write-phase)
|
||||
(a pass writes only when no turn is in flight) and
|
||||
[`cadence.md → Concurrency`](../memory/cadence.md#concurrency). What
|
||||
follows is why.
|
||||
|
||||
## What this closes vs. defers
|
||||
|
||||
Closes:
|
||||
|
||||
- **Mentioned places.** A place the prose establishes but the scene
|
||||
doesn't enter, which the per-turn writer drops as prose, is created
|
||||
by the periodic classifier behind a story setting.
|
||||
- **Which writer wins in one window.** The turn, by sequencing: a pass
|
||||
never writes while a turn is in flight on its branch.
|
||||
- **The closure-gap re-check** the parked entry asks for when the
|
||||
classifier creates entities other than characters, for places.
|
||||
|
||||
Defers:
|
||||
|
||||
- **Implementation**, to a standalone PR (the followups entry).
|
||||
- **Factions**, the rest of the followup, with the problem this design
|
||||
found for it: membership facts make a pass write `entities.state`,
|
||||
which a World Save writes whole.
|
||||
- **Placing a mentioned place later**: re-parenting a place at the root
|
||||
once the prose places it, a kind of place outside the tree for
|
||||
places nobody can locate yet, and World's missing merge of two
|
||||
unflagged rows. Parked.
|
||||
|
||||
## Ground truth that shaped it
|
||||
|
||||
- **This is enrichment.** The 2026-10-10 design moved mentioned places
|
||||
from a per-turn toggle to the classifier because nothing breaks
|
||||
while they're missing. Their job is mainly the narrator's
|
||||
consistency (a place the story established keeps its name, its place
|
||||
in the tree and its first description), and secondly the world
|
||||
record, which also tells the user which places exist.
|
||||
- **A per-turn place is thin.** `newRow`
|
||||
(`lib/pipeline/definitions/per-turn-creates.ts`) stores a name, a
|
||||
clamped description and a parent, `active`, `auto`, with no keywords
|
||||
or `condition`. A place made from a mention has the same shape, so
|
||||
two rows for one place differ only in which description they carry,
|
||||
and the entering turn's is usually the better one.
|
||||
- **The per-turn writer matches against the entities it read when its
|
||||
narrative phase started** (`lib/pipeline/definitions/per-turn.ts`,
|
||||
the working set at the phase's start, used by `planTurnCreates`
|
||||
after the stream), and a pass's writes can land mid-stream: the
|
||||
classifier is `no-gate`, and neither pipeline's `blockedBy` names the
|
||||
other. The common flow puts them together: a turn mentions a place
|
||||
and crosses the cadence, the pass starts, and the next turn, the one
|
||||
the user sends, goes there. A pass that creates the place during that
|
||||
turn's stream leaves the writer creating a second, unflagged one.
|
||||
- **The pass already re-reads after its model call**
|
||||
(`lib/pipeline/definitions/periodic-classifier.ts`), but it plans
|
||||
before taking its write lock, with the character reconciliation's
|
||||
embed calls in between.
|
||||
- **Only two things wait on a pass.** A prose reversal waits with
|
||||
`cancel`, and runs outside any turn (regenerate brackets before and
|
||||
after its run, `lib/actions/turns/regenerate-turn.ts`). Chapter
|
||||
close's phase 0 waits with `finish`, and a chained close starts after
|
||||
the turn has left `txState`.
|
||||
- **A prose edit's sweep spares entity creates of every kind**
|
||||
(`isReversible` in `lib/actions/story-entries/classifier-facts.ts`),
|
||||
CTRL-Z skips classifier groups, and a pass's abort runs inside its
|
||||
lock.
|
||||
- **Layer A hides a staged entity whose name is in recent prose**
|
||||
([`edge-cases.md → Layer A`](../memory/edge-cases.md#layer-a--retrieval-time-same-name-suppression)).
|
||||
A mentioned place's name always is.
|
||||
|
||||
## Decisions
|
||||
|
||||
**Behind a story setting, off by default.** Until now the map grew
|
||||
only with places the story visited; with this setting it grows with
|
||||
every place the story mentions. That's the cost the user opts into.
|
||||
|
||||
**Created `active`, not `staged`.** "Exists, not yet visited" reads
|
||||
like `staged`, but Layer A would then keep the place out of retrieval
|
||||
at exactly the turns that mention it, which defeats the second
|
||||
purpose. The map, exempt from Layer A, would show it either way.
|
||||
|
||||
**Which mentions count.** Any named place the prose establishes as
|
||||
real in the story's world, however far off ("she grew up in Varn"), and
|
||||
an unnamed one with an identity the story could return to; not a
|
||||
generic kind of place, not one in a hypothetical, a dream or a figure
|
||||
of speech, and not a listed place under another name, which becomes an
|
||||
alias. Rejected: only places the story points toward, which needs the
|
||||
model to guess the story's direction; and any place the prose
|
||||
describes, which is noise for little consistency.
|
||||
|
||||
**The pass sees every place as a tree, whether or not the setting is
|
||||
on.** It needs the tree to parent a new place, and the nesting helps it
|
||||
tell one "back room" from another when it names a happening's site.
|
||||
Rejected: the narrator's map, which hides places the pass must still
|
||||
name; and a tree only when the setting is on, which keeps two prompt
|
||||
shapes for one gain.
|
||||
|
||||
**An unknown parent puts the place at the root, where it stays.** After
|
||||
the first write, `parent_location_id` is the user's. Rejected for now:
|
||||
a pass re-parenting a root place once the prose places it, which widens
|
||||
the authorship contract and needs user precedence and reversal handling
|
||||
for the parent, and breaks `same-parent` flags; and holding a place
|
||||
back until its parent is known, which can't tell a top-level region
|
||||
from a place whose parent the model doesn't know. Parked, with the
|
||||
developer's idea of a kind of place that sits outside the tree.
|
||||
|
||||
**The turn wins, by sequencing.** A pass writes only when no turn is in
|
||||
flight on its branch, and plans its places inside its write lock
|
||||
against the rows as they stand. In an overlap the turn's create always
|
||||
commits first and the pass reuses it: the turn wrote from the newest
|
||||
prose. Rejected: whoever commits first wins, with both writers
|
||||
re-matching, where the pass's older description wins the race and
|
||||
making the turn win means a second machine-written description against
|
||||
the authorship contract; and flagging instead of resolving, which
|
||||
leaves every race as a pair to merge.
|
||||
|
||||
**The pass matches by the per-turn writer's sibling rule, over real
|
||||
children.** One rule for both writers keeps World's `same-parent`
|
||||
review the single place a doubtful pair goes. The per-turn writer
|
||||
rebuilds the set its map hid; the pass saw every place, so its real
|
||||
children are the whole set. A reused staged place stays staged because
|
||||
a mention, unlike a move, doesn't bring the story there.
|
||||
|
||||
**The setting is read once per pass,** so a reply is never judged by a
|
||||
setting its prompt didn't follow.
|
||||
|
||||
## Adversarial pass
|
||||
|
||||
- **Load-bearing: nothing but the turn writes an entity's row while a
|
||||
turn is in flight.** Verified from the declarations: the turn's hard
|
||||
gate blocks user writes, every other kind is blocked by `per-turn` or
|
||||
chained after it, and the pass now waits. Assumed, not checked: the
|
||||
embedding sync stage and the background embed worker touch vectors
|
||||
and `embedding_stale` only.
|
||||
- **The admission race.** A turn starting between the pass's check and
|
||||
the end of its burst would reopen the race; the plan has to close it
|
||||
without refusing a Send.
|
||||
- **The pass re-reads global stores.** Nothing found cancels a pass
|
||||
when its story closes, so a re-read could see another branch's rows
|
||||
(unverified, and older than this design). The wait doesn't widen
|
||||
it, since it lasts only while a turn runs on the pass's branch, but
|
||||
planning under the lock should check the stores still hold its branch,
|
||||
as the per-turn working set does.
|
||||
- **Two parents, one place.** A place the pass puts at the root and a
|
||||
turn nests elsewhere are never compared, so they stay a duplicate,
|
||||
and World can't merge them. Parked with re-parenting.
|
||||
- **A mention edited away.** A prose edit's sweep keeps the place, as
|
||||
it keeps a classifier-made character, so it stays on the narrator's
|
||||
map until the user deletes it.
|
||||
- **The map's size.** Mentioned places add lines the structural floor
|
||||
pays for. Unmeasured; the token breakdown followup is the signal.
|
||||
- **The significance rule** is untested with real models; the setting
|
||||
is off by default.
|
||||
- **Starvation.** If turns ever ran back to back with no gap, a pass
|
||||
would never write.
|
||||
- **The closure gap.** Re-checked by reading the reversal canon and
|
||||
code: only the existing boot-recovery window can remove a pass's
|
||||
place while a later turn's JSON names it.
|
||||
|
|
@ -13,41 +13,49 @@ for the placement rule.
|
|||
|
||||
## Data-model
|
||||
|
||||
- **The periodic classifier creates factions and mentioned places.**
|
||||
Factions follow the character path: a new-faction list in the
|
||||
extraction, reconciled by Layer B
|
||||
- **Implement mentioned places.** Designed 2026-10-16
|
||||
([exploration record](./explorations/2026-10-16-mentioned-places.md));
|
||||
the spec is canon:
|
||||
[`classifier.md → Mentioned places`](./memory/classifier.md#mentioned-places)
|
||||
for the setting, the pass's place tree, its `newPlaces` list, the
|
||||
row, its parent and the matcher;
|
||||
[`generation-pipeline.md → No-gate write phase`](./generation-pipeline.md#no-gate-write-phase)
|
||||
for a pass writing only when no turn is in flight on its branch; and
|
||||
[`story-settings.md → Classifier`](./ui/screens/story-settings/story-settings.md#classifier)
|
||||
for the toggle. Three things the code must make sure of: a turn can't
|
||||
start between a pass's check and the end of its burst, and waits
|
||||
rather than being refused; the pass's planning inside its lock
|
||||
checks the stores still hold its branch, since nothing found cancels
|
||||
a pass when its story closes (triage carries the older character
|
||||
path); and the embedding sync stage and the background embed worker
|
||||
write only vectors and `embedding_stale` during a turn, which the
|
||||
canon's "nothing but the turn writes its branch's entities" assumes
|
||||
and the design didn't check.
|
||||
|
||||
- **The periodic classifier creates factions.** Factions follow the
|
||||
character path: a new-faction list in the extraction, reconciled by
|
||||
Layer B
|
||||
([`classifier.md → Disambiguation on new-character mentions`](./memory/classifier.md#disambiguation-on-new-character-mentions))
|
||||
with the same matcher, flag, partner and reason, plus membership
|
||||
facts writing characters' `faction_id`, which the authorship contract
|
||||
gives the classifier and nothing machine-writes yet. Mentioned places
|
||||
are places the prose establishes but the scene doesn't enter: the
|
||||
periodic classifier creates them behind a story setting that defaults
|
||||
to off, matched by the per-turn writer's sibling rule
|
||||
([`piggyback.md → Matching before creating`](./memory/piggyback.md#matching-before-creating))
|
||||
and parented from the window. The classifier sees every place, flat,
|
||||
so its matcher compares a new place with its real siblings; to
|
||||
parent one it gets the places as a full tree, every place in any mode
|
||||
or status with its id, in place of the flat location lines in Known
|
||||
entities, not the narrator's map, which hides places it must still
|
||||
name. Its planner sends a retired parent to the root, logged, as the
|
||||
per-turn writer does, since nothing active may sit inside a retired
|
||||
place
|
||||
([`data-model.md → LocationState shape`](./data-model.md#locationstate-shape));
|
||||
the create handler refuses one that slips through, which fails the
|
||||
whole pass. Open: which writer wins when the
|
||||
per-turn writer and a pass create the same new place in one window
|
||||
(leaning the pass, or the pair goes to collision review); and
|
||||
re-checking the parked
|
||||
[closure gap](./parked.md#the-reversal-closure-doesnt-follow-ids-inside-json-columns),
|
||||
since a pass is reversed by positional sweeps while later turns stay.
|
||||
Raised 2026-10-10.
|
||||
gives the classifier and nothing machine-writes yet. Membership facts
|
||||
make a pass write `entities.state`, which a World Save writes whole
|
||||
from the row as the pane rendered it, so a Save can overwrite a
|
||||
pass's membership
|
||||
([`cadence.md → User edits and classifier writes`](./memory/cadence.md#user-edits-and-classifier-writes));
|
||||
the design settles how the two merge. Open: re-checking the parked
|
||||
[closure gap](./parked.md#the-reversal-closure-doesnt-follow-ids-inside-json-columns)
|
||||
for the faction ids a pass writes into `state`. Raised 2026-10-10;
|
||||
mentioned places split off and were designed 2026-10-16.
|
||||
|
||||
- **Chapter-close agent design.** Chapter close is designed in
|
||||
[`memory/chapter-close.md`](./memory/chapter-close.md), but not
|
||||
concretely enough to implement. Promoted from the developer's notes
|
||||
2026-10-10, with an addition: upkeep of the locations and items the
|
||||
per-turn writer creates with only a name, a short description and a
|
||||
parent, meaning their state (`condition`), keywords and a
|
||||
parent, and of the places a pass creates from mentions
|
||||
([`classifier.md → Mentioned places`](./memory/classifier.md#mentioned-places)),
|
||||
meaning their state (`condition`), keywords and a
|
||||
description refresh. A refresh reverses the authorship contract's
|
||||
user-only description
|
||||
([`data-model.md → Authorship contract`](./data-model.md#authorship-contract)),
|
||||
|
|
|
|||
|
|
@ -381,7 +381,10 @@ create only places and items, with handles that start with `new:`,
|
|||
and accept a handle only where
|
||||
[`piggyback.md → Creating a place or an item`](./memory/piggyback.md#creating-a-place-or-an-item)
|
||||
allows; characters are the periodic classifier's
|
||||
([`classifier.md → Disambiguation on new-character mentions`](./memory/classifier.md#disambiguation-on-new-character-mentions)).
|
||||
([`classifier.md → Disambiguation on new-character mentions`](./memory/classifier.md#disambiguation-on-new-character-mentions)),
|
||||
which also creates places the prose mentions without entering, with
|
||||
the same `new:` handles
|
||||
([`classifier.md → Mentioned places`](./memory/classifier.md#mentioned-places)).
|
||||
|
||||
#### Failure modes
|
||||
|
||||
|
|
@ -1786,8 +1789,8 @@ const translationRetryPipeline: Pipeline = {
|
|||
| Running | Wants to start | Resolution |
|
||||
| --------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| (idle) | per-turn | starts |
|
||||
| per-turn | periodic-classifier | classifier's blockedBy lacks per-turn → starts |
|
||||
| periodic-classifier | per-turn | per-turn's blockedBy lacks classifier → starts; both run |
|
||||
| per-turn | periodic-classifier | classifier's blockedBy lacks per-turn → starts; its writes wait for the turn ([No-gate write phase](#no-gate-write-phase)) |
|
||||
| periodic-classifier | per-turn | per-turn's blockedBy lacks classifier → starts; both run, and the classifier's writes wait for the turn |
|
||||
| per-turn | chapter-close (manual) | chapter-close's blockedBy includes per-turn → blocked |
|
||||
| per-turn (committing) | chapter-close (chained) | chained path bypasses concurrencyPolicy → starts |
|
||||
| chapter-close | periodic-classifier | classifier's blockedBy includes chapter-close → blocked |
|
||||
|
|
@ -1811,7 +1814,10 @@ The architectural premise (`memory/cadence.md → Concurrency`) is that
|
|||
the periodic classifier and the per-turn pipeline write disjoint
|
||||
field sets at SQLite row granularity (with the documented
|
||||
`entities.status` monotonic-overlap caveat), so concurrent runs don't
|
||||
clobber each other. The classifier + chapter-close case is not
|
||||
clobber each other. Creating places is the one overlap disjointness
|
||||
can't cover, so it is sequenced instead: a pass writes only when no
|
||||
turn is in flight on its branch
|
||||
([No-gate write phase](#no-gate-write-phase)). The classifier + chapter-close case is not
|
||||
covered by field disjointness — chapter-close phase 0 instead drains
|
||||
the in-flight classifier explicitly before doing its own classifier
|
||||
work.
|
||||
|
|
@ -1972,10 +1978,10 @@ and opens only after the detail pane's dirty guard has awaited its
|
|||
Save, and Reader undo needs the Reader's focus. The build is
|
||||
synchronous, writes nothing and takes no lock, since the hold isn't
|
||||
reentrant. A `no-gate` run takes
|
||||
the lock exclusive at its first emitted write — after its model
|
||||
the lock exclusive before its first write — after its model
|
||||
call, embedding and reconciliation, so World stays editable through
|
||||
those — and waits for every shared holder, so a write already in
|
||||
flight lands first. The run's own writes go through
|
||||
those, and after the wait for a turn below — and waits for every
|
||||
shared holder, so a write already in flight lands first. The run's own writes go through
|
||||
inside the hold, which ends when the run settles: after the watermark
|
||||
write on success, after the abort's reversal on failure, whether it
|
||||
commits or leaves the run to boot recovery. Ending it on an
|
||||
|
|
@ -1997,6 +2003,27 @@ entity-delete and collision-merge actions take it, and the
|
|||
orchestrator commits a pass's writes through `applyDeltaAction`
|
||||
directly.
|
||||
|
||||
**A pass writes only when no turn is in flight on its branch.** Both
|
||||
writers create places
|
||||
([`classifier.md → Mentioned places`](./memory/classifier.md#mentioned-places)),
|
||||
and the per-turn writer matches its creates against the entities it
|
||||
read when its narrative phase started, so a place a pass created
|
||||
mid-stream would be one the turn never saw, and the turn would create
|
||||
it again. Before taking the lock exclusive, a pass waits until no
|
||||
`per-turn` run is in flight on its branch, then plans its places inside
|
||||
the hold, against the rows as they stand. A turn that would start
|
||||
between that check and the end of the burst starts after it, a few
|
||||
milliseconds later, and is never refused. A `'cancel'` during the wait
|
||||
discards the run, which has committed nothing. The wait is on
|
||||
`per-turn` runs only: chapter close's phase 0 drains the pass with
|
||||
`'finish'`, and a chained close starts once the turn has left
|
||||
`txState`, so waiting on chapter close too would deadlock. While a turn
|
||||
is in flight, nothing but the turn writes its branch's entities: its
|
||||
hard gate holds off user writes, every other kind is blocked by
|
||||
`per-turn` or chained after it, and a pass waits. The cost is a pass's
|
||||
writes landing up to one turn late, a bound that holds while turns
|
||||
leave a gap between them, as the user's next action always does.
|
||||
|
||||
### Chained start bypasses concurrencyPolicy
|
||||
|
||||
Per-turn's `chainsTo` returning `'chapter-close'` triggers a
|
||||
|
|
@ -2043,8 +2070,10 @@ to any of these alongside whatever causes the relaxation.
|
|||
the kind-keyed `generationContext` getter.
|
||||
- **Disjoint write sets between concurrent pipelines.** Per-turn
|
||||
(and its piggyback / fallback-classifier variants) writes a
|
||||
different field set from the periodic classifier, and creates
|
||||
different entity kinds. Prose-enforced
|
||||
different field set from the periodic classifier. Both create
|
||||
places, but never at once: a pass writes only when no turn is in
|
||||
flight on its branch
|
||||
([No-gate write phase](#no-gate-write-phase)). Prose-enforced
|
||||
via narrow action functions named for their field-set scope. Any
|
||||
new action function should be reviewed for write-set overlap
|
||||
with concurrent pipelines.
|
||||
|
|
|
|||
|
|
@ -400,6 +400,24 @@ slice-planning gate forces its resolution before that slice is planned.
|
|||
(`lib/prompts/filters.ts`), so every entity disabled by its own mode
|
||||
reaches it with its id, characters, items and factions too. Raised
|
||||
2026-10-12 by the retired and disabled places implementation.
|
||||
Since 2026-10-16, `principles.md` says the mode governs what the
|
||||
narrator sees and the periodic classifier sees every row whatever its
|
||||
mode, which the 2026-10-12 design decided for the periodic classifier;
|
||||
the fallback, which extracts but can also move the scene, is the case
|
||||
that rule doesn't settle.
|
||||
|
||||
- **A periodic pass isn't cancelled when its story closes.** After its
|
||||
model call the pass re-reads the global entity stores
|
||||
(`lib/pipeline/definitions/periodic-classifier.ts`, the re-read before
|
||||
reconciliation) and filters them by its branch, with no check that the
|
||||
stores still hold that branch, as `loadPerTurnWorkingSet` makes for a
|
||||
turn. Nothing found in `lib/stores/generation` or the story actions
|
||||
cancels a pass when the user closes the story or switches branch, so a
|
||||
pass finishing then could reconcile its new characters against an
|
||||
empty roster and create them unmatched. Unverified: not traced through
|
||||
story close, and not reproduced. The mentioned-places implementation
|
||||
makes the same check for places; this item is the character path,
|
||||
which predates it. Raised 2026-10-16 by the mentioned places design.
|
||||
|
||||
- **The scene editor can write a character into a retired place.**
|
||||
Removing a character from the tail's scene sets their
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ the shape.
|
|||
across a story.
|
||||
- **[classifier.md](./classifier.md)** — periodic classifier
|
||||
contract: what it reads, write set, disambiguation on new-character
|
||||
mentions, background-task framing.
|
||||
mentions, mentioned places, background-task framing.
|
||||
- **[chapter-close.md](./chapter-close.md)** — 5-phase chapter-close
|
||||
pipeline: catch-up, boundary selection, metadata, lore-mgmt
|
||||
(5 sub-jobs), lifecycle review, failure modes, manual close.
|
||||
|
|
|
|||
|
|
@ -159,25 +159,30 @@ bias prompts more rigorously.
|
|||
|
||||
The piggyback agent and the periodic classifier write to disjoint
|
||||
field sets, with one documented overlap on `entities.status`. Both
|
||||
create entities, but of disjoint kinds: piggyback the locations and
|
||||
items its state names
|
||||
create entities: piggyback the locations and items its state names
|
||||
([`piggyback.md → New locations and items`](./piggyback.md#new-locations-and-items)),
|
||||
the classifier characters.
|
||||
the classifier characters and, with the story's setting on, the places
|
||||
the prose mentions without entering
|
||||
([`classifier.md → Mentioned places`](./classifier.md#mentioned-places)).
|
||||
Places are the one kind both create, so the two never write at once: a
|
||||
pass writes only when no turn is in flight on its branch
|
||||
([`generation-pipeline.md → No-gate write phase`](../generation-pipeline.md#no-gate-write-phase)),
|
||||
and the turn's read of the entities stays current for its whole run.
|
||||
|
||||
| Field | Piggyback | Classifier |
|
||||
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `story_entries.metadata` (current entry) | ✓ | — |
|
||||
| `entities.state.visual.*` | ✓ | — |
|
||||
| `entities.state` (location, equipped, inventory, stackables, lastSeenAt) | ✓ | — |
|
||||
| `entities.status` | ✓ (staged → active only, on a `sceneEntities` or `<current_location>` ID hit, or a reused staged place) | ✓ (staged → active slow path; active → retired) |
|
||||
| `entities` creates (name, description, collision flag, partner, reason) | ✓ (locations and items) | ✓ (characters) |
|
||||
| `entities.description` | ✓ (its own creates only) | ✓ (first introduction only; see authorship contract) |
|
||||
| `entities.keywords` | — | ✓ (append-only; new characters and later passes) |
|
||||
| `entities.retired_reason` | — | ✓ (with active → retired) |
|
||||
| `happenings` | — | ✓ |
|
||||
| `happening_involvements` | — | ✓ |
|
||||
| `happening_awareness` | — | ✓ |
|
||||
| `character_relationships` | — | ✓ |
|
||||
| Field | Piggyback | Classifier |
|
||||
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| `story_entries.metadata` (current entry) | ✓ | — |
|
||||
| `entities.state.visual.*` | ✓ | — |
|
||||
| `entities.state` (location, equipped, inventory, stackables, lastSeenAt) | ✓ | — |
|
||||
| `entities.status` | ✓ (staged → active only, on a `sceneEntities` or `<current_location>` ID hit, or a reused staged place) | ✓ (staged → active slow path; active → retired) |
|
||||
| `entities` creates (name, description, a place's parent, collision flag, partner, reason) | ✓ (locations and items) | ✓ (characters; mentioned places, never during a turn) |
|
||||
| `entities.description` | ✓ (its own creates only) | ✓ (first introduction only; see authorship contract) |
|
||||
| `entities.keywords` | — | ✓ (append-only; new characters and places, later passes) |
|
||||
| `entities.retired_reason` | — | ✓ (with active → retired) |
|
||||
| `happenings` | — | ✓ |
|
||||
| `happening_involvements` | — | ✓ |
|
||||
| `happening_awareness` | — | ✓ |
|
||||
| `character_relationships` | — | ✓ |
|
||||
|
||||
**Field-overlap invariant.** `entities.status` is the only field
|
||||
both writers touch. They never collide on the same entity at the
|
||||
|
|
|
|||
|
|
@ -18,12 +18,15 @@ refer to rather than pointing above. Its user half carries the
|
|||
branch's world as lists the classifier addresses by placeholder id
|
||||
([ID handling](#id-handling-in-classifier-output)), then the turns:
|
||||
|
||||
- **Known entities** — every entity on the branch, of any kind and
|
||||
status, with its description and keywords. The classifier sees the
|
||||
whole roster, so the
|
||||
- **Known entities** — every character, item and faction on the
|
||||
branch, of any status, with its description and keywords. The
|
||||
classifier sees the whole roster, so the
|
||||
[reconciliation](#disambiguation-on-new-character-mentions)
|
||||
below is a backstop for a row the model missed, not a lookup the
|
||||
model can't do.
|
||||
- **Places** — every place on the branch, of any status and mode, as
|
||||
a tree with the same details per line
|
||||
([Mentioned places](#mentioned-places)).
|
||||
- **Known relationships** — the stored views, as the relationships
|
||||
bullet below describes.
|
||||
- **Known happenings** — the branch's happenings by title.
|
||||
|
|
@ -67,11 +70,14 @@ alias list (the keywords bullet below).
|
|||
prose stands too, even when written before the pass started (see
|
||||
[`cadence.md → User edits and classifier writes`](./cadence.md#user-edits-and-classifier-writes)).
|
||||
|
||||
- **Mentioned places** — with the story's setting on, the places the
|
||||
turns mention but the scene doesn't enter
|
||||
([Mentioned places](#mentioned-places)).
|
||||
- **First-introduction descriptions** — when the classifier creates
|
||||
a character, one the
|
||||
[reconciliation](#disambiguation-on-new-character-mentions) didn't
|
||||
absorb into an existing row, it authors the initial `description`
|
||||
from prose. After
|
||||
absorb into an existing row, or a mentioned place, it authors the
|
||||
initial `description` from prose. After
|
||||
first introduction, the classifier never amends `description` (the
|
||||
authorship contract in
|
||||
[`data-model.md → World-state storage`](../data-model.md#world-state-storage)
|
||||
|
|
@ -210,10 +216,11 @@ are not compared with each other.
|
|||
keywords hold "the innkeeper", against a row named "the Innkeeper").
|
||||
|
||||
The rule pairs rows of one kind, works for any kind, and lives in
|
||||
`lib/world` beside World's pairing. The classifier reconciles
|
||||
characters, the only kind it creates; the per-turn writer's locations
|
||||
and items match under their own rules
|
||||
([`piggyback.md → New locations and items`](./piggyback.md#new-locations-and-items)). A name in a script written without spaces never
|
||||
`lib/world` beside World's pairing. The classifier reconciles the
|
||||
characters it creates; the places it creates, and the per-turn
|
||||
writer's locations and items, match under their own rules
|
||||
([Mentioned places](#mentioned-places),
|
||||
[`piggyback.md → New locations and items`](./piggyback.md#new-locations-and-items)). A name in a script written without spaces never
|
||||
contains another, so it matches only exactly or through a keyword.
|
||||
|
||||
**Signals.**
|
||||
|
|
@ -264,9 +271,10 @@ from that partner:
|
|||
| `in-scene` | Partial, `sim < τ_low`, in the scene |
|
||||
| `no-signal` | No score |
|
||||
|
||||
The per-turn writer adds one more reason, `same-parent`, for a
|
||||
location it creates beside a similarly named sibling, and gives an
|
||||
item's exact namesake `alike` or `ambiguous` by the same bands. World pairs a
|
||||
The per-turn writer and a pass creating a mentioned place add one more
|
||||
reason, `same-parent`, for a location created beside a similarly named
|
||||
sibling, and the per-turn writer gives an item's exact namesake `alike`
|
||||
or `ambiguous` by the same bands. World pairs a
|
||||
flagged row with its partner and states the reason in the resolve
|
||||
dialog
|
||||
([`world.md → Collision review and entity merge`](../ui/screens/world/world.md#collision-review-and-entity-merge)).
|
||||
|
|
@ -275,19 +283,103 @@ Thresholds (`τ_high`, `τ_low`) are tunable. Defaults TBD empirically
|
|||
once real story data exists; sensible starting ranges (e.g. 0.75 /
|
||||
0.50 cosine) until tuning lands.
|
||||
|
||||
## Mentioned places
|
||||
|
||||
With `stories.settings.classifierMentionedPlaces` on, off by default,
|
||||
a pass also creates the places the turns mention but the scene doesn't
|
||||
enter, which the per-turn writer leaves as prose
|
||||
([`piggyback.md → Creating a place or an item`](./piggyback.md#creating-a-place-or-an-item)).
|
||||
They keep the narrator consistent: a place the story established stays
|
||||
on the `# Places` map, under its parent, with the description it was
|
||||
first given, and World shows which places exist. The cost is a map that
|
||||
grows with every place the story mentions, not only the ones it visits.
|
||||
The pass reads the setting once, when it builds its prompt, so a reply
|
||||
carrying new places creates them even if the setting was turned off
|
||||
while the model ran. Turning it on doesn't revisit prose an earlier
|
||||
pass read.
|
||||
|
||||
**What the pass sees.** Places aren't in Known entities. A `Places`
|
||||
list nests every place on the branch by `parent_location_id`, children
|
||||
sorted by name, whatever its status or mode, each with its placeholder
|
||||
id, a status note when it isn't active, its description and keywords.
|
||||
Unlike the narrator's map
|
||||
([`piggyback.md → What the writer sees`](./piggyback.md#what-the-writer-sees))
|
||||
it hides nothing, and a retired place keeps its id, since a happening
|
||||
can still name it as where something happened. The tree renders
|
||||
whether or not the setting is on: its nesting tells one back room from
|
||||
another when the pass names a happening's site.
|
||||
|
||||
**What it emits.** With the setting on, the reply's schema carries a
|
||||
`newPlaces` list and the system half carries the rule below; with it
|
||||
off, neither appears. An entry has a handle starting `new:`, in one
|
||||
namespace with the `newCharacters` handles, a name, a description,
|
||||
keywords, a `sourceTurn` and an optional `parent`: a place's id from the
|
||||
tree or another new place's handle. The handle stands wherever a
|
||||
listed place's id would: in a happening's involvements and as another
|
||||
new place's parent. The rule:
|
||||
|
||||
> Add a place the turns mention but the scene doesn't enter when the
|
||||
> prose establishes it exists in the story's world with its own
|
||||
> identity: a named place (a town, a keep, a region, an inn), or an
|
||||
> unnamed one the story could come back to ("the old mill on the
|
||||
> ridge"). Not a generic kind of place ("a tavern", "the road north"),
|
||||
> not one that exists only in a hypothetical, a dream or a figure of
|
||||
> speech, and not a place already in the Places list, even under
|
||||
> another name: use its ID and add the name form as an alias.
|
||||
|
||||
**The row.** `active`, mode `auto`, the entry's keywords and its
|
||||
description, clamped to a new character's bounds (the first
|
||||
introduction above), landing `embedding_stale = 1` with `sourceTurn` as
|
||||
its survival anchor. Not `staged`, though the story hasn't been there:
|
||||
Layer A would keep a staged place out of retrieval on exactly the turns
|
||||
that name it
|
||||
([`edge-cases.md → Layer A`](./edge-cases.md#layer-a--retrieval-time-same-name-suppression)).
|
||||
|
||||
**Parent.** Parents resolve top-down, so the child of a new place hangs
|
||||
under whatever row that place resolved to. A place whose parent the
|
||||
turns don't give goes to the root and stays there, since only the user
|
||||
moves a place after its first write
|
||||
([`data-model.md → Authorship contract`](../data-model.md#authorship-contract)).
|
||||
A retired parent, a parent that names nothing, or one whose chain is
|
||||
already broken above it puts the place at the root too, logged, as it
|
||||
does for the per-turn writer
|
||||
([`piggyback.md → Creating a place or an item`](./piggyback.md#creating-a-place-or-an-item)).
|
||||
The create handler refuses a place under a retired parent that slips
|
||||
through, which fails the whole pass.
|
||||
|
||||
**Matching.** Each new place is matched by the per-turn writer's
|
||||
sibling rule
|
||||
([`piggyback.md → Matching before creating`](./piggyback.md#matching-before-creating)),
|
||||
with its parent's real children as the candidates, since the pass saw
|
||||
every place. An exact name reuses an active or staged sibling, never a
|
||||
retired one; an exact retired namesake or a partial match creates the
|
||||
row with `name_collision_flag = 1`, reason `same-parent`, the partner
|
||||
picked in that rule's order; places under different parents are never
|
||||
compared. A reused sibling keeps its description and its status, since
|
||||
a mention doesn't enter a staged place, gains the entry's new keywords,
|
||||
and stands for the handle in the rest of the reply. Two new places with
|
||||
one name and parent map to the first.
|
||||
|
||||
**When it writes.** The pass plans its places inside its write lock,
|
||||
against the rows as they stand, and takes that lock only once no turn
|
||||
is in flight on its branch
|
||||
([`generation-pipeline.md → No-gate write phase`](../generation-pipeline.md#no-gate-write-phase)).
|
||||
So when a turn and a pass both create a place, the turn, which wrote
|
||||
from the newer prose, commits first, and the pass reuses its row.
|
||||
|
||||
## Background-task framing
|
||||
|
||||
The periodic classifier runs as a background pipeline — a Pipeline
|
||||
declaration in the framework's registry, same shape as per-turn and
|
||||
chapter-close but with different concurrency / gating values:
|
||||
|
||||
| Field | Value |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `kind` | `'periodic-classifier'` |
|
||||
| `gateBehavior` | `'no-gate'` — doesn't block user-source writes; they wait out its write phase ([no-gate write phase](../generation-pipeline.md#no-gate-write-phase)) |
|
||||
| `concurrencyPolicy` | `{ blockedBy: ['periodic-classifier', 'chapter-close'] }` — no double passes; blocked from starting during chapter-close |
|
||||
| `affordance` | `'pill-only'` — folds into the generation indicator at low priority (see below) |
|
||||
| Write set (prose, not declared) | happenings, happening_involvements, happening_awareness, new character entities (first-introduction description, keywords, collision flag with its partner and reason), entity status flips with retired reasons, entity keyword appends (aliases and absorbed candidates), character relationships |
|
||||
| Field | Value |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `kind` | `'periodic-classifier'` |
|
||||
| `gateBehavior` | `'no-gate'` — doesn't block user-source writes; they wait out its write phase ([no-gate write phase](../generation-pipeline.md#no-gate-write-phase)) |
|
||||
| `concurrencyPolicy` | `{ blockedBy: ['periodic-classifier', 'chapter-close'] }` — no double passes; blocked from starting during chapter-close |
|
||||
| `affordance` | `'pill-only'` — folds into the generation indicator at low priority (see below) |
|
||||
| Write set (prose, not declared) | happenings, happening_involvements, happening_awareness, new character entities and, with the setting on, mentioned places (first-introduction description, keywords, a place's parent, collision flag with its partner and reason), entity status flips with retired reasons, entity keyword appends (aliases, absorbed candidates and reused places), character relationships |
|
||||
|
||||
Write-set boundaries between the classifier and the piggyback / per-turn
|
||||
pipeline are enforced via narrow action functions named for field-set
|
||||
|
|
@ -296,11 +388,16 @@ scope (see
|
|||
not a typed declaration. The single-writer invariant relaxes to
|
||||
**single-writer-per-write-set** in v1; piggyback's write-set and the
|
||||
classifier's write-set are disjoint at the row-and-field granularity,
|
||||
apart from the monotonic `entities.status` overlap (see
|
||||
apart from the monotonic `entities.status` overlap, and both create
|
||||
places, the classifier only with the mentioned-places setting on. They
|
||||
never write at once: a pass writes only when no turn is in flight on
|
||||
its branch (see
|
||||
[`cadence.md → Concurrency`](./cadence.md#concurrency)).
|
||||
|
||||
If the user starts a new turn while the classifier is mid-run, both
|
||||
proceed. The classifier holds its own `actionId` for its writes; the
|
||||
run, and the pass's writes wait for the turn to finish
|
||||
([no-gate write phase](../generation-pipeline.md#no-gate-write-phase)).
|
||||
The classifier holds its own `actionId` for its writes; the
|
||||
user-turn pipeline holds its own. That `actionId` is stamped
|
||||
`source = periodic_classifier` (distinct from per-turn piggyback's
|
||||
`piggyback_tagged_block`) and is **never an undo target** — CTRL-Z's
|
||||
|
|
@ -311,8 +408,8 @@ It survives only for crash recovery.
|
|||
The classifier's `concurrencyPolicy.yieldsTo` stays empty — it does
|
||||
NOT abort itself when a foreground _forward_ pipeline starts.
|
||||
Discarding in-flight classifier work that doesn't conflict with the
|
||||
new turn's writes is wasteful; the disjoint-write-set property makes
|
||||
coexistence safe.
|
||||
new turn's writes is wasteful; the pass's model call runs alongside
|
||||
the turn and its writes wait for it, so coexistence is safe.
|
||||
|
||||
A _reversal_ is the exception, handled outside `yieldsTo`. Regenerate,
|
||||
entry-delete rollback, swipe-switch, and CTRL-Z of a turn reverse prose
|
||||
|
|
@ -322,9 +419,10 @@ sweep with `await awaitRunTerminal('periodic-classifier', 'cancel')` plus a
|
|||
[`generation-pipeline.md → Prose reversals and the classifier barrier`](../generation-pipeline.md#prose-reversals-and-the-classifier-barrier)).
|
||||
The classifier's part of that contract is one **abort-free critical
|
||||
section**: its commit burst — one LLM response, one burst of deltas —
|
||||
ignores `signal.aborted` once parsing begins and never returns
|
||||
ignores `signal.aborted` once it holds its write lock and never returns
|
||||
`aborted` holding committed deltas. So `'cancel'` either discards a
|
||||
not-yet-committed run (the LLM stream is cancelled, keeping the
|
||||
not-yet-committed run (the LLM stream is cancelled, or the
|
||||
reconciliation or the wait for a turn is cut short, keeping the
|
||||
reversal near-instant) or lets a committed burst stand for the
|
||||
positional sweep to reverse; the standard abort path needs no special
|
||||
disposition.
|
||||
|
|
@ -346,8 +444,9 @@ state for the active branch in a compact panel.
|
|||
|
||||
### Cadence config
|
||||
|
||||
In-place edit of `stories.settings.classifierCadence` and
|
||||
`piggybackMode`. The buffer-aware indicator from
|
||||
In-place edit of `stories.settings.classifierCadence`,
|
||||
`piggybackMode` and `classifierMentionedPlaces`
|
||||
([Mentioned places](#mentioned-places)). The buffer-aware indicator from
|
||||
[`cadence.md → User-tunable knobs`](./cadence.md#user-tunable-knobs)
|
||||
renders inline so the user sees the cadence-vs-recent-buffer
|
||||
overlap.
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ documents rather than chases.
|
|||
|
||||
Two layers handle the case where the AI invents a character whose name
|
||||
matches an existing (often staged) entity's, exactly or in part. A
|
||||
place or an item the per-turn writer creates is matched when it is
|
||||
created instead
|
||||
([`piggyback.md → Matching before creating`](./piggyback.md#matching-before-creating)).
|
||||
place or an item the per-turn writer creates, or a place the periodic
|
||||
classifier creates from a mention, is matched when it is created
|
||||
instead
|
||||
([`piggyback.md → Matching before creating`](./piggyback.md#matching-before-creating),
|
||||
[`classifier.md → Mentioned places`](./classifier.md#mentioned-places)).
|
||||
|
||||
### Layer A — retrieval-time same-name suppression
|
||||
|
||||
|
|
|
|||
|
|
@ -97,7 +97,12 @@ receives. Both are per-turn state, so a missing row makes that turn's
|
|||
location or inventory wrong at once. Characters are narrative
|
||||
introductions the periodic classifier makes
|
||||
([`classifier.md → Disambiguation on new-character mentions`](./classifier.md#disambiguation-on-new-character-mentions)),
|
||||
so the two write sets stay disjoint by kind.
|
||||
as are, with the story's setting on, the places the prose mentions
|
||||
without entering
|
||||
([`classifier.md → Mentioned places`](./classifier.md#mentioned-places)).
|
||||
A pass never writes while a turn is in flight on its branch, so the
|
||||
entities this writer read at the turn's start are still the branch's
|
||||
when it creates.
|
||||
|
||||
### What the writer sees
|
||||
|
||||
|
|
@ -202,7 +207,9 @@ tags still parses clean in a block that declares no creates.
|
|||
the last transfer in the block naming its handle gives it to a
|
||||
character, so an item taken and then put down in the same block stays
|
||||
prose. Anything else is dropped and logged, so a place the prose
|
||||
merely mentions, or an item only seen or put down, stays prose. A
|
||||
merely mentions, or an item only seen or put down, stays prose; the
|
||||
periodic classifier may create the place later
|
||||
([`classifier.md → Mentioned places`](./classifier.md#mentioned-places)). A
|
||||
block holding nothing but creates can keep none of them, so it counts
|
||||
as empty: it fails `state`, the fallback fires, and its creates report
|
||||
`dropped`. The prompt adds the significance rules the model applies:
|
||||
|
|
@ -341,8 +348,8 @@ turn, in the same pipeline run as the narrative call that produced
|
|||
the malformed block (same `action_id`, same hard gate — never a
|
||||
separate background pass). This replaces an earlier draft of this
|
||||
doc's claim that "the periodic classifier eventually picks up the
|
||||
prose mention": the periodic classifier never writes any of
|
||||
piggyback's fields (disjoint write-set, see
|
||||
prose mention": the periodic classifier covers none of piggyback's
|
||||
fields (see
|
||||
[`cadence.md → Concurrency`](./cadence.md#concurrency)), so it could
|
||||
never have recovered them — a parse failure needs the per-turn
|
||||
classifier, not the periodic one, exactly like a `piggybackMode='off'`
|
||||
|
|
|
|||
|
|
@ -1117,11 +1117,24 @@ so the reasons above hold for it, with a branch fork's reverse-apply
|
|||
also taking every later write and an aborted turn having nothing after
|
||||
it. The boot-recovery caveat above stands.
|
||||
|
||||
Re-checked 2026-10-16 for the places a pass creates from mentions
|
||||
([`classifier.md → Mentioned places`](./memory/classifier.md#mentioned-places)),
|
||||
by reading the reversal canon and code: a prose edit's sweep spares
|
||||
entity creates of every kind (`isReversible` in
|
||||
`lib/actions/story-entries/classifier-facts.ts`), CTRL-Z skips
|
||||
classifier groups, rollback and regenerate take every later turn
|
||||
with the place, and a pass's abort runs inside its write lock, which
|
||||
it takes only when no turn is in flight. So only the boot-recovery
|
||||
window above can remove such a place while a later turn's writes name
|
||||
it in JSON: an entry's `currentLocationId`, a character's
|
||||
`current_location_id` or `lastSeenAt`, an item's `at_location_id`, or
|
||||
a new child place's `parent_location_id`. More fields than for
|
||||
characters, the same window.
|
||||
|
||||
A fix is a JSON-path reference registry the closure queries through
|
||||
`json_each`. Parked 2026-10-05, re-checked 2026-10-10; the signal to
|
||||
revisit now is the periodic classifier creating entities other than
|
||||
characters (the factions and mentioned-places followup), since its
|
||||
passes are reversed by positional sweeps while later turns stay.
|
||||
`json_each`. Parked 2026-10-05, re-checked 2026-10-10 and 2026-10-16;
|
||||
the signal to revisit now is the factions design, whose membership
|
||||
facts put faction ids a pass creates into characters' `state`.
|
||||
|
||||
The group runner's conflict check has the same blind spot and would be
|
||||
the registry's second consumer: `rowRefs` (`lib/actions/delta/live-refs.ts`)
|
||||
|
|
@ -1506,6 +1519,27 @@ named `__proto__`, which the pane and piggyback store deliberately:
|
|||
zod's record skips that key. Parked 2026-10-09; the signal is a writer
|
||||
that can emit model-shaped state, such as an agentic classifier.
|
||||
|
||||
#### Placing a mentioned place later
|
||||
|
||||
A place the periodic classifier creates from a mention goes to the root
|
||||
when the turns don't say where it is, and stays there, since only the
|
||||
user moves a place after its first write
|
||||
([`classifier.md → Mentioned places`](./memory/classifier.md#mentioned-places)).
|
||||
When the story later places it ("Varn, in the Eastern Reach"), nothing
|
||||
moves it, and a turn that goes there under the Eastern Reach without
|
||||
using Varn's id creates a second Varn: places under different parents
|
||||
are never compared. World can't merge the two either, since merge
|
||||
exists only inside the collision dialog, so delete or rename is the
|
||||
only fix. Two ideas from the developer for when it bites: a step that
|
||||
re-parents a place from the root once the prose places it, in a pass
|
||||
or at chapter close, with user precedence and reversal handling for
|
||||
`parent_location_id`, which the authorship contract keeps user-only
|
||||
after the first write; and a kind of place that sits outside the tree,
|
||||
for mentioned places nobody can locate yet. A merge of any two rows of
|
||||
one kind would cover what both miss. Parked 2026-10-16 by the mentioned
|
||||
places design; the signal is duplicates of root-level mentioned places
|
||||
in real stories.
|
||||
|
||||
### UX (parked)
|
||||
|
||||
#### Drag-and-drop file import for ImportDialog
|
||||
|
|
|
|||
|
|
@ -820,6 +820,12 @@ For a place, `disabled` covers every place inside it too, with the
|
|||
places around the story exempt while the story is inside
|
||||
([`data-model.md → LocationState shape`](../data-model.md#locationstate-shape)).
|
||||
|
||||
The mode governs what the narrator sees. The periodic classifier,
|
||||
which extracts from prose rather than writing it, sees every row
|
||||
whatever its mode, so it can attribute a fact to a disabled row
|
||||
rather than create it again as new
|
||||
([`memory/classifier.md → What the classifier reads`](../memory/classifier.md#what-the-classifier-reads)).
|
||||
|
||||
**Default: `auto`.** `happenings` deliberately don't expose this —
|
||||
the `happening_awareness` graph IS their structural injection rule.
|
||||
|
||||
|
|
|
|||
|
|
@ -1107,6 +1107,13 @@
|
|||
</div>
|
||||
<span class="toggle on"></span>
|
||||
</div>
|
||||
<div class="switch-row" role="switch" aria-checked="false">
|
||||
<div class="switch-row-text">
|
||||
<div class="switch-row-label">create mentioned places</div>
|
||||
<div class="switch-row-hint">the background classifier also adds places the story mentions without going there; the place map grows with every one</div>
|
||||
</div>
|
||||
<span class="toggle off"></span>
|
||||
</div>
|
||||
|
||||
<div class="tab-section"><div class="tab-section-title">Story Defaults · Retrieval budgets <span style="font-size: 10px; color: var(--ink-faint); font-weight: 400;">· per-type token budgets, hard partitions</span></div></div>
|
||||
<div class="field-row">
|
||||
|
|
|
|||
|
|
@ -580,8 +580,8 @@ stories.
|
|||
### Memory
|
||||
|
||||
Mirrors Story Settings · Memory's
|
||||
[Chapter close + Prompt context + Classifier (cadence + piggyback
|
||||
only) + Retrieval budgets + Embedder default](../story-settings/story-settings.md#memory-tab)
|
||||
[Chapter close + Prompt context + Classifier (cadence, piggyback and
|
||||
mentioned places only) + Retrieval budgets + Embedder default](../story-settings/story-settings.md#memory-tab)
|
||||
sections, bound to `app_settings.default_story_settings.*` instead
|
||||
of `stories.settings.*`. Form components literally reused; only
|
||||
data binding differs.
|
||||
|
|
|
|||
|
|
@ -796,6 +796,14 @@
|
|||
<span class="toggle on"></span>
|
||||
</div>
|
||||
|
||||
<div class="switch-row" role="switch" aria-checked="false">
|
||||
<div class="switch-row-text">
|
||||
<div class="switch-row-label">create mentioned places</div>
|
||||
<div class="switch-row-hint">the background classifier also adds places the story mentions without going there — the place map grows with every one; applies from the next run</div>
|
||||
</div>
|
||||
<span class="toggle off"></span>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field-label">classifier context
|
||||
<span class="hint">trailing entries the per-turn fallback classifier sees</span>
|
||||
|
|
|
|||
|
|
@ -492,11 +492,19 @@ this section captures what the screen renders.
|
|||
In that state the toggle reads as off, a one-line hint explains
|
||||
the gate (`Requires a narrative model with structured-output
|
||||
capability`), and the **per-turn fallback classifier** covers the
|
||||
work on every turn — not the periodic classifier, whose write set is
|
||||
disjoint from piggyback's and could never supply those fields (see
|
||||
work on every turn — not the periodic classifier, whose write set
|
||||
covers none of piggyback's fields and could never supply them (see
|
||||
[`memory/piggyback.md → Capability gate`](../../../memory/piggyback.md#capability-gate)).
|
||||
Swapping the narrative model to a capable profile re-enables the
|
||||
toggle automatically.
|
||||
- **Create mentioned places** — a SwitchRow bound to
|
||||
`stories.settings.classifierMentionedPlaces`, off by default. On,
|
||||
the periodic classifier also creates the places the story mentions
|
||||
without entering
|
||||
([`memory/classifier.md → Mentioned places`](../../../memory/classifier.md#mentioned-places)).
|
||||
Its hint says what it costs: the place map grows with every place
|
||||
the story mentions, not only the ones it visits. A change applies
|
||||
from the next pass; prose already classified isn't revisited.
|
||||
- **Status block** — current state for the active branch, one of
|
||||
_idle / running / retrying / failed-persistent_. Each state
|
||||
carries its own copy and inline actions; failed-persistent
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue