@sixb/sqlite 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,106 @@ function queryAgentList(input) {
3721
3843
  return { rows, total, hasMore: offset + rows.length < total };
3722
3844
  }
3723
3845
 
3846
+ // src/agents/checkpoints.ts
3847
+ class SqliteAgentContextCheckpointStore {
3848
+ db;
3849
+ constructor(db) {
3850
+ this.db = db;
3851
+ }
3852
+ async create(input) {
3853
+ assertCreateAgentContextCheckpointInput(input, "SixbSqlite");
3854
+ return this.db.transaction(() => {
3855
+ const runRow = this.db.query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?").get(input.projectId, input.createdByRunId);
3856
+ const threadRow = this.db.query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?").get(input.projectId, input.threadId);
3857
+ assertAgentContextCheckpointAuthority({
3858
+ create: input,
3859
+ run: runRow ? rowToRunRecord(runRow) : null,
3860
+ thread: threadRow ? rowToThreadRecord(threadRow) : null,
3861
+ prefix: "SixbSqlite"
3862
+ });
3863
+ const latest = this.findLatest(input.projectId, input.threadId);
3864
+ 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);
3865
+ const existingForRun = this.db.query("SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND created_by_run_id = ?").get(input.projectId, input.createdByRunId);
3866
+ if (existingForRun) {
3867
+ const record = rowToContextCheckpointRecord(existingForRun);
3868
+ if (agentContextCheckpointMatchesCreateInput(record, input)) {
3869
+ assertAgentContextCheckpointReplayState({
3870
+ create: input,
3871
+ existing: record,
3872
+ latest,
3873
+ headSeq: headRow.seq,
3874
+ prefix: "SixbSqlite"
3875
+ });
3876
+ return record;
3877
+ }
3878
+ throw new AgentStorageError("invalid_state", `[SixbSqlite] Agent run '${input.createdByRunId}' already created a different context checkpoint.`);
3879
+ }
3880
+ const duplicateId = this.db.query("SELECT 1 FROM agent_context_checkpoints WHERE project_id = ? AND id = ?").get(input.projectId, input.id);
3881
+ if (duplicateId) {
3882
+ throw new AgentStorageError("duplicate_id", `[SixbSqlite] Agent context checkpoint '${input.id}' already exists for project '${input.projectId}'.`);
3883
+ }
3884
+ 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);
3885
+ assertAgentContextCheckpointAnchors({
3886
+ create: input,
3887
+ latest,
3888
+ headSeq: headRow.seq,
3889
+ firstRetained: firstRetainedRow ? rowToMessageRecord(firstRetainedRow) : null,
3890
+ prefix: "SixbSqlite"
3891
+ });
3892
+ const createdAt = input.createdAt ?? new Date;
3893
+ this.db.query(`
3894
+ INSERT INTO agent_context_checkpoints (
3895
+ project_id,
3896
+ id,
3897
+ thread_id,
3898
+ created_by_run_id,
3899
+ previous_checkpoint_id,
3900
+ reason,
3901
+ summary,
3902
+ summary_format_version,
3903
+ summarized_through_seq,
3904
+ observed_head_seq,
3905
+ estimated_input_tokens_before,
3906
+ estimated_input_tokens_after,
3907
+ summary_model_id,
3908
+ created_at
3909
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3910
+ `).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());
3911
+ return this.requireById(input.projectId, input.id);
3912
+ })();
3913
+ }
3914
+ async getLatest(input) {
3915
+ return this.findLatest(input.projectId, input.threadId);
3916
+ }
3917
+ findLatest(projectId, threadId) {
3918
+ const row = this.db.query(`
3919
+ SELECT * FROM agent_context_checkpoints
3920
+ WHERE project_id = ? AND thread_id = ?
3921
+ ORDER BY summarized_through_seq DESC, created_at DESC, id DESC
3922
+ LIMIT 1
3923
+ `).get(projectId, threadId);
3924
+ return row ? rowToContextCheckpointRecord(row) : null;
3925
+ }
3926
+ requireById(projectId, id) {
3927
+ const row = this.db.query("SELECT * FROM agent_context_checkpoints WHERE project_id = ? AND id = ?").get(projectId, id);
3928
+ if (!row) {
3929
+ throw new AgentStorageError("invalid_state", `[SixbSqlite] Failed to load agent context checkpoint '${id}' for project '${projectId}'.`);
3930
+ }
3931
+ return rowToContextCheckpointRecord(row);
3932
+ }
3933
+ }
3934
+
3935
+ // src/agents/messages.ts
3936
+ import { AGENT_MESSAGE_CONTENT_VERSION } from "@sixb/core/internal/agents";
3937
+ import {
3938
+ AgentStorageError as AgentStorageError2
3939
+ } from "@sixb/core/storage";
3940
+
3941
+ // src/storage-errors.ts
3942
+ function isUniqueConstraintError2(error) {
3943
+ return error instanceof Error && error.message.includes("UNIQUE constraint failed");
3944
+ }
3945
+
3724
3946
  // src/agents/messages.ts
