@avocadostudio-ai/orchestrator-core 0.22.0 → 0.23.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 (75) hide show
  1. package/dist/agent/agent-loop-openai.js +3 -1
  2. package/dist/agent/agent-provider.js +2 -2
  3. package/dist/agents/backtest.d.ts +35 -0
  4. package/dist/agents/backtest.js +92 -0
  5. package/dist/agents/builtins.d.ts +21 -0
  6. package/dist/agents/builtins.js +102 -0
  7. package/dist/agents/draft-spec.d.ts +44 -0
  8. package/dist/agents/draft-spec.js +195 -0
  9. package/dist/agents/edit-safety-resolve.d.ts +70 -0
  10. package/dist/agents/edit-safety-resolve.js +195 -0
  11. package/dist/agents/edit-safety-runner.d.ts +68 -0
  12. package/dist/agents/edit-safety-runner.js +156 -0
  13. package/dist/agents/edit-safety.d.ts +82 -0
  14. package/dist/agents/edit-safety.js +425 -0
  15. package/dist/agents/fix-writer.d.ts +38 -0
  16. package/dist/agents/fix-writer.js +197 -0
  17. package/dist/agents/inbox.d.ts +141 -0
  18. package/dist/agents/inbox.js +360 -0
  19. package/dist/agents/learning.d.ts +32 -0
  20. package/dist/agents/learning.js +71 -0
  21. package/dist/agents/mention.d.ts +76 -0
  22. package/dist/agents/mention.js +161 -0
  23. package/dist/agents/run-spec.d.ts +27 -0
  24. package/dist/agents/run-spec.js +82 -0
  25. package/dist/agents/schedule.d.ts +22 -0
  26. package/dist/agents/schedule.js +53 -0
  27. package/dist/agents/settings.d.ts +30 -0
  28. package/dist/agents/settings.js +64 -0
  29. package/dist/agents/spec.d.ts +84 -0
  30. package/dist/agents/spec.js +70 -0
  31. package/dist/agents/stats.d.ts +15 -0
  32. package/dist/agents/stats.js +48 -0
  33. package/dist/agents/templates.d.ts +34 -0
  34. package/dist/agents/templates.js +268 -0
  35. package/dist/agents/tick.d.ts +59 -0
  36. package/dist/agents/tick.js +161 -0
  37. package/dist/chat/anthropic-model-caps.d.ts +19 -0
  38. package/dist/chat/anthropic-model-caps.js +56 -0
  39. package/dist/chat/anthropic-planner.d.ts +1 -0
  40. package/dist/chat/anthropic-planner.js +72 -15
  41. package/dist/chat/chat-pipeline.js +54 -1
  42. package/dist/chat/model-defaults.js +6 -3
  43. package/dist/checks/rules-draft.d.ts +6 -1
  44. package/dist/checks/rules-draft.js +10 -7
  45. package/dist/checks/rules-i18n.d.ts +25 -0
  46. package/dist/checks/rules-i18n.js +193 -0
  47. package/dist/checks/run-checks.d.ts +16 -1
  48. package/dist/checks/run-checks.js +80 -63
  49. package/dist/checks/session-runner.js +7 -0
  50. package/dist/checks/types.d.ts +6 -0
  51. package/dist/durable/in-memory-durable-store.d.ts +34 -1
  52. package/dist/durable/in-memory-durable-store.js +122 -1
  53. package/dist/durable/index.d.ts +1 -1
  54. package/dist/durable/sqlite-durable-store.d.ts +19 -1
  55. package/dist/durable/sqlite-durable-store.js +220 -1
  56. package/dist/durable/types.d.ts +146 -0
  57. package/dist/handler/create-orchestrator.js +191 -1
  58. package/dist/http/history-actions.d.ts +22 -0
  59. package/dist/http/history-actions.js +33 -5
  60. package/dist/http/inbox-actions.d.ts +156 -0
  61. package/dist/http/inbox-actions.js +498 -0
  62. package/dist/http/ops-actions.d.ts +17 -1
  63. package/dist/http/ops-actions.js +36 -0
  64. package/dist/http/variation-preview-actions.d.ts +30 -0
  65. package/dist/http/variation-preview-actions.js +75 -0
  66. package/dist/index.d.ts +10 -1
  67. package/dist/index.js +9 -0
  68. package/dist/nlp/deterministic-planner.d.ts +1 -1
  69. package/dist/nlp/intent-detection.d.ts +15 -0
  70. package/dist/nlp/intent-detection.js +5 -0
  71. package/dist/state/session-state.d.ts +11 -3
  72. package/dist/state/session-state.js +8 -0
  73. package/dist/state/sqlite-store.d.ts +12 -0
  74. package/dist/telemetry/usage.js +4 -0
  75. package/package.json +3 -3
