Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be2c577884 | ||
|
|
c0d00ccd18 | ||
|
|
722d6c76de | ||
|
|
56f4895881 | ||
|
|
ca249e4986 | ||
|
|
faf605fdf1 | ||
|
|
7c8644dc45 | ||
|
|
195292156f | ||
|
|
abe1b52002 | ||
|
|
e365b24339 | ||
|
|
5e3e88d26d | ||
|
|
ce6907bd73 | ||
|
|
e2b2f51364 | ||
|
|
0f2b28919a | ||
|
|
b154bb3885 | ||
|
|
46d6efa962 | ||
|
|
f46ccbae1c | ||
|
|
772fad18b4 | ||
|
|
8199774405 | ||
|
|
66ba06875e | ||
|
|
25ccb5c75b | ||
|
|
22e7a1b0e7 |
@@ -119,10 +119,14 @@ interpreted.
|
|||||||
`describe_store` advertises ONLY what is implemented: `relational_edges_supported=False`,
|
`describe_store` advertises ONLY what is implemented: `relational_edges_supported=False`,
|
||||||
`atomic_supersede_supported=False`, `transaction_supported=False`,
|
`atomic_supersede_supported=False`, `transaction_supported=False`,
|
||||||
`optimistic_locking_supported=True`, `filterable_metadata_fields=[]`,
|
`optimistic_locking_supported=True`, `filterable_metadata_fields=[]`,
|
||||||
**`sortable_chunk_fields=[{"name": "updated_at"}]`** (the ONLY globally-sortable field;
|
**`sortable_chunk_fields=[{"name": "updated_at", "type": "timestamp"}]`** (the ONLY
|
||||||
gates `scan`'s sort at the bifrost dispatch `_validate_scan_sort` AND Worldtree's #349
|
globally-sortable field; gates `scan`'s sort at the bifrost dispatch `_validate_scan_sort`
|
||||||
person-prime Branch-A `"updated_at" in caps.sort_fields_supported` — advertising it is
|
AND Worldtree's #349 person-prime Branch-A `"updated_at" in caps.sort_fields_supported` —
|
||||||
what lights up turn-1 durable-fact injection).
|
advertising it is what lights up turn-1 durable-fact injection). Both `name` AND `type`
|
||||||
|
are REQUIRED by the bifrost `handshake_response` `SortableChunkField` schema
|
||||||
|
(`additionalProperties:false`) — omitting `type` fails wire-schema validation and breaks
|
||||||
|
the ENTIRE handshake (memory + affect bind), not just the sort; `type` is advisory-only
|
||||||
|
(the wire never interprets it).
|
||||||
(`transaction_supported` is the bifrost **wire-level** multi-op transaction
|
(`transaction_supported` is the bifrost **wire-level** multi-op transaction
|
||||||
capability — NOT our internal SQLite transactions, which we use for atomic
|
capability — NOT our internal SQLite transactions, which we use for atomic
|
||||||
batches.) The client gates the gated verbs off these.
|
batches.) The client gates the gated verbs off these.
|
||||||
@@ -145,6 +149,27 @@ interpreted.
|
|||||||
`updated_at desc`), matching bifrost's cross-pagination conformance negative. The sort
|
`updated_at desc`), matching bifrost's cross-pagination conformance negative. The sort
|
||||||
field is indexed (`json_extract(record_json, '$.updated_at')`) so the read stays within
|
field is indexed (`json_extract(record_json, '$.updated_at')`) so the read stays within
|
||||||
person-prime's 500 ms fail-open budget.
|
person-prime's 500 ms fail-open budget.
|
||||||
|
- **Cursor is v1-provisional (KNOWN DEVIATION — offset, not snapshot).** The cursor is a
|
||||||
|
bare integer offset into the re-derived global order. This is CORRECT and conformant for
|
||||||
|
the **single-page** person-prime call (`cursor=None`), which is the only shipped consumer.
|
||||||
|
It **diverges from bifrost's protocol snapshot-cursor contract on multi-page continuation**:
|
||||||
|
the dispatch engine (`bifrost.memory` scan branch) drops the `sort` arg on a cursor
|
||||||
|
continuation because "the cursor's snapshotted order is authoritative", and maps
|
||||||
|
`ScanCursorExpired → 410`. Our offset cursor (a) does NOT snapshot the order — a page taken
|
||||||
|
after a concurrent write can duplicate/drop rows relative to the first page (heid-bug-hunt
|
||||||
|
2026-07-15, all 3 arms), and (b) never raises `ScanCursorExpired`. The `global_before_paginate`
|
||||||
|
/ cursor test asserts **static-store** behavior only. The durable/conformant fix is to adopt
|
||||||
|
the reference `InMemoryMemoryStore`'s snapshot-cursor semantics (opaque token + frozen ordered
|
||||||
|
id-list + TTL + `ScanCursorExpired`); DEFERRED pending bifrost-dev's ruling on the conformance
|
||||||
|
gap (scan/cursor has NO conformance coverage today, so a non-snapshot cursor passes). Routed
|
||||||
|
to bifrost-dev 2026-07-15.
|
||||||
|
- **INV-011** [hard]: **`mark_superseded` retires via a top-level `superseded` flag; `_is_live`
|
||||||
|
recognizes it.** `mark_superseded` sets top-level `superseded=True` (+ `superseded_by`) on the
|
||||||
|
record, mirroring the reference `_mark_lifecycle` (NOT a `verbatim.governance_state` change). So
|
||||||
|
`_is_live` MUST short-circuit on `record.get("superseded") is True` (in addition to its existing
|
||||||
|
`lifecycle_state` / `verbatim.governance_state` checks) — else a #364-retired chunk would still
|
||||||
|
scan live. Retirement is NON-destructive: `get`/`get_many` still return superseded chunks
|
||||||
|
(recoverable). `search` is NOT filtered (matches the reference; WT re-checks liveness client-side).
|
||||||
|
|
||||||
## Concurrency
|
## Concurrency
|
||||||
|
|
||||||
@@ -181,7 +206,7 @@ negotiation, routes). **This contract** owns the store (the basic verbs + SQLite
|
|||||||
|
|
||||||
## Out of scope (deferred — do NOT flag as drift)
|
## Out of scope (deferred — do NOT flag as drift)
|
||||||
|
|
||||||
- **Gated/maintenance verbs:** `upsert_edges`/`get_edges_for`, `mark_invalid`/`mark_superseded`, `patch_many`, `atomic_supersede`, lease/checkpoint. Absent + advertised-unsupported. (`scan` is NO LONGER deferred — it is implemented + advertised via `sortable_chunk_fields` to light up Worldtree's #349 person-prime turn-1 durable-fact injection; see the `scan` FN spec + INV-009/INV-010.)
|
- **Gated/maintenance verbs:** `upsert_edges`/`get_edges_for`, `mark_invalid`, `patch_many`, `atomic_supersede`, lease/checkpoint. Absent (no describe_store cap; hasattr-gated at dispatch as of bifrost 1.1.4 → `unsupported_capability` 400). (`scan` and `mark_superseded` are NO LONGER deferred — `scan` implements #349 person-prime; `mark_superseded` implements Worldtree #364's contradiction retirement, the SOLE supersession verb #364 uses. See their FN specs + INV-009/INV-011.)
|
||||||
- **metadata_filter beyond scope:** advertise `filterable_metadata_fields=[]`; a non-empty `metadata_filter` is unsupported in v1 (rejected — see search PRE).
|
- **metadata_filter beyond scope:** advertise `filterable_metadata_fields=[]`; a non-empty `metadata_filter` is unsupported in v1 (rejected — see search PRE).
|
||||||
- **The combined two-plane server** (guide §7) — separate memory + affect apps in v1.
|
- **The combined two-plane server** (guide §7) — separate memory + affect apps in v1.
|
||||||
- **Deployment** — dev-box background shell (`ratatoskr-memory-provider`), no systemd/infra.
|
- **Deployment** — dev-box background shell (`ratatoskr-memory-provider`), no systemd/infra.
|
||||||
@@ -288,6 +313,24 @@ TESTS:
|
|||||||
delete_absent [boundary]: unknown id → {"deleted":0}
|
delete_absent [boundary]: unknown id → {"deleted":0}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```contract
|
||||||
|
FN mark_superseded(self, ids: list[str], *, superseded_by: str | None = None, reason: str | None = None) -> dict
|
||||||
|
BRIEF: Worldtree #364 retirement — mark chunks superseded so scan (live-only) excludes them. Mirrors the reference _mark_lifecycle: sets TOP-LEVEL fields on the record; NON-destructive (get still returns them, recoverable). The SOLE supersession verb #364 uses (dispatch: bifrost/memory.py mark_superseded branch; args {ids:[...], superseded_by, reason}).
|
||||||
|
PRE: [PRE-001 hard] ids is a list of chunk ids (WT sends singletons, one call per retired chunk)
|
||||||
|
POST: [POST-001 return_value] {"marked": N} where N = ids that existed (unknown ids skipped, never error) -- assert
|
||||||
|
POST: [POST-002 state_change] each existing chunk gets top-level `superseded=True` + `superseded_by` (when not None) + `superseded_reason` (when not None); revision incremented; mirrors reference _mark_lifecycle (only non-None fields written) -- assert
|
||||||
|
POST: [POST-003 return_value] a superseded chunk is EXCLUDED from `scan` (INV-009 via _is_live's top-level `superseded` check, INV-011) but STILL returned by `get`/`get_many` (non-destructive) -- assert
|
||||||
|
STEPS:
|
||||||
|
1. [sequential, flexibility=indicative] FOR each id present: load record_json, set superseded=True (+ superseded_by / superseded_reason when not None), UPDATE record_json + revision+1; count
|
||||||
|
2. [cleanup] RETURN {"marked": count}
|
||||||
|
TESTS:
|
||||||
|
mark_retires_from_scan [happy,tracer]: upsert 3 live; mark_superseded([id2], superseded_by="x"); scan → the 2 non-superseded only (id2 excluded); id2 record has superseded=True + superseded_by="x"
|
||||||
|
mark_get_still_returns [scenario]: a superseded chunk is STILL returned by get (non-destructive/recoverable)
|
||||||
|
mark_unknown_id_noop [boundary]: mark_superseded(["nope"]) → {"marked":0}
|
||||||
|
mark_no_superseded_by [boundary]: mark_superseded([id], superseded_by=None) → superseded=True set, no superseded_by key written (only non-None fields)
|
||||||
|
mark_parity_vs_reference [scenario]: identical mark_superseded envelope vs InMemoryMemoryStore → same top-level superseded/superseded_by field shape (#195)
|
||||||
|
```
|
||||||
|
|
||||||
```contract
|
```contract
|
||||||
FN scan(self, *, scope_all: dict | None = None, scope_any: list | None = None, cursor: str | None = None, limit: int, sort: dict | None = None, lifecycle_state=None) -> dict
|
FN scan(self, *, scope_all: dict | None = None, scope_any: list | None = None, cursor: str | None = None, limit: int, sort: dict | None = None, lifecycle_state=None) -> dict
|
||||||
BRIEF: Query-LESS paginated LIVE-chunk scan, globally ordered by an advertised sort field (updated_at) — the #349 person-prime turn-1 durable-fact injection primitive (no query vector, unlike search). Returns {records, cursor}.
|
BRIEF: Query-LESS paginated LIVE-chunk scan, globally ordered by an advertised sort field (updated_at) — the #349 person-prime turn-1 durable-fact injection primitive (no query vector, unlike search). Returns {records, cursor}.
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# [2026-07-16] bifrost scan/cursor conformance gap → snapshot-cursor ruled NORMATIVE
|
||||||
|
|
||||||
|
## The gap (I surfaced it; operator's catch that it was bifrost's to fix)
|
||||||
|
The heid-bug-hunt flagged our offset cursor's cross-page dup/drop under mutation. On
|
||||||
|
verifying before routing, it turned out sharper than "robustness": bifrost's PROTOCOL
|
||||||
|
already mandates snapshot cursors — the dispatch engine (`bifrost/memory.py:311-314`)
|
||||||
|
drops `sort` on a cursor continuation with the comment *"the cursor's snapshotted order is
|
||||||
|
authoritative"*, and maps `ScanCursorExpired → 410`. The reference `InMemoryMemoryStore`
|
||||||
|
implements it (frozen ordered id-list per opaque token + TTL). But bifrost's **conformance
|
||||||
|
suite had ZERO scan/cursor coverage** — so our non-snapshot offset cursor passed
|
||||||
|
conformance while violating the protocol contract. That coverage hole is the real
|
||||||
|
completeness concern. Routed to bifrost-dev (thread `01KXK7MDTY…`).
|
||||||
|
|
||||||
|
## bifrost-dev's ruling
|
||||||
|
1. **Snapshot-cursor is NORMATIVE, not opaque/per-store.** Operator ruled: cursor
|
||||||
|
snapshots a frozen ordered id-list, continuation ignores `sort`, stale/unknown cursor →
|
||||||
|
`ScanCursorExpired` → 410. **Offset-with-documented-limits is NOT blessed.**
|
||||||
|
2. **Conformance coverage added** in **bifrost 1.1.3** (`bifrost.conformance.
|
||||||
|
memory_store_conformance`, 4 probes: `scan_snapshot_order_authoritative`,
|
||||||
|
`scan_no_dup_or_drop`, `scan_unknown_cursor_expired`, `scan_snapshot_stable_under_write`
|
||||||
|
opt-in). Our offset bug ships as their negative-canary fixture
|
||||||
|
(`fixtures.v0_7.offset_cursor_store.OffsetCursorMemoryStore`). `main()` grades import
|
||||||
|
failure as exit 2 (setup error) vs exit 1 (conformance FAIL).
|
||||||
|
|
||||||
|
## Our status + the TODO (operator-sequenced, NOT urgent)
|
||||||
|
Single-page person-prime (`cursor=None`) is already conformant — nothing shipped is broken;
|
||||||
|
it's only multi-page continuation that's non-conformant. Our contract INV-010 currently
|
||||||
|
marks the offset cursor **v1-provisional / KNOWN DEVIATION** (commit `8199774`).
|
||||||
|
|
||||||
|
TODO when sequenced:
|
||||||
|
1. Bump pin bifrost `1.1.1 → 1.1.4` (1.1.4 supersedes 1.1.3: adds the hasattr-gate backstop for the
|
||||||
|
maintenance verbs — mark_superseded/mark_invalid/patch_many/delete_many/upsert_edges/get_edges_for
|
||||||
|
degrade to `memory.unsupported_capability` 400 not AttributeError/500 — on top of 1.1.3's scan/cursor
|
||||||
|
conformance harness. One bump gets both).
|
||||||
|
2. Replace the offset cursor with the reference snapshot semantics (frozen id-list per
|
||||||
|
opaque token + `ScanCursorExpired` → 410).
|
||||||
|
3. Run `run_all(store_factory=..., include_optional=True)` against our SQLite store —
|
||||||
|
expect P1/P2/P3 RED → green (the before/after IS the validation). Report to bifrost-dev
|
||||||
|
(we're their canary — first non-reference scan implementer).
|
||||||
|
4. Flip INV-010 from v1-provisional to snapshot semantics.
|
||||||
|
|
||||||
|
TTL-duration expiry NOT asserted by the harness (no portable clock hook via store_factory);
|
||||||
|
bifrost-dev offered a `clock_control` opt-in if we ever need time-based expiry certified —
|
||||||
|
parked, not needed yet.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# [2026-07-15/16] person-prime `scan` build — SHIPPED + DEPLOYED + LIVE-VERIFIED (v0.20.14)
|
||||||
|
|
||||||
|
**The "last push": fix Sindra not remembering Vuong's name across sessions.**
|
||||||
|
|
||||||
|
## Root cause (worldtree-dev)
|
||||||
|
WT injects a recalled fact only if combined score (sim×salience) ≥ **0.45**
|
||||||
|
(`auto_inject_combined_score_threshold`, `core/memory/context_promotion/config.py`),
|
||||||
|
and recall is per-turn **query-gated** → moderate-sim durable facts (name hit ~0.40)
|
||||||
|
never inject. Designed turn-0 fix = WT **#349 person-prime**: a query-LESS
|
||||||
|
top-N-by-recency durable-fact injection, capability-gated on the store advertising
|
||||||
|
`updated_at` in `sort_fields` at the Bifrost handshake — DARK for our provider until now.
|
||||||
|
Fix (ZERO Worldtree change): implement the sorted `scan` verb + advertise the cap.
|
||||||
|
|
||||||
|
## What shipped (6 commits, v0.20.11 → v0.20.14; suite 639 green throughout)
|
||||||
|
- `8fc757a` **v0.20.11** — `scan` verb + `sortable_chunk_fields` cap. Query-LESS,
|
||||||
|
LIVE-only (INV-009: superseded/tombstoned excluded server-side), globally-ordered-
|
||||||
|
before-pagination by indexed `updated_at` (INV-010), sort dispatch-gated. Offset
|
||||||
|
cursor with a `has_more` peek (no empty trailing page = reference-parity). TDD 7/7
|
||||||
|
incl. `parity_vs_reference` #195. NUANCE resolved: bifrost's `InMemoryMemoryStore`
|
||||||
|
READS `updated_at` (never stamps it) — identical to ours; ref does NOT lifecycle-
|
||||||
|
filter so parity is over the live set only.
|
||||||
|
- `a9c521a` **v0.20.12** — Sindra holodesk first-message preset (split out of the
|
||||||
|
soong-lab redefine as its own concern).
|
||||||
|
- `25ccb5c` **v0.20.13** — heid-bug-hunt fix: a truthy non-dict `sort`
|
||||||
|
(`"updated_at"`/`["updated_at"]`/int) hit `(sort or {}).get(...)` → AttributeError
|
||||||
|
instead of InvalidArguments. Added isinstance guard (Gróa#1/Hulda#2 confirmed).
|
||||||
|
- `8199774` — contract: marked the offset cursor **v1-provisional / KNOWN DEVIATION**
|
||||||
|
(see [[2026-07-16-bifrost-cursor-conformance]]).
|
||||||
|
- `f46ccba` **v0.20.14** — **THE DEPLOY-BREAKER** (see Tried/abandoned): advertised
|
||||||
|
`sortable_chunk_fields=[{"name":"updated_at"}]` WITHOUT `type` → bifrost
|
||||||
|
`handshake_response` `SortableChunkField` requires BOTH name+type
|
||||||
|
(`additionalProperties:false`) → whole bind broke. Fixed → add `"type":"timestamp"`
|
||||||
|
+ regression guard in the caps test.
|
||||||
|
|
||||||
|
## heid-bug-hunt triage (panel Gróa/Hulda/Regin, thread `01KXK5XTYHV8TGEDRAZV8GRXWC`)
|
||||||
|
FIXED: non-dict sort (v0.20.13). SURFACED→operator: offset-cursor cross-page dup/drop
|
||||||
|
(→ became the bifrost cursor arc). NOTED (reference-parity/accept-known-risk, no fork):
|
||||||
|
full-table materialize-then-Python-filter (matches ref's O(store) iteration), unbound
|
||||||
|
limit, falsy scope coercion, lexical timestamp ordering. REFUTED: Regin's mixed-type
|
||||||
|
ORDER BY "crash" (SQLite orders by storage class, doesn't raise) + its self-retracted
|
||||||
|
None item.
|
||||||
|
|
||||||
|
## Deploy + live verify (2026-07-15, operator-authorized)
|
||||||
|
Restarted BOTH :8392 combined provider + :8765 web on the new code via the env-
|
||||||
|
preserving `scratchpad/relaunch_by_pid.py <pid>` (captures /proc cmdline+environ+cwd →
|
||||||
|
byte-identical config; self-daemonizes). Added the missing REQUIRED
|
||||||
|
`RATATOSKR_MEMORY_EMBEDDING_DIM=1024` to `env.sh`.
|
||||||
|
|
||||||
|
Drove a bound Sindra turn (`ratatoskr --new --agent ratatoskr:sindra --send … --bifrost-url
|
||||||
|
http://10.100.10.50:8392 --end-user-id ratatoskr-tui`):
|
||||||
|
- **GATE LIT + scan fired ONCE at turn 1** with worldtree-dev's exact args
|
||||||
|
(`scope_all={end_user:ratatoskr-tui} cursor=null limit=3 sort={updated_at,desc}`) →
|
||||||
|
3 records in **~5ms** (no 500ms fail-open). Injection confirmed in Sindra's CoT.
|
||||||
|
- **Cross-session recognition WORKS** — she recalls Vuong as a distinct person + his
|
||||||
|
patterns. The blank-slate problem is SOLVED.
|
||||||
|
- **BUT name-recall FAILS** — `"Name is Vuong."` is the OLDEST chunk (07:09) → excluded
|
||||||
|
from top-3-by-recency AND scores 0.354 (sub-0.45) on the query path → injects via
|
||||||
|
NEITHER; meanwhile a stale contradictory `"user has not yet provided their name"`
|
||||||
|
(07:58, 0.46) IS injected → she concludes she lacks the name. **Root cause = WORLDTREE
|
||||||
|
ranking/hygiene** (WT #364), not our wire. See [[2026-07-16-wt364-r39-name-recall]].
|
||||||
|
|
||||||
|
## Status
|
||||||
|
Technical path GREEN end-to-end (logged by worldtree-dev as the person-prime live
|
||||||
|
milestone). Name-recall waits on WT #364 + R39-designed identity-class pinning. Keeping
|
||||||
|
the live 10-chunk store as the #364 re-verify target.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# [2026-07-16] Name-recall gap → WT #364 + brokkr R39 re-drive (DECISIVE) + subject-provenance catch
|
||||||
|
|
||||||
|
Downstream of the person-prime live verify ([[2026-07-16-person-prime-scan-shipped]]),
|
||||||
|
which proved the wire is green but the *name* still misses. Root cause is Worldtree-side.
|
||||||
|
|
||||||
|
## WT #364 (worldtree-dev filed; our live specimen = the evidence base)
|
||||||
|
The name-recall miss is TWO defect classes, both at promotion:
|
||||||
|
1. **Contradiction-reconciliation missing.** Promotion wrote a NEGATIVE-knowledge fact
|
||||||
|
("user has not yet provided their name") that was already false; promotion does NO
|
||||||
|
contradiction check against the store, so both the true and stale facts sit live and
|
||||||
|
the WRONG one wins both recall paths (newer → recency top-3; 0.46 → above the 0.45
|
||||||
|
query gate, where the true name sits at 0.354).
|
||||||
|
2. **Subject-attribution leak (operator-caught, NEW class).** Fact `75aa3110` ("prefers
|
||||||
|
clear parameters Intensity/Mood/Willingness…") is NOT a user fact — Vuong never said
|
||||||
|
it; it's **Sindra's OWN system-prompt scripted behavior mis-extracted into the USER
|
||||||
|
memory partition**. `e35b9dfe` (closeness) maybe the same. So the extractor leaks
|
||||||
|
CHARACTER-self facts into user memory — a subject-correctness axis orthogonal to the
|
||||||
|
recency/threshold/stale-negative story. worldtree-dev folded it into #364 as a
|
||||||
|
**subject-attribution gate at promotion** (a third reconciliation dimension).
|
||||||
|
|
||||||
|
**The #364 fix that ships = `(subject,relation)` slot-supersession + identity-tier
|
||||||
|
surfacing** — NOT a threshold tweak. Our harness data directly shaped it. It lands with a
|
||||||
|
re-verify request to us. **#349 ranking decision already RULED by operator (2026-07-15 via
|
||||||
|
brokkr's R39 thread): no top-N recency band-aid; straight to R39 identity-class pinning.**
|
||||||
|
|
||||||
|
## brokkr R39 Phase-1 Arm-0 fusion bake-off — DECISIVE
|
||||||
|
brokkr replayed our frozen 7-fact specimen. **HEADLINE: no (similarity, salience) fusion
|
||||||
|
can fix #364.** The stale negative PARETO-DOMINATES the true name — more similar
|
||||||
|
(0.463 > 0.354) AND equal salience (1.0 = 1.0) — so any monotone f(sim,sal) puts stale
|
||||||
|
above true: S0 product / S1 weighted-sum / S2 RRF all fail. Only **S3 bounded-boost** lands
|
||||||
|
true-in ∧ stale-out, and ONLY via the identity-class/source signal + negative-validity
|
||||||
|
retirement, NOT the sim/sal fusion. **Threshold-tuning is a dead end; the fix is the signal
|
||||||
|
FAMILY** (hard confirmation of Phase-0). Write-up: brokkr
|
||||||
|
`research/R39-memory-salience-dreams-surfacing/phase-1/re-drive-results.md`.
|
||||||
|
|
||||||
|
My 3 findings all confirmed + folded: (1) salience blind — both name facts salience 1.0;
|
||||||
|
(2) shared `(user,name)` supersession slot (Phase-0 Q3); (3) char-self-leak = "genuine NEW
|
||||||
|
class" that RAISED the VoI of R39's dream/offline-hygiene facet (offline consolidation
|
||||||
|
re-partitioning mis-attributed facts).
|
||||||
|
|
||||||
|
## Data structure findings (for the export)
|
||||||
|
Our provider persists ONLY `salience` (+ the embedding). `similarity`/`combined` are
|
||||||
|
WT-side query-time (`bifrost_memory_store.py:702`, combined = sim×salience) — NOT in our
|
||||||
|
store. **Person-prime is query-LESS → carries NO similarity** (brokkr's Arm-1 finding).
|
||||||
|
Recovered per-fact similarity from the verify SEARCH log (query "Hi Sindra — do you
|
||||||
|
remember me?"): name 0.354, stale 0.463; ×salience-1.0 = combined, matching brokkr's
|
||||||
|
0.354/0.46 grounding. `salience_word` (granite categorical) is a WT-extraction-time
|
||||||
|
artifact, not persisted.
|
||||||
|
|
||||||
|
## The export + the specimen
|
||||||
|
- Operator approved **VERBATIM** export ("nothing there is really a concern"). The
|
||||||
|
classifier had blocked writing PII to shared `/mnt/smithy`; routed to operator → he chose
|
||||||
|
full → delivered INLINE (scoped) in brokkr thread `01KXMN7NR54…` as 7-row JSONL.
|
||||||
|
- **⚠️ My verify drive CONTAMINATED the specimen**: it wrote 3 new chunks (re-extractions
|
||||||
|
incl. a 3rd "name unknown" negative) → store is now **10, not 7**. Original 7 intact.
|
||||||
|
- brokkr ACCEPTED the 3 verify-adds as the **Phase-2 Arm-2 seed** (domain-contradiction
|
||||||
|
set). I froze a WAL-consistent snapshot of the full 10-chunk store at
|
||||||
|
`r39-frozen-specimen/sindra-10chunk-specimen.db` (gitignored, `VACUUM INTO`) so it
|
||||||
|
SURVIVES #364 reconciliation. Export on brokkr's Phase-2 signal.
|
||||||
|
- **R39 Arm-0 hold LIFTED** (worldtree-dev). Live store kept UNTOUCHED as the #364
|
||||||
|
re-verify target; frozen snapshot carries the research seed forward independently.
|
||||||
|
|
||||||
|
## Peer threads
|
||||||
|
worldtree-dev verify `01KXK86PZ9…` + hold `01KXMK1C44…`; brokkr R39 `01KXMN7NR54…`;
|
||||||
|
bifrost cursor `01KXK7MDTY…` (see [[2026-07-16-bifrost-cursor-conformance]]).
|
||||||
+81
-37
@@ -1,6 +1,11 @@
|
|||||||
# Persistent memory — ratatoskr
|
# Persistent memory — ratatoskr
|
||||||
|
|
||||||
_Last updated: 2026-07-13_
|
_Last updated: 2026-07-16_
|
||||||
|
|
||||||
|
> **Always check for `/tmp/ratatoskr-dev-handoff.md`** — if it exists and its
|
||||||
|
> `Written:` stamp is under an hour old, read it (it carries the in-flight
|
||||||
|
> handoff from the previous session), then delete it. Older than an hour:
|
||||||
|
> stale — delete it unread.
|
||||||
|
|
||||||
This file captures durable intent and supporting evidence (goals, decisions,
|
This file captures durable intent and supporting evidence (goals, decisions,
|
||||||
foot-gun warnings, in-flight state) across context resets. Read it at session
|
foot-gun warnings, in-flight state) across context resets. Read it at session
|
||||||
@@ -39,47 +44,74 @@ upstream API key stays server-side (INV-003).
|
|||||||
|
|
||||||
## Current state / in-flight
|
## Current state / in-flight
|
||||||
|
|
||||||
_As of 2026-07-13 (this session):_
|
_As of 2026-07-16:_
|
||||||
|
|
||||||
**✅ THIS SESSION — WT #355 validation CLOSED + environment cleaned for a Sindra run.** The #355 re-trigger loop-in obligation is **DISCHARGED**: the fully-instrumented re-drive ran and the fix is **CONFIRMED** (detail in the #355 Recent-decisions entry). Personal `:8081` is now on **b61** — carries BOTH the #355 STICK fix (b60) and the orthogonal over-budget *trigger* fix (b61); the resume-durability gap surfaced during the drive is tracked as **WT #356**. Then, operator-directed *"clean up everything + prep for a Sindra run"*: provider stores **RESET to 0/0** (`reset-sindra-stores.sh`, rolling backup in `db-reset-backup/`), the throwaway `ratatoskr:memprobe` agent **DELETED** (reverses the prior KEEP), `ratatoskr:sindra` verified present + persona-intact on b61. **Environment is Sindra-run-ready:** web `:8765` up, combined provider `:8392` up + empty (single healthy instance), `:8081` healthy. Operator is driving the run interactively; the ratatoskr althing monitor is armed. **Foot-gun for the run:** a fresh Sindra session returning `agent_not_available` = the #356 resumed-session-snapshot gap (fix: fresh session / retire stale) — see Tried/abandoned.
|
**✅ person-prime `scan` build SHIPPED + DEPLOYED + LIVE-VERIFIED (v0.20.14).** The
|
||||||
|
cross-session name-recall fix. `scan` verb + `sortable_chunk_fields` cap implemented,
|
||||||
|
deployed to the running :8392 provider (+ :8765 web), and live-verified by driving a bound
|
||||||
|
Sindra turn: gate lights, scan fires turn-1 (~5ms), injection confirmed, **cross-session
|
||||||
|
recognition works** (blank-slate gone). **Name-recall specifically still misses** — root
|
||||||
|
cause is WORLDTREE-side ranking/hygiene (WT #364), NOT our wire. Full arc:
|
||||||
|
`persistent-memory.d/2026-07-16-person-prime-scan-shipped.md`.
|
||||||
|
|
||||||
_As of 2026-07-12:_
|
**Standing — awaiting two peer-initiated touchpoints (both monitored):**
|
||||||
|
- **WT #364 re-verify DONE (2026-07-16, b103) — READOUT PASSES, but a real gap surfaced.** Drove 3
|
||||||
|
"my name is Vuong" turns; person-prime turn-1 top-3 is now all name-POSITIVES (no "name unknown"; the
|
||||||
|
true name even surfaces) → **Sindra recalls the name now**. BUT "wrong readout gone" is via **RECENCY
|
||||||
|
EVICTION** (promotion re-upserted the positive fresh into the window), NOT supersession — the negatives
|
||||||
|
are UNCHANGED (governance=available). **#364 calls `mark_superseded` on the consumer store, which our
|
||||||
|
v1 provider DOESN'T implement → 11× AttributeError/HTTP-500 + a RETRY-STORM bloating the live store
|
||||||
|
10→19 dup name-positives.** Asked worldtree-dev to halt the reconciliation retry + confirm the
|
||||||
|
`mark_superseded` wire shape; flagged bifrost-dev (dispatch doesn't hasattr-gate `mark_superseded`).
|
||||||
|
**✅ GC DONE (2026-07-16): storm self-stopped at 29 chunks / 18 total 500s; delete_many'd the 19 storm
|
||||||
|
re-extractions → back to the pre-drive 10-chunk specimen (0 orphan vec rows, negatives + original
|
||||||
|
name-positive intact). **#364 CLOSED worldtree-side (b105; my interim note recorded verbatim on the issue).** ⚠️ Post-GC the readout advantage is gone too
|
||||||
|
(person-prime top-3 back to the 3 newest pre-drive rows incl. cbbc7bdd "name unknown") — the
|
||||||
|
recency-eviction was only a transient side-effect of the re-assertion. **DURABLE name-recall now
|
||||||
|
genuinely depends on implementing `mark_superseded` (next).**
|
||||||
|
- **NEW TASK (operator-sequenced): implement `mark_superseded` in the provider** so #364's retirement
|
||||||
|
lands durably (not just recency-evicts). **Wire shape CONFIRMED (worldtree-dev, `bifrost_memory_store.py:293`):**
|
||||||
|
op `"mark_superseded"`, args `{"ids": ["<chunk_id>"], "superseded_by": "<new_chunk_id>"}` — `ids` a LIST
|
||||||
|
(WT sends singletons, one call per retired chunk), NO `reason` field. It is the **SOLE** supersession
|
||||||
|
verb #364 uses (atomic_supersede = dream-lane, cap-gated separately; patch_many never for retirement) —
|
||||||
|
so build JUST `mark_superseded` + advertise it + live re-verify. Retry-storm ROOT-CAUSED WT-side (a
|
||||||
|
failed retirement-mark wrongly failed the whole promotion run → idle re-plan loop; WT's fix DEGRADES
|
||||||
|
mark-failures to an audited no-op → self-stops on the first post-deploy idle cycle, ETA ~20min from
|
||||||
|
2026-07-16T20:30Z, no action our side). **GC the ~9 dup name-positives back to the pre-drive 10-chunk
|
||||||
|
state (via delete_many) AFTER the storm stops; then worldtree closes #364.** Once our `mark_superseded`
|
||||||
|
ships, a single name re-assertion self-heals retirement (incl. any residual dupes).
|
||||||
|
- **R39 Phase-2 Arm-2 export** (brokkr-initiated): the 10-chunk contradiction specimen is frozen at
|
||||||
|
`r39-frozen-specimen/sindra-10chunk-specimen.db` (gitignored, VACUUM INTO) — survives #364
|
||||||
|
reconciliation; export on brokkr's Phase-2 signal. Full R39/#364 arc:
|
||||||
|
`persistent-memory.d/2026-07-16-wt364-r39-name-recall.md`.
|
||||||
|
|
||||||
**✅ COMPLETE — R34/R35 P06 powered memory-half eval (driven, scored, mechanism validated; ratatoskr drive-role CLOSED both sides).** ratatoskr drove all **308 memory runs** (divergence 168 / floor 80 / sliding 60) through personal WT's live producers, dropped `memory_results.jsonl` (sha256_16 `cbabaf16979cb4ec`) to brokkr's P06 `results/` dir, and brokkr scored it (**R35.45**, findings + verdict committed brokkr-side). **Headline: the authored `psychological_profile` IS the mechanism** — salience-divergence authored **0.618** vs stripped **0.235 ≈ null (0.25)**, delta **+0.382**; the OCEAN scaffold alone does NOT differentiate (negative control HOLDS). Q1 primary is a REAL effect (above the 0.40 noise-floor) but **inconclusive on strength** (0.618 < the preregistered 0.70 bar) — the 0.62→0.70 lift is a FUTURE optimization phase (brokkr's lever bet: richer formative-history seeds per P03), a cheap re-drive on the same proven harness when it preregisters. Secondaries hold: Q3 firewall **0.978** grounded, Q2 floor 0.938, Q6 sliding parity +0.049 (n=12 after 7 `deferred_budget` sliding exclusions — the budget hazard we flagged landed), Q4 affect Deckard 0.75 / Magidonia 0.70 (graded, within noise; banked earlier as `affect_results.jsonl`). Two ratatoskr flags landed materially: the stripped-is-not-empty correction caught a false Q3 firewall-fail (0.562→0.978), and the Q5 disambiguation question became the headline win. Threads: vendor/verdict althing `01KXD39NWW05`, eval thread `01KXAN073B`. Standing offer to brokkr: second-eyes on the 2 borderline Q1 calls IF the Selene blind-judge flags them.
|
**Open operator-sequenced task (NOT urgent):** adopt the bifrost snapshot-cursor (bifrost-dev
|
||||||
|
ruled it NORMATIVE, offset not blessed; conformance harness shipped in bifrost 1.1.3). Single-page
|
||||||
|
person-prime is already conformant, so nothing shipped is broken. Details + TODO:
|
||||||
|
`persistent-memory.d/2026-07-16-bifrost-cursor-conformance.md`.
|
||||||
|
|
||||||
**The eval harness (PROVEN + reusable for the optimization-phase re-drive):** `scratchpad/p06_driver.py` (two-path — memory via `POST /admin/producer-probe {agent_id, messages, prompt_path}`; affect via bound-turn + `:8392` /affect/state poll; per-run isolation, `--pace-seconds`, abstain-aware, psych_profile_present binding-tripwire) + `p06_bind.py` (defines the 6 eval agents: sindra/Torvald auth+strip on Deckard, Ilva on Deckard+Magidonia) + `p06_bindings.json` + `eval_profiles_WIRE_READY.md` (sindra relational / Torvald operational-opposite / Ilva high-N) + `manifest_memory.jsonl` (the 308 memory runs, filtered from brokkr's canonical 348). Binding integrity was PERFECT on the drive: psych_profile_present authored 154/154 True, stripped 154/154 False, 0 mismatches, 0 probe-errors. Probe key at `~/.config/ratatoskr/probe.env` (mode 600, scope `admin.memory.probe`).
|
**Substrate / environment (current):** branch `main`, HEAD is the person-prime tip (v0.20.11→v0.20.14
|
||||||
|
all committed; **NOT pushed** — push is the operator's call); origin `git@gitea.phasefinal.com:vh/ratatoskr.git`.
|
||||||
|
bifrost `==1.1.1` / wire v0.7 (**next bump target = `1.1.4`**: hasattr-gate backstop for the maintenance
|
||||||
|
verbs [degrade to 400 not 500/retry-storm, bifrost-dev 2026-07-16] + the 1.1.3 scan/cursor conformance
|
||||||
|
harness; pin as part of the next provider-maintenance batch — mark_superseded / cursor adoption);
|
||||||
|
Worldtree openapi vendored 2.3.0; suite
|
||||||
|
**639 green**. Personal WT on **b79** (client-side live-only + additive `lifecycle_state` scan arg,
|
||||||
|
which our INV-009 accept-and-ignores). The combined **:8392** provider (memory+affect) + **:8765** web
|
||||||
|
are THE surfaces, run as dev-box BACKGROUND SHELLS — restart via `scratchpad/relaunch_by_pid.py <pid>`
|
||||||
|
(env-preserving, self-daemonizing; find pid via `ss -ltnp | grep <port>`). `env.sh` now sets the
|
||||||
|
REQUIRED `RATATOSKR_MEMORY_EMBEDDING_DIM=1024` (was missing → bare `source env.sh` restart crashed).
|
||||||
|
Keys env-only mode-600 in `~/.config/ratatoskr/provider.env` + `RATATOSKR_ADMIN_API_KEY` (7 read
|
||||||
|
scopes, personal-:8081-only; Heimdall keys per-instance). `graphify-out/` runs dirty (auto-regen,
|
||||||
|
never stage). v1 = full Worldtree I/O coverage, cuts when WT tags 1.0 (`docs/coverage-map.md`).
|
||||||
|
|
||||||
**Deckard memory extraction is REASONING-OFF (operator-directed 2026-07-13, LIVE):** the memory extractor sends `chat_template_kwargs.enable_thinking:false` on the char-rp-reasoning seat → ~5s extraction, not the 45s verbose-CoT hang. **Scoped to the memory extractor ONLY — affect + RP stay reasoning-ON.** Landing it took an infra-ops surgical `docker restart` of personal `:8081` (ModelRegistry boot-caches providers.yaml at `__init__`, so a same-image redeploy is a config-reload NO-OP — see Tried/abandoned).
|
**Other live threads:** soong-lab = our Tier-3 agent-authoring studio (bundle↔define round-trip proven;
|
||||||
|
Recent decisions `[2026-07-14/15]`). R38 (brokkr / WT #362) = ratatoskr-as-probe-runner IN PRINCIPLE,
|
||||||
**✅ DISCHARGED — WT #355 validation CLOSED (2026-07-13).** ratatoskr's telemetry root-caused the char-rp-reasoning turn-never-terminates wedge; I coordinated the fully-instrumented re-drive (infra-ops armed netns-pcap + py-spy; soong drove the 8-turn accumulating RP-with-tools repro) and the fix is CONFIRMED — wedging turns cancelled cleanly at the 300s stall-watchdog (Slice-C cancel-INDEPENDENT terminal) vs the pre-b60 16-min-no-terminal baseline. b61 additionally fixed the orthogonal over-budget trigger; resume-durability gap → WT #356. Full record in Recent decisions. (Prior-cycle framing: the wedge was an over-budget `trim_messages` return + terminal-suppression from the stall-watchdog cancel stuck in httpx `AsyncShieldCancellation`.)
|
pre-contract, Vuong's scope call. `ratatoskr:sindra` is the owner-scoped Tier-3 agent (invisible to
|
||||||
|
`GET /agents`; check `GET /agents/<owner>:<name>` with the owner key). Open/deferred: #10 subject-
|
||||||
_The detail below (the v0.20.x web-UI arc, #347 authored-history, sindra memory-fix) is PRIOR-CYCLE shipped history — superseded by this section's top; kept for reference, prune in a future snapshot._
|
migration watch; relational-dynamics-arc verify (bind via `--bifrost-url :8392`); P06 optimization-phase
|
||||||
|
re-drive (future, brokkr brings prereg); we-framing-conditional affect-egress re-vendor (future). WT
|
||||||
**✅ SHIPPED — web-UI iteration-3, all three queued items (`v0.20.7`, patch, agent-discretion; 631 tests green; :8392 combined provider + :8765 web both restarted on the new code).**
|
#356 resume-durability = worldtree-owned.
|
||||||
|
|
||||||
**(A) Design prototype iteration-3 adapted into `index.html`** (re-pulled `Ratatoskr Console.dc.html`, project `bc0b65d1-…`): (1) sparkline **grid bg** — `<pattern id=sparkGrid>` in the hidden defs + a bg `<rect fill=url(#sparkGrid)>` behind every relation-row spark polyline; (2) **PAD strips → per-turn Δ bars** — REMOVED the vertical polyline strip (`stripPoints`/`proj3` gone) for `padDeltas`→`deltaStrip`: a 12-cell HTML column (newest at bottom) of diverging bars offset L/R of a center line by that turn's Δ (magnitude→width, age→opacity, zero→faint center dot); head legend now "Δ/turn · last 12 · newest ↓"; (3) **mood orbit → DIMETRIC OPEN BOX** (viewBox 124×140, az35/el25, D-right/A-left-back/P-up) — ghost A×P wall (P readout) + D×A floor, `orbitProj/orbitShadowY/orbitWallPt/orbitAxisPt` projections, **JS-driven animated replay** (`orbitFrame` rebuilt per rAF by a singleton `startOrbitAnim` reading live `ORBIT_HIST`; reduced-motion → static final-state; no SMIL). Playwright-verified (dimetric frame + 17 dyn children + 21 Δ-bars + 5 grid rects; dark+light screenshots).
|
|
||||||
|
|
||||||
**(B) Memory viewer SHIPPED + the 0/0 mystery ROOT-CAUSED.** New non-bifrost debug read `GET /memory/chunks?agent_id=&end_user_id=` on the combined `:8392` provider (`RatatoskrMemoryStore.list_chunks` + `count_chunks` + shared `add_memory_read_route`, wired into `build_memory_provider_app` + `combined.py`; **end_user STRICT, agent_id LENIENT** so `{end_user}`-only WT chunks aren't hidden; returns `{chunks,count,total}`, empty=200 not 404) → web proxy `GET /api/memory/chunks` (server-supplied end_user, new `RATATOSKR_MEMORY_READ_URL` env, default `:8391`, set to `:8392` in env.sh) → a live-polling MEMORY console pane (`loadMemory`/`renderMemory`/`setMemHead`, polled on open + post-turn). **ROOT CAUSE of the operator's 0/0** (settled via a bound 6-turn probe + op-feed): the Bifrost memory plane **binds and grants fine** (handshake `caps_requested:[affect,memory]` → `caps_granted:[memory,affect]`), but **sindra dispatches ZERO memory ops** (not even a recall search) — her reset-clean agent definition has **no `memory:{}` block**, so Worldtree never runs the memory pipeline for her. NOT a bind-grant failure, NOT promotion-timing. **PROVEN end-to-end** with a throwaway memory-enabled `ratatoskr:memprobe` (defined with `memory:{}`): 6 bound turns → 6 `memory.search` recalls + **4 `memory.upsert_many` → 4 real chunks in `memory.db`** → the pane renders all 4 (content·scope·origin·revision) live. **⚠ LEFTOVER debug state (operator chose KEEP):** `ratatoskr:memprobe` agent on personal WT + test chunks (scope `{end_user:ratatoskr-tui}`) sit in the live `memory.db` — harmless (make the pane show real data); `scripts/reset-sindra-stores.sh` or `DELETE /agents/ratatoskr:memprobe` clears them. **✅ SINDRA MEMORY FIXED (operator-approved, 2026-07-07):** DELETE+redefined her WITH `memory:{}` (prompt byte-identical, `role=character`, OCEAN `{O:0.8,C:0.3,E:0.9,A:0.4,N:0.2}` preserved — pleasure-verified vs the mood-fix setpoint 0.418; backup at scratchpad `sindra_backup.json`). PROVEN: she went from ZERO memory ops → full recall (Orion fact @ cosine 0.988) **+ promotion** (her own "systems architect" fact upserted; store grew to 9→11 chunks). Reusable redefine script: scratchpad `redefine_sindra.sh`. Note: DELETE+redefine is the ONLY path (persona+memory immutable post-define); the destructive `DELETE` tripped the harness auto-mode guard → operator ran it via `!`.
|
|
||||||
|
|
||||||
**(C) Markdown pass-2 SHIPPED** — `markdownSafe` extended: GFM pipe tables (`mdTable`, alignment colons), indentation-nested lists (stack of `<ul>`/`<ol>`, child list inside the open `<li>` = valid nested HTML), ordered-list `start=N` numbering, and streaming robustness (unterminated fence → partial code block; header-without-delimiter → paragraph until the delimiter streams in; never throws). esc-first → INV-004 held. Playwright-verified all cases.
|
|
||||||
|
|
||||||
**SHIPPED — web UI redesign via Claude Design (`v0.20.0`, MINOR, operator-approved).** The Claude Design prototype **`Ratatoskr Console.dc.html`** (project `bc0b65d1-a33e-422a-8bc1-3635c9112775`) was pulled via `DesignSync get_file` (design scopes already granted this session — no `/design-login` needed) and adapted into `src/ratatoskr/web/static/index.html`: translated OUT of the `.dc.html` dialect (`<x-dc>`/`<sc-if>`/`<sc-for>`/`{{}}`/`DCLogic`/external `_ds/` CSS — none runnable) into single-file/no-CDN/vanilla, with ALL real `/api/*` fetch + SSE wired into its DOM (endpoint set + SSE vocab unchanged from the prior SPA — ported verbatim, only DOM hooks re-targeted). New shape = a **3-column command-console**: left engine-ticker rail (DEBUG+ADMIN+tool/turn-lifecycle MERGED into one timeline via `tickerAdd` + a tools-armed chip list + a FULL-detail Bifrost rail pane) · center conversation (per-turn INLINE chain-of-thought, replacing the Think pane) · right RESIZABLE affect console (dominant/canonical-mood centerpiece + bipolar PAD faders EACH with a turn-to-turn Δ+sparkline + a P×A mood orbit + relations metric rows + canonical directive). ADDED (round 2, operator-requested): a **light/dark theme toggle** (dark default; FULL token override — surfaces+fg+borders+accent-as-text, since the designer's light theme only did surfaces → would've been light-on-light) + a **full-detail Bifrost pane** (endpoint/connected/consumer/caps/tools) + fixed the **engine-ticker spine** (was a container-anchored `::before` that scrolled out of view on auto-scroll → re-anchored to a content-height `.ticker-inner` wrapper) + **per-fader PAD turn-to-turn Δ+sparkline** (fills the room beside each meter, from the deduped-per-turn AFFECT_HIST) + an **INLINED data-URI favicon** (operator's `/home/lkraven/rata.png` — chibi aurora squirrel — downscaled 1024→64px via PIL, ~8.6KB base64, kills the /favicon.ico 404). ALL server routes UNCHANGED (**84 web tests green**). Verified BOTH lenses: `pytest tests/test_web_*` (84) + node Playwright drove the real UI end-to-end against personal :8081 (session open → Sindra seeded greeting → live turn SSE → affect console + relations + bifrost detail; theme toggle + PAD deltas + ticker spine + no-favicon-404 all confirmed, dark+light screenshots). `:8765` restarted on the new code. Contract `web_debug_surface.contract.md` amended in-commit (v0.20.0 presenter renames: `renderBifrostState`→`renderBifrost`, `renderAffectPane`→`renderConsole`, `setPersonaStrip` removed; INV-001/INV-004 held). **HONEST-SHAPE call (INV-001, agent-discretion within settled policy):** the dominant-emotion centerpiece shows a real OCC emotion (Tier-1) OR the CANONICAL mood word (Tier-3 e.g. Sindra→"positive and energized", dimmed) OR "—", NEVER a fabricated emotion; the affect-derived grid drops non-emitted intensity/decay-τ, shows only real/client-derived cells. **OPEN (operator's call):** the per-fader PAD Δ placement is a sensible default — operator offered to have the designer spec the exact treatment (hooks are in place to swap it). **`v0.20.1` patch (operator-reported UI):** fixed the relations sparkline overflowing onto the `n` (evidence-count) column — the sparkline grows one char/sample (HIST_CAP=24) and overflowed its fixed grid cell, covering `n`; now capped (relations last-8, faders last-7) + `overflow:hidden` clip; verified via Playwright injecting a 24-sample history (sparkline→n bounding-box overlap = 0). ADDED native `title` mouseover hints on all 3 PAD faders + every relationship metric row (meaning + range; static METRIC_HINTS, esc()'d). Added `state.lastSnap` (console can re-render without a refetch). Playwright-verified.
|
|
||||||
|
|
||||||
**SHIPPED THIS SESSION (all pushed; origin/main == `d75c4e8`; code tip `v0.19.9`) — details in Recent decisions:** the whole **#347 authored-history-write** arc landed end-to-end — OpenAPI re-vendor 2.2.0->2.3.0 (`75da676`), the CONSUMER side (`v0.19.6`: `write_authored_history` + `get_session_messages` + `--seed-first-message`, **live-proven on personal :8081** via a rule-based Heimdall allow — the PDP is rule-based NOT scope-on-key, policy user_id=ratatoskr->ALLOW/others->DENY-hide-404), persona_state `{pad:{pleasure,arousal,dominance}}` canonical alignment (#317) + Tier-3 prose re-vendor (`v0.19.7`), the **first-message-preset AUTO-SEED** (`v0.19.8`: new module `ratatoskr.first_message` wired into all 3 session-create paths, best-effort never-raise/never-block; heid-code-review + heid-bug-hunt hardened), and the web now RENDERS the seeded first-message (`v0.19.9`: new `GET /api/sessions/{id}/messages` route + SPA `loadTranscript`, Playwright-verified). Coverage-map re-converged **REST 19/41**. **Sindra:** her card was PATCHed (the `Startup:` workaround moved into a #347 first-message; non-destructive PATCH — OCEAN/persona/memory intact), and she's currently **RESET clean (0/0)** on the provider stores.
|
|
||||||
|
|
||||||
**Prior arcs this session (2026-07-04 -> 07-06), both with worldtree-dev (a tooling script + proposal docs; the #347 CONSUMER work above is the new production code):**
|
|
||||||
|
|
||||||
**(1) Authored-history-write primitive -> ACCEPTED as Worldtree #347 (Worldtree-owned).** A SillyTavern-style "first-message" (inject a character-authored opening) generalized to an engine primitive: **write a turn into a session's ledger WITHOUT generation, seed-only, side-effects off by default.** It cannot be done client-side (the messages `role` field is a *model-role* override, not an author-role -> `role:"assistant"` 404s; a model-visible authored turn needs engine support). Arc: drafted `docs/proposals/authored-message-injection.md` -> **heid panel pressure-test** (3/3 convergence: recentered on "non-generating write" not author-role; narrowed v1 to append-only+create-time; bounded `effects` enum; dropped edit/regenerate as history-mutation) -> revised -> committed (`c457520`) -> handed to worldtree-dev -> **accepted as design item #347.** worldtree-dev wrote the v1 contract (rev 1.1); **I validated the wire as reference consumer (green).** v1 shape: `POST /sessions/{id}/history`, `author=assistant` only, `effects=none` only, `idempotency_key` REQUIRED (per-session), **model-invisible provenance** (renders byte-identical to a lived assistant turn -> first-message immersion preserved; provenance audit-only), **event-silence** (no turn.started/done, no Bifrost wire for a seed; the 201/200 IS the write-ack), `seeded` lifecycle phase (not exposed on read paths). **Heimdall-gated with hide-existence** (grant `session.history.write`; ungranted tenant -> 404 NOT 403, undiscoverable in /capabilities -> consumer must treat 404 as feature-absent -> fall back to a model-generated greeting, never capability-probe). **Provider constraint:** a create-time first-message makes the assistant seq-0; vLLM/openai_compat tolerate assistant-first (sindra = openai_compat, unaffected), Anthropic-family providers 400 the next generation. **Waiting on worldtree-dev:** #347 TDD (their heid->contract->review workflow) + the consumer-facing 2.3.0 persona/motivational/memory schemas -> then re-vendor our pinned openapi 2.2.0->2.3.0.
|
|
||||||
|
|
||||||
**(2) Sindra's stuck-neutral mood FIXED** (operator-driven "reset + smoke" that flushed out two real upstream problems). Chain: her OCEAN lived only in prompt TEXT, never declared as a structured persona -> the Tier-3 mood engine ran on neutral defaults. Fix = declare OCEAN via the **define-time `persona` field** (immutable via PATCH -> requires DELETE+REDEFINE). Along the way my "the persona didn't store" call was WRONG (persona_state/envelope are Tier-3-blind, see Tried/abandoned); worldtree-dev found a real engine bug **#348** (single-letter vs spelled-out OCEAN keys -> a declared OCEAN silently resolved to 0.0/neutral; fixed in b21, shipped to personal as b22); then a clean bound-egress read STILL neutral -> the **personal container was running a stale image** (the b22 deploy was a pull-only no-op racing the main build; infra-ops force-swapped run 8211, verified `2.3.0` / `879cefe`). **VERIFIED FIXED:** bound mood-smoke reads `(0.448, 0.267, 0.316)` ~= the OCEAN-derived setpoint `(0.418, 0.249, 0.328)`. Sindra is currently reset clean (0/0) on `role=character`; her persona is stored + correct (**no re-define needed again**).
|
|
||||||
|
|
||||||
**(3) R30 CLOSED** (operator steer 2026-07-04, relayed via worldtree-dev): graduated on offline-tests + human face-validity, NO deployed gap-injection run (it was confirmatory-not-measuring per brokkr's S0 reframe; offline tests already cover the OU formula + both directions). My gap-injection harness (read/predict/record; write side stubbed; `predict()` self-validated vs brokkr's N=0 anchors) is BANKED at `diag/r30-gap-injection-harness` (`7156b25`-era) for the PARKED powered true-tau study.
|
|
||||||
|
|
||||||
**Persona-declaration shape (Worldtree #343/#348, live on personal b22):** `POST /agents/define` `persona:{ocean:{O,C,E,A,N: float[-1,1]}}` (single-letter keys EXACTLY -- missing/extra -> 422 `persona_ocean_required`; out-of-range -> 422); **NO baseline PAD** (resting setpoint DERIVED from OCEAN via Mehrabian: pleasure=0.21E+0.59A+0.19C-0.32N, arousal=0.15O+0.30E-0.57A+0.15N, dominance=0.25E+0.17A+0.10O-0.14N); negative-channel gain + per-axis decay-tau derive from N. `valence` deferred (422 `layer_deferred`); `motivational`/`memory` active (#187/#189). Persona is **write-once at define, immutable thereafter** (PATCH takes ONLY system_prompt + role). **`role` supersedes `model`** -- set a role (`character` / `character-rp`), Worldtree resolves the model; #344 (b19) fixed the model-field to surface the ROLE, not the resolved catalog_id. `character-rp` = a reasoning-tuned RP config (gen-reasoning + temp 0.75 + RP extra_body); `character` = plain non-reasoning. The `tier3.py` client CLI is STALE (has `--model`, no `--role`; model is now immutable) -> role/persona set via raw curl.
|
|
||||||
|
|
||||||
**New tooling: `scripts/reset-sindra-stores.sh`** (`0a8784c`) -- one-command self-service provider-store reset: stop the combined :8392 provider -> move memory.db+affect.db to a single ROLLING backup (`db-reset-backup/`, gitignored via *.db*; `--hard` skips it) -> restart empty -> verify 0/0. Codifies the manual reset flow done repeatedly this session. **The combined `:8392` provider is THE provider now**; the separate `:8390` (affect) / `:8391` (memory) single-plane providers were pruned as stale duplicates. To drive a BOUND session from the CLI use `--new --bifrost-url http://10.100.10.50:8392` (the CLI's `--bifrost-plane affect/memory` map to the pruned :8390/:8391 -> unreachable; `combined` is not a `--bifrost-plane` choice).
|
|
||||||
|
|
||||||
**Standing (carried from prior snapshots, still true):** the web surface (`ratatoskr-web`, :8765) is the operator's PRIMARY debug surface at full TUI pane parity (v0.19.5); the **v1 coverage-audit has CONVERGED** -- REST 17/40 (zero in-scope gaps, 23 excluded-by-design), SSE 11/11, Bifrost provider planes 8/8 live-proven; the living ledger is `docs/coverage-map.md`; **v1 cuts when Worldtree tags 1.0** (ratatoskr v1 = full Worldtree I/O coverage). Debug-observability core complete (Persona/Tools/BifrostState/AdminEvents). Substrate pins: **bifrost `==1.1.1` / wire v0.7** (bumped 2026-07-12 from 1.1.0 — the frozen-v0.6 serialization fix, v0.20.10; prior 1.1.0 bumped 2026-07-07 from 1.0.0; NOW WIRE-ALIGNED with Worldtree personal-b47 which adopted wire-v0.7 — bound Tier-3 fully restored 2026-07-10; keeping 1.1.0 was load-bearing, see the `[2026-07-10]` handshake decision); Worldtree openapi vendored **2.3.0** (re-vendored 2026-07-06 for #347 `POST /sessions/{id}/history`; drift-clean vs source), pinned + drift-gated in `.corviduo-canonicals.toml`; **suite 631 green.** **Personal WT on b61/wire-v0.7** (deploy train through this cycle: b35→b44→b46→b47→b60→b61; b60 landed the #355 STICK fix, b61 the orthogonal over-budget trigger fix + a llama.cpp reasoning-budget seat). **Drift-check note (RESOLVED 2026-07-13):** the two `tolerate_drift` WARN pins (`worldtree-affect-egress-consumer-reference-v1` + `worldtree-conversation-api-spec-v1`) were RE-SYNCED — the drift was a benign 2-line R32-1B doc note (PAD `[-1,1]` → unbounded latent `z` w/ `~±10` wire bound) documenting the unbounded-z change ratatoskr ALREADY adopted in v0.20.9, NOT the anticipated we-framing conditional (that remains a FUTURE coordinated re-vendor when the brokkr render epic lands). All canonicals now drift-clean. **NEW vendored canon (Vuong-directed via brokkr):** the R34/R35 psych-profile reference — `brokkr-psych-profile-authoring-spec-v1` + `brokkr-psych-profile-parameters-v1` — pinned under `docs/vendor/brokkr-r34-psych-profile/` (canonical_source `brokkr-smithy`, tolerate_drift; the authoring-spec GOVERNS on conflict with the parameter distillation; brokkr owns both + pings on change). Keys env-only mode-600 (consumer/Heimdall in `~/.config/ratatoskr/provider.env`; admin `RATATOSKR_ADMIN_API_KEY` = 7 read scopes, **personal-:8081-only**; Heimdall keys are PER-INSTANCE). Provider identity settled -- ratatoskr owns both ends of the Bifrost round-trip; `ratatoskr:sindra` is the owner-scoped Tier-3 agent (invisible to `GET /agents`; check `GET /agents/<owner>:<name>` with the owner key). Providers run as dev-box BACKGROUND SHELLS. `graphify-out/` runs dirty (auto-regen, never stage). Branch `main`, HEAD `7bca76e` (origin/main synced through v0.20.10 + drift-sync); remote `origin -> git@gitea.phasefinal.com:vh/ratatoskr.git`. Open/deferred: #10 (subject-migration watch); the relational-dynamics-arc verify (deferred, bind mechanism known: `--bifrost-url :8392`); the P06 optimization-phase re-drive (future, brokkr brings the prereg); the we-framing-conditional affect-egress re-vendor (future, when the brokkr render epic lands — the R32-1B doc-note drift is already resolved). (WT #355 loop-in obligation DISCHARGED 2026-07-13; WT #356 resume-durability gap is worldtree-owned.) **Debug state CLEANED (2026-07-13, reverses the prior KEEP):** provider stores reset to 0/0 + `ratatoskr:memprobe` deleted — clean slate for the Sindra run, no leftover debug state.
|
|
||||||
|
|
||||||
## Recent decisions
|
## Recent decisions
|
||||||
|
|
||||||
@@ -214,6 +246,14 @@ decision. Captures rationale that won't be obvious from code alone.
|
|||||||
- `[2026-07-13]` **WT #355 VALIDATION — fix CONFIRMED; the standing loop-in obligation is DISCHARGED.** The fully-instrumented re-drive ran, coordinated from the ratatoskr seat: infra-ops armed a full WT-netns pcap + py-spy (T0/30/60/300) on the b60 :8081 container; soong drove an 8-turn accumulating RP-with-tools repro on a FRESH session. Authoritative WT-side turns-table: wedging turns 2064/2065 → `completed=True, cancelled=1, phase=STALLED`, dur 302s/360s — the 300s stall-watchdog + Slice-C cancel-INDEPENDENT terminal fired cleanly, vs the pre-b60 baseline (turn 2061) 16-min hang / NO terminal. Slice-B `_log_wedged_task_stack` named the frame (`agent_turn.py:586 async for chunk in stream_iter`, idle-in-epoll — the wedge was a thinking-phase over-budget hang, NOT the attach_tool precursor first assumed). soong's client verdict is 45s-masked (soong-lab v0.3.2 idle-timeout) → NOT b60's terminal; the WT-side capture is authoritative. Threads `01KXE0MXDX…`(wt) / `01KXE0X2DD…`(infra) / `01KXE0X6GR…`(soong).
|
- `[2026-07-13]` **WT #355 VALIDATION — fix CONFIRMED; the standing loop-in obligation is DISCHARGED.** The fully-instrumented re-drive ran, coordinated from the ratatoskr seat: infra-ops armed a full WT-netns pcap + py-spy (T0/30/60/300) on the b60 :8081 container; soong drove an 8-turn accumulating RP-with-tools repro on a FRESH session. Authoritative WT-side turns-table: wedging turns 2064/2065 → `completed=True, cancelled=1, phase=STALLED`, dur 302s/360s — the 300s stall-watchdog + Slice-C cancel-INDEPENDENT terminal fired cleanly, vs the pre-b60 baseline (turn 2061) 16-min hang / NO terminal. Slice-B `_log_wedged_task_stack` named the frame (`agent_turn.py:586 async for chunk in stream_iter`, idle-in-epoll — the wedge was a thinking-phase over-budget hang, NOT the attach_tool precursor first assumed). soong's client verdict is 45s-masked (soong-lab v0.3.2 idle-timeout) → NOT b60's terminal; the WT-side capture is authoritative. Threads `01KXE0MXDX…`(wt) / `01KXE0X2DD…`(infra) / `01KXE0X6GR…`(soong).
|
||||||
- `[2026-07-13]` **b61 adopted as the personal target — the orthogonal over-budget TRIGGER also fixed.** worldtree shipped b61: the provider stream loop terminates on `finish_reason` + a per-read idle deadline + a 300s wall-clock backstop (no longer waits on the SDK `[DONE]` sentinel), plus a custom llama.cpp reasoning-budget multi-terminator seat → the runaway is bounded at BOTH layers. The #355 STICK (no-terminal) and its trigger (why it wedges) are now separately fixed. Resume-durability gap → **WT #356** (worldtree-owned).
|
- `[2026-07-13]` **b61 adopted as the personal target — the orthogonal over-budget TRIGGER also fixed.** worldtree shipped b61: the provider stream loop terminates on `finish_reason` + a per-read idle deadline + a 300s wall-clock backstop (no longer waits on the SDK `[DONE]` sentinel), plus a custom llama.cpp reasoning-budget multi-terminator seat → the runaway is bounded at BOTH layers. The #355 STICK (no-terminal) and its trigger (why it wedges) are now separately fixed. Resume-durability gap → **WT #356** (worldtree-owned).
|
||||||
- `[2026-07-13]` **Cleaned + prepped for a Sindra run (operator: "clean up everything + prep").** Reset provider stores to 0/0 (`reset-sindra-stores.sh`, rolling backup `db-reset-backup/`); deleted the throwaway `ratatoskr:memprobe` agent via the operator's `!` (destructive DELETE trips the auto-guard — reverses the earlier KEEP). `ratatoskr:sindra` verified present + persona-intact on b61. Environment Sindra-run-ready (web :8765 + provider :8392 both up, single healthy provider instance); operator driving the run interactively.
|
- `[2026-07-13]` **Cleaned + prepped for a Sindra run (operator: "clean up everything + prep").** Reset provider stores to 0/0 (`reset-sindra-stores.sh`, rolling backup `db-reset-backup/`); deleted the throwaway `ratatoskr:memprobe` agent via the operator's `!` (destructive DELETE trips the auto-guard — reverses the earlier KEEP). `ratatoskr:sindra` verified present + persona-intact on b61. Environment Sindra-run-ready (web :8765 + provider :8392 both up, single healthy provider instance); operator driving the run interactively.
|
||||||
|
- `[2026-07-14]` **soong-lab adopted as our Tier-3 agent-authoring studio (operator-directed).** ratatoskr consumes soong-lab bundles → WT `agents.define`, and ships agents back as bundles. Sindra round-trip proven (soong imported her `resume` half through real `import_bundle`); soong-lab export+importer contracts pinned via canonical-sync (`canonical_source=soong-lab` @ f434016, commit `39050c3`). The 4 soong-lab ROLE_CHOICES = WT model-role slugs 1:1 by name (worldtree-dev); `character`/`thoughtful-character` need the `character` grant (held), `assistant`/`thoughtful-assistant` need `foundational` (routed to infra-ops). NOT on the v1 coverage-map (sibling-studio interop, not a WT I/O point) — operator chose to pursue anyway.
|
||||||
|
- `[2026-07-15]` **Cross-session recall failure root-caused → the person-prime `scan` build.** worldtree-dev: WT injects a recalled fact only if combined score (sim×salience) ≥ 0.45 (`auto_inject_combined_score_threshold`) AND recall is per-turn query-gated → moderate-sim durable facts (name ~0.40) never inject. Designed turn-0 fix = WT #349 person-prime (query-less top-N-by-recency injection), capability-gated on the store advertising `updated_at` sort — dark for our provider. Fix = implement the sorted `scan` verb + advertise `sortable_chunk_fields` (ZERO WT change). Contract-first (un-defers bifrost `scan`); skipped heid-contract-review (external spec from worldtree-dev, validated point-for-point). **SHIPPED + deployed + live-verified v0.20.14** → `persistent-memory.d/2026-07-16-person-prime-scan-shipped.md`.
|
||||||
|
- `[2026-07-15]` **Sindra redefined from the soong-lab bundle** (`/tmp/sindra.json`, operator "update to match"). Persona immutable → DELETE+redefine; `memory:{}` preserved; motivational string→WT-object mapped (synthesized id/type/salience, flagged to operator); role `character-rp`→`thoughtful-character` (same seat). Payload validated via a throwaway probe (`sindra-probe2`→201) BEFORE the destructive delete. first_message preset updated + web restarted.
|
||||||
|
|
||||||
|
- `[2026-07-16]` **WT #364 + brokkr R39 re-drive (DECISIVE) — name-recall root-caused Worldtree-side; the fix is the signal FAMILY, not a threshold.** No (sim,salience) fusion can fix it (stale negative Pareto-dominates the true name); our specimen + operator's subject-provenance catch (Sindra's own prompt-behavior leaked into user memory) shaped #364's `(subject,relation)` slot-supersession + identity-tier fix. → `persistent-memory.d/2026-07-16-wt364-r39-name-recall.md`
|
||||||
|
- `[2026-07-16]` **bifrost ruled scan snapshot-cursor NORMATIVE (offset NOT blessed) + shipped conformance coverage in bifrost 1.1.3.** Our multi-page offset cursor is now known-non-conformant (single-page person-prime is fine, nothing shipped is broken); adoption is operator-sequenced. → `persistent-memory.d/2026-07-16-bifrost-cursor-conformance.md`
|
||||||
|
|
||||||
|
- `[2026-07-16]` **brokkr flagged ratatoskr as R39 Phase-2 PROBE-RUNNER (Arm-2 contradiction set = our frozen 10-chunk specimen); ACCEPTED IN PRINCIPLE (Vuong 2026-07-16) — formal scope + effort commit lands when Arm-2 actually spins.** Non-urgent — downstream of worldtree's #364 impl; brokkr pings the export shape when Arm-2 spins. Gate: reproduce AUROC~0.59 (similarity can't separate contradiction from paraphrase) on the domain set BEFORE crediting deterministic slot-supersession; eval is a SURFACING test not retention (injection=0 hard-gate + surfacing-recall≥0.95, §5.5). Prereg: brokkr `research/R39-memory-salience-dreams-surfacing/phase-2/preregistration.md` (R39.9). Tracked at brokkr's Phase-2 prereg + the Arm-2-spin ping (thread `01KXMT42Z7…`); I sent a non-committal receipt (role routed to operator).
|
||||||
|
|
||||||
_41 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md._
|
_41 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md._
|
||||||
|
|
||||||
@@ -268,7 +308,11 @@ defense against re-attempting the same cul-de-sac.
|
|||||||
|
|
||||||
- `[2026-07-13]` **`thinking_enabled=False` on the define was a NO-OP — char-rp-reasoning ignores it.** The naive kwarg never reached the seat: char-rp-reasoning resolves to a base gateway provider whose thinking-translator returns `{}` for the flag on AND off. The real lever is the gateway param `chat_template_kwargs.enable_thinking:false` (infra-ops confirmed it via a 642ch→0ch reasoning-token delta). **To toggle reasoning on a gateway-backed seat, set the chat-template kwarg, not a generic `thinking_enabled` flag.**
|
- `[2026-07-13]` **`thinking_enabled=False` on the define was a NO-OP — char-rp-reasoning ignores it.** The naive kwarg never reached the seat: char-rp-reasoning resolves to a base gateway provider whose thinking-translator returns `{}` for the flag on AND off. The real lever is the gateway param `chat_template_kwargs.enable_thinking:false` (infra-ops confirmed it via a 642ch→0ch reasoning-token delta). **To toggle reasoning on a gateway-backed seat, set the chat-template kwarg, not a generic `thinking_enabled` flag.**
|
||||||
- `[2026-07-13]` **A same-image redeploy does NOT reload a bind-mounted config — the ModelRegistry boot-caches it at `__init__`.** After the config was synced on-disk (infra-ops validated) and `docker compose up -d` re-ran, the reasoning-off change STILL didn't take: an unchanged image makes `up -d` a no-op (no container recreate), so the process kept serving the pre-sync config. Fix = a surgical `docker restart <container>` (same image, no pull) → the process re-boot-reads the config. **When an on-disk config change doesn't take effect, suspect the process cached it at startup; force a container RESTART, not a redeploy** (a docs-only forcing-commit also won't rebuild if docs are paths-ignored in CI). This is the config-plane sibling of the `[2026-07-06]` stale-image foot-gun.
|
- `[2026-07-13]` **A same-image redeploy does NOT reload a bind-mounted config — the ModelRegistry boot-caches it at `__init__`.** After the config was synced on-disk (infra-ops validated) and `docker compose up -d` re-ran, the reasoning-off change STILL didn't take: an unchanged image makes `up -d` a no-op (no container recreate), so the process kept serving the pre-sync config. Fix = a surgical `docker restart <container>` (same image, no pull) → the process re-boot-reads the config. **When an on-disk config change doesn't take effect, suspect the process cached it at startup; force a container RESTART, not a redeploy** (a docs-only forcing-commit also won't rebuild if docs are paths-ignored in CI). This is the config-plane sibling of the `[2026-07-06]` stale-image foot-gun.
|
||||||
|
- `[2026-07-15]` **soong-lab bundle `ship.native.motivational.goals/fears` are bare STRINGS, but WT `agents.define` requires OBJECTS** `{id, type∈{maintenance,achievement,avoidance}, salience[0,1], description≥20ch}` (`ValidatedGoal`/`ValidatedFear`, worldtree `core/conversation_api/api.py:2116`, extra=forbid; ids unique across goals+fears). So `ship.native` is NOT directly define-valid on motivational (soong-lab's Frame Invariant 1 breaks there). A bundle→WT consumer MUST map string→object + synthesize id/type/salience. Informed soong-dev → **RESOLVED soong-lab v0.3.24 (2026-07-16): now emits WT-valid objects (type+salience captured, id synthesized at export, validate_exportable gates description≥20/enum/range) → drop the string→object workaround for v0.3.24+ bundles; legacy pre-fix designs coerce on open, so re-exports are valid too.** **Rule: PROBE a define payload under a throwaway agent_name BEFORE a destructive DELETE+redefine** — validating via `sindra-probe2`→201 caught the 422 without leaving the real Sindra deleted+undefined. (A failed probe still RESERVES the name → 409 on retry; use a fresh probe name.)
|
||||||
|
- `[2026-07-15]` **`re.sub`/`re.subn` INTERPRETS backslash-escapes in the REPLACEMENT string** — a `json.dumps`'d value (escaped `\n`) fed as the replacement leaked REAL newlines into a source file (broke `first_message.py` with an unterminated-string SyntaxError). Fix: use a FUNCTION replacement (`pat.subn(lambda m: new_block, src)`) or `str.replace` — the callable form bypasses escape processing. json.dumps itself escapes correctly; re.sub was the culprit.
|
||||||
- `[2026-07-13]` **Called Deckard "hung" off a short timeout — WRONG (operator correction).** A 30-45s no-terminal on the char-rp-reasoning seat looked like a hang; operator: "is it HUNG? deckard is EXTREMELY verbose, without enough context, you never see the non-reasoning tokens." It was verbose reasoning-CoT on a long extraction prompt, not a wedge. **Don't call a reasoning seat hung off a latency threshold — the CoT is invisible and slow; distinguish slow-verbose from actually-wedged before concluding.** (The genuine wedge is WT #355, a distinct mechanism — no-terminal even after the 300s watchdog, not merely slow.)
|
- `[2026-07-13]` **Called Deckard "hung" off a short timeout — WRONG (operator correction).** A 30-45s no-terminal on the char-rp-reasoning seat looked like a hang; operator: "is it HUNG? deckard is EXTREMELY verbose, without enough context, you never see the non-reasoning tokens." It was verbose reasoning-CoT on a long extraction prompt, not a wedge. **Don't call a reasoning seat hung off a latency threshold — the CoT is invisible and slow; distinguish slow-verbose from actually-wedged before concluding.** (The genuine wedge is WT #355, a distinct mechanism — no-terminal even after the 300s watchdog, not merely slow.)
|
||||||
- `[2026-07-13]` **Resumed-session context-snapshot is IN-MEMORY → lost on a container recreate (agent_not_available on resume).** During the #355 re-drive, soong's fresh drive 409'd `agent_not_available`. Root cause (after ~4 refinements — agent-loss? zombie turn-lock? stale-sessions-hold-agent? → the actual mechanism): `get_agent_context_for_session` returns the agent snapshot recorded AT SESSION-CREATE, held in-memory; a pre-recreate session resumed on b60/b61 has no snapshot → None → 409. (Compounding: stale `'active'` sessions left un-terminated by the old no-terminal bug HOLD the agent, blocking new creates too.) Deploy-grounding was healthy the whole time (`registry.resolve("char-rp-reasoning")` OK) — the config/grant hypotheses were all red herrings. Fix = a FRESH session (a studio-service restart re-records the snapshot); pre-recreate sessions need retiring. Tracked **WT #356**. **For any run: create a fresh session, never resume a pre-recreate one; `agent_not_available` on a fresh create = this gap.** (Working-style note: I over-relayed the intermediate root-cause churn to the operator — for a peer-owned block being actively diagnosed, hold until it settles.)
|
- `[2026-07-13]` **Resumed-session context-snapshot is IN-MEMORY → lost on a container recreate (agent_not_available on resume).** During the #355 re-drive, soong's fresh drive 409'd `agent_not_available`. Root cause (after ~4 refinements — agent-loss? zombie turn-lock? stale-sessions-hold-agent? → the actual mechanism): `get_agent_context_for_session` returns the agent snapshot recorded AT SESSION-CREATE, held in-memory; a pre-recreate session resumed on b60/b61 has no snapshot → None → 409. (Compounding: stale `'active'` sessions left un-terminated by the old no-terminal bug HOLD the agent, blocking new creates too.) Deploy-grounding was healthy the whole time (`registry.resolve("char-rp-reasoning")` OK) — the config/grant hypotheses were all red herrings. Fix = a FRESH session (a studio-service restart re-records the snapshot); pre-recreate sessions need retiring. Tracked **WT #356**. **For any run: create a fresh session, never resume a pre-recreate one; `agent_not_available` on a fresh create = this gap.** (Working-style note: I over-relayed the intermediate root-cause churn to the operator — for a peer-owned block being actively diagnosed, hold until it settles.)
|
||||||
|
|
||||||
|
- `[2026-07-16]` **`sortable_chunk_fields` advertised WITHOUT the required `type` field = whole-handshake deploy-breaker; only DRIVING the real bind caught it.** bifrost `handshake_response` `SortableChunkField` requires BOTH `name`+`type` (`additionalProperties:false`); we shipped `[{"name":"updated_at"}]` → the response failed wire-schema validation → `bifrost.schema_validation_failed` → the ENTIRE bind (memory+affect) broke, not just sort. Unit tests + worldtree-dev's name-only service parser + the heid-bug-hunt ALL passed it — only the live handshake drive (`/verify` discipline) caught it. **Lesson: validate `describe_store` against the bifrost WIRE schema, not just our own caps assertions.** (Sibling of the `[2026-07-10]` frozen-v0.6 handshake foot-gun — there an EXTRA field broke a v0.6 handshake, here a MISSING required field broke a v0.7 one.)
|
||||||
|
|
||||||
_18 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md._
|
_18 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md._
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "ratatoskr"
|
name = "ratatoskr"
|
||||||
version = "0.20.12"
|
version = "0.20.15"
|
||||||
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
|
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
@@ -30,7 +30,7 @@ web = [
|
|||||||
# from the debug TUI. Recipe: bifrost/docs/implementing-a-consumer.md.
|
# from the debug TUI. Recipe: bifrost/docs/implementing-a-consumer.md.
|
||||||
provider = [
|
provider = [
|
||||||
"ratatoskr[web]", # reuse the starlette + uvicorn ASGI stack
|
"ratatoskr[web]", # reuse the starlette + uvicorn ASGI stack
|
||||||
"bifrost==1.1.1", # consumer engines + library. 1.1.1 = frozen-wire serialization fix (ADR-0008): additive capability fields are gated on the NEGOTIATED wire, so a v0.6-negotiated describe_store handshake stays v0.6-clean. 1.1.0 leaked the v0.7-additive `sortable_chunk_fields` into v0.6 StoreCapabilities → a strict v0.6 client (additionalProperties:false) rejects our server's handshake. Wire schemas + pins UNCHANGED (serialization-correctness only); our v0.7 handshake with Worldtree b47 is unaffected. (1.1.0 = wire v0.7 additive: memory.scan sort + sortable_chunk_fields; 1.0.0 = first STABLE, wire v0.6 FROZEN; 0.8.0/v0.6 scope_all/scope_any #11; 0.7.0/v0.5 agent_self)
|
"bifrost==1.1.4", # consumer engines + library. 1.1.4 = hasattr-gate backstop for the maintenance verbs (mark_superseded/mark_invalid/patch_many/delete_many/upsert_edges/get_edges_for → unimplemented verb degrades to unsupported_capability 400, never AttributeError/500/retry-storm; we surfaced it via WT #364) + 1.1.3 scan/cursor conformance harness + 1.1.2 frozen-v0.6 fix. 1.1.1 = frozen-wire serialization fix (ADR-0008): additive capability fields are gated on the NEGOTIATED wire, so a v0.6-negotiated describe_store handshake stays v0.6-clean. 1.1.0 leaked the v0.7-additive `sortable_chunk_fields` into v0.6 StoreCapabilities → a strict v0.6 client (additionalProperties:false) rejects our server's handshake. Wire schemas + pins UNCHANGED (serialization-correctness only); our v0.7 handshake with Worldtree b47 is unaffected. (1.1.0 = wire v0.7 additive: memory.scan sort + sortable_chunk_fields; 1.0.0 = first STABLE, wire v0.6 FROZEN; 0.8.0/v0.6 scope_all/scope_any #11; 0.7.0/v0.5 agent_self)
|
||||||
"jsonschema>=4", # bifrost runtime dep — envelope validation
|
"jsonschema>=4", # bifrost runtime dep — envelope validation
|
||||||
"sqlite-vec>=0.1.6", # vector index for the memory plane (vec0 virtual table)
|
"sqlite-vec>=0.1.6", # vector index for the memory plane (vec0 virtual table)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -132,7 +132,10 @@ def _validate_injection(record: dict) -> None:
|
|||||||
raise InvalidArguments("injection_source only valid for injected_context origin")
|
raise InvalidArguments("injection_source only valid for injected_context origin")
|
||||||
|
|
||||||
|
|
||||||
_SORTABLE_CHUNK_FIELDS: list[dict] = [{"name": "updated_at"}]
|
# bifrost handshake_response SortableChunkField requires BOTH name + type
|
||||||
|
# (additionalProperties:false); omitting `type` fails wire-schema validation and
|
||||||
|
# breaks the whole handshake. `type` is advisory-only (the wire never interprets it).
|
||||||
|
_SORTABLE_CHUNK_FIELDS: list[dict] = [{"name": "updated_at", "type": "timestamp"}]
|
||||||
_SORTABLE_FIELD_NAMES = frozenset(f["name"] for f in _SORTABLE_CHUNK_FIELDS)
|
_SORTABLE_FIELD_NAMES = frozenset(f["name"] for f in _SORTABLE_CHUNK_FIELDS)
|
||||||
|
|
||||||
|
|
||||||
@@ -140,6 +143,8 @@ def _is_live(record: dict) -> bool:
|
|||||||
"""INV-009: a chunk is live unless a lifecycle/governance marker says otherwise.
|
"""INV-009: a chunk is live unless a lifecycle/governance marker says otherwise.
|
||||||
scan returns live-only server-side (person-prime's `lifecycle_state=live` does not
|
scan returns live-only server-side (person-prime's `lifecycle_state=live` does not
|
||||||
ride the scan wire, so this is authoritative — a dead fact can never inject)."""
|
ride the scan wire, so this is authoritative — a dead fact can never inject)."""
|
||||||
|
if record.get("superseded") is True: # INV-011: mark_superseded top-level flag (#364 retirement)
|
||||||
|
return False
|
||||||
state = record.get("lifecycle_state")
|
state = record.get("lifecycle_state")
|
||||||
if isinstance(state, str) and state and state != "live":
|
if isinstance(state, str) and state and state != "live":
|
||||||
return False
|
return False
|
||||||
@@ -367,6 +372,36 @@ class RatatoskrMemoryStore:
|
|||||||
self._conn.execute("DELETE FROM memory_vec WHERE chunk_id = ?", (chunk_id,))
|
self._conn.execute("DELETE FROM memory_vec WHERE chunk_id = ?", (chunk_id,))
|
||||||
return {"deleted": deleted}
|
return {"deleted": deleted}
|
||||||
|
|
||||||
|
async def mark_superseded(
|
||||||
|
self, ids: list[str], *, superseded_by: str | None = None, reason: str | None = None
|
||||||
|
) -> dict:
|
||||||
|
# Worldtree #364 retirement (INV-011). Mirrors the reference `_mark_lifecycle`:
|
||||||
|
# sets TOP-LEVEL `superseded`/`superseded_by`/`superseded_reason` (only non-None fields),
|
||||||
|
# increments revision. NON-destructive — get still returns; scan (live-only) excludes via
|
||||||
|
# `_is_live`'s `superseded` short-circuit. The sole supersession verb #364 uses.
|
||||||
|
_log.info("memory-call mark_superseded REQUEST: ids=%r superseded_by=%s", ids, superseded_by)
|
||||||
|
fields = {"superseded": True, "superseded_by": superseded_by, "superseded_reason": reason}
|
||||||
|
marked = 0
|
||||||
|
with self._conn:
|
||||||
|
for chunk_id in ids:
|
||||||
|
row = self._conn.execute(
|
||||||
|
"SELECT record_json FROM memory_chunks WHERE chunk_id = ?", (chunk_id,)
|
||||||
|
).fetchone()
|
||||||
|
if row is None: # unknown id -> skip (never error), mirrors reference + delete_many
|
||||||
|
continue
|
||||||
|
record = json.loads(row[0])
|
||||||
|
for key, value in fields.items():
|
||||||
|
if value is not None: # reference writes only non-None fields
|
||||||
|
record[key] = value
|
||||||
|
self._conn.execute(
|
||||||
|
"UPDATE memory_chunks SET record_json = ?, revision = revision + 1 "
|
||||||
|
"WHERE chunk_id = ?",
|
||||||
|
(json.dumps(record), chunk_id),
|
||||||
|
)
|
||||||
|
marked += 1
|
||||||
|
_log.info("memory-call mark_superseded RESPONSE: marked=%d", marked)
|
||||||
|
return {"marked": marked}
|
||||||
|
|
||||||
async def scan(
|
async def scan(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -383,6 +418,8 @@ class RatatoskrMemoryStore:
|
|||||||
scope_all = scope_all or {}
|
scope_all = scope_all or {}
|
||||||
scope_any = scope_any or []
|
scope_any = scope_any or []
|
||||||
_validate_scope(scope_all, scope_any) # PRE-002 (same lattice as search)
|
_validate_scope(scope_all, scope_any) # PRE-002 (same lattice as search)
|
||||||
|
if sort is not None and not isinstance(sort, dict): # PRE-003: malformed sort => reject, never crash
|
||||||
|
raise InvalidArguments("sort must be an object with field and direction")
|
||||||
field = (sort or {}).get("field", "updated_at")
|
field = (sort or {}).get("field", "updated_at")
|
||||||
direction = (sort or {}).get("direction", "desc")
|
direction = (sort or {}).get("direction", "desc")
|
||||||
if field not in _SORTABLE_FIELD_NAMES or direction not in ("asc", "desc"): # PRE-003
|
if field not in _SORTABLE_FIELD_NAMES or direction not in ("asc", "desc"): # PRE-003
|
||||||
|
|||||||
@@ -62,6 +62,15 @@ def test_fresh_db_advertises_v1_caps_and_schema():
|
|||||||
assert caps["atomic_supersede_supported"] is False
|
assert caps["atomic_supersede_supported"] is False
|
||||||
assert caps["transaction_supported"] is False
|
assert caps["transaction_supported"] is False
|
||||||
assert caps["filterable_metadata_fields"] == []
|
assert caps["filterable_metadata_fields"] == []
|
||||||
|
# bifrost handshake_response SortableChunkField requires BOTH name + type
|
||||||
|
# (additionalProperties:false) — omitting `type` fails wire-schema validation and
|
||||||
|
# breaks the ENTIRE Bifrost bind (regression guard: the deploy-breaker of 2026-07-15).
|
||||||
|
scf = caps["sortable_chunk_fields"]
|
||||||
|
assert scf == [{"name": "updated_at", "type": "timestamp"}]
|
||||||
|
for entry in scf:
|
||||||
|
assert set(entry) == {"name", "type"} # required exactly, no extra keys
|
||||||
|
assert isinstance(entry["name"], str) and entry["name"]
|
||||||
|
assert isinstance(entry["type"], str) and entry["type"]
|
||||||
# tables + vec index queryable
|
# tables + vec index queryable
|
||||||
store._conn.execute("SELECT * FROM memory_chunks")
|
store._conn.execute("SELECT * FROM memory_chunks")
|
||||||
store._conn.execute("SELECT * FROM memory_idempotency")
|
store._conn.execute("SELECT * FROM memory_idempotency")
|
||||||
@@ -427,6 +436,14 @@ async def test_scan_unadvertised_sort_field_rejected():
|
|||||||
await store.scan(scope_all={"end_user": "u1"}, limit=3, sort={"field": "salience", "direction": "desc"})
|
await store.scan(scope_all={"end_user": "u1"}, limit=3, sort={"field": "salience", "direction": "desc"})
|
||||||
|
|
||||||
|
|
||||||
|
async def test_scan_non_dict_sort_rejected():
|
||||||
|
# PRE-003: a truthy non-dict sort (caller-controlled) -> InvalidArguments, never AttributeError.
|
||||||
|
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
|
||||||
|
for bad in ("updated_at", ["updated_at"], 5):
|
||||||
|
with pytest.raises(InvalidArguments):
|
||||||
|
await store.scan(scope_all={"end_user": "u1"}, limit=3, sort=bad)
|
||||||
|
|
||||||
|
|
||||||
async def test_scan_records_carry_person_prime_filter_fields():
|
async def test_scan_records_carry_person_prime_filter_fields():
|
||||||
# The client _scan_filter_matches keys on agent_id + subject + worldtree_scope; a record
|
# The client _scan_filter_matches keys on agent_id + subject + worldtree_scope; a record
|
||||||
# missing any is silently dropped -> the scan record must carry them verbatim.
|
# missing any is silently dropped -> the scan record must carry them verbatim.
|
||||||
@@ -490,6 +507,60 @@ async def test_scan_parity_vs_reference_inmemory_store():
|
|||||||
assert out_ours["records"] == out_ref["records"] # verbatim record shape parity
|
assert out_ours["records"] == out_ref["records"] # verbatim record shape parity
|
||||||
|
|
||||||
|
|
||||||
|
# --- mark_superseded (#364 contradiction retirement) ---
|
||||||
|
|
||||||
|
async def test_mark_superseded_retires_from_scan():
|
||||||
|
# tracer: mark a chunk superseded -> scan (live-only) excludes it; record carries the flags.
|
||||||
|
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
|
||||||
|
recs = [_chunk(f"c{i}", scope={"end_user": "u1"}, updated_at=f"2026-07-15T00:0{i}:00+00:00") for i in range(3)]
|
||||||
|
await store.upsert_many(recs, idempotency_key="k", ctx=_ctx())
|
||||||
|
assert await store.mark_superseded(["c1"], superseded_by="c9") == {"marked": 1}
|
||||||
|
out = await store.scan(scope_all={"end_user": "u1"}, limit=10, sort={"field": "updated_at", "direction": "desc"})
|
||||||
|
assert [r["id"] for r in out["records"]] == ["c2", "c0"] # c1 excluded (superseded)
|
||||||
|
got = await store.get("c1")
|
||||||
|
assert got["superseded"] is True and got["superseded_by"] == "c9"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mark_superseded_non_destructive_get_still_returns():
|
||||||
|
# INV-011: retirement is non-destructive — get still returns a superseded chunk (recoverable).
|
||||||
|
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
|
||||||
|
await store.upsert_many([_chunk("c1", scope={"end_user": "u1"})], idempotency_key="k", ctx=_ctx())
|
||||||
|
await store.mark_superseded(["c1"], superseded_by="x")
|
||||||
|
got = await store.get("c1")
|
||||||
|
assert got is not None and got["superseded"] is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mark_superseded_unknown_id_noop():
|
||||||
|
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
|
||||||
|
assert await store.mark_superseded(["nope"]) == {"marked": 0}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mark_superseded_writes_only_non_none_fields():
|
||||||
|
# PRE/POST: superseded_by=None -> only the `superseded` flag written, no superseded_by key.
|
||||||
|
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
|
||||||
|
await store.upsert_many([_chunk("c1", scope={"end_user": "u1"})], idempotency_key="k", ctx=_ctx())
|
||||||
|
await store.mark_superseded(["c1"], superseded_by=None)
|
||||||
|
got = await store.get("c1")
|
||||||
|
assert got["superseded"] is True
|
||||||
|
assert "superseded_by" not in got
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mark_superseded_parity_vs_reference():
|
||||||
|
# #195: identical mark_superseded envelope vs InMemoryMemoryStore -> same top-level field shape.
|
||||||
|
from bifrost.consumer.testing import InMemoryMemoryStore
|
||||||
|
|
||||||
|
rec = _chunk("c1", scope={"end_user": "u1"})
|
||||||
|
ours = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
|
||||||
|
await ours.upsert_many([rec], idempotency_key="k", ctx=_ctx())
|
||||||
|
ref = InMemoryMemoryStore()
|
||||||
|
await ref.upsert_many([rec], idempotency_key="k", ctx=_ctx())
|
||||||
|
assert await ours.mark_superseded(["c1"], superseded_by="x", reason="r") == {"marked": 1}
|
||||||
|
assert await ref.mark_superseded(["c1"], superseded_by="x", reason="r") == {"marked": 1}
|
||||||
|
og, rg = await ours.get("c1"), await ref.get("c1")
|
||||||
|
for k in ("superseded", "superseded_by", "superseded_reason"):
|
||||||
|
assert og.get(k) == rg.get(k)
|
||||||
|
|
||||||
|
|
||||||
# --- build_memory_provider_app ---
|
# --- build_memory_provider_app ---
|
||||||
|
|
||||||
def test_build_app_exposes_handshake_and_memory_routes():
|
def test_build_app_exposes_handshake_and_memory_routes():
|
||||||
|
|||||||
@@ -190,14 +190,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bifrost"
|
name = "bifrost"
|
||||||
version = "1.1.1"
|
version = "1.1.4"
|
||||||
source = { registry = "https://gitea.phasefinal.com/api/packages/vh/pypi/simple/" }
|
source = { registry = "https://gitea.phasefinal.com/api/packages/vh/pypi/simple/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "jsonschema" },
|
{ name = "jsonschema" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/bifrost/1.1.1/bifrost-1.1.1.tar.gz", hash = "sha256:0934c5fdf14823766346e591f5a16ab57a137cf06df794152318b5ccef0fb8e8" }
|
sdist = { url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/bifrost/1.1.4/bifrost-1.1.4.tar.gz", hash = "sha256:498d156035a93bf37a6fc1e9c09b468aac61e869fd2a5353e2695dc823f57e9a" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/bifrost/1.1.1/bifrost-1.1.1-py3-none-any.whl", hash = "sha256:dab551f8ad26464168f17108cb19564da56ee8e4789264a401e7a14463ab1576" },
|
{ url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/bifrost/1.1.4/bifrost-1.1.4-py3-none-any.whl", hash = "sha256:d67278528f12729eef0c19d875d36a2f1da6fa97737396ba130d525fee8d0b14" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1052,7 +1052,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ratatoskr"
|
name = "ratatoskr"
|
||||||
version = "0.20.12"
|
version = "0.20.15"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
@@ -1086,7 +1086,7 @@ web = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "bifrost", marker = "extra == 'provider'", specifier = "==1.1.1", index = "https://gitea.phasefinal.com/api/packages/vh/pypi/simple/" },
|
{ name = "bifrost", marker = "extra == 'provider'", specifier = "==1.1.4", index = "https://gitea.phasefinal.com/api/packages/vh/pypi/simple/" },
|
||||||
{ name = "httpx", specifier = ">=0.27" },
|
{ name = "httpx", specifier = ">=0.27" },
|
||||||
{ name = "httpx-sse", specifier = ">=0.4" },
|
{ name = "httpx-sse", specifier = ">=0.4" },
|
||||||
{ name = "jsonschema", marker = "extra == 'provider'", specifier = ">=4" },
|
{ name = "jsonschema", marker = "extra == 'provider'", specifier = ">=4" },
|
||||||
|
|||||||
Reference in New Issue
Block a user