Skip to content

feat(tension): persistent cognition substrate — tension store, salience-gated cycles, thread injection (PN-95) - #96

Merged
dnacenta merged 3 commits into
mainfrom
feat/PN-95-persistent-cognition-substrate
Aug 19, 2026
Merged

feat(tension): persistent cognition substrate — tension store, salience-gated cycles, thread injection (PN-95)#96
dnacenta merged 3 commits into
mainfrom
feat/PN-95-persistent-cognition-substrate

Conversation

@dnacenta

Copy link
Copy Markdown
Owner

Closes #95.

Spec: /opt/pulse-vault/pulse-null/specs/in-progress/persistent-cognition-substrate-spec.md (v1.0, D + Echo).

Merge order: after PN-94 (interest-triggered outreach) per spec §9 — this store is the Phase 2 trigger for that channel; outreach ships first so tension has an outlet.

What's in

  • Layer 1 — tension store (src/tension/): arithmetic-only per-tick update, zero LLM. No decay to zero (untouched threads get louder); discharge requires a verified WorkArtifact (file diff outside the journal / resolved prediction / tool rounds) — text mentions cannot discharge, and cannot even move last_touched (mentions feed a separate observational counter). Refused work claims raise an alert instead of being silently dropped. Persisted at {entity}/tension.json, flock-guarded fail-closed; forward-incompatible files quarantine loudly.
  • Layer 2 — salience-gated cycles: SalienceEval gates cycle spend on max tension, vigil decline, or the mandatory floor_interval starvation guard (0 fails toward always-fire). Registered as evaluator "salience"opt-in; no schedule.json task uses it yet, so nothing changes behaviour until a task is pointed at it.
  • Layer 3 — thread selection: top-k threads by tension injected into the per-cycle prompt path (not the boot-time snapshot). §3 discriminator (Spearman ρ tension-vs-recency + reach metric) emitted inline in the per-cycle payload.
  • [tension] config exactly per spec §5 (constants marked INITIAL GUESS); triage-not-drop at max_live_threads; tombstoned abandonment with reason; store readable via pulse-null status.

Deliberate deviations (details in commit bodies)

  • No Unknown serde catch-alls on ThreadOrigin/ThreadResolution — unknown variants hit the fail-closed quarantine instead of half-parsing.
  • Thread carries content, resolution_reason, and §3 bookkeeping beyond the §2.1 field list (required by §8 Q3/Q4 and the metrics).
  • ResolutionVerdict input type makes Answered/Superseded unconstructible without an artifact.
  • Tick period is a documented constant integrated over wall time, not a fifth config knob.

Verification

scripts/gate.sh PASS; cargo clippy --features discord-text --all-targets -- -D warnings clean; cargo test --features discord-text 902 passed / 0 failed (~85 new); release build OK.

Operational notes

  • On this deployment the tool-result discharge lane is inert (ClaudeCodeProvider::supports_tools() == false) and the prediction store is 94 pending / 0 resolved, so discharge will lean on file-diff evidence — expect threads to climb and the 40-thread triage to surface sooner than the constants suggest.
  • Tick loop runs under a coordinator tenure regardless of [scheduler] enabled so accrual continues while the schedule is paused; honours the isolation backstop.
  • PN-94 merge surface: two lines in src/config/mod.rs. Name overlap note: SalienceEval here is the cycle gate, unrelated to PN-94's EntityEvent::Salience.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EDtGQtXxs4fVK4g484Y1Fj

…-95)

Layers 1 and 2 of the persistent cognition substrate. The claim is that
"something wouldn't leave me alone" is a property of a state object with
the right update law, not of a process with the right uptime: between
cycles the entity's state currently changes only if an LLM call changes
it, so every inter-cycle transition has to be paid for in tokens and in
practice none happen. This is the missing arithmetic layer.

