sessionlog

package
v0.23.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package sessionlog is memcode's episodic memory: a local, append-only record of the high-level causal trail of a session — user messages, assistant messages, meaningful actions (commands, edits, commits), and approvals — written to disk as it happens, independent of the LLM context window. It is NOT a stdout landfill: internal reads/greps and raw tool output stay out (raw mechanics belong in an optional debug trace, not canonical memory).

The doctrine it serves:

working context → fast but lossy
session log     → episodic truth (why/when/under whose approval/side effects)
git             → code-diff truth
claims/memories → distilled doctrine

events.jsonl is the append-only source of truth (replayable); transcript.md is a human-readable rendering written alongside. Readers are deterministic scans — no model, no index — so the agent can self-recall before acting on fuzzy memory.

Index

Constants

View Source
const (
	KindSessionStarted   = "session_started"
	KindUserMessage      = "user_message"
	KindAssistantMessage = "assistant_message"
	KindToolCall         = "tool_call"      // a meaningful action: command, edit, commit — NOT internal reads
	KindApproval         = "approval"       // a user allow/deny decision
	KindToolResult       = "tool_result"    // reserved for an optional debug trace; not written to the canonical log
	KindCompaction       = "compaction"     // older turns summarized in-session (the warm layer's durable record)
	KindPlanPresented    = "plan_presented" // a plan was presented for approval — full text + slug (recoverable via RecentPlans)
	KindPlanCancelled    = "plan_cancelled" // planning was ABANDONED before execution — the ask is still open work
	KindSessionFinished  = "session_finished"
	KindPreferenceSignal = "preference_signal" // a captured user directive — the CANONICAL copy for the prefs reducer
	KindLessonSignal     = "lesson_signal"     // a distilled lesson — the CANONICAL copy for the lessons reducer
	KindContextInlined   = "context_inlined"   // which promoted rules (lesson/pref ids) rode this session's system prompt
	KindAdherence        = "adherence"         // a rule-adherence verdict about TargetSession — the CANONICAL copy for reducer weighting
	KindFacts            = "facts"             // one atomic extracted fact about THIS session (post-session cognition) — indexed by ranked search
)

Record kinds. Keep these stable — they're the on-disk contract.

Variables

View Source
var Ranking = RankingOptions{}

Ranking is the live configuration. The bench mutates it between adapter runs; the product path only ever reads it.

Functions

func HasActions

func HasActions(threads []Thread) bool

HasActions reports whether any thread carries a recorded action (command, edit, approval) — i.e. the session actually did something rather than just exchanging messages. A brand-new session that has only said "hi" has none; that's the cue to recap the previous distinct session instead of this near-empty one.

func IsGitCommitPush

func IsGitCommitPush(input string) bool

IsGitCommitPush reports whether a tool-call input is an actual `git commit`/`git push` INVOCATION — parsed with the shell AST, never a substring match. `rg "git commit"` (the phrase inside a quoted search arg) is NOT a commit; the old substring check flagged it, marking a strand Completed and logging a phantom commit. The input may be a raw command or a bash tool's JSON ({"command":"…"}); both are handled.

func RenderRecap

func RenderRecap(threads []Thread) string

RenderRecap formats threads as a numbered checklist with a drill-deeper hint.

Types

type Digest

type Digest struct {
	Requests []string // user asks in order — the session's sidequests
	Commits  []string // git commit/push commands run
	Tests    int      // verification runs (test/vet/build/lint)
	Edits    int      // file-edit tool calls
}

Digest is a compact, reduced projection of a session's raw events — the form orientation/prediction should consume (NOT raw events.jsonl, which is noisy and private). It answers "where did we leave off": what was asked, what was committed/pushed, how much was tested/edited.

func Reduce

func Reduce(recs []Record) Digest

Reduce projects raw records into a Digest. This is the "session reducer" seam: raw log → compact facts → orientation. Keep it deterministic and lossy.

func (Digest) Lines

func (d Digest) Lines() []string

Lines renders the digest as compact one-liners for an orientation prompt.

type PlanRec

type PlanRec struct {
	Slug    string
	Title   string
	Text    string
	Session string
	TS      time.Time
}

PlanRec is a presented plan recovered from a session log — the full markdown plus its slug and where/when it was presented. The canonical, project-scoped record of a plan (independent of the user-level ~/.memcode/plans store), so a plan is recoverable even if that store is empty.

func RecentPlans

func RecentPlans(root string, limit int) ([]PlanRec, error)

RecentPlans scans every session's log for presented-plan records, newest first, deduped by slug (the most recent revision of each plan wins). limit ≤ 0 means no limit. Records with no slug (legacy) are kept individually. This is the recovery path recall reaches for when the user-level store doesn't have a plan — e.g. a plan from a prior session, a wiped store, or a new machine.

