@sixb/sqlite 0.1.2 → 0.1.4

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 (40) hide show
  1. package/dist/agents/agent-storage.d.ts +5 -3
  2. package/dist/agents/agent-storage.d.ts.map +1 -1
  3. package/dist/agents/checkpoints.d.ts +18 -0
  4. package/dist/agents/checkpoints.d.ts.map +1 -0
  5. package/dist/agents/index.d.ts +1 -0
  6. package/dist/agents/index.d.ts.map +1 -1
  7. package/dist/agents/messages.d.ts +2 -1
  8. package/dist/agents/messages.d.ts.map +1 -1
  9. package/dist/agents/rows.d.ts +18 -1
  10. package/dist/agents/rows.d.ts.map +1 -1
  11. package/dist/ai-cost-storage.d.ts +15 -0
  12. package/dist/ai-cost-storage.d.ts.map +1 -0
  13. package/dist/ai-usage-storage.d.ts +2 -1
  14. package/dist/ai-usage-storage.d.ts.map +1 -1
  15. package/dist/index.d.ts +4 -3
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +979 -74
  18. package/dist/index.js.map +15 -13
  19. package/dist/migrations.d.ts.map +1 -1
  20. package/dist/object-storage.d.ts +4 -3
  21. package/dist/object-storage.d.ts.map +1 -1
  22. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  23. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  24. package/package.json +3 -3
  25. package/src/agents/agent-storage.ts +6 -3
  26. package/src/agents/checkpoints.ts +205 -0
  27. package/src/agents/index.ts +1 -0
  28. package/src/agents/messages.ts +59 -0
  29. package/src/agents/rows.ts +44 -0
  30. package/src/ai-cost-storage.ts +744 -0
  31. package/src/ai-usage-storage.ts +19 -0
  32. package/src/index.ts +8 -2
  33. package/src/migrations/026-ai-cost-accounting.sql +57 -0
  34. package/src/migrations/027-agent-context-checkpoints.sql +28 -0
  35. package/src/migrations/028-object-override-edit-times.sql +3 -0
  36. package/src/migrations.ts +10 -0
  37. package/src/object-query-compiler.ts +50 -0
  38. package/src/object-storage.ts +138 -34
  39. package/src/ontology-storage/materialization-state.ts +2 -0
  40. package/src/ontology-storage/materialization-writer.ts +6 -3
package/dist/index.js CHANGED
@@ -2918,6 +2918,103 @@ CREATE INDEX idx_connector_connections_status
2918
2918
  ON connector_connections (project_id, connector_id, status, id);
2919
2919
  `;
2920
2920
 
2921
+ // src/migrations/026-ai-cost-accounting.sql
2922
+ var _026_ai_cost_accounting_default = `CREATE TABLE ai_model_call_valuations (
2923
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
2924
+ usage_record_id TEXT NOT NULL CHECK (length(trim(usage_record_id)) > 0),
2925
+ status TEXT NOT NULL CHECK (status IN ('rated', 'unpriceable')),
2926
+ provider_id TEXT CHECK (provider_id IS NULL OR length(trim(provider_id)) > 0),
2927
+ model_id TEXT CHECK (model_id IS NULL OR length(trim(model_id)) > 0),
2928
+ currency TEXT CHECK (
2929
+ currency IS NULL OR (length(currency) = 3 AND currency GLOB '[A-Z][A-Z][A-Z]')
2930
+ ),
2931
+ amount_nanos INTEGER CHECK (
2932
+ amount_nanos IS NULL OR (typeof(amount_nanos) = 'integer' AND amount_nanos >= 0)
2933
+ ),
2934
+ reason TEXT CHECK (
2935
+ reason IS NULL OR reason IN (
2936
+ 'missingBillingIdentity',
2937
+ 'missingCatalogEntry',
2938
+ 'missingUsageMeter',
2939
+ 'unsupportedPricingDimension',
2940
+ 'invalidUsageForFormula'
2941
+ )
2942
+ ),
2943
+ details TEXT NOT NULL CHECK (json_valid(details) AND json_type(details) = 'object'),
2944
+ rated_at TEXT NOT NULL CHECK (length(trim(rated_at)) > 0),
2945
+ PRIMARY KEY (project_id, usage_record_id),
2946
+ FOREIGN KEY (project_id, usage_record_id)
2947
+ REFERENCES ai_model_call_usage(project_id, id)
2948
+ ON DELETE RESTRICT,
2949
+ CHECK (
2950
+ (
2951
+ status = 'rated'
2952
+ AND provider_id IS NOT NULL
2953
+ AND model_id IS NOT NULL
2954
+ AND currency IS NOT NULL
2955
+ AND amount_nanos IS NOT NULL
2956
+ AND reason IS NULL
2957
+ )
2958
+ OR
2959
+ (
2960
+ status = 'unpriceable'
2961
+ AND currency IS NULL
2962
+ AND amount_nanos IS NULL
2963
+ AND reason IS NOT NULL
2964
+ AND (
2965
+ (reason = 'missingBillingIdentity' AND provider_id IS NULL AND model_id IS NULL)
2966
+ OR
2967
+ (reason <> 'missingBillingIdentity' AND provider_id IS NOT NULL AND model_id IS NOT NULL)
2968
+ )
2969
+ )
2970
+ )
2971
+ );
2972
+
2973
+ CREATE INDEX idx_ai_model_call_usage_provider_model_time
2974
+ ON ai_model_call_usage (project_id, provider_id, requested_model_id, occurred_at, id);
2975
+ CREATE INDEX idx_ai_model_call_usage_model_time
2976
+ ON ai_model_call_usage (project_id, requested_model_id, occurred_at, id);
2977
+ CREATE INDEX idx_ai_model_call_valuations_summary
2978
+ ON ai_model_call_valuations (project_id, status, currency, usage_record_id);
2979
+ `;
2980
+
2981
+ // src/migrations/027-agent-context-checkpoints.sql
2982
+ var _027_agent_context_checkpoints_default = `CREATE TABLE agent_context_checkpoints (
2983
+ project_id TEXT NOT NULL,
2984
+ id TEXT NOT NULL,
2985
+ thread_id TEXT NOT NULL,
2986
+ created_by_run_id TEXT NOT NULL,
2987
+ previous_checkpoint_id TEXT,
2988
+ reason TEXT NOT NULL CHECK (reason IN ('threshold', 'overflow')),
2989
+ summary TEXT NOT NULL CHECK (length(trim(summary)) > 0),
2990
+ summary_format_version INTEGER NOT NULL CHECK (summary_format_version = 1),
2991
+ summarized_through_seq INTEGER NOT NULL CHECK (summarized_through_seq >= 1),
2992
+ observed_head_seq INTEGER NOT NULL,
2993
+ estimated_input_tokens_before INTEGER NOT NULL CHECK (estimated_input_tokens_before >= 0),
2994
+ estimated_input_tokens_after INTEGER NOT NULL CHECK (estimated_input_tokens_after >= 0),
2995
+ summary_model_id TEXT NOT NULL CHECK (length(trim(summary_model_id)) > 0),
2996
+ created_at TEXT NOT NULL,
2997
+ PRIMARY KEY (project_id, id),
2998
+ UNIQUE (project_id, created_by_run_id),
2999
+ FOREIGN KEY (project_id, thread_id)
3000
+ REFERENCES agent_threads(project_id, id) ON DELETE CASCADE,
3001
+ FOREIGN KEY (project_id, created_by_run_id)
3002
+ REFERENCES agent_runs(project_id, id) ON DELETE RESTRICT,
3003
+ FOREIGN KEY (project_id, previous_checkpoint_id)
3004
+ REFERENCES agent_context_checkpoints(project_id, id) ON DELETE RESTRICT,
3005
+ CHECK (summarized_through_seq < observed_head_seq)
3006
+ );
3007
+
3008
+ CREATE INDEX idx_agent_context_checkpoints_thread_latest
3009
+ ON agent_context_checkpoints(project_id, thread_id, summarized_through_seq DESC, created_at DESC, id DESC);
3010
+ `;
3011
+
3012
+ // src/migrations/028-object-override-edit-times.sql
3013
+ var _028_object_override_edit_times_default = `ALTER TABLE ontology_object_overrides
3014
+ ADD COLUMN edited_at TEXT NOT NULL DEFAULT '{}'
3015
+ CHECK (json_valid(edited_at) AND json_type(edited_at) = 'object');
3016
+ `;
3017
+
2921
3018
  // src/migrations.ts
2922
3019
  var MIGRATIONS_TABLE_SQL = `
2923
3020
  CREATE TABLE IF NOT EXISTS sixb_migrations (
@@ -2977,7 +3074,10 @@ var sqliteStorageMigrations = defineMigrations({
2977
3074
  }
2978
3075
  db.run(_024_ontology_commit_executions_default);
2979
3076
  }, { checksum: checksum(_024_ontology_commit_executions_default) }),
2980
- sqliteSql("025-connector-connections", _025_connector_connections_default)
3077
+ sqliteSql("025-connector-connections", _025_connector_connections_default),
3078
+ sqliteSql("026-ai-cost-accounting", _026_ai_cost_accounting_default),
3079
+ sqliteSql("027-agent-context-checkpoints", _027_agent_context_checkpoints_default),
3080
+ sqliteSql("028-object-override-edit-times", _028_object_override_edit_times_default)
2981
3081
  ]
2982
3082
  });
