@ainyc/canonry 2.0.0 → 2.2.1

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.
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  IntelligenceService,
3
+ agentMemory,
3
4
  agentSessions,
4
5
  apiKeys,
5
6
  auditLog,
@@ -26,7 +27,7 @@ import {
26
27
  runs,
27
28
  schedules,
28
29
  usageCounters
29
- } from "./chunk-GH6WGN5B.js";
30
+ } from "./chunk-TAII35VC.js";
30
31
 
31
32
  // src/config.ts
32
33
  import fs from "fs";
@@ -337,11 +338,11 @@ function printCliError(err, format) {
337
338
 
338
339
  // src/server.ts
339
340
  import { createRequire as createRequire2 } from "module";
340
- import crypto23 from "crypto";
341
+ import crypto24 from "crypto";
341
342
  import fs8 from "fs";
342
343
  import path9 from "path";
343
344
  import { fileURLToPath as fileURLToPath2 } from "url";
344
- import { eq as eq25 } from "drizzle-orm";
345
+ import { eq as eq26 } from "drizzle-orm";
345
346
  import Fastify from "fastify";
346
347
 
347
348
  // ../contracts/src/config-schema.ts
@@ -1404,6 +1405,20 @@ function escapeRegExp(value) {
1404
1405
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1405
1406
  }
1406
1407
 
1408
+ // ../contracts/src/agent.ts
1409
+ import { z as z13 } from "zod";
1410
+ var memorySourceSchema = z13.enum(["aero", "user", "compaction"]);
1411
+ var MemorySources = memorySourceSchema.enum;
1412
+ var AGENT_MEMORY_VALUE_MAX_BYTES = 2 * 1024;
1413
+ var AGENT_MEMORY_KEY_MAX_LENGTH = 128;
1414
+ var agentMemoryUpsertRequestSchema = z13.object({
1415
+ key: z13.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH),
1416
+ value: z13.string().min(1)
1417
+ });
1418
+ var agentMemoryDeleteRequestSchema = z13.object({
1419
+ key: z13.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH)
1420
+ });
1421
+
1407
1422
  // ../api-routes/src/auth.ts
1408
1423
  import crypto2 from "crypto";
1409
1424
  import { eq } from "drizzle-orm";
@@ -14787,8 +14802,8 @@ var RunCoordinator = class {
14787
14802
  };
14788
14803
 
14789
14804
  // src/agent/session-registry.ts
14790
- import crypto22 from "crypto";
14791
- import { eq as eq23 } from "drizzle-orm";
14805
+ import crypto23 from "crypto";
14806
+ import { eq as eq24 } from "drizzle-orm";
14792
14807
 
14793
14808
  // src/agent/session.ts
14794
14809
  import fs7 from "fs";
