API contract

August 23, 2026 · View on GitHub

dsh-memory exposes a local Typert remote namespace named memory.

The browser UI can call only this namespace. It cannot supply a memory path, execute Git, or request arbitrary filesystem operations.

Setting

memory.enabled: boolean

Default: true.

When the setting is changed, the host updates its memory system-prompt section immediately; the next model call observes the new state without restarting DSH. When enabled, that section also contains a bounded (16 KiB) summary.md snapshot marked as [DPSK MEMORY: UNTRUSTED CONTEXT]; unreadable summaries fall back to the static instructions.

Methods

memory.getSettings()

Success:

{ "ok": true, "value": { "enabled": true } }

Failure:

{ "ok": false, "error": { "code": "settings-unavailable" } }

memory.setEnabled({ enabled })

Input:

{ "enabled": false }

Success:

{ "ok": true, "value": { "enabled": false } }

Failures use settings-invalid-request, settings-unavailable, or settings-write-failed.

memory.status()

Success:

{
  "ok": true,
  "value": {
    "empty": false,
    "dataFileCount": 6,
    "targetDirty": false,
    "recoverable": true
  }
}

This method reports metadata only. It never returns a memory document, Git diff, transcript, or credential.

Failures use repo-unavailable or unsafe-layout.

memory.clear({ confirmation })

The only accepted confirmation is the exact machine value:

{ "confirmation": "DELETE_MEMORY" }

Success when data was cleared:

{
  "ok": true,
  "value": {
    "alreadyEmpty": false,
    "clearedFileCount": 6,
    "recoveryCommit": "<git commit id>",
    "clearCommit": "<git commit id>"
  }
}

Success for an already empty repository uses alreadyEmpty: true, clearedFileCount: 0, and null commit values.

For a nonempty repository, recoveryCommit is always the recovery point that parents clearCommit. When the target memory paths are clean, it is the existing pre-clear HEAD and no redundant checkpoint commit is created. When target memory paths are dirty, it is a dedicated checkpoint commit containing their live pre-clear contents.

Failures use stable codes:

CodeMeaning
repo-unavailablethe configured memory root is not an accessible Git repository
unsafe-layoutthe root or target layout includes an unsafe path/symbolic link
checkpoint-failedthe recovery checkpoint could not be built
clear-failedclear staging or confirmation failed safely
commit-failedlive index/head update could not be completed safely

The user interface shows a human-readable two-step confirmation before it sends the machine confirmation above.

memory.status() (v0.2)

The response adds schema and journal observability:

{
  "ok": true,
  "value": {
    "empty": false,
    "dataFileCount": 6,
    "targetDirty": false,
    "recoverable": true,
    "schemaVersion": 1,
    "legacyFileCount": 2,
    "pendingMigration": true,
    "lastRun": {
      "runId": "20260819T120000Z-a1b2c3d4",
      "status": "applied",
      "changedFileCount": 2,
      "applyCommit": "550010e76d7e5c6995a1e15f7e439873586dff40"
    }
  }
}

schemaVersion is the front matter schema version. legacyFileCount counts payload records (handbook/, rollouts/, archive/) without front matter; summary.md is a navigation file and never counts as legacy. pendingMigration is true when at least one legacy record exists. lastRun is null before the first journaled run and otherwise reflects .sync/last-run.json.

memory.legacyRecords() (v0.6)

Returns metadata-only descriptions of legacy Markdown records under handbook/, rollouts/, and archive/. Files that already have valid front matter are not returned. The response never includes the record body:

{
  "ok": true,
  "value": {
    "count": 1,
    "pendingMigration": true,
    "records": [
      {
        "path": "handbook/old-note.md",
        "id": "legacy-4f2b7c1a9e0d3c55",
        "frontMatter": "---\nschema_version: 1\nid: legacy-4f2b7c1a9e0d3c55\ncreated_at: 2026-08-20\nupdated_at: 2026-08-20\n---\n\n",
        "createdAt": "2026-08-20",
        "updatedAt": "2026-08-20",
        "bytes": 128
      }
    ]
  }
}

Failures use repo-unavailable, unsafe-layout, or a migration-specific filesystem error code.

memory.migrateLegacy({ dryRun }) (v0.6)

dryRun is required and must be a boolean. true returns the pending records and planned paths without changing the worktree, journal, watermark, or Git:

{
  "ok": true,
  "value": {
    "dryRun": true,
    "status": "pending",
    "legacyCount": 1,
    "migratedCount": 0,
    "changedPaths": ["handbook/old-note.md"],
    "recoveryCommit": null,
    "applyCommit": null,
    "journalCommit": null
  }
}

{ "dryRun": false } applies deterministic front matter through the same staging, validation, operation-lock, recovery/apply-commit, and metadata-only journal path used by host-owned sync. The original Markdown body is preserved. Applying again returns status: "no_change" with no new payload commit.

