@desplega.ai/agent-swarm 1.92.0 → 1.92.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.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/openapi.json +276 -3
  3. package/package.json +6 -6
  4. package/plugin/skills/pages/SKILL.md +5 -2
  5. package/src/be/db.ts +327 -20
  6. package/src/be/memory/constants.ts +2 -1
  7. package/src/be/memory/providers/openai-embedding.ts +2 -5
  8. package/src/be/memory/providers/sqlite-store.ts +293 -76
  9. package/src/be/memory/types.ts +35 -0
  10. package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
  11. package/src/be/migrations/085_script_runs_kind.sql +9 -0
  12. package/src/be/migrations/086_pages_default_authed.sql +64 -0
  13. package/src/be/migrations/087_skill_files.sql +19 -0
  14. package/src/be/modelsdev-cache.json +264 -328
  15. package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
  16. package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
  17. package/src/be/seed-scripts/catalog/compound-insights.ts +94 -0
  18. package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
  19. package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
  20. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +34 -439
  21. package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
  22. package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
  23. package/src/be/seed-scripts/index.ts +32 -4
  24. package/src/be/seed-skills/index.ts +0 -7
  25. package/src/be/skill-sync.ts +91 -7
  26. package/src/commands/runner.ts +6 -2
  27. package/src/heartbeat/templates.ts +20 -16
  28. package/src/http/index.ts +41 -7
  29. package/src/http/mcp-user.ts +23 -0
  30. package/src/http/mcp.ts +58 -0
  31. package/src/http/memory.ts +58 -0
  32. package/src/http/pages.ts +1 -1
  33. package/src/http/script-runs.ts +2 -0
  34. package/src/http/scripts.ts +39 -2
  35. package/src/http/skills.ts +225 -0
  36. package/src/providers/claude-adapter.ts +56 -24
  37. package/src/script-workflows/workflow-ctx.ts +7 -3
  38. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  39. package/src/scripts-runtime/swarm-sdk.ts +13 -0
  40. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  41. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  42. package/src/server.ts +2 -0
  43. package/src/tests/claude-adapter-binary.test.ts +135 -81
  44. package/src/tests/create-page-tool.test.ts +19 -2
  45. package/src/tests/heartbeat-checklist.test.ts +36 -0
  46. package/src/tests/mcp-transport-gc.test.ts +58 -0
  47. package/src/tests/memory-health-endpoint.test.ts +78 -0
  48. package/src/tests/memory-store.test.ts +221 -1
  49. package/src/tests/pages-http.test.ts +20 -2
  50. package/src/tests/pages-storage.test.ts +26 -0
  51. package/src/tests/scripts-mcp-e2e.test.ts +53 -0
  52. package/src/tests/seed-scripts.test.ts +123 -3
  53. package/src/tests/skill-files-http.test.ts +171 -0
  54. package/src/tests/skill-files.test.ts +162 -0
  55. package/src/tests/skill-get-file-tool.test.ts +110 -0
  56. package/src/tests/skill-sync.test.ts +125 -6
  57. package/src/tools/create-page.ts +2 -2
  58. package/src/tools/skills/index.ts +1 -0
  59. package/src/tools/skills/skill-get-file.ts +80 -0
  60. package/src/tools/tool-config.ts +2 -1
  61. package/src/types.ts +20 -0
  62. package/src/utils/internal-ai/complete-structured.ts +2 -2
  63. package/templates/schedules/daily-blocker-digest/content.md +68 -54
  64. package/templates/schedules/daily-compounding-reflection/content.md +4 -4
  65. package/templates/schedules/daily-hn-briefing/content.md +5 -5
  66. package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
  67. package/templates/schedules/gtm-weekly-review/content.md +9 -9
  68. package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
  69. package/templates/skills/agentmail-sending/content.md +6 -7
  70. package/templates/skills/desloppify/content.md +8 -9
  71. package/templates/skills/jira-interaction/content.md +25 -33
  72. package/templates/skills/kapso-whatsapp/content.md +29 -30
  73. package/templates/skills/linear-interaction/content.md +8 -9
  74. package/templates/skills/profile-corruption-escalation/content.md +44 -85
  75. package/templates/skills/sprite-cli/content.md +4 -5
  76. package/templates/skills/turso-interaction/content.md +14 -17
  77. package/templates/skills/workflow-iterate/content.md +38 -391
  78. package/templates/skills/x-api-interactions/content.md +4 -6
  79. package/templates/skills/scheduled-task-resilience/config.json +0 -14
  80. package/templates/skills/scheduled-task-resilience/content.md +0 -95
