Skip to content

Cross-task knowledge share

CrossTaskKB is the public surface on top of the existing tag-indexed SQLite memory store. It lets one task publish a fact under a tag and another task subscribe on that tag, without writing files into a shared worktree path and hoping the next agent reads them.

The facade is a thin wrapper. It does not introduce new storage. Every fact is persisted as a row in the existing memory table (with type='cross_task') plus an attribution sidecar row that carries the lineage-style triple (producer_task_id, ts_ns, content_hash).

Contract

from bernstein.core.memory.cross_task_kb import CrossTaskKB
from bernstein.core.memory.sqlite_store import SQLiteMemoryStore

store = SQLiteMemoryStore(Path(".sdd/memory/memory.db"))
facade = CrossTaskKB(store, run_id="r-1", producer_task_id="t-7")

# Publish
facade.publish(tag="api-schema", key="users", value="...", scope="run")

# Subscribe (yields newest-first, one fact per key)
for fact in facade.subscribe(tag="api-schema", scope="run"):
    use(fact.value)

Fact attribution

Every fact carries the same triple a lineage entry would:

Field Type Meaning
producer_task_id str Task ID of the publisher.
ts_ns int Wall-clock nanoseconds at publish time.
content_hash str sha256:<hex> over the UTF-8 value bytes.

This mirrors bernstein.core.lineage.recorder.LineageRecorder.record_write, so an auditor can correlate a fact with the lineage entry of the artefact that produced it.

Scope rules

Scope Visibility Use when
run Same run_id only. Short-lived hand-offs between agents in the same orchestration run.
project Any task in the same .sdd/ root. Conventions, decisions, schemas the whole project should see across runs.

Both scopes are single-host. Cross-machine replication is out of scope; the ticket calls it out explicitly as v2.

Conflict resolution

Two tasks publishing under the same (scope, tag, key): last-write-wins. The earlier rows are not deleted, so the audit log retains every version, but subscribe() only returns the newest one per key. A warning is logged into the trace when a prior fact under the same identity is overwritten.

Operator-policy resolution (allow/deny/prompt) is a v2 follow-up.

Lifecycle hook

Each publish fires the kb.fact_published lifecycle event. Hook plugins receive the attribution payload plus tag, key, scope. The hook is best-effort: a misbehaving plugin cannot block the publish path.

CLI

bernstein memory share <key> <value> --tag <tag> --scope run|project
bernstein memory query --tag <tag> --scope run|project [--raw]

query redacts emails, phone numbers, SSNs, and credit card numbers in the output by default. Pass --raw to print stored values verbatim - useful for operator debugging when the redaction is itself the issue.

The CLI reads BERNSTEIN_RUN_ID and BERNSTEIN_TASK_ID from the environment for attribution. Outside an orchestrator-spawned session the producer task ID defaults to manual-cli.

Telemetry

CrossTaskKB.counter.snapshot() returns (publish, subscribe) counts for the current process. The orchestrator reads these into the run summary.

Worked example

A researcher publishes the discovered API schema:

researcher = CrossTaskKB(store, run_id="r-1", producer_task_id="researcher-1")
researcher.publish(
    tag="api-schema",
    key="users",
    value="{name: str, email: str}",
    scope="project",
)

A backend agent in a later task subscribes on the same tag:

backend = CrossTaskKB(store, run_id="r-1", producer_task_id="backend-1")
for fact in backend.subscribe(tag="api-schema", scope="project"):
    print(fact.key, fact.value, "from", fact.producer_task_id)

The backend agent sees users / {name: str, email: str} / from researcher-1.

Out of scope (v1)

  • Vector or embedding similarity for tag matching. v1 = exact tag string only.
  • Cross-machine replication.
  • Conflict resolution beyond last-write-wins.
  • Auto-injection into role prompts. Agents have to opt in by calling the facade or the CLI.