How gstack Domain Skills Persist Per-Site Knowledge Across Sessions
gstack domain skills persist per-site knowledge across sessions by writing append-only JSONL records to project-scoped or global files in ~/.gstack, using host normalization and a state machine to manage quarantined, active, and global states.
The gstack browser agent (garrytan/gstack) implements a lightweight knowledge management system called domain skills that captures per-site insights and makes them available across process restarts. Unlike ephemeral context windows, this system ensures that lessons learned about specific websites survive session termination through durable, append-only storage and a careful promotion lifecycle.
Storage Architecture and File Locations
Domain skills use two distinct storage scopes depending on visibility requirements. According to the source code in browse/src/domain-skills.ts (lines 69-75), the system defines the following paths:
- Project-scoped notes:
~/.gstack/projects/<project-slug>/learnings.jsonl - Global notes:
~/.gstack/global-domain-skills.jsonl
The helper functions that resolve these paths are implemented as follows:
// storage paths
function globalFile(): string {
return path.join(gstackHome(), 'global-domain-skills.jsonl');
}
function projectFile(slug: string): string {
return path.join(gstackHome(), 'projects', slug, 'learnings.jsonl');
}
Host Normalization for Consistent Keys
Before any write operation, the system canonicalizes hostnames to ensure that https://www.Example.com:443 and example.com resolve to the same key. The normalizeHost function (lines 79-90 in browse/src/domain-skills.ts) strips protocols, ports, and the www. prefix, then lowercases the result.
export function normalizeHost(input: string): string { … }
This normalization guarantees that every visit to the same origin maps to a single, persistent knowledge entry regardless of URL formatting variations.
The Save Operation and Data Structure
When an agent generates a note, the writeSkill function creates a quarantined row and appends it to the project-level JSONL file. As implemented in browse/src/domain-skills.ts (lines 52-85), each row contains:
- The full markdown body
- A SHA-256 hash for integrity
- Timestamps and version numbers
- The classifier score (must be < 0.85 to proceed)
export async function writeSkill(input: WriteSkillInput): Promise<DomainSkillRow> {
// …compute hash, version, create row…
await appendRow(projectFile(input.projectSlug), row);
}
State Machine Lifecycle
The domain skill system employs a three-state machine to prevent prompt injection attacks and ensure quality control, documented in the header comment (lines 9-22) and enforced throughout browse/src/domain-skills.ts.
Quarantined State
All newly saved notes begin in the quarantined state. These rows are persisted to disk but are explicitly excluded from prompts. This isolation prevents untested knowledge from immediately influencing agent behavior.
Promotion to Active
After a quarantined skill has been used PROMOTE_THRESHOLD (3) times without triggering classifier flags, the recordSkillUse function (lines 18-26) automatically promotes it to active. Only active skills are eligible for injection into agent prompts.
Global Promotion
An explicit domain-skill promote-to-global command invokes promoteToGlobal (lines 42-71), which copies the active row to the global file. This makes the note visible to every project, effectively creating a cross-project knowledge base.
Reading and Retrieval Logic
When the browser agent needs context for a specific site, the readSkill function (lines 20-38) executes a hierarchical lookup:
- First, search the project-specific
learnings.jsonlfile - If not found, fall back to
~/.gstack/global-domain-skills.jsonl - Return only rows whose state is
active(project) orglobal
Quarantined rows are filtered out during this retrieval phase, ensuring that unverified content never reaches the context window.
export async function readSkill(host: string, projectSlug: string): Promise<ReadSkillResult | null> {
// look at project → global, ignore quarantined rows
}
Durability and Crash Safety
All writes use fs.open with the O_APPEND flag followed by an explicit fsync call. This guarantees that each JSON Lines entry is atomically persisted (under Linux PIPE_BUF limits) and survives process crashes. Deletions are handled via tombstone rows rather than file rewrites, with a periodic compactor routine rewriting files to reclaim space.
Command-Line Interface
The top-level dispatcher in browse/src/domain-skill-commands.ts (lines 58-99) exposes the domain skill system through the $B domain-skill sub-command structure:
# Save the current page’s note (agent writes the body to stdin)
$ B domain-skill save < notes.md
# List all active notes for the current project (project + global)
$ B domain-skill list
# Show the full markdown body for a specific host
$ B domain-skill show example.com
# Promote a project-local note to the global pool
$ B domain-skill promote-to-global example.com
# Roll back a note to the previous version
$ B domain-skill rollback example.com --global
Programmatic usage is also supported by importing the core functions:
import { writeSkill, readSkill, recordSkillUse } from './domain-skills';
// Save a new note (quarantined)
await writeSkill({
host: 'example.com',
body: '# My notes\n…',
projectSlug: 'my-project',
source: 'agent',
classifierScore: 0.12, // must be < 0.85
});
// When the agent later uses the note:
await recordSkillUse('example.com', 'my-project', false);
// Retrieve the note for prompting:
const skill = await readSkill('example.com', 'my-project');
if (skill) {
// skill.row.body contains the markdown
}
Summary
- gstack domain skills store per-site knowledge in append-only JSONL files at
~/.gstack/projects/<slug>/learnings.jsonlor~/.gstack/global-domain-skills.jsonl - Host normalization ensures consistent keys across protocol variations via
normalizeHost()inbrowse/src/domain-skills.ts - Three-state lifecycle (quarantined → active → global) protects against prompt injection through classifier gating and usage thresholds
- Atomic durability is achieved through
O_APPENDwrites with explicitfsynccalls - Hierarchical retrieval checks project scope first, then global, ignoring quarantined entries
Frequently Asked Questions
Where does gstack store domain skills?
gstack writes project-scoped domain skills to ~/.gstack/projects/<project-slug>/learnings.jsonl and global skills to ~/.gstack/global-domain-skills.jsonl, as defined by the projectFile() and globalFile() functions in browse/src/domain-skills.ts (lines 69-75).
How does gstack prevent malicious content in domain skills?
The system implements a quarantine state for all new notes. Content remains isolated from prompts until recordSkillUse() promotes it after PROMOTE_THRESHOLD (3) successful uses without classifier flags. Additionally, the writeSkill function requires a classifier score below 0.85 before accepting new entries.
What triggers a domain skill to become active?
A domain skill transitions from quarantined to active automatically after being successfully referenced three times without triggering safety classifiers. This promotion logic resides in recordSkillUse() within browse/src/domain-skills.ts (lines 18-26).
Can domain skills be shared across projects?
Yes. Once a skill reaches the active state within a project, you can promote it to global scope using B domain-skill promote-to-global <host>. This copies the row to ~/.gstack/global-domain-skills.jsonl, making it available to all projects when readSkill() performs its fallback lookup.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →