Project memory and resume
Keep each task's turn history in Sandbox, and start a new sandbox from the last saved workspace.
Two features let work on a task outlive the sandbox it ran in:
| Need | Use |
|---|---|
| Remember what each turn did (meta, summary, trajectory, diff, screenshots) | Project memory |
| Continue a task's files in a new sandbox after the old one stopped, expired, or was destroyed | Resume in a new sandbox |
Both are scoped to X-Sandbox-Tenant-Id + X-Sandbox-Project-Id, like every
other Sandbox resource. Nothing is shared across projects.
Project memory
Project memory is a small store of turn records per task, served at
/sandbox/memory/v1. Any app that uses Sandbox can write to it and read it back,
for example to show a task's history or to brief the agent that runs the next turn.
Both tenancy headers are required; a request without them gets 400.
Write a turn
curl -sS -X PUT \
-H "X-Api-Key: $SANDBOX_API_KEY" \
-H "X-Sandbox-Tenant-Id: $TENANT" -H "X-Sandbox-Project-Id: $PROJECT" \
-H "Content-Type: application/json" \
-d '{
"meta": {
"agent_id": "claude-code",
"model": "claude-sonnet-4-5",
"job_id": "job-123",
"sandbox_id": "sbx-abc",
"commit": "abc1234",
"exit_code": 0,
"started_at": "2026-11-04T10:00:00Z",
"ended_at": "2026-11-04T10:05:00Z",
"tokens": {"input": 1200, "output": 340},
"cost": 0.04,
"claude_session_id": "sess-1"
},
"summary_md": "## Turn 1\nAdded the login form and its tests."
}' \
"$SANDBOX_GATEWAY_URL/sandbox/memory/v1/tasks/task-42/turns/1"The call is an upsert. It returns the turn record
(task_id, turn, meta, summary_md, artifacts, created_at, updated_at,
retain_until). Writing the same turn again replaces its meta and summary and
keeps its artifacts.
- Every
metafield is optional, and any extra keys you send are kept.metais capped at 64 KiB andsummary_mdat 1 MiB. task_idmust match[A-Za-z0-9][A-Za-z0-9._-]{0,127}.turnmust be between 0 and 9999. Turns are stored asturns/0001/,turns/0002/, and so on.
Attach artifacts
Send the raw bytes. The Content-Type you send is stored and returned on download.
curl -sS -X PUT \
-H "X-Api-Key: $SANDBOX_API_KEY" \
-H "X-Sandbox-Tenant-Id: $TENANT" -H "X-Sandbox-Project-Id: $PROJECT" \
-H "Content-Type: application/x-ndjson" \
--data-binary @trajectory.jsonl \
"$SANDBOX_GATEWAY_URL/sandbox/memory/v1/tasks/task-42/turns/1/artifacts/trajectory.jsonl"Typical names are trajectory.jsonl, atif.json, diff.patch,
claude-session.jsonl and screenshot-001.png. A name must match
[A-Za-z0-9._-]{1,128}; any other name gets 400 (or 404 if it contains /).
An artifact can be up to 50 MiB; a larger one gets 413. Uploading an artifact
for a turn you have not written yet creates that turn with empty meta.
Read it back
| Call | Returns |
|---|---|
GET /sandbox/memory/v1/tasks?limit=50&cursor= | Tasks, most recently updated first: task_id, latest_turn, turn_count, updated_at, retain_until. Send next_cursor back as cursor to get the next page. |
GET /sandbox/memory/v1/tasks/{task_id} | The task index: every turn with its meta and artifact list. 404 if the task has no memory. |
GET /sandbox/memory/v1/tasks/{task_id}/turns/{turn} | One turn's meta, summary_md and artifact list |
GET /sandbox/memory/v1/tasks/{task_id}/turns/{turn}/artifacts/{name} | The artifact bytes, with the stored Content-Type |
GET /sandbox/memory/v1/project.md | The project digest (Markdown) |
project.md has one section per task: its id, latest turn, turn count, last
update, and the first ~2,000 characters of its newest summary. It is rebuilt from
the task catalog every time you read it, so it always matches what has been
written. Rewriting an older turn does not replace the newer summary.
Delete a task
curl -sS -X DELETE \
-H "X-Api-Key: $SANDBOX_API_KEY" \
-H "X-Sandbox-Tenant-Id: $TENANT" -H "X-Sandbox-Project-Id: $PROJECT" \
"$SANDBOX_GATEWAY_URL/sandbox/memory/v1/tasks/task-42"This removes every turn, artifact and index entry for the task, and returns 204
even if the task was already gone. A Keycloak writer can delete only tasks it
created; project_admin and above can delete any task in the project. Any
project writer can add turns to any task.
Retention
Every write sets retain_until to the current time plus
SANDBOX_MEMORY_RETENTION_SEC, which defaults to 30 days. So a task is kept for
30 days after its last write. The timestamp is recorded on the index and the task
list; deleting after that date is left to storage lifecycle rules.
Permissions
Keycloak callers need the UMA resource sandbox-memory. All roles can read;
writers and above can write and delete, and delete is further limited to the
task's owner as described above. Platform API keys and team tokens follow the
same tenancy rules as the rest of Sandbox.
Resume in a new sandbox
Each time a sandbox that belongs to a task saves a checkpoint (on stop, autosave,
expiry, destroy or workspace push), Sandbox records it as that task's latest
workspace. A sandbox belongs to a task when it is created with task_id,
labels.task_id, labels.ctp_task_id or labels.workspace_key. A new sandbox
can then start from that snapshot, even after the old sandbox has been destroyed.
Explicit: source.type = workspace_latest
curl -sS -X POST \
-H "X-Api-Key: $SANDBOX_API_KEY" \
-H "X-Sandbox-Tenant-Id: $TENANT" -H "X-Sandbox-Project-Id: $PROJECT" \
-H "Content-Type: application/json" \
-d '{
"profile": "sandboxes-default",
"task_id": "task-42",
"source": {"type": "workspace_latest"}
}' \
"$SANDBOX_GATEWAY_URL/sandbox/sandboxes/v1"The create response names the snapshot it restored:
{
"source": {
"type": "workspace_latest",
"workspace_key": "task-42",
"sandbox_id": "8aee8bce-…",
"snapshot_seq": 3
}
}That pinned snapshot is also what the sandbox goes back to if it is restarted before it has saved a checkpoint of its own.
- To restore a different task's workspace, pass
source.workspace_keyinstead of labelling the sandbox. - To restore one exact snapshot, pass
source.sandbox_idandsource.snapshot_seq. - If the task has no snapshot yet (its first turn), the sandbox starts with an
empty workspace and
sourcecomes back withoutsandbox_id. - If there is no task label and no
workspace_key, the create fails with400.
Automatic, with reuse: restore_latest: true
{
"profile": "sandboxes-default",
"task_id": "task-42",
"reuse": true,
"restore_latest": true
}If a running or stopped sandbox matches the reuse fingerprint, it is returned as
before. If none matches, the new sandbox starts from the task's latest workspace
snapshot. If the task has never saved one, the new sandbox starts empty, exactly
as without the flag. restore_latest also works without reuse. It is ignored
when you send an explicit source, and it defaults to false, so existing
callers see no change.
Which snapshot counts as latest
Normally it is the most recent checkpoint for the task. The exception: once a newer sandbox on the task has saved a checkpoint, an older sandbox can no longer take the pointer back. This matters when the old sandbox is destroyed or expires after its replacement has done new work. Its final checkpoint is saved, but the task still resumes from the replacement's work.
Not for pass@k
Every Pass@K attempt must start from the same clean workspace. Do not use
workspace_latest or restore_latest for attempts that must be independent.
Related: Long-lived sandboxes, Project memory API, Sandboxes API.