3725
3947
  class SqliteAgentMessageStore {
3726
3948
  db;
@@ -3732,12 +3954,12 @@ class SqliteAgentMessageStore {
3732
3954
  return this.db.transaction(() => {
3733
3955
  const thread = this.db.query("SELECT 1 AS present FROM agent_threads WHERE project_id = ? AND id = ?").get(input.projectId, input.threadId);
3734
3956
  if (!thread) {
3735
- throw new AgentStorageError("thread_not_found", `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
3957
+ throw new AgentStorageError2("thread_not_found", `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
3736
3958
  }
3737
3959
  if (input.runId !== null) {
3738
3960
  const run = this.db.query("SELECT 1 AS present FROM agent_runs WHERE project_id = ? AND id = ?").get(input.projectId, input.runId);
3739
3961
  if (!run) {
3740
- throw new AgentStorageError("run_not_found", `[SixbSqlite] Agent run '${input.runId}' not found for project '${input.projectId}'.`);
3962
+ throw new AgentStorageError2("run_not_found", `[SixbSqlite] Agent run '${input.runId}' not found for project '${input.projectId}'.`);
3741
3963
  }
3742
3964
  }
3743
3965
  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 +3983,7 @@ class SqliteAgentMessageStore {
3761
3983
  `).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
3984
  } catch (error) {
3763
3985
  if (isUniqueConstraintError2(error)) {
3764
- throw new AgentStorageError("duplicate_id", `[SixbSqlite] Agent message '${input.id}' already exists for project '${input.projectId}'.`);
3986
+ throw new AgentStorageError2("duplicate_id", `[SixbSqlite] Agent message '${input.id}' already exists for project '${input.projectId}'.`);
3765
3987
  }
3766
3988
  throw error;
3767
3989
  }
@@ -3773,6 +3995,35 @@ class SqliteAgentMessageStore {
3773
3995
  return this.requireMessage(input.projectId, input.id);
3774
3996
  })();
3775
3997
  }
3998
+ async deleteByRunId(input) {
3999
+ return this.db.transaction(() => {
4000
+ const thread = this.db.query("SELECT 1 AS present FROM agent_threads WHERE project_id = ? AND id = ?").get(input.projectId, input.threadId);
4001
+ if (!thread) {
4002
+ throw new AgentStorageError2("thread_not_found", `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
4003
+ }
4004
+ 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);
4005
+ if (deleted.changes === 0) {
4006
+ return 0;
4007
+ }
4008
+ const updatedAt = new Date().toISOString();
4009
+ this.db.query(`
4010
+ UPDATE agent_threads
4011
+ SET
4012
+ message_count = (
4013
+ SELECT COUNT(*) FROM agent_messages WHERE project_id = ? AND thread_id = ?
4014
+ ),
4015
+ last_message_at = (
4016
+ SELECT created_at FROM agent_messages
4017
+ WHERE project_id = ? AND thread_id = ?
4018
+ ORDER BY seq DESC
4019
+ LIMIT 1
4020
+ ),
4021
+ updated_at = ?
4022
+ WHERE project_id = ? AND id = ?
4023
+ `).run(input.projectId, input.threadId, input.projectId, input.threadId, updatedAt, input.projectId, input.threadId);
4024
+ return deleted.changes;
4025
+ })();
4026
+ }
3776
4027
  async getById(params) {
3777
4028
  const row = this.db.query("SELECT * FROM agent_messages WHERE project_id = ? AND id = ?").get(params.projectId, params.id);
3778
4029
  return row ? rowToMessageRecord(row) : null;
@@ -3783,6 +4034,10 @@ class SqliteAgentMessageStore {
3783
4034
  }
3784
4035
  const whereClauses = ["project_id = ?", "thread_id = ?"];
3785
4036
  const args = [input.projectId, input.threadId];
4037
+ if (input.afterSeq !== undefined) {
4038
+ whereClauses.push("seq > ?");
4039
+ args.push(input.afterSeq);
4040
+ }
3786
4041
  if (input.roles) {
3787
4042
  whereClauses.push(`role IN (${input.roles.map(() => "?").join(", ")})`);
3788
4043
  args.push(...input.roles);
@@ -3802,7 +4057,7 @@ class SqliteAgentMessageStore {
3802
4057
  requireMessage(projectId, id) {
3803
4058
  const row = this.db.query("SELECT * FROM agent_messages WHERE project_id = ? AND id = ?").get(projectId, id);
3804
4059
  if (!row) {
3805
- throw new AgentStorageError("invalid_state", `[SixbSqlite] Failed to load agent message '${id}' for project '${projectId}'.`);
4060
+ throw new AgentStorageError2("invalid_state", `[SixbSqlite] Failed to load agent message '${id}' for project '${projectId}'.`);
3806
4061
  }
3807
4062
  return rowToMessageRecord(row);
3808
4063
  }
@@ -3814,7 +4069,7 @@ import { normalizeRequesterGroupIds } from "@sixb/core/internal/auth";
3814
4069
  import { serializeSixbFailure } from "@sixb/core/internal/errors";
3815
4070
  import {
3816
4071
  AGENT_RUN_FAILURE_CODES as AGENT_RUN_FAILURE_CODES2,
3817
- AgentStorageError as AgentStorageError2
4072
+ AgentStorageError as AgentStorageError3
3818
4073
  } from "@sixb/core/storage";
3819
4074
 
3820
4075
  // src/run-list-query.ts
@@ -3890,10 +4145,10 @@ class SqliteAgentRunStore {
3890
4145
  return this.db.transaction(() => {
3891
4146
  const thread = this.db.query("SELECT active_run_id FROM agent_threads WHERE project_id = ? AND id = ?").get(input.projectId, input.threadId);
3892
4147
  if (!thread) {
3893
- throw new AgentStorageError2("thread_not_found", `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
4148
+ throw new AgentStorageError3("thread_not_found", `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
3894
4149
  }
3895
4150
  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}'.`);
4151
+ throw new AgentStorageError3("active_run_exists", `[SixbSqlite] Agent thread '${input.threadId}' already has an active run '${thread.active_run_id}'.`);
3897
4152
  }
3898
4153
  try {
3899
4154
  this.db.query(`
@@ -3912,7 +4167,7 @@ class SqliteAgentRunStore {
3912
4167
  `).run(input.projectId, input.id, input.executionId, input.threadId, input.agentId, input.triggerMessageId, JSON.stringify(normalizeRequesterGroupIds(input.requesterGroupIds)), createdAt.toISOString());
3913
4168
  } catch (error) {
3914
4169
  if (isUniqueConstraintError2(error)) {
3915
- throw new AgentStorageError2("duplicate_id", `[SixbSqlite] Agent run '${input.id}' already exists for project '${input.projectId}'.`);
4170
+ throw new AgentStorageError3("duplicate_id", `[SixbSqlite] Agent run '${input.id}' already exists for project '${input.projectId}'.`);
3916
4171
  }
3917
4172
  throw error;
3918
4173
  }
@@ -3969,7 +4224,7 @@ class SqliteAgentRunStore {
3969
4224
  return this.db.transaction(() => {
3970
4225
  const run = this.requireRunning(input.projectId, input.id);
3971
4226
  if (run.execution_token !== input.executionToken) {
3972
- throw new AgentStorageError2("execution_lost", `[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`);
4227
+ throw new AgentStorageError3("execution_lost", `[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`);
3973
4228
  }
3974
4229
  this.db.query(`
3975
4230
  UPDATE agent_runs
@@ -3985,7 +4240,7 @@ class SqliteAgentRunStore {
3985
4240
  return this.db.transaction(() => {
3986
4241
  const run = this.requireRunning(input.projectId, input.id);
3987
4242
  if (run.execution_token !== input.executionToken) {
3988
- throw new AgentStorageError2("execution_lost", `[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`);
4243
+ throw new AgentStorageError3("execution_lost", `[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`);
3989
4244
  }
3990
4245
  this.db.query(`
3991
4246
  UPDATE agent_runs
@@ -4057,10 +4312,10 @@ class SqliteAgentRunStore {
4057
4312
  requireStatus(projectId, id, status) {
4058
4313
  const row = this.db.query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?").get(projectId, id);
4059
4314
  if (!row) {
4060
- throw new AgentStorageError2("run_not_found", `[SixbSqlite] Agent run '${id}' not found for project '${projectId}'.`);
4315
+ throw new AgentStorageError3("run_not_found", `[SixbSqlite] Agent run '${id}' not found for project '${projectId}'.`);
4061
4316
  }
4062
4317
  if (row.status !== status) {
4063
- throw new AgentStorageError2("invalid_state", `[SixbSqlite] Agent run '${id}' is not ${status} (status '${row.status}').`);
4318
+ throw new AgentStorageError3("invalid_state", `[SixbSqlite] Agent run '${id}' is not ${status} (status '${row.status}').`);
4064
4319
  }
4065
4320
  return row;
4066
4321
  }
@@ -4074,7 +4329,7 @@ class SqliteAgentRunStore {
4074
4329
  requireRun(projectId, id) {
4075
4330
  const row = this.db.query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?").get(projectId, id);
4076
4331
  if (!row) {
4077
- throw new AgentStorageError2("run_not_found", `[SixbSqlite] Agent run '${id}' not found for project '${projectId}'.`);
4332
+ throw new AgentStorageError3("run_not_found", `[SixbSqlite] Agent run '${id}' not found for project '${projectId}'.`);
4078
4333
  }
4079
4334
  return rowToRunRecord(row);
4080
4335
  }
@@ -4082,7 +4337,7 @@ class SqliteAgentRunStore {
4082
4337
 
4083
4338
  // src/agents/threads.ts
4084
4339
  import {
4085
- AgentStorageError as AgentStorageError3
4340
+ AgentStorageError as AgentStorageError4
4086
4341
  } from "@sixb/core/storage";
4087
4342
  class SqliteAgentThreadStore {
4088
4343
  db;
@@ -4111,7 +4366,7 @@ class SqliteAgentThreadStore {
4111
4366
  `).run(input.projectId, input.id, input.agentId, input.ownerPrincipal.type, input.ownerPrincipal.id, input.title ?? null, input.status ?? "active", createdAt.toISOString(), updatedAt.toISOString());
4112
4367
  } catch (error) {
4113
4368
  if (isUniqueConstraintError2(error)) {
4114
- throw new AgentStorageError3("duplicate_id", `[SixbSqlite] Agent thread '${input.id}' already exists for project '${input.projectId}'.`);
4369
+ throw new AgentStorageError4("duplicate_id", `[SixbSqlite] Agent thread '${input.id}' already exists for project '${input.projectId}'.`);
4115
4370
  }
4116
4371
  throw error;
4117
4372
  }
@@ -4158,7 +4413,7 @@ class SqliteAgentThreadStore {
4158
4413
  requireThread(projectId, id) {
4159
4414
  const row = this.db.query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?").get(projectId, id);
4160
4415
  if (!row) {
4161
- throw new AgentStorageError3("invalid_state", `[SixbSqlite] Failed to load agent thread '${id}' for project '${projectId}'.`);
4416
+ throw new AgentStorageError4("invalid_state", `[SixbSqlite] Failed to load agent thread '${id}' for project '${projectId}'.`);
4162
4417
  }
4163
4418
  return rowToThreadRecord(row);
4164
4419
  }
@@ -4169,6 +4424,7 @@ class SqliteAgentStorage {
4169
4424
  threads;
4170
4425
  runs;
4171
4426
  messages;
4427
+ checkpoints;
4172
4428
  connection;
4173
4429
  constructor(options) {
4174
4430
  this.connection = openSqliteStoreConnection(options);
@@ -4178,11 +4434,497 @@ class SqliteAgentStorage {
4178
4434
  this.threads = new SqliteAgentThreadStore(this.connection.db);
4179
4435
  this.runs = new SqliteAgentRunStore(this.connection.db, options.executions);
4180
4436
  this.messages = new SqliteAgentMessageStore(this.connection.db);
4437
+ this.checkpoints = new SqliteAgentContextCheckpointStore(this.connection.db);
4181
4438
  }
4182
4439
  close() {
4183
4440
  closeSqliteStoreConnection(this.connection);
4184
4441
  }
4185
4442
  }
4443
+ // src/ai-cost-storage.ts
4444
+ import {
4445
+ aiModelCallCostDetails,
4446
+ aiModelCallCostMatchesUsage,
4447
+ buildAiAccountingOverviewFromFragments,
4448
+ normalizeAiAccountingQuery,
4449
+ normalizeAiModelCallAccountingQuery,
4450
+ normalizeAiModelCallCostRecord,
4451
+ parseAiModelCallCostDetails,
4452
+ toAccountingItem
4453
+ } from "@sixb/core/internal/ai-cost-storage-provider";
4454
+ import {
4455
+ AiCostStorageError
4456
+ } from "@sixb/core/storage";
4457
+ class SqliteAiCostStorage {
4458
+ connection;
4459
+ constructor(connection) {
4460
+ this.connection = connection;
4461
+ }
4462
+ async recordModelCallCost(input) {
4463
+ const record = normalizeAiModelCallCostRecord(input);
4464
+ runImmediateTransaction(this.connection.db, () => {
4465
+ const usage = this.findUsage(record.projectId, record.usageRecordId);
4466
+ if (!usage)
4467
+ throw missingUsage(record);
4468
+ assertCostMatchesUsage(record, usage);
4469
+ if (this.findCost(record.projectId, record.usageRecordId))
4470
+ return;
4471
+ try {
4472
+ this.insertCost(record);
4473
+ } catch (error) {
4474
+ if (!isUniqueConstraintError2(error))
4475
+ throw error;
4476
+ if (!this.findCost(record.projectId, record.usageRecordId))
4477
+ throw error;
4478
+ }
4479
+ });
4480
+ }
4481
+ async summarizeExecutions(input) {
4482
+ assertNonBlank(input.projectId, "cost summary projectId");
4483
+ for (const id of input.executionIds)
4484
+ assertNonBlank(id, "cost summary executionId");
4485
+ if (input.executionIds.length === 0)
4486
+ return [];
4487
+ const rows = this.connection.db.query(`
4488
+ WITH requested AS (SELECT DISTINCT value AS execution_id FROM json_each(?))
4489
+ SELECT usage.execution_id,
4490
+ cost.status,
4491
+ cost.currency,
4492
+ CAST(cost.amount_nanos AS TEXT) AS amount_nanos
4493
+ FROM requested
4494
+ JOIN ai_model_call_usage AS usage
4495
+ ON usage.project_id = ? AND usage.execution_id = requested.execution_id
4496
+ LEFT JOIN ai_model_call_valuations AS cost
4497
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
4498
+ `).all(JSON.stringify([...new Set(input.executionIds)]), input.projectId);
4499
+ return aggregateSummaryRows(input.executionIds, rows);
4500
+ }
4501
+ async queryProjectOverview(input) {
4502
+ const query = normalizeAiAccountingQuery(input);
4503
+ 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);
4504
+ return buildAiAccountingOverviewFromFragments(input, rows.map(aggregateFragmentFromRow));
4505
+ }
4506
+ async listModelCalls(input) {
4507
+ const query = normalizeAiModelCallAccountingQuery(input);
4508
+ const parameters = accountingListFilterParameters(query);
4509
+ const count = this.connection.db.query(SQLITE_ACCOUNTING_LIST_COUNT_SQL).get(...parameters);
4510
+ const rows = this.connection.db.query(SQLITE_ACCOUNTING_LIST_PAGE_SQL).all(...parameters, query.limit, query.offset);
4511
+ const items = rows.map((row) => toAccountingItem(accountingItemFromRow(row)));
4512
+ const total = safeSqlCount(count.total, "model-call list total");
4513
+ return structuredClone({
4514
+ items,
4515
+ total,
4516
+ hasMore: query.offset + items.length < total
4517
+ });
4518
+ }
4519
+ insertCost(record) {
4520
+ const rated = record.status === "rated" ? record : undefined;
4521
+ const details = aiModelCallCostDetails(record);
4522
+ this.connection.db.query(`INSERT INTO ai_model_call_valuations (
4523
+ project_id, usage_record_id, status, provider_id, model_id,
4524
+ currency, amount_nanos, reason, details, rated_at
4525
+ ) 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());
4526
+ }
4527
+ findCost(projectId, usageId) {
4528
+ const row = this.connection.db.query(`SELECT *, CAST(amount_nanos AS TEXT) AS amount_nanos
4529
+ FROM ai_model_call_valuations
4530
+ WHERE project_id = ? AND usage_record_id = ?`).get(projectId, usageId);
4531
+ return row ? rowToCost(row) : null;
4532
+ }
4533
+ findUsage(projectId, id) {
4534
+ const row = this.connection.db.query("SELECT * FROM ai_model_call_usage WHERE project_id = ? AND id = ?").get(projectId, id);
4535
+ return row ? usageFromRow(row, []) : null;
4536
+ }
4537
+ }
4538
+ var SQLITE_ACCOUNTING_LIST_FILTER = `
4539
+ usage.project_id = ? AND usage.occurred_at >= ? AND usage.occurred_at < ?
4540
+ AND (? IS NULL OR usage.provider_id = ?)
4541
+ AND (? IS NULL OR usage.requested_model_id = ?)
4542
+ AND (? IS NULL OR usage.execution_id = ?)
4543
+ AND (
4544
+ ? IS NULL OR ? = CASE
4545
+ WHEN cost.status = 'rated' THEN 'rated'
4546
+ WHEN cost.status = 'unpriceable' THEN 'unpriceable'
4547
+ ELSE 'unvalued'
4548
+ END
4549
+ )
4550
+ `;
4551
+ var SQLITE_ACCOUNTING_LIST_COUNT_SQL = `
4552
+ SELECT CAST(COUNT(*) AS TEXT) AS total
4553
+ FROM ai_model_call_usage AS usage
4554
+ LEFT JOIN ai_model_call_valuations AS cost
4555
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
4556
+ WHERE ${SQLITE_ACCOUNTING_LIST_FILTER}
4557
+ `;
4558
+ var SQLITE_ACCOUNTING_LIST_PAGE_SQL = `
4559
+ SELECT usage.*,
4560
+ CASE
4561
+ WHEN direct_agent.id IS NOT NULL THEN 'agent'
4562
+ WHEN workflow_agent.node_run_id IS NOT NULL THEN 'workflowAgent'
4563
+ ELSE NULL
4564
+ END AS attribution_kind,
4565
+ COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
4566
+ direct_agent.id AS attribution_agent_run_id,
4567
+ direct_agent.thread_id AS attribution_thread_id,
4568
+ workflow_agent.node_run_id AS attribution_node_run_id,
4569
+ workflow_node.workflow_id AS attribution_workflow_id,
4570
+ workflow_node.workflow_run_id AS attribution_workflow_run_id,
4571
+ COALESCE((
4572
+ SELECT json_group_array(group_id) FROM (
4573
+ SELECT group_id FROM ai_model_call_usage_groups
4574
+ WHERE project_id = usage.project_id AND usage_record_id = usage.id
4575
+ ORDER BY group_id
4576
+ )
4577
+ ), '[]') AS requester_group_ids,
4578
+ cost.status AS cost_status,
4579
+ cost.provider_id AS cost_provider_id,
4580
+ cost.model_id AS cost_model_id,
4581
+ cost.currency AS cost_currency,
4582
+ CAST(cost.amount_nanos AS TEXT) AS cost_amount_nanos,
4583
+ cost.reason AS cost_reason,
4584
+ cost.details AS cost_details,
4585
+ cost.rated_at AS cost_rated_at
4586
+ FROM ai_model_call_usage AS usage
4587
+ LEFT JOIN agent_runs AS direct_agent
4588
+ ON direct_agent.project_id = usage.project_id
4589
+ AND direct_agent.execution_id = usage.execution_id
4590
+ LEFT JOIN workflow_agent_node_runs AS workflow_agent
4591
+ ON workflow_agent.project_id = usage.project_id
4592
+ AND workflow_agent.execution_id = usage.execution_id
4593
+ LEFT JOIN workflow_node_runs AS workflow_node
4594
+ ON workflow_node.project_id = workflow_agent.project_id
4595
+ AND workflow_node.id = workflow_agent.node_run_id
4596
+ LEFT JOIN ai_model_call_valuations AS cost
4597
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
4598
+ WHERE ${SQLITE_ACCOUNTING_LIST_FILTER}
4599
+ ORDER BY usage.occurred_at DESC, usage.id ASC
4600
+ LIMIT ? OFFSET ?
4601
+ `;
4602
+ var SQLITE_ACCOUNTING_OVERVIEW_SQL = `
4603
+ WITH classified AS (
4604
+ SELECT usage.provider_id,
4605
+ usage.requested_model_id,
4606
+ usage.occurred_at,
4607
+ usage.input_tokens,
4608
+ usage.output_tokens,
4609
+ usage.uncached_input_tokens,
4610
+ usage.cache_read_input_tokens,
4611
+ usage.cache_write_input_tokens,
4612
+ usage.text_output_tokens,
4613
+ usage.reasoning_output_tokens,
4614
+ usage.reporting_status,
4615
+ COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
4616
+ workflow_node.workflow_id AS attribution_workflow_id,
4617
+ CASE
4618
+ WHEN cost.status = 'rated' THEN 'rated'
4619
+ WHEN cost.status = 'unpriceable' THEN 'unpriceable'
4620
+ ELSE 'unvalued'
4621
+ END AS valuation_status,
4622
+ CASE WHEN cost.status = 'rated' THEN cost.currency ELSE NULL END AS amount_currency,
4623
+ CASE WHEN cost.status = 'rated' THEN cost.amount_nanos ELSE NULL END AS amount_nanos
4624
+ FROM ai_model_call_usage AS usage
4625
+ LEFT JOIN agent_runs AS direct_agent
4626
+ ON direct_agent.project_id = usage.project_id
4627
+ AND direct_agent.execution_id = usage.execution_id
4628
+ LEFT JOIN workflow_agent_node_runs AS workflow_agent
4629
+ ON workflow_agent.project_id = usage.project_id
4630
+ AND workflow_agent.execution_id = usage.execution_id
4631
+ LEFT JOIN workflow_node_runs AS workflow_node
4632
+ ON workflow_node.project_id = workflow_agent.project_id
4633
+ AND workflow_node.id = workflow_agent.node_run_id
4634
+ LEFT JOIN ai_model_call_valuations AS cost
4635
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
4636
+ WHERE usage.project_id = ? AND usage.occurred_at >= ? AND usage.occurred_at < ?
4637
+ AND (? IS NULL OR usage.provider_id = ?)
4638
+ AND (? IS NULL OR usage.requested_model_id = ?)
4639
+ ), expanded AS (
4640
+ SELECT 'totals' AS dimension, NULL AS key_1, NULL AS key_2, NULL AS bucket_start,
4641
+ classified.*
4642
+ FROM classified
4643
+ UNION ALL
4644
+ SELECT 'series', NULL, NULL,
4645
+ CASE ?
4646
+ WHEN 'hour' THEN strftime('%Y-%m-%dT%H:00:00.000Z', occurred_at)
4647
+ WHEN 'day' THEN strftime('%Y-%m-%dT00:00:00.000Z', occurred_at)
4648
+ ELSE strftime(
4649
+ '%Y-%m-%dT00:00:00.000Z',
4650
+ occurred_at,
4651
+ printf('-%d days', (CAST(strftime('%w', occurred_at) AS INTEGER) + 6) % 7)
4652
+ )
4653
+ END,
4654
+ classified.*
4655
+ FROM classified
4656
+ UNION ALL
4657
+ SELECT 'model', provider_id, requested_model_id, NULL, classified.* FROM classified
4658
+ UNION ALL
4659
+ SELECT 'agent', attribution_agent_id, NULL, NULL, classified.*
4660
+ FROM classified WHERE attribution_agent_id IS NOT NULL
4661
+ UNION ALL
4662
+ SELECT 'workflow', attribution_workflow_id, NULL, NULL, classified.*
4663
+ FROM classified WHERE attribution_workflow_id IS NOT NULL
4664
+ )
4665
+ SELECT dimension,
4666
+ key_1,
4667
+ key_2,
4668
+ bucket_start,
4669
+ amount_currency,
4670
+ CAST(COUNT(*) AS TEXT) AS model_call_count,
4671
+ CAST(SUM(CASE WHEN reporting_status <> 'unavailable' THEN 1 ELSE 0 END) AS TEXT)
4672
+ AS reported_usage_call_count,
4673
+ CAST(COUNT(input_tokens) AS TEXT) AS input_tokens_count,
4674
+ CAST(SUM(input_tokens) AS TEXT) AS input_tokens_sum,
4675
+ CAST(COUNT(output_tokens) AS TEXT) AS output_tokens_count,
4676
+ CAST(SUM(output_tokens) AS TEXT) AS output_tokens_sum,
4677
+ CAST(COUNT(uncached_input_tokens) AS TEXT) AS uncached_input_tokens_count,
4678
+ CAST(SUM(uncached_input_tokens) AS TEXT) AS uncached_input_tokens_sum,
4679
+ CAST(COUNT(cache_read_input_tokens) AS TEXT) AS cache_read_input_tokens_count,
4680
+ CAST(SUM(cache_read_input_tokens) AS TEXT) AS cache_read_input_tokens_sum,
4681
+ CAST(COUNT(cache_write_input_tokens) AS TEXT) AS cache_write_input_tokens_count,
4682
+ CAST(SUM(cache_write_input_tokens) AS TEXT) AS cache_write_input_tokens_sum,
4683
+ CAST(COUNT(text_output_tokens) AS TEXT) AS text_output_tokens_count,
4684
+ CAST(SUM(text_output_tokens) AS TEXT) AS text_output_tokens_sum,
4685
+ CAST(COUNT(reasoning_output_tokens) AS TEXT) AS reasoning_output_tokens_count,
4686
+ CAST(SUM(reasoning_output_tokens) AS TEXT) AS reasoning_output_tokens_sum,
4687
+ CAST(SUM(CASE WHEN valuation_status = 'rated' THEN 1 ELSE 0 END) AS TEXT)
4688
+ AS rated_call_count,
4689
+ CAST(SUM(CASE WHEN valuation_status = 'unpriceable' THEN 1 ELSE 0 END) AS TEXT)
4690
+ AS unpriceable_call_count,
4691
+ CAST(SUM(CASE WHEN valuation_status = 'unvalued' THEN 1 ELSE 0 END) AS TEXT)
4692
+ AS unvalued_call_count,
4693
+ CAST(SUM(amount_nanos / 1000000000000) AS TEXT) AS amount_high,
4694
+ CAST(SUM((amount_nanos / 1000000) % 1000000) AS TEXT) AS amount_middle,
4695
+ CAST(SUM(amount_nanos % 1000000) AS TEXT) AS amount_low
4696
+ FROM expanded
4697
+ GROUP BY dimension, key_1, key_2, bucket_start, amount_currency
4698
+ `;
4699
+ function accountingListFilterParameters(query) {
4700
+ return [
4701
+ query.projectId,
4702
+ query.from.toISOString(),
4703
+ query.to.toISOString(),
4704
+ query.providerId ?? null,
4705
+ query.providerId ?? null,
4706
+ query.modelId ?? null,
4707
+ query.modelId ?? null,
4708
+ query.executionId ?? null,
4709
+ query.executionId ?? null,
4710
+ query.valuationStatus ?? null,
4711
+ query.valuationStatus ?? null
4712
+ ];
4713
+ }
4714
+ function aggregateFragmentFromRow(row) {
4715
+ 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)) } : {};
4716
+ return {
4717
+ dimension: row.dimension,
4718
+ ...dimensionKeys,
4719
+ modelCallCount: row.model_call_count,
4720
+ usage: {
4721
+ reportedCallCount: row.reported_usage_call_count,
4722
+ inputTokens: meterFragment(row.input_tokens_count, row.input_tokens_sum),
4723
+ outputTokens: meterFragment(row.output_tokens_count, row.output_tokens_sum),
4724
+ uncachedInputTokens: meterFragment(row.uncached_input_tokens_count, row.uncached_input_tokens_sum),
4725
+ cacheReadInputTokens: meterFragment(row.cache_read_input_tokens_count, row.cache_read_input_tokens_sum),
4726
+ cacheWriteInputTokens: meterFragment(row.cache_write_input_tokens_count, row.cache_write_input_tokens_sum),
4727
+ textOutputTokens: meterFragment(row.text_output_tokens_count, row.text_output_tokens_sum),
4728
+ reasoningOutputTokens: meterFragment(row.reasoning_output_tokens_count, row.reasoning_output_tokens_sum)
4729
+ },
4730
+ costs: {
4731
+ ...row.amount_currency === null || row.amount_high === null || row.amount_middle === null || row.amount_low === null ? {} : {
4732
+ amount: {
4733
+ currency: row.amount_currency,
4734
+ amountNanos: sqliteAggregateAmount(row)
4735
+ }
4736
+ },
4737
+ ratedCallCount: row.rated_call_count,
4738
+ unpriceableCallCount: row.unpriceable_call_count,
4739
+ unvaluedCallCount: row.unvalued_call_count
4740
+ }
4741
+ };
4742
+ }
4743
+ function meterFragment(presentCallCount, total) {
4744
+ return { presentCallCount, total };
4745
+ }
4746
+ function sqliteAggregateAmount(row) {
4747
+ return (BigInt(required(row.amount_high)) * 1000000000000n + BigInt(required(row.amount_middle)) * 1000000n + BigInt(required(row.amount_low))).toString();
4748
+ }
4749
+ function accountingItemFromRow(row) {
4750
+ const usage = usageFromRow(row, parseStringArray(row.requester_group_ids));
4751
+ const cost = row.cost_status === null ? undefined : costFromJoinedRow(row);
4752
+ const attribution = row.attribution_kind === null ? undefined : row.attribution_kind === "agent" ? {
4753
+ kind: "agent",
4754
+ agentId: required(row.attribution_agent_id),
4755
+ agentRunId: required(row.attribution_agent_run_id),
4756
+ threadId: required(row.attribution_thread_id)
4757
+ } : {
4758
+ kind: "workflowAgent",
4759
+ agentId: required(row.attribution_agent_id),
4760
+ nodeRunId: required(row.attribution_node_run_id),
4761
+ workflowId: required(row.attribution_workflow_id),
4762
+ workflowRunId: required(row.attribution_workflow_run_id)
4763
+ };
4764
+ return {
4765
+ usage,
4766
+ ...attribution ? { attribution } : {},
4767
+ ...cost ? { cost } : {}
4768
+ };
4769
+ }
4770
+ function usageFromRow(row, requesterGroupIds) {
4771
+ return {
4772
+ id: row.id,
4773
+ projectId: row.project_id,
4774
+ executionId: row.execution_id,
4775
+ attempt: row.attempt,
4776
+ callId: row.call_id,
4777
+ requesterGroupIds,
4778
+ providerId: row.provider_id,
4779
+ requestedModelId: row.requested_model_id,
4780
+ ...row.response_model_id === null ? {} : { responseModelId: row.response_model_id },
4781
+ responseId: row.response_id,
4782
+ usage: {
4783
+ ...row.input_tokens === null ? {} : { inputTokens: row.input_tokens },
4784
+ ...row.output_tokens === null ? {} : { outputTokens: row.output_tokens },
4785
+ ...row.total_tokens === null ? {} : { totalTokens: row.total_tokens },
4786
+ ...row.uncached_input_tokens === null ? {} : { uncachedInputTokens: row.uncached_input_tokens },
4787
+ ...row.cache_read_input_tokens === null ? {} : { cacheReadInputTokens: row.cache_read_input_tokens },
4788
+ ...row.cache_write_input_tokens === null ? {} : { cacheWriteInputTokens: row.cache_write_input_tokens },
4789
+ ...row.text_output_tokens === null ? {} : { textOutputTokens: row.text_output_tokens },
4790
+ ...row.reasoning_output_tokens === null ? {} : { reasoningOutputTokens: row.reasoning_output_tokens },
4791
+ reportingStatus: row.reporting_status
4792
+ },
4793
+ ...row.raw_usage === null ? {} : { rawUsage: JSON.parse(row.raw_usage) },
4794
+ occurredAt: new Date(row.occurred_at),
4795
+ recordedAt: new Date(row.recorded_at)
4796
+ };
4797
+ }
4798
+ function rowToCost(row) {
4799
+ return costRecord({
4800
+ projectId: row.project_id,
4801
+ usageId: row.usage_record_id,
4802
+ status: row.status,
4803
+ providerId: row.provider_id,
4804
+ modelId: row.model_id,
4805
+ currency: row.currency,
4806
+ amountNanos: row.amount_nanos,
4807
+ reason: row.reason,
4808
+ details: row.details,
4809
+ ratedAt: row.rated_at
4810
+ });
4811
+ }
4812
+ function costFromJoinedRow(row) {
4813
+ return costRecord({
4814
+ projectId: row.project_id,
4815
+ usageId: row.id,
4816
+ status: required(row.cost_status),
4817
+ providerId: row.cost_provider_id,
4818
+ modelId: row.cost_model_id,
4819
+ currency: row.cost_currency,
4820
+ amountNanos: row.cost_amount_nanos,
4821
+ reason: row.cost_reason,
4822
+ details: required(row.cost_details),
4823
+ ratedAt: required(row.cost_rated_at)
4824
+ });
4825
+ }
4826
+ function costRecord(input) {
4827
+ const details = parseAiModelCallCostDetails(input.details);
4828
+ const base = {
4829
+ projectId: input.projectId,
4830
+ usageRecordId: input.usageId,
4831
+ pricingContext: details.pricingContext,
4832
+ priceSource: {
4833
+ ...details.priceSource,
4834
+ observedAt: new Date(details.priceSource.observedAt)
4835
+ },
4836
+ ratedAt: new Date(input.ratedAt)
4837
+ };
4838
+ if (input.status === "rated") {
4839
+ return normalizeAiModelCallCostRecord({
4840
+ ...base,
4841
+ status: "rated",
4842
+ billingIdentity: { providerId: required(input.providerId), modelId: required(input.modelId) },
4843
+ money: { currency: required(input.currency), amountNanos: required(input.amountNanos) },
4844
+ components: required(details.components)
4845
+ });
4846
+ }
4847
+ return normalizeAiModelCallCostRecord({
4848
+ ...base,
4849
+ status: "unpriceable",
4850
+ ...input.providerId && input.modelId ? { billingIdentity: { providerId: input.providerId, modelId: input.modelId } } : {},
4851
+ reason: required(input.reason),
4852
+ ...details.missingMeters ? { missingMeters: details.missingMeters } : {}
4853
+ });
4854
+ }
4855
+ function aggregateSummaryRows(ids, rows) {
4856
+ const map = new Map(ids.map((id) => [id, accumulator()]));
4857
+ for (const row of rows) {
4858
+ const value = map.get(row.execution_id);
4859
+ if (!value)
4860
+ continue;
4861
+ if (row.status === null)
4862
+ value.unvaluedCallCount += 1;
4863
+ else if (row.status === "unpriceable")
4864
+ value.unpriceableCallCount += 1;
4865
+ else {
4866
+ value.ratedCallCount += 1;
4867
+ add(value.amounts, required(row.currency), required(row.amount_nanos));
4868
+ }
4869
+ }
4870
+ return ids.map((id) => finish(map.get(id)));
4871
+ }
4872
+ function accumulator() {
4873
+ return {
4874
+ amounts: new Map,
4875
+ ratedCallCount: 0,
4876
+ unpriceableCallCount: 0,
4877
+ unvaluedCallCount: 0
4878
+ };
4879
+ }
4880
+ function finish(value) {
4881
+ return {
4882
+ amounts: [...value.amounts].sort(([left], [right]) => left.localeCompare(right)).map(([currency, amount]) => ({ currency, amountNanos: amount.toString() })),
4883
+ ratedCallCount: value.ratedCallCount,
4884
+ unpriceableCallCount: value.unpriceableCallCount,
4885
+ unvaluedCallCount: value.unvaluedCallCount
4886
+ };
4887
+ }
4888
+ function add(amounts, currency, amount) {
4889
+ amounts.set(currency, (amounts.get(currency) ?? 0n) + BigInt(amount));
4890
+ }
4891
+ function assertCostMatchesUsage(record, usage) {
4892
+ if (!aiModelCallCostMatchesUsage(record, usage)) {
4893
+ throw new AiCostStorageError("cost_mismatch", `[SixbSqlite] AI valuation for usage '${record.usageRecordId}' does not match its usage record.`);
4894
+ }
4895
+ }
4896
+ function missingUsage(record) {
4897
+ return new AiCostStorageError("missing_usage", `[SixbSqlite] AI usage '${record.usageRecordId}' does not exist.`);
4898
+ }
4899
+ function parseStringArray(value) {
4900
+ const parsed = JSON.parse(value);
4901
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
4902
+ throw new Error("[SixbSqlite] AI requester group ids are invalid.");
4903
+ }
4904
+ return parsed;
4905
+ }
4906
+ function assertNonBlank(value, field) {
4907
+ if (typeof value !== "string" || value.trim().length === 0) {
4908
+ throw new TypeError(`[Sixb] AI ${field} must be nonblank.`);
4909
+ }
4910
+ }
4911
+ function safeSqlCount(value, field) {
4912
+ if (!/^(0|[1-9]\d*)$/.test(value)) {
4913
+ throw new Error(`[SixbSqlite] AI accounting ${field} is invalid.`);
4914
+ }
4915
+ const count = Number(value);
4916
+ if (!Number.isSafeInteger(count)) {
4917
+ throw new TypeError(`[Sixb] AI accounting ${field} exceeds the safe integer range.`);
4918
+ }
4919
+ return count;
4920
+ }
4921
+ function required(value) {
4922
+ if (value === null || value === undefined) {
4923
+ throw new Error("[SixbSqlite] AI valuation column is unexpectedly null.");
4924
+ }
4925
+ return value;
4926
+ }
4927
+
4186
4928
  // src/ai-usage-storage.ts
4187
4929
  import {
4188
4930
  AiUsageStorageError,
@@ -4308,7 +5050,7 @@ class SqliteAiUsageStorage {
4308
5050
  ...row.response_model_id === null ? {} : { responseModelId: row.response_model_id },
4309
5051
  responseId: row.response_id,
4310
5052
  usage: {
4311
- ...usageFromRow(row),
5053
+ ...usageFromRow2(row),
4312
5054
  ...row.total_tokens === null ? {} : { totalTokens: row.total_tokens },
4313
5055
  reportingStatus: row.reporting_status
4314
5056
  },
@@ -4342,7 +5084,7 @@ function aggregateExecutionRows(executionIds, rows) {
4342
5084
  for (const executionId of executionIds)
4343
5085
  usageByExecutionId.set(executionId, []);
4344
5086
  for (const row of rows) {
4345
- usageByExecutionId.get(row.execution_id)?.push(usageFromRow(row));
5087
+ usageByExecutionId.get(row.execution_id)?.push(usageFromRow2(row));
4346
5088
  }
4347
5089
  return executionIds.map((executionId) => {
4348
5090
  const usages = usageByExecutionId.get(executionId) ?? [];
@@ -4352,7 +5094,7 @@ function aggregateExecutionRows(executionIds, rows) {
4352
5094
  };
4353
5095
  });
4354
5096
  }
4355
- function usageFromRow(row) {
5097
+ function usageFromRow2(row) {
4356
5098
  return {
4357
5099
  ...row.input_tokens === null ? {} : { inputTokens: row.input_tokens },
4358
5100
  ...row.output_tokens === null ? {} : { outputTokens: row.output_tokens },
@@ -6582,7 +7324,7 @@ function connectionRunFromRow(row) {
6582
7324
  ...base,
6583
7325
  status: "waiting",
6584
7326
  waitingFor: "provider_authorization",
6585
- expiresAt: new Date(required(row.expires_at, "run expiry"))
7327
+ expiresAt: new Date(required2(row.expires_at, "run expiry"))
6586
7328
  };
6587
7329
  }
6588
7330
  if (row.status === "waiting" && row.waiting_for === "account_selection") {
@@ -6590,28 +7332,28 @@ function connectionRunFromRow(row) {
6590
7332
  ...base,
6591
7333
  status: "waiting",
6592
7334
  waitingFor: "account_selection",
6593
- authorizationId: required(row.authorization_id, "authorization id"),
6594
- expiresAt: new Date(required(row.expires_at, "run expiry"))
7335
+ authorizationId: required2(row.authorization_id, "authorization id"),
7336
+ expiresAt: new Date(required2(row.expires_at, "run expiry"))
6595
7337
  };
6596
7338
  }
6597
7339
  if (row.status === "running") {
6598
7340
  return {
6599
7341
  ...base,
6600
7342
  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")),
7343
+ processingId: required2(row.processing_id, "processing id"),
7344
+ callbackStartedAt: new Date(required2(row.callback_started_at, "callback start")),
7345
+ expiresAt: new Date(required2(row.expires_at, "run expiry")),
6604
7346
  ...row.authorization_id === null ? {} : { authorizationId: row.authorization_id }
6605
7347
  };
6606
7348
  }
6607
- const finishedAt = new Date(required(row.finished_at, "run finish"));
7349
+ const finishedAt = new Date(required2(row.finished_at, "run finish"));
6608
7350
  if (row.status === "succeeded") {
6609
7351
  return {
6610
7352
  ...base,
6611
7353
  status: "succeeded",
6612
- authorizationId: required(row.authorization_id, "authorization id"),
7354
+ authorizationId: required2(row.authorization_id, "authorization id"),
6613
7355
  ...row.cleanup_authorization_id === null ? {} : { cleanupAuthorizationId: row.cleanup_authorization_id },
6614
- connections: parseJson(required(row.connections, "connections")).map(connectionFromSerialized),
7356
+ connections: parseJson(required2(row.connections, "connections")).map(connectionFromSerialized),
6615
7357
  finishedAt
6616
7358
  };
6617
7359
  }
@@ -6619,7 +7361,7 @@ function connectionRunFromRow(row) {
6619
7361
  return {
6620
7362
  ...base,
6621
7363
  status: "failed",
6622
- error: parseJson(required(row.error, "run error")),
7364
+ error: parseJson(required2(row.error, "run error")),
6623
7365
  ...row.authorization_id === null ? {} : { authorizationId: row.authorization_id },
6624
7366
  finishedAt
6625
7367
  };
@@ -6689,20 +7431,20 @@ function authorizationFromRow(row) {
6689
7431
  function mutationFromRow(row) {
6690
7432
  if (row.mutation_id === null)
6691
7433
  return;
6692
- const kind = required(row.mutation_kind, "credential mutation kind");
6693
- const phase = required(row.mutation_phase, "credential mutation phase");
7434
+ const kind = required2(row.mutation_kind, "credential mutation kind");
7435
+ const phase = required2(row.mutation_phase, "credential mutation phase");
6694
7436
  const stagedCredentials = row.staged_credentials === null ? undefined : {
6695
7437
  credentials: parseJson(row.staged_credentials),
6696
7438
  ...row.staged_credential_expires_at === null ? {} : { credentialExpiresAt: new Date(row.staged_credential_expires_at) },
6697
- scopes: parseJson(required(row.staged_scopes, "staged credential scopes"))
7439
+ scopes: parseJson(required2(row.staged_scopes, "staged credential scopes"))
6698
7440
  };
6699
7441
  return {
6700
7442
  id: row.mutation_id,
6701
7443
  kind,
6702
7444
  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")),
7445
+ holderId: required2(row.mutation_holder_id, "credential mutation holder"),
7446
+ expiresAt: new Date(required2(row.mutation_expires_at, "credential mutation expiry")),
7447
+ deadlineAt: new Date(required2(row.mutation_deadline_at, "credential mutation deadline")),
6706
7448
  ...row.mutation_expected_connection_ids === null ? {} : {
6707
7449
  expectedConnectionIds: parseJson(row.mutation_expected_connection_ids)
6708
7450
  },
@@ -6745,7 +7487,7 @@ function iso(value) {
6745
7487
  function optionalIso(value) {
6746
7488
  return value?.toISOString() ?? null;
6747
7489
  }
6748
- function required(value, label) {
7490
+ function required2(value, label) {
6749
7491
  if (value !== null)
6750
7492
  return value;
6751
7493
  throw new Error(`[SixbSqlite] Stored ${label} is missing.`);
@@ -8561,13 +9303,13 @@ class SqliteMaterializationStateReader {
8561
9303
  ORDER BY requested.scope_sort_key, ${linkSortExpression("links")}
8562
9304
  `).iterate(requestedJson, this.projectId);
8563
9305
  for (const row of rows) {
8564
- const accumulator = accumulators.get(row.scope_sort_key);
8565
- if (!accumulator) {
9306
+ const accumulator2 = accumulators.get(row.scope_sort_key);
9307
+ if (!accumulator2) {
8566
9308
  throw new MaterializationConflictError2("effective-state", `Unexpected link scope '${row.scope_sort_key}' returned by storage.`);
8567
9309
  }
8568
9310
  const snapshot = effectiveLinkSnapshot(row);
8569
- appendScopeSnapshot(accumulator, snapshot);
8570
- effective.set(row.scope_sort_key, accumulator.effectiveCount === 1 ? snapshot : null);
9311
+ appendScopeSnapshot(accumulator2, snapshot);
9312
+ effective.set(row.scope_sort_key, accumulator2.effectiveCount === 1 ? snapshot : null);
8571
9313
  }
8572
9314
  return scopes.map(({ source, linkId }) => {
8573
9315
  const key = linkScopeSortKey(source, linkId);
@@ -8927,6 +9669,7 @@ function storedObjectOverride(row) {
8927
9669
  return {
8928
9670
  ref,
8929
9671
  value: parseJson2(row.value),
9672
+ editedAt: parseJson2(row.edited_at),
8930
9673
  lastCommitId: row.last_commit_id,
8931
9674
  updatedAt: row.updated_at
8932
9675
  };
@@ -9416,8 +10159,9 @@ class SqliteMaterializationWriter {
9416
10159
  if (item.expectedLastCommitId === null) {
9417
10160
  try {
9418
10161
  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);
10162
+ project_id, object_type_id, primary_id, value, edited_at,
10163
+ last_commit_id, updated_at
10164
+ ) VALUES (?, ?, ?, json(?), json(?), ?, ?)`).run(projectId, item.ref.objectTypeId, item.ref.primaryId, canonicalJson(item.value), canonicalJson(item.editedAt), item.lastCommitId, item.updatedAt);
9421
10165
  } catch (error) {
9422
10166
  if (isSqliteConstraintError(error)) {
9423
10167
  throw effectiveConflict("Expected object override changed.");
@@ -9427,9 +10171,9 @@ class SqliteMaterializationWriter {
9427
10171
  continue;
9428
10172
  }
9429
10173
  requireChanges(this.db.query(`UPDATE ontology_object_overrides
9430
- SET value = json(?), last_commit_id = ?, updated_at = ?
10174
+ SET value = json(?), edited_at = json(?), last_commit_id = ?, updated_at = ?
9431
10175
  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.");
10176
+ 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
10177
  }
9434
10178
  for (const item of chunk.overrides.objects.deletes) {
9435
10179
  requireChanges(this.db.query(`DELETE FROM ontology_object_overrides
@@ -12984,6 +13728,7 @@ class SqliteStorage {
12984
13728
  executions;
12985
13729
  agents;
12986
13730
  aiUsage;
13731
+ aiCosts;
12987
13732
  actionRuns;
12988
13733
  pipelineRuns;
12989
13734
  syncRuns;
@@ -13030,6 +13775,7 @@ class SqliteStorage {
13030
13775
  this.executions = createOperationScopedFacade(stores.executions, scope);
13031
13776
  this.agents = createAgentOperationScope(stores.agents, scope);
13032
13777
  this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope);
13778
+ this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope);
13033
13779
  this.actionRuns = createOperationScopedFacade(stores.actionRuns, scope);
13034
13780
  this.pipelineRuns = createOperationScopedFacade(stores.pipelineRuns, scope);
13035
13781
  this.timeseries = createOperationScopedFacade(readTimeseries, readScope);
@@ -13149,6 +13895,7 @@ function createSqliteStores(connection, options) {
13149
13895
  executions,
13150
13896
  agents: new SqliteAgentStorage({ connection, executions }),
13151
13897
  aiUsage: new SqliteAiUsageStorage({ connection }),
13898
+ aiCosts: new SqliteAiCostStorage(connection),
13152
13899
  actionRuns: new SqliteActionRunStorage({ connection, executions }),
13153
13900
  pipelineRuns: new SqlitePipelineRunStorage({ connection, executions }),
13154
13901
  timeseries: new SqliteTimeseriesStorage({ connection }),
@@ -13166,4 +13913,4 @@ export {
13166
13913
  SqliteStorage
13167
13914
  };
13168
13915
 
13169
- //# debugId=7215BBCFE4EA2F6564756E2164756E21
13916
+ //# debugId=3913310C430C57B764756E2164756E21