- Plexer: always-active terminal multiplexing. NativePlexerController/ TmuxPlexerController share one interface, chosen by which env var is already set (TAI_PLEXER_SOCKET vs TMUX_PANE); NativePlexerBackend is the pty.fork()-based wrapper (raw mode, SIGWINCH resize forwarding, a Unix-socket control protocol, continuous session logging, a Ctrl+A prefix key distinct from tmux's own Ctrl+B). Auto-wraps new interactive shells via a .bashrc hook mirroring the existing command-not-found install pattern, gated on $TMUX/$TAI_PLEXER_ACTIVE/ a $HOME/.tai-no-plexer bypass. New plexer_send/read/type/status tools, env-gated on a detected controller. - tests/container.py + docker-compose.yml + tests/docker/Dockerfile: a disposable podman/docker image for the tests that must never touch a real machine (installing the plexer hook into a real ~/.bashrc, a live tmux session). Required suite (test_seal.py, test_tai.py, tests/plexer.py, tests/bashrc.py) gates the result; tests/rag.py runs as best-effort since its real-LLM dependency can't be satisfied inside a disposable container with no nested engine. - tests/bashrc.py: real end-to-end hook install + bash -i auto-wrap, anti-recursion, tmux-active skip, non-interactive skip, a live TmuxPlexerController test, and an install/uninstall round-trip. - tests/plexer.py: NativePlexerBackend under a real pty, plus a dedicated robustness pass (file permissions, >64KB payload framing, a malformed control request, a genuine self-deadlock fix in the write-to-child path). - tai_test.py: the shared async process-driving framework (pipe/pty transport, group-wide kill-and-reap, a StdinBridge for --interactive) both test files build on. - ChatClient last-resort backend: when there's no local opencode CLI but the sandbox box is already running, install opencode into it (official Node tarball, not apt -- apt's nodejs/npm pull in webpack/babel/jest as transitive deps) and route through it instead of giving up. Only reachable after the first retry round already failed everything else; never creates the box itself. TmuxPlexerController.status() fixed to use `list-panes` instead of `display-message -p`, which silently fell back to the default pane instead of erroring against a dead target. - Fix 4 pre-existing test_tai.py bugs surfaced by running it in a clean environment for the first time: two stale mock signatures (subprocess.run's stdout= kwarg, Chat.complete's deadline= kwarg), one $TMUX-dependent catalog assertion, one tty-dependent color assertion. - research/: a sourced research archive (Source Dossier/Citation Card/Conclusion tiers) backing the above design decisions, with its own audit.py consistency checker. - .gitignore: fix a blanket *.md rule that was about to silently drop the entire research/ archive from version control; add __pycache__/ and the one-off debug transcript this session started from. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7.3 KiB
7.3 KiB
Conclusion: Last-Used / Use-Count Tracking Proposal for the tai Vault
Metadata
| Field | Value |
|---|---|
| Institute | Molodetz Institute — Research & Retrieval Division |
| Subject | agent-memory-recency-tracking |
| Document type | Conclusion |
| Tier | N/A |
| Date decided | 2026-10-09 |
| Decided by | retoor, advised by tai (agent, Claude Sonnet 5) |
| Status | Active (proposal recorded, not yet implemented) |
| Confidence | Medium |
Question / problem statement
retoor asked for a way to track when a vault record (and, by extension, a chunk) was last used, or was recently part of a search result, with an eye toward a future LRU-style prioritization or faster-access mechanism. The explicit instruction was to research this thoroughly first — specifically looking at how agent-memory systems already do this, and at systems/database techniques for doing it cheaply — and to produce a concrete proposal, with building anything explicitly forbidden for this task.
Conclusion
The recommended design (not yet built) is:
- Add a
last_used INTEGERcolumn (Unix epoch seconds) to both therecordsandchunkstables. This is a deliberate exception to the vault's otherwise-universal ISO-8601 TEXT timestamp convention, justified specifically because this column is compared, sorted, and range-scanned on a hot path in a way the vault's other timestamp columns are not. - Add a
use_count INTEGERcolumn tochunks.recordsalready has an existingreadscounter that serves the same purpose, so no duplicate column is needed there. - "Used" is triggered by two kinds of event: (a) an explicit
record_read/get_recordcall, and (b) a record or chunk appearing insearchresults — a chunk hit should also propagate an update to its parent record'slast_used, since the parent is "the record" in retoor's own framing of the request. - Writes must be throttled/conditional, never unconditional on every hit:
UPDATE ... SET last_used = ? WHERE last_used IS NULL OR last_used < ?with a coalescing window (a starting estimate of roughly one minute), modeled directly on Linux'srelatimemount option and the real, documented GitLablast_downloaded_atproduction incident. - No eviction policy and no search-ranking integration is being built in this phase. This is tracking-substrate only — recording the raw signal so a future, separate decision can use it, matching how both the Generative Agents paper and Mem0's product keep recency as an independent signal rather than baking a final blended formula in at write time.
Supporting sources
arxiv-2304-03442-generative-agents.md(Tier 1) — the foundational precedent for keeping recency, importance, and relevance as three independent, un-collapsed signals combined only at retrieval time.arxiv-2310-08560-memgpt.md,letta-agent-memory-blog.md(Tier 1) — establish that even the most prominent "OS-for-LLMs" memory architecture uses FIFO eviction, not LRU, and keeps ranking (recency-aware) separate from eviction/compaction — informing the decision to defer eviction entirely rather than assume a policy.mem0-introducing-memory-decay.md,mem0-memory-eviction-forgetting.md,arxiv-2504-19413-mem0-paper.md(Tier 1) — the closest existing commercial precedent: recency as a soft search-time re-ranking multiplier, not a deletion trigger; and an explicit distinction between decay (ranking), TTL (lifecycle), and deletion APIs (user-visible forgetting) that this proposal deliberately mirrors by staying purely in the "raw signal" category.arpit-2q-cache-algorithm.md,arxiv-2511-21958-clock2q-plus.md,arxiv-2512-22995-buffer-management-survey.md(Tier 1) — background precedent for low-write-overhead recency tracking (2Q, CLOCK), recorded for a possible future eviction-policy phase, not used directly in the current tracking-only proposal.redhat-relatime-drive-access.md,gitlab-last-downloaded-at-write-amplification.md(Tier 1) — the direct justification for the throttled/conditional-UPDATE write pattern:relatimeis the exact same tradeoff already running on effectively every Linux filesystem read, and the GitLab incident is concrete, quantified, real-world proof that the naive unconditional-update approach causes genuine production problems.sqlite-forum-timestamp-integer-vs-text.md,sqlite-org-bm25-sign-flip-commit.md(Tier 1) — justify the Unix-epoch-INTEGER column choice and flag the BM25 ranking-direction gotcha for whoever eventually builds the ranking-integration phase.glama-code-index-mcp-bm25-hybrid.md(Tier 1) — a concrete, reusable BM25-normalization-and-fusion reference for that same future phase.
Alternatives considered and rejected
- Unconditional
UPDATEon every access. Rejected outright, per the GitLab incident's own documented production consequences (row-version and index-entry multiplication on a hot read path) — this is the one alternative with direct, quantified evidence against it, not just theoretical concern. - In-memory batched writes flushed periodically (the
lazytimeapproach). Not rejected — deferred as a second-stage optimization, to be built only if the simple throttled-UPDATE approach proves insufficient under real measured load. Recorded explicitly as "not rejected, just not needed yet," not as a worse option. - A single blended LRFU-style score computed and stored at write time, instead of separate
last_used/use_countcolumns. Rejected for this phase because collapsing recency and frequency into one opaque number at write time would destroy the ability to use them as independent inputs to whatever ranking or eviction decision gets made later — a decision that has not been made yet. This mirrors the Generative Agents / Mem0 pattern of keeping signals separate until the moment they're actually used. - ISO-8601 TEXT for
last_used, matching every other timestamp column in the vault. Considered, since consistency has its own value and was retoor's own suggested fallback ("if it doesn't matter just use the same date format as the other columns"). Rejected specifically for this one column because it is the one column in the whole schema expected to be compared/range-scanned on a genuine hot path, where the SQLite-forum-documented integer advantages (no text parsing, smaller index entries, no timezone ambiguity) are the kind of thing that could plausibly matter — this is the one deliberate inconsistency in the schema, and it is being flagged as such rather than introduced silently.
Risks / open questions
- The ~one-minute throttle window is an engineering estimate, not a measured value against tai's actual read/search volume — it has not been benchmarked.
- Whether chunk-level
use_countshould itself decay over time (as in Mem0's or YourMemory's Ebbinghaus-curve approaches) is an open question explicitly deferred to a future decision, not resolved here. - Whether a chunk hit should always propagate to its parent record's
last_used, or only under some condition, is a design detail that was decided one way here (always propagate) but not independently stress-tested against real usage patterns.
Action taken
None yet. retoor's instructions for this research task explicitly forbade building anything — this document records the proposal and its supporting research only. Implementation remains a future decision.
Changelog
| Date | Change |
|---|---|
| 2026-10-09 | Initial conclusion recorded. |