bantamkit 0.35.4__tar.gz → 0.35.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bantamkit-0.35.4 → bantamkit-0.35.6}/PKG-INFO +3 -3
- {bantamkit-0.35.4 → bantamkit-0.35.6}/README.md +2 -2
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/memory_recall.json +3 -2
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/__init__.py +1 -1
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/hookadapter.py +125 -6
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/memory/component.py +114 -3
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/memory/store.py +34 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/served-tool-surface.json +3 -2
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_hookadapter.py +379 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_memory.py +27 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_memory_component.py +171 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/.gitignore +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/contracts/default.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/manifest.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/HISTORY.md +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/README.md +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/docs/architecture.md +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/docs/runbook.md +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/issues/142-settlement-timeout.md +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/patches/0009-retry-budget.patch +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/src/ledger/__init__.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/src/ledger/config.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/src/ledger/errors.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/src/ledger/posting.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/src/ledger/registry.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/src/ledger/report.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/src/ledger/retry.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/src/ledger/settle.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/src/ledger/validate.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/tests/test_posting.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/repo/tests/test_settle.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/tasks/dt-error-contract.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/tasks/dt-handler-map.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/tasks/dt-patch-before-after.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/tasks/dt-retry-attempts.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/tasks/dt-settlement-config.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/tasks/dt-symbol-home.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/tasks/dt-trace-blame.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/devteam/tasks/dt-unread-key.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/document/tasks/doc-large-in-137.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/document/tasks/doc-large-in-359.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/document/tasks/doc-large-in-372.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/document/tasks/doc-large-out-11764.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/document/tasks/doc-large-out-4137.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/document/tasks/doc-large-out-8022.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/document/tasks/doc-small-137.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/document/tasks/doc-small-261.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/document/tasks/doc-small-388.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/fixtures/.gitkeep +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/fixtures/catalog.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/perturbations/task-completion.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/.gitkeep +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/extract-contact.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/extract-invoice.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/extract-order.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/extract-schedule.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/extract-versions.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/nav-prod-port.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/nav-release-bundle.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/recall-audit-retention.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/recall-cache-ttl.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/recall-db-port.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/recall-deploy.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/recall-env-endpoint.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/recall-oncall-rotation.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/recall-oncall.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/recall-org-quota.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/recall-owner.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/shop-basket-total.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/shop-cheapest.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/shop-compare.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/shop-gadget-value.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/shop-stock-total.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/evals/tasks/shop-total.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/pricing/default.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/profiles/default.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/profiles/patient.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/rubrics/.gitkeep +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/rubrics/code-quality.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/rubrics/grounded-completion.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/rubrics/task-completion.yaml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/schemas/shiftwork-checkpoint.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/skills/.gitkeep +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/skills/file-graph.md +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/skills/memory.md +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/.gitkeep +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/bantamkit_read.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/bantamkit_status.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/build_identity.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/document_list.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/document_read.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/file_graph.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/memory_compact.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/memory_dream.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/memory_save.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/repo_map.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/shiftwork_clock_in.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/shiftwork_clock_out.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/shiftwork_plan.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/shiftwork_status.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/skill_audit.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/token_ledger.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/validate_json.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/_assets/tools/work_plan.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/hatch_build.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/pyproject.toml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/agent.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/assets.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/budget.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/client.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/contract.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/criticreplay.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/critique.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/docmanifest.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/docread.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/evalrun.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/eventlog.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/filegraph.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/hostinstall.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/loopguard.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/mcpreport.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/mcpserver.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/memory/__init__.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/memory/__main__.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/memory/divergence.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/memory/dream.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/memory/layers.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/pdfread.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/pricing.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/profile.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/repomap.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/selfupdate.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/shiftwork.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/skillaudit.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/statusline.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/structured.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/textutil.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/tokenledger.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/updatecheck.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/src/bantamkit/workplan.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/cli_exit_status_probe.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/conftest.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/bad-crc.docx +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/charref-4301-digits.html +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/charset-table.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/compression-method-9.docx +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/corrupt-deflate.docx +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/encrypted-member.docx +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/encrypted-mimetype.odt +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/eszett-cell-ref.xlsx +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/internal-dtd-entity.docx +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/rfc2231-charset.eml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/rfc822-nested-twice.eml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/unicode-digit-shared-string.xlsx +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/docread/x-uuencode.eml +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/f8404ab-perturbation-baseline.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/data/platform-assumption-baseline.json +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/docread_fixtures.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/perturbation_baseline_harness.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/rbp16_effect_probe.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/rbp18_payload_probe.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_adapter.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_agent.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_amendguard.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_bantamkit_gitignore.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_bantamkit_read_tool.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_budget.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_build_identity.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_client.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_compaction_corpus_survey.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_conformance.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_conformance_harness_resilience.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_conformance_suite_table_gate.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_contract_fanout.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_criticreplay.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_critique.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_doc_commands_gate.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_docread.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_docread_ceilings.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_document_manifest_parity.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_document_setup.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_document_tasks.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_document_tools.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_encoding_gate.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_evalrun.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_eventlog.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_field_program_gates.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_field_programs.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_filegraph.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_hostinstall.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_hostinstall_hooks.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_install_shape.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_ladder_statistics.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_launcher_which.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_layers.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_loopguard.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_mcp_endpoint.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_mcpdrift.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_mcpreport.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_mcpserver.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_memory_compact_tool.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_memory_divergence.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_memory_dream.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_memory_layers.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_memory_store_tripwire.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_mutmatrix.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_nativeexport.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_newline_gate.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_packaging.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_pdfread.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_pinharness_ledger.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_platform_assumption_gate.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_pricing.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_readme_separation.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_repo_map_tool.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_repomap.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_selfupdate.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_served_tool_count_records.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_shiftwork.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_skillaudit.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_status_surface.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_statusline.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_structured.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_thread_exception_gate.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_tokenledger.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_tool_manifest.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_updatecheck.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_version_agreement.py +0 -0
- {bantamkit-0.35.4 → bantamkit-0.35.6}/tests/test_workplan.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bantamkit
|
|
3
|
-
Version: 0.35.
|
|
3
|
+
Version: 0.35.6
|
|
4
4
|
Summary: Memory MCP server for Claude Code, Cursor, VS Code Copilot and Claude Desktop, plus a Python library that lifts small-model agents. Install once, run offline.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Ink01101011/bantamkit
|
|
6
6
|
Project-URL: Repository, https://github.com/Ink01101011/bantamkit
|
|
@@ -103,9 +103,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
|
|
|
103
103
|
one platform only), copy `wheels/` across, and install from it:
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
|
-
python -m pip download "bantamkit[mcp]==0.35.
|
|
106
|
+
python -m pip download "bantamkit[mcp]==0.35.6" -d wheels
|
|
107
107
|
python -m venv <env>
|
|
108
|
-
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.
|
|
108
|
+
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.6"
|
|
109
109
|
<env>/bin/bantamkit-mcp --install cursor
|
|
110
110
|
```
|
|
111
111
|
|
|
@@ -74,9 +74,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
|
|
|
74
74
|
one platform only), copy `wheels/` across, and install from it:
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
|
-
python -m pip download "bantamkit[mcp]==0.35.
|
|
77
|
+
python -m pip download "bantamkit[mcp]==0.35.6" -d wheels
|
|
78
78
|
python -m venv <env>
|
|
79
|
-
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.
|
|
79
|
+
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.6"
|
|
80
80
|
<env>/bin/bantamkit-mcp --install cursor
|
|
81
81
|
```
|
|
82
82
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory_recall",
|
|
3
|
-
"description": "Search persistent memory. Call BEFORE starting a task that resembles past work. Returns
|
|
3
|
+
"description": "Search persistent memory. Call BEFORE starting a task that resembles past work. Returns the top word matches, never fewer than the store default of 3 when that many match; a query that is exactly a fact's name returns that fact alone.",
|
|
4
4
|
"surfaces": [
|
|
5
5
|
"agent",
|
|
6
6
|
"mcp"
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"k": {
|
|
19
19
|
"type": "integer",
|
|
20
20
|
"minimum": 1,
|
|
21
|
-
"maximum": 5
|
|
21
|
+
"maximum": 5,
|
|
22
|
+
"description": "Facts wanted. Raised to the store default of 3 when smaller; never lowered below it."
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
},
|
|
@@ -29,4 +29,4 @@ from bantamkit.structured import StructuredOutputError, extract_json, structured
|
|
|
29
29
|
# file as its dynamic version source, so the wheel's metadata and the string the MCP
|
|
30
30
|
# server advertises are the same committed bytes, and neither is a function of when
|
|
31
31
|
# someone last ran `pip`.
|
|
32
|
-
__version__ = "0.35.
|
|
32
|
+
__version__ = "0.35.6"
|
|
@@ -375,6 +375,25 @@ def _write_ledger(run: HookRun, session_id: Any, ledger: dict[str, Any]) -> None
|
|
|
375
375
|
fh.write(_dumps(ledger))
|
|
376
376
|
|
|
377
377
|
|
|
378
|
+
def _injected_seen(ledger: dict[str, Any], transcript: str) -> dict[str, Any]:
|
|
379
|
+
"""The fact names this CONTEXT has already been shown, `name -> ISO stamp` (job64, J64-2).
|
|
380
|
+
|
|
381
|
+
Lives under `ledger["injected"][<transcript>]`, beside `reads`, so it inherits the
|
|
382
|
+
ledger's reset points: `PostCompact` and `SessionStart source=compact` unlink the file,
|
|
383
|
+
which is exactly when the model's window has been rebuilt and a name it was shown is
|
|
384
|
+
gone again. The context key is the same one `reads` uses -- `transcript_path` when the
|
|
385
|
+
host sends it, `session_id` otherwise -- because a subagent shares the parent's
|
|
386
|
+
`session_id` (J64-0, Q3 step 11) and therefore this FILE, while its window has never
|
|
387
|
+
held what the parent was shown. A missing or malformed entry is "nothing seen", never
|
|
388
|
+
an exception, for the reason `_read_ledger` gives.
|
|
389
|
+
"""
|
|
390
|
+
everyone = ledger.get("injected")
|
|
391
|
+
if not isinstance(everyone, dict):
|
|
392
|
+
return {}
|
|
393
|
+
mine = everyone.get(transcript)
|
|
394
|
+
return mine if isinstance(mine, dict) else {}
|
|
395
|
+
|
|
396
|
+
|
|
378
397
|
def _write_json_safe(file: str, value: Any) -> None:
|
|
379
398
|
"""Best-effort marker write. A marker that cannot be written costs a repeated dream."""
|
|
380
399
|
try:
|
|
@@ -909,6 +928,19 @@ def _session_start(run: HookRun, payload: dict[str, Any]) -> None:
|
|
|
909
928
|
os.unlink(_ledger_path(run, payload.get("session_id")))
|
|
910
929
|
except OSError:
|
|
911
930
|
pass
|
|
931
|
+
elif payload.get("source") == "clear":
|
|
932
|
+
# `/clear` empties the window the way a compaction does, and the host MAY keep the
|
|
933
|
+
# `session_id` across it (the real log cannot settle this: 4 kept, 12 changed, 15
|
|
934
|
+
# unknown over 31 clears, with concurrent sessions confounding every count --
|
|
935
|
+
# `.shiftwork/notes-job64/J64-2.md`). So the names this session was shown are
|
|
936
|
+
# forgotten here either way; if the id changed there is no ledger and this is a
|
|
937
|
+
# no-op. Only the seen-set: whether a re-read after `/clear` should still be
|
|
938
|
+
# refused is the read ledger's own question, and `resume` restores the window, so
|
|
939
|
+
# it resets nothing. No ledger is created when none exists (job64, J64-2).
|
|
940
|
+
ledger = _read_ledger(run, payload.get("session_id"))
|
|
941
|
+
if "injected" in ledger:
|
|
942
|
+
del ledger["injected"]
|
|
943
|
+
_write_ledger(run, payload.get("session_id"), ledger)
|
|
912
944
|
# `profileFacts` keeps its old meaning -- files in the store -- so older records stay
|
|
913
945
|
# comparable; `profileInjected` / `profileDropped` are what the block carried and did
|
|
914
946
|
# not, and `dropRule` is the order the drop followed. The project trio appears only when
|
|
@@ -1027,7 +1059,12 @@ def _user_prompt_submit(run: HookRun, payload: dict[str, Any]) -> None:
|
|
|
1027
1059
|
_log(run, {"event": "UserPromptSubmit", "action": "skip", "reason": "short-or-command"})
|
|
1028
1060
|
return
|
|
1029
1061
|
m = Memory.layered(str(payload.get("cwd") or os.getcwd()))
|
|
1030
|
-
|
|
1062
|
+
# `stamp=False` (job64, J64-1): an injection is the HOOK reading the store, not the model
|
|
1063
|
+
# asking for a fact, so it must not date `last_recalled`. Before this it did, on every
|
|
1064
|
+
# prompt, for up to three files -- and the rules keyed on that date (compaction's
|
|
1065
|
+
# stalest-first, the SessionStart drop rule, the Stop dream's store fingerprint) were
|
|
1066
|
+
# reading this arm's traffic. An explicit `memory_recall` still stamps.
|
|
1067
|
+
o = m.recall_outcome(prompt, 3, stamp=False)
|
|
1031
1068
|
if o.status != "answered":
|
|
1032
1069
|
_log(
|
|
1033
1070
|
run,
|
|
@@ -1046,7 +1083,42 @@ def _user_prompt_submit(run: HookRun, payload: dict[str, Any]) -> None:
|
|
|
1046
1083
|
if not heads:
|
|
1047
1084
|
_log(run, {"event": "UserPromptSubmit", "action": "none", "reason": "no-headers"})
|
|
1048
1085
|
return
|
|
1049
|
-
|
|
1086
|
+
# WHAT THIS CONTEXT HAS ALREADY BEEN SHOWN IS NOT SHOWN AGAIN (job64, J64-2). Measured
|
|
1087
|
+
# 2026-09-25: 333 of 598 injections in a week repeated a name injected earlier in the
|
|
1088
|
+
# same session. The top 3 are still asked for (RB-P1's floor stays) and the seen ones are
|
|
1089
|
+
# DROPPED, not refilled from rank 4 onward: what leaves is always a subset of what the
|
|
1090
|
+
# un-deduped arm would have sent, so its precision can only rise, and a repeated prompt
|
|
1091
|
+
# says nothing rather than walking down the ranking on every repeat. The seen-set is the
|
|
1092
|
+
# session ledger's, so it is forgotten exactly when the window is (compaction, `/clear`).
|
|
1093
|
+
transcript = _js_str(payload.get("transcript_path") or payload.get("session_id") or "")
|
|
1094
|
+
ledger = _read_ledger(run, payload.get("session_id"))
|
|
1095
|
+
seen = _injected_seen(ledger, transcript)
|
|
1096
|
+
fresh: list[str] = []
|
|
1097
|
+
suppressed: list[str] = []
|
|
1098
|
+
for line in heads:
|
|
1099
|
+
matched = RECALL_HEADER.match(line)
|
|
1100
|
+
name = matched.group(2) if matched else ""
|
|
1101
|
+
if name in seen:
|
|
1102
|
+
suppressed.append(name)
|
|
1103
|
+
else:
|
|
1104
|
+
fresh.append(line)
|
|
1105
|
+
if not fresh:
|
|
1106
|
+
# Everything picked was already in the window. Nothing is emitted, and the record
|
|
1107
|
+
# says WHY with the names, so an audit can count what dedupe withheld.
|
|
1108
|
+
_log(
|
|
1109
|
+
run,
|
|
1110
|
+
{
|
|
1111
|
+
"event": "UserPromptSubmit",
|
|
1112
|
+
"action": "suppress",
|
|
1113
|
+
"hits": len(heads),
|
|
1114
|
+
"source": o.source,
|
|
1115
|
+
"session": payload.get("session_id"),
|
|
1116
|
+
"prompt": _prompt_fingerprint(prompt),
|
|
1117
|
+
"suppressed": suppressed,
|
|
1118
|
+
},
|
|
1119
|
+
)
|
|
1120
|
+
return
|
|
1121
|
+
joined = "\n".join(fresh)
|
|
1050
1122
|
ctx = _cap_lines(
|
|
1051
1123
|
"[bantamkit recall — memories that match this prompt; call "
|
|
1052
1124
|
'mcp__bantamkit__memory_recall with {"query":"<name>"} for the body]\n'
|
|
@@ -1058,13 +1130,25 @@ def _user_prompt_submit(run: HookRun, payload: dict[str, Any]) -> None:
|
|
|
1058
1130
|
# asks "was an INJECTED name later used", a question a name the model never saw would
|
|
1059
1131
|
# poison. `hits` keeps its old meaning (headers picked, pre-cap) so the 487 records
|
|
1060
1132
|
# written before this change stay comparable; `dropped` is the difference the old shape
|
|
1061
|
-
# could not show
|
|
1133
|
+
# could not show, counted over the unseen headers only, so that
|
|
1134
|
+
# `hits == injected + dropped + suppressed` on every record.
|
|
1062
1135
|
query_tokens = tokens(prompt)
|
|
1063
1136
|
injected = [
|
|
1064
1137
|
scored
|
|
1065
1138
|
for scored in (_score_header(line, query_tokens) for line in ctx.split("\n"))
|
|
1066
1139
|
if scored is not None
|
|
1067
1140
|
]
|
|
1141
|
+
# Remembered AFTER the cap, off `injected`: a name the cap cut never reached the window
|
|
1142
|
+
# and must be eligible next time.
|
|
1143
|
+
at = _stamp()
|
|
1144
|
+
for scored in injected:
|
|
1145
|
+
seen[scored["name"]] = at
|
|
1146
|
+
everyone = ledger.get("injected")
|
|
1147
|
+
if not isinstance(everyone, dict):
|
|
1148
|
+
everyone = {}
|
|
1149
|
+
ledger["injected"] = everyone
|
|
1150
|
+
everyone[transcript] = seen
|
|
1151
|
+
_write_ledger(run, payload.get("session_id"), ledger)
|
|
1068
1152
|
_log(
|
|
1069
1153
|
run,
|
|
1070
1154
|
{
|
|
@@ -1079,7 +1163,8 @@ def _user_prompt_submit(run: HookRun, payload: dict[str, Any]) -> None:
|
|
|
1079
1163
|
"session": payload.get("session_id"),
|
|
1080
1164
|
"prompt": _prompt_fingerprint(prompt),
|
|
1081
1165
|
"injected": injected,
|
|
1082
|
-
"dropped": len(
|
|
1166
|
+
"dropped": len(fresh) - len(injected),
|
|
1167
|
+
"suppressed": suppressed,
|
|
1083
1168
|
},
|
|
1084
1169
|
)
|
|
1085
1170
|
_emit({"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": ctx}})
|
|
@@ -1773,7 +1858,40 @@ def _same_path(a: str, b: str) -> bool:
|
|
|
1773
1858
|
return _real_dir(a) == _real_dir(b)
|
|
1774
1859
|
|
|
1775
1860
|
|
|
1861
|
+
def _fact_content_digest(data: bytes) -> str:
|
|
1862
|
+
"""sha256 of a fact file with the frontmatter's `last_recalled:` line left out.
|
|
1863
|
+
|
|
1864
|
+
THE DATE IS NOT CONTENT. Every recall rewrites that one line (and the file's mtime), so a
|
|
1865
|
+
fingerprint that saw it re-armed the dream preview on every `memory_recall` -- measured
|
|
1866
|
+
2026-09-25 (job64): 103 of 236 previews in a week reported the identical `wouldMerge 14 /
|
|
1867
|
+
wouldConsume 14`. Name, description, type, created, links and the body all stay in.
|
|
1868
|
+
|
|
1869
|
+
Byte-exact and identical in both runtimes (`factContentDigest` in
|
|
1870
|
+
`runtime-ts/src/hookadapter.ts`): the split is on `\\n`, a trailing `\\r` is ignored only
|
|
1871
|
+
for the two comparisons (a Windows-written file keeps its bytes in the hash), and only the
|
|
1872
|
+
lines between the first two `---` fences are frontmatter -- a body line that happens to
|
|
1873
|
+
start with `last_recalled:` is still content.
|
|
1874
|
+
"""
|
|
1875
|
+
h = hashlib.sha256()
|
|
1876
|
+
fences = 0
|
|
1877
|
+
for line in data.split(b"\n"):
|
|
1878
|
+
bare = line[:-1] if line.endswith(b"\r") else line
|
|
1879
|
+
if bare == b"---" and fences < 2:
|
|
1880
|
+
fences += 1
|
|
1881
|
+
elif fences == 1 and bare.startswith(b"last_recalled:"):
|
|
1882
|
+
continue
|
|
1883
|
+
h.update(line)
|
|
1884
|
+
h.update(b"\n")
|
|
1885
|
+
return h.hexdigest()
|
|
1886
|
+
|
|
1887
|
+
|
|
1776
1888
|
def _store_fingerprint(roots: list[str]) -> str:
|
|
1889
|
+
"""One hash over both layers: each root's name, then every `facts/*.md` as name + content
|
|
1890
|
+
digest. Adding, removing or renaming a fact moves it; editing any field but `last_recalled`
|
|
1891
|
+
moves it; a recall (which rewrites only that line and the mtime) does not. Neither size nor
|
|
1892
|
+
mtime is in it any more: J64-0 measured that a same-day re-stamp moves the mtime ALONE, so
|
|
1893
|
+
no stat field could be kept as a content signal (`.shiftwork/notes-job64/J64-0.md`, Q4).
|
|
1894
|
+
"""
|
|
1777
1895
|
h = hashlib.sha256()
|
|
1778
1896
|
for root in roots:
|
|
1779
1897
|
h.update(f"\u0000{root}\u0000".encode())
|
|
@@ -1785,10 +1903,11 @@ def _store_fingerprint(roots: list[str]) -> str:
|
|
|
1785
1903
|
names = [] # a layer with no facts/ contributes its name and nothing else
|
|
1786
1904
|
for n in names:
|
|
1787
1905
|
try:
|
|
1788
|
-
|
|
1906
|
+
with open(os.path.join(root, "facts", n), "rb") as f:
|
|
1907
|
+
data = f.read()
|
|
1789
1908
|
except OSError:
|
|
1790
1909
|
continue
|
|
1791
|
-
h.update(f"{n}\u0000{
|
|
1910
|
+
h.update(f"{n}\u0000{_fact_content_digest(data)}\u0000".encode())
|
|
1792
1911
|
return h.hexdigest()
|
|
1793
1912
|
|
|
1794
1913
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
from collections.abc import Iterator
|
|
7
8
|
from contextlib import ExitStack, contextmanager
|
|
8
9
|
from dataclasses import dataclass
|
|
@@ -23,6 +24,7 @@ from bantamkit.memory.layers import (
|
|
|
23
24
|
)
|
|
24
25
|
from bantamkit.memory.store import (
|
|
25
26
|
DEFAULT_INDEX_BUDGET,
|
|
27
|
+
MIN_RATIO_RANGE,
|
|
26
28
|
RECALL_MIN_SCORE_RATIO,
|
|
27
29
|
Fact,
|
|
28
30
|
MemoryBudgetExceeded,
|
|
@@ -69,6 +71,12 @@ class RecallOutcome:
|
|
|
69
71
|
`project`, `extra` or `profile` — deliberately not the layer's full label, because
|
|
70
72
|
`extra:<name>` carries a directory name off the operator's disk and the log this
|
|
71
73
|
feeds is metadata-only.
|
|
74
|
+
|
|
75
|
+
`lookup` (job64, J64-4) is what the exact-name walk decided, again read off the branch
|
|
76
|
+
and not off the reply: `hit` when the query WAS a fact's name and that fact alone is the
|
|
77
|
+
reply, `miss` when the query was shaped like a name, no layer held it, and the reply
|
|
78
|
+
leads with the line that says so, `None` for every other query — the walk that has
|
|
79
|
+
always run, unchanged.
|
|
72
80
|
"""
|
|
73
81
|
|
|
74
82
|
reply: str
|
|
@@ -80,6 +88,7 @@ class RecallOutcome:
|
|
|
80
88
|
candidates: int
|
|
81
89
|
source: str | None = None
|
|
82
90
|
unreadable: int = 0
|
|
91
|
+
lookup: str | None = None
|
|
83
92
|
|
|
84
93
|
|
|
85
94
|
def normalize_name(name: str) -> str:
|
|
@@ -96,6 +105,26 @@ def normalize_name(name: str) -> str:
|
|
|
96
105
|
return name.strip().lower().replace("_", "-").replace(" ", "-")
|
|
97
106
|
|
|
98
107
|
|
|
108
|
+
# THE NAME SHAPE (job64, J64-4), spelled once here and once in the port: a recall query,
|
|
109
|
+
# after `str.strip()`, that the store's own `NAME_RE` would accept as a fact name AND that
|
|
110
|
+
# carries a hyphen. `fullmatch` rather than `NAME_RE.match`, because Python's `$` matches
|
|
111
|
+
# before a trailing newline and the strip has already removed one; the port's `pyStrip` +
|
|
112
|
+
# anchored test is the same set. A query in this shape is looked up BY NAME in every layer
|
|
113
|
+
# before any scoring happens, and a miss on it is said out loud. The hyphen is the whole
|
|
114
|
+
# distinction: `deploy` is a word anyone might search, and stays the word search it has
|
|
115
|
+
# always been even when a fact happens to be named `deploy` (the layered dedupe test pins
|
|
116
|
+
# that); `deploy-command` is a name somebody typed from an index.
|
|
117
|
+
_NAME_SHAPE = re.compile(r"[a-z0-9][a-z0-9-]*")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _is_name_shaped(query: str) -> bool:
|
|
121
|
+
return "-" in query and _NAME_SHAPE.fullmatch(query) is not None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _miss_line(name: str) -> str:
|
|
125
|
+
return f"no fact named '{name}' in any layer bound here; matching by words instead:"
|
|
126
|
+
|
|
127
|
+
|
|
99
128
|
@dataclass(frozen=True)
|
|
100
129
|
class CompactOutcome:
|
|
101
130
|
"""What `compact` DID, beside the sentence it says about it.
|
|
@@ -371,6 +400,8 @@ class Memory:
|
|
|
371
400
|
query: str,
|
|
372
401
|
k: int | None = None,
|
|
373
402
|
min_ratio: float = RECALL_MIN_SCORE_RATIO,
|
|
403
|
+
*,
|
|
404
|
+
stamp: bool = True,
|
|
374
405
|
) -> RecallOutcome:
|
|
375
406
|
"""`recall`, carrying the walk it performed as numbers rather than as prose.
|
|
376
407
|
|
|
@@ -398,8 +429,45 @@ class Memory:
|
|
|
398
429
|
store, so it surfaces as the error it is rather than as an `unreadable` count —
|
|
399
430
|
the read-only-layer `except` below would otherwise file a caller's bad argument as
|
|
400
431
|
a corrupt grant.
|
|
432
|
+
|
|
433
|
+
`stamp` (job64, J64-1) is whether a hit in the WRITABLE layer gets its
|
|
434
|
+
`last_recalled` dated. It defaults to True, which is what every explicit recall —
|
|
435
|
+
the `memory_recall` tool, the memory CLI, `Memory.recall` — has always done, and
|
|
436
|
+
those callers do not pass it. The one caller that passes False is the hook's
|
|
437
|
+
`UserPromptSubmit` arm: an automatic injection is not the model asking for a fact,
|
|
438
|
+
and dating it as one was measured (2026-09-25) to stamp 25 of 42 facts in one
|
|
439
|
+
store as "recalled today", so every rule keyed on `last_recalled` — compaction's
|
|
440
|
+
stalest-first, SessionStart's drop rule — was reading injection traffic instead of
|
|
441
|
+
model demand. Read-only layers are never stamped whatever this says.
|
|
442
|
+
|
|
443
|
+
THE EXACT-NAME WALK (job64, J64-4) runs first, and only for a query in the name
|
|
444
|
+
shape (`_is_name_shaped`: `strip()`ped, `NAME_RE`-valid, and carrying a hyphen —
|
|
445
|
+
a bare word is a word search even when a fact is named by it). Measured
|
|
446
|
+
2026-09-25 on the operator's own
|
|
447
|
+
stores: 4 of 28 `memory_recall` calls whose query was exactly a fact name came back
|
|
448
|
+
with OTHER facts. One of those names existed in the PROFILE layer and was never
|
|
449
|
+
reached, because the project layer's fuzzy hits filled the budget and the loop below
|
|
450
|
+
breaks before the next layer is read — a name lookup is a request for one known fact,
|
|
451
|
+
and the `k`-floor (RB-P1, ruled to stay 2026-09-25) has nothing to say about it. So:
|
|
452
|
+
every layer is asked, in precedence order, `store.lookup(name)`; the first that
|
|
453
|
+
holds it answers ALONE, stamped only if it is the writable layer, and no other layer
|
|
454
|
+
is read after it. If no layer holds it, the reply leads with `_miss_line` and the
|
|
455
|
+
ordinary walk follows, byte for byte what it would have said alone. Every query
|
|
456
|
+
outside the shape never enters this block. The range
|
|
457
|
+
check on `min_ratio` runs BEFORE the walk for the same reason it runs before any
|
|
458
|
+
file is read in `MemoryStore.recall`: a caller's bad argument must surface as the
|
|
459
|
+
error it is, not be swallowed by an exact hit.
|
|
401
460
|
"""
|
|
461
|
+
if not 0.0 <= min_ratio <= 1.0:
|
|
462
|
+
raise MemoryValidationError(MIN_RATIO_RANGE)
|
|
402
463
|
budget = self.k if k is None else max(k, self.k)
|
|
464
|
+
name = query.strip()
|
|
465
|
+
lookup: str | None = None
|
|
466
|
+
if _is_name_shaped(name):
|
|
467
|
+
named = self._lookup_by_name(name, budget, stamp)
|
|
468
|
+
if named is not None:
|
|
469
|
+
return named
|
|
470
|
+
lookup = "miss"
|
|
403
471
|
picked: list[tuple[str, Fact]] = []
|
|
404
472
|
seen: set[str] = set()
|
|
405
473
|
reached = 0
|
|
@@ -410,7 +478,7 @@ class Memory:
|
|
|
410
478
|
break # budget spent: later (read-only) layers are never even read
|
|
411
479
|
reached += 1
|
|
412
480
|
try:
|
|
413
|
-
facts = store.recall(query, budget, stamp=writable, min_ratio=min_ratio)
|
|
481
|
+
facts = store.recall(query, budget, stamp=writable and stamp, min_ratio=min_ratio)
|
|
414
482
|
except (BantamError, OSError, UnicodeDecodeError):
|
|
415
483
|
if writable:
|
|
416
484
|
raise # the project layer failing is a real error, as in v1
|
|
@@ -434,17 +502,60 @@ class Memory:
|
|
|
434
502
|
"returned": len(picked),
|
|
435
503
|
"candidates": candidates,
|
|
436
504
|
"unreadable": unreadable,
|
|
505
|
+
"lookup": lookup,
|
|
437
506
|
}
|
|
507
|
+
lead = f"{_miss_line(name)}\n\n" if lookup == "miss" else ""
|
|
438
508
|
if not picked:
|
|
439
509
|
status, reply = self._nothing_to_report()
|
|
440
|
-
return RecallOutcome(reply=reply, status=status, **counts)
|
|
510
|
+
return RecallOutcome(reply=lead + reply, status=status, **counts)
|
|
441
511
|
return RecallOutcome(
|
|
442
|
-
reply="\n\n".join(self._format(label, fact) for label, fact in picked),
|
|
512
|
+
reply=lead + "\n\n".join(self._format(label, fact) for label, fact in picked),
|
|
443
513
|
status="answered",
|
|
444
514
|
source=picked[0][0].split(":", 1)[0],
|
|
445
515
|
**counts,
|
|
446
516
|
)
|
|
447
517
|
|
|
518
|
+
def _lookup_by_name(self, name: str, budget: int, stamp: bool) -> RecallOutcome | None:
|
|
519
|
+
"""The exact-name walk: the first layer holding a fact named `name` answers alone.
|
|
520
|
+
|
|
521
|
+
The counters mean what they mean in `recall_outcome`, over the layers this walk
|
|
522
|
+
READ — `reached` stops at the layer that answered, `candidates` counts the facts in
|
|
523
|
+
the layers up to and including it, and a corrupt read-only layer raises
|
|
524
|
+
`unreadable` and is skipped exactly as the scoring walk skips it. `None` means no
|
|
525
|
+
layer holds the name, and the caller falls through to the scoring walk with its
|
|
526
|
+
own, fresh counters: the numbers a miss logs are the numbers the ordinary walk logs.
|
|
527
|
+
"""
|
|
528
|
+
reached = 0
|
|
529
|
+
unreadable = 0
|
|
530
|
+
candidates = 0
|
|
531
|
+
for label, store, writable in self._layers:
|
|
532
|
+
reached += 1
|
|
533
|
+
try:
|
|
534
|
+
fact = store.lookup(name, stamp=writable and stamp)
|
|
535
|
+
except (BantamError, OSError, UnicodeDecodeError):
|
|
536
|
+
if writable:
|
|
537
|
+
raise # the project layer failing is a real error, as in the scoring walk
|
|
538
|
+
unreadable += 1
|
|
539
|
+
continue
|
|
540
|
+
try:
|
|
541
|
+
candidates += count_facts(store.root)
|
|
542
|
+
except OSError:
|
|
543
|
+
pass # it listed a moment ago; a race must not fail the recall for a log field
|
|
544
|
+
if fact is not None:
|
|
545
|
+
return RecallOutcome(
|
|
546
|
+
reply=self._format(label, fact),
|
|
547
|
+
status="answered",
|
|
548
|
+
source=label.split(":", 1)[0],
|
|
549
|
+
budget=budget,
|
|
550
|
+
layers=len(self._layers),
|
|
551
|
+
reached=reached,
|
|
552
|
+
returned=1,
|
|
553
|
+
candidates=candidates,
|
|
554
|
+
unreadable=unreadable,
|
|
555
|
+
lookup="hit",
|
|
556
|
+
)
|
|
557
|
+
return None
|
|
558
|
+
|
|
448
559
|
def index_accounting(self) -> tuple[int | None, int]:
|
|
449
560
|
"""`(index bytes, budget)` for the writable project store; bytes may be `None`.
|
|
450
561
|
|
|
@@ -75,6 +75,10 @@ RECALL_MIN_SCORE_RATIO = 0.0
|
|
|
75
75
|
# interpolated: Python renders `2.0` as `2.0` and JavaScript renders it as `2`, so a
|
|
76
76
|
# sentence carrying the number would be a divergence manufactured by float formatting.
|
|
77
77
|
_MIN_RATIO_RANGE = "recall min-score ratio must be between 0.0 and 1.0"
|
|
78
|
+
# PUBLIC SINCE J64-4, ON BOTH SIDES, for the same reason `tokens` is: `component.Memory` runs
|
|
79
|
+
# the range check itself now, before an exact-name walk reads a file, and it must raise the
|
|
80
|
+
# store's sentence and not a second spelling of it. The private name is kept as an alias.
|
|
81
|
+
MIN_RATIO_RANGE = _MIN_RATIO_RANGE
|
|
78
82
|
|
|
79
83
|
# The index is loaded into the prompt every session, so this is a context bill, not a
|
|
80
84
|
# disk limit. It was 4096 and that number was never measured against a real store.
|
|
@@ -614,6 +618,36 @@ class MemoryStore:
|
|
|
614
618
|
self._stamp(fact)
|
|
615
619
|
return hits
|
|
616
620
|
|
|
621
|
+
def lookup(self, name: str, stamp: bool = True) -> Fact | None:
|
|
622
|
+
"""The fact whose `name` field is exactly `name`, or `None`; no scoring, no ranking.
|
|
623
|
+
|
|
624
|
+
WHY THIS EXISTS (job64, J64-4): `component.Memory` walks the layers looking for a
|
|
625
|
+
fact the model asked for BY NAME, and `recall` cannot answer that question. It
|
|
626
|
+
scores by token overlap, so the named fact ties with any fact whose name and
|
|
627
|
+
description happen to contain the same words, the tie is broken by name, and
|
|
628
|
+
every hit inside `k` is stamped — there is no `k` that returns "the one named X
|
|
629
|
+
and dates only that one". A caller reaching for `_facts`/`_stamp` instead would be
|
|
630
|
+
the private-name crossing the `tokens` docstring above spends a paragraph refusing.
|
|
631
|
+
|
|
632
|
+
The comparison is `==` against the `name` field as `_facts` resolved it, not against
|
|
633
|
+
the file name: a hand-edited `name:` that no longer matches its path is found by
|
|
634
|
+
what it SAYS it is, which is what `recall` and the index line already go by. Two
|
|
635
|
+
files claiming one name answer the first in `_fact_paths` order, the same order
|
|
636
|
+
`recall` breaks a tie in. A non-string `name` (`name: 7`) never equals a string, so
|
|
637
|
+
it is never an exact hit; the port compares with `===` for the same reason.
|
|
638
|
+
|
|
639
|
+
`stamp` dates the hit through `_stamp`, exactly as `recall` dates its hits, snapshot
|
|
640
|
+
semantics included; nothing else on disk is touched, and a miss writes nothing.
|
|
641
|
+
An unreadable store raises out of `_facts` here as it does out of `recall`, and
|
|
642
|
+
`component.Memory` treats the two the same way per layer.
|
|
643
|
+
"""
|
|
644
|
+
for fact in self._snapshot if self._snapshot is not None else self._facts():
|
|
645
|
+
if fact.name == name:
|
|
646
|
+
if stamp:
|
|
647
|
+
self._stamp(fact)
|
|
648
|
+
return fact
|
|
649
|
+
return None
|
|
650
|
+
|
|
617
651
|
def lint(self) -> None:
|
|
618
652
|
"""Every fact parses and carries a valid type, and the index fits its budget.
|
|
619
653
|
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
79
|
"memory_recall": {
|
|
80
|
-
"description": "Search persistent memory. Call BEFORE starting a task that resembles past work. Returns
|
|
80
|
+
"description": "Search persistent memory. Call BEFORE starting a task that resembles past work. Returns the top word matches, never fewer than the store default of 3 when that many match; a query that is exactly a fact's name returns that fact alone.",
|
|
81
81
|
"inputSchema": {
|
|
82
82
|
"properties": {
|
|
83
83
|
"query": {
|
|
@@ -87,7 +87,8 @@
|
|
|
87
87
|
"k": {
|
|
88
88
|
"type": "integer",
|
|
89
89
|
"minimum": 1,
|
|
90
|
-
"maximum": 5
|
|
90
|
+
"maximum": 5,
|
|
91
|
+
"description": "Facts wanted. Raised to the store default of 3 when smaller; never lowered below it."
|
|
91
92
|
}
|
|
92
93
|
},
|
|
93
94
|
"required": [
|