@@ -14814,7 +14829,7 @@ var AGENT_PROVIDERS = {
14814
14829
  [AgentProviderIds.gemini]: {
14815
14830
  piAiProvider: "google",
14816
14831
  label: "Google (Gemini)",
14817
- defaultModel: "gemini-2.5-pro",
14832
+ defaultModel: "gemini-2.5-flash",
14818
14833
  autoDetectPriority: 2
14819
14834
  },
14820
14835
  [AgentProviderIds.zai]: {
@@ -15005,6 +15020,105 @@ function buildSkillDocTools() {
15005
15020
 
15006
15021
  // src/agent/tools.ts
15007
15022
  import { Type as Type2 } from "@sinclair/typebox";
15023
+
15024
+ // src/agent/memory-store.ts
15025
+ import crypto22 from "crypto";
15026
+ import { and as and11, desc as desc9, eq as eq23, like, sql as sql6 } from "drizzle-orm";
15027
+ var COMPACTION_KEY_PREFIX = "compaction:";
15028
+ var COMPACTION_NOTES_PER_SESSION = 3;
15029
+ function rowToDto(row) {
15030
+ return {
15031
+ id: row.id,
15032
+ key: row.key,
15033
+ value: row.value,
15034
+ source: row.source,
15035
+ createdAt: row.createdAt,
15036
+ updatedAt: row.updatedAt
15037
+ };
15038
+ }
15039
+ function listMemoryEntries(db, projectId, opts = {}) {
15040
+ const query = db.select().from(agentMemory).where(eq23(agentMemory.projectId, projectId)).orderBy(desc9(agentMemory.updatedAt));
15041
+ const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
15042
+ return rows.map(rowToDto);
15043
+ }
15044
+ function upsertMemoryEntry(db, args) {
15045
+ if (Buffer.byteLength(args.value, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
15046
+ throw new Error(
15047
+ `memory value exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes (got ${Buffer.byteLength(args.value, "utf8")})`
15048
+ );
15049
+ }
15050
+ if (args.source !== MemorySources.compaction && args.key.startsWith(COMPACTION_KEY_PREFIX)) {
15051
+ throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
15052
+ }
15053
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15054
+ const id = crypto22.randomUUID();
15055
+ db.insert(agentMemory).values({
15056
+ id,
15057
+ projectId: args.projectId,
15058
+ key: args.key,
15059
+ value: args.value,
15060
+ source: args.source,
15061
+ createdAt: now,
15062
+ updatedAt: now
15063
+ }).onConflictDoUpdate({
15064
+ target: [agentMemory.projectId, agentMemory.key],
15065
+ set: {
15066
+ value: args.value,
15067
+ source: args.source,
15068
+ updatedAt: now
15069
+ }
15070
+ }).run();
15071
+ const row = db.select().from(agentMemory).where(and11(eq23(agentMemory.projectId, args.projectId), eq23(agentMemory.key, args.key))).get();
15072
+ if (!row) throw new Error("memory upsert produced no row");
15073
+ return rowToDto(row);
15074
+ }
15075
+ function deleteMemoryEntry(db, projectId, key) {
15076
+ const result = db.delete(agentMemory).where(and11(eq23(agentMemory.projectId, projectId), eq23(agentMemory.key, key))).run();
15077
+ const changes = result.changes ?? 0;
15078
+ return changes > 0;
15079
+ }
15080
+ function loadRecentForHydrate(db, projectId, limit) {
15081
+ return listMemoryEntries(db, projectId, { limit });
15082
+ }
15083
+ function writeCompactionNote(db, args) {
15084
+ if (Buffer.byteLength(args.summary, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
15085
+ throw new Error(
15086
+ `compaction summary exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes; summarizer produced too much text`
15087
+ );
15088
+ }
15089
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15090
+ const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
15091
+ const id = crypto22.randomUUID();
15092
+ let inserted;
15093
+ db.transaction((tx) => {
15094
+ tx.insert(agentMemory).values({
15095
+ id,
15096
+ projectId: args.projectId,
15097
+ key,
15098
+ value: args.summary,
15099
+ source: MemorySources.compaction,
15100
+ createdAt: now,
15101
+ updatedAt: now
15102
+ }).run();
15103
+ const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
15104
+ const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
15105
+ and11(
15106
+ eq23(agentMemory.projectId, args.projectId),
15107
+ like(agentMemory.key, `${sessionPrefix}%`)
15108
+ )
15109
+ ).orderBy(desc9(agentMemory.updatedAt)).all();
15110
+ const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
15111
+ if (stale.length > 0) {
15112
+ tx.delete(agentMemory).where(sql6`${agentMemory.id} IN (${sql6.join(stale.map((s) => sql6`${s}`), sql6`, `)})`).run();
15113
+ }
15114
+ const row = tx.select().from(agentMemory).where(and11(eq23(agentMemory.projectId, args.projectId), eq23(agentMemory.key, key))).get();
15115
+ if (row) inserted = rowToDto(row);
15116
+ });
15117
+ if (!inserted) throw new Error("compaction note write produced no row");
15118
+ return inserted;
15119
+ }
15120
+
15121
+ // src/agent/tools.ts
15008
15122
  var MAX_TOOL_RESULT_CHARS = 2e4;
15009
15123
  function truncate(json) {
15010
15124
  if (json.length <= MAX_TOOL_RESULT_CHARS) return json;
@@ -15144,6 +15258,27 @@ function buildGetRunTool(ctx) {
15144
15258
  }
15145
15259
  };
15146
15260
  }
15261
+ var RecallSchema = Type2.Object({
15262
+ limit: Type2.Optional(
15263
+ Type2.Number({
15264
+ description: "Max notes to return, ordered newest-first. Default 50. Max 100.",
15265
+ minimum: 1,
15266
+ maximum: 100
15267
+ })
15268
+ )
15269
+ });
15270
+ function buildRecallTool(ctx) {
15271
+ return {
15272
+ name: "recall",
15273
+ label: "Recall memory",
15274
+ description: "Read project-scoped durable notes Aero has stored via `remember` (plus compaction summaries). Returns entries newest-first. The N most-recent entries are also injected into the system prompt at session start, so you usually do not need to call this \u2014 reach for it when you need older context or the full note value.",
15275
+ parameters: RecallSchema,
15276
+ execute: async (_toolCallId, params) => {
15277
+ const entries = listMemoryEntries(ctx.db, ctx.projectId, { limit: params.limit ?? 50 });
15278
+ return textResult2({ entries });
15279
+ }
15280
+ };
15281
+ }
15147
15282
  function buildReadTools(ctx) {
15148
15283
  return [
15149
15284
  buildGetStatusTool(ctx),
@@ -15152,7 +15287,8 @@ function buildReadTools(ctx) {
15152
15287
  buildGetInsightsTool(ctx),
15153
15288
  buildListKeywordsTool(ctx),
15154
15289
  buildListCompetitorsTool(ctx),
15155
- buildGetRunTool(ctx)
15290
+ buildGetRunTool(ctx),
15291
+ buildRecallTool(ctx)
15156
15292
  ];
15157
15293
  }
15158
15294
  var RunSweepSchema = Type2.Object({
@@ -15307,6 +15443,58 @@ function buildAttachAgentWebhookTool(ctx) {
15307
15443
  }
15308
15444
  };
15309
15445
  }
15446
+ var RememberSchema = Type2.Object({
15447
+ key: Type2.String({
15448
+ description: `Stable identifier for this note (max ${AGENT_MEMORY_KEY_MAX_LENGTH} chars). Writing the same key overwrites the prior value. Do NOT use the "${COMPACTION_KEY_PREFIX}" prefix \u2014 that namespace is reserved for transcript compaction summaries.`,
15449
+ minLength: 1,
15450
+ maxLength: AGENT_MEMORY_KEY_MAX_LENGTH
15451
+ }),
15452
+ value: Type2.String({
15453
+ description: `Plain-text note to persist (max ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes). Use for durable operator preferences, migration context, or non-obvious reasoning you'll want on a future turn. Do NOT duplicate data canonry already tracks (runs, insights, timelines) \u2014 query those instead.`,
15454
+ minLength: 1
15455
+ })
15456
+ });
15457
+ function buildRememberTool(ctx) {
15458
+ return {
15459
+ name: "remember",
15460
+ label: "Remember",
15461
+ description: "Persist a project-scoped durable note visible to every future Aero session for this project. Upsert \u2014 writing the same key replaces the prior value. Capped at 2 KB per note.",
15462
+ parameters: RememberSchema,
15463
+ execute: async (_toolCallId, params) => {
15464
+ const entry = upsertMemoryEntry(ctx.db, {
15465
+ projectId: ctx.projectId,
15466
+ key: params.key,
15467
+ value: params.value,
15468
+ source: MemorySources.aero
15469
+ });
15470
+ return textResult2({ status: "remembered", entry });
15471
+ }
15472
+ };
15473
+ }
15474
+ var ForgetSchema = Type2.Object({
15475
+ key: Type2.String({
15476
+ description: "Exact key of the note to remove. No-op (status=missing) when no note exists for that key.",
15477
+ minLength: 1,
15478
+ maxLength: AGENT_MEMORY_KEY_MAX_LENGTH
15479
+ })
15480
+ });
15481
+ function buildForgetTool(ctx) {
15482
+ return {
15483
+ name: "forget",
15484
+ label: "Forget",
15485
+ description: "Delete a durable note by key. Use when a previously-remembered fact is wrong or no longer relevant.",
15486
+ parameters: ForgetSchema,
15487
+ execute: async (_toolCallId, params) => {
15488
+ if (params.key.startsWith(COMPACTION_KEY_PREFIX)) {
15489
+ throw new Error(
15490
+ `cannot forget compaction notes directly \u2014 they are pruned automatically (key prefix "${COMPACTION_KEY_PREFIX}" is reserved)`
15491
+ );
15492
+ }
15493
+ const removed = deleteMemoryEntry(ctx.db, ctx.projectId, params.key);
15494
+ return textResult2({ status: removed ? "forgotten" : "missing", key: params.key });
15495
+ }
15496
+ };
15497
+ }
15310
15498
  function buildWriteTools(ctx) {
15311
15499
  return [
15312
15500
  buildRunSweepTool(ctx),
@@ -15314,7 +15502,9 @@ function buildWriteTools(ctx) {
15314
15502
  buildAddKeywordsTool(ctx),
15315
15503
  buildAddCompetitorsTool(ctx),
15316
15504
  buildUpdateScheduleTool(ctx),
15317
- buildAttachAgentWebhookTool(ctx)
15505
+ buildAttachAgentWebhookTool(ctx),
15506
+ buildRememberTool(ctx),
15507
+ buildForgetTool(ctx)
15318
15508
  ];
15319
15509
  }
15320
15510
  function buildAllTools(ctx) {
@@ -15366,7 +15556,13 @@ function createAeroSession(opts) {
15366
15556
  if (!provider) throw new Error(missingProviderMessage());
15367
15557
  const model = resolveAeroModel(provider, opts.modelId);
15368
15558
  const toolScope = opts.toolScope ?? "all";
15369
- const stateTools = toolScope === "read-only" ? buildReadTools({ client: opts.client, projectName: opts.projectName }) : buildAllTools({ client: opts.client, projectName: opts.projectName });
15559
+ const toolCtx = {
15560
+ client: opts.client,
15561
+ projectName: opts.projectName,
15562
+ db: opts.db,
15563
+ projectId: opts.projectId
15564
+ };
15565
+ const stateTools = toolScope === "read-only" ? buildReadTools(toolCtx) : buildAllTools(toolCtx);
15370
15566
  const defaultTools = [...stateTools, ...buildSkillDocTools()];
15371
15567
  const tools = opts.tools ?? defaultTools;
15372
15568
  return new Agent({
@@ -15387,13 +15583,151 @@ function resolveSessionProviderAndModel(config, opts) {
15387
15583
  return { provider, modelId };
15388
15584
  }
15389
15585
 
15586
+ // src/agent/compaction.ts
15587
+ import { complete } from "@mariozechner/pi-ai";
15588
+
15589
+ // src/agent/compaction-config.ts
15590
+ var COMPACTION_TOKEN_THRESHOLD = 6e4;
15591
+ var COMPACTION_TARGET_RATIO = 0.5;
15592
+ var COMPACTION_PRESERVE_TAIL_MESSAGES = 10;
15593
+ var COMPACTION_MAX_MESSAGES = 400;
15594
+
15595
+ // src/agent/token-counter.ts
15596
+ var CHARS_PER_TOKEN = 4;
15597
+ function estimateMessageTokens(message) {
15598
+ const content = message.content;
15599
+ if (content === void 0) return 0;
15600
+ if (typeof content === "string") {
15601
+ return Math.ceil(content.length / CHARS_PER_TOKEN);
15602
+ }
15603
+ if (!Array.isArray(content)) return 0;
15604
+ let chars = 0;
15605
+ for (const part of content) {
15606
+ if (part && typeof part === "object" && "type" in part) {
15607
+ const p = part;
15608
+ switch (p.type) {
15609
+ case "text":
15610
+ chars += (p.text ?? "").length;
15611
+ break;
15612
+ case "thinking":
15613
+ chars += (p.thinking ?? "").length;
15614
+ break;
15615
+ case "toolCall":
15616
+ try {
15617
+ chars += JSON.stringify(p.arguments ?? {}).length;
15618
+ } catch {
15619
+ chars += 64;
15620
+ }
15621
+ break;
15622
+ case "image":
15623
+ chars += 1024;
15624
+ break;
15625
+ default:
15626
+ break;
15627
+ }
15628
+ }
15629
+ }
15630
+ return Math.ceil(chars / CHARS_PER_TOKEN);
15631
+ }
15632
+ function estimateTranscriptTokens(messages) {
15633
+ let total = 0;
15634
+ for (const m of messages) total += estimateMessageTokens(m);
15635
+ return total;
15636
+ }
15637
+
15638
+ // src/agent/compaction.ts
15639
+ function shouldCompact(messages) {
15640
+ if (messages.length >= COMPACTION_MAX_MESSAGES) return true;
15641
+ return estimateTranscriptTokens(messages) >= COMPACTION_TOKEN_THRESHOLD;
15642
+ }
15643
+ function findSafeSplit(messages, targetIndex) {
15644
+ const maxSplit = messages.length - COMPACTION_PRESERVE_TAIL_MESSAGES;
15645
+ if (maxSplit <= 0) return 0;
15646
+ const boundedTarget = Math.max(0, Math.min(targetIndex, maxSplit));
15647
+ for (let i = boundedTarget; i <= maxSplit; i++) {
15648
+ const m = messages[i];
15649
+ if (m && m.role === "user") return i;
15650
+ }
15651
+ return 0;
15652
+ }
15653
+ function toLlmMessages(messages) {
15654
+ const out = [];
15655
+ for (const m of messages) {
15656
+ if (m.role === "user" || m.role === "assistant" || m.role === "toolResult") {
15657
+ out.push(m);
15658
+ }
15659
+ }
15660
+ return out;
15661
+ }
15662
+ var SUMMARY_SYSTEM_PROMPT = `You compress an AI agent conversation transcript into a short durable note.
15663
+
15664
+ Extract only:
15665
+ - User intents and requests
15666
+ - Actions the agent took and their outcomes
15667
+ - Key findings, insights, decisions
15668
+ - Outstanding TODOs or deferred follow-ups
15669
+
15670
+ Style: dense bullet points. No preamble, no closing remarks, no agent self-commentary. Keep the note under 1500 characters.`;
15671
+ function truncateToByteLimit(text, maxBytes) {
15672
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
15673
+ const suffix = "\u2026[truncated]";
15674
+ const budget = maxBytes - Buffer.byteLength(suffix, "utf8");
15675
+ let buf = Buffer.from(text, "utf8").subarray(0, budget);
15676
+ while (buf.length > 0 && (buf[buf.length - 1] & 192) === 128) {
15677
+ buf = buf.subarray(0, buf.length - 1);
15678
+ }
15679
+ return buf.toString("utf8") + suffix;
15680
+ }
15681
+ async function runSummaryLlm(args) {
15682
+ const context = {
15683
+ systemPrompt: SUMMARY_SYSTEM_PROMPT,
15684
+ messages: toLlmMessages(args.chunk)
15685
+ };
15686
+ const apiKey = args.getApiKey?.(args.model.provider);
15687
+ const resp = await complete(args.model, context, apiKey ? { apiKey } : {});
15688
+ const parts = resp.content.filter((p) => p.type === "text");
15689
+ const text = parts.map((p) => p.text).join("\n").trim();
15690
+ if (!text) throw new Error("summary LLM returned no text content");
15691
+ return text;
15692
+ }
15693
+ async function compactMessages(args) {
15694
+ const target = Math.floor(args.messages.length * COMPACTION_TARGET_RATIO);
15695
+ const split = findSafeSplit(args.messages, target);
15696
+ if (split === 0) return null;
15697
+ const chunk = args.messages.slice(0, split);
15698
+ const suffix = args.messages.slice(split);
15699
+ const summarize = args.summarize ?? runSummaryLlm;
15700
+ const rawSummary = await summarize({ model: args.model, chunk, getApiKey: args.getApiKey });
15701
+ const summary = truncateToByteLimit(rawSummary, AGENT_MEMORY_VALUE_MAX_BYTES);
15702
+ writeCompactionNote(args.db, {
15703
+ projectId: args.projectId,
15704
+ sessionId: args.sessionId,
15705
+ summary,
15706
+ removedCount: chunk.length
15707
+ });
15708
+ return { messages: suffix, removedCount: chunk.length, summary };
15709
+ }
15710
+
15390
15711
  // src/agent/session-registry.ts
15391
15712
  var log7 = createLogger("SessionRegistry");
15713
+ var MAX_HYDRATE_NOTES = 20;
15714
+ var MAX_HYDRATE_BYTES = 32 * 1024;
15715
+ function escapeMemoryFragment(value) {
15716
+ return value.replace(/<(\/?)memory>/gi, "<$1\u200Cmemory>");
15717
+ }
15392
15718
  var SessionRegistry = class {
15393
15719
  live = /* @__PURE__ */ new Map();
15394
15720
  pending = /* @__PURE__ */ new Map();
15395
15721
  /** Last tool scope used on the live Agent for a project. Read in getOrCreate to know when to swap. */
15396
15722
  scopes = /* @__PURE__ */ new Map();
15723
+ /** Cached resolved project id per project name, used so alignScope can rebuild tool context without a DB roundtrip. */
15724
+ projectIds = /* @__PURE__ */ new Map();
15725
+ /**
15726
+ * In-flight compaction promises keyed by project name. A second
15727
+ * `acquireForTurn` that arrives while the first is still summarizing
15728
+ * awaits the same promise instead of kicking off a duplicate LLM call.
15729
+ */
15730
+ compactions = /* @__PURE__ */ new Map();
15397
15731
  opts;
15398
15732
  constructor(opts) {
15399
15733
  this.opts = opts;
@@ -15424,19 +15758,22 @@ var SessionRegistry = class {
15424
15758
  modelProvider: effectiveProvider,
15425
15759
  modelId: effectiveModelId,
15426
15760
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15427
- }).where(eq23(agentSessions.projectId, projectId)).run();
15761
+ }).where(eq24(agentSessions.projectId, projectId)).run();
15428
15762
  }
15429
15763
  const agent2 = createAeroSession({
15430
15764
  projectName,
15431
15765
  client: this.opts.client,
15432
15766
  config: this.opts.config,
15767
+ db: this.opts.db,
15768
+ projectId,
15433
15769
  provider: effectiveProvider,
15434
15770
  modelId: effectiveModelId,
15435
- systemPromptOverride: row.systemPrompt,
15771
+ systemPromptOverride: this.buildHydratedSystemPrompt(projectId, row.systemPrompt),
15436
15772
  initialMessages: persistedMessages,
15437
15773
  toolScope: preferences?.toolScope
15438
15774
  });
15439
15775
  this.scopes.set(projectName, preferences?.toolScope ?? "all");
15776
+ this.projectIds.set(projectName, projectId);
15440
15777
  if (queued.length > 0) {
15441
15778
  this.appendPending(projectName, queued);
15442
15779
  this.updateRow(projectId, { followUpQueue: "[]" });
@@ -15451,14 +15788,21 @@ var SessionRegistry = class {
15451
15788
  projectName,
15452
15789
  client: this.opts.client,
15453
15790
  config: this.opts.config,
15791
+ db: this.opts.db,
15792
+ projectId,
15454
15793
  provider,
15455
15794
  modelId,
15456
- systemPromptOverride: systemPrompt,
15795
+ // Hydrate on the fresh path too — a brand-new session may still see
15796
+ // notes if they were seeded via CLI/API before the first prompt.
15797
+ systemPromptOverride: this.buildHydratedSystemPrompt(projectId, systemPrompt),
15457
15798
  toolScope: preferences?.toolScope
15458
15799
  });
15459
15800
  this.scopes.set(projectName, preferences?.toolScope ?? "all");
15801
+ this.projectIds.set(projectName, projectId);
15460
15802
  this.insertRow({
15461
15803
  projectId,
15804
+ // Persist the raw (unhydrated) prompt so the DB remains canonical —
15805
+ // the `<memory>` block is rebuilt from the notes table on every load.
15462
15806
  systemPrompt,
15463
15807
  modelProvider: provider,
15464
15808
  modelId,
@@ -15469,6 +15813,63 @@ var SessionRegistry = class {
15469
15813
  this.registerDrainHook(agent, projectName);
15470
15814
  return agent;
15471
15815
  }
15816
+ /**
15817
+ * Append the `<memory>` block to a base system prompt, sourced from the
15818
+ * `agent_memory` table. Returns the base prompt unchanged when no notes
15819
+ * exist — an empty block would just be prompt noise. Truncates to
15820
+ * `MAX_HYDRATE_BYTES`, dropping oldest-first, so the block is bounded
15821
+ * even when notes sit near their 2 KB cap.
15822
+ *
15823
+ * Note values come from LLM-authored compaction summaries and operator
15824
+ * input, so they are treated as untrusted data: closing tags that could
15825
+ * escape the `<memory>` wrapper are neutralized before interpolation.
15826
+ */
15827
+ buildHydratedSystemPrompt(projectId, basePrompt) {
15828
+ const entries = loadRecentForHydrate(this.opts.db, projectId, MAX_HYDRATE_NOTES);
15829
+ if (entries.length === 0) return basePrompt;
15830
+ let totalBytes = 0;
15831
+ const kept = [];
15832
+ for (const entry of entries) {
15833
+ const escaped = {
15834
+ source: escapeMemoryFragment(entry.source),
15835
+ key: escapeMemoryFragment(entry.key),
15836
+ value: escapeMemoryFragment(entry.value)
15837
+ };
15838
+ const line = `- [${escaped.source}] ${escaped.key}: ${escaped.value}
15839
+ `;
15840
+ const bytes = Buffer.byteLength(line, "utf8");
15841
+ if (totalBytes + bytes > MAX_HYDRATE_BYTES) break;
15842
+ kept.push(escaped);
15843
+ totalBytes += bytes;
15844
+ }
15845
+ if (kept.length === 0) return basePrompt;
15846
+ const lines = kept.map((e) => `- [${e.source}] ${e.key}: ${e.value}`);
15847
+ return `${basePrompt.trimEnd()}
15848
+
15849
+ ---
15850
+
15851
+ <memory>
15852
+ Project-scoped durable notes (newest first). Use remember/forget/recall to manage. Entries tagged [compaction] are LLM-summarized transcript slices.
15853
+
15854
+ ${lines.join("\n")}
15855
+ </memory>`;
15856
+ }
15857
+ /**
15858
+ * Rebuild the live agent's system prompt from the latest `agent_memory`
15859
+ * rows. Called after out-of-band memory writes (CLI/API PUT/DELETE) so
15860
+ * the next turn on a hot session sees the updated notes without waiting
15861
+ * for compaction or a cold restart. No-op when no live agent exists —
15862
+ * the next `getOrCreate` will hydrate from DB anyway.
15863
+ */
15864
+ rehydrateLiveMemory(projectName) {
15865
+ const agent = this.live.get(projectName);
15866
+ if (!agent) return;
15867
+ const projectId = this.tryResolveProjectId(projectName);
15868
+ if (!projectId) return;
15869
+ const row = this.loadRow(projectId);
15870
+ if (!row) return;
15871
+ agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
15872
+ }
15472
15873
  /**
15473
15874
  * Acquire the Agent for an upcoming prompt/turn.
15474
15875
  *
@@ -15484,7 +15885,7 @@ var SessionRegistry = class {
15484
15885
  * Persists the new model choice to the DB row so subsequent invocations
15485
15886
  * stay on it unless overridden again.
15486
15887
  */
15487
- acquireForTurn(projectName, preferences) {
15888
+ async acquireForTurn(projectName, preferences) {
15488
15889
  const agent = this.getOrCreate(projectName);
15489
15890
  if (agent.state.isStreaming) {
15490
15891
  throw agentBusy(projectName);
@@ -15493,11 +15894,70 @@ var SessionRegistry = class {
15493
15894
  if (preferences?.provider || preferences?.modelId) {
15494
15895
  this.alignModel(projectName, agent, preferences);
15495
15896
  }
15897
+ await this.maybeCompact(projectName, agent);
15496
15898
  return agent;
15497
15899
  }
15900
+ /**
15901
+ * Summarize the oldest half of the transcript into a `compaction:`
15902
+ * memory row when the transcript crosses the token/message threshold.
15903
+ * Runs before the caller's next `agent.prompt()` so the model sees the
15904
+ * trimmed transcript + a refreshed `<memory>` block that now includes
15905
+ * the new summary.
15906
+ *
15907
+ * Races are deduped through `this.compactions`: a concurrent call for
15908
+ * the same project awaits the in-flight promise instead of launching a
15909
+ * duplicate summarizer run. Failures are logged and swallowed — a flaky
15910
+ * summarizer must never block a user turn.
15911
+ */
15912
+ async maybeCompact(projectName, agent) {
15913
+ const inflight = this.compactions.get(projectName);
15914
+ if (inflight) {
15915
+ await inflight;
15916
+ return;
15917
+ }
15918
+ if (!shouldCompact(agent.state.messages)) return;
15919
+ const promise = this.runCompaction(projectName, agent).finally(() => {
15920
+ this.compactions.delete(projectName);
15921
+ });
15922
+ this.compactions.set(projectName, promise);
15923
+ await promise;
15924
+ }
15925
+ async runCompaction(projectName, agent) {
15926
+ const projectId = this.projectIds.get(projectName) ?? this.resolveProjectId(projectName);
15927
+ this.projectIds.set(projectName, projectId);
15928
+ const row = this.loadRow(projectId);
15929
+ if (!row) return;
15930
+ try {
15931
+ const result = await compactMessages({
15932
+ db: this.opts.db,
15933
+ projectId,
15934
+ sessionId: row.id,
15935
+ messages: agent.state.messages,
15936
+ model: agent.state.model,
15937
+ getApiKey: buildApiKeyResolver(this.opts.config)
15938
+ });
15939
+ if (!result) return;
15940
+ agent.state.messages = result.messages;
15941
+ agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
15942
+ this.save(projectName);
15943
+ log7.info("compaction.completed", {
15944
+ projectName,
15945
+ removedCount: result.removedCount,
15946
+ summaryBytes: Buffer.byteLength(result.summary, "utf8")
15947
+ });
15948
+ } catch (err) {
15949
+ log7.error("compaction.failed", {
15950
+ projectName,
15951
+ error: err instanceof Error ? err.message : String(err)
15952
+ });
15953
+ }
15954
+ }
15498
15955
  alignScope(projectName, agent, wantScope) {
15499
15956
  if (this.scopes.get(projectName) === wantScope) return;
15500
- const stateTools = wantScope === "read-only" ? buildReadTools({ client: this.opts.client, projectName }) : buildAllTools({ client: this.opts.client, projectName });
15957
+ const projectId = this.projectIds.get(projectName) ?? this.resolveProjectId(projectName);
15958
+ this.projectIds.set(projectName, projectId);
15959
+ const toolCtx = { client: this.opts.client, projectName, db: this.opts.db, projectId };
15960
+ const stateTools = wantScope === "read-only" ? buildReadTools(toolCtx) : buildAllTools(toolCtx);
15501
15961
  agent.state.tools = [...stateTools, ...buildSkillDocTools()];
15502
15962
  this.scopes.set(projectName, wantScope);
15503
15963
  }
@@ -15516,7 +15976,7 @@ var SessionRegistry = class {
15516
15976
  modelProvider: nextProvider,
15517
15977
  modelId: nextModelId,
15518
15978
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15519
- }).where(eq23(agentSessions.projectId, projectId)).run();
15979
+ }).where(eq24(agentSessions.projectId, projectId)).run();
15520
15980
  }
15521
15981
  /** Persist a session's transcript back to the DB. Call after any run settles. */
15522
15982
  save(projectName) {
@@ -15573,7 +16033,7 @@ var SessionRegistry = class {
15573
16033
  let agent;
15574
16034
  try {
15575
16035
  const scope = this.scopes.get(projectName) ?? "read-only";
15576
- agent = this.acquireForTurn(projectName, { toolScope: scope });
16036
+ agent = await this.acquireForTurn(projectName, { toolScope: scope });
15577
16037
  } catch (err) {
15578
16038
  if (err.code === "AGENT_BUSY") return;
15579
16039
  throw err;
@@ -15608,6 +16068,7 @@ var SessionRegistry = class {
15608
16068
  this.live.delete(projectName);
15609
16069
  this.pending.delete(projectName);
15610
16070
  this.scopes.delete(projectName);
16071
+ this.projectIds.delete(projectName);
15611
16072
  }
15612
16073
  /** Evict every live Agent. Durable state in DB is untouched. */
15613
16074
  clear() {
@@ -15677,17 +16138,17 @@ var SessionRegistry = class {
15677
16138
  return id;
15678
16139
  }
15679
16140
  tryResolveProjectId(projectName) {
15680
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq23(projects.name, projectName)).get();
16141
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq24(projects.name, projectName)).get();
15681
16142
  return row?.id;
15682
16143
  }
15683
16144
  loadRow(projectId) {
15684
- const row = this.opts.db.select().from(agentSessions).where(eq23(agentSessions.projectId, projectId)).get();
16145
+ const row = this.opts.db.select().from(agentSessions).where(eq24(agentSessions.projectId, projectId)).get();
15685
16146
  return row ?? null;
15686
16147
  }
15687
16148
  insertRow(params) {
15688
16149
  const now = (/* @__PURE__ */ new Date()).toISOString();
15689
16150
  this.opts.db.insert(agentSessions).values({
15690
- id: crypto22.randomUUID(),
16151
+ id: crypto23.randomUUID(),
15691
16152
  projectId: params.projectId,
15692
16153
  systemPrompt: params.systemPrompt,
15693
16154
  modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
@@ -15700,14 +16161,14 @@ var SessionRegistry = class {
15700
16161
  }
15701
16162
  updateRow(projectId, patch) {
15702
16163
  const now = (/* @__PURE__ */ new Date()).toISOString();
15703
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq23(agentSessions.projectId, projectId)).run();
16164
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq24(agentSessions.projectId, projectId)).run();
15704
16165
  }
15705
16166
  };
15706
16167
 
15707
16168
  // src/agent/agent-routes.ts
15708
- import { eq as eq24 } from "drizzle-orm";
16169
+ import { eq as eq25 } from "drizzle-orm";
15709
16170
  function resolveProject2(db, name) {
15710
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq24(projects.name, name)).get();
16171
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq25(projects.name, name)).get();
15711
16172
  if (!row) throw notFound("project", name);
15712
16173
  return row;
15713
16174
  }
@@ -15716,7 +16177,7 @@ function registerAgentRoutes(app, opts) {
15716
16177
  "/projects/:name/agent/transcript",
15717
16178
  async (request) => {
15718
16179
  const project = resolveProject2(opts.db, request.params.name);
15719
- const row = opts.db.select().from(agentSessions).where(eq24(agentSessions.projectId, project.id)).get();
16180
+ const row = opts.db.select().from(agentSessions).where(eq25(agentSessions.projectId, project.id)).get();
15720
16181
  if (!row) {
15721
16182
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
15722
16183
  }
@@ -15740,7 +16201,7 @@ function registerAgentRoutes(app, opts) {
15740
16201
  async (request) => {
15741
16202
  const project = resolveProject2(opts.db, request.params.name);
15742
16203
  opts.sessionRegistry.reset(project.name);
15743
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(agentSessions.projectId, project.id)).run();
16204
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(agentSessions.projectId, project.id)).run();
15744
16205
  return { status: "reset" };
15745
16206
  }
15746
16207
  );
@@ -15749,7 +16210,7 @@ function registerAgentRoutes(app, opts) {
15749
16210
  const promptText = (request.body?.prompt ?? "").trim();
15750
16211
  if (!promptText) throw validationError('"prompt" is required');
15751
16212
  const requestedScope = request.body?.scope === "all" ? "all" : "read-only";
15752
- const agent = opts.sessionRegistry.acquireForTurn(project.name, {
16213
+ const agent = await opts.sessionRegistry.acquireForTurn(project.name, {
15753
16214
  provider: request.body?.provider,
15754
16215
  modelId: request.body?.modelId,
15755
16216
  toolScope: requestedScope
@@ -15800,6 +16261,57 @@ function registerAgentRoutes(app, opts) {
15800
16261
  }
15801
16262
  return reply;
15802
16263
  });
16264
+ app.get(
16265
+ "/projects/:name/agent/memory",
16266
+ async (request) => {
16267
+ const project = resolveProject2(opts.db, request.params.name);
16268
+ return { entries: listMemoryEntries(opts.db, project.id) };
16269
+ }
16270
+ );
16271
+ app.put(
16272
+ "/projects/:name/agent/memory",
16273
+ async (request) => {
16274
+ const project = resolveProject2(opts.db, request.params.name);
16275
+ const parsed = agentMemoryUpsertRequestSchema.safeParse(request.body);
16276
+ if (!parsed.success) {
16277
+ throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
16278
+ }
16279
+ if (parsed.data.key.startsWith(COMPACTION_KEY_PREFIX)) {
16280
+ throw validationError(
16281
+ `key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`
16282
+ );
16283
+ }
16284
+ if (Buffer.byteLength(parsed.data.value, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
16285
+ throw validationError(`"value" exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes`);
16286
+ }
16287
+ const entry = upsertMemoryEntry(opts.db, {
16288
+ projectId: project.id,
16289
+ key: parsed.data.key,
16290
+ value: parsed.data.value,
16291
+ source: MemorySources.user
16292
+ });
16293
+ opts.sessionRegistry.rehydrateLiveMemory(project.name);
16294
+ return { status: "ok", entry };
16295
+ }
16296
+ );
16297
+ app.delete(
16298
+ "/projects/:name/agent/memory",
16299
+ async (request) => {
16300
+ const project = resolveProject2(opts.db, request.params.name);
16301
+ const parsed = agentMemoryDeleteRequestSchema.safeParse(request.body);
16302
+ if (!parsed.success) {
16303
+ throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
16304
+ }
16305
+ if (parsed.data.key.startsWith(COMPACTION_KEY_PREFIX)) {
16306
+ throw validationError(
16307
+ `key prefix "${COMPACTION_KEY_PREFIX}" is reserved; compaction notes are pruned automatically`
16308
+ );
16309
+ }
16310
+ const removed = deleteMemoryEntry(opts.db, project.id, parsed.data.key);
16311
+ if (removed) opts.sessionRegistry.rehydrateLiveMemory(project.name);
16312
+ return { status: removed ? "forgotten" : "missing", key: parsed.data.key };
16313
+ }
16314
+ );
15803
16315
  }
15804
16316
 
15805
16317
  // src/client.ts
@@ -15915,6 +16427,26 @@ var ApiClient = class {
15915
16427
  `/projects/${encodeURIComponent(project)}/agent/providers`
15916
16428
  );
15917
16429
  }
16430
+ async listAgentMemory(project) {
16431
+ return this.request(
16432
+ "GET",
16433
+ `/projects/${encodeURIComponent(project)}/agent/memory`
16434
+ );
16435
+ }
16436
+ async setAgentMemory(project, body) {
16437
+ return this.request(
16438
+ "PUT",
16439
+ `/projects/${encodeURIComponent(project)}/agent/memory`,
16440
+ body
16441
+ );
16442
+ }
16443
+ async forgetAgentMemory(project, key) {
16444
+ return this.request(
16445
+ "DELETE",
16446
+ `/projects/${encodeURIComponent(project)}/agent/memory`,
16447
+ { key }
16448
+ );
16449
+ }
15918
16450
  /**
15919
16451
  * POST a request whose response body the caller intends to consume as a
15920
16452
  * stream (e.g. the Aero agent SSE endpoint). Shares the probe + auth +
@@ -17074,7 +17606,7 @@ function summarizeProviderConfig(provider, config) {
17074
17606
  };
17075
17607
  }
17076
17608
  function hashApiKey(key) {
17077
- return crypto23.createHash("sha256").update(key).digest("hex");
17609
+ return crypto24.createHash("sha256").update(key).digest("hex");
17078
17610
  }
17079
17611
  function parseCookies2(header) {
17080
17612
  if (!header) return {};
@@ -17232,7 +17764,7 @@ async function createServer(opts) {
17232
17764
  intelligenceService,
17233
17765
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
17234
17766
  async ({ runId, projectId, insightCount, criticalOrHigh }) => {
17235
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq25(projects.id, projectId)).get();
17767
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq26(projects.id, projectId)).get();
17236
17768
  if (!project) return;
17237
17769
  sessionRegistry.queueFollowUp(project.name, {
17238
17770
  role: "user",
@@ -17326,7 +17858,7 @@ async function createServer(opts) {
17326
17858
  return removed;
17327
17859
  }
17328
17860
  };
17329
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto23.randomBytes(32).toString("hex");
17861
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto24.randomBytes(32).toString("hex");
17330
17862
  const googleConnectionStore = {
17331
17863
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
17332
17864
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
@@ -17372,11 +17904,11 @@ async function createServer(opts) {
17372
17904
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
17373
17905
  if (opts.config.apiKey) {
17374
17906
  const keyHash = hashApiKey(opts.config.apiKey);
17375
- const existing = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, keyHash)).get();
17907
+ const existing = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, keyHash)).get();
17376
17908
  if (!existing) {
17377
17909
  const prefix = opts.config.apiKey.slice(0, 12);
17378
17910
  opts.db.insert(apiKeys).values({
17379
- id: `key_${crypto23.randomBytes(8).toString("hex")}`,
17911
+ id: `key_${crypto24.randomBytes(8).toString("hex")}`,
17380
17912
  name: "default",
17381
17913
  keyHash,
17382
17914
  keyPrefix: prefix,
@@ -17400,7 +17932,7 @@ async function createServer(opts) {
17400
17932
  };
17401
17933
  const createSession = (apiKeyId) => {
17402
17934
  pruneExpiredSessions();
17403
- const sessionId = crypto23.randomBytes(32).toString("hex");
17935
+ const sessionId = crypto24.randomBytes(32).toString("hex");
17404
17936
  sessions.set(sessionId, {
17405
17937
  apiKeyId,
17406
17938
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -17424,7 +17956,7 @@ async function createServer(opts) {
17424
17956
  };
17425
17957
  const getDefaultApiKey = () => {
17426
17958
  if (!opts.config.apiKey) return void 0;
17427
- return opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
17959
+ return opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
17428
17960
  };
17429
17961
  const createPasswordSession = (reply) => {
17430
17962
  const key = getDefaultApiKey();
@@ -17481,12 +18013,12 @@ async function createServer(opts) {
17481
18013
  return reply.send({ authenticated: true });
17482
18014
  }
17483
18015
  if (apiKey) {
17484
- const key = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(apiKey))).get();
18016
+ const key = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey(apiKey))).get();
17485
18017
  if (!key || key.revokedAt) {
17486
18018
  const err2 = authInvalid();
17487
18019
  return reply.status(err2.statusCode).send(err2.toJSON());
17488
18020
  }
17489
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(apiKeys.id, key.id)).run();
18021
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(apiKeys.id, key.id)).run();
17490
18022
  const sessionId = createSession(key.id);
17491
18023
  reply.header("set-cookie", serializeSessionCookie({
17492
18024
  name: SESSION_COOKIE_NAME,
@@ -17633,7 +18165,7 @@ async function createServer(opts) {
17633
18165
  const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
17634
18166
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
17635
18167
  opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
17636
- id: crypto23.randomUUID(),
18168
+ id: crypto24.randomUUID(),
17637
18169
  projectId,
17638
18170
  actor: "api",
17639
18171
  action: existing ? "provider.updated" : "provider.created",