@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
Section titled “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
Section titled “Classes”GitFailure
Section titled “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
Section titled “Extends”GitFailure_base
Constructors
Section titled “Constructors”Constructor
Section titled “Constructor”new GitFailure(...args): GitFailure;Parameters
Section titled “Parameters”…[props: object, options?: MakeOptions]
Returns
Section titled “Returns”Inherited from
Section titled “Inherited from”GitFailure_base.constructor
Properties
Section titled “Properties”command
Section titled “command”readonly command: string;Inherited from
Section titled “Inherited from”GitFailure_base.command
exitCode
Section titled “exitCode”readonly exitCode: number | null;The process exit code, or null when the process never started.
Inherited from
Section titled “Inherited from”GitFailure_base.exitCode
Interfaces
Section titled “Interfaces”ExcludeResult
Section titled “ExcludeResult”What one call did.
Properties
Section titled “Properties”readonly added: readonly string[];The patterns this call appended; empty when every one was already present.
readonly file: string;The exclude file, absolute.
Plumbing
Section titled “Plumbing”Methods
Section titled “Methods”commitTree()
Section titled “commitTree()”commitTree( tree, parents, message): Effect<string, GitFailure>;commit-tree, message on stdin; the commit sha.
Parameters
Section titled “Parameters”string
parents
Section titled “parents”readonly string[]
message
Section titled “message”string
Returns
Section titled “Returns”Effect<string, GitFailure>
deleteRefs()
Section titled “deleteRefs()”deleteRefs(refs): Effect<void, GitFailure>;update-ref --stdin with one delete per ref, as one transaction; nothing when refs is empty.
Parameters
Section titled “Parameters”readonly string[]
Returns
Section titled “Returns”Effect<void, GitFailure>
diffTreePaths()
Section titled “diffTreePaths()”diffTreePaths(from, to): Effect<readonly string[], GitFailure>;diff-tree -r --name-only --no-renames -z from to.
Parameters
Section titled “Parameters”string
string
Returns
Section titled “Returns”Effect<readonly string[], GitFailure>
dirtyPaths()
Section titled “dirtyPaths()”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
Section titled “Parameters”readonly string[]
Returns
Section titled “Returns”Effect<readonly string[], GitFailure>
forEachRef()
Section titled “forEachRef()”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
Section titled “Parameters”prefix
Section titled “prefix”string
Returns
Section titled “Returns”Effect<readonly RefEntry[], GitFailure>
hashObjectWrite()
Section titled “hashObjectWrite()”hashObjectWrite(bytes): Effect<string, GitFailure>;hash-object -w --stdin; the blob sha.
Parameters
Section titled “Parameters”Uint8Array
Returns
Section titled “Returns”Effect<string, GitFailure>
headRef()
Section titled “headRef()”headRef(): Effect<string | null, GitFailure>;The branch HEAD points at (refs/heads/...), or null when detached. Shared index.
Returns
Section titled “Returns”Effect<string | null, GitFailure>
isAncestor()
Section titled “isAncestor()”isAncestor(sha, ref): Effect<boolean, GitFailure>;merge-base --is-ancestor sha ref: true when sha is reachable from ref.
Parameters
Section titled “Parameters”string
string
Returns
Section titled “Returns”Effect<boolean, GitFailure>
readTree()
Section titled “readTree()”readTree(sha): Effect<void, GitFailure>;read-tree <sha> into the session index file.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Effect<void, GitFailure>
readTreeIntoWorktree()
Section titled “readTreeIntoWorktree()”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
Section titled “Parameters”string
string
Returns
Section titled “Returns”Effect<void, GitFailure>
revParse()
Section titled “revParse()”revParse(ref): Effect<string | null, GitFailure>;The commit a ref resolves to, or null when it does not resolve.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Effect<string | null, GitFailure>
updateIndexAdd()
Section titled “updateIndexAdd()”updateIndexAdd(entries): Effect<void, GitFailure>;update-index --add --index-info, one child for the whole batch.
Parameters
Section titled “Parameters”entries
Section titled “entries”readonly object[]
Returns
Section titled “Returns”Effect<void, GitFailure>
updateIndexRemove()
Section titled “updateIndexRemove()”updateIndexRemove(paths): Effect<void, GitFailure>;update-index --force-remove -z --stdin, one child for the whole batch.
Parameters
Section titled “Parameters”readonly string[]
Returns
Section titled “Returns”Effect<void, GitFailure>
updateRef()
Section titled “updateRef()”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
Section titled “Parameters”string
string
expected
Section titled “expected”string | null
Returns
Section titled “Returns”Effect<"updated" | "raced", GitFailure>
writeTree()
Section titled “writeTree()”writeTree(): Effect<string, GitFailure>;write-tree over the session index file; the tree sha.
Returns
Section titled “Returns”Effect<string, GitFailure>
PruneCount
Section titled “PruneCount”What one category of state amounts to: removed, or under dryRun, would be.
Properties
Section titled “Properties”readonly count: number;PruneReport
Section titled “PruneReport”Properties
Section titled “Properties”readonly busy: readonly string[];Ids whose session lock a live writer held: nothing of theirs was read or removed.
cutoff
Section titled “cutoff”readonly cutoff: string;Everything last written before this instant is a candidate.
dryRun
Section titled “dryRun”readonly dryRun: boolean;indexes
Section titled “indexes”readonly indexes: PruneCount & object;Type Declaration
Section titled “Type Declaration”readonly bytes: number;readonly locks: PruneCount;readonly logs: PruneCount;olderThanMs
Section titled “olderThanMs”readonly olderThanMs: number;readonly refs: PruneCount;unlanded
Section titled “unlanded”readonly unlanded: readonly UnlandedSession[];Logs older than the cutoff that still hold ops, oldest first. Kept.
unreadable
Section titled “unreadable”readonly unreadable: readonly string[];Logs that do not decode as a session log: kept, for an operator to read.
RefEntry
Section titled “RefEntry”One row of Plumbing.forEachRef.
Properties
Section titled “Properties”committedAt
Section titled “committedAt”readonly committedAt: number | null;readonly ref: string;readonly sha: string;Session
Section titled “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, 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 and
nothing is written, so the caller reads it again and decides. The ops are what is compared, not
the base: 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 settles it first, and replaceOps refuses.
Properties
Section titled “Properties”baseSha
Section titled “baseSha”readonly baseSha: string;readonly id: string;readonly ops: readonly OverlayOp[];pending?
Section titled “pending?”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).
readonly ref: string;refs/heads/main by default; refs/heads/curate/<date> for a curator.
readonly root: string;The repository root. Not persisted: the log is found by root plus id, so it cannot disagree.
SessionBase
Section titled “SessionBase”The version startSession starts a session on. Only its sha is read, so a bare { sha }
fits and so does a HeadView.
Properties
Section titled “Properties”readonly sha: string;SessionLockOptions
Section titled “SessionLockOptions”Test seams for withSessionLock.
Properties
Section titled “Properties”updateMs?
Section titled “updateMs?”readonly updateMs?: number;How often the holder refreshes the lock’s mtime, in milliseconds; proper-lockfile’s default (half of SESSION_LOCK_STALE_MS) unless a test shortens it to see a refresh happen.
UnlandedSession
Section titled “UnlandedSession”One log that still holds ops: never removed.
Properties
Section titled “Properties”readonly ageMs: number;readonly id: string;modifiedAt
Section titled “modifiedAt”readonly modifiedAt: string;The log’s last write.
readonly ops: number;pending
Section titled “pending”readonly pending: string | null;A commit an earlier call built and may have landed; session commit settles it.
VacateFollower
Section titled “VacateFollower”One op afterDroppedVacates took or rewrote, with the archive or move it followed.
Properties
Section titled “Properties”detail
Section titled “detail”readonly detail: string;The edge’s rel, or the entity a label or unlabel names.
readonly home?: string;The path the record still has, for an op made there or an edge aimed back at it.
readonly kind: "link" | "put" | "archive" | "unlink" | "label" | "unlabel" | "move";readonly path: string;The op’s path as the log held it.
vacate
Section titled “vacate”readonly vacate: OverlayOp;ValidateOptions
Section titled “ValidateOptions”The options validateOps and isReservedPath take. scope defaults to session.
Properties
Section titled “Properties”batchCap?
Section titled “batchCap?”readonly batchCap?: number;The most ops the batch may carry; 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?
Section titled “scope?”readonly scope?: CommitScope;writeBar?
Section titled “writeBar?”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
Section titled “Type Aliases”CommitOutcome
Section titled “CommitOutcome”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
Section titled “Union Members”Type Literal
Section titled “Type Literal”{ 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
Section titled “contradictions”readonly contradictions: ReadonlyArray<{ against: string; path: string;}>;Each put step 3 linked to a frame-key rival (against, its destination when moved).
readonly kind: "committed";readonly paths: ReadonlyArray<string>;recovered
Section titled “recovered”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
Section titled “restatements”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).
readonly sha: string;worktreeSynced
Section titled “worktreeSynced”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
Section titled “Type Literal”{ kind: "rebase-needed"; mainSha: string; overlapping: ReadonlyArray<string>;}Type Literal
Section titled “Type Literal”{ kind: "refused"; violations: ReadonlyArray<Violation>;}Type Literal
Section titled “Type Literal”{ kind: "worktree-dirty"; paths: ReadonlyArray<string>;}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.
readonly paths: ReadonlyArray<string>;CommitScope
Section titled “CommitScope”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
Section titled “Landing”type Landing = | { kind: "landed"; worktreeSynced: boolean;} | { kind: "raced"; tip: string | null;} | { kind: "worktree-dirty"; paths: ReadonlyArray<string>;};What landOnRef did.
Union Members
Section titled “Union Members”Type Literal
Section titled “Type Literal”{ kind: "landed"; worktreeSynced: boolean;}readonly kind: "landed";The ref moved to to; the checkout moved with it when worktreeSynced.
worktreeSynced
Section titled “worktreeSynced”readonly worktreeSynced: boolean;Type Literal
Section titled “Type Literal”{ kind: "raced"; tip: string | null;}readonly kind: "raced";The ref no longer named expected. Nothing moved; tip is what it names now.
readonly tip: string | null;Type Literal
Section titled “Type Literal”{ kind: "worktree-dirty"; paths: ReadonlyArray<string>;}readonly kind: "worktree-dirty";HEAD is the ref and the checkout holds uncommitted state at these paths. Nothing moved.
readonly paths: ReadonlyArray<string>;PlumbingFactory
Section titled “PlumbingFactory”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
Section titled “Parameters”indexFile
Section titled “indexFile”string | undefined
Returns
Section titled “Returns”RestatementReason
Section titled “RestatementReason”type RestatementReason = typeof RESTATEMENT_REASONS[number];Violation
Section titled “Violation”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
Section titled “Union Members”Type Literal
Section titled “Type Literal”{ kind: "format"; path: string; reasons: ReadonlyArray<string>;}Type Literal
Section titled “Type Literal”{ existing: string; kind: "duplicate"; path: string;}Type Literal
Section titled “Type Literal”{ kind: "claim-edit"; path: string;}A put over an active path with a different content hash. Claims are never edited in place.
Type Literal
Section titled “Type Literal”{ 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
Section titled “Type Literal”{ cap: number; count: number; kind: "batch-cap";}Type Literal
Section titled “Type Literal”{ kind: "write-bar"; path: string; reasons: ReadonlyArray<string>;}A new record that fails the store’s write bar (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
Section titled “Variables”BATCH_CAP
Section titled “BATCH_CAP”const BATCH_CAP: 200 = 200;The most ops one commit carries. A larger batch is two sessions.
DEFAULT_REF
Section titled “DEFAULT_REF”const DEFAULT_REF: "refs/heads/main" = "refs/heads/main";GIT_REPO_SELECTION_ENV
Section titled “GIT_REPO_SELECTION_ENV”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
Section titled “HEAD_SOCKET”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 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
Section titled “LOG_MOVED”const LOG_MOVED: "session.moved" = "session.moved";The failure a writer gets when the log changed between its read and its write.
OverlayOpSchema
Section titled “OverlayOpSchema”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
Section titled “RESTATEMENT_REASONS”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
Section titled “SESSION_ID”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
Section titled “SESSION_LOCK_STALE_MS”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
Section titled “SESSION_PENDING”const SESSION_PENDING: "session.pending" = "session.pending";The prefix of the failure 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
Section titled “SESSION_REFS_PREFIX”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
Section titled “SESSIONS_DIR”const SESSIONS_DIR: ".memhtml/sessions" = ".memhtml/sessions";The directory every session’s state lives under, relative to the repo root.
UNANCHORED_REASON
Section titled “UNANCHORED_REASON”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
Section titled “Functions”afterDroppedVacates()
Section titled “afterDroppedVacates()”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
Section titled “Parameters”HeadView
readonly OverlayOp[]
dropped
Section titled “dropped”readonly OverlayOp[]
Returns
Section titled “Returns”object
readonly homes: ReadonlyMap<string, string>;readonly keep: readonly OverlayOp[];removed
Section titled “removed”readonly removed: readonly VacateFollower[];retargeted
Section titled “retargeted”readonly retargeted: readonly VacateFollower[];returned
Section titled “returned”readonly returned: readonly VacateFollower[];appendOps()
Section titled “appendOps()”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 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
Section titled “Parameters”session
Section titled “session”readonly OverlayOp[]
Returns
Section titled “Returns”Effect<Session, StorageFailure>
archiveBody()
Section titled “archiveBody()”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
Section titled “Parameters”string
archivedAt
Section titled “archivedAt”string
Returns
Section titled “Returns”string
checkoutLockTarget()
Section titled “checkoutLockTarget()”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
Section titled “Parameters”string
Returns
Section titled “Returns”string
commitMessage()
Section titled “commitMessage()”function commitMessage( summary, sessionId, scope?): string;Subject, blank line, one trailer. The id is flattened so it cannot open a second trailer line.
Parameters
Section titled “Parameters”summary
Section titled “summary”string
sessionId
Section titled “sessionId”string
scope?
Section titled “scope?”CommitScope = "session"
Returns
Section titled “Returns”string
commitSession()
Section titled “commitSession()”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.
- Recovery (
settlePending,settle.ts). A log carryingpendingnames a commit an earlier call built and may have landed before it was killed. Reachable from the ref: answercommittedfor it withrecovered: trueand clear the log. Not reachable: undo whatever checkout update it made, droppendingfrom the log, and continue. Every append settlespendingthe same way before it writes (appendOps), so the ops a landedpendingleaves are exactly the ones it committed, and clearing them loses nothing. validateOps(head, ops, { scope }); any violation isrefusedand writes nothing.- The ref must be at the version that was validated. A ref that moved past
head.shaisrebase-neededwhether 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.overlappingstill names the path collisions. - Stage the ops over the head. A head op the validator passed as a change whose editor hands
back its file unchanged (
changedNothing) isrefusedas aformatviolation 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 underrestatementsand left unlinked. - Blobs, index, tree, commit object, through a temporary index file removed on every outcome.
- Write
pending: committo 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. - landOnRef under the checkout lock: the ref must still be at step 2’s value (else
rebase-needed); whenHEADis the ref, uncommitted state at a session path isworktree-dirty, else the checkout follows; then the compare-and-swap, whose loss rolls the checkout back and isrebase-needed.worktree-dirtyandrebase-neededclearpending. The checkout follows before the ref moves because the reverse order would leaveHEADahead of the index on any failure, and git then reports the session’s own files as staged deletions that the nextgit commit -aor v1 write commits. - Persist the log (base moved to the commit, ops empty, no pending) right after the swap.
Parameters
Section titled “Parameters”archivedAt?
Section titled “archivedAt?”string
The instant archive ops stamp; defaults to now. Explicit so a test can pin it.
batchCap?
Section titled “batchCap?”number
The validator’s batch cap (ValidateOptions); BATCH_CAP unless a replay lifts it.
checkoutLock?
Section titled “checkoutLock?”CheckoutLockOptions
The checkout lock’s wait; 15 s unless a test shortens it.
HeadView & object
message
Section titled “message”string
plumbing?
Section titled “plumbing?”The plumbing; PlumbingFactory. A test seam: the default is the real git plumbing.
scope?
Section titled “scope?”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
Section titled “session”writeBar?
Section titled “writeBar?”WriteBarPolicy
The store’s write bar (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
Section titled “Returns”Effect<CommitOutcome, StorageFailure | GitFailure>
commitSubject()
Section titled “commitSubject()”function commitSubject(summary, scope?): string;memhtml(<scope>): <summary>, one line, capped, never empty.
Parameters
Section titled “Parameters”summary
Section titled “summary”string
scope?
Section titled “scope?”CommitScope = "session"
Returns
Section titled “Returns”string
decodeLog()
Section titled “decodeLog()”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
Section titled “Parameters”string
string
Returns
Section titled “Returns”Effect<Omit<Session, "root">, StorageFailure>
ensureExcluded()
Section titled “ensureExcluded()”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
Section titled “Parameters”string
patterns
Section titled “patterns”readonly string[]
Returns
Section titled “Returns”Effect<ExcludeResult, StorageFailure | GitFailure>
ensureExcludedQuietly()
Section titled “ensureExcludedQuietly()”function ensureExcludedQuietly(root, patterns): Effect<ExcludeResult | null>;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
Section titled “Parameters”string
patterns
Section titled “patterns”readonly string[]
Returns
Section titled “Returns”Effect<ExcludeResult | null>
gitPathOf()
Section titled “gitPathOf()”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
Section titled “Parameters”string
relative
Section titled “relative”string
Returns
Section titled “Returns”Effect<string, GitFailure>
isReservedPath()
Section titled “isReservedPath()”function isReservedPath(path, scope?): boolean;True when a writer under scope may not write the path.
Parameters
Section titled “Parameters”string
scope?
Section titled “scope?”CommitScope = "session"
Returns
Section titled “Returns”boolean
isSessionId()
Section titled “isSessionId()”function isSessionId(id): boolean;Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”boolean
landOnRef()
Section titled “landOnRef()”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:
- The ref must still name
expected; otherwiseracedbefore anything is touched. - When
HEADis the ref, a path inpathswith uncommitted state isworktree-dirty; otherwiseread-tree -m -u from tobrings the shared index and working tree toto(a two-tree merge, so uncommitted edits at other paths survive) while the ref still namesfrom. update-ref ref to expected, the compare-and-swap.raced(a writer that does not take the lock moved it) rolls the checkout back tofrom, 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
Section titled “Parameters”expected
Section titled “expected”string | null
What the ref must still name: the compare-and-swap’s old value; null when it must not exist.
string
The commit the checkout holds now: expected, or a new session’s base when the ref is unborn.
CheckoutLockOptions
readonly string[]
The paths from..to changes; uncommitted state at any of them is worktree-dirty.
string
string
string
Returns
Section titled “Returns”Effect<Landing, StorageFailure | GitFailure>
makePlumbing()
Section titled “makePlumbing()”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
Section titled “Parameters”indexFile?
Section titled “indexFile?”string
string
Returns
Section titled “Returns”missingRepoints()
Section titled “missingRepoints()”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’ 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 takes after them, and land the rest. A log already over
cap gains nothing and names nothing: its own batch-cap refusal stands.
Parameters
Section titled “Parameters”HeadView
readonly OverlayOp[]
scope?
Section titled “scope?”CommitScope = "session"
number = BATCH_CAP
Returns
Section titled “Returns”object
readonly ops: readonly OverlayOp[];overflow
Section titled “overflow”readonly overflow: readonly Violation[];pruneSessions()
Section titled “pruneSessions()”function pruneSessions(input): Effect<PruneReport, StorageFailure | GitFailure>;Parameters
Section titled “Parameters”dryRun?
Section titled “dryRun?”boolean
number
The instant ages are measured from; now unless a test pins it.
olderThanMs
Section titled “olderThanMs”number
string
Returns
Section titled “Returns”Effect<PruneReport, StorageFailure | GitFailure>
rebaseSession()
Section titled “rebaseSession()”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
Section titled “Parameters”session
Section titled “session”HeadView & object
Returns
Section titled “Returns”replaceOps()
Section titled “replaceOps()”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,
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) 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
Section titled “Parameters”session
Section titled “session”readonly OverlayOp[]
Returns
Section titled “Returns”Effect<Session, StorageFailure>
resumeSession()
Section titled “resumeSession()”function resumeSession(input): Effect<Session, StorageFailure>;Reopen a session from its log. A missing or malformed log is a StorageFailure.
Parameters
Section titled “Parameters”string
string
Returns
Section titled “Returns”Effect<Session, StorageFailure>
saveSession()
Section titled “saveSession()”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 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
Section titled “Parameters”session
Section titled “session”Returns
Section titled “Returns”Effect<void, StorageFailure>
sessionStateFile()
Section titled “sessionStateFile()”function sessionStateFile(root, id): string;Parameters
Section titled “Parameters”string
string
Returns
Section titled “Returns”string
startSession()
Section titled “startSession()”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
Section titled “Parameters”force?
Section titled “force?”boolean
string
string
string
Returns
Section titled “Returns”Effect<Session, StorageFailure | GitFailure>
touchedPaths()
Section titled “touchedPaths()”function touchedPaths(op): readonly string[];Every path an op reads or writes, for the disjointness check and the reserved-path check.
Parameters
Section titled “Parameters”OverlayOp
Returns
Section titled “Returns”readonly string[]
validateOps()
Section titled “validateOps()”function validateOps( view, ops, options?): readonly Violation[];Parameters
Section titled “Parameters”HeadView
readonly OverlayOp[]
options?
Section titled “options?”ValidateOptions = {}
Returns
Section titled “Returns”readonly Violation[]
withCheckoutLock()
Section titled “withCheckoutLock()”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
Section titled “Type Parameters”A
E
R
Parameters
Section titled “Parameters”string
effect
Section titled “effect”Effect<A, E, R>
options?
Section titled “options?”CheckoutLockOptions
Returns
Section titled “Returns”Effect<A, E | StorageFailure, R>
withRepoints()
Section titled “withRepoints()”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 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: 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 links aim at the old path are not the view’s to know; validateOps
names them.
Parameters
Section titled “Parameters”HeadView
readonly OverlayOp[]
scope?
Section titled “scope?”CommitScope = "session"
Returns
Section titled “Returns”readonly OverlayOp[]
withSessionLock()
Section titled “withSessionLock()”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) 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
Section titled “Type Parameters”A
E
R
Parameters
Section titled “Parameters”string
string
effect
Section titled “effect”Effect<A, E, R>
options?
Section titled “options?”SessionLockOptions = {}
Returns
Section titled “Returns”Effect<A, StorageFailure | E, R>
writeBarReasons()
Section titled “writeBarReasons()”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
Section titled “Parameters”string
string
policy?
Section titled “policy?”WriteBarPolicy = DEFAULT_WRITE_BAR
Returns
Section titled “Returns”readonly string[]