Failures use migration-invalid-request, repo-unavailable, unsafe-layout, operation-in-progress, interrupted-run, or a stable migration-* error code. If an older process left an active run, the host journals it as status: "interrupted" and stops; the operator must inspect health() and recover the repository explicitly before applying another migration.

memory.status() (v0.3)

The response adds a pending-preview field:

{
  "ok": true,
  "value": {
    "empty": false,
    "dataFileCount": 6,
    "targetDirty": false,
    "recoverable": true,
    "schemaVersion": 1,
    "legacyFileCount": 2,
    "pendingMigration": true,
    "lastRun": { "runId": "...", "status": "applied" },
    "pendingPreview": null
  }
}

pendingPreview is the newest non-expired preview under .sync/previews/, or null when none exists. Previews expire after a host-defined TTL and are cleaned lazily; an expired preview is never reported as pending.

memory.health()

Read-only repository health summary used by the settings UI:

{
  "ok": true,
  "value": {
    "memoryRoot": "$DSH_HOME/storages/memory",
    "rootSafe": true,
    "gitAvailable": true,
    "dataFileCount": 6,
    "payloadDirty": false,
    "operationLock": {
      "operation": "sync",
      "pid": 12345,
      "runId": "20260820T100000Z-a1b2c3d4",
      "startedAt": "2026-08-20T10:00:00Z",
      "active": false
    },
    "activeRun": {
      "run_id": "20260820T100000Z-a1b2c3d4",
      "operation": "sync",
      "status": "running",
      "phase": "applying",
      "pid": 12345,
      "started_at": "2026-08-20T10:00:00Z",
      "state": "running"
    },
    "interruptedRun": null,
    "pendingPreview": null,
    "pendingPreviewCount": 0,
    "journalReadable": true,
    "needsManualRecovery": false
  }
}

operationLock is the current .sync/operation.lock state (null when no lock is held). activeRun.state is running when the recorded pid is alive, interrupted when the pid is gone, and null when no active run exists. interruptedRun is the interrupted run record that should be recovered or audited. needsManualRecovery is true when the payload is dirty, an active run is interrupted, an interrupted run exists in the journal, or the journal is not readable.

Failures use repo-unavailable or unsafe-layout.

memory.runs({ limit, operation, status })

Lists recent journaled runs (newest first, default limit 20). operation and status are optional exact-match filters. Run records include the v0.3 fields phase (staging/validating/applying/finalizing/complete), duration_ms, rejected_file_count, changed_path_count, and staging_digest:

{
  "ok": true,
  "value": {
    "runs": [
      {
        "schema_version": 1,
        "run_id": "20260819T120000Z-a1b2c3d4",
        "operation": "sync",
        "status": "applied",
        "phase": "complete",
        "changed_paths": ["handbook/preferences.md"],
        "recovery_commit": "...",
        "apply_commit": "...",
        "duration_ms": 1234,
        "rejected_file_count": 0,
        "changed_path_count": 1
      }
    ]
  }
}

For a sync run, candidate_sessions, processed_sessions, and skipped_sessions are host-lifecycle counts, not values inferred from a model report: candidates passed the watermark and one-hour-idle guards; processed candidates were successfully submitted to headless DSH; skipped candidates were withheld by the host before submission. On the current successful incremental path, every candidate is submitted, so processed_sessions equals candidate_sessions and skipped_sessions is 0. A model's per-session NO_SIGNAL judgment is intentionally not parsed from natural-language output.

memory.rollback({ runId, confirmation })

Rolls back a whole journaled run by creating a new rollback commit. The only accepted confirmation is the exact machine value "ROLLBACK_MEMORY".

Preconditions:

  • the run exists and its status is applied;
  • the run's apply commit is still the latest payload write (journal commits after it are fine);
  • the repository layout is safe.

Failures use stable codes:

CodeMeaning
rollback-invalid-confirmationthe confirmation value is wrong
rollback-run-not-foundno such journaled run
rollback-not-applicablethe run is not applied or cannot be rolled back
rollback-conflictnewer memory writes happened after the apply commit
rollback-failedthe rollback commit could not be completed safely

The rollback commit restores the payload to the run's recovery commit tree, preserves .sync and .last-sync, and records the rollback itself in the journal. It never uses reset, force update, or history deletion.

memory.previews() (v0.3.1)

Lists pending (non-expired) previews, newest first:

{
  "ok": true,
  "value": {
    "previews": [
      {
        "preview_id": "20260820T150000Z-a1b2c3d4",
        "created_at": "2026-08-20T15:00:00Z",
        "expires_at": "2026-08-27T15:00:00Z",
        "candidate_sessions": 1,
        "changed_paths": ["handbook/preferences.md"],
        "status": "pending"
      }
    ]
  }
}

Previews are created by dsh-memory-sync --preview <id>, which captures the candidate diff (baseline + model edits) under <root>/.sync/previews/<id> without applying anything. Expired previews are never listed and cannot be applied.

