@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.
- package/assets/agent-workspace/skills/aero/references/memory-patterns.md +37 -18
- package/assets/assets/{index-DnlDoqE-.js → index-J73csS93.js} +75 -75
- package/assets/index.html +1 -1
- package/dist/{chunk-YZKLIUH4.js → chunk-2QNWFP6R.js} +567 -35
- package/dist/{chunk-GH6WGN5B.js → chunk-TAII35VC.js} +29 -1
- package/dist/cli.js +130 -3
- package/dist/index.js +2 -2
- package/dist/{intelligence-service-LHWXONQJ.js → intelligence-service-C5LAYDFM.js} +1 -1
- package/package.json +7 -7
|
@@ -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-
|
|
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
|
|
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
|
|
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
|
|
14791
|
-
import { eq as
|
|
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-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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 ??
|
|
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(
|
|
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_${
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
18168
|
+
id: crypto24.randomUUID(),
|
|
17637
18169
|
projectId,
|
|
17638
18170
|
actor: "api",
|
|
17639
18171
|
action: existing ? "provider.updated" : "provider.created",
|