@ainyc/canonry 2.0.0 → 2.2.3

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";
@@ -291,6 +292,11 @@ function usageError(displayMessage, options) {
291
292
  details: options?.details
292
293
  });
293
294
  }
295
+ function isEndpointMissing(err) {
296
+ if (!(err instanceof CliError)) return false;
297
+ const status = err.details?.httpStatus;
298
+ return status === 404 || status === 405;
299
+ }
294
300
  function printCliError(err, format) {
295
301
  if (format === "json") {
296
302
  if (err instanceof CliError) {
@@ -337,11 +343,11 @@ function printCliError(err, format) {
337
343
 
338
344
  // src/server.ts
339
345
  import { createRequire as createRequire2 } from "module";
340
- import crypto23 from "crypto";
346
+ import crypto24 from "crypto";
341
347
  import fs8 from "fs";
342
348
  import path9 from "path";
343
349
  import { fileURLToPath as fileURLToPath2 } from "url";
344
- import { eq as eq25 } from "drizzle-orm";
350
+ import { eq as eq26 } from "drizzle-orm";
345
351
  import Fastify from "fastify";
346
352
 
347
353
  // ../contracts/src/config-schema.ts
@@ -986,6 +992,13 @@ var querySnapshotDtoSchema = z8.object({
986
992
  location: z8.string().nullable().optional(),
987
993
  createdAt: z8.string()
988
994
  });
995
+ var runDetailDtoSchema = runDtoSchema.extend({
996
+ snapshots: z8.array(querySnapshotDtoSchema).optional()
997
+ });
998
+ var latestProjectRunDtoSchema = z8.object({
999
+ totalRuns: z8.number().int().nonnegative(),
1000
+ run: runDetailDtoSchema.nullable()
1001
+ });
989
1002
  var auditLogEntrySchema = z8.object({
990
1003
  id: z8.string(),
991
1004
  projectId: z8.string().nullable().optional(),
@@ -1404,6 +1417,20 @@ function escapeRegExp(value) {
1404
1417
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1405
1418
  }
1406
1419
 
1420
+ // ../contracts/src/agent.ts
1421
+ import { z as z13 } from "zod";
1422
+ var memorySourceSchema = z13.enum(["aero", "user", "compaction"]);
1423
+ var MemorySources = memorySourceSchema.enum;
1424
+ var AGENT_MEMORY_VALUE_MAX_BYTES = 2 * 1024;
1425
+ var AGENT_MEMORY_KEY_MAX_LENGTH = 128;
1426
+ var agentMemoryUpsertRequestSchema = z13.object({
1427
+ key: z13.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH),
1428
+ value: z13.string().min(1)
1429
+ });
1430
+ var agentMemoryDeleteRequestSchema = z13.object({
1431
+ key: z13.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH)
1432
+ });
1433
+
1407
1434
  // ../api-routes/src/auth.ts
1408
1435
  import crypto2 from "crypto";
1409
1436
  import { eq } from "drizzle-orm";
@@ -1981,7 +2008,7 @@ async function competitorRoutes(app) {
1981
2008
 
1982
2009
  // ../api-routes/src/runs.ts
1983
2010
  import crypto8 from "crypto";
1984
- import { eq as eq7, asc, desc } from "drizzle-orm";
2011
+ import { eq as eq7, asc, desc, sql as sql2 } from "drizzle-orm";
1985
2012
 
1986
2013
  // ../api-routes/src/run-queue.ts
1987
2014
  import crypto7 from "crypto";
@@ -2121,6 +2148,19 @@ async function runRoutes(app, opts) {
2121
2148
  const rows = limit == null ? app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(asc(runs.createdAt)).all() : app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt)).limit(limit).all().reverse();
2122
2149
  return reply.send(rows.map(formatRun));
2123
2150
  });
2151
+ app.get("/projects/:name/runs/latest", async (request, reply) => {
2152
+ const project = resolveProject(app.db, request.params.name);
2153
+ const countRow = app.db.select({ count: sql2`count(*)` }).from(runs).where(eq7(runs.projectId, project.id)).get();
2154
+ const totalRuns = countRow?.count ?? 0;
2155
+ const latestRun = app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt)).limit(1).get();
2156
+ if (!latestRun) {
2157
+ return reply.send({ totalRuns: 0, run: null });
2158
+ }
2159
+ return reply.send({
2160
+ totalRuns,
2161
+ run: loadRunDetail(app, latestRun)
2162
+ });
2163
+ });
2124
2164
  app.get("/runs", async (_request, reply) => {
2125
2165
  const rows = app.db.select().from(runs).all();
2126
2166
  return reply.send(rows.map(formatRun));
@@ -2209,55 +2249,7 @@ async function runRoutes(app, opts) {
2209
2249
  app.get("/runs/:id", async (request, reply) => {
2210
2250
  const run = app.db.select().from(runs).where(eq7(runs.id, request.params.id)).get();
2211
2251
  if (!run) throw notFound("Run", request.params.id);
2212
- const project = app.db.select({
2213
- displayName: projects.displayName,
2214
- canonicalDomain: projects.canonicalDomain,
2215
- ownedDomains: projects.ownedDomains
2216
- }).from(projects).where(eq7(projects.id, run.projectId)).get();
2217
- const snapshots = app.db.select({
2218
- id: querySnapshots.id,
2219
- runId: querySnapshots.runId,
2220
- keywordId: querySnapshots.keywordId,
2221
- keyword: keywords.keyword,
2222
- provider: querySnapshots.provider,
2223
- model: querySnapshots.model,
2224
- citationState: querySnapshots.citationState,
2225
- answerMentioned: querySnapshots.answerMentioned,
2226
- answerText: querySnapshots.answerText,
2227
- citedDomains: querySnapshots.citedDomains,
2228
- competitorOverlap: querySnapshots.competitorOverlap,
2229
- recommendedCompetitors: querySnapshots.recommendedCompetitors,
2230
- location: querySnapshots.location,
2231
- rawResponse: querySnapshots.rawResponse,
2232
- createdAt: querySnapshots.createdAt
2233
- }).from(querySnapshots).leftJoin(keywords, eq7(querySnapshots.keywordId, keywords.id)).where(eq7(querySnapshots.runId, run.id)).all();
2234
- return reply.send({
2235
- ...formatRun(run),
2236
- snapshots: snapshots.map((s) => {
2237
- const rawParsed = parseSnapshotRawResponse(s.rawResponse);
2238
- const answerMentioned = project ? resolveSnapshotAnswerMentioned(s, project) : s.answerMentioned ?? false;
2239
- return {
2240
- id: s.id,
2241
- runId: s.runId,
2242
- keywordId: s.keywordId,
2243
- keyword: s.keyword,
2244
- provider: s.provider,
2245
- citationState: s.citationState,
2246
- answerMentioned,
2247
- visibilityState: project ? resolveSnapshotVisibilityState(s, project) : answerMentioned ? "visible" : "not-visible",
2248
- answerText: s.answerText,
2249
- citedDomains: parseJsonColumn(s.citedDomains, []),
2250
- competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
2251
- recommendedCompetitors: parseJsonColumn(s.recommendedCompetitors, []),
2252
- matchedTerms: project ? resolveSnapshotMatchedTerms(s, project) : [],
2253
- model: s.model ?? rawParsed.model,
2254
- location: s.location,
2255
- groundingSources: rawParsed.groundingSources,
2256
- searchQueries: rawParsed.searchQueries,
2257
- createdAt: s.createdAt
2258
- };
2259
- })
2260
- });
2252
+ return reply.send(loadRunDetail(app, run));
2261
2253
  });
2262
2254
  }
2263
2255
  function formatRun(row) {
@@ -2282,6 +2274,57 @@ function parseSnapshotRawResponse(raw) {
2282
2274
  model: parsed.model ?? null
2283
2275
  };
2284
2276
  }
2277
+ function loadRunDetail(app, run) {
2278
+ const project = app.db.select({
2279
+ displayName: projects.displayName,
2280
+ canonicalDomain: projects.canonicalDomain,
2281
+ ownedDomains: projects.ownedDomains
2282
+ }).from(projects).where(eq7(projects.id, run.projectId)).get();
2283
+ const snapshots = app.db.select({
2284
+ id: querySnapshots.id,
2285
+ runId: querySnapshots.runId,
2286
+ keywordId: querySnapshots.keywordId,
2287
+ keyword: keywords.keyword,
2288
+ provider: querySnapshots.provider,
2289
+ model: querySnapshots.model,
2290
+ citationState: querySnapshots.citationState,
2291
+ answerMentioned: querySnapshots.answerMentioned,
2292
+ answerText: querySnapshots.answerText,
2293
+ citedDomains: querySnapshots.citedDomains,
2294
+ competitorOverlap: querySnapshots.competitorOverlap,
2295
+ recommendedCompetitors: querySnapshots.recommendedCompetitors,
2296
+ location: querySnapshots.location,
2297
+ rawResponse: querySnapshots.rawResponse,
2298
+ createdAt: querySnapshots.createdAt
2299
+ }).from(querySnapshots).leftJoin(keywords, eq7(querySnapshots.keywordId, keywords.id)).where(eq7(querySnapshots.runId, run.id)).all();
2300
+ return {
2301
+ ...formatRun(run),
2302
+ snapshots: snapshots.map((s) => {
2303
+ const rawParsed = parseSnapshotRawResponse(s.rawResponse);
2304
+ const answerMentioned = project ? resolveSnapshotAnswerMentioned(s, project) : s.answerMentioned ?? false;
2305
+ return {
2306
+ id: s.id,
2307
+ runId: s.runId,
2308
+ keywordId: s.keywordId,
2309
+ keyword: s.keyword,
2310
+ provider: s.provider,
2311
+ citationState: s.citationState,
2312
+ answerMentioned,
2313
+ visibilityState: project ? resolveSnapshotVisibilityState(s, project) : answerMentioned ? "visible" : "not-visible",
2314
+ answerText: s.answerText,
2315
+ citedDomains: parseJsonColumn(s.citedDomains, []),
2316
+ competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
2317
+ recommendedCompetitors: parseJsonColumn(s.recommendedCompetitors, []),
2318
+ matchedTerms: project ? resolveSnapshotMatchedTerms(s, project) : [],
2319
+ model: s.model ?? rawParsed.model,
2320
+ location: s.location,
2321
+ groundingSources: rawParsed.groundingSources,
2322
+ searchQueries: rawParsed.searchQueries,
2323
+ createdAt: s.createdAt
2324
+ };
2325
+ })
2326
+ };
2327
+ }
2285
2328
 
2286
2329
  // ../api-routes/src/apply.ts
2287
2330
  import crypto10 from "crypto";
@@ -3915,6 +3958,16 @@ var routeCatalog = [
3915
3958
  200: { description: "Runs returned." }
3916
3959
  }
3917
3960
  },
3961
+ {
3962
+ method: "get",
3963
+ path: "/api/v1/projects/{name}/runs/latest",
3964
+ summary: "Get the latest project run",
3965
+ tags: ["runs"],
3966
+ parameters: [nameParameter],
3967
+ responses: {
3968
+ 200: { description: "Latest run returned." }
3969
+ }
3970
+ },
3918
3971
  {
3919
3972
  method: "get",
3920
3973
  path: "/api/v1/runs",
@@ -5903,10 +5956,9 @@ async function snapshotRoutes(app, opts) {
5903
5956
 
5904
5957
  // ../api-routes/src/telemetry.ts
5905
5958
  async function telemetryRoutes(app, opts) {
5906
- app.get("/telemetry", async (_request, reply) => {
5959
+ app.get("/telemetry", async () => {
5907
5960
  if (!opts.getTelemetryStatus) {
5908
- const err = notImplemented("Telemetry status is not available in this deployment");
5909
- return reply.status(err.statusCode).send(err.toJSON());
5961
+ throw notImplemented("Telemetry status is not available in this deployment");
5910
5962
  }
5911
5963
  const status = opts.getTelemetryStatus();
5912
5964
  return {
@@ -5914,15 +5966,13 @@ async function telemetryRoutes(app, opts) {
5914
5966
  anonymousId: status.anonymousId ? status.anonymousId.slice(0, 8) + "..." : void 0
5915
5967
  };
5916
5968
  });
5917
- app.put("/telemetry", async (request, reply) => {
5969
+ app.put("/telemetry", async (request) => {
5918
5970
  if (!opts.setTelemetryEnabled) {
5919
- const err = notImplemented("Telemetry configuration is not available in this deployment");
5920
- return reply.status(err.statusCode).send(err.toJSON());
5971
+ throw notImplemented("Telemetry configuration is not available in this deployment");
5921
5972
  }
5922
5973
  const { enabled } = request.body ?? {};
5923
5974
  if (typeof enabled !== "boolean") {
5924
- const err = validationError("enabled (boolean) is required");
5925
- return reply.status(err.statusCode).send(err.toJSON());
5975
+ throw validationError("enabled (boolean) is required");
5926
5976
  }
5927
5977
  opts.setTelemetryEnabled(enabled);
5928
5978
  const status = opts.getTelemetryStatus?.();
@@ -6175,7 +6225,7 @@ function formatNotification(row) {
6175
6225
 
6176
6226
  // ../api-routes/src/google.ts
6177
6227
  import crypto14 from "crypto";
6178
- import { eq as eq14, and as and3, desc as desc5, sql as sql2 } from "drizzle-orm";
6228
+ import { eq as eq14, and as and3, desc as desc5, sql as sql3 } from "drizzle-orm";
6179
6229
 
6180
6230
  // ../integration-google/src/constants.ts
6181
6231
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -7298,11 +7348,11 @@ async function googleRoutes(app, opts) {
7298
7348
  const { startDate, endDate, query, page, limit } = request.query;
7299
7349
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
7300
7350
  const conditions = [eq14(gscSearchData.projectId, project.id)];
7301
- if (startDate) conditions.push(sql2`${gscSearchData.date} >= ${startDate}`);
7302
- else if (cutoffDate) conditions.push(sql2`${gscSearchData.date} >= ${cutoffDate}`);
7303
- if (endDate) conditions.push(sql2`${gscSearchData.date} <= ${endDate}`);
7304
- if (query) conditions.push(sql2`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
7305
- if (page) conditions.push(sql2`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
7351
+ if (startDate) conditions.push(sql3`${gscSearchData.date} >= ${startDate}`);
7352
+ else if (cutoffDate) conditions.push(sql3`${gscSearchData.date} >= ${cutoffDate}`);
7353
+ if (endDate) conditions.push(sql3`${gscSearchData.date} <= ${endDate}`);
7354
+ if (query) conditions.push(sql3`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
7355
+ if (page) conditions.push(sql3`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
7306
7356
  const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc5(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
7307
7357
  return rows.map((r) => ({
7308
7358
  date: r.date,
@@ -8528,7 +8578,7 @@ async function cdpRoutes(app, opts) {
8528
8578
 
8529
8579
  // ../api-routes/src/ga.ts
8530
8580
  import crypto16 from "crypto";
8531
- import { eq as eq17, desc as desc7, and as and6, sql as sql3 } from "drizzle-orm";
8581
+ import { eq as eq17, desc as desc7, and as and6, sql as sql4 } from "drizzle-orm";
8532
8582
  function gaLog(level, action, ctx) {
8533
8583
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
8534
8584
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -8770,8 +8820,8 @@ async function ga4Routes(app, opts) {
8770
8820
  tx.delete(gaTrafficSnapshots).where(
8771
8821
  and6(
8772
8822
  eq17(gaTrafficSnapshots.projectId, project.id),
8773
- sql3`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
8774
- sql3`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
8823
+ sql4`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
8824
+ sql4`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
8775
8825
  )
8776
8826
  ).run();
8777
8827
  for (const row of rows) {
@@ -8792,8 +8842,8 @@ async function ga4Routes(app, opts) {
8792
8842
  tx.delete(gaAiReferrals).where(
8793
8843
  and6(
8794
8844
  eq17(gaAiReferrals.projectId, project.id),
8795
- sql3`${gaAiReferrals.date} >= ${summary.periodStart}`,
8796
- sql3`${gaAiReferrals.date} <= ${summary.periodEnd}`
8845
+ sql4`${gaAiReferrals.date} >= ${summary.periodStart}`,
8846
+ sql4`${gaAiReferrals.date} <= ${summary.periodEnd}`
8797
8847
  )
8798
8848
  ).run();
8799
8849
  for (const row of aiReferrals) {
@@ -8815,8 +8865,8 @@ async function ga4Routes(app, opts) {
8815
8865
  tx.delete(gaSocialReferrals).where(
8816
8866
  and6(
8817
8867
  eq17(gaSocialReferrals.projectId, project.id),
8818
- sql3`${gaSocialReferrals.date} >= ${summary.periodStart}`,
8819
- sql3`${gaSocialReferrals.date} <= ${summary.periodEnd}`
8868
+ sql4`${gaSocialReferrals.date} >= ${summary.periodStart}`,
8869
+ sql4`${gaSocialReferrals.date} <= ${summary.periodEnd}`
8820
8870
  )
8821
8871
  ).run();
8822
8872
  for (const row of socialReferrals) {
@@ -8885,15 +8935,15 @@ async function ga4Routes(app, opts) {
8885
8935
  const cutoff = windowCutoff(window);
8886
8936
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
8887
8937
  const snapshotConditions = [eq17(gaTrafficSnapshots.projectId, project.id)];
8888
- if (cutoffDate) snapshotConditions.push(sql3`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
8938
+ if (cutoffDate) snapshotConditions.push(sql4`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
8889
8939
  const aiConditions = [eq17(gaAiReferrals.projectId, project.id)];
8890
- if (cutoffDate) aiConditions.push(sql3`${gaAiReferrals.date} >= ${cutoffDate}`);
8940
+ if (cutoffDate) aiConditions.push(sql4`${gaAiReferrals.date} >= ${cutoffDate}`);
8891
8941
  const socialConditions = [eq17(gaSocialReferrals.projectId, project.id)];
8892
- if (cutoffDate) socialConditions.push(sql3`${gaSocialReferrals.date} >= ${cutoffDate}`);
8942
+ if (cutoffDate) socialConditions.push(sql4`${gaSocialReferrals.date} >= ${cutoffDate}`);
8893
8943
  const summaryRow = cutoffDate ? app.db.select({
8894
- totalSessions: sql3`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
8895
- totalOrganicSessions: sql3`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
8896
- totalUsers: sql3`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
8944
+ totalSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
8945
+ totalOrganicSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
8946
+ totalUsers: sql4`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
8897
8947
  }).from(gaTrafficSnapshots).where(and6(...snapshotConditions)).get() : app.db.select({
8898
8948
  totalSessions: gaTrafficSummaries.totalSessions,
8899
8949
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
@@ -8905,27 +8955,27 @@ async function ga4Routes(app, opts) {
8905
8955
  }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).get();
8906
8956
  const rows = app.db.select({
8907
8957
  landingPage: gaTrafficSnapshots.landingPage,
8908
- sessions: sql3`SUM(${gaTrafficSnapshots.sessions})`,
8909
- organicSessions: sql3`SUM(${gaTrafficSnapshots.organicSessions})`,
8910
- users: sql3`SUM(${gaTrafficSnapshots.users})`
8911
- }).from(gaTrafficSnapshots).where(and6(...snapshotConditions)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql3`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
8958
+ sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8959
+ organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8960
+ users: sql4`SUM(${gaTrafficSnapshots.users})`
8961
+ }).from(gaTrafficSnapshots).where(and6(...snapshotConditions)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
8912
8962
  const aiReferrals = app.db.select({
8913
8963
  source: gaAiReferrals.source,
8914
8964
  medium: gaAiReferrals.medium,
8915
8965
  sourceDimension: gaAiReferrals.sourceDimension,
8916
- sessions: sql3`SUM(${gaAiReferrals.sessions})`,
8917
- users: sql3`SUM(${gaAiReferrals.users})`
8918
- }).from(gaAiReferrals).where(and6(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql3`SUM(${gaAiReferrals.sessions}) DESC`).all();
8966
+ sessions: sql4`SUM(${gaAiReferrals.sessions})`,
8967
+ users: sql4`SUM(${gaAiReferrals.users})`
8968
+ }).from(gaAiReferrals).where(and6(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
8919
8969
  const aiDeduped = app.db.select({
8920
- sessions: sql3`SUM(max_sessions)`,
8921
- users: sql3`SUM(max_users)`
8970
+ sessions: sql4`SUM(max_sessions)`,
8971
+ users: sql4`SUM(max_users)`
8922
8972
  }).from(
8923
- sql3`(
8973
+ sql4`(
8924
8974
  SELECT date, source, medium,
8925
8975
  MAX(sessions) AS max_sessions,
8926
8976
  MAX(users) AS max_users
8927
8977
  FROM ga_ai_referrals
8928
- WHERE project_id = ${project.id}${cutoffDate ? sql3` AND date >= ${cutoffDate}` : sql3``}
8978
+ WHERE project_id = ${project.id}${cutoffDate ? sql4` AND date >= ${cutoffDate}` : sql4``}
8929
8979
  GROUP BY date, source, medium
8930
8980
  )`
8931
8981
  ).get();
@@ -8933,12 +8983,12 @@ async function ga4Routes(app, opts) {
8933
8983
  source: gaSocialReferrals.source,
8934
8984
  medium: gaSocialReferrals.medium,
8935
8985
  channelGroup: gaSocialReferrals.channelGroup,
8936
- sessions: sql3`SUM(${gaSocialReferrals.sessions})`,
8937
- users: sql3`SUM(${gaSocialReferrals.users})`
8938
- }).from(gaSocialReferrals).where(and6(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql3`SUM(${gaSocialReferrals.sessions}) DESC`).all();
8986
+ sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
8987
+ users: sql4`SUM(${gaSocialReferrals.users})`
8988
+ }).from(gaSocialReferrals).where(and6(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql4`SUM(${gaSocialReferrals.sessions}) DESC`).all();
8939
8989
  const socialTotals = app.db.select({
8940
- sessions: sql3`SUM(${gaSocialReferrals.sessions})`,
8941
- users: sql3`SUM(${gaSocialReferrals.users})`
8990
+ sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
8991
+ users: sql4`SUM(${gaSocialReferrals.users})`
8942
8992
  }).from(gaSocialReferrals).where(and6(...socialConditions)).get();
8943
8993
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
8944
8994
  const total = summaryRow?.totalSessions ?? 0;
@@ -8988,7 +9038,7 @@ async function ga4Routes(app, opts) {
8988
9038
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8989
9039
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8990
9040
  const conditions = [eq17(gaAiReferrals.projectId, project.id)];
8991
- if (cutoffDate) conditions.push(sql3`${gaAiReferrals.date} >= ${cutoffDate}`);
9041
+ if (cutoffDate) conditions.push(sql4`${gaAiReferrals.date} >= ${cutoffDate}`);
8992
9042
  const rows = app.db.select({
8993
9043
  date: gaAiReferrals.date,
8994
9044
  source: gaAiReferrals.source,
@@ -9004,7 +9054,7 @@ async function ga4Routes(app, opts) {
9004
9054
  requireGa4Connection(opts, project.name, project.canonicalDomain);
9005
9055
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
9006
9056
  const conditions = [eq17(gaSocialReferrals.projectId, project.id)];
9007
- if (cutoffDate) conditions.push(sql3`${gaSocialReferrals.date} >= ${cutoffDate}`);
9057
+ if (cutoffDate) conditions.push(sql4`${gaSocialReferrals.date} >= ${cutoffDate}`);
9008
9058
  const rows = app.db.select({
9009
9059
  date: gaSocialReferrals.date,
9010
9060
  source: gaSocialReferrals.source,
@@ -9025,10 +9075,10 @@ async function ga4Routes(app, opts) {
9025
9075
  d.setDate(d.getDate() - n);
9026
9076
  return fmt(d);
9027
9077
  };
9028
- const sumSocial = (from, to) => app.db.select({ sessions: sql3`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and6(
9078
+ const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and6(
9029
9079
  eq17(gaSocialReferrals.projectId, project.id),
9030
- sql3`${gaSocialReferrals.date} >= ${from}`,
9031
- sql3`${gaSocialReferrals.date} < ${to}`
9080
+ sql4`${gaSocialReferrals.date} >= ${from}`,
9081
+ sql4`${gaSocialReferrals.date} < ${to}`
9032
9082
  )).get();
9033
9083
  const current7d = sumSocial(daysAgo2(7), fmt(today));
9034
9084
  const prev7d = sumSocial(daysAgo2(14), daysAgo2(7));
@@ -9037,19 +9087,19 @@ async function ga4Routes(app, opts) {
9037
9087
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
9038
9088
  const sourceCurrent = app.db.select({
9039
9089
  source: gaSocialReferrals.source,
9040
- sessions: sql3`SUM(${gaSocialReferrals.sessions})`
9090
+ sessions: sql4`SUM(${gaSocialReferrals.sessions})`
9041
9091
  }).from(gaSocialReferrals).where(and6(
9042
9092
  eq17(gaSocialReferrals.projectId, project.id),
9043
- sql3`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
9044
- sql3`${gaSocialReferrals.date} < ${fmt(today)}`
9093
+ sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
9094
+ sql4`${gaSocialReferrals.date} < ${fmt(today)}`
9045
9095
  )).groupBy(gaSocialReferrals.source).all();
9046
9096
  const sourcePrev = app.db.select({
9047
9097
  source: gaSocialReferrals.source,
9048
- sessions: sql3`SUM(${gaSocialReferrals.sessions})`
9098
+ sessions: sql4`SUM(${gaSocialReferrals.sessions})`
9049
9099
  }).from(gaSocialReferrals).where(and6(
9050
9100
  eq17(gaSocialReferrals.projectId, project.id),
9051
- sql3`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
9052
- sql3`${gaSocialReferrals.date} < ${daysAgo2(7)}`
9101
+ sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
9102
+ sql4`${gaSocialReferrals.date} < ${daysAgo2(7)}`
9053
9103
  )).groupBy(gaSocialReferrals.source).all();
9054
9104
  const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
9055
9105
  let biggestMover = null;
@@ -9088,15 +9138,15 @@ async function ga4Routes(app, opts) {
9088
9138
  return fmt(d);
9089
9139
  };
9090
9140
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
9091
- const sumTotal = (from, to) => app.db.select({ sessions: sql3`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and6(eq17(gaTrafficSnapshots.projectId, project.id), sql3`${gaTrafficSnapshots.date} >= ${from}`, sql3`${gaTrafficSnapshots.date} < ${to}`)).get();
9092
- const sumOrganic = (from, to) => app.db.select({ sessions: sql3`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and6(eq17(gaTrafficSnapshots.projectId, project.id), sql3`${gaTrafficSnapshots.date} >= ${from}`, sql3`${gaTrafficSnapshots.date} < ${to}`)).get();
9093
- const sumAi = (from, to) => app.db.select({ sessions: sql3`COALESCE(SUM(max_sessions), 0)` }).from(sql3`(
9141
+ const sumTotal = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and6(eq17(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
9142
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and6(eq17(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
9143
+ const sumAi = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(max_sessions), 0)` }).from(sql4`(
9094
9144
  SELECT date, source, medium, MAX(sessions) AS max_sessions
9095
9145
  FROM ga_ai_referrals
9096
9146
  WHERE project_id = ${project.id} AND date >= ${from} AND date < ${to}
9097
9147
  GROUP BY date, source, medium
9098
9148
  )`).get();
9099
- const sumSocial = (from, to) => app.db.select({ sessions: sql3`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql3`${gaSocialReferrals.date} >= ${from}`, sql3`${gaSocialReferrals.date} < ${to}`)).get();
9149
+ const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${from}`, sql4`${gaSocialReferrals.date} < ${to}`)).get();
9100
9150
  const todayStr = fmt(today);
9101
9151
  const buildTrend = (sum) => {
9102
9152
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -9105,18 +9155,18 @@ async function ga4Routes(app, opts) {
9105
9155
  const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
9106
9156
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
9107
9157
  };
9108
- const aiSourceCurrent = app.db.select({ source: sql3`source`, sessions: sql3`COALESCE(SUM(max_sessions), 0)` }).from(sql3`(
9158
+ const aiSourceCurrent = app.db.select({ source: sql4`source`, sessions: sql4`COALESCE(SUM(max_sessions), 0)` }).from(sql4`(
9109
9159
  SELECT date, source, medium, MAX(sessions) AS max_sessions
9110
9160
  FROM ga_ai_referrals
9111
9161
  WHERE project_id = ${project.id} AND date >= ${daysAgo2(7)} AND date < ${todayStr}
9112
9162
  GROUP BY date, source, medium
9113
- )`).groupBy(sql3`source`).all();
9114
- const aiSourcePrev = app.db.select({ source: sql3`source`, sessions: sql3`COALESCE(SUM(max_sessions), 0)` }).from(sql3`(
9163
+ )`).groupBy(sql4`source`).all();
9164
+ const aiSourcePrev = app.db.select({ source: sql4`source`, sessions: sql4`COALESCE(SUM(max_sessions), 0)` }).from(sql4`(
9115
9165
  SELECT date, source, medium, MAX(sessions) AS max_sessions
9116
9166
  FROM ga_ai_referrals
9117
9167
  WHERE project_id = ${project.id} AND date >= ${daysAgo2(14)} AND date < ${daysAgo2(7)}
9118
9168
  GROUP BY date, source, medium
9119
- )`).groupBy(sql3`source`).all();
9169
+ )`).groupBy(sql4`source`).all();
9120
9170
  const findBiggestMover = (current, prev) => {
9121
9171
  const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
9122
9172
  let mover = null;
@@ -9131,8 +9181,8 @@ async function ga4Routes(app, opts) {
9131
9181
  }
9132
9182
  return mover;
9133
9183
  };
9134
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql3`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql3`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql3`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
9135
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql3`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql3`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql3`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
9184
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql4`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
9185
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql4`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
9136
9186
  return {
9137
9187
  total: buildTrend(sumTotal),
9138
9188
  organic: buildTrend(sumOrganic),
@@ -9147,12 +9197,12 @@ async function ga4Routes(app, opts) {
9147
9197
  requireGa4Connection(opts, project.name, project.canonicalDomain);
9148
9198
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
9149
9199
  const conditions = [eq17(gaTrafficSnapshots.projectId, project.id)];
9150
- if (cutoffDate) conditions.push(sql3`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
9200
+ if (cutoffDate) conditions.push(sql4`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
9151
9201
  const rows = app.db.select({
9152
9202
  date: gaTrafficSnapshots.date,
9153
- sessions: sql3`SUM(${gaTrafficSnapshots.sessions})`,
9154
- organicSessions: sql3`SUM(${gaTrafficSnapshots.organicSessions})`,
9155
- users: sql3`SUM(${gaTrafficSnapshots.users})`
9203
+ sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
9204
+ organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
9205
+ users: sql4`SUM(${gaTrafficSnapshots.users})`
9156
9206
  }).from(gaTrafficSnapshots).where(and6(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
9157
9207
  return rows.map((r) => ({
9158
9208
  date: r.date,
@@ -9166,10 +9216,10 @@ async function ga4Routes(app, opts) {
9166
9216
  requireGa4Connection(opts, project.name, project.canonicalDomain);
9167
9217
  const trafficPages = app.db.select({
9168
9218
  landingPage: gaTrafficSnapshots.landingPage,
9169
- sessions: sql3`SUM(${gaTrafficSnapshots.sessions})`,
9170
- organicSessions: sql3`SUM(${gaTrafficSnapshots.organicSessions})`,
9171
- users: sql3`SUM(${gaTrafficSnapshots.users})`
9172
- }).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql3`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
9219
+ sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
9220
+ organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
9221
+ users: sql4`SUM(${gaTrafficSnapshots.users})`
9222
+ }).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
9173
9223
  return {
9174
9224
  pages: trafficPages.map((r) => ({
9175
9225
  landingPage: r.landingPage,
@@ -13442,7 +13492,7 @@ import crypto18 from "crypto";
13442
13492
  import fs4 from "fs";
13443
13493
  import path5 from "path";
13444
13494
  import os4 from "os";
13445
- import { and as and7, eq as eq18, inArray as inArray3, sql as sql4 } from "drizzle-orm";
13495
+ import { and as and7, eq as eq18, inArray as inArray3, sql as sql5 } from "drizzle-orm";
13446
13496
 
13447
13497
  // src/citation-utils.ts
13448
13498
  function domainMatches(domain, canonicalDomain) {
@@ -13956,7 +14006,7 @@ var JobRunner = class {
13956
14006
  updatedAt: now
13957
14007
  }).onConflictDoUpdate({
13958
14008
  target: [usageCounters.scope, usageCounters.period, usageCounters.metric],
13959
- set: { count: sql4`${usageCounters.count} + ${count}`, updatedAt: now }
14009
+ set: { count: sql5`${usageCounters.count} + ${count}`, updatedAt: now }
13960
14010
  }).run();
13961
14011
  }
13962
14012
  flushProviderUsage(projectId, providerDispatchCounts) {
@@ -14009,7 +14059,7 @@ function getCurrentUsageDay() {
14009
14059
 
14010
14060
  // src/gsc-sync.ts
14011
14061
  import crypto19 from "crypto";
14012
- import { eq as eq19, and as and8, sql as sql5 } from "drizzle-orm";
14062
+ import { eq as eq19, and as and8, sql as sql6 } from "drizzle-orm";
14013
14063
  var log2 = createLogger("GscSync");
14014
14064
  function formatDate2(d) {
14015
14065
  return d.toISOString().split("T")[0];
@@ -14063,8 +14113,8 @@ async function executeGscSync(db, runId, projectId, opts) {
14063
14113
  db.delete(gscSearchData).where(
14064
14114
  and8(
14065
14115
  eq19(gscSearchData.projectId, projectId),
14066
- sql5`${gscSearchData.date} >= ${startDate}`,
14067
- sql5`${gscSearchData.date} <= ${endDate}`
14116
+ sql6`${gscSearchData.date} >= ${startDate}`,
14117
+ sql6`${gscSearchData.date} <= ${endDate}`
14068
14118
  )
14069
14119
  ).run();
14070
14120
  const batchSize = 500;
@@ -14787,8 +14837,8 @@ var RunCoordinator = class {
14787
14837
  };
14788
14838
 
14789
14839
  // src/agent/session-registry.ts
14790
- import crypto22 from "crypto";
14791
- import { eq as eq23 } from "drizzle-orm";
14840
+ import crypto23 from "crypto";
14841
+ import { eq as eq24 } from "drizzle-orm";
14792
14842
 
14793
14843
  // src/agent/session.ts
14794
14844
  import fs7 from "fs";
@@ -14814,7 +14864,7 @@ var AGENT_PROVIDERS = {
14814
14864
  [AgentProviderIds.gemini]: {
14815
14865
  piAiProvider: "google",
14816
14866
  label: "Google (Gemini)",
14817
- defaultModel: "gemini-2.5-pro",
14867
+ defaultModel: "gemini-2.5-flash",
14818
14868
  autoDetectPriority: 2
14819
14869
  },
14820
14870
  [AgentProviderIds.zai]: {
@@ -15005,6 +15055,105 @@ function buildSkillDocTools() {
15005
15055
 
15006
15056
  // src/agent/tools.ts
15007
15057
  import { Type as Type2 } from "@sinclair/typebox";
15058
+
15059
+ // src/agent/memory-store.ts
15060
+ import crypto22 from "crypto";
15061
+ import { and as and11, desc as desc9, eq as eq23, like, sql as sql7 } from "drizzle-orm";
15062
+ var COMPACTION_KEY_PREFIX = "compaction:";
15063
+ var COMPACTION_NOTES_PER_SESSION = 3;
15064
+ function rowToDto(row) {
15065
+ return {
15066
+ id: row.id,
15067
+ key: row.key,
15068
+ value: row.value,
15069
+ source: row.source,
15070
+ createdAt: row.createdAt,
15071
+ updatedAt: row.updatedAt
15072
+ };
15073
+ }
15074
+ function listMemoryEntries(db, projectId, opts = {}) {
15075
+ const query = db.select().from(agentMemory).where(eq23(agentMemory.projectId, projectId)).orderBy(desc9(agentMemory.updatedAt));
15076
+ const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
15077
+ return rows.map(rowToDto);
15078
+ }
15079
+ function upsertMemoryEntry(db, args) {
15080
+ if (Buffer.byteLength(args.value, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
15081
+ throw new Error(
15082
+ `memory value exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes (got ${Buffer.byteLength(args.value, "utf8")})`
15083
+ );
15084
+ }
15085
+ if (args.source !== MemorySources.compaction && args.key.startsWith(COMPACTION_KEY_PREFIX)) {
15086
+ throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
15087
+ }
15088
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15089
+ const id = crypto22.randomUUID();
15090
+ db.insert(agentMemory).values({
15091
+ id,
15092
+ projectId: args.projectId,
15093
+ key: args.key,
15094
+ value: args.value,
15095
+ source: args.source,
15096
+ createdAt: now,
15097
+ updatedAt: now
15098
+ }).onConflictDoUpdate({
15099
+ target: [agentMemory.projectId, agentMemory.key],
15100
+ set: {
15101
+ value: args.value,
15102
+ source: args.source,
15103
+ updatedAt: now
15104
+ }
15105
+ }).run();
15106
+ const row = db.select().from(agentMemory).where(and11(eq23(agentMemory.projectId, args.projectId), eq23(agentMemory.key, args.key))).get();
15107
+ if (!row) throw new Error("memory upsert produced no row");
15108
+ return rowToDto(row);
15109
+ }
15110
+ function deleteMemoryEntry(db, projectId, key) {
15111
+ const result = db.delete(agentMemory).where(and11(eq23(agentMemory.projectId, projectId), eq23(agentMemory.key, key))).run();
15112
+ const changes = result.changes ?? 0;
15113
+ return changes > 0;
15114
+ }
15115
+ function loadRecentForHydrate(db, projectId, limit) {
15116
+ return listMemoryEntries(db, projectId, { limit });
15117
+ }
15118
+ function writeCompactionNote(db, args) {
15119
+ if (Buffer.byteLength(args.summary, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
15120
+ throw new Error(
15121
+ `compaction summary exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes; summarizer produced too much text`
15122
+ );
15123
+ }
15124
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15125
+ const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
15126
+ const id = crypto22.randomUUID();
15127
+ let inserted;
15128
+ db.transaction((tx) => {
15129
+ tx.insert(agentMemory).values({
15130
+ id,
15131
+ projectId: args.projectId,
15132
+ key,
15133
+ value: args.summary,
15134
+ source: MemorySources.compaction,
15135
+ createdAt: now,
15136
+ updatedAt: now
15137
+ }).run();
15138
+ const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
15139
+ const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
15140
+ and11(
15141
+ eq23(agentMemory.projectId, args.projectId),
15142
+ like(agentMemory.key, `${sessionPrefix}%`)
15143
+ )
15144
+ ).orderBy(desc9(agentMemory.updatedAt)).all();
15145
+ const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
15146
+ if (stale.length > 0) {
15147
+ tx.delete(agentMemory).where(sql7`${agentMemory.id} IN (${sql7.join(stale.map((s) => sql7`${s}`), sql7`, `)})`).run();
15148
+ }
15149
+ const row = tx.select().from(agentMemory).where(and11(eq23(agentMemory.projectId, args.projectId), eq23(agentMemory.key, key))).get();
15150
+ if (row) inserted = rowToDto(row);
15151
+ });
15152
+ if (!inserted) throw new Error("compaction note write produced no row");
15153
+ return inserted;
15154
+ }
15155
+
15156
+ // src/agent/tools.ts
15008
15157
  var MAX_TOOL_RESULT_CHARS = 2e4;
15009
15158
  function truncate(json) {
15010
15159
  if (json.length <= MAX_TOOL_RESULT_CHARS) return json;
@@ -15144,6 +15293,27 @@ function buildGetRunTool(ctx) {
15144
15293
  }
15145
15294
  };
15146
15295
  }
15296
+ var RecallSchema = Type2.Object({
15297
+ limit: Type2.Optional(
15298
+ Type2.Number({
15299
+ description: "Max notes to return, ordered newest-first. Default 50. Max 100.",
15300
+ minimum: 1,
15301
+ maximum: 100
15302
+ })
15303
+ )
15304
+ });
15305
+ function buildRecallTool(ctx) {
15306
+ return {
15307
+ name: "recall",
15308
+ label: "Recall memory",
15309
+ 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.",
15310
+ parameters: RecallSchema,
15311
+ execute: async (_toolCallId, params) => {
15312
+ const entries = listMemoryEntries(ctx.db, ctx.projectId, { limit: params.limit ?? 50 });
15313
+ return textResult2({ entries });
15314
+ }
15315
+ };
15316
+ }
15147
15317
  function buildReadTools(ctx) {
15148
15318
  return [
15149
15319
  buildGetStatusTool(ctx),
@@ -15152,7 +15322,8 @@ function buildReadTools(ctx) {
15152
15322
  buildGetInsightsTool(ctx),
15153
15323
  buildListKeywordsTool(ctx),
15154
15324
  buildListCompetitorsTool(ctx),
15155
- buildGetRunTool(ctx)
15325
+ buildGetRunTool(ctx),
15326
+ buildRecallTool(ctx)
15156
15327
  ];
15157
15328
  }
15158
15329
  var RunSweepSchema = Type2.Object({
@@ -15307,6 +15478,58 @@ function buildAttachAgentWebhookTool(ctx) {
15307
15478
  }
15308
15479
  };
15309
15480
  }
15481
+ var RememberSchema = Type2.Object({
15482
+ key: Type2.String({
15483
+ 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.`,
15484
+ minLength: 1,
15485
+ maxLength: AGENT_MEMORY_KEY_MAX_LENGTH
15486
+ }),
15487
+ value: Type2.String({
15488
+ 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.`,
15489
+ minLength: 1
15490
+ })
15491
+ });
15492
+ function buildRememberTool(ctx) {
15493
+ return {
15494
+ name: "remember",
15495
+ label: "Remember",
15496
+ 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.",
15497
+ parameters: RememberSchema,
15498
+ execute: async (_toolCallId, params) => {
15499
+ const entry = upsertMemoryEntry(ctx.db, {
15500
+ projectId: ctx.projectId,
15501
+ key: params.key,
15502
+ value: params.value,
15503
+ source: MemorySources.aero
15504
+ });
15505
+ return textResult2({ status: "remembered", entry });
15506
+ }
15507
+ };
15508
+ }
15509
+ var ForgetSchema = Type2.Object({
15510
+ key: Type2.String({
15511
+ description: "Exact key of the note to remove. No-op (status=missing) when no note exists for that key.",
15512
+ minLength: 1,
15513
+ maxLength: AGENT_MEMORY_KEY_MAX_LENGTH
15514
+ })
15515
+ });
15516
+ function buildForgetTool(ctx) {
15517
+ return {
15518
+ name: "forget",
15519
+ label: "Forget",
15520
+ description: "Delete a durable note by key. Use when a previously-remembered fact is wrong or no longer relevant.",
15521
+ parameters: ForgetSchema,
15522
+ execute: async (_toolCallId, params) => {
15523
+ if (params.key.startsWith(COMPACTION_KEY_PREFIX)) {
15524
+ throw new Error(
15525
+ `cannot forget compaction notes directly \u2014 they are pruned automatically (key prefix "${COMPACTION_KEY_PREFIX}" is reserved)`
15526
+ );
15527
+ }
15528
+ const removed = deleteMemoryEntry(ctx.db, ctx.projectId, params.key);
15529
+ return textResult2({ status: removed ? "forgotten" : "missing", key: params.key });
15530
+ }
15531
+ };
15532
+ }
15310
15533
  function buildWriteTools(ctx) {
15311
15534
  return [
15312
15535
  buildRunSweepTool(ctx),
@@ -15314,7 +15537,9 @@ function buildWriteTools(ctx) {
15314
15537
  buildAddKeywordsTool(ctx),
15315
15538
  buildAddCompetitorsTool(ctx),
15316
15539
  buildUpdateScheduleTool(ctx),
15317
- buildAttachAgentWebhookTool(ctx)
15540
+ buildAttachAgentWebhookTool(ctx),
15541
+ buildRememberTool(ctx),
15542
+ buildForgetTool(ctx)
15318
15543
  ];
15319
15544
  }
15320
15545
  function buildAllTools(ctx) {
@@ -15366,7 +15591,13 @@ function createAeroSession(opts) {
15366
15591
  if (!provider) throw new Error(missingProviderMessage());
15367
15592
  const model = resolveAeroModel(provider, opts.modelId);
15368
15593
  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 });
15594
+ const toolCtx = {
15595
+ client: opts.client,
15596
+ projectName: opts.projectName,
15597
+ db: opts.db,
15598
+ projectId: opts.projectId
15599
+ };
15600
+ const stateTools = toolScope === "read-only" ? buildReadTools(toolCtx) : buildAllTools(toolCtx);
15370
15601
  const defaultTools = [...stateTools, ...buildSkillDocTools()];
15371
15602
  const tools = opts.tools ?? defaultTools;
15372
15603
  return new Agent({
@@ -15387,13 +15618,151 @@ function resolveSessionProviderAndModel(config, opts) {
15387
15618
  return { provider, modelId };
15388
15619
  }
15389
15620
 
15621
+ // src/agent/compaction.ts
15622
+ import { complete } from "@mariozechner/pi-ai";
15623
+
15624
+ // src/agent/compaction-config.ts
15625
+ var COMPACTION_TOKEN_THRESHOLD = 6e4;
15626
+ var COMPACTION_TARGET_RATIO = 0.5;
15627
+ var COMPACTION_PRESERVE_TAIL_MESSAGES = 10;
15628
+ var COMPACTION_MAX_MESSAGES = 400;
15629
+
15630
+ // src/agent/token-counter.ts
15631
+ var CHARS_PER_TOKEN = 4;
15632
+ function estimateMessageTokens(message) {
15633
+ const content = message.content;
15634
+ if (content === void 0) return 0;
15635
+ if (typeof content === "string") {
15636
+ return Math.ceil(content.length / CHARS_PER_TOKEN);
15637
+ }
15638
+ if (!Array.isArray(content)) return 0;
15639
+ let chars = 0;
15640
+ for (const part of content) {
15641
+ if (part && typeof part === "object" && "type" in part) {
15642
+ const p = part;
15643
+ switch (p.type) {
15644
+ case "text":
15645
+ chars += (p.text ?? "").length;
15646
+ break;
15647
+ case "thinking":
15648
+ chars += (p.thinking ?? "").length;
15649
+ break;
15650
+ case "toolCall":
15651
+ try {
15652
+ chars += JSON.stringify(p.arguments ?? {}).length;
15653
+ } catch {
15654
+ chars += 64;
15655
+ }
15656
+ break;
15657
+ case "image":
15658
+ chars += 1024;
15659
+ break;
15660
+ default:
15661
+ break;
15662
+ }
15663
+ }
15664
+ }
15665
+ return Math.ceil(chars / CHARS_PER_TOKEN);
15666
+ }
15667
+ function estimateTranscriptTokens(messages) {
15668
+ let total = 0;
15669
+ for (const m of messages) total += estimateMessageTokens(m);
15670
+ return total;
15671
+ }
15672
+
15673
+ // src/agent/compaction.ts
15674
+ function shouldCompact(messages) {
15675
+ if (messages.length >= COMPACTION_MAX_MESSAGES) return true;
15676
+ return estimateTranscriptTokens(messages) >= COMPACTION_TOKEN_THRESHOLD;
15677
+ }
15678
+ function findSafeSplit(messages, targetIndex) {
15679
+ const maxSplit = messages.length - COMPACTION_PRESERVE_TAIL_MESSAGES;
15680
+ if (maxSplit <= 0) return 0;
15681
+ const boundedTarget = Math.max(0, Math.min(targetIndex, maxSplit));
15682
+ for (let i = boundedTarget; i <= maxSplit; i++) {
15683
+ const m = messages[i];
15684
+ if (m && m.role === "user") return i;
15685
+ }
15686
+ return 0;
15687
+ }
15688
+ function toLlmMessages(messages) {
15689
+ const out = [];
15690
+ for (const m of messages) {
15691
+ if (m.role === "user" || m.role === "assistant" || m.role === "toolResult") {
15692
+ out.push(m);
15693
+ }
15694
+ }
15695
+ return out;
15696
+ }
15697
+ var SUMMARY_SYSTEM_PROMPT = `You compress an AI agent conversation transcript into a short durable note.
15698
+
15699
+ Extract only:
15700
+ - User intents and requests
15701
+ - Actions the agent took and their outcomes
15702
+ - Key findings, insights, decisions
15703
+ - Outstanding TODOs or deferred follow-ups
15704
+
15705
+ Style: dense bullet points. No preamble, no closing remarks, no agent self-commentary. Keep the note under 1500 characters.`;
15706
+ function truncateToByteLimit(text, maxBytes) {
15707
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
15708
+ const suffix = "\u2026[truncated]";
15709
+ const budget = maxBytes - Buffer.byteLength(suffix, "utf8");
15710
+ let buf = Buffer.from(text, "utf8").subarray(0, budget);
15711
+ while (buf.length > 0 && (buf[buf.length - 1] & 192) === 128) {
15712
+ buf = buf.subarray(0, buf.length - 1);
15713
+ }
15714
+ return buf.toString("utf8") + suffix;
15715
+ }
15716
+ async function runSummaryLlm(args) {
15717
+ const context = {
15718
+ systemPrompt: SUMMARY_SYSTEM_PROMPT,
15719
+ messages: toLlmMessages(args.chunk)
15720
+ };
15721
+ const apiKey = args.getApiKey?.(args.model.provider);
15722
+ const resp = await complete(args.model, context, apiKey ? { apiKey } : {});
15723
+ const parts = resp.content.filter((p) => p.type === "text");
15724
+ const text = parts.map((p) => p.text).join("\n").trim();
15725
+ if (!text) throw new Error("summary LLM returned no text content");
15726
+ return text;
15727
+ }
15728
+ async function compactMessages(args) {
15729
+ const target = Math.floor(args.messages.length * COMPACTION_TARGET_RATIO);
15730
+ const split = findSafeSplit(args.messages, target);
15731
+ if (split === 0) return null;
15732
+ const chunk = args.messages.slice(0, split);
15733
+ const suffix = args.messages.slice(split);
15734
+ const summarize = args.summarize ?? runSummaryLlm;
15735
+ const rawSummary = await summarize({ model: args.model, chunk, getApiKey: args.getApiKey });
15736
+ const summary = truncateToByteLimit(rawSummary, AGENT_MEMORY_VALUE_MAX_BYTES);
15737
+ writeCompactionNote(args.db, {
15738
+ projectId: args.projectId,
15739
+ sessionId: args.sessionId,
15740
+ summary,
15741
+ removedCount: chunk.length
15742
+ });
15743
+ return { messages: suffix, removedCount: chunk.length, summary };
15744
+ }
15745
+
15390
15746
  // src/agent/session-registry.ts
15391
15747
  var log7 = createLogger("SessionRegistry");
15748
+ var MAX_HYDRATE_NOTES = 20;
15749
+ var MAX_HYDRATE_BYTES = 32 * 1024;
15750
+ function escapeMemoryFragment(value) {
15751
+ return value.replace(/<(\/?)memory>/gi, "<$1\u200Cmemory>");
15752
+ }
15392
15753
  var SessionRegistry = class {
15393
15754
  live = /* @__PURE__ */ new Map();
15394
15755
  pending = /* @__PURE__ */ new Map();
15395
15756
  /** Last tool scope used on the live Agent for a project. Read in getOrCreate to know when to swap. */
15396
15757
  scopes = /* @__PURE__ */ new Map();
15758
+ /** Cached resolved project id per project name, used so alignScope can rebuild tool context without a DB roundtrip. */
15759
+ projectIds = /* @__PURE__ */ new Map();
15760
+ /**
15761
+ * In-flight compaction promises keyed by project name. A second
15762
+ * `acquireForTurn` that arrives while the first is still summarizing
15763
+ * awaits the same promise instead of kicking off a duplicate LLM call.
15764
+ */
15765
+ compactions = /* @__PURE__ */ new Map();
15397
15766
  opts;
15398
15767
  constructor(opts) {
15399
15768
  this.opts = opts;
@@ -15424,19 +15793,22 @@ var SessionRegistry = class {
15424
15793
  modelProvider: effectiveProvider,
15425
15794
  modelId: effectiveModelId,
15426
15795
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15427
- }).where(eq23(agentSessions.projectId, projectId)).run();
15796
+ }).where(eq24(agentSessions.projectId, projectId)).run();
15428
15797
  }
15429
15798
  const agent2 = createAeroSession({
15430
15799
  projectName,
15431
15800
  client: this.opts.client,
15432
15801
  config: this.opts.config,
15802
+ db: this.opts.db,
15803
+ projectId,
15433
15804
  provider: effectiveProvider,
15434
15805
  modelId: effectiveModelId,
15435
- systemPromptOverride: row.systemPrompt,
15806
+ systemPromptOverride: this.buildHydratedSystemPrompt(projectId, row.systemPrompt),
15436
15807
  initialMessages: persistedMessages,
15437
15808
  toolScope: preferences?.toolScope
15438
15809
  });
15439
15810
  this.scopes.set(projectName, preferences?.toolScope ?? "all");
15811
+ this.projectIds.set(projectName, projectId);
15440
15812
  if (queued.length > 0) {
15441
15813
  this.appendPending(projectName, queued);
15442
15814
  this.updateRow(projectId, { followUpQueue: "[]" });
@@ -15451,14 +15823,21 @@ var SessionRegistry = class {
15451
15823
  projectName,
15452
15824
  client: this.opts.client,
15453
15825
  config: this.opts.config,
15826
+ db: this.opts.db,
15827
+ projectId,
15454
15828
  provider,
15455
15829
  modelId,
15456
- systemPromptOverride: systemPrompt,
15830
+ // Hydrate on the fresh path too — a brand-new session may still see
15831
+ // notes if they were seeded via CLI/API before the first prompt.
15832
+ systemPromptOverride: this.buildHydratedSystemPrompt(projectId, systemPrompt),
15457
15833
  toolScope: preferences?.toolScope
15458
15834
  });
15459
15835
  this.scopes.set(projectName, preferences?.toolScope ?? "all");
15836
+ this.projectIds.set(projectName, projectId);
15460
15837
  this.insertRow({
15461
15838
  projectId,
15839
+ // Persist the raw (unhydrated) prompt so the DB remains canonical —
15840
+ // the `<memory>` block is rebuilt from the notes table on every load.
15462
15841
  systemPrompt,
15463
15842
  modelProvider: provider,
15464
15843
  modelId,
@@ -15469,6 +15848,63 @@ var SessionRegistry = class {
15469
15848
  this.registerDrainHook(agent, projectName);
15470
15849
  return agent;
15471
15850
  }
15851
+ /**
15852
+ * Append the `<memory>` block to a base system prompt, sourced from the
15853
+ * `agent_memory` table. Returns the base prompt unchanged when no notes
15854
+ * exist — an empty block would just be prompt noise. Truncates to
15855
+ * `MAX_HYDRATE_BYTES`, dropping oldest-first, so the block is bounded
15856
+ * even when notes sit near their 2 KB cap.
15857
+ *
15858
+ * Note values come from LLM-authored compaction summaries and operator
15859
+ * input, so they are treated as untrusted data: closing tags that could
15860
+ * escape the `<memory>` wrapper are neutralized before interpolation.
15861
+ */
15862
+ buildHydratedSystemPrompt(projectId, basePrompt) {
15863
+ const entries = loadRecentForHydrate(this.opts.db, projectId, MAX_HYDRATE_NOTES);
15864
+ if (entries.length === 0) return basePrompt;
15865
+ let totalBytes = 0;
15866
+ const kept = [];
15867
+ for (const entry of entries) {
15868
+ const escaped = {
15869
+ source: escapeMemoryFragment(entry.source),
15870
+ key: escapeMemoryFragment(entry.key),
15871
+ value: escapeMemoryFragment(entry.value)
15872
+ };
15873
+ const line = `- [${escaped.source}] ${escaped.key}: ${escaped.value}
15874
+ `;
15875
+ const bytes = Buffer.byteLength(line, "utf8");
15876
+ if (totalBytes + bytes > MAX_HYDRATE_BYTES) break;
15877
+ kept.push(escaped);
15878
+ totalBytes += bytes;
15879
+ }
15880
+ if (kept.length === 0) return basePrompt;
15881
+ const lines = kept.map((e) => `- [${e.source}] ${e.key}: ${e.value}`);
15882
+ return `${basePrompt.trimEnd()}
15883
+
15884
+ ---
15885
+
15886
+ <memory>
15887
+ Project-scoped durable notes (newest first). Use remember/forget/recall to manage. Entries tagged [compaction] are LLM-summarized transcript slices.
15888
+
15889
+ ${lines.join("\n")}
15890
+ </memory>`;
15891
+ }
15892
+ /**
15893
+ * Rebuild the live agent's system prompt from the latest `agent_memory`
15894
+ * rows. Called after out-of-band memory writes (CLI/API PUT/DELETE) so
15895
+ * the next turn on a hot session sees the updated notes without waiting
15896
+ * for compaction or a cold restart. No-op when no live agent exists —
15897
+ * the next `getOrCreate` will hydrate from DB anyway.
15898
+ */
15899
+ rehydrateLiveMemory(projectName) {
15900
+ const agent = this.live.get(projectName);
15901
+ if (!agent) return;
15902
+ const projectId = this.tryResolveProjectId(projectName);
15903
+ if (!projectId) return;
15904
+ const row = this.loadRow(projectId);
15905
+ if (!row) return;
15906
+ agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
15907
+ }
15472
15908
  /**
15473
15909
  * Acquire the Agent for an upcoming prompt/turn.
15474
15910
  *
@@ -15484,7 +15920,7 @@ var SessionRegistry = class {
15484
15920
  * Persists the new model choice to the DB row so subsequent invocations
15485
15921
  * stay on it unless overridden again.
15486
15922
  */
15487
- acquireForTurn(projectName, preferences) {
15923
+ async acquireForTurn(projectName, preferences) {
15488
15924
  const agent = this.getOrCreate(projectName);
15489
15925
  if (agent.state.isStreaming) {
15490
15926
  throw agentBusy(projectName);
@@ -15493,11 +15929,70 @@ var SessionRegistry = class {
15493
15929
  if (preferences?.provider || preferences?.modelId) {
15494
15930
  this.alignModel(projectName, agent, preferences);
15495
15931
  }
15932
+ await this.maybeCompact(projectName, agent);
15496
15933
  return agent;
15497
15934
  }
15935
+ /**
15936
+ * Summarize the oldest half of the transcript into a `compaction:`
15937
+ * memory row when the transcript crosses the token/message threshold.
15938
+ * Runs before the caller's next `agent.prompt()` so the model sees the
15939
+ * trimmed transcript + a refreshed `<memory>` block that now includes
15940
+ * the new summary.
15941
+ *
15942
+ * Races are deduped through `this.compactions`: a concurrent call for
15943
+ * the same project awaits the in-flight promise instead of launching a
15944
+ * duplicate summarizer run. Failures are logged and swallowed — a flaky
15945
+ * summarizer must never block a user turn.
15946
+ */
15947
+ async maybeCompact(projectName, agent) {
15948
+ const inflight = this.compactions.get(projectName);
15949
+ if (inflight) {
15950
+ await inflight;
15951
+ return;
15952
+ }
15953
+ if (!shouldCompact(agent.state.messages)) return;
15954
+ const promise = this.runCompaction(projectName, agent).finally(() => {
15955
+ this.compactions.delete(projectName);
15956
+ });
15957
+ this.compactions.set(projectName, promise);
15958
+ await promise;
15959
+ }
15960
+ async runCompaction(projectName, agent) {
15961
+ const projectId = this.projectIds.get(projectName) ?? this.resolveProjectId(projectName);
15962
+ this.projectIds.set(projectName, projectId);
15963
+ const row = this.loadRow(projectId);
15964
+ if (!row) return;
15965
+ try {
15966
+ const result = await compactMessages({
15967
+ db: this.opts.db,
15968
+ projectId,
15969
+ sessionId: row.id,
15970
+ messages: agent.state.messages,
15971
+ model: agent.state.model,
15972
+ getApiKey: buildApiKeyResolver(this.opts.config)
15973
+ });
15974
+ if (!result) return;
15975
+ agent.state.messages = result.messages;
15976
+ agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
15977
+ this.save(projectName);
15978
+ log7.info("compaction.completed", {
15979
+ projectName,
15980
+ removedCount: result.removedCount,
15981
+ summaryBytes: Buffer.byteLength(result.summary, "utf8")
15982
+ });
15983
+ } catch (err) {
15984
+ log7.error("compaction.failed", {
15985
+ projectName,
15986
+ error: err instanceof Error ? err.message : String(err)
15987
+ });
15988
+ }
15989
+ }
15498
15990
  alignScope(projectName, agent, wantScope) {
15499
15991
  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 });
15992
+ const projectId = this.projectIds.get(projectName) ?? this.resolveProjectId(projectName);
15993
+ this.projectIds.set(projectName, projectId);
15994
+ const toolCtx = { client: this.opts.client, projectName, db: this.opts.db, projectId };
15995
+ const stateTools = wantScope === "read-only" ? buildReadTools(toolCtx) : buildAllTools(toolCtx);
15501
15996
  agent.state.tools = [...stateTools, ...buildSkillDocTools()];
15502
15997
  this.scopes.set(projectName, wantScope);
15503
15998
  }
@@ -15516,7 +16011,7 @@ var SessionRegistry = class {
15516
16011
  modelProvider: nextProvider,
15517
16012
  modelId: nextModelId,
15518
16013
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15519
- }).where(eq23(agentSessions.projectId, projectId)).run();
16014
+ }).where(eq24(agentSessions.projectId, projectId)).run();
15520
16015
  }
15521
16016
  /** Persist a session's transcript back to the DB. Call after any run settles. */
15522
16017
  save(projectName) {
@@ -15573,7 +16068,7 @@ var SessionRegistry = class {
15573
16068
  let agent;
15574
16069
  try {
15575
16070
  const scope = this.scopes.get(projectName) ?? "read-only";
15576
- agent = this.acquireForTurn(projectName, { toolScope: scope });
16071
+ agent = await this.acquireForTurn(projectName, { toolScope: scope });
15577
16072
  } catch (err) {
15578
16073
  if (err.code === "AGENT_BUSY") return;
15579
16074
  throw err;
@@ -15608,6 +16103,7 @@ var SessionRegistry = class {
15608
16103
  this.live.delete(projectName);
15609
16104
  this.pending.delete(projectName);
15610
16105
  this.scopes.delete(projectName);
16106
+ this.projectIds.delete(projectName);
15611
16107
  }
15612
16108
  /** Evict every live Agent. Durable state in DB is untouched. */
15613
16109
  clear() {
@@ -15677,17 +16173,17 @@ var SessionRegistry = class {
15677
16173
  return id;
15678
16174
  }
15679
16175
  tryResolveProjectId(projectName) {
15680
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq23(projects.name, projectName)).get();
16176
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq24(projects.name, projectName)).get();
15681
16177
  return row?.id;
15682
16178
  }
15683
16179
  loadRow(projectId) {
15684
- const row = this.opts.db.select().from(agentSessions).where(eq23(agentSessions.projectId, projectId)).get();
16180
+ const row = this.opts.db.select().from(agentSessions).where(eq24(agentSessions.projectId, projectId)).get();
15685
16181
  return row ?? null;
15686
16182
  }
15687
16183
  insertRow(params) {
15688
16184
  const now = (/* @__PURE__ */ new Date()).toISOString();
15689
16185
  this.opts.db.insert(agentSessions).values({
15690
- id: crypto22.randomUUID(),
16186
+ id: crypto23.randomUUID(),
15691
16187
  projectId: params.projectId,
15692
16188
  systemPrompt: params.systemPrompt,
15693
16189
  modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
@@ -15700,14 +16196,14 @@ var SessionRegistry = class {
15700
16196
  }
15701
16197
  updateRow(projectId, patch) {
15702
16198
  const now = (/* @__PURE__ */ new Date()).toISOString();
15703
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq23(agentSessions.projectId, projectId)).run();
16199
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq24(agentSessions.projectId, projectId)).run();
15704
16200
  }
15705
16201
  };
15706
16202
 
15707
16203
  // src/agent/agent-routes.ts
15708
- import { eq as eq24 } from "drizzle-orm";
16204
+ import { eq as eq25 } from "drizzle-orm";
15709
16205
  function resolveProject2(db, name) {
15710
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq24(projects.name, name)).get();
16206
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq25(projects.name, name)).get();
15711
16207
  if (!row) throw notFound("project", name);
15712
16208
  return row;
15713
16209
  }
@@ -15716,7 +16212,7 @@ function registerAgentRoutes(app, opts) {
15716
16212
  "/projects/:name/agent/transcript",
15717
16213
  async (request) => {
15718
16214
  const project = resolveProject2(opts.db, request.params.name);
15719
- const row = opts.db.select().from(agentSessions).where(eq24(agentSessions.projectId, project.id)).get();
16215
+ const row = opts.db.select().from(agentSessions).where(eq25(agentSessions.projectId, project.id)).get();
15720
16216
  if (!row) {
15721
16217
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
15722
16218
  }
@@ -15740,7 +16236,7 @@ function registerAgentRoutes(app, opts) {
15740
16236
  async (request) => {
15741
16237
  const project = resolveProject2(opts.db, request.params.name);
15742
16238
  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();
16239
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(agentSessions.projectId, project.id)).run();
15744
16240
  return { status: "reset" };
15745
16241
  }
15746
16242
  );
@@ -15749,7 +16245,7 @@ function registerAgentRoutes(app, opts) {
15749
16245
  const promptText = (request.body?.prompt ?? "").trim();
15750
16246
  if (!promptText) throw validationError('"prompt" is required');
15751
16247
  const requestedScope = request.body?.scope === "all" ? "all" : "read-only";
15752
- const agent = opts.sessionRegistry.acquireForTurn(project.name, {
16248
+ const agent = await opts.sessionRegistry.acquireForTurn(project.name, {
15753
16249
  provider: request.body?.provider,
15754
16250
  modelId: request.body?.modelId,
15755
16251
  toolScope: requestedScope
@@ -15800,6 +16296,57 @@ function registerAgentRoutes(app, opts) {
15800
16296
  }
15801
16297
  return reply;
15802
16298
  });
16299
+ app.get(
16300
+ "/projects/:name/agent/memory",
16301
+ async (request) => {
16302
+ const project = resolveProject2(opts.db, request.params.name);
16303
+ return { entries: listMemoryEntries(opts.db, project.id) };
16304
+ }
16305
+ );
16306
+ app.put(
16307
+ "/projects/:name/agent/memory",
16308
+ async (request) => {
16309
+ const project = resolveProject2(opts.db, request.params.name);
16310
+ const parsed = agentMemoryUpsertRequestSchema.safeParse(request.body);
16311
+ if (!parsed.success) {
16312
+ throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
16313
+ }
16314
+ if (parsed.data.key.startsWith(COMPACTION_KEY_PREFIX)) {
16315
+ throw validationError(
16316
+ `key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`
16317
+ );
16318
+ }
16319
+ if (Buffer.byteLength(parsed.data.value, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
16320
+ throw validationError(`"value" exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes`);
16321
+ }
16322
+ const entry = upsertMemoryEntry(opts.db, {
16323
+ projectId: project.id,
16324
+ key: parsed.data.key,
16325
+ value: parsed.data.value,
16326
+ source: MemorySources.user
16327
+ });
16328
+ opts.sessionRegistry.rehydrateLiveMemory(project.name);
16329
+ return { status: "ok", entry };
16330
+ }
16331
+ );
16332
+ app.delete(
16333
+ "/projects/:name/agent/memory",
16334
+ async (request) => {
16335
+ const project = resolveProject2(opts.db, request.params.name);
16336
+ const parsed = agentMemoryDeleteRequestSchema.safeParse(request.body);
16337
+ if (!parsed.success) {
16338
+ throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
16339
+ }
16340
+ if (parsed.data.key.startsWith(COMPACTION_KEY_PREFIX)) {
16341
+ throw validationError(
16342
+ `key prefix "${COMPACTION_KEY_PREFIX}" is reserved; compaction notes are pruned automatically`
16343
+ );
16344
+ }
16345
+ const removed = deleteMemoryEntry(opts.db, project.id, parsed.data.key);
16346
+ if (removed) opts.sessionRegistry.rehydrateLiveMemory(project.name);
16347
+ return { status: removed ? "forgotten" : "missing", key: parsed.data.key };
16348
+ }
16349
+ );
15803
16350
  }
15804
16351
 
15805
16352
  // src/client.ts
@@ -15890,7 +16437,7 @@ var ApiClient = class {
15890
16437
  const msg = errorObj?.message ? String(errorObj.message) : `HTTP ${res.status}: ${res.statusText}`;
15891
16438
  const code = errorObj?.code ? String(errorObj.code) : "API_ERROR";
15892
16439
  const exitCode = res.status >= 500 ? EXIT_SYSTEM_ERROR : EXIT_USER_ERROR;
15893
- throw new CliError({ code, message: msg, exitCode });
16440
+ throw new CliError({ code, message: msg, exitCode, details: { httpStatus: res.status } });
15894
16441
  }
15895
16442
  if (res.status === 204) {
15896
16443
  return void 0;
@@ -15915,6 +16462,26 @@ var ApiClient = class {
15915
16462
  `/projects/${encodeURIComponent(project)}/agent/providers`
15916
16463
  );
15917
16464
  }
16465
+ async listAgentMemory(project) {
16466
+ return this.request(
16467
+ "GET",
16468
+ `/projects/${encodeURIComponent(project)}/agent/memory`
16469
+ );
16470
+ }
16471
+ async setAgentMemory(project, body) {
16472
+ return this.request(
16473
+ "PUT",
16474
+ `/projects/${encodeURIComponent(project)}/agent/memory`,
16475
+ body
16476
+ );
16477
+ }
16478
+ async forgetAgentMemory(project, key) {
16479
+ return this.request(
16480
+ "DELETE",
16481
+ `/projects/${encodeURIComponent(project)}/agent/memory`,
16482
+ { key }
16483
+ );
16484
+ }
15918
16485
  /**
15919
16486
  * POST a request whose response body the caller intends to consume as a
15920
16487
  * stream (e.g. the Aero agent SSE endpoint). Shares the probe + auth +
@@ -15960,7 +16527,7 @@ var ApiClient = class {
15960
16527
  const msg = errorObj?.message ? String(errorObj.message) : `HTTP ${res.status}: ${res.statusText}`;
15961
16528
  const code = errorObj?.code ? String(errorObj.code) : "API_ERROR";
15962
16529
  const exitCode = res.status >= 500 ? EXIT_SYSTEM_ERROR : EXIT_USER_ERROR;
15963
- throw new CliError({ code, message: msg, exitCode });
16530
+ throw new CliError({ code, message: msg, exitCode, details: { httpStatus: res.status } });
15964
16531
  }
15965
16532
  return res;
15966
16533
  }
@@ -16001,6 +16568,9 @@ var ApiClient = class {
16001
16568
  const query = limit != null ? `?limit=${encodeURIComponent(String(limit))}` : "";
16002
16569
  return this.request("GET", `/projects/${encodeURIComponent(project)}/runs${query}`);
16003
16570
  }
16571
+ async getLatestRun(project) {
16572
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/runs/latest`);
16573
+ }
16004
16574
  async getRun(id) {
16005
16575
  return this.request("GET", `/runs/${encodeURIComponent(id)}`);
16006
16576
  }
@@ -17074,7 +17644,7 @@ function summarizeProviderConfig(provider, config) {
17074
17644
  };
17075
17645
  }
17076
17646
  function hashApiKey(key) {
17077
- return crypto23.createHash("sha256").update(key).digest("hex");
17647
+ return crypto24.createHash("sha256").update(key).digest("hex");
17078
17648
  }
17079
17649
  function parseCookies2(header) {
17080
17650
  if (!header) return {};
@@ -17232,7 +17802,7 @@ async function createServer(opts) {
17232
17802
  intelligenceService,
17233
17803
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
17234
17804
  async ({ runId, projectId, insightCount, criticalOrHigh }) => {
17235
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq25(projects.id, projectId)).get();
17805
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq26(projects.id, projectId)).get();
17236
17806
  if (!project) return;
17237
17807
  sessionRegistry.queueFollowUp(project.name, {
17238
17808
  role: "user",
@@ -17326,7 +17896,7 @@ async function createServer(opts) {
17326
17896
  return removed;
17327
17897
  }
17328
17898
  };
17329
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto23.randomBytes(32).toString("hex");
17899
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto24.randomBytes(32).toString("hex");
17330
17900
  const googleConnectionStore = {
17331
17901
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
17332
17902
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
@@ -17372,11 +17942,11 @@ async function createServer(opts) {
17372
17942
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
17373
17943
  if (opts.config.apiKey) {
17374
17944
  const keyHash = hashApiKey(opts.config.apiKey);
17375
- const existing = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, keyHash)).get();
17945
+ const existing = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, keyHash)).get();
17376
17946
  if (!existing) {
17377
17947
  const prefix = opts.config.apiKey.slice(0, 12);
17378
17948
  opts.db.insert(apiKeys).values({
17379
- id: `key_${crypto23.randomBytes(8).toString("hex")}`,
17949
+ id: `key_${crypto24.randomBytes(8).toString("hex")}`,
17380
17950
  name: "default",
17381
17951
  keyHash,
17382
17952
  keyPrefix: prefix,
@@ -17400,7 +17970,7 @@ async function createServer(opts) {
17400
17970
  };
17401
17971
  const createSession = (apiKeyId) => {
17402
17972
  pruneExpiredSessions();
17403
- const sessionId = crypto23.randomBytes(32).toString("hex");
17973
+ const sessionId = crypto24.randomBytes(32).toString("hex");
17404
17974
  sessions.set(sessionId, {
17405
17975
  apiKeyId,
17406
17976
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -17424,7 +17994,7 @@ async function createServer(opts) {
17424
17994
  };
17425
17995
  const getDefaultApiKey = () => {
17426
17996
  if (!opts.config.apiKey) return void 0;
17427
- return opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
17997
+ return opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
17428
17998
  };
17429
17999
  const createPasswordSession = (reply) => {
17430
18000
  const key = getDefaultApiKey();
@@ -17481,12 +18051,12 @@ async function createServer(opts) {
17481
18051
  return reply.send({ authenticated: true });
17482
18052
  }
17483
18053
  if (apiKey) {
17484
- const key = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(apiKey))).get();
18054
+ const key = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey(apiKey))).get();
17485
18055
  if (!key || key.revokedAt) {
17486
18056
  const err2 = authInvalid();
17487
18057
  return reply.status(err2.statusCode).send(err2.toJSON());
17488
18058
  }
17489
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(apiKeys.id, key.id)).run();
18059
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(apiKeys.id, key.id)).run();
17490
18060
  const sessionId = createSession(key.id);
17491
18061
  reply.header("set-cookie", serializeSessionCookie({
17492
18062
  name: SESSION_COOKIE_NAME,
@@ -17633,7 +18203,7 @@ async function createServer(opts) {
17633
18203
  const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
17634
18204
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
17635
18205
  opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
17636
- id: crypto23.randomUUID(),
18206
+ id: crypto24.randomUUID(),
17637
18207
  projectId,
17638
18208
  actor: "api",
17639
18209
  action: existing ? "provider.updated" : "provider.created",
@@ -17890,6 +18460,7 @@ export {
17890
18460
  EXIT_SYSTEM_ERROR,
17891
18461
  CliError,
17892
18462
  usageError,
18463
+ isEndpointMissing,
17893
18464
  printCliError,
17894
18465
  providerQuotaPolicySchema,
17895
18466
  ProviderNames,