memory.applyPreview({ previewId }) (v0.3.1)

Applies a pending preview's staged payload as a normal sync transaction (recovery + apply commits), consumes the preview, and journals the run under operation: "preview":

{
  "ok": true,
  "value": {
    "status": "applied",
    "previewId": "20260820T150000Z-a1b2c3d4",
    "changed_paths": ["handbook/preferences.md"],
    "recovery_commit": "...",
    "apply_commit": "...",
    "journaled": true
  }
}

Failures use preview-invalid-request, preview-not-found, preview-expired, live-memory-changed, operation-in-progress, or preview-apply-failed.

memory.discardPreview({ previewId }) (v0.3.1)

Removes a pending preview without applying it:

{ "ok": true, "value": { "removed": true, "previewId": "20260820T150000Z-a1b2c3d4" } }

Failures use preview-invalid-request or preview-not-found.

memory.search({ query, limit, scope }) (v0.4/v0.8.1)

Local full-text search over active payload records (handbook/, rollouts/) by default. Pass scope: "all" or scope: "archive" to include archived records explicitly. The query is tokenized on non-letter/number boundaries; each record is scored by front matter fields (id matches weigh most) and body text. Expired records are excluded. Returns matches sorted by score with a short body snippet and provenance metadata:

{
  "ok": true,
  "value": {
    "query": "codegen",
    "count": 1,
    "results": [
      {
        "path": "handbook/project.md",
        "score": 5,
        "id": "project/codegen",
        "type": "decision",
        "updated_at": "2026-08-10",
        "content_hash": "sha256:…",
        "generation": 1,
        "source_rollouts": ["rollouts/2026-08-10-session.md"],
        "snippet": "We chose TypeScript for the codegen pipeline…",
        "citation": "[source: handbook/project.md · id: project/codegen]"
      }
    ]
  }
}

scope defaults to active; limit defaults to 20. Legacy records use their deterministic legacy-<path-hash> id without rewriting the file. Failures use search-invalid-request, repo-unavailable, or search-failed.

memory.context({ query, limit, scope }) (v0.8.1)

Host-owned read path for a bounded memory context. query is optional; when omitted, records are ordered by usage feedback. When present, deterministic full-text matches are selected first and then ordered by effective usage, last_usage, generation, logical id, and path. Expired records are excluded.

{
  "ok": true,
  "value": {
    "query": "codegen",
    "count": 1,
    "records": [
      {
        "path": "handbook/project.md",
        "id": "project/codegen",
        "type": "decision",
        "content": "We chose TypeScript for the codegen pipeline.\n",
        "truncated": false,
        "citation": "[source: handbook/project.md · id: project/codegen]",
        "usage_count": 3,
        "last_usage": "2026-08-21T02:00:00.000Z",
        "logical_id": "project/codegen",
        "generation": 1,
        "content_hash": "sha256:…",
        "source_rollouts": [],
        "score": 5
      }
    ]
  }
}

scope defaults to active, which excludes archive/; use all or archive for explicit broader reads. limit defaults to 10 and is restricted to 1–20. Each returned body is bounded to 4 KiB. A successful context read increments metadata-only usage in .sync/usage.json; the sidecar is atomically written with owner-only permissions and is excluded from Git. It contains no memory body, transcript, prompt, or credential. truncated is true when the body reached the 4 KiB limit. Failures use context-invalid-request, usage-invalid, unsafe-layout, repo-unavailable, or context-failed.

Agent tools (v0.8.x)

The host registers two read-path tools when the tools service is available:

  • memory_search({ query, limit }) returns the same ranked snippets and citations as memory.search().
  • memory_context({ query?, limit?, scope? }) returns bounded records and updates only the metadata-only usage sidecar used for deterministic ordering.

Both tools return a JSON string to the model. They do not expose the memory root, Git operations, journal contents, transcripts, prompts, or credentials.

Front matter schema (v0.4)

Record ids may be namespaced with a single /: project/codegen or user/preferences. Each segment is [a-z0-9][a-z0-9-]*; ids without a namespace remain valid.

Provenance fields (all optional, completed as null by the host when absent):

FieldTypeMeaning
source_hashstringstable hash of the source that produced the record
created_bystringagent or process that created the record
content_hashstringcontent identity used for usage generations
source_session_digeststringredacted source-session identity
supersedesstringprior logical record replaced by this record
conflicts_withstringrecord kept as an explicit conflict
review_afterdatesuggested review date (YYYY-MM-DD)
expires_atdatevalidity deadline (YYYY-MM-DD)

expires_at is a lazy projection: an expired record is excluded from search() and from deterministic conflict resolution without rewriting its front matter. The deterministic conflict key is type plus the id's namespace (fact:project), and the winner is chosen by status precedence (active > candidate > conflicted > superseded > archived), then newest updated_at, then lexicographically smallest id. Expired records never win.