MythOS Sessions and Workflows form the two primitive interfaces for agentic loops. Sessions track work; Workflows run it. Together they give a library owner a living conversational surface — chat in, worklist updates and roadmapped items out — powered by scheduled and on-demand agentic loops with human gates at every consequential boundary.
What Sessions Track
An agent session is a named unit of work with a purpose, a heartbeat, and a request counter. It is created via POST /api/internal/sessions and tracked in the agent_sessions Mongo collection. The session id (x-mythos-session header) is propagated by the MythOS agent into every downstream tool call, so all activity from a single logical session — even across multiple cron ticks or subagent delegations — carries the same identity.
// src/lib/middleware/agent-session.ts
interface AgentIdentity { uid?: string; username?: string; keyId?: string; }
// Every MCP/chat request reads this header and stamps the session document.
// requestCount increments; lastActiveAt refreshes; uid/username/keyId update.
async function trackAgentSession(req: NextRequest, identity?: AgentIdentity)Sessions are not authentication. They are operational tracking — a way to answer "what was that loop doing last Tuesday" without patching every tool to carry provenance.
The session document shape:
What Workflows Run
A workflow is a memo whose body conforms to the four-section dialect the compiler reads. The memo has memoType: "workflow" set via the workflows import surface; the execution engine resolves the manifest from the current memo content on every run.
The Four Sections
Trigger — what starts a run. Each workflow declares a command (the slash name), an invoke (what fires it), a state (live/paused/draft), and a one-line summary.
## Trigger
- **Command** `/my-memo-drafting`
- **Invoke** A row appears in the workflow's queue
- **Status** `live`
- **Summary** Drafts one memo from a queued brief, holds for review before publishingLoad Contract — which memos load into each run and by what mechanism. Four load classes:
Gates and hooks are guaranteed-load; trigger and search are conditional. The boundary the surface draws between "always in context" and "resolved at run time" is load-class-driven.
Steps — the run sequence. Four step kinds:
Policy — how the engine behaves. Key fields:
The Execution Engine
The runner lives at src/lib/workflows/runner.ts. It implements the bulk-import-runner pattern: atomic claim, heartbeat between steps, bounded recovery, terminal failed.
// src/lib/workflows/runner.ts
const RUNS = "workflow_runs";
export const STUCK_RUN_MS = 10 * 60 * 1000; // 10 minutes
const MAX_RECOVERY_ATTEMPTS = 5;
const MAX_ITEMS_PER_PASS = 3;
const MAX_RESUMES_PER_PASS = 5;
// Entry point — called by the cron every 5 minutes
export async function runWorkflowPass(
db: Db,
runnerId: string,
): Promise<WorkflowPassReport> {
// 1. Count pending items — zero model calls if queue is empty
const pending = await countPendingItems(db);
if (pending === 0) return report;
// 2. Claim up to MAX_ITEMS_PER_PASS pending items
for (let i = 0; i < MAX_ITEMS_PER_PASS; i++) {
const item = await claimNextItem(db, `claim:${runnerId}:${Date.now()}:${i}`);
if (!item) break;
// 3. Compile manifest from current memo content (fresh every run)
const resolved = await resolveRunManifest(db, item.workflowMemoId);
// 4. Insert run record; skip if run already exists (idempotent dedupe)
await runCollection(db).insertOne({ _id: runId, status: "running", ... });
// 5. Execute the run to completion or first suspend
await executeWorkflowRun(db, runId);
}
}The run record (workflow_runs) holds the compiled manifest, per-step status/token/duration, error taxonomy, guard outcomes, and dispatch links. Acceptance bar: "run records every pass."
Dispatch: Output Lands in the World
The dispatch seam (src/lib/workflows/dispatch.ts) is where the runner writes outward. Two destinations exist today:
memo — creates a new unlisted memo in the owner's library, or updates an existing stub memo in place (the drafting loop pattern). Update-in-place uses CAS on contentVersion to refuse clobbering a human edit that raced the dispatch.
worktree / reply — refused at compile time; no native dispatcher yet.
// src/lib/workflows/dispatch.ts — readLandingTarget
export function readLandingTarget(
payload: Record<string, unknown> | null,
): WorkflowLandingTarget | null {
// payload.targetMemoId → update-in-place
// payload.targetNotes / payload.tags → both tag zones from payload
// payload.source.{contentHash, notesHash} → move guard
}The Queue: How Work Is Discovered
The queue (workflow_queue_items) is the discovery layer. Whoever knows work exists writes a row; the runner never goes looking.
// src/lib/workflows/queue.ts
interface WorkflowQueueItem {
_id: ObjectId;
ownerUid: string;
workflowMemoId: string;
dedupeKey: string; // Idempotency: one row per (workflow, key)
payload: unknown; // Passed verbatim to the model
status: "pending" | "claimed" | "done" | "failed";
runId: string | null; // Set when claimed
createdAt: number;
claimedAt: number | null;
settledAt: number | null;
}
// Enqueue is idempotent: duplicate (workflow, dedupeKey) is a no-op
export async function enqueueWorkflowItem(
db, { ownerUid, workflowMemoId, dedupeKey, payload }
): Promise<{ enqueued: boolean }>
// Claim is atomic: status CAS + runId stamp in one write
export async function claimNextItem(
db, runId: string,
): Promise<WorkflowQueueItem | null>Cron-triggered producers (e.g., a weekly digest loop) write queue items directly. On-demand producers (e.g., an agent started mid-session) also write queue items. In both cases, the runner discovers the work the same way: a pending count.
Stewardship Gates: Human-in-the-Loop
When Policy.Approval = hold, the output step suspends and opens a stewardship hold. The hold surfaces in the owner's moderation queue as a workflow-run card.
// src/lib/stewardship/run-holds.ts
export async function openWorkflowRunHold(
db, { ownerUid, runId, workflowName, gateLabel, provenance? }
) {
// Opens a hold scoped to the library, referencing the run document by id.
// Approving the hold → runner resumes the suspended run.
// Rejecting the hold → run settled as cancelled.
}The steward decision is the at-least-once boundary: approval is recorded on the run record, the runner resumes only on a recorded decision, and nothing resumes on the hold row alone.
Cron Integration
The cron-run-workflows Railway service hits GET /api/internal/cron/run-workflows every 5 minutes with x-internal-key. Three things happen in order:
resumeApprovedRuns— resume every suspended run whose gate carries a recorded approval. Runs before discovery so an approved gate never waits behind new work.runWorkflowPass— claim pending items, compile manifests, execute runs.recoverStuckRuns— claim runs whose heartbeat went quiet past 10 minutes. Bounded at 5 recovery attempts, then settled asrecovery_exhausted.
// src/app/api/internal/cron/run-workflows/route.ts
export async function GET(req: Request) {
// Auth: timingSafeCompare on x-internal-key header
const runnerId = `cron:${randomUUID()}`;
const resumed = await resumeApprovedRuns(db); // MAX_RESUMES_PER_PASS = 5
const pass = await runWorkflowPass(db, runnerId); // MAX_ITEMS_PER_PASS = 3
const recovery = await recoverStuckRuns(db); // STUCK_RUN_MS = 10 min
return NextResponse.json({ pending, started, failedItems, resumed, recovered, exhausted });
}An empty queue is provably zero model calls: countPendingItems is an indexed count with no manifest, memo, or model involved.
Sessions + Workflows: How They Connect
User chat message
│
▼
MythOS Chat API (src/app/api/chat/route.ts)
│ — RAG retrieval, system prompt, model call
│ — If owner + tools available: handleOwnerAgenticChat()
▼
Agent decides: enqueue a workflow item?
│
├── No → plain chat response
│
└── Yes → enqueueWorkflowItem()
workflowMemoId = "<the workflow's memo id>"
dedupeKey = "<idempotency key for this trigger>"
payload = { targetMemoId?, ... }
│
▼
cron-run-workflows (every 5 min)
│
▼
claimNextItem() → runWorkflowPass()
│
▼
resolveRunManifest() — fresh compile from memo
│
▼
executeWorkflowRun()
trigger → context → process → output
│
▼
If Approval = hold:
openWorkflowRunHold()
status → suspended
→ steward reviews in queue
│
▼ (approved)
resumeApprovedRuns()
executeWorkflowRun() continues
│
▼
dispatchWorkflowOutput()
→ new memo created/updated
→ memo appears in owner's library
│
▼
Session heartbeat: x-mythos-session header
tracks every call under the same logical runThe session wraps the whole span: from the chat message that triggered the enqueue, through the runner's multiple cron ticks, to the final dispatch. requestCount and lastActiveAt give observability into loop health without requiring the loop itself to log.
Edges of Understanding: What the System Does Not Yet Know
The following are open questions the system is still working through:
worktreeandreplydestinations have no dispatcher. A workflow declaring either destination compiles successfully but fails at dispatch with"destination \`worktree\` has no native dispatcher yet". These require additional implementation.- The chat session mirroring Slack threads is not yet wired. A MythOS memo that mirrors a Slack thread in real time — appending as loop items complete — is a surface layer not yet implemented. The primitives exist (chat thread tracking, memo writes, session tracking); the binding layer between a Slack thread and a MythOS memo does not.
- Edges-of-understanding tracking is a design still being formed. The concept of a "edges of understanding" section in a weekly progress report — surfacing things the system ran into but couldn't resolve — is not yet formalised in any schema. It is the right framing; the implementation shape is still open.
- On-demand loop invocation from a chat agent requires the agent to hold a MythOS API key and call
enqueueWorkflowItemdirectly, or to call a named workflow command via the CLI. A skill interface (the/loopcommand in Hermes) that surfaces available workflows and lets an agent pick one is not yet wired.
Key Files
See Also
- 📝MythOS Chat Interface: Agentic Loops as a Conversation — system overview and UX vision
- 📝Loop Memo Template — the runbook template for documenting a loop
- 📝Development Roadmap Template — the roadmap template loops feed into