Layer 1 — tension store (`src/tension/`), persisted to
`{entity}/tension.json` with the same discipline as predictions.json:
atomic rename, in-process mutex plus flock, and a fail-closed loader in
the write path that quarantines rather than overwriting.

The two non-negotiable properties are enforced structurally, not by
convention:

* No time-decay to zero. `tick` has no decay term and no discharge term
  at all — it can only add — so an untouched thread gets louder. Accrual
  is integrated over wall time, so a restart neither loses pressure nor
  double-counts it, and a long outage resumes bounded.

* Discharge requires work, not attention. The only subtracting method
  demands a WorkArtifact, and the only minter of one checks the claim
  against something outside the text: a file whose mtime moved outside
  the journal, a prediction that actually resolved, or a tool that
  actually ran. Refused claims are alerted, never silently dropped — an
  ignored claim reads exactly like a granted one. Mentions move an
  observational counter and nothing else; in particular they do not move
  `last_touched`, so a thread cannot be kept quiet by being written about
  either. Spec §7 risk 3 names this the most likely failure; it is
  covered by tests that assert prose changes nothing.

Ingest is narrow (§7 risk 2): prediction errors above the surprise
threshold, and explicit [THREAD:] markers. Threads carry self-contained
content so they survive the fold that eats their referent (§8 Q4).
Retirement is tombstoned with a reason and retained (§8 Q3).

At `max_live_threads` the store admits the thread and raises a triage
demand carrying candidates. It does not drop the newest (the intent
queue's push defect) or the oldest (the predictions prune defect); this
is the third store and it does not reproduce them a third time.

Layer 2 — `SalienceEval` gates a cycle on max tension, declining vigil
signals, or the floor interval. The floor clause is a starvation guard
and is mandatory: without it a quiet period silently becomes a period of
no cognition and looks healthy from inside because nothing is reporting.
A floor of 0 therefore means "always fire", not "guard disabled" — a
misconfiguration fails toward running, never toward silence.

The §3 discriminator is implemented and emitted per cycle: Spearman rho
between tension-ordering and recency-ordering (Pearson over average
ranks, so ties stay correct), plus the reach tally. Both are exposed via
`pulse-null status` along with what the entity is carrying (§8 Q2).
Layer 3, the payoff. At cycle start the prompt receives the top-k threads
by tension rather than a positional slice of a journal file, so what the
entity picks up is chosen by an accumulator it cannot edit in prose
rather than by whatever survived the last fold. A thread's tension is
immune to folding because it lives outside the folded document.

The block goes into `build_task_system_prompt_budgeted`, which
`execute_task` and `execute_intent` rebuild on every fire — deliberately
NOT into the boot-time system prompt. `AppState.system_prompt` has no
writer, so a thread list assembled there would be frozen at whatever the
store held the last time systemd restarted the service, which is
strictly worse than the document reading this replaces.

Tier High, not Low: under budget pressure the block may be truncated, but
dropping it outright would hand selection straight back to document
layout, which is the defect Layer 3 exists to remove.

The §3 metrics — rho against recency, and the reach count — ship inline
in this routine payload rather than as a separate quality report,
because a coverage denominator that ships separately does not get read.
An outstanding triage demand rides along in the same block, so the cap
obligation reaches the entity in the same breath as the threads it is
about.

The autonomy context documents the three marker grammars and states the
artifact rule where the entity will actually read it: discharge needs a
file changed outside the journal, a prediction resolved, or a tool that
ran; writing about a thread does not lower its tension; a give-up needs a
reason, is kept as a tombstone, and is visible to D.
…-cognition-substrate

# Conflicts:
#	src/config/mod.rs
#	src/plugins/manager.rs
#	src/server/auth.rs
#	src/server/e2e_tests.rs
#	src/server/prompt.rs
#	src/server/trust.rs
@dnacenta
dnacenta merged commit 5866df2 into main Aug 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Persistent cognition substrate — tension store, salience-gated cycles, thread selection

1 participant