2983
3083
  function sqliteStoragePath(basePath) {
@@ -3628,17 +3728,18 @@ function isUniqueConstraintError(error) {
3628
3728
  return error instanceof Error && error.message.includes("UNIQUE constraint failed");
3629
3729
  }
3630
3730
 
3631
- // src/agents/messages.ts
3632
- import { AGENT_MESSAGE_CONTENT_VERSION } from "@sixb/core/internal/agents";
3731
+ // src/agents/checkpoints.ts
3732
+ import {
3733
+ agentContextCheckpointMatchesCreateInput,
3734
+ assertAgentContextCheckpointAnchors,
3735
+ assertAgentContextCheckpointAuthority,
3736
+ assertAgentContextCheckpointReplayState,
3737
+ assertCreateAgentContextCheckpointInput
3738
+ } from "@sixb/core/internal/agent-run-storage-provider";
3633
3739
  import {
3634
3740
  AgentStorageError
3635
3741
  } from "@sixb/core/storage";
3636
3742
 
3637
- // src/storage-errors.ts
3638
- function isUniqueConstraintError2(error) {
3639
- return error instanceof Error && error.message.includes("UNIQUE constraint failed");
3640
- }
3641
-
3642
3743
  // src/agents/rows.ts
3643
3744
  import { parseSixbFailure } from "@sixb/core/internal/errors";
3644
3745
  import {
@@ -3700,6 +3801,27 @@ function rowToMessageRecord(row) {
3700
3801
  completedAt: row.completed_at ? new Date(row.completed_at) : undefined
3701
3802
  };
3702
3803
  }
3804
+ function rowToContextCheckpointRecord(row) {
3805
+ if (row.summary_format_version !== 1) {
3806
+ throw new Error(`[SixbSqlite] Unsupported agent context checkpoint summary format version '${row.summary_format_version}'.`);
3807
+ }
3808
+ return {
3809
+ id: row.id,
3810
+ projectId: row.project_id,
3811
+ threadId: row.thread_id,
3812
+ createdByRunId: row.created_by_run_id,
3813
+ previousCheckpointId: row.previous_checkpoint_id ?? undefined,
3814
+ reason: row.reason,
3815
+ summary: row.summary,
3816
+ summaryFormatVersion: 1,
3817
+ summarizedThroughSeq: row.summarized_through_seq,
3818
+ observedHeadSeq: row.observed_head_seq,
3819
+ estimatedInputTokensBefore: row.estimated_input_tokens_before,
3820
+ estimatedInputTokensAfter: row.estimated_input_tokens_after,
3821
+ summaryModelId: row.summary_model_id,
3822
+ createdAt: new Date(row.created_at)
3823
+ };
3824
+ }
3703
3825
  function principalFromColumns(type, id) {
3704
3826
  return type && id ? { type, id } : undefined;
3705
3827
  }
@@ -3721,6 +3843,124 @@ function queryAgentList(input) {
3721
3843
  return { rows, total, hasMore: offset + rows.length < total };
3722
3844
  }
3723
3845
 
3846
+ // src/agents/checkpoints.ts
3847
+ var SQLITE_CHECKPOINT_RUN_ID_BATCH_SIZE = 500;
3848
+
3849
+ class SqliteAgentContextCheckpointStore {
3850
+ db;
3851
+ constructor(db) {
3852
+ this.db = db;
3853
+ }
3854
+ async create(input) {
3855
+ assertCreateAgentContextCheckpointInput(input, "SixbSqlite");
3856
+ return this.db.transaction(() => {
3857
+ const runRow = this.db.query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?").get(input.projectId, input.createdByRunId);
3858
+ const threadRow = this.db.query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?").get(input.projectId, input.threadId);
3859
+ assertAgentContextCheckpointAuthority({
3860
+ create: input,
3861
+ run: runRow ? rowToRunRecord(runRow) : null,
3862
+ thread: threadRow ? rowToThreadRecord(threadRow) : null,
3863
+ prefix: "SixbSqlite"
3864
+ });
3865
+ const latest = this.findLatest(input.projectId, input.threadId);
3866
+ const headRow = this.db.query("SELECT COALESCE(MAX(seq), 0) AS seq FROM agent_messages WHERE project_id = ? AND thread_id = ?").get(input.projectId, input.threadId);
3867
+ const existingForRun = this.db.query("SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND created_by_run_id = ?").get(input.projectId, input.createdByRunId);
3868
+ if (existingForRun) {
3869
+ const record = rowToContextCheckpointRecord(existingForRun);
3870
+ if (agentContextCheckpointMatchesCreateInput(record, input)) {
3871
+ assertAgentContextCheckpointReplayState({
3872
+ create: input,
3873
+ existing: record,
3874
+ latest,
3875
+ headSeq: headRow.seq,
3876
+ prefix: "SixbSqlite"
3877
+ });
3878
+ return record;
3879
+ }
3880
+ throw new AgentStorageError("invalid_state", `[SixbSqlite] Agent run '${input.createdByRunId}' already created a different context checkpoint.`);
3881
+ }
3882
+ const duplicateId = this.db.query("SELECT 1 FROM agent_context_checkpoints WHERE project_id = ? AND id = ?").get(input.projectId, input.id);
3883
+ if (duplicateId) {
3884
+ throw new AgentStorageError("duplicate_id", `[SixbSqlite] Agent context checkpoint '${input.id}' already exists for project '${input.projectId}'.`);
3885
+ }
3886
+ const firstRetainedRow = this.db.query("SELECT * FROM agent_messages WHERE project_id = ? AND thread_id = ? AND seq > ? ORDER BY seq ASC LIMIT 1").get(input.projectId, input.threadId, input.summarizedThroughSeq);
3887
+ assertAgentContextCheckpointAnchors({
3888
+ create: input,
3889
+ latest,
3890
+ headSeq: headRow.seq,
3891
+ firstRetained: firstRetainedRow ? rowToMessageRecord(firstRetainedRow) : null,
3892
+ prefix: "SixbSqlite"
3893
+ });
3894
+ const createdAt = input.createdAt ?? new Date;
3895
+ this.db.query(`
3896
+ INSERT INTO agent_context_checkpoints (
3897
+ project_id,
3898
+ id,
3899
+ thread_id,
3900
+ created_by_run_id,
3901
+ previous_checkpoint_id,
3902
+ reason,
3903
+ summary,
3904
+ summary_format_version,
3905
+ summarized_through_seq,
3906
+ observed_head_seq,
3907
+ estimated_input_tokens_before,
3908
+ estimated_input_tokens_after,
3909
+ summary_model_id,
3910
+ created_at
3911
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3912
+ `).run(input.projectId, input.id, input.threadId, input.createdByRunId, input.expectedPreviousCheckpointId, input.reason, input.summary, input.summaryFormatVersion, input.summarizedThroughSeq, input.observedHeadSeq, input.estimatedInputTokensBefore, input.estimatedInputTokensAfter, input.summaryModelId, createdAt.toISOString());
3913
+ return this.requireById(input.projectId, input.id);
3914
+ })();
3915
+ }
3916
+ async getLatest(input) {
3917
+ return this.findLatest(input.projectId, input.threadId);
3918
+ }
3919
+ async getByRunIds(input) {
3920
+ const runIds = [...new Set(input.runIds)];
3921
+ if (runIds.length === 0)
3922
+ return [];
3923
+ const rows = [];
3924
+ for (let offset = 0;offset < runIds.length; offset += SQLITE_CHECKPOINT_RUN_ID_BATCH_SIZE) {
3925
+ const batch = runIds.slice(offset, offset + SQLITE_CHECKPOINT_RUN_ID_BATCH_SIZE);
3926
+ const placeholders = batch.map(() => "?").join(", ");
3927
+ rows.push(...this.db.query(`SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND created_by_run_id IN (${placeholders})`).all(input.projectId, ...batch));
3928
+ }
3929
+ const byRunId = new Map(rows.map((row) => [row.created_by_run_id, rowToContextCheckpointRecord(row)]));
3930
+ return runIds.flatMap((runId) => {
3931
+ const checkpoint = byRunId.get(runId);
3932
+ return checkpoint ? [checkpoint] : [];
3933
+ });
3934
+ }
3935
+ findLatest(projectId, threadId) {
3936
+ const row = this.db.query(`
3937
+ SELECT * FROM agent_context_checkpoints
3938
+ WHERE project_id = ? AND thread_id = ?
3939
+ ORDER BY summarized_through_seq DESC, created_at DESC, id DESC
3940
+ LIMIT 1
3941
+ `).get(projectId, threadId);
3942
+ return row ? rowToContextCheckpointRecord(row) : null;
3943
+ }
3944
+ requireById(projectId, id) {
3945
+ const row = this.db.query("SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND id = ?").get(projectId, id);
3946
+ if (!row) {
3947
+ throw new AgentStorageError("invalid_state", `[SixbSqlite] Failed to load agent context checkpoint '${id}' for project '${projectId}'.`);
3948
+ }
3949
+ return rowToContextCheckpointRecord(row);
3950
+ }
3951
+ }
3952
+
3953
+ // src/agents/messages.ts
3954
+ import { AGENT_MESSAGE_CONTENT_VERSION } from "@sixb/core/internal/agents";
3955
+ import {
3956
+ AgentStorageError as AgentStorageError2
3957
+ } from "@sixb/core/storage";
3958
+
3959
+ // src/storage-errors.ts
3960
+ function isUniqueConstraintError2(error) {
3961
+ return error instanceof Error && error.message.includes("UNIQUE constraint failed");
3962
+ }
3963
+
3724
3964
  // src/agents/messages.ts
3725
3965
  class SqliteAgentMessageStore {
3726
3966
  db;
@@ -3732,12 +3972,12 @@ class SqliteAgentMessageStore {
3732
3972
  return this.db.transaction(() => {
3733
3973
  const thread = this.db.query("SELECT 1 AS present FROM agent_threads WHERE project_id = ? AND id = ?").get(input.projectId, input.threadId);
3734
3974
  if (!thread) {
3735
- throw new AgentStorageError("thread_not_found", `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
3975
+ throw new AgentStorageError2("thread_not_found", `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
3736
3976
  }
3737
3977
  if (input.runId !== null) {
3738
3978
  const run = this.db.query("SELECT 1 AS present FROM agent_runs WHERE project_id = ? AND id = ?").get(input.projectId, input.runId);
3739
3979
  if (!run) {
3740
- throw new AgentStorageError("run_not_found", `[SixbSqlite] Agent run '${input.runId}' not found for project '${input.projectId}'.`);
3980
+ throw new AgentStorageError2("run_not_found", `[SixbSqlite] Agent run '${input.runId}' not found for project '${input.projectId}'.`);
3741
3981
  }
3742
3982
  }
3743
3983
  const seqRow = this.db.query("SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM agent_messages WHERE project_id = ? AND thread_id = ?").get(input.projectId, input.threadId);
@@ -3761,7 +4001,7 @@ class SqliteAgentMessageStore {
3761
4001
  `).run(input.projectId, input.id, input.threadId, input.runId, input.role, input.authorPrincipal?.type ?? null, input.authorPrincipal?.id ?? null, seqRow.next, JSON.stringify(input.parts), input.metadata === undefined ? null : JSON.stringify(input.metadata), AGENT_MESSAGE_CONTENT_VERSION, createdAt.toISOString(), input.completedAt ? input.completedAt.toISOString() : null);
3762
4002
  } catch (error) {
3763
4003
  if (isUniqueConstraintError2(error)) {
3764
- throw new AgentStorageError("duplicate_id", `[SixbSqlite] Agent message '${input.id}' already exists for project '${input.projectId}'.`);
4004
+ throw new AgentStorageError2("duplicate_id", `[SixbSqlite] Agent message '${input.id}' already exists for project '${input.projectId}'.`);
3765
4005
  }
3766
4006
  throw error;
3767
4007
  }
@@ -3773,6 +4013,35 @@ class SqliteAgentMessageStore {
3773
4013
  return this.requireMessage(input.projectId, input.id);
3774
4014
  })();
3775
4015
  }
4016
+ async deleteByRunId(input) {
4017
+ return this.db.transaction(() => {
4018
+ const thread = this.db.query("SELECT 1 AS present FROM agent_threads WHERE project_id = ? AND id = ?").get(input.projectId, input.threadId);
4019
+ if (!thread) {
4020
+ throw new AgentStorageError2("thread_not_found", `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
4021
+ }
4022
+ const deleted = this.db.query("DELETE FROM agent_messages WHERE project_id = ? AND thread_id = ? AND run_id = ?").run(input.projectId, input.threadId, input.runId);
4023
+ if (deleted.changes === 0) {
4024
+ return 0;
4025
+ }
4026
+ const updatedAt = new Date().toISOString();
4027
+ this.db.query(`
4028
+ UPDATE agent_threads
4029
+ SET
4030
+ message_count = (
4031
+ SELECT COUNT(*) FROM agent_messages WHERE project_id = ? AND thread_id = ?
4032
+ ),
4033
+ last_message_at = (
4034
+ SELECT created_at FROM agent_messages
4035
+ WHERE project_id = ? AND thread_id = ?
4036
+ ORDER BY seq DESC
4037
+ LIMIT 1
4038
+ ),
4039
+ updated_at = ?
4040
+ WHERE project_id = ? AND id = ?
4041
+ `).run(input.projectId, input.threadId, input.projectId, input.threadId, updatedAt, input.projectId, input.threadId);
4042
+ return deleted.changes;
4043
+ })();
4044
+ }
3776
4045
  async getById(params) {
3777
4046
  const row = this.db.query("SELECT * FROM agent_messages WHERE project_id = ? AND id = ?").get(params.projectId, params.id);
3778
4047
  return row ? rowToMessageRecord(row) : null;
@@ -3783,6 +4052,10 @@ class SqliteAgentMessageStore {
3783
4052
  }
3784
4053
  const whereClauses = ["project_id = ?", "thread_id = ?"];
3785
4054
  const args = [input.projectId, input.threadId];
4055
+ if (input.afterSeq !== undefined) {
4056
+ whereClauses.push("seq > ?");
4057
+ args.push(input.afterSeq);
4058
+ }
3786
4059
  if (input.roles) {
3787
4060
  whereClauses.push(`role IN (${input.roles.map(() => "?").join(", ")})`);
3788
4061
  args.push(...input.roles);
@@ -3802,7 +4075,7 @@ class SqliteAgentMessageStore {
3802
4075
  requireMessage(projectId, id) {
3803
4076
  const row = this.db.query("SELECT * FROM agent_messages WHERE project_id = ? AND id = ?").get(projectId, id);
3804
4077
  if (!row) {
3805
- throw new AgentStorageError("invalid_state", `[SixbSqlite] Failed to load agent message '${id}' for project '${projectId}'.`);
4078
+ throw new AgentStorageError2("invalid_state", `[SixbSqlite] Failed to load agent message '${id}' for project '${projectId}'.`);
3806
4079
  }
3807
4080
  return rowToMessageRecord(row);
3808
4081
  }
@@ -3814,7 +4087,7 @@ import { normalizeRequesterGroupIds } from "@sixb/core/internal/auth";
3814
4087
  import { serializeSixbFailure } from "@sixb/core/internal/errors";
3815
4088
  import {
3816
4089
  AGENT_RUN_FAILURE_CODES as AGENT_RUN_FAILURE_CODES2,
3817
- AgentStorageError as AgentStorageError2
4090
+ AgentStorageError as AgentStorageError3
3818
4091
  } from "@sixb/core/storage";
3819
4092
 
3820
4093
  // src/run-list-query.ts
@@ -3890,10 +4163,10 @@ class SqliteAgentRunStore {
3890
4163
  return this.db.transaction(() => {
3891
4164
  const thread = this.db.query("SELECT active_run_id FROM agent_threads WHERE project_id = ? AND id = ?").get(input.projectId, input.threadId);
3892
4165
  if (!thread) {
3893
- throw new AgentStorageError2("thread_not_found", `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
4166
+ throw new AgentStorageError3("thread_not_found", `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
3894
4167
  }
3895
4168
  if (thread.active_run_id !== null) {
3896
- throw new AgentStorageError2("active_run_exists", `[SixbSqlite] Agent thread '${input.threadId}' already has an active run '${thread.active_run_id}'.`);
4169
+ throw new AgentStorageError3("active_run_exists", `[SixbSqlite] Agent thread '${input.threadId}' already has an active run '${thread.active_run_id}'.`);
3897
4170
  }
3898
4171
  try {
3899
4172
  this.db.query(`
@@ -3912,7 +4185,7 @@ class SqliteAgentRunStore {
3912
4185
  `).run(input.projectId, input.id, input.executionId, input.threadId, input.agentId, input.triggerMessageId, JSON.stringify(normalizeRequesterGroupIds(input.requesterGroupIds)), createdAt.toISOString());
3913
4186
  } catch (error) {
3914
4187
  if (isUniqueConstraintError2(error)) {
3915
- throw new AgentStorageError2("duplicate_id", `[SixbSqlite] Agent run '${input.id}' already exists for project '${input.projectId}'.`);
4188
+ throw new AgentStorageError3("duplicate_id", `[SixbSqlite] Agent run '${input.id}' already exists for project '${input.projectId}'.`);
3916
4189
  }
3917
4190
  throw error;
3918
4191
  }
@@ -3969,7 +4242,7 @@ class SqliteAgentRunStore {
3969
4242
  return this.db.transaction(() => {
3970
4243
  const run = this.requireRunning(input.projectId, input.id);
3971
4244
  if (run.execution_token !== input.executionToken) {
3972
- throw new AgentStorageError2("execution_lost", `[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`);
4245
+ throw new AgentStorageError3("execution_lost", `[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`);
3973
4246
  }
3974
4247
  this.db.query(`
3975
4248
  UPDATE agent_runs
@@ -3985,7 +4258,7 @@ class SqliteAgentRunStore {
3985
4258
  return this.db.transaction(() => {
3986
4259
  const run = this.requireRunning(input.projectId, input.id);
3987
4260
  if (run.execution_token !== input.executionToken) {
3988
- throw new AgentStorageError2("execution_lost", `[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`);
4261
+ throw new AgentStorageError3("execution_lost", `[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`);
3989
4262
  }
3990
4263
  this.db.query(`
3991
4264
  UPDATE agent_runs
@@ -4057,10 +4330,10 @@ class SqliteAgentRunStore {
4057
4330
  requireStatus(projectId, id, status) {
4058
4331
  const row = this.db.query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?").get(projectId, id);
4059
4332
  if (!row) {
4060
- throw new AgentStorageError2("run_not_found", `[SixbSqlite] Agent run '${id}' not found for project '${projectId}'.`);
4333
+ throw new AgentStorageError3("run_not_found", `[SixbSqlite] Agent run '${id}' not found for project '${projectId}'.`);
4061
4334
  }
4062
4335
  if (row.status !== status) {
4063
- throw new AgentStorageError2("invalid_state", `[SixbSqlite] Agent run '${id}' is not ${status} (status '${row.status}').`);
4336
+ throw new AgentStorageError3("invalid_state", `[SixbSqlite] Agent run '${id}' is not ${status} (status '${row.status}').`);
4064
4337
  }
4065
4338
  return row;
4066
4339
  }
@@ -4074,7 +4347,7 @@ class SqliteAgentRunStore {
4074
4347
  requireRun(projectId, id) {
4075
4348
  const row = this.db.query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?").get(projectId, id);
4076
4349
  if (!row) {
4077
- throw new AgentStorageError2("run_not_found", `[SixbSqlite] Agent run '${id}' not found for project '${projectId}'.`);
4350
+ throw new AgentStorageError3("run_not_found", `[SixbSqlite] Agent run '${id}' not found for project '${projectId}'.`);
4078
4351
  }
4079
4352
  return rowToRunRecord(row);
4080
4353
  }
@@ -4082,7 +4355,7 @@ class SqliteAgentRunStore {
4082
4355
 
4083
4356
  // src/agents/threads.ts
4084
4357
  import {
4085
- AgentStorageError as AgentStorageError3
4358
+ AgentStorageError as AgentStorageError4
4086
4359
  } from "@sixb/core/storage";
4087
4360
  class SqliteAgentThreadStore {
4088
4361
  db;
@@ -4111,7 +4384,7 @@ class SqliteAgentThreadStore {
4111
4384
  `).run(input.projectId, input.id, input.agentId, input.ownerPrincipal.type, input.ownerPrincipal.id, input.title ?? null, input.status ?? "active", createdAt.toISOString(), updatedAt.toISOString());
4112
4385
  } catch (error) {
4113
4386
  if (isUniqueConstraintError2(error)) {
4114
- throw new AgentStorageError3("duplicate_id", `[SixbSqlite] Agent thread '${input.id}' already exists for project '${input.projectId}'.`);
4387
+ throw new AgentStorageError4("duplicate_id", `[SixbSqlite] Agent thread '${input.id}' already exists for project '${input.projectId}'.`);
4115
4388
  }
4116
4389
  throw error;
4117
4390
  }
@@ -4158,7 +4431,7 @@ class SqliteAgentThreadStore {
4158
4431
  requireThread(projectId, id) {
4159
4432
  const row = this.db.query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?").get(projectId, id);
4160
4433
  if (!row) {
4161
- throw new AgentStorageError3("invalid_state", `[SixbSqlite] Failed to load agent thread '${id}' for project '${projectId}'.`);
4434
+ throw new AgentStorageError4("invalid_state", `[SixbSqlite] Failed to load agent thread '${id}' for project '${projectId}'.`);
4162
4435
  }
4163
4436
  return rowToThreadRecord(row);
4164
4437
  }
@@ -4169,6 +4442,7 @@ class SqliteAgentStorage {
4169
4442
  threads;
4170
4443
  runs;
4171
4444
  messages;
4445
+ checkpoints;
4172
4446
  connection;
4173
4447
  constructor(options) {
4174
4448
  this.connection = openSqliteStoreConnection(options);
@@ -4178,11 +4452,497 @@ class SqliteAgentStorage {
4178
4452
  this.threads = new SqliteAgentThreadStore(this.connection.db);
4179
4453
  this.runs = new SqliteAgentRunStore(this.connection.db, options.executions);
4180
4454
  this.messages = new SqliteAgentMessageStore(this.connection.db);
4455
+ this.checkpoints = new SqliteAgentContextCheckpointStore(this.connection.db);
4181
4456
  }
4182
4457
  close() {
4183
4458
  closeSqliteStoreConnection(this.connection);
4184
4459
  }
4185
4460
  }
4461
+ // src/ai-cost-storage.ts
4462
+ import {
4463
+ aiModelCallCostDetails,
4464
+ aiModelCallCostMatchesUsage,
4465
+ buildAiAccountingOverviewFromFragments,
4466
+ normalizeAiAccountingQuery,
4467
+ normalizeAiModelCallAccountingQuery,
4468
+ normalizeAiModelCallCostRecord,
4469
+ parseAiModelCallCostDetails,
4470
+ toAccountingItem
4471
+ } from "@sixb/core/internal/ai-cost-storage-provider";
4472
+ import {
4473
+ AiCostStorageError
4474
+ } from "@sixb/core/storage";
4475
+ class SqliteAiCostStorage {
4476
+ connection;
4477
+ constructor(connection) {
4478
+ this.connection = connection;
4479
+ }
4480
+ async recordModelCallCost(input) {
4481
+ const record = normalizeAiModelCallCostRecord(input);
4482
+ runImmediateTransaction(this.connection.db, () => {
4483
+ const usage = this.findUsage(record.projectId, record.usageRecordId);
4484
+ if (!usage)
4485
+ throw missingUsage(record);
4486
+ assertCostMatchesUsage(record, usage);
4487
+ if (this.findCost(record.projectId, record.usageRecordId))
4488
+ return;
4489
+ try {
4490
+ this.insertCost(record);
4491
+ } catch (error) {
4492
+ if (!isUniqueConstraintError2(error))
4493
+ throw error;
4494
+ if (!this.findCost(record.projectId, record.usageRecordId))
4495
+ throw error;
4496
+ }
4497
+ });
4498
+ }
4499
+ async summarizeExecutions(input) {
4500
+ assertNonBlank(input.projectId, "cost summary projectId");
4501
+ for (const id of input.executionIds)
4502
+ assertNonBlank(id, "cost summary executionId");
4503
+ if (input.executionIds.length === 0)
4504
+ return [];
4505
+ const rows = this.connection.db.query(`
4506
+ WITH requested AS (SELECT DISTINCT value AS execution_id FROM json_each(?))
4507
+ SELECT usage.execution_id,
4508
+ cost.status,
4509
+ cost.currency,
4510
+ CAST(cost.amount_nanos AS TEXT) AS amount_nanos
4511
+ FROM requested
4512
+ JOIN ai_model_call_usage AS usage
4513
+ ON usage.project_id = ? AND usage.execution_id = requested.execution_id
4514
+ LEFT JOIN ai_model_call_valuations AS cost
4515
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
4516
+ `).all(JSON.stringify([...new Set(input.executionIds)]), input.projectId);
4517
+ return aggregateSummaryRows(input.executionIds, rows);
4518
+ }
4519
+ async queryProjectOverview(input) {
4520
+ const query = normalizeAiAccountingQuery(input);
4521
+ const rows = this.connection.db.query(SQLITE_ACCOUNTING_OVERVIEW_SQL).all(query.projectId, query.from.toISOString(), query.to.toISOString(), query.providerId ?? null, query.providerId ?? null, query.modelId ?? null, query.modelId ?? null, input.bucket);
4522
+ return buildAiAccountingOverviewFromFragments(input, rows.map(aggregateFragmentFromRow));
4523
+ }
4524
+ async listModelCalls(input) {
4525
+ const query = normalizeAiModelCallAccountingQuery(input);
4526
+ const parameters = accountingListFilterParameters(query);
4527
+ const count = this.connection.db.query(SQLITE_ACCOUNTING_LIST_COUNT_SQL).get(...parameters);
4528
+ const rows = this.connection.db.query(SQLITE_ACCOUNTING_LIST_PAGE_SQL).all(...parameters, query.limit, query.offset);
4529
+ const items = rows.map((row) => toAccountingItem(accountingItemFromRow(row)));
4530
+ const total = safeSqlCount(count.total, "model-call list total");
4531
+ return structuredClone({
4532
+ items,
4533
+ total,
4534
+ hasMore: query.offset + items.length < total
4535
+ });
4536
+ }
4537
+ insertCost(record) {
4538
+ const rated = record.status === "rated" ? record : undefined;
4539
+ const details = aiModelCallCostDetails(record);
4540
+ this.connection.db.query(`INSERT INTO ai_model_call_valuations (
4541
+ project_id, usage_record_id, status, provider_id, model_id,
4542
+ currency, amount_nanos, reason, details, rated_at
4543
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(record.projectId, record.usageRecordId, record.status, record.billingIdentity?.providerId ?? null, record.billingIdentity?.modelId ?? null, rated?.money.currency ?? null, rated ? BigInt(rated.money.amountNanos) : null, record.status === "unpriceable" ? record.reason : null, JSON.stringify(details), record.ratedAt.toISOString());
4544
+ }
4545
+ findCost(projectId, usageId) {
4546
+ const row = this.connection.db.query(`SELECT *, CAST(amount_nanos AS TEXT) AS amount_nanos
4547
+ FROM ai_model_call_valuations
4548
+ WHERE project_id = ? AND usage_record_id = ?`).get(projectId, usageId);
4549
+ return row ? rowToCost(row) : null;
4550
+ }
4551
+ findUsage(projectId, id) {
4552
+ const row = this.connection.db.query("SELECT * FROM ai_model_call_usage WHERE project_id = ? AND id = ?").get(projectId, id);
4553
+ return row ? usageFromRow(row, []) : null;
4554
+ }
4555
+ }
4556
+ var SQLITE_ACCOUNTING_LIST_FILTER = `
4557
+ usage.project_id = ? AND usage.occurred_at >= ? AND usage.occurred_at < ?
4558
+ AND (? IS NULL OR usage.provider_id = ?)
4559
+ AND (? IS NULL OR usage.requested_model_id = ?)
4560
+ AND (? IS NULL OR usage.execution_id = ?)
4561
+ AND (
4562
+ ? IS NULL OR ? = CASE
4563
+ WHEN cost.status = 'rated' THEN 'rated'
4564
+ WHEN cost.status = 'unpriceable' THEN 'unpriceable'
4565
+ ELSE 'unvalued'
4566
+ END
4567
+ )
4568
+ `;
4569
+ var SQLITE_ACCOUNTING_LIST_COUNT_SQL = `
4570
+ SELECT CAST(COUNT(*) AS TEXT) AS total
4571
+ FROM ai_model_call_usage AS usage
4572
+ LEFT JOIN ai_model_call_valuations AS cost
4573
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
4574
+ WHERE ${SQLITE_ACCOUNTING_LIST_FILTER}
4575
+ `;
4576
+ var SQLITE_ACCOUNTING_LIST_PAGE_SQL = `
4577
+ SELECT usage.*,
4578
+ CASE
4579
+ WHEN direct_agent.id IS NOT NULL THEN 'agent'
4580
+ WHEN workflow_agent.node_run_id IS NOT NULL THEN 'workflowAgent'
4581
+ ELSE NULL
4582
+ END AS attribution_kind,
4583
+ COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
4584
+ direct_agent.id AS attribution_agent_run_id,
4585
+ direct_agent.thread_id AS attribution_thread_id,
4586
+ workflow_agent.node_run_id AS attribution_node_run_id,
4587
+ workflow_node.workflow_id AS attribution_workflow_id,
4588
+ workflow_node.workflow_run_id AS attribution_workflow_run_id,
4589
+ COALESCE((
4590
+ SELECT json_group_array(group_id) FROM (
4591
+ SELECT group_id FROM ai_model_call_usage_groups
4592
+ WHERE project_id = usage.project_id AND usage_record_id = usage.id
4593
+ ORDER BY group_id
4594
+ )
4595
+ ), '[]') AS requester_group_ids,
4596
+ cost.status AS cost_status,
4597
+ cost.provider_id AS cost_provider_id,
4598
+ cost.model_id AS cost_model_id,
4599
+ cost.currency AS cost_currency,
4600
+ CAST(cost.amount_nanos AS TEXT) AS cost_amount_nanos,
4601
+ cost.reason AS cost_reason,
4602
+ cost.details AS cost_details,
4603
+ cost.rated_at AS cost_rated_at
4604
+ FROM ai_model_call_usage AS usage
4605
+ LEFT JOIN agent_runs AS direct_agent
4606
+ ON direct_agent.project_id = usage.project_id
4607
+ AND direct_agent.execution_id = usage.execution_id
4608
+ LEFT JOIN workflow_agent_node_runs AS workflow_agent
4609
+ ON workflow_agent.project_id = usage.project_id
4610
+ AND workflow_agent.execution_id = usage.execution_id
4611
+ LEFT JOIN workflow_node_runs AS workflow_node
4612
+ ON workflow_node.project_id = workflow_agent.project_id
4613
+ AND workflow_node.id = workflow_agent.node_run_id
4614
+ LEFT JOIN ai_model_call_valuations AS cost
4615
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
4616
+ WHERE ${SQLITE_ACCOUNTING_LIST_FILTER}
4617
+ ORDER BY usage.occurred_at DESC, usage.id ASC
4618
+ LIMIT ? OFFSET ?
4619
+ `;
4620
+ var SQLITE_ACCOUNTING_OVERVIEW_SQL = `
4621
+ WITH classified AS (
4622
+ SELECT usage.provider_id,
4623
+ usage.requested_model_id,
4624
+ usage.occurred_at,
4625
+ usage.input_tokens,
4626
+ usage.output_tokens,
4627
+ usage.uncached_input_tokens,
4628
+ usage.cache_read_input_tokens,
4629
+ usage.cache_write_input_tokens,
4630
+ usage.text_output_tokens,
4631
+ usage.reasoning_output_tokens,
4632
+ usage.reporting_status,
4633
+ COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
4634
+ workflow_node.workflow_id AS attribution_workflow_id,
4635
+ CASE
4636
+ WHEN cost.status = 'rated' THEN 'rated'
4637
+ WHEN cost.status = 'unpriceable' THEN 'unpriceable'
4638
+ ELSE 'unvalued'
4639
+ END AS valuation_status,
4640
+ CASE WHEN cost.status = 'rated' THEN cost.currency ELSE NULL END AS amount_currency,
4641
+ CASE WHEN cost.status = 'rated' THEN cost.amount_nanos ELSE NULL END AS amount_nanos
4642
+ FROM ai_model_call_usage AS usage
4643
+ LEFT JOIN agent_runs AS direct_agent
4644
+ ON direct_agent.project_id = usage.project_id
4645
+ AND direct_agent.execution_id = usage.execution_id
4646
+ LEFT JOIN workflow_agent_node_runs AS workflow_agent
4647
+ ON workflow_agent.project_id = usage.project_id
4648
+ AND workflow_agent.execution_id = usage.execution_id
4649
+ LEFT JOIN workflow_node_runs AS workflow_node
4650
+ ON workflow_node.project_id = workflow_agent.project_id
4651
+ AND workflow_node.id = workflow_agent.node_run_id
4652
+ LEFT JOIN ai_model_call_valuations AS cost
4653
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
4654
+ WHERE usage.project_id = ? AND usage.occurred_at >= ? AND usage.occurred_at < ?
4655
+ AND (? IS NULL OR usage.provider_id = ?)
4656
+ AND (? IS NULL OR usage.requested_model_id = ?)
4657
+ ), expanded AS (
4658
+ SELECT 'totals' AS dimension, NULL AS key_1, NULL AS key_2, NULL AS bucket_start,
4659
+ classified.*
4660
+ FROM classified
4661
+ UNION ALL
4662
+ SELECT 'series', NULL, NULL,
4663
+ CASE ?
4664
+ WHEN 'hour' THEN strftime('%Y-%m-%dT%H:00:00.000Z', occurred_at)
4665
+ WHEN 'day' THEN strftime('%Y-%m-%dT00:00:00.000Z', occurred_at)
4666
+ ELSE strftime(
4667
+ '%Y-%m-%dT00:00:00.000Z',
4668
+ occurred_at,
4669
+ printf('-%d days', (CAST(strftime('%w', occurred_at) AS INTEGER) + 6) % 7)
4670
+ )
4671
+ END,
4672
+ classified.*
4673
+ FROM classified
4674
+ UNION ALL
4675
+ SELECT 'model', provider_id, requested_model_id, NULL, classified.* FROM classified
4676
+ UNION ALL
4677
+ SELECT 'agent', attribution_agent_id, NULL, NULL, classified.*
4678
+ FROM classified WHERE attribution_agent_id IS NOT NULL
4679
+ UNION ALL
4680
+ SELECT 'workflow', attribution_workflow_id, NULL, NULL, classified.*
4681
+ FROM classified WHERE attribution_workflow_id IS NOT NULL
4682
+ )
4683
+ SELECT dimension,
4684
+ key_1,
4685
+ key_2,
4686
+ bucket_start,
4687
+ amount_currency,
4688
+ CAST(COUNT(*) AS TEXT) AS model_call_count,
4689
+ CAST(SUM(CASE WHEN reporting_status <> 'unavailable' THEN 1 ELSE 0 END) AS TEXT)
4690
+ AS reported_usage_call_count,
4691
+ CAST(COUNT(input_tokens) AS TEXT) AS input_tokens_count,
4692
+ CAST(SUM(input_tokens) AS TEXT) AS input_tokens_sum,
4693
+ CAST(COUNT(output_tokens) AS TEXT) AS output_tokens_count,
4694
+ CAST(SUM(output_tokens) AS TEXT) AS output_tokens_sum,
4695
+ CAST(COUNT(uncached_input_tokens) AS TEXT) AS uncached_input_tokens_count,
4696
+ CAST(SUM(uncached_input_tokens) AS TEXT) AS uncached_input_tokens_sum,
4697
+ CAST(COUNT(cache_read_input_tokens) AS TEXT) AS cache_read_input_tokens_count,
4698
+ CAST(SUM(cache_read_input_tokens) AS TEXT) AS cache_read_input_tokens_sum,
4699
+ CAST(COUNT(cache_write_input_tokens) AS TEXT) AS cache_write_input_tokens_count,
4700
+ CAST(SUM(cache_write_input_tokens) AS TEXT) AS cache_write_input_tokens_sum,
4701
+ CAST(COUNT(text_output_tokens) AS TEXT) AS text_output_tokens_count,
4702
+ CAST(SUM(text_output_tokens) AS TEXT) AS text_output_tokens_sum,
4703
+ CAST(COUNT(reasoning_output_tokens) AS TEXT) AS reasoning_output_tokens_count,
4704
+ CAST(SUM(reasoning_output_tokens) AS TEXT) AS reasoning_output_tokens_sum,
4705
+ CAST(SUM(CASE WHEN valuation_status = 'rated' THEN 1 ELSE 0 END) AS TEXT)
4706
+ AS rated_call_count,
4707
+ CAST(SUM(CASE WHEN valuation_status = 'unpriceable' THEN 1 ELSE 0 END) AS TEXT)
4708
+ AS unpriceable_call_count,
4709
+ CAST(SUM(CASE WHEN valuation_status = 'unvalued' THEN 1 ELSE 0 END) AS TEXT)
4710
+ AS unvalued_call_count,
4711
+ CAST(SUM(amount_nanos / 1000000000000) AS TEXT) AS amount_high,
4712
+ CAST(SUM((amount_nanos / 1000000) % 1000000) AS TEXT) AS amount_middle,
4713
+ CAST(SUM(amount_nanos % 1000000) AS TEXT) AS amount_low
4714
+ FROM expanded
4715
+ GROUP BY dimension, key_1, key_2, bucket_start, amount_currency
4716
+ `;
4717
+ function accountingListFilterParameters(query) {
4718
+ return [
4719
+ query.projectId,
4720
+ query.from.toISOString(),
4721
+ query.to.toISOString(),
4722
+ query.providerId ?? null,
4723
+ query.providerId ?? null,
4724
+ query.modelId ?? null,
4725
+ query.modelId ?? null,
4726
+ query.executionId ?? null,
4727
+ query.executionId ?? null,
4728
+ query.valuationStatus ?? null,
4729
+ query.valuationStatus ?? null
4730
+ ];
4731
+ }
4732
+ function aggregateFragmentFromRow(row) {
4733
+ const dimensionKeys = row.dimension === "model" ? { providerId: required(row.key_1), modelId: required(row.key_2) } : row.dimension === "agent" ? { agentId: required(row.key_1) } : row.dimension === "workflow" ? { workflowId: required(row.key_1) } : row.dimension === "series" ? { start: new Date(required(row.bucket_start)) } : {};
4734
+ return {
4735
+ dimension: row.dimension,
4736
+ ...dimensionKeys,
4737
+ modelCallCount: row.model_call_count,
4738
+ usage: {
4739
+ reportedCallCount: row.reported_usage_call_count,
4740
+ inputTokens: meterFragment(row.input_tokens_count, row.input_tokens_sum),
4741
+ outputTokens: meterFragment(row.output_tokens_count, row.output_tokens_sum),
4742
+ uncachedInputTokens: meterFragment(row.uncached_input_tokens_count, row.uncached_input_tokens_sum),
4743
+ cacheReadInputTokens: meterFragment(row.cache_read_input_tokens_count, row.cache_read_input_tokens_sum),
4744
+ cacheWriteInputTokens: meterFragment(row.cache_write_input_tokens_count, row.cache_write_input_tokens_sum),
4745
+ textOutputTokens: meterFragment(row.text_output_tokens_count, row.text_output_tokens_sum),
4746
+ reasoningOutputTokens: meterFragment(row.reasoning_output_tokens_count, row.reasoning_output_tokens_sum)
4747
+ },
4748
+ costs: {
4749
+ ...row.amount_currency === null || row.amount_high === null || row.amount_middle === null || row.amount_low === null ? {} : {
4750
+ amount: {
4751
+ currency: row.amount_currency,
4752
+ amountNanos: sqliteAggregateAmount(row)
4753
+ }
4754
+ },
4755
+ ratedCallCount: row.rated_call_count,
4756
+ unpriceableCallCount: row.unpriceable_call_count,
4757
+ unvaluedCallCount: row.unvalued_call_count
4758
+ }
4759
+ };
4760
+ }
4761
+ function meterFragment(presentCallCount, total) {
4762
+ return { presentCallCount, total };
4763
+ }
4764
+ function sqliteAggregateAmount(row) {
4765
+ return (BigInt(required(row.amount_high)) * 1000000000000n + BigInt(required(row.amount_middle)) * 1000000n + BigInt(required(row.amount_low))).toString();
4766
+ }
4767
+ function accountingItemFromRow(row) {
4768
+ const usage = usageFromRow(row, parseStringArray(row.requester_group_ids));
4769
+ const cost = row.cost_status === null ? undefined : costFromJoinedRow(row);
4770
+ const attribution = row.attribution_kind === null ? undefined : row.attribution_kind === "agent" ? {
4771
+ kind: "agent",
4772
+ agentId: required(row.attribution_agent_id),
4773
+ agentRunId: required(row.attribution_agent_run_id),
4774
+ threadId: required(row.attribution_thread_id)
4775
+ } : {
4776
+ kind: "workflowAgent",
4777
+ agentId: required(row.attribution_agent_id),
4778
+ nodeRunId: required(row.attribution_node_run_id),
4779
+ workflowId: required(row.attribution_workflow_id),
4780
+ workflowRunId: required(row.attribution_workflow_run_id)
4781
+ };
4782
+ return {
4783
+ usage,
4784
+ ...attribution ? { attribution } : {},
4785
+ ...cost ? { cost } : {}
4786
+ };
4787
+ }
4788
+ function usageFromRow(row, requesterGroupIds) {
4789
+ return {
4790
+ id: row.id,
4791
+ projectId: row.project_id,
4792
+ executionId: row.execution_id,
4793
+ attempt: row.attempt,
4794
+ callId: row.call_id,
4795
+ requesterGroupIds,
4796
+ providerId: row.provider_id,
4797
+ requestedModelId: row.requested_model_id,
4798
+ ...row.response_model_id === null ? {} : { responseModelId: row.response_model_id },
4799
+ responseId: row.response_id,
4800
+ usage: {
4801
+ ...row.input_tokens === null ? {} : { inputTokens: row.input_tokens },
4802
+ ...row.output_tokens === null ? {} : { outputTokens: row.output_tokens },
4803
+ ...row.total_tokens === null ? {} : { totalTokens: row.total_tokens },
4804
+ ...row.uncached_input_tokens === null ? {} : { uncachedInputTokens: row.uncached_input_tokens },
4805
+ ...row.cache_read_input_tokens === null ? {} : { cacheReadInputTokens: row.cache_read_input_tokens },
4806
+ ...row.cache_write_input_tokens === null ? {} : { cacheWriteInputTokens: row.cache_write_input_tokens },
4807
+ ...row.text_output_tokens === null ? {} : { textOutputTokens: row.text_output_tokens },
4808
+ ...row.reasoning_output_tokens === null ? {} : { reasoningOutputTokens: row.reasoning_output_tokens },
4809
+ reportingStatus: row.reporting_status
4810
+ },
4811
+ ...row.raw_usage === null ? {} : { rawUsage: JSON.parse(row.raw_usage) },
4812
+ occurredAt: new Date(row.occurred_at),
4813
+ recordedAt: new Date(row.recorded_at)
4814
+ };
4815
+ }
4816
+ function rowToCost(row) {
4817
+ return costRecord({
4818
+ projectId: row.project_id,
4819
+ usageId: row.usage_record_id,
4820
+ status: row.status,
4821
+ providerId: row.provider_id,
4822
+ modelId: row.model_id,
4823
+ currency: row.currency,
4824
+ amountNanos: row.amount_nanos,
4825
+ reason: row.reason,
4826
+ details: row.details,
4827
+ ratedAt: row.rated_at
4828
+ });
4829
+ }
4830
+ function costFromJoinedRow(row) {
4831
+ return costRecord({
4832
+ projectId: row.project_id,
4833
+ usageId: row.id,
4834
+ status: required(row.cost_status),
4835
+ providerId: row.cost_provider_id,
4836
+ modelId: row.cost_model_id,
4837
+ currency: row.cost_currency,
4838
+ amountNanos: row.cost_amount_nanos,
4839
+ reason: row.cost_reason,
4840
+ details: required(row.cost_details),
4841
+ ratedAt: required(row.cost_rated_at)
4842
+ });
4843
+ }
4844
+ function costRecord(input) {
4845
+ const details = parseAiModelCallCostDetails(input.details);
4846
+ const base = {
4847
+ projectId: input.projectId,
4848
+ usageRecordId: input.usageId,
4849
+ pricingContext: details.pricingContext,
4850
+ priceSource: {
4851
+ ...details.priceSource,
4852
+ observedAt: new Date(details.priceSource.observedAt)
4853
+ },
4854
+ ratedAt: new Date(input.ratedAt)
4855
+ };
4856
+ if (input.status === "rated") {
4857
+ return normalizeAiModelCallCostRecord({
4858
+ ...base,
4859
+ status: "rated",
4860
+ billingIdentity: { providerId: required(input.providerId), modelId: required(input.modelId) },
4861
+ money: { currency: required(input.currency), amountNanos: required(input.amountNanos) },
4862
+ components: required(details.components)
4863
+ });
4864
+ }
4865
+ return normalizeAiModelCallCostRecord({
4866
+ ...base,
4867
+ status: "unpriceable",
4868
+ ...input.providerId && input.modelId ? { billingIdentity: { providerId: input.providerId, modelId: input.modelId } } : {},
4869
+ reason: required(input.reason),
4870
+ ...details.missingMeters ? { missingMeters: details.missingMeters } : {}
4871
+ });
4872
+ }
4873
+ function aggregateSummaryRows(ids, rows) {
4874
+ const map = new Map(ids.map((id) => [id, accumulator()]));
4875
+ for (const row of rows) {
4876
+ const value = map.get(row.execution_id);
4877
+ if (!value)
4878
+ continue;
4879
+ if (row.status === null)
4880
+ value.unvaluedCallCount += 1;
4881
+ else if (row.status === "unpriceable")
4882
+ value.unpriceableCallCount += 1;
4883
+ else {
4884
+ value.ratedCallCount += 1;
4885
+ add(value.amounts, required(row.currency), required(row.amount_nanos));
4886
+ }
4887
+ }
4888
+ return ids.map((id) => finish(map.get(id)));
4889
+ }
4890
+ function accumulator() {
4891
+ return {
4892
+ amounts: new Map,
4893
+ ratedCallCount: 0,
4894
+ unpriceableCallCount: 0,
4895
+ unvaluedCallCount: 0
4896
+ };
4897
+ }
4898
+ function finish(value) {
4899
+ return {
4900
+ amounts: [...value.amounts].sort(([left], [right]) => left.localeCompare(right)).map(([currency, amount]) => ({ currency, amountNanos: amount.toString() })),
4901
+ ratedCallCount: value.ratedCallCount,
4902
+ unpriceableCallCount: value.unpriceableCallCount,
4903
+ unvaluedCallCount: value.unvaluedCallCount
4904
+ };
4905
+ }
4906
+ function add(amounts, currency, amount) {
4907
+ amounts.set(currency, (amounts.get(currency) ?? 0n) + BigInt(amount));
4908
+ }
4909
+ function assertCostMatchesUsage(record, usage) {
4910
+ if (!aiModelCallCostMatchesUsage(record, usage)) {
4911
+ throw new AiCostStorageError("cost_mismatch", `[SixbSqlite] AI valuation for usage '${record.usageRecordId}' does not match its usage record.`);
4912
+ }
4913
+ }
4914
+ function missingUsage(record) {
4915
+ return new AiCostStorageError("missing_usage", `[SixbSqlite] AI usage '${record.usageRecordId}' does not exist.`);
4916
+ }
4917
+ function parseStringArray(value) {
4918
+ const parsed = JSON.parse(value);
4919
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
4920
+ throw new Error("[SixbSqlite] AI requester group ids are invalid.");
4921
+ }
4922
+ return parsed;
4923
+ }
4924
+ function assertNonBlank(value, field) {
4925
+ if (typeof value !== "string" || value.trim().length === 0) {
4926
+ throw new TypeError(`[Sixb] AI ${field} must be nonblank.`);
4927
+ }
4928
+ }
4929
+ function safeSqlCount(value, field) {
4930
+ if (!/^(0|[1-9]\d*)$/.test(value)) {
4931
+ throw new Error(`[SixbSqlite] AI accounting ${field} is invalid.`);
4932
+ }
4933
+ const count = Number(value);
4934
+ if (!Number.isSafeInteger(count)) {
4935
+ throw new TypeError(`[Sixb] AI accounting ${field} exceeds the safe integer range.`);
4936
+ }
4937
+ return count;
4938
+ }
4939
+ function required(value) {
4940
+ if (value === null || value === undefined) {
4941
+ throw new Error("[SixbSqlite] AI valuation column is unexpectedly null.");
4942
+ }
4943
+ return value;
4944
+ }
4945
+
4186
4946
  // src/ai-usage-storage.ts
4187
4947
  import {
4188
4948
  AiUsageStorageError,
@@ -4232,6 +4992,17 @@ class SqliteAiUsageStorage {
4232
4992
  });
4233
4993
  return summary;
4234
4994
  }
4995
+ async getLatestForExecution(input) {
4996
+ assertNonBlankProjectId(input.projectId);
4997
+ assertAiUsageExecutionId(input.executionId);
4998
+ const row = this.connection.db.query(`
4999
+ SELECT * FROM ai_model_call_usage
5000
+ WHERE project_id = ? AND execution_id = ?
5001
+ ORDER BY occurred_at DESC, id DESC
5002
+ LIMIT 1
5003
+ `).get(input.projectId, input.executionId);
5004
+ return row ? this.rowToRecord(row) : null;
5005
+ }
4235
5006
  async summarizeExecutions(input) {
4236
5007
  assertNonBlankProjectId(input.projectId);
4237
5008
  for (const executionId of input.executionIds)
@@ -4308,7 +5079,7 @@ class SqliteAiUsageStorage {
4308
5079
  ...row.response_model_id === null ? {} : { responseModelId: row.response_model_id },
4309
5080
  responseId: row.response_id,
4310
5081
  usage: {
4311
- ...usageFromRow(row),
5082
+ ...usageFromRow2(row),
4312
5083
  ...row.total_tokens === null ? {} : { totalTokens: row.total_tokens },
4313
5084
  reportingStatus: row.reporting_status
4314
5085
  },
@@ -4342,7 +5113,7 @@ function aggregateExecutionRows(executionIds, rows) {
4342
5113
  for (const executionId of executionIds)
4343
5114
  usageByExecutionId.set(executionId, []);
4344
5115
  for (const row of rows) {
4345
- usageByExecutionId.get(row.execution_id)?.push(usageFromRow(row));
5116
+ usageByExecutionId.get(row.execution_id)?.push(usageFromRow2(row));
4346
5117
  }
4347
5118
  return executionIds.map((executionId) => {
4348
5119
  const usages = usageByExecutionId.get(executionId) ?? [];
@@ -4352,7 +5123,7 @@ function aggregateExecutionRows(executionIds, rows) {
4352
5123
  };
4353
5124
  });
4354
5125
  }
4355
- function usageFromRow(row) {
5126
+ function usageFromRow2(row) {
4356
5127
  return {
4357
5128
  ...row.input_tokens === null ? {} : { inputTokens: row.input_tokens },
4358
5129
  ...row.output_tokens === null ? {} : { outputTokens: row.output_tokens },
@@ -6582,7 +7353,7 @@ function connectionRunFromRow(row) {
6582
7353
  ...base,
6583
7354
  status: "waiting",
6584
7355
  waitingFor: "provider_authorization",
6585
- expiresAt: new Date(required(row.expires_at, "run expiry"))
7356
+ expiresAt: new Date(required2(row.expires_at, "run expiry"))
6586
7357
  };
6587
7358
  }
6588
7359
  if (row.status === "waiting" && row.waiting_for === "account_selection") {
@@ -6590,28 +7361,28 @@ function connectionRunFromRow(row) {
6590
7361
  ...base,
6591
7362
  status: "waiting",
6592
7363
  waitingFor: "account_selection",
6593
- authorizationId: required(row.authorization_id, "authorization id"),
6594
- expiresAt: new Date(required(row.expires_at, "run expiry"))
7364
+ authorizationId: required2(row.authorization_id, "authorization id"),
7365
+ expiresAt: new Date(required2(row.expires_at, "run expiry"))
6595
7366
  };
6596
7367
  }
6597
7368
  if (row.status === "running") {
6598
7369
  return {
6599
7370
  ...base,
6600
7371
  status: "running",
6601
- processingId: required(row.processing_id, "processing id"),
6602
- callbackStartedAt: new Date(required(row.callback_started_at, "callback start")),
6603
- expiresAt: new Date(required(row.expires_at, "run expiry")),
7372
+ processingId: required2(row.processing_id, "processing id"),
7373
+ callbackStartedAt: new Date(required2(row.callback_started_at, "callback start")),
7374
+ expiresAt: new Date(required2(row.expires_at, "run expiry")),
6604
7375
  ...row.authorization_id === null ? {} : { authorizationId: row.authorization_id }
6605
7376
  };
6606
7377
  }
6607
- const finishedAt = new Date(required(row.finished_at, "run finish"));
7378
+ const finishedAt = new Date(required2(row.finished_at, "run finish"));
6608
7379
  if (row.status === "succeeded") {
6609
7380
  return {
6610
7381
  ...base,
6611
7382
  status: "succeeded",
6612
- authorizationId: required(row.authorization_id, "authorization id"),
7383
+ authorizationId: required2(row.authorization_id, "authorization id"),
6613
7384
  ...row.cleanup_authorization_id === null ? {} : { cleanupAuthorizationId: row.cleanup_authorization_id },
6614
- connections: parseJson(required(row.connections, "connections")).map(connectionFromSerialized),
7385
+ connections: parseJson(required2(row.connections, "connections")).map(connectionFromSerialized),
6615
7386
  finishedAt
6616
7387
  };
6617
7388
  }
@@ -6619,7 +7390,7 @@ function connectionRunFromRow(row) {
6619
7390
  return {
6620
7391
  ...base,
6621
7392
  status: "failed",
6622
- error: parseJson(required(row.error, "run error")),
7393
+ error: parseJson(required2(row.error, "run error")),
6623
7394
  ...row.authorization_id === null ? {} : { authorizationId: row.authorization_id },
6624
7395
  finishedAt
6625
7396
  };
@@ -6689,20 +7460,20 @@ function authorizationFromRow(row) {
6689
7460
  function mutationFromRow(row) {
6690
7461
  if (row.mutation_id === null)
6691
7462
  return;
6692
- const kind = required(row.mutation_kind, "credential mutation kind");
6693
- const phase = required(row.mutation_phase, "credential mutation phase");
7463
+ const kind = required2(row.mutation_kind, "credential mutation kind");
7464
+ const phase = required2(row.mutation_phase, "credential mutation phase");
6694
7465
  const stagedCredentials = row.staged_credentials === null ? undefined : {
6695
7466
  credentials: parseJson(row.staged_credentials),
6696
7467
  ...row.staged_credential_expires_at === null ? {} : { credentialExpiresAt: new Date(row.staged_credential_expires_at) },
6697
- scopes: parseJson(required(row.staged_scopes, "staged credential scopes"))
7468
+ scopes: parseJson(required2(row.staged_scopes, "staged credential scopes"))
6698
7469
  };
6699
7470
  return {
6700
7471
  id: row.mutation_id,
6701
7472
  kind,
6702
7473
  phase,
6703
- holderId: required(row.mutation_holder_id, "credential mutation holder"),
6704
- expiresAt: new Date(required(row.mutation_expires_at, "credential mutation expiry")),
6705
- deadlineAt: new Date(required(row.mutation_deadline_at, "credential mutation deadline")),
7474
+ holderId: required2(row.mutation_holder_id, "credential mutation holder"),
7475
+ expiresAt: new Date(required2(row.mutation_expires_at, "credential mutation expiry")),
7476
+ deadlineAt: new Date(required2(row.mutation_deadline_at, "credential mutation deadline")),
6706
7477
  ...row.mutation_expected_connection_ids === null ? {} : {
6707
7478
  expectedConnectionIds: parseJson(row.mutation_expected_connection_ids)
6708
7479
  },
@@ -6745,7 +7516,7 @@ function iso(value) {
6745
7516
  function optionalIso(value) {
6746
7517
  return value?.toISOString() ?? null;
6747
7518
  }
6748
- function required(value, label) {
7519
+ function required2(value, label) {
6749
7520
  if (value !== null)
6750
7521
  return value;
6751
7522
  throw new Error(`[SixbSqlite] Stored ${label} is missing.`);
@@ -6850,6 +7621,9 @@ function isDuplicateExecutionError(error) {
6850
7621
  return error instanceof Error && error.message.includes("UNIQUE constraint failed: executions.project_id, executions.id");
6851
7622
  }
6852
7623
 
7624
+ // src/object-storage.ts
7625
+ import { linkBatchKey, objectBatchKey } from "@sixb/core/storage";
7626
+
6853
7627
  // src/object-query-compiler.ts
6854
7628
  import {
6855
7629
  ObjectQueryExecutionError
@@ -6865,6 +7639,8 @@ function compileObjectQueryInternal(projectId, query, ctx) {
6865
7639
  switch (query.kind) {
6866
7640
  case "start":
6867
7641
  return compileStart(projectId, query);
7642
+ case "refs":
7643
+ return compileRefs(projectId, query);
6868
7644
  case "filter":
6869
7645
  return compileFilter(projectId, query.input, query.predicate);
6870
7646
  case "sort":
@@ -6912,6 +7688,50 @@ function compileStart(projectId, query) {
6912
7688
  }
6913
7689
  };
6914
7690
  }
7691
+ function compileRefs(projectId, query) {
7692
+ if (query.refs.length === 0) {
7693
+ throw new Error("[Sixb] SQLite object storage requires at least one ref");
7694
+ }
7695
+ const order = compileOrder(identityOrderFields());
7696
+ const selectedOrder = compileOrder(identityOrderFields(), "selected");
7697
+ const refsJson = JSON.stringify(query.refs);
7698
+ const requested = `
7699
+ SELECT DISTINCT
7700
+ json_extract(ref.value, '$.objectTypeId') AS object_type_id,
7701
+ json_extract(ref.value, '$.primaryId') AS primary_id
7702
+ FROM json_each(?) AS ref
7703
+ `;
7704
+ const sql = `
7705
+ WITH requested AS (${requested})
7706
+ SELECT selected.*, selected.properties AS _cursor_properties
7707
+ FROM requested
7708
+ JOIN objects AS selected
7709
+ ON selected.project_id = ?
7710
+ AND selected.object_type_id = requested.object_type_id
7711
+ AND selected.primary_id = requested.primary_id
7712
+ ORDER BY ${selectedOrder.sql}
7713
+ `;
7714
+ return {
7715
+ sql,
7716
+ args: [refsJson, projectId, ...selectedOrder.args],
7717
+ totalSql: `
7718
+ WITH requested AS (${requested})
7719
+ SELECT COUNT(*) AS total
7720
+ FROM requested
7721
+ JOIN objects AS selected
7722
+ ON selected.project_id = ?
7723
+ AND selected.object_type_id = requested.object_type_id
7724
+ AND selected.primary_id = requested.primary_id
7725
+ `,
7726
+ totalArgs: [refsJson, projectId],
7727
+ order,
7728
+ hasMore: () => false,
7729
+ trimRows: identityRows,
7730
+ nextPageToken: () => {
7731
+ return;
7732
+ }
7733
+ };
7734
+ }
6915
7735
  function compileFilter(projectId, inputQuery, predicateNode) {
6916
7736
  const predicate = compilePredicate(predicateNode);
6917
7737
  return compileWhere(projectId, inputQuery, predicate);
@@ -7655,6 +8475,7 @@ var SQLITE_OBJECT_QUERY_CAPABILITIES = {
7655
8475
  facetObjects: true,
7656
8476
  nodes: {
7657
8477
  start: true,
8478
+ refs: true,
7658
8479
  filter: true,
7659
8480
  text: true,
7660
8481
  sort: true,
@@ -7706,7 +8527,7 @@ var SQLITE_OBJECT_QUERY_CAPABILITIES = {
7706
8527
  stablePageTokens: true
7707
8528
  },
7708
8529
  notes: [
7709
- "SQLite object query pushdown supports start/filter/text/sort/limit/page/traverse/set/project/expand over JSON properties and object links.",
8530
+ "SQLite object query pushdown supports start/refs/filter/text/sort/limit/page/traverse/set/project/expand over JSON properties and object links.",
7710
8531
  "expand hydrates linked objects in-database (top-N per parent via row_number() + json_group_array); core resolves each expansion's cardinality before pushdown, and a mixed/unresolved one stays on the fallback.",
7711
8532
  "Ordered decimal predicates and sorting use the bounded core fallback because SQLite has no native exact decimal type; canonical decimal equality remains pushdown-safe.",
7712
8533
  "Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases."
@@ -7785,12 +8606,16 @@ class SqliteObjectStorage {
7785
8606
  const result = new Map;
7786
8607
  if (params.items.length === 0)
7787
8608
  return result;
7788
- const stmt = this.db.query("SELECT * FROM objects WHERE project_id = ? AND object_type_id = ? AND primary_id = ?");
7789
- for (const item of params.items) {
7790
- const row = stmt.get(params.projectId, item.objectTypeId, item.primaryId);
7791
- if (row) {
7792
- result.set(`${item.objectTypeId}:${item.primaryId}`, this.rowToObject(row));
7793
- }
8609
+ const rows = this.db.query(`
8610
+ SELECT object.*
8611
+ FROM json_each(?) AS requested
8612
+ JOIN objects AS object
8613
+ ON object.project_id = ?
8614
+ AND object.object_type_id = json_extract(requested.value, '$.objectTypeId')
8615
+ AND object.primary_id = json_extract(requested.value, '$.primaryId')
8616
+ `).all(JSON.stringify(params.items), params.projectId);
8617
+ for (const row of rows) {
8618
+ result.set(objectBatchKey(row.object_type_id, row.primary_id), this.rowToObject(row));
7794
8619
  }
7795
8620
  return result;
7796
8621
  }
@@ -7802,25 +8627,95 @@ class SqliteObjectStorage {
7802
8627
  for (const item of params.items) {
7803
8628
  const rows = stmt.all(params.projectId, item.objectTypeId, item.objectId, item.linkId);
7804
8629
  if (rows.length > 0) {
7805
- result.set(`${item.objectTypeId}:${item.objectId}:${item.linkId}`, rows.map((r) => this.rowToLink(r)));
8630
+ result.set(linkBatchKey(item.objectTypeId, item.objectId, item.linkId), rows.map((r) => this.rowToLink(r)));
7806
8631
  }
7807
8632
  }
7808
8633
  return result;
7809
8634
  }
7810
- async listIncidentLinksBatch(params) {
7811
- const deduped = new Map;
7812
- for (const item of params.items) {
7813
- const rows = await this.listLinks({
7814
- projectId: params.projectId,
7815
- objectTypeId: item.objectTypeId,
7816
- objectId: item.objectId,
7817
- direction: "both"
7818
- });
7819
- for (const row of rows) {
7820
- deduped.set(`${row.sourceTypeId}:${row.sourceId}:${row.linkId}:${row.targetTypeId}:${row.targetId}`, row);
7821
- }
8635
+ async queryLinks(params) {
8636
+ assertLinkQueryLimit(params.limit);
8637
+ if (params.objectRefs.length === 0 || params.endpointObjectTypeIds?.length === 0) {
8638
+ return { links: [], hasMore: false };
8639
+ }
8640
+ const sourceJoin = `
8641
+ SELECT link.*
8642
+ FROM links AS link
8643
+ JOIN requested
8644
+ ON requested.object_type_id = link.source_type_id
8645
+ AND requested.object_id = link.source_id
8646
+ WHERE link.project_id = ?
8647
+ `;
8648
+ const targetJoin = `
8649
+ SELECT link.*
8650
+ FROM links AS link
8651
+ JOIN requested
8652
+ ON requested.object_type_id = link.target_type_id
8653
+ AND requested.object_id = link.target_id
8654
+ WHERE link.project_id = ?
8655
+ `;
8656
+ const incidentSql = params.direction === "outgoing" ? sourceJoin : params.direction === "incoming" ? targetJoin : `${sourceJoin} UNION ${targetJoin}`;
8657
+ const args = [JSON.stringify(params.objectRefs), params.projectId];
8658
+ if (params.direction === "both")
8659
+ args.push(params.projectId);
8660
+ const predicates = [];
8661
+ if (params.linkId !== undefined) {
8662
+ predicates.push("link_id = ?");
8663
+ args.push(params.linkId);
8664
+ }
8665
+ if (params.endpointObjectTypeIds !== undefined) {
8666
+ const allowedTypes = JSON.stringify([...new Set(params.endpointObjectTypeIds)]);
8667
+ predicates.push("source_type_id IN (SELECT value FROM json_each(?))", "target_type_id IN (SELECT value FROM json_each(?))");
8668
+ args.push(allowedTypes, allowedTypes);
7822
8669
  }
7823
- return [...deduped.values()];
8670
+ if (params.after) {
8671
+ predicates.push("(source_type_id, source_id, link_id, target_type_id, target_id) > (?, ?, ?, ?, ?)");
8672
+ args.push(...params.after);
8673
+ }
8674
+ args.push(params.limit + 1);
8675
+ const whereSql = predicates.length > 0 ? `WHERE ${predicates.join(" AND ")}` : "";
8676
+ const rows = this.db.query(`
8677
+ WITH requested AS (
8678
+ SELECT DISTINCT
8679
+ json_extract(value, '$.objectTypeId') AS object_type_id,
8680
+ json_extract(value, '$.primaryId') AS object_id
8681
+ FROM json_each(?)
8682
+ ), incident AS (${incidentSql})
8683
+ SELECT *
8684
+ FROM incident
8685
+ ${whereSql}
8686
+ ORDER BY source_type_id, source_id, link_id, target_type_id, target_id
8687
+ LIMIT ?
8688
+ `).all(...args);
8689
+ return {
8690
+ links: rows.slice(0, params.limit).map((row) => this.rowToLink(row)),
8691
+ hasMore: rows.length > params.limit
8692
+ };
8693
+ }
8694
+ async listIncidentLinksBatch(params) {
8695
+ if (params.items.length === 0)
8696
+ return [];
8697
+ const rows = this.db.query(`
8698
+ WITH requested AS (
8699
+ SELECT
8700
+ json_extract(value, '$.objectTypeId') AS object_type_id,
8701
+ json_extract(value, '$.objectId') AS object_id
8702
+ FROM json_each(?)
8703
+ )
8704
+ SELECT link.*
8705
+ FROM links AS link
8706
+ JOIN requested
8707
+ ON requested.object_type_id = link.source_type_id
8708
+ AND requested.object_id = link.source_id
8709
+ WHERE link.project_id = ?
8710
+ UNION
8711
+ SELECT link.*
8712
+ FROM links AS link
8713
+ JOIN requested
8714
+ ON requested.object_type_id = link.target_type_id
8715
+ AND requested.object_id = link.target_id
8716
+ WHERE link.project_id = ?
8717
+ `).all(JSON.stringify(params.items), params.projectId, params.projectId);
8718
+ return rows.map((row) => this.rowToLink(row));
7824
8719
  }
7825
8720
  async listByPrimaryIdPage(params) {
7826
8721
  assertReconciliationPageLimit(params.limit);
@@ -7932,6 +8827,11 @@ function assertReconciliationPageLimit(limit) {
7932
8827
  throw new Error("Object reconciliation page limit must be a positive safe integer.");
7933
8828
  }
7934
8829
  }
8830
+ function assertLinkQueryLimit(limit) {
8831
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
8832
+ throw new Error("Object link query limit must be a positive safe integer.");
8833
+ }
8834
+ }
7935
8835
  function readTotal(db, compiled) {
7936
8836
  const row = db.query(compiled.totalSql).get(...compiled.totalArgs);
7937
8837
  return row.total;
@@ -8561,13 +9461,13 @@ class SqliteMaterializationStateReader {
8561
9461
  ORDER BY requested.scope_sort_key, ${linkSortExpression("links")}
8562
9462
  `).iterate(requestedJson, this.projectId);
8563
9463
  for (const row of rows) {
8564
- const accumulator = accumulators.get(row.scope_sort_key);
8565
- if (!accumulator) {
9464
+ const accumulator2 = accumulators.get(row.scope_sort_key);
9465
+ if (!accumulator2) {
8566
9466
  throw new MaterializationConflictError2("effective-state", `Unexpected link scope '${row.scope_sort_key}' returned by storage.`);
8567
9467
  }
8568
9468
  const snapshot = effectiveLinkSnapshot(row);
8569
- appendScopeSnapshot(accumulator, snapshot);
8570
- effective.set(row.scope_sort_key, accumulator.effectiveCount === 1 ? snapshot : null);
9469
+ appendScopeSnapshot(accumulator2, snapshot);
9470
+ effective.set(row.scope_sort_key, accumulator2.effectiveCount === 1 ? snapshot : null);
8571
9471
  }
8572
9472
  return scopes.map(({ source, linkId }) => {
8573
9473
  const key = linkScopeSortKey(source, linkId);
@@ -8927,6 +9827,7 @@ function storedObjectOverride(row) {
8927
9827
  return {
8928
9828
  ref,
8929
9829
  value: parseJson2(row.value),
9830
+ editedAt: parseJson2(row.edited_at),
8930
9831
  lastCommitId: row.last_commit_id,
8931
9832
  updatedAt: row.updated_at
8932
9833
  };
@@ -9416,8 +10317,9 @@ class SqliteMaterializationWriter {
9416
10317
  if (item.expectedLastCommitId === null) {
9417
10318
  try {
9418
10319
  this.db.query(`INSERT INTO ontology_object_overrides (
9419
- project_id, object_type_id, primary_id, value, last_commit_id, updated_at
9420
- ) VALUES (?, ?, ?, json(?), ?, ?)`).run(projectId, item.ref.objectTypeId, item.ref.primaryId, canonicalJson(item.value), item.lastCommitId, item.updatedAt);
10320
+ project_id, object_type_id, primary_id, value, edited_at,
10321
+ last_commit_id, updated_at
10322
+ ) VALUES (?, ?, ?, json(?), json(?), ?, ?)`).run(projectId, item.ref.objectTypeId, item.ref.primaryId, canonicalJson(item.value), canonicalJson(item.editedAt), item.lastCommitId, item.updatedAt);
9421
10323
  } catch (error) {
9422
10324
  if (isSqliteConstraintError(error)) {
9423
10325
  throw effectiveConflict("Expected object override changed.");
@@ -9427,9 +10329,9 @@ class SqliteMaterializationWriter {
9427
10329
  continue;
9428
10330
  }
9429
10331
  requireChanges(this.db.query(`UPDATE ontology_object_overrides
9430
- SET value = json(?), last_commit_id = ?, updated_at = ?
10332
+ SET value = json(?), edited_at = json(?), last_commit_id = ?, updated_at = ?
9431
10333
  WHERE project_id = ? AND object_type_id = ? AND primary_id = ?
9432
- AND last_commit_id = ?`).run(canonicalJson(item.value), item.lastCommitId, item.updatedAt, projectId, item.ref.objectTypeId, item.ref.primaryId, item.expectedLastCommitId).changes, "effective-state", "Expected object override changed.");
10334
+ AND last_commit_id = ?`).run(canonicalJson(item.value), canonicalJson(item.editedAt), item.lastCommitId, item.updatedAt, projectId, item.ref.objectTypeId, item.ref.primaryId, item.expectedLastCommitId).changes, "effective-state", "Expected object override changed.");
9433
10335
  }
9434
10336
  for (const item of chunk.overrides.objects.deletes) {
9435
10337
  requireChanges(this.db.query(`DELETE FROM ontology_object_overrides
@@ -12984,6 +13886,7 @@ class SqliteStorage {
12984
13886
  executions;
12985
13887
  agents;
12986
13888
  aiUsage;
13889
+ aiCosts;
12987
13890
  actionRuns;
12988
13891
  pipelineRuns;
12989
13892
  syncRuns;
@@ -13030,6 +13933,7 @@ class SqliteStorage {
13030
13933
  this.executions = createOperationScopedFacade(stores.executions, scope);
13031
13934
  this.agents = createAgentOperationScope(stores.agents, scope);
13032
13935
  this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope);
13936
+ this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope);
13033
13937
  this.actionRuns = createOperationScopedFacade(stores.actionRuns, scope);
13034
13938
  this.pipelineRuns = createOperationScopedFacade(stores.pipelineRuns, scope);
13035
13939
  this.timeseries = createOperationScopedFacade(readTimeseries, readScope);
@@ -13149,6 +14053,7 @@ function createSqliteStores(connection, options) {
13149
14053
  executions,
13150
14054
  agents: new SqliteAgentStorage({ connection, executions }),
13151
14055
  aiUsage: new SqliteAiUsageStorage({ connection }),
14056
+ aiCosts: new SqliteAiCostStorage(connection),
13152
14057
  actionRuns: new SqliteActionRunStorage({ connection, executions }),
13153
14058
  pipelineRuns: new SqlitePipelineRunStorage({ connection, executions }),
13154
14059
  timeseries: new SqliteTimeseriesStorage({ connection }),
@@ -13166,4 +14071,4 @@ export {
13166
14071
  SqliteStorage
13167
14072
  };
13168
14073
 
13169
- //# debugId=7215BBCFE4EA2F6564756E2164756E21
14074
+ //# debugId=2D24EFD822513C9E64756E2164756E21