Documentation
¶
Overview ¶
Package guardsessions is the local, queryable INDEX of `fak guard` sessions — the durable answer to "which guard sessions are running (or recently ran) on this box, and how do I reference one?". Every `fak guard` launch appends one row (a short stable HANDLE, the trace id, the wrapped agent, pid, cwd, the audit-journal path, and the start time) to an append-only JSONL index; `fak guard sessions` folds and lists it, and Resolve turns a short prefix (of the handle OR the trace id) into the one session it names — so an operator can say `fak guard sessions <prefix>` instead of scraping Slack or grepping the outbox for the nonce.
The package is pure and stdlib-only over an injected filesystem path: Record appends, Load folds the append-only log to the latest row per handle, and Resolve does the prefix/handle/trace match. It imports nothing internal, off the hot path — the guard start pays one small append, the query side reads the file.
Index ¶
Constants ¶
const IndexFileName = "guard_sessions.jsonl"
IndexFileName is the durable index basename under the registry dir.
const Schema = "fak.guard-session.v1"
Schema tags the index rows so a forward-extended reader can tell a guard-session row from any other JSONL that might share the file.
Variables ¶
var GatewayPublishEpoch = time.Date(2026, 7, 28, 0, 0, 0, 0, time.UTC)
GatewayPublishEpoch is the UTC instant the PRODUCER half of gateway discovery landed (#5400): from that build forward, a `fak guard` launch re-records its row with gateway_url (and the read-scoped bearer) stamped once its listener is actually serving. It exists so a READER can tell the two causes of a missing gateway_url APART instead of guessing at one: a row started BEFORE this instant was written by a fak that had no publisher at all, so the field could not have been set; a row started AFTER it and still missing the field means that session published nothing — it bound no gateway, or its re-record failed. Same empty field, opposite diagnoses.
Functions ¶
func Compact ¶ added in v0.42.0
Compact runs the size-gated fold-and-rewrite over the guard-session index under regDir. It is the best-effort seam Record triggers after an append; a caller may also invoke it directly. Returns the number of superseded lines the fold collapsed (0 when the gate held).
func CompactFile ¶ added in v0.42.0
CompactFile rewrites the append-only guard-session index at path to exactly its folded LoadFile output — one row per handle, newest-start first — when the raw line count has outgrown its folded footprint. The rewrite is LOSSLESS by construction: it writes the same set every reader already computes (foldReader is the shared fold), so a superseded row it drops was already dead weight the read path ignored. It returns the number of superseded lines collapsed (0 when the gate held or nothing was rewritten) and never surfaces an internal I/O error as a failure — a compaction that cannot proceed simply leaves the append-only file in place.
Concurrency: the box is shared — many `fak guard` launches append to this file concurrently — so a naive read-fold-rename can DROP a row appended between the snapshot read and the rename. This is handled the way #3497 relies on a conservative gate plus an atomic rename, and tightened here for the per-append trigger: (1) any bytes appended past the snapshot offset are carried verbatim into the rewrite before the rename, so a row that arrives during the fold survives (LoadFile re-folds them on the next read, so a duplicate or newer handle simply wins there); (2) the temp file has a unique name, so two guard launches that cross the gate at once cannot clobber each other's staging file. A RESIDUAL micro-window remains — a line appended after the tail re-read but before os.Rename is lost — and is NOT fully closed: it is bounded by the conservative gate (a rewrite is rare) and self-heals, since a dropped guard-session row re-records on the session's next lifecycle transition and the atomic rename never exposes a torn file to a reader.
func Handle ¶
Handle derives a short, stable, human-referenceable id for a guard session from its trace id and start time. It is deterministic (same trace+start → same handle) and short enough to type, but seeded by the start instant so two sessions that reuse the same default trace id ("guard") on one box still get distinct handles. The form is "g" + the first 8 hex of sha256(traceID|unixNano) — a git-short-sha feel an operator can prefix-match.
func Record ¶
Record appends one row to the index under regDir, creating the dir and file as needed. Best-effort by contract: a guard launch must never fail because its index append failed, so the caller ignores the returned error for anything but diagnostics. The append is a single Write of one line, so concurrent guard starts interleave at line granularity (O_APPEND) without corrupting rows.
Types ¶
type ResolveResult ¶
type ResolveResult struct {
// Row is the single matched session (valid only when Matched == 1).
Row Row
// Matched is how many sessions the query matched: 0 (none), 1 (unambiguous — Row is
// set), or >1 (ambiguous — Candidates lists them so the caller can report the tie).
Matched int
Candidates []Row
}
ResolveResult is the outcome of resolving a query string against the index.
func Resolve ¶
func Resolve(rows []Row, query string) ResolveResult
Resolve turns a query into the one session it names. The match order is:
- an EXACT handle or trace-id equality (a full id always wins, even if it is also a prefix of a longer one), else
- a case-insensitive PREFIX of the handle or the trace id.
A query matching exactly one session returns Matched==1 with Row set; matching several returns Matched==len(Candidates) so the caller can print the ambiguity; matching none returns Matched==0. An empty query matches nothing. This is the "reference a specific session by a short prefix" resolver the goal asks for.
type Row ¶
type Row struct {
Schema string `json:"schema"`
Handle string `json:"handle"`
TraceID string `json:"trace_id"`
Agent string `json:"agent"`
PID int `json:"pid"`
CWD string `json:"cwd,omitempty"`
AuditPath string `json:"audit,omitempty"`
StartedAt string `json:"started_utc"`
Nonce string `json:"nonce,omitempty"`
// Relaunch fields are the OS-independent contract consumed by the host-crash
// actuator. Interactive distinguishes operator tabs from dispatcher-owned workers.
Interactive bool `json:"interactive,omitempty"`
ResumeHandle string `json:"resume_handle,omitempty"`
Command []string `json:"command,omitempty"`
WindowID string `json:"window_id,omitempty"`
TabID string `json:"tab_id,omitempty"`
GoalState string `json:"goal_state,omitempty"`
LoopState string `json:"loop_state,omitempty"`
EndedAt string `json:"ended_at,omitempty"`
// GatewayURL and Bearer publish the session's live loopback gateway so a second
// process can discover and authenticate to it from the index alone, with no prior
// port knowledge. Bearer is read-scoped: it admits status reads, not control. Both
// are omitempty — a session that has not bound a gateway simply omits them.
GatewayURL string `json:"gateway_url,omitempty"`
Bearer string `json:"bearer,omitempty"`
}
Row is one recorded guard session. Only the fields the index needs are typed; a forward-extended row (extra keys) still decodes. Handle is the short, stable, human-referenceable id; TraceID is the guardTraceID the gateway uses; the rest is provenance an operator wants when picking a session to act on.
func LiveInteractive ¶ added in v0.41.0
LiveInteractive returns actuator-ready sessions whose latest lifecycle row is not a clean-exit tombstone. Dispatched workers and incomplete legacy rows are excluded.
func Load ¶
Load folds the append-only index into the latest row per handle, newest-start first. A missing/unreadable file yields no rows (never an error): an absent index is simply no recorded sessions. Malformed lines and rows carrying the wrong schema are skipped, so a foreign line in the file can never surface as a fake session.
func NewInteractiveRow ¶ added in v0.41.0
func NewInteractiveRow(traceID, agent string, pid int, cwd, auditPath, nonce string, startedAt time.Time, command []string) Row
NewInteractiveRow builds the durable relaunch specification for an operator-owned Guard session. Command is an argv vector, not a shell string, so the actuator can reconstruct it without lossy quoting or command injection.
func NewRow ¶
NewRow builds a fully-populated index row, assigning the derived handle. startedAt is stamped UTC RFC3339; a zero time is treated as now by the caller before this (Record does not read the clock — it stays pure over its inputs).
func (Row) Ended ¶ added in v0.41.0
Ended returns a clean-exit tombstone. A host crash cannot execute this transition, so its latest row remains live for the resurrection watchdog.
func (Row) PredatesGatewayPublish ¶ added in v0.42.0
PredatesGatewayPublish reports whether the row started before the producer existed, so a missing GatewayURL on it is a build fact rather than a session fault. A row whose start time is absent or unparseable counts as predating: an unstamped start is a legacy shape, and attributing it to a live session's publish would be the same wrong guess this distinction removes.
func (Row) WithGateway ¶ added in v0.42.0
WithGateway stamps the published loopback gateway URL and its read-scoped bearer onto the row, returning it for chaining off NewRow. It is the one seam that records how an operator (or a sibling process) reaches a live session's status endpoint.