package/src/be/db.ts CHANGED
@@ -64,6 +64,7 @@ import type {
64
64
  ScheduledTaskSummary,
65
65
  ScriptRun,
66
66
  ScriptRunJournalEntry,
67
+ ScriptRunKind,
67
68
  ScriptRunStatus,
68
69
  Service,
69
70
  ServiceStatus,
@@ -71,6 +72,7 @@ import type {
71
72
  SessionCostSource,
72
73
  SessionLog,
73
74
  Skill,
75
+ SkillFile,
74
76
  SkillScope,
75
77
  SkillType,
76
78
  SkillWithInstallInfo,
@@ -113,6 +115,26 @@ export function isSqliteVecAvailable(): boolean {
113
115
  return sqliteVecAvailable;
114
116
  }
115
117
 
118
+ function loadSqliteVec(database: Database): void {
119
+ sqliteVecAvailable = false;
120
+ try {
121
+ const extensionPath = process.env.SQLITE_VEC_EXTENSION_PATH;
122
+ if (extensionPath) {
123
+ database.loadExtension(extensionPath);
124
+ } else {
125
+ const sqliteVec = require("sqlite-vec");
126
+ sqliteVec.load(database);
127
+ }
128
+ sqliteVecAvailable = true;
129
+ console.log(`[db] sqlite-vec loaded${extensionPath ? ` from ${extensionPath}` : ""}`);
130
+ } catch (err) {
131
+ console.warn(
132
+ "[db] sqlite-vec not available, falling back to in-memory cosine:",
133
+ (err as Error).message,
134
+ );
135
+ }
136
+ }
137
+
116
138
  export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
117
139
  if (db) {
118
140
  return db;
@@ -129,6 +151,7 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
129
151
  db = Database.deserialize(templateBytes);
130
152
  db.run("PRAGMA busy_timeout = 5000;");
131
153
  db.run("PRAGMA foreign_keys = ON;");
154
+ loadSqliteVec(db);
132
155
  configureDbResolver(resolvePromptTemplate);
133
156
  // Ensure the encryption key is resolved even when restoring from the test
134
157
  // template. The cache may have been cleared via __resetEncryptionKeyForTests
@@ -154,22 +177,7 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
154
177
  // `require.resolve("sqlite-vec-<platform>/vec0.so")` can't find the native
155
178
  // asset — so we prefer an explicit filesystem path when set, and only fall
156
179
  // back to the npm resolver for normal dev runs.
157
- try {
158
- const extensionPath = process.env.SQLITE_VEC_EXTENSION_PATH;
159
- if (extensionPath) {
160
- database.loadExtension(extensionPath);
161
- } else {
162
- const sqliteVec = require("sqlite-vec");
163
- sqliteVec.load(database);
164
- }
165
- sqliteVecAvailable = true;
166
- console.log(`[db] sqlite-vec loaded${extensionPath ? ` from ${extensionPath}` : ""}`);
167
- } catch (err) {
168
- console.warn(
169
- "[db] sqlite-vec not available, falling back to in-memory cosine:",
170
- (err as Error).message,
171
- );
172
- }
180
+ loadSqliteVec(database);
173
181
 
174
182
  // Run database migrations (schema creation + incremental changes)
175
183
  runMigrations(database);
@@ -347,6 +355,7 @@ export function closeDb(): void {
347
355
  db.close();
348
356
  db = null;
349
357
  }
358
+ sqliteVecAvailable = false;
350
359
  }
351
360
 
352
361
  // ============================================================================
@@ -7091,7 +7100,7 @@ export function createPage(data: {
7091
7100
  title: string;
7092
7101
  description?: string;
7093
7102
  contentType: PageContentType;
7094
- authMode: PageAuthMode;
7103
+ authMode?: PageAuthMode;
7095
7104
  passwordHash?: string;
7096
7105
  body: string;
7097
7106
  needsCredentials?: string[];
@@ -7110,7 +7119,7 @@ export function createPage(data: {
7110
7119
  data.title,
7111
7120
  data.description ?? null,
7112
7121
  data.contentType,
7113
- data.authMode,
7122
+ data.authMode ?? "authed",
7114
7123
  data.passwordHash ?? null,
7115
7124
  data.body,
7116
7125
  data.needsCredentials ? JSON.stringify(data.needsCredentials) : null,
@@ -8692,6 +8701,119 @@ function rowToSkillWithInstall(row: SkillWithInstallRow): SkillWithInstallInfo {
8692
8701
  };
8693
8702
  }
8694
8703
 
8704
+ type SkillFileRow = {
8705
+ id: string;
8706
+ skillId: string;
8707
+ path: string;
8708
+ content: string;
8709
+ mimeType: string;
8710
+ isBinary: number;
8711
+ size: number | null;
8712
+ createdAt: string;
8713
+ lastUpdatedAt: string;
8714
+ };
8715
+
8716
+ function rowToSkillFile(row: SkillFileRow): SkillFile {
8717
+ return {
8718
+ id: row.id,
8719
+ skillId: row.skillId,
8720
+ path: row.path,
8721
+ content: row.content,
8722
+ mimeType: row.mimeType,
8723
+ isBinary: row.isBinary === 1,
8724
+ size: row.size,
8725
+ createdAt: row.createdAt,
8726
+ lastUpdatedAt: row.lastUpdatedAt,
8727
+ };
8728
+ }
8729
+
8730
+ export type SkillFileInput = {
8731
+ path: string;
8732
+ content: string;
8733
+ mimeType?: string;
8734
+ isBinary?: boolean;
8735
+ size?: number | null;
8736
+ };
8737
+
8738
+ export type SkillFileManifestEntry = Omit<SkillFile, "content">;
8739
+ type NormalizedSkillFileInput = {
8740
+ path: string;
8741
+ content: string;
8742
+ mimeType: string;
8743
+ isBinary: boolean;
8744
+ size: number;
8745
+ };
8746
+
8747
+ export const SKILL_FILE_LIMITS = {
8748
+ maxCount: Number(process.env.SKILL_FILES_MAX_COUNT ?? 100),
8749
+ maxTotalBytes: Number(process.env.SKILL_FILES_MAX_TOTAL_BYTES ?? 10 * 1024 * 1024),
8750
+ maxFileBytes: Number(process.env.SKILL_FILES_MAX_FILE_BYTES ?? 500 * 1024),
8751
+ };
8752
+
8753
+ const BINARY_SKILL_FILE_PLACEHOLDER = "[binary file - not synced]";
8754
+
8755
+ export function normalizeSkillFilePath(path: string): string {
8756
+ const raw = path.trim().replace(/\\/g, "/");
8757
+ if (!raw) throw new Error("File path is required");
8758
+ if (raw.startsWith("/")) throw new Error("File path must be relative");
8759
+
8760
+ const parts = raw.split("/").filter(Boolean);
8761
+ if (parts.length === 0) throw new Error("File path is required");
8762
+ if (parts.some((part) => part === "." || part === "..")) {
8763
+ throw new Error("File path cannot contain traversal segments");
8764
+ }
8765
+
8766
+ const normalized = parts.join("/");
8767
+ if (normalized === "SKILL.md") {
8768
+ throw new Error("SKILL.md is stored on the skill record, not in skill_files");
8769
+ }
8770
+ return normalized;
8771
+ }
8772
+
8773
+ function byteSize(content: string): number {
8774
+ return Buffer.byteLength(content, "utf8");
8775
+ }
8776
+
8777
+ function normalizeSkillFileInput(input: SkillFileInput): NormalizedSkillFileInput {
8778
+ const path = normalizeSkillFilePath(input.path);
8779
+ const isBinary = input.isBinary === true;
8780
+ const content = isBinary ? input.content || BINARY_SKILL_FILE_PLACEHOLDER : input.content;
8781
+ const size = input.size ?? byteSize(content);
8782
+ if (!Number.isFinite(size) || size < 0) {
8783
+ throw new Error("File size must be a non-negative number");
8784
+ }
8785
+ if (size > SKILL_FILE_LIMITS.maxFileBytes) {
8786
+ throw new Error(`File ${path} exceeds max size ${SKILL_FILE_LIMITS.maxFileBytes}`);
8787
+ }
8788
+
8789
+ return {
8790
+ path,
8791
+ content,
8792
+ mimeType: input.mimeType ?? "text/plain",
8793
+ isBinary,
8794
+ size,
8795
+ };
8796
+ }
8797
+
8798
+ function assertSkillFileLimits(skillId: string, incoming: SkillFileInput[], replaceAll: boolean) {
8799
+ const existing = replaceAll ? [] : listSkillFileManifest(skillId);
8800
+ const byPath = new Map(existing.map((file) => [file.path, file.size ?? 0]));
8801
+
8802
+ for (const input of incoming) {
8803
+ const normalized = normalizeSkillFileInput(input);
8804
+ byPath.set(normalized.path, normalized.size);
8805
+ }
8806
+
8807
+ if (byPath.size > SKILL_FILE_LIMITS.maxCount) {
8808
+ throw new Error(`Skill file count exceeds max ${SKILL_FILE_LIMITS.maxCount}`);
8809
+ }
8810
+
8811
+ const total = [...byPath.values()].reduce((sum, size) => sum + size, 0);
8812
+ if (total > SKILL_FILE_LIMITS.maxTotalBytes) {
8813
+ throw new Error(`Skill files exceed max total size ${SKILL_FILE_LIMITS.maxTotalBytes}`);
8814
+ }
8815
+ }
8816
+
8695
8817
  export interface SkillInsert {
8696
8818
  name: string;
8697
8819
  description: string;
@@ -8865,6 +8987,124 @@ export function updateSkill(
8865
8987
  return row ? rowToSkill(row) : null;
8866
8988
  }
8867
8989
 
8990
+ function bumpSkillVersion(skillId: string, now = new Date().toISOString()) {
8991
+ getDb()
8992
+ .prepare("UPDATE skills SET version = version + 1, lastUpdatedAt = ? WHERE id = ?")
8993
+ .run(now, skillId);
8994
+ }
8995
+
8996
+ export function listSkillFileManifest(skillId: string): SkillFileManifestEntry[] {
8997
+ return getDb()
8998
+ .prepare<SkillFileRow, [string]>(
8999
+ `SELECT id, skillId, path, content, mimeType, isBinary, size, createdAt, lastUpdatedAt
9000
+ FROM skill_files
9001
+ WHERE skillId = ?
9002
+ ORDER BY path ASC`,
9003
+ )
9004
+ .all(skillId)
9005
+ .map((row) => {
9006
+ const { content: _content, ...manifest } = rowToSkillFile(row);
9007
+ return manifest;
9008
+ });
9009
+ }
9010
+
9011
+ export function getSkillFiles(skillId: string): SkillFile[] {
9012
+ return getDb()
9013
+ .prepare<SkillFileRow, [string]>(
9014
+ `SELECT id, skillId, path, content, mimeType, isBinary, size, createdAt, lastUpdatedAt
9015
+ FROM skill_files
9016
+ WHERE skillId = ?
9017
+ ORDER BY path ASC`,
9018
+ )
9019
+ .all(skillId)
9020
+ .map(rowToSkillFile);
9021
+ }
9022
+
9023
+ export function getSkillFile(skillId: string, path: string): SkillFile | null {
9024
+ const normalizedPath = normalizeSkillFilePath(path);
9025
+ const row = getDb()
9026
+ .prepare<SkillFileRow, [string, string]>(
9027
+ `SELECT id, skillId, path, content, mimeType, isBinary, size, createdAt, lastUpdatedAt
9028
+ FROM skill_files
9029
+ WHERE skillId = ? AND path = ?`,
9030
+ )
9031
+ .get(skillId, normalizedPath);
9032
+ return row ? rowToSkillFile(row) : null;
9033
+ }
9034
+
9035
+ export function upsertSkillFile(skillId: string, input: SkillFileInput): SkillFile {
9036
+ const payload = normalizeSkillFileInput(input);
9037
+ assertSkillFileLimits(skillId, [payload], false);
9038
+
9039
+ const id = crypto.randomUUID();
9040
+ const now = new Date().toISOString();
9041
+ return upsertSkillFileUnchecked(skillId, payload, id, now, true);
9042
+ }
9043
+
9044
+ function upsertSkillFileUnchecked(
9045
+ skillId: string,
9046
+ payload: NormalizedSkillFileInput,
9047
+ id: string,
9048
+ now: string,
9049
+ bumpVersion: boolean,
9050
+ ): SkillFile {
9051
+ const row = getDb()
9052
+ .prepare<SkillFileRow, (string | number | null)[]>(
9053
+ `INSERT INTO skill_files (
9054
+ id, skillId, path, content, mimeType, isBinary, size, createdAt, lastUpdatedAt
9055
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
9056
+ ON CONFLICT(skillId, path) DO UPDATE SET
9057
+ content = excluded.content,
9058
+ mimeType = excluded.mimeType,
9059
+ isBinary = excluded.isBinary,
9060
+ size = excluded.size,
9061
+ lastUpdatedAt = excluded.lastUpdatedAt
9062
+ RETURNING *`,
9063
+ )
9064
+ .get(
9065
+ id,
9066
+ skillId,
9067
+ payload.path,
9068
+ payload.content,
9069
+ payload.mimeType,
9070
+ payload.isBinary ? 1 : 0,
9071
+ payload.size,
9072
+ now,
9073
+ now,
9074
+ );
9075
+
9076
+ if (!row) throw new Error("Failed to upsert skill file");
9077
+ if (bumpVersion) bumpSkillVersion(skillId, now);
9078
+ return rowToSkillFile(row);
9079
+ }
9080
+
9081
+ export function upsertSkillFiles(skillId: string, files: SkillFileInput[]): SkillFile[] {
9082
+ if (files.length === 0) return [];
9083
+ const normalized = files.map(normalizeSkillFileInput);
9084
+ assertSkillFileLimits(skillId, normalized, false);
9085
+
9086
+ const now = new Date().toISOString();
9087
+ return getDb().transaction(() => {
9088
+ const rows = normalized.map((file) =>
9089
+ upsertSkillFileUnchecked(skillId, file, crypto.randomUUID(), now, false),
9090
+ );
9091
+ bumpSkillVersion(skillId, now);
9092
+ return rows;
9093
+ })();
9094
+ }
9095
+
9096
+ export function deleteSkillFile(skillId: string, path: string): boolean {
9097
+ const normalizedPath = normalizeSkillFilePath(path);
9098
+ const result = getDb()
9099
+ .prepare("DELETE FROM skill_files WHERE skillId = ? AND path = ?")
9100
+ .run(skillId, normalizedPath);
9101
+ if (result.changes > 0) {
9102
+ bumpSkillVersion(skillId);
9103
+ return true;
9104
+ }
9105
+ return false;
9106
+ }
9107
+
8868
9108
  export function deleteSkill(id: string): boolean {
8869
9109
  const result = getDb().prepare("DELETE FROM skills WHERE id = ?").run(id);
8870
9110
  return result.changes > 0;
@@ -11231,6 +11471,7 @@ type ScriptRunRow = {
11231
11471
  scriptName: string | null;
11232
11472
  source: string;
11233
11473
  args: string;
11474
+ kind: string;
11234
11475
  status: string;
11235
11476
  pid: number | null;
11236
11477
  startedAt: string;
@@ -11256,6 +11497,7 @@ function rowToScriptRun(row: ScriptRunRow): ScriptRun {
11256
11497
  scriptName: row.scriptName ?? undefined,
11257
11498
  source: row.source,
11258
11499
  args: JSON.parse(row.args),
11500
+ kind: row.kind as ScriptRunKind,
11259
11501
  status: row.status as ScriptRunStatus,
11260
11502
  pid: row.pid ?? undefined,
11261
11503
  startedAt: row.startedAt,
@@ -11322,6 +11564,67 @@ export function createScriptRun(data: {
11322
11564
  return { run: rowToScriptRun(row), existing: false };
11323
11565
  }
11324
11566
 
11567
+ // Persist a synchronous inline run (POST /api/scripts/run) as an already-terminal
11568
+ // row. Unlike createScriptRun these never get a journal and never use the
11569
+ // idempotencyKey column (inline idempotency lives in the kv table).
11570
+ export function recordInlineScriptRun(data: {
11571
+ id: string;
11572
+ agentId: string;
11573
+ source: string;
11574
+ args: unknown;
11575
+ scriptName?: string;
11576
+ status: "completed" | "failed";
11577
+ output?: unknown;
11578
+ error?: string;
11579
+ startedAt: string;
11580
+ finishedAt: string;
11581
+ requestedByUserId?: string;
11582
+ createdBy?: string;
11583
+ }): ScriptRun {
11584
+ const row = getDb()
11585
+ .prepare<
11586
+ ScriptRunRow,
11587
+ [
11588
+ string,
11589
+ string,
11590
+ string | null,
11591
+ string,
11592
+ string,
11593
+ string,
11594
+ string | null,
11595
+ string | null,
11596
+ string,
11597
+ string,
11598
+ string | null,
11599
+ string | null,
11600
+ string | null,
11601
+ ]
11602
+ >(
11603
+ `INSERT INTO script_runs
11604
+ (id, agentId, scriptName, source, args, kind, status, output, error,
11605
+ startedAt, finishedAt, requestedByUserId, created_by, updated_by)
11606
+ VALUES (?, ?, ?, ?, ?, 'inline', ?, ?, ?, ?, ?, ?, ?, ?)
11607
+ RETURNING *`,
11608
+ )
11609
+ .get(
11610
+ data.id,
11611
+ data.agentId,
11612
+ data.scriptName ?? null,
11613
+ data.source,
11614
+ JSON.stringify(data.args ?? null),
11615
+ data.status,
11616
+ data.output === undefined ? null : JSON.stringify(data.output),
11617
+ data.error ?? null,
11618
+ data.startedAt,
11619
+ data.finishedAt,
11620
+ data.requestedByUserId ?? null,
11621
+ data.createdBy ?? null,
11622
+ data.createdBy ?? null,
11623
+ );
11624
+ if (!row) throw new Error("Failed to record inline script run");
11625
+ return rowToScriptRun(row);
11626
+ }
11627
+
11325
11628
  export function getScriptRun(id: string): ScriptRun | null {
11326
11629
  const row = getDb()
11327
11630
  .prepare<ScriptRunRow, [string]>("SELECT * FROM script_runs WHERE id = ?")
@@ -11459,6 +11762,7 @@ type ScriptRunJournalRow = {
11459
11762
  error: string | null;
11460
11763
  startedAt: string;
11461
11764
  completedAt: string | null;
11765
+ durationMs: number | null;
11462
11766
  created_by: string | null;
11463
11767
  updated_by: string | null;
11464
11768
  };
@@ -11475,6 +11779,7 @@ function rowToScriptRunJournalEntry(row: ScriptRunJournalRow): ScriptRunJournalE
11475
11779
  error: row.error ?? undefined,
11476
11780
  startedAt: row.startedAt,
11477
11781
  completedAt: row.completedAt ?? undefined,
11782
+ durationMs: row.durationMs ?? undefined,
11478
11783
  };
11479
11784
  }
11480
11785
 
@@ -11498,11 +11803,12 @@ export function upsertScriptRunJournalStep(data: {
11498
11803
  status: "completed" | "failed";
11499
11804
  result?: unknown;
11500
11805
  error?: string;
11806
+ durationMs?: number;
11501
11807
  }): void {
11502
11808
  getDb().run(
11503
11809
  `INSERT OR IGNORE INTO script_run_journal
11504
- (id, runId, stepKey, stepType, config, status, result, error, completedAt)
11505
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
11810
+ (id, runId, stepKey, stepType, config, status, result, error, durationMs, completedAt)
11811
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
11506
11812
  [
11507
11813
  crypto.randomUUID(),
11508
11814
  data.runId,
@@ -11512,6 +11818,7 @@ export function upsertScriptRunJournalStep(data: {
11512
11818
  data.status,
11513
11819
  data.result !== undefined ? JSON.stringify(data.result) : null,
11514
11820
  data.error ?? null,
11821
+ data.durationMs ?? null,
11515
11822
  ],
11516
11823
  );
11517
11824
  }
@@ -22,5 +22,6 @@ export const ACCESS_BOOST_RECENCY_WINDOW_HOURS = numEnv("MEMORY_ACCESS_RECENCY_H
22
22
  export const CANDIDATE_SET_MULTIPLIER = numEnv("MEMORY_CANDIDATE_MULTIPLIER", 3);
23
23
 
24
24
  // Embedding defaults
25
- export const DEFAULT_EMBEDDING_DIMENSIONS = 512;
25
+ export const EMBEDDING_DIMENSIONS = numEnv("EMBEDDING_DIMENSIONS", 512);
26
+ export const DEFAULT_EMBEDDING_DIMENSIONS = EMBEDDING_DIMENSIONS;
26
27
  export const DEFAULT_EMBEDDING_MODEL = "openai/text-embedding-3-small";
@@ -1,5 +1,5 @@
1
1
  import OpenAI from "openai";
2
- import { DEFAULT_EMBEDDING_DIMENSIONS, DEFAULT_EMBEDDING_MODEL } from "../constants";
2
+ import { DEFAULT_EMBEDDING_MODEL, EMBEDDING_DIMENSIONS } from "../constants";
3
3
  import type { EmbeddingProvider } from "../types";
4
4
 
5
5
  interface OpenAIEmbeddingConfig {
@@ -21,10 +21,7 @@ export class OpenAIEmbeddingProvider implements EmbeddingProvider {
21
21
 
22
22
  this.model = config?.model ?? process.env.EMBEDDING_MODEL ?? "text-embedding-3-small";
23
23
 
24
- this.dimensions =
25
- config?.dimensions ??
26
- Number(process.env.EMBEDDING_DIMENSIONS) ??
27
- DEFAULT_EMBEDDING_DIMENSIONS;
24
+ this.dimensions = config?.dimensions ?? EMBEDDING_DIMENSIONS;
28
25
 
29
26
  this.name = config?.model
30
27
  ? `openai/${config.model}`