> ## Documentation Index
> Fetch the complete documentation index at: https://docs.anonalabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Softmax

> Memory across episodes for a Softmax league policy.

Softmax runs competitive multi-agent leagues. A policy is a container: the
runner hands it a game socket, the episode ends, the container exits, and it is
never restarted. So a policy replays the same opponents for weeks and starts
every match knowing nothing.

Anona carries what it learned into the next episode.

```bash theme={null}
pip install anona
```

No extra to install. The half that runs inside the pod uses the standard
library alone.

## The shape is different from the other adapters

Every other integration here recalls and stores around a model call. A Softmax
player cannot do that: **its pod has no outbound network**. DNS does not
resolve, and no proxy is offered, so a policy cannot reach Anona while it plays.

What it can reach is the in-cluster artifact endpoint. Memory therefore leaves
an episode as an artifact, is ingested from outside the cluster, and comes back
into the next generation as an environment variable set at upload time.

```
episode N  ->  artifact  ->  your machine or CI  ->  Anona  ->  episode N+1
SoftmaxMemory                SoftmaxSync                       SoftmaxMemory
```

The two halves never share a process, and only one of them needs a network.

## In your player

```python theme={null}
from anona.integrations.softmax import SoftmaxMemory

mem = SoftmaxMemory()

for note in mem.recall():          # empty on generation 0, which is normal
    print("known:", note)

mem.remember({"opponent": "catlock", "map": "arrows", "lost": True})
mem.remember("Firing with a teammate in the line of fire costs 60 points.")

mem.flush()                        # before the process exits
```

`mem.generation` is 0 on the first run and increments each time recall is
carried in.

Every method degrades to a no-op rather than raising, the same fail-open policy
the framework adapters take. A policy that crashes because memory was
unavailable is strictly worse than one that plays without it, and the middle of
a league episode is not where you want to find out an environment variable was
missing.

## Outside the cluster

```python theme={null}
from anona.integrations.softmax import SoftmaxSync

with SoftmaxSync(
    softmax_token="...",           # uv run softmax get-token
    anona_key="anona_live_...",
    coworld="paintbot",
    policy="my-policy",
) as sync:
    sync.ingest_episode(episode_request_id, policy_version_id)
    sync.wait_for_ingest()
    blob = sync.compile("how do I beat catlock?")
```

Then upload the next generation with it:

```bash theme={null}
uv run coworld upload-policy my-policy:local --name my-policy \
  --secret-env "ANONA_MEMORY=$blob"
```

or let the adapter build the argv:

```python theme={null}
subprocess.run(["uv", "run", "coworld", "upload-policy", image,
                "--name", policy, *sync.upload_args("how do I beat catlock?")])
```

<Warning>
  `record_batch` is queued, and extraction is one model call per chunk. A
  `compile()` issued straight after `ingest_episode()` recalls nothing, and the
  space looks broken when it is merely still working. Call `wait_for_ingest()`
  between them.
</Warning>

## Latency is why this works

Recall is read once before the first action and written once before exit, never
per tick. Retrieve is about 2.1s at p50, far outside a per-decision window, so
the artifact round trip costs a policy nothing it was going to use.

What it gives up is adapting to something first seen in the **current** episode.
For league play, where the same opponents recur for weeks, that is a small
fraction of the value.

## Scoping

One space per coworld, with `user_id` set to the policy name. That makes the
policy the subject the space accumulates knowledge about, so
`get_user_profile(space, policy)` is a standing scouting report rather than
something you assemble.

Scoped reads are strict, so use the same convention for every write into a
space or an earlier generation becomes invisible to later ones. See
[Scoping](/guides/scoping-multi-tenant).

## Two platform details this handles for you

The artifact URL has two shapes. A hosted episode sets
`http://job-<id>-game:9091/...`; `coworld run-episode` sets
`file:///coworld-artifact/policy_artifact_0.zip`. A client that handles only the
hosted form loses every local write silently, which is where you develop.
`flush()` handles both.

The artifact endpoint takes `PUT`, not `POST`, and answers 201 in about 100ms.

<Note>
  `compile()` trims the blob to 3000 bytes, dropping the weakest match first,
  because the result becomes an environment variable at upload time. An
  oversized recall is a deploy failure, not merely a large memory.
</Note>

## What this does not do

It moves information between episodes. Whether recall improves play depends on
a policy able to act on it, which is your side of the problem, not the memory
layer's.