type RankingOptions

type RankingOptions struct {
	FieldWeights bool // per-kind multipliers: facts > compaction > user > assistant > tool
	RM3          bool // pseudo-relevance feedback query expansion (skipped on exact-tier hits)
	Adjacency    bool // a strong turn credits its immediate neighbors — evidence sits next door
	EntityPPR    bool // personalized PageRank over the facts entity graph, RRF-fused at session level
}

RankingOptions toggles the retrieval features layered on the BM25 core so membench can isolate each one's lift. Defaults are the measured winners (membench ablation, 2026-07-25): ALL OFF. Facts indexing — the data layer, not a knob here — delivered the lift on both benchmarks (LoCoMo R@5 0.502→0.612, LME R@5 held 0.93+); every global reranking layer measured neutral-to-harmful on at least one dataset (field weights flip sign across granularities, RM3 and adjacency trade R@1 for tail recall, session-level PPR+RRF scrambles good lexical orderings). They remain as options for ablation and for callers with a matching corpus shape.

type Record

type Record struct {
	TS        time.Time `json:"ts"`
	Kind      string    `json:"kind"`
	Text      string    `json:"text,omitempty"`        // user/assistant message text
	Tool      string    `json:"tool,omitempty"`        // tool_call/result tool name
	Input     string    `json:"input,omitempty"`       // tool_call input (redacted JSON)
	Decision  string    `json:"decision,omitempty"`    // approval: approved | denied | …
	ToolUseID string    `json:"tool_use_id,omitempty"` // links a result to its call (debug trace)
	Content   string    `json:"content,omitempty"`     // tool_result output (debug trace) / $ shell output
	IsError   bool      `json:"is_error,omitempty"`    // tool_result / $ shell failed
	Exit      int       `json:"exit,omitempty"`        // $ shell command exit code
	Model     string    `json:"model,omitempty"`       // session_started
	Mode      string    `json:"mode,omitempty"`        // session_started
	HeadSHA   string    `json:"head_sha,omitempty"`    // session_started
	Slug      string    `json:"slug,omitempty"`        // plan_presented: the saved-plan slug (~/.memcode/plans/<slug>.md)
	Axis      string    `json:"axis,omitempty"`        // preference_signal: which axis (workflow/gating/…)
	Scope     string    `json:"scope,omitempty"`       // preference_signal: this-repo vs global
	Strength  float64   `json:"strength,omitempty"`    // preference_signal / lesson_signal: evidence weight
	Trigger   string    `json:"trigger,omitempty"`     // lesson_signal: the recurring failure condition
	Strategy  string    `json:"strategy,omitempty"`    // lesson_signal: what to do when the trigger holds
	Entities  []string  `json:"entities,omitempty"`    // facts: lowercase entity keys the fact is about (feeds the session entity graph)

	// Post-session learning loop fields.
	LessonIDs     []string `json:"lesson_ids,omitempty"`     // context_inlined: promoted lesson ids in this session's prompt
	PrefIDs       []string `json:"pref_ids,omitempty"`       // context_inlined: confirmed pref ids in this session's prompt
	RuleKind      string   `json:"rule_kind,omitempty"`      // adherence: "lesson" | "pref"
	RuleID        string   `json:"rule_id,omitempty"`        // adherence: the rule's stable id
	Verdict       string   `json:"verdict,omitempty"`        // adherence: followed | violated | not_applicable
	Outcome       string   `json:"outcome,omitempty"`        // adherence: the target session's git outcome (accepted/corrected/rejected)
	TargetSession string   `json:"target_session,omitempty"` // adherence / lesson_signal: the session the verdict/lesson is ABOUT (the record lives in the judging session's log)

	// SessionID is stamped IN MEMORY by the multi-session readers (RecentBurstExcluding,
	// Recent) so a merged record knows which session it came from — used to name a
	// thread's source session in orientation. json:"-": never written to disk (the dir
	// name IS the id; persisting it would be redundant and could drift).
	SessionID string `json:"-"`
}

Record is one append-only line in events.jsonl. Fields are sparse (omitempty); which ones are set depends on Kind.

func AdherenceRecords

func AdherenceRecords(root string) ([]Record, error)

AdherenceRecords returns every adherence record across all sessions, oldest first — the reducers' backfill path for adherence weighting (files canonical, SQLite derived; same contract as LessonSignals).

func Commits

func Commits(root string) ([]Record, error)

Commits returns the git commit/push tool calls across all sessions (newest first) — the accountability trail of "did we already commit/push this?".

func LastShell

func LastShell(root string) (Record, bool)

LastShell returns the most recent `$` direct-shell command (Tool "shell"), across all sessions newest-first — its command (Input), output (Content), and exit (Exit/IsError). This is the data behind explain/fix-last: "why did that fail?" / "fix it" consult it. Empty (false) when no `$` command has run.

