From 9dbd1fe667b6810f0a9b0f52f7e495c6f141ca32 Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 10 Oct 2026 04:36:05 +0200 Subject: [PATCH] Add plexer subsystem, container test infra, and last-resort opencode box backend - 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 --- .claude/hooks/research-reminder.sh | 9 + .claude/settings.json | 16 + .gitignore | 10 + AGENTS.md | 18 +- README.md | 11 +- docker-compose.yml | 17 + research/INDEX.md | 41 + research/README.md | 205 ++++ ...sprogrammers-datetime-index-performance.md | 28 + .../arpit-2q-cache-algorithm.md | 52 + .../arxiv-2304-03442-generative-agents.md | 53 ++ .../arxiv-2310-08560-memgpt.md | 51 + .../arxiv-2504-19413-mem0-paper.md | 46 + .../arxiv-2506-23306-gatsim.md | 28 + ...iv-2510-19497-toulouse-transport-agents.md | 28 + .../arxiv-2511-21958-clock2q-plus.md | 50 + ...xiv-2512-22995-buffer-management-survey.md | 49 + ...rxiv-2601-14192-efficient-agents-survey.md | 28 + .../arxiv-2601-16986-crystal-kv.md | 28 + .../arxiv-2605-29630-entity-collision.md | 28 + ...xiv-2606-12945-multi-factor-value-model.md | 28 + .../arxiv-2607-16848-memory-leaderboards.md | 28 + ...v-2607-21503-agentic-context-management.md | 28 + .../arxiv-2608-00009-agentmembench.md | 28 + .../arxiv-2608-31057-agent-working-memory.md | 28 + ...atlantis-press-recency-frequency-policy.md | 28 + .../blackglory-sqlite-fts5-bm25-note.md | 28 + .../chiark-sqlite-datefunc-docs.md | 28 + .../coddy-sqlite-date-and-time.md | 28 + .../codegarrettmills-caching-archive.md | 28 + .../last-used-tracking-proposal.md | 61 ++ .../deeplearningai-llms-as-os-course.md | 28 + ...o-sudarshangouda-intelligent-forgetting.md | 28 + .../docsrs-kindling-provider.md | 28 + .../emergentmind-generative-agents.md | 28 + .../emergentmind-memgpt.md | 28 + ...rontiersin-generative-agents-psychology.md | 28 + .../github-yourmemory-ebbinghaus.md | 48 + ...-last-downloaded-at-write-amplification.md | 51 + ...glama-claude-memory-mcp-semantic-search.md | 28 + .../glama-code-index-mcp-bm25-hybrid.md | 48 + .../glama-gnosis-mcp-sqlite-backend.md | 28 + .../glama-mcp-memory-service-hybrid-test.md | 28 + .../hipc-mflru-poster.md | 28 + .../letta-agent-memory-blog.md | 51 + .../linguanguo-letta-research-notes.md | 28 + .../lwn-atime-kernel-article.md | 28 + .../medium-artgor-generative-agents-review.md | 48 + .../medium-piyush-letta-memgpt-deepdive.md | 28 + .../mem0-ai-memory-management-4-layers.md | 28 + .../mem0-context-engineering-memory-decay.md | 28 + .../mem0-how-to-create-long-term-memory.md | 28 + .../mem0-introducing-memory-decay.md | 51 + .../mem0-library-guides.md | 28 + .../mem0-memory-eviction-forgetting.md | 50 + .../mem0-memory-in-agents-guide.md | 28 + .../paperswelove-2q-paper.md | 28 + .../redhat-relatime-drive-access.md | 51 + .../rmarcus-dbscholar-clock2q.md | 28 + .../sitepoint-integer-created-at-column.md | 28 + .../sqlite-forum-second-thread-datetime.md | 28 + .../sqlite-forum-timestamp-integer-vs-text.md | 50 + .../sqlite-org-bm25-sign-flip-commit.md | 47 + .../tarantool-vinyl-throttle-patches.md | 28 + .../usenix-zhou-traces.md | 28 + .../uspatent-write-amplification-family.md | 28 + .../uspto-patent-6996676-arc-cache.md | 28 + .../uspto-patent-7167953-adaptive-paging.md | 28 + .../vectorize-mem0-vs-letta.md | 28 + .../vldb-dblp-2q-listing.md | 28 + .../webmasterworld-datetime-index-thread.md | 28 + .../xojo-forum-datetime-to-sqlite.md | 28 + .../agentskills-letta-cancel-async-tasks.md | 28 + ...aharia-kill-subprocess-children-timeout.md | 48 + .../all-hands-integration-tests-base.md | 28 + .../baeldung-redirect-process-output.md | 28 + ...o-wait-for-timeout-leaves-process-alive.md | 48 + .../chromiumos-chromite-tee-py.md | 28 + .../conclusions/tai-test-framework-design.md | 53 ++ .../cpython-asyncio-subprocess-mirrors.md | 28 + ...cs-unb-ca-asyncio-subprocess-doc-mirror.md | 28 + .../debian-pyte-csail-mirror.md | 28 + .../discourse-merge-two-streams.md | 48 + .../discourse-subprocess-process-gc.md | 47 + ...patch-blog-subprocesses-outlive-program.md | 28 + ...n-org-locale-mirrors-asyncio-subprocess.md | 28 + ...cumentation-help-361-asyncio-subprocess.md | 28 + .../execa-termination-docs.md | 47 + .../gaudi-doxygen-allclasses-mirror.md | 28 + .../gaudi-subprocess-base-test.md | 49 + .../gem5-test-infrastructure-commit.md | 50 + .../gitea-bunkerweb-test-py.md | 28 + ...13-opendev-distcloud-subprocess-cleanup.md | 28 + ...thub-agents-issue1-controlling-terminal.md | 49 + .../github-gist-nonblocking-stdin-os-read.md | 50 + .../github-h0lley-nonblockingstdin.md | 28 + .../glama-mcp-remote-py-stdio-jsonl.md | 49 + .../hermesedi-test-local-interrupt-cleanup.md | 28 + .../huggingface-openbb-test-session.md | 28 + ...g-code-assistant-asyncio-subprocess-doc.md | 28 + ...iannuccisolutions-timeout-process-group.md | 47 + .../ipython-utils-io-mirror.md | 28 + ...kevinmccarthy-streaming-subprocess-blog.md | 28 + .../kiso-testing-pykiso-test-case.md | 28 + ...tackoverflow-run-external-command-async.md | 28 + .../medium-denismakogon-nonblocking-pipe.md | 28 + .../micropython-pr-4859.md | 28 + .../oslotest-timeout-fixture.md | 47 + .../pelicandd-nonblocking-stdin.md | 28 + .../ptyprocess-docs.md | 50 + .../py-toolbox-io-module-docs.md | 28 + .../pypi-asyncio-pipe.md | 28 + .../async-pty-process-testing/pypi-ckill.md | 28 + .../pypi-pexpect-version-mirrors.md | 28 + .../async-pty-process-testing/pypi-pexpect.md | 50 + .../pypi-shellous-0.4.0.md | 28 + .../pypi-shellous.md | 50 + .../pypi-stitch-pty.md | 48 + .../pyte-terminal-emulator.md | 48 + .../python-asyncio-issue499-add-reader-eof.md | 48 + .../python-docs-asyncio-subprocess.md | 48 + .../python-tulip-google-groups-stdin.md | 28 + ...ython373-test-asyncio-subprocess-source.md | 28 + .../python373-test-pty-source.md | 28 + .../pythonlib-ru-pexpect-article.md | 28 + .../pythonsheets-asyncio-advanced.md | 28 + .../runxbuild-wait-communicate-poll-blog.md | 28 + .../rust-users-forum-stdin-loop.md | 28 + ...simonwillison-til-subprocess-time-limit.md | 28 + .../slingacademy-stop-kill-child-process.md | 46 + .../tessl-io-sh-docs.md | 28 + .../tutorialspoint-redirect-process-output.md | 28 + research/audit.py | 135 +++ .../1devtool-terminal-notifier-ai-agents.md | 28 + .../acchapm1-tmux-claude-code-deepdive.md | 28 + .../android-adb-cfmakeraw-commit.md | 47 + .../arcolinux-tmux-servers-sessions.md | 28 + ...ash-manual-interactive-shell-definition.md | 47 + .../bbs-archlinux-second-thread.md | 28 + .../bbs-archlinux-tmux-env-guard.md | 47 + .../biriukov-dev-terminals-pseudoterminals.md | 28 + .../codegenes-net-ssh-tty-tmux-guard.md | 47 + .../coderwall-ssh-connection-check.md | 28 + .../multiplexer-proposal-design-basis.md | 55 ++ .../coral-mdt-console-sigwinch-commit.md | 46 + .../cyberciti-nixcraft-dollar-dash-test.md | 47 + .../dasroot-net-tmux-vs-zellij.md | 47 + .../deepwiki-tmux-tmux-wiki-root.md | 28 + ...ection-rule-interactive-terminal-python.md | 47 + ...r0stb1rd-tmux-login-autorestart-logging.md | 47 + ...it-alternativebit-ptyfwd-source-listing.md | 28 + .../github-bitcanon-autotyper.md | 47 + ...ity-discussion-188494-ai-agent-terminal.md | 28 + ...-11425-pty-interactive-prompt-detection.md | 47 + ...-tmux-discussion-3567-no-raw-socket-api.md | 47 + ...mux-discussion-4016-control-mode-output.md | 47 + .../github-tmux-wiki-control-mode.md | 47 + ...thub-vscode-community-discussion-161238.md | 47 + .../github-vscode-issue-253265.md | 47 + ...-vscode-issue-313160-respect-user-shell.md | 28 + ...ab-mr-3383-stdin-stdout-isatty-mismatch.md | 47 + .../glama-terminally-mcp-tmuxmanager.md | 28 + .../huggingface-bash-bashrc-mirrors.md | 28 + .../inskillclaude-tmux-custom-prompt-guide.md | 28 + .../k6-browser-presssequentially.md | 46 + .../k6-v2-3-presssequentially-mirror.md | 28 + .../keyboardmaestro-forum-random-delay.md | 28 + .../kx-cloudingenium-zellij-guide.md | 28 + .../libtmux-pane-interaction.md | 48 + .../libtmux-python-github-commit.md | 28 + ...-2003-sigwinch-foreground-process-group.md | 47 + .../lkml-mailing-list-tty-s-insufficient.md | 47 + .../lkml-sigwinch-thread-replies.md | 28 + .../mager-co-orchestrating-agents-tmux.md | 28 + .../man7-tmux-manual.md | 49 + .../microsoft-terminal-issue-5612.md | 28 + .../mintlify-wiki-control-mode-mirror.md | 28 + .../nncron-send-keys-delay-docs.md | 46 + .../oneuptime-tmux-session-recovery.md | 46 + .../openskillindex-autoskill-typing.md | 28 + .../oreilly-bash-cookbook-interactive-test.md | 47 + .../packagist-piuu-typing-simulation.md | 28 + ...oramicdata-detecting-ai-agent-terminals.md | 49 + .../petronellatech-zellij-guide.md | 28 + .../pypi-tmux-control.md | 28 + ...ntutorials-net-ps1-child-process-caveat.md | 47 + .../raycast-fake-typing-effect.md | 46 + .../rdrr-io-tmuxr-send-keys.md | 28 + .../santini-tmux-safety-file.md | 47 + .../skills-rest-tmux-skill.md | 28 + .../stalkr-remote-tty-resize-design.md | 47 + .../stonecharioteer-zellij-til.md | 46 + .../systemd-ptyfwd-resize-forwarding.md | 46 + .../tabby-typewriter-mode-docs.md | 47 + .../tao-of-tmux-server-internals.md | 48 + .../tek-tips-interactive-shell-forum.md | 28 + .../termdock-terminal-multiplexing-mirrors.md | 28 + .../theregister-tiling-multiplexer-survey.md | 46 + .../tmux-man-page-mirrors.md | 28 + .../tmuxai-dev-control-mode.md | 48 + .../tmuxai-dev-faq.md | 48 + .../toolradar-zellij-vs-tmux.md | 28 + .../unix-stackexchange-ssh-tmux-nesting.md | 47 + .../velog-tmux-vs-screen-vs-zellij.md | 28 + .../yarnpkg-gpt-type-effect.md | 28 + .../alexgarcia-sqlite-vec-hybrid-search.md | 51 + ...04-05324-hybrid-retrieval-hallucination.md | 28 + .../arxiv-2601-05264-engineering-rag-stack.md | 28 + .../arxiv-2602-03368-rag-medical-domain.md | 28 + .../bigdataboutique-rrf.md | 50 + .../byteplus-chunking-topic.md | 28 + .../cloudthat-chunking-strategies-guide.md | 51 + .../cohere-chunking-strategies.md | 28 + .../no-vectors-bm25-chunked-rag.md | 48 + .../databricks-ultimate-guide-chunking.md | 28 + .../devto-ayush-kumar-practical-chunking.md | 28 + .../devto-ditched-vector-db-fts5.md | 50 + .../devto-rasinmuhammed-chunking-tui.md | 28 + .../f22labs-7-chunking-strategies.md | 50 + .../glama-daemonmcp-fusion.md | 28 + .../glama-orneryd-mimir-rrf.md | 28 + .../glukhov-chunking-strategies-rag.md | 28 + .../ingoampt-bm25-fts5-google.md | 28 + .../medium-privexbot-chunking-strategies.md | 28 + .../memx-rrf-glossary.md | 28 + .../novel-rag-github.md | 51 + .../sqlite-rag-hybrid-search/paradedb-rrf.md | 47 + .../popoto-hybrid-retrieval-rrf.md | 28 + .../quizzly-chunking-heuristics.md | 28 + .../redis-reciprocal-rank-fusion.md | 28 + ...tware-engineer-blog-fts5-inverted-index.md | 28 + .../spiceai-reciprocal-rank-fusion.md | 50 + .../sqlite-search-github.md | 50 + .../stackai-chunking-strategies.md | 28 + .../talentica-sqlite-vector-db.md | 48 + .../towardsai-21-chunking-strategies.md | 28 + .../towardsai-stop-blaming-llm-chunking.md | 28 + .../unimon-rrf-glossary.md | 28 + tai.py | 885 +++++++++++++++++- tai_test.py | 527 +++++++++++ test_tai.py | 15 +- tests/__init__.py | 0 tests/bashrc.py | 389 ++++++++ tests/container.py | 136 +++ tests/docker/Dockerfile | 30 + tests/plexer.py | 289 ++++++ tests/rag.py | 268 ++++++ 247 files changed, 11187 insertions(+), 26 deletions(-) create mode 100755 .claude/hooks/research-reminder.sh create mode 100644 .claude/settings.json create mode 100644 docker-compose.yml create mode 100644 research/INDEX.md create mode 100644 research/README.md create mode 100644 research/agent-memory-recency-tracking/accessprogrammers-datetime-index-performance.md create mode 100644 research/agent-memory-recency-tracking/arpit-2q-cache-algorithm.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2304-03442-generative-agents.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2310-08560-memgpt.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2504-19413-mem0-paper.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2506-23306-gatsim.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2510-19497-toulouse-transport-agents.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2511-21958-clock2q-plus.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2512-22995-buffer-management-survey.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2601-14192-efficient-agents-survey.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2601-16986-crystal-kv.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2605-29630-entity-collision.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2606-12945-multi-factor-value-model.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2607-16848-memory-leaderboards.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2607-21503-agentic-context-management.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2608-00009-agentmembench.md create mode 100644 research/agent-memory-recency-tracking/arxiv-2608-31057-agent-working-memory.md create mode 100644 research/agent-memory-recency-tracking/atlantis-press-recency-frequency-policy.md create mode 100644 research/agent-memory-recency-tracking/blackglory-sqlite-fts5-bm25-note.md create mode 100644 research/agent-memory-recency-tracking/chiark-sqlite-datefunc-docs.md create mode 100644 research/agent-memory-recency-tracking/coddy-sqlite-date-and-time.md create mode 100644 research/agent-memory-recency-tracking/codegarrettmills-caching-archive.md create mode 100644 research/agent-memory-recency-tracking/conclusions/last-used-tracking-proposal.md create mode 100644 research/agent-memory-recency-tracking/deeplearningai-llms-as-os-course.md create mode 100644 research/agent-memory-recency-tracking/devto-sudarshangouda-intelligent-forgetting.md create mode 100644 research/agent-memory-recency-tracking/docsrs-kindling-provider.md create mode 100644 research/agent-memory-recency-tracking/emergentmind-generative-agents.md create mode 100644 research/agent-memory-recency-tracking/emergentmind-memgpt.md create mode 100644 research/agent-memory-recency-tracking/frontiersin-generative-agents-psychology.md create mode 100644 research/agent-memory-recency-tracking/github-yourmemory-ebbinghaus.md create mode 100644 research/agent-memory-recency-tracking/gitlab-last-downloaded-at-write-amplification.md create mode 100644 research/agent-memory-recency-tracking/glama-claude-memory-mcp-semantic-search.md create mode 100644 research/agent-memory-recency-tracking/glama-code-index-mcp-bm25-hybrid.md create mode 100644 research/agent-memory-recency-tracking/glama-gnosis-mcp-sqlite-backend.md create mode 100644 research/agent-memory-recency-tracking/glama-mcp-memory-service-hybrid-test.md create mode 100644 research/agent-memory-recency-tracking/hipc-mflru-poster.md create mode 100644 research/agent-memory-recency-tracking/letta-agent-memory-blog.md create mode 100644 research/agent-memory-recency-tracking/linguanguo-letta-research-notes.md create mode 100644 research/agent-memory-recency-tracking/lwn-atime-kernel-article.md create mode 100644 research/agent-memory-recency-tracking/medium-artgor-generative-agents-review.md create mode 100644 research/agent-memory-recency-tracking/medium-piyush-letta-memgpt-deepdive.md create mode 100644 research/agent-memory-recency-tracking/mem0-ai-memory-management-4-layers.md create mode 100644 research/agent-memory-recency-tracking/mem0-context-engineering-memory-decay.md create mode 100644 research/agent-memory-recency-tracking/mem0-how-to-create-long-term-memory.md create mode 100644 research/agent-memory-recency-tracking/mem0-introducing-memory-decay.md create mode 100644 research/agent-memory-recency-tracking/mem0-library-guides.md create mode 100644 research/agent-memory-recency-tracking/mem0-memory-eviction-forgetting.md create mode 100644 research/agent-memory-recency-tracking/mem0-memory-in-agents-guide.md create mode 100644 research/agent-memory-recency-tracking/paperswelove-2q-paper.md create mode 100644 research/agent-memory-recency-tracking/redhat-relatime-drive-access.md create mode 100644 research/agent-memory-recency-tracking/rmarcus-dbscholar-clock2q.md create mode 100644 research/agent-memory-recency-tracking/sitepoint-integer-created-at-column.md create mode 100644 research/agent-memory-recency-tracking/sqlite-forum-second-thread-datetime.md create mode 100644 research/agent-memory-recency-tracking/sqlite-forum-timestamp-integer-vs-text.md create mode 100644 research/agent-memory-recency-tracking/sqlite-org-bm25-sign-flip-commit.md create mode 100644 research/agent-memory-recency-tracking/tarantool-vinyl-throttle-patches.md create mode 100644 research/agent-memory-recency-tracking/usenix-zhou-traces.md create mode 100644 research/agent-memory-recency-tracking/uspatent-write-amplification-family.md create mode 100644 research/agent-memory-recency-tracking/uspto-patent-6996676-arc-cache.md create mode 100644 research/agent-memory-recency-tracking/uspto-patent-7167953-adaptive-paging.md create mode 100644 research/agent-memory-recency-tracking/vectorize-mem0-vs-letta.md create mode 100644 research/agent-memory-recency-tracking/vldb-dblp-2q-listing.md create mode 100644 research/agent-memory-recency-tracking/webmasterworld-datetime-index-thread.md create mode 100644 research/agent-memory-recency-tracking/xojo-forum-datetime-to-sqlite.md create mode 100644 research/async-pty-process-testing/agentskills-letta-cancel-async-tasks.md create mode 100644 research/async-pty-process-testing/alexandra-zaharia-kill-subprocess-children-timeout.md create mode 100644 research/async-pty-process-testing/all-hands-integration-tests-base.md create mode 100644 research/async-pty-process-testing/baeldung-redirect-process-output.md create mode 100644 research/async-pty-process-testing/bpo-wait-for-timeout-leaves-process-alive.md create mode 100644 research/async-pty-process-testing/chromiumos-chromite-tee-py.md create mode 100644 research/async-pty-process-testing/conclusions/tai-test-framework-design.md create mode 100644 research/async-pty-process-testing/cpython-asyncio-subprocess-mirrors.md create mode 100644 research/async-pty-process-testing/cs-unb-ca-asyncio-subprocess-doc-mirror.md create mode 100644 research/async-pty-process-testing/debian-pyte-csail-mirror.md create mode 100644 research/async-pty-process-testing/discourse-merge-two-streams.md create mode 100644 research/async-pty-process-testing/discourse-subprocess-process-gc.md create mode 100644 research/async-pty-process-testing/dispatch-blog-subprocesses-outlive-program.md create mode 100644 research/async-pty-process-testing/docs-python-org-locale-mirrors-asyncio-subprocess.md create mode 100644 research/async-pty-process-testing/documentation-help-361-asyncio-subprocess.md create mode 100644 research/async-pty-process-testing/execa-termination-docs.md create mode 100644 research/async-pty-process-testing/gaudi-doxygen-allclasses-mirror.md create mode 100644 research/async-pty-process-testing/gaudi-subprocess-base-test.md create mode 100644 research/async-pty-process-testing/gem5-test-infrastructure-commit.md create mode 100644 research/async-pty-process-testing/gitea-bunkerweb-test-py.md create mode 100644 research/async-pty-process-testing/gitea13-opendev-distcloud-subprocess-cleanup.md create mode 100644 research/async-pty-process-testing/github-agents-issue1-controlling-terminal.md create mode 100644 research/async-pty-process-testing/github-gist-nonblocking-stdin-os-read.md create mode 100644 research/async-pty-process-testing/github-h0lley-nonblockingstdin.md create mode 100644 research/async-pty-process-testing/glama-mcp-remote-py-stdio-jsonl.md create mode 100644 research/async-pty-process-testing/hermesedi-test-local-interrupt-cleanup.md create mode 100644 research/async-pty-process-testing/huggingface-openbb-test-session.md create mode 100644 research/async-pty-process-testing/huggingface-rag-code-assistant-asyncio-subprocess-doc.md create mode 100644 research/async-pty-process-testing/iannuccisolutions-timeout-process-group.md create mode 100644 research/async-pty-process-testing/ipython-utils-io-mirror.md create mode 100644 research/async-pty-process-testing/kevinmccarthy-streaming-subprocess-blog.md create mode 100644 research/async-pty-process-testing/kiso-testing-pykiso-test-case.md create mode 100644 research/async-pty-process-testing/kiwix-stackoverflow-run-external-command-async.md create mode 100644 research/async-pty-process-testing/medium-denismakogon-nonblocking-pipe.md create mode 100644 research/async-pty-process-testing/micropython-pr-4859.md create mode 100644 research/async-pty-process-testing/oslotest-timeout-fixture.md create mode 100644 research/async-pty-process-testing/pelicandd-nonblocking-stdin.md create mode 100644 research/async-pty-process-testing/ptyprocess-docs.md create mode 100644 research/async-pty-process-testing/py-toolbox-io-module-docs.md create mode 100644 research/async-pty-process-testing/pypi-asyncio-pipe.md create mode 100644 research/async-pty-process-testing/pypi-ckill.md create mode 100644 research/async-pty-process-testing/pypi-pexpect-version-mirrors.md create mode 100644 research/async-pty-process-testing/pypi-pexpect.md create mode 100644 research/async-pty-process-testing/pypi-shellous-0.4.0.md create mode 100644 research/async-pty-process-testing/pypi-shellous.md create mode 100644 research/async-pty-process-testing/pypi-stitch-pty.md create mode 100644 research/async-pty-process-testing/pyte-terminal-emulator.md create mode 100644 research/async-pty-process-testing/python-asyncio-issue499-add-reader-eof.md create mode 100644 research/async-pty-process-testing/python-docs-asyncio-subprocess.md create mode 100644 research/async-pty-process-testing/python-tulip-google-groups-stdin.md create mode 100644 research/async-pty-process-testing/python373-test-asyncio-subprocess-source.md create mode 100644 research/async-pty-process-testing/python373-test-pty-source.md create mode 100644 research/async-pty-process-testing/pythonlib-ru-pexpect-article.md create mode 100644 research/async-pty-process-testing/pythonsheets-asyncio-advanced.md create mode 100644 research/async-pty-process-testing/runxbuild-wait-communicate-poll-blog.md create mode 100644 research/async-pty-process-testing/rust-users-forum-stdin-loop.md create mode 100644 research/async-pty-process-testing/simonwillison-til-subprocess-time-limit.md create mode 100644 research/async-pty-process-testing/slingacademy-stop-kill-child-process.md create mode 100644 research/async-pty-process-testing/tessl-io-sh-docs.md create mode 100644 research/async-pty-process-testing/tutorialspoint-redirect-process-output.md create mode 100755 research/audit.py create mode 100644 research/shell-multiplexer-auto-attach/1devtool-terminal-notifier-ai-agents.md create mode 100644 research/shell-multiplexer-auto-attach/acchapm1-tmux-claude-code-deepdive.md create mode 100644 research/shell-multiplexer-auto-attach/android-adb-cfmakeraw-commit.md create mode 100644 research/shell-multiplexer-auto-attach/arcolinux-tmux-servers-sessions.md create mode 100644 research/shell-multiplexer-auto-attach/bash-manual-interactive-shell-definition.md create mode 100644 research/shell-multiplexer-auto-attach/bbs-archlinux-second-thread.md create mode 100644 research/shell-multiplexer-auto-attach/bbs-archlinux-tmux-env-guard.md create mode 100644 research/shell-multiplexer-auto-attach/biriukov-dev-terminals-pseudoterminals.md create mode 100644 research/shell-multiplexer-auto-attach/codegenes-net-ssh-tty-tmux-guard.md create mode 100644 research/shell-multiplexer-auto-attach/coderwall-ssh-connection-check.md create mode 100644 research/shell-multiplexer-auto-attach/conclusions/multiplexer-proposal-design-basis.md create mode 100644 research/shell-multiplexer-auto-attach/coral-mdt-console-sigwinch-commit.md create mode 100644 research/shell-multiplexer-auto-attach/cyberciti-nixcraft-dollar-dash-test.md create mode 100644 research/shell-multiplexer-auto-attach/dasroot-net-tmux-vs-zellij.md create mode 100644 research/shell-multiplexer-auto-attach/deepwiki-tmux-tmux-wiki-root.md create mode 100644 research/shell-multiplexer-auto-attach/elastic-detection-rule-interactive-terminal-python.md create mode 100644 research/shell-multiplexer-auto-attach/fr0stb1rd-tmux-login-autorestart-logging.md create mode 100644 research/shell-multiplexer-auto-attach/git-alternativebit-ptyfwd-source-listing.md create mode 100644 research/shell-multiplexer-auto-attach/github-bitcanon-autotyper.md create mode 100644 research/shell-multiplexer-auto-attach/github-community-discussion-188494-ai-agent-terminal.md create mode 100644 research/shell-multiplexer-auto-attach/github-fak-issue-11425-pty-interactive-prompt-detection.md create mode 100644 research/shell-multiplexer-auto-attach/github-tmux-discussion-3567-no-raw-socket-api.md create mode 100644 research/shell-multiplexer-auto-attach/github-tmux-discussion-4016-control-mode-output.md create mode 100644 research/shell-multiplexer-auto-attach/github-tmux-wiki-control-mode.md create mode 100644 research/shell-multiplexer-auto-attach/github-vscode-community-discussion-161238.md create mode 100644 research/shell-multiplexer-auto-attach/github-vscode-issue-253265.md create mode 100644 research/shell-multiplexer-auto-attach/github-vscode-issue-313160-respect-user-shell.md create mode 100644 research/shell-multiplexer-auto-attach/gitlab-mr-3383-stdin-stdout-isatty-mismatch.md create mode 100644 research/shell-multiplexer-auto-attach/glama-terminally-mcp-tmuxmanager.md create mode 100644 research/shell-multiplexer-auto-attach/huggingface-bash-bashrc-mirrors.md create mode 100644 research/shell-multiplexer-auto-attach/inskillclaude-tmux-custom-prompt-guide.md create mode 100644 research/shell-multiplexer-auto-attach/k6-browser-presssequentially.md create mode 100644 research/shell-multiplexer-auto-attach/k6-v2-3-presssequentially-mirror.md create mode 100644 research/shell-multiplexer-auto-attach/keyboardmaestro-forum-random-delay.md create mode 100644 research/shell-multiplexer-auto-attach/kx-cloudingenium-zellij-guide.md create mode 100644 research/shell-multiplexer-auto-attach/libtmux-pane-interaction.md create mode 100644 research/shell-multiplexer-auto-attach/libtmux-python-github-commit.md create mode 100644 research/shell-multiplexer-auto-attach/lkml-2003-sigwinch-foreground-process-group.md create mode 100644 research/shell-multiplexer-auto-attach/lkml-mailing-list-tty-s-insufficient.md create mode 100644 research/shell-multiplexer-auto-attach/lkml-sigwinch-thread-replies.md create mode 100644 research/shell-multiplexer-auto-attach/mager-co-orchestrating-agents-tmux.md create mode 100644 research/shell-multiplexer-auto-attach/man7-tmux-manual.md create mode 100644 research/shell-multiplexer-auto-attach/microsoft-terminal-issue-5612.md create mode 100644 research/shell-multiplexer-auto-attach/mintlify-wiki-control-mode-mirror.md create mode 100644 research/shell-multiplexer-auto-attach/nncron-send-keys-delay-docs.md create mode 100644 research/shell-multiplexer-auto-attach/oneuptime-tmux-session-recovery.md create mode 100644 research/shell-multiplexer-auto-attach/openskillindex-autoskill-typing.md create mode 100644 research/shell-multiplexer-auto-attach/oreilly-bash-cookbook-interactive-test.md create mode 100644 research/shell-multiplexer-auto-attach/packagist-piuu-typing-simulation.md create mode 100644 research/shell-multiplexer-auto-attach/panoramicdata-detecting-ai-agent-terminals.md create mode 100644 research/shell-multiplexer-auto-attach/petronellatech-zellij-guide.md create mode 100644 research/shell-multiplexer-auto-attach/pypi-tmux-control.md create mode 100644 research/shell-multiplexer-auto-attach/pythontutorials-net-ps1-child-process-caveat.md create mode 100644 research/shell-multiplexer-auto-attach/raycast-fake-typing-effect.md create mode 100644 research/shell-multiplexer-auto-attach/rdrr-io-tmuxr-send-keys.md create mode 100644 research/shell-multiplexer-auto-attach/santini-tmux-safety-file.md create mode 100644 research/shell-multiplexer-auto-attach/skills-rest-tmux-skill.md create mode 100644 research/shell-multiplexer-auto-attach/stalkr-remote-tty-resize-design.md create mode 100644 research/shell-multiplexer-auto-attach/stonecharioteer-zellij-til.md create mode 100644 research/shell-multiplexer-auto-attach/systemd-ptyfwd-resize-forwarding.md create mode 100644 research/shell-multiplexer-auto-attach/tabby-typewriter-mode-docs.md create mode 100644 research/shell-multiplexer-auto-attach/tao-of-tmux-server-internals.md create mode 100644 research/shell-multiplexer-auto-attach/tek-tips-interactive-shell-forum.md create mode 100644 research/shell-multiplexer-auto-attach/termdock-terminal-multiplexing-mirrors.md create mode 100644 research/shell-multiplexer-auto-attach/theregister-tiling-multiplexer-survey.md create mode 100644 research/shell-multiplexer-auto-attach/tmux-man-page-mirrors.md create mode 100644 research/shell-multiplexer-auto-attach/tmuxai-dev-control-mode.md create mode 100644 research/shell-multiplexer-auto-attach/tmuxai-dev-faq.md create mode 100644 research/shell-multiplexer-auto-attach/toolradar-zellij-vs-tmux.md create mode 100644 research/shell-multiplexer-auto-attach/unix-stackexchange-ssh-tmux-nesting.md create mode 100644 research/shell-multiplexer-auto-attach/velog-tmux-vs-screen-vs-zellij.md create mode 100644 research/shell-multiplexer-auto-attach/yarnpkg-gpt-type-effect.md create mode 100644 research/sqlite-rag-hybrid-search/alexgarcia-sqlite-vec-hybrid-search.md create mode 100644 research/sqlite-rag-hybrid-search/arxiv-2504-05324-hybrid-retrieval-hallucination.md create mode 100644 research/sqlite-rag-hybrid-search/arxiv-2601-05264-engineering-rag-stack.md create mode 100644 research/sqlite-rag-hybrid-search/arxiv-2602-03368-rag-medical-domain.md create mode 100644 research/sqlite-rag-hybrid-search/bigdataboutique-rrf.md create mode 100644 research/sqlite-rag-hybrid-search/byteplus-chunking-topic.md create mode 100644 research/sqlite-rag-hybrid-search/cloudthat-chunking-strategies-guide.md create mode 100644 research/sqlite-rag-hybrid-search/cohere-chunking-strategies.md create mode 100644 research/sqlite-rag-hybrid-search/conclusions/no-vectors-bm25-chunked-rag.md create mode 100644 research/sqlite-rag-hybrid-search/databricks-ultimate-guide-chunking.md create mode 100644 research/sqlite-rag-hybrid-search/devto-ayush-kumar-practical-chunking.md create mode 100644 research/sqlite-rag-hybrid-search/devto-ditched-vector-db-fts5.md create mode 100644 research/sqlite-rag-hybrid-search/devto-rasinmuhammed-chunking-tui.md create mode 100644 research/sqlite-rag-hybrid-search/f22labs-7-chunking-strategies.md create mode 100644 research/sqlite-rag-hybrid-search/glama-daemonmcp-fusion.md create mode 100644 research/sqlite-rag-hybrid-search/glama-orneryd-mimir-rrf.md create mode 100644 research/sqlite-rag-hybrid-search/glukhov-chunking-strategies-rag.md create mode 100644 research/sqlite-rag-hybrid-search/ingoampt-bm25-fts5-google.md create mode 100644 research/sqlite-rag-hybrid-search/medium-privexbot-chunking-strategies.md create mode 100644 research/sqlite-rag-hybrid-search/memx-rrf-glossary.md create mode 100644 research/sqlite-rag-hybrid-search/novel-rag-github.md create mode 100644 research/sqlite-rag-hybrid-search/paradedb-rrf.md create mode 100644 research/sqlite-rag-hybrid-search/popoto-hybrid-retrieval-rrf.md create mode 100644 research/sqlite-rag-hybrid-search/quizzly-chunking-heuristics.md create mode 100644 research/sqlite-rag-hybrid-search/redis-reciprocal-rank-fusion.md create mode 100644 research/sqlite-rag-hybrid-search/software-engineer-blog-fts5-inverted-index.md create mode 100644 research/sqlite-rag-hybrid-search/spiceai-reciprocal-rank-fusion.md create mode 100644 research/sqlite-rag-hybrid-search/sqlite-search-github.md create mode 100644 research/sqlite-rag-hybrid-search/stackai-chunking-strategies.md create mode 100644 research/sqlite-rag-hybrid-search/talentica-sqlite-vector-db.md create mode 100644 research/sqlite-rag-hybrid-search/towardsai-21-chunking-strategies.md create mode 100644 research/sqlite-rag-hybrid-search/towardsai-stop-blaming-llm-chunking.md create mode 100644 research/sqlite-rag-hybrid-search/unimon-rrf-glossary.md create mode 100644 tai_test.py create mode 100644 tests/__init__.py create mode 100644 tests/bashrc.py create mode 100644 tests/container.py create mode 100644 tests/docker/Dockerfile create mode 100644 tests/plexer.py create mode 100644 tests/rag.py diff --git a/.claude/hooks/research-reminder.sh b/.claude/hooks/research-reminder.sh new file mode 100755 index 0000000..04ba6f1 --- /dev/null +++ b/.claude/hooks/research-reminder.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# PostToolUse hook, matcher: WebSearch. +# Guardrail against silently skipping this project's research-archiving rule +# (AGENTS.md "Research Rule" / research/README.md) — it happened once already, +# so this fires on every WebSearch call rather than relying on memory. +# Never blocks (no "decision" field) and does not inspect stdin: the reminder +# is the same every time, so there is nothing to parse. +jq -n --arg msg "Research archiving reminder: this WebSearch result must be archived under research// per AGENTS.md's Research Rule before this task is considered done. See research/README.md for the Source Dossier / Source Citation Card format, and research/INDEX.md to check whether an existing subject already covers this topic. There is no \"this one does not count\" exception (research/README.md section 4) -- that exact rationalization is what caused this rule to be skipped once already." \ + '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $msg}}' diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..dccee0c --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "WebSearch", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/research-reminder.sh", + "timeout": 5 + } + ] + } + ] + } +} diff --git a/.gitignore b/.gitignore index e28090a..ff1f8b5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ # HTML scratch files *.html *.md +!research/**/*.md # Backup files *.bak @@ -19,3 +20,12 @@ # OS artifacts .DS_Store Thumbs.db + +# Integration test run logs (tai_test.py / tests/*.py artifacts) +tests/logs/ + +# Python bytecode cache +__pycache__/ + +# Stray pasted debug transcripts +tmux-scrollback-skill-issue.txt diff --git a/AGENTS.md b/AGENTS.md index d20b3b0..800791d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,14 @@ - Use plain `import` and `from` statements in Python files. - Use the `.py` extension for Python source and test files. -- Keep source and test files at the repository top level. -- Name test files `test_*.py`. +- Keep source files at the repository top level. +- There are two kinds of test, in two different places, on purpose — see "Testing" below for the full split. In short: unit tests are top-level `test_*.py`; integration tests are `tests/.py` (no `test_` prefix) built on `tai_test.py`. + +## Testing + +**Unit tests**: `test_tai.py`, `test_seal.py`, at the repository top level, named `test_*.py`, `unittest`-based, import `tai.py` as a module directly, no subprocess, fast. `python3 test_tai.py` / `python3 test_seal.py`, or a single test via `python3 -m unittest test_tai.SomeClass.test_method`. + +**Integration tests**: `tests/.py` (e.g. `tests/rag.py`), deliberately *not* named `test_*.py` and *not* at the top level, so the two kinds are never confused. These drive the real `tai.py` binary as a subprocess (one-shot or REPL) and check actual CLI behavior, using `tai_test.py` (the framework, at the repository root) as the one and only base to build on — never a one-off `subprocess`/`asyncio` harness per file. **They must exercise a real LLM call through the real binary — never a mocked model or a database write standing in for what the agent would have done; the resulting flakiness from a free, rotating backend is accepted, not engineered away.** Full conventions, that rule's full rationale, the required isolation rule (always a throwaway `TAI_HOME`), and why verification must check vault state rather than scrape CLI rendering, are in `tests/README.md` — read it before adding a new one. Run with `python3 tests/.py` (`--filter`, `--interactive`, `--timeout` all supported). ## Agent Overview @@ -73,4 +79,10 @@ - Keep code maintainable - Document decisions - Version control changes -- Collaborate effectively \ No newline at end of file +- Collaborate effectively + +## Research Rule + +All external research conducted for this project (web searches, reading documentation to settle a design question, competitive/prior-art analysis) is archived under `research/`, maintained under the auspices of the **Molodetz Institute — Research & Retrieval Division**. The full specification — folder layout, the three required document types (Source Dossier, Source Citation Card, Conclusion Document), required metadata, and the quality bar — lives in `research/README.md`; `research/INDEX.md` is the living registry and statistics page. Read `research/README.md` in full before writing into this archive. + +The binding rule, in short: **a research task is not done until it is written into `research/`.** Every source actually consulted gets its own document (Tier 1 if read and synthesized, Tier 2 if only seen in a result listing — never skipped, never inflated past what's actually known), every decision the research produced gets its own Conclusion document in that subject's `conclusions/` folder, and `research/INDEX.md` gets updated in the same pass. Check `research/INDEX.md` for an existing subject before opening a new one. \ No newline at end of file diff --git a/README.md b/README.md index e235b50..4962825 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,20 @@ Nothing here is decorative. Every mechanism below exists because something speci ## Requirements -- Python 3.10 or newer, no third-party packages. +- Python **3.6 or newer** (yes, really — see below), no third-party packages. +- Any CPU word size. 32-bit is fine. Very well tested, fuck me. - Optional: `podman` or `docker` for the sandbox and Telegram voice notes. - Optional: `tmux` for terminal content capture and full-history export. - Optional: `git` only if you install a skill straight from a git URL — every other skill/install path is pure stdlib. +### Why 3.6, specifically + +There is deliberate, load-bearing Python 3.6 backward compatibility baked into `tai.py`, for a sick situation retoor does not want to talk about. Concretely: `subprocess` calls stick to `universal_newlines` plus explicit `PIPE`s instead of the `text`/`capture_output` kwargs (3.7+ only), and `datetime.fromisoformat` — also 3.7+ — is never called directly; everything routes through a hand-rolled `_fromisoformat()` backport (native when available, a regex-plus-`strptime` fallback when not, `Z`/offset-aware either way). No walrus operator, no `match` statements, no PEP 604 `X | Y` type hints, nowhere in the file — not because nobody thought of it, but because every single one of them would quietly break the 3.6 floor. Don't introduce one. + +### Why 32-bit doesn't matter + +The whole thing is pure stdlib Python with zero `ctypes`, zero hand-packed `struct` pointer trickery, and zero code that assumes a 64-bit word anywhere. SQLite, `subprocess`, `urllib`, `socket`/`ssl` for the voice protocol — none of it cares about your CPU's register width. If it runs on your architecture's Python at all, it runs correctly, full stop. + ## Quick start ./tai.py diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d519175 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + tai-test: + build: + context: . + dockerfile: tests/docker/Dockerfile + command: ["python3", "tests/container.py", "--inside"] + # No volumes, no network restrictions, no persistence on purpose: this + # service exists to run the container-only test suite (see + # tests/container.py and tests/bashrc.py) inside a throwaway environment + # that it is safe to install a real ~/.bashrc hook into and start a real + # tmux session in. `docker compose run --rm tai-test` or + # `podman compose run --rm tai-test` both work; tests/container.py + # itself drives plain build/run directly (via tai.container_engine(), + # the same podman-then-docker detection the project's own sandbox + # feature already uses) so the automated path never depends on which + # compose implementation, if any, happens to be installed -- this file + # is for a human who wants to run the same thing by hand. diff --git a/research/INDEX.md b/research/INDEX.md new file mode 100644 index 0000000..38fbc65 --- /dev/null +++ b/research/INDEX.md @@ -0,0 +1,41 @@ +# Research Archive Index + +**Molodetz Institute — Research & Retrieval Division.** This is the living registry of every subject in `research/`. It is updated in the same pass as any new research, per the standing rule in `README.md` §4. Never edit a subject's documents without also updating its row here. + +## Global statistics + +| Metric | Value | +|---|---| +| Subjects | 4 | +| Total source documents | 225 | +| — Tier 1 (Source Dossier, independently analyzed) | 84 | +| — Tier 2 (Source Citation Card, listed only) | 141 | +| Total conclusion documents | 4 | +| Archive opened | 2026-10-09 | +| Last updated | 2026-10-09 | + +## Subjects + +| Subject | Sources (T1 / T2) | Conclusions | Opened | Last updated | Driving task | +|---|---|---|---|---|---| +| [`sqlite-rag-hybrid-search`](sqlite-rag-hybrid-search/) | 32 (10 / 22) | 1 | 2026-10-09 | 2026-10-09 | Designing tmux-log export + chunked retrieval; settled on BM25-only, no vector DB | +| [`agent-memory-recency-tracking`](agent-memory-recency-tracking/) | 63 (16 / 47) | 1 | 2026-10-09 | 2026-10-09 | Designing an LRU-style last-used/use-count tracker for vault records and chunks | +| [`async-pty-process-testing`](async-pty-process-testing/) | 59 (20 / 39) | 1 | 2026-10-09 | 2026-10-09 | Designing tai_test.py (pty vs. pipes, non-blocking stdin, process-group cleanup, test-framework shape) — backfilled after it was initially skipped, see activity log | +| [`shell-multiplexer-auto-attach`](shell-multiplexer-auto-attach/) | 71 (38 / 33) | 1 | 2026-10-09 | 2026-10-09 | Designing a tai-managed terminal multiplexer + bashrc auto-attach: tmux-as-substrate recommendation, interactive-vs-automation shell detection | + +## Recent activity log + +| Date | Subject | Event | +|---|---|---| +| 2026-10-09 | — | Archive created: specification (`README.md`), index (this file), and both subjects above backfilled from the day's research in full, per explicit instruction to retroactively capture every source consulted that day. | +| 2026-10-09 | `sqlite-rag-hybrid-search` | 32 sources (10 Tier 1 / 22 Tier 2) + 1 conclusion recorded (the conclusion: pure SQLite FTS5/BM25 chunked search, deliberately no embeddings/vector DB — see `conclusions/`). Counts verified directly against the written files, not taken from the authoring agent's self-report. | +| 2026-10-09 | — | Spec change: `Tier` split into its own standalone metadata row (`1` / `2` / `N/A`) instead of a parenthetical inside `Document type`, so it's directly filterable. All 97 existing documents across both subjects backfilled to match in the same pass; counts re-verified afterward (unchanged: 26 Tier 1, 69 Tier 2, 2 Conclusions). | +| 2026-10-09 | `agent-memory-recency-tracking` | 63 sources (16 Tier 1 / 47 Tier 2) + 1 conclusion recorded (the conclusion: `last_used`/`use_count` tracking proposal for records and chunks, throttled-write pattern, no eviction policy yet — see `conclusions/`). One source (`glama-code-index-mcp-bm25-hybrid.md`) was promoted from the originally-planned Tier 2 to Tier 1 by the authoring agent mid-task because it contained specific attributable numeric findings — exactly the judgment call §3 of `README.md` calls for, logged here rather than silently absorbed. Counts verified directly against the written files. | +| 2026-10-09 | `async-pty-process-testing` | New subject, opened as a correction: the eight `WebSearch` calls behind `tai_test.py`'s design (pty vs. pipes, non-blocking stdin, process-group cleanup, test-framework patterns) were initially skipped from the archive under the reasoning "informal engineering reference, not a design decision" — a silent, unilateral exception to the Research Rule, and wrong (see `README.md` §4's added "no exception, ever" paragraph). Backfilled: 59 sources (20 Tier 1 / 39 Tier 2) + 1 conclusion (`conclusions/tai-test-framework-design.md`). Counts verified directly against the written files via `research/audit.py`, not taken from the authoring agent's self-report alone. A `PostToolUse`/`WebSearch` hook (`.claude/settings.json`) was also added the same day specifically to stop this from recurring. | +| 2026-10-09 | `shell-multiplexer-auto-attach` | 71 sources (38 Tier 1 / 33 Tier 2) + 1 conclusion recorded (the conclusion: build on tmux as a substrate rather than a from-scratch multiplexer; reliable human-vs-automation shell detection has no single solution, only a combination of weaker signals plus a safety-file escape hatch). Counts verified directly against the written files via `research/audit.py`. Hook fired correctly for all 8 underlying WebSearch calls during this task. | + +## How to use this index + +- Before starting new research, search this table for an existing subject before opening a new one (see `README.md` §2, "one subject, one home"). +- Every subject folder is self-contained: its sources live flat inside it, its conclusions live in its `conclusions/` subfolder. +- Tier 1 vs Tier 2 is a statement about how deeply a source was actually read, not about its importance — see `README.md` §3 for the exact distinction. diff --git a/research/README.md b/research/README.md new file mode 100644 index 0000000..e96cf66 --- /dev/null +++ b/research/README.md @@ -0,0 +1,205 @@ +# The tai Research Archive + +**Maintained under the auspices of the Molodetz Institute — Research & Retrieval Division.** + +This directory is a permanent, append-only archive of every piece of external research conducted in support of the `tai` project. It is not a scratch pad. It is not a cache. Once a document lands here it is never deleted and never silently rewritten — it is corrected forward, with a changelog entry, the way a real research institute corrects the record. + +This file is the specification. Every document under `research/` must conform to it exactly. If a future research session cannot make a source or a conclusion fit this specification, the specification gets amended first (with a changelog entry below), and only then does the document get written — the rule is never quietly bent for one entry. + +## 1. Why this exists + +Research happens, gets used for one decision, and then evaporates — the next person (or the next session of the same agent) re-derives it from scratch or, worse, never finds out it was already settled. This archive exists so that: + +- every external claim this project's design relies on has a traceable source, a date, and an honest confidence level; +- a conclusion (a decision actually made because of research) is recorded separately from the raw source material that informed it, so "what we found" and "what we decided" never get smeared into one document that's impossible to audit later; +- the next person — human or model — can answer "have we already looked into this?" by reading an index instead of repeating a web search. + +## 2. Directory layout + +``` +research/ + README.md <- this specification (the only file allowed at the root besides INDEX.md) + INDEX.md <- the living registry of every subject, every document, and archive-wide statistics + / + .md <- one file per external source consulted for this subject + conclusions/ + .md <- one file per distinct conclusion/decision drawn from this subject's sources +``` + +**Subject** is a research topic, not a single query — "sqlite-rag-hybrid-search" or "agent-memory-recency-tracking", not "search query #4". A new research task that is clearly a continuation of an existing subject adds documents into that subject's existing folder; it does not fork a near-duplicate subject. When genuinely unsure whether something is a new subject or a continuation, prefer the existing subject — subject sprawl is a worse failure mode than a slightly-too-broad subject. + +**Slugs** are lowercase, hyphen-separated, ASCII, no stop words beyond what's needed for uniqueness, derived from the source's domain plus a short topic fragment (e.g. `mem0-introducing-memory-decay.md`, `arxiv-2310-08560-memgpt.md` for a numbered arXiv paper — the arXiv id goes in the slug verbatim, hyphenated, because it's the only stable identifier a paper has). Two different pages on the same domain get distinguishing fragments, never `-2`, `-3` suffixes — a suffix number tells a future reader nothing about what's inside the file. + +## 3. The three document types + +There are exactly three kinds of document in this archive. Nothing else is permitted at the file level; a new need gets solved by extending one of these three, with a changelog entry here, not by inventing a fourth ad hoc. + +### 3.1 Source Dossier (Tier 1 — independently analyzed) + +Used when the source was actually read and synthesized in enough depth to state real findings, pros, cons, and a relevance judgment. This is the default, full-weight document. Required sections, in this exact order: + +```markdown +# + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | | +| Source URL | | +| Source publisher / author | | +| Source type | | +| Source date (published) | | +| Date researched | | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | | +| Confidence | | +| Status | | + +## Summary + +Two to five sentences. What does this source actually claim. No hedging filler — if the source is thin, the summary says so plainly rather than padding it. + +## Key findings + +Bullet list of concrete, attributable claims. Each bullet should be something a reader could independently go verify against the source. + +## Pros / strengths + +What this source gets right, or what makes it a good argument for its position. + +## Cons / limitations / caveats + +What's missing, unproven, vendor-biased, outdated, or contradicted elsewhere. A Tier 1 dossier with an empty cons section is a red flag that it wasn't read critically — write "none identified, see confidence rating" explicitly if that's genuinely the case, rather than leaving the section out. + +## Relevance to tai + +Why this mattered to the actual engineering decision at hand. A source can be excellent and still irrelevant; say so if that's the case. + +## Changelog + +| Date | Change | +|---|---| +| | Initial dossier created. | +``` + +### 3.2 Source Citation Card (Tier 2 — listed, not independently analyzed) + +Used for a source that turned up during research (a search result, a related link) but was **not** independently read in enough depth to responsibly claim pros, cons, or specific findings. This tier exists specifically so volume of sources consulted is never achieved by quietly inflating what's actually known about each one — it is better, and more professional, to log a source honestly as "seen, not analyzed" than to fabricate a dossier for it. Required sections: + +```markdown +# + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | | +| Source URL | | +| Source type | | +| Date researched | | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +One or two sentences: what search surfaced it, and why it looked potentially relevant to the subject. + +## Changelog + +| Date | Change | +|---|---| +| | Logged as consulted-but-not-analyzed. | +``` + +A Tier 2 card may later be promoted to a Tier 1 dossier if someone actually reads the source in depth — promotion means rewriting the file to the Tier 1 template and adding a changelog line noting the promotion and its date, never duplicating it as a second file. + +### 3.3 Conclusion Document + +A conclusion is a decision or a standing judgment the project actually made *because of* research — never just a restatement of one source. It lives in `/conclusions/` and is written only once the underlying research is done. Required sections: + +```markdown +# Conclusion: + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | <subject-slug> | +| Document type | Conclusion | +| Tier | N/A | +| Date decided | <YYYY-MM-DD> | +| Decided by | retoor, advised by tai (agent, Claude Sonnet 5) | +| Status | <Active \| Superseded by <link> \| Reverted> | +| Confidence | <High \| Medium \| Low> | + +## Question / problem statement + +What question was this research actually trying to answer. One paragraph, no padding. + +## Conclusion + +The actual decision or standing judgment, stated as a plain declarative claim a reader could act on without reading anything else. + +## Supporting sources + +Linked list of the Tier 1 dossiers (and, where genuinely load-bearing, Tier 2 cards) in this subject that back this conclusion, each with a one-line note on what it contributed. + +## Alternatives considered and rejected + +What else was on the table, and the specific reason each alternative lost. "We didn't think of it" is a valid entry if true; inventing a more flattering rejection reason is not. + +## Risks / open questions + +What could make this conclusion wrong later, and what would have to change for it to be revisited. + +## Action taken + +What actually happened as a result (a feature shipped, a design rule adopted, a decision deferred on purpose). If nothing was built yet, say so explicitly — a conclusion is allowed to be "we now know what to do, we have not done it yet." + +## Changelog + +| Date | Change | +|---|---| +| <YYYY-MM-DD> | Initial conclusion recorded. | +``` + +## 4. The standing rule (also stated in `AGENTS.md`) + +**Every time research is conducted for this project — a web search, a round of searches, reading documentation to settle a design question — it is written into this archive before the research is considered "done," not as an afterthought.** Concretely, every research task produces: + +1. a subject folder (new, or an existing one reused); +2. one document per source actually consulted, Tier 1 if it was read and synthesized, Tier 2 if it was only seen in a result listing — no source gets skipped just because it turned out to be low-value; a low-value source is itself a finding, recorded honestly; +3. a conclusion document for every decision the research actually produced, if any was reached; +4. an updated `INDEX.md` — the subject's row, the global counts, and the "Recent activity" log all get updated in the same pass, never left for later. + +A research task that produces no written archive entries did not happen, as far as this project is concerned — if it isn't here, it gets redone and re-verified the next time someone needs it, which is the exact waste this archive exists to prevent. + +**There is no "this one doesn't really count" exception, ever.** This rule was broken once already — the research behind `tai_test.py` (eight `WebSearch` calls on pty/asyncio/process-testing patterns) was skipped on the judgment that it was "informal engineering reference, not a design decision," a call made silently and alone, never surfaced to anyone, and wrong. "It's just engineering," "it's too minor," "I'll remember to do it after I finish the actual task" are exactly the thoughts that preceded that miss, not reasons it was fine. If a research task feels like an exception, that is the signal to archive it anyway, not a reason to skip it — and if there's ever real ambiguity about whether something should be archived, that ambiguity gets stated out loud (to the user, or as a visible note in the response) instead of resolved unilaterally and invisibly. A decision nobody can see is a decision nobody can catch. + +## 5. Quality bar + +- **No fabricated specifics.** A Tier 1 dossier's "Key findings" must be things actually said by the source, not plausible-sounding inferences dressed up as findings. When synthesis, not direct reading, is the basis for a claim (e.g. a web-search tool's own summary of a page), that's still fine for Tier 1 as long as it's a faithful restatement — it only drops to Tier 2 when there isn't enough independent content to responsibly summarize at all. +- **Confidence ratings are honest, not reassuring.** "Low" confidence on a real finding is more useful to a future reader than "High" confidence dressed up to look thorough. +- **Dates are never guessed silently.** "Unknown — not stated" is a correct value for a metadata field; a made-up date is not. +- **Corrections are forward, not silent edits.** If a later research pass finds a dossier was wrong, the file gets updated and the changelog gets a new row explaining what changed and why — the old claim doesn't just vanish from history. +- **One subject, one home.** Before creating a new subject folder, check `INDEX.md` for an existing one that already covers the topic. +- **`Tier` is its own metadata field, not text buried in `Document type`.** Every document's metadata table has a standalone `Tier` row (`1`, `2`, or `N/A` for a Conclusion) precisely so tier can be greped/filtered on directly — `grep "^| Tier | 1 |"` — instead of matching a parenthetical inside a longer string. `Document type` states only the document kind (`Source Dossier`, `Source Citation Card`, `Conclusion`); it never repeats the tier. + +## 6. Changelog (of this specification itself) + +| Date | Change | +|---|---| +| 2026-10-09 | Initial specification written, alongside the first two subjects (`sqlite-rag-hybrid-search`, `agent-memory-recency-tracking`) backfilled from the same day's research. | +| 2026-10-09 | Split `Tier` out into its own metadata field (`1` / `2` / `N/A`) instead of a parenthetical inside `Document type`, so it can be filtered on directly. All existing documents backfilled to match in the same pass. | diff --git a/research/agent-memory-recency-tracking/accessprogrammers-datetime-index-performance.md b/research/agent-memory-recency-tracking/accessprogrammers-datetime-index-performance.md new file mode 100644 index 0000000..ccfaac1 --- /dev/null +++ b/research/agent-memory-recency-tracking/accessprogrammers-datetime-index-performance.md @@ -0,0 +1,28 @@ +# Performance of Date-Only vs Date-Time Indexes (forum thread) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Performance of Date-Only vs Date-Time Indexes | +| Source URL | https://www.access-programmers.co.uk/forums/goto/post?id=1120144 | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "SQLite last_accessed column unix timestamp vs datetime performance indexing"; an MS Access-focused forum discussion on date-index performance, tangential to SQLite specifically but conceptually adjacent. Not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/arpit-2q-cache-algorithm.md b/research/agent-memory-recency-tracking/arpit-2q-cache-algorithm.md new file mode 100644 index 0000000..5f61bf3 --- /dev/null +++ b/research/agent-memory-recency-tracking/arpit-2q-cache-algorithm.md @@ -0,0 +1,52 @@ +# 2Q Cache Management Algorithm + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | 2Q Cache Management Algorithm | +| Source URL | https://arpit.substack.com/p/2q-cache-management-algorithm | +| Source publisher / author | Arpit (Substack newsletter), explaining Johnson & Shasha's 1994 VLDB paper | +| Source type | blog post (explainer of a primary paper) | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +An accessible explainer of Johnson & Shasha's 1994 "2Q" buffer-replacement algorithm — a constant-overhead, no-tuning alternative to full LRU that influenced real database buffer-pool design, most notably PostgreSQL's. + +## Key findings + +- 2Q answers whether a constant-overhead-per-access scheme can match LRU/2 page-replacement performance; the 1994 paper's answer is yes, with no tuning required. +- The design splits the buffer into three structures: a main LRU queue for confirmed-hot pages, a first-in (probationary) queue for newly-seen pages, and a ghost queue that stores only the identifiers of recently-evicted pages (not their data). +- A page enters the probationary queue on first access; if it's re-referenced while still represented in the ghost queue, it gets promoted into the main queue. +- This design is specifically scan-resistant: a one-time sequential scan fills and drains the probationary queue without ever polluting the main hot-page queue. +- Per the source, 2Q's simplicity and effectiveness influenced PostgreSQL's own buffer-management design. + +## Pros / strengths + +- O(1), constant-overhead-per-access, no tuning parameters — directly addresses the "speed is important" constraint behind this entire research task, as a possible template if tai ever needs a real eviction policy rather than just tracking. +- Real production lineage (PostgreSQL) gives confidence this isn't just an academic curiosity. + +## Cons / limitations / caveats + +- 2Q's own main-LRU-queue component is itself expensive to make concurrent under high contention — this is exactly the weakness that `arxiv-2511-21958-clock2q-plus.md` (Clock2Q+) sets out to fix by replacing that component with a CLOCK-style structure. +- 2Q is an eviction-policy design for buffer pools; tai is not (yet) building eviction at all, only tracking — so 2Q is background context for a possible future phase, not something applicable today. + +## Relevance to tai + +Background/comparative context only for this research pass — explicitly not part of the current proposal (which is tracking-only, no eviction), but recorded here so a future eviction-policy decision doesn't have to rediscover 2Q as a candidate from scratch. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/arxiv-2304-03442-generative-agents.md b/research/agent-memory-recency-tracking/arxiv-2304-03442-generative-agents.md new file mode 100644 index 0000000..aa0cea9 --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2304-03442-generative-agents.md @@ -0,0 +1,53 @@ +# Generative Agents: Interactive Simulacra of Human Behavior + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Generative Agents: Interactive Simulacra of Human Behavior | +| Source URL | https://arxiv.org/pdf/2304.03442 | +| Source publisher / author | Park, Joon Sung et al. (Stanford / Google), 2023 | +| Source type | paper | +| Source date (published) | 2023-04-07 (arXiv submission) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +This is the original "generative agents" paper that introduced a retrieval-scoring scheme for agent memory combining recency, importance, and relevance into a single retrieval score. It is the foundational reference almost every later agent-memory system (MemGPT, Letta, Mem0, and the smaller forgetting-curve projects) cites or reacts against. + +## Key findings + +- Recency is modeled as an exponential decay over time since a memory was *last retrieved* (not last created), with a decay factor of 0.995 per sandbox game hour. +- Importance is an LLM-assigned integer score (1–10 "poignancy" scale) — the paper found that simply asking the model to output an integer score worked well in practice. +- Relevance is cosine similarity between embedding vectors of the memory text and the current situation's text. +- The three signals are combined into one retrieval score; sources disagree on the exact combination — some report equal weighting after normalization, others note the released implementation simply uses a weight of 1 for all three. +- The paper separately introduces "reflection": higher-level, more abstract thoughts generated by the agent, which are themselves injected back into the memory stream and participate in retrieval. + +## Pros / strengths + +- Recency, importance, and relevance are kept as three independent signals combined only at retrieval time, not pre-collapsed into one opaque score at write time — this is a reusable design principle independent of the specific decay constant chosen. +- The scheme is simple enough to reimplement and has become the de facto reference point for the whole field, which makes it a useful common vocabulary even where tai's design diverges from it. + +## Cons / limitations / caveats + +- The importance score depends on an LLM call per memory, which is a real cost this paper does not need to justify for its own research-simulation use case, but which matters for a production system. +- The exact weighting of the three signals is not consistently reported across secondary sources — the dossier author was not able to confirm the canonical weights straight from the paper text, only from secondary synthesis of it, so treat exact weight numbers as uncertain pending a direct read of the paper's equations. +- Decay is calibrated to "sandbox game hours" in a simulated environment; the constant (0.995/hour) is not inherently meaningful for tai's real-wall-clock vault usage and would need its own calibration if ever adopted. + +## Relevance to tai + +This is the primary precedent for keeping recency as an independent, un-collapsed signal rather than baking a final blended score into storage — directly informing the recommendation in this subject's conclusion to store `last_used` and `use_count` as raw signals rather than a single derived priority number. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/arxiv-2310-08560-memgpt.md b/research/agent-memory-recency-tracking/arxiv-2310-08560-memgpt.md new file mode 100644 index 0000000..a9e8b1c --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2310-08560-memgpt.md @@ -0,0 +1,51 @@ +# MemGPT: Towards LLMs as Operating Systems + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | MemGPT: Towards LLMs as Operating Systems | +| Source URL | https://arxiv.org/pdf/2310.08560 | +| Source publisher / author | Packer, Charles et al., 2023 | +| Source type | paper | +| Source date (published) | 2023-10-12 (arXiv submission) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +MemGPT proposes an OS-inspired memory architecture for LLM agents: a bounded in-context "main context" and an external, larger "archival"/"recall" storage, with a queue manager handling overflow. Important for this subject because it is the most commonly cited "LRU-ish" agent memory architecture, and it turns out its actual eviction policy is FIFO, not LRU. + +## Key findings + +- Eviction policy is queue-based and FIFO, not recency-based: a queue manager controls context overflow via a queue eviction policy. +- Before an eviction actually happens, the queue manager inserts a system message warning the LLM that eviction is imminent, so the model can proactively save important content to working context or to archival storage first — the eviction decision point is made visible to the model, not hidden. +- One implementation pattern described in newer benchmark work pairs a bounded main-context FIFO with a FAISS-backed archival recall store — i.e. a vector index sits behind the FIFO-managed front end. +- Overflow handling in some descriptions is characterized as a "swap": evict, recursively summarize, and store into recall storage, triggered at a fraction of fast-context capacity. + +## Pros / strengths + +- Makes the overflow/eviction moment an explicit, LLM-visible event rather than a silent background process — directly useful precedent for "don't silently throw memory away." +- The tiering concept (fast/bounded vs. slow/large) maps cleanly onto database buffer-pool thinking, which is where this subject's CLOCK/2Q research (see `arpit-2q-cache-algorithm.md`, `arxiv-2511-21958-clock2q-plus.md`) becomes relevant as a possible future eviction mechanism if tai ever needs one. + +## Cons / limitations / caveats + +- FIFO is a simpler policy than LRU and explicitly does not use access recency to decide what gets evicted — so despite being the paper most people reach for when discussing "agent memory eviction," it is not actually a precedent for LRU-style tracking, only for tiered memory in general. +- The paper predates, and is architecturally distinct from, the current Letta product (see `letta-agent-memory-blog.md`), which has evolved considerably since; details attributed to "MemGPT" in secondary sources sometimes actually describe current Letta behavior instead. + +## Relevance to tai + +Confirms that even the most prominent "OS-for-LLMs" memory paper does not use LRU for eviction — reinforcing that tai's proposed `last_used`/`use_count` tracking is building a *new* capability for this project, not replicating an established pattern that already has a known-good eviction policy to copy. tai's conclusion deliberately defers the eviction-policy question rather than assuming FIFO or LRU is obviously correct. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/arxiv-2504-19413-mem0-paper.md b/research/agent-memory-recency-tracking/arxiv-2504-19413-mem0-paper.md new file mode 100644 index 0000000..5948908 --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2504-19413-mem0-paper.md @@ -0,0 +1,46 @@ +# Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory | +| Source URL | https://arxiv.org/pdf/2504.19413 | +| Source publisher / author | Mem0 team (official paper) | +| Source type | paper | +| Source date (published) | 2025 (per arXiv id range; exact date unstated in material reviewed) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +The official research paper underlying the Mem0 product, describing its production long-term-memory architecture. This research pass surfaced it mainly as the primary-source counterpart to the Mem0 vendor blog posts (`mem0-introducing-memory-decay.md`, `mem0-memory-eviction-forgetting.md`), rather than extracting independent technical detail from it directly. + +## Key findings + +- Establishes Mem0 as a scalable long-term-memory layer intended for production agent deployments, which is the context the decay/eviction features documented in the companion blog-post dossiers were built for. + +## Pros / strengths + +- Being a peer-reviewable paper rather than pure marketing copy, it is a stronger source of record for Mem0's architecture than the blog posts alone, even though this specific research pass did not extract line-level technical detail from it. + +## Cons / limitations / caveats + +- This research pass's synthesis of this specific paper was thin relative to the blog-post dossiers — the search results centered discussion on the blog posts, not the paper's own equations or benchmarks. A future pass that reads the paper directly should promote or supplement this dossier with the paper's actual decay/scoring formulas if they are more precisely specified there than in the blog posts. + +## Relevance to tai + +Primary-source backing for the Mem0 precedent cited throughout this subject's conclusion; included at Tier 1 rather than Tier 2 because it is the official paper behind a directly relevant product, even though the specific numeric formulas could not be independently confirmed from it in this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/arxiv-2506-23306-gatsim.md b/research/agent-memory-recency-tracking/arxiv-2506-23306-gatsim.md new file mode 100644 index 0000000..37f9ab4 --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2506-23306-gatsim.md @@ -0,0 +1,28 @@ +# GATSim: Urban Mobility Simulation with Generative Agents + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | GATSim: Urban Mobility Simulation with Generative Agents | +| Source URL | https://arxiv.org/pdf/2506.23306 | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LLM agent memory recency importance relevance retrieval scoring generative agents paper" as an applied follow-up using the generative-agents memory architecture for urban mobility simulation; not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/arxiv-2510-19497-toulouse-transport-agents.md b/research/agent-memory-recency-tracking/arxiv-2510-19497-toulouse-transport-agents.md new file mode 100644 index 0000000..95a8e72 --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2510-19497-toulouse-transport-agents.md @@ -0,0 +1,28 @@ +# Modeling realistic human behavior using generative agents in a multimodal transport system: Software architecture and Application to Toulouse + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Modeling realistic human behavior using generative agents in a multimodal transport system: Software architecture and Application to Toulouse | +| Source URL | https://arxiv.org/pdf/2510.19497 | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LLM agent memory recency importance relevance retrieval scoring generative agents paper" as another applied generative-agents simulation (transport modeling in Toulouse); not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/arxiv-2511-21958-clock2q-plus.md b/research/agent-memory-recency-tracking/arxiv-2511-21958-clock2q-plus.md new file mode 100644 index 0000000..89f90b4 --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2511-21958-clock2q-plus.md @@ -0,0 +1,50 @@ +# Clock2Q+: A Simple and Efficient Replacement Algorithm for Metadata Cache in VMware vSAN + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Clock2Q+: A Simple and Efficient Replacement Algorithm for Metadata Cache in VMware vSAN | +| Source URL | https://arxiv.org/pdf/2511.21958 | +| Source publisher / author | VMware vSAN team (per title context), 2025 | +| Source type | paper | +| Source date (published) | 2025 (per arXiv id; exact date unstated in material reviewed) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A 2025 paper proposing Clock2Q+, which keeps 2Q's three-queue structure but replaces its expensive main LRU queue with a CLOCK-style reference-bit structure, directly targeting the concurrency/overhead weakness of classic 2Q — and the single strongest primary-source explanation found in this research pass for *why* CLOCK-style tracking is cheap. + +## Key findings + +- 2Q's main LRU queue has high CPU and memory overhead and is hard to make concurrent, per the paper's own stated motivation. +- Clock2Q+ replaces that main structure with a CLOCK-style structure: on a hit, the algorithm only sets a reference bit, with no list mutation and no lock-protected reordering on the hot path. +- The paper also argues the classic design choice (allocating 25% of the cache to the small/probationary FIFO queue, a figure from 1994) is outdated now that caches are far larger than they were 30 years ago, and that shrinking it frees more room for the main structure. +- Adds a "correlation window" to filter burst-correlated metadata hits, intended to prevent cache pollution while still promoting genuinely hot blocks. + +## Pros / strengths + +- This is the clearest, most directly-stated primary-source explanation in this entire subject for the general principle behind low-write-overhead recency tracking: a hit only flips a bit instead of mutating a data structure. This principle, not the specific vSAN metadata-cache application, is what's actually relevant to tai. +- Being a 2025 paper, it's a current, non-dated reference for this specific design pattern. + +## Cons / limitations / caveats + +- The paper's actual subject is a VMware vSAN metadata cache, a very different workload (block-storage metadata, extremely high access rates) from tai's vault (a local SQLite file with comparatively low concurrent access). The specific tuning (correlation windows, FIFO queue sizing) does not transfer; only the general "reference bit instead of list mutation" principle does. + +## Relevance to tai + +The clearest primary-source justification, among everything found in this subject, for *why* the throttled/conditional-UPDATE pattern recommended in this subject's conclusion is the right shape of optimization (minimize writes on the hot read path) even though tai's proposal uses a timestamp column and a conditional SQL UPDATE rather than a literal CLOCK reference bit — the underlying principle (don't mutate proportional to every access) is the same one being applied. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/arxiv-2512-22995-buffer-management-survey.md b/research/agent-memory-recency-tracking/arxiv-2512-22995-buffer-management-survey.md new file mode 100644 index 0000000..51f8af4 --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2512-22995-buffer-management-survey.md @@ -0,0 +1,49 @@ +# Evolution of Buffer Management in Database Systems: From Classical Algorithms to Machine Learning and Disaggregated Memory + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Evolution of Buffer Management in Database Systems: From Classical Algorithms to Machine Learning and Disaggregated Memory | +| Source URL | https://arxiv.org/pdf/2512.22995 | +| Source publisher / author | Unknown authors (per material reviewed) | +| Source type | paper (survey) | +| Source date (published) | 2025 (per arXiv id; exact date unstated in material reviewed) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A 2025 survey of database buffer-management algorithms spanning classical approaches through ML-based and disaggregated-memory techniques. This research pass drew one specific comparative point from it: its coverage of LIRS and its comparison-table characterization of 2Q. + +## Key findings + +- Describes LIRS (Low Inter-reference Recency Set) as ranking pages by *inter-reference recency* — the gap between successive accesses — rather than by plain recency (time since last access alone). +- Its comparison table characterizes 2Q as scan-resistant with O(1) complexity, corroborating the `arpit-2q-cache-algorithm.md` dossier from an independent, more academic source. + +## Pros / strengths + +- As a survey, it situates 2Q and CLOCK-family algorithms within the broader, still-evolving landscape of buffer management (including newer ML-based and disaggregated-memory approaches), which is useful context even though those newer approaches are well beyond anything tai would need. +- Independent corroboration of 2Q's O(1)/scan-resistant characterization strengthens confidence in that specific claim. + +## Cons / limitations / caveats + +- This research pass did not extract detail on the survey's ML-based or disaggregated-memory sections — only the classical-algorithms comparison was actually used, so claims about those newer sections cannot responsibly be made from this dossier. +- LIRS itself (inter-reference recency) is noted but not explored further in this pass — flagged here as a candidate for deeper investigation only if tai ever gets to a real eviction-policy design phase. + +## Relevance to tai + +Secondary corroboration for the 2Q/CLOCK-family background research; not independently load-bearing for the subject's conclusion, which does not require (or recommend) adopting any specific eviction algorithm yet. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/arxiv-2601-14192-efficient-agents-survey.md b/research/agent-memory-recency-tracking/arxiv-2601-14192-efficient-agents-survey.md new file mode 100644 index 0000000..11b505c --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2601-14192-efficient-agents-survey.md @@ -0,0 +1,28 @@ +# Toward Efficient Agents: Memory, Tool learning, and Planning + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Toward Efficient Agents: Memory, Tool learning, and Planning | +| Source URL | https://arxiv.org/pdf/2601.14192 | +| Source type | paper (survey) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LLM agent memory recency importance relevance retrieval scoring generative agents paper"; a broad survey covering memory among other agent-efficiency topics, not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/arxiv-2601-16986-crystal-kv.md b/research/agent-memory-recency-tracking/arxiv-2601-16986-crystal-kv.md new file mode 100644 index 0000000..2f68c78 --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2601-16986-crystal-kv.md @@ -0,0 +1,28 @@ +# Crystal-KV: Efficient KV Cache Management for Chain-of-Thought LLMs via Answer-First Principle + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Crystal-KV: Efficient KV Cache Management for Chain-of-Thought LLMs via Answer-First Principle | +| Source URL | https://arxiv.org/pdf/2601.16986 | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LRFU combined recency frequency cache replacement formula least recently frequently used"; the research synthesis noted that newer work applies the LRFU "combined recency and frequency" scoring idea to LLM inference KV-cache management specifically (scoring cache entries by a sum of lambda^(t-t_j) over past hit times) — this paper is the likely source of that specific application, though it was not independently opened to confirm the exact formula variant used. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/arxiv-2605-29630-entity-collision.md b/research/agent-memory-recency-tracking/arxiv-2605-29630-entity-collision.md new file mode 100644 index 0000000..bf3d3f4 --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2605-29630-entity-collision.md @@ -0,0 +1,28 @@ +# Entity-Collision: A Stratified Protocol for Attributing Retrieval Lift in Agent Memory + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Entity-Collision: A Stratified Protocol for Attributing Retrieval Lift in Agent Memory | +| Source URL | https://arxiv.org/pdf/2605.29630 | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "MemGPT Letta memory tiering LRU eviction agent context management"; not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/arxiv-2606-12945-multi-factor-value-model.md b/research/agent-memory-recency-tracking/arxiv-2606-12945-multi-factor-value-model.md new file mode 100644 index 0000000..7d94deb --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2606-12945-multi-factor-value-model.md @@ -0,0 +1,28 @@ +# Learning What to Remember: A Cognitively Grounded Multi-Factor Value Model for Agentic Memory + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Learning What to Remember: A Cognitively Grounded Multi-Factor Value Model for Agentic Memory | +| Source URL | https://arxiv.org/pdf/2606.12945 | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LLM agent memory recency importance relevance retrieval scoring generative agents paper"; title suggests direct relevance to multi-factor memory value scoring (recency+frequency+importance style), a plausible candidate for deeper reading in a future research pass, but not opened or quoted during this one. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/arxiv-2607-16848-memory-leaderboards.md b/research/agent-memory-recency-tracking/arxiv-2607-16848-memory-leaderboards.md new file mode 100644 index 0000000..021d057 --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2607-16848-memory-leaderboards.md @@ -0,0 +1,28 @@ +# Beyond Memory Leaderboards: Evaluating Scientific Memory as Budgeted Context Restoration + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Beyond Memory Leaderboards: Evaluating Scientific Memory as Budgeted Context Restoration | +| Source URL | https://arxiv.org/pdf/2607.16848 | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LLM agent memory recency importance relevance retrieval scoring generative agents paper" as a related follow-up paper on evaluating agent memory systems; not opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/arxiv-2607-21503-agentic-context-management.md b/research/agent-memory-recency-tracking/arxiv-2607-21503-agentic-context-management.md new file mode 100644 index 0000000..18d0c6d --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2607-21503-agentic-context-management.md @@ -0,0 +1,28 @@ +# Agentic Context Management: Solving Agent Memory and Cost by Treating Them as Lifecycle and Architecture Problems + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Agentic Context Management: Solving Agent Memory and Cost by Treating Them as Lifecycle and Architecture Problems | +| Source URL | https://arxiv.org/pdf/2607.21503 | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "MemGPT Letta memory tiering LRU eviction agent context management"; title suggests direct relevance to lifecycle-based memory management, a plausible candidate for deeper reading in a future research pass, but not opened or quoted during this one. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/arxiv-2608-00009-agentmembench.md b/research/agent-memory-recency-tracking/arxiv-2608-00009-agentmembench.md new file mode 100644 index 0000000..9862a9a --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2608-00009-agentmembench.md @@ -0,0 +1,28 @@ +# AgentMemBench: A Systematic Benchmark for Evaluating Long-Term Memory Management Strategies in Conversational AI Agents + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | AgentMemBench: A Systematic Benchmark for Evaluating Long-Term Memory Management Strategies in Conversational AI Agents | +| Source URL | https://arxiv.org/pdf/2608.00009 | +| Source type | paper (benchmark) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "MemGPT Letta memory tiering LRU eviction agent context management"; the search synthesis noted this benchmark's MemGPT-style adapter uses "a bounded main-context FIFO + FAISS archival recall" — a useful implementation detail — but the dossier for this card was not independently expanded beyond that single attributed detail, which is recorded instead in `arxiv-2310-08560-memgpt.md`'s Tier 1 dossier. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/arxiv-2608-31057-agent-working-memory.md b/research/agent-memory-recency-tracking/arxiv-2608-31057-agent-working-memory.md new file mode 100644 index 0000000..3090570 --- /dev/null +++ b/research/agent-memory-recency-tracking/arxiv-2608-31057-agent-working-memory.md @@ -0,0 +1,28 @@ +# Measure Before You Manage: Evaluating Agent Working Memory in Coding Agents + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Measure Before You Manage: Evaluating Agent Working Memory in Coding Agents | +| Source URL | https://arxiv.org/pdf/2608.31057 | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LLM agent memory recency importance relevance retrieval scoring generative agents paper"; relevant to coding-agent working memory specifically, which overlaps with tai's own domain (a coding agent), but not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/atlantis-press-recency-frequency-policy.md b/research/agent-memory-recency-tracking/atlantis-press-recency-frequency-policy.md new file mode 100644 index 0000000..d82f01c --- /dev/null +++ b/research/agent-memory-recency-tracking/atlantis-press-recency-frequency-policy.md @@ -0,0 +1,28 @@ +# An Improved Adaptive Policy Based on Recency and Frequency (Atlantis Press) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | An Improved Adaptive Policy Based on Recency and Frequency | +| Source URL | https://www.atlantis-press.com/article/21835.pdf | +| Source type | paper (conference proceedings, AMEII 2015) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LRFU combined recency frequency cache replacement formula least recently frequently used"; per the search synthesis, this 2015 paper proposes an adaptive variant of LRFU that lets the recency/frequency tradeoff parameter (lambda) adjust dynamically rather than staying fixed, addressing the main criticism of classic LRFU. A second, related paper from the same Atlantis Press proceedings (`https://www.atlantis-press.com/article/21834.pdf`) also surfaced and is treated as the same source family, logged under this one card. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/blackglory-sqlite-fts5-bm25-note.md b/research/agent-memory-recency-tracking/blackglory-sqlite-fts5-bm25-note.md new file mode 100644 index 0000000..df7bbbd --- /dev/null +++ b/research/agent-memory-recency-tracking/blackglory-sqlite-fts5-bm25-note.md @@ -0,0 +1,28 @@ +# bm25 (BlackGlory's digital garden — SQLite FTS5 auxiliary function note) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | bm25 (auxiliary function note) | +| Source URL | https://blackglory.me/notes/sqlite/SQLite3/FTS_Full-Text_Search/FTS5/Querying/Auxiliary_Functions/bm25?variant=en | +| Source type | other (personal notes/digital garden) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "boost BM25 FTS5 search ranking by recency last accessed hybrid scoring formula" as personal notes on FTS5's `bm25()` auxiliary function; closely related to the sign-convention point recorded at Tier 1 in `sqlite-org-bm25-sign-flip-commit.md`, but this specific note page was not independently opened. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/chiark-sqlite-datefunc-docs.md b/research/agent-memory-recency-tracking/chiark-sqlite-datefunc-docs.md new file mode 100644 index 0000000..8f01513 --- /dev/null +++ b/research/agent-memory-recency-tracking/chiark-sqlite-datefunc-docs.md @@ -0,0 +1,28 @@ +# lang_datefunc (chiark.greenend.org.uk mirror of SQLite docs) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | lang_datefunc | +| Source URL | https://www.chiark.greenend.org.uk/doc/sqlite3/lang_datefunc.html | +| Source type | vendor docs (third-party documentation mirror) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "SQLite last_accessed column unix timestamp vs datetime performance indexing" as a mirror of SQLite's official date-function documentation. Its specific technical point — that an INTEGER value needs the `'unixepoch'` modifier or SQLite's date functions will misread it as a Julian day number — is recorded directly in the `sqlite-forum-timestamp-integer-vs-text.md` Tier 1 dossier, sourced from the broader search synthesis rather than an independent read of this specific mirror page. A second mirror of the same documentation (sao.ru) also surfaced and is treated as the same underlying source. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/coddy-sqlite-date-and-time.md b/research/agent-memory-recency-tracking/coddy-sqlite-date-and-time.md new file mode 100644 index 0000000..db55a68 --- /dev/null +++ b/research/agent-memory-recency-tracking/coddy-sqlite-date-and-time.md @@ -0,0 +1,28 @@ +# SQLite date and time (Coddy.tech docs mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | date and time | +| Source URL | https://coddy.tech/docs/de/sqlite/date-and-time | +| Source type | vendor docs (third-party documentation mirror) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "SQLite last_accessed column unix timestamp vs datetime performance indexing"; its content (TEXT/REAL/INTEGER as the three date/time storage representations) is corroborated directly in the `sqlite-forum-timestamp-integer-vs-text.md` Tier 1 dossier, but this specific mirror page was not independently opened. A near-identical English-language and Japanese-language mirror of the same documentation also surfaced elsewhere in this subject's research and is treated as the same underlying source. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/codegarrettmills-caching-archive.md b/research/agent-memory-recency-tracking/codegarrettmills-caching-archive.md new file mode 100644 index 0000000..b335b98 --- /dev/null +++ b/research/agent-memory-recency-tracking/codegarrettmills-caching-archive.md @@ -0,0 +1,28 @@ +# Papers We Love — caching archive mirror (code.garrettmills.dev) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Papers We Love — caching directory (git archive mirror) | +| Source URL | https://code.garrettmills.dev/Archives/papers-we-love_papers-we-love/src/commit/23fff13692fb378bcac40e9ace7b7c1b0ec9f83d/caching | +| Source type | repository (git mirror) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "approximate LRU algorithm CLOCK 2Q low write overhead cache eviction database" as a mirrored git archive of the Papers We Love caching-papers directory. A second, near-identical commit of the same mirror also surfaced in the same result set and is treated as the same source, not filed separately. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/conclusions/last-used-tracking-proposal.md b/research/agent-memory-recency-tracking/conclusions/last-used-tracking-proposal.md new file mode 100644 index 0000000..826e222 --- /dev/null +++ b/research/agent-memory-recency-tracking/conclusions/last-used-tracking-proposal.md @@ -0,0 +1,61 @@ +# 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 INTEGER` column (Unix epoch seconds) to both the `records` and `chunks` tables. 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 INTEGER` column to `chunks`. `records` already has an existing `reads` counter 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_record` call, and (b) a record or chunk appearing in `search` results — a chunk hit should also propagate an update to its parent record's `last_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's `relatime` mount option and the real, documented GitLab `last_downloaded_at` production 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: `relatime` is 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 `UPDATE` on 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 `lazytime` approach). 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_count` columns. 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_count` should 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. | diff --git a/research/agent-memory-recency-tracking/deeplearningai-llms-as-os-course.md b/research/agent-memory-recency-tracking/deeplearningai-llms-as-os-course.md new file mode 100644 index 0000000..7f0f4be --- /dev/null +++ b/research/agent-memory-recency-tracking/deeplearningai-llms-as-os-course.md @@ -0,0 +1,28 @@ +# LLMs as Operating Systems: Agent Memory (DeepLearning.AI course) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | LLMs as Operating Systems: Agent Memory | +| Source URL | https://www.deeplearning.ai/courses/llms-as-operating-systems-agent-memory | +| Source type | vendor docs (course page) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "MemGPT Letta memory tiering LRU eviction agent context management" as a DeepLearning.AI course page covering the same OS-metaphor memory architecture as MemGPT/Letta; not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/devto-sudarshangouda-intelligent-forgetting.md b/research/agent-memory-recency-tracking/devto-sudarshangouda-intelligent-forgetting.md new file mode 100644 index 0000000..6163cab --- /dev/null +++ b/research/agent-memory-recency-tracking/devto-sudarshangouda-intelligent-forgetting.md @@ -0,0 +1,28 @@ +# AI Agent Memory Part 2: The Case for Intelligent Forgetting + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | AI Agent Memory Part 2: The Case for Intelligent Forgetting | +| Source URL | https://dev.to/sudarshangouda/ai-agent-memory-part-2-the-case-for-intelligent-forgetting-4i48 | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "Mem0 memory scoring decay forgetting curve AI agent long term memory"; a independent blog series entry on intelligent forgetting in agent memory, conceptually adjacent to the Mem0/YourMemory Tier 1 dossiers but not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/docsrs-kindling-provider.md b/research/agent-memory-recency-tracking/docsrs-kindling-provider.md new file mode 100644 index 0000000..de6f91c --- /dev/null +++ b/research/agent-memory-recency-tracking/docsrs-kindling-provider.md @@ -0,0 +1,28 @@ +# kindling-provider (docs.rs) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | kindling-provider | +| Source URL | https://docs.rs/kindling-provider | +| Source type | repository (Rust crate documentation) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "boost BM25 FTS5 search ranking by recency last accessed hybrid scoring formula" as a Rust crate's documentation page; relevance to the subject was not established — not opened or quoted during this pass, logged for completeness only. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/emergentmind-generative-agents.md b/research/agent-memory-recency-tracking/emergentmind-generative-agents.md new file mode 100644 index 0000000..b7ce1ac --- /dev/null +++ b/research/agent-memory-recency-tracking/emergentmind-generative-agents.md @@ -0,0 +1,28 @@ +# Generative Agents: Human-like AI Behaviors + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Generative Agents: Human-like AI Behaviors | +| Source URL | https://www.emergentmind.com/topics/generative-agents | +| Source type | vendor/aggregator topic page | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LLM agent memory recency importance relevance retrieval scoring generative agents paper" as an EmergentMind topic-overview page on the Generative Agents paper; potentially a useful secondary overview but not opened or quoted during this research pass — the primary paper (`arxiv-2304-03442-generative-agents.md`) was used instead. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/emergentmind-memgpt.md b/research/agent-memory-recency-tracking/emergentmind-memgpt.md new file mode 100644 index 0000000..ddca2b6 --- /dev/null +++ b/research/agent-memory-recency-tracking/emergentmind-memgpt.md @@ -0,0 +1,28 @@ +# MemGPT: OS-Inspired Memory Architecture + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | MemGPT: OS-Inspired Memory Architecture | +| Source URL | https://www.emergentmind.com/topics/memgpt | +| Source type | vendor/aggregator topic page | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "MemGPT Letta memory tiering LRU eviction agent context management" as an EmergentMind overview page; the primary MemGPT paper (`arxiv-2310-08560-memgpt.md`) was used as the Tier 1 source instead. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/frontiersin-generative-agents-psychology.md b/research/agent-memory-recency-tracking/frontiersin-generative-agents-psychology.md new file mode 100644 index 0000000..0b3cb74 --- /dev/null +++ b/research/agent-memory-recency-tracking/frontiersin-generative-agents-psychology.md @@ -0,0 +1,28 @@ +# Frontiers in Psychology — Generative agents article + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | (Frontiers in Psychology journal article, title not individually captured by this research pass) | +| Source URL | https://www.frontiersin.org/journals/psychology/articles/10.3389/fpsyg.2025.1591618/full | +| Source type | paper (journal article) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LLM agent memory recency importance relevance retrieval scoring generative agents paper" as a psychology-journal angle on generative agents; not opened or quoted during this pass, and even its own title was not individually captured in the search synthesis. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/github-yourmemory-ebbinghaus.md b/research/agent-memory-recency-tracking/github-yourmemory-ebbinghaus.md new file mode 100644 index 0000000..ade35d9 --- /dev/null +++ b/research/agent-memory-recency-tracking/github-yourmemory-ebbinghaus.md @@ -0,0 +1,48 @@ +# YourMemory — Agentic AI memory with Ebbinghaus forgetting curve decay + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | YourMemory: Agentic AI memory with Ebbinghaus forgetting curve decay | +| Source URL | https://github.com/sachitrafa/YourMemory | +| Source publisher / author | sachitrafa (GitHub, independent/open-source project) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +An open-source, smaller competitor project to Mem0 that implements agent memory aging explicitly on an Ebbinghaus forgetting-curve model, rather than Mem0's simpler linear-ish 0.3x–1.5x recency multiplier — and claims a measurable recall improvement over Mem0 on a named benchmark. + +## Key findings + +- Every memory is described as aging on an Ebbinghaus forgetting curve: stale, unused facts fade; important and frequently-recalled ones persist. +- The project claims "+16pp better recall than Mem0 on LoCoMo" (a named long-context-memory benchmark). + +## Pros / strengths + +- Represents a concrete, implemented alternative to Mem0's simpler decay curve, grounded in an actual cognitive-science forgetting model rather than an ad hoc multiplier range — useful as a second data point on "how might decay actually be shaped" if tai ever builds a ranking-integration phase. + +## Cons / limitations / caveats + +- The "+16pp better recall than Mem0" figure is the competing project's own self-reported claim, on its own chosen benchmark, with no independent verification found during this research pass. It should be treated as a marketing/positioning claim, not a verified result, until independently reproduced. +- Smaller, less established project than Mem0 or Letta — lower confidence that its described behavior matches real-world production experience at scale. + +## Relevance to tai + +A second, independent example (alongside Mem0) that agent-memory products are converging on recency-as-decay-curve for ranking rather than for eviction — reinforces, but does not change, the subject's conclusion that tai's current proposal should stop at raw tracking and defer any decay-curve decision. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/gitlab-last-downloaded-at-write-amplification.md b/research/agent-memory-recency-tracking/gitlab-last-downloaded-at-write-amplification.md new file mode 100644 index 0000000..3d70999 --- /dev/null +++ b/research/agent-memory-recency-tracking/gitlab-last-downloaded-at-write-amplification.md @@ -0,0 +1,51 @@ +# S16 follow-up: three open surfaces of the last_downloaded_at write-amplification argument + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | S16 follow-up: three open surfaces of the last_downloaded_at write-amplification argument | +| Source URL | https://gitlab.com/gitlab-org/ops/artifact-registry/-/work_items/680 | +| Source publisher / author | GitLab (internal engineering work item, publicly visible) | +| Source type | other (production engineering issue tracker entry) | +| Source date (published) | Unknown — not stated (exact work item date not captured by this research pass) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A real, named production incident write-up from GitLab's own artifact registry engineering work, documenting the exact failure mode this subject's conclusion is designed to avoid: an unconditional "touch" update on a hot read path causing serious database write amplification — with a concrete, quantified load estimate and GitLab's own fix. + +## Key findings + +- The `last_downloaded_at` column in question is part of a btree index, so updating it cannot be satisfied by a heap-only-tuple (HOT) update — every bump therefore creates a new row version *and* a new entry in every index the row belongs to. +- Concrete load estimate given: a 10-layer container image pulled 100 times a minute, with every layer resolving to the same underlying row, works out to roughly 17 row versions per second plus 51 index entries per second on that one row — from what looks like "just incrementing a timestamp." +- GitLab's actual fix was a throttled update: the write is skipped based on the freshness of the row's own existing timestamp, so repeated reads within the throttle window are no-ops at the database layer. +- The work item notes one of the two write routes was throttled this way while a second (blob reads, which have no manifest row to check against) was not yet fixed at time of writing — i.e. even GitLab's own fix was partial/in-progress, not a universally complete solution captured in this source. + +## Pros / strengths + +- This is a real, quantified, named production incident — not a theoretical cache paper — making it the single most concrete evidence in this entire subject that "just add a last_used column and UPDATE it on every read" is a genuine, not hypothetical, performance risk. +- The fix GitLab actually applied (throttle based on the row's own existing timestamp) is structurally identical to the pattern this subject's conclusion recommends for tai (`UPDATE ... WHERE last_used IS NULL OR last_used < ?`). + +## Cons / limitations / caveats + +- This is PostgreSQL-specific in its mechanics (HOT updates, btree index versioning) — SQLite (tai's actual database) has different internals, so the exact row-version/index-entry multiplication described here does not transfer number-for-number. The general lesson (indexed, frequently-touched timestamp columns cause real write amplification) does transfer; the specific "17 row versions/sec, 51 index entries/sec" figures are PostgreSQL-specific illustrations, not a universal constant. +- The source itself documents an incomplete, in-progress fix (one route throttled, one not) — it should not be read as "GitLab solved this cleanly," only as "GitLab hit this problem and applied a partial, same-shaped fix." + +## Relevance to tai + +This is the primary real-world justification, cited directly in this subject's conclusion, for why tai's `last_used` column must never be updated unconditionally on every read/hit, and for why the recommended fix is a conditional/throttled UPDATE rather than a naive touch-on-every-access implementation. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/glama-claude-memory-mcp-semantic-search.md b/research/agent-memory-recency-tracking/glama-claude-memory-mcp-semantic-search.md new file mode 100644 index 0000000..dc49c28 --- /dev/null +++ b/research/agent-memory-recency-tracking/glama-claude-memory-mcp-semantic-search.md @@ -0,0 +1,28 @@ +# claude-memory-mcp — semantic-search.ts (Glama MCP server listing) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | semantic search (claude-memory-mcp) | +| Source URL | https://glama.ai/mcp/servers/@WhenMoon-afk/claude-memory-mcp/blob/5f8d26e416fc516efffb63d3efffe7d00a7c17cc/src/search/semantic-search.ts | +| Source type | repository (hosted source blob) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "boost BM25 FTS5 search ranking by recency last accessed hybrid scoring formula" as an MCP memory server's semantic-search implementation file; not opened or quoted during this pass. The specific numeric hybrid-fusion details used in this subject's research came from a different result in the same search, recorded at Tier 1 in `glama-code-index-mcp-bm25-hybrid.md`'s sibling card below. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/glama-code-index-mcp-bm25-hybrid.md b/research/agent-memory-recency-tracking/glama-code-index-mcp-bm25-hybrid.md new file mode 100644 index 0000000..d870df1 --- /dev/null +++ b/research/agent-memory-recency-tracking/glama-code-index-mcp-bm25-hybrid.md @@ -0,0 +1,48 @@ +# BM25 Hybrid Search Implementation Summary (Code-Index-MCP) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | BM25 Hybrid Search Implementation Summary | +| Source URL | https://glama.ai/mcp/servers/@ViperJuice/Code-Index-MCP/blob/e3183d0106c586aec03535faabe632b4db4d6046/docs/implementation/BM25_HYBRID_SEARCH_IMPLEMENTATION.md | +| Source publisher / author | ViperJuice (Code-Index-MCP project) | +| Source type | repository (implementation documentation) | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +An implementation-documentation file from an MCP code-indexing server describing how it normalizes raw BM25 scores and fuses them with semantic/vector scores for hybrid search. Promoted to Tier 1 (rather than the plain citation-card treatment of its sibling results in the same search) because the research synthesis attributed specific, concrete numeric details to this exact document, meeting the "independently analyzed" bar the Tier 1/Tier 2 distinction is built on. + +## Key findings + +- Normalizes raw BM25 rank to a [0,1] scale via a linear, clamped mapping: a raw rank of 0 maps to 1.0 (best), -5 maps to 0.5, and -10-or-worse clamps to 0.0; positive values are clamped to 1.0. +- Fuses keyword (BM25) and semantic (vector) scores as a weighted linear sum, with stated defaults of 0.3 (keyword) and 0.7 (semantic), configurable via custom weights. + +## Pros / strengths + +- Gives a concrete, already-implemented example of score normalization for BM25 specifically (not just the sign-convention point from `sqlite-org-bm25-sign-flip-commit.md`, but an actual usable clamp-and-scale formula) — directly reusable reference if tai ever builds a phase that blends `last_used`/`use_count` into search ranking alongside BM25. + +## Cons / limitations / caveats + +- The specific clamp thresholds (0, -5, -10) are this project's own tuning, calibrated to its own corpus and BM25 configuration — not a universal constant, and not verified against tai's own `fts_docs` table's actual score distribution. +- This dossier is based on the search tool's synthesis of the document, not a direct fetch and read of the full file — treat the two numeric details above as reliably attributed, but do not assume the rest of the document's content beyond what's stated here. + +## Relevance to tai + +A candidate reference implementation for a future (explicitly out-of-scope-for-now) phase where `last_used`/`use_count` get blended into tai's existing `bm25(fts_docs)`-based ranking — not used in the current tracking-only proposal, but recorded so that future phase doesn't start from zero. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created (promoted to Tier 1 on authoring, since specific attributable numeric findings were available, rather than filed as a bare Tier 2 citation card). | diff --git a/research/agent-memory-recency-tracking/glama-gnosis-mcp-sqlite-backend.md b/research/agent-memory-recency-tracking/glama-gnosis-mcp-sqlite-backend.md new file mode 100644 index 0000000..f0aff8f --- /dev/null +++ b/research/agent-memory-recency-tracking/glama-gnosis-mcp-sqlite-backend.md @@ -0,0 +1,28 @@ +# gnosis-mcp — SQLite backend (aiosqlite + FTS5 + optional sqlite-vec) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | SQLite backend using aiosqlite + FTS5 + optional sqlite-vec for hybrid search | +| Source URL | https://glama.ai/mcp/servers/@nicholasglazer/gnosis-mcp/blob/c98bf9ef8f3067a6a63b78e3c3ff9c126d52042c/src/gnosis_mcp/sqlite_backend.py | +| Source type | repository (hosted source blob) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "boost BM25 FTS5 search ranking by recency last accessed hybrid scoring formula" as another MCP server using an aiosqlite+FTS5+optional-sqlite-vec hybrid backend — architecturally similar in spirit to the `sqlite-rag-hybrid-search` subject's own research, but not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/glama-mcp-memory-service-hybrid-test.md b/research/agent-memory-recency-tracking/glama-mcp-memory-service-hybrid-test.md new file mode 100644 index 0000000..d74a2b6 --- /dev/null +++ b/research/agent-memory-recency-tracking/glama-mcp-memory-service-hybrid-test.md @@ -0,0 +1,28 @@ +# mcp-memory-service — test_hybrid_search.py (Glama MCP server listing) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | test hybrid search | +| Source URL | https://glama.ai/mcp/servers/@doobidoo/mcp-memory-service/blob/8f6d4f397e1c5cc9cd94234732635810bb972a4a/tests/storage/test_hybrid_search.py | +| Source type | repository (test suite file) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "boost BM25 FTS5 search ranking by recency last accessed hybrid scoring formula" as another MCP memory server's hybrid-search test file; not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/hipc-mflru-poster.md b/research/agent-memory-recency-tracking/hipc-mflru-poster.md new file mode 100644 index 0000000..a7f48ba --- /dev/null +++ b/research/agent-memory-recency-tracking/hipc-mflru-poster.md @@ -0,0 +1,28 @@ +# MFLRU Replacement (HiPC 2002 poster) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | MFLRU Replacement | +| Source URL | https://www.hipc.org/hipc2002/2002Posters/MFLRU_Replacement.pdf | +| Source type | paper (conference poster) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LRFU combined recency frequency cache replacement formula least recently frequently used" as a 2002 HiPC conference poster on an MFLRU (Most-Frequent-Least-Recently-Used, per name inference) replacement scheme; not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/letta-agent-memory-blog.md b/research/agent-memory-recency-tracking/letta-agent-memory-blog.md new file mode 100644 index 0000000..5bf5093 --- /dev/null +++ b/research/agent-memory-recency-tracking/letta-agent-memory-blog.md @@ -0,0 +1,51 @@ +# Agent Memory: How to Build Agents That Learn and Remember + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Agent Memory: How to Build Agents That Learn and Remember | +| Source URL | https://www.letta.com/blog/agent-memory/ | +| Source publisher / author | Letta (vendor blog) | +| Source type | blog post / vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Letta (the company that productized MemGPT's ideas) describes its current tiered memory model: Core (in-context, RAM-analogue), Recall (cache-analogue), Archival (cold-storage analogue), with the model itself responsible for paging decisions through explicit tool calls. + +## Key findings + +- Tiers are explicitly analogized to a memory hierarchy: Core (RAM) / Recall (cache) / Archival (cold). +- The LLM itself manages memory autonomously via function calls such as `search_memory` and `add_to_working`, rather than an external, opaque eviction process. +- Retrieval uses a hybrid of embedding similarity and recency-based scoring — recency plays a role in *ranking* results, but the source material does not tie it to any formal LRU *eviction* rule; eviction and ranking are presented as separate concerns. +- A separate 2026-dated survey (per the research synthesis) describes newer Letta features: developer-defined memory blocks, automatic compaction of older messages, and experimental "sleep-time" agents that process memory in the background between interactions. + +## Pros / strengths + +- Clear, current (vendor-maintained) articulation of a three-tier memory hierarchy that other agent-memory systems implicitly reference. +- Explicitly separates "how do we rank what to retrieve" (recency+embeddings) from "how do we decide what to evict/compact" (the model's own tool calls, or background "sleep-time" compaction) — a clean separation tai's own design should probably preserve. + +## Cons / limitations / caveats + +- It is vendor marketing content, not an independent technical analysis or paper — claims about Letta's internals should be treated as the vendor's own self-description, not independently verified. +- "Recency-based scoring" is mentioned without a documented formula; this dossier cannot cite an exact decay function because the source did not state one in the material the search tool returned. + +## Relevance to tai + +Reinforces the pattern (also seen in MemGPT and Mem0) of keeping ranking and eviction/compaction as separate mechanisms, and of letting recency inform ranking without being the sole driver of deletion — consistent with this subject's conclusion to track `last_used`/`use_count` now while explicitly deferring any eviction or ranking-integration decision. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/linguanguo-letta-research-notes.md b/research/agent-memory-recency-tracking/linguanguo-letta-research-notes.md new file mode 100644 index 0000000..66eaeb5 --- /dev/null +++ b/research/agent-memory-recency-tracking/linguanguo-letta-research-notes.md @@ -0,0 +1,28 @@ +# Letta - LLM Agent Research (notes) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Letta - LLM Agent Research | +| Source URL | https://lin-guanguo.github.io/llm-memory-research/letta.research/ | +| Source type | blog post (personal research notes) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "MemGPT Letta memory tiering LRU eviction agent context management" as an individual researcher's personal notes on Letta; not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/lwn-atime-kernel-article.md b/research/agent-memory-recency-tracking/lwn-atime-kernel-article.md new file mode 100644 index 0000000..ce3c194 --- /dev/null +++ b/research/agent-memory-recency-tracking/lwn-atime-kernel-article.md @@ -0,0 +1,28 @@ +# Once Upon Atime (LWN.net kernel article) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Once Upon Atime | +| Source URL | https://lwn.net/Articles/244829 | +| Source type | other (kernel-development news article) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "Linux relatime lazytime atime update reduce write overhead filesystem technique" as LWN's own historical coverage of the atime/relatime kernel discussion; the Red Hat documentation (`redhat-relatime-drive-access.md`) was used as the Tier 1 source for the actual relatime/lazytime mechanics instead of this article, which was not independently opened. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/medium-artgor-generative-agents-review.md b/research/agent-memory-recency-tracking/medium-artgor-generative-agents-review.md new file mode 100644 index 0000000..8112a57 --- /dev/null +++ b/research/agent-memory-recency-tracking/medium-artgor-generative-agents-review.md @@ -0,0 +1,48 @@ +# Paper Review: Generative Agents: Interactive Simulacra of Human Behavior + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Paper Review: Generative Agents: Interactive Simulacra of Human Behavior | +| Source URL | https://artgor.medium.com/paper-review-generative-agents-interactive-simulacra-of-human-behavior-cc5f8294b4ac | +| Source publisher / author | artgor (Medium) | +| Source type | blog post (secondary review) | +| Source date (published) | Unknown — not stated; the search tool flagged this piece as roughly 1,150 days old relative to the search date, i.e. a multi-year-old secondary source | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A secondary review/summary of the Generative Agents paper (see `arxiv-2304-03442-generative-agents.md`), useful mainly as corroboration of the recency/importance/relevance retrieval-score structure from an independent reader rather than as a primary source in its own right. + +## Key findings + +- Corroborates the three-part retrieval score (recency, importance, relevance) and the normalize-then-combine framing. +- Does not appear to add technical detail beyond the original paper; its value here is as a second, independent restatement that agrees with the primary source. + +## Pros / strengths + +- Independent confirmation reduces the risk that the primary dossier's synthesis of the original paper was a search-tool misreading. + +## Cons / limitations / caveats + +- Multi-year-old secondary commentary; the field has moved substantially since (see the Mem0 and MemGPT/Letta dossiers in this same subject), so anything in this review not corroborated by the primary paper or by current vendor documentation should be treated as dated. +- Adds no new findings beyond the primary source. + +## Relevance to tai + +Low standalone relevance — included for completeness and cross-checking, not as an independent input to the conclusion in this subject. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/medium-piyush-letta-memgpt-deepdive.md b/research/agent-memory-recency-tracking/medium-piyush-letta-memgpt-deepdive.md new file mode 100644 index 0000000..a12b8e4 --- /dev/null +++ b/research/agent-memory-recency-tracking/medium-piyush-letta-memgpt-deepdive.md @@ -0,0 +1,28 @@ +# Stateful AI Agents: A Deep Dive into Letta (MemGPT) Memory Models + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Stateful AI Agents: A Deep Dive into Letta (MemGPT) Memory Models | +| Source URL | https://medium.com/@piyush.jhamb4u/stateful-ai-agents-a-deep-dive-into-letta-memgpt-memory-models-a2ffc01a7ea1 | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "MemGPT Letta memory tiering LRU eviction agent context management" as an independent deep-dive blog post; the Letta vendor blog and MemGPT paper were used as the Tier 1 sources instead of this secondary writeup. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/mem0-ai-memory-management-4-layers.md b/research/agent-memory-recency-tracking/mem0-ai-memory-management-4-layers.md new file mode 100644 index 0000000..9e972e8 --- /dev/null +++ b/research/agent-memory-recency-tracking/mem0-ai-memory-management-4-layers.md @@ -0,0 +1,28 @@ +# AI Memory Management: 4 Layers & Production Benchmarks (Mem0) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | AI Memory Management: 4 Layers & Production Benchmarks | +| Source URL | https://mem0.ai/blog/ai-memory-management-for-llms-and-agents | +| Source type | blog post / vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "Mem0 memory scoring decay forgetting curve AI agent long term memory" as a broader Mem0 blog post on a "4 layers" memory-management framework; not opened or quoted independently during this pass, with the decay-specific and eviction-specific posts used as Tier 1 sources instead. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/mem0-context-engineering-memory-decay.md b/research/agent-memory-recency-tracking/mem0-context-engineering-memory-decay.md new file mode 100644 index 0000000..3a50b3f --- /dev/null +++ b/research/agent-memory-recency-tracking/mem0-context-engineering-memory-decay.md @@ -0,0 +1,28 @@ +# Agent Memory Staleness: How Recency-Aware Ranking Fixes Retrieval Drift (Mem0) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Agent Memory Staleness: How Recency-Aware Ranking Fixes Retrieval Drift | +| Source URL | https://mem0.ai/library/context-engineering/memory-decay-for-long-running-agents-how-recency-aware-ranking-fixes-retrieval-staleness | +| Source type | blog post / vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "Mem0 memory scoring decay forgetting curve AI agent long term memory" as a context-engineering-focused Mem0 library article specifically about recency-aware ranking and retrieval staleness — closely related to the decay mechanism covered at Tier 1 in `mem0-introducing-memory-decay.md`, but this specific article was not independently opened during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/mem0-how-to-create-long-term-memory.md b/research/agent-memory-recency-tracking/mem0-how-to-create-long-term-memory.md new file mode 100644 index 0000000..dcbd052 --- /dev/null +++ b/research/agent-memory-recency-tracking/mem0-how-to-create-long-term-memory.md @@ -0,0 +1,28 @@ +# How to create AI agents with long-term memory (Mem0) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | How to create AI agents with long-term memory | +| Source URL | https://mem0.ai/blog/how-to-create-ai-agents-with-long-term-memory | +| Source type | blog post / vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "Mem0 memory scoring decay forgetting curve AI agent long term memory" as a how-to-oriented blog post; not opened or quoted independently during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/mem0-introducing-memory-decay.md b/research/agent-memory-recency-tracking/mem0-introducing-memory-decay.md new file mode 100644 index 0000000..b72465c --- /dev/null +++ b/research/agent-memory-recency-tracking/mem0-introducing-memory-decay.md @@ -0,0 +1,51 @@ +# Introducing Memory Decay in Mem0 + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Introducing Memory Decay in Mem0 | +| Source URL | https://mem0.ai/blog/introducing-memory-decay-in-mem0 | +| Source publisher / author | Mem0 (vendor blog) | +| Source type | blog post / vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Mem0's "Memory Decay" feature is a search-time re-ranking multiplier based on recency since last access — not a deletion/forgetting mechanism. It is the closest thing in current commercial agent-memory products to what tai is being asked to build the tracking substrate for. + +## Key findings + +- Each memory receives a recency-based scaling factor between 0.3x and 1.5x: recently accessed memories get boosted up to 1.5x, idle ones drift down toward a 0.3x floor. +- Nothing is ever deleted by this mechanism — the 0.3x floor means an idle-but-genuinely-relevant memory can still surface for the right query, just lower in the ranking. +- Access to a memory resets/raises its decay score — i.e. "used" directly and immediately affects future ranking. +- Mem0's own A/B test is reported to have moved the correct memory to rank 1 in 3 of 4 test cases, and widened score separation from a ~0.0001 margin to a 0.15+ gap. + +## Pros / strengths + +- Exactly the "soft re-ranking, not hard eviction" pattern this subject's conclusion recommends tai consider for a *future* phase (explicitly not being built now) — recording this now means the next phase doesn't have to rediscover this precedent. +- The 0.3x–1.5x framing is simple enough to reason about and reimplement if/when tai decides to build a ranking-integration phase. + +## Cons / limitations / caveats + +- The "3 of 4 test cases" and "0.15+ gap" figures are vendor-reported, from a vendor's own blog post, on an unstated and likely small sample size — this is marketing evidence, not an independently reproduced benchmark, and should not be treated as a general guarantee. +- No exact decay formula (e.g. half-life, decay constant) is disclosed in the material available to this research pass — the mechanism's existence and rough behavior are known; its precise math is not. + +## Relevance to tai + +Directly relevant precedent for a possible future phase of tai's `last_used`/`use_count` work (ranking integration), explicitly out of scope for the current proposal, which is tracking-only. Cited in the subject's conclusion document as the main real-world analogue for "recency as a soft ranking signal, not a deletion trigger." + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/mem0-library-guides.md b/research/agent-memory-recency-tracking/mem0-library-guides.md new file mode 100644 index 0000000..f03fe3e --- /dev/null +++ b/research/agent-memory-recency-tracking/mem0-library-guides.md @@ -0,0 +1,28 @@ +# AI Agent Memory Library: Guides & Tutorials (Mem0) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | AI Agent Memory Library: Guides & Tutorials | +| Source URL | https://mem0.ai/library | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "Mem0 memory scoring decay forgetting curve AI agent long term memory" as Mem0's general guide/tutorial library index page; the specific decay and eviction blog posts were used as Tier 1 sources instead of this general index. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/mem0-memory-eviction-forgetting.md b/research/agent-memory-recency-tracking/mem0-memory-eviction-forgetting.md new file mode 100644 index 0000000..a6f7e0b --- /dev/null +++ b/research/agent-memory-recency-tracking/mem0-memory-eviction-forgetting.md @@ -0,0 +1,50 @@ +# Memory eviction and forgetting in AI agents + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Memory eviction and forgetting in AI agents | +| Source URL | https://mem0.ai/blog/memory-eviction-and-forgetting-in-ai-agents | +| Source publisher / author | Mem0 (vendor blog) | +| Source type | blog post / vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Mem0's broader guidance on eviction/forgetting beyond the Memory Decay feature specifically: it distinguishes soft re-ranking (decay) from real pruning, and recommends TTL policies plus explicit deletion APIs for genuine forgetting, with tiered memory lifetimes by scope. + +## Key findings + +- Session memory is described as persisting minutes-to-hours and being cleared explicitly; user- and organization-level memory are long-lived by contrast. +- For episodic data specifically, the guidance is to use timestamps plus explicit TTL policies rather than relying on decay alone. +- The source explicitly states memory systems need deletion APIs and UX patterns that expose what is remembered — i.e. forgetting should be an observable, intentional operation, not an opaque side effect. +- Decay (see `mem0-introducing-memory-decay.md`) is characterized here as "decays low-relevance entries over time, freeing up space and attention" — a description, not a documented formula. + +## Pros / strengths + +- Clearly separates three distinct concerns that are easy to conflate: ranking (decay), lifecycle/expiry (TTL), and user-visible deletion (explicit API) — a useful three-way framework for tai to keep in mind even though only the tracking substrate for the first concern is being built right now. + +## Cons / limitations / caveats + +- Lacks a documented formula for its own decay mechanism (consistent with the companion decay-announcement post); this source is guidance-level, not implementation-level detail. +- Vendor content, not independently verified. + +## Relevance to tai + +Useful for correctly scoping what tai's `last_used`/`use_count` proposal is and is not: it is tracking substrate that could later feed a decay-style ranking signal (per Mem0's own distinction), but it is explicitly not a TTL/expiry mechanism and not a deletion mechanism — those would be separate, later decisions if ever made. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/mem0-memory-in-agents-guide.md b/research/agent-memory-recency-tracking/mem0-memory-in-agents-guide.md new file mode 100644 index 0000000..ddfdafd --- /dev/null +++ b/research/agent-memory-recency-tracking/mem0-memory-in-agents-guide.md @@ -0,0 +1,28 @@ +# AI Agent Memory: Complete Guide & Architecture (Mem0) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | AI Agent Memory: Complete Guide & Architecture | +| Source URL | https://mem0.ai/blog/memory-in-agents-what-why-and-how | +| Source type | blog post / vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "Mem0 memory scoring decay forgetting curve AI agent long term memory" as a general architecture-overview blog post; not opened or quoted independently during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/paperswelove-2q-paper.md b/research/agent-memory-recency-tracking/paperswelove-2q-paper.md new file mode 100644 index 0000000..8e427db --- /dev/null +++ b/research/agent-memory-recency-tracking/paperswelove-2q-paper.md @@ -0,0 +1,28 @@ +# 2Q: A Low Overhead High Performance Buffer Management Replacement Algorithm (Papers We Love) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | 2Q: A Low Overhead High Performance Buffer Management Replacement Algorithm | +| Source URL | https://paperswelove.org/papers/2q-a-low-overhead-high-performance-buffer-manageme-98fa2c32/ | +| Source type | paper (hosted copy of the original Johnson & Shasha 1994 VLDB paper) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "approximate LRU algorithm CLOCK 2Q low write overhead cache eviction database" as the original primary paper behind the 2Q algorithm already summarized at Tier 1 via `arpit-2q-cache-algorithm.md`'s explainer; this is the primary document itself, logged here rather than promoted to Tier 1 because this research pass worked from the explainer, not a direct read of the original paper's full text. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/redhat-relatime-drive-access.md b/research/agent-memory-recency-tracking/redhat-relatime-drive-access.md new file mode 100644 index 0000000..6f4a61f --- /dev/null +++ b/research/agent-memory-recency-tracking/redhat-relatime-drive-access.md @@ -0,0 +1,51 @@ +# Relatime Drive Access Optimization + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Relatime Drive Access Optimization | +| Source URL | https://docs.redhat.com/en/documentation/Red_Hat_Enterprise_Linux/7/html/power_management_guide/relatime | +| Source publisher / author | Red Hat (official product documentation) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated (RHEL 7 era documentation) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +Official Red Hat documentation of the Linux `relatime` mount option, which is the single clearest, most battle-tested real-world precedent found in this entire subject for "track last-access time without writing on every single access." `relatime` has been Linux's default mount behavior since kernel 2.6.20. + +## Key findings + +- The underlying problem: maintaining atime (access time) traditionally requires a write to storage on every read, even when the read itself was satisfied entirely from page cache — turning cheap reads into disk writes purely for bookkeeping. +- `noatime` disables access-time tracking entirely, but this can break software that depends on it (some mail clients comparing mtime/atime to determine unread status, `tmpwatch`, some backup tools). +- `relatime` (the default since kernel 2.6.20) is the middle ground: atime is only written to disk if the file has been modified since atime was last updated, OR if the existing atime is older than a threshold (default: one day). +- `lazytime` goes further: atime/mtime/ctime updates are kept purely in memory and only flushed to disk on a non-time-related change, an explicit `fsync`/`syncfs`/`sync` call, or inode eviction — acceptable under POSIX specifically because there's no crash-durability guarantee for a value without an explicit fsync. + +## Pros / strengths + +- This is a genuinely load-bearing, decades-battle-tested real-world precedent, not a theoretical cache-algorithm paper — Linux runs this exact tradeoff on essentially every filesystem read on the planet. +- Gives two distinct, usable patterns at two different levels of aggressiveness: `relatime`'s "write only if stale" rule maps directly onto a conditional SQL `UPDATE ... WHERE last_used < threshold`; `lazytime`'s "batch in memory, flush later" maps onto an in-process batching layer if the simpler approach ever proves insufficient. + +## Cons / limitations / caveats + +- This is filesystem-level precedent, not database-level — the exact mechanics (inode eviction, fsync semantics) don't transfer literally to a SQLite table, only the *principle* (don't write on every read; write only when stale, or batch in memory) transfers. +- The one-day default staleness threshold for `relatime` is tuned for filesystem atime semantics and general-purpose workloads; it is not necessarily the right throttle window for tai's `last_used` tracking, which this subject's conclusion separately estimates at roughly one minute as a starting point — that number is this project's own guess, not something `relatime` itself recommends. + +## Relevance to tai + +This is the primary justification, cited directly in this subject's conclusion, for using a throttled/conditional-UPDATE pattern (`UPDATE ... WHERE last_used IS NULL OR last_used < ?`) instead of an unconditional write on every hit — the exact same engineering tradeoff Linux already made, battle-tested at a far larger scale than tai will ever need. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/rmarcus-dbscholar-clock2q.md b/research/agent-memory-recency-tracking/rmarcus-dbscholar-clock2q.md new file mode 100644 index 0000000..59dbd9c --- /dev/null +++ b/research/agent-memory-recency-tracking/rmarcus-dbscholar-clock2q.md @@ -0,0 +1,28 @@ +# rmarcus.info Database Paper Browser — Clock2Q+ entry + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Database Paper Browser (rmarcus.info dbscholar) | +| Source URL | https://rmarcus.info/dbscholar/papers/hc626719af823404a | +| Source type | other (paper-browser/aggregator entry) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "approximate LRU algorithm CLOCK 2Q low write overhead cache eviction database" as an entry in Ryan Marcus's personal database-paper browser tool, pointing to Clock2Q+-related material. Two further rmarcus.info paper-browser pages (`https://rmarcus.info/dbscholar/papers/8213` and `https://rmarcus.info/dbscholar/papers/he7c2c6333f9a3929`) also surfaced during the same and related searches and are treated as the same source family — logged under this one card rather than filed separately, since all three are the same aggregator tool pointing at adjacent database-paper-browser entries rather than independently distinct content. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/sitepoint-integer-created-at-column.md b/research/agent-memory-recency-tracking/sitepoint-integer-created-at-column.md new file mode 100644 index 0000000..85c7361 --- /dev/null +++ b/research/agent-memory-recency-tracking/sitepoint-integer-created-at-column.md @@ -0,0 +1,28 @@ +# Is there advantage of using Integer in table column created_at? (SitePoint forum) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Is there advantage of using Integer in table column created_at? | +| Source URL | https://www.sitepoint.com/community/t/is-there-advantage-of-using-integer-in-table-column-created-at/343308 | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "SQLite last_accessed column unix timestamp vs datetime performance indexing" as a directly on-topic forum thread (integer vs. other types for a timestamp column); not opened or quoted independently during this pass beyond what's already captured in the Tier 1 dossier for the primary SQLite forum source. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/sqlite-forum-second-thread-datetime.md b/research/agent-memory-recency-tracking/sqlite-forum-second-thread-datetime.md new file mode 100644 index 0000000..3f9b4a8 --- /dev/null +++ b/research/agent-memory-recency-tracking/sqlite-forum-second-thread-datetime.md @@ -0,0 +1,28 @@ +# SQLite Forum — second datetime-related thread + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | (SQLite Forum thread, title not individually captured) | +| Source URL | https://sqlite.org/forum/info/5203f28a039a028754fda31591bbfb3c9ca9a949277cb45d789a2f78aecbd52f?t=c | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "SQLite last_accessed column unix timestamp vs datetime performance indexing" as a second, distinct SQLite forum thread from the one used as the Tier 1 source (`sqlite-forum-timestamp-integer-vs-text.md`); not opened or quoted independently during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/sqlite-forum-timestamp-integer-vs-text.md b/research/agent-memory-recency-tracking/sqlite-forum-timestamp-integer-vs-text.md new file mode 100644 index 0000000..525a943 --- /dev/null +++ b/research/agent-memory-recency-tracking/sqlite-forum-timestamp-integer-vs-text.md @@ -0,0 +1,50 @@ +# SQLite Forum: INTEGER vs TEXT timestamp storage discussion + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | SQLite Forum post on date/time storage representations | +| Source URL | https://sqlite.org/forum/forumpost/92d9dbc02808629a?raw | +| Source publisher / author | SQLite official forum (community post, unnamed individual poster) | +| Source type | forum thread | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A SQLite community forum discussion on the tradeoffs between storing timestamps as Unix-epoch INTEGER versus ISO-8601 TEXT. Explicitly an opinion/forum post, not a controlled benchmark — the search tool itself flagged this distinction, which this dossier preserves. + +## Key findings + +- SQLite has no dedicated datetime column type; it natively supports three representations: TEXT (`YYYY-MM-DD HH:MM:SS.SSS`), REAL (Julian day number), and INTEGER (Unix time). +- The forum poster's argument for integers: faster indexed range queries (`BETWEEN`/`>=`/`<=` directly on the integer index), no text parsing needed, smaller storage footprint than ISO-8601 text. +- The poster's argument is explicitly their own opinion, not a measured benchmark, per the search synthesis of this thread. +- Integer Unix timestamps are unambiguous across daylight-saving-time transitions in a way that naive local-time text values are not (a correctness point, separate from the raw-speed point). +- A counterpoint noted in the broader discussion: ISO-8601 text sorts correctly as a plain string and remains the most human-readable choice; its index entries are simply larger, not functionally broken. + +## Pros / strengths + +- Directly surfaces both sides of exactly the tradeoff this subject's conclusion needed to resolve (integer vs. text for a `last_used` column), from a venue (SQLite's own community forum) with direct relevant expertise. + +## Cons / limitations / caveats + +- Self-admittedly opinion-based, not a controlled benchmark — no actual timing numbers were produced in the material reviewed. The practical recommendation this subject reaches (use INTEGER for `last_used` specifically, because it's a hot, comparison-heavy column, while leaving the vault's other timestamp columns as ISO-8601 TEXT) is a judgment call informed by this thread's reasoning, not a number verified against tai's actual workload. + +## Relevance to tai + +Directly informs the one explicitly-flagged exception in this subject's conclusion: recommending `last_used` be stored as Unix-epoch INTEGER even though every other timestamp column in tai's vault schema is ISO-8601 TEXT, specifically because this column is compared/sorted/range-scanned on the hot path in a way the others generally are not. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/sqlite-org-bm25-sign-flip-commit.md b/research/agent-memory-recency-tracking/sqlite-org-bm25-sign-flip-commit.md new file mode 100644 index 0000000..877eb4c --- /dev/null +++ b/research/agent-memory-recency-tracking/sqlite-org-bm25-sign-flip-commit.md @@ -0,0 +1,47 @@ +# SQLite source history: bm25() sign-convention change + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | SQLite source/commit history entry concerning the bm25() auxiliary function | +| Source URL | https://sqlite.org/src/info/3ee7b5a9f987c269 | +| Source publisher / author | SQLite project (official source repository) | +| Source type | other (source control history entry) | +| Source date (published) | Unknown — not stated (historical commit; exact date not captured by this research pass) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A SQLite source-history record documenting that the `bm25()` FTS5 auxiliary ranking function's result is multiplied by -1, so that a *lower* (more negative) score means a *better* match — meaning correct usage requires `ORDER BY rank` ascending, not descending, which is the opposite of the intuitive "higher score = better" assumption. + +## Key findings + +- `bm25()` scores are inverted relative to naive expectation: better matches produce lower (more negative) numbers. +- Consequently, ranking queries must sort ascending (`ORDER BY rank` with no `DESC`) to get best-matches-first, not descending. + +## Pros / strengths + +- Primary-source (SQLite's own project history) confirmation of a detail that is easy to get backwards and silently misrank results if assumed rather than checked. + +## Cons / limitations / caveats + +- This is a narrow, mechanical fact about FTS5's API, not itself evidence about recency tracking or write amplification — it's included here because it surfaced in the same research query about combining BM25 with recency-aware ranking, and is directly relevant to *correctly implementing* any future phase that blends `last_used` into search ranking. + +## Relevance to tai + +Tai's existing `fts_search()` code already sorts by `bm25(fts_docs)` ascending (confirmed independently in the codebase, not from this source) — this dossier exists so that if/when a future phase blends a recency signal into ranking, whoever implements it does not accidentally assume "higher bm25 score is better" and invert the ranking. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/agent-memory-recency-tracking/tarantool-vinyl-throttle-patches.md b/research/agent-memory-recency-tracking/tarantool-vinyl-throttle-patches.md new file mode 100644 index 0000000..397ce3d --- /dev/null +++ b/research/agent-memory-recency-tracking/tarantool-vinyl-throttle-patches.md @@ -0,0 +1,28 @@ +# Tarantool Vinyl: throttle tx to ensure compaction keeps up with dumps (patch series) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | vinyl: throttle tx to ensure compaction keeps up with dumps | +| Source URL | https://lists.tarantool.org/pipermail/tarantool-patches/2019-January/004332.html | +| Source type | other (mailing-list patch series) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "throttled lazy timestamp update avoid write amplification last_accessed database pattern". This research pass's own synthesis explicitly flagged this result (and two further patch-series revisions in the same thread, v2 and v3, treated here as the same source family) as off-target/tangential: it concerns throttling transactions to keep LSM-tree compaction ahead of memory dumps in Tarantool's Vinyl storage engine, which is a related-but-distinct throttling concern from the specific "don't write a timestamp on every read" pattern this subject needed. Logged honestly as a near-miss rather than omitted or misrepresented as directly on-topic. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (off-target / tangential to this subject). | diff --git a/research/agent-memory-recency-tracking/usenix-zhou-traces.md b/research/agent-memory-recency-tracking/usenix-zhou-traces.md new file mode 100644 index 0000000..e2b94c3 --- /dev/null +++ b/research/agent-memory-recency-tracking/usenix-zhou-traces.md @@ -0,0 +1,28 @@ +# USENIX 2001 — Zhou et al., trace analysis section + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | (USENIX 2001 full paper by Zhou et al., "Traces" section) | +| Source URL | https://static.usenix.org/event/usenix01/full_papers/zhou/zhou_html/node3.html | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LRFU combined recency frequency cache replacement formula least recently frequently used" as a related-work mention alongside LRFU, FBR, LRU-K, 2Q, and ARC; this specific "Traces" section of a 2001 USENIX paper was not independently opened during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/uspatent-write-amplification-family.md b/research/agent-memory-recency-tracking/uspatent-write-amplification-family.md new file mode 100644 index 0000000..931c7f2 --- /dev/null +++ b/research/agent-memory-recency-tracking/uspatent-write-amplification-family.md @@ -0,0 +1,28 @@ +# Preventing write amplification during frequent data updates (US Patents 10,037,155 B2 and 11,494,276 B2) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Preventing write amplification during frequent data updates; Reduced write amplification using small-block log | +| Source URL | https://patents.google.com/patent/US10037155 (related: https://patents.google.com/patent/US11494276B2) | +| Source type | patent | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "throttled lazy timestamp update avoid write amplification last_accessed database pattern" as two related patents on write-amplification mitigation in storage systems; logged together as one source family since both surfaced from the same query for the same general concern and neither was independently opened. These are storage-device/firmware-level write-amplification patents (a different mechanism, closer to SSD wear-leveling concerns) rather than the database-row-update write amplification this subject's conclusion is actually about — flagged here as adjacent-but-distinct, consistent with this research pass's honesty requirement. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (adjacent concern: storage-level, not row-update-level, write amplification). | diff --git a/research/agent-memory-recency-tracking/uspto-patent-6996676-arc-cache.md b/research/agent-memory-recency-tracking/uspto-patent-6996676-arc-cache.md new file mode 100644 index 0000000..cccfc32 --- /dev/null +++ b/research/agent-memory-recency-tracking/uspto-patent-6996676-arc-cache.md @@ -0,0 +1,28 @@ +# System and method for implementing an adaptive replacement cache policy (US Patent 6,996,676 — the ARC patent) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | System and method for implementing an adaptive replacement cache policy | +| Source URL | https://image-ppubs.uspto.gov/dirsearch-public/print/downloadPdf/6996676 | +| Source type | patent | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LRFU combined recency frequency cache replacement formula least recently frequently used" — this is the patent underlying the well-known ARC (Adaptive Replacement Cache) algorithm, notable by name even though this research pass did not open or independently verify its specific claims. Worth flagging for a future deeper pass given ARC's real-world prominence (used in ZFS, among other systems), but out of scope for the current tracking-only proposal. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/uspto-patent-7167953-adaptive-paging.md b/research/agent-memory-recency-tracking/uspto-patent-7167953-adaptive-paging.md new file mode 100644 index 0000000..3809c55 --- /dev/null +++ b/research/agent-memory-recency-tracking/uspto-patent-7167953-adaptive-paging.md @@ -0,0 +1,28 @@ +# System and method for adaptively managing pages in a memory (US Patent 7,167,953) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | System and method for adaptively managing pages in a memory | +| Source URL | https://image-ppubs.uspto.gov/dirsearch-public/print/downloadPdf/7167953 | +| Source type | patent | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "LRFU combined recency frequency cache replacement formula least recently frequently used" as a patent in the adaptive-page-replacement space; not opened or quoted during this pass. Patents in this space are logged for completeness but treated with extra caution since patent claims describe legal scope, not necessarily the clearest technical explanation — the LRFU explainer content used for the subject's background research came from academic-paper summaries, not this patent. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/vectorize-mem0-vs-letta.md b/research/agent-memory-recency-tracking/vectorize-mem0-vs-letta.md new file mode 100644 index 0000000..68968be --- /dev/null +++ b/research/agent-memory-recency-tracking/vectorize-mem0-vs-letta.md @@ -0,0 +1,28 @@ +# Mem0 vs Letta (MemGPT): AI Agent Memory Compared (2026) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Mem0 vs Letta (MemGPT): AI Agent Memory Compared (2026) | +| Source URL | https://vectorize.io/articles/mem0-vs-letta | +| Source type | blog post (vendor-adjacent comparison) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "MemGPT Letta memory tiering LRU eviction agent context management" as a direct comparison of the two leading products discussed in this subject's Tier 1 dossiers; a strong candidate for deeper reading in a future pass, but not opened or quoted during this one since the vendor-primary sources were used instead. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/vldb-dblp-2q-listing.md b/research/agent-memory-recency-tracking/vldb-dblp-2q-listing.md new file mode 100644 index 0000000..80f610e --- /dev/null +++ b/research/agent-memory-recency-tracking/vldb-dblp-2q-listing.md @@ -0,0 +1,28 @@ +# VLDB 1994 DBLP bibliography listing + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | VLDB '94 proceedings — DBLP bibliography listing | +| Source URL | https://mail.vldb.org/dblp/db/conf/vldb/vldb94-439.html | +| Source type | other (bibliography index page) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "approximate LRU algorithm CLOCK 2Q low write overhead cache eviction database" as the DBLP bibliographic record for the VLDB 1994 proceedings containing the original 2Q paper; purely a citation-index page, not opened for content. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/webmasterworld-datetime-index-thread.md b/research/agent-memory-recency-tracking/webmasterworld-datetime-index-thread.md new file mode 100644 index 0000000..078faa9 --- /dev/null +++ b/research/agent-memory-recency-tracking/webmasterworld-datetime-index-thread.md @@ -0,0 +1,28 @@ +# WebmasterWorld PHP forum — datetime index thread + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | (WebmasterWorld PHP forum thread, title not individually captured) | +| Source URL | https://www.webmasterworld.com/php/3532632.htm | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "SQLite last_accessed column unix timestamp vs datetime performance indexing"; a MySQL-focused (not SQLite) forum thread on the same general timestamp-type question, which the search synthesis noted concluded with "there is no substitute for running your own performance tests." Not opened directly or quoted beyond that one summarized takeaway, which is instead recorded in the `sqlite-forum-timestamp-integer-vs-text.md` Tier 1 dossier. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/agent-memory-recency-tracking/xojo-forum-datetime-to-sqlite.md b/research/agent-memory-recency-tracking/xojo-forum-datetime-to-sqlite.md new file mode 100644 index 0000000..ef8e167 --- /dev/null +++ b/research/agent-memory-recency-tracking/xojo-forum-datetime-to-sqlite.md @@ -0,0 +1,28 @@ +# DateTime to SQLite (Xojo forum thread) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | agent-memory-recency-tracking | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | DateTime to SQLite | +| Source URL | https://forum.xojo.com/t/datetime-to-sqlite/42307?page=2 | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the search "SQLite last_accessed column unix timestamp vs datetime performance indexing" as a Xojo-framework-specific forum thread on converting DateTime values for SQLite storage; not opened or quoted during this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/agentskills-letta-cancel-async-tasks.md b/research/async-pty-process-testing/agentskills-letta-cancel-async-tasks.md new file mode 100644 index 0000000..bd4825e --- /dev/null +++ b/research/async-pty-process-testing/agentskills-letta-cancel-async-tasks.md @@ -0,0 +1,28 @@ +# Letta AI Skills: Cancel Async Tasks — agentskills.so + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Letta AI Skills: Cancel Async Tasks | +| Source URL | https://agentskills.so/skills/letta-ai-skills-cancel-async-tasks | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio create_subprocess_exec start_new_session" search query — a Letta/agent-skills guide on cancelling async tasks (the research synthesis mentioned it recommending `asyncio.shield()` for critical cleanup, but that specific point was not independently re-verified against this exact page). A near-duplicate localized page (https://agentskills.so/zh/skills/letta-ai-skills-cancel-async-tasks) also surfaced and is treated as the same source family. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/alexandra-zaharia-kill-subprocess-children-timeout.md b/research/async-pty-process-testing/alexandra-zaharia-kill-subprocess-children-timeout.md new file mode 100644 index 0000000..bdf5385 --- /dev/null +++ b/research/async-pty-process-testing/alexandra-zaharia-kill-subprocess-children-timeout.md @@ -0,0 +1,48 @@ +# Kill a Python subprocess and its children when a timeout is reached + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Kill a Python subprocess and its children when a timeout is reached | +| Source URL | https://alexandra-zaharia.github.io/posts/kill-subprocess-and-its-children-on-timeout-python/ | +| Source publisher / author | Alexandra Zaharia (personal blog) | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A worked, example-driven blog post covering the same group-signal approach as `iannuccisolutions-timeout-process-group.md`, with an added emphasis on explicitly reaping the process afterward (calling `wait()`/`communicate()` again post-kill) so it doesn't linger as a zombie. + +## Key findings + +- Confirms the process-group-signal approach (start a new session/group, signal the whole group on timeout) as a working pattern, consistent with other sources in this subject. +- Adds the specific point that killing a process does not by itself reap it — an explicit `wait()`/`communicate()` call afterward is required, or the dead child remains a zombie entry in the process table until its parent collects its exit status. + +## Pros / strengths + +- Reinforces, from an independent author, the same group-kill pattern already found elsewhere in this research — converging evidence rather than a single source's opinion. +- The explicit reaping point is a detail some other sources mention only implicitly. + +## Cons / limitations / caveats + +- A personal blog post with a worked example, not a formal specification; the exact code shown was not independently re-run or verified against `asyncio`'s API in this research pass. + +## Relevance to tai + +Reinforced `tai_test.py`'s `ManagedProcess.terminate()` always ending with `await asyncio.wait_for(self.proc.wait(), 5.0)` regardless of which signal actually stopped the process — the comment in the code ("reap so no zombie remains") traces directly to this point. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/all-hands-integration-tests-base.md b/research/async-pty-process-testing/all-hands-integration-tests-base.md new file mode 100644 index 0000000..e81cc73 --- /dev/null +++ b/research/async-pty-process-testing/all-hands-integration-tests-base.md @@ -0,0 +1,28 @@ +# All-Hands integration_tests/tests/base — docs.all-hands.dev + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | integration_tests/tests/base | +| Source URL | https://docs.all-hands.dev/modules/python/evaluation/integration_tests/tests/base | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "python integration test framework design base class" search query — notably, this is documentation for an *agent evaluation* integration-test base class (an abstract class with a "prepare environment" classmethod and a "verify after the agent performs the task" method), conceptually close to `tai_test.py`'s own purpose of testing an AI agent's CLI. Not individually opened or quoted in this research pass, but worth flagging as a close conceptual match for anyone extending this subject later. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/baeldung-redirect-process-output.md b/research/async-pty-process-testing/baeldung-redirect-process-output.md new file mode 100644 index 0000000..0baf4d2 --- /dev/null +++ b/research/async-pty-process-testing/baeldung-redirect-process-output.md @@ -0,0 +1,28 @@ +# Redirect Process Output — Baeldung on Linux + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Redirect Process Output | +| Source URL | https://www.baeldung.com/linux/redirect-process-output | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio tee process output to file and stdout" search query as a general Linux-level (shell `tee`) article on redirecting process output; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/bpo-wait-for-timeout-leaves-process-alive.md b/research/async-pty-process-testing/bpo-wait-for-timeout-leaves-process-alive.md new file mode 100644 index 0000000..9c097c9 --- /dev/null +++ b/research/async-pty-process-testing/bpo-wait-for-timeout-leaves-process-alive.md @@ -0,0 +1,48 @@ +# CPython bug reports: asyncio.wait_for() timeout does not kill the awaited process + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | bugs.python.org msg233486, and companion msg357930 | +| Source URL | https://bugs.python.org/msg233486 | +| Source publisher / author | Python core development bug tracker (bugs.python.org / BPO) | +| Source type | other (official bug tracker message) | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +Official CPython bug tracker messages confirming that `asyncio.wait_for()` timing out does not, by itself, terminate the coroutine/process it was waiting on — the awaited operation can continue running in the background after the `TimeoutError` is raised to the caller. + +## Key findings + +- A bug report describes `wait_for()` timing out while the process it was waiting on continued living in the background — the timeout only stops *waiting*, it does not reach into and stop the underlying operation. +- This is documented as a known, discussed behavior in CPython's own bug tracker (two related messages), not a one-off misunderstanding. + +## Pros / strengths + +- Primary-source, from the language's own bug tracker — about as authoritative as a finding in this subject gets. +- Directly actionable: it tells you precisely what `wait_for()` does *not* do for you, closing a dangerous assumption gap. + +## Cons / limitations / caveats + +- Only the two referenced messages were reviewed, not the full bug thread's eventual resolution or whether later Python versions changed this behavior. + +## Relevance to tai + +This is the specific reason `tai_test.py.TestCase.execute()` never treats a caught `asyncio.TimeoutError` (from the `asyncio.wait_for(self.run(), timeout=self.timeout)` wrapping every test) as sufficient cleanup on its own. The `finally` block unconditionally calls `await proc.terminate()` for every spawned process, precisely because this source confirms the timeout itself does nothing to stop the process — only the explicit `terminate()` call (with its own `killpg` escalation and reap) does. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/chromiumos-chromite-tee-py.md b/research/async-pty-process-testing/chromiumos-chromite-tee-py.md new file mode 100644 index 0000000..527bfa4 --- /dev/null +++ b/research/async-pty-process-testing/chromiumos-chromite-tee-py.md @@ -0,0 +1,28 @@ +# ChromiumOS chromite lib/tee.py + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | lib/tee.py (chromite) | +| Source URL | https://cos.googlesource.com/mirrors/cros/chromiumos/chromite/+/HEAD/lib/tee.py | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio tee process output to file and stdout" search query as ChromiumOS's own `tee` implementation in its build tooling; a second, near-identical mirror of the same file at a different ChromiumOS source host also surfaced and is treated as the same source family. Not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/conclusions/tai-test-framework-design.md b/research/async-pty-process-testing/conclusions/tai-test-framework-design.md new file mode 100644 index 0000000..6590d05 --- /dev/null +++ b/research/async-pty-process-testing/conclusions/tai-test-framework-design.md @@ -0,0 +1,53 @@ +# Conclusion: tai_test.py's Transport, Cleanup, and Framework Design + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Conclusion | +| Tier | N/A | +| Date decided | 2026-10-09 | +| Decided by | retoor, advised by tai (agent, Claude Sonnet 5) | +| Status | Active | +| Confidence | High | + +## Question / problem statement + +How should `tai_test.py` — a reusable integration-test framework for driving `tai.py` both as a one-shot subprocess and as a persistent interactive REPL (including scripting input and detecting turn boundaries) — be built so that it's safe (no leaked zombie processes on timeout, crash, or cancellation) and extensible (a clean base class future tests can subclass), using only the Python standard library, consistent with `tai.py`'s own zero-dependency rule? + +## Conclusion + +The framework uses two unified transports behind one `asyncio.StreamReader`-based interface: plain `asyncio.create_subprocess_exec` with `PIPE` streams for one-shot calls, and a real pty (stdlib `pty.openpty()`, the slave fd given to the child as stdin/stdout/stderr, the master fd wrapped into a `StreamReader` via `loop.connect_read_pipe`) for anything that needs the child to behave as if a human were typing — because `tai.py`'s own code branches on `sys.stdin.isatty()`. Optional live human stdin passthrough is bridged via `loop.add_reader` + raw `os.read` on the fd (never buffered `sys.stdin`, which blocks), with no background thread, mirroring how the child's own output is already consumed. Every process is started with `start_new_session=True` and torn down through an escalating SIGTERM-then-SIGKILL `os.killpg` call that always finishes with `proc.wait()` to reap it, so a timeout or crash never leaves an orphan. The `TestCase` base class (`setup`/`run`/`verify`/`teardown` hooks, typed exceptions carrying the full transcript) follows the declarative-base-class-plus-override-hooks pattern used by real prior art (CERN's Gaudi, gem5, OpenStack's oslotest) rather than being invented from scratch. + +## Supporting sources + +- `../ptyprocess-docs.md`, `../pypi-shellous.md`, `../github-agents-issue1-controlling-terminal.md` — established why a real pty (not a pipe) was needed, and the documented, accepted limitation that `tai_test.py`'s pty mode does not call `ioctl(TIOCSCTTY)` and so doesn't guarantee job-control signal semantics. +- `../github-gist-nonblocking-stdin-os-read.md`, `../python-asyncio-issue499-add-reader-eof.md`, `../glama-mcp-remote-py-stdio-jsonl.md` — the exact techniques `StdinBridge` (raw `os.read` via `add_reader`) and `ManagedProcess`'s pty-mode `StreamReader` wrapping (`loop.connect_read_pipe`) implement. +- `../pypi-pexpect.md`, `../pyte-terminal-emulator.md`, `../pypi-stitch-pty.md` — the alternatives surveyed and not adopted. +- `../iannuccisolutions-timeout-process-group.md`, `../python-docs-asyncio-subprocess.md`, `../alexandra-zaharia-kill-subprocess-children-timeout.md`, `../execa-termination-docs.md` — the direct justification for `start_new_session` + `killpg` + always-reap. +- `../discourse-merge-two-streams.md` — the reason a generic multi-stream-merge utility was deliberately **not** built. +- `../gaudi-subprocess-base-test.md`, `../gem5-test-infrastructure-commit.md`, `../oslotest-timeout-fixture.md` — the test-framework architecture `TestCase` is modeled on. +- `../discourse-subprocess-process-gc.md`, `../slingacademy-stop-kill-child-process.md`, `../bpo-wait-for-timeout-leaves-process-alive.md` — confirmation that relying on timeout or garbage collection alone to clean up a process is documented, known-unreliable, which is why `execute()`'s `finally` block always calls `terminate()` explicitly regardless of how the test ended. + +## Alternatives considered and rejected + +- **pexpect** — rejected because it's a blocking/synchronous library and `tai_test.py` is asyncio-native throughout; mixing a blocking library in would have meant either wrapping every call in a thread executor or accepting blocking the event loop, neither of which fit. +- **stitch-pty** — rejected as too young/unverified to depend on for core framework plumbing, and as a compiled Rust-backed dependency it would also have violated the project's stdlib-only constraint. +- **A generic tagged-queue multi-stream-merge utility** — rejected because the Discourse thread's own participants described the resulting async-generator plumbing as hard to maintain, and `tai_test.py`'s actual need (merging stdout+stderr in pipe mode) is fully satisfied by the simpler `stderr=STDOUT` subprocess argument, making the general utility unnecessary complexity. +- **A background thread for bridging live stdin** — not rejected as wrong, but passed over in favor of `loop.add_reader` + `os.read` specifically so stdin-bridging follows the same no-thread, fd-based pattern as the child's own output handling, rather than mixing two different concurrency models in one framework. + +## Risks / open questions + +The pty mode does not explicitly call `ioctl(TIOCSCTTY)`, so there is no guarantee the child's session becomes the pty's controlling terminal in the full job-control sense. `isatty()`-based behavior (all that `tai.py`'s own code checks) is unaffected, since that's a property of the fd itself, not of session leadership — but SIGWINCH-based resize forwarding and tty-line-discipline-generated Ctrl-C-as-SIGINT are not guaranteed to work. This is accepted because no test built so far needs either capability; every test built on `tai_test.py` so far only scripts line-based input/output. + +## Action taken + +Implemented the same day as this research: `tai_test.py` (`ManagedProcess`, `StdinBridge`, `TeeLogger`, `TestCase`, `TestRunner`, `main_cli`) at the repository root, and `tests/rag.py` built on it — both already in use and passing against the real `tai.py` binary. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial conclusion recorded. | diff --git a/research/async-pty-process-testing/cpython-asyncio-subprocess-mirrors.md b/research/async-pty-process-testing/cpython-asyncio-subprocess-mirrors.md new file mode 100644 index 0000000..8c04f1c --- /dev/null +++ b/research/async-pty-process-testing/cpython-asyncio-subprocess-mirrors.md @@ -0,0 +1,28 @@ +# CPython asyncio-subprocess.rst — googlesource/gitea/remotebranch mirrors + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | asyncio-subprocess.rst (llvm.googlesource.com mirror) | +| Source URL | https://llvm.googlesource.com/third_party/cpython/+/refs/heads/main/Doc/library/asyncio-subprocess.rst | +| Source type | standard (mirrored stdlib source) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio create_subprocess_exec start_new_session" search query: several near-duplicate mirrors of the same CPython `asyncio-subprocess.rst` documentation source surfaced across different git-hosting forks — several commit-pinned `chromium.googlesource.com` variants, `git.joshthomas.dev`'s mirror of the 3.9 branch, and `remotebranch.eu`'s mirror of the 3.15 branch, in addition to the llvm.googlesource.com one listed above. These are all redundant mirrors of the same stdlib source file already covered at Tier 1 in `python-docs-asyncio-subprocess.md`, not independent sources, logged together as one card. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (redundant mirrors of a Tier 1 source). | diff --git a/research/async-pty-process-testing/cs-unb-ca-asyncio-subprocess-doc-mirror.md b/research/async-pty-process-testing/cs-unb-ca-asyncio-subprocess-doc-mirror.md new file mode 100644 index 0000000..e8de6bd --- /dev/null +++ b/research/async-pty-process-testing/cs-unb-ca-asyncio-subprocess-doc-mirror.md @@ -0,0 +1,28 @@ +# Python 3.9.2 asyncio-subprocess docs (CS UNB mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Subprocesses — Python 3.9.2 documentation (teaching mirror) | +| Source URL | https://www.cs.unb.ca/~bremner/teaching/cs2613/books/python3-doc/library/asyncio-subprocess.html | +| Source type | standard (mirrored stdlib documentation) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio subprocess timeout kill process group" search query — a university teaching-site mirror of the same stdlib `asyncio-subprocess` documentation already covered at Tier 1 in `python-docs-asyncio-subprocess.md`. Logged as a redundant mirror. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (mirror of a Tier 1 source). | diff --git a/research/async-pty-process-testing/debian-pyte-csail-mirror.md b/research/async-pty-process-testing/debian-pyte-csail-mirror.md new file mode 100644 index 0000000..c182efa --- /dev/null +++ b/research/async-pty-process-testing/debian-pyte-csail-mirror.md @@ -0,0 +1,28 @@ +# python3-pyte — CSAIL Debian package mirror + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | python3-pyte (CSAIL mirror) | +| Source URL | https://packages-pkgmirror-csail.debian.org/python3-pyte | +| Source type | repository (package page mirror) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "pexpect pyte vs asyncio pty" search query — a CSAIL mirror of the same `python3-pyte` Debian package page already covered at Tier 1 in `pyte-terminal-emulator.md`. Logged as a redundant mirror, not an independent source. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (mirror of a Tier 1 source). | diff --git a/research/async-pty-process-testing/discourse-merge-two-streams.md b/research/async-pty-process-testing/discourse-merge-two-streams.md new file mode 100644 index 0000000..e5ce2da --- /dev/null +++ b/research/async-pty-process-testing/discourse-merge-two-streams.md @@ -0,0 +1,48 @@ +# Asyncio: How to merge two streams — discuss.python.org + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Asyncio: How to merge two streams | +| Source URL | https://discuss.python.org/t/asyncio-how-to-merge-two-streams/106616 | +| Source publisher / author | Python Discourse community | +| Source type | forum thread | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A Python Discourse thread discussing how to merge two independent asyncio streams (e.g. a subprocess's stdout and stderr) into one consumable sequence. The community-arrived-at pattern is one background task per stream, each pushing tagged chunks into a shared `asyncio.Queue`, with a single consumer draining the queue — but participants in the thread itself described the resulting plumbing as messy and said they were no longer comfortable maintaining it. + +## Key findings + +- The tagged-queue pattern (one task per stream, push `(stream_name, chunk)` tuples into a shared queue, one consumer loop) is the community's working answer, confirmed to function. +- Multiple participants in the same thread independently reported the approach grew complex enough to become a maintenance burden, and the thread never converged on one canonical, widely-endorsed implementation. +- `asyncio`'s own standard pattern naturally supports running several tasks (and even several subprocesses) concurrently via `asyncio.gather`, which the tagged-queue approach builds on top of. + +## Pros / strengths + +- Honest about the pattern's real-world maintainability cost, not just its mechanical correctness — useful signal for a framework design decision, not just a working snippet. + +## Cons / limitations / caveats + +- No single reference implementation to point to; "write your own small, well-tested helper" was effectively the thread's own conclusion, which is not a strong endorsement of the pattern as a reusable abstraction. + +## Relevance to tai + +This is the direct reason `tai_test.py` does **not** implement a generic multi-stream-merge utility. Its actual need — combining a subprocess's stdout and stderr into one stream for pipe-mode tests — is fully satisfied by the much simpler `stderr=asyncio.subprocess.STDOUT` argument to `create_subprocess_exec`, which merges them at the OS/subprocess level before asyncio ever sees two separate streams, sidestepping the complexity this thread describes entirely. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/discourse-subprocess-process-gc.md b/research/async-pty-process-testing/discourse-subprocess-process-gc.md new file mode 100644 index 0000000..331a751 --- /dev/null +++ b/research/async-pty-process-testing/discourse-subprocess-process-gc.md @@ -0,0 +1,47 @@ +# Understanding asyncio.subprocess.Process garbage collection — discuss.python.org + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Understanding asyncio.subprocess.Process garbage collection | +| Source URL | https://discuss.python.org/t/understanding-asyncio-subprocess-process-garbage-collection/105973 | +| Source publisher / author | Python Discourse community | +| Source type | forum thread | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A Discourse thread examining what happens to a still-running `asyncio.subprocess.Process` object when it's garbage collected. The stdlib documentation states the child gets killed as a safety net in that case — but a forum participant reported observing output continuing to appear after the "owning" script had already finished, suggesting this safety net is not reliably instantaneous or complete in practice. + +## Key findings + +- `asyncio.subprocess.Process`'s own documentation states that if the process object is garbage collected while the process is still running, the child process will be killed — a documented safety net, not an accident of implementation. +- Despite that documented behavior, a thread participant reported a case where a subprocess's "Step" output kept appearing after the main script had already exited, implying the GC-triggered kill either didn't fire promptly or didn't fully suppress output in that instance. + +## Pros / strengths + +- A concrete counter-example to "just let Python's GC clean it up" as a cleanup strategy — directly useful as evidence against relying on implicit cleanup. + +## Cons / limitations / caveats + +- A single forum report of unexpected behavior, not a reproduced, isolated bug report with a minimal test case — the exact conditions that produced it were not independently verified in this research pass. + +## Relevance to tai + +Reinforced the decision that `tai_test.py.TestCase.execute()` must explicitly call `await proc.terminate()` for every spawned process in its `finally` block, regardless of whether the test passed, failed, or was cancelled — never relying on Python's garbage collector or process-object destruction to clean anything up, consistent with the companion finding in `bpo-wait-for-timeout-leaves-process-alive.md` that a bare timeout alone doesn't guarantee cleanup either. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/dispatch-blog-subprocesses-outlive-program.md b/research/async-pty-process-testing/dispatch-blog-subprocesses-outlive-program.md new file mode 100644 index 0000000..4580042 --- /dev/null +++ b/research/async-pty-process-testing/dispatch-blog-subprocesses-outlive-program.md @@ -0,0 +1,28 @@ +# Your Subprocesses Outlive Your Program. Here's How to Kill Them For Real. + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Your Subprocesses Outlive Your Program. Here's How to Kill Them For Real. | +| Source URL | https://dispatch-blog.hashnode.dev/your-subprocesses-outlive-your-program-here-s-how-to-kill-them-for-real | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio subprocess timeout kill process group" search query — title strongly suggests it covers the exact same ground as the Tier 1 sources in this subject (`iannuccisolutions-timeout-process-group.md` etc.), but it was not individually opened or quoted, so it stays Tier 2. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/docs-python-org-locale-mirrors-asyncio-subprocess.md b/research/async-pty-process-testing/docs-python-org-locale-mirrors-asyncio-subprocess.md new file mode 100644 index 0000000..f4823a0 --- /dev/null +++ b/research/async-pty-process-testing/docs-python-org-locale-mirrors-asyncio-subprocess.md @@ -0,0 +1,28 @@ +# asyncio-subprocess docs — locale/version mirrors + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | asyncio subprocess (Russian locale, Python 3.16) | +| Source URL | https://docs.python.org/ru/3.16/library/asyncio-subprocess.html | +| Source type | standard (mirrored stdlib documentation) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio merge multiple streams" search query: several locale/version mirrors of the same stdlib `asyncio-subprocess` documentation page (already covered in English at Tier 1 in `python-docs-asyncio-subprocess.md`) all appeared in the same result set — Turkish (https://docs.python.org/tr/3.16/library/asyncio-subprocess.html), Polish (https://docs.python.org/pl/3.16/library/asyncio-subprocess.html), Traditional Chinese for Python 3.7 (https://docs.python.org/zh-tw/3.7/library/asyncio-subprocess.html), and a gitverse.ru mirror of the 3.9 branch's `.rst` source (https://gitverse.ru/githubmirror/cpython/tag/3.9/Doc/library/asyncio-subprocess.rst). These are redundant translations/mirrors of the same source document, not independent sources, logged together as one card. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (redundant locale/version mirrors of a Tier 1 source). | diff --git a/research/async-pty-process-testing/documentation-help-361-asyncio-subprocess.md b/research/async-pty-process-testing/documentation-help-361-asyncio-subprocess.md new file mode 100644 index 0000000..71f6139 --- /dev/null +++ b/research/async-pty-process-testing/documentation-help-361-asyncio-subprocess.md @@ -0,0 +1,28 @@ +# Python 3.6.1 asyncio-subprocess docs (documentation.help mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | 18.5.6. Subprocess — Python 3.6.1 Documentation | +| Source URL | https://documentation.help/Python-3.6.1/asyncio-subprocess.html | +| Source type | standard (mirrored stdlib documentation) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio subprocess timeout kill process group" search query — an older-version (3.6.1) mirror of the same stdlib documentation already covered at Tier 1 (current version) in `python-docs-asyncio-subprocess.md`. Logged as a redundant, older-version mirror. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (older-version mirror of a Tier 1 source). | diff --git a/research/async-pty-process-testing/execa-termination-docs.md b/research/async-pty-process-testing/execa-termination-docs.md new file mode 100644 index 0000000..71728ce --- /dev/null +++ b/research/async-pty-process-testing/execa-termination-docs.md @@ -0,0 +1,47 @@ +# execa termination.md — cross-language prior art (Node.js) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | execa — termination documentation | +| Source URL | https://github.com/sindresorhus/execa/blob/78edcb9f2ba4d52ee3e28a499544b038c1cc3f99/docs/termination.md | +| Source publisher / author | sindresorhus (execa project, Node.js) | +| Source type | repository (documentation) | +| Source date (published) | Unknown — not stated (pinned to a specific commit) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +execa is a well-regarded Node.js process-execution library, not Python — included here deliberately as cross-language prior art rather than a Python-specific source. Its termination documentation describes the same terminate-then-escalate-to-kill pattern found in the Python-specific sources in this subject, plus an explicit warning against relying on garbage collection for process cleanup. + +## Key findings + +- execa documents the same two-stage termination approach (a polite signal first, a forceful one after a grace period) as the Python sources in this subject, suggesting this is a language-agnostic, generally-accepted pattern for process lifecycle management rather than a Python-specific idiom. +- Emphasizes explicit, deliberate process-tree cleanup as a design principle, independent of any particular runtime's garbage collector. + +## Pros / strengths + +- Cross-language convergence: that an unrelated ecosystem (Node.js) arrives at the same termination pattern independently is a meaningfully stronger signal than one more Python source saying the same thing. + +## Cons / limitations / caveats + +- Not a Python source at all — none of its specific API details (`execa`'s own function signatures, option names) transfer directly to `tai_test.py`; only the general pattern does. This dossier is explicit about that distinction so it isn't mistaken for Python-specific guidance. + +## Relevance to tai + +Served as independent, cross-ecosystem confirmation that `tai_test.py`'s terminate-then-kill-then-reap design (see `iannuccisolutions-timeout-process-group.md` and `alexandra-zaharia-kill-subprocess-children-timeout.md`) is sound general practice, not an idiosyncratic Python-only convention. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/gaudi-doxygen-allclasses-mirror.md b/research/async-pty-process-testing/gaudi-doxygen-allclasses-mirror.md new file mode 100644 index 0000000..d2fa9d4 --- /dev/null +++ b/research/async-pty-process-testing/gaudi-doxygen-allclasses-mirror.md @@ -0,0 +1,28 @@ +# Gaudi merge request !1605 — diff view + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Gaudi merge request !1605 (diffs view) | +| Source URL | https://gaudi.web.cern.ch:8443/gaudi/Gaudi/-/merge_requests/1605/diffs | +| Source type | repository (merge request diff view) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "python integration test framework design base class" search query — the diff view of the same Gaudi merge request already analyzed at Tier 1 in `gaudi-subprocess-base-test.md`. Logged as a redundant view of the same source rather than an independent one. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (alternate view of a Tier 1 source). | diff --git a/research/async-pty-process-testing/gaudi-subprocess-base-test.md b/research/async-pty-process-testing/gaudi-subprocess-base-test.md new file mode 100644 index 0000000..97b3f32 --- /dev/null +++ b/research/async-pty-process-testing/gaudi-subprocess-base-test.md @@ -0,0 +1,49 @@ +# CERN Gaudi — SubprocessBaseTest / GaudiExeTest + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Gaudi merge request !1605 (SubprocessBaseTest / GaudiExeTest), and companion doxygen source page | +| Source URL | https://gitlab.cern.ch/gaudi/Gaudi/-/merge_requests/1605 | +| Source publisher / author | CERN (Gaudi physics-software framework project) | +| Source type | repository (merge request + generated source documentation) | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +CERN's Gaudi framework (used across large physics experiments) includes a `SubprocessBaseTest` base class and a `GaudiExeTest` subclass for running and verifying subprocess-based tests. Test authors configure tests by setting class attributes (command, environment, timeout, expected return code) rather than writing imperative test logic, with an overridable `preprocessor` hook for customizing output handling. This companion source (`gaudi.web.cern.ch/doxygen/.../_base_test_8py_source.html`) is the generated source documentation for the same base class. + +## Key findings + +- `SubprocessBaseTest` is explicitly documented as providing "the setup and utility methods for ensuring tests run in a consistent environment" — the test author's job is reduced to declaring configuration, not reimplementing process management per test. +- Configuration happens through class attributes: command, environment, timeout, and expected return code are all named, documented fields on the test class. +- Output handling is customizable via an overridable `preprocessor` attribute on `GaudiExeTest`, letting a subclass change how reference-output comparison works without touching the runner itself. + +## Pros / strengths + +- A mature, battle-tested framework used in large, real scientific-computing codebases — strong evidence this "declarative base class + override hooks" shape scales to many tests written by many different authors over time, which is exactly the goal stated for `tai_test.py` ("a structure that scales and will be allowed [to be] followed"). + +## Cons / limitations / caveats + +- Domain-specific to physics-software testing (comparing program output against reference files); not every detail (e.g. reference-file resolution relative to the test's own location) was adopted, since `tai_test.py`'s tests assert on vault state and search output rather than diffing against golden files. +- Reviewed via its merge request description and generated source documentation, not a full read of the implementation's actual Python source. + +## Relevance to tai + +The direct architectural model for `tai_test.py.TestCase`: class-level attributes (`name`, `timeout`, `mode`) instead of constructor boilerplate, and override hooks (`setup`/`run`/`verify`/`teardown`) instead of one monolithic test method — mirroring Gaudi's class-attribute configuration plus its overridable `preprocessor` hook. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/gem5-test-infrastructure-commit.md b/research/async-pty-process-testing/gem5-test-infrastructure-commit.md new file mode 100644 index 0000000..0386cca --- /dev/null +++ b/research/async-pty-process-testing/gem5-test-infrastructure-commit.md @@ -0,0 +1,50 @@ +# gem5 test infrastructure — Timer-based timeout + typed exceptions + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | tests: Add test infrastructure as a Python module (commit), and companion helpers.py | +| Source URL | https://git.sanchayanmaity.net/sanchayanmaity/gem5/commit/f385adc8af744e7f6b78ff9adafd9c3ecf65ce68 | +| Source publisher / author | gem5 project (computer architecture simulator) | +| Source type | repository (commit + source file) | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A commit adding Python test infrastructure to the gem5 simulator project, together with its companion `tests/testing/helpers.py`. The helper uses a `threading.Timer` to enforce a timeout on a running subprocess: the timer's callback terminates the child process, then the surrounding code raises a dedicated exception carrying both the captured exit status and the process's output. + +## Key findings + +- Timeout enforcement pattern: `Timer(timeout, on_timeout_handler)` started before `communicate()`-ing with the process, cancelled afterward if the process finished in time. +- On timeout, the handler terminates the child and a dedicated exception type is raised carrying the captured status and output together — the caller doesn't have to separately go fetch output after catching a generic timeout error. +- gem5's newer test framework (noted in the same research pass) further splits each test into a "run" stage and a "verify" stage, where verify units are automatically skipped if their corresponding run stage didn't execute. + +## Pros / strengths + +- The "exception carries the captured output" design is directly actionable and immediately reusable — it solves the exact problem of "how do you debug a timed-out test" by attaching the evidence to the failure itself rather than requiring separate log inspection. +- The run/verify stage split is a clean idea for keeping a test's "did it crash" question separate from "did it produce the right result." + +## Cons / limitations / caveats + +- Uses `threading.Timer`, a synchronous/thread-based mechanism, not `asyncio.wait_for` — `tai_test.py` adapted the *idea* (typed exception carrying output) to an asyncio-native mechanism (`asyncio.wait_for` plus try/except) rather than reusing gem5's thread-based implementation directly. +- Reviewed via commit metadata and the helper file's described behavior, not gem5's full test suite or its actual usage patterns in practice. + +## Relevance to tai + +The direct model for `tai_test.py`'s exception hierarchy: `TestTimeout`, `TestCrashed`, and `TestAssertionFailed` all subclass `TaiTestError`, which carries a `transcript` list — so a failing test's exception *is* the debugging evidence, exactly matching gem5's "exception carries status and output" pattern, just built on `asyncio.wait_for`/try-except instead of `threading.Timer`. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/gitea-bunkerweb-test-py.md b/research/async-pty-process-testing/gitea-bunkerweb-test-py.md new file mode 100644 index 0000000..ac4b3d5 --- /dev/null +++ b/research/async-pty-process-testing/gitea-bunkerweb-test-py.md @@ -0,0 +1,28 @@ +# BunkerWeb tests/Test.py + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Test.py (BunkerWeb) | +| Source URL | https://gitea.com/palmcoasty/bunkerweb/src/branch/master/tests/Test.py | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "python integration test framework design base class" search query as another real project's custom `Test` base class; not individually opened or quoted — the Tier 1 architectural models used for this subject's conclusion are Gaudi, gem5, and oslotest instead. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/gitea13-opendev-distcloud-subprocess-cleanup.md b/research/async-pty-process-testing/gitea13-opendev-distcloud-subprocess-cleanup.md new file mode 100644 index 0000000..6a57f68 --- /dev/null +++ b/research/async-pty-process-testing/gitea13-opendev-distcloud-subprocess-cleanup.md @@ -0,0 +1,28 @@ +# distributedcloud/dccommon/subprocess_cleanup.py — StarlingX distcloud + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | subprocess_cleanup.py (StarlingX distcloud) | +| Source URL | https://gitea13.opendev.org:3081/starlingx/distcloud/src/commit/6435d6c3572562ff2e8e6b23075b4086d42ac8ae/distributedcloud/dccommon/subprocess_cleanup.py | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio create_subprocess_exec start_new_session" search query as a real project's dedicated subprocess-cleanup module; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/github-agents-issue1-controlling-terminal.md b/research/async-pty-process-testing/github-agents-issue1-controlling-terminal.md new file mode 100644 index 0000000..fde7a84 --- /dev/null +++ b/research/async-pty-process-testing/github-agents-issue1-controlling-terminal.md @@ -0,0 +1,49 @@ +# sync: interactive launches strip the harness of its controlling terminal (start_new_session=True) · Issue #1 · anntnzrb/agents + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | sync: interactive launches strip the harness of its controlling terminal (start_new_session=True) | +| Source URL | https://github.com/anntnzrb/agents/issues/1 | +| Source publisher / author | GitHub user anntnzrb | +| Source type | repository (issue) | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A GitHub issue documenting a real, encountered bug: a test/automation harness launched an interactive child process with `start_new_session=True`, which left the harness without a controlling terminal and broke window-resize and Ctrl-C handling for the child. The issue includes a small probe script for diagnosing whether a child process actually has a controlling terminal. + +## Key findings + +- `start_new_session=True` makes the child a new session leader; if that child is then given a pty as stdin/stdout/stderr, whether the pty becomes its *controlling* terminal (in the POSIX job-control sense) is not automatic — the issue describes a concrete case where it did not, breaking SIGWINCH-based resize and tty-line-discipline Ctrl-C-as-SIGINT. +- The reporter's diagnostic approach: a probe script that checks `os.isatty(0)`, `os.tcgetpgrp(0)`, and whether `/dev/tty` can be opened, to confirm a child process actually has a controlling terminal rather than just an open pty fd. + +## Pros / strengths + +- A real, lived-through failure report rather than theoretical speculation — exactly the kind of pitfall that's easy to get subtly wrong when combining `start_new_session=True` (needed for safe process-group-wide `killpg` cleanup) with pty-based spawning (needed for `isatty()`-correct interactive behavior). +- The diagnostic probe script is a concrete, reusable technique for anyone needing to verify controlling-terminal status. + +## Cons / limitations / caveats + +- A single GitHub issue report, not a kernel/POSIX specification — the exact conditions under which a session leader's first terminal open becomes its controlling terminal can depend on how the fd was obtained (opened directly by the child vs. inherited already-open from the parent), and this issue doesn't fully disambiguate that for the inherited-fd case `tai_test.py` actually uses. +- No resolution/fix is confirmed as verified in this research pass (the issue's eventual outcome was not reviewed). + +## Relevance to tai + +Directly shaped `tai_test.py`'s documented, accepted limitation: its pty mode does not explicitly call `ioctl(TIOCSCTTY)`, so controlling-terminal/job-control semantics (SIGWINCH forwarding, Ctrl-C-as-SIGINT from the tty line discipline) are not guaranteed. This was judged acceptable because `tai.py`'s own code only checks `sys.stdin.isatty()` (a property of the fd itself, unaffected by session/controlling-terminal status) and `tai_test.py`'s tests only script line-based input/output, never relying on resize or terminal-generated signals. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/github-gist-nonblocking-stdin-os-read.md b/research/async-pty-process-testing/github-gist-nonblocking-stdin-os-read.md new file mode 100644 index 0000000..442daa2 --- /dev/null +++ b/research/async-pty-process-testing/github-gist-nonblocking-stdin-os-read.md @@ -0,0 +1,50 @@ +# Non blocking file reader for python asyncio · GitHub Gist + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Non blocking file reader for python asyncio | +| Source URL | https://gist.github.com/nilp0inter/5f400b6afb6df7c0b553 | +| Source publisher / author | GitHub user nilp0inter | +| Source type | repository (gist) | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A GitHub gist demonstrating the correct way to read stdin (or any fd) without blocking the asyncio event loop: use `loop.add_reader(fd, callback)` where the callback does a raw `os.read(fd, n)`, never a call through Python's buffered `sys.stdin` object, which blocks regardless of `add_reader`. + +## Key findings + +- Calling `sys.stdin.read()` inside an `add_reader` callback still blocks, because Python's buffered stdin object waits for more data internally — `add_reader` only tells the event loop the fd is readable, it does not change how a buffered read call behaves. +- The correct pattern is `os.read(fileno, n)` on the raw file descriptor inside the callback. +- On EOF (an empty read), the callback should call `loop.remove_reader(fileno)` to stop watching the fd. + +## Pros / strengths + +- Identifies a genuine, easy-to-make mistake (buffered read inside a non-blocking callback) with a precise fix, backed by a reported working example. +- The pattern is minimal and requires no third-party dependency — pure stdlib (`os`, `asyncio`). + +## Cons / limitations / caveats + +- A gist, not an authoritative reference — no discussion of edge cases like partial reads mid-line or how this interacts with terminal raw/cooked mode. +- Only covers the read side; doesn't address writing back to the child or coordinating with a separate output stream. + +## Relevance to tai + +This is the exact pattern `tai_test.py`'s `StdinBridge._on_readable()` implements: `os.read(self._fd, 4096)` inside an `add_reader` callback, buffering until a newline, and calling `self.stop()` (which internally calls `loop.remove_reader`) on an empty read. Using `sys.stdin.read()` instead would have silently reintroduced blocking behavior that defeats the entire point of the non-blocking design. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/github-h0lley-nonblockingstdin.md b/research/async-pty-process-testing/github-h0lley-nonblockingstdin.md new file mode 100644 index 0000000..51c53dd --- /dev/null +++ b/research/async-pty-process-testing/github-h0lley-nonblockingstdin.md @@ -0,0 +1,28 @@ +# h0lley/nonBlockingStdin — GitHub + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | nonBlockingStdin | +| Source URL | https://github.com/h0lley/nonBlockingStdin | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio non-blocking stdin read" search query as a small repository dedicated to exactly this problem; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/glama-mcp-remote-py-stdio-jsonl.md b/research/async-pty-process-testing/glama-mcp-remote-py-stdio-jsonl.md new file mode 100644 index 0000000..a09343d --- /dev/null +++ b/research/async-pty-process-testing/glama-mcp-remote-py-stdio-jsonl.md @@ -0,0 +1,49 @@ +# mcp-remote-py stdio_jsonl.py — stdin bridged via loop.connect_read_pipe + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | mcp_remote_py/stdio_jsonl.py | +| Source URL | https://glama.ai/mcp/servers/@elphinsnow/mcp-remote-py/blob/8ea9ac8b79f7ad844362b51b53f354834c9f5828/mcp_remote_py/stdio_jsonl.py | +| Source publisher / author | GitHub user elphinsnow (mcp-remote-py project), via glama.ai mirror | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +Source code from an MCP (Model Context Protocol) server implementation showing a Unix-only, thread-free pattern for bridging real stdin into asyncio: build an `asyncio.StreamReader` plus a `StreamReaderProtocol`, then call `loop.connect_read_pipe(lambda: protocol, sys.stdin.buffer)`. On Windows, the same codebase falls back to a blocking-readline-in-a-thread approach instead. + +## Key findings + +- `loop.connect_read_pipe(protocol_factory, pipe)` is the mechanism that turns an arbitrary readable file object into a proper `asyncio.StreamReader`-backed stream, integrated with the event loop's own backpressure and scheduling — not just a raw `add_reader` callback. +- This technique is explicitly Unix-only in the source reviewed; the same project uses a different (thread-based) strategy on Windows, acknowledging the platform split rather than pretending one approach covers both. + +## Pros / strengths + +- A concrete, working, real-world example of integrating stdin into asyncio with no background thread — directly reusable technique, not just a theoretical description. +- The explicit Windows fallback shows the author understood and documented the technique's platform limits rather than hiding them. + +## Cons / limitations / caveats + +- The dossier reflects a synthesis of this one file's approach, not a full read of the surrounding MCP server codebase or its test coverage. +- Applies the technique to real stdin; `tai_test.py` adapts the same mechanism to a different fd (a pty master), which was not something this source itself demonstrated — the adaptation is `tai_test.py`'s own design step, not something directly sourced here. + +## Relevance to tai + +This is the direct technical basis for `ManagedProcess.start()`'s pty-mode branch: rather than manually polling a pty master fd with raw `add_reader` callbacks and hand-rolled buffering, it wraps the master fd in an `asyncio.StreamReader` via `loop.connect_read_pipe(lambda: protocol, self._master_file)`, giving `wait_for_pattern()`/`drain_until_eof()` a uniform, native `StreamReader.read()` interface identical to the one `asyncio.subprocess.PIPE` streams already provide in pipe mode. This is what let `tai_test.py` expose one interface for both transports instead of two different ones. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/hermesedi-test-local-interrupt-cleanup.md b/research/async-pty-process-testing/hermesedi-test-local-interrupt-cleanup.md new file mode 100644 index 0000000..7cbacad --- /dev/null +++ b/research/async-pty-process-testing/hermesedi-test-local-interrupt-cleanup.md @@ -0,0 +1,28 @@ +# test_local_interrupt_cleanup.py — hermes-agent + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | test_local_interrupt_cleanup.py | +| Source URL | https://download.hermesedi.duckdns.org/.hermes/hermes-agent/tests/tools/test_local_interrupt_cleanup.py | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio create_subprocess_exec start_new_session" search query — a test file from another agent project specifically about interrupt/cleanup behavior; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/huggingface-openbb-test-session.md b/research/async-pty-process-testing/huggingface-openbb-test-session.md new file mode 100644 index 0000000..c2bbd86 --- /dev/null +++ b/research/async-pty-process-testing/huggingface-openbb-test-session.md @@ -0,0 +1,28 @@ +# OpenBB CLI test_session.py + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | test_session.py (OpenBB CLI tests) | +| Source URL | https://huggingface.co/spaces/CatPtain/OpenBB/blob/main/cli/tests/test_session.py | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio subprocess pty interactive CLI testing" search query as an example of CLI session testing code in a real project; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/huggingface-rag-code-assistant-asyncio-subprocess-doc.md b/research/async-pty-process-testing/huggingface-rag-code-assistant-asyncio-subprocess-doc.md new file mode 100644 index 0000000..32bdcfc --- /dev/null +++ b/research/async-pty-process-testing/huggingface-rag-code-assistant-asyncio-subprocess-doc.md @@ -0,0 +1,28 @@ +# rag-code-assistant: asyncio-subprocess.html (doc mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | asyncio-subprocess.html | +| Source URL | https://huggingface.co/spaces/parthtamu/rag-code-assistant/blob/main/docs/asyncio-subprocess.html | +| Source type | repository (mirrored documentation) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio subprocess pty interactive CLI testing" search query as another mirror/bundling of the stdlib asyncio-subprocess documentation (inside an unrelated RAG-assistant project's doc bundle); not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/iannuccisolutions-timeout-process-group.md b/research/async-pty-process-testing/iannuccisolutions-timeout-process-group.md new file mode 100644 index 0000000..1671471 --- /dev/null +++ b/research/async-pty-process-testing/iannuccisolutions-timeout-process-group.md @@ -0,0 +1,47 @@ +# Your timeout killed the process, not the process group — Iannucci Solutions + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Your timeout killed the process, not the process group | +| Source URL | https://www.iannuccisolutions.com/blog/timeout-does-not-kill-child-processes.html | +| Source publisher / author | Iannucci Solutions (blog) | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A blog post documenting a real incident: a script's timeout mechanism killed only the top-level process it had launched, while a worker process that script had itself spawned kept running in the background. The root cause and fix described are directly applicable to any subprocess-timeout design. + +## Key findings + +- `proc.kill()`/`proc.terminate()` signal only the direct child process — if that child spawns further children (a shell command, a subagent, a worker), killing the direct child does not propagate to them. +- The documented fix: start the child in its own session/process group (e.g. `start_new_session=True` in Python), which makes the child's PID equal to its process group ID (PGID), then signal the whole group (`os.killpg(pgid, sig)`) rather than just the one process. + +## Pros / strengths + +- A concrete, lived incident report, not a theoretical warning — the exact failure mode (worker alive after the "parent" was killed) is precisely the risk a test framework's timeout handling needs to guard against, since `tai.py` itself can spawn subagents and shell commands. + +## Cons / limitations / caveats + +- Describes the synchronous `subprocess` module's behavior; the asyncio adaptation (confirming `start_new_session=True` is forwarded correctly by `asyncio.create_subprocess_exec`) was treated as a reasonable extrapolation, not independently verified against asyncio's own documentation in this specific source. + +## Relevance to tai + +This is the direct, primary justification for `tai_test.py`'s `ManagedProcess` always spawning with `start_new_session=True` and `terminate()` using `os.killpg(self.proc.pid, sig)` rather than `self.proc.terminate()`. Without this, a timed-out test driving `tai.py` through a shell command or a forked subagent could leave that spawned work running indefinitely in the background after the test framework believed it had cleaned up. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/ipython-utils-io-mirror.md b/research/async-pty-process-testing/ipython-utils-io-mirror.md new file mode 100644 index 0000000..f9a7a7e --- /dev/null +++ b/research/async-pty-process-testing/ipython-utils-io-mirror.md @@ -0,0 +1,28 @@ +# IPython utils/io.py (mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | IPython utils/io.py | +| Source URL | https://mitra.stanford.edu/kundaje/marinovg/oak/various/programs/IPython/utils/io.py | +| Source type | repository (mirrored source) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio tee process output to file and stdout" search query as IPython's own I/O utility source code, on the same mirror site that surfaced CPython test files for other queries in this subject; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/kevinmccarthy-streaming-subprocess-blog.md b/research/async-pty-process-testing/kevinmccarthy-streaming-subprocess-blog.md new file mode 100644 index 0000000..1bf6a23 --- /dev/null +++ b/research/async-pty-process-testing/kevinmccarthy-streaming-subprocess-blog.md @@ -0,0 +1,28 @@ +# Streaming subprocess stdin and stdout with asyncio in Python + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Streaming subprocess stdin and stdout with asyncio in Python | +| Source URL | https://kevinmccarthy.org/2016/07/25/streaming-subprocess-stdin-and-stdout-with-asyncio-in-python/ | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio subprocess pty interactive CLI testing" search query as a blog post on the same general topic (streaming subprocess I/O with asyncio); not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/kiso-testing-pykiso-test-case.md b/research/async-pty-process-testing/kiso-testing-pykiso-test-case.md new file mode 100644 index 0000000..bb50112 --- /dev/null +++ b/research/async-pty-process-testing/kiso-testing-pykiso-test-case.md @@ -0,0 +1,28 @@ +# pykiso test_coordinator/test_case.py — kiso-testing docs + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | pykiso.test_coordinator.test_case | +| Source URL | https://kiso-testing.readthedocs.io/en/latest/_modules/pykiso/test_coordinator/test_case.html | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "python integration test framework design base class" search query as another embedded/hardware-testing framework's base test case module; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/kiwix-stackoverflow-run-external-command-async.md b/research/async-pty-process-testing/kiwix-stackoverflow-run-external-command-async.md new file mode 100644 index 0000000..ee7c6f6 --- /dev/null +++ b/research/async-pty-process-testing/kiwix-stackoverflow-run-external-command-async.md @@ -0,0 +1,28 @@ +# How can I run an external command asynchronously from Python? — Stack Overflow (Kiwix mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | How can I run an external command asynchronously from Python? | +| Source URL | https://dev.library.kiwix.org/content/stackoverflow_en_nopic_2021-08/questions/636561/how-can-i-run-an-external-command-asynchronously-from-python | +| Source type | forum thread (Stack Overflow, offline Kiwix mirror) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio merge multiple streams" search query as a general Stack Overflow question on running async external commands from Python, archived via a Kiwix offline mirror; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/medium-denismakogon-nonblocking-pipe.md b/research/async-pty-process-testing/medium-denismakogon-nonblocking-pipe.md new file mode 100644 index 0000000..c90125d --- /dev/null +++ b/research/async-pty-process-testing/medium-denismakogon-nonblocking-pipe.md @@ -0,0 +1,28 @@ +# Python 3: fight for nonblocking pipe — Medium + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Python 3: fight for nonblocking pipe | +| Source URL | https://medium.com/@denismakogon/python-3-fight-for-nonblocking-pipe-68f92429d18e | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio non-blocking stdin read" search query as a blog post describing difficulty getting non-blocking pipe reads working in Python 3; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/micropython-pr-4859.md b/research/async-pty-process-testing/micropython-pr-4859.md new file mode 100644 index 0000000..60166ac --- /dev/null +++ b/research/async-pty-process-testing/micropython-pr-4859.md @@ -0,0 +1,28 @@ +# MicroPython PR #4859 + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | MicroPython pull request #4859 | +| Source URL | https://github.com/micropython/micropython/pull/4859 | +| Source type | repository (pull request) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio non-blocking stdin read" search query. Off-target/tangential: MicroPython is a different, resource-constrained Python implementation for microcontrollers, not CPython's `asyncio` — logged honestly as a near-miss rather than silently dropped. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (off-target: different Python implementation/runtime). | diff --git a/research/async-pty-process-testing/oslotest-timeout-fixture.md b/research/async-pty-process-testing/oslotest-timeout-fixture.md new file mode 100644 index 0000000..cc2bdc8 --- /dev/null +++ b/research/async-pty-process-testing/oslotest-timeout-fixture.md @@ -0,0 +1,47 @@ +# OpenStack oslotest — timeout moved from base class into a fixture + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | oslotest/timeout.py, and companion base.py commit | +| Source URL | https://gitea10.opendev.org:3081/x/deb-oslotest/src/branch/master/oslotest/timeout.py | +| Source publisher / author | OpenStack project (oslotest) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +OpenStack's `oslotest` originally read a timeout value from an `OS_TEST_TIMEOUT` environment variable directly inside its base test class, then later moved that behavior into a separate, opt-in fixture (`fixtures.Timeout`) rather than something every test inherits unconditionally. + +## Key findings + +- The commit message for the change explicitly frames it as replacing inline base-class code that checked `OS_TEST_TIMEOUT` with a dedicated fixture, so tests opt into timeout behavior rather than being forced to inherit it. +- This is a structural decision about *where* configuration lives (base class vs. opt-in fixture), not a new timeout algorithm. + +## Pros / strengths + +- A real example of a mature test framework deliberately refactoring away from "everything lives in the base class" toward more granular, optional composition — relevant precedent for deciding how configurable a shared test base class should be. + +## Cons / limitations / caveats + +- Only the timeout-related commit and file were reviewed; the broader `oslotest` fixture ecosystem and how widely this opt-in pattern is used elsewhere in the project were not examined. + +## Relevance to tai + +Informed the decision to make `timeout` a per-`TestCase` class attribute (with a sensible default, overridable per test and via the `--timeout` CLI flag in `main_cli`) rather than a single hardcoded constant baked into the framework's execution loop — closer in spirit to oslotest's "configurable, not force-inherited" approach than to a one-size-fits-all timeout. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/pelicandd-nonblocking-stdin.md b/research/async-pty-process-testing/pelicandd-nonblocking-stdin.md new file mode 100644 index 0000000..5d049bc --- /dev/null +++ b/research/async-pty-process-testing/pelicandd-nonblocking-stdin.md @@ -0,0 +1,28 @@ +# Non-blocking reading from stdin in Python — blog.pelicandd.com + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Non-blocking reading from stdin in Python | +| Source URL | https://blog.pelicandd.com/article/191 | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio non-blocking stdin read" search query as a blog post directly on this topic; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/ptyprocess-docs.md b/research/async-pty-process-testing/ptyprocess-docs.md new file mode 100644 index 0000000..fd92d28 --- /dev/null +++ b/research/async-pty-process-testing/ptyprocess-docs.md @@ -0,0 +1,50 @@ +# Ptyprocess — Ptyprocess 0.7 documentation + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Ptyprocess — Ptyprocess 0.7 documentation | +| Source URL | https://ptyprocess.readthedocs.io/en/latest/ | +| Source publisher / author | ptyprocess maintainers (readthedocs project) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Ptyprocess is a Python library for spawning a child process attached to a real pseudo-terminal (pty) rather than plain pipes. Its documentation describes the behavioral guarantees a real pty gives you that a pipe cannot: unified stdio, window-size tracking with SIGWINCH delivery, and line-discipline signal generation (Ctrl-C becomes SIGINT). + +## Key findings + +- When a program runs under ptyprocess, all three of its standard streams (stdin/stdout/stderr) are connected to a single pty, not three separate pipes. +- Ptyprocess tracks the terminal's window size and notifies the child process with a SIGWINCH signal when it changes. +- In cooked (line-buffered) terminal mode, the kernel's tty line discipline turns a Ctrl-C keystroke (byte 0x03) into a SIGINT delivered to the foreground process group — this only happens because the device is a real pty, not a pipe. + +## Pros / strengths + +- Documents, in one place, exactly which "feels like a real terminal" behaviors a pty provides that a pipe does not — this is the core justification for why `tai_test.py` needed pty support at all, since `tai.py`'s own code branches on `sys.stdin.isatty()`. +- The SIGWINCH and line-discipline-SIGINT points are concrete, testable claims, not vague marketing. + +## Cons / limitations / caveats + +- The dossier is based on a web-search synthesis of the documentation, not a full read of the library's source or a hands-on trial of ptyprocess itself — `tai_test.py` does not depend on ptyprocess as a library, only on the general pty behavior it documents (which was reimplemented directly against the stdlib `pty` module). +- Says nothing about asyncio integration; ptyprocess itself is a synchronous/blocking library. + +## Relevance to tai + +Directly informed the decision to give `tai_test.py`'s pty mode a real pseudo-terminal rather than a pipe, since `tai.py` branches on `isatty()` for approval prompts and readline behavior. The SIGWINCH/Ctrl-C-SIGINT behaviors this source describes are explicitly **not** relied on by `tai_test.py` — see the companion dossier on the `start_new_session`/controlling-terminal pitfall (`github-agents-issue1-controlling-terminal.md`) and the framework's own documented limitation in its module docstring. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/py-toolbox-io-module-docs.md b/research/async-pty-process-testing/py-toolbox-io-module-docs.md new file mode 100644 index 0000000..7ee2fa2 --- /dev/null +++ b/research/async-pty-process-testing/py-toolbox-io-module-docs.md @@ -0,0 +1,28 @@ +# py-toolbox io module docs + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | pytb.io module | +| Source URL | https://py-toolbox.readthedocs.io/en/latest/modules/io.html | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio tee process output to file and stdout" search query as a general-purpose I/O utility module's documentation; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/pypi-asyncio-pipe.md b/research/async-pty-process-testing/pypi-asyncio-pipe.md new file mode 100644 index 0000000..8838062 --- /dev/null +++ b/research/async-pty-process-testing/pypi-asyncio-pipe.md @@ -0,0 +1,28 @@ +# asyncio-pipe — PyPI + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | asyncio-pipe | +| Source URL | https://pypi.org/project/asyncio-pipe/ | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio non-blocking stdin read" search query as a small package apparently dedicated to pipe/stdin integration with asyncio; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/pypi-ckill.md b/research/async-pty-process-testing/pypi-ckill.md new file mode 100644 index 0000000..c7c0f84 --- /dev/null +++ b/research/async-pty-process-testing/pypi-ckill.md @@ -0,0 +1,28 @@ +# ckill 1.0.1 — PyPI + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | ckill 1.0.1 | +| Source URL | https://pypi.org/project/ckill/1.0.1/ | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio subprocess timeout kill process group" search query as a small package apparently dedicated to child-process killing; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/pypi-pexpect-version-mirrors.md b/research/async-pty-process-testing/pypi-pexpect-version-mirrors.md new file mode 100644 index 0000000..3da5bbe --- /dev/null +++ b/research/async-pty-process-testing/pypi-pexpect-version-mirrors.md @@ -0,0 +1,28 @@ +# pexpect — version-pinned PyPI mirrors + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | pexpect 4.0.1 (and other version-pinned mirrors) | +| Source URL | https://pypi.org/project/pexpect/4.0.1/ | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "pexpect pyte vs asyncio pty" search query: several version-pinned PyPI pages for pexpect all appeared in the same result set — 4.0.1 (listed as the primary URL above), 4.0 (https://pypi.org/project/pexpect/4.0/), 3.2 (https://pypi.python.org/pypi/pexpect/3.2), 4.1.0 (https://pypi.org/project/pexpect/4.1.0), and a CERN package-mirror listing of 4.9.0 (https://simple-repository.app.cern.ch/project/pexpect). These are redundant version/mirror listings of the same project already covered at Tier 1 in `pypi-pexpect.md`, not independent sources, so they are logged together as one card rather than five separate ones. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (redundant version mirrors of a Tier 1 source). | diff --git a/research/async-pty-process-testing/pypi-pexpect.md b/research/async-pty-process-testing/pypi-pexpect.md new file mode 100644 index 0000000..814307e --- /dev/null +++ b/research/async-pty-process-testing/pypi-pexpect.md @@ -0,0 +1,50 @@ +# pexpect — PyPI project page + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | pexpect | +| Source URL | https://pypi.org/project/pexpect | +| Source publisher / author | pexpect maintainers (PyPI) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +pexpect is a well-established pure-Python library for spawning child applications, controlling them, and responding to expected patterns in their output. Its own documentation explicitly lists automated software testing as a use case. It is built on the stdlib `pty` module (Unix-only for its main features) and is a blocking/synchronous library. + +## Key findings + +- pexpect's documented purpose statement explicitly includes "automated software testing" alongside general automation. +- Its core features require the stdlib `pty` module, which limits full functionality to Unix-like systems (some features — waiting for patterns from file descriptors or subprocesses — are also available on Windows, per the project's own docs). +- It is blocking for async I/O (a third-party comparison table makes this claim, though the research flagged that table as coming from a competing library's vendor, i.e. a potentially biased source — still consistent with pexpect's own synchronous API design). + +## Pros / strengths + +- The most mature, widely-used library in this space, with a long track record specifically for testing interactive terminal programs. +- `expect()`-style pattern matching against output is exactly the turn-boundary-detection problem `tai_test.py` needed to solve for REPL-mode testing. + +## Cons / limitations / caveats + +- Synchronous/blocking — adopting it inside `tai_test.py` would have meant either running it in a thread executor on every call or accepting that it blocks the event loop, neither of which fit an asyncio-native framework cleanly. +- Not independently trialed in this research pass; findings are based on its documented feature list and use-case statement, not hands-on testing of its API against `tai.py`. + +## Relevance to tai + +Surveyed and explicitly not adopted. `tai_test.py`'s `ManagedProcess.wait_for_pattern()` is functionally pexpect's `expect()` reimplemented natively in asyncio (regex matching against an accumulating, ANSI-stripped buffer read from an `asyncio.StreamReader`), chosen specifically so the whole framework stays asyncio-native rather than mixing in a blocking dependency. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/pypi-shellous-0.4.0.md b/research/async-pty-process-testing/pypi-shellous-0.4.0.md new file mode 100644 index 0000000..5e81ad3 --- /dev/null +++ b/research/async-pty-process-testing/pypi-shellous-0.4.0.md @@ -0,0 +1,28 @@ +# shellous 0.4.0 — older version listing + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | shellous 0.4.0 | +| Source URL | https://pypi.org/project/shellous/0.4.0 | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio tee process output to file and stdout" search query — an older version listing of `shellous`, already covered at Tier 1 in `pypi-shellous.md`. Logged as a redundant version mirror, not an independent source. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (version mirror of a Tier 1 source). | diff --git a/research/async-pty-process-testing/pypi-shellous.md b/research/async-pty-process-testing/pypi-shellous.md new file mode 100644 index 0000000..c86ec4a --- /dev/null +++ b/research/async-pty-process-testing/pypi-shellous.md @@ -0,0 +1,50 @@ +# shellous — Async Processes and Pipelines + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | shellous: Async Processes and Pipelines | +| Source URL | https://pypi.org/project/shellous | +| Source publisher / author | shellous project (PyPI) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +shellous is an asyncio-native subprocess library. Its feature list includes running a program attached to a pseudo-terminal (pty, Unix-only) and `send()`/`expect()` methods for interactively driving a subprocess by pattern-matching its output — functionally the closest existing library to what `tai_test.py` ended up building itself. + +## Key findings + +- shellous explicitly supports pty-backed subprocess execution, and documents that this is Unix-only. +- It offers `send()`/`expect()` primitives, i.e. "write input, then wait for a pattern in the output" — the same shape as `tai_test.py`'s `ManagedProcess.send_line()` + `wait_for_pattern()`. +- It is built on asyncio, unlike pexpect (see `pypi-pexpect.md`), which is synchronous. + +## Pros / strengths + +- Proof that the exact combination `tai_test.py` needed — asyncio-native, pty-capable, expect-style interaction — is a real, previously-solved design space, not a one-off idea. +- Having `send()`/`expect()` as named primitives validates `tai_test.py`'s own `send_line()`/`wait_for_pattern()` naming and shape. + +## Cons / limitations / caveats + +- Only the top-level feature list was reviewed, not the actual implementation or its API stability/maturity; no version number, changelog, or usage examples were independently inspected. +- `tai_test.py` did not adopt shellous as a dependency — it is pure stdlib (`pty`, `asyncio`) by design, consistent with the `tai.py` project's own zero-dependency rule, so this source served as validation of the approach rather than as a library to use. + +## Relevance to tai + +Confirmed that building a pty-capable, asyncio-native, expect-style subprocess controller is a well-trodden pattern, which gave confidence in `tai_test.py`'s own from-scratch implementation (`ManagedProcess`) rather than needing to invent an unprecedented design. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/pypi-stitch-pty.md b/research/async-pty-process-testing/pypi-stitch-pty.md new file mode 100644 index 0000000..1fa64c2 --- /dev/null +++ b/research/async-pty-process-testing/pypi-stitch-pty.md @@ -0,0 +1,48 @@ +# stitch-pty — PyPI project page + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | stitch-pty | +| Source URL | https://pypi.org/project/stitch-pty/ | +| Source publisher / author | stitch-pty project (PyPI) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Low | +| Status | Active | + +## Summary + +stitch-pty is described as a cross-platform, async pseudo-terminal library for Python with integrated terminal emulation, implemented in Rust with Python bindings. The research flagged it as a young project it could not independently verify further. + +## Key findings + +- Claims cross-platform support (notably including Windows, unlike the stdlib `pty` module), unlike every other option surveyed in this subject. +- Implemented in Rust with bindings — a compiled-extension dependency, not a pure-Python one. + +## Pros / strengths + +- If its claims hold up, it would solve the Windows-support gap that every stdlib-`pty`-based approach (including `tai_test.py`'s own) has. + +## Cons / limitations / caveats + +- Explicitly flagged during research as young and unverified — no maturity signal (adoption, release history, test coverage) was found or checked. +- A compiled Rust extension is a real dependency, directly in tension with `tai.py`'s own zero-dependency, pure-stdlib project constraint that `tai_test.py` was built to respect. + +## Relevance to tai + +Surveyed and explicitly not adopted, for two independent reasons: insufficient verified maturity, and a hard conflict with the project's stdlib-only constraint. `tai_test.py` built its pty support directly on the stdlib `pty` module instead, accepting the resulting Windows limitation (POSIX-only, `pty` module is not available on Windows) as the simpler, dependency-free tradeoff. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/pyte-terminal-emulator.md b/research/async-pty-process-testing/pyte-terminal-emulator.md new file mode 100644 index 0000000..b51f8f5 --- /dev/null +++ b/research/async-pty-process-testing/pyte-terminal-emulator.md @@ -0,0 +1,48 @@ +# python3-pyte — Debian package page + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | python3-pyte | +| Source URL | https://packages.debian.org/python3-pyte | +| Source publisher / author | pyte maintainers, packaged for Debian | +| Source type | repository (package page) | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +pyte is a simple VT100/VTxxx-compatible terminal emulator written in Python 3. Unlike pexpect or shellous, it does not spawn or control processes itself — it is fed raw bytes (that some other program wrote to a terminal) and maintains a rendered screen buffer you can then assert against (cursor position, character colors, line contents). + +## Key findings + +- pyte's job is purely to interpret terminal control sequences and produce a rendered screen state, not to manage any process lifecycle. +- This makes it suited to checking what a TUI actually *displays* (cursor position, colors, screen layout) rather than just what raw bytes a program emitted. + +## Pros / strengths + +- Solves a different, complementary problem to `tai_test.py`'s own scope: verifying rendered screen state, not just text content. + +## Cons / limitations / caveats + +- Adds no value for `tai_test.py`'s actual tests, which assert on text content (e.g. does a reply contain `mem:<id>`), never on cursor position or rendered color — using pyte would have been complexity with no corresponding benefit for the tests actually written. +- Findings are based on general knowledge of what pyte does plus the package page's description, not a hands-on trial. + +## Relevance to tai + +Considered and explicitly not needed. If a future integration test ever needs to assert on *rendered* terminal output (e.g. verifying a specific color or cursor position in `tai.py`'s colored REPL prompt, rather than just its text), pyte is the documented tool to reach for — but no current test in `tests/` requires this, so it was not adopted. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/python-asyncio-issue499-add-reader-eof.md b/research/async-pty-process-testing/python-asyncio-issue499-add-reader-eof.md new file mode 100644 index 0000000..0fdf311 --- /dev/null +++ b/research/async-pty-process-testing/python-asyncio-issue499-add-reader-eof.md @@ -0,0 +1,48 @@ +# `loop.add_reader` and `<<EOF` · Issue #499 · python/asyncio + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | `loop.add_reader` and `<<EOF` · Issue #499 | +| Source URL | https://github.com/python/asyncio/issues/499 | +| Source publisher / author | python/asyncio project (GitHub) | +| Source type | repository (issue) | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +An upstream issue in the (now-archived, pre-stdlib-merge) `python/asyncio` project discussing edge cases of `loop.add_reader` combined with shell here-document (`<<EOF`) input and EOF handling on stdin. + +## Key findings + +- `add_reader`/stdin EOF handling is treated as a known rough edge in asyncio's own issue tracker, not a fully solved, documented-everywhere corner of the API. +- The issue's existence itself is evidence that different stdin-feeding mechanisms (interactive shell vs. here-doc vs. piped file) can surface different edge-case behavior with `add_reader`. + +## Pros / strengths + +- Primary-source confirmation (from the asyncio project itself) that this is a genuinely tricky area, not something `tai_test.py`'s own testing was wrong to be cautious about. + +## Cons / limitations / caveats + +- This dossier is based on a web-search synthesis of the issue's existence and general topic, not a full read of the issue thread's technical discussion or resolution — the specific EOF-handling edge case it describes was not independently verified against `tai_test.py`'s own `StdinBridge` implementation. +- The issue predates asyncio's merge into the stdlib and may reference an older API surface. + +## Relevance to tai + +Reinforced the decision to keep `StdinBridge.start()` defensive: it returns `False` (no-op) when `sys.stdin.isatty()` is false, rather than attempting `add_reader` against every possible stdin configuration (pipes, here-docs, redirected files) and hoping EOF behaves consistently. `tai_test.py` only ever activates the live-stdin bridge when a real interactive terminal is confirmed present. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/python-docs-asyncio-subprocess.md b/research/async-pty-process-testing/python-docs-asyncio-subprocess.md new file mode 100644 index 0000000..305cb2f --- /dev/null +++ b/research/async-pty-process-testing/python-docs-asyncio-subprocess.md @@ -0,0 +1,48 @@ +# Subprocesses — Python 3 documentation (asyncio.subprocess) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Subprocesses — Python 3.14 documentation | +| Source URL | https://docs.python.org/3/library/asyncio-subprocess.html | +| Source publisher / author | Python Software Foundation | +| Source type | standard (official stdlib documentation) | +| Source date (published) | Unknown — not stated (continuously updated) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +The canonical stdlib reference for `asyncio.subprocess`, confirming the exact semantics of `terminate()`/`kill()`, how `Process.stdout`/`Process.stderr` expose `StreamReader` instances when `PIPE` is used, and the deadlock risk of reading those streams manually instead of via `communicate()`. + +## Key findings + +- On POSIX, `Process.terminate()` sends SIGTERM to the child; `Process.kill()` sends SIGKILL. On Windows, `terminate()` calls the Win32 `TerminateProcess` API; there is no separate POSIX-signal distinction. +- When `stdout=PIPE` or `stderr=PIPE` is passed, `Process.stdout`/`Process.stderr` are `asyncio.StreamReader` instances — the same abstraction `tai_test.py` wraps a pty master fd into via `loop.connect_read_pipe`, for a uniform interface across both transports. +- The docs warn that reading those streams manually (rather than via `communicate()`) can deadlock, because the child can block on a full pipe buffer if its output isn't drained quickly enough while it's also waiting on something else. + +## Pros / strengths + +- Authoritative, primary-source documentation — the single most load-bearing source in this entire subject for API semantics. + +## Cons / limitations / caveats + +- Documents stdlib behavior as of whatever version was fetched; does not itself discuss process-group-wide termination (`killpg`) or the controlling-terminal/pty nuances this subject's other sources cover — those required separate sources. + +## Relevance to tai + +The definitive basis for `tai_test.py`'s choice of `terminate()` (SIGTERM) before escalating to `kill()` (SIGKILL) in `ManagedProcess.terminate()`'s escalation loop, and for exposing `Process.stdout`/`Process.stderr` as the pipe-mode reader rather than hand-rolling a reader over raw fds in that transport. The deadlock warning is why `ManagedProcess.drain_until_eof()` continuously reads in a loop until EOF rather than attempting a single large read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/python-tulip-google-groups-stdin.md b/research/async-pty-process-testing/python-tulip-google-groups-stdin.md new file mode 100644 index 0000000..01bb6a3 --- /dev/null +++ b/research/async-pty-process-testing/python-tulip-google-groups-stdin.md @@ -0,0 +1,28 @@ +# Interacting with sys.stdin/sys.stdout/sys.stderr — python-tulip Google Group + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Interacting with sys.stdin/sys.stdout/sys.stderr | +| Source URL | https://groups.google.com/g/python-tulip/c/6tRswyRhwE4 | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio non-blocking stdin read" search query — "python-tulip" was asyncio's pre-stdlib-merge codename, so this is an early-era discussion of the exact same stdin-integration problem; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/python373-test-asyncio-subprocess-source.md b/research/async-pty-process-testing/python373-test-asyncio-subprocess-source.md new file mode 100644 index 0000000..00432e7 --- /dev/null +++ b/research/async-pty-process-testing/python373-test-asyncio-subprocess-source.md @@ -0,0 +1,28 @@ +# CPython 3.7.3 Lib/test/test_asyncio/test_subprocess.py (mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Lib/test/test_asyncio/test_subprocess.py (CPython 3.7.3) | +| Source URL | https://mitra.stanford.edu/kundaje/marinovg/oak/programs/Python-3.7.3/Lib/test/test_asyncio/test_subprocess.py | +| Source type | repository (mirrored stdlib source) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio subprocess pty interactive CLI testing" search query, same mirror site as `python373-test-pty-source.md`, this time CPython's own `asyncio.subprocess` test suite; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/python373-test-pty-source.md b/research/async-pty-process-testing/python373-test-pty-source.md new file mode 100644 index 0000000..f6a79e5 --- /dev/null +++ b/research/async-pty-process-testing/python373-test-pty-source.md @@ -0,0 +1,28 @@ +# CPython 3.7.3 Lib/test/test_pty.py (mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Lib/test/test_pty.py (CPython 3.7.3) | +| Source URL | https://mitra.stanford.edu/kundaje/marinovg/oak/programs/Python-3.7.3/Lib/test/test_pty.py | +| Source type | repository (mirrored stdlib source) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio subprocess pty interactive CLI testing" search query as a mirror of CPython's own stdlib `pty` module test suite; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/pythonlib-ru-pexpect-article.md b/research/async-pty-process-testing/pythonlib-ru-pexpect-article.md new file mode 100644 index 0000000..8552d39 --- /dev/null +++ b/research/async-pty-process-testing/pythonlib-ru-pexpect-article.md @@ -0,0 +1,28 @@ +# PEXPECT - Automation of the terminal — pythonlib.ru + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | PEXPECT - Automation of the terminal | +| Source URL | https://pythonlib.ru/en/library-theme75 | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "pexpect pyte vs asyncio pty" search query as an introductory article on pexpect; the primary pexpect source used for Tier 1 is `pypi-pexpect.md`, so this secondary article was not individually opened or quoted. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/pythonsheets-asyncio-advanced.md b/research/async-pty-process-testing/pythonsheets-asyncio-advanced.md new file mode 100644 index 0000000..de53503 --- /dev/null +++ b/research/async-pty-process-testing/pythonsheets-asyncio-advanced.md @@ -0,0 +1,28 @@ +# python-asyncio-advanced — Python Sheets + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | python-asyncio-advanced | +| Source URL | https://pythonsheets.com/_sources/notes/asyncio/python-asyncio-advanced.rst | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio merge multiple streams" search query as a general advanced-asyncio cheat-sheet; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/runxbuild-wait-communicate-poll-blog.md b/research/async-pty-process-testing/runxbuild-wait-communicate-poll-blog.md new file mode 100644 index 0000000..eeb4f84 --- /dev/null +++ b/research/async-pty-process-testing/runxbuild-wait-communicate-poll-blog.md @@ -0,0 +1,28 @@ +# Waiting in Python: wait(), communicate(), poll(), and the Deadlock Between Them + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Waiting in Python: wait(), communicate(), poll(), and the Deadlock Between Them | +| Source URL | https://www.runxbuild.com/blog/wait-command-python/ | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio subprocess timeout kill process group" search query as a blog post specifically about the deadlock risks between `subprocess`'s `wait()`/`communicate()`/`poll()` methods — a topic adjacent to, but not individually opened/quoted for, the Tier 1 findings on that risk in `python-docs-asyncio-subprocess.md`. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/rust-users-forum-stdin-loop.md b/research/async-pty-process-testing/rust-users-forum-stdin-loop.md new file mode 100644 index 0000000..c8cf392 --- /dev/null +++ b/research/async-pty-process-testing/rust-users-forum-stdin-loop.md @@ -0,0 +1,28 @@ +# How to read stdin without stopping a loop? — Rust Users Forum + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | How to read stdin without stopping a loop? | +| Source URL | https://users.rust-lang.org/t/how-to-read-stdin-without-stopping-a-loop/29372 | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio non-blocking stdin read" search query. Off-target/tangential: this is a Rust forum discussing Rust's own stdin-handling APIs, not Python or asyncio — logged honestly as a near-miss rather than silently dropped, per this archive's quality bar of not pretending every search result was on-topic. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed (off-target: wrong language). | diff --git a/research/async-pty-process-testing/simonwillison-til-subprocess-time-limit.md b/research/async-pty-process-testing/simonwillison-til-subprocess-time-limit.md new file mode 100644 index 0000000..b0de209 --- /dev/null +++ b/research/async-pty-process-testing/simonwillison-til-subprocess-time-limit.md @@ -0,0 +1,28 @@ +# Running Python code in a subprocess with a time limit — Simon Willison's TIL + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Running Python code in a subprocess with a time limit | +| Source URL | https://til.simonwillison.net/python/subprocess-time-limit | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio subprocess timeout kill process group" search query as a "today I learned" note on enforcing a subprocess time limit; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/slingacademy-stop-kill-child-process.md b/research/async-pty-process-testing/slingacademy-stop-kill-child-process.md new file mode 100644 index 0000000..0dbfac8 --- /dev/null +++ b/research/async-pty-process-testing/slingacademy-stop-kill-child-process.md @@ -0,0 +1,46 @@ +# Python asyncio: How to stop/kill a child process — Sling Academy + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Python asyncio: How to stop/kill a child process | +| Source URL | https://www.slingacademy.com/article/python-asyncio-how-to-stop-kill-a-child-process/ | +| Source publisher / author | Sling Academy | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A practical how-to article confirming the terminate()-then-kill() escalation pattern specifically for `asyncio` subprocesses (as opposed to the synchronous `subprocess` module, which several other sources in this subject focus on). + +## Key findings + +- Confirms the same two-stage (SIGTERM, then SIGKILL after a grace period) escalation pattern applies directly to `asyncio.subprocess.Process`, not just the synchronous `subprocess.Popen`. + +## Pros / strengths + +- Asyncio-specific confirmation, closing a small gap left by sources that primarily discuss the synchronous `subprocess` module. + +## Cons / limitations / caveats + +- A how-to-style article without deep discussion of process groups or zombie-reaping; those details came from other sources in this subject (`iannuccisolutions-timeout-process-group.md`, `alexandra-zaharia-kill-subprocess-children-timeout.md`). + +## Relevance to tai + +One more confirming data point that `ManagedProcess.terminate()`'s SIGTERM-then-SIGKILL escalation is correct for the asyncio subprocess API specifically, not just a synchronous-API pattern being assumed to transfer. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/async-pty-process-testing/tessl-io-sh-docs.md b/research/async-pty-process-testing/tessl-io-sh-docs.md new file mode 100644 index 0000000..80549c4 --- /dev/null +++ b/research/async-pty-process-testing/tessl-io-sh-docs.md @@ -0,0 +1,28 @@ +# tessl.io — sh I/O handling docs + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | io-handling.md (pypi-sh package docs) | +| Source URL | https://tessl.io/registry/tessl/pypi-sh/2.2.0/files/docs/io-handling.md | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio tee process output to file and stdout" search query as documentation for the `sh` package's I/O handling; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/async-pty-process-testing/tutorialspoint-redirect-process-output.md b/research/async-pty-process-testing/tutorialspoint-redirect-process-output.md new file mode 100644 index 0000000..492776e --- /dev/null +++ b/research/async-pty-process-testing/tutorialspoint-redirect-process-output.md @@ -0,0 +1,28 @@ +# Redirect output of process to a file and streams — TutorialsPoint + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | async-pty-process-testing | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Redirect output of process to a file and streams | +| Source URL | https://www.tutorialspoint.com/article/redirect-output-of-process-to-a-file-and-streams | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "asyncio tee process output to file and stdout" search query as a general article on redirecting process output; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/audit.py b/research/audit.py new file mode 100755 index 0000000..459a190 --- /dev/null +++ b/research/audit.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""research/audit.py — verify the research archive is internally consistent. + +Run after any research session, and any time someone asks "is research/ up +to date": `python3 research/audit.py`. Stdlib only, no dependencies. + +Checks, per subject folder: + - every source .md has a Tier metadata row (1 or 2) and the required + Source URL / Document type fields; + - every Tier 1 dossier has non-empty Pros/Cons sections (an empty one is + the "wasn't actually read critically" smell research/README.md calls out); + - the counts in this script's own scan match what research/INDEX.md claims + (catches INDEX.md drift, the thing that bit this archive once already). + +This does NOT check whether a research task that happened got archived at +all — that failure mode (skipping archival silently) is caught by the +PostToolUse/WebSearch hook in .claude/settings.json, not by auditing files +that were never written. This script only audits what's already on disk. +""" +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +TIER_RE = re.compile(r"^\|\s*Tier\s*\|\s*(1|2|N/A)\s*\|", re.MULTILINE) +DOCTYPE_RE = re.compile(r"^\|\s*Document type\s*\|\s*(.+?)\s*\|", re.MULTILINE) +SOURCE_URL_RE = re.compile(r"^\|\s*Source URL\s*\|", re.MULTILINE) +INDEX_SUBJECT_ROW_RE = re.compile( + r"\[`([a-z0-9-]+)`\]\([a-z0-9-]+/\)\s*\|\s*(\d+)\s*\(\s*(\d+)\s*/\s*(\d+)\s*\)\s*\|\s*(\d+)\s*\|" +) + + +def subject_dirs(): + return sorted(p for p in ROOT.iterdir() if p.is_dir() and p.name != "conclusions") + + +def scan_subject(path): + sources = sorted(p for p in path.glob("*.md")) + conclusions = sorted((path / "conclusions").glob("*.md")) if (path / "conclusions").is_dir() else [] + tier1 = tier2 = 0 + problems = [] + for src in sources: + text = src.read_text(encoding="utf-8", errors="replace") + tier_match = TIER_RE.search(text) + if not tier_match: + problems.append("%s: no 'Tier' metadata row" % src.name) + continue + tier = tier_match.group(1) + if tier == "1": + tier1 += 1 + if "## Pros / strengths" in text and "## Cons / limitations / caveats" in text: + pros = text.split("## Pros / strengths", 1)[1].split("## Cons", 1)[0].strip() + cons = text.split("## Cons / limitations / caveats", 1)[1].split("## Relevance", 1)[0].strip() + if not pros: + problems.append("%s: Tier 1 but empty Pros section" % src.name) + if not cons: + problems.append("%s: Tier 1 but empty Cons section" % src.name) + else: + problems.append("%s: Tier 1 missing Pros/Cons sections entirely" % src.name) + if not SOURCE_URL_RE.search(text): + problems.append("%s: Tier 1 missing Source URL field" % src.name) + elif tier == "2": + tier2 += 1 + else: + problems.append("%s: unexpected Tier value" % src.name) + for conc in conclusions: + text = conc.read_text(encoding="utf-8", errors="replace") + for required in ("## Question / problem statement", "## Conclusion", "## Action taken"): + if required not in text: + problems.append("%s: missing required section %r" % (conc.name, required)) + return { + "name": path.name, + "sources": len(sources), + "tier1": tier1, + "tier2": tier2, + "conclusions": len(conclusions), + "problems": problems, + } + + +def parse_index_claims(): + index_path = ROOT / "INDEX.md" + if not index_path.exists(): + return {} + text = index_path.read_text(encoding="utf-8", errors="replace") + claims = {} + for match in INDEX_SUBJECT_ROW_RE.finditer(text): + subject, total, t1, t2, conclusions = match.groups() + claims[subject] = {"sources": int(total), "tier1": int(t1), "tier2": int(t2), "conclusions": int(conclusions)} + return claims + + +def main(): + subjects = subject_dirs() + if not subjects: + print("no subject folders found under research/ — nothing to audit") + return 0 + + claims = parse_index_claims() + exit_code = 0 + total_sources = total_tier1 = total_tier2 = total_conclusions = 0 + + for path in subjects: + report = scan_subject(path) + total_sources += report["sources"] + total_tier1 += report["tier1"] + total_tier2 += report["tier2"] + total_conclusions += report["conclusions"] + + print("%s: %d sources (T1=%d T2=%d), %d conclusions" % ( + report["name"], report["sources"], report["tier1"], report["tier2"], report["conclusions"] + )) + + for problem in report["problems"]: + print(" PROBLEM: %s" % problem) + exit_code = 1 + + claim = claims.get(report["name"]) + if claim is None: + print(" PROBLEM: INDEX.md has no row for this subject") + exit_code = 1 + elif claim != {"sources": report["sources"], "tier1": report["tier1"], "tier2": report["tier2"], "conclusions": report["conclusions"]}: + print(" PROBLEM: INDEX.md claims %s, disk has sources=%d (T1=%d T2=%d) conclusions=%d" + % (claim, report["sources"], report["tier1"], report["tier2"], report["conclusions"])) + exit_code = 1 + + print("\ntotal: %d subjects, %d sources (T1=%d T2=%d), %d conclusions" % ( + len(subjects), total_sources, total_tier1, total_tier2, total_conclusions + )) + print("PASS" if exit_code == 0 else "FAIL -- see PROBLEM lines above") + return exit_code + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/research/shell-multiplexer-auto-attach/1devtool-terminal-notifier-ai-agents.md b/research/shell-multiplexer-auto-attach/1devtool-terminal-notifier-ai-agents.md new file mode 100644 index 0000000..fb2d304 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/1devtool-terminal-notifier-ai-agents.md @@ -0,0 +1,28 @@ +# Terminal Notifier for AI Agents (1devtool.com) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Terminal Notifier for AI Agents | +| Source URL | https://1devtool.com/blog/terminal-notifier-for-ai-agents | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the automation-detection query; tangentially related (notifications about AI agent terminal activity, not detection of agent terminals per se), not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/acchapm1-tmux-claude-code-deepdive.md b/research/shell-multiplexer-auto-attach/acchapm1-tmux-claude-code-deepdive.md new file mode 100644 index 0000000..153d2f9 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/acchapm1-tmux-claude-code-deepdive.md @@ -0,0 +1,28 @@ +# Tmux + Claude Code — Deep Dive + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Tmux + Claude Code — Deep Dive | +| Source URL | https://acchapm1.github.io/tutorials/Tmux-Claude-Code/tmux-claude-code-deep-dive | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the tmux-architecture query; looked potentially relevant given it specifically covers tmux alongside an AI coding agent, directly adjacent to this subject's topic. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/android-adb-cfmakeraw-commit.md b/research/shell-multiplexer-auto-attach/android-adb-cfmakeraw-commit.md new file mode 100644 index 0000000..65750b0 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/android-adb-cfmakeraw-commit.md @@ -0,0 +1,47 @@ +# Android adb — Raw Mode via cfmakeraw (commit a826579d) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | adb: platform/packages/modules/adb commit a826579d | +| Source URL | https://android.googlesource.com/platform/packages/modules/adb/+/a826579d%5E%21 | +| Source publisher / author | Android Open Source Project (Google) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A real, shipped commit in Android's `adb` tool showing the standard raw-mode pattern: save the current `termios` state for the local terminal, apply `cfmakeraw` to a *copy* of it, and restore the original state on exit — including on signal-driven exit paths, not just normal return. + +## Key findings + +- `cfmakeraw` is applied to a saved copy, never mutating the live state in place without a saved original to restore from. +- Restoration must happen on every exit path, explicitly including signals — a program that puts the terminal in raw mode and then dies to an uncaught signal without restoring leaves the user's shell visibly broken (no echo, no line editing) until they run `reset` or `stty sane`. + +## Pros / strengths + +- Production code from a widely-used, heavily-tested tool (`adb`) rather than a toy example — this is the pattern battle-tested at scale. + +## Cons / limitations / caveats + +- A single commit view; doesn't show the surrounding signal-handler wiring in full, only confirms the save/apply/restore shape exists and is deliberate. + +## Relevance to tai + +The exact pattern any raw-mode terminal handling inside a tai-managed session (if tai ever needs to read raw keystrokes itself, rather than delegating entirely to tmux) must follow: save-copy-restore, restoration guaranteed on every exit path including signals, or a bug can leave the user's real terminal broken after the fact. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/arcolinux-tmux-servers-sessions.md b/research/shell-multiplexer-auto-attach/arcolinux-tmux-servers-sessions.md new file mode 100644 index 0000000..6ec743d --- /dev/null +++ b/research/shell-multiplexer-auto-attach/arcolinux-tmux-servers-sessions.md @@ -0,0 +1,28 @@ +# Everything You Need to Know About tmux Servers and Sessions (ArcoLinux) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Everything you need to know about tmux | +| Source URL | https://arcolinux.com/everthing-you-need-to-know-about-tmux-servers-and-sessions/ | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the tmux-architecture query as a general tmux servers/sessions explainer; not independently opened since the man7 and tao-of-tmux dossiers already cover this ground with individually-attributed detail. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/bash-manual-interactive-shell-definition.md b/research/shell-multiplexer-auto-attach/bash-manual-interactive-shell-definition.md new file mode 100644 index 0000000..a69d20d --- /dev/null +++ b/research/shell-multiplexer-auto-attach/bash-manual-interactive-shell-definition.md @@ -0,0 +1,47 @@ +# Is This Shell Interactive? (Bash manual, via w3.pppl.gov mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | 5.3: Is This Shell Interactive? | +| Source URL | https://w3.pppl.gov/info/bash/Is_This_Shell_Interactive$3f.html | +| Source publisher / author | GNU Bash manual (mirrored by w3.pppl.gov) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +Bash's own official definition: an interactive shell is one whose input and output are both connected to a terminal (as determined by `isatty(3)`), **or** one explicitly started with the `-i` option. This is the ground truth every other heuristic in this subject is approximating. + +## Key findings + +- The `-i` clause matters: a shell can be interactive even with redirected stdin/stdout if explicitly started with `-i` — a pure `isatty()` check on both streams would miss this case. +- Conversely, a shell can have both stdin and stdout on a real terminal and still not be "interactive" in bash's sense if started non-interactively with neither `-i` nor a terminal-backed invocation that sets it. + +## Pros / strengths + +- This is the authoritative definition, not a third party's interpretation — any detection heuristic built for the proposal should be checked against this definition, not against folklore. + +## Cons / limitations / caveats + +- Being the formal definition, it does not itself address the proposal's actual hard problem: distinguishing a human sitting at a real interactive shell from an automation tool that also happens to satisfy this exact definition (a pty-backed agent terminal passes `isatty()` on both streams too). + +## Relevance to tai + +The floor every detection heuristic in the proposal must be built on: `$-` containing `i` (bash's own exposed signal for this definition) is the right primitive to start from — but, per the other dossiers in this subject, satisfying bash's own "interactive" definition is necessary, not sufficient, to conclude a *human* is present. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/bbs-archlinux-second-thread.md b/research/shell-multiplexer-auto-attach/bbs-archlinux-second-thread.md new file mode 100644 index 0000000..cc577fb --- /dev/null +++ b/research/shell-multiplexer-auto-attach/bbs-archlinux-second-thread.md @@ -0,0 +1,28 @@ +# Arch Linux Forum — Second tmux Auto-Attach Thread + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | bbs.archlinux.org forum thread (id 207282) | +| Source URL | https://bbs.archlinux.org/viewtopic.php?id=207282 | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the auto-attach-pattern query as a second, distinct Arch forum thread on the same topic as the Tier 1 `$TMUX`-guard dossier; not independently read since the core pattern was already captured there. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/bbs-archlinux-tmux-env-guard.md b/research/shell-multiplexer-auto-attach/bbs-archlinux-tmux-env-guard.md new file mode 100644 index 0000000..c277840 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/bbs-archlinux-tmux-env-guard.md @@ -0,0 +1,47 @@ +# Arch Linux Forum Thread — The $TMUX Guard Pattern + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | bbs.archlinux.org forum thread (tmux auto-attach) | +| Source URL | https://bbs.archlinux.org/viewtopic.php?id=144544 | +| Source publisher / author | Arch Linux Forums (community) | +| Source type | forum thread | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Establishes the core, near-universal guard for auto-attaching tmux from a shell rc file: `[[ -z "$TMUX" ]]`, since tmux itself sets the `$TMUX` environment variable inside any pane it creates. A forum participant confirms a simple `if [[ -z "$TMUX" ]]; then tmux attach; fi` block worked for them in practice. + +## Key findings + +- `$TMUX` is tmux's own self-identifying environment variable, set automatically inside every pane — checking for its absence is the standard way to avoid launching tmux inside itself (nested sessions). +- This is community-tested, working advice, not a theoretical suggestion. + +## Pros / strengths + +- Simple, single-variable, directly tied to tmux's own behavior rather than an inferred heuristic — about as reliable a signal as exists for "am I already inside a tmux pane." + +## Cons / limitations / caveats + +- Only detects nesting on the *same host* — per the query D SSH-nesting dossier in this subject, `$TMUX` does not survive an SSH hop, so this guard alone is insufficient once remote sessions are involved. + +## Relevance to tai + +The mandatory first guard for the proposal's `.bashrc` hook: never attempt to auto-attach if `$TMUX` (or tai's own equivalent marker) is already set, to prevent runaway nested sessions — this is non-negotiable baseline safety, not an optional refinement. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/biriukov-dev-terminals-pseudoterminals.md b/research/shell-multiplexer-auto-attach/biriukov-dev-terminals-pseudoterminals.md new file mode 100644 index 0000000..cd11902 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/biriukov-dev-terminals-pseudoterminals.md @@ -0,0 +1,28 @@ +# Terminals and Pseudoterminals (biriukov.dev) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Terminals and pseudoterminals | +| Source URL | https://biriukov.dev/docs/fd-pipe-session-terminal/4-terminals-and-pseudoterminals/ | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the tmux-architecture query; a general pty/terminal fundamentals reference, potentially relevant background for anyone implementing raw-mode/pty handling, but not individually read for this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/codegenes-net-ssh-tty-tmux-guard.md b/research/shell-multiplexer-auto-attach/codegenes-net-ssh-tty-tmux-guard.md new file mode 100644 index 0000000..1de9976 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/codegenes-net-ssh-tty-tmux-guard.md @@ -0,0 +1,47 @@ +# How to Automatically Start tmux on SSH Session (Codegenes) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | How to automatically start tmux on SSH session | +| Source URL | https://www.codegenes.net/blog/how-to-automatically-start-tmux-on-ssh-session/ | +| Source publisher / author | Codegenes.net | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A fuller auto-attach recipe that combines two conditions: `[[ -n "$SSH_TTY" ]]` (only act when the session is an actual SSH login) and `[[ -z "$TMUX" ]]` (not already nested), rather than the bare `$TMUX` guard alone. + +## Key findings + +- `$SSH_TTY` is set specifically for an SSH login session with a pty, making it a useful qualifier for "only auto-attach over SSH logins," a narrower and safer scope than "every bash startup everywhere." +- Combining it with the `$TMUX` guard from the Arch forum dossier gives a two-condition gate rather than relying on one signal. + +## Pros / strengths + +- Narrows the blast radius of auto-attach logic specifically to a scenario (fresh SSH login) where multiplexing is genuinely almost always wanted, rather than firing on every local shell too. + +## Cons / limitations / caveats + +- Still vulnerable to the SSH-nesting failure mode described in this subject's unix.stackexchange dossier, since it doesn't address `$TMUX` failing to propagate across the SSH hop itself. + +## Relevance to tai + +A useful narrower variant for the proposal to consider: rather than auto-attaching on *every* interactive bash start, scope the riskiest/most valuable case (fresh remote logins) first, and treat local-terminal auto-attach as a separate, more cautious decision. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/coderwall-ssh-connection-check.md b/research/shell-multiplexer-auto-attach/coderwall-ssh-connection-check.md new file mode 100644 index 0000000..e74d09c --- /dev/null +++ b/research/shell-multiplexer-auto-attach/coderwall-ssh-connection-check.md @@ -0,0 +1,28 @@ +# Auto-Attach or Start tmux at Login (Coderwall, mirrored) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Auto-attach or start tmux at login | +| Source URL | https://coderwallhtbprolcom-s.evpn.library.nenu.edu.cn/p/tgm2la/auto-attach-or-start-tmux-at-login | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the auto-attach-pattern query (via an institutional proxy mirror URL); the search synthesis attributed a `$SSH_CONNECTION`-based guard variant to this source specifically, but it was not independently opened and read in full for this research pass — logged as Tier 2 rather than claiming more depth than was verified. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/conclusions/multiplexer-proposal-design-basis.md b/research/shell-multiplexer-auto-attach/conclusions/multiplexer-proposal-design-basis.md new file mode 100644 index 0000000..6aa92ec --- /dev/null +++ b/research/shell-multiplexer-auto-attach/conclusions/multiplexer-proposal-design-basis.md @@ -0,0 +1,55 @@ +# Conclusion: Build on tmux, Never Guess at "Is This a Human" Alone + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Conclusion | +| Tier | N/A | +| Date decided | 2026-10-09 | +| Decided by | retoor, advised by tai (agent, Claude Sonnet 5) | +| Status | Active (research complete, proposal pending decision, no code written) | +| Confidence | High | + +## Question / problem statement + +retoor asked for a tai-managed terminal multiplexer, auto-attached transparently whenever a real interactive bash shell starts — but explicitly *not* when bash is invoked by automation, scripts, or tools (including tai/Claude Code's own shell tool) that happen to also source `.bashrc` — supporting resize, sound/bell passthrough, events, raw keystroke handling, a named-socket or fd-based control channel, programmatic commands (read pane content, grep it, write/type at a position, animated typing), and continuous full-session logging. Deep research was explicitly requested before any implementation. + +## Conclusion + +Build this as a thin control layer **on top of tmux**, not as a new PTY multiplexer kernel. tmux already provides, battle-tested, exactly the substrate this proposal needs: a client-server architecture over a configurable Unix socket (`-L`/`-S`), persistent sessions that survive disconnects, native SIGWINCH/resize handling, bell passthrough, hooks, a human-typing interface (`send-keys`/`capture-pane`), a structured scripting interface (control mode, `-CC`), and a continuous-logging primitive (`pipe-pane`) that directly satisfies "remember the whole process log." tai (per its own root `README.md`) already optionally depends on tmux and already shells out to `tmux capture-pane` rather than reimplementing terminal capture — this proposal is a natural extension of that existing posture, not a new direction. + +The one genuinely hard, unsolved-by-tooling part of the request is reliably distinguishing a human-opened interactive shell from bash being invoked by a script, IDE, or AI coding agent sourcing the same `.bashrc`. This research found it is a recognized, actively-discussed, unsolved-in-general problem (VS Code's own agent-terminal issues, security detection-rule false-positive guidance, dedicated "AI agent terminal detection" posts) with **no single reliable signal** — only a combination of weaker ones (`$-` containing `i`, `$TMUX` unset, `$SSH_TTY`/`$SSH_CONNECTION`, parent process name, an agent-identifying env var), each individually defeatable in one direction or the other. + +## Supporting sources + +- `../man7-tmux-manual.md`, `../tao-of-tmux-server-internals.md`, `../github-tmux-discussion-3567-no-raw-socket-api.md` — tmux's client-server/socket architecture and the hard boundary of what's scriptable (commands + control mode, no raw socket protocol). +- `../tmuxai-dev-control-mode.md`, `../github-tmux-discussion-4016-control-mode-output.md`, `../github-tmux-wiki-control-mode.md`, `../libtmux-pane-interaction.md`, `../tmuxai-dev-faq.md` — the concrete mechanisms (`-CC` framing, send-keys/capture-pane loop, `pipe-pane` streaming) the control layer is built from. +- `../android-adb-cfmakeraw-commit.md`, `../stalkr-remote-tty-resize-design.md`, `../systemd-ptyfwd-resize-forwarding.md`, `../coral-mdt-console-sigwinch-commit.md`, `../lkml-2003-sigwinch-foreground-process-group.md` — what would have to be hand-built (raw termios mode, SIGWINCH handling, resize-over-socket forwarding) if tmux were not used; the cost this recommendation avoids entirely, since tmux already does all of it internally. +- `../bash-manual-interactive-shell-definition.md`, `../cyberciti-nixcraft-dollar-dash-test.md`, `../oreilly-bash-cookbook-interactive-test.md`, `../pythontutorials-net-ps1-child-process-caveat.md`, `../gitlab-mr-3383-stdin-stdout-isatty-mismatch.md`, `../lkml-mailing-list-tty-s-insufficient.md` — why `$-` is the right first-line interactivity check, and why `isatty`/`PS1`-alone checks fail in documented, specific ways. +- `../elastic-detection-rule-interactive-terminal-python.md`, `../panoramicdata-detecting-ai-agent-terminals.md`, `../github-vscode-community-discussion-161238.md`, `../github-vscode-issue-253265.md`, `../github-fak-issue-11425-pty-interactive-prompt-detection.md` — independent, cross-domain confirmation that human-vs-automation terminal detection is a recognized hard problem with no single reliable signal, only combined weaker ones plus explicit allowlisting by known tool identity. +- `../bbs-archlinux-tmux-env-guard.md`, `../codegenes-net-ssh-tty-tmux-guard.md`, `../unix-stackexchange-ssh-tmux-nesting.md`, `../santini-tmux-safety-file.md` — the concrete guard conditions (`$TMUX`, `$SSH_TTY`/`$SSH_CONNECTION`) and, critically, the mandatory safety-file bypass pattern for when the auto-attach logic itself is wrong. +- `../dasroot-net-tmux-vs-zellij.md`, `../theregister-tiling-multiplexer-survey.md` — why tmux specifically, not Zellij, fits this proposal's scripting-and-SSH-heavy requirements. +- `../github-bitcanon-autotyper.md`, `../nncron-send-keys-delay-docs.md`, `../tabby-typewriter-mode-docs.md`, `../raycast-fake-typing-effect.md`, `../k6-browser-presssequentially.md` — the basis for a typing-animation command syntax (per-character delay, inline pause/speed tokens, optional randomization). + +## Alternatives considered and rejected + +- **A from-scratch terminal multiplexer** (raw termios mode, SIGWINCH handling, a hand-rolled Unix-socket IPC protocol, session persistence, all built independently). Rejected: this re-solves a problem tmux has already solved, for a project whose own stated design philosophy (root `README.md`) is dependency-light and pragmatic, and which already optionally depends on tmux and already shells out to it for capture rather than reimplementing. The "research is done, confidence high" status above specifically reflects how thoroughly this exact problem surface (raw mode, resize forwarding, socket IPC) turned out to already be solved, documented, and battle-tested elsewhere. +- **Zellij as the substrate instead of tmux.** Considered, not dismissed out of hand — but tmux fits better given tai already optionally depends on it, and the research (`dasroot-net-tmux-vs-zellij.md`) found tmux specifically stronger for scripting and SSH/remote use, which is the actual requirement here; Zellij's documented strength is a discoverable interactive UI, not a scripting substrate. +- **A single boolean "is this shell interactive" check as the sole auto-attach gate.** Rejected: both the bash-detection dossiers and the automation-false-positive dossiers in this subject independently show this fails in both directions (automation tools that pass `isatty()`/`$-` checks; genuinely interactive shells an over-narrow check might miss). + +## Risks / open questions + +Unconditionally auto-wrapping every bash session from `.bashrc` is a high-blast-radius, hard-to-reverse-if-wrong system change: it can break automation, CI, IDE-integrated terminals, and — if misdetected — the agent's own shell tool (the exact tool this research is being conducted through). This was explicitly **not implemented** as part of this research task; it is proposed, pending the human's explicit review and confirmation of the exact activation condition and the mandatory safety-bypass mechanism (per `santini-tmux-safety-file.md`) before any `.bashrc` on a real machine is touched. + +## Action taken + +None yet. This was a research-and-proposal task; implementation is explicitly deferred pending retoor's review of the proposal presented alongside this archive. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial conclusion recorded. | diff --git a/research/shell-multiplexer-auto-attach/coral-mdt-console-sigwinch-commit.md b/research/shell-multiplexer-auto-attach/coral-mdt-console-sigwinch-commit.md new file mode 100644 index 0000000..01cfe26 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/coral-mdt-console-sigwinch-commit.md @@ -0,0 +1,46 @@ +# mdt console — SIGWINCH Self-Pipe Pattern (coral.googlesource.com) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | console: Add in SIGWINCH support | +| Source URL | https://coral.googlesource.com/mdt/+/d5b37aa3c97daf3e29b0aeacfce1c5d3beb1dff8%5E%21/ | +| Source publisher / author | Google Coral (mdt project) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A commit adding SIGWINCH support to Google's `mdt` console tool, using the standard "signal handler just sets a flag (or writes to a self-pipe); the real work happens in the main loop" pattern — the handler itself never calls the ioctl or touches shared state directly. + +## Key findings + +- Signal handlers should do the absolute minimum (set a flag / write one byte to a self-pipe) because most libc/syscall operations are not safe to call from inside a signal handler; the actual `TIOCGWINSZ` read and any forwarding happens back in the normal event loop once it notices the flag/pipe-byte. + +## Pros / strengths + +- A real, shipped example of the "minimal signal handler, defer the work" discipline applied specifically to SIGWINCH, in Python-adjacent/C-level console tooling from a credible engineering org. + +## Cons / limitations / caveats + +- Specific to `mdt`'s own event-loop shape; the general principle (minimal handler, deferred work) transfers, the exact code structure doesn't. + +## Relevance to tai + +If tai ever writes its own `SIGWINCH` handler (e.g. in a thin Python control process around tmux), it must follow this exact discipline — `signal.signal(signal.SIGWINCH, lambda *a: flag.set())` and do the real `ioctl`/forwarding work in the main loop, never inside the handler itself. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/cyberciti-nixcraft-dollar-dash-test.md b/research/shell-multiplexer-auto-attach/cyberciti-nixcraft-dollar-dash-test.md new file mode 100644 index 0000000..aa3550f --- /dev/null +++ b/research/shell-multiplexer-auto-attach/cyberciti-nixcraft-dollar-dash-test.md @@ -0,0 +1,47 @@ +# Bash Check If Shell Is Interactive or Not (nixCraft / cyberciti.biz) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Bash Check If Shell Is Interactive or Not Under Linux / Unix Oses | +| Source URL | https://www.cyberciti.biz/faq/linux-unix-bash-check-interactive-shell/ | +| Source publisher / author | nixCraft (cyberciti.biz) | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A practical guide recommending the `$-` special parameter (which contains the literal character `i` when the shell is interactive) as the preferred interactivity test, with the standard idiom `case "$-" in *i*) ... ;; esac`. + +## Key findings + +- `$-` is bash's own exposed flag string, set to include `i` precisely when the shell meets bash's formal interactive definition — it's reading bash's own self-assessment, not re-deriving it from `isatty()` calls. +- The canonical test form is a `case` statement matching `*i*` against `$-`. + +## Pros / strengths + +- Simple, portable within bash, and directly backed by bash's own internal state rather than an external inference. + +## Cons / limitations / caveats + +- Like any `isatty`-adjacent signal, `$-` containing `i` says "bash thinks it's interactive," which — per the query E dossiers in this subject — a pty-backed automation tool or AI agent terminal can also satisfy. It answers "is bash interactive" correctly; it does not answer "is a human present." + +## Relevance to tai + +This is the first-line check for the proposal's auto-attach guard (`.bashrc` should do nothing at all unless `$-` contains `i`), but per this subject's conclusion, it must be combined with other signals, not used alone, to approximate "a human is here." + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/dasroot-net-tmux-vs-zellij.md b/research/shell-multiplexer-auto-attach/dasroot-net-tmux-vs-zellij.md new file mode 100644 index 0000000..4f63d2d --- /dev/null +++ b/research/shell-multiplexer-auto-attach/dasroot-net-tmux-vs-zellij.md @@ -0,0 +1,47 @@ +# Terminal Multiplexers: tmux vs Zellij — A Comprehensive Comparison (dasroot.net) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Terminal Multiplexers: tmux vs Zellij – A Comprehensive Comparison | +| Source URL | https://dasroot.net/posts/2026/02/terminal-multiplexers-tmux-vs-zellij-comparison/ | +| Source publisher / author | dasroot.net | +| Source type | blog post | +| Source date (published) | 2026-02 (approximate) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A comparison framing tmux as the Unix-tradition "small, composable tools" choice — plain-text config, shell-script-and-plugin extensibility — against Zellij's mode-based, discoverable, Rust-plus-WASM-plugin design aimed more at interactive usability than scripting. + +## Key findings + +- tmux is explicitly credited as stronger for scripting and remote/SSH use specifically — the exact two properties this proposal's "control layer" and "auto-attach over SSH" requirements both need. +- Zellij's strengths (discoverability, on-screen key hints, built-in layouts) target interactive human usability, not programmatic control. + +## Pros / strengths + +- A direct, explicit comparison on exactly the axis that matters for this decision (scriptability vs. interactive UX), rather than a generic feature checklist. + +## Cons / limitations / caveats + +- A blog post, not a rigorous benchmark; no data on resource usage, performance under heavy automation load, or memory footprint for either tool. + +## Relevance to tai + +Directly supports choosing tmux over Zellij as the substrate: the proposal's actual requirements (scriptable control channel, SSH-friendly persistence, programmatic send/capture) are precisely tmux's documented strengths, not Zellij's. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/deepwiki-tmux-tmux-wiki-root.md b/research/shell-multiplexer-auto-attach/deepwiki-tmux-tmux-wiki-root.md new file mode 100644 index 0000000..829972a --- /dev/null +++ b/research/shell-multiplexer-auto-attach/deepwiki-tmux-tmux-wiki-root.md @@ -0,0 +1,28 @@ +# tmux/tmux (DeepWiki) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | tmux/tmux | +| Source URL | https://deepwiki.com/tmux/tmux | +| Source type | other (AI-generated wiki) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the tmux-architecture query as the general DeepWiki root for the tmux project (distinct from the specific control-mode sub-page already dossiered at Tier 1); noted as an AI-generated wiki, so its specifics are treated as secondary even where they'd otherwise be read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/elastic-detection-rule-interactive-terminal-python.md b/research/shell-multiplexer-auto-attach/elastic-detection-rule-interactive-terminal-python.md new file mode 100644 index 0000000..7c92778 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/elastic-detection-rule-interactive-terminal-python.md @@ -0,0 +1,47 @@ +# Interactive Terminal Spawned via Python (Elastic detection rule) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Interactive Terminal Spawned via Python | +| Source URL | https://detection.fyi/elastic/detection-rules/linux/execution_python_tty_shell/ | +| Source publisher / author | Elastic (detection-rules), mirrored by detection.fyi | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A security detection rule (also mirrored at elastic.co's own docs) that fires when a Python process spawns an interactive pty-backed shell — a pattern commonly associated with reverse shells, but one the rule's own documentation explicitly acknowledges is also produced by legitimate administrative scripts and automation tools. The prescribed remediation for false positives is to identify and allowlist the specific known-good tools, not to weaken the detection signal itself. + +## Key findings + +- A security vendor, writing a detection rule specifically meant to catch malicious activity, independently concludes that "spawns an interactive pty" is not by itself a reliable signal of anything in particular (malicious or benign) — it needs context (which process, with what parent, doing what) to be meaningful. +- The recommended fix for false positives is allowlisting by known tool identity, not by relaxing the pty-interactivity check — i.e., the right axis to add is "who/what is doing this," not "make the interactivity test looser." + +## Pros / strengths + +- Comes from the security-detection domain, which has spent years dealing with exactly this ambiguity at scale (distinguishing legitimate automation from malicious activity using the same low-level signals) — directly transferable experience for this subject's human-vs-automation problem. + +## Cons / limitations / caveats + +- Framed around detecting malicious reverse shells, not around a shell startup hook's design; the finding (allowlist by identity, not by loosening the raw signal) transfers, but the specific rule logic itself does not. + +## Relevance to tai + +Validates the proposal's approach of combining `$-`/`$TMUX`/`$SSH_TTY` with an explicit allowlist/denylist of known parent-process names or environment markers (e.g. skip auto-attach when the parent is `claude`, `code`, a CI runner, etc.) rather than trying to find one perfect universal signal — exactly the security industry's own conclusion on the identical underlying ambiguity. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/fr0stb1rd-tmux-login-autorestart-logging.md b/research/shell-multiplexer-auto-attach/fr0stb1rd-tmux-login-autorestart-logging.md new file mode 100644 index 0000000..4932f62 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/fr0stb1rd-tmux-login-autorestart-logging.md @@ -0,0 +1,47 @@ +# Run a Bash Script in tmux at Login with Auto Restart and Logging (fr0stb1rd) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | run a bash script in tmux at login with auto restart and logging | +| Source URL | https://fr0stb1rd.is-a.dev/posts/run-a-bash-script-in-tmux-at-login-with-auto-restart-and-logging/ | +| Source publisher / author | fr0stb1rd.is-a.dev | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A walkthrough of launching a long-running script inside a tmux session at login, with auto-restart on crash and output logging — a pattern for a persistent, supervised, logged session rather than a one-off interactive attach. + +## Key findings + +- tmux sessions are a reasonable place to supervise a long-running process with restart-on-crash logic, since the session (and its scrollback/log) survives independently of any one attached client. +- Combines naturally with `pipe-pane`-style continuous logging (see the tmuxai.dev FAQ dossier) for a supervised, logged, auto-restarting background session. + +## Pros / strengths + +- A concrete worked example of exactly the "persistent, logged, supervised session" shape the proposal's "remember the whole process log" requirement needs, just applied to a background script rather than an interactive login shell. + +## Cons / limitations / caveats + +- Login-script use case, not login-shell-auto-attach — adjacent to, but not identical to, the proposal's actual scenario (wrapping a human's interactive shell). + +## Relevance to tai + +A useful pattern reference for a tai daemon/supervisor session that stays alive independent of any attach/detach cycle, logging continuously via `pipe-pane`, even when no human is currently attached. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/git-alternativebit-ptyfwd-source-listing.md b/research/shell-multiplexer-auto-attach/git-alternativebit-ptyfwd-source-listing.md new file mode 100644 index 0000000..af68b23 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/git-alternativebit-ptyfwd-source-listing.md @@ -0,0 +1,28 @@ +# ptyfwd.c Source Listing (git.alternativebit.fr) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | src/shared/ptyfwd.c | +| Source URL | https://git.alternativebit.fr/picnoir/Systemd/src/commit/32fea77178ff9aaa09a7b043965fce854fdf8874/src/shared/ptyfwd.c | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the pty/SIGWINCH query as the full source-browse view of the same `ptyfwd.c` file already dossiered at Tier 1 via its specific resize-forwarding commit — this card is the companion full-file listing, not independently re-read beyond what the Tier 1 commit dossier already covers. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/github-bitcanon-autotyper.md b/research/shell-multiplexer-auto-attach/github-bitcanon-autotyper.md new file mode 100644 index 0000000..c7376a9 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/github-bitcanon-autotyper.md @@ -0,0 +1,47 @@ +# AutoTyper — CLI Terminal Input Simulator (bitcanon/autotyper) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | autotyper | +| Source URL | https://github.com/bitcanon/autotyper | +| Source publisher / author | bitcanon (GitHub) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A real, purpose-built CLI tool for simulating terminal input for demos, tests, and CLI automation — configurable per-character delay (`--char-delay`, default 75ms) plus separate pre- and post-command delays. + +## Key findings + +- 75ms is used as the tool's own default "looks human" per-character delay — a reasonable starting default for the proposal's own animated-typing command. +- Separate pre-command and post-command delays (not just inter-character delay) matter for a convincing demo/animation effect, beyond just character timing. + +## Pros / strengths + +- A real, maintained, purpose-fit tool rather than a repurposed library — its defaults and flag design are a direct, usable reference for the proposal's own "typing animated" command syntax. + +## Cons / limitations / caveats + +- Fixed per-character delay by default (no randomization/jitter) — per the Tabby dossier in this subject, a fixed delay reads as more "robotic" than randomized timing; worth noting as a design choice, not a flaw in the tool itself. + +## Relevance to tai + +A direct implementation reference for the proposal's animated-typing command: a configurable per-character delay (default ~75ms) plus pre/post-command pauses, fed into tmux via repeated single-character `send-keys -l` calls. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/github-community-discussion-188494-ai-agent-terminal.md b/research/shell-multiplexer-auto-attach/github-community-discussion-188494-ai-agent-terminal.md new file mode 100644 index 0000000..2bf34f6 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/github-community-discussion-188494-ai-agent-terminal.md @@ -0,0 +1,28 @@ +# AI Agent Terminal (GitHub Community Discussion #188494) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | AI Agent Terminal | +| Source URL | https://github.com/orgs/community/discussions/188494 | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the automation-detection query; directly on-topic (AI agent terminal discussion) but not individually read — the Tier 1 PanoramicData and VS Code dossiers already cover this ground with attributed detail. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/github-fak-issue-11425-pty-interactive-prompt-detection.md b/research/shell-multiplexer-auto-attach/github-fak-issue-11425-pty-interactive-prompt-detection.md new file mode 100644 index 0000000..ab15508 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/github-fak-issue-11425-pty-interactive-prompt-detection.md @@ -0,0 +1,47 @@ +# fak Issue #11425 — PTY Substrate for Interactive Prompt Detection + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | feat(codetools): terminal PTY substrate for shell execution with interactive prompt detection | +| Source URL | https://github.com/anthony-chaudhary/fak/issues/11425 | +| Source publisher / author | anthony-chaudhary/fak (GitHub repository) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A feature request/issue proposing a dedicated PTY substrate purpose-built for "interactive prompt detection" as its own named engineering problem within a coding-tools project — direct evidence that another project independently identified this as a distinct, non-trivial piece of infrastructure worth building deliberately, not a one-line check. + +## Key findings + +- The issue frames "interactive prompt detection" as requiring its own substrate/abstraction layer, not a quick heuristic bolted onto existing shell-execution code. +- This mirrors the earlier finding (from the agent's own prior work this session, building `tai_test.py`) that correctly driving/detecting interactive terminal state is substantial enough to warrant a dedicated framework rather than ad hoc code per call site. + +## Pros / strengths + +- Independent confirmation, from a completely different project tackling an adjacent problem (AI coding tool shell execution), that this is recognized, real engineering work. + +## Cons / limitations / caveats + +- A single open issue/feature request, not a shipped, documented solution — there is no implementation to inspect here, only the problem statement. + +## Relevance to tai + +Validates treating the detection logic in this proposal as a first-class, carefully-designed component (with its own tests and an explicit bypass, per the santini dossier) rather than a quick inline shell snippet in `.bashrc` — the same seriousness this project already applied to `tai_test.py`'s pty handling should apply here too. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/github-tmux-discussion-3567-no-raw-socket-api.md b/research/shell-multiplexer-auto-attach/github-tmux-discussion-3567-no-raw-socket-api.md new file mode 100644 index 0000000..904c632 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/github-tmux-discussion-3567-no-raw-socket-api.md @@ -0,0 +1,47 @@ +# tmux GitHub Discussion #3567 — No Raw Socket API Planned + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Replies: 3 comments (tmux/tmux discussion #3567) | +| Source URL | https://github.com/tmux/tmux/discussions/3567 | +| Source publisher / author | tmux project (GitHub Discussions) | +| Source type | forum thread | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A GitHub Discussion in which a user asks for a way to send raw command strings directly to the tmux server socket, bypassing the existing command/control-mode interfaces. The maintainer's recorded position is that there are no plans to offer anything beyond what already exists (the command set and control mode). + +## Key findings + +- tmux's server socket protocol is not a documented, stable wire format open for third-party reimplementation — the only sanctioned ways in are the `tmux` CLI commands and control mode (`-C`/`-CC`). +- This forecloses the option of a tai layer talking to the tmux server socket directly without going through the `tmux` binary itself. + +## Pros / strengths + +- Directly settles a design question before it becomes a dead end: don't try to speak tmux's internal socket protocol yourself. + +## Cons / limitations / caveats + +- A single maintainer comment in a discussion thread, not a formal API stability guarantee — could change, but is the best available signal today. + +## Relevance to tai + +Confirms the tai control layer must shell out to the `tmux` binary (plain commands or `-CC` control mode), not attempt to speak to `tmux`'s socket protocol directly — a cleaner, more future-proof design than reverse-engineering an undocumented wire format. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/github-tmux-discussion-4016-control-mode-output.md b/research/shell-multiplexer-auto-attach/github-tmux-discussion-4016-control-mode-output.md new file mode 100644 index 0000000..1a6e1e2 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/github-tmux-discussion-4016-control-mode-output.md @@ -0,0 +1,47 @@ +# tmux Discussion #4016 — Reading Command Output in Control Mode + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | How to read command output when in Control mode (-CC) | +| Source URL | https://github.com/orgs/tmux/discussions/4016 | +| Source publisher / author | tmux project (GitHub Discussions) | +| Source type | forum thread | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A real reported integration bug: a Rust program driving tmux with a single `-C` found that tmux detached the client as soon as its stdin was piped (non-interactive), and could not read command output back on stdout at all. The fix required passing `-CC` twice. + +## Key findings + +- `-C` alone is not sufficient for a piped/non-interactive client — tmux's detach-on-piped-stdin behavior silently breaks naive automation. +- `-CC` (doubled) was the empirically-confirmed working flag for a programmatic client that pipes stdin and reads stdout, in the reporter's tmux version. + +## Pros / strengths + +- A concrete, reproduced failure mode with a confirmed fix — exactly the kind of gotcha that would otherwise cost real debugging time when building a tai control-mode client. + +## Cons / limitations / caveats + +- Behavior tied to a specific tmux version; the thread itself notes to check the current man page, since `-C`/`-CC` semantics have shifted across releases. + +## Relevance to tai + +Directly actionable: when tai spawns `tmux -CC` as a subprocess with piped stdin/stdout (the natural way to drive it from Python), use `-CC` (doubled), not a single `-C`, and verify against the installed tmux version's man page before shipping. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/github-tmux-wiki-control-mode.md b/research/shell-multiplexer-auto-attach/github-tmux-wiki-control-mode.md new file mode 100644 index 0000000..b21381e --- /dev/null +++ b/research/shell-multiplexer-auto-attach/github-tmux-wiki-control-mode.md @@ -0,0 +1,47 @@ +# tmux Wiki — Control Mode (GitHub + DeepWiki mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Control Mode | +| Source URL | https://github.com/tmux/tmux/wiki/Control-Mode | +| Source publisher / author | tmux project wiki | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +The tmux project's own wiki page on control mode, describing it as a way for a client to talk to tmux using normal tmux commands over plain text (built originally for iTerm2's tmux integration) plus `%`-prefixed control-only notifications. A near-identical mirror of the same material exists at deepwiki.com/tmux/tmux/7.1-control-mode. + +## Key findings + +- Control mode deliberately reuses the normal tmux command set rather than inventing a separate scripting API — a client only needs to learn the notification framing on top of commands it may already know. +- Originally built for a GUI client (iTerm2) to render tmux panes natively, which is why it emphasizes structured notifications over raw terminal bytes. + +## Pros / strengths + +- Being the project's own wiki (not a third party's reverse-engineering), this is a materially more trustworthy reference than most blog posts on the topic. + +## Cons / limitations / caveats + +- A wiki page, not versioned documentation — it may drift from a specific tmux release's actual behavior; cross-check against the installed version's man page. + +## Relevance to tai + +The authoritative reference to build a tai control-mode client parser against — command reuse plus structured notifications is exactly the "accept commands + emit events" shape the proposal asks for. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/github-vscode-community-discussion-161238.md b/research/shell-multiplexer-auto-attach/github-vscode-community-discussion-161238.md new file mode 100644 index 0000000..c9d07e8 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/github-vscode-community-discussion-161238.md @@ -0,0 +1,47 @@ +# VS Code Copilot — Terminal Command Completion Not Reliably Detected (Discussion #161238) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Copilot does not detect terminal command has completed or is not getting terminal output | +| Source URL | https://github.com/orgs/community/discussions/161238 | +| Source publisher / author | GitHub Community Discussions (Copilot) | +| Source type | forum thread | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Documents that VS Code's own terminal API does not provide a reliable signal for "has this shell command finished," forcing Copilot's agent integration to fall back on heuristics — direct evidence that even a tool built by the same vendor as the terminal itself struggles with reliable terminal-state detection. + +## Key findings + +- The unreliability isn't specific to third-party automation reading a terminal from outside; it's present even for first-party tooling with presumably the best possible access to the terminal's actual state. +- This is framed as an ongoing, acknowledged gap, not a solved problem with a known fix. + +## Pros / strengths + +- Strong corroborating evidence (from a major, well-resourced vendor's own open issue tracker) that reliable terminal-state/command-completion detection is a genuinely hard, unsolved-in-general problem — not something this proposal is failing to find an obvious answer to because of insufficient research. + +## Cons / limitations / caveats + +- About command-completion detection specifically, a related but distinct problem from this subject's main question (human vs. automation at shell *startup*) — cited here as supporting evidence for "terminal state detection is hard in general," not as a direct answer to the startup-detection question. + +## Relevance to tai + +Sets realistic expectations for the proposal: whatever detection heuristic ships, it should be designed to fail safe (default to NOT auto-wrapping when uncertain, with an easy manual opt-in) rather than promising perfect detection — because even well-resourced, first-party tooling hasn't solved the adjacent problem of terminal-state detection in general. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/github-vscode-issue-253265.md b/research/shell-multiplexer-auto-attach/github-vscode-issue-253265.md new file mode 100644 index 0000000..5b1fdeb --- /dev/null +++ b/research/shell-multiplexer-auto-attach/github-vscode-issue-253265.md @@ -0,0 +1,47 @@ +# VS Code Issue #253265 — Agent/Chat Extension Cannot See Terminal Command Output + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Agent/Chat extension cannot see terminal command output, but shows normally in terminal | +| Source URL | https://github.com/microsoft/vscode/issues/253265 | +| Source publisher / author | Microsoft (vscode repository) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A filed VS Code bug where an agent extension's view of a terminal diverges from what a human actually sees rendered in the same terminal — the agent and the human are, in effect, looking at two different realities of the "same" shell session. + +## Key findings + +- Terminal output visible to a human is not guaranteed to be visible, in the same form, to a program also attached to that terminal — buffering, rendering, and capture paths can diverge. +- This is a second concrete example (alongside #161238) of VS Code's own first-party integration hitting a reliability gap in terminal introspection. + +## Pros / strengths + +- A specific, filed, reproducible bug report rather than a vague generalization — adds concrete weight to the pattern already established by the other VS Code dossier in this subject. + +## Cons / limitations / caveats + +- A single bug report; doesn't establish how common or how severe the general class of issue is across tools, just that it occurs in a major one. + +## Relevance to tai + +Reinforces the same "design for graceful degradation, not perfect detection/capture" principle for the proposal: if a tai control layer captures pane content for its own `grep`/read commands, it should not assume its view will always exactly match whatever VS Code, another tool, or the human sees rendered — and should log/flag divergence rather than silently trusting a possibly-stale capture. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/github-vscode-issue-313160-respect-user-shell.md b/research/shell-multiplexer-auto-attach/github-vscode-issue-313160-respect-user-shell.md new file mode 100644 index 0000000..2a8b775 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/github-vscode-issue-313160-respect-user-shell.md @@ -0,0 +1,28 @@ +# Respect the User's Preferred Shell in Agent Host Terminals (vscode #313160) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Respect the user's preferred shell in agent host terminals | +| Source URL | https://github.com/microsoft/vscode/issues/313160 | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the automation-detection query; a VS Code agent-terminal issue tangentially related (shell preference, not detection per se) to this subject's core question, not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/gitlab-mr-3383-stdin-stdout-isatty-mismatch.md b/research/shell-multiplexer-auto-attach/gitlab-mr-3383-stdin-stdout-isatty-mismatch.md new file mode 100644 index 0000000..b2f8710 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/gitlab-mr-3383-stdin-stdout-isatty-mismatch.md @@ -0,0 +1,47 @@ +# Check stdin rather than stdout for interactive terminals (GitLab MR #3383) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Check stdin rather than stdout for interactive terminals | +| Source URL | https://gitlab.com/gitlab-org/gitlab-development-kit/-/merge_requests/3383/pipelines | +| Source publisher / author | GitLab (gitlab-development-kit project) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A real-world merge request fixing an interactivity check that tested `stdout.isatty()` when it should have tested `stdin.isatty()` — the two can legitimately disagree (e.g. output piped to a pager while input stays a real terminal, or vice versa). + +## Key findings + +- `stdin.isatty()` and `stdout.isatty()` are independent facts and can differ in either direction in real, common shell usage (piping, redirection). +- Which one is the "right" one to check depends on what the check is actually trying to answer (can I read user input? vs. can I write formatted/colored output?) — conflating them is a real, shipped bug class. + +## Pros / strengths + +- A fixed, real production bug (not a hypothetical) — concrete evidence this mismatch actually bites people, not just a theoretical edge case. + +## Cons / limitations / caveats + +- Specific to one project's bug fix; doesn't generalize a universal rule beyond "check the stream that matches what you actually need, and don't assume stdin/stdout agree." + +## Relevance to tai + +A direct implementation warning for the proposal's `.bashrc` detection logic: if it ever needs to distinguish "can prompt a human" from "output looks like a terminal," check `stdin` specifically (`[ -t 0 ]`), not `stdout`, and never assume the two will match. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/glama-terminally-mcp-tmuxmanager.md b/research/shell-multiplexer-auto-attach/glama-terminally-mcp-tmuxmanager.md new file mode 100644 index 0000000..4ef143e --- /dev/null +++ b/research/shell-multiplexer-auto-attach/glama-terminally-mcp-tmuxmanager.md @@ -0,0 +1,28 @@ +# Terminally-mcp tmuxManager.ts (glama.ai) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | tmuxManager.ts (NightTrek/Terminally-mcp) | +| Source URL | https://glama.ai/mcp/servers/@NightTrek/Terminally-mcp/blob/f44a4f3662a146eebcde20784e651a84f9477ea3/src/services/tmuxManager.ts | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the control-mode query; an MCP server's TypeScript tmux-management service, a candidate reference implementation for a tai control layer's architecture, not independently read in this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/huggingface-bash-bashrc-mirrors.md b/research/shell-multiplexer-auto-attach/huggingface-bash-bashrc-mirrors.md new file mode 100644 index 0000000..b2877c8 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/huggingface-bash-bashrc-mirrors.md @@ -0,0 +1,28 @@ +# System-wide bash.bashrc Mirrors (Hugging Face datasets) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | # System-wide .bashrc file for interactive bash(1) shells. | +| Source URL | https://huggingface.co/datasets/ACCC1380/private-model/blob/d408768214efcb32a4c5f6ba0ae26dae26382007/etc/bash.bashrc | +| Source type | other (dataset mirror of a config file) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the interactive-shell-detection query as a mirrored copy of a stock `/etc/bash.bashrc` (two further near-identical mirrors of the same file also surfaced: `raw/0d510755.../etc/bash.bashrc` and `refs%2Fpr%2F9/etc/bash.bashrc`, same underlying file, logged here as one family rather than three cards). Relevant only as an example of the stock interactive-shell guard comment already present in most distro bashrc files, not independently analyzed. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/inskillclaude-tmux-custom-prompt-guide.md b/research/shell-multiplexer-auto-attach/inskillclaude-tmux-custom-prompt-guide.md new file mode 100644 index 0000000..929a36a --- /dev/null +++ b/research/shell-multiplexer-auto-attach/inskillclaude-tmux-custom-prompt-guide.md @@ -0,0 +1,28 @@ +# tmux Custom Prompt & Execution Guide (inskillclaude.com) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | tmux Custom Prompt & Execution Guide | +| Source URL | https://www.inskillclaude.com/skills/tmux | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the control-mode query; appears to be a Claude-skill-oriented tmux usage guide, relevant to the "AI agent drives tmux" pattern but not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/k6-browser-presssequentially.md b/research/shell-multiplexer-auto-attach/k6-browser-presssequentially.md new file mode 100644 index 0000000..a13ecd3 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/k6-browser-presssequentially.md @@ -0,0 +1,46 @@ +# k6 Browser Locator — pressSequentially + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Locator.pressSequentially() | +| Source URL | https://grafana.com/docs/k6/next/javascript-api/k6-browser/locator/presssequentially.md | +| Source publisher / author | Grafana (k6) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A browser-automation API method (Playwright-style) that types a string one character at a time with a configurable inter-keystroke delay (e.g. 100ms), used to approximate real user typing speed when driving a web page. + +## Key findings + +- The same "type one character at a time with a delay" pattern recurs independently in browser automation, confirming it's a general, cross-domain technique for simulating realistic input, not something specific to terminal tooling. + +## Pros / strengths + +- A well-documented, widely-used API from a credible, maintained testing tool — strong confirmation the per-character-delay approach is a standard, understood pattern across the whole automation-tooling industry, not a niche terminal-specific hack. + +## Cons / limitations / caveats + +- Browser DOM input, not terminal/pty input — the delay-timing concept transfers; the actual API and event model do not. + +## Relevance to tai + +Further corroborates the proposal's planned typing-animation approach (character-by-character `send-keys -l` with a configurable delay) as a standard, well-understood pattern across the automation tooling industry, not something tai would be inventing from scratch. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/k6-v2-3-presssequentially-mirror.md b/research/shell-multiplexer-auto-attach/k6-v2-3-presssequentially-mirror.md new file mode 100644 index 0000000..2f6d811 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/k6-v2-3-presssequentially-mirror.md @@ -0,0 +1,28 @@ +# Locator.pressSequentially (k6 v2.3.x docs mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Locator.pressSequentially (v2.3.x) | +| Source URL | https://grafana.com/docs/k6/v2.3.x/javascript-api/k6-browser/locator/presssequentially.md | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the typing-animation query as an older-version-pinned mirror of the same k6 `pressSequentially` documentation already dossiered at Tier 1 via the `next` docs version. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/keyboardmaestro-forum-random-delay.md b/research/shell-multiplexer-auto-attach/keyboardmaestro-forum-random-delay.md new file mode 100644 index 0000000..fb8d4b4 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/keyboardmaestro-forum-random-delay.md @@ -0,0 +1,28 @@ +# Insert Text by Typing - Random Speed (Keyboard Maestro forum) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Insert text by typing - random speed | +| Source URL | https://forum.keyboardmaestro.com/t/insert-text-by-typing-random-speed/27400 | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the typing-animation query; a macOS automation forum thread on randomizing keystroke delay, a minor independent data point already better covered by the Tabby dossier, not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/kx-cloudingenium-zellij-guide.md b/research/shell-multiplexer-auto-attach/kx-cloudingenium-zellij-guide.md new file mode 100644 index 0000000..fc54d98 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/kx-cloudingenium-zellij-guide.md @@ -0,0 +1,28 @@ +# Zellij Terminal Multiplexer: Modern tmux/screen Alternative Guide (kx.cloudingenium.com) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Zellij terminal multiplexer: modern tmux/screen alternative guide | +| Source URL | https://kx.cloudingenium.com/es/zellij-terminal-multiplexer-modern-tmux-screen-alternative-guide | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the multiplexer-comparison query; another general Zellij-as-alternative guide, not independently read for the same reason as the other Zellij-promotional dossiers in this batch (core comparison already attributed elsewhere). + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/libtmux-pane-interaction.md b/research/shell-multiplexer-auto-attach/libtmux-pane-interaction.md new file mode 100644 index 0000000..6403b5e --- /dev/null +++ b/research/shell-multiplexer-auto-attach/libtmux-pane-interaction.md @@ -0,0 +1,48 @@ +# Pane Interaction — libtmux docs + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Pane Interaction - libtmux 0.53.0 documentation | +| Source URL | https://libtmux.git-pull.com/topics/pane_interaction.html | +| Source publisher / author | libtmux project | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +libtmux's own documentation for the send-keys/capture-pane automation pattern: spawn or attach a session, send keystrokes with `send_keys`, read the rendered screen back with `capture_pane` (optionally the full scrollback via `-S -`), and loop until the expected state appears. + +## Key findings + +- The canonical "drive and poll" loop: `send_keys()` then `capture_pane()`, waiting briefly for output to catch up before reading. +- A documented, easy-to-miss gotcha: a command is **not executed** unless `Enter` is sent as its own, separate key event — typing the text alone leaves it sitting unexecuted in the prompt. +- Output can lag behind the command that produced it, so libtmux's own examples add short sleeps before capturing. + +## Pros / strengths + +- Comes from a maintained Python wrapper library that has clearly hit these exact gotchas in practice — the Enter-must-be-separate warning in particular is the kind of thing that silently breaks automation until someone documents it. + +## Cons / limitations / caveats + +- The send+capture+sleep loop is inherently a polling approach with a race condition (how long is "briefly"?) — it's not real event-driven I/O the way control-mode notifications are. + +## Relevance to tai + +This is the baseline implementation for the proposal's "write/type at a position" and "read pane content" commands when NOT using control mode — and the Enter-as-separate-key gotcha must be built into the design from day one, not discovered via a bug report later. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/libtmux-python-github-commit.md b/research/shell-multiplexer-auto-attach/libtmux-python-github-commit.md new file mode 100644 index 0000000..727c368 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/libtmux-python-github-commit.md @@ -0,0 +1,28 @@ +# libtmux Control-Mode Support Commit + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | libtmux commit 646c050716ed23965db0b9de1ebe4a3230704cd3 | +| Source URL | https://github.com/tmux-python/libtmux/commit/646c050716ed23965db0b9de1ebe4a3230704cd3 | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the control-mode query; appears to be the commit adding control-mode support to libtmux (Python), a potentially directly reusable library for a tai control layer, flagged for later deeper review rather than analyzed now. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/lkml-2003-sigwinch-foreground-process-group.md b/research/shell-multiplexer-auto-attach/lkml-2003-sigwinch-foreground-process-group.md new file mode 100644 index 0000000..1e692c5 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/lkml-2003-sigwinch-foreground-process-group.md @@ -0,0 +1,47 @@ +# LKML 2003 — SIGWINCH Only Reaches the Foreground Process Group + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | RFC/CFT 1/1: SIGWINCH - behaviour change | +| Source URL | https://lkml.iu.edu/hypermail/linux/kernel/0302.1/1591.html | +| Source publisher / author | Linux kernel mailing list (2003) | +| Source type | forum thread | +| Source date (published) | 2003 (approximate, per thread era) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A kernel-mailing-list discussion establishing that `SIGWINCH` is only ever delivered to the *foreground* process group of a terminal — a backgrounded or detached job (exactly tmux's own panes when no client is attached) will not receive it even if the underlying terminal size genuinely changed. + +## Key findings + +- A process group needs to be in the foreground to receive `SIGWINCH` at all; this is a kernel-level restriction, not an application bug. +- The direct consequence: relying solely on the signal to learn about a resize is insufficient for anything that can be backgrounded or detached (a tmux session with no client, or any session reattached later) — the size must be re-read and re-applied explicitly on resume/reattach, not assumed to have been kept in sync via signals the whole time. + +## Pros / strengths + +- Primary-source kernel discussion, not a secondary summary — authoritative on kernel signal-delivery semantics. + +## Cons / limitations / caveats + +- Old (2003) thread; kernel behavior in this specific area has been stable since, per the broader search synthesis, but worth a sanity check against current kernel docs if correctness here is load-bearing for a safety property. + +## Relevance to tai + +Directly explains why tmux itself re-queries and re-applies window size on `attach`/`reattach` rather than relying purely on signals (this is exactly the detach/reattach cycle the proposal's whole persistent-session model depends on) — confirms that if tai ever builds any of this logic itself rather than delegating to tmux, it must re-sync size explicitly on every reattach, never assume signals alone kept it current. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/lkml-mailing-list-tty-s-insufficient.md b/research/shell-multiplexer-auto-attach/lkml-mailing-list-tty-s-insufficient.md new file mode 100644 index 0000000..49080ea --- /dev/null +++ b/research/shell-multiplexer-auto-attach/lkml-mailing-list-tty-s-insufficient.md @@ -0,0 +1,47 @@ +# Linux Makefile color-ls thread — tty -s Is Not an Interactivity Check (lkml mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Re: Linux Makefile uses 'ls', causes problems with color-ls (PATCH) | +| Source URL | https://lkml.rescloud.iu.edu/9903.3/1188.html | +| Source publisher / author | Linux kernel mailing list (mirrored) | +| Source type | forum thread | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +An old kernel-mailing-list reply making the point directly: `tty -s` (or a bare `isatty()` check) does not by itself detect an "interactive shell" — it can still read true in a non-interactive shell simply because that shell's input wasn't redirected away from a terminal. + +## Key findings + +- A non-interactive shell (e.g. one running a script) can still have a terminal-connected stdin if nobody redirected it — `isatty()` alone conflates "connected to a terminal" with "is an interactive shell," which are different facts. +- This is a decades-old, well-understood pitfall, not a new or contested one. + +## Pros / strengths + +- A long-standing, hard-learned lesson from kernel/toolchain discussion, independently corroborating the bash manual's own choice to define "interactive" via `-i` OR `isatty()`, not `isatty()` alone. + +## Cons / limitations / caveats + +- Old (1999-era) message, color-ls-specific context — but the underlying `isatty()` limitation it demonstrates hasn't changed. + +## Relevance to tai + +Reinforces (from yet another independent angle, now four sources deep in this subject) that no single `isatty`/`tty -s`-style check is sufficient for the proposal's human-detection problem — it must combine `$-`, not just raw tty-connectedness. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/lkml-sigwinch-thread-replies.md b/research/shell-multiplexer-auto-attach/lkml-sigwinch-thread-replies.md new file mode 100644 index 0000000..5cdf120 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/lkml-sigwinch-thread-replies.md @@ -0,0 +1,28 @@ +# LKML — SIGWINCH Problem with Terminal Apps Still Alive (reply threads) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Re: [PATCH 0/5] SIGWINCH problem with terminal apps still alive | +| Source URL | https://lkml.iu.edu/hypermail/linux/kernel/0810.2/0113.html | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the pty/SIGWINCH query as part of a 2008-era kernel mailing-list thread family about SIGWINCH delivery to terminal apps (two further replies in the same thread also surfaced: `0810.1/2485.html` and `0810.2/0656.html`, logged here as one family). The core foreground-process-group finding from this thread family is already captured at Tier 1 via the 2003 RFC/CFT dossier; these later replies were not independently read in full. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/mager-co-orchestrating-agents-tmux.md b/research/shell-multiplexer-auto-attach/mager-co-orchestrating-agents-tmux.md new file mode 100644 index 0000000..76fe2f7 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/mager-co-orchestrating-agents-tmux.md @@ -0,0 +1,28 @@ +# tmux: Orchestrating Agents with send-keys and capture-pane (mager.co) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | tmux: orchestrating agents with send-keys and capture-pane | +| Source URL | https://www.mager.co/blog/2026-08-23-orchestrating-agents-with-tmux/ | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the control-mode query; directly on-topic (using tmux to orchestrate AI agents), a strong candidate for later promotion to Tier 1 if read in depth, but not analyzed in this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/man7-tmux-manual.md b/research/shell-multiplexer-auto-attach/man7-tmux-manual.md new file mode 100644 index 0000000..71bd65e --- /dev/null +++ b/research/shell-multiplexer-auto-attach/man7-tmux-manual.md @@ -0,0 +1,49 @@ +# tmux(1) Manual Page (man7.org) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | tmux(1) - Linux manual page | +| Source URL | https://man7.org/linux/man-pages/man1/tmux.1.html | +| Source publisher / author | man7.org (Michael Kerrisk's man-pages mirror) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +The canonical tmux manual page. Confirms tmux is a client-server program: the server and each attaching client are separate processes communicating through a Unix domain socket stored under `/tmp` (or `$TMPDIR`), with the socket name/path configurable via `-L`/`-S`. + +## Key findings + +- tmux's server holds all session state; a client is only a terminal attaching to it — this is the architecture a tai-managed multiplexer would want to reuse rather than reimplement. +- The socket location is explicit and overridable (`-L name` picks a name under the default directory, `-S path` picks a full path), which is exactly the hook point needed to run a tai-dedicated tmux server on its own socket, isolated from any tmux the user runs themselves. +- Session persistence across disconnects (SSH timeout, terminal close) is a direct consequence of the server living outside any one client's process. + +## Pros / strengths + +- Authoritative, current reference for the exact flags (`-L`, `-S`, `-C`, `-CC`) a control layer would depend on. +- Confirms the socket-based design is a stable, documented contract, not an internal implementation detail likely to change. + +## Cons / limitations / caveats + +- A man page describes the interface, not the internal event loop or data structures — nothing here on performance characteristics or server resource usage under many panes. + +## Relevance to tai + +Directly supports the proposal's core recommendation: a tai-managed multiplexer should run its own tmux server on a dedicated socket (`-S ~/.tai/tmux.sock` or similar) rather than building a PTY multiplexer from scratch. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/microsoft-terminal-issue-5612.md b/research/shell-multiplexer-auto-attach/microsoft-terminal-issue-5612.md new file mode 100644 index 0000000..f22da15 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/microsoft-terminal-issue-5612.md @@ -0,0 +1,28 @@ +# Implement tmux control mode (-CC) — microsoft/terminal #5612 + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Implement tmux control mode (-CC) | +| Source URL | https://github.com/microsoft/terminal/issues/5612 | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the control-mode query; a feature request for Windows Terminal to support tmux -CC natively, relevant context for cross-platform control-mode adoption but not individually read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/mintlify-wiki-control-mode-mirror.md b/research/shell-multiplexer-auto-attach/mintlify-wiki-control-mode-mirror.md new file mode 100644 index 0000000..c1ae420 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/mintlify-wiki-control-mode-mirror.md @@ -0,0 +1,28 @@ +# Control Mode (mintlify.wiki mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Control Mode - tmux | +| Source URL | https://mintlify.wiki/tmux/tmux/guides/control-mode | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the control-mode query as another mirror of the same control-mode documentation already dossiered via the tmux project wiki and tmuxai.dev. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/nncron-send-keys-delay-docs.md b/research/shell-multiplexer-auto-attach/nncron-send-keys-delay-docs.md new file mode 100644 index 0000000..c705c12 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/nncron-send-keys-delay-docs.md @@ -0,0 +1,46 @@ +# Keyboard Input Emulation — SEND-KEYS-DELAY (nncron.ru) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Keyboard Input Emulation | +| Source URL | https://nncron.ru/help/EN/commands/send_keys.htm | +| Source publisher / author | nnCron | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Documentation for a Windows automation tool's keystroke-injection command, supporting a global `SEND-KEYS-DELAY` directive for inter-keystroke delay ("slow typing" emulation) plus an inline `{DELAY n}` token embeddable directly inside the string being sent, for one-off pauses at specific points. + +## Key findings + +- Two levels of timing control: a global default delay, and inline per-position overrides — a more flexible model than a single fixed delay for the whole string. + +## Pros / strengths + +- The inline-token approach (`{DELAY n}` inside the string) is a clean, compact syntax worth borrowing for a tai scripting command, rather than requiring separate delay arguments per chunk of text. + +## Cons / limitations / caveats + +- Windows-specific tool (keystroke injection via the Windows API), not directly portable, but the *syntax idea* (inline delay tokens) is platform-agnostic. + +## Relevance to tai + +A good model for the proposal's animated-typing command syntax: support both a default per-character delay and inline markup for pauses at specific points in the typed text, rather than only a single global speed setting. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/oneuptime-tmux-session-recovery.md b/research/shell-multiplexer-auto-attach/oneuptime-tmux-session-recovery.md new file mode 100644 index 0000000..e849995 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/oneuptime-tmux-session-recovery.md @@ -0,0 +1,46 @@ +# How to Recover Disconnected Sessions with tmux on Ubuntu (OneUptime) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | How to Recover Disconnected Sessions with tmux on Ubuntu | +| Source URL | https://oneuptime.com/blog/post/2026-03-02-how-to-recover-disconnected-sessions-with-tmux-on-ubuntu/markdown | +| Source publisher / author | OneUptime | +| Source type | blog post | +| Source date (published) | 2026-03-02 | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Covers the recovery workflow when an SSH connection drops mid-session: the tmux server keeps running independently, and `tmux attach` reconnects to exactly the state the session was in — the practical payoff of the client-server architecture described in the man7 dossier. + +## Key findings + +- Session persistence across disconnects isn't just a theoretical architecture benefit — this source frames it specifically as the recovery procedure for the common real failure (dropped SSH connection), which is the actual day-to-day value proposition. + +## Pros / strengths + +- Concrete, recent (2026) operational guide rather than abstract architecture description — grounds the "why bother with tmux at all" case in a real daily annoyance it solves. + +## Cons / limitations / caveats + +- Ubuntu-specific framing, though the underlying tmux behavior is platform-agnostic. + +## Relevance to tai + +Reinforces the core value proposition the whole proposal rests on: wrapping every shell in a tai-managed tmux session means a dropped connection, closed terminal, or SSH timeout never loses the session's state or scrollback — exactly what "enforcing a user is always terminal multiplexed" is meant to guarantee. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/openskillindex-autoskill-typing.md b/research/shell-multiplexer-auto-attach/openskillindex-autoskill-typing.md new file mode 100644 index 0000000..7805bf1 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/openskillindex-autoskill-typing.md @@ -0,0 +1,28 @@ +# AutoSkill Python Console Simulation with Typing Effect (OpenSkillIndex) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | AutoSkill Python Console Simulation with Typing Effect | +| Source URL | https://openskillindex.com/skills/ecnu-icalk-autoskill-python-console-simulation-with-typing-effect | +| Source type | other (skill listing) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the typing-animation query; a listed "skill" simulating a Python console with a typing effect — directly on-topic for terminal typing animation, not independently read in this pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/oreilly-bash-cookbook-interactive-test.md b/research/shell-multiplexer-auto-attach/oreilly-bash-cookbook-interactive-test.md new file mode 100644 index 0000000..bfbe20d --- /dev/null +++ b/research/shell-multiplexer-auto-attach/oreilly-bash-cookbook-interactive-test.md @@ -0,0 +1,47 @@ +# Bash Cookbook — Testing for an Interactive Shell (O'Reilly) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Bash Cookbook, "Use the following case" excerpt | +| Source URL | https://www.oreilly.com/library/view/bash-cookbook/0596526784/ch01s10.html | +| Source publisher / author | O'Reilly Media (Bash Cookbook) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +An independent, published-book confirmation of the `$-` test, explicitly stating it as the *preferred* method over checking whether `$PS1` is set. + +## Key findings + +- Corroborates the nixCraft guide's recommendation from a second, independent, edited (not just blog) source. +- Explicitly ranks `$-` above the `$PS1`-is-set check, rather than treating them as interchangeable. + +## Pros / strengths + +- A published, edited technical book is a stronger signal of broad community consensus than any single blog post — two independent sources agreeing on the same preferred test is meaningful corroboration. + +## Cons / limitations / caveats + +- Older reference material (the Bash Cookbook predates most AI-agent-terminal concerns); it has nothing to say about the automation-vs-human distinction this subject's proposal actually needs. + +## Relevance to tai + +Corroborating evidence (not new information) for using `$-` over `$PS1` as the proposal's first-line interactivity check. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/packagist-piuu-typing-simulation.md b/research/shell-multiplexer-auto-attach/packagist-piuu-typing-simulation.md new file mode 100644 index 0000000..61106f5 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/packagist-piuu-typing-simulation.md @@ -0,0 +1,28 @@ +# piuu — Typing Simulation (Packagist, PHP) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | wahyulingu/piuu | +| Source URL | https://packagist.org/packages/wahyulingu/piuu | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the typing-animation query; a PHP package adding random delays, skipped letters, and vowel swaps to simulate typing errors — a third independent implementation of the typo-injection idea already dossiered via Tabby, not independently read in detail. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/panoramicdata-detecting-ai-agent-terminals.md b/research/shell-multiplexer-auto-attach/panoramicdata-detecting-ai-agent-terminals.md new file mode 100644 index 0000000..2442f6d --- /dev/null +++ b/research/shell-multiplexer-auto-attach/panoramicdata-detecting-ai-agent-terminals.md @@ -0,0 +1,49 @@ +# Detecting AI Agent Terminals in PowerShell (PanoramicData community) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Detecting AI Agent Terminals in PowerShell | +| Source URL | https://community.panoramicdata.com/t/detecting-ai-agent-terminals-in-powershell/116 | +| Source publisher / author | PanoramicData community forum | +| Source type | forum thread | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A direct discussion of this exact problem — telling an AI coding agent's terminal apart from a genuine human one — on a different shell (PowerShell, not bash), using a combination of agent-specific environment-variable checks, parent-process-name inspection, and interactive-mode detection together, rather than any single signal. + +## Key findings + +- Real tools in this space (per the search synthesis, including a shell-prompt configuration project) already check for an `AI_AGENT`-style environment variable and skip normal interactive behavior (loading a prompt theme, etc.) when both that variable is set AND the shell is otherwise non-interactive. +- A separate tool mentioned in the synthesis takes a different approach: it identifies agent CLIs by process name (`claude`, `codex`, `gemini`, `amp`) rather than by environment variable, specifically to avoid raising false alerts for ordinary bash sessions. +- Confirms multiple independent tools in the wild have already converged on "combine several weak signals" as the working answer, on a different OS/shell, for essentially the same problem this subject is researching for bash. + +## Pros / strengths + +- Cross-platform corroboration (PowerShell, not bash) of the same conclusion this subject's bash-specific dossiers reach independently — strengthens confidence this is a fundamental property of the problem, not a bash-specific quirk. +- Names a concrete, already-existing convention (an `AI_AGENT`-style env var) that tai could adopt or recognize for interoperability with other tools doing the same detection. + +## Cons / limitations / caveats + +- PowerShell-specific implementation details don't transfer directly to bash; only the *strategy* (combine env var + parent process + mode) transfers. + +## Relevance to tai + +Directly supports the proposal's recommended detection design: check a combination of `$-` (interactive flag), `$TMUX`/`$SSH_TTY` (not-already-wrapped), and the parent process name/an agent-identifying env var (to explicitly exclude known automation tools, including Claude Code's own shell tool) — never a single signal. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/petronellatech-zellij-guide.md b/research/shell-multiplexer-auto-attach/petronellatech-zellij-guide.md new file mode 100644 index 0000000..fa39d4a --- /dev/null +++ b/research/shell-multiplexer-auto-attach/petronellatech-zellij-guide.md @@ -0,0 +1,28 @@ +# Zellij Terminal Multiplexer Guide 2026 (PetronellaTech) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Zellij Terminal Multiplexer Guide 2026 | +| Source URL | https://petronellatech.com/blog/zellij-terminal-multiplexer-guide-2026/ | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the multiplexer-comparison query; a general Zellij guide, not independently read since the dasroot.net comparison dossier already covers the tmux-vs-Zellij tradeoff with attributed detail. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/pypi-tmux-control.md b/research/shell-multiplexer-auto-attach/pypi-tmux-control.md new file mode 100644 index 0000000..b51e9de --- /dev/null +++ b/research/shell-multiplexer-auto-attach/pypi-tmux-control.md @@ -0,0 +1,28 @@ +# tmux-control (PyPI) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | tmux control | +| Source URL | https://pypi.org/project/tmux-control | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the control-mode query; a Python package apparently wrapping tmux control mode, potentially a reusable reference implementation if tai's control layer is written in Python, but not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/pythontutorials-net-ps1-child-process-caveat.md b/research/shell-multiplexer-auto-attach/pythontutorials-net-ps1-child-process-caveat.md new file mode 100644 index 0000000..50e683a --- /dev/null +++ b/research/shell-multiplexer-auto-attach/pythontutorials-net-ps1-child-process-caveat.md @@ -0,0 +1,47 @@ +# Checking for Interactive Shell in a Python Script (pythontutorials.net) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | How to Check if a Shell is Interactive in a Python Script: Preferred Methods Explained | +| Source URL | https://www.pythontutorials.net/blog/checking-for-interactive-shell-in-a-python-script/ | +| Source publisher / author | pythontutorials.net | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Suggests checking whether `PS1` exists in a Python script's environment as a proxy for shell interactivity — flagged independently (not by this source, but by general knowledge noted in the search synthesis) as unreliable, since `PS1` is normally a shell variable that is not exported to child processes. + +## Key findings + +- `PS1`, unless explicitly `export`ed, is invisible to any child process (including a Python script, or tai itself if written in Python) inspecting its own environment — `os.environ.get("PS1")` will typically be empty even in a genuinely interactive parent shell. +- This caveat is a real correctness bug if a tai component written in Python ever tried to re-derive "is the parent shell interactive" from its own `os.environ`. + +## Pros / strengths + +- Surfaces a specific, concrete correctness trap for exactly the kind of cross-language (bash-to-Python) interactivity check the proposal would otherwise be tempted to write. + +## Cons / limitations / caveats + +- The source itself suggests the `PS1`-in-environment check as a *method*; the caveat about its unreliability from a child process comes from general knowledge noted alongside the search synthesis, not from the source's own text — treat the caveat as independently well-established (PS1 export behavior is standard shell semantics), but attribute it honestly as outside this specific article. + +## Relevance to tai + +A hard "don't do this" for the proposal: any Python-side component of tai must never try to detect shell interactivity by reading `PS1` from `os.environ` — it won't be there even when the parent genuinely is interactive. Detection has to happen in the shell (`.bashrc`, via `$-`) and be passed to tai explicitly (an env var, an argument), not re-derived downstream. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/raycast-fake-typing-effect.md b/research/shell-multiplexer-auto-attach/raycast-fake-typing-effect.md new file mode 100644 index 0000000..5822ec6 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/raycast-fake-typing-effect.md @@ -0,0 +1,46 @@ +# Fake Typing Effect (Raycast) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Fake Typing Effect | +| Source URL | https://www.raycast.com/cyberistic/fake-typing-effect | +| Source publisher / author | Raycast (cyberistic extension) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A Raycast extension offering a small inline markup language for animated typing: `[0.5]` for a half-second pause, `[speed:20]`/`[speed:100]` to change typing speed mid-string, and special tokens like `[enter]` and `[tab]` for non-printable keys — explicitly positioned for demos, tutorials, and presentations. + +## Key findings + +- A compact, readable inline-markup syntax (`[pause]`, `[speed:N]`, `[enter]`, `[tab]`) that mixes timing control and special-key injection in one string, rather than separate parameters. + +## Pros / strengths + +- The cleanest example found of a single, simple string syntax that a human could write by hand for a scripted demo — directly adoptable as the shape of a tai "type" command's argument. + +## Cons / limitations / caveats + +- A Raycast (macOS launcher) extension, not a terminal tool — the markup idea transfers, the implementation does not. + +## Relevance to tai + +The strongest direct model for the proposal's typing-animation command syntax: something like `tai type "echo hello[0.5][speed:20]world[enter]"` — inline pause/speed/special-key tokens in one readable string, rather than a complex multi-argument API. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/rdrr-io-tmuxr-send-keys.md b/research/shell-multiplexer-auto-attach/rdrr-io-tmuxr-send-keys.md new file mode 100644 index 0000000..93a349e --- /dev/null +++ b/research/shell-multiplexer-auto-attach/rdrr-io-tmuxr-send-keys.md @@ -0,0 +1,28 @@ +# tmuxr send_keys (R package docs) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | send_keys: Send keys to a tmux pane in datascienceworkshops/tmuxr | +| Source URL | https://rdrr.io/github/datascienceworkshops/tmuxr/man/send_keys.html | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the control-mode query; an R-language tmux wrapper's send_keys docs, corroborating (by existing at all) that the send-keys pattern has bindings across multiple languages, but not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/santini-tmux-safety-file.md b/research/shell-multiplexer-auto-attach/santini-tmux-safety-file.md new file mode 100644 index 0000000..f3724f7 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/santini-tmux-safety-file.md @@ -0,0 +1,47 @@ +# My tmux setup (santini.di.unimi.it) — The Safety-File Escape Hatch + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | my tmux setup | +| Source URL | https://santini.di.unimi.it/extras/ph/my-tmux-setup | +| Source publisher / author | santini.di.unimi.it (personal page) | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Describes a personal dotfiles setup that auto-attaches tmux from `.bashrc`, with a specific safety mechanism: the presence of a marker file on disk bypasses the auto-attach logic entirely. If a broken rc configuration would otherwise lock the user out of a usable shell (especially over SSH, where there's no other way in), creating that file lets them log in to a plain, unwrapped shell to fix the configuration. + +## Key findings + +- A broken auto-attach hook in `.bashrc` is a realistic, self-inflicted lockout risk, severe enough that at least one real setup builds a dedicated bypass for it. +- The bypass is simple and robust: a file's mere existence, checked before any other logic runs, with no dependency on the auto-attach logic itself being correct. + +## Pros / strengths + +- A genuinely important, under-discussed safety pattern — auto-attach hooks in shell rc files are exactly the kind of thing that can strand a user with no way back in except an out-of-band recovery path (single-user mode, a cloud console's serial/VNC access, etc.) if something goes wrong. + +## Cons / limitations / caveats + +- A personal setup description, not a widely-adopted standard — but the underlying risk it addresses is universal to any rc-file auto-launch design, including this proposal's. + +## Relevance to tai + +This is a mandatory safety requirement for the proposal, not an optional nicety: before any `.bashrc` hook is live on a real machine, there must be an unconditional, dead-simple bypass (e.g. `TAI_PLEXER_DISABLE=1` env var, or a marker file like `~/.tai/no-plexer`) checked *before* any other detection logic, so a bug in the detection code itself can never fully lock the user out of a plain shell. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/skills-rest-tmux-skill.md b/research/shell-multiplexer-auto-attach/skills-rest-tmux-skill.md new file mode 100644 index 0000000..2f902a5 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/skills-rest-tmux-skill.md @@ -0,0 +1,28 @@ +# List tmux Sessions, Capture Pane Output, Send Input (skills.rest) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | tmux: List tmux sessions, capture pane output, and send input to panes | +| Source URL | https://skills.rest/skill/tmux-p-sree-sai-pavan | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the control-mode query; another AI-agent-skill-style tmux wrapper, directly on-topic for "accept commands like reading/writing a pane" but not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/stalkr-remote-tty-resize-design.md b/research/shell-multiplexer-auto-attach/stalkr-remote-tty-resize-design.md new file mode 100644 index 0000000..859ef63 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/stalkr-remote-tty-resize-design.md @@ -0,0 +1,47 @@ +# Remote TTY with Terminal Resize (StalkR/misc/pty, Go) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Remote TTY with terminal resize (Linux) | +| Source URL | https://beta.pkg.go.dev/github.com/StalkR/misc/pty | +| Source publisher / author | StalkR (GitHub, misc repository) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A Go package design for a remote pty with working resize support: on `SIGWINCH`, read the new size via the `TIOCGWINSZ` ioctl, forward it over a control channel to the remote side, which applies it to its own pty master via `TIOCSWINSZ`. + +## Key findings + +- The ioctl pair (`TIOCGWINSZ` read locally, `TIOCSWINSZ` set remotely) is the standard mechanism for propagating terminal size across a process boundary (local terminal to a remote/forwarded pty). +- A control channel, separate from the raw data stream, is needed to carry resize (and other non-data) events — the write-up notes this explicitly as a multiplexing requirement on the socket design. + +## Pros / strengths + +- Names the exact two ioctls needed and the data flow between them, which is precise, directly implementable guidance rather than a vague description. + +## Cons / limitations / caveats + +- A documentation page for a Go package, not Python — the ioctl calls (`fcntl.ioctl` in Python, per the search synthesis's own sketch) are the same underlying kernel interface, but the surrounding code shape differs by language. + +## Relevance to tai + +If a tai layer ever forwards resize events itself (rather than letting tmux handle resize natively, which it already does), this is the exact mechanism: `signal.SIGWINCH` handler -> `fcntl.ioctl(fd, termios.TIOCGWINSZ, ...)` to read -> send over the control socket -> `TIOCSWINSZ` on the far end. Using tmux (which already does all of this internally) avoids needing to implement it at all. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/stonecharioteer-zellij-til.md b/research/shell-multiplexer-auto-attach/stonecharioteer-zellij-til.md new file mode 100644 index 0000000..7a7302f --- /dev/null +++ b/research/shell-multiplexer-auto-attach/stonecharioteer-zellij-til.md @@ -0,0 +1,46 @@ +# TIL: Zellij — Terminal Workspace (stonecharioteer.com) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Zellij - Terminal Workspace | +| Source URL | https://tech.stonecharioteer.com/posts/2021/til-zellij-terminal-multiplexer | +| Source publisher / author | stonecharioteer.com | +| Source type | blog post | +| Source date (published) | 2021 | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Low | +| Status | Active | + +## Summary + +A short "today I learned" note on first impressions of Zellij, emphasizing its on-screen key-hint discoverability as the standout feature versus tmux's learn-the-keybindings-from-memory model. + +## Key findings + +- Zellij's discoverability advantage (visible key hints) is a genuine, repeatedly-cited differentiator for human-facing interactive use, consistent with the dasroot.net comparison dossier. + +## Pros / strengths + +- An early (2021), first-hand account, useful as a sanity check that Zellij's core positioning (discoverable UX over tmux's muscle-memory UX) has been consistent since its early days, not a recent marketing reframe. + +## Cons / limitations / caveats + +- Old (2021) and short; Zellij has evolved since, and this note shouldn't be treated as reflecting its current (2026) feature set in detail. + +## Relevance to tai + +Minor corroborating context only — doesn't change the substrate recommendation (tmux), since the proposal's actual need is scriptability, not on-screen discoverability for a human driving the multiplexer's own UI directly. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/systemd-ptyfwd-resize-forwarding.md b/research/shell-multiplexer-auto-attach/systemd-ptyfwd-resize-forwarding.md new file mode 100644 index 0000000..131daa7 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/systemd-ptyfwd-resize-forwarding.md @@ -0,0 +1,46 @@ +# systemd ptyfwd.c — Resize Forwarding Implementation + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | ptyfwd: optionally override terminal width/height (commit) | +| Source URL | https://git.alternativebit.fr/picnoir/Systemd/commit/d435a18244ff52132d873859126e7a3ece66a9ba | +| Source publisher / author | systemd project (mirrored) | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | High | +| Status | Active | + +## Summary + +A real commit in systemd's `ptyfwd.c` implementing the mirror-image of the StalkR dossier's design: reading the size from the local stdout and applying it to a pty master via `TIOCSWINSZ`, i.e. the production-grade version of the same resize-forwarding pattern in a widely-deployed init system's own pty-forwarding code. + +## Key findings + +- Confirms the `TIOCGWINSZ`/`TIOCSWINSZ` pattern is not just a hobby-project technique — it's how one of the most widely deployed pieces of Linux infrastructure (systemd) forwards terminal size across a pty boundary. + +## Pros / strengths + +- Production code in a security- and correctness-sensitive project (systemd); a strong confidence signal for the pattern's correctness and necessity. + +## Cons / limitations / caveats + +- A single commit diff; doesn't show the full surrounding event-loop integration (how/when the resize check gets triggered) in this excerpt. + +## Relevance to tai + +Second independent confirmation (alongside StalkR) of the exact resize-forwarding mechanism — further strengthens the case that this is solved, known, and would be pure reinvention-cost if a from-scratch multiplexer were built instead of wrapping tmux (which already implements this internally and transparently). + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/tabby-typewriter-mode-docs.md b/research/shell-multiplexer-auto-attach/tabby-typewriter-mode-docs.md new file mode 100644 index 0000000..53c92ca --- /dev/null +++ b/research/shell-multiplexer-auto-attach/tabby-typewriter-mode-docs.md @@ -0,0 +1,47 @@ +# Tabby — Typewriter Mode + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | typewriter mode | +| Source URL | https://www.mintlify.com/TabbyAIKeyboard/tabby/features/automation/typewriter-mode | +| Source publisher / author | Tabby (TabbyAIKeyboard project) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Documents a feature that varies per-keystroke delay randomly, adds extra pauses after punctuation and spaces, and injects then backspace-corrects QWERTY-neighbor-key typos at a configurable rate (an example config shown uses roughly a 3% error rate with 20–100ms base delays) — a notably more elaborate "humanized typing" model than a fixed per-character delay. + +## Key findings + +- Randomized delay (a range, not a fixed value) plus punctuation/space-triggered extra pauses reads as meaningfully more human than constant-interval typing. +- Injected-then-corrected typos (not just timing variance) are the next level of realism beyond timing alone. + +## Pros / strengths + +- The most sophisticated "realistic typing" model found in this research round — a genuinely useful reference if the proposal's animated-typing command is meant to look convincingly human, not just non-instant. + +## Cons / limitations / caveats + +- Tabby's own documentation explicitly frames this feature as a way to get past paste-detection in coding interviews and proctored assessments. **That use case is out of scope and not endorsed by this dossier or by tai's design** — it is logged here purely as a reference for the underlying delay/typo-injection *mechanism*, which has entirely legitimate uses (demos, tutorials, accessibility, testing realistic input timing) independent of defeating any detection system. + +## Relevance to tai + +The mechanism (randomized delay range + punctuation pauses + optional typo-inject-and-correct) is a good advanced option for the proposal's typing-animation command, explicitly for legitimate demo/tutorial/test purposes — tai's own use is for terminal automation and demos, not deception. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/tao-of-tmux-server-internals.md b/research/shell-multiplexer-auto-attach/tao-of-tmux-server-internals.md new file mode 100644 index 0000000..c7bc608 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/tao-of-tmux-server-internals.md @@ -0,0 +1,48 @@ +# Server — tao-of-tmux + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Server — tao-of-tmux v1.0.2 documentation | +| Source URL | https://tao-of-tmux.readthedocs.io/en/latest/manuscript/04-server.html | +| Source publisher / author | tao-of-tmux project (secondary/community docs, not the tmux project itself) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A community-written explainer of tmux's server internals: persistent state kept in global red-black trees for sessions/windows/panes, a single-threaded event loop (`server_loop()`) driven by a command queue (`cmdq_next()`), and one pseudoterminal opened per pane — none of which is ever a controlling terminal for the server itself. + +## Key findings + +- Sessions/windows/panes are tracked in red-black trees inside the server process, not per-client. +- The server is single-threaded with a command queue, meaning tmux's own concurrency model is simple to reason about when scripting against it. +- Pane PTYs are opened by the server and are never its own controlling terminal — relevant if a tai layer needs to understand why signals like SIGHUP behave the way they do inside tmux panes. + +## Pros / strengths + +- Gives a mental model (event loop + command queue + red-black trees) that's useful for predicting tmux's behavior under heavy scripted automation (many rapid `send-keys`/`capture-pane` calls). + +## Cons / limitations / caveats + +- Secondary, community-authored source, not tmux's own source code or official docs — the search synthesis itself flagged this as needing verification against `server.c`/`client.c`/`proc.c` for precision. Treat specifics (libevent usage, exact tree names) as plausible, not confirmed. + +## Relevance to tai + +Useful background for anyone building a tai control layer that issues many rapid control-mode/send-keys commands — explains why tmux can serialize them safely (single command queue) but also why a flood of commands could back up behind one slow one. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/tek-tips-interactive-shell-forum.md b/research/shell-multiplexer-auto-attach/tek-tips-interactive-shell-forum.md new file mode 100644 index 0000000..b8c1639 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/tek-tips-interactive-shell-forum.md @@ -0,0 +1,28 @@ +# bash: test for an interactive shell (Tek-Tips forum) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | bash: test for an interactive shell | +| Source URL | https://www.tek-tips.com/threads/bash-test-for-an-interactive-shell.1400118/ | +| Source type | forum thread | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the interactive-shell-detection query; another forum thread on the same `$-`/`$PS1` question already covered in depth by the nixCraft and Bash Cookbook dossiers, not individually read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/termdock-terminal-multiplexing-mirrors.md b/research/shell-multiplexer-auto-attach/termdock-terminal-multiplexing-mirrors.md new file mode 100644 index 0000000..62137e8 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/termdock-terminal-multiplexing-mirrors.md @@ -0,0 +1,28 @@ +# Terminal Multiplexing: tmux, Termdock, Zellij (termdock.com mirrors) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Terminal multiplexing: tmux, termdock, zellij | +| Source URL | https://termdock.com/en/blog/terminal-multiplexing-tmux-termdock-zellij | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the multiplexer-comparison query (a near-identical `www.termdock.com` URL variant of the same article also surfaced, logged as the same source here). A vendor-affiliated comparison promoting the company's own product (Termdock) alongside tmux and Zellij — flagged explicitly as promotional per the search synthesis, and not independently read for that reason. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/theregister-tiling-multiplexer-survey.md b/research/shell-multiplexer-auto-attach/theregister-tiling-multiplexer-survey.md new file mode 100644 index 0000000..ed87abf --- /dev/null +++ b/research/shell-multiplexer-auto-attach/theregister-tiling-multiplexer-survey.md @@ -0,0 +1,46 @@ +# Tiling Terminal Multiplexers for the Console Connoisseur (The Register) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Tiling terminal multiplexers for the console connoisseur | +| Source URL | https://www.theregister.com/2025/06/24/tiling_multiplexers_survey/ | +| Source publisher / author | The Register | +| Source type | blog post | +| Source date (published) | 2025-06-24 | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A broader survey of terminal multiplexers (tmux, screen, Zellij, and others) from a mainstream tech publication, giving an independent, non-vendor-affiliated vantage point on the current state of the field. + +## Key findings + +- Confirms tmux, screen, and Zellij as the three names that keep recurring across independent surveys of this space in 2025-2026 — there isn't a fourth major contender being overlooked by this subject's research. + +## Pros / strengths + +- A non-promotional, editorially independent tech-press source, which is a useful counterweight to the more vendor- or project-affiliated blog posts elsewhere in this subject. + +## Cons / limitations / caveats + +- A survey/overview piece — light on deep technical detail compared to the man pages and project wikis already dossiered in this subject; useful for breadth, not depth. + +## Relevance to tai + +Corroborates that the choice space for a substrate is essentially tmux vs. screen vs. Zellij, and that tmux remains a live, actively-covered, mainstream-recognized choice in 2025-2026 — not a legacy tool being proposed out of inertia. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/tmux-man-page-mirrors.md b/research/shell-multiplexer-auto-attach/tmux-man-page-mirrors.md new file mode 100644 index 0000000..c597984 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/tmux-man-page-mirrors.md @@ -0,0 +1,28 @@ +# tmux(1) Man Page Mirrors (OpenBSD, die.net, commandlinux.com) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | tmux(1) - OpenBSD manual pages | +| Source URL | https://man.openbsd.org/tmux | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the tmux-architecture query as mirrors of the same man page already dossiered via man7.org (this card also stands in for https://linux.die.net/man/1/tmux and https://commandlinux.com/man-page/man1/tmux/, all the same underlying document). + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/tmuxai-dev-control-mode.md b/research/shell-multiplexer-auto-attach/tmuxai-dev-control-mode.md new file mode 100644 index 0000000..d4be209 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/tmuxai-dev-control-mode.md @@ -0,0 +1,48 @@ +# How to use tmux control mode? (tmuxai.dev) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | How to use tmux control mode? | +| Source URL | https://tmuxai.dev/tmux-control-mode/ | +| Source publisher / author | tmuxai.dev | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Explains tmux's control mode (`-C`/`-CC`): a text-only protocol where a client writes newline-terminated tmux commands to stdin, and each command's output is framed between a `%begin` line (with timestamp and command number) and a closing `%end` (success) or `%error` (failure). Asynchronous events arrive as `%`-prefixed notifications that never appear inside an output block. + +## Key findings + +- Deterministic framing (`%begin`...`%end`/`%error`) means a program can reliably know where one command's output stops, which plain `send-keys`+`capture-pane` polling cannot guarantee on its own. +- Notifications are guaranteed not to interleave inside a command's output block — a real concurrency guarantee useful for building a parser. +- `-CC` additionally disables echo, which matters for programmatic clients that don't want their own commands echoed back. + +## Pros / strengths + +- Gives the exact framing contract (`%begin`/`%end`/`%error`) needed to write a correct parser for a control-mode client, which is the structured alternative to screen-scraping `capture-pane` output. + +## Cons / limitations / caveats + +- A blog post, not tmux's own specification — cross-check framing details against the tmux source or man page before depending on them in production code. + +## Relevance to tai + +This is the mechanism for the "events" requirement in the proposal — control mode's notification stream is how a tai layer would receive pane-output/window-change events asynchronously rather than polling `capture-pane` in a loop. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/tmuxai-dev-faq.md b/research/shell-multiplexer-auto-attach/tmuxai-dev-faq.md new file mode 100644 index 0000000..687b8b3 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/tmuxai-dev-faq.md @@ -0,0 +1,48 @@ +# Tmux FAQ (tmuxai.dev) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Tmux FAQ - Frequently Asked Questions - TmuxAI | +| Source URL | https://tmuxai.dev/tmux-faq/ | +| Source publisher / author | tmuxai.dev | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A FAQ covering two distinct tmux capabilities relevant to the proposal: `capture-pane -pS - -E -` for reading the *entire* scrollback in one shot (not just the visible screen), and `pipe-pane -o 'cat >> file'` for continuously streaming a pane's live output into a file or command, stopped by re-running `pipe-pane` with no arguments. + +## Key findings + +- `capture-pane -pS - -E -` is the snapshot-read primitive: the full scrollback, start to end, in one call. +- `pipe-pane` is the streaming primitive: everything the pane produces from now on gets piped to a target (a file, or another command such as `grep`) continuously, until `pipe-pane` is invoked again with no target to stop it. +- These are two genuinely different operations (snapshot vs. stream) that a tai layer needs both of, not just one. + +## Pros / strengths + +- Gives both halves of the "remember the whole process log" requirement directly: `pipe-pane` for continuous append-only logging as it happens, `capture-pane -S -` for an on-demand full-history snapshot. + +## Cons / limitations / caveats + +- A FAQ-style page with short answers, no discussion of `pipe-pane`'s behavior across pane resize, tmux server restart, or very high-throughput output (buffering/backpressure). + +## Relevance to tai + +Directly solves the "remember the whole process log" requirement: run `pipe-pane -o 'cat >> ~/.tai/termlogs/<session>.log'` once per pane at creation time, and the entire session's raw output streams continuously into a file tai can feed through its own existing chunked FTS5 search (see `export_terminal_log` in `tai.py`) without needing a one-shot export step at all. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/toolradar-zellij-vs-tmux.md b/research/shell-multiplexer-auto-attach/toolradar-zellij-vs-tmux.md new file mode 100644 index 0000000..cf4b56e --- /dev/null +++ b/research/shell-multiplexer-auto-attach/toolradar-zellij-vs-tmux.md @@ -0,0 +1,28 @@ +# zellij vs tmux (ToolRadar) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | zellij vs tmux | +| Source URL | https://toolradar.com/compare/zellij-vs-tmux | +| Source type | other (comparison aggregator site) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the multiplexer-comparison query; a generic tool-comparison aggregator page, not independently read since the dasroot.net dossier already covers this comparison with attributed detail. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/unix-stackexchange-ssh-tmux-nesting.md b/research/shell-multiplexer-auto-attach/unix-stackexchange-ssh-tmux-nesting.md new file mode 100644 index 0000000..6353dff --- /dev/null +++ b/research/shell-multiplexer-auto-attach/unix-stackexchange-ssh-tmux-nesting.md @@ -0,0 +1,47 @@ +# Unix & Linux SE — Don't Nest tmux in a Remote SSH Shell (mirror) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | dont nest tmux in a remote ssh shell | +| Source URL | https://kompas.wasmer.app/https_unix_stackexchange_com/questions/420733/dont-nest-tmux-in-a-remote-ssh-shell | +| Source publisher / author | Unix & Linux Stack Exchange (mirrored) | +| Source type | forum thread | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Describes the specific real-world failure mode of SSH + auto-attach: `$TMUX` is a local environment variable and is never forwarded across an SSH connection, so a naive "attach if `$TMUX` unset" guard, applied to .bashrc on both the local and remote machine (e.g. a shared dotfiles setup), still ends up nesting a remote tmux session inside the local one — because the remote shell's `$TMUX` genuinely is unset, even while the user is already inside a local tmux pane that SSH'd out. + +## Key findings + +- `$TMUX` is a *local* process-tree fact, not a *session* fact — it does not and cannot know about multiplexing happening on a different machine up the SSH chain. +- The fix requires detecting the SSH hop itself (e.g. `$SSH_CONNECTION`) and treating "I am the target of an incoming SSH connection" differently from "I am a plain local shell," rather than relying on `$TMUX` to carry information it structurally cannot carry. + +## Pros / strengths + +- Precisely names a failure mode that would otherwise only be discovered by a real user hitting "sessions should be nested with care" warnings in production. + +## Cons / limitations / caveats + +- Describes the problem and its cause clearly; does not by itself prescribe one canonical fix (several exist, with tradeoffs, per the Codegenes and Coderwall dossiers in this subject). + +## Relevance to tai + +Critical for the proposal's auto-attach design: the guard logic must account for multi-hop SSH explicitly (checking `$SSH_CONNECTION`/`$SSH_TTY`, not just `$TMUX`), or a user SSHing from inside one tai-managed session into another host will get nested sessions despite the basic guard being "correct" on each host individually. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/shell-multiplexer-auto-attach/velog-tmux-vs-screen-vs-zellij.md b/research/shell-multiplexer-auto-attach/velog-tmux-vs-screen-vs-zellij.md new file mode 100644 index 0000000..69ac8bc --- /dev/null +++ b/research/shell-multiplexer-auto-attach/velog-tmux-vs-screen-vs-zellij.md @@ -0,0 +1,28 @@ +# tmux vs screen vs zellij (velog.io) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | tmux vs screen vs zellij | +| Source URL | https://velog.io/@vanillacake369/tmux-vs-screen-vs-zellij | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the multiplexer-comparison query; a three-way comparison including screen, which the other dossiered comparison sources cover less thoroughly, but not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/shell-multiplexer-auto-attach/yarnpkg-gpt-type-effect.md b/research/shell-multiplexer-auto-attach/yarnpkg-gpt-type-effect.md new file mode 100644 index 0000000..dc87f08 --- /dev/null +++ b/research/shell-multiplexer-auto-attach/yarnpkg-gpt-type-effect.md @@ -0,0 +1,28 @@ +# gpt-type-effect (classic Yarn package) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | shell-multiplexer-auto-attach | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | gpt type effect | +| Source URL | https://classic.yarnpkg.com/en/package/gpt-type-effect | +| Source type | repository | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the typing-animation query; a JavaScript package simulating GPT-style streamed typing output, adjacent but not identical to terminal keystroke simulation, not independently read. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/alexgarcia-sqlite-vec-hybrid-search.md b/research/sqlite-rag-hybrid-search/alexgarcia-sqlite-vec-hybrid-search.md new file mode 100644 index 0000000..5e910f6 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/alexgarcia-sqlite-vec-hybrid-search.md @@ -0,0 +1,51 @@ +# Hybrid Full-Text Search and Vector Search with SQLite + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Hybrid full-text search and vector search with SQLite | +| Source URL | https://alexgarcia.xyz/blog/2024/sqlite-vec-hybrid-search/index.html | +| Source publisher / author | Alex Garcia (author of the `sqlite-vec` extension) | +| Source type | blog post | +| Source date (published) | 2024 (exact day unstated) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Written by the author of `sqlite-vec` itself, this post explains how to combine SQLite's native FTS5 full-text search with `sqlite-vec` vector search in one database file: run an FTS5 query and a KNN (vector) query as separate steps/CTEs, then combine the two result sets with a `UNION ALL`, favoring keyword matches first and augmenting with vector results. + +## Key findings + +- The author's own recommended pattern is "FTS5 first, vector search augments" rather than always blending the two symmetrically — a deliberate design choice from the extension's own author, from 2024, so may not reflect the author's current (later) recommendation. +- `sqlite-vec` is explicitly a separate SQLite extension, not part of SQLite core or the Python standard library — it must be compiled/loaded as a loadable extension. +- The combination pattern (FTS5 CTE + vector CTE + UNION ALL) is lightweight and keeps everything inside a single SQLite connection, no external services. + +## Pros / strengths + +- Primary-source credibility: this is the extension author explaining the intended usage pattern, not a third party guessing at it. +- Demonstrates that hybrid search can be done with "plain SQL assembly" rather than a bespoke fusion algorithm, which is architecturally simple. + +## Cons / limitations / caveats + +- `sqlite-vec` requires SQLite's extension-loading mechanism to be enabled and the compiled extension to be present on the host — this is exactly the kind of non-stdlib, non-pure-Python dependency that conflicts with a project that must stay dependency-free (see Relevance below). +- The post is from 2024; vector-search tooling in this space moves quickly and guidance from 2024 should be treated as dated rather than current best practice without re-verification. +- "FTS5 first, vector augments" is a design opinion, not a universally agreed-upon pattern — other sources in this subject (e.g. `bigdataboutique-rrf.md`) treat true rank fusion (RRF) as the more standard approach rather than ordered augmentation. + +## Relevance to tai + +This was the single clearest piece of evidence that bringing vector search into `tai` would mean adding a compiled, non-stdlib SQLite extension (`sqlite-vec`) — directly incompatible with `tai`'s single-file, Python-standard-library-only design constraint. This was a load-bearing input to the decision in `conclusions/no-vectors-bm25-chunked-rag.md` to reject vector search entirely, independent of the project owner's separate "too heavy to create" objection to embeddings. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/sqlite-rag-hybrid-search/arxiv-2504-05324-hybrid-retrieval-hallucination.md b/research/sqlite-rag-hybrid-search/arxiv-2504-05324-hybrid-retrieval-hallucination.md new file mode 100644 index 0000000..881bfc1 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/arxiv-2504-05324-hybrid-retrieval-hallucination.md @@ -0,0 +1,28 @@ +# Hybrid Retrieval for Hallucination Mitigation in Large Language Models: A Comparative Analysis + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Hybrid Retrieval for Hallucination Mitigation in Large Language Models: A Comparative Analysis | +| Source URL | https://arxiv.org/pdf/2504.05324 | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the same hybrid-search query; title suggests relevance to hybrid (lexical+dense) retrieval quality, but not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/arxiv-2601-05264-engineering-rag-stack.md b/research/sqlite-rag-hybrid-search/arxiv-2601-05264-engineering-rag-stack.md new file mode 100644 index 0000000..7e1a4ed --- /dev/null +++ b/research/sqlite-rag-hybrid-search/arxiv-2601-05264-engineering-rag-stack.md @@ -0,0 +1,28 @@ +# Engineering the RAG Stack: A Comprehensive Review of the Architecture and Trust Frameworks for Retrieval-Augmented Generation Systems + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Engineering the RAG Stack: A Comprehensive Review of the Architecture and Trust Frameworks for Retrieval-Augmented Generation Systems | +| Source URL | https://arxiv.org/pdf/2601.05264 | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the "sqlite RAG best practices hybrid search FTS5 BM25 vector embeddings sqlite-vec" query as a general architecture-review paper for RAG systems; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/arxiv-2602-03368-rag-medical-domain.md b/research/sqlite-rag-hybrid-search/arxiv-2602-03368-rag-medical-domain.md new file mode 100644 index 0000000..61de3fe --- /dev/null +++ b/research/sqlite-rag-hybrid-search/arxiv-2602-03368-rag-medical-domain.md @@ -0,0 +1,28 @@ +# Pursuing Best Industrial Practices for Retrieval-Augmented Generation in the Medical Domain + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Pursuing Best Industrial Practices for Retrieval-Augmented Generation in the Medical Domain | +| Source URL | https://arxiv.org/pdf/2602.03368 | +| Source type | paper | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the same hybrid-search query; the surrounding search synthesis attributed the general "exact metadata lookup + BM25 candidate retrieval + optional dense retrieval + optional reranking" pipeline shape to "a 2026 guide" without a clean one-to-one citation to this specific paper, so that pipeline shape is noted here only as context, not as a verified finding of this specific source. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/bigdataboutique-rrf.md b/research/sqlite-rag-hybrid-search/bigdataboutique-rrf.md new file mode 100644 index 0000000..c3d201e --- /dev/null +++ b/research/sqlite-rag-hybrid-search/bigdataboutique-rrf.md @@ -0,0 +1,50 @@ +# Reciprocal Rank Fusion: How It Works and When to Use It + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | Reciprocal Rank Fusion: how it works and when to use it | +| Source URL | https://bigdataboutique.com/blog/reciprocal-rank-fusion-how-it-works-and-when-to-use-it | +| Source publisher / author | BigData Boutique (consultancy blog) | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Explains *why* RRF, specifically, is the standard fusion mechanism for combining a BM25 ranking with a vector-similarity ranking: the two score types (an unbounded positive BM25 score vs. a bounded cosine-similarity score) are not directly comparable, so fusing by rank position rather than raw score avoids an apples-to-oranges comparison. Cites the original Cormack, Clarke & Büttcher 2009 SIGIR paper, and states RRF is now the default hybrid-search fusion method in OpenSearch, Elasticsearch, Azure AI Search, MongoDB Atlas, and Weaviate. + +## Key findings + +- The core justification for RRF over naive score-averaging: BM25 and cosine similarity live on fundamentally different scales, so only rank order is safely comparable between them. +- Names five major production search systems (OpenSearch, Elasticsearch, Azure AI Search, MongoDB Atlas, Weaviate) that ship RRF as their default hybrid-fusion method — this is a strong, checkable claim about industry adoption. +- Traces RRF's academic origin to Cormack, Clarke & Büttcher (SIGIR 2009), giving a verifiable citation trail rather than treating RRF as a folk technique. + +## Pros / strengths + +- Strongest adoption evidence of any source in this subject — naming specific, well-known production systems makes this a falsifiable, checkable claim rather than a vague "widely used" assertion. +- Correctly identifies the *mechanism* of the problem RRF solves (incomparable score scales), not just that it's popular. + +## Cons / limitations / caveats + +- A consultancy blog post, not the original academic paper — the Cormack/Clarke/Büttcher citation should be treated as secondhand attribution unless the original 2009 paper is independently pulled. +- Does not quantify how much better RRF performs versus alternatives in any benchmark; the adoption claim is about popularity, not about measured superiority. + +## Relevance to tai + +Was the strongest single piece of evidence, during the now-superseded hybrid-search design phase, that RRF (not score-weighted fusion) would have been the correct choice had `tai` kept a vector layer. Retained for the record and in case a future, non-vector second ranked list ever needs fusing with BM25. See `conclusions/no-vectors-bm25-chunked-rag.md`. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/sqlite-rag-hybrid-search/byteplus-chunking-topic.md b/research/sqlite-rag-hybrid-search/byteplus-chunking-topic.md new file mode 100644 index 0000000..6dc0216 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/byteplus-chunking-topic.md @@ -0,0 +1,28 @@ +# BytePlus Chunking Topic Page + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | (topic page, title not individually captured) | +| Source URL | https://www.byteplus.com/en/topic/578976 | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the chunking-strategy query as a BytePlus topic page related to RAG chunking; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/cloudthat-chunking-strategies-guide.md b/research/sqlite-rag-hybrid-search/cloudthat-chunking-strategies-guide.md new file mode 100644 index 0000000..a5cecbb --- /dev/null +++ b/research/sqlite-rag-hybrid-search/cloudthat-chunking-strategies-guide.md @@ -0,0 +1,51 @@ +# A Practical Guide to Chunking Strategies in RAG Systems + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | A Practical Guide to Chunking Strategies in RAG Systems | +| Source URL | https://www.cloudthat.com/resources/blog/a-practical-guide-to-chunking-strategies-in-rag-systems | +| Source publisher / author | CloudThat (vendor blog) | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A practitioner-oriented guide recommending a "define goals, pick a strategy, measure, iterate" workflow for RAG chunking, with a concrete overlap-size recommendation (20-50% of chunk size) and an explicit warning that changing chunking strategy later requires reindexing the entire knowledge base from scratch. + +## Key findings + +- Recommends chunk overlap in the 20-50% range as a starting point, trading some content duplication for reduced risk of splitting an answer across a chunk boundary. +- States plainly that overlap increases redundancy and processing cost — it is presented as a tradeoff, not a free win. +- Flags that chunking-strategy changes are expensive after the fact: the entire corpus needs reindexing, which argues for picking a reasonable default deliberately rather than treating it as trivially changeable later. +- Recommends a measure-then-iterate workflow (test real queries against retrieval quality) rather than picking a chunk size by intuition alone. + +## Pros / strengths + +- Gives an actual numeric starting range for overlap (20-50%) rather than vague guidance, which is directly usable as a design input. +- The "reindexing is costly" warning is operationally important and often omitted by other chunking-strategy writeups. + +## Cons / limitations / caveats + +- The 20-50% overlap range is presented as general practitioner guidance, not derived from a controlled experiment in this source; it should be treated as a reasonable starting point, not a proven optimum. +- No chunking-strategy guidance specific to command-line/terminal-log text was found in this source — it is written for general document/knowledge-base RAG. + +## Relevance to tai + +The overlap-size guidance directly shaped `tai`'s `chunk_lines()` design, though `tai` ultimately chose a smaller, line-count-based overlap (3 lines) rather than a 20-50% character-based overlap, judged a better fit for line-oriented terminal/log content where whole-line boundaries matter more than character-percentage boundaries. The "reindexing is costly" warning was a factor in deciding to commit to one chunking function used everywhere (`chunk_lines()`) rather than letting each feature invent its own chunking rule. See `conclusions/no-vectors-bm25-chunked-rag.md`. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/sqlite-rag-hybrid-search/cohere-chunking-strategies.md b/research/sqlite-rag-hybrid-search/cohere-chunking-strategies.md new file mode 100644 index 0000000..8210b4d --- /dev/null +++ b/research/sqlite-rag-hybrid-search/cohere-chunking-strategies.md @@ -0,0 +1,28 @@ +# Chunking Strategies (Cohere Docs) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | chunking strategies | +| Source URL | https://docs.cohere.com/page/chunking-strategies | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the chunking-strategy query as Cohere's own official documentation page on chunking; not individually opened or quoted during this research pass (a near-duplicate URL for the same page also surfaced and is treated as the same source here). + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/conclusions/no-vectors-bm25-chunked-rag.md b/research/sqlite-rag-hybrid-search/conclusions/no-vectors-bm25-chunked-rag.md new file mode 100644 index 0000000..c0f1206 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/conclusions/no-vectors-bm25-chunked-rag.md @@ -0,0 +1,48 @@ +# Conclusion: tai's RAG Is Pure SQLite FTS5/BM25, Chunked, With No Vector Layer + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Conclusion | +| Tier | N/A | +| Date decided | 2026-10-09 | +| Decided by | retoor, advised by tai (agent, Claude Sonnet 5) | +| Status | Active | +| Confidence | High | + +## Question / problem statement + +How should `tai` implement retrieval over large stored text — terminal log exports, saved records, spilled shell/subagent output — in a way that is fast, searchable, and filterable at retrieval-grade (not just document-grade) granularity, while staying consistent with `tai`'s hard constraint of being a single Python file depending on nothing beyond the standard library? + +## Conclusion + +`tai` implements its RAG system using pure SQLite FTS5 full-text search with SQLite's built-in `bm25()` ranking function. Content is split into overlapping, line-bounded chunks (`chunk_lines()`: ~60 lines or 4000 characters per chunk, whichever comes first, 3-line overlap, never splitting mid-line) and each chunk is indexed as its own row in the existing unified `fts_docs` FTS5 table via SQL triggers. There is deliberately **no vector embedding layer and no vector database** anywhere in this system — no `sqlite-vec`, no hosted embedding API, no local embedding model. + +## Supporting sources + +- `../novel-rag-github.md`, `../devto-ditched-vector-db-fts5.md`, `../alexgarcia-sqlite-vec-hybrid-search.md`, `../sqlite-search-github.md`, `../talentica-sqlite-vector-db.md` — established that "SQLite FTS5 + a vector extension" is a real, assembled pattern elsewhere, which grounded the initial (superseded) hybrid proposal and confirmed the rejection below was made against a genuinely viable alternative, not a strawman. +- `../f22labs-7-chunking-strategies.md` — basis for treating fixed-size/line-bounded chunking as a legitimate choice (not merely a lazy fallback) specifically for unstructured, low-punctuation text such as terminal output. +- `../cloudthat-chunking-strategies-guide.md` — basis for using overlap at all, and for treating the chunking function as a single, deliberately-chosen piece of infrastructure rather than something to vary per feature (its warning that changing chunking strategy later requires a full reindex). +- `../spiceai-reciprocal-rank-fusion.md`, `../bigdataboutique-rrf.md`, `../paradedb-rrf.md` — researched as the fusion mechanism for the hybrid BM25+vector design that was ultimately not adopted; retained because the RRF formula and k=60 convention remain reusable if a second ranked list is ever introduced for a non-vector reason. + +## Alternatives considered and rejected + +- **Hybrid BM25 + vector search, fused with RRF** (the design actually researched and initially proposed, backed by essentially all ten Tier 1 sources above). Rejected for two independent reasons: (1) the project owner (retoor) explicitly vetoed embeddings mid-conversation as too heavy/expensive to create; (2) independent of that veto, `alexgarcia-sqlite-vec-hybrid-search.md` confirmed that any real vector-search option requires a compiled, non-stdlib SQLite extension (`sqlite-vec`) or a hosted embedding API — either way, a new dependency that conflicts with `tai`'s single-file, pure-stdlib design rule. +- **`sqlite-vec`-only vector search (no lexical fallback)** — never seriously entered consideration once vectors were vetoed; noted here only for completeness. + +## Risks / open questions + +BM25-only search cannot find content that is semantically similar but lexically different — e.g. a paraphrased error description that doesn't share the original's exact wording. This is an accepted tradeoff given `tai`'s dependency constraints, and is judged low-risk given the actual content this system searches (command output, logs, literal error strings, file paths) where lexical match is usually the right signal to begin with. This judgment has not been empirically tested against real retrieval-quality metrics (no Hit@K/MRR-style evaluation, unlike `../novel-rag-github.md`'s own approach) — revisit if real-world search misses become a reported problem. + +## Action taken + +Implemented the same day as this research: `chunk_lines()` and `collapse_repeated_lines()` helper functions, a new `chunks` SQL table wired into the existing trigger-based `fts_docs` FTS5 index, wired into `record_save`, the shell/subagent output spill path (`spill_output`), and a new `export_terminal_log` tool, all in `tai.py`. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial conclusion recorded. | diff --git a/research/sqlite-rag-hybrid-search/databricks-ultimate-guide-chunking.md b/research/sqlite-rag-hybrid-search/databricks-ultimate-guide-chunking.md new file mode 100644 index 0000000..35ba937 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/databricks-ultimate-guide-chunking.md @@ -0,0 +1,28 @@ +# The Ultimate Guide to Chunking Strategies for RAG Applications + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | The Ultimate Guide to Chunking Strategies for RAG Applications | +| Source URL | https://community.databricks.com/t5/technical-blog/the-ultimate-guide-to-chunking-strategies-for-rag-applications/bc-p/115892 | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the chunking-strategy query from Databricks's own community technical-blog platform; not individually opened or quoted during this research pass (a near-duplicate URL for the same article also surfaced and is treated as the same source here). + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/devto-ayush-kumar-practical-chunking.md b/research/sqlite-rag-hybrid-search/devto-ayush-kumar-practical-chunking.md new file mode 100644 index 0000000..d9d8b59 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/devto-ayush-kumar-practical-chunking.md @@ -0,0 +1,28 @@ +# Practical RAG Chunking Strategies for Reliable RAG Pipelines + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Practical RAG Chunking Strategies for Reliable RAG Pipelines | +| Source URL | https://dev.to/ayush_kumar_085a0f2c54e3f/practical-rag-chunking-strategies-for-reliable-rag-pipelines-2f5e | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the chunking-strategy query; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/devto-ditched-vector-db-fts5.md b/research/sqlite-rag-hybrid-search/devto-ditched-vector-db-fts5.md new file mode 100644 index 0000000..d3cba05 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/devto-ditched-vector-db-fts5.md @@ -0,0 +1,50 @@ +# I Ditched Cloud Vector Databases for SQLite FTS5 — and My RAG Pipeline Got 10x Better + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | I Ditched Cloud Vector Databases for SQLite FTS5 — and My RAG Pipeline Got 10x Better | +| Source URL | https://dev.to/cagrik34/i-ditched-cloud-vector-databases-for-sqlite-fts5-and-my-rag-pipeline-got-10x-better-759 | +| Source publisher / author | dev.to user cagrik34 | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Low | +| Status | Active | + +## Summary + +A first-person account of replacing a cloud vector database with plain SQLite FTS5 for a RAG pipeline, with the headline claim of a 10x improvement. The specific metric being 10x'd (latency, cost, accuracy, or something else) was not recoverable from the search synthesis alone. + +## Key findings + +- The author's lexical-search-only pipeline reportedly outperformed their prior cloud-vector-database pipeline for their specific workload. +- Positioned as direct practitioner evidence that BM25-only search over SQLite FTS5 is not automatically inferior to a vector pipeline, at least for some real workloads. + +## Pros / strengths + +- First-person, concrete practitioner experience rather than vendor marketing — useful as a counterweight to the general industry assumption that "more vector search is always better." +- Directly relevant case: a developer actively chose to remove vector search and keep FTS5, which is close to the decision `tai` ultimately made (see conclusion). + +## Cons / limitations / caveats + +- The "10x better" claim's exact metric, workload, and corpus size were not established during this research pass — this is a title-level claim, not an independently verified benchmark. +- Single-author blog post, not peer-reviewed, no reproduction data available to this research pass. +- Should not be treated as generalizable evidence that FTS5 beats vector search broadly — only as one data point that it can, for some workloads. + +## Relevance to tai + +Directly supportive, anecdotally, of the path `tai` ultimately took: dropping vector search entirely in favor of SQLite FTS5 + BM25. Used as corroborating (not conclusive) evidence in `conclusions/no-vectors-bm25-chunked-rag.md`. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/sqlite-rag-hybrid-search/devto-rasinmuhammed-chunking-tui.md b/research/sqlite-rag-hybrid-search/devto-rasinmuhammed-chunking-tui.md new file mode 100644 index 0000000..396c8e7 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/devto-rasinmuhammed-chunking-tui.md @@ -0,0 +1,28 @@ +# I Built a TUI to Visualize RAG Chunking Because chunk_size=1000 Is a Lie + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | I built a TUI to visualize RAG chunking because chunk_size=1000 is a lie | +| Source URL | https://dev.to/rasinmuhammed/i-built-a-tui-to-visualize-rag-chunking-because-chunksize1000-is-a-lie-1dg0 | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the chunking-strategy query; describes a terminal-UI tool for visualizing chunk boundaries, which looked topically close to this project's own terminal-oriented tooling. Not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/f22labs-7-chunking-strategies.md b/research/sqlite-rag-hybrid-search/f22labs-7-chunking-strategies.md new file mode 100644 index 0000000..c72403c --- /dev/null +++ b/research/sqlite-rag-hybrid-search/f22labs-7-chunking-strategies.md @@ -0,0 +1,50 @@ +# 7 Chunking Strategies in RAG You Need to Know + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | 7 Chunking Strategies in RAG You Need to Know | +| Source URL | https://www.f22labs.com/blogs/7-chunking-strategies-in-rag-you-need-to-know/ | +| Source publisher / author | F22 Labs (vendor blog) | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +An enumeration of seven distinct chunking strategies used in RAG pipelines (ranging from naive fixed-size splitting to semantic/structure-aware splitting), framing chunk-boundary choice as "the most under-estimated hyperparameter in RAG." + +## Key findings + +- Explicitly names fixed-size splitting as the simplest and fastest method, but notes it often breaks sentences abruptly and ignores meaning. +- States that fixed-size splitting is a reasonable fit specifically for unstructured text with little inherent structure (its example: old transcripts without punctuation), which is notably close to the terminal-log use case `tai` was designing for. +- Frames chunk-boundary quality as directly affecting retrieval quality, not merely a storage/performance detail — a bad boundary can split the one line that answers the query away from its context. + +## Pros / strengths + +- Directly actionable: gives a concrete decision rule ("if the text lacks structure/punctuation, fixed-size splitting is a legitimate choice, not just a fallback") rather than only advocating for the most sophisticated option. +- Frames chunking as a first-class design decision rather than an implementation detail, which matched the level of attention this subject's research gave it. + +## Cons / limitations / caveats + +- A single vendor blog post enumerating strategies; it does not provide controlled benchmark comparisons between the seven strategies it names. +- No specific guidance was found on chunk-overlap size from this particular source (that came from `cloudthat-chunking-strategies-guide.md` instead). + +## Relevance to tai + +Directly informed the decision to use a simple line-bounded fixed-window chunker (`chunk_lines()`) for terminal logs and other operational text, rather than attempting semantic/sentence-aware chunking — this source's framing of fixed-size splitting as legitimate (not merely lazy) for unstructured, punctuation-sparse text matched exactly what terminal output looks like. See `conclusions/no-vectors-bm25-chunked-rag.md`. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/sqlite-rag-hybrid-search/glama-daemonmcp-fusion.md b/research/sqlite-rag-hybrid-search/glama-daemonmcp-fusion.md new file mode 100644 index 0000000..97a8f3e --- /dev/null +++ b/research/sqlite-rag-hybrid-search/glama-daemonmcp-fusion.md @@ -0,0 +1,28 @@ +# Daemon-MCP fusion.py + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | daem0nmcp/fusion.py | +| Source URL | https://glama.ai/mcp/servers/@DasBluEyedDevil/Daemon-MCP/blob/65abe11f4760a5899e1b81084499127b34acc02b/daem0nmcp/fusion.py | +| Source type | repository (source file) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the RRF-formula query as an MCP server's own RRF fusion implementation file; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/glama-orneryd-mimir-rrf.md b/research/sqlite-rag-hybrid-search/glama-orneryd-mimir-rrf.md new file mode 100644 index 0000000..d58ad0b --- /dev/null +++ b/research/sqlite-rag-hybrid-search/glama-orneryd-mimir-rrf.md @@ -0,0 +1,28 @@ +# Mimir reciprocal-rank-fusion.ts + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | reciprocal-rank-fusion.ts (Mimir MCP server) | +| Source URL | https://glama.ai/mcp/servers/@orneryd/Mimir/blob/01c0203d447ab4f6e04aa15d0c830d92bd4f2749/src/utils/reciprocal-rank-fusion.ts | +| Source type | repository (source file) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the RRF-formula query as a second MCP server's own RRF implementation file (TypeScript), alongside `glama-daemonmcp-fusion.md`; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/glukhov-chunking-strategies-rag.md b/research/sqlite-rag-hybrid-search/glukhov-chunking-strategies-rag.md new file mode 100644 index 0000000..ca6713a --- /dev/null +++ b/research/sqlite-rag-hybrid-search/glukhov-chunking-strategies-rag.md @@ -0,0 +1,28 @@ +# Chunking Strategies in RAG: Comparison, Trade-offs, and Examples + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Chunking Strategies in RAG Comparison: Alternatives, Trade‑offs, and Examples | +| Source URL | https://www.glukhov.org/rag/retrieval/chunking-strategies-in-rag/ | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the chunking-strategy query; a personal technical blog comparing chunking approaches. Not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/ingoampt-bm25-fts5-google.md b/research/sqlite-rag-hybrid-search/ingoampt-bm25-fts5-google.md new file mode 100644 index 0000000..1346da2 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/ingoampt-bm25-fts5-google.md @@ -0,0 +1,28 @@ +# BM25 Is a Ranking Algorithm. FTS5 Is a Full-Text Search Engine Inside SQLite. Does Google Use BM25? + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | BM25 is a ranking algorithm. FTS5 is a full-text search engine inside SQLite. Does Google use BM25? | +| Source URL | https://ingoampt.com/bm25-is-a-ranking-algorithm-fts5-is-a-full-text-search-engine-module-inside-sqlite-does-google-use-bm25/ | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the same hybrid-search query as a BM25-vs-FTS5 explainer; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/medium-privexbot-chunking-strategies.md b/research/sqlite-rag-hybrid-search/medium-privexbot-chunking-strategies.md new file mode 100644 index 0000000..fc7726e --- /dev/null +++ b/research/sqlite-rag-hybrid-search/medium-privexbot-chunking-strategies.md @@ -0,0 +1,28 @@ +# Chunking Strategies: Optimizing Knowledge Base Performance + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Chunking Strategies: Optimizing Knowledge Base Performance | +| Source URL | https://medium.com/@privexbot/chunking-strategies-optimizing-knowledge-base-performance-e2ef08b0ce22 | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the chunking-strategy query; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/memx-rrf-glossary.md b/research/sqlite-rag-hybrid-search/memx-rrf-glossary.md new file mode 100644 index 0000000..cc1e64c --- /dev/null +++ b/research/sqlite-rag-hybrid-search/memx-rrf-glossary.md @@ -0,0 +1,28 @@ +# Retrieval & Context — Reciprocal Rank Fusion (memx.app Glossary) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Retrieval & Context (Reciprocal Rank Fusion glossary entry) | +| Source URL | https://memx.app/glossary/reciprocal-rank-fusion-rrf/ | +| Source type | other (glossary page) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the RRF-formula query as a glossary-style definition page; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/novel-rag-github.md b/research/sqlite-rag-hybrid-search/novel-rag-github.md new file mode 100644 index 0000000..61a6db1 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/novel-rag-github.md @@ -0,0 +1,51 @@ +# novel-rag: Hybrid BM25 + Dense Retrieval over SQLite FTS5 + sqlite-vec + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | novel-rag: Hybrid BM25 + dense retrieval (RAG) over a fiction library -> SQLite FTS5 + sqlite-vec, fused with Reciprocal Rank Fusion, with a Hit@K/MRR eval harness | +| Source URL | https://github.com/Rohit-rockan/novel-rag | +| Source publisher / author | GitHub user Rohit-rockan | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A personal/hobby RAG project that retrieves passages from a fiction library using a hybrid pipeline: SQLite FTS5 for lexical (BM25) search, `sqlite-vec` for dense vector search, the two fused with Reciprocal Rank Fusion, evaluated with a Hit@K / MRR harness over the fiction corpus. + +## Key findings + +- Demonstrates the "SQLite FTS5 + sqlite-vec + RRF" pattern as something an individual developer can assemble without a dedicated vector database service. +- Explicitly builds an evaluation harness (Hit@K, MRR) rather than shipping the hybrid design untested — a methodological point worth noting independent of the specific repo's code quality. +- Confirms RRF (not score-weighted fusion) is a viable, implementable choice for fusing a lexical and a dense ranking from the same SQLite file. + +## Pros / strengths + +- Concrete, runnable existence proof that the FTS5+sqlite-vec+RRF combination works end-to-end in a single SQLite file, which is the architecture family this project's "tai" vault already resembles (FTS5 is already in use). +- Including an eval harness is a good practice signal — the repo isn't just claiming the hybrid approach works, it measures it. + +## Cons / limitations / caveats + +- A single hobby/personal repository, not a peer-reviewed or widely-adopted reference implementation; no independent confirmation of the eval numbers was found during this research pass. +- No information was gathered on its actual Hit@K/MRR results, chunking strategy, or RRF constant (k) choice — only the high-level architecture description was available from the search result. +- "Fiction library" is a narrow, favorable domain for lexical search (named characters, specific phrases) and may not generalize to the kind of operational/log text `tai` needs to search. + +## Relevance to tai + +Served as the first concrete evidence, early in this research pass, that "SQLite FTS5 + vector extension + RRF" is a real, assembled pattern other developers use for local-first RAG — this grounded the initial (later superseded) proposal to give `tai` a hybrid BM25+vector search layer. See `conclusions/no-vectors-bm25-chunked-rag.md` for why that proposal was ultimately not adopted. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/sqlite-rag-hybrid-search/paradedb-rrf.md b/research/sqlite-rag-hybrid-search/paradedb-rrf.md new file mode 100644 index 0000000..f093d70 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/paradedb-rrf.md @@ -0,0 +1,47 @@ +# What Is Reciprocal Rank Fusion? + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | What is Reciprocal Rank Fusion? | +| Source URL | https://www.paradedb.com/learn/search-concepts/reciprocal-rank-fusion | +| Source publisher / author | ParadeDB (vendor "learn" page) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +Covers the same RRF mechanism as the other two RRF sources in this subject, but is the specific source for RRF's main documented *limitation*: because RRF only uses rank position, it treats a document that is overwhelmingly the best match in one list only slightly better than the runner-up — the magnitude of the win is discarded, not just normalized. + +## Key findings + +- RRF's score-magnitude blindness is named explicitly as the technique's main limitation, with the suggestion that a score-aware fusion or a post-RRF reranker can help when score gaps genuinely matter. +- Corroborates (independently of `spiceai-reciprocal-rank-fusion.md`) the standard formula and the k=60 convention. + +## Pros / strengths + +- The clearest, most direct statement among the three RRF sources of RRF's actual weakness, rather than treating RRF as a strictly-better free lunch. + +## Cons / limitations / caveats + +- Vendor "learn" page (ParadeDB, a Postgres-search company); the limitation is stated plainly but no quantitative example or benchmark accompanies it in what was gathered during this research pass. + +## Relevance to tai + +Directly informed the final proposal's framing (in the separate `agent-memory-recency-tracking` subject, not this one) that if `tai` ever revisits combining signals — e.g. lexical BM25 plus some future recency/frequency signal — it should go in eyes-open about RRF discarding score magnitude, which matters more for a small, low-cardinality signal like recency than for two large ranked lists. See `conclusions/no-vectors-bm25-chunked-rag.md`. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/sqlite-rag-hybrid-search/popoto-hybrid-retrieval-rrf.md b/research/sqlite-rag-hybrid-search/popoto-hybrid-retrieval-rrf.md new file mode 100644 index 0000000..ebb0c77 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/popoto-hybrid-retrieval-rrf.md @@ -0,0 +1,28 @@ +# Hybrid Retrieval: BM25 + RRF Fusion (Popoto Docs) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Hybrid Retrieval: BM25 + RRF Fusion | +| Source URL | https://popoto.readthedocs.io/en/latest/features/hybrid-retrieval/ | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the RRF-formula query as a ReadTheDocs page for a project named Popoto describing its own BM25+RRF hybrid retrieval feature; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/quizzly-chunking-heuristics.md b/research/sqlite-rag-hybrid-search/quizzly-chunking-heuristics.md new file mode 100644 index 0000000..ff504fd --- /dev/null +++ b/research/sqlite-rag-hybrid-search/quizzly-chunking-heuristics.md @@ -0,0 +1,28 @@ +# Chunk Size, Overlap, and Separator Heuristics in Practice + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Chunk Size, Overlap, and Separator Heuristics in Practice | +| Source URL | https://quizzly.ai/play/chunk-size-overlap-and-separator-heuristics-in-practice | +| Source type | other (interactive quiz/practice page) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the chunking-strategy query as a quizzly.ai interactive practice page on chunk-size/overlap heuristics; a closely related second quizzly.ai page ("Text Chunking and Overlap Heuristics") also surfaced in the same search and is treated as the same source family here. Not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/redis-reciprocal-rank-fusion.md b/research/sqlite-rag-hybrid-search/redis-reciprocal-rank-fusion.md new file mode 100644 index 0000000..0d9bad1 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/redis-reciprocal-rank-fusion.md @@ -0,0 +1,28 @@ +# Reciprocal Rank Fusion (Redis Blog) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | reciprocal rank fusion | +| Source URL | https://redis.io/en/blog/reciprocal-rank-fusion/ | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the RRF-formula query as Redis's own blog explainer of RRF (relevant since Redis offers hybrid search features); not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/software-engineer-blog-fts5-inverted-index.md b/research/sqlite-rag-hybrid-search/software-engineer-blog-fts5-inverted-index.md new file mode 100644 index 0000000..f3de8b6 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/software-engineer-blog-fts5-inverted-index.md @@ -0,0 +1,28 @@ +# SQLite FTS5: How Full-Text Search Actually Works + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | SQLite FTS5: How Full-Text Search Actually Works | +| Source URL | https://software-engineer-blog.com/content/sqlite-fts5-how-full-text-search-actually-works-inverted-index-bm25?id=131 | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the same hybrid-search query as an FTS5 internals (inverted index, bm25) explainer; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/spiceai-reciprocal-rank-fusion.md b/research/sqlite-rag-hybrid-search/spiceai-reciprocal-rank-fusion.md new file mode 100644 index 0000000..60f9768 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/spiceai-reciprocal-rank-fusion.md @@ -0,0 +1,50 @@ +# What Is Reciprocal Rank Fusion (RRF)? + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | What is Reciprocal Rank Fusion (RRF)? | +| Source URL | https://spice.ai/learn/reciprocal-rank-fusion | +| Source publisher / author | Spice AI (vendor docs/learn page) | +| Source type | vendor docs | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +An explainer defining Reciprocal Rank Fusion precisely: for a document d across n ranked lists, RRF(d) = Σ 1/(k + rank_i(d)), where rank_i(d) is d's 1-indexed position in list i and k is a damping constant, typically 60. A document absent from a list contributes 0 for that list. Weighted variants multiply each list's term by a per-list weight. + +## Key findings + +- Gives the exact formula with the standard constant (k=60) named explicitly, which is more precise than most of the other sources consulted on this subject. +- Explains the purpose of k: dampening the influence of very high ranks so one list's #1 result doesn't dominate the fused score disproportionately. +- Confirms the weighted-RRF variant exists (per-list multiplier) as a documented extension of the base formula, not an ad hoc addition. + +## Pros / strengths + +- The most mathematically precise of the RRF sources consulted in this subject — gives the actual formula rather than only a qualitative description. +- Directly usable as an implementation reference if RRF is ever revisited for `tai`. + +## Cons / limitations / caveats + +- Vendor "learn" page (Spice AI), so framing may lean toward making RRF look simpler/more universally applicable than it is in edge cases (e.g. very short candidate lists). +- Does not address RRF's documented weakness (also see `paradedb-rrf.md`) that it discards score magnitude, only rank order. + +## Relevance to tai + +This was the primary source for the exact RRF formula during the (ultimately not adopted) hybrid BM25+vector design phase. Preserved here because the formula and the k=60 convention remain directly reusable if `tai` ever revisits combining multiple ranked lists (e.g. fusing BM25 results with a future non-vector secondary signal). See `conclusions/no-vectors-bm25-chunked-rag.md` for why RRF itself is not currently used. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/sqlite-rag-hybrid-search/sqlite-search-github.md b/research/sqlite-rag-hybrid-search/sqlite-search-github.md new file mode 100644 index 0000000..bb053c9 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/sqlite-search-github.md @@ -0,0 +1,50 @@ +# sqlite_search: Full-Text, Vector, and Hybrid Search for ActiveRecord + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | sqlite_search: Full-text, vector, and hybrid search for ActiveRecord, without leaving SQLite | +| Source URL | https://github.com/radioactive-labs/sqlite_search | +| Source publisher / author | GitHub organization radioactive-labs | +| Source type | repository | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Medium | +| Status | Active | + +## Summary + +A Ruby/ActiveRecord library adding full-text, vector, and hybrid search to a Rails app using only SQLite — no separate search service. Notably, its query parser strips FTS5 operator syntax out of untrusted input before it reaches SQLite, as a deliberate safety measure. + +## Key findings + +- Demonstrates the "search stays inside SQLite, no external search service" philosophy applied at the application-framework level (ActiveRecord/Rails), not just in a one-off script. +- Explicitly sanitizes/strips FTS5 query operator syntax from user input — a concrete, named security practice (preventing FTS5 MATCH syntax injection from untrusted search boxes). +- Offers full-text, vector, and hybrid modes as named options, implying all three were considered legitimate, supported configurations by the library's authors, not just a single blessed path. + +## Pros / strengths + +- The input-sanitization detail is a genuinely transferable, concrete security practice independent of the Ruby ecosystem: raw user text should never be handed to FTS5's MATCH operator unescaped. +- Evidence that "hybrid search entirely inside one SQLite file" is being productized as a reusable library, not only hand-rolled per project. + +## Cons / limitations / caveats + +- Ruby/ActiveRecord-specific; no architectural detail on its vector-search implementation (which extension, if any) was recoverable from the search synthesis. +- No benchmark or production-scale evidence was found during this research pass — only the existence and stated feature set of the library. + +## Relevance to tai + +The FTS5-input-sanitization point directly reinforced existing `tai` behavior worth double-checking: `tai.py`'s own `fts_escape()`/`fts_match()` functions already tokenize and quote search terms before building the FTS5 `MATCH` expression, which is the same defensive pattern this library implements independently. This source corroborated that `tai`'s existing approach (never passing raw user text into a MATCH clause) matches recognized practice elsewhere. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/sqlite-rag-hybrid-search/stackai-chunking-strategies.md b/research/sqlite-rag-hybrid-search/stackai-chunking-strategies.md new file mode 100644 index 0000000..e94df64 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/stackai-chunking-strategies.md @@ -0,0 +1,28 @@ +# Chunking Strategies for RAG: How to Optimize Document Retrieval + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Chunking Strategies for RAG: How to Optimize Document Retrieval | +| Source URL | https://www.stack-ai.com/insights/chunking-strategies-for-rag-how-to-optimize-document-retrieval | +| Source type | vendor docs | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the chunking-strategy query (a `stackai.com` domain mirror of the same article also surfaced and is treated as the same source here); not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/talentica-sqlite-vector-db.md b/research/sqlite-rag-hybrid-search/talentica-sqlite-vector-db.md new file mode 100644 index 0000000..a3efd3e --- /dev/null +++ b/research/sqlite-rag-hybrid-search/talentica-sqlite-vector-db.md @@ -0,0 +1,48 @@ +# SQLite as a Vector DB: A Guide to Isolated Local-First RAG + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Dossier | +| Tier | 1 | +| Source title | SQLite as a Vector DB: A Guide to Isolated Local-First RAG | +| Source URL | https://www.talentica.com/blogs/sqlite-vector-database/ | +| Source publisher / author | Talentica Software (vendor blog) | +| Source type | blog post | +| Source date (published) | Unknown — not stated | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | web search synthesis | +| Confidence | Low | +| Status | Active | + +## Summary + +A vendor/consultancy blog post pitching SQLite (via a vector extension) as a viable local-first vector database for RAG applications that need isolation (e.g. per-tenant or per-device databases) rather than a shared cloud vector store. + +## Key findings + +- Frames SQLite-as-vector-DB primarily around the "local-first / isolated" use case — single-file portability and no shared infrastructure — rather than around raw performance claims. +- Reinforces, from a different commercial angle than `alexgarcia-sqlite-vec-hybrid-search.md`, that a vector extension is a prerequisite for this pattern. + +## Pros / strengths + +- Useful as a second, independent voice confirming that "SQLite as the whole RAG backend" is being discussed industry-wide in 2024/2025-era content, not just by extension authors and hobbyists. + +## Cons / limitations / caveats + +- A consultancy/vendor marketing post; it is implicitly selling the author's own integration services, so claims should be weighted lower than a neutral or primary-source account. +- No benchmark data, no specific extension-version detail, and no code was independently verified during this research pass. + +## Relevance to tai + +Contributed mainly as corroborating context that "SQLite + vector extension" is a recognized industry pattern, which strengthened confidence that the *rejection* of vectors in `conclusions/no-vectors-bm25-chunked-rag.md` was a deliberate tradeoff against a real, viable alternative — not a decision made in ignorance of one. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Initial dossier created. | diff --git a/research/sqlite-rag-hybrid-search/towardsai-21-chunking-strategies.md b/research/sqlite-rag-hybrid-search/towardsai-21-chunking-strategies.md new file mode 100644 index 0000000..35e6aa5 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/towardsai-21-chunking-strategies.md @@ -0,0 +1,28 @@ +# 21 Chunking Strategies That Will Fix Your Broken RAG System + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | 21 Chunking Strategies That Will Fix Your Broken RAG System | +| Source URL | https://pub.towardsai.net/21-chunking-strategies-that-will-fix-your-broken-rag-system-14dac3f2b067 | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the chunking-strategy query as an extensive (21-strategy) enumeration; not individually opened or quoted during this research pass — `f22labs-7-chunking-strategies.md` served as this subject's actual Tier 1 chunking-taxonomy source instead. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/towardsai-stop-blaming-llm-chunking.md b/research/sqlite-rag-hybrid-search/towardsai-stop-blaming-llm-chunking.md new file mode 100644 index 0000000..d91df99 --- /dev/null +++ b/research/sqlite-rag-hybrid-search/towardsai-stop-blaming-llm-chunking.md @@ -0,0 +1,28 @@ +# Stop Blaming Your LLM, Your RAG System Is Failing Because of Chunking + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | Stop Blaming Your LLM, Your RAG System Is Failing Because of Chunking | +| Source URL | https://pub.towardsai.net/stop-blaming-your-llm-your-rag-system-is-failing-because-of-chunking-521e4dbdd20d | +| Source type | blog post | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the chunking-strategy query; the general theme ("definitions split across chunk boundaries, so the model misses the key line") echoed in the search synthesis without being individually attributed to this exact article. Not independently opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/research/sqlite-rag-hybrid-search/unimon-rrf-glossary.md b/research/sqlite-rag-hybrid-search/unimon-rrf-glossary.md new file mode 100644 index 0000000..421e5dc --- /dev/null +++ b/research/sqlite-rag-hybrid-search/unimon-rrf-glossary.md @@ -0,0 +1,28 @@ +# RRF Glossary Entry (unimon.co.th) + +## Metadata + +| Field | Value | +|---|---| +| Institute | Molodetz Institute — Research & Retrieval Division | +| Subject | sqlite-rag-hybrid-search | +| Document type | Source Citation Card | +| Tier | 2 | +| Source title | RRF (glossary entry) | +| Source URL | https://unimon.co.th/en/glossary/rrf | +| Source type | other (glossary page) | +| Date researched | 2026-10-09 | +| Researched by | tai (agent), model: Claude Sonnet 5, on behalf of retoor | +| Research method | Search aggregator result listing (not independently opened or quoted) | +| Confidence | Not independently verified | +| Status | Active | + +## Why it's here + +Surfaced by the RRF-formula query as a glossary-style definition page; not individually opened or quoted during this research pass. + +## Changelog + +| Date | Change | +|---|---| +| 2026-10-09 | Logged as consulted-but-not-analyzed. | diff --git a/tai.py b/tai.py index d51e48c..a9195c1 100755 --- a/tai.py +++ b/tai.py @@ -17,6 +17,7 @@ import random import re import shutil import shlex +import signal import socket import sqlite3 import ssl @@ -43,6 +44,14 @@ try: except ImportError: fcntl = None +try: + import pty + import select + import termios + import tty +except ImportError: + pty = select = termios = tty = None + # Python 3.6 compatibility notes: # - subprocess 'text' and 'capture_output' kwargs were added in 3.7, so this # module only uses 'universal_newlines' plus explicit PIPEs (works on 3.6+). @@ -224,7 +233,7 @@ RestartSec=10 [Install] WantedBy=default.target """ -INSTALL_TARGETS = ("binary", "bash-hook", "venv", "scheduler-service", "telegram-service", "container") +INSTALL_TARGETS = ("binary", "bash-hook", "plexer-hook", "venv", "scheduler-service", "telegram-service", "container") CONTEXT_CAP = 32000 COMPACT_RATIO = 0.8 KEEP_TURNS = 6 @@ -1682,6 +1691,80 @@ def box_speak(engine, text, voice=""): return done.stdout +BOX_OPENCODE_NODE_VERSION = "20.19.2" +_BOX_OPENCODE_CACHE = {} + +# Installed on demand, not baked into BOX_CONTAINERFILE: the box exists for +# STT/TTS today and most installs already have one running, so this only +# pays Node's weight (~800MB, dominated by opencode-ai's own node_modules, +# not the runtime itself) on the one machine, the one time, it's actually +# needed as a last-resort chat backend (see ChatClient.all_candidates()). +# The official nodejs.org tarball, not apt: Debian's nodejs/npm packages +# drag in webpack/babel/jest as transitive deps (measured: 1.49GB vs 971MB +# for the exact same opencode-ai install) and are not relevant here at all. +BOX_OPENCODE_INSTALL_SCRIPT = ( + "set -e; " + "ARCH=\"$(dpkg --print-architecture)\"; " + "case \"$ARCH\" in amd64) NODE_ARCH=x64;; arm64) NODE_ARCH=arm64;; armhf) NODE_ARCH=armv7l;; i386) NODE_ARCH=x86;; *) echo \"unsupported arch $ARCH\" >&2; exit 1;; esac; " + "apt-get update -qq && apt-get install -y -qq --no-install-recommends curl xz-utils ca-certificates; " + "curl -fsSL \"https://nodejs.org/dist/v%(ver)s/node-v%(ver)s-linux-$NODE_ARCH.tar.xz\" -o /tmp/node.tar.xz; " + "tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1; " + "rm /tmp/node.tar.xz; " + "apt-get purge -y -qq curl xz-utils; apt-get autoremove -y -qq; rm -rf /var/lib/apt/lists/*; " + "npm install -g opencode-ai --no-fund --no-audit" +) % {"ver": BOX_OPENCODE_NODE_VERSION} + + +def box_opencode_binary(engine): + """True once the opencode CLI is confirmed present inside the sandbox + box, installing Node.js + `npm install -g opencode-ai` into the box + first if it isn't there yet. Only ever called as a last resort (see + ChatClient.all_candidates()) and only against a box that is ALREADY + running -- this never creates the box itself; that stays the explicit, + approval-gated action it already is (ensure_box(), via --env sandbox or + the install tool). Cached per engine for the process lifetime so normal + turns that succeed through a real backend never pay this cost twice.""" + if engine in _BOX_OPENCODE_CACHE: + return _BOX_OPENCODE_CACHE[engine] + try: + probe = box_exec(engine, ["which", "opencode"], timeout=10) + except (OSError, subprocess.SubprocessError): + _BOX_OPENCODE_CACHE[engine] = False + return False + if probe.returncode == 0: + _BOX_OPENCODE_CACHE[engine] = True + return True + print(paint( + "opencode: no local CLI on this machine; installing it into the existing sandbox " + "box (container '%s') instead, as a last-resort backend. Nothing outside that " + "container is touched. Streaming the install below:" % BOX_NAME, Ansi.DIM + ), file=sys.stderr) + try: + # Deliberately not box_exec(): this one streams straight to the + # real stdout/stderr instead of capturing, so the person watching + # sees the actual apt/curl/npm output live, not a silent pause. + done = subprocess.run([engine, "exec", BOX_NAME, "sh", "-c", BOX_OPENCODE_INSTALL_SCRIPT], timeout=600) + except (OSError, subprocess.SubprocessError) as exc: + print(paint("opencode box install failed: %s" % short_error(exc), Ansi.DIM), file=sys.stderr) + _BOX_OPENCODE_CACHE[engine] = False + return False + ok = done.returncode == 0 + print(paint("opencode box install %s" % ("succeeded" if ok else "failed (exit %d)" % done.returncode), Ansi.DIM), file=sys.stderr) + _BOX_OPENCODE_CACHE[engine] = ok + return ok + + +def box_opencode_run(engine, model, prompt, timeout): + argv = ["opencode", "run", "--pure", "--agent", OPENCODE_AGENT, "--format", "json", "-m", "opencode/" + model] + try: + done = box_exec(engine, argv, input_data=prompt.encode("utf-8"), timeout=timeout) + except subprocess.TimeoutExpired: + raise BackendError("opencode (box) run timed out after %ds" % timeout) + except (OSError, subprocess.SubprocessError) as exc: + raise BackendError("opencode (box) run failed: %s" % short_error(exc)) + return opencode_parse_events(done.stdout.decode("utf-8", "replace"), done.returncode, done.stderr.decode("utf-8", "replace")) + + def split_models(spec): return [item.strip() for item in (spec or "").replace("\n", ",").split(",") if item.strip()] @@ -2771,20 +2854,12 @@ def opencode_parse(text): return {"content": text, "tool_calls": []} -def opencode_run(model, prompt, timeout): - binary = opencode_binary() - if not binary: - raise BackendError("opencode CLI not found (OPENCODE_BIN or PATH)") - argv = [binary, "run", "--pure", "--agent", OPENCODE_AGENT, "--format", "json", "-m", "opencode/" + model] - with tempfile.TemporaryDirectory(prefix="tai-opencode-") as folder: - try: - done = subprocess.run(argv, input=prompt, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, cwd=folder, timeout=timeout) - except subprocess.TimeoutExpired: - raise BackendError("opencode run timed out after %ds" % timeout) - except OSError as exc: - raise BackendError("opencode run failed: %s" % short_error(exc)) +def opencode_parse_events(stdout_text, returncode, stderr_text): + """Shared by opencode_run() (local CLI) and box_opencode_run() (same CLI, + executed inside the sandbox box) -- the JSON-lines event stream is + identical either way, only how the process gets launched differs.""" texts = [] - for line in done.stdout.splitlines(): + for line in stdout_text.splitlines(): try: event = json.loads(line) except ValueError: @@ -2798,10 +2873,25 @@ def opencode_run(model, prompt, timeout): if event.get("type") == "text" and part.get("text"): texts.append(part["text"]) if not texts: - raise BackendError("opencode returned no text (exit %d): %s" % (done.returncode, (done.stderr or done.stdout)[-300:])) + raise BackendError("opencode returned no text (exit %d): %s" % (returncode, (stderr_text or stdout_text)[-300:])) return "".join(texts) +def opencode_run(model, prompt, timeout): + binary = opencode_binary() + if not binary: + raise BackendError("opencode CLI not found (OPENCODE_BIN or PATH)") + argv = [binary, "run", "--pure", "--agent", OPENCODE_AGENT, "--format", "json", "-m", "opencode/" + model] + with tempfile.TemporaryDirectory(prefix="tai-opencode-") as folder: + try: + done = subprocess.run(argv, input=prompt, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, cwd=folder, timeout=timeout) + except subprocess.TimeoutExpired: + raise BackendError("opencode run timed out after %ds" % timeout) + except OSError as exc: + raise BackendError("opencode run failed: %s" % short_error(exc)) + return opencode_parse_events(done.stdout, done.returncode, done.stderr) + + def opencode_free_models(): binary = opencode_binary() if not binary: @@ -3814,13 +3904,61 @@ class ChatClient: if give_up is not None and time.time() + pause > give_up: raise BackendError("all backends failed after %d rounds (%s)" % (round_no, "; ".join(errors[-6:]))) print(paint("all %d backends failed (round %d), retrying in %ds: %s" % (len(cands), round_no, pause, (errors[-1] if errors else "")[:160]), Ansi.DIM), file=sys.stderr) + if round_no == 1 and not opencode_binary(): + engine = container_engine() + if engine and box_state(engine) == "missing": + print(paint( + "no local opencode CLI, and no sandbox box set up yet either -- that box " + "would also work as a last-resort backend (see box_opencode_candidates()). " + "Run `tai.py --env sandbox` once to build it: a small image, podman or " + "docker, and it only ever touches that one container, never the rest of " + "this machine.", Ansi.DIM + ), file=sys.stderr) time.sleep(pause) def all_candidates(self): http = [cand for cand in self.candidates() if cand["chat_url"] not in CLI_CHAT_URLS] cli = [cand for cand in self.candidates() if cand["chat_url"] in CLI_CHAT_URLS] ranked = sorted(enumerate(http), key=lambda pair: (openrouter_free_tier(pair[1]), -self.health.weight(pair[1]["provider"], pair[1]["model"]), pair[0])) - return [cand for _, cand in ranked] + cli + return [cand for _, cand in ranked] + cli + self.box_opencode_candidates() + + def box_opencode_candidates(self): + """Last-resort only -- complete() reaches this via all_candidates(), + never via ordered_candidates() (the round-0 fast path), so a normal + turn that succeeds through a real backend never touches the box at + all. If there's already a local opencode CLI, self.candidates() + covers it directly and this stays empty; it only fires when that's + missing AND the sandbox box is already running (never auto-created + here -- see ensure_box(), which stays an explicit, approval-gated + action via --env sandbox or the install tool).""" + if not getattr(self.config, "opencode_enabled", True) or opencode_binary(): + return [] + engine = container_engine() + if not engine or box_state(engine) != "running": + return [] + if not box_opencode_binary(engine): + return [] + models = self.box_opencode_models(engine) + return [ + {"provider": OPENCODE_LABEL, "chat_url": OPENCODE_CHAT_URL, "models_url": OPENCODE_MODELS_URL, + "key": "", "model": model, "profile": "", "runner": "box", "engine": engine} + for model in models + ] + + def box_opencode_models(self, engine): + cached = self.health.cached_list("opencode:box:free") + if cached is not None: + return prefer_muse(cached) + try: + done = box_exec(engine, ["opencode", "models", "opencode"], timeout=MODEL_LIST_TIMEOUT * 4) + except (OSError, subprocess.SubprocessError): + return [] + text = done.stdout.decode("utf-8", "replace") + models = [line.strip().split("/", 1)[1] for line in text.splitlines() if line.strip().startswith("opencode/")] + models = [item for item in models if is_free_model(item)] + if models: + self.health.store_list("opencode:box:free", models) + return prefer_muse(models) def call_candidate(self, cand, messages, tools, attempt, stream_sink, ephemeral): label, chat_url, key, model = cand["provider"], cand["chat_url"], cand["key"], cand["model"] @@ -3837,6 +3975,8 @@ class ChatClient: if chat_url == GEMINI_CHAT_URL: return self.gemini_turn(messages, tools, stream_sink, label, timeout=timeout) if chat_url == OPENCODE_CHAT_URL: + if cand.get("runner") == "box": + return self.box_opencode_turn(messages, tools, stream_sink, label, model, cand["engine"], timeout=timeout) return self.opencode_turn(messages, tools, stream_sink, label, model, timeout=timeout) if stream_sink is None: return self.single_shot(chat_url, key, attempt, label, extra) @@ -4020,6 +4160,12 @@ class ChatClient: stream_sink(answer["content"]) return self.muse_reply(answer, label) + def box_opencode_turn(self, messages, tools, stream_sink, label, model, engine, timeout=HTTP_TIMEOUT): + answer = opencode_parse(box_opencode_run(engine, model, opencode_prompt(messages, tools), timeout)) + if stream_sink is not None and answer.get("content"): + stream_sink(answer["content"]) + return self.muse_reply(answer, label + " (box)") + def grok_turn(self, messages, tools, stream_sink, label, timeout=HTTP_TIMEOUT): answer = grok_parse_output(grok_run(opencode_prompt(messages, tools), timeout)) if stream_sink is not None and answer.get("content"): @@ -4426,6 +4572,10 @@ TOOL_SCHEMAS = [ tool_schema("load_skill", "Load a skill by name. Returns full instructions plus bundled file paths. Missing skills with a blueprint are researched and built on demand.", {"name": {"type": "string"}}, ["name"]), tool_schema("get_current_terminal_content", "Capture visible text of the current tmux pane including scrollback. Works inside tmux or against a running tmux server.", {"lines": {"type": "integer"}}, []), tool_schema("export_terminal_log", "Only available inside a live tmux session. Exports the full pane scrollback (tmux capture-pane -pS -, the entire history, not just what's visible) to a log file under tai's home dir, then chunks it and indexes the chunks for retrieval with SQLite FTS5 + bm25 (lexical only, no embeddings). From then on, use the search tool (kind chunk) to answer questions about anything that happened in this terminal; it returns the actual chunk text, not just a snippet.", {"tags": {"type": "array"}}, []), + tool_schema("plexer_send", "Only available when a tai-managed terminal session is active (either tai's own native wrapper, or an active tmux pane). Sends literal keystrokes into that session's shell, as if typed. Set enter to also press Enter and actually run it. No output is returned by this call; follow up with plexer_read.", {"text": {"type": "string"}, "enter": {"type": "boolean"}}, ["text"]), + tool_schema("plexer_read", "Only available when a tai-managed terminal session is active. Reads back the last N lines the session's shell has produced (its actual rendered output, not a tool-call summary).", {"lines": {"type": "integer"}}, []), + tool_schema("plexer_type", "Only available when a tai-managed terminal session is active. Types text into the session one character at a time with a delay between keystrokes, for a demo/animated-typing effect, then presses Enter unless enter is set to false.", {"text": {"type": "string"}, "delay": {"type": "number"}, "enter": {"type": "boolean"}}, ["text"]), + tool_schema("plexer_status", "Only available when a tai-managed terminal session is active. Reports which backend is managing the current session (tai's own native pty wrapper, or tmux) and backend-specific details (pid/socket/log, or pane id).", {}, []), tool_schema("fork", "Spawn a background subagent with its own context that works while you continue. Returns an agent id immediately. Collect its summarized result with poll. Subagents get a smaller step budget and a time limit.", {"task": {"type": "string"}, "timeout": {"type": "integer"}, "profile": {"type": "string"}}, ["task"]), tool_schema("poll", "Collect a background subagent result by id. Waits up to wait seconds, then reports running or the result.", {"id": {"type": "integer"}, "wait": {"type": "integer"}}, ["id"]), tool_schema("sysinfo", "Inspect the host machine where tai runs. Runs os, python, venv, root, container, binaries, cpu, and disk checks in parallel and reports each with timing. Pass checks to run a named subset.", {"checks": {"type": "array"}}, []), @@ -4462,6 +4612,10 @@ LAZY_NAME_STOP = ("get", "set", "list", "add") # specific environment (e.g. a tmux pane to capture). TOOL_ENV_GATES = { "export_terminal_log": lambda: bool(os.environ.get("TMUX")), + "plexer_send": lambda: PlexerControllerFactory.detect() is not None, + "plexer_read": lambda: PlexerControllerFactory.detect() is not None, + "plexer_type": lambda: PlexerControllerFactory.detect() is not None, + "plexer_status": lambda: PlexerControllerFactory.detect() is not None, } @@ -4483,6 +4637,10 @@ TOOL_TAGS = { "load_skill": ("skill", "capability"), "get_current_terminal_content": ("terminal", "tmux", "pane", "scrollback", "screen"), "export_terminal_log": ("terminal", "tmux", "export", "log", "capture", "history", "rag", "index"), + "plexer_send": ("plexer", "terminal", "tmux", "keys", "send-keys", "type", "keystroke"), + "plexer_read": ("plexer", "terminal", "tmux", "pane", "read", "capture"), + "plexer_type": ("plexer", "terminal", "tmux", "type", "animate", "keystroke", "demo"), + "plexer_status": ("plexer", "terminal", "tmux", "status", "session"), "fork": ("fork", "subagent", "background", "parallel", "delegate", "spawn"), "poll": ("poll", "collect"), "sysinfo": ("sysinfo", "system", "host", "machine", "hardware", "specs", "installed", "python", "container"), @@ -4632,6 +4790,575 @@ def create_skill_prompt(name, brief, skill_dir): ) +PLEXER_DIR_NAME = "plexer" +PLEXER_PREFIX_KEY = b"\x01" # Ctrl+A, deliberately distinct from tmux's default Ctrl+B +PLEXER_SOCKET_ENV = "TAI_PLEXER_SOCKET" +PLEXER_ACTIVE_ENV = "TAI_PLEXER_ACTIVE" +PLEXER_RECV_MAX = 1 << 20 + + +def plexer_dir(home): + path = os.path.join(home, PLEXER_DIR_NAME) + os.makedirs(path, exist_ok=True) + try: + os.chmod(path, 0o700) + except OSError: + pass + return path + + +class PlexerError(Exception): + """Raised by a PlexerController when the backend can't be reached or refuses a command.""" + + +# --------------------------------------------------------------------------- # +# Controller side: what Tools.run_plexer_* actually calls. One small +# interface, two backends, chosen by which environment variable is set — +# TAI_PLEXER_SOCKET (our own NativePlexerBackend already wrapping this shell) +# or TMUX_PANE (tmux is already the active manager of this shell). Neither +# set means no plexer session is active and the plexer_* tools stay hidden +# (see TOOL_ENV_GATES below) rather than erroring. +# --------------------------------------------------------------------------- # + +class PlexerController: + kind = "abstract" + + def send_keys(self, text, enter=False): + raise NotImplementedError + + def read_pane(self, lines=200): + raise NotImplementedError + + def type_text(self, text, delay=0.03, enter=True): + for ch in text: + self.send_keys(ch) + time.sleep(delay) + if enter: + self.send_keys("", enter=True) + + def status(self): + raise NotImplementedError + + +class NativePlexerController(PlexerController): + kind = "native" + + def __init__(self, socket_path): + self.socket_path = socket_path + + def _call(self, payload): + try: + conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + conn.settimeout(10) + conn.connect(self.socket_path) + except OSError as exc: + raise PlexerError("cannot reach native plexer backend: " + short_error(exc)) + try: + conn.sendall((json.dumps(payload) + "\n").encode("utf-8")) + chunks = [] + while True: + chunk = conn.recv(65536) + if not chunk: + break + chunks.append(chunk) + if chunk.endswith(b"\n"): + break + except OSError as exc: + raise PlexerError("native plexer backend read/write failed: " + short_error(exc)) + finally: + conn.close() + raw = b"".join(chunks).decode("utf-8", "replace").strip() + try: + reply = json.loads(raw) if raw else {} + except ValueError: + raise PlexerError("native plexer backend sent an unparseable reply") + if not reply.get("ok", False): + raise PlexerError(reply.get("error") or "native plexer backend refused the command") + return reply.get("data") + + def send_keys(self, text, enter=False): + return self._call({"cmd": "send", "text": text, "enter": bool(enter)}) + + def read_pane(self, lines=200): + return self._call({"cmd": "read", "lines": int(lines)}) + + def status(self): + return self._call({"cmd": "status"}) + + +class TmuxPlexerController(PlexerController): + kind = "tmux" + + def __init__(self, pane): + self.pane = pane + + def _tmux(self, *args): + try: + done = subprocess.run(["tmux"] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=15) + except (OSError, subprocess.SubprocessError) as exc: + raise PlexerError("tmux call failed: " + short_error(exc)) + if done.returncode != 0: + raise PlexerError("tmux error: " + (done.stderr or "").strip()[:300]) + return done.stdout + + def send_keys(self, text, enter=False): + if text: + self._tmux("send-keys", "-t", self.pane, "-l", text) + if enter: + self._tmux("send-keys", "-t", self.pane, "Enter") + return True + + def read_pane(self, lines=200): + return self._tmux("capture-pane", "-p", "-t", self.pane, "-S", "-%d" % max(1, int(lines))) + + def status(self): + # list-panes, not display-message -p: display-message -p silently + # falls back to tmux's default/current pane when -t names a target + # that doesn't exist (confirmed empirically -- it still exits 0 with + # blank format substitutions), while list-panes -t actually validates + # the target and errors like send-keys/capture-pane already do. + info = self._tmux("list-panes", "-t", self.pane, "-F", "#{session_name}:#{window_index}.#{pane_index} #{pane_current_command}") + return {"backend": "tmux", "pane": self.pane, "info": info.strip()} + + +class PlexerControllerFactory: + """The whole detection flow lives in one place: which env var is set + decides which backend is already managing this shell. Nothing here ever + spawns a backend — spawning only happens via `tai.py --plexer-wrap`, + invoked by the bashrc hook, and only when neither var is already set.""" + + @staticmethod + def detect(): + socket_path = os.environ.get(PLEXER_SOCKET_ENV) + if socket_path: + return NativePlexerController(socket_path) + pane = os.environ.get("TMUX_PANE") + if pane: + return TmuxPlexerController(pane) + return None + + +# --------------------------------------------------------------------------- # +# Backend side: only NativePlexerBackend actually exists, because tmux +# already IS a backend when it's active — there is nothing for tai to wrap +# in that case (see PlexerControllerFactory). PlexerBackend is kept as the +# extension point for a future alternative (e.g. a screen-based backend) +# without touching the controller side or the detection flow above. +# --------------------------------------------------------------------------- # + +class PlexerBackend: + def run(self): + raise NotImplementedError + + +class NativePlexerBackend(PlexerBackend): + """Owns the pty directly: forks the user's real shell onto one, relays + bytes between the real terminal and that pty in both directions, forwards + SIGWINCH as a resize, tees everything to a log file, and serves control + commands (send/read/status) over a Unix socket so a separate tai process + (the agent the user is actually talking to) can drive this session. + + One prefix key (Ctrl+A, PLEXER_PREFIX_KEY) is reserved for tai's own + shortcuts in the raw keystroke stream. There is deliberately no + tmux-compatible keymap here: when tmux is the active backend, tai never + owns the raw keystroke stream at all (see TmuxPlexerController), so there + is nothing to avoid colliding with. + + A characteristic worth knowing, not a bug: this relay loop is single- + threaded and synchronous, so its own os.write(stdout_fd, ...) applies + normal pty backpressure -- if whatever is reading the OTHER end of the + outer pty (normally a real terminal emulator, which always reads + continuously) ever stops draining for a stretch, a large enough burst of + child output can block that write and stall the whole loop until the + reader catches up. This is standard for any synchronous pty relay + (tmux, ssh, `script` all have the same property) and is why writes + headed INTO the child are queued and drained cooperatively rather than + written directly (see _pending_to_child) -- that fix was for a + self-inflicted deadlock on the inner side; this is normal, external + backpressure on the outer side, not something to engineer around. + """ + + def __init__(self, home, profile, shell_cmd=None): + self.home = home + self.profile = profile + self.shell_cmd = shell_cmd or [os.environ.get("SHELL") or "/bin/bash"] + self.session_id = "%s-%d" % (datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S"), os.getpid()) + self.socket_path = os.path.join(plexer_dir(home), self.session_id + ".sock") + self.log_path = os.path.join(plexer_dir(home), self.session_id + ".log") + self.pid = None + self.master_fd = None + self._log_fh = None + self._server = None + self._saved_termios = None + self._resize_pending = False + self._running = False + self._pending_to_child = bytearray() + + def _set_raw(self): + if termios is None: + return + fd = sys.stdin.fileno() + try: + self._saved_termios = termios.tcgetattr(fd) + tty.setraw(fd) + except (termios.error, OSError, ValueError): + self._saved_termios = None + + def _restore_tty(self): + if termios is None or self._saved_termios is None: + return + try: + termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._saved_termios) + except (termios.error, OSError): + pass + + def _get_winsize(self, fd): + try: + packed = fcntl.ioctl(fd, termios.TIOCGWINSZ, b"\0" * 8) + except OSError: + return None + rows, cols = struct.unpack("HH", packed[:4]) + return rows, cols + + def _apply_winsize(self): + size = self._get_winsize(sys.stdin.fileno()) + if size is None or self.master_fd is None: + return + rows, cols = size + try: + fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0)) + except OSError: + pass + + def _on_sigwinch(self, _signum, _frame): + self._resize_pending = True + + def _spawn_child(self): + pid, master_fd = pty.fork() + if pid == 0: + env = dict(os.environ) + env[PLEXER_ACTIVE_ENV] = "1" + env[PLEXER_SOCKET_ENV] = self.socket_path + try: + os.execvpe(self.shell_cmd[0], self.shell_cmd, env) + except OSError as exc: + sys.stderr.write("tai plexer: failed to exec %s: %s\n" % (self.shell_cmd[0], exc)) + os._exit(127) + return pid, master_fd + + def _open_server(self): + try: + os.unlink(self.socket_path) + except OSError: + pass + server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + server.bind(self.socket_path) + # Same reasoning as the log file: a plain AF_UNIX bind() creates the + # socket file with umask-default permissions, often world-connectable + # on a multi-user machine -- which would let another local user send + # keystrokes into this terminal session. Owner-only, no exceptions. + try: + os.chmod(self.socket_path, 0o600) + except OSError: + pass + server.listen(8) + server.setblocking(False) + return server + + def _recv_request(self, conn): + """Mirrors NativePlexerController._call()'s own framing exactly: loop + until a newline-terminated chunk arrives, not a single recv() call. + json.dumps always escapes a real newline inside a string as the two + characters '\\n', so the one literal 0x0A byte in the whole message + is always the trailing terminator added below -- a single recv() would + silently truncate any request too large for one read (e.g. a long + plexer_type payload), which is exactly the kind of bug that only shows + up under real load, not in a quick manual check.""" + chunks = [] + total = 0 + while True: + chunk = conn.recv(65536) + if not chunk: + break + chunks.append(chunk) + total += len(chunk) + if chunk.endswith(b"\n") or total >= PLEXER_RECV_MAX: + break + return b"".join(chunks) + + def _handle_control(self, conn): + try: + conn.settimeout(2) + raw = self._recv_request(conn) + request = json.loads(raw.decode("utf-8", "replace")) if raw.strip() else {} + if not isinstance(request, dict): + raise ValueError("request must be a JSON object") + reply = self._dispatch(request) + except Exception as exc: + # Deliberately broad: a malformed or unexpected request must never + # be able to crash this relay loop and take the user's whole live + # terminal session down with it. This is the one place in the + # whole backend where "catch everything" is the correct choice, + # not laziness -- everything else in this class keeps narrow, + # specific except clauses on purpose. + reply = {"ok": False, "error": short_error(exc)} + try: + conn.sendall((json.dumps(reply) + "\n").encode("utf-8")) + except OSError: + pass + finally: + conn.close() + + def _dispatch(self, request): + cmd = request.get("cmd") + if cmd == "send": + text = str(request.get("text") or "") + if text: + self._pending_to_child += text.encode("utf-8", "replace") + if request.get("enter"): + self._pending_to_child += b"\r" + return {"ok": True, "data": True} + if cmd == "read": + limit = max(1, min(int(request.get("lines") or 200), 5000)) + return {"ok": True, "data": self._tail_log(limit)} + if cmd == "status": + return {"ok": True, "data": {"backend": "native", "pid": self.pid, "log": self.log_path, "socket": self.socket_path}} + return {"ok": False, "error": "unknown command %r" % cmd} + + def _tail_log(self, lines, chunk_size=65536, max_bytes=1 << 24): + """Reads only the tail, in growing byte chunks from the end, instead + of loading the whole file -- a session log is exactly the kind of + file this feature encourages to grow large over a long-running + terminal session, and read_pane()/plexer_read get called repeatedly, + so re-reading the entire file on every call would make the backend + slower the longer the very session it's meant to support stays open.""" + try: + size = os.path.getsize(self.log_path) + except OSError: + return "" + with open(self.log_path, "rb") as handle: + read_from = size + data = b"" + while True: + read_from = max(0, read_from - chunk_size) + handle.seek(read_from) + data = handle.read(size - read_from) + if data.count(b"\n") >= lines or read_from == 0 or len(data) >= max_bytes: + break + text = data.decode("utf-8", "replace") + return "\n".join(text.splitlines()[-lines:]) + + def run(self): + if pty is None or termios is None or select is None: + return "error: pty/termios/select not available on this platform" + self.pid, self.master_fd = self._spawn_child() + # The log captures the entire terminal session verbatim -- anything typed, + # including passwords and tokens -- so it must never be created world- or + # group-readable. os.open's mode is applied atomically at creation (no + # window where a looser default permission briefly exists), unlike a + # plain open() + a later os.chmod. + log_fd = os.open(self.log_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) + self._log_fh = os.fdopen(log_fd, "ab", buffering=0) + self._server = self._open_server() + self._set_raw() + self._apply_winsize() + try: + old_handler = signal.signal(signal.SIGWINCH, self._on_sigwinch) if hasattr(signal, "SIGWINCH") else None + except (ValueError, OSError): + old_handler = None + self._running = True + crash = None + try: + self._relay_loop() + except Exception as exc: # noqa: BLE001 -- see comment below + # Deliberately broad, and deliberately not re-raised bare: letting + # an unexpected exception propagate straight out of run() would + # skip the waitpid() below entirely (a bare `finally` only runs + # cleanup, it does not stop the exception from continuing past + # it), orphaning the wrapped shell with no supervisor left to + # reap it. The user's actual terminal child process must not be + # allowed to leak just because something in the relay loop broke. + crash = exc + finally: + self._running = False + if old_handler is not None: + try: + signal.signal(signal.SIGWINCH, old_handler) + except (ValueError, OSError): + pass + self._restore_tty() + try: + self._log_fh.close() + except OSError: + pass + try: + self._server.close() + except OSError: + pass + try: + os.unlink(self.socket_path) + except OSError: + pass + if crash is not None: + try: + os.killpg(self.pid, signal.SIGKILL) + except OSError: + try: + os.kill(self.pid, signal.SIGKILL) + except OSError: + pass + sys.stderr.write("tai plexer: wrapper crashed (%s), terminating the session\n" % short_error(crash)) + try: + _, status = os.waitpid(self.pid, 0) + return os.waitstatus_to_exitcode(status) if hasattr(os, "waitstatus_to_exitcode") else (1 if crash else 0) + except OSError: + return 1 if crash else 0 + + def _relay_loop(self): + """Every byte headed INTO the child goes through self._pending_to_child, + drained here in bounded chunks only when select() says master_fd is + writable -- never a direct, unbounded os.write(self.master_fd, ...) + anywhere, including from the keystroke path below. A real deadlock + was found and fixed here: a large plexer_send payload written + directly and synchronously into the pty (echo-enabled, so the write + itself generates output) filled the pty's buffer while the only + thing that could ever drain that output -- this very loop -- was + itself blocked inside that same write call. Queuing and draining + cooperatively, alongside reading master_fd's output on the same + select() pass, is what actually avoids that, not a bigger buffer.""" + stdin_fd = sys.stdin.fileno() + stdout_fd = sys.stdout.fileno() + pending_prefix = False + while True: + if self._resize_pending: + self._resize_pending = False + self._apply_winsize() + watch_read = [stdin_fd, self.master_fd, self._server.fileno()] + watch_write = [self.master_fd] if self._pending_to_child else [] + try: + readable, writable, _ = select.select(watch_read, watch_write, [], 1.0) + except InterruptedError: + continue + if not readable and not writable and not self._process_alive(): + break + if self._server.fileno() in readable: + try: + conn, _addr = self._server.accept() + self._handle_control(conn) + except OSError: + pass + if self.master_fd in writable and self._pending_to_child: + try: + written = os.write(self.master_fd, bytes(self._pending_to_child[:4096])) + del self._pending_to_child[:written] + except OSError: + self._pending_to_child = bytearray() + if self.master_fd in readable: + try: + data = os.read(self.master_fd, 65536) + except OSError: + data = b"" + if not data: + break + self._log_fh.write(data) + os.write(stdout_fd, data) + if stdin_fd in readable: + try: + data = os.read(stdin_fd, 4096) + except OSError: + data = b"" + if not data: + break + data, pending_prefix = self._strip_prefix_commands(data, pending_prefix) + if data: + self._pending_to_child += data + if not self._process_alive(): + break + + def _strip_prefix_commands(self, data, pending_prefix): + """Intercepts tai's own Ctrl+A shortcuts; everything else passes through untouched. + + 'q' quits the wrapped session. Verified empirically (see + tests/plexer.py) that in this environment an interactive bash running + as a pty.fork() session leader does not die from an external SIGHUP + OR SIGTERM -- confirmed independent of this class with a bare + pty.fork()+bash+os.kill script, so this is not something tai is + doing wrong, it is this environment's actual bash behavior. SIGKILL + is the one signal no process can ignore, so that's what's used here. + This is a clean kill, not a true tmux-style detach-and-reattach -- + there is no session persistence to reattach to yet, so it is named + and documented as "quit," not "detach", to not promise what isn't built.""" + out = bytearray() + i = 0 + while i < len(data): + byte = data[i:i + 1] + if pending_prefix: + pending_prefix = False + if byte == b"q": + # killpg, not kill: pty.fork()'s child is its own session + # and process-group leader (pid == pgid), so this reaches + # a plain foreground child the bare shell pid alone would + # miss. It does NOT reach every descendant, and verified + # empirically rather than assumed: an interactive bash's + # own job control puts each backgrounded job (`sleep &`) + # in a NEW process group of its own, specifically so + # signals to the shell's group -- Ctrl+C, this killpg, + # closing a real terminal -- don't also take down its + # background jobs. A backgrounded job surviving `quit` is + # therefore expected, standard shell behavior, identical + # to what closing a real terminal does, not a gap to + # close here. + try: + os.killpg(self.pid, signal.SIGKILL) + except OSError: + os.kill(self.pid, signal.SIGKILL) + elif byte == PLEXER_PREFIX_KEY: + out += PLEXER_PREFIX_KEY + i += 1 + continue + if byte == PLEXER_PREFIX_KEY: + pending_prefix = True + i += 1 + continue + out += byte + i += 1 + return bytes(out), pending_prefix + + def _process_alive(self): + try: + pid, _status = os.waitpid(self.pid, os.WNOHANG) + except OSError: + return False + return pid == 0 + + +def plexer_should_wrap(env): + """The Python-side half of the detection flow (see research/shell-multiplexer-auto-attach/). + The cheap, fast checks ($- interactive flag, isatty) already happened in the bashrc + block itself before tai.py was even invoked -- this only covers what needs Python: + never wrap if a plexer is already active, and never wrap if tmux already is.""" + if env.get(PLEXER_ACTIVE_ENV): + return False + if env.get("TMUX"): + return False + if os.path.exists(os.path.join(os.path.expanduser("~"), ".tai-no-plexer")): + return False + return True + + +PLEXER_BASHRC_MARK_BEGIN = "# tai plexer hook - start" +PLEXER_BASHRC_MARK_END = "# tai plexer hook - end" +PLEXER_BASHRC_BLOCK = ( + PLEXER_BASHRC_MARK_BEGIN + "\n" + "if [ -z \"$TAI_PLEXER_ACTIVE\" ] && [ -z \"$TMUX\" ] && [ ! -e \"$HOME/.tai-no-plexer\" ] && case \"$-\" in *i*) true;; *) false;; esac; then\n" + " if [ -t 0 ] && [ -t 1 ]; then\n" + " exec \"$HOME/.local/bin/tai.py\" --plexer-wrap\n" + " fi\n" + "fi\n" + + PLEXER_BASHRC_MARK_END + "\n" +) + + class Tools: def __init__(self, app): self.app = app @@ -4649,6 +5376,10 @@ class Tools: "load_skill": self.run_load_skill, "get_current_terminal_content": self.run_terminal_content, "export_terminal_log": self.run_export_terminal_log, + "plexer_send": self.run_plexer_send, + "plexer_read": self.run_plexer_read, + "plexer_type": self.run_plexer_type, + "plexer_status": self.run_plexer_status, "fork": self.run_fork, "poll": self.run_poll, "sysinfo": self.run_sysinfo, @@ -5219,6 +5950,58 @@ class Tools: self.app.store.add_chunks("record", record_id, chunks, profile) return "captured %s to %s (%d bytes), indexed as %s in %d chunks -- use search (kind chunk) to answer questions about this terminal session" % (pane_header, path, len(raw.encode("utf-8")), record_id, len(chunks)) + def run_plexer_send(self, args): + controller = PlexerControllerFactory.detect() + if controller is None: + return "error: no plexer session active (neither tai's native wrapper nor tmux)" + text = str(args.get("text") or "") + try: + controller.send_keys(text, enter=bool(args.get("enter"))) + except PlexerError as exc: + return "error: " + str(exc) + return "sent %d chars via %s backend%s" % (len(text), controller.kind, " + enter" if args.get("enter") else "") + + def run_plexer_read(self, args): + controller = PlexerControllerFactory.detect() + if controller is None: + return "error: no plexer session active (neither tai's native wrapper nor tmux)" + try: + lines = max(1, min(int(args.get("lines") or 200), 5000)) + except (TypeError, ValueError): + lines = 200 + try: + content = controller.read_pane(lines) + except PlexerError as exc: + return "error: " + str(exc) + return truncate(content or "(empty)", 6000, 2000) + + def run_plexer_type(self, args): + controller = PlexerControllerFactory.detect() + if controller is None: + return "error: no plexer session active (neither tai's native wrapper nor tmux)" + text = str(args.get("text") or "") + if not text: + return "error: empty text" + try: + delay = max(0.0, min(float(args.get("delay") or 0.03), 2.0)) + except (TypeError, ValueError): + delay = 0.03 + try: + controller.type_text(text, delay=delay, enter=args.get("enter") is not False) + except PlexerError as exc: + return "error: " + str(exc) + return "typed %d chars via %s backend at %.2fs/char" % (len(text), controller.kind, delay) + + def run_plexer_status(self, args): + controller = PlexerControllerFactory.detect() + if controller is None: + return "no plexer session active (neither tai's native wrapper nor tmux)" + try: + data = controller.status() + except PlexerError as exc: + return "error: " + str(exc) + return json.dumps(data) + def run_fork(self, args): task = str(args.get("task") or "").strip() if not task: @@ -7073,6 +7856,52 @@ def remove_bashrc_block(path): return True +def plexer_hook_present(path): + try: + with open(path, "r", encoding="utf-8") as handle: + return PLEXER_BASHRC_BLOCK in handle.read() + except OSError: + return False + + +def remove_plexer_bashrc_block(path): + try: + with open(path, "r", encoding="utf-8") as handle: + raw = handle.read() + except OSError: + return False + stripped = re.sub(re.escape(PLEXER_BASHRC_MARK_BEGIN) + r".*?" + re.escape(PLEXER_BASHRC_MARK_END) + r"\n?", "", raw, flags=re.DOTALL) + if stripped == raw: + return False + backup = path + ".bak-tai" + if raw and not os.path.exists(backup): + with open(backup, "w", encoding="utf-8") as handle: + handle.write(raw) + with open(path, "w", encoding="utf-8") as handle: + handle.write(stripped) + return True + + +def upsert_plexer_bashrc_block(path): + try: + with open(path, "r", encoding="utf-8") as handle: + raw = handle.read() + except OSError: + raw = "" + if PLEXER_BASHRC_BLOCK in raw: + return False + backup = path + ".bak-tai" + if raw and not os.path.exists(backup): + with open(backup, "w", encoding="utf-8") as handle: + handle.write(raw) + stripped = re.sub(re.escape(PLEXER_BASHRC_MARK_BEGIN) + r".*?" + re.escape(PLEXER_BASHRC_MARK_END) + r"\n?", "", raw, flags=re.DOTALL) + if stripped and not stripped.endswith("\n"): + stripped += "\n" + with open(path, "w", encoding="utf-8") as handle: + handle.write(stripped + PLEXER_BASHRC_BLOCK) + return True + + def box_venv_state(engine): try: probe = box_exec(engine, ["test", "-x", BOX_PYTHON], timeout=30) @@ -7089,6 +7918,7 @@ def status_lines(home, store=None): else: lines.append("binary: not installed") lines.append("bash-hook: %s in ~/.bashrc" % ("installed" if hook_present(os.path.join(os.path.expanduser("~"), ".bashrc")) else "not installed")) + lines.append("plexer-hook: %s in ~/.bashrc" % ("installed" if plexer_hook_present(os.path.join(os.path.expanduser("~"), ".bashrc")) else "not installed")) python = venv_python(home) if python: lines.append("venv: ready %s" % python) @@ -7174,6 +8004,21 @@ def op_hook(mode, home): return "bash-hook: already present" +def op_plexer_hook(mode, home): + path = os.path.join(os.path.expanduser("~"), ".bashrc") + if mode == "uninstall": + return "plexer-hook: %s" % ("removed" if remove_plexer_bashrc_block(path) else "not present") + if mode == "reinstall": + remove_plexer_bashrc_block(path) + try: + changed = upsert_plexer_bashrc_block(path) + except OSError as exc: + return "plexer-hook: write failed: %s" % short_error(exc) + if changed: + return "plexer-hook: installed, run: source ~/.bashrc (new interactive shells auto-wrap; existing ones are unaffected until restarted)" + return "plexer-hook: already present" + + def op_venv(mode, home): if mode == "uninstall": folder = venv_dir(home) @@ -7286,6 +8131,8 @@ def install_report(action, targets, home, store=None): lines.append(op_binary(action, home)) elif target == "bash-hook": lines.append(op_hook(action, home)) + elif target == "plexer-hook": + lines.append(op_plexer_hook(action, home)) elif target == "venv": lines.append(op_venv(action, home)) elif target == "scheduler-service": @@ -7442,6 +8289,7 @@ def build_parser(): parser.add_argument("--install", action="store_true") parser.add_argument("--install-telegram", action="store_true") parser.add_argument("--uninstall-telegram", action="store_true") + parser.add_argument("--plexer-wrap", action="store_true", help="internal: invoked by the bashrc plexer hook, not meant to be run by hand") parser.add_argument("prompt", nargs="*") return parser @@ -7529,6 +8377,11 @@ def main(argv=None): if args.version: print("tai %s" % VERSION) return 0 + if args.plexer_wrap: + home = os.environ.get("TAI_HOME") or os.path.join(os.path.expanduser("~"), ".tai") + profile = args.profile or "default" + backend = NativePlexerBackend(home, profile) + return backend.run() if args.profile is None: args.profile = "telegram" if args.telegram else "default" if not PROFILE_RE.match(args.profile): diff --git a/tai_test.py b/tai_test.py new file mode 100644 index 0000000..2d33932 --- /dev/null +++ b/tai_test.py @@ -0,0 +1,527 @@ +#!/usr/bin/env python3 +"""tai_test.py — the integration-test framework for driving tai.py as a real subprocess. + +This is a library, not a test itself (it has no "test_" prefix on purpose — nothing +here is collected by unittest/pytest). Integration tests that use it live under +tests/ and import it; see tests/README.md for the conventions and tests/rag.py for +a worked example. + +Design, and why, in one paragraph per decision (full research notes are archived +under research/ — see research/README.md for how this project keeps that archive): + +- Two transport modes, one interface. "pipe" mode uses asyncio's native + create_subprocess_exec with PIPE streams — the simplest, most deterministic + choice, and what one-shot `tai.py <prompt>` invocations should use, since they + just run once and exit. "pty" mode opens a real pseudo-terminal (the stdlib + `pty` module) and gives the child its slave end as stdin/stdout/stderr, because + tai.py's own code branches on `sys.stdin.isatty()` (approval prompts, readline) + — a plain pipe makes it behave as if piped, not as if a human were typing. Both + modes end up exposing the same asyncio.StreamReader-based interface (the pty + master fd is wrapped into one via loop.connect_read_pipe), so callers never + need to know which transport a given test is using. +- start_new_session=True, always, plus a kill-the-whole-group escalation + (SIGTERM, wait, then SIGKILL, then always proc.wait() to reap). A plain + proc.terminate() only signals the direct child; if tai.py ever forks a + subagent or a shell command, a timeout needs to take the whole tree down, not + just the leader, and failing to reap after killing leaves a zombie. +- Non-blocking everything, no polling. The child's output is consumed through + asyncio.StreamReader (native backpressure, no busy loop). A human's live + stdin, when a test opts into --interactive, is bridged into the loop with + loop.add_reader + raw os.read on the fd (never the buffered sys.stdin, which + blocks) rather than a background thread — matching how the child's own output + is handled, and confirmed against multiple independent sources as the correct + pattern. Full sourcing for every design decision in this module — pty vs. + pipes, the connect_read_pipe trick, the killpg escalation, the TestCase + shape itself — is archived at research/async-pty-process-testing/ per this + project's standing research rule (see AGENTS.md). +- A known, accepted limitation: the pty mode does not explicitly call + ioctl(TIOCSCTTY), so the child's session may not become the pty's controlling + terminal in the job-control sense. isatty() on the fd is unaffected by this + (it's a property of the fd, not of session leadership) so approval-prompt and + readline behavior work correctly; what would NOT work is SIGWINCH-based resize + forwarding or Ctrl-C-as-SIGINT generated by the tty line discipline. Tests + that only script line-based input/output (the normal case) are unaffected. +""" +import argparse +import asyncio +import dataclasses +import datetime +import os +import pty +import re +import signal +import sys +import time +import traceback +from pathlib import Path + +DEFAULT_TAI_PATH = Path(__file__).resolve().parent / "tai.py" +ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") +REPL_PROMPT_RE = re.compile(r"tai\[[^\]]*\]\xe2\x80\xba|tai\[[^\]]*\]›") + + +def strip_ansi(text): + return ANSI_RE.sub("", text) + + +# --------------------------------------------------------------------------- # +# Exceptions. Every failure a test can hit is one of these, and every one of +# them carries the transcript collected so far, so a failing test is debuggable +# from its log file alone, without reproducing it. +# --------------------------------------------------------------------------- # + +class TaiTestError(Exception): + def __init__(self, message, transcript=None): + super().__init__(message) + self.transcript = list(transcript or []) + + +class TestTimeout(TaiTestError): + """Raised when a step, or the test as a whole, exceeds its timeout.""" + + +class TestCrashed(TaiTestError): + """Raised when the child process exited (or died to a signal) unexpectedly.""" + + def __init__(self, message, returncode, transcript=None): + super().__init__(message, transcript) + self.returncode = returncode + + +class TestAssertionFailed(TaiTestError): + """Raised by a test's verify() when the captured behavior doesn't match expectations.""" + + +# --------------------------------------------------------------------------- # +# Transcript + result data types. +# --------------------------------------------------------------------------- # + +@dataclasses.dataclass +class OutputLine: + stream: str # "stdout" | "stdin" (stderr is merged into stdout in pipe mode; pty merges all three) + text: str + ts: float + + +@dataclasses.dataclass +class TestResult: + name: str + passed: bool + duration: float + error: str = None + transcript: list = dataclasses.field(default_factory=list) + log_path: str = None + + +# --------------------------------------------------------------------------- # +# TeeLogger: every byte a test sees also goes to screen and to a log file, +# synchronously (plain stdlib file I/O, flushed per write) — no extra +# dependency, and fast enough that it never becomes the bottleneck next to a +# network-bound LLM call or a human typing. +# --------------------------------------------------------------------------- # + +class TeeLogger: + def __init__(self, log_path, echo=True): + self.log_path = Path(log_path) + self.log_path.parent.mkdir(parents=True, exist_ok=True) + self._fh = open(self.log_path, "a", encoding="utf-8") + self.echo = echo + + def write(self, text): + self._fh.write(text) + self._fh.flush() + if self.echo: + sys.stdout.write(text) + sys.stdout.flush() + + def close(self): + try: + self._fh.close() + except OSError: + pass + + +# --------------------------------------------------------------------------- # +# StdinBridge: bridges the REAL terminal's stdin into an asyncio.Queue of +# complete lines, without blocking the event loop and without a thread. +# No-ops safely (returns False from start()) if real stdin isn't a tty, e.g. +# under CI — a test must not hang waiting for input nobody can type. +# --------------------------------------------------------------------------- # + +class StdinBridge: + def __init__(self): + self.queue = asyncio.Queue() + self._fd = None + self._buf = b"" + self._active = False + self._loop = None + + def start(self): + if not sys.stdin.isatty(): + return False + self._loop = asyncio.get_running_loop() + self._fd = sys.stdin.fileno() + self._loop.add_reader(self._fd, self._on_readable) + self._active = True + return True + + def _on_readable(self): + try: + chunk = os.read(self._fd, 4096) + except OSError: + chunk = b"" + if not chunk: + self.stop() + self.queue.put_nowait(None) + return + self._buf += chunk + while b"\n" in self._buf: + line, self._buf = self._buf.split(b"\n", 1) + self.queue.put_nowait(line.decode(errors="replace")) + + def stop(self): + if self._active: + try: + self._loop.remove_reader(self._fd) + except (ValueError, OSError): + pass + self._active = False + + +# --------------------------------------------------------------------------- # +# ManagedProcess: spawns and safely controls one subprocess, in "pipe" or +# "pty" mode, with a single interface either way. +# --------------------------------------------------------------------------- # + +class ManagedProcess: + def __init__(self, command, mode="pipe", cwd=None, env=None, on_line=None): + self.command = list(command) + self.mode = mode + self.cwd = cwd + self.env = env + self._on_line = on_line + self.proc = None + self._master_fd = None + self._master_file = None + self._reader = None + self._lines = [] + + async def start(self): + loop = asyncio.get_running_loop() + if self.mode == "pty": + master_fd, slave_fd = pty.openpty() + self.proc = await asyncio.create_subprocess_exec( + *self.command, + stdin=slave_fd, stdout=slave_fd, stderr=slave_fd, + cwd=self.cwd, env=self.env, + start_new_session=True, + ) + os.close(slave_fd) + self._master_fd = master_fd + self._master_file = os.fdopen(master_fd, "rb", buffering=0) + reader = asyncio.StreamReader(loop=loop) + protocol = asyncio.StreamReaderProtocol(reader, loop=loop) + await loop.connect_read_pipe(lambda: protocol, self._master_file) + self._reader = reader + else: + self.proc = await asyncio.create_subprocess_exec( + *self.command, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + cwd=self.cwd, env=self.env, + start_new_session=True, + ) + self._reader = self.proc.stdout + return self + + async def send_line(self, text): + data = (text + "\n").encode("utf-8", "replace") + self._record("stdin", text + "\n") + if self.mode == "pty": + os.write(self._master_fd, data) + else: + self.proc.stdin.write(data) + await self.proc.stdin.drain() + + def _record(self, stream, text): + line = OutputLine(stream, text, time.time()) + self._lines.append(line) + if self._on_line: + self._on_line(line) + + async def _read_chunk(self, n=4096): + try: + chunk = await self._reader.read(n) + except (OSError, ValueError, asyncio.IncompleteReadError): + return b"" + return chunk + + async def wait_for_pattern(self, pattern, timeout=30.0): + """Accumulates output until `pattern` (compiled regex) matches the + ANSI-stripped buffer; returns everything read before the match.""" + buf = "" + deadline = time.monotonic() + timeout + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TestTimeout("timed out waiting for pattern %r" % pattern.pattern, self._lines) + try: + chunk = await asyncio.wait_for(self._read_chunk(), timeout=remaining) + except asyncio.TimeoutError: + raise TestTimeout("timed out waiting for pattern %r" % pattern.pattern, self._lines) + if not chunk: + raise TestCrashed( + "process ended before pattern %r was seen" % pattern.pattern, + self.proc.returncode, self._lines, + ) + text = chunk.decode("utf-8", "replace") + self._record("stdout", text) + buf += strip_ansi(text) + match = pattern.search(buf) + if match: + return buf[:match.start()] + + async def drain_until_eof(self, timeout=120.0): + """Reads everything until the process closes its output — the one-shot case.""" + deadline = time.monotonic() + timeout + out = [] + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TestTimeout("timed out waiting for process output to end", self._lines) + try: + chunk = await asyncio.wait_for(self._read_chunk(), timeout=remaining) + except asyncio.TimeoutError: + raise TestTimeout("timed out waiting for process output to end", self._lines) + if not chunk: + break + text = chunk.decode("utf-8", "replace") + self._record("stdout", text) + out.append(text) + return "".join(out) + + async def wait(self, timeout=None): + if timeout is None: + return await self.proc.wait() + try: + return await asyncio.wait_for(self.proc.wait(), timeout) + except asyncio.TimeoutError: + raise TestTimeout("process did not exit within %.1fs" % timeout, self._lines) + + async def terminate(self, grace=5.0): + """Escalating, group-wide, always-reaping shutdown. Safe to call on an + already-exited process, and safe to call twice.""" + if self.proc is None or self.proc.returncode is not None: + return self.proc.returncode if self.proc else None + pgid = self.proc.pid + for sig, wait_s in ((signal.SIGTERM, grace), (signal.SIGKILL, None)): + try: + os.killpg(pgid, sig) + except ProcessLookupError: + break + except PermissionError: + break + if wait_s is None: + break + try: + await asyncio.wait_for(self.proc.wait(), wait_s) + break + except asyncio.TimeoutError: + continue + try: + return await asyncio.wait_for(self.proc.wait(), 5.0) + except asyncio.TimeoutError: + return None # best-effort; we tried SIGKILL already + finally: + if self._master_file is not None: + try: + self._master_file.close() + except OSError: + pass + + @property + def transcript(self): + return list(self._lines) + + +# --------------------------------------------------------------------------- # +# TestCase: the thing you extend. setup()/run()/verify()/teardown() are the +# override points; execute() is the orchestration and is not meant to be +# overridden. run() can spawn() zero, one, or many ManagedProcess instances — +# the framework doesn't assume a 1:1 test-to-process relationship, since a +# one-shot-style test naturally spawns many short-lived processes while a +# REPL-style test spawns exactly one long-lived one. +# --------------------------------------------------------------------------- # + +class TestCase: + name = None + timeout = 60.0 + mode = "pipe" # default transport for spawn() when not overridden per call + extra_args = () # args always passed to tai.py, before whatever spawn() adds + + def __init__(self, tai_path=None, interactive=False): + self.name = self.name or type(self).__name__ + self.tai_path = Path(tai_path or DEFAULT_TAI_PATH) + self.interactive = interactive + self.logger = None + self._processes = [] + self.env_overrides = {} + + # ---- override points -------------------------------------------------- + + async def setup(self): + """Optional. Prepare fixtures/state (e.g. an isolated TAI_HOME) before any process starts.""" + + async def run(self): + """Required. Drive the process(es) via self.spawn(); return whatever verify() needs.""" + raise NotImplementedError + + async def verify(self, result): + """Required. Raise TestAssertionFailed (with a clear message) if `result` doesn't hold up.""" + raise NotImplementedError + + async def teardown(self): + """Optional. Runs after every spawned process has been terminated, pass or fail.""" + + # ---- helpers for run() -------------------------------------------------- + + def build_env(self): + env = dict(os.environ) + env.update(self.env_overrides) + return env + + def build_command(self, extra_args=()): + return [sys.executable, str(self.tai_path)] + list(self.extra_args) + list(extra_args) + + async def spawn(self, extra_args=(), mode=None, env_overrides=None): + env = self.build_env() + env.update(env_overrides or {}) + proc = ManagedProcess(self.build_command(extra_args), mode=mode or self.mode, env=env, on_line=self._on_line) + await proc.start() + self._processes.append(proc) + return proc + + def _on_line(self, line): + if self.logger: + self.logger.write(line.text) + + def all_transcript(self): + out = [] + for proc in self._processes: + out.extend(proc.transcript) + return out + + # ---- orchestration (do not override) ----------------------------------- + + async def execute(self, log_dir): + start = time.monotonic() + stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S") + log_path = Path(log_dir) / ("%s-%s.log" % (self.name, stamp)) + self.logger = TeeLogger(log_path, echo=True) + self.logger.write("=== %s starting %s ===\n" % (self.name, stamp)) + bridge = StdinBridge() if self.interactive else None + bridge_task = None + try: + await self.setup() + if bridge is not None and bridge.start(): + bridge_task = asyncio.ensure_future(self._pump_bridge(bridge)) + payload = await asyncio.wait_for(self.run(), timeout=self.timeout) + await self.verify(payload) + return TestResult(self.name, True, time.monotonic() - start, transcript=self.all_transcript(), log_path=str(log_path)) + except asyncio.TimeoutError: + err = TestTimeout("test exceeded its %.1fs timeout" % self.timeout, self.all_transcript()) + return self._failure(err, start, log_path) + except TaiTestError as exc: + return self._failure(exc, start, log_path) + except Exception: + err = TaiTestError(traceback.format_exc(), self.all_transcript()) + return self._failure(err, start, log_path) + finally: + if bridge is not None: + bridge.stop() + if bridge_task is not None: + bridge_task.cancel() + for proc in self._processes: + await proc.terminate() + await self.teardown() + self.logger.write("=== %s finished ===\n" % self.name) + self.logger.close() + + def _failure(self, exc, start, log_path): + return TestResult(self.name, False, time.monotonic() - start, error=str(exc), transcript=exc.transcript, log_path=str(log_path)) + + async def _pump_bridge(self, bridge): + """Forwards live human typing (when --interactive) to whichever process + was spawned most recently — the normal case of exactly one long-lived + REPL-mode process.""" + while True: + line = await bridge.queue.get() + if line is None: + break + if self._processes: + await self._processes[-1].send_line(line) + + +# --------------------------------------------------------------------------- # +# TestRunner + CLI: runs a list of TestCase instances sequentially (they +# share a real terminal and, in --interactive mode, real human attention, so +# parallel execution is deliberately not offered here), prints a summary. +# --------------------------------------------------------------------------- # + +class TestRunner: + def __init__(self, cases, log_dir="tests/logs"): + self.cases = cases + self.log_dir = log_dir + + async def run_all(self): + results = [] + for case in self.cases: + print("\n--- running %s ---" % case.name) + result = await case.execute(self.log_dir) + status = "PASS" if result.passed else "FAIL" + print("--- %s: %s (%.1fs, log: %s) ---" % (case.name, status, result.duration, result.log_path)) + if not result.passed: + print(" %s" % (result.error or "").splitlines()[-1] if result.error else " (no error message)") + results.append(result) + self.print_summary(results) + return results + + def print_summary(self, results): + print("\n=== summary ===") + for result in results: + status = "PASS" if result.passed else "FAIL" + print("%-6s %-40s %6.1fs %s" % (status, result.name, result.duration, result.log_path)) + passed = sum(1 for r in results if r.passed) + print("%d/%d passed" % (passed, len(results))) + + +def main_cli(cases, default_log_dir=None): + """Standard entrypoint for a tests/*.py module: sys.exit(main_cli(ALL_CASES)). + + default_log_dir should normally be the calling module's own directory, + e.g. str(Path(__file__).resolve().parent / "logs") — passed explicitly + because __file__ inside this function would resolve to tai_test.py's own + location, not the caller's.""" + parser = argparse.ArgumentParser(description="run tai integration tests") + parser.add_argument("--interactive", action="store_true", help="let you type into the running process live, alongside the scripted input") + parser.add_argument("--filter", default=None, help="only run tests whose name contains this substring") + parser.add_argument("--timeout", type=float, default=None, help="override every selected test's timeout, in seconds") + parser.add_argument("--log-dir", default=default_log_dir or str(Path(__file__).resolve().parent / "logs")) + args = parser.parse_args() + selected = [case for case in cases if not args.filter or args.filter in case.name] + if not selected: + print("no tests matched filter %r" % args.filter) + return 1 + for case in selected: + case.interactive = args.interactive + if args.timeout: + case.timeout = args.timeout + runner = TestRunner(selected, log_dir=args.log_dir) + results = asyncio.run(runner.run_all()) + return 0 if all(r.passed for r in results) else 1 + + +if __name__ == "__main__": + print(__doc__) + sys.exit(0) diff --git a/test_tai.py b/test_tai.py index b7f7624..c43b974 100644 --- a/test_tai.py +++ b/test_tai.py @@ -1550,6 +1550,8 @@ class LazyTests(unittest.TestCase): name = schema["function"]["name"] if name in tai.CORE_TOOLS: continue + if not tai.tool_env_ok(name): + continue self.assertIn("- %s:" % name, catalog) def test_tool_results_feed_selection(self): @@ -1567,7 +1569,7 @@ class LazyTests(unittest.TestCase): seen = {} reply = {"role": "assistant", "content": "hi", "reasoning": "", "tool_calls": [], "backend": "x"} - def fake_complete(messages, tools, stream_sink=None): + def fake_complete(messages, tools, stream_sink=None, deadline=None): seen["tools"] = {schema["function"]["name"] for schema in tools} return dict(reply) @@ -1662,7 +1664,7 @@ class InstallOpsTests(unittest.TestCase): with mock.patch.object(tai, "venv_available", return_value=False): self.assertIn("unavailable", tai.op_venv("install", self.config.home)) - def fake_run(argv, capture_output=True, text=True, timeout=300): + def fake_run(argv, stdout=None, stderr=None, universal_newlines=True, timeout=300): folder = argv[-1] python = os.path.join(folder, "bin", "python") os.makedirs(os.path.dirname(python)) @@ -1827,7 +1829,7 @@ class BotsTests(unittest.TestCase): seen = [] reply = {"role": "assistant", "content": "coded-it", "reasoning": "", "tool_calls": [], "backend": "x"} - def fake_complete(messages, tools, stream_sink=None): + def fake_complete(messages, tools, stream_sink=None, deadline=None): seen.append(messages[0]["content"]) return dict(reply) @@ -2527,9 +2529,10 @@ class DiffTests(unittest.TestCase): def test_show_result_renders_and_clears(self): agent = tai.Agent(self.config, self.store, persist=False, quiet=False) agent.tools.diff_preview = {"path": "a.py", "old": "x = 1\n", "new": "x = 2\n"} - with mock.patch("builtins.print") as shown: - agent.show_result("ok", 1) - agent.show_result("ok", 1) + with mock.patch.object(sys.stdout, "isatty", return_value=False): + with mock.patch("builtins.print") as shown: + agent.show_result("ok", 1) + agent.show_result("ok", 1) printed = "\n".join(str(call.args[0]) for call in shown.call_args_list) self.assertEqual(printed.count("── a.py"), 1) self.assertEqual(printed.count("└─"), 2) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/bashrc.py b/tests/bashrc.py new file mode 100644 index 0000000..0c200e9 --- /dev/null +++ b/tests/bashrc.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +"""tests/bashrc.py — the tests that can only run honestly inside the throwaway +container (see tests/docker/Dockerfile, docker-compose.yml, tests/container.py). + +Everything here either installs a real hook into a real $HOME/.bashrc and +spawns a real `bash -i` to watch it fire, or drives a real `tmux` session. +Both are things no test should ever do against the machine actually running +Claude Code -- so every test case that touches $HOME/.bashrc checks +TAI_CONTAINER_TEST=1 in setup() and refuses outright if it's missing, rather +than trusting whoever runs this file to remember to only run it in the +container. tests/docker/Dockerfile sets that var; nothing else should. + +Deterministic throughout, same as tests/plexer.py: no LLM involved, this is +pty/shell/signal mechanics, not a model decision. +""" +import asyncio +import glob +import os +import re +import shutil +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from tai_test import TestCase, TestAssertionFailed, ManagedProcess, main_cli # noqa: E402 +import tai # noqa: E402 + + +async def _drain_forever(proc): + """Same requirement as tests/plexer.py's helper of the same name (see + that file for the full postmortem): only needed when the spawned process + itself runs a second relay loop that would otherwise self-deadlock on its + own unread output -- i.e. once bash execs into `tai.py --plexer-wrap`. + Plain `bash -i` that never wraps doesn't need this; those tests read + directly instead.""" + try: + while True: + chunk = await proc._read_chunk() + if not chunk: + await asyncio.sleep(0.05) + except asyncio.CancelledError: + pass + + +class _BashrcHookTestBase(TestCase): + """Shared fixture for every test that installs the real plexer hook into + a real .bashrc: a throwaway $HOME, the real tai.py binary copied into + $HOME/.local/bin via tai.op_binary (exactly what `tai.py --install` does + on a real machine), and the hook itself via tai.op_plexer_hook -- the + same functions a real install runs, not a hand-rolled substitute.""" + + mode = "pty" + timeout = 30.0 + + async def setup(self): + if os.environ.get("TAI_CONTAINER_TEST") != "1": + raise TestAssertionFailed( + "refusing to run (TAI_CONTAINER_TEST is not set): this test installs a real " + "hook into $HOME/.bashrc and must only ever run inside the disposable test " + "container (see tests/docker/Dockerfile), never against a real machine's home" + ) + self.tmp_home = tempfile.mkdtemp(prefix="tai-test-home-") + os.makedirs(os.path.join(self.tmp_home, ".local", "bin"), exist_ok=True) + real_home = os.environ.get("HOME") + os.environ["HOME"] = self.tmp_home + try: + tai.op_binary("install", self.tmp_home) + tai.op_plexer_hook("install", self.tmp_home) + finally: + if real_home is not None: + os.environ["HOME"] = real_home + else: + os.environ.pop("HOME", None) + self._drain_task = None + + async def _spawn_bash(self, args, mode=None, env_overrides=None): + env = self.build_env() + env.update(env_overrides or {}) + proc = ManagedProcess(["bash"] + list(args), mode=mode or self.mode, env=env, on_line=self._on_line) + await proc.start() + self._processes.append(proc) + return proc + + def _sock_matches(self): + return glob.glob(os.path.join(self.tmp_home, ".tai", "plexer", "*.sock")) + + async def teardown(self): + if self._drain_task is not None: + self._drain_task.cancel() + shutil.rmtree(self.tmp_home, ignore_errors=True) + + +class BashrcInstallAndAutoWrapTest(_BashrcHookTestBase): + """The actual point of building a container for this: a real `bash -i`, + with a real installed hook in its real $HOME/.bashrc, auto-wraps into a + real NativePlexerBackend with no human and no flag telling it to.""" + + name = "bashrc-install-and-auto-wrap" + + async def run(self): + proc = await self._spawn_bash(["-i"], env_overrides={"HOME": self.tmp_home}) + self._drain_task = asyncio.ensure_future(_drain_forever(proc)) + + sock_path = None + for _ in range(50): + matches = self._sock_matches() + if matches: + sock_path = matches[0] + break + await asyncio.sleep(0.2) + if not sock_path: + raise TestAssertionFailed( + "bash -i never auto-wrapped into a plexer backend via the installed .bashrc hook" + ) + + controller = tai.NativePlexerController(sock_path) + await asyncio.sleep(0.3) + status = controller.status() + + controller.send_keys("echo $TAI_PLEXER_SOCKET", enter=True) + await asyncio.sleep(0.4) + pane = controller.read_pane(50) + + return {"sock_path": sock_path, "pane": pane, "status": status} + + async def verify(self, result): + if result["sock_path"] not in result["pane"]: + raise TestAssertionFailed( + "the auto-wrapped inner shell's $TAI_PLEXER_SOCKET did not match the backend's " + "own socket path: %r" % result["pane"][-300:] + ) + if not result["status"].get("pid") or result["status"]["pid"] <= 0: + raise TestAssertionFailed("status() did not report a live child pid after auto-wrap") + + +class AntiRecursionTest(_BashrcHookTestBase): + """Already-wrapped ($TAI_PLEXER_ACTIVE=1) must never wrap again.""" + + name = "bashrc-hook-anti-recursion" + + async def run(self): + proc = await self._spawn_bash( + ["-i"], env_overrides={"HOME": self.tmp_home, "TAI_PLEXER_ACTIVE": "1"} + ) + await proc.send_line("echo anti_recursion_marker_ok") + await proc.wait_for_pattern(re.compile(r"anti_recursion_marker_ok"), timeout=8.0) + await asyncio.sleep(0.3) + return {"sock_matches": self._sock_matches()} + + async def verify(self, result): + if result["sock_matches"]: + raise TestAssertionFailed( + "a plexer socket was created even though TAI_PLEXER_ACTIVE was already set -- " + "recursive wrap was not prevented" + ) + + +class TmuxActiveSkipTest(_BashrcHookTestBase): + """An already-active tmux pane ($TMUX set) must also never wrap.""" + + name = "bashrc-hook-tmux-active-skip" + + async def run(self): + proc = await self._spawn_bash( + ["-i"], env_overrides={"HOME": self.tmp_home, "TMUX": "dummy-session:0.0"} + ) + await proc.send_line("echo tmux_skip_marker_ok") + await proc.wait_for_pattern(re.compile(r"tmux_skip_marker_ok"), timeout=8.0) + await asyncio.sleep(0.3) + return {"sock_matches": self._sock_matches()} + + async def verify(self, result): + if result["sock_matches"]: + raise TestAssertionFailed( + "a plexer socket was created even though $TMUX was already set -- tmux should " + "have been left in sole control of the session" + ) + + +class NonInteractiveSkipTest(_BashrcHookTestBase): + """A non-interactive shell must never wrap, even if it explicitly sources + the hook (isolates the hook's own `case $- in *i*)` guard from bash's + separate, unrelated default of not reading ~/.bashrc for `-c` at all).""" + + name = "bashrc-hook-non-interactive-skip" + mode = "pipe" + + async def run(self): + proc = await self._spawn_bash( + ["-c", 'source "$HOME/.bashrc" 2>&1; echo marker_non_interactive_ok'], + mode="pipe", + env_overrides={"HOME": self.tmp_home}, + ) + output = await proc.drain_until_eof(timeout=10.0) + returncode = await proc.wait(timeout=5.0) + return {"output": output, "returncode": returncode} + + async def verify(self, result): + if "marker_non_interactive_ok" not in result["output"]: + raise TestAssertionFailed( + "a non-interactive `bash -c` that explicitly sourced the hook never reached past " + "it: %r" % result["output"][-500:] + ) + if result["returncode"] != 0: + raise TestAssertionFailed( + "non-interactive shell exited %r after sourcing the hook" % result["returncode"] + ) + if self._sock_matches(): + raise TestAssertionFailed( + "a plexer socket was created from a non-interactive shell -- the " + "`case $- in *i*)` guard failed" + ) + + +class TmuxControllerLiveTest(TestCase): + """Drives TmuxPlexerController against a real `tmux new-session -d`, not + a mock -- the one TmuxPlexerController code path tests/plexer.py cannot + cover, since that file only ever exercises NativePlexerBackend.""" + + name = "bashrc-tmux-controller-live" + timeout = 20.0 + + async def setup(self): + if not shutil.which("tmux"): + raise TestAssertionFailed( + "tmux is not installed -- required to test TmuxPlexerController against a real session" + ) + self.session_name = "tai-test-%d" % os.getpid() + + async def run(self): + create = await asyncio.create_subprocess_exec( + "tmux", "new-session", "-d", "-s", self.session_name, "-x", "80", "-y", "24", + ) + await create.wait() + if create.returncode != 0: + raise TestAssertionFailed("tmux new-session -d failed (returncode %r)" % create.returncode) + + pane = "%s:0.0" % self.session_name + controller = tai.TmuxPlexerController(pane) + + status = controller.status() + if status.get("backend") != "tmux": + raise TestAssertionFailed("status() did not report backend=tmux: %r" % status) + + controller.send_keys("echo tmux_controller_marker_ok", enter=True) + await asyncio.sleep(0.6) + pane_text = controller.read_pane(50) + if "tmux_controller_marker_ok" not in pane_text: + raise TestAssertionFailed( + "send_keys/read_pane against a real tmux session failed: %r" % pane_text[-300:] + ) + + controller.type_text("echo tmux_typed_marker_ok", delay=0.01, enter=True) + await asyncio.sleep(0.6) + pane_text_2 = controller.read_pane(50) + if "tmux_typed_marker_ok" not in pane_text_2: + raise TestAssertionFailed( + "type_text against a real tmux session failed: %r" % pane_text_2[-300:] + ) + + bogus = tai.TmuxPlexerController("nonexistent-session-%d:99.99" % os.getpid()) + raised = False + try: + bogus.status() + except tai.PlexerError: + raised = True + return {"raised": raised} + + async def verify(self, result): + if not result["raised"]: + raise TestAssertionFailed( + "TmuxPlexerController against a nonexistent pane should raise PlexerError, not " + "silently succeed" + ) + + async def teardown(self): + kill = await asyncio.create_subprocess_exec( + "tmux", "kill-session", "-t", self.session_name, + stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, + ) + await kill.wait() + + +class PlexerHookInstallUninstallRoundTripTest(TestCase): + """Purely deterministic, no subprocess, no real $HOME -- the install + functions are tested directly against a throwaway file path, so this one + is safe to run anywhere, not just in the container.""" + + name = "bashrc-hook-install-uninstall-roundtrip" + timeout = 10.0 + + async def setup(self): + self.tmp_dir = tempfile.mkdtemp(prefix="tai-test-bashrc-") + self.bashrc_path = os.path.join(self.tmp_dir, ".bashrc") + with open(self.bashrc_path, "w", encoding="utf-8") as handle: + handle.write("# pre-existing user content\nexport FOO=bar\n") + + async def run(self): + with open(self.bashrc_path, encoding="utf-8") as handle: + original = handle.read() + + installed_first = tai.upsert_plexer_bashrc_block(self.bashrc_path) + present_after_first = tai.plexer_hook_present(self.bashrc_path) + with open(self.bashrc_path, encoding="utf-8") as handle: + content_after_first = handle.read() + + installed_second = tai.upsert_plexer_bashrc_block(self.bashrc_path) + with open(self.bashrc_path, encoding="utf-8") as handle: + content_after_second = handle.read() + + backup_path = self.bashrc_path + ".bak-tai" + backup_exists = os.path.exists(backup_path) + backup_content = None + if backup_exists: + with open(backup_path, encoding="utf-8") as handle: + backup_content = handle.read() + + removed = tai.remove_plexer_bashrc_block(self.bashrc_path) + present_after_remove = tai.plexer_hook_present(self.bashrc_path) + with open(self.bashrc_path, encoding="utf-8") as handle: + content_after_remove = handle.read() + + reinstalled = tai.upsert_plexer_bashrc_block(self.bashrc_path) + present_after_reinstall = tai.plexer_hook_present(self.bashrc_path) + + return { + "original": original, + "installed_first": installed_first, + "present_after_first": present_after_first, + "content_after_first": content_after_first, + "installed_second": installed_second, + "content_after_second": content_after_second, + "backup_exists": backup_exists, + "backup_content": backup_content, + "removed": removed, + "present_after_remove": present_after_remove, + "content_after_remove": content_after_remove, + "reinstalled": reinstalled, + "present_after_reinstall": present_after_reinstall, + } + + async def verify(self, result): + if not result["installed_first"]: + raise TestAssertionFailed("first upsert_plexer_bashrc_block() reported no change on a clean file") + if not result["present_after_first"]: + raise TestAssertionFailed("plexer_hook_present() returned false right after installing") + if "export FOO=bar" not in result["content_after_first"]: + raise TestAssertionFailed("installing the hook clobbered pre-existing .bashrc content") + + if result["installed_second"]: + raise TestAssertionFailed("upsert_plexer_bashrc_block() was not idempotent -- reported a change on a second call") + if result["content_after_second"] != result["content_after_first"]: + raise TestAssertionFailed("a second install duplicated the hook block instead of being a no-op") + + if not result["backup_exists"]: + raise TestAssertionFailed("no .bak-tai backup was created before modifying .bashrc") + if result["backup_content"] != result["original"]: + raise TestAssertionFailed("the .bak-tai backup does not match the original pre-install content") + + if not result["removed"]: + raise TestAssertionFailed("remove_plexer_bashrc_block() reported no change when the hook was present") + if result["present_after_remove"]: + raise TestAssertionFailed("plexer_hook_present() still true after removal") + if "export FOO=bar" not in result["content_after_remove"]: + raise TestAssertionFailed("uninstalling the hook lost pre-existing .bashrc content") + if tai.PLEXER_BASHRC_MARK_BEGIN in result["content_after_remove"]: + raise TestAssertionFailed("uninstall left hook marker text behind") + + if not result["reinstalled"]: + raise TestAssertionFailed("could not reinstall the hook after a clean uninstall") + if not result["present_after_reinstall"]: + raise TestAssertionFailed("hook not present after reinstalling post-uninstall") + + async def teardown(self): + shutil.rmtree(self.tmp_dir, ignore_errors=True) + + +ALL_CASES = [ + BashrcInstallAndAutoWrapTest(), + AntiRecursionTest(), + TmuxActiveSkipTest(), + NonInteractiveSkipTest(), + TmuxControllerLiveTest(), + PlexerHookInstallUninstallRoundTripTest(), +] + +if __name__ == "__main__": + sys.exit(main_cli(ALL_CASES, default_log_dir=str(Path(__file__).resolve().parent / "logs"))) diff --git a/tests/container.py b/tests/container.py new file mode 100644 index 0000000..21ca6f4 --- /dev/null +++ b/tests/container.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""tests/container.py — run the full test suite inside a throwaway container. + +Two things can only be tested honestly inside a disposable environment, not +on the machine actually running Claude Code: installing the plexer hook into +a real ~/.bashrc and watching a real `bash -i` actually get wrapped by it +(tests/bashrc.py), and driving TmuxPlexerController against a real tmux +session. This is the harness for that -- not a replacement for test_seal.py, +test_tai.py, or tests/plexer.py, which also run inside the same container +first, so "did it work in a clean environment" is answered for everything, +not just the two new container-only tests. tests/rag.py runs too, but as a +best-effort check, not a required one -- see BEST_EFFORT_SUITE below for why. + +Engine detection deliberately reuses tai.container_engine() (podman first, +then docker, same as the project's own sandbox feature) rather than +reimplementing "which one is installed" a second way. docker-compose.yml at +the repo root exists for a human who wants `docker compose run --rm +tai-test` / `podman compose run --rm tai-test`; this script drives plain +build/run directly so the automated path never depends on whether a compose +implementation happens to be installed on top of the engine itself. + +Usage: + python3 tests/container.py # build the image, run everything inside it + python3 tests/container.py --inside # (used by the container itself) actually run the suite +""" +import argparse +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT)) +import tai # noqa: E402 + +IMAGE_TAG = "tai-test:latest" + +# Run in this order: cheapest/most-fundamental first, so a basic break fails +# fast instead of burning time on tmux/bashrc tests that assume the basics +# already work. These must all pass for the container run to count as green. +REQUIRED_SUITE = [ + ("test_seal.py", ["python3", "test_seal.py"]), + ("test_tai.py", ["python3", "-m", "unittest", "test_tai", "-v"]), + ("tests/plexer.py", ["python3", "tests/plexer.py"]), + ("tests/bashrc.py", ["python3", "tests/bashrc.py"]), +] + +# tests/rag.py's RagIndexAndSearchTest needs a real, working LLM backend. +# Confirmed by direct investigation (not assumed): inside this disposable +# container there is no opencode/claude/codex/gemini/grok CLI (the host's +# own successful run used a locally authenticated `opencode` CLI that has +# no business being baked into a throwaway test image), and the remaining +# free HTTP backends fail for reasons that are about the shared sandbox +# network, not this code -- openrouter's free tier returned "HTTP 401: No +# cookie auth credentials found" for every model (anonymous free-tier auth +# needs a browser cookie session this container doesn't have), and +# pollinations returned "HTTP 429: Queue full for IP: ...: 1 requests +# already queued" (its free tier's per-IP concurrency cap, already +# exhausted by other traffic sharing this sandbox's egress address). Both +# reproduced with a bare urllib POST outside of tai.py entirely, so this is +# not a tai.py bug to fix. It's run and reported like everything else, but +# never fails the overall container result -- tests/rag.py already runs +# (and passes) against the host directly, which is where a real LLM +# backend actually exists; see tests/README.md's "test with the real AI, +# accept the flakes" rule, which this is really just a container-flavored +# instance of. +BEST_EFFORT_SUITE = [ + ("tests/rag.py", ["python3", "tests/rag.py"]), +] + + +def run_outside(): + engine = tai.container_engine() + if not engine: + print("no container engine found (checked podman, then docker) -- refusing to run the " + "container-only test suite. Install either one to run tests/bashrc.py and the " + "TmuxPlexerController live test; everything else already ran on the host directly.") + return 1 + + print("using container engine: %s" % engine, flush=True) + build = subprocess.run([engine, "build", "-t", IMAGE_TAG, "-f", "tests/docker/Dockerfile", "."], cwd=str(ROOT)) + if build.returncode != 0: + print("image build failed", flush=True) + return build.returncode + + run = subprocess.run([engine, "run", "--rm", IMAGE_TAG, "python3", "tests/container.py", "--inside"], cwd=str(ROOT)) + return run.returncode + + +def _run_suite(suite): + results = [] + for name, cmd in suite: + print("\n=== %s ===" % name) + done = subprocess.run(cmd, cwd=str(ROOT)) + results.append((name, done.returncode == 0)) + print("--- %s: %s ---" % (name, "PASS" if done.returncode == 0 else "FAIL")) + return results + + +def run_inside(): + required = _run_suite(REQUIRED_SUITE) + best_effort = _run_suite(BEST_EFFORT_SUITE) + + print("\n=== container test summary ===") + for name, passed in required: + print("%-6s %s" % ("PASS" if passed else "FAIL", name)) + for name, passed in best_effort: + print("%-6s %s (best-effort: needs a real LLM backend, see comment in this file)" % ("PASS" if passed else "FAIL", name)) + + failed_required = [name for name, passed in required if not passed] + failed_best_effort = [name for name, passed in best_effort if not passed] + total = len(required) + len(best_effort) + passed_count = total - len(failed_required) - len(failed_best_effort) + print("%d/%d passed" % (passed_count, total), end="") + if failed_best_effort: + print(" (ignoring best-effort failures: %s)" % ", ".join(failed_best_effort)) + else: + print("") + + if failed_required: + print("REQUIRED tests failed: %s" % ", ".join(failed_required)) + return 1 + return 0 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--inside", action="store_true", help="internal: actually run the suite (used by the container itself)") + args = parser.parse_args() + if args.inside: + return run_inside() + return run_outside() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile new file mode 100644 index 0000000..5a28216 --- /dev/null +++ b/tests/docker/Dockerfile @@ -0,0 +1,30 @@ +# Minimal, disposable environment for the tests that cannot safely or +# meaningfully run against a real machine: anything that installs the +# plexer hook into a real ~/.bashrc, or needs a real tmux session to test +# TmuxPlexerController against. Built fresh per run (see tests/container.py); +# nothing here is meant to persist, which is the entire point -- these tests +# get to do things no test should ever do to the machine actually running +# Claude Code. +FROM python:3.11-slim + +# bash: the thing being wrapped. tmux: TmuxPlexerController's real backend. +# procps: `ps`, used by tests/plexer.py and tests/bashrc.py to prove a +# process is actually gone, not just assumed gone. git: sysinfo/create_skill +# paths touch it; harmless to have for a "simulate everything" image. +RUN apt-get update \ + && apt-get install -y --no-install-recommends bash tmux procps git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY . /app + +# Safety marker, not a feature flag: tests/bashrc.py refuses to install a +# real hook into $HOME/.bashrc unless this is set, so a copy-paste of this +# test file running directly on a developer's machine fails loud instead of +# quietly rewriting their actual shell startup. +ENV TAI_CONTAINER_TEST=1 + +# No ENTRYPOINT/CMD here on purpose -- tests/container.py always passes an +# explicit command (python3 tests/container.py --inside ...), so this image +# has one job and docker-compose.yml's `command:` is the single source of +# truth for what that job is, not two places that can drift apart. diff --git a/tests/plexer.py b/tests/plexer.py new file mode 100644 index 0000000..74e1678 --- /dev/null +++ b/tests/plexer.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +"""tests/plexer.py — integration tests for tai's Plexer subsystem (NativePlexerBackend). + +Deterministic throughout: this drives tai.py --plexer-wrap, a plumbing mode +that forks a real shell onto a real pty with no LLM involvement at all, so +"test with the real AI" (tests/README.md) doesn't apply here the way it does +to tests/rag.py — there is no model decision anywhere in this path to test. +What's real instead is the pty/signal/socket mechanics themselves: the whole +point of this file is that it exercises the actual `tai.py --plexer-wrap` +binary under a real pty (via tai_test.py's own pty mode — testing a pty +wrapper by driving it through another pty), not mocked termios/signal calls. + +A real bug was caught by actually running this, not by reading the code: an +external SIGTERM (and SIGHUP) does not terminate an interactive bash running +as a pty.fork() session leader in this environment -- verified with a bare +pty.fork()+bash+os.kill script, independent of any tai.py code. +NativePlexerBackend's Ctrl+A,q shortcut uses SIGKILL specifically because of +this, not out of caution -- SIGTERM was tried first and silently did nothing +for ~10s in a real run. +""" +import glob +import os +import shutil +import struct +import sys +import tempfile +import asyncio +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from tai_test import TestCase, TestAssertionFailed, main_cli # noqa: E402 +import tai # noqa: E402 + + +async def _drain_forever(proc): + """A real terminal emulator is always reading, continuously, for as long + as it's open -- that's what keeps a pty's output buffer from ever + backing up. This test harness has to replicate that explicitly: without + a background task doing this, NativePlexerBackend's relay loop can block + on its own os.write(stdout_fd, ...) the moment a command's echoed output + is large enough to fill the (unread) outer pty buffer, which stalls the + *entire* relay loop -- including replying to later control-socket calls, + which is what made this look like a control-protocol deadlock the first + time this was debugged, when the actual missing piece was this drain. + + A second real bug, found immediately after fixing the first: once the + wrapped shell exits, the stream hits EOF and read() returns b"" forever + without blocking -- so a plain `while True: await proc._read_chunk()` + busy-spins at that point instead of idling, and spins hard enough to + starve the event loop so badly that even asyncio.wait_for's own timeout + machinery stops getting a fair turn. Hence the explicit EOF check.""" + try: + while True: + chunk = await proc._read_chunk() + if not chunk: + await asyncio.sleep(0.05) + except asyncio.CancelledError: + pass + + +class NativePlexerBackendTest(TestCase): + """Spawns `tai.py --plexer-wrap` under a real pty (tai_test's pty mode), + then drives it as an external controller would: over its control socket, + plus raw keystrokes written straight to the outer pty master to exercise + the Ctrl+A prefix-key path exactly as a human's terminal would deliver it.""" + + name = "plexer-native-backend" + mode = "pty" + timeout = 30.0 + + async def setup(self): + self.tmp_home = tempfile.mkdtemp(prefix="tai-test-home-") + self.env_overrides = {"TAI_HOME": self.tmp_home, "TAI_PASSPHRASE": ""} + + async def run(self): + proc = await self.spawn(extra_args=["--plexer-wrap"]) + self._drain_task = asyncio.ensure_future(_drain_forever(proc)) + sock_path = None + for _ in range(50): + matches = glob.glob(os.path.join(self.tmp_home, "plexer", "*.sock")) + if matches: + sock_path = matches[0] + break + await asyncio.sleep(0.2) + if not sock_path: + raise TestAssertionFailed("NativePlexerBackend never created its control socket") + + controller = tai.NativePlexerController(sock_path) + await asyncio.sleep(0.3) + + status = controller.status() + child_pid = status.get("pid") + if not child_pid or child_pid <= 0: + raise TestAssertionFailed("status() did not report a valid child pid: %r" % status) + + controller.send_keys("echo plexer_test_marker", enter=True) + await asyncio.sleep(0.4) + after_send = controller.read_pane(50) + if "plexer_test_marker" not in after_send: + raise TestAssertionFailed("send_keys/read_pane round trip failed: %r" % after_send[-300:]) + + controller.type_text("echo typed_marker_ok", delay=0.01, enter=True) + await asyncio.sleep(0.4) + after_type = controller.read_pane(50) + if "typed_marker_ok" not in after_type: + raise TestAssertionFailed("type_text did not reach the shell: %r" % after_type[-300:]) + + import fcntl + import termios + fcntl.ioctl(proc._master_fd, termios.TIOCSWINSZ, struct.pack("HHHH", 17, 66, 0, 0)) + os.kill(proc.proc.pid, tai.signal.SIGWINCH) + await asyncio.sleep(0.4) + controller.send_keys("stty size", enter=True) + await asyncio.sleep(0.4) + after_resize = controller.read_pane(50) + if "17 66" not in after_resize: + raise TestAssertionFailed("SIGWINCH resize was not forwarded to the inner shell: %r" % after_resize[-300:]) + + controller.send_keys("echo contains_a_d_character", enter=True) + await asyncio.sleep(0.3) + after_plain = controller.read_pane(50) + if "contains_a_d_character" not in after_plain: + raise TestAssertionFailed("a plain command containing the letter 'd' was mangled by prefix-key handling") + + os.write(proc._master_fd, tai.PLEXER_PREFIX_KEY) + os.write(proc._master_fd, b"q") + await proc.wait(timeout=10.0) + await asyncio.sleep(0.2) + socket_survived = os.path.exists(sock_path) + + import subprocess + still_running = subprocess.run(["ps", "-p", str(child_pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 + + log_matches = glob.glob(os.path.join(self.tmp_home, "plexer", "*.log")) + log_has_marker = False + if log_matches: + with open(log_matches[0], "rb") as handle: + log_has_marker = b"plexer_test_marker" in handle.read() + + return { + "socket_survived": socket_survived, + "still_running": still_running, + "log_has_marker": log_has_marker, + } + + async def verify(self, result): + if result["socket_survived"]: + raise TestAssertionFailed("control socket file was not cleaned up after Ctrl+A,q") + if result["still_running"]: + raise TestAssertionFailed("the wrapped shell process was still alive after Ctrl+A,q (leaked process)") + if not result["log_has_marker"]: + raise TestAssertionFailed("the continuous session log did not capture the session's actual output") + + async def teardown(self): + self._drain_task.cancel() + shutil.rmtree(self.tmp_home, ignore_errors=True) + + +class NativePlexerBackendRobustnessTest(TestCase): + """Targets three specific bugs found and fixed during a deliberate review + pass after NativePlexerBackendTest first went green -- a happy-path test + passing does not mean the code is reliable, and all three of these would + have sailed through that test unnoticed: + + 1. File permissions: the control socket and the session log (which + captures the ENTIRE terminal session verbatim -- anything typed, + including passwords) were being created with umask-default + permissions, often world-readable/connectable on a multi-user box. + 2. The control protocol's server side did a single recv() instead of + looping like the client side already did, silently truncating any + request too large for one read -- untestable with short demo strings, + which is exactly why the first test never caught it. + 3. A malformed (non-JSON-object) request on the control socket was not + defensively handled and would have propagated an uncaught exception + out of the relay loop, crashing the user's entire live wrapped + terminal session over one bad control message. + 4. A genuine deadlock: plexer_send used to write its text directly into + the pty master from inside the control-request handler. For a large + payload on an echo-enabled pty, that write can block waiting for + buffer space that only frees up once something reads the echoed + output back out -- which is this very relay loop's own job, and it + was the one blocked. Fixed by queuing all writes-to-child and only + ever draining them from the main select() loop, cooperatively with + reading the child's output, never as a blocking write from inside + request handling. + """ + + name = "plexer-native-backend-robustness" + mode = "pty" + timeout = 30.0 + + async def setup(self): + self.tmp_home = tempfile.mkdtemp(prefix="tai-test-home-") + self.env_overrides = {"TAI_HOME": self.tmp_home, "TAI_PASSPHRASE": ""} + + async def run(self): + proc = await self.spawn(extra_args=["--plexer-wrap"]) + self._drain_task = asyncio.ensure_future(_drain_forever(proc)) + sock_path = None + for _ in range(50): + matches = glob.glob(os.path.join(self.tmp_home, "plexer", "*.sock")) + if matches: + sock_path = matches[0] + break + await asyncio.sleep(0.2) + if not sock_path: + raise TestAssertionFailed("NativePlexerBackend never created its control socket") + controller = tai.NativePlexerController(sock_path) + await asyncio.sleep(0.3) + + plexer_dir_path = os.path.dirname(sock_path) + log_matches = glob.glob(os.path.join(plexer_dir_path, "*.log")) + if not log_matches: + raise TestAssertionFailed("no log file found to check permissions on") + log_path = log_matches[0] + + dir_mode = os.stat(plexer_dir_path).st_mode & 0o777 + sock_mode = os.stat(sock_path).st_mode & 0o777 + log_mode = os.stat(log_path).st_mode & 0o777 + + # Large payload: bigger than the control protocol's 65536-byte recv + # chunk size, to actually exercise the server-side framing loop + # rather than the common case that fits in one read. + needle_head = "LARGE_PAYLOAD_START_" + needle_tail = "_LARGE_PAYLOAD_END" + padding = "x" * 90000 + large_text = "echo %s%s%s" % (needle_head, padding, needle_tail) + controller.send_keys(large_text, enter=True) + await asyncio.sleep(0.6) + large_result = controller.read_pane(2000) + + # Malformed control request: connect directly to the socket (bypassing + # the well-formed NativePlexerController entirely) and send something + # that is valid JSON but not an object, which the old code would have + # handed straight to request.get("cmd") and crashed on. + import socket as socket_module + import json as json_module + raw_sock = socket_module.socket(socket_module.AF_UNIX, socket_module.SOCK_STREAM) + raw_sock.settimeout(5) + raw_sock.connect(sock_path) + raw_sock.sendall(b"[1, 2, 3]\n") + malformed_reply_raw = raw_sock.recv(65536) + raw_sock.close() + malformed_reply = json_module.loads(malformed_reply_raw.decode("utf-8", "replace")) + + # The backend must still be alive and responsive after that malformed + # request -- this is the actual point of the test, not the error + # shape above. + status_after = controller.status() + + controller.send_keys("exit", enter=True) + await proc.wait(timeout=10.0) + + return { + "dir_mode": dir_mode, + "sock_mode": sock_mode, + "log_mode": log_mode, + "large_result": large_result, + "malformed_reply": malformed_reply, + "status_after_pid": status_after.get("pid"), + } + + async def verify(self, result): + if result["dir_mode"] != 0o700: + raise TestAssertionFailed("plexer dir mode is %o, expected 0700 (owner-only)" % result["dir_mode"]) + if result["sock_mode"] != 0o600: + raise TestAssertionFailed("control socket mode is %o, expected 0600 (owner-only)" % result["sock_mode"]) + if result["log_mode"] != 0o600: + raise TestAssertionFailed("session log mode is %o, expected 0600 (owner-only) -- it captures everything typed" % result["log_mode"]) + + large = result["large_result"] + if "LARGE_PAYLOAD_START_" not in large or "_LARGE_PAYLOAD_END" not in large: + raise TestAssertionFailed("large (>65536 byte) plexer_send payload was truncated by the control protocol") + + if result["malformed_reply"].get("ok") is not False: + raise TestAssertionFailed("a malformed control request did not come back as a clean error: %r" % result["malformed_reply"]) + + if not result["status_after_pid"] or result["status_after_pid"] <= 0: + raise TestAssertionFailed("backend did not survive the malformed request -- a bad control message must never crash the whole wrapped session") + + async def teardown(self): + self._drain_task.cancel() + shutil.rmtree(self.tmp_home, ignore_errors=True) + + +ALL_CASES = [NativePlexerBackendTest(), NativePlexerBackendRobustnessTest()] + +if __name__ == "__main__": + sys.exit(main_cli(ALL_CASES, default_log_dir=str(Path(__file__).resolve().parent / "logs"))) diff --git a/tests/rag.py b/tests/rag.py new file mode 100644 index 0000000..a1bb8e1 --- /dev/null +++ b/tests/rag.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +"""tests/rag.py — integration tests for tai's chunked FTS5/BM25 RAG system. + +Built on tai_test.py (see tests/README.md for the conventions this file +follows). Two test cases, deliberately different in how deterministic they +are: + +- InteractiveReplSmokeTest drives a persistent REPL over a real pty with + nothing but REPL slash commands (/skills, /search). Slash commands dispatch + tools directly, with no LLM call involved, so this test is fully + deterministic and fast — it exists to prove the pty transport, the + prompt-detection turn boundary, and the REPL plumbing all actually work, + independent of model behavior. +- RagIndexAndSearchTest is the real ask: index a sample of this project's own + research/ archive into a fresh, isolated vault, then verify it comes back + through search. Indexing (asking the agent to call record_save) goes + through the LLM, which is inherently probabilistic — a free, rotating + backend (see README.md's "Backends" section) can pick a different model + per request. This test accepts that honestly: it reports how many of the + sampled files were actually indexed rather than demanding all-or-nothing, + and only hard-fails if indexing came back empty or the deterministic + search step (itself a REPL slash command, no LLM) doesn't find anything + that was actually indexed. +""" +import difflib +import os +import re +import shutil +import sqlite3 +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from tai_test import TestCase, TestAssertionFailed, main_cli # noqa: E402 + +ROOT = Path(__file__).resolve().parent.parent +RESEARCH_DIR = ROOT / "research" +REPL_PROMPT_RE = re.compile(r"tai\[[^\]]*\]\s*[›>]") +SAMPLE_SIZE = int(os.environ.get("RAG_TEST_SAMPLE_SIZE", "5")) +SOURCE_URL_RE = re.compile(r"\|\s*Source URL\s*\|\s*(\S+)\s*\|") + + +def pick_sample_files(limit=SAMPLE_SIZE): + # Only source dossiers/citation cards: each has exactly one "Source URL" + # metadata field, which is what gives us a per-file, globally-unique + # fingerprint. Conclusions cite several URLs each, so they don't fit + # this scheme and are excluded. + files = sorted( + p for p in RESEARCH_DIR.rglob("*.md") + if p.name not in ("README.md", "INDEX.md") and "conclusions" not in p.parts + ) + return files[:limit] + + +class InteractiveReplSmokeTest(TestCase): + """Deterministic: pure REPL slash commands, no LLM call, proves the pty transport.""" + + name = "repl-pty-smoke" + mode = "pty" + timeout = 30.0 + + async def setup(self): + self.tmp_home = tempfile.mkdtemp(prefix="tai-test-home-") + self.env_overrides = {"TAI_HOME": self.tmp_home, "TAI_PASSPHRASE": ""} + + async def run(self): + proc = await self.spawn(extra_args=["--profile", "smoketest"]) + await proc.wait_for_pattern(REPL_PROMPT_RE, timeout=15.0) + await proc.send_line("/skills") + skills_reply = await proc.wait_for_pattern(REPL_PROMPT_RE, timeout=15.0) + await proc.send_line("/search nonexistent-term-xyz") + search_reply = await proc.wait_for_pattern(REPL_PROMPT_RE, timeout=15.0) + await proc.send_line("/quit") + await proc.wait(timeout=10.0) + return {"skills_reply": skills_reply, "search_reply": search_reply} + + async def verify(self, result): + if "skills" not in result["skills_reply"] and "no skills" not in result["skills_reply"]: + raise TestAssertionFailed("/skills produced no recognizable output: %r" % result["skills_reply"][:300]) + if "no matches" not in result["search_reply"] and "error" in result["search_reply"].lower(): + raise TestAssertionFailed("/search errored instead of reporting no matches: %r" % result["search_reply"][:300]) + + async def teardown(self): + shutil.rmtree(self.tmp_home, ignore_errors=True) + + +class RagIndexAndSearchTest(TestCase): + """The real ask, proven with per-file traceability, not just 'something came back'. + + For each sampled file this test: + 1. extracts that file's Source URL as a fingerprint, and confirms it's + not shared by any other file in the sample (a non-unique fingerprint + would make a search hit ambiguous as "proof"); + 2. asks the agent to record_save it, exactly as before; + 3. reads the saved record straight out of the vault DB and diffs it + against what was actually sent (not the whole file — what we asked + the model to save verbatim), catching silent paraphrasing/truncation; + 4. confirms the fingerprint survived chunking, scoped to that record's + own chunks specifically (not "some chunk somewhere"); + 5. runs a /search for that one file's fingerprint and requires the + reply to name that exact record's mem: id. + + Every step is reported per file (see verify()'s failure message and the + log), so a partial result is auditable rather than a single opaque + pass/fail. + """ + + name = "rag-index-and-search" + mode = "pipe" + timeout = 60.0 * 8 # two LLM round trips per sampled file now (save + per-file search) + + async def setup(self): + self.tmp_home = tempfile.mkdtemp(prefix="tai-test-home-") + self.env_overrides = {"TAI_HOME": self.tmp_home, "TAI_PASSPHRASE": ""} + self.files = pick_sample_files() + if not self.files: + raise TestAssertionFailed("no documents found under research/ to index — did research/README.md's archive get built?") + + async def run(self): + fingerprints = self._extract_and_verify_unique_fingerprints(self.files) + + per_file = [] + for path, fingerprint in fingerprints: + sent_content = path.read_text(encoding="utf-8", errors="replace")[:3000] + title = path.relative_to(RESEARCH_DIR).as_posix() + prompt = ( + "Call the record_save tool right now with kind=\"research\", " + "title=\"%s\", and content set to exactly the text below, verbatim. " + "Do not summarize it, do not explain yourself, just call the tool once.\n\n%s" + % (title, sent_content) + ) + proc = await self.spawn(extra_args=["--yes", "--profile", "ragtest", prompt]) + await proc.drain_until_eof(timeout=90.0) + await proc.wait(timeout=10.0) + + record = self._find_record_by_title(title) + fidelity = self._fidelity_ratio(sent_content, record["content"]) if record else 0.0 + chunk_has_fingerprint = self._record_chunk_contains(record["id"], fingerprint) if record else False + + per_file.append({ + "title": title, + "fingerprint": fingerprint, + "saved": record is not None, + "record_id": record["id"] if record else None, + "fidelity": fidelity, + "chunk_has_fingerprint": chunk_has_fingerprint, + }) + + search_results = [] + for entry in per_file: + if not entry["saved"]: + continue + proc = await self.spawn(extra_args=["--profile", "ragtest"], mode="pty") + await proc.wait_for_pattern(REPL_PROMPT_RE, timeout=15.0) + await proc.send_line("/search %s" % entry["fingerprint"]) + reply = await proc.wait_for_pattern(REPL_PROMPT_RE, timeout=15.0) + await proc.send_line("/quit") + await proc.wait(timeout=10.0) + found_own_record = entry["record_id"] in reply + search_results.append({"title": entry["title"], "reply": reply, "found_own_record": found_own_record}) + entry["search_found_own_record"] = found_own_record + + return {"attempted": len(self.files), "per_file": per_file} + + def _extract_and_verify_unique_fingerprints(self, files): + pairs = [] + seen = {} + for path in files: + text = path.read_text(encoding="utf-8", errors="replace") + match = SOURCE_URL_RE.search(text) + if not match: + raise TestAssertionFailed("%s has no 'Source URL' metadata row — pick_sample_files() should have excluded it" % path) + fingerprint = match.group(1) + if fingerprint in seen: + raise TestAssertionFailed( + "fingerprint %r is not unique: shared by %s and %s — cannot use it as proof of traceability" + % (fingerprint, seen[fingerprint], path) + ) + seen[fingerprint] = path + pairs.append((path, fingerprint)) + return pairs + + def _connect(self): + return sqlite3.connect(str(Path(self.tmp_home) / "memory.db")) + + def _find_record_by_title(self, title): + conn = self._connect() + try: + row = conn.execute("SELECT id, content FROM records WHERE kind = 'research' AND title = ?", (title,)).fetchone() + finally: + conn.close() + return {"id": row[0], "content": row[1]} if row else None + + def _record_chunk_contains(self, record_id, fingerprint): + conn = self._connect() + try: + rows = conn.execute("SELECT text FROM chunks WHERE parent_kind = 'record' AND parent_id = ?", (record_id,)).fetchall() + finally: + conn.close() + return any(fingerprint in row[0] for row in rows) + + @staticmethod + def _fidelity_ratio(sent, saved): + if not saved: + return 0.0 + return difflib.SequenceMatcher(None, sent, saved).ratio() + + async def verify(self, result): + per_file = result["per_file"] + report_lines = [ + "%-60s saved=%-5s fidelity=%.2f chunked=%-5s found_by_search=%-5s" + % (e["title"], e["saved"], e["fidelity"], e["chunk_has_fingerprint"], e.get("search_found_own_record")) + for e in per_file + ] + report = "per-file breakdown:\n " + "\n ".join(report_lines) + + saved_count = sum(1 for e in per_file if e["saved"]) + if saved_count == 0: + raise TestAssertionFailed( + "indexed 0 of %d sampled files (checked the vault's records table directly) — this may be " + "free-backend flakiness, not a RAG bug; re-run before treating as a regression\n%s" + % (result["attempted"], report) + ) + + fully_proven = [ + e for e in per_file + if e["saved"] and e["fidelity"] >= 0.90 and e["chunk_has_fingerprint"] and e.get("search_found_own_record") + ] + if not fully_proven: + raise TestAssertionFailed( + "saved %d/%d file(s), but none passed the full chain (fidelity>=0.90 AND fingerprint survived " + "chunking AND /search for that file's own fingerprint named that exact record) — no file is " + "provably traceable end-to-end\n%s" % (saved_count, result["attempted"], report) + ) + + low_fidelity = [e for e in per_file if e["saved"] and e["fidelity"] < 0.90] + if low_fidelity: + raise TestAssertionFailed( + "%d/%d saved file(s) had content fidelity below 0.90 against what was actually sent — the model " + "paraphrased or truncated instead of saving verbatim: %s\n%s" + % (len(low_fidelity), saved_count, [e["title"] for e in low_fidelity], report) + ) + + not_chunked = [e for e in per_file if e["saved"] and not e["chunk_has_fingerprint"]] + if not_chunked: + raise TestAssertionFailed( + "%d/%d saved file(s) lost their fingerprint during chunking (present in the record but not in any " + "of that record's own chunks): %s\n%s" % (len(not_chunked), saved_count, [e["title"] for e in not_chunked], report) + ) + + not_found = [e for e in per_file if e["saved"] and not e.get("search_found_own_record")] + if not_found: + raise TestAssertionFailed( + "%d/%d saved file(s) did not come back when searching for their own unique fingerprint: %s\n%s" + % (len(not_found), saved_count, [e["title"] for e in not_found], report) + ) + + print(report) + + async def teardown(self): + shutil.rmtree(self.tmp_home, ignore_errors=True) + + +ALL_CASES = [InteractiveReplSmokeTest(), RagIndexAndSearchTest()] + +if __name__ == "__main__": + sys.exit(main_cli(ALL_CASES, default_log_dir=str(Path(__file__).resolve().parent / "logs")))