@@ -16,16 +16,19 @@ import { DRAFT_RULES } from "./rules-draft.js";
16
16
  export function fingerprintFor(scopeKey, slug, ruleId, key = "") {
17
17
  return createHash("sha256").update([scopeKey, slug, ruleId, key].join("\u0000")).digest("hex").slice(0, 32);
18
18
  }
19
- export async function runDraftChecks(args) {
20
- const store = args.store ?? getDurableStore();
21
- const now = args.now ?? Date.now;
19
+ /**
20
+ * The rules over a site, with no store and no run ledger — pure in-memory work
21
+ * over the pages given. `runDraftChecks` records what this returns; the first
22
+ * run's backtest and the re-check before a fix is applied use it directly,
23
+ * because neither may write a finding.
24
+ */
25
+ export function evaluateDraftChecks(args) {
22
26
  const rules = args.rules ?? DRAFT_RULES;
23
- const runId = args.runId ?? randomUUID();
24
- const startedAt = now();
25
27
  const site = {
26
28
  slugs: args.pages.map((p) => p.slug),
27
29
  pages: args.pages.map((p) => ({ slug: p.slug, title: p.title, ...(p.meta ? { meta: p.meta } : {}) })),
28
30
  config: args.siteConfig ?? {},
31
+ page: (slug) => args.pages.find((p) => p.slug === slug),
29
32
  // Present only when the caller could enumerate the site's documents.
30
33
  // Passing `[]` where it could not would make every file link report broken.
31
34
  ...(args.assets ? { assets: args.assets } : {})
@@ -50,73 +53,85 @@ export async function runDraftChecks(args) {
50
53
  linksBySlug.set(page.slug, walkPageLinks(page, args.manifest));
51
54
  }
52
55
  const weights = computePageWeights({ pages: site.pages, fieldsBySlug, config: site.config });
56
+ const findings = [];
57
+ const seen = new Set();
58
+ let error;
59
+ for (const page of scanned) {
60
+ const pageWeight = weights.get(page.slug)?.weight ?? NEUTRAL_PAGE_WEIGHT;
61
+ const ctx = {
62
+ scopeKey: args.scopeKey,
63
+ page,
64
+ site,
65
+ manifest: args.manifest,
66
+ fields: fieldsBySlug.get(page.slug) ?? [],
67
+ links: linksBySlug.get(page.slug) ?? []
68
+ };
69
+ for (const rule of rules) {
70
+ // One rule throwing must not cost the run every other rule's findings —
71
+ // a checker that goes dark because a custom block had an unexpected
72
+ // prop shape is worse than one that reports twelve of thirteen rules.
73
+ let produced;
74
+ try {
75
+ produced = rule.run(ctx);
76
+ }
77
+ catch (err) {
78
+ error ??= `${rule.id}: ${err instanceof Error ? err.message : String(err)}`;
79
+ continue;
80
+ }
81
+ for (const finding of produced) {
82
+ const fingerprint = fingerprintFor(args.scopeKey, page.slug, rule.id, finding.key);
83
+ /*
84
+ * Two findings from one rule sharing a key are one finding as far as
85
+ * the store is concerned — the second upsert overwrites the first.
86
+ * Collapsing them here instead keeps the ledger honest (the second
87
+ * was being counted as an `updated` row) and makes which one survives
88
+ * a decision rather than an accident of iteration order.
89
+ */
90
+ if (seen.has(fingerprint))
91
+ continue;
92
+ seen.add(fingerprint);
93
+ const severity = finding.severity ?? rule.severity;
94
+ findings.push({
95
+ fingerprint,
96
+ scopeKey: args.scopeKey,
97
+ slug: page.slug,
98
+ ruleId: rule.id,
99
+ agent: rule.agent,
100
+ severity,
101
+ impact: impactFor(severity, pageWeight),
102
+ title: finding.title,
103
+ ...(finding.detail ? { detail: finding.detail } : {}),
104
+ ...(finding.evidence ? { evidence: finding.evidence } : {}),
105
+ ...(finding.proposedOps ? { proposedOps: finding.proposedOps } : {})
106
+ });
107
+ }
108
+ }
109
+ }
110
+ return { findings, scannedSlugs: scanned.map((p) => p.slug), ...(error ? { error } : {}) };
111
+ }
112
+ export async function runDraftChecks(args) {
113
+ const store = args.store ?? getDurableStore();
114
+ const now = args.now ?? Date.now;
115
+ const rules = args.rules ?? DRAFT_RULES;
116
+ const runId = args.runId ?? randomUUID();
117
+ const startedAt = now();
53
118
  await store.startCheckRun({
54
119
  id: runId,
55
120
  scopeKey: args.scopeKey,
56
- agent: rules.length === 1 ? rules[0].agent : "checks",
121
+ agent: args.agent ?? (rules.length === 1 ? rules[0].agent : "checks"),
57
122
  trigger: args.trigger ?? "manual",
58
123
  startedAt
59
124
  });
60
- const findings = [];
61
- const seen = new Set();
62
125
  let error;
63
126
  try {
64
- for (const page of scanned) {
65
- const pageWeight = weights.get(page.slug)?.weight ?? NEUTRAL_PAGE_WEIGHT;
66
- const ctx = {
67
- scopeKey: args.scopeKey,
68
- page,
69
- site,
70
- manifest: args.manifest,
71
- fields: fieldsBySlug.get(page.slug) ?? [],
72
- links: linksBySlug.get(page.slug) ?? []
73
- };
74
- for (const rule of rules) {
75
- // One rule throwing must not cost the run every other rule's findings —
76
- // a checker that goes dark because a custom block had an unexpected
77
- // prop shape is worse than one that reports twelve of thirteen rules.
78
- let produced;
79
- try {
80
- produced = rule.run(ctx);
81
- }
82
- catch (err) {
83
- error ??= `${rule.id}: ${err instanceof Error ? err.message : String(err)}`;
84
- continue;
85
- }
86
- for (const finding of produced) {
87
- const fingerprint = fingerprintFor(args.scopeKey, page.slug, rule.id, finding.key);
88
- /*
89
- * Two findings from one rule sharing a key are one finding as far as
90
- * the store is concerned — the second upsert overwrites the first.
91
- * Collapsing them here instead keeps the ledger honest (the second
92
- * was being counted as an `updated` row) and makes which one survives
93
- * a decision rather than an accident of iteration order.
94
- */
95
- if (seen.has(fingerprint))
96
- continue;
97
- seen.add(fingerprint);
98
- const severity = finding.severity ?? rule.severity;
99
- findings.push({
100
- fingerprint,
101
- scopeKey: args.scopeKey,
102
- slug: page.slug,
103
- ruleId: rule.id,
104
- agent: rule.agent,
105
- severity,
106
- impact: impactFor(severity, pageWeight),
107
- title: finding.title,
108
- ...(finding.detail ? { detail: finding.detail } : {}),
109
- ...(finding.evidence ? { evidence: finding.evidence } : {}),
110
- ...(finding.proposedOps ? { proposedOps: finding.proposedOps } : {})
111
- });
112
- }
113
- }
114
- }
127
+ const evaluated = evaluateDraftChecks(args);
128
+ const findings = evaluated.findings;
129
+ error = evaluated.error;
115
130
  const { opened } = await store.recordFindings(runId, findings);
116
131
  // Reconcile per agent, not once for the whole run. A run of only the SEO
117
132
  // rules that closed everything in scope would mark this morning's
118
133
  // accessibility findings fixed without having looked at them.
119
- const scannedSlugs = scanned.map((p) => p.slug);
134
+ const scannedSlugs = evaluated.scannedSlugs;
120
135
  let closed = 0;
121
136
  for (const agent of new Set(rules.map((r) => r.agent))) {
122
137
  const result = await store.reconcileFindings({
@@ -131,11 +146,11 @@ export async function runDraftChecks(args) {
131
146
  const record = {
132
147
  id: runId,
133
148
  scopeKey: args.scopeKey,
134
- agent: rules.length === 1 ? rules[0].agent : "checks",
149
+ agent: args.agent ?? (rules.length === 1 ? rules[0].agent : "checks"),
135
150
  trigger: args.trigger ?? "manual",
136
151
  startedAt,
137
152
  finishedAt: now(),
138
- pagesScanned: scanned.length,
153
+ pagesScanned: scannedSlugs.length,
139
154
  findingsOpened: opened,
140
155
  findingsClosed: closed,
141
156
  costUsd: 0,
@@ -153,7 +168,9 @@ export async function runDraftChecks(args) {
153
168
  }
154
169
  catch (err) {
155
170
  const reason = err instanceof Error ? err.message : String(err);
156
- await store.finishCheckRun(runId, { finishedAt: now(), pagesScanned: scanned.length, error: reason });
171
+ const wanted = args.slugs ? new Set(args.slugs) : null;
172
+ const pagesScanned = args.pages.filter((p) => (wanted ? wanted.has(p.slug) : true)).length;
173
+ await store.finishCheckRun(runId, { finishedAt: now(), pagesScanned, error: reason });
157
174
  throw err;
158
175
  }
159
176
  }
@@ -54,6 +54,13 @@ function enabled(flag) {
54
54
  return process.env.CHECKS_ON_PUBLISH !== "0";
55
55
  }
56
56
  function runInBackground(scopeKey, trigger, log) {
57
+ // Custom agents switched on for this event run after the draft checker,
58
+ // on the same engine (agents/run-spec.ts). Imported lazily: the agents
59
+ // module reads settings from the durable store, which the checker alone
60
+ // never needs.
61
+ void import("../agents/run-spec.js")
62
+ .then(({ runCustomAgentsOn }) => runCustomAgentsOn({ scopeKey, on: trigger === "on_apply" ? "apply" : "publish", ...(log ? { log } : {}) }))
63
+ .catch((err) => log?.error({ err: String(err), scopeKey, trigger }, "agent runs failed"));
57
64
  void runChecksForSession({ scopeKey, trigger })
58
65
  .then((run) => {
59
66
  log?.info({ scopeKey, trigger, opened: run.findingsOpened, closed: run.findingsClosed }, "checks run complete");
@@ -60,6 +60,12 @@ export type SiteView = {
60
60
  meta?: PageDoc["meta"];
61
61
  }>;
62
62
  config: SiteConfig;
63
+ /**
64
+ * Another page in full, by slug. Most rules need only titles; the
65
+ * translation rules compare a page with its source-language counterpart
66
+ * block by block. Absent where a caller built a SiteView by hand.
67
+ */
68
+ page?: (slug: string) => PageDoc | undefined;
63
69
  /**
64
70
  * The documents the site holds, when it can enumerate them.
65
71
  *
@@ -1,4 +1,4 @@
1
- import type { CheckRunInput, CheckRunPatch, CorrectionInput, CorrectionQuery, DurableStore, FindingInput, FindingQuery, FindingSeverity, FindingStatus, MemoryInput, MemoryQuery, MemoryStatus, ProposalInput, ProposalQuery, ProposalStatus } from "./types.ts";
1
+ import type { CheckRunInput, CheckRunPatch, CorrectionInput, CorrectionQuery, DurableStore, FindingFix, FindingInput, FindingQuery, FindingSeverity, FindingStatus, MemoryInput, MemoryQuery, MemoryStatus, ProposalInput, ProposalQuery, ProposalStatus, InboxItemInput, InboxItemQuery, InboxItemRecord, InboxItemResolution, AgentSettingInput } from "./types.ts";
2
2
  export type InMemoryDurableStoreOptions = {
3
3
  now?: () => number;
4
4
  };
@@ -10,6 +10,8 @@ export declare class InMemoryDurableStore implements DurableStore {
10
10
  private readonly memory;
11
11
  private readonly corrections;
12
12
  private readonly proposals;
13
+ private readonly inbox;
14
+ private readonly agents;
13
15
  private readonly now;
14
16
  constructor(options?: InMemoryDurableStoreOptions);
15
17
  /** Keep the newest `cap` rows per scope, matching the SQLite store's trim. */
@@ -40,6 +42,7 @@ export declare class InMemoryDurableStore implements DurableStore {
40
42
  evidence?: import("./types.ts").FindingEvidence;
41
43
  proposedOps?: import("@avocadostudio-ai/shared").Operation[];
42
44
  id: string;
45
+ fix?: FindingFix;
43
46
  status: FindingStatus;
44
47
  lastRunId: string;
45
48
  firstSeenAt: number;
@@ -60,6 +63,7 @@ export declare class InMemoryDurableStore implements DurableStore {
60
63
  evidence?: import("./types.ts").FindingEvidence;
61
64
  proposedOps?: import("@avocadostudio-ai/shared").Operation[];
62
65
  id: string;
66
+ fix?: FindingFix;
63
67
  status: FindingStatus;
64
68
  lastRunId: string;
65
69
  firstSeenAt: number;
@@ -67,6 +71,7 @@ export declare class InMemoryDurableStore implements DurableStore {
67
71
  resolvedAt?: number;
68
72
  snoozedUntil?: number;
69
73
  } | null>;
74
+ setFindingFix(id: string, fix: FindingFix | null): Promise<void>;
70
75
  setFindingStatus(id: string, status: FindingStatus, at?: number): Promise<void>;
71
76
  wakeSnoozedFindings(scopeKey: string, now?: number): Promise<{
72
77
  woken: number;
@@ -200,4 +205,32 @@ export declare class InMemoryDurableStore implements DurableStore {
200
205
  expireProposals(now?: number): Promise<{
201
206
  expired: number;
202
207
  }>;
208
+ putInboxItem(input: InboxItemInput): Promise<InboxItemRecord>;
209
+ getInboxItem(id: string): Promise<InboxItemRecord | null>;
210
+ listInboxItems(query: InboxItemQuery): Promise<InboxItemRecord[]>;
211
+ resolveInboxItem(id: string, resolution: InboxItemResolution): Promise<InboxItemRecord | null>;
212
+ reopenInboxItem(id: string, change?: {
213
+ payload?: Record<string, unknown>;
214
+ at?: number;
215
+ }): Promise<InboxItemRecord | null>;
216
+ putAgentSetting(input: AgentSettingInput, at?: number): Promise<{
217
+ scopeKey: string;
218
+ agentId: string;
219
+ enabled: boolean;
220
+ owner?: string;
221
+ spec?: Record<string, unknown>;
222
+ createdAt: number;
223
+ updatedAt: number;
224
+ }>;
225
+ deleteAgentSetting(scopeKey: string, agentId: string): Promise<boolean>;
226
+ listAgentScopes(): Promise<string[]>;
227
+ listAgentSettings(scopeKey: string): Promise<{
228
+ scopeKey: string;
229
+ agentId: string;
230
+ enabled: boolean;
231
+ owner?: string;
232
+ spec?: Record<string, unknown>;
233
+ createdAt: number;
234
+ updatedAt: number;
235
+ }[]>;
203
236
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { CHECK_RUN_CAP, CORRECTION_CAP, PROPOSAL_CAP, SNOOZE_WINDOW_MS } from "./sqlite-durable-store.js";
2
+ import { CHECK_RUN_CAP, CORRECTION_CAP, INBOX_RESOLVED_CAP, PROPOSAL_CAP, SNOOZE_WINDOW_MS } from "./sqlite-durable-store.js";
3
3
  import { fallbackImpact } from "./finding-impact.js";
4
4
  /*
5
5
  * In-memory `DurableStore`. Two jobs, both real.
@@ -28,6 +28,8 @@ export class InMemoryDurableStore {
28
28
  memory = new Map();
29
29
  corrections = [];
30
30
  proposals = new Map();
31
+ inbox = new Map();
32
+ agents = new Map();
31
33
  now;
32
34
  constructor(options = {}) {
33
35
  this.now = options.now ?? Date.now;
@@ -154,6 +156,17 @@ export class InMemoryDurableStore {
154
156
  const record = this.findings.get(id);
155
157
  return record ? { ...record } : null;
156
158
  }
159
+ async setFindingFix(id, fix) {
160
+ const record = this.findings.get(id);
161
+ if (!record)
162
+ return;
163
+ const next = { ...record };
164
+ if (fix)
165
+ next.fix = structuredClone(fix);
166
+ else
167
+ delete next.fix;
168
+ this.findings.set(id, next);
169
+ }
157
170
  async setFindingStatus(id, status, at) {
158
171
  const record = this.findings.get(id);
159
172
  if (!record)
@@ -360,4 +373,112 @@ export class InMemoryDurableStore {
360
373
  }
361
374
  return { expired };
362
375
  }
376
+ // Inbox items ------------------------------------------------------------
377
+ async putInboxItem(input) {
378
+ const at = this.now();
379
+ const existing = this.inbox.get(input.id);
380
+ const record = existing
381
+ ? {
382
+ ...input,
383
+ createdAt: existing.createdAt,
384
+ scopeKey: existing.scopeKey,
385
+ status: existing.status,
386
+ revision: existing.revision + 1,
387
+ updatedAt: at,
388
+ ...(existing.resolvedAt != null ? { resolvedAt: existing.resolvedAt } : {}),
389
+ ...(existing.resolution != null ? { resolution: existing.resolution } : {}),
390
+ ...(existing.resolvedBy != null ? { resolvedBy: existing.resolvedBy } : {}),
391
+ ...(existing.note != null ? { note: existing.note } : {})
392
+ }
393
+ : { ...input, status: "open", revision: 1, updatedAt: at };
394
+ this.inbox.set(input.id, record);
395
+ const closed = [...this.inbox.values()]
396
+ .filter((row) => row.scopeKey === record.scopeKey && row.status !== "open")
397
+ .sort((a, b) => b.createdAt - a.createdAt);
398
+ closed.slice(INBOX_RESOLVED_CAP).forEach((row) => this.inbox.delete(row.id));
399
+ return structuredClone(record);
400
+ }
401
+ async getInboxItem(id) {
402
+ const record = this.inbox.get(id);
403
+ return record ? structuredClone(record) : null;
404
+ }
405
+ async listInboxItems(query) {
406
+ const statuses = query.status ? (Array.isArray(query.status) ? query.status : [query.status]) : null;
407
+ const limit = Math.max(1, Math.min(query.limit ?? 200, 1000));
408
+ return [...this.inbox.values()]
409
+ .filter((row) => row.scopeKey === query.scopeKey)
410
+ .filter((row) => (statuses ? statuses.includes(row.status) : true))
411
+ .filter((row) => (query.kind ? row.kind === query.kind : true))
412
+ .filter((row) => (query.agent ? row.agent === query.agent : true))
413
+ .filter((row) => (query.updatedSince != null ? row.updatedAt > query.updatedSince : true))
414
+ .sort((a, b) => b.createdAt - a.createdAt)
415
+ .slice(0, limit)
416
+ .map((row) => structuredClone(row));
417
+ }
418
+ async resolveInboxItem(id, resolution) {
419
+ const current = this.inbox.get(id);
420
+ if (!current || current.status !== "open")
421
+ return null;
422
+ const at = resolution.at ?? this.now();
423
+ const payload = resolution.payload ? { ...(current.payload ?? {}), ...resolution.payload } : current.payload;
424
+ const next = {
425
+ ...current,
426
+ status: "resolved",
427
+ resolvedAt: at,
428
+ resolution: resolution.resolution,
429
+ revision: current.revision + 1,
430
+ updatedAt: at,
431
+ ...(payload ? { payload } : {}),
432
+ ...(resolution.resolvedBy != null ? { resolvedBy: resolution.resolvedBy } : {}),
433
+ ...(resolution.note != null ? { note: resolution.note } : {})
434
+ };
435
+ this.inbox.set(id, next);
436
+ return structuredClone(next);
437
+ }
438
+ async reopenInboxItem(id, change = {}) {
439
+ const current = this.inbox.get(id);
440
+ if (!current || current.status !== "resolved")
441
+ return null;
442
+ const at = change.at ?? this.now();
443
+ const payload = change.payload ? { ...(current.payload ?? {}), ...change.payload } : current.payload;
444
+ const { resolvedAt: _r, resolution: _s, resolvedBy: _b, note: _n, ...rest } = current;
445
+ const next = {
446
+ ...rest,
447
+ status: "open",
448
+ revision: current.revision + 1,
449
+ updatedAt: at,
450
+ ...(payload ? { payload } : {})
451
+ };
452
+ this.inbox.set(id, next);
453
+ return structuredClone(next);
454
+ }
455
+ // Agent settings ---------------------------------------------------------
456
+ async putAgentSetting(input, at) {
457
+ const now = at ?? this.now();
458
+ const key = `${input.scopeKey}\0${input.agentId}`;
459
+ const existing = this.agents.get(key);
460
+ const record = {
461
+ scopeKey: input.scopeKey,
462
+ agentId: input.agentId,
463
+ enabled: input.enabled,
464
+ ...(input.owner != null ? { owner: input.owner } : {}),
465
+ ...(input.spec ? { spec: structuredClone(input.spec) } : existing?.spec ? { spec: existing.spec } : {}),
466
+ createdAt: existing?.createdAt ?? now,
467
+ updatedAt: now
468
+ };
469
+ this.agents.set(key, record);
470
+ return { ...record };
471
+ }
472
+ async deleteAgentSetting(scopeKey, agentId) {
473
+ return this.agents.delete(`${scopeKey}\0${agentId}`);
474
+ }
475
+ async listAgentScopes() {
476
+ return [...new Set([...this.agents.values()].map((row) => row.scopeKey))].sort();
477
+ }
478
+ async listAgentSettings(scopeKey) {
479
+ return [...this.agents.values()]
480
+ .filter((row) => row.scopeKey === scopeKey)
481
+ .sort((a, b) => a.agentId.localeCompare(b.agentId))
482
+ .map((row) => ({ ...row }));
483
+ }
363
484
  }
@@ -2,4 +2,4 @@ export { getDurableStore, resetDurableStore, setDurableStore, durableStoreIsEphe
2
2
  export { loadPendingPlan, savePendingPlan, clearPendingPlan, peekPendingPlan } from "./pending-plan-store.ts";
3
3
  export { SqliteDurableStore, type SqliteDurableStoreOptions } from "./sqlite-durable-store.ts";
4
4
  export { InMemoryDurableStore, type InMemoryDurableStoreOptions } from "./in-memory-durable-store.ts";
5
- export type { DurableStore, FindingInput, FindingRecord, FindingQuery, FindingSeverity, FindingStatus, FindingEvidence, CheckRunInput, CheckRunRecord, CheckRunPatch, CheckRunTrigger, MemoryInput, MemoryRecord, MemoryQuery, MemoryScope, MemoryKind, MemorySource, MemoryStatus, CorrectionInput, CorrectionRecord, CorrectionQuery, CorrectionOutcome, ProposalInput, ProposalRecord, ProposalQuery, ProposalStatus } from "./types.ts";
5
+ export type { DurableStore, FindingInput, FindingRecord, FindingQuery, FindingSeverity, FindingStatus, FindingEvidence, CheckRunInput, CheckRunRecord, CheckRunPatch, CheckRunTrigger, MemoryInput, MemoryRecord, MemoryQuery, MemoryScope, MemoryKind, MemorySource, MemoryStatus, CorrectionInput, CorrectionRecord, CorrectionQuery, CorrectionOutcome, ProposalInput, ProposalRecord, ProposalQuery, ProposalStatus, InboxItemKind, InboxItemStatus, InboxItemInput, InboxItemRecord, InboxItemQuery, InboxItemResolution, AgentSettingInput, AgentSettingRecord } from "./types.ts";
@@ -1,8 +1,13 @@
1
1
  import type { Database as BetterSqliteDatabase } from "better-sqlite3";
2
- import type { CheckRunInput, CheckRunPatch, CheckRunRecord, CorrectionInput, CorrectionQuery, CorrectionRecord, DurableStore, FindingInput, FindingQuery, FindingRecord, FindingStatus, MemoryInput, MemoryQuery, MemoryRecord, MemoryStatus, ProposalInput, ProposalQuery, ProposalRecord, ProposalStatus } from "./types.ts";
2
+ import type { CheckRunInput, CheckRunPatch, CheckRunRecord, CorrectionInput, CorrectionQuery, CorrectionRecord, DurableStore, FindingFix, FindingInput, FindingQuery, FindingRecord, FindingStatus, MemoryInput, MemoryQuery, MemoryRecord, MemoryStatus, ProposalInput, ProposalQuery, ProposalRecord, ProposalStatus, InboxItemInput, InboxItemQuery, InboxItemRecord, InboxItemResolution, AgentSettingInput, AgentSettingRecord } from "./types.ts";
3
3
  export declare const PROPOSAL_CAP = 200;
4
4
  export declare const CORRECTION_CAP = 500;
5
5
  export declare const CHECK_RUN_CAP = 200;
6
+ /**
7
+ * Only closed items count against it: an open item is somebody's decision still
8
+ * to make, and trimming it would silently lift a publish hold.
9
+ */
10
+ export declare const INBOX_RESOLVED_CAP = 500;
6
11
  /**
7
12
  * How long a snooze lasts.
8
13
  *
@@ -38,6 +43,7 @@ export declare class SqliteDurableStore implements DurableStore {
38
43
  }>;
39
44
  listFindings(query: FindingQuery): Promise<FindingRecord[]>;
40
45
  getFinding(id: string): Promise<FindingRecord | null>;
46
+ setFindingFix(id: string, fix: FindingFix | null): Promise<void>;
41
47
  setFindingStatus(id: string, status: FindingStatus, at?: number): Promise<void>;
42
48
  wakeSnoozedFindings(scopeKey: string, now?: number): Promise<{
43
49
  woken: number;
@@ -68,4 +74,16 @@ export declare class SqliteDurableStore implements DurableStore {
68
74
  expireProposals(now?: number): Promise<{
69
75
  expired: number;
70
76
  }>;
77
+ putInboxItem(input: InboxItemInput): Promise<InboxItemRecord>;
78
+ getInboxItem(id: string): Promise<InboxItemRecord | null>;
79
+ listInboxItems(query: InboxItemQuery): Promise<InboxItemRecord[]>;
80
+ resolveInboxItem(id: string, resolution: InboxItemResolution): Promise<InboxItemRecord | null>;
81
+ reopenInboxItem(id: string, change?: {
82
+ payload?: Record<string, unknown>;
83
+ at?: number;
84
+ }): Promise<InboxItemRecord | null>;
85
+ putAgentSetting(input: AgentSettingInput, at?: number): Promise<AgentSettingRecord>;
86
+ deleteAgentSetting(scopeKey: string, agentId: string): Promise<boolean>;
87
+ listAgentScopes(): Promise<string[]>;
88
+ listAgentSettings(scopeKey: string): Promise<AgentSettingRecord[]>;
71
89
  }