func LatestRecent

func LatestRecent(root string, n int) ([]Record, error)

LatestRecent returns the last n records of the most recently active session (by directory mtime) — the signal orientation/prediction pulls from to know "where you left off." Empty (nil, nil) if no sessions exist yet.

func LatestRecentExcluding

func LatestRecentExcluding(root, excludeID string, n int) ([]Record, error)

LatestRecentExcluding returns the last n records of the most recently active session whose id differs from excludeID — i.e. "the previous distinct session" when excludeID is the current one. This is what answers "what was the last session about?" from a brand-new session, which is already non-empty (it holds the user's opening message) and would otherwise shadow the prior one. Empty (nil, nil) if there is no other session.

func LessonSignals

func LessonSignals(root string) ([]Record, error)

LessonSignals returns every lesson_signal record across all sessions, oldest first, each stamped with its session id. The lessons reducer's backfill path — same contract as PreferenceSignals (files canonical, SQLite derived).

func PreferenceSignals

func PreferenceSignals(root string) ([]Record, error)

PreferenceSignals returns every preference_signal record across ALL sessions on disk, oldest-first, each stamped with its source session id. This is the CANONICAL read for the prefs reducer's backfill: the SQLite events table is a derived index, so after a state.db wipe the signals are recovered from the append-only files here.

func Recent

func Recent(root, sessionID string, n int) ([]Record, error)

Recent returns the last n records of one session (n<=0 = all).

func RecentBurstExcluding

func RecentBurstExcluding(root, excludeID string, maxSessions int, burstGap time.Duration, perSession int) ([]Record, error)

RecentBurstExcluding returns the merged records of the current WORK BURST of prior sessions (excludeID skipped), in CHRONOLOGICAL order — oldest session first, so a reducer sees one continuous stream and later work supersedes earlier work naturally. perSession caps each session's tail (0 = all).

The burst rule replaces both bad windows. A flat "last session" baked in amnesia (the thread from two sessions ago — minutes older — vanished); a flat "last K sessions" hauls year-old work into today's orientation just to fill a quota. Instead: ALWAYS take the most recent prior session, however old — that is "where you left off" — then keep walking older only while the gap between CONSECUTIVE sessions stays within burstGap, capped at maxSessions. Sessions clustered in one stretch of work arrive together; a long break ends the window.

func Search(root, query string, n int) ([]Record, error)

Search scans every session (newest first) for records whose text/content/input /tool contains query (case-insensitive). An empty query matches everything. Returns at most n hits (n<=0 = unlimited).

func SessionRecords

func SessionRecords(root, id string) ([]Record, error)

SessionRecords returns the full record list of ONE session by id, oldest first — the post-session learning loop reads a finished session's trail to build the adherence digest. (nil, nil) when the session has no log.

func Sidequests

func Sidequests(root, sessionID string) ([]Record, error)

Sidequests returns the user messages of one session — the sequence of things the user actually asked for, in order.

type SessionSummary

type SessionSummary struct {
	ID       string
	Started  time.Time
	Headline string
	Actions  int // meaningful tool calls (commands/edits) — a sense of how much happened
}

SessionSummary is a one-line digest of a past session for "what were the last couple sessions about?" — the headline is the session's first user request.

func RecentSessions

func RecentSessions(root, excludeID string, n int) ([]SessionSummary, error)

RecentSessions returns the most recent sessions newest-first, EXCLUDING excludeID (the current one). n<=0 means all. This is the "recent sessions" affordance the agent needs to answer about session history — distinct from Recent(), which reads a SINGLE session's records.

type Thread

type Thread struct {
	Request string
	Actions []string
}

Thread is one strand of work in a session: a user request and the meaningful actions taken under it. A session's Threads read like a checklist of "what we did," each with a line of detail — the right grain for recall (signal, not the raw event firehose; drill into events.jsonl when you need 100%).

func Recap

func Recap(recs []Record) []Thread

Recap groups records into Threads: each user message starts a strand, and the commands/edits/approvals that follow attach to it. This is the "we built these N things, with a bit about each" view.

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer appends records for one session to .memcode/sessions/<id>/.

func Open

func Open(root, sessionID string) (*Writer, error)

Open creates (or reopens) the on-disk log for a session and returns a Writer. A nil Writer is safe to Append/Close on, so callers can ignore the error and degrade gracefully if the directory can't be created.

func (*Writer) Append

func (w *Writer) Append(r Record)

Append writes one record to the append-only log and mirrors a human line into the transcript. Safe on a nil Writer and safe under concurrent calls.

func (*Writer) Close

func (w *Writer) Close() error

Close flushes and closes the underlying files. Safe on a nil Writer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL