---
title: @memhtml/session
---

`@memhtml/session` is a session's overlay log and the mechanical path that commits it. A session is a base commit, the ref its commit will land on, and an ordered list of operations, kept in one JSON file per id under `.memhtml/sessions/`. The package depends on `@memhtml/contracts`, `@memhtml/domain` and `@memhtml/html`, plus `effect`, `proper-lockfile` and `write-file-atomic`. It never imports `@memhtml/store`. It runs `git` itself, because the store's git service strips `GIT_INDEX_FILE` and a commit stages into an index file of its own. The CLI in `apps/cli` uses most of the package, and `@memhtml/curator` imports `BATCH_CAP` and the `Violation` type. The design is `docs/design.md` §17.

`Session` is a session's state in memory, plus the store `root`, which is not saved, so a log is always found by root and id. `startSession` takes a `SessionBase` and reads only its `sha`. It checks that the sha names a commit and writes an empty log. Unless `force` is set, it refuses with `session.exists` an id that already has a log, so a retried start cannot replace work in progress. The new session's `ref` is `DEFAULT_REF`, `refs/heads/main`, unless the caller passes one: it does not follow the branch the checkout has open. `resumeSession` reopens a log. `decodeLog` turns a log's text into session state and fails on text that is not JSON, not a log, or another id's log. A log's ops decode through `OverlayOpSchema`, which the head server's wire protocol uses too. An id must match `SESSION_ID`, one path segment, so a log cannot leave `SESSIONS_DIR`. `isSessionId` tests an id and `sessionStateFile` names its log.

Every write to a log runs inside `withSessionLock`, which holds an exclusive `proper-lockfile` lock directory, `<id>.lock`, beside the log. It covers one id in one checkout, across processes. A second writer does not wait. It fails at once with `session.locked`, because the right answer to a command sent twice is a refusal, not a second commit. The holder refreshes the lock's mtime, and a lock with no refresh for `SESSION_LOCK_STALE_MS` (30 s) can be taken by the next writer of the same id. `SessionLockOptions` lets a test shorten the refresh.

A lock alone would still lose an update, because a caller reads the log, works for a while, and writes after. So `appendOps`, `saveSession`, `replaceOps` and `commitSession` read the log again under the lock and go ahead only when it still holds the ops the caller read. Otherwise each fails with `LOG_MOVED` (`session.moved`) and writes nothing. Only the ops are compared, not the base. `appendOps` adds ops and returns the session to keep using. `rebaseSession` is pure: it moves `baseSha` to the version it is given and writes nothing, and `saveSession` persists the result. `replaceOps` swaps the whole list, which `session commit --drop-refused` does to take refused ops out.

`pending` is a commit that `commitSession` built and wrote to the log before it touched the checkout. A process that dies before clearing it leaves that record, and the next `commitSession` or `appendOps` on that id settles it under the checkout lock. If the commit is reachable from the ref, it landed, and the session moves onto it and drops the ops it carried. If not, the settle tries to undo any checkout update the dead call made, and the ops stay. `appendOps` settles `pending` before it adds anything, so new ops never sit beside a commit that may have landed. `replaceOps` refuses a log that carries one, with a failure that starts with `SESSION_PENDING`.

`commitSession` lands the whole log as one commit and calls no model. It holds the session lock for the whole call. It settles `pending` first, and answers `committed` with `recovered` set when that commit had landed. It runs `validateOps` against the `head` the caller passes and answers `refused` on any violation. It answers `rebase-needed` when the ref names a commit other than that head's `sha`, because a duplicate or a frame-key rival can arrive on paths the session never touched. It applies the ops over the head and builds the blobs, tree and commit through a temporary index file under the OS temp directory, so building the commit never touches the shared index. Then it writes the commit to the log as `pending`, lands it with `landOnRef`, and saves the log with the commit as its base and no ops. `CommitOutcome` is the answer: `committed`, `rebase-needed`, `refused` with each `Violation`, or `worktree-dirty`.

While `commitSession` applies the ops, a put gets a `contradicts` edge to each other active record in the head whose claim shares its frame key, and both stay live. A rival that agrees with the put, that the put supersedes, or that the batch archives gets no edge. That pair comes back under `restatements` with a `RestatementReason`, one of `RESTATEMENT_REASONS`. `commitSubject` builds the one-line subject `memhtml(<scope>): <summary>`, with the summary capped and never empty. `commitMessage` adds a `Memhtml-Session: <id>` trailer, which is how a session commit names its session. `archiveBody` stamps `memhtml-status archived` and a `memhtml-archived` instant into the HTML head of a record, so the article and its content hash do not change.

`landOnRef` moves the checkout and then the ref as one section under `withCheckoutLock`. That lock is the directory `.memhtml/checkout.lock` in one checkout, and `@memhtml/store` computes the same path for its writes, so a session commit and a v1 write in that checkout take the same lock. A waiter retries for up to 15 s by default and then fails with `checkout.locked`. The lock is reentrant within one fiber, and once it is held the section cannot be interrupted. `checkoutLockTarget` names the lock's target, `.memhtml/checkout` under a root.

Inside the section, the ref must still name `expected`, or the answer is `raced` and nothing moves. When `HEAD` is the ref, uncommitted state at a path the commit changes is `worktree-dirty`; with those paths clean, a two-tree `read-tree -m -u` moves the shared index and working tree to the new commit and keeps uncommitted edits at other paths. Last, `update-ref` with the expected old value is a compare-and-swap that git enforces on that one ref, even against a writer that skips the checkout lock. When the swap is lost and `HEAD` is the ref, the reverse `read-tree` runs to put the checkout back. The checkout moves first because the other order would leave `HEAD` ahead of the index on a failure, and git would show the session's files as staged deletions. `Landing` is `landed`, `raced` or `worktree-dirty`, a lost swap is `raced` too, and `commitSession` answers `raced` with `rebase-needed`.

`validateOps` is a pure function of a `HeadView` and a list of ops that returns a list of `Violation` values and does no I/O. The CLI calls it before `session put` and `session exec` append and in its curation commands, so they judge ops with the function the commit runs. `ValidateOptions` carries the `scope`, the store's `writeBar`, and a `batchCap` that defaults to `BATCH_CAP`, 200 ops. A `CommitScope` is `session` or `curate`, and it is the caller's to pass: `validateOps` and `commitSession` default to `session` and do not read it from the ref. `isReservedPath` says which paths a scope may not write: a path under `.memhtml/`, `memhtml.config.json`, any `index.html` or `sitemap.xml`, and outside `curate`, `areas/arcs/` and `resources/people/`. `touchedPaths` lists the paths one op reads or writes.

`writeBarReasons` judges one new record against the store's write bar: a type the store refuses, and, when the bar requires an anchor, a record that has no entity and is not under `projects/`. `UNANCHORED_REASON` is the reason that rule gives. The library default refuses nothing, so a caller judging a real store passes that store's policy. Besides the reserved paths every op is held to, a `link` op is checked for a live source, a rel in the edge vocabulary, a root-relative href, and a target that is a record in the head or the batch. It is not checked against the endpoint classes that `memhtml link` enforces in `@memhtml/store`, so a session `link` can join a task and a memory with a rel that `memhtml link` refuses for that pair.

A `move` or an `archive` leaves its old path, so a batch repoints the live edges to that path with `unlink` and `link` pairs. `withRepoints` adds those pairs, read from the view's inbound index, before the batch's first move or archive. `missingRepoints` finds the pairs a log already written lacks against the version it lands on, for the caller to append, and names under `overflow` each archive or move whose pairs would not fit under the cap. `afterDroppedVacates` works out what a log keeps when `session commit --drop-refused` drops an archive or a move: the repoint pairs it needed go with it. `VacateFollower` reports each op it took or rewrote.

