opencode-superagent 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -2
  3. package/agent/superagent-router.md +7 -0
  4. package/agent/superagent-worker.md +10 -0
  5. package/dist/server.d.ts +3 -0
  6. package/dist/server.js +115 -0
  7. package/dist/src/abort.d.ts +31 -0
  8. package/dist/src/abort.js +176 -0
  9. package/dist/src/agents.d.ts +51 -0
  10. package/dist/src/agents.js +83 -0
  11. package/dist/src/context.d.ts +20 -0
  12. package/dist/src/context.js +98 -0
  13. package/dist/src/dirs.d.ts +45 -0
  14. package/dist/src/dirs.js +129 -0
  15. package/dist/src/dispatch.d.ts +38 -0
  16. package/dist/src/dispatch.js +214 -0
  17. package/dist/src/format.d.ts +3 -0
  18. package/dist/src/format.js +14 -0
  19. package/dist/src/guard.d.ts +4 -0
  20. package/dist/src/guard.js +9 -0
  21. package/dist/src/prompts.d.ts +1 -0
  22. package/dist/src/prompts.js +36 -0
  23. package/dist/src/protocol.d.ts +210 -0
  24. package/dist/src/protocol.js +88 -0
  25. package/dist/src/recall.d.ts +58 -0
  26. package/dist/src/recall.js +309 -0
  27. package/dist/src/report.d.ts +72 -0
  28. package/dist/src/report.js +147 -0
  29. package/dist/src/router.d.ts +35 -0
  30. package/dist/src/router.js +209 -0
  31. package/dist/src/session-meta.d.ts +42 -0
  32. package/dist/src/session-meta.js +91 -0
  33. package/dist/src/state.d.ts +124 -0
  34. package/dist/src/state.js +431 -0
  35. package/dist/src/ticket-tools.d.ts +45 -0
  36. package/dist/src/ticket-tools.js +148 -0
  37. package/dist/src/ticket-view.d.ts +62 -0
  38. package/dist/src/ticket-view.js +135 -0
  39. package/dist/src/ticket.d.ts +246 -0
  40. package/dist/src/ticket.js +700 -0
  41. package/dist/src/tools.d.ts +85 -0
  42. package/dist/src/tools.js +376 -0
  43. package/dist/src/watchdog.d.ts +16 -0
  44. package/dist/src/watchdog.js +77 -0
  45. package/dist/src/watcher.d.ts +11 -0
  46. package/dist/src/watcher.js +181 -0
  47. package/dist/tui.d.ts +3 -0
  48. package/dist/tui.js +1027 -0
  49. package/package.json +79 -4
@@ -0,0 +1,209 @@
1
+ import { Agent } from "@opencode/plugin/effect";
2
+ import { Effect, Option } from "effect";
3
+ import { ROUTER_AGENT } from "./agents.js";
4
+ import { threadDirHint } from "./dirs.js";
5
+ import { formatTrigger } from "./format.js";
6
+ import { ROUTER_TITLE, superagentMetadata } from "./session-meta.js";
7
+ import { ROUTER_KEY, store, toSessionID } from "./state.js";
8
+ import { noteRouterTrigger } from "./ticket.js";
9
+ import { ROUTER_PERMISSIONS } from "./tools.js";
10
+ const MAX_FLUSH = 20;
11
+ /** KV prefix for crash-safe pending triggers. Owned by the router; `state.ts` stays untouched. */
12
+ export const PENDING_PREFIX = "pending:";
13
+ const pendingKey = (id) => `${PENDING_PREFIX}${id}`;
14
+ /** Stable queue identity: explicit id, else source/thread/creation time. */
15
+ function pendingID(trigger, arrivedAt) {
16
+ return trigger.id ?? `${trigger.source}:${trigger.threadID}:${trigger.createdAt ?? arrivedAt}`;
17
+ }
18
+ function keyOf(item) {
19
+ return pendingKey(pendingID(item.trigger, item.arrivedAt));
20
+ }
21
+ function isRecord(value) {
22
+ return typeof value === "object" && value !== null;
23
+ }
24
+ function optionalString(record, key) {
25
+ const value = record[key];
26
+ return typeof value === "string" ? value : undefined;
27
+ }
28
+ function optionalNumber(record, key) {
29
+ const value = record[key];
30
+ return typeof value === "number" ? value : undefined;
31
+ }
32
+ function decodePending(value) {
33
+ if (!isRecord(value))
34
+ return undefined;
35
+ const raw = value["trigger"];
36
+ if (!isRecord(raw))
37
+ return undefined;
38
+ if (typeof raw["source"] !== "string" || typeof raw["threadID"] !== "string")
39
+ return undefined;
40
+ if (typeof raw["text"] !== "string")
41
+ return undefined;
42
+ const arrivedAt = optionalNumber(value, "arrivedAt");
43
+ if (arrivedAt === undefined)
44
+ return undefined;
45
+ return {
46
+ trigger: {
47
+ id: optionalString(raw, "id"),
48
+ source: raw["source"],
49
+ threadID: raw["threadID"],
50
+ author: optionalString(raw, "author"),
51
+ text: raw["text"],
52
+ title: optionalString(raw, "title"),
53
+ createdAt: optionalNumber(raw, "createdAt"),
54
+ },
55
+ arrivedAt,
56
+ };
57
+ }
58
+ function encodePending(item) {
59
+ return {
60
+ trigger: {
61
+ ...(item.trigger.id === undefined ? {} : { id: item.trigger.id }),
62
+ source: item.trigger.source,
63
+ threadID: item.trigger.threadID,
64
+ ...(item.trigger.author === undefined ? {} : { author: item.trigger.author }),
65
+ text: item.trigger.text,
66
+ ...(item.trigger.title === undefined ? {} : { title: item.trigger.title }),
67
+ ...(item.trigger.createdAt === undefined ? {} : { createdAt: item.trigger.createdAt }),
68
+ },
69
+ arrivedAt: item.arrivedAt,
70
+ };
71
+ }
72
+ /**
73
+ * Enqueue a trigger in memory and in KV. The trigger gets a stable id when
74
+ * the caller did not supply one, so a crash-restarted process can dedupe and
75
+ * rehydrate it. Same-id enqueues are ignored.
76
+ */
77
+ export const enqueuePending = Effect.fn("superagent.enqueuePending")(function* (ctx, input) {
78
+ const arrivedAt = Date.now();
79
+ const id = pendingID(input, arrivedAt);
80
+ if (store.pending.some((item) => pendingID(item.trigger, item.arrivedAt) === id))
81
+ return;
82
+ const item = {
83
+ trigger: input.id === undefined ? { ...input, id } : { ...input },
84
+ arrivedAt,
85
+ };
86
+ store.pending.push(item);
87
+ yield* ctx.storage
88
+ .set(keyOf(item), encodePending(item))
89
+ .pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to persist pending trigger", { cause })));
90
+ });
91
+ /**
92
+ * Reload unflushed triggers left in KV by a crashed process back into the
93
+ * memory queue. Never creates a router session: the queue waits until the
94
+ * next `router` RPC or trigger asks for a flush.
95
+ */
96
+ export const hydratePending = Effect.fn("superagent.hydratePending")(function* (ctx) {
97
+ let after;
98
+ do {
99
+ const page = yield* ctx.storage
100
+ .scan({ prefix: PENDING_PREFIX, after, limit: 100 })
101
+ .pipe(Effect.catch(() => Effect.succeed({ entries: [], next: undefined })));
102
+ for (const entry of page.entries) {
103
+ const item = decodePending(entry.value);
104
+ if (item === undefined) {
105
+ yield* ctx.storage.remove(entry.key).pipe(Effect.catch(() => Effect.void));
106
+ continue;
107
+ }
108
+ const id = pendingID(item.trigger, item.arrivedAt);
109
+ if (store.pending.some((existing) => pendingID(existing.trigger, existing.arrivedAt) === id))
110
+ continue;
111
+ store.pending.push(item.trigger.id === undefined ? { trigger: { ...item.trigger, id }, arrivedAt: item.arrivedAt } : item);
112
+ }
113
+ after = page.next;
114
+ } while (after !== undefined);
115
+ });
116
+ /**
117
+ * Return the live router session, creating it when missing. A router id
118
+ * whose session no longer exists is forgotten before creating a new one,
119
+ * so a user-deleted router is recovered automatically.
120
+ */
121
+ export const ensureRouter = Effect.fn("superagent.ensureRouter")(function* (ctx) {
122
+ const sessions = ctx.session;
123
+ const storage = ctx.storage;
124
+ if (store.routerID !== undefined) {
125
+ const cached = yield* sessions.get({ sessionID: toSessionID(store.routerID) }).pipe(Effect.option);
126
+ if (Option.isSome(cached))
127
+ return store.routerID;
128
+ store.routerID = undefined;
129
+ store.routerBusy = false;
130
+ yield* storage.remove(ROUTER_KEY).pipe(Effect.catch(() => Effect.void));
131
+ }
132
+ const stored = yield* storage.get(ROUTER_KEY).pipe(Effect.catch(() => Effect.succeed(undefined)));
133
+ if (typeof stored === "string") {
134
+ const info = yield* sessions.get({ sessionID: toSessionID(stored) }).pipe(Effect.option);
135
+ if (Option.isSome(info)) {
136
+ store.routerID = info.value.id;
137
+ return store.routerID;
138
+ }
139
+ yield* storage.remove(ROUTER_KEY).pipe(Effect.catch(() => Effect.void));
140
+ }
141
+ const agents = ctx.agent;
142
+ const routerAgent = yield* agents.get({ agentID: Agent.ID.make(ROUTER_AGENT) }).pipe(Effect.map((result) => result.data.id), Effect.catch((cause) => Effect.logWarning("superagent router agent missing, creating session without it", { cause }).pipe(Effect.as(undefined))));
143
+ const created = yield* sessions.create({
144
+ title: ROUTER_TITLE,
145
+ ...(routerAgent === undefined ? {} : { agent: routerAgent }),
146
+ metadata: superagentMetadata("router"),
147
+ // Derived from the tool catalog: a tool whose permission action is not
148
+ // allowed here is dropped from the Router's toolset by `Tool.snapshot`,
149
+ // so adding a tool without its action would hide it silently.
150
+ permissions: ROUTER_PERMISSIONS,
151
+ });
152
+ store.routerID = created.id;
153
+ store.routerBusy = false;
154
+ yield* storage
155
+ .set(ROUTER_KEY, created.id)
156
+ .pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to persist router", { cause })));
157
+ return created.id;
158
+ });
159
+ /**
160
+ * Merge queued triggers into one synthetic prompt for the router. Items
161
+ * leave the queue (memory and KV) only after the injection succeeds; on
162
+ * failure they stay queued for the next attempt.
163
+ */
164
+ export const flushPending = Effect.fn("superagent.flushPending")(function* (ctx) {
165
+ if (store.pending.length === 0)
166
+ return;
167
+ const routerID = yield* ensureRouter(ctx);
168
+ const batch = store.pending.slice(0, MAX_FLUSH);
169
+ const text = [
170
+ `${batch.length} new event(s) for the router:`,
171
+ ...batch.map((item) => {
172
+ const base = formatTrigger(item.trigger);
173
+ const hint = threadDirHint(item.trigger.threadID);
174
+ return hint === undefined ? base : `${base}\n${hint}`;
175
+ }),
176
+ ].join("\n\n");
177
+ const delivered = yield* ctx.session
178
+ .synthetic({ sessionID: toSessionID(routerID), text, description: "superagent triggers", delivery: "queue" })
179
+ .pipe(Effect.option);
180
+ if (Option.isNone(delivered)) {
181
+ yield* Effect.logWarning("superagent failed to flush pending triggers, keeping them queued", {
182
+ count: batch.length,
183
+ });
184
+ return;
185
+ }
186
+ store.pending.splice(0, batch.length);
187
+ // Remember what the Router was just told: `ticket_create` copies this
188
+ // trigger into the ticket's authorization (id + quote).
189
+ const newest = batch[batch.length - 1];
190
+ if (newest !== undefined)
191
+ noteRouterTrigger(newest.trigger);
192
+ for (const item of batch) {
193
+ yield* ctx.storage.remove(keyOf(item)).pipe(Effect.catch(() => Effect.void));
194
+ }
195
+ });
196
+ /**
197
+ * Router idle hook: when the router goes idle with nothing pending in the
198
+ * background, flush queued triggers into the same execution. An empty queue
199
+ * returns without admitting input, so the hook never loops on its own.
200
+ */
201
+ export const registerRouterIdle = Effect.fn("superagent.registerRouterIdle")(function* (ctx) {
202
+ yield* ctx.session.hook("experimental.idle", (event) => Effect.gen(function* () {
203
+ if (store.routerID === undefined || event.sessionID !== store.routerID)
204
+ return;
205
+ if (event.background.length > 0)
206
+ return;
207
+ yield* flushPending(ctx);
208
+ }).pipe(Effect.catch((cause) => Effect.logWarning("superagent router idle flush failed", { cause }))));
209
+ });
@@ -0,0 +1,42 @@
1
+ import type { Plugin } from "@opencode/plugin/effect";
2
+ import { Effect, type Schema } from "effect";
3
+ /**
4
+ * Session identity for the Super-agent prototype.
5
+ *
6
+ * A session's role lives in its durable `metadata.superagent` record, not in
7
+ * its title: the title is user-editable and auto-generated titles are not a
8
+ * contract. Every session this plugin creates writes the metadata at create
9
+ * time, and `hydrateSessionMeta` backfills sessions created before that.
10
+ */
11
+ export type SuperagentRole = "router" | "worker";
12
+ export declare const ROUTER_TITLE = "\uD83E\uDDED router-main";
13
+ /** The role recorded in a session's metadata, when the metadata is a Super-agent one. */
14
+ export declare function superagentRole(metadata: unknown): SuperagentRole | undefined;
15
+ export declare function isSuperagentSession(session: {
16
+ readonly metadata?: unknown;
17
+ }): boolean;
18
+ export declare function isRouterSession(session: {
19
+ readonly metadata?: unknown;
20
+ }): boolean;
21
+ export declare function isWorkerSession(session: {
22
+ readonly metadata?: unknown;
23
+ }): boolean;
24
+ /** The `superagent` metadata entry written at create time. */
25
+ export declare function superagentMetadata(role: SuperagentRole, threads?: ReadonlyArray<string>): Record<string, Schema.Json>;
26
+ /**
27
+ * Worker session title. It is convenience only: the session stays
28
+ * identifiable from metadata when a user renames it.
29
+ */
30
+ export declare function workerTitle(input: {
31
+ readonly task: string;
32
+ readonly title?: string;
33
+ readonly threadID?: string;
34
+ }): string;
35
+ /**
36
+ * Backfill `metadata.superagent` on sessions this plugin already owns (the
37
+ * router id plus every recorded worker) so role detection also covers
38
+ * sessions created before the metadata existed. Idempotent: a session whose
39
+ * role already matches is left untouched, and existing metadata is merged
40
+ * rather than replaced.
41
+ */
42
+ export declare const hydrateSessionMeta: (ctx: Plugin.Context) => Effect.Effect<void, never, never>;
@@ -0,0 +1,91 @@
1
+ import { Effect, Option } from "effect";
2
+ import { store, toSessionID } from "./state.js";
3
+ const WORKER_TITLE_PREFIX = "🔧 worker-";
4
+ export const ROUTER_TITLE = "🧭 router-main";
5
+ const TITLE_SLUG_MAX = 40;
6
+ const THREAD_LABEL_MAX = 12;
7
+ function isRecord(value) {
8
+ return typeof value === "object" && value !== null;
9
+ }
10
+ /** The role recorded in a session's metadata, when the metadata is a Super-agent one. */
11
+ export function superagentRole(metadata) {
12
+ if (!isRecord(metadata))
13
+ return undefined;
14
+ const superagent = metadata["superagent"];
15
+ if (!isRecord(superagent))
16
+ return undefined;
17
+ const role = superagent["role"];
18
+ return role === "router" || role === "worker" ? role : undefined;
19
+ }
20
+ export function isSuperagentSession(session) {
21
+ return superagentRole(session.metadata) !== undefined;
22
+ }
23
+ export function isRouterSession(session) {
24
+ return superagentRole(session.metadata) === "router";
25
+ }
26
+ export function isWorkerSession(session) {
27
+ return superagentRole(session.metadata) === "worker";
28
+ }
29
+ /** The `superagent` metadata entry written at create time. */
30
+ export function superagentMetadata(role, threads) {
31
+ return {
32
+ superagent: role === "worker" ? { role, threads: threads === undefined ? [] : [...threads] } : { role },
33
+ };
34
+ }
35
+ /**
36
+ * Short label for a thread id: the last `:`-separated segment, so
37
+ * `telegram:123456789` becomes `123456789` and long ids keep their tail.
38
+ */
39
+ function threadLabel(threadID) {
40
+ const segment = (threadID.split(":").pop() ?? threadID).trim();
41
+ if (segment === "")
42
+ return "thread";
43
+ return segment.length > THREAD_LABEL_MAX ? segment.slice(-THREAD_LABEL_MAX) : segment;
44
+ }
45
+ function titleSlug(text) {
46
+ const clean = text.replace(/\s+/g, " ").trim();
47
+ if (clean === "")
48
+ return "task";
49
+ return clean.length > TITLE_SLUG_MAX ? `${clean.slice(0, TITLE_SLUG_MAX - 1).trimEnd()}…` : clean;
50
+ }
51
+ /**
52
+ * Worker session title. It is convenience only: the session stays
53
+ * identifiable from metadata when a user renames it.
54
+ */
55
+ export function workerTitle(input) {
56
+ const subject = titleSlug(input.title ?? input.task.split("\n")[0] ?? "");
57
+ const label = input.threadID === undefined ? subject : `${threadLabel(input.threadID)}-${subject}`;
58
+ return `${WORKER_TITLE_PREFIX}${label}`;
59
+ }
60
+ /**
61
+ * Backfill `metadata.superagent` on sessions this plugin already owns (the
62
+ * router id plus every recorded worker) so role detection also covers
63
+ * sessions created before the metadata existed. Idempotent: a session whose
64
+ * role already matches is left untouched, and existing metadata is merged
65
+ * rather than replaced.
66
+ */
67
+ export const hydrateSessionMeta = Effect.fn("superagent.hydrateSessionMeta")(function* (ctx) {
68
+ const targets = [];
69
+ if (store.routerID !== undefined)
70
+ targets.push({ sessionID: store.routerID, role: "router" });
71
+ for (const worker of store.workers.values())
72
+ targets.push({ sessionID: worker.sessionID, role: "worker" });
73
+ for (const target of targets) {
74
+ const current = yield* ctx.session.get({ sessionID: toSessionID(target.sessionID) }).pipe(Effect.option);
75
+ if (Option.isNone(current))
76
+ continue;
77
+ const metadata = current.value.metadata;
78
+ if (superagentRole(metadata) === target.role)
79
+ continue;
80
+ const threads = target.role === "worker" ? store.workers.get(target.sessionID)?.threads : undefined;
81
+ yield* ctx.session
82
+ .update({
83
+ sessionID: toSessionID(target.sessionID),
84
+ metadata: { ...(metadata ?? {}), ...superagentMetadata(target.role, threads) },
85
+ })
86
+ .pipe(Effect.catch((cause) => Effect.logWarning("superagent failed to backfill session metadata", {
87
+ sessionID: target.sessionID,
88
+ cause,
89
+ })));
90
+ }
91
+ });
@@ -0,0 +1,124 @@
1
+ import type { Plugin } from "@opencode/plugin/effect";
2
+ import type { Session } from "@opencode/schema/session";
3
+ import { Effect, type Schema } from "effect";
4
+ export type WorkerState = "running" | "idle" | "failed" | "done" | "aborted";
5
+ export interface WorkerRecord {
6
+ readonly sessionID: string;
7
+ readonly title?: string;
8
+ /**
9
+ * Primary thread this worker was dispatched for. Legacy records decode to
10
+ * `""` and `hydrate` backfills it from `thread.workers`, so every live record
11
+ * carries a forward link to its thread without losing the multi-thread
12
+ * `threads` list.
13
+ */
14
+ threadID: string;
15
+ /** Trigger that authorized the dispatch, when the record knows it. */
16
+ triggerID?: string;
17
+ readonly threads: Array<string>;
18
+ state: WorkerState;
19
+ readonly createdAt: number;
20
+ /** Last persisted write. Refreshed by `saveWorker`, so an aborted record keeps a traceable timestamp. */
21
+ updatedAt?: number;
22
+ lastReport?: string;
23
+ directory?: string;
24
+ }
25
+ export interface ThreadRecord {
26
+ readonly threadID: string;
27
+ readonly workers: Array<string>;
28
+ readonly updatedAt: number;
29
+ }
30
+ export interface TriggerInput {
31
+ readonly id?: string;
32
+ readonly source: string;
33
+ readonly threadID: string;
34
+ readonly author?: string;
35
+ readonly text: string;
36
+ readonly title?: string;
37
+ readonly createdAt?: number;
38
+ }
39
+ export interface PendingTrigger {
40
+ readonly trigger: TriggerInput;
41
+ readonly arrivedAt: number;
42
+ }
43
+ export type TriggerRoute = "router" | "worker";
44
+ export interface TriggerRecord {
45
+ readonly id: string;
46
+ readonly source: string;
47
+ readonly threadID: string;
48
+ readonly author?: string;
49
+ readonly text: string;
50
+ readonly title?: string;
51
+ readonly createdAt: number;
52
+ route?: TriggerRoute;
53
+ sessionID?: string;
54
+ /** Workers dispatched from this trigger (reverse of `WorkerRecord.triggerID`). */
55
+ workerIDs?: Array<string>;
56
+ }
57
+ export interface DirectTriggerInput {
58
+ readonly id: string;
59
+ readonly source: string;
60
+ readonly threadID: string;
61
+ readonly author?: string;
62
+ readonly text: string;
63
+ readonly createdAt: number;
64
+ }
65
+ export declare function triggerIdFor(input: TriggerInput): string;
66
+ /**
67
+ * Queue that transparently records trigger history. `server.ts` and
68
+ * `router.ts` keep using plain `push`/`splice`; interception here writes
69
+ * the memory map synchronously and persists to KV in the background, so
70
+ * no caller changes are needed.
71
+ */
72
+ export declare class PendingQueue extends Array<PendingTrigger> {
73
+ push(...items: Array<PendingTrigger>): number;
74
+ splice(start: number, deleteCount?: number, ...items: Array<PendingTrigger>): Array<PendingTrigger>;
75
+ }
76
+ /**
77
+ * Record a trigger delivered straight to a worker (it never passes the
78
+ * pending queue). The text comes from the admitted prompt, so it carries
79
+ * the same truncation as the worker-visible format; queued triggers keep
80
+ * their full text via `rememberQueued`.
81
+ */
82
+ export declare function upsertDirectTrigger(input: DirectTriggerInput, sessionID: string): TriggerRecord;
83
+ export interface Store {
84
+ routerID: string | undefined;
85
+ routerBusy: boolean;
86
+ readonly workers: Map<string, WorkerRecord>;
87
+ readonly threads: Map<string, Array<string>>;
88
+ readonly pending: Array<PendingTrigger>;
89
+ readonly triggers: Map<string, TriggerRecord>;
90
+ triggerTotal: number;
91
+ }
92
+ export declare const store: Store;
93
+ export declare const ROUTER_KEY = "router";
94
+ export declare const WORKER_PREFIX = "worker:";
95
+ export declare const THREAD_PREFIX = "thread:";
96
+ export declare const TRIGGER_PREFIX = "trigger:";
97
+ export declare const workerKey: (sessionID: string) => string;
98
+ export declare const threadKey: (threadID: string) => string;
99
+ export declare const triggerKey: (id: string) => string;
100
+ /** Brands are compile-time only, so a validated session id string converts freely. */
101
+ export declare const toSessionID: (id: string) => Session.ID;
102
+ export declare function decodeWorker(value: Schema.Json | undefined): WorkerRecord | undefined;
103
+ export declare function decodeThread(value: Schema.Json | undefined): ThreadRecord | undefined;
104
+ export declare function decodeTrigger(value: Schema.Json | undefined): TriggerRecord | undefined;
105
+ export declare function encodeWorker(record: WorkerRecord): Schema.Json;
106
+ export declare function encodeThread(record: ThreadRecord): Schema.Json;
107
+ export declare function encodeTrigger(record: TriggerRecord): Schema.Json;
108
+ /**
109
+ * Load KV state into the in-memory maps. Storage is the unique source of
110
+ * truth; every loaded session is validated with `session.get` and stale
111
+ * entries are cleaned from both memory and KV.
112
+ */
113
+ export declare const hydrate: (ctx: Plugin.Context) => Effect.Effect<void, never, never>;
114
+ /** Every worker write refreshes `updatedAt`, so an aborted record keeps a traceable timestamp. */
115
+ export declare const saveWorker: (ctx: Plugin.Context, record: WorkerRecord) => Effect.Effect<void, never, never>;
116
+ export declare const saveTrigger: (ctx: Plugin.Context, record: TriggerRecord) => Effect.Effect<void, never, never>;
117
+ /**
118
+ * Record the worker on the trigger that authorized it (`trigger.workerIDs`),
119
+ * completing the reverse edge of `WorkerRecord.triggerID`. Memory holds only
120
+ * the newest triggers, so an older trigger is read from KV once; a missing
121
+ * trigger is not invented. Repeating the call is a no-op.
122
+ */
123
+ export declare const linkTriggerToWorker: (ctx: Plugin.Context, triggerID: string, sessionID: string) => Effect.Effect<boolean, never, never>;
124
+ export declare const saveThread: (ctx: Plugin.Context, record: ThreadRecord) => Effect.Effect<void, never, never>;