ContextQ Integration — Cyberine long-term memory on the Agent Memory Layer
Status: design spec (drives implementation). Grounded against ContextQ as it ships today (shared-context-server, agent layer migration 0043) and Cyberine memory waves 1-4.
This document maps Cyberine's file-based episodic memory onto ContextQ's now-shipped
Agent Memory Layer, defines the vendor-independent MemoryProvider port Cyberine will
introduce, and lays out a phased rollout. Every claim below cites a real symbol or
file:line in one of the two repos.
- Cyberine memory internals:
src/store/MemoryStore.ts,src/store/memoryRetrieval.ts,src/launchBg.ts,src/store/runTypes.ts,USAGE.mdsection "Long-term memory". - ContextQ surface:
src/routes/agent.ts,src/services/agent.service.ts,src/services/agent-boot.service.ts,src/db/schema.ts(theagent_*tables),mcp/src/tools.ts(theagent_*tools),src/plugins/auth.ts.
1. Capability-fit summary
Cyberine today owns a self-contained, file-based episodic memory subsystem. ContextQ ships a server-side, multi-tenant, embedding-backed Agent Memory Layer that is a near-superset of Cyberine's primitives plus capabilities Cyberine does not have yet (mid-run checkpoints, verifiable task trees, kNN-recalled lessons, LLM handoffs).
| Capability | Cyberine local memory | ContextQ Agent Memory Layer |
|---|---|---|
| Episodic run record | MemoryRecord written by writeRecord(run, cwd) (MemoryStore.ts:344) to records/<runId>.json, append-only | agent_sessions row via startSession (agent.service.ts:161); one session ~ one run |
| Project scoping | projectSlug(cwd) = sanitized basename (MemoryStore.ts:69) | tenant + workspaces.slug + optional projects.slug (agent.service.ts:61-80) |
| Outcome structure | outcome.{summary,accomplished,issues,next} parsed by parseOutcomeSections (MemoryStore.ts:278) | split across agent_tasks (the checklist) + agent_lessons + handoff context body |
| Open-issue ledger | issues.jsonl, fold last-event-wins (listOpenIssues, MemoryStore.ts:544) | agent_tasks with status in {pending,in_progress,blocked} (the open set, agent.service.ts:55) |
| Lineage / resume chain | RunLineage{parentRunId,chatSessionId,previousRunId,rootRunId} (runTypes.ts:22) | agent_sessions.parent_session_id (schema.ts:1342) + parent_session_id body param (agent.ts:114) |
| Retrieval into prompt | buildMemoryPreamble(cwd,query,opts) (memoryRetrieval.ts:202), keyword + recency + lineage + optional cosine | buildBootPack (agent-boot.service.ts:255): resume + handoff + lessons + facts, token-budgeted |
| Embedding recall | optional embeddings.jsonl + cosine axis (memoryRetrieval.ts:240), local Ollama | pgvector 768-dim on agent_lessons.embedding + contexts.embedding; kNN in findRelevantLessons (agent.service.ts:534) |
| Mid-run checkpoint / resume | NOT PRESENT (records are written only at terminal state, MemoryStore.ts:346) | agent_checkpoints (schema.ts:1400) + writeCheckpoint/getLatestCheckpoint + /resume (agent.ts:239) |
| Verifiable task ticking | the ACCOMPLISHED/NEXT trailer is free text; no evidence gate | tickTask enforces evidence on verified (EvidenceRequiredError, agent.service.ts:342) |
| Handoff document | none (the preamble is the only synthesis) | agent_handoff wraps the dream-handoff LLM generator (agent.ts:413) |
| Storage / multi-tenant | single-user files under $CYBERINE_HOME/memory | Postgres, tenant-scoped on every read/write; cross-tenant id -> 404 (agent.service.ts:205) |
Net: ContextQ can back every Cyberine memory primitive and adds the four Cyberine is missing
(checkpoint/resume, evidence-gated tasks, recalled lessons, handoff). The one
structural mismatch Cyberine must bridge is the outcome model — Cyberine stores one flat
outcome blob per run; ContextQ splits it into tasks + lessons + a handoff body.
2. Field-by-field mapping
The unit of correspondence: one Cyberine run (one MemoryRecord) ~ one ContextQ
agent_session. Everything else hangs off that.
2.1 Identity / scoping
| Cyberine field (source) | ContextQ target (column / tool param) | Notes |
|---|---|---|
stable Cyberine agent handle (new constant, e.g. "cyberine") | agents.slug via agent_boot.agent / agent_session_start.agent (agent.ts:68,109) | ensureAgent is idempotent per (tenant, slug) (agent.service.ts:98); slug lowercased, max 64 chars |
MemoryRecord.cwd -> projectSlug(cwd) (MemoryStore.ts:69) | agent_session_start.project -> projects.slug (resolved by resolveProjectId, agent.service.ts:70) | requires the workspace to exist; project is optional |
CYBERINE_CONTEXTQ_WORKSPACE (new env) | agent_session_start.workspace -> workspaces.slug (agent.service.ts:61) | a workspace must be pre-created in ContextQ; Cyberine does not auto-create workspaces |
MemoryRecord.runId (UUID) | NO direct column — carry in agent_sessions.metadata.cyberineRunId (metadata jsonb, schema.ts:1349) | ContextQ session ids are serial ints; Cyberine keeps its own runId as the join key in metadata |
MemoryRecord.workflowName | agent_sessions.metadata.workflowName | no first-class column |
2.2 Run -> session
| Cyberine field | ContextQ target | Notes |
|---|---|---|
MemoryRecord.task (stripped of preamble, MemoryStore.ts:353) | agent_session_start.goal -> agent_sessions.goal (schema.ts:1339) | also feeds agent_boot.goal for facts/lessons retrieval |
MemoryRecord.state (done/error/cancelled) | agent_session_end.status -> agent_sessions.status enum (agentSessionStatusEnum, schema.ts:93) | map: done->completed, error->stalled or abandoned, cancelled->abandoned. ContextQ stamps ended_at on completed/abandoned (agent.service.ts:231) |
MemoryRecord.startedAt / endedAt (epoch ms) | agent_sessions.started_at (server defaultNow) / ended_at (set on terminal status) | Cyberine timestamps are informational only; ContextQ owns its own. Store Cyberine's in metadata if exact replay is needed |
MemoryRecord.workers[] | agent_sessions.metadata.workers (jsonb) | no first-class column; metadata blob |
MemoryRecord.verdictSummary | agent_sessions.metadata.verdict OR the handoff body | one-line judge verdict |
2.3 Lineage -> resume chain
Cyberine lineage field (runTypes.ts:22) | ContextQ target | Notes |
|---|---|---|
lineage.parentRunId | resolve the parent run's session id, pass as agent_session_start.parent_session_id (agent.ts:114 -> parent_session_id -> schema.ts:1342) | ContextQ validates the parent belongs to the tenant (agent.service.ts:173) |
lineage.previousRunId | same mechanism — chain via parent_session_id, OR record in metadata.previousRunId | ContextQ has only ONE self-ref (parent_session_id); Cyberine has two distinct links (parent vs previous). Keep previousRunId in metadata to avoid overloading the FK |
lineage.chatSessionId | agent_sessions.metadata.chatSessionId | no first-class column |
lineage.rootRunId | agent_sessions.metadata.rootRunId | derived; metadata only |
Caveat: Cyberine lineage is a richer graph (4 distinct links) than ContextQ's single
parent_session_id. The mapping keeps parent_session_id for the primary resume
edge and parks the other three in metadata rather than forcing them onto one FK.
2.4 Outcome -> tasks + lessons + handoff
This is the structural split. MemoryRecord.outcome (MemoryStore.ts:33-38) is one
blob; ContextQ wants it decomposed:
| Cyberine outcome field | ContextQ target (tool -> column) | Notes |
|---|---|---|
outcome.summary | the handoff body via agent_handoff (agent.ts:413), OR agent_checkpoint.summary | summary is a synthesis, not a checklist item |
outcome.accomplished[] | one agent_task_upsert per item with status:"verified" + evidence:<text> (agent.ts:282, agent.service.ts:276) | MUST supply evidence — tickTask/upsert-to-verified rejects empty evidence (EvidenceRequiredError, agent.service.ts:342). Cyberine has no per-item evidence today, so the writer must synthesize it (e.g. the verdict line) or write status:"pending" instead |
outcome.next[] | one agent_task_upsert per item with status:"pending" (agent.ts:272) | the open work — maps to ContextQ's open-task set |
outcome.issues[] (issue slugs) | one agent_lesson_add per issue (what_failed, agent.ts:365 -> agent_lessons.what_failed, schema.ts:1445) OR agent_task_upsert status:"blocked" | lessons get embedded for kNN recall at next boot; blocked tasks stay in the open set. Recommend lessons for recurring failures, blocked tasks for run-local blockers |
IssueEvent{event:opened/resolved} (MemoryStore.ts:44) | agent_task_tick status transitions (blocked <-> verified/skipped, agent.ts:336) | Cyberine's fold-last-event-wins ledger maps to ContextQ task status mutation; the closest analogue |
2.5 Retrieval (preamble) -> Boot Pack
Cyberine buildMemoryPreamble part (memoryRetrieval.ts) | ContextQ BootPack slot (agent-boot.service.ts) |
|---|---|
| top-K scored prior runs | facts[] (search over goal, agent-boot.service.ts:220) + resume (latest non-terminal session, agent-boot.service.ts:128) |
OPEN ISSUES: block (memoryRetrieval.ts:255) | resume.open_tasks[] (pending/in_progress/blocked, agent-boot.service.ts:139) |
| recency + keyword + cosine ranking | server-side: findRelevantLessons kNN (agent.service.ts:534) + searchContexts for facts; token-budgeted with budget.dropped (no silent cut) |
MEMORY (...) header/footer markers (MemoryStore.ts:322) | Cyberine-side concern — the provider renders the BootPack JSON back into the same MEMORY (...)/END MEMORY envelope so the worker prompt format is unchanged |
ContextQ's boot is richer (it adds a dedicated handoff slot and embedding-recalled
lessons), but the injected prompt shape stays an Cyberine responsibility: the adapter
must serialize BootPack into the existing preamble envelope so stripMemoryPreamble
(MemoryStore.ts:329) still round-trips and memory never re-ingests itself.
2.6 The one Cyberine gap and the one ContextQ caveat
-
Cyberine gap — no mid-run checkpoint/resume. Cyberine writes a record only at terminal state (
writeRecordearly-returns unlessstateis terminal,MemoryStore.ts:346). There is no equivalent ofagent_checkpoint//resume. A crashed Cyberine run leaves no resumable state — the next run starts from the preamble only. ContextQ'sagent_checkpoints(schema.ts:1400) + monotonicseqoptimistic lock (agent.service.ts:404) is net-new capability Cyberine gains by adopting the layer (rollout phase 4). -
ContextQ caveats:
- Auth scope. Reads (
agent_boot,agent_resume, get session/tasks) needrequireScope('read'); writes (session start/end, checkpoint, task upsert/tick, lesson add, handoff) needrequireScope('write')(agent.ts:78,120,213,...). A programmaticsk_live_key therefore needs BOTHreadandwritescopes (oradmin, which subsumes both,auth.ts:331). See section 3. - Tenant/workspace must pre-exist. Sessions are tenant-scoped from the key;
the workspace + project slugs must already exist in ContextQ —
resolveWorkspaceIdreturns null for unknown slugs (agent.service.ts:61) and the session silently falls back to no workspace, which then breaksagent_handoff(it 400s withno_workspace,agent.ts:438). Cyberine must provision the workspace once, out of band. - Search type-enum is narrower than create.
POST /api/contextsaccepts 9 types incl.synthesis/map/moc(contexts.ts:41), andctx_save(MCP) advertises the same 9 (tools.ts:55). ButPOST /api/searchstill rejects anything beyond the original 6 —z.enum(['reference','feedback','project','incident','lesson','user'])(search.ts:12). Consequence for phase 1 (genericctx_save/ctx_search): Cyberine can WRITE a record under any of the 6 search-visible types, but if it writessynthesis/handoff-shaped contexts it cannot later filter-search them by type. Stick to one of the 6 (recommendreferencefor run records,lessonfor issues) until the search enum is widened. The dedicatedagent_*endpoints are unaffected — they do not go through the search type-enum.
- Auth scope. Reads (
3. Auth / provisioning model
Cyberine authenticates to ContextQ as a programmatic client with a sk_live_ API key.
-
The user mints the key manually in the ContextQ dashboard. The key must carry BOTH
readandwritescopes (oradmin). Do NOT attempt to find, generate, or derive a key from code.requireScope('write')gates every write endpoint andrequireScope('read')every read; a key missing a scope gets 403scope_denied(auth.ts:356). The key is presented asAuthorization: Bearer sk_live_...(auth.ts:131) and binds the request to its issuing tenant (auth.ts:201). -
Where Cyberine stores it: under
.local/(gitignored) per the repo convention — e.g..local/contextq/contextq.env. Never inline a key in source or commit it (git-workflow.mdNOT-authorized list forbids secret commits). Leave a breadcrumb if a key file moves. -
Env vars (new, all read by the ContextQ adapter):
Env var Purpose Default CYBERINE_CONTEXTQmaster switch for the ContextQ backend; unset/ 0keeps local files(off) CYBERINE_MEMORY_BACKENDselects the provider: local(default) |contextq|mcplocalCYBERINE_CONTEXTQ_URLbase URL of the ContextQ API (e.g. https://ctx.example.com)(none) CYBERINE_CONTEXTQ_API_KEYthe sk_live_write+read key(none) CYBERINE_CONTEXTQ_WORKSPACEworkspace slug all Cyberine sessions/contexts scope to (none) CYBERINE_MEMORY_BACKEND=contextqrequiresCYBERINE_CONTEXTQ_URL+CYBERINE_CONTEXTQ_API_KEYCYBERINE_CONTEXTQ_WORKSPACE; on any missing value or transport error the provider factory falls back tolocaland logs (best-effort, mirroringMemoryStore's never-throw contract,MemoryStore.ts:480). The existingCYBERINE_MEMORY=0master switch (MemoryStore.ts:317) still hard-disables all memory regardless of backend. Themcpbackend reuses the same key via the ContextQ MCP server'sCONTEXT_API_KEY/CONTEXT_API_URLenvs (mcp/src/tools.ts:8,32).
4. The MemoryProvider port
Cyberine introduces a MemoryProvider interface so memory is backend-agnostic. The local
file store is refactored behind it (LocalMemoryProvider wrapping today's
MemoryStore functions); ContextQ is the FIRST alternate adapter, not a hardcoded
dependency. Selection is config-only via CYBERINE_MEMORY_BACKEND.
Update 2026-09-22: the shipped seam differs from this spec. The local
MemoryStorestays canonical, not refactored behind aMemoryProviderport; a backend instead implements the augmentation interfaceMemoryAugmentationBackend(src/store/memoryBackend.ts:29), chosen viaselectMemoryBackend()(src/store/memoryBackend.ts:329). There is noMemoryProvider/LocalMemoryProvidersymbol in source.
4.1 Required methods (every backend MUST implement)
These are exactly the surface launchBg.ts and the cyberine memory CLI consume today,
so the local store satisfies them by definition:
interface MemoryProvider {
// writes (terminal-state record)
writeRecord(run: Run, cwd: string): Promise<MemoryRecord | null>; // MemoryStore.ts:344
// reads
listRecords(cwd: string): Promise<MemoryRecord[]>; // MemoryStore.ts:487
getRecord(cwd: string, runId: string): Promise<MemoryRecord | null>; // MemoryStore.ts:503
latestRecord(cwd: string): Promise<MemoryRecord | null>; // MemoryStore.ts:513
listOpenIssues(cwd: string): Promise<IssueEvent[]>; // MemoryStore.ts:544
// retrieval -> prompt
buildMemoryPreamble( // memoryRetrieval.ts:202
cwd: string, query: string,
opts?: { topK?: number; maxChars?: number; chatSessionId?: string; parentRunId?: string },
): Promise<string>;
}Contract notes: all methods are best-effort (return null/[]/'', never throw
into the caller) so a backend outage degrades to "no memory", never a failed run.
The ContextQ adapter satisfies these by translating to/from agent_* calls + the
Boot Pack (see the mapping in section 2) and re-rendering the MEMORY (...) envelope.
4.2 Optional capabilities (a backend MAY implement; callers feature-detect)
Only the ContextQ/MCP backends implement these initially; local returns
not-supported and Cyberine keeps its current behavior. Exposed via an optional
capabilities bag + narrow methods:
interface MemoryProviderCapabilities {
boot?: boolean; // server-assembled Boot Pack (agent_boot)
checkpoint?: boolean; // mid-run snapshot + resume (agent_checkpoint / /resume)
tasks?: boolean; // evidence-gated verifiable task tree (agent_task_*)
lessons?: boolean; // embedded, kNN-recalled lessons (agent_lesson_add)
handoff?: boolean; // LLM handoff document (agent_handoff)
}| Optional method | Maps to ContextQ | Used by |
|---|---|---|
boot(agent, goal, workspace, budget) | agent_boot / POST /api/agent/boot (agent.ts:76) | phase 2 preamble |
startSession / endSession | agent_session_start / agent_session_end (agent.ts:118,173) | phase 3 |
checkpoint(sessionId, state, summary) / resume(sessionId) | agent_checkpoint / agent_resume (agent.ts:211,239) | phase 4 |
upsertTask / tickTask | agent_task_upsert / agent_task_tick (agent.ts:282,336) | phase 5 |
addLesson | agent_lesson_add (agent.ts:372) | phase 5 |
handoff(sessionId, workspace) | agent_handoff (agent.ts:413) | phase 5 |
Vendor-independence rule: Cyberine core (engine, launchBg, CLI) depends ONLY on the
MemoryProvider interface. No agent_* symbol, no ContextQ URL, and no
sk_live_ handling leaks above the adapter boundary — a future backend (e.g. a
different memory server) drops in by implementing the same port.
5. Phased rollout
Each phase is independently shippable and degrades to the prior phase on failure. "Cyberine seam" names the file/function to change; "ContextQ" names the tool/endpoint.
Phase 1 — generic ctx_save / ctx_search (works against ContextQ today)
- What: define the
MemoryProviderport; refactor today'sMemoryStorecalls behindLocalMemoryProvider; add aContextqMemoryProviderthat onwriteRecordPOSTs a single context (ctx_save->POST /api/contexts,tools.ts:88) carrying the serializedMemoryRecord, and onbuildMemoryPreamblerunsctx_search(POST /api/search,tools.ts:151) and renders hits into the existing envelope. - Cyberine seam:
src/store/MemoryStore.ts(writeRecord),src/store/memoryRetrieval.ts(buildMemoryPreamble),src/launchBg.ts:80(the only preamble call site), plus a newsrc/store/MemoryProvider.ts+src/store/ContextqMemoryProvider.ts.Update 2026-09-22: shipped as
src/store/memoryBackend.ts(ContextqBackendclass) plussrc/store/contextq.ts(HTTP wrappers); neitherMemoryProvider.tsnorContextqMemoryProvider.tsexists. - ContextQ:
POST /api/contexts,POST /api/search. - Caveat: use
type:"reference"for run records andtype:"lesson"for issues — both are in the search-visible 6 (search.ts:12). Do NOT usesynthesis/map/mocyet (not search-filterable, section 2.6.3). No agent layer needed — proves the transport, key, and workspace before adopting the richer surface.
Phase 2 — agent_boot for the preamble
- What:
buildMemoryPreambleon the ContextQ backend callsagent_bootand renders the returnedBootPack(resume + handoff + lessons + facts) into theMEMORY (...)/END MEMORYenvelope instead of hand-rollingctx_search. - Cyberine seam:
ContextqMemoryProvider.buildMemoryPreamble; the call site atlaunchBg.ts:80is unchanged (still calls the port). - ContextQ:
agent_boot/POST /api/agent/boot(agent.ts:76,agent-boot.service.ts:255). Passgoal=finalTask,workspace=$CYBERINE_CONTEXTQ_WORKSPACE,token_budgetfromCYBERINE_MEMORY_MAX_TOKENS.
Phase 3 — runs -> agent_sessions + lineage -> resume chains
- What: map each Cyberine run to a session:
agent_session_startat dispatch,agent_session_endat terminal state with the status mapping (section 2.2). ThreadRunLineage.parentRunId->parent_session_id; parkpreviousRunId/chatSessionId/rootRunIdinmetadata(section 2.3). - Cyberine seam:
src/launchBg.ts(session start at dispatch, stash session id keyed by runId),src/store/MemoryStore.tswriteRecord(session end). Resolve parent session id from the parent run's storedmetadata.cyberineRunId. - ContextQ:
agent_session_start(agent.ts:118),agent_session_end(agent.ts:173).
Phase 4 — agent_checkpoint / agent_resume mid-run (closes the Cyberine gap)
- What: add periodic checkpoints during long runs (the capability Cyberine lacks today).
Snapshot working state after each step; on restart,
agent_resumereloads the latest checkpoint + open tasks. - Cyberine seam: the
WorkflowEnginestep loop (src/engine/) emitscheckpoint(sessionId, state, summary)per completed step; a new resume path readsagent_resumeon relaunch. This is net-new Cyberine behavior, gated oncapabilities.checkpoint. - ContextQ:
agent_checkpoint/POST /api/agent/sessions/:id/checkpoint(agent.ts:211),agent_resume/GET /api/agent/sessions/:id/resume(agent.ts:239). Mind the monotonicseqoptimistic lock (agent.service.ts:404) under concurrent writers.
Phase 5 — agent_task / agent_lesson / agent_handoff writers
- What: decompose
outcomeper section 2.4 —accomplished-> verified tasks (with synthesizedevidence),next-> pending tasks,issues-> lessons (embedded for recall) or blocked tasks; thenagent_handoffat session end to synthesize the workspace handoff doc. - Cyberine seam: a new
src/store/outcomeToContextq.tsmapping helper invoked fromwriteRecordon the ContextQ backend; gated oncapabilities.tasks/lessons/handoff.Update 2026-09-22: shipped as
src/store/outcomeMapping.ts(outcomeToTaskPayloads/outcomeToLessonPayloads), invoked fromContextqBackend.pushSession(src/store/memoryBackend.ts:133-136), not fromwriteRecord. - ContextQ:
agent_task_upsert(agent.ts:282),agent_task_tick(agent.ts:336, evidence-gated),agent_lesson_add(agent.ts:372),agent_handoff(agent.ts:413, needs an LLM provider configured server-side or it 503s,agent.ts:467).
6. Gaps vs the design draft
Differences between what the earlier agent-memory-layer design draft predicted and
what ContextQ actually ships, plus the Cyberine-side mismatches to plan around:
-
Search type-enum did NOT get widened to match create. The draft surface treats context types uniformly, but in the shipped code
POST /api/contextsandctx_saveaccept 9 types (incl.synthesis/map/moc,contexts.ts:41,tools.ts:55) whilePOST /api/searchstill hard-rejects anything beyond the original 6 (search.ts:12). Phase 1 must avoid the extra types or run records become un-search-filterable. Flag for the ContextQ team: widensearchSchema.typeto the full set. -
Embedding dimension is 768, not 384. The Drizzle custom type is named
vector384but its SQL type isvector(768)(schema.ts:28-32) and lessons embed at 768 (agent.service.ts). Any Cyberine-side embedding parity work must target 768-dim; the name is misleading. -
Lessons are written without the vector through Drizzle.
addLessoninserts the row first, then writes the embedding via a rawUPDATE ... ::vectorSQL (agent.service.ts:483-501) because the custom type has notoDriverserializer. If embeddings are disabled server-side, lessons store with a null vector and boot falls back to recency ordering (agent.service.ts:572) — Cyberine should not assume kNN recall is always available. -
Single
parent_session_idvs Cyberine's 4-way lineage. ContextQ ships exactly one self-referential session link (schema.ts:1342); Cyberine'sRunLineagehas four distinct edges (runTypes.ts:22). The mapping parks three of them inmetadata— there is no first-class home forpreviousRunId/chatSessionId/rootRunId. -
agent_handoffis not self-contained — it needs a server LLM + a workspace. It wraps the dream-handoff generator and 503s when no LLM provider is configured (agent.ts:467), 400s (no_workspace) when neither the body nor the session carries a workspace (agent.ts:438). Phase 5 must provision both; it is not a pure data write. -
No first-class column for
runId,workflowName,workers, orverdict. All ride inagent_sessions.metadatajsonb (schema.ts:1349). The runId-as-metadata join key is the load-bearing link between Cyberine's UUID world and ContextQ's serial ids — get it wrong and resume-chain resolution (phase 3) breaks.
Bus/Presence/MCP Auth Scope (T584)
apiTokenIsValid (src/store/apiTokens.ts) is wired into exactly one place: the REST authorized() check in src/web/security.ts, which is itself skipped entirely when the web server is bound to...
Plugin Marketplace / Discovery Registry — Build vs Defer (T542)
This note decides ONE thing: whether Cyberine should build a hosted marketplace / discovery registry for plugins — a remote index a user can search/browse, with install-from-registry as a third...