`pruneSessions` removes session state that nothing reads again, each item only when it is older than `olderThanMs`. That is logs with zero ops, lock directories no live holder refreshes, and the `<id>.idx` index files and `SESSION_REFS_PREFIX` refs that older builds wrote. It never removes a log that holds ops, and lists one older than the cutoff under `unlanded` as an `UnlandedSession`, for an operator to commit. Outside a dry run it handles each id under that id's session lock, and either way an id whose lock is held is listed under `busy`. With `dryRun` it takes no lock, removes nothing, and reports what it would remove. `PruneReport` is the answer, with a `PruneCount` per kind of state.

`makePlumbing` returns the `Plumbing` the commit path runs: each method runs a `git -C` child with a few variables pinned and the `GIT_REPO_SELECTION_ENV` variables removed, so a `GIT_DIR` a hook exported cannot point a call at another repository. The four staging methods use the index file the plumbing was made with, so without one they cannot stage into the shared index. `PlumbingFactory` is how `commitSession` gets a plumbing for its temporary index, and a test seam. `RefEntry` is one row of `forEachRef`. `gitPathOf` asks git where a file under the git directory lives, because a linked worktree keeps `info/exclude` in its common directory.

`ensureExcluded` appends ignore patterns to the clone's `info/exclude`, each once, because a store's committed `.gitignore` may predate that state and rewriting it would dirty the tree. `ensureExcludedQuietly` is the best-effort form, which logs a failure and goes on. `startSession` passes `.memhtml/sessions/`, and the head server passes `HEAD_SOCKET`. `ExcludeResult` names the file and the patterns a call added. `GitFailure` is the shared class from `@memhtml/contracts`, re-exported so one `_tag` covers this package and the store. Its `command` is the git subcommand alone, never the arguments, because arguments carry memory paths and a failure goes back to an agent.

## Modules

The package publishes one import path, `@memhtml/session`, and its `index.ts` re-exports a chosen set of names from nine of the ten modules below, so the reference on this page is that set. A name `index.ts` does not re-export is left off the page and appears unlinked where a signature or doc comment names it. The table lists the package's own, and the types `HeadView`, `OverlayOp`, `StorageFailure`, `CheckoutLockOptions` and `WriteBarPolicy` come from `@memhtml/contracts`.

| Module             | What it holds                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `checkout-lock.ts` | `checkoutLockTarget` and `withCheckoutLock`.                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `commit.ts`        | `RESTATEMENT_REASONS`, `RestatementReason`, `CommitOutcome`, `commitSubject`, `commitMessage`, `archiveBody`, `PlumbingFactory`, `Landing`, `landOnRef` and `commitSession`, and a re-export of `CommitScope`, plus `COMMIT_SUBJECT_MAX` and `SESSION_TRAILER`, which are not exported from `@memhtml/session`.                                                                                                                                                     |
| `errors.ts`        | `GitFailure`, re-exported from `@memhtml/contracts`.                                                                                                                                                                                                                                                                                                                                                                                                                |
| `exclude.ts`       | `HEAD_SOCKET`, `ExcludeResult`, `ensureExcluded` and `ensureExcludedQuietly`.                                                                                                                                                                                                                                                                                                                                                                                       |
| `moves.ts`         | `withRepoints`, `missingRepoints`, `VacateFollower` and `afterDroppedVacates`.                                                                                                                                                                                                                                                                                                                                                                                      |
| `plumbing.ts`      | `GIT_REPO_SELECTION_ENV`, `Plumbing`, `RefEntry`, `gitPathOf` and `makePlumbing`.                                                                                                                                                                                                                                                                                                                                                                                   |
| `prune.ts`         | `PruneCount`, `UnlandedSession`, `PruneReport` and `pruneSessions`.                                                                                                                                                                                                                                                                                                                                                                                                 |
| `session.ts`       | `Session`, `DEFAULT_REF`, `SESSIONS_DIR`, `SESSION_ID`, `isSessionId`, `SESSION_LOCK_STALE_MS`, `sessionStateFile`, `SESSION_REFS_PREFIX`, `OverlayOpSchema`, `SessionLockOptions`, `withSessionLock`, `SessionBase`, `startSession`, `decodeLog`, `resumeSession`, `LOG_MOVED`, `SESSION_PENDING`, `appendOps`, `rebaseSession`, `saveSession` and `replaceOps`, plus `requireLogUnmoved` and `saveSessionLocked`, which are not exported from `@memhtml/session`. |
| `settle.ts`        | `settlePending`, which `commitSession` and `appendOps` call and `index.ts` does not re-export.                                                                                                                                                                                                                                                                                                                                                                      |
| `validate.ts`      | `Violation`, `BATCH_CAP`, `CommitScope`, `ValidateOptions`, `isReservedPath`, `UNANCHORED_REASON`, `writeBarReasons`, `touchedPaths` and `validateOps`.                                                                                                                                                                                                                                                                                                             |
| `index.ts`         | The re-exports above, type-only for the interfaces and type aliases.                                                                                                                                                                                                                                                                                                                                                                                                |

## Classes

### GitFailure

A git subprocess exited non-zero, or could not be spawned. `command` is the subcommand name
only, never the full argv, because arguments carry memory paths and commit subjects carry memory
titles, and a `GitFailure` is returned to an agent through a tool response. The stderr text goes
to `Effect.logError` at the spawning boundary instead of into the payload.

Declared here rather than in `@memhtml/store` because two packages spawn git: the store through
its `Git` service, and `@memhtml/session` through its own `execFile` (it needs `GIT_INDEX_FILE`,
which the store scrubs). One class means one `_tag`, so the CLI's `ERR_GIT` mapping and an
`instanceof` check hold for either producer.

#### Extends

- `GitFailure_base`

#### Constructors

##### Constructor

```ts
new GitFailure(...args): GitFailure;
```

###### Parameters

###### args

...\[`props`: `object`, `options?`: `MakeOptions`\]

###### Returns

[`GitFailure`](/api/session/#gitfailure)

###### Inherited from

`GitFailure_base.constructor`

#### Properties

##### command

```ts
readonly command: string;
```

###### Inherited from

`GitFailure_base.command`

##### exitCode

```ts
readonly exitCode: number | null;
```

The process exit code, or `null` when the process never started.

###### Inherited from

`GitFailure_base.exitCode`

## Interfaces

### ExcludeResult

What one call did.

#### Properties

##### added

```ts
readonly added: readonly string[];
```

The patterns this call appended; empty when every one was already present.

##### file

```ts
readonly file: string;
```

The exclude file, absolute.

***

### Plumbing

#### Methods

##### commitTree()

```ts
commitTree(
   tree, 
   parents, 
   message
): Effect<string, GitFailure>;
```

`commit-tree`, message on stdin; the commit sha.

###### Parameters

###### tree

`string`

###### parents

readonly `string`[]

###### message

`string`

###### Returns

`Effect`\<`string`, [`GitFailure`](/api/session/#gitfailure)\>

##### deleteRefs()

```ts
deleteRefs(refs): Effect<void, GitFailure>;
```

`update-ref --stdin` with one `delete` per ref, as one transaction; nothing when `refs` is empty.

###### Parameters

###### refs

readonly `string`[]

###### Returns

`Effect`\<`void`, [`GitFailure`](/api/session/#gitfailure)\>

##### diffTreePaths()

```ts
diffTreePaths(from, to): Effect<readonly string[], GitFailure>;
```

`diff-tree -r --name-only --no-renames -z from to`.

###### Parameters

###### from

`string`

###### to

`string`

###### Returns

`Effect`\<readonly `string`[], [`GitFailure`](/api/session/#gitfailure)\>

##### dirtyPaths()

```ts
dirtyPaths(paths): Effect<readonly string[], GitFailure>;
```

The subset of `paths` that `status --porcelain` reports over the shared index and working tree:
staged, modified, or untracked. Empty when every named path is clean and absent when untracked.
Each path is read as itself, never as a pattern, and an empty one fails before git runs.

###### Parameters

###### paths

readonly `string`[]

###### Returns

`Effect`\<readonly `string`[], [`GitFailure`](/api/session/#gitfailure)\>

##### forEachRef()

```ts
forEachRef(prefix): Effect<readonly RefEntry[], GitFailure>;
```

`for-each-ref` under `prefix` (`refs/memhtml/sessions/`): each ref, the object it names, and the
committer time of that commit in epoch seconds (`null` when it names no commit).

###### Parameters

###### prefix

`string`

###### Returns

`Effect`\<readonly [`RefEntry`](/api/session/#refentry)[], [`GitFailure`](/api/session/#gitfailure)\>

##### hashObjectWrite()

```ts
hashObjectWrite(bytes): Effect<string, GitFailure>;
```

`hash-object -w --stdin`; the blob sha.

###### Parameters

###### bytes

`Uint8Array`

###### Returns

`Effect`\<`string`, [`GitFailure`](/api/session/#gitfailure)\>

##### headRef()

```ts
headRef(): Effect<string | null, GitFailure>;
```

The branch `HEAD` points at (`refs/heads/...`), or `null` when detached. Shared index.

###### Returns

`Effect`\<`string` \| `null`, [`GitFailure`](/api/session/#gitfailure)\>

##### isAncestor()

```ts
isAncestor(sha, ref): Effect<boolean, GitFailure>;
```

`merge-base --is-ancestor sha ref`: true when `sha` is reachable from `ref`.

###### Parameters

###### sha

`string`

###### ref

`string`

###### Returns

`Effect`\<`boolean`, [`GitFailure`](/api/session/#gitfailure)\>

##### readTree()

```ts
readTree(sha): Effect<void, GitFailure>;
```

`read-tree <sha>` into the session index file.

###### Parameters

###### sha

`string`

###### Returns

`Effect`\<`void`, [`GitFailure`](/api/session/#gitfailure)\>

##### readTreeIntoWorktree()

```ts
readTreeIntoWorktree(from, to): Effect<void, GitFailure>;
```

`read-tree -m -u from to` on the SHARED index, so the working tree follows a commit. A two-tree
merge: a path unchanged between `from` and `to` keeps whatever the index and working tree hold,
so uncommitted edits elsewhere survive; a path that changed must be clean or git refuses. The
shared index lock is contended by every writer of the checkout, so `index.lock` being held is
retried briefly before it is a failure.

###### Parameters

###### from

`string`

###### to

`string`

###### Returns

`Effect`\<`void`, [`GitFailure`](/api/session/#gitfailure)\>

##### revParse()

```ts
revParse(ref): Effect<string | null, GitFailure>;
```

The commit a ref resolves to, or `null` when it does not resolve.

###### Parameters

###### ref

`string`

###### Returns

`Effect`\<`string` \| `null`, [`GitFailure`](/api/session/#gitfailure)\>

##### updateIndexAdd()

```ts
updateIndexAdd(entries): Effect<void, GitFailure>;
```

`update-index --add --index-info`, one child for the whole batch.

###### Parameters

###### entries

readonly `object`[]

###### Returns

`Effect`\<`void`, [`GitFailure`](/api/session/#gitfailure)\>

##### updateIndexRemove()

```ts
updateIndexRemove(paths): Effect<void, GitFailure>;
```

`update-index --force-remove -z --stdin`, one child for the whole batch.

###### Parameters

###### paths

readonly `string`[]

###### Returns

`Effect`\<`void`, [`GitFailure`](/api/session/#gitfailure)\>

##### updateRef()

```ts
updateRef(
   ref, 
   next, 
   expected
): Effect<"updated" | "raced", GitFailure>;
```

`update-ref <ref> <next> <expected>` as a compare-and-swap. `expected: null` means the ref must
not exist. A non-zero exit whose stderr names the ref as locked or moved is `"raced"`; any other
non-zero exit is a `GitFailure`.

###### Parameters

###### ref

`string`

###### next

`string`

###### expected

`string` \| `null`

###### Returns

`Effect`\<`"updated"` \| `"raced"`, [`GitFailure`](/api/session/#gitfailure)\>

##### writeTree()

```ts
writeTree(): Effect<string, GitFailure>;
```

`write-tree` over the session index file; the tree sha.

###### Returns

`Effect`\<`string`, [`GitFailure`](/api/session/#gitfailure)\>

***

### PruneCount

What one category of state amounts to: removed, or under `dryRun`, would be.

#### Properties

##### count

```ts
readonly count: number;
```

***

### PruneReport

#### Properties

##### busy

```ts
readonly busy: readonly string[];
```

Ids whose session lock a live writer held: nothing of theirs was read or removed.

##### cutoff

```ts
readonly cutoff: string;
```

Everything last written before this instant is a candidate.

##### dryRun

```ts
readonly dryRun: boolean;
```

##### indexes

```ts
readonly indexes: PruneCount & object;
```

###### Type Declaration

###### bytes

```ts
readonly bytes: number;
```

##### locks

```ts
readonly locks: PruneCount;
```

##### logs

```ts
readonly logs: PruneCount;
```

##### olderThanMs

```ts
readonly olderThanMs: number;
```

##### refs

```ts
readonly refs: PruneCount;
```

##### unlanded

```ts
readonly unlanded: readonly UnlandedSession[];
```

Logs older than the cutoff that still hold ops, oldest first. Kept.

##### unreadable

```ts
readonly unreadable: readonly string[];
```

Logs that do not decode as a session log: kept, for an operator to read.

***

### RefEntry

One row of [Plumbing.forEachRef](/api/session/#plumbing).

#### Properties

##### committedAt

```ts
readonly committedAt: number | null;
```

##### ref

```ts
readonly ref: string;
```

##### sha

```ts
readonly sha: string;
```

***

### Session

A session: a pointer to a base version, an overlay log of its own operations, and nothing else.
Its durable twin on disk is one file, `<root>/.memhtml/sessions/<id>.json`, the log, under
`.memhtml/`, which the store's scaffold gitignores. A commit stages through a temporary index file
of its own that it removes on every outcome (`commit.ts`), so nothing else is kept between calls.

One writer per id at a time. Every write to the log and the whole of a commit run under
[withSessionLock](/api/session/#withsessionlock), an exclusive lock at `<id>.lock` (proper-lockfile: an atomic `mkdir`,
its mtime refreshed while held so a lock left by a killed process goes stale and is reclaimed by
the next writer of the same id). A second writer does not wait: it fails with `session.locked`,
because a second commit of the same id would race the first for the log's `pending` and its ref.
A lock left by a killed process under an id nobody uses again (a run's) is never reclaimed by a
writer, which is why `pruneSessions` removes stale lock directories.

Every write replaces the whole file, and a caller computes what it writes from the log it read
earlier, outside the lock (a `session exec` reads the log, runs a script for seconds, then
appends its harvest). So the lock alone would still lose an update: two execs that read the same
log each append to it, and the second write drops the first's ops. Every writer therefore re-reads
the log under the lock and writes only when the file still holds the ops the caller read
(`requireLogUnmoved`); a log whose ops changed is refused with [LOG\_MOVED](/api/session/#log_moved) and
nothing is written, so the caller reads it again and decides. The ops are what is compared, not
the base: [rebaseSession](/api/session/#rebasesession) is pure and a caller may commit or append over a rebased session
it has not saved, which moves the base on purpose.

A log that carries `pending` (a commit killed after it wrote its intent) holds exactly the ops that
commit built, and the next commit clears them when it finds the commit landed. So no write adds
ops beside a `pending`: [appendOps](/api/session/#appendops) settles it first, and [replaceOps](/api/session/#replaceops) refuses.

#### Properties

##### baseSha

```ts
readonly baseSha: string;
```

##### id

```ts
readonly id: string;
```

##### ops

```ts
readonly ops: readonly OverlayOp[];
```

##### pending?

```ts
readonly pending?: string;
```

The commit a `commitSession` built and was about to land when the log was last written, or
absent. Written before the ref moves and cleared after, so a process killed between the two
leaves a record: the next commit checks whether it reached the ref and answers `committed` for
it instead of refusing the same ops as duplicates of themselves. An append settles it the same
way before it adds an op ([appendOps](/api/session/#appendops)).

##### ref

```ts
readonly ref: string;
```

`refs/heads/main` by default; `refs/heads/curate/<date>` for a curator.

##### root

```ts
readonly root: string;
```

The repository root. Not persisted: the log is found by root plus id, so it cannot disagree.

***

### SessionBase

The version [startSession](/api/session/#startsession) starts a session on. Only its `sha` is read, so a bare `{ sha }`
fits and so does a `HeadView`.

#### Properties

##### sha

```ts
readonly sha: string;
```

***

### SessionLockOptions

Test seams for [withSessionLock](/api/session/#withsessionlock).

#### Properties

##### updateMs?

```ts
readonly updateMs?: number;
```

How often the holder refreshes the lock's mtime, in milliseconds; proper-lockfile's default (half
of [SESSION\_LOCK\_STALE\_MS](/api/session/#session_lock_stale_ms)) unless a test shortens it to see a refresh happen.

***

### UnlandedSession

One log that still holds ops: never removed.

#### Properties

##### ageMs

```ts
readonly ageMs: number;
```

##### id

```ts
readonly id: string;
```

##### modifiedAt

```ts
readonly modifiedAt: string;
```

The log's last write.

##### ops

```ts
readonly ops: number;
```

##### pending

```ts
readonly pending: string | null;
```

A commit an earlier call built and may have landed; `session commit` settles it.

***

### VacateFollower

One op [afterDroppedVacates](/api/session/#afterdroppedvacates) took or rewrote, with the archive or move it followed.

#### Properties

##### detail

```ts
readonly detail: string;
```

The edge's rel, or the entity a `label` or `unlabel` names.

##### home?

```ts
readonly home?: string;
```

The path the record still has, for an op made there or an edge aimed back at it.

##### kind

```ts
readonly kind: "link" | "put" | "archive" | "unlink" | "label" | "unlabel" | "move";
```

##### path

```ts
readonly path: string;
```

The op's path as the log held it.

##### vacate

```ts
readonly vacate: OverlayOp;
```

***

### ValidateOptions

The options `validateOps` and `isReservedPath` take. `scope` defaults to `session`.

#### Properties

##### batchCap?

```ts
readonly batchCap?: number;
```

The most ops the batch may carry; [BATCH\_CAP](/api/session/#batch_cap) when omitted. `curate merge` passes
`Infinity` for a replay, whose ops are the union of commits that each passed the cap on the
curate branch: the 2026-09-24 collapse shape lands 2,747 ops, which one capped batch cannot.

##### scope?

```ts
readonly scope?: CommitScope;
```

##### writeBar?

```ts
readonly writeBar?: WriteBarPolicy;
```

The store's write bar (`writeBar` in `memhtml.config.json`, read by the CLI's
`loadStorePolicy`): the types it refuses and whether every new record must be anchored.
`DEFAULT_WRITE_BAR` when omitted, which refuses no type and anchors nothing, so a caller
judging a real store passes the store's policy. Format, integrity, reserved paths, and the
batch cap hold whatever it says.

## Type Aliases

### CommitOutcome

```ts
type CommitOutcome = 
  | {
  contradictions: ReadonlyArray<{
     against: string;
     path: string;
  }>;
  kind: "committed";
  paths: ReadonlyArray<string>;
  recovered: boolean;
  restatements: ReadonlyArray<{
     against: string;
     path: string;
     reason: RestatementReason;
  }>;
  sha: string;
  worktreeSynced: boolean;
}
  | {
  kind: "rebase-needed";
  mainSha: string;
  overlapping: ReadonlyArray<string>;
}
  | {
  kind: "refused";
  violations: ReadonlyArray<Violation>;
}
  | {
  kind: "worktree-dirty";
  paths: ReadonlyArray<string>;
};
```

#### Union Members

##### Type Literal

```ts
{
  contradictions: ReadonlyArray<{
     against: string;
     path: string;
  }>;
  kind: "committed";
  paths: ReadonlyArray<string>;
  recovered: boolean;
  restatements: ReadonlyArray<{
     against: string;
     path: string;
     reason: RestatementReason;
  }>;
  sha: string;
  worktreeSynced: boolean;
}
```

###### contradictions

```ts
readonly contradictions: ReadonlyArray<{
  against: string;
  path: string;
}>;
```

Each put step 3 linked to a frame-key rival (`against`, its destination when moved).

###### kind

```ts
readonly kind: "committed";
```

###### paths

```ts
readonly paths: ReadonlyArray<string>;
```

###### recovered

```ts
readonly recovered: boolean;
```

True when this call found the commit an earlier call had landed before it was killed (the
log still carried it as `pending`) and reports that commit instead of building one. The
contradictions and restatements of a recovered commit are not re-derived and come back
empty.

###### restatements

```ts
readonly restatements: ReadonlyArray<{
  against: string;
  path: string;
  reason: RestatementReason;
}>;
```

Each frame-key rival step 3 left unlinked, named the way `contradictions` names one, with
the reason ([RestatementReason](/api/session/#restatementreason)).

###### sha

```ts
readonly sha: string;
```

###### worktreeSynced

```ts
readonly worktreeSynced: boolean;
```

True when `HEAD` is the session's ref, so the shared index and working tree were moved to
the new commit before the ref was. False when the ref is not checked out and nothing
needed to follow.

***

##### Type Literal

```ts
{
  kind: "rebase-needed";
  mainSha: string;
  overlapping: ReadonlyArray<string>;
}
```

***

##### Type Literal

```ts
{
  kind: "refused";
  violations: ReadonlyArray<Violation>;
}
```

***

##### Type Literal

```ts
{
  kind: "worktree-dirty";
  paths: ReadonlyArray<string>;
}
```

###### kind

```ts
readonly kind: "worktree-dirty";
```

`HEAD` is the session's ref and the checkout has uncommitted state (staged, modified, or
untracked) at a path this commit writes or removes, so the shared index could not follow
the ref. Nothing was committed; the caller cleans up or commits from another checkout.

###### paths

```ts
readonly paths: ReadonlyArray<string>;
```

***

### CommitScope

```ts
type CommitScope = "session" | "curate";
```

Who is writing. `session` is an ordinary session on `main`; `curate` is a run of the curator
(`@memhtml/curator`) on its own `curate/<date>` branch. The scope names the commit subject
(`memhtml(<scope>): ...`) and widens what the validator lets through: the arcs and people
prefixes are the curator's to write and nobody else's.

***

### Landing

```ts
type Landing = 
  | {
  kind: "landed";
  worktreeSynced: boolean;
}
  | {
  kind: "raced";
  tip: string | null;
}
  | {
  kind: "worktree-dirty";
  paths: ReadonlyArray<string>;
};
```

What [landOnRef](/api/session/#landonref) did.

#### Union Members

##### Type Literal

```ts
{
  kind: "landed";
  worktreeSynced: boolean;
}
```

###### kind

```ts
readonly kind: "landed";
```

The ref moved to `to`; the checkout moved with it when `worktreeSynced`.

###### worktreeSynced

```ts
readonly worktreeSynced: boolean;
```

***

##### Type Literal

```ts
{
  kind: "raced";
  tip: string | null;
}
```

###### kind

```ts
readonly kind: "raced";
```

The ref no longer named `expected`. Nothing moved; `tip` is what it names now.

###### tip

```ts
readonly tip: string | null;
```

***

##### Type Literal

```ts
{
  kind: "worktree-dirty";
  paths: ReadonlyArray<string>;
}
```

###### kind

```ts
readonly kind: "worktree-dirty";
```

`HEAD` is the ref and the checkout holds uncommitted state at these paths. Nothing moved.

###### paths

```ts
readonly paths: ReadonlyArray<string>;
```

***

### PlumbingFactory

```ts
type PlumbingFactory = (indexFile) => Plumbing;
```

How a commit gets its plumbing: called with the index file the staging calls use (a commit's own
temporary file), or `undefined` for the calls that stage nothing. The default is
`makePlumbing({ root, indexFile })`; a test wraps it to act at one exact step of the sequence (a v1
commit landing right after the checkout moved, a process dying right after it).

#### Parameters

##### indexFile

`string` \| `undefined`

#### Returns

[`Plumbing`](/api/session/#plumbing)

***

### RestatementReason

```ts
type RestatementReason = typeof RESTATEMENT_REASONS[number];
```

***

### Violation

```ts
type Violation = 
  | {
  kind: "format";
  path: string;
  reasons: ReadonlyArray<string>;
}
  | {
  existing: string;
  kind: "duplicate";
  path: string;
}
  | {
  kind: "claim-edit";
  path: string;
}
  | {
  kind: "reserved-path";
  path: string;
}
  | {
  cap: number;
  count: number;
  kind: "batch-cap";
}
  | {
  kind: "write-bar";
  path: string;
  reasons: ReadonlyArray<string>;
};
```

The mechanical checks a commit runs before it writes anything. No model call, no I/O: every
check reads the head view and the ops, so a refusal costs nothing and leaves the tree untouched.

#### Union Members

##### Type Literal

```ts
{
  kind: "format";
  path: string;
  reasons: ReadonlyArray<string>;
}
```

***

##### Type Literal

```ts
{
  existing: string;
  kind: "duplicate";
  path: string;
}
```

***

##### Type Literal

```ts
{
  kind: "claim-edit";
  path: string;
}
```

A put over an active path with a different content hash. Claims are never edited in place.

***

##### Type Literal

```ts
{
  kind: "reserved-path";
  path: string;
}
```

`.memhtml/`, `index.html`, `sitemap.xml` under every scope; `areas/arcs/` and `resources/people/`
too unless the scope is `curate`.

***

##### Type Literal

```ts
{
  cap: number;
  count: number;
  kind: "batch-cap";
}
```

***

##### Type Literal

```ts
{
  kind: "write-bar";
  path: string;
  reasons: ReadonlyArray<string>;
}
```

A new record that fails the store's write bar ([writeBarReasons](/api/session/#writebarreasons)): a type the store
refuses, or, under the anchor rule, a record that names nothing it is about. Every scope and
every type is judged; only an archive destination is not, because it is an existing record
moving. Under the anchor rule an `unlabel` that leaves its record with no entity and no
workspace path fails too, and so does a `move` destination that names nothing.

## Variables

### BATCH\_CAP

```ts
const BATCH_CAP: 200 = 200;
```

The most ops one commit carries. A larger batch is two sessions.

***

### DEFAULT\_REF

```ts
const DEFAULT_REF: "refs/heads/main" = "refs/heads/main";
```

***

### GIT\_REPO\_SELECTION\_ENV

```ts
const GIT_REPO_SELECTION_ENV: ReadonlyArray<string>;
```

The variables by which the environment, not `-C`, decides which repository a git call operates
on. Removed from every child for the reason the store removes them: a hook exports an absolute
`GIT_DIR` and a child would silently operate on the hook's repository. The same list as
`packages/store/src/git.ts` `GIT_REPO_SELECTION_ENV`, minus nothing: `GIT_INDEX_FILE` is scrubbed
here too and then set explicitly for the calls that stage into the session index.

***

### HEAD\_SOCKET

```ts
const HEAD_SOCKET: ".memhtml/head.sock" = ".memhtml/head.sock";
```

The head server's Unix domain socket (`memhtml head serve`), as an ignore pattern. The server
passes it to [ensureExcludedQuietly](/api/session/#ensureexcludedquietly) before it binds, the way a session start passes
`.memhtml/sessions/` and a snapshot write `.memhtml/snapshots/`, so a store whose `.gitignore`
predates the server keeps the socket out of `git status` too. The store package's
`V2_IGNORE_PATTERNS` names the same path for a new store's `.gitignore`.

***

### LOG\_MOVED

```ts
const LOG_MOVED: "session.moved" = "session.moved";
```

The failure a writer gets when the log changed between its read and its write.

***

### OverlayOpSchema

```ts
const OverlayOpSchema: Union<readonly [Struct<{
  html: String;
  kind: Literal<"put">;
  path: String;
}>, Struct<{
  html: String;
  kind: Literal<"archive">;
  path: String;
  to: String;
}>, Struct<{
  href: String;
  kind: Literal<"link">;
  path: String;
  rel: String;
}>, Struct<{
  href: String;
  kind: Literal<"unlink">;
  path: String;
  rel: String;
}>, Struct<{
  entity: String;
  kind: Literal<"label">;
  path: String;
}>, Struct<{
  entity: String;
  kind: Literal<"unlabel">;
  path: String;
}>, Struct<{
  html: String;
  kind: Literal<"move">;
  path: String;
  to: String;
}>]>;
```

One `OverlayOp` as JSON: the log's schema, and the head server's for every route that carries a
session's ops (`/v1/neighbors` in `apps/cli/src/head-protocol.ts`), so the log and the wire decode
the same kinds and a kind the log gains is one the wire reads.

***

### RESTATEMENT\_REASONS

```ts
const RESTATEMENT_REASONS: readonly ["same-value", "superseded", "archived"];
```

Why step 3 left a frame-key rival unlinked: it agrees with the put (`same-value`: one value or
one claim, and for two verdicts one ruling), the put carries a `supersedes` edge to it or to
where the batch archives it (`superseded`), or the batch archives it (`archived`). A pair that
meets more than one is reported with the first, in that order.

***

### SESSION\_ID

```ts
const SESSION_ID: RegExp;
```

A session id: one path segment, so it can never leave `.memhtml/sessions/`. Shared with the CLI's usage check.

***

### SESSION\_LOCK\_STALE\_MS

```ts
const SESSION_LOCK_STALE_MS: 30000 = 30_000;
```

How long a lock may go without an mtime refresh before another writer may reclaim it.

***

### SESSION\_PENDING

```ts
const SESSION_PENDING: "session.pending" = "session.pending";
```

The prefix of the failure [replaceOps](/api/session/#replaceops) gets over a log that carries a `pending` commit: the
whole operation is `session.pending: <what to do>`, naming the `session commit` that settles it.

***

### SESSION\_REFS\_PREFIX

```ts
const SESSION_REFS_PREFIX: "refs/memhtml/sessions/" = "refs/memhtml/sessions/";
```

The namespace a session commit once recorded provenance under (`refs/memhtml/sessions/<id>`). No
commit writes it: the `Memhtml-Session` trailer on every session commit names the id, and nothing
read the refs. `pruneSessions` removes the ones a store still carries.

***

### SESSIONS\_DIR

```ts
const SESSIONS_DIR: ".memhtml/sessions" = ".memhtml/sessions";
```

The directory every session's state lives under, relative to the repo root.

***

### UNANCHORED\_REASON

```ts
const UNANCHORED_REASON: "names no system, project, or person: add at least one memhtml-entity (<meta name=\"memhtml-entity\" content=\"system:<name>\"> in the head, an entities value in a write op, or a label op) or write it in a workspace (projects/<slug>/)" = 'names no system, project, or person: add at least one memhtml-entity (<meta name="memhtml-entity" content="system:<name>"> in the head, an entities value in a write op, or a label op) or write it in a workspace (projects/<slug>/)';
```

The reason a record that names nothing gives, worded so the writer (a person, an agent, or the
curator's model reading a refused proposal) knows exactly what to add.

## Functions

### afterDroppedVacates()

```ts
function afterDroppedVacates(
   view, 
   keep, 
   dropped
): object;
```

What a log keeps once `session commit --drop-refused` has taken `dropped` out of it, for each
`archive` or `move` among them.

Its own repoint pairs always go with it: each `link` to its destination and the `unlink` of the
same holder and rel from its source, so the holder keeps its edge to the record where it is,
whatever holds the destination, and no other op on the holder is refused for it.

Two rewrites follow only when neither `view` nor a kept op holds the destination and the record
is still live in `view`, at the vacate's source or, for a chain the round dropped whole (a move
and a second move or an archive of the same record), at the source the chain began from. Each
kept head op at a dropped move's destination is made there instead: a repoint the commit
appended after the move, for a holder the move took, then repoints the holder where it stays.
Every other edge a kept `put` or `link` aims at the destination is aimed back there too: a
successor's `supersedes` edge to the slot of a record whose archive was dropped then names the
live record, which the curator archives later as it does any superseded record, rather than a
file that was never written. A held destination keeps the edges aimed at it, which name the
record that holds it; a record that is not live (an archive of a record the batch put, dropped
with the put) leaves them as they are. So does a vacate whose destination two distinct dropped ops
aim at, and a chain through such a path or through one two dropped ops leave while a dropped op
also aims at it or the log puts there: neither names one record. A path only the version's
record leaves twice, as a move written twice or a move and an archive of one record do, still
names that record; one record moved out and back and on is left as written.

`removed` lists the ops taken, `retargeted` each edge aimed back, and `returned` each op made at
the record's path, all with the vacate they followed; `homes` maps each dropped move's
destination that got the rewrites to that path.

#### Parameters

##### view

`HeadView`

##### keep

readonly `OverlayOp`[]

##### dropped

readonly `OverlayOp`[]

#### Returns

`object`

##### homes

```ts
readonly homes: ReadonlyMap<string, string>;
```

##### keep

```ts
readonly keep: readonly OverlayOp[];
```

##### removed

```ts
readonly removed: readonly VacateFollower[];
```

##### retargeted

```ts
readonly retargeted: readonly VacateFollower[];
```

##### returned

```ts
readonly returned: readonly VacateFollower[];
```

***

### appendOps()

```ts
function appendOps(session, ops): Effect<Session, StorageFailure>;
```

Append to the overlay log and persist it. The returned session is the one to keep using. Refused
with [LOG\_MOVED](/api/session/#log_moved) when the log is no longer the one `session` was read from.

A log that carries a `pending` commit is settled first (`settleStored`): when that commit had
landed, the returned session sits on it (its `baseSha` is the landed commit) and holds only `ops`;
otherwise it keeps its ops and gains `ops`. So an op appended after a commit was killed past its
swap is committed by the next `session commit` rather than cleared with the landed ops.

#### Parameters

##### session

[`Session`](/api/session/#session)

##### ops

readonly `OverlayOp`[]

#### Returns

`Effect`\<[`Session`](/api/session/#session), `StorageFailure`\>

***

### archiveBody()

```ts
function archiveBody(html, archivedAt): string;
```

The archive body: `memhtml-status archived` and `memhtml-archived <instant>` stamped into the
head. `setMeta` splices head lines only, so the article bytes and the content hash are unchanged.

#### Parameters

##### html

`string`

##### archivedAt

`string`

#### Returns

`string`

***

### checkoutLockTarget()

```ts
function checkoutLockTarget(root): string;
```

The checkout lock's target for a repository root (`@memhtml/contracts` `checkout-lock.ts`): the lock
is the directory `<root>/.memhtml/checkout.lock`. `@memhtml/store` computes the same string from
the same constant, which is what makes a session commit and a v1 store write exclude each other.

#### Parameters

##### root

`string`

#### Returns

`string`

***

### commitMessage()

```ts
function commitMessage(
   summary, 
   sessionId, 
   scope?
): string;
```

Subject, blank line, one trailer. The id is flattened so it cannot open a second trailer line.

#### Parameters

##### summary

`string`

##### sessionId

`string`

##### scope?

[`CommitScope`](/api/session/#commitscope) = `"session"`

#### Returns

`string`

***

### commitSession()

```ts
function commitSession(input): Effect<CommitOutcome, StorageFailure | GitFailure>;
```

The commit path, in order. The whole call holds the session's lock (`withSessionLock`), so two
commits of one id cannot interleave and a `session put` cannot rewrite the log mid-commit. Every
step that touches the shared checkout runs under the repository's checkout lock as well, which
every v1 store write holds from its first stage to its commit.

0. Recovery (`settlePending`, `settle.ts`). A log carrying `pending` names a commit an earlier
   call built and may have landed before it was killed. Reachable from the ref: answer `committed`
   for it with `recovered: true` and clear the log. Not reachable: undo whatever checkout update it
   made, drop `pending` from the log, and continue. Every append settles `pending` the same way
   before it writes (`appendOps`), so the ops a landed `pending` leaves are exactly the ones it
   committed, and clearing them loses nothing.
1. `validateOps(head, ops, { scope })`; any violation is `refused` and writes nothing.
2. The ref must be at the version that was validated. A ref that moved past `head.sha` is
   `rebase-needed` whether or not the changed paths overlap: duplicate hashes and frame keys are
   collisions between different paths, so a path-disjoint advance can still carry one, and only
   a head rebuilt at the tip can judge it. `overlapping` still names the path collisions.
3. Stage the ops over the head. A head op the validator passed as a change whose editor hands
   back its file unchanged (`changedNothing`) is `refused` as a `format` violation and
   writes nothing, so an editor that disagrees with the reader cannot land an empty commit. Then
   frame-key contradictions, marked on the new file's head before hashing. A rival that agrees
   with the put, that the put supersedes, or that the batch archives is reported under
   `restatements` and left unlinked.
4. Blobs, index, tree, commit object, through a temporary index file removed on every outcome.
5. Write `pending: commit` to the log, BEFORE anything shared is touched: from here on a process
   killed at any instant leaves a log the next call settles in step 0.
6. [landOnRef](/api/session/#landonref) under the checkout lock: the ref must still be at step 2's value (else
   `rebase-needed`); when `HEAD` is the ref, uncommitted state at a session path is
   `worktree-dirty`, else the checkout follows; then the compare-and-swap, whose loss rolls the
   checkout back and is `rebase-needed`. `worktree-dirty` and `rebase-needed` clear `pending`.
   The checkout follows before the ref moves because the reverse order would leave `HEAD` ahead of
   the index on any failure, and git then reports the session's own files as staged deletions
   that the next `git commit -a` or v1 write commits.
7. Persist the log (base moved to the commit, ops empty, no pending) right after the swap.

#### Parameters

##### input

###### archivedAt?

`string`

The instant archive ops stamp; defaults to now. Explicit so a test can pin it.

###### batchCap?

`number`

The validator's batch cap ([ValidateOptions](/api/session/#validateoptions)); `BATCH_CAP` unless a replay lifts it.

###### checkoutLock?

`CheckoutLockOptions`

The checkout lock's wait; 15 s unless a test shortens it.

###### head

`HeadView` & `object`

###### message

`string`

###### plumbing?

[`PlumbingFactory`](/api/session/#plumbingfactory)

The plumbing; [PlumbingFactory](/api/session/#plumbingfactory). A test seam: the default is the real git plumbing.

###### scope?

[`CommitScope`](/api/session/#commitscope)

The subject's scope and the validator's; `session` unless a curator run says otherwise. Under
`curate` the arcs and people prefixes are writable.

###### session

[`Session`](/api/session/#session)

###### writeBar?

`WriteBarPolicy`

The store's write bar ([ValidateOptions](/api/session/#validateoptions) `writeBar`), judged in step 1. The library
default when omitted, which refuses no type and anchors nothing, so a caller landing on a real
store passes the policy its `memhtml.config.json` states.

#### Returns

`Effect`\<[`CommitOutcome`](/api/session/#commitoutcome), `StorageFailure` \| [`GitFailure`](/api/session/#gitfailure)\>

***

### commitSubject()

```ts
function commitSubject(summary, scope?): string;
```

`memhtml(<scope>): <summary>`, one line, capped, never empty.

#### Parameters

##### summary

`string`

##### scope?

[`CommitScope`](/api/session/#commitscope) = `"session"`

#### Returns

`string`

***

### decodeLog()

```ts
function decodeLog(raw, id): Effect<Omit<Session, "root">, StorageFailure>;
```

A log's text as the session state it holds, for the session `id` it must name. Not JSON is
`session.parse`, not a log is `session.decode`, another id's log is `session.id-mismatch`.

#### Parameters

##### raw

`string`

##### id

`string`

#### Returns

`Effect`\<`Omit`\<[`Session`](/api/session/#session), `"root"`\>, `StorageFailure`\>

***

### ensureExcluded()

```ts
function ensureExcluded(root, patterns): Effect<ExcludeResult, StorageFailure | GitFailure>;
```

Append every pattern of `patterns` that `.git/info/exclude` does not already carry, as a line
each. Idempotent: a second call with the same patterns appends nothing. A pattern is matched as a
whole trimmed line, so `.memhtml/sessions/` present as a comment does not count as present.

#### Parameters

##### root

`string`

##### patterns

readonly `string`[]

#### Returns

`Effect`\<[`ExcludeResult`](/api/session/#excluderesult), `StorageFailure` \| [`GitFailure`](/api/session/#gitfailure)\>

***

### ensureExcludedQuietly()

```ts
function ensureExcludedQuietly(root, patterns): Effect<ExcludeResult | null>;
```

[ensureExcluded](/api/session/#ensureexcluded) as a best effort: a failure is logged and the caller proceeds, because an
ignore rule that could not be written is a noisier `git status`, and the write it was guarding is
still the right write.

#### Parameters

##### root

`string`

##### patterns

readonly `string`[]

#### Returns

`Effect`\<[`ExcludeResult`](/api/session/#excluderesult) \| `null`\>

***

### gitPathOf()

```ts
function gitPathOf(root, relative): Effect<string, GitFailure>;
```

Where git keeps `relative` for this repository (`rev-parse --git-path`), absolute. `info/exclude`
of a linked worktree lives in the common directory and `.git` may be a file, so the path is asked
of git rather than spelled as `<root>/.git/<relative>`.

#### Parameters

##### root

`string`

##### relative

`string`

#### Returns

`Effect`\<`string`, [`GitFailure`](/api/session/#gitfailure)\>

***

### isReservedPath()

```ts
function isReservedPath(path, scope?): boolean;
```

True when a writer under `scope` may not write the path.

#### Parameters

##### path

`string`

##### scope?

[`CommitScope`](/api/session/#commitscope) = `"session"`

#### Returns

`boolean`

***

### isSessionId()

```ts
function isSessionId(id): boolean;
```

#### Parameters

##### id

`string`

#### Returns

`boolean`

***

### landOnRef()

```ts
function landOnRef(input): Effect<Landing, StorageFailure | GitFailure>;
```

Move `ref` from `expected` to `to` with the checkout following, as one critical section under the
repository's checkout lock (`withCheckoutLock`), so no other writer of the shared index (a v1
store write in any process, another session's commit, a curate merge) runs between the checkout
moving and the ref moving:

1. The ref must still name `expected`; otherwise `raced` before anything is touched.
2. When `HEAD` is the ref, a path in `paths` with uncommitted state is `worktree-dirty`; otherwise
   `read-tree -m -u from to` brings the shared index and working tree to `to` (a two-tree merge,
   so uncommitted edits at other paths survive) while the ref still names `from`.
3. `update-ref ref to expected`, the compare-and-swap. `raced` (a writer that does not take the
   lock moved it) rolls the checkout back to `from`, and so does a git failure that left the ref
   where it was.

The three steps are uninterruptible once the lock is held, so an interrupt cannot leave the
checkout one step ahead of a ref that never moved; only waiting for the lock can be interrupted.

#### Parameters

##### input

###### expected

`string` \| `null`

What the ref must still name: the compare-and-swap's old value; `null` when it must not exist.

###### from

`string`

The commit the checkout holds now: `expected`, or a new session's base when the ref is unborn.

###### git

[`Plumbing`](/api/session/#plumbing)

###### lock?

`CheckoutLockOptions`

###### paths

readonly `string`[]

The paths `from..to` changes; uncommitted state at any of them is `worktree-dirty`.

###### ref

`string`

###### root

`string`

###### to

`string`

#### Returns

`Effect`\<[`Landing`](/api/session/#landing), `StorageFailure` \| [`GitFailure`](/api/session/#gitfailure)\>

***

### makePlumbing()

```ts
function makePlumbing(input): Plumbing;
```

The plumbing over `root`. `indexFile` is the index the staging calls (`readTree`, `updateIndexAdd`,
`updateIndexRemove`, `writeTree`) read and write: a commit's own temporary file
(`withTemporaryIndex` in `commit.ts`). Without one those four fail with a `GitFailure` before any
child runs, so a caller that only reads refs, moves a ref, or updates the checkout can never stage
into the shared index by accident.

#### Parameters

##### input

###### indexFile?

`string`

###### root

`string`

#### Returns

[`Plumbing`](/api/session/#plumbing)

***

### missingRepoints()

```ts
function missingRepoints(
   view, 
   ops, 
   scope?, 
   cap?
): object;
```

The repoints a log already written lacks against `view`, the version a commit lands it on, made
to go after the log rather than inside it, so the log is only ever appended to (§D2).

They are [withRepoints](/api/session/#withrepoints)' pairs with each holder at the path the log moves it to, since an
edit after a move is made at the destination. Every pair is judged by `validateOps` as part of the
log, whose dangling-edge rule runs after the whole batch, so a pair after the archive it repoints
for passes like one before it. A log that is already complete gains nothing.

`overflow` names, as a `format` violation on its source, each archive or move whose pairs would
take a log within `cap` over it, taking the pairs in log order while they fit; when it is not
empty the commit should write nothing and refuse with it, so `--drop-refused` can drop those ops,
with what [afterDroppedVacates](/api/session/#afterdroppedvacates) takes after them, and land the rest. A log already over
`cap` gains nothing and names nothing: its own `batch-cap` refusal stands.

#### Parameters

##### view

`HeadView`

##### ops

readonly `OverlayOp`[]

##### scope?

[`CommitScope`](/api/session/#commitscope) = `"session"`

##### cap?

`number` = `BATCH_CAP`

#### Returns

`object`

##### ops

```ts
readonly ops: readonly OverlayOp[];
```

##### overflow

```ts
readonly overflow: readonly Violation[];
```

***

### pruneSessions()

```ts
function pruneSessions(input): Effect<PruneReport, StorageFailure | GitFailure>;
```

#### Parameters

##### input

###### dryRun?

`boolean`

###### now?

`number`

The instant ages are measured from; now unless a test pins it.

###### olderThanMs

`number`

###### root

`string`

#### Returns

`Effect`\<[`PruneReport`](/api/session/#prunereport), `StorageFailure` \| [`GitFailure`](/api/session/#gitfailure)\>

***

### rebaseSession()

```ts
function rebaseSession(session, onto): Session;
```

Move the session's base to a newer version, keeping its ops. Pure: the log on disk moves when the
caller next appends or commits, and validation of the kept ops happens at that commit.

#### Parameters

##### session

[`Session`](/api/session/#session)

##### onto

`HeadView` & `object`

#### Returns

[`Session`](/api/session/#session)

***

### replaceOps()

```ts
function replaceOps(session, ops): Effect<Session, StorageFailure>;
```

Replace the overlay log's ops with `ops`, keeping the base and the ref. The one way ops leave a log
short of a commit: `session commit --drop-refused` takes out the ops a rebase revealed as
unlandable (a `duplicate` or a `claim-edit` against what `main` gained meanwhile) so the rest can
land, and writes back the kept ops with the repoints a drop took from them appended again
(`missingRepoints`). Short of this, ops are only ever appended. Refused with [LOG\_MOVED](/api/session/#log_moved),
like every other log write, when the log is no longer the one `session` was read from, so a put
or exec that landed meanwhile is never lost.

Also refused, with `session.pending: ...` ([SESSION\_PENDING](/api/session/#session_pending)) and nothing written, when the
stored log carries a `pending` commit. `ops` was computed from ops that commit built, and when it
landed they are on the ref, so writing them back would commit them twice (or record them as
dropped); the commit that settles `pending` decides that, and the failure names it. A commit settles
`pending` before it refuses anything, so this is reached only when another commit of the same
session ran between the refusal and the replacement.

#### Parameters

##### session

[`Session`](/api/session/#session)

##### ops

readonly `OverlayOp`[]

#### Returns

`Effect`\<[`Session`](/api/session/#session), `StorageFailure`\>

***

### resumeSession()

```ts
function resumeSession(input): Effect<Session, StorageFailure>;
```

Reopen a session from its log. A missing or malformed log is a `StorageFailure`.

#### Parameters

##### input

###### id

`string`

###### root

`string`

#### Returns

`Effect`\<[`Session`](/api/session/#session), `StorageFailure`\>

***

### saveSession()

```ts
function saveSession(session): Effect<void, StorageFailure>;
```

Persist a rebased or otherwise changed session without appending. Takes the session's lock, and is
refused with [LOG\_MOVED](/api/session/#log_moved) when the log's ops are no longer `session`'s: a rebase keeps every
op it read, so an op appended since would otherwise be dropped.

#### Parameters

##### session

[`Session`](/api/session/#session)

#### Returns

`Effect`\<`void`, `StorageFailure`\>

***

### sessionStateFile()

```ts
function sessionStateFile(root, id): string;
```

#### Parameters

##### root

`string`

##### id

`string`

#### Returns

`string`

***

### startSession()

```ts
function startSession(input): Effect<Session, StorageFailure | GitFailure>;
```

Start a session on a base version. Creates the state directory, checks that the base names a
commit (`rev-parse`; a `GitFailure` when it does not), and writes an empty log. Fails when the id
is not a single path segment.

An id that already has a log is refused with `session.exists` unless `force` is set: a retried
`session start` or two agents choosing one id would otherwise replace an in-progress overlay with
an empty one and report success. `resumeSession` is the way to reopen a log; `force` is the way to
discard one on purpose.

Only the base's `sha` is read (for the check and the log), so a caller that has no head loaded
passes `{ sha }`: the implicit start of `session put` and `session exec` under `MEMHTML_SESSION`
costs a `rev-parse` rather than a head load. A `HeadView` with its sha still fits.

#### Parameters

##### input

###### base

[`SessionBase`](/api/session/#sessionbase)

###### force?

`boolean`

###### id

`string`

###### ref?

`string`

###### root

`string`

#### Returns

`Effect`\<[`Session`](/api/session/#session), `StorageFailure` \| [`GitFailure`](/api/session/#gitfailure)\>

***

### touchedPaths()

```ts
function touchedPaths(op): readonly string[];
```

Every path an op reads or writes, for the disjointness check and the reserved-path check.

#### Parameters

##### op

`OverlayOp`

#### Returns

readonly `string`[]

***

### validateOps()

```ts
function validateOps(
   view, 
   ops, 
   options?
): readonly Violation[];
```

#### Parameters

##### view

`HeadView`

##### ops

readonly `OverlayOp`[]

##### options?

[`ValidateOptions`](/api/session/#validateoptions) = `{}`

#### Returns

readonly [`Violation`](/api/session/#violation)[]

***

### withCheckoutLock()

```ts
function withCheckoutLock<A, E, R>(
   root, 
   effect, 
   options?
): Effect<A, E | StorageFailure, R>;
```

Run `effect` holding the repository's checkout lock: wait up to 15 s for another writer of the
shared index (a v1 write from any process, another session's commit, a curate merge), then fail
with `StorageFailure` `checkout.locked`. Released on every exit. Reentrant within one fiber.

#### Type Parameters

##### A

`A`

##### E

`E`

##### R

`R`

#### Parameters

##### root

`string`

##### effect

`Effect`\<`A`, `E`, `R`\>

##### options?

`CheckoutLockOptions`

#### Returns

`Effect`\<`A`, `E` \| `StorageFailure`, `R`\>

***

### withRepoints()

```ts
function withRepoints(
   view, 
   ops, 
   scope?
): readonly OverlayOp[];
```

The edge repoints a batch of moves and archives needs, completed from the view.

A `move` relocates one record and nothing else, and an `archive` moves one under its
`archive/<YYYY>/` slot, so every live edge that named the old path has to be moved by an `unlink`
of it and a `link` of the same rel to the destination (the slot, for an archive), in the same
batch; `validateOps` refuses a batch that leaves one behind. The pairs are mechanical (the view's
`inbound` index names every holder), so a writer should never have to compute them: `session
exec` completes its harvest with this, the curator's binder completes a proposal with it before
it judges it, `curate collapse` completes the archives its driver adds, and `curate merge`
completes a replay against the tip it lands on. A log already written is completed by
[missingRepoints](/api/session/#missingrepoints) instead, which appends what it lacks.

The repoints go in before the batch's first `move` or `archive`, so a holder the batch itself
moves is edited at its old path and the edit travels with it. A holder the batch archives is left
alone (its archived copy keeps the edge as history, which `validateOps` allows), an edge the batch
already unlinks (at the holder's path, or at the path the batch moved it to) is left to the batch,
and a move's source the batch puts a new record at needs nothing, nor does an archive's source
the batch puts at after the archive (a claim edit, refused whatever the edges). Nor does a source
under `archive/`, or at a path that is not a usable memory path (`activeRecordPathViolation`):
`validateOps` refuses that archive or move, and a pair for it would aim a live edge under
`archive/<YYYY>/archive/`, or out of the archive, at a path no commit writes. A holder `scope` may
not write ([isReservedPath](/api/session/#isreservedpath): an arc or a people record under `session`) is left alone too,
its edge the curator's to repoint; `validateOps` lets an archive leave it and names it on a move.
Edges a batch's own puts or `link`s aim at the old path are not the view's to know; `validateOps`
names them.

#### Parameters

##### view

`HeadView`

##### ops

readonly `OverlayOp`[]

##### scope?

[`CommitScope`](/api/session/#commitscope) = `"session"`

#### Returns

readonly `OverlayOp`[]

***

### withSessionLock()

```ts
function withSessionLock<A, E, R>(
   root, 
   id, 
   effect, 
   options?
): Effect<A, StorageFailure | E, R>;
```

Run `effect` holding the session's exclusive lock. A lock already held fails with
`session.locked` at once rather than waiting: the holder is another writer of the same id, and
the right answer for a double-submitted command is a refusal, not a second commit.

A lock that is compromised while held (its directory removed, or its mtime moved by another
writer that judged it stale, because this holder missed its refreshes for
[SESSION\_LOCK\_STALE\_MS](/api/session/#session_lock_stale_ms)) is logged as a warning when the holder finishes, rather than
proper-lockfile's default of throwing from its refresh timer, which would kill the process
mid-commit: in `head serve` that is every request in flight. The effect runs to its end either
way, as it would have without the lock's loss being noticed; the release that follows touches
nothing another writer took.

#### Type Parameters

##### A

`A`

##### E

`E`

##### R

`R`

#### Parameters

##### root

`string`

##### id

`string`

##### effect

`Effect`\<`A`, `E`, `R`\>

##### options?

[`SessionLockOptions`](/api/session/#sessionlockoptions) = `{}`

#### Returns

`Effect`\<`A`, `StorageFailure` \| `E`, `R`\>

***

### writeBarReasons()

```ts
function writeBarReasons(
   path, 
   html, 
   policy?
): readonly string[];
```

Why a new record falls below the store's write bar, or nothing when it clears it.

The bar has two halves. This is the mechanical one, and its rules are the store's
(`writeBar` in `memhtml.config.json`): a type the store refuses (`refusedTypes`, each with the
reason its refusal gives), and, under `requireAnchor`, a record that does not name what it is
about through at least one `memhtml-entity` or by sitting in a workspace under
`projects/<slug>/`. The other half (a mechanism or a decision, something a future run would look
up) needs a model's judgment and is the writing agent's, stated in the `session put` and
`session exec` help and, for the curator, in its two charters.

The bar holds under every scope and for every type, the curator's writes and tasks included: a
task under `projects/<slug>/tasks/` is anchored by its path, one in the inbox by an entity. An
archive destination is an existing record moving, never a new one, and is not judged. A file that
does not parse answers nothing here, because the format check has already refused it. With the
library default (`DEFAULT_WRITE_BAR`) nothing is refused.

#### Parameters

##### path

`string`

##### html

`string`

##### policy?

`WriteBarPolicy` = `DEFAULT_WRITE_BAR`

#### Returns

readonly `string`[]