@sixb/sqlite 0.1.6 → 0.1.7

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 (50) hide show
  1. package/dist/agents/rows.d.ts +7 -4
  2. package/dist/agents/rows.d.ts.map +1 -1
  3. package/dist/agents/runs.d.ts +3 -2
  4. package/dist/agents/runs.d.ts.map +1 -1
  5. package/dist/agents/threads.d.ts.map +1 -1
  6. package/dist/ai-cost-groups.d.ts +5 -0
  7. package/dist/ai-cost-groups.d.ts.map +1 -0
  8. package/dist/ai-cost-storage.d.ts +2 -1
  9. package/dist/ai-cost-storage.d.ts.map +1 -1
  10. package/dist/ai-limit-storage.d.ts +35 -0
  11. package/dist/ai-limit-storage.d.ts.map +1 -0
  12. package/dist/ai-usage-storage.d.ts +1 -0
  13. package/dist/ai-usage-storage.d.ts.map +1 -1
  14. package/dist/auth-storage/access-tokens.d.ts +1 -1
  15. package/dist/auth-storage/access-tokens.d.ts.map +1 -1
  16. package/dist/auth-storage/auth-storage.d.ts +4 -1
  17. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  18. package/dist/auth-storage/device-authorizations.d.ts +29 -0
  19. package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
  20. package/dist/auth-storage/rows.d.ts +19 -1
  21. package/dist/auth-storage/rows.d.ts.map +1 -1
  22. package/dist/auth-storage/shared.d.ts +1 -1
  23. package/dist/auth-storage/shared.d.ts.map +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1898 -356
  27. package/dist/index.js.map +18 -15
  28. package/dist/migrations.d.ts.map +1 -1
  29. package/package.json +3 -3
  30. package/src/agents/rows.ts +36 -9
  31. package/src/agents/runs.ts +152 -13
  32. package/src/agents/threads.ts +2 -17
  33. package/src/ai-cost-groups.ts +100 -0
  34. package/src/ai-cost-storage.ts +67 -25
  35. package/src/ai-limit-storage.ts +981 -0
  36. package/src/ai-usage-storage.ts +24 -1
  37. package/src/auth-storage/access-tokens.ts +77 -68
  38. package/src/auth-storage/auth-storage.ts +80 -1
  39. package/src/auth-storage/device-authorizations.ts +153 -0
  40. package/src/auth-storage/rows.ts +41 -0
  41. package/src/auth-storage/shared.ts +1 -0
  42. package/src/index.ts +6 -0
  43. package/src/migrations/029-model-accounting.sql +79 -0
  44. package/src/migrations/030-ai-usage-limits.sql +100 -0
  45. package/src/migrations/031-subagent-runs.sql +231 -0
  46. package/src/migrations/032-conversation-run-spec.sql +78 -0
  47. package/src/migrations/033-retire-agent-definitions.sql +89 -0
  48. package/src/migrations/034-device-authorizations.sql +22 -0
  49. package/src/migrations.ts +18 -0
  50. package/src/workflow-run-storage.ts +8 -8
package/dist/index.js CHANGED
@@ -3015,6 +3015,623 @@ ADD COLUMN edited_at TEXT NOT NULL DEFAULT '{}'
3015
3015
  CHECK (json_valid(edited_at) AND json_type(edited_at) = 'object');
3016
3016
  `;
3017
3017
 
3018
+ // src/migrations/029-model-accounting.sql
3019
+ var _029_model_accounting_default = `-- Call-time model configuration and provider identity. Historical identity remains unknown.
3020
+ ALTER TABLE ai_model_call_usage
3021
+ ADD COLUMN requested_reasoning TEXT
3022
+ CHECK (
3023
+ requested_reasoning IS NULL OR (
3024
+ json_valid(requested_reasoning) AND
3025
+ json_type(requested_reasoning) IN ('text', 'object')
3026
+ )
3027
+ );
3028
+ ALTER TABLE ai_model_call_usage ADD COLUMN provider_ids TEXT
3029
+ CHECK (provider_ids IS NULL OR json_valid(provider_ids));
3030
+
3031
+ -- Upgrade the shipped valuation reason while retaining the selected-cost projection.
3032
+ CREATE TABLE ai_model_call_valuations_next (
3033
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
3034
+ usage_record_id TEXT NOT NULL CHECK (length(trim(usage_record_id)) > 0),
3035
+ status TEXT NOT NULL CHECK (status IN ('rated', 'unpriceable')),
3036
+ provider_id TEXT CHECK (provider_id IS NULL OR length(trim(provider_id)) > 0),
3037
+ model_id TEXT CHECK (model_id IS NULL OR length(trim(model_id)) > 0),
3038
+ currency TEXT CHECK (
3039
+ currency IS NULL OR (length(currency) = 3 AND currency GLOB '[A-Z][A-Z][A-Z]')
3040
+ ),
3041
+ amount_nanos INTEGER CHECK (
3042
+ amount_nanos IS NULL OR (typeof(amount_nanos) = 'integer' AND amount_nanos >= 0)
3043
+ ),
3044
+ reason TEXT CHECK (
3045
+ reason IS NULL OR reason IN (
3046
+ 'missingBillingIdentity',
3047
+ 'missingRateCard',
3048
+ 'missingUsageMeter',
3049
+ 'unsupportedPricingDimension',
3050
+ 'invalidUsageForFormula'
3051
+ )
3052
+ ),
3053
+ details TEXT NOT NULL CHECK (json_valid(details) AND json_type(details) = 'object'),
3054
+ rated_at TEXT NOT NULL CHECK (length(trim(rated_at)) > 0),
3055
+ PRIMARY KEY (project_id, usage_record_id),
3056
+ FOREIGN KEY (project_id, usage_record_id)
3057
+ REFERENCES ai_model_call_usage(project_id, id)
3058
+ ON DELETE RESTRICT,
3059
+ CHECK (
3060
+ (
3061
+ status = 'rated'
3062
+ AND provider_id IS NOT NULL
3063
+ AND model_id IS NOT NULL
3064
+ AND currency IS NOT NULL
3065
+ AND amount_nanos IS NOT NULL
3066
+ AND reason IS NULL
3067
+ )
3068
+ OR
3069
+ (
3070
+ status = 'unpriceable'
3071
+ AND currency IS NULL
3072
+ AND amount_nanos IS NULL
3073
+ AND reason IS NOT NULL
3074
+ AND (
3075
+ (reason = 'missingBillingIdentity' AND provider_id IS NULL AND model_id IS NULL)
3076
+ OR
3077
+ (reason <> 'missingBillingIdentity' AND provider_id IS NOT NULL AND model_id IS NOT NULL)
3078
+ )
3079
+ )
3080
+ )
3081
+ );
3082
+
3083
+ INSERT INTO ai_model_call_valuations_next (
3084
+ project_id, usage_record_id, status, provider_id, model_id,
3085
+ currency, amount_nanos, reason, details, rated_at
3086
+ )
3087
+ SELECT project_id, usage_record_id, status, provider_id, model_id,
3088
+ currency, amount_nanos,
3089
+ CASE WHEN reason = 'missingCatalogEntry' THEN 'missingRateCard' ELSE reason END,
3090
+ details, rated_at
3091
+ FROM ai_model_call_valuations;
3092
+
3093
+ DROP TABLE ai_model_call_valuations;
3094
+ ALTER TABLE ai_model_call_valuations_next RENAME TO ai_model_call_valuations;
3095
+
3096
+ CREATE INDEX idx_ai_model_call_valuations_summary
3097
+ ON ai_model_call_valuations (project_id, status, currency, usage_record_id);
3098
+ `;
3099
+
3100
+ // src/migrations/030-ai-usage-limits.sql
3101
+ var _030_ai_usage_limits_default = `CREATE TABLE ai_usage_limits (
3102
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
3103
+ id TEXT NOT NULL CHECK (length(trim(id)) > 0),
3104
+ subject_type TEXT NOT NULL CHECK (
3105
+ subject_type IN ('project', 'group', 'user', 'serviceAccount')
3106
+ ),
3107
+ subject_id TEXT NOT NULL CHECK (
3108
+ (subject_type = 'project' AND subject_id = '')
3109
+ OR (subject_type <> 'project' AND length(trim(subject_id)) > 0)
3110
+ ),
3111
+ meter TEXT NOT NULL CHECK (meter IN ('tokens.total', 'cost.catalogEstimated')),
3112
+ currency TEXT NOT NULL CHECK (
3113
+ (meter = 'tokens.total' AND currency = '')
3114
+ OR (meter = 'cost.catalogEstimated' AND currency = 'USD')
3115
+ ),
3116
+ limit_amount TEXT NOT NULL CHECK (
3117
+ limit_amount = '0'
3118
+ OR (
3119
+ limit_amount NOT GLOB '*[^0-9]*'
3120
+ AND substr(limit_amount, 1, 1) BETWEEN '1' AND '9'
3121
+ )
3122
+ ),
3123
+ period_kind TEXT NOT NULL CHECK (period_kind = 'calendarMonth'),
3124
+ enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
3125
+ created_at TEXT NOT NULL,
3126
+ updated_at TEXT NOT NULL,
3127
+ PRIMARY KEY (project_id, id),
3128
+ UNIQUE (project_id, subject_type, subject_id, meter, currency)
3129
+ );
3130
+
3131
+ CREATE INDEX idx_ai_usage_limits_project_enabled
3132
+ ON ai_usage_limits (project_id, enabled, subject_type, subject_id, meter, currency);
3133
+
3134
+ CREATE TABLE ai_usage_limit_periods (
3135
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
3136
+ subject_type TEXT NOT NULL CHECK (
3137
+ subject_type IN ('project', 'group', 'user', 'serviceAccount')
3138
+ ),
3139
+ subject_id TEXT NOT NULL CHECK (
3140
+ (subject_type = 'project' AND subject_id = '')
3141
+ OR (subject_type <> 'project' AND length(trim(subject_id)) > 0)
3142
+ ),
3143
+ meter TEXT NOT NULL CHECK (meter IN ('tokens.total', 'cost.catalogEstimated')),
3144
+ currency TEXT NOT NULL CHECK (
3145
+ (meter = 'tokens.total' AND currency = '')
3146
+ OR (meter = 'cost.catalogEstimated' AND currency = 'USD')
3147
+ ),
3148
+ period_start TEXT NOT NULL,
3149
+ period_end TEXT NOT NULL,
3150
+ actual_amount TEXT NOT NULL DEFAULT '0',
3151
+ reserved_amount TEXT NOT NULL DEFAULT '0',
3152
+ unknown_amount TEXT NOT NULL DEFAULT '0',
3153
+ accounting_status TEXT NOT NULL DEFAULT 'complete' CHECK (
3154
+ accounting_status IN ('complete', 'unavailable')
3155
+ ),
3156
+ updated_at TEXT NOT NULL,
3157
+ PRIMARY KEY (
3158
+ project_id, subject_type, subject_id, meter, currency, period_start
3159
+ )
3160
+ );
3161
+
3162
+ CREATE INDEX idx_ai_usage_limit_periods_subject_period
3163
+ ON ai_usage_limit_periods (
3164
+ project_id, subject_type, subject_id, period_start, period_end, meter, currency
3165
+ );
3166
+
3167
+ CREATE TABLE ai_model_call_reservations (
3168
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
3169
+ execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
3170
+ attempt INTEGER NOT NULL CHECK (attempt >= 1),
3171
+ call_id TEXT NOT NULL CHECK (length(trim(call_id)) > 0),
3172
+ buckets TEXT NOT NULL CHECK (json_valid(buckets) AND json_type(buckets) = 'array'),
3173
+ request_key TEXT NOT NULL,
3174
+ period_start TEXT NOT NULL,
3175
+ period_end TEXT NOT NULL,
3176
+ state TEXT NOT NULL CHECK (state IN ('active', 'reconciled', 'unknown')),
3177
+ usage_record_id TEXT,
3178
+ reserved_at TEXT NOT NULL,
3179
+ updated_at TEXT NOT NULL,
3180
+ PRIMARY KEY (project_id, execution_id, attempt, call_id),
3181
+ FOREIGN KEY (project_id, execution_id)
3182
+ REFERENCES executions(project_id, id)
3183
+ ON DELETE RESTRICT,
3184
+ FOREIGN KEY (project_id, usage_record_id)
3185
+ REFERENCES ai_model_call_usage(project_id, id)
3186
+ ON DELETE RESTRICT,
3187
+ CHECK (
3188
+ (
3189
+ state = 'reconciled'
3190
+ AND usage_record_id IS NOT NULL
3191
+ )
3192
+ OR (
3193
+ state <> 'reconciled'
3194
+ AND usage_record_id IS NULL
3195
+ )
3196
+ )
3197
+ );
3198
+
3199
+ CREATE INDEX idx_ai_model_call_reservations_active
3200
+ ON ai_model_call_reservations (project_id, state, period_end, execution_id, attempt, call_id);
3201
+ `;
3202
+
3203
+ // src/migrations/031-subagent-runs.sql
3204
+ var _031_subagent_runs_default = `-- SQLite cannot alter a CHECK constraint in place. Rebuild the execution table to align SQL with
3205
+ -- the durable Agent authority model: managed service account or exact inherited user/disabled.
3206
+ CREATE TABLE executions_v3 (
3207
+ project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
3208
+ id TEXT NOT NULL CHECK (length(trim(id)) > 0),
3209
+ executor_kind TEXT NOT NULL CHECK (
3210
+ executor_kind IN (
3211
+ 'request', 'action', 'pipeline', 'projection', 'rule', 'sync', 'webhook', 'workflow',
3212
+ 'agent', 'kernel'
3213
+ )
3214
+ ),
3215
+ executor_id TEXT NOT NULL CHECK (length(trim(executor_id)) > 0),
3216
+ source_kind TEXT NOT NULL CHECK (
3217
+ source_kind IN ('http', 'webhook', 'schedule', 'event', 'datasetVersion', 'execution')
3218
+ ),
3219
+ source_id TEXT NOT NULL CHECK (length(trim(source_id)) > 0),
3220
+ requested_by_user_id TEXT,
3221
+ requested_by_service_account_id TEXT,
3222
+ correlation_id TEXT NOT NULL CHECK (length(trim(correlation_id)) > 0),
3223
+ parent_execution_id TEXT,
3224
+ authority_kind TEXT NOT NULL CHECK (
3225
+ authority_kind IN ('principal', 'trustedPrimitive', 'kernel', 'disabled')
3226
+ ),
3227
+ authority_user_id TEXT,
3228
+ authority_service_account_id TEXT,
3229
+ authority_session_id TEXT,
3230
+ authority_access_token_id TEXT,
3231
+ authority_primitive_kind TEXT CHECK (
3232
+ authority_primitive_kind IS NULL
3233
+ OR authority_primitive_kind IN (
3234
+ 'action', 'pipeline', 'projection', 'rule', 'sync', 'webhook', 'workflow'
3235
+ )
3236
+ ),
3237
+ authority_primitive_id TEXT,
3238
+ authority_kernel_operation TEXT CHECK (
3239
+ authority_kernel_operation IS NULL OR authority_kernel_operation = 'ontology.recover'
3240
+ ),
3241
+ created_at TEXT NOT NULL,
3242
+ PRIMARY KEY (project_id, id),
3243
+ FOREIGN KEY (project_id, parent_execution_id) REFERENCES executions (project_id, id),
3244
+ FOREIGN KEY (project_id, requested_by_user_id) REFERENCES auth_users (project_id, id),
3245
+ FOREIGN KEY (project_id, requested_by_service_account_id)
3246
+ REFERENCES auth_service_accounts (project_id, id),
3247
+ FOREIGN KEY (project_id, authority_user_id) REFERENCES auth_users (project_id, id),
3248
+ FOREIGN KEY (project_id, authority_service_account_id)
3249
+ REFERENCES auth_service_accounts (project_id, id),
3250
+ FOREIGN KEY (project_id, authority_session_id) REFERENCES auth_sessions (project_id, id),
3251
+ FOREIGN KEY (project_id, authority_access_token_id)
3252
+ REFERENCES auth_access_tokens (project_id, id),
3253
+ CHECK ((requested_by_user_id IS NOT NULL) + (requested_by_service_account_id IS NOT NULL) <= 1),
3254
+ CHECK (
3255
+ (source_kind = 'execution' AND parent_execution_id = source_id)
3256
+ OR (source_kind <> 'execution' AND parent_execution_id IS NULL)
3257
+ ),
3258
+ CHECK (
3259
+ (
3260
+ authority_kind = 'principal'
3261
+ AND (authority_user_id IS NOT NULL) + (authority_service_account_id IS NOT NULL) = 1
3262
+ AND (authority_session_id IS NOT NULL) + (authority_access_token_id IS NOT NULL) <= 1
3263
+ AND (authority_session_id IS NULL OR authority_user_id IS NOT NULL)
3264
+ AND authority_primitive_kind IS NULL
3265
+ AND authority_primitive_id IS NULL
3266
+ AND authority_kernel_operation IS NULL
3267
+ )
3268
+ OR (
3269
+ authority_kind = 'trustedPrimitive'
3270
+ AND authority_user_id IS NULL
3271
+ AND authority_service_account_id IS NULL
3272
+ AND authority_session_id IS NULL
3273
+ AND authority_access_token_id IS NULL
3274
+ AND authority_primitive_kind IS NOT NULL
3275
+ AND authority_primitive_id IS NOT NULL
3276
+ AND length(trim(authority_primitive_id)) > 0
3277
+ AND authority_kernel_operation IS NULL
3278
+ )
3279
+ OR (
3280
+ authority_kind = 'kernel'
3281
+ AND authority_user_id IS NULL
3282
+ AND authority_service_account_id IS NULL
3283
+ AND authority_session_id IS NULL
3284
+ AND authority_access_token_id IS NULL
3285
+ AND authority_primitive_kind IS NULL
3286
+ AND authority_primitive_id IS NULL
3287
+ AND authority_kernel_operation IS NOT NULL
3288
+ )
3289
+ OR (
3290
+ authority_kind = 'disabled'
3291
+ AND authority_user_id IS NULL
3292
+ AND authority_service_account_id IS NULL
3293
+ AND authority_session_id IS NULL
3294
+ AND authority_access_token_id IS NULL
3295
+ AND authority_primitive_kind IS NULL
3296
+ AND authority_primitive_id IS NULL
3297
+ AND authority_kernel_operation IS NULL
3298
+ )
3299
+ ),
3300
+ CHECK (
3301
+ (
3302
+ executor_kind = 'request'
3303
+ AND source_kind = 'http'
3304
+ AND executor_id = source_id
3305
+ AND (
3306
+ (
3307
+ authority_kind = 'principal'
3308
+ AND requested_by_user_id IS authority_user_id
3309
+ AND requested_by_service_account_id IS authority_service_account_id
3310
+ )
3311
+ OR (
3312
+ authority_kind = 'disabled'
3313
+ AND requested_by_user_id IS NULL
3314
+ AND requested_by_service_account_id IS NULL
3315
+ )
3316
+ )
3317
+ )
3318
+ OR (
3319
+ executor_kind IN ('action', 'pipeline', 'projection', 'rule', 'sync', 'webhook', 'workflow')
3320
+ AND authority_kind = 'trustedPrimitive'
3321
+ AND executor_kind = authority_primitive_kind
3322
+ )
3323
+ OR (
3324
+ executor_kind = 'agent'
3325
+ AND source_kind = 'execution'
3326
+ AND (
3327
+ (
3328
+ authority_kind = 'principal'
3329
+ AND authority_service_account_id IS NOT NULL
3330
+ AND authority_session_id IS NULL
3331
+ AND authority_access_token_id IS NULL
3332
+ )
3333
+ OR (
3334
+ authority_kind = 'principal'
3335
+ AND authority_user_id IS NOT NULL
3336
+ AND (authority_session_id IS NOT NULL) + (authority_access_token_id IS NOT NULL) = 1
3337
+ )
3338
+ OR authority_kind = 'disabled'
3339
+ )
3340
+ )
3341
+ OR (executor_kind = 'kernel' AND authority_kind = 'kernel')
3342
+ )
3343
+ );
3344
+
3345
+ INSERT INTO executions_v3 SELECT * FROM executions;
3346
+ DROP TABLE executions;
3347
+ ALTER TABLE executions_v3 RENAME TO executions;
3348
+
3349
+ CREATE TABLE agent_runs_v3 (
3350
+ project_id TEXT NOT NULL,
3351
+ id TEXT NOT NULL,
3352
+ execution_id TEXT NOT NULL,
3353
+ kind TEXT NOT NULL DEFAULT 'conversation' CHECK (kind IN ('conversation', 'subagent')),
3354
+ thread_id TEXT,
3355
+ agent_id TEXT,
3356
+ trigger_message_id TEXT,
3357
+ parent_run_id TEXT,
3358
+ spawn_key TEXT,
3359
+ spec TEXT CHECK (spec IS NULL OR json_valid(spec)),
3360
+ result TEXT CHECK (result IS NULL OR json_valid(result)),
3361
+ requester_group_ids TEXT NOT NULL DEFAULT '[]'
3362
+ CHECK (json_valid(requester_group_ids) AND json_type(requester_group_ids) = 'array'),
3363
+ status TEXT NOT NULL CHECK (
3364
+ status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')
3365
+ ),
3366
+ model_id TEXT,
3367
+ finish_reason TEXT,
3368
+ error TEXT CHECK (error IS NULL OR json_valid(error)),
3369
+ diagnostics TEXT,
3370
+ attempt INTEGER NOT NULL DEFAULT 0 CHECK (attempt >= 0),
3371
+ execution_token TEXT,
3372
+ execution_queue_lease_expires_at TEXT,
3373
+ created_at TEXT NOT NULL,
3374
+ started_at TEXT,
3375
+ completed_at TEXT,
3376
+ PRIMARY KEY (project_id, id),
3377
+ UNIQUE (project_id, execution_id),
3378
+ UNIQUE (project_id, parent_run_id, spawn_key),
3379
+ FOREIGN KEY (project_id, execution_id)
3380
+ REFERENCES executions (project_id, id)
3381
+ ON DELETE RESTRICT,
3382
+ FOREIGN KEY (project_id, parent_run_id)
3383
+ REFERENCES agent_runs_v3 (project_id, id)
3384
+ ON DELETE RESTRICT,
3385
+ CHECK ((execution_token IS NULL) = (execution_queue_lease_expires_at IS NULL)),
3386
+ CHECK (
3387
+ (
3388
+ kind = 'conversation'
3389
+ AND thread_id IS NOT NULL
3390
+ AND agent_id IS NOT NULL
3391
+ AND trigger_message_id IS NOT NULL
3392
+ AND parent_run_id IS NULL
3393
+ AND spawn_key IS NULL
3394
+ AND spec IS NULL
3395
+ AND result IS NULL
3396
+ )
3397
+ OR
3398
+ (
3399
+ kind = 'subagent'
3400
+ AND thread_id IS NULL
3401
+ AND agent_id IS NULL
3402
+ AND trigger_message_id IS NULL
3403
+ AND parent_run_id IS NOT NULL
3404
+ AND spawn_key IS NOT NULL
3405
+ AND spec IS NOT NULL
3406
+ )
3407
+ ),
3408
+ CHECK (kind <> 'subagent' OR status <> 'succeeded' OR result IS NOT NULL)
3409
+ );
3410
+
3411
+ INSERT INTO agent_runs_v3 (
3412
+ project_id, id, execution_id, kind, thread_id, agent_id, trigger_message_id,
3413
+ requester_group_ids, status, model_id, finish_reason, error, diagnostics, attempt,
3414
+ execution_token, execution_queue_lease_expires_at, created_at, started_at, completed_at
3415
+ )
3416
+ SELECT
3417
+ project_id, id, execution_id, 'conversation', thread_id, agent_id, trigger_message_id,
3418
+ requester_group_ids, status, model_id, finish_reason, error, diagnostics, attempt,
3419
+ execution_token, execution_queue_lease_expires_at, created_at, started_at, completed_at
3420
+ FROM agent_runs;
3421
+
3422
+ DROP TABLE agent_runs;
3423
+ ALTER TABLE agent_runs_v3 RENAME TO agent_runs;
3424
+
3425
+ CREATE INDEX idx_agent_runs_project_started
3426
+ ON agent_runs(project_id, COALESCE(started_at, created_at) DESC);
3427
+ CREATE INDEX idx_agent_runs_project_thread_started
3428
+ ON agent_runs(project_id, thread_id, COALESCE(started_at, created_at) DESC);
3429
+ CREATE INDEX idx_agent_runs_project_agent_started
3430
+ ON agent_runs(project_id, agent_id, COALESCE(started_at, created_at) DESC);
3431
+ CREATE INDEX idx_agent_runs_project_status_started
3432
+ ON agent_runs(project_id, status, COALESCE(started_at, created_at) DESC);
3433
+ CREATE INDEX idx_agent_runs_project_parent_started
3434
+ ON agent_runs(project_id, parent_run_id, status, COALESCE(started_at, created_at) DESC);
3435
+ `;
3436
+
3437
+ // src/migrations/032-conversation-run-spec.sql
3438
+ var _032_conversation_run_spec_default = `CREATE TABLE agent_runs_v4 (
3439
+ project_id TEXT NOT NULL,
3440
+ id TEXT NOT NULL,
3441
+ execution_id TEXT NOT NULL,
3442
+ kind TEXT NOT NULL DEFAULT 'conversation' CHECK (kind IN ('conversation', 'subagent')),
3443
+ thread_id TEXT,
3444
+ agent_id TEXT,
3445
+ trigger_message_id TEXT,
3446
+ parent_run_id TEXT,
3447
+ spawn_key TEXT,
3448
+ spec TEXT CHECK (spec IS NULL OR json_valid(spec)),
3449
+ result TEXT CHECK (result IS NULL OR json_valid(result)),
3450
+ requester_group_ids TEXT NOT NULL DEFAULT '[]'
3451
+ CHECK (json_valid(requester_group_ids) AND json_type(requester_group_ids) = 'array'),
3452
+ status TEXT NOT NULL CHECK (
3453
+ status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')
3454
+ ),
3455
+ model_id TEXT,
3456
+ finish_reason TEXT,
3457
+ error TEXT CHECK (error IS NULL OR json_valid(error)),
3458
+ diagnostics TEXT,
3459
+ attempt INTEGER NOT NULL DEFAULT 0 CHECK (attempt >= 0),
3460
+ execution_token TEXT,
3461
+ execution_queue_lease_expires_at TEXT,
3462
+ created_at TEXT NOT NULL,
3463
+ started_at TEXT,
3464
+ completed_at TEXT,
3465
+ PRIMARY KEY (project_id, id),
3466
+ UNIQUE (project_id, execution_id),
3467
+ UNIQUE (project_id, parent_run_id, spawn_key),
3468
+ FOREIGN KEY (project_id, execution_id)
3469
+ REFERENCES executions (project_id, id)
3470
+ ON DELETE RESTRICT,
3471
+ FOREIGN KEY (project_id, parent_run_id)
3472
+ -- Use the final name: Bun's legacy ALTER mode preserves references when foreign keys are off.
3473
+ REFERENCES agent_runs (project_id, id)
3474
+ ON DELETE RESTRICT,
3475
+ CHECK ((execution_token IS NULL) = (execution_queue_lease_expires_at IS NULL)),
3476
+ CHECK (
3477
+ (
3478
+ kind = 'conversation'
3479
+ AND thread_id IS NOT NULL
3480
+ AND agent_id IS NOT NULL
3481
+ AND trigger_message_id IS NOT NULL
3482
+ AND parent_run_id IS NULL
3483
+ AND spawn_key IS NULL
3484
+ AND result IS NULL
3485
+ )
3486
+ OR
3487
+ (
3488
+ kind = 'subagent'
3489
+ AND thread_id IS NULL
3490
+ AND agent_id IS NULL
3491
+ AND trigger_message_id IS NULL
3492
+ AND parent_run_id IS NOT NULL
3493
+ AND spawn_key IS NOT NULL
3494
+ AND spec IS NOT NULL
3495
+ )
3496
+ ),
3497
+ CHECK (kind <> 'subagent' OR status <> 'succeeded' OR result IS NOT NULL)
3498
+ );
3499
+
3500
+ INSERT INTO agent_runs_v4 SELECT * FROM agent_runs;
3501
+ DROP TABLE agent_runs;
3502
+ ALTER TABLE agent_runs_v4 RENAME TO agent_runs;
3503
+
3504
+ CREATE INDEX idx_agent_runs_project_thread_started
3505
+ ON agent_runs (project_id, thread_id, COALESCE(started_at, created_at) DESC, id DESC);
3506
+ CREATE INDEX idx_agent_runs_project_status_created
3507
+ ON agent_runs (project_id, status, created_at, id);
3508
+ CREATE INDEX idx_agent_runs_project_parent_started
3509
+ ON agent_runs (
3510
+ project_id,
3511
+ parent_run_id,
3512
+ status,
3513
+ COALESCE(started_at, created_at) DESC,
3514
+ id DESC
3515
+ );
3516
+ `;
3517
+
3518
+ // src/migrations/033-retire-agent-definitions.sql
3519
+ var _033_retire_agent_definitions_default = `DROP INDEX idx_agent_threads_project_agent_activity;
3520
+ ALTER TABLE workflow_agent_node_runs RENAME COLUMN agent_id TO actor_id;
3521
+ ALTER TABLE agent_threads DROP COLUMN agent_id;
3522
+
3523
+ CREATE TABLE agent_runs_v5 (
3524
+ project_id TEXT NOT NULL,
3525
+ id TEXT NOT NULL,
3526
+ execution_id TEXT NOT NULL,
3527
+ kind TEXT NOT NULL DEFAULT 'conversation' CHECK (kind IN ('conversation', 'subagent')),
3528
+ thread_id TEXT,
3529
+ trigger_message_id TEXT,
3530
+ parent_run_id TEXT,
3531
+ spawn_key TEXT,
3532
+ spec TEXT CHECK (spec IS NULL OR json_valid(spec)),
3533
+ result TEXT CHECK (result IS NULL OR json_valid(result)),
3534
+ requester_group_ids TEXT NOT NULL DEFAULT '[]'
3535
+ CHECK (json_valid(requester_group_ids) AND json_type(requester_group_ids) = 'array'),
3536
+ status TEXT NOT NULL CHECK (
3537
+ status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')
3538
+ ),
3539
+ model_id TEXT,
3540
+ finish_reason TEXT,
3541
+ error TEXT CHECK (error IS NULL OR json_valid(error)),
3542
+ diagnostics TEXT,
3543
+ attempt INTEGER NOT NULL DEFAULT 0 CHECK (attempt >= 0),
3544
+ execution_token TEXT,
3545
+ execution_queue_lease_expires_at TEXT,
3546
+ created_at TEXT NOT NULL,
3547
+ started_at TEXT,
3548
+ completed_at TEXT,
3549
+ PRIMARY KEY (project_id, id),
3550
+ UNIQUE (project_id, execution_id),
3551
+ UNIQUE (project_id, parent_run_id, spawn_key),
3552
+ FOREIGN KEY (project_id, execution_id)
3553
+ REFERENCES executions (project_id, id)
3554
+ ON DELETE RESTRICT,
3555
+ FOREIGN KEY (project_id, parent_run_id)
3556
+ -- Use the final name: Bun's legacy ALTER mode preserves references when foreign keys are off.
3557
+ REFERENCES agent_runs (project_id, id)
3558
+ ON DELETE RESTRICT,
3559
+ CHECK ((execution_token IS NULL) = (execution_queue_lease_expires_at IS NULL)),
3560
+ CHECK (
3561
+ (
3562
+ kind = 'conversation'
3563
+ AND thread_id IS NOT NULL
3564
+ AND trigger_message_id IS NOT NULL
3565
+ AND parent_run_id IS NULL
3566
+ AND spawn_key IS NULL
3567
+ AND result IS NULL
3568
+ )
3569
+ OR
3570
+ (
3571
+ kind = 'subagent'
3572
+ AND thread_id IS NULL
3573
+ AND trigger_message_id IS NULL
3574
+ AND parent_run_id IS NOT NULL
3575
+ AND spawn_key IS NOT NULL
3576
+ AND spec IS NOT NULL
3577
+ )
3578
+ ),
3579
+ CHECK (kind <> 'subagent' OR status <> 'succeeded' OR result IS NOT NULL)
3580
+ );
3581
+
3582
+ INSERT INTO agent_runs_v5 (
3583
+ project_id, id, execution_id, kind, thread_id, trigger_message_id, parent_run_id, spawn_key,
3584
+ spec, result, requester_group_ids, status, model_id, finish_reason, error, diagnostics, attempt,
3585
+ execution_token, execution_queue_lease_expires_at, created_at, started_at, completed_at
3586
+ )
3587
+ SELECT
3588
+ project_id, id, execution_id, kind, thread_id, trigger_message_id, parent_run_id, spawn_key,
3589
+ spec, result, requester_group_ids, status, model_id, finish_reason, error, diagnostics, attempt,
3590
+ execution_token, execution_queue_lease_expires_at, created_at, started_at, completed_at
3591
+ FROM agent_runs;
3592
+
3593
+ DROP TABLE agent_runs;
3594
+ ALTER TABLE agent_runs_v5 RENAME TO agent_runs;
3595
+
3596
+ CREATE INDEX idx_agent_runs_project_thread_started
3597
+ ON agent_runs (project_id, thread_id, COALESCE(started_at, created_at) DESC, id DESC);
3598
+ CREATE INDEX idx_agent_runs_project_status_created
3599
+ ON agent_runs (project_id, status, created_at, id);
3600
+ CREATE INDEX idx_agent_runs_project_parent_started
3601
+ ON agent_runs (
3602
+ project_id,
3603
+ parent_run_id,
3604
+ status,
3605
+ COALESCE(started_at, created_at) DESC,
3606
+ id DESC
3607
+ );
3608
+ `;
3609
+
3610
+ // src/migrations/034-device-authorizations.sql
3611
+ var _034_device_authorizations_default = `CREATE TABLE auth_device_authorizations (
3612
+ project_id TEXT NOT NULL,
3613
+ id TEXT NOT NULL,
3614
+ device_code_hash TEXT NOT NULL,
3615
+ user_code TEXT NOT NULL,
3616
+ client_name TEXT NOT NULL,
3617
+ token_name TEXT NOT NULL,
3618
+ token_expires_at TEXT NOT NULL,
3619
+ status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'denied', 'consumed')),
3620
+ approved_user_id TEXT,
3621
+ approved_session_id TEXT,
3622
+ created_at TEXT NOT NULL,
3623
+ expires_at TEXT NOT NULL,
3624
+ approved_at TEXT,
3625
+ denied_at TEXT,
3626
+ consumed_at TEXT,
3627
+ PRIMARY KEY (project_id, id),
3628
+ UNIQUE (project_id, user_code)
3629
+ );
3630
+
3631
+ CREATE INDEX idx_auth_device_authorizations_pending
3632
+ ON auth_device_authorizations(project_id, status, expires_at);
3633
+ `;
3634
+
3018
3635
  // src/migrations.ts
3019
3636
  var MIGRATIONS_TABLE_SQL = `
3020
3637
  CREATE TABLE IF NOT EXISTS sixb_migrations (
@@ -3077,7 +3694,13 @@ var sqliteStorageMigrations = defineMigrations({
3077
3694
  sqliteSql("025-connector-connections", _025_connector_connections_default),
3078
3695
  sqliteSql("026-ai-cost-accounting", _026_ai_cost_accounting_default),
3079
3696
  sqliteSql("027-agent-context-checkpoints", _027_agent_context_checkpoints_default),
3080
- sqliteSql("028-object-override-edit-times", _028_object_override_edit_times_default)
3697
+ sqliteSql("028-object-override-edit-times", _028_object_override_edit_times_default),
3698
+ sqliteSql("029-model-accounting", _029_model_accounting_default),
3699
+ sqliteSql("030-ai-usage-limits", _030_ai_usage_limits_default),
3700
+ sqliteSql("031-subagent-runs", _031_subagent_runs_default),
3701
+ sqliteSql("032-conversation-run-spec", _032_conversation_run_spec_default),
3702
+ sqliteSql("033-retire-agent-definitions", _033_retire_agent_definitions_default),
3703
+ sqliteSql("034-device-authorizations", _034_device_authorizations_default)
3081
3704
  ]
3082
3705
  });
3083
3706
  function sqliteStoragePath(basePath) {
@@ -3750,7 +4373,6 @@ function rowToThreadRecord(row) {
3750
4373
  return {
3751
4374
  id: row.id,
3752
4375
  projectId: row.project_id,
3753
- agentId: row.agent_id,
3754
4376
  ownerPrincipal: { type: row.owner_principal_type, id: row.owner_principal_id },
3755
4377
  title: row.title ?? undefined,
3756
4378
  status: row.status,
@@ -3762,13 +4384,10 @@ function rowToThreadRecord(row) {
3762
4384
  };
3763
4385
  }
3764
4386
  function rowToRunRecord(row) {
3765
- return {
4387
+ const base = {
3766
4388
  id: row.id,
3767
4389
  projectId: row.project_id,
3768
4390
  executionId: row.execution_id,
3769
- threadId: row.thread_id,
3770
- agentId: row.agent_id,
3771
- triggerMessageId: row.trigger_message_id,
3772
4391
  requesterGroupIds: JSON.parse(row.requester_group_ids),
3773
4392
  status: row.status,
3774
4393
  modelId: row.model_id ?? undefined,
@@ -3784,6 +4403,29 @@ function rowToRunRecord(row) {
3784
4403
  startedAt: row.started_at ? new Date(row.started_at) : undefined,
3785
4404
  completedAt: row.completed_at ? new Date(row.completed_at) : undefined
3786
4405
  };
4406
+ if (row.kind === "conversation") {
4407
+ if (!row.thread_id || !row.trigger_message_id) {
4408
+ throw new Error(`[SixbSqlite] Conversational Agent run '${row.id}' has invalid persisted fields.`);
4409
+ }
4410
+ return {
4411
+ ...base,
4412
+ kind: "conversation",
4413
+ threadId: row.thread_id,
4414
+ triggerMessageId: row.trigger_message_id,
4415
+ ...row.spec === null ? {} : { spec: JSON.parse(row.spec) }
4416
+ };
4417
+ }
4418
+ if (!row.parent_run_id || !row.spawn_key || row.spec === null) {
4419
+ throw new Error(`[SixbSqlite] Subagent run '${row.id}' has invalid persisted fields.`);
4420
+ }
4421
+ return {
4422
+ ...base,
4423
+ kind: "subagent",
4424
+ parentRunId: row.parent_run_id,
4425
+ spawnKey: row.spawn_key,
4426
+ spec: JSON.parse(row.spec),
4427
+ ...row.result === null ? {} : { result: JSON.parse(row.result) }
4428
+ };
3787
4429
  }
3788
4430
  function rowToMessageRecord(row) {
3789
4431
  return {
@@ -4082,7 +4724,13 @@ class SqliteAgentMessageStore {
4082
4724
  }
4083
4725
 
4084
4726
  // src/agents/runs.ts
4085
- import { assertAgentRunExecution } from "@sixb/core/internal/agent-run-storage-provider";
4727
+ import {
4728
+ assertAgentRunExecution,
4729
+ assertConversationAgentRunSpec,
4730
+ assertCreateSubagentRunInput,
4731
+ assertSubagentRunResult,
4732
+ subagentRunMatchesCreateInput
4733
+ } from "@sixb/core/internal/agent-run-storage-provider";
4086
4734
  import { normalizeRequesterGroupIds } from "@sixb/core/internal/auth";
4087
4735
  import { serializeSixbFailure } from "@sixb/core/internal/errors";
4088
4736
  import {
@@ -4152,13 +4800,13 @@ class SqliteAgentRunStore {
4152
4800
  this.executions = executions;
4153
4801
  }
4154
4802
  async create(input) {
4803
+ assertConversationAgentRunSpec(input.spec, "SixbSqlite");
4155
4804
  const createdAt = input.createdAt ?? new Date;
4156
4805
  await assertAgentRunExecution({
4157
4806
  executions: this.executions,
4158
4807
  projectId: input.projectId,
4159
4808
  executionId: input.executionId,
4160
- runId: input.id,
4161
- agentId: input.agentId
4809
+ runId: input.id
4162
4810
  });
4163
4811
  return this.db.transaction(() => {
4164
4812
  const thread = this.db.query("SELECT active_run_id FROM agent_threads WHERE project_id = ? AND id = ?").get(input.projectId, input.threadId);
@@ -4174,15 +4822,16 @@ class SqliteAgentRunStore {
4174
4822
  project_id,
4175
4823
  id,
4176
4824
  execution_id,
4825
+ kind,
4177
4826
  thread_id,
4178
- agent_id,
4179
4827
  trigger_message_id,
4828
+ spec,
4180
4829
  requester_group_ids,
4181
4830
  status,
4182
4831
  attempt,
4183
4832
  created_at
4184
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?)
4185
- `).run(input.projectId, input.id, input.executionId, input.threadId, input.agentId, input.triggerMessageId, JSON.stringify(normalizeRequesterGroupIds(input.requesterGroupIds)), createdAt.toISOString());
4833
+ ) VALUES (?, ?, ?, 'conversation', ?, ?, ?, ?, 'queued', 0, ?)
4834
+ `).run(input.projectId, input.id, input.executionId, input.threadId, input.triggerMessageId, JSON.stringify(input.spec), JSON.stringify(normalizeRequesterGroupIds(input.requesterGroupIds)), createdAt.toISOString());
4186
4835
  } catch (error) {
4187
4836
  if (isUniqueConstraintError2(error)) {
4188
4837
  throw new AgentStorageError3("duplicate_id", `[SixbSqlite] Agent run '${input.id}' already exists for project '${input.projectId}'.`);
@@ -4190,7 +4839,65 @@ class SqliteAgentRunStore {
4190
4839
  throw error;
4191
4840
  }
4192
4841
  this.db.query("UPDATE agent_threads SET active_run_id = ?, updated_at = ? WHERE project_id = ? AND id = ?").run(input.id, createdAt.toISOString(), input.projectId, input.threadId);
4193
- return this.requireRun(input.projectId, input.id);
4842
+ const created = this.requireRun(input.projectId, input.id);
4843
+ if (created.kind !== "conversation") {
4844
+ throw new Error(`[SixbSqlite] Agent run insert '${input.id}' returned the wrong run kind.`);
4845
+ }
4846
+ return created;
4847
+ })();
4848
+ }
4849
+ async createSubagent(input) {
4850
+ assertCreateSubagentRunInput(input, "SixbSqlite");
4851
+ const execution = await assertAgentRunExecution({
4852
+ executions: this.executions,
4853
+ projectId: input.projectId,
4854
+ executionId: input.executionId,
4855
+ runId: input.id
4856
+ });
4857
+ return this.db.transaction(() => {
4858
+ const parentRow = this.db.query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?").get(input.projectId, input.parentRunId);
4859
+ if (!parentRow || parentRow.kind !== "conversation") {
4860
+ throw new AgentStorageError3("run_not_found", `[SixbSqlite] Parent Agent run '${input.parentRunId}' was not found.`);
4861
+ }
4862
+ const existingRow = this.db.query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?").get(input.projectId, input.id);
4863
+ if (existingRow) {
4864
+ const existing = rowToRunRecord(existingRow);
4865
+ if (subagentRunMatchesCreateInput(existing, input))
4866
+ return existing;
4867
+ throw new AgentStorageError3("duplicate_id", `[SixbSqlite] Subagent run '${input.id}' already exists with different immutable inputs.`);
4868
+ }
4869
+ if (parentRow.status !== "running") {
4870
+ throw new AgentStorageError3("invalid_state", `[SixbSqlite] Parent Agent run '${parentRow.id}' is not running (status '${parentRow.status}').`);
4871
+ }
4872
+ if (execution.source.type !== "execution" || execution.source.executionId !== parentRow.execution_id) {
4873
+ throw new AgentStorageError3("invalid_input", `[SixbSqlite] Subagent execution '${execution.id}' is not a child of parent execution '${parentRow.execution_id}'.`);
4874
+ }
4875
+ if (parentRow.execution_token !== input.parentExecutionToken) {
4876
+ throw new AgentStorageError3("execution_lost", `[SixbSqlite] Parent Agent run '${parentRow.id}' is no longer owned by this execution.`);
4877
+ }
4878
+ const count = this.db.query(`SELECT COUNT(*) AS count FROM agent_runs
4879
+ WHERE project_id = ? AND parent_run_id = ? AND status IN ('queued', 'running')`).get(input.projectId, input.parentRunId);
4880
+ if (count.count >= input.maxActiveChildren) {
4881
+ throw new AgentStorageError3("active_child_limit", `[SixbSqlite] Agent run '${parentRow.id}' already has ${count.count} active subagents.`);
4882
+ }
4883
+ try {
4884
+ this.db.query(`
4885
+ INSERT INTO agent_runs (
4886
+ project_id, id, execution_id, kind, parent_run_id, spawn_key, spec,
4887
+ requester_group_ids, status, attempt, created_at
4888
+ ) VALUES (?, ?, ?, 'subagent', ?, ?, ?, ?, 'queued', 0, ?)
4889
+ `).run(input.projectId, input.id, input.executionId, input.parentRunId, input.spawnKey, JSON.stringify(input.spec), parentRow.requester_group_ids, (input.createdAt ?? new Date).toISOString());
4890
+ } catch (error) {
4891
+ if (isUniqueConstraintError2(error)) {
4892
+ throw new AgentStorageError3("duplicate_id", `[SixbSqlite] Subagent run '${input.id}' conflicts with an existing durable spawn.`);
4893
+ }
4894
+ throw error;
4895
+ }
4896
+ const created = this.requireRun(input.projectId, input.id);
4897
+ if (created.kind !== "subagent") {
4898
+ throw new Error(`[SixbSqlite] Subagent insert '${input.id}' returned the wrong run kind.`);
4899
+ }
4900
+ return created;
4194
4901
  })();
4195
4902
  }
4196
4903
  async start(input) {
@@ -4260,6 +4967,12 @@ class SqliteAgentRunStore {
4260
4967
  if (run.execution_token !== input.executionToken) {
4261
4968
  throw new AgentStorageError3("execution_lost", `[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`);
4262
4969
  }
4970
+ if (input.status === "succeeded" && run.kind === "subagent") {
4971
+ assertSubagentRunResult(input.result, run.id, "SixbSqlite");
4972
+ }
4973
+ if (input.status === "succeeded" && run.kind === "conversation" && input.result !== undefined) {
4974
+ throw new AgentStorageError3("invalid_input", `[SixbSqlite] Conversational Agent run '${run.id}' cannot persist a subagent result.`);
4975
+ }
4263
4976
  this.db.query(`
4264
4977
  UPDATE agent_runs
4265
4978
  SET
@@ -4268,11 +4981,12 @@ class SqliteAgentRunStore {
4268
4981
  finish_reason = ?,
4269
4982
  error = ?,
4270
4983
  diagnostics = ?,
4984
+ result = ?,
4271
4985
  execution_token = NULL,
4272
4986
  execution_queue_lease_expires_at = NULL,
4273
4987
  completed_at = ?
4274
4988
  WHERE project_id = ? AND id = ?
4275
- `).run(input.status, input.modelId ?? null, input.finishReason ?? null, errorValue, input.diagnostics === undefined ? null : JSON.stringify(input.diagnostics), completedAt.toISOString(), input.projectId, input.id);
4989
+ `).run(input.status, input.modelId ?? null, input.finishReason ?? null, errorValue, input.diagnostics === undefined ? null : JSON.stringify(input.diagnostics), input.status === "succeeded" && input.result !== undefined ? JSON.stringify(input.result) : null, completedAt.toISOString(), input.projectId, input.id);
4276
4990
  this.releaseThread(run, completedAt);
4277
4991
  return this.requireRun(input.projectId, input.id);
4278
4992
  })();
@@ -4299,18 +5013,22 @@ class SqliteAgentRunStore {
4299
5013
  });
4300
5014
  }
4301
5015
  async list(input) {
4302
- if (hasEmptyStatuses(input)) {
5016
+ if (hasEmptyStatuses(input) || input.kinds?.length === 0) {
4303
5017
  return { runs: [], hasMore: false, total: 0 };
4304
5018
  }
4305
5019
  const whereClauses = ["project_id = ?"];
4306
5020
  const args = [input.projectId];
5021
+ if (input.kinds) {
5022
+ whereClauses.push(`kind IN (${input.kinds.map(() => "?").join(", ")})`);
5023
+ args.push(...input.kinds);
5024
+ }
4307
5025
  if (input.threadId) {
4308
5026
  whereClauses.push("thread_id = ?");
4309
5027
  args.push(input.threadId);
4310
5028
  }
4311
- if (input.agentId) {
4312
- whereClauses.push("agent_id = ?");
4313
- args.push(input.agentId);
5029
+ if (input.parentRunId) {
5030
+ whereClauses.push("parent_run_id = ?");
5031
+ args.push(input.parentRunId);
4314
5032
  }
4315
5033
  appendRunListFilters(whereClauses, args, input, "COALESCE(started_at, created_at)");
4316
5034
  const { rows, total, hasMore } = queryRunList({
@@ -4338,6 +5056,8 @@ class SqliteAgentRunStore {
4338
5056
  return row;
4339
5057
  }
4340
5058
  releaseThread(run, completedAt) {
5059
+ if (run.kind !== "conversation")
5060
+ return;
4341
5061
  this.db.query(`
4342
5062
  UPDATE agent_threads
4343
5063
  SET active_run_id = NULL, updated_at = ?
@@ -4370,7 +5090,6 @@ class SqliteAgentThreadStore {
4370
5090
  INSERT INTO agent_threads (
4371
5091
  project_id,
4372
5092
  id,
4373
- agent_id,
4374
5093
  owner_principal_type,
4375
5094
  owner_principal_id,
4376
5095
  title,
@@ -4380,8 +5099,8 @@ class SqliteAgentThreadStore {
4380
5099
  message_count,
4381
5100
  created_at,
4382
5101
  updated_at
4383
- ) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, 0, ?, ?)
4384
- `).run(input.projectId, input.id, input.agentId, input.ownerPrincipal.type, input.ownerPrincipal.id, input.title ?? null, input.status ?? "active", createdAt.toISOString(), updatedAt.toISOString());
5102
+ ) VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, 0, ?, ?)
5103
+ `).run(input.projectId, input.id, input.ownerPrincipal.type, input.ownerPrincipal.id, input.title ?? null, input.status ?? "active", createdAt.toISOString(), updatedAt.toISOString());
4385
5104
  } catch (error) {
4386
5105
  if (isUniqueConstraintError2(error)) {
4387
5106
  throw new AgentStorageError4("duplicate_id", `[SixbSqlite] Agent thread '${input.id}' already exists for project '${input.projectId}'.`);
@@ -4390,93 +5109,391 @@ class SqliteAgentThreadStore {
4390
5109
  }
4391
5110
  return this.requireThread(input.projectId, input.id);
4392
5111
  }
4393
- async getById(params) {
4394
- const row = this.db.query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?").get(params.projectId, params.id);
4395
- return row ? rowToThreadRecord(row) : null;
5112
+ async getById(params) {
5113
+ const row = this.db.query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?").get(params.projectId, params.id);
5114
+ return row ? rowToThreadRecord(row) : null;
5115
+ }
5116
+ async list(input) {
5117
+ if (input.statuses !== undefined && input.statuses.length === 0) {
5118
+ return { threads: [], hasMore: false, total: 0 };
5119
+ }
5120
+ const whereClauses = ["project_id = ?"];
5121
+ const args = [input.projectId];
5122
+ if (input.statuses) {
5123
+ whereClauses.push(`status IN (${input.statuses.map(() => "?").join(", ")})`);
5124
+ args.push(...input.statuses);
5125
+ }
5126
+ if (input.ownerPrincipal) {
5127
+ whereClauses.push("owner_principal_type = ? AND owner_principal_id = ?");
5128
+ args.push(input.ownerPrincipal.type, input.ownerPrincipal.id);
5129
+ }
5130
+ const dir = input.order === "asc" ? "ASC" : "DESC";
5131
+ const { rows, total, hasMore } = queryAgentList({
5132
+ db: this.db,
5133
+ table: "agent_threads",
5134
+ whereClauses,
5135
+ args,
5136
+ orderBy: `COALESCE(last_message_at, created_at) ${dir}, id ${dir}`,
5137
+ limit: input.limit,
5138
+ offset: input.offset
5139
+ });
5140
+ return { threads: rows.map(rowToThreadRecord), hasMore, total };
5141
+ }
5142
+ requireThread(projectId, id) {
5143
+ const row = this.db.query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?").get(projectId, id);
5144
+ if (!row) {
5145
+ throw new AgentStorageError4("invalid_state", `[SixbSqlite] Failed to load agent thread '${id}' for project '${projectId}'.`);
5146
+ }
5147
+ return rowToThreadRecord(row);
5148
+ }
5149
+ }
5150
+
5151
+ // src/agents/agent-storage.ts
5152
+ class SqliteAgentStorage {
5153
+ threads;
5154
+ runs;
5155
+ messages;
5156
+ checkpoints;
5157
+ connection;
5158
+ constructor(options) {
5159
+ this.connection = openSqliteStoreConnection(options);
5160
+ if (this.connection.installFreshSchema) {
5161
+ installFreshSqliteSchema(this.connection.db);
5162
+ }
5163
+ this.threads = new SqliteAgentThreadStore(this.connection.db);
5164
+ this.runs = new SqliteAgentRunStore(this.connection.db, options.executions);
5165
+ this.messages = new SqliteAgentMessageStore(this.connection.db);
5166
+ this.checkpoints = new SqliteAgentContextCheckpointStore(this.connection.db);
5167
+ }
5168
+ close() {
5169
+ closeSqliteStoreConnection(this.connection);
5170
+ }
5171
+ }
5172
+ // src/ai-cost-storage.ts
5173
+ import {
5174
+ aiModelCallCostDetails,
5175
+ aiModelCallCostMatchesUsage,
5176
+ buildAiAccountingOverviewFromFragments,
5177
+ normalizeAiAccountingQuery,
5178
+ normalizeAiModelCallAccountingQuery as normalizeAiModelCallAccountingQuery2,
5179
+ normalizeAiModelCallCostRecord,
5180
+ parseAiModelCallCostDetails,
5181
+ toAccountingItem
5182
+ } from "@sixb/core/internal/ai-cost-storage-provider";
5183
+ import { normalizeModelProviderIds as normalizeModelProviderIds2 } from "@sixb/core/models";
5184
+ import {
5185
+ AiCostStorageError
5186
+ } from "@sixb/core/storage";
5187
+
5188
+ // src/ai-cost-groups.ts
5189
+ import {
5190
+ aiModelCallGroupFragmentFromRow,
5191
+ buildAiModelCallGroupsFromFragments,
5192
+ normalizeAiModelCallAccountingQuery
5193
+ } from "@sixb/core/internal/ai-cost-storage-provider";
5194
+ async function listAiModelCallGroups(connection, input) {
5195
+ const query = normalizeAiModelCallAccountingQuery(input);
5196
+ const parameters = [
5197
+ query.projectId,
5198
+ query.from.toISOString(),
5199
+ query.to.toISOString(),
5200
+ query.providerId ?? null,
5201
+ query.modelId ?? null,
5202
+ query.valuationStatus ?? null
5203
+ ];
5204
+ const count = connection.db.query(COUNT_SQL).get(...parameters);
5205
+ const rows = connection.db.query(PAGE_SQL).all(...parameters, query.limit, query.offset);
5206
+ const total = Number(count?.total);
5207
+ if (!Number.isSafeInteger(total) || total < 0)
5208
+ throw new RangeError("[Sixb] Invalid model-call group count.");
5209
+ const items = buildAiModelCallGroupsFromFragments(rows.map(aiModelCallGroupFragmentFromRow));
5210
+ return { items, total, hasMore: query.offset + items.length < total };
5211
+ }
5212
+ var FILTERED_SQL = `
5213
+ WITH filtered AS (
5214
+ SELECT COALESCE(root_agent.execution_id, usage.execution_id) AS root_execution_id,
5215
+ usage.execution_id, usage.provider_id, usage.requested_model_id, usage.occurred_at,
5216
+ usage.total_tokens,
5217
+ COALESCE(cost.status, 'unvalued') AS valuation_status,
5218
+ cost.currency AS amount_currency, cost.amount_nanos,
5219
+ root_agent.id AS root_agent_run_id,
5220
+ root_agent.thread_id AS root_thread_id,
5221
+ direct_agent.kind AS direct_kind,
5222
+ direct_agent.id AS direct_run_id,
5223
+ direct_agent.parent_run_id AS parent_run_id,
5224
+ workflow_node.node_id AS workflow_agent_step_id,
5225
+ workflow_agent.node_run_id AS workflow_node_run_id,
5226
+ workflow_node.workflow_id AS workflow_id,
5227
+ workflow_node.workflow_run_id AS workflow_run_id
5228
+ FROM ai_model_call_usage AS usage
5229
+ LEFT JOIN agent_runs AS direct_agent
5230
+ ON direct_agent.project_id = usage.project_id AND direct_agent.execution_id = usage.execution_id
5231
+ LEFT JOIN agent_runs AS root_agent
5232
+ ON root_agent.project_id = usage.project_id
5233
+ AND root_agent.id = COALESCE(direct_agent.parent_run_id, direct_agent.id)
5234
+ LEFT JOIN workflow_agent_node_runs AS workflow_agent
5235
+ ON workflow_agent.project_id = usage.project_id AND workflow_agent.execution_id = usage.execution_id
5236
+ LEFT JOIN workflow_node_runs AS workflow_node
5237
+ ON workflow_node.project_id = workflow_agent.project_id AND workflow_node.id = workflow_agent.node_run_id
5238
+ LEFT JOIN ai_model_call_valuations AS cost
5239
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
5240
+ WHERE usage.project_id = ?1
5241
+ AND usage.occurred_at >= ?2
5242
+ AND usage.occurred_at < ?3
5243
+ AND (?4 IS NULL OR usage.provider_id = ?4)
5244
+ AND (?5 IS NULL OR usage.requested_model_id = ?5)
5245
+ AND (?6 IS NULL OR COALESCE(cost.status, 'unvalued') = ?6)
5246
+ ), roots AS (
5247
+ SELECT root_execution_id, MAX(occurred_at) AS last_call_at
5248
+ FROM filtered GROUP BY root_execution_id
5249
+ )
5250
+ `;
5251
+ var COUNT_SQL = `${FILTERED_SQL} SELECT CAST(COUNT(*) AS TEXT) AS total FROM roots`;
5252
+ var PAGE_SQL = `${FILTERED_SQL}, page AS (
5253
+ SELECT root_execution_id FROM roots
5254
+ ORDER BY last_call_at DESC, root_execution_id ASC LIMIT ?7 OFFSET ?8
5255
+ )
5256
+ SELECT filtered.root_execution_id, execution_id, provider_id, requested_model_id,
5257
+ MIN(occurred_at) AS first_call_at, MAX(occurred_at) AS last_call_at,
5258
+ CAST(COUNT(*) AS TEXT) AS call_count,
5259
+ CAST(COUNT(total_tokens) AS TEXT) AS token_count,
5260
+ CAST(SUM(total_tokens) AS TEXT) AS total_tokens,
5261
+ valuation_status, amount_currency,
5262
+ CAST(SUM(amount_nanos / 1000000000000) AS TEXT) AS amount_high,
5263
+ CAST(SUM((amount_nanos / 1000000) % 1000000) AS TEXT) AS amount_middle,
5264
+ CAST(SUM(amount_nanos % 1000000) AS TEXT) AS amount_low,
5265
+ MAX(root_agent_run_id) AS root_agent_run_id,
5266
+ MAX(root_thread_id) AS root_thread_id,
5267
+ MAX(direct_kind) AS direct_kind,
5268
+ MAX(direct_run_id) AS direct_run_id,
5269
+ MAX(parent_run_id) AS parent_run_id,
5270
+ MAX(workflow_agent_step_id) AS workflow_agent_step_id,
5271
+ MAX(workflow_node_run_id) AS workflow_node_run_id,
5272
+ MAX(workflow_id) AS workflow_id,
5273
+ MAX(workflow_run_id) AS workflow_run_id
5274
+ FROM filtered JOIN page ON page.root_execution_id = filtered.root_execution_id
5275
+ GROUP BY filtered.root_execution_id, execution_id, provider_id, requested_model_id,
5276
+ valuation_status, amount_currency
5277
+ `;
5278
+
5279
+ // src/ai-usage-storage.ts
5280
+ import { isModelReasoning, normalizeModelProviderIds } from "@sixb/core/models";
5281
+ import {
5282
+ AiUsageStorageError,
5283
+ aggregateAiModelCallUsage,
5284
+ assertAiUsageExecutionId,
5285
+ normalizeAiModelCallRecord
5286
+ } from "@sixb/core/storage";
5287
+ class SqliteAiUsageStorage {
5288
+ connection;
5289
+ constructor(options = {}) {
5290
+ this.connection = openSqliteStoreConnection(options);
5291
+ if (this.connection.installFreshSchema) {
5292
+ installFreshSqliteSchema(this.connection.db);
5293
+ }
5294
+ }
5295
+ async recordModelCall(input) {
5296
+ const record = normalizeAiModelCallRecord(input);
5297
+ return runImmediateTransaction(this.connection.db, () => {
5298
+ const existing = this.findByIdempotencyKey(record);
5299
+ if (existing)
5300
+ return { record: existing, created: false };
5301
+ this.assertExecutionExists(record.projectId, record.executionId);
5302
+ try {
5303
+ this.insertRecord(record);
5304
+ } catch (error) {
5305
+ if (!isUniqueConstraintError2(error))
5306
+ throw error;
5307
+ const concurrent = this.findByIdempotencyKey(record);
5308
+ if (concurrent)
5309
+ return { record: concurrent, created: false };
5310
+ throw new AiUsageStorageError("duplicate_id", `[SixbSqlite] AI usage record '${record.id}' already exists for project '${record.projectId}'.`);
5311
+ }
5312
+ for (const groupId of record.requesterGroupIds) {
5313
+ this.connection.db.query(`
5314
+ INSERT INTO ai_model_call_usage_groups (
5315
+ project_id, usage_record_id, group_id, occurred_at
5316
+ ) VALUES (?, ?, ?, ?)
5317
+ `).run(record.projectId, record.id, groupId, record.occurredAt.toISOString());
5318
+ }
5319
+ return { record: this.requireById(record.projectId, record.id), created: true };
5320
+ });
5321
+ }
5322
+ async summarizeExecution(input) {
5323
+ const [summary] = await this.summarizeExecutions({
5324
+ projectId: input.projectId,
5325
+ executionIds: [input.executionId]
5326
+ });
5327
+ return summary;
5328
+ }
5329
+ async getLatestForExecution(input) {
5330
+ assertNonBlankProjectId(input.projectId);
5331
+ assertAiUsageExecutionId(input.executionId);
5332
+ const row = this.connection.db.query(`
5333
+ SELECT * FROM ai_model_call_usage
5334
+ WHERE project_id = ? AND execution_id = ?
5335
+ ORDER BY occurred_at DESC, id DESC
5336
+ LIMIT 1
5337
+ `).get(input.projectId, input.executionId);
5338
+ return row ? this.rowToRecord(row) : null;
5339
+ }
5340
+ async summarizeExecutions(input) {
5341
+ assertNonBlankProjectId(input.projectId);
5342
+ for (const executionId of input.executionIds)
5343
+ assertAiUsageExecutionId(executionId);
5344
+ if (input.executionIds.length === 0)
5345
+ return [];
5346
+ const rows = this.executionRows(input);
5347
+ return aggregateExecutionRows(input.executionIds, rows);
5348
+ }
5349
+ close() {
5350
+ closeSqliteStoreConnection(this.connection);
5351
+ }
5352
+ insertRecord(record) {
5353
+ this.connection.db.query(`
5354
+ INSERT INTO ai_model_call_usage (
5355
+ project_id,
5356
+ id,
5357
+ execution_id,
5358
+ attempt,
5359
+ call_id,
5360
+ provider_id,
5361
+ provider_ids,
5362
+ requested_model_id,
5363
+ requested_reasoning,
5364
+ response_model_id,
5365
+ response_id,
5366
+ input_tokens,
5367
+ output_tokens,
5368
+ total_tokens,
5369
+ uncached_input_tokens,
5370
+ cache_read_input_tokens,
5371
+ cache_write_input_tokens,
5372
+ text_output_tokens,
5373
+ reasoning_output_tokens,
5374
+ reporting_status,
5375
+ raw_usage,
5376
+ occurred_at,
5377
+ recorded_at
5378
+ ) VALUES (
5379
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
5380
+ )
5381
+ `).run(record.projectId, record.id, record.executionId, record.attempt, record.callId, record.providerId, record.providerIds === undefined ? null : JSON.stringify(record.providerIds), record.requestedModelId, record.requestedReasoning === undefined ? null : JSON.stringify(record.requestedReasoning), record.responseModelId ?? null, record.responseId, record.usage.inputTokens ?? null, record.usage.outputTokens ?? null, record.usage.totalTokens ?? null, record.usage.uncachedInputTokens ?? null, record.usage.cacheReadInputTokens ?? null, record.usage.cacheWriteInputTokens ?? null, record.usage.textOutputTokens ?? null, record.usage.reasoningOutputTokens ?? null, record.usage.reportingStatus, record.rawUsage === undefined ? null : JSON.stringify(record.rawUsage), record.occurredAt.toISOString(), record.recordedAt.toISOString());
5382
+ }
5383
+ findByIdempotencyKey(record) {
5384
+ const row = this.connection.db.query(`
5385
+ SELECT * FROM ai_model_call_usage
5386
+ WHERE project_id = ?
5387
+ AND execution_id = ?
5388
+ AND attempt = ?
5389
+ AND call_id = ?
5390
+ AND response_id = ?
5391
+ `).get(record.projectId, record.executionId, record.attempt, record.callId, record.responseId);
5392
+ return row ? this.rowToRecord(row) : null;
4396
5393
  }
4397
- async list(input) {
4398
- if (input.statuses !== undefined && input.statuses.length === 0 || input.agentIds !== undefined && input.agentIds.length === 0) {
4399
- return { threads: [], hasMore: false, total: 0 };
4400
- }
4401
- const whereClauses = ["project_id = ?"];
4402
- const args = [input.projectId];
4403
- if (input.agentId) {
4404
- whereClauses.push("agent_id = ?");
4405
- args.push(input.agentId);
4406
- }
4407
- if (input.agentIds) {
4408
- whereClauses.push(`agent_id IN (${input.agentIds.map(() => "?").join(", ")})`);
4409
- args.push(...input.agentIds);
4410
- }
4411
- if (input.statuses) {
4412
- whereClauses.push(`status IN (${input.statuses.map(() => "?").join(", ")})`);
4413
- args.push(...input.statuses);
4414
- }
4415
- if (input.ownerPrincipal) {
4416
- whereClauses.push("owner_principal_type = ? AND owner_principal_id = ?");
4417
- args.push(input.ownerPrincipal.type, input.ownerPrincipal.id);
4418
- }
4419
- const dir = input.order === "asc" ? "ASC" : "DESC";
4420
- const { rows, total, hasMore } = queryAgentList({
4421
- db: this.db,
4422
- table: "agent_threads",
4423
- whereClauses,
4424
- args,
4425
- orderBy: `COALESCE(last_message_at, created_at) ${dir}, id ${dir}`,
4426
- limit: input.limit,
4427
- offset: input.offset
4428
- });
4429
- return { threads: rows.map(rowToThreadRecord), hasMore, total };
5394
+ requireById(projectId, id) {
5395
+ const row = this.connection.db.query("SELECT * FROM ai_model_call_usage WHERE project_id = ? AND id = ?").get(projectId, id);
5396
+ if (!row)
5397
+ throw new Error(`[SixbSqlite] AI usage record '${id}' disappeared after insert.`);
5398
+ return this.rowToRecord(row);
4430
5399
  }
4431
- requireThread(projectId, id) {
4432
- const row = this.db.query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?").get(projectId, id);
4433
- if (!row) {
4434
- throw new AgentStorageError4("invalid_state", `[SixbSqlite] Failed to load agent thread '${id}' for project '${projectId}'.`);
4435
- }
4436
- return rowToThreadRecord(row);
5400
+ rowToRecord(row) {
5401
+ const groupRows = this.connection.db.query(`
5402
+ SELECT group_id FROM ai_model_call_usage_groups
5403
+ WHERE project_id = ? AND usage_record_id = ?
5404
+ ORDER BY group_id
5405
+ `).all(row.project_id, row.id);
5406
+ return {
5407
+ id: row.id,
5408
+ projectId: row.project_id,
5409
+ executionId: row.execution_id,
5410
+ attempt: row.attempt,
5411
+ callId: row.call_id,
5412
+ requesterGroupIds: groupRows.map((group) => group.group_id),
5413
+ providerId: row.provider_id,
5414
+ ...row.provider_ids === null ? {} : { providerIds: normalizeModelProviderIds(JSON.parse(row.provider_ids)) },
5415
+ requestedModelId: row.requested_model_id,
5416
+ ...row.requested_reasoning === null ? {} : { requestedReasoning: requestedReasoningFromRow(row.requested_reasoning) },
5417
+ ...row.response_model_id === null ? {} : { responseModelId: row.response_model_id },
5418
+ responseId: row.response_id,
5419
+ usage: {
5420
+ ...usageFromRow(row),
5421
+ ...row.total_tokens === null ? {} : { totalTokens: row.total_tokens },
5422
+ reportingStatus: row.reporting_status
5423
+ },
5424
+ ...row.raw_usage === null ? {} : { rawUsage: JSON.parse(row.raw_usage) },
5425
+ occurredAt: new Date(row.occurred_at),
5426
+ recordedAt: new Date(row.recorded_at)
5427
+ };
5428
+ }
5429
+ executionRows(input) {
5430
+ const requested = JSON.stringify([...new Set(input.executionIds)]);
5431
+ return this.connection.db.query(`
5432
+ WITH requested AS (
5433
+ SELECT DISTINCT value AS execution_id
5434
+ FROM json_each(?)
5435
+ )
5436
+ SELECT usage.* FROM requested
5437
+ JOIN ai_model_call_usage AS usage
5438
+ ON usage.project_id = ?
5439
+ AND usage.execution_id = requested.execution_id
5440
+ `).all(requested, input.projectId);
5441
+ }
5442
+ assertExecutionExists(projectId, executionId) {
5443
+ const execution = this.connection.db.query("SELECT 1 FROM executions WHERE project_id = ? AND id = ?").get(projectId, executionId);
5444
+ if (execution)
5445
+ return;
5446
+ throw new AiUsageStorageError("missing_execution", `[SixbSqlite] AI usage execution '${executionId}' does not exist in project '${projectId}'.`);
4437
5447
  }
4438
5448
  }
4439
-
4440
- // src/agents/agent-storage.ts
4441
- class SqliteAgentStorage {
4442
- threads;
4443
- runs;
4444
- messages;
4445
- checkpoints;
4446
- connection;
4447
- constructor(options) {
4448
- this.connection = openSqliteStoreConnection(options);
4449
- if (this.connection.installFreshSchema) {
4450
- installFreshSqliteSchema(this.connection.db);
4451
- }
4452
- this.threads = new SqliteAgentThreadStore(this.connection.db);
4453
- this.runs = new SqliteAgentRunStore(this.connection.db, options.executions);
4454
- this.messages = new SqliteAgentMessageStore(this.connection.db);
4455
- this.checkpoints = new SqliteAgentContextCheckpointStore(this.connection.db);
5449
+ function aggregateExecutionRows(executionIds, rows) {
5450
+ const usageByExecutionId = new Map;
5451
+ for (const executionId of executionIds)
5452
+ usageByExecutionId.set(executionId, []);
5453
+ for (const row of rows) {
5454
+ usageByExecutionId.get(row.execution_id)?.push(usageFromRow(row));
4456
5455
  }
4457
- close() {
4458
- closeSqliteStoreConnection(this.connection);
5456
+ return executionIds.map((executionId) => {
5457
+ const usages = usageByExecutionId.get(executionId) ?? [];
5458
+ return {
5459
+ modelCallCount: usages.length,
5460
+ usage: aggregateAiModelCallUsage(usages)
5461
+ };
5462
+ });
5463
+ }
5464
+ function usageFromRow(row) {
5465
+ return {
5466
+ ...row.input_tokens === null ? {} : { inputTokens: row.input_tokens },
5467
+ ...row.output_tokens === null ? {} : { outputTokens: row.output_tokens },
5468
+ ...row.uncached_input_tokens === null ? {} : { uncachedInputTokens: row.uncached_input_tokens },
5469
+ ...row.cache_read_input_tokens === null ? {} : { cacheReadInputTokens: row.cache_read_input_tokens },
5470
+ ...row.cache_write_input_tokens === null ? {} : { cacheWriteInputTokens: row.cache_write_input_tokens },
5471
+ ...row.text_output_tokens === null ? {} : { textOutputTokens: row.text_output_tokens },
5472
+ ...row.reasoning_output_tokens === null ? {} : { reasoningOutputTokens: row.reasoning_output_tokens }
5473
+ };
5474
+ }
5475
+ function requestedReasoningFromRow(value) {
5476
+ const parsed = JSON.parse(value);
5477
+ if (!isModelReasoning(parsed)) {
5478
+ throw new Error("[SixbSqlite] Stored AI usage requested reasoning is invalid.");
5479
+ }
5480
+ return parsed;
5481
+ }
5482
+ function assertNonBlankProjectId(projectId) {
5483
+ if (typeof projectId !== "string" || projectId.trim().length === 0) {
5484
+ throw new TypeError("[Sixb] AI usage projectId must be nonblank.");
4459
5485
  }
4460
5486
  }
5487
+
4461
5488
  // 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
5489
  class SqliteAiCostStorage {
4476
5490
  connection;
4477
5491
  constructor(connection) {
4478
5492
  this.connection = connection;
4479
5493
  }
5494
+ async listModelCallGroups(input) {
5495
+ return listAiModelCallGroups(this.connection, input);
5496
+ }
4480
5497
  async recordModelCallCost(input) {
4481
5498
  const record = normalizeAiModelCallCostRecord(input);
4482
5499
  runImmediateTransaction(this.connection.db, () => {
@@ -4522,7 +5539,7 @@ class SqliteAiCostStorage {
4522
5539
  return buildAiAccountingOverviewFromFragments(input, rows.map(aggregateFragmentFromRow));
4523
5540
  }
4524
5541
  async listModelCalls(input) {
4525
- const query = normalizeAiModelCallAccountingQuery(input);
5542
+ const query = normalizeAiModelCallAccountingQuery2(input);
4526
5543
  const parameters = accountingListFilterParameters(query);
4527
5544
  const count = this.connection.db.query(SQLITE_ACCOUNTING_LIST_COUNT_SQL).get(...parameters);
4528
5545
  const rows = this.connection.db.query(SQLITE_ACCOUNTING_LIST_PAGE_SQL).all(...parameters, query.limit, query.offset);
@@ -4550,7 +5567,7 @@ class SqliteAiCostStorage {
4550
5567
  }
4551
5568
  findUsage(projectId, id) {
4552
5569
  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;
5570
+ return row ? usageFromRow2(row, []) : null;
4554
5571
  }
4555
5572
  }
4556
5573
  var SQLITE_ACCOUNTING_LIST_FILTER = `
@@ -4576,13 +5593,15 @@ var SQLITE_ACCOUNTING_LIST_COUNT_SQL = `
4576
5593
  var SQLITE_ACCOUNTING_LIST_PAGE_SQL = `
4577
5594
  SELECT usage.*,
4578
5595
  CASE
4579
- WHEN direct_agent.id IS NOT NULL THEN 'agent'
5596
+ WHEN direct_agent.kind = 'conversation' THEN 'agent'
5597
+ WHEN direct_agent.kind = 'subagent' THEN 'subagent'
4580
5598
  WHEN workflow_agent.node_run_id IS NOT NULL THEN 'workflowAgent'
4581
5599
  ELSE NULL
4582
5600
  END AS attribution_kind,
4583
- COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
5601
+ workflow_node.node_id AS attribution_agent_step_id,
4584
5602
  direct_agent.id AS attribution_agent_run_id,
4585
5603
  direct_agent.thread_id AS attribution_thread_id,
5604
+ direct_agent.parent_run_id AS attribution_parent_run_id,
4586
5605
  workflow_agent.node_run_id AS attribution_node_run_id,
4587
5606
  workflow_node.workflow_id AS attribution_workflow_id,
4588
5607
  workflow_node.workflow_run_id AS attribution_workflow_run_id,
@@ -4630,7 +5649,12 @@ var SQLITE_ACCOUNTING_OVERVIEW_SQL = `
4630
5649
  usage.text_output_tokens,
4631
5650
  usage.reasoning_output_tokens,
4632
5651
  usage.reporting_status,
4633
- COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
5652
+ CASE
5653
+ WHEN direct_agent.kind = 'conversation' THEN 'agent'
5654
+ WHEN workflow_agent.node_run_id IS NOT NULL THEN 'workflowAgent'
5655
+ ELSE NULL
5656
+ END AS attribution_agent_kind,
5657
+ workflow_node.node_id AS attribution_agent_step_id,
4634
5658
  workflow_node.workflow_id AS attribution_workflow_id,
4635
5659
  CASE
4636
5660
  WHEN cost.status = 'rated' THEN 'rated'
@@ -4655,11 +5679,11 @@ var SQLITE_ACCOUNTING_OVERVIEW_SQL = `
4655
5679
  AND (? IS NULL OR usage.provider_id = ?)
4656
5680
  AND (? IS NULL OR usage.requested_model_id = ?)
4657
5681
  ), expanded AS (
4658
- SELECT 'totals' AS dimension, NULL AS key_1, NULL AS key_2, NULL AS bucket_start,
5682
+ SELECT 'totals' AS dimension, NULL AS key_1, NULL AS key_2, NULL AS key_3, NULL AS bucket_start,
4659
5683
  classified.*
4660
5684
  FROM classified
4661
5685
  UNION ALL
4662
- SELECT 'series', NULL, NULL,
5686
+ SELECT 'series', NULL, NULL, NULL,
4663
5687
  CASE ?
4664
5688
  WHEN 'hour' THEN strftime('%Y-%m-%dT%H:00:00.000Z', occurred_at)
4665
5689
  WHEN 'day' THEN strftime('%Y-%m-%dT00:00:00.000Z', occurred_at)
@@ -4672,17 +5696,18 @@ var SQLITE_ACCOUNTING_OVERVIEW_SQL = `
4672
5696
  classified.*
4673
5697
  FROM classified
4674
5698
  UNION ALL
4675
- SELECT 'model', provider_id, requested_model_id, NULL, classified.* FROM classified
5699
+ SELECT 'model', provider_id, requested_model_id, NULL, NULL, classified.* FROM classified
4676
5700
  UNION ALL
4677
- SELECT 'agent', attribution_agent_id, NULL, NULL, classified.*
4678
- FROM classified WHERE attribution_agent_id IS NOT NULL
5701
+ SELECT 'agent', attribution_agent_kind, attribution_workflow_id, attribution_agent_step_id, NULL, classified.*
5702
+ FROM classified WHERE attribution_agent_kind IS NOT NULL
4679
5703
  UNION ALL
4680
- SELECT 'workflow', attribution_workflow_id, NULL, NULL, classified.*
5704
+ SELECT 'workflow', attribution_workflow_id, NULL, NULL, NULL, classified.*
4681
5705
  FROM classified WHERE attribution_workflow_id IS NOT NULL
4682
5706
  )
4683
5707
  SELECT dimension,
4684
5708
  key_1,
4685
5709
  key_2,
5710
+ key_3,
4686
5711
  bucket_start,
4687
5712
  amount_currency,
4688
5713
  CAST(COUNT(*) AS TEXT) AS model_call_count,
@@ -4712,7 +5737,7 @@ var SQLITE_ACCOUNTING_OVERVIEW_SQL = `
4712
5737
  CAST(SUM((amount_nanos / 1000000) % 1000000) AS TEXT) AS amount_middle,
4713
5738
  CAST(SUM(amount_nanos % 1000000) AS TEXT) AS amount_low
4714
5739
  FROM expanded
4715
- GROUP BY dimension, key_1, key_2, bucket_start, amount_currency
5740
+ GROUP BY dimension, key_1, key_2, key_3, bucket_start, amount_currency
4716
5741
  `;
4717
5742
  function accountingListFilterParameters(query) {
4718
5743
  return [
@@ -4730,7 +5755,10 @@ function accountingListFilterParameters(query) {
4730
5755
  ];
4731
5756
  }
4732
5757
  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)) } : {};
5758
+ const dimensionKeys = row.dimension === "model" ? { providerId: required(row.key_1), modelId: required(row.key_2) } : row.dimension === "agent" ? {
5759
+ agentKind: required(row.key_1),
5760
+ ...row.key_1 === "workflowAgent" ? { workflowId: required(row.key_2), agentStepId: required(row.key_3) } : {}
5761
+ } : row.dimension === "workflow" ? { workflowId: required(row.key_1) } : row.dimension === "series" ? { start: new Date(required(row.bucket_start)) } : {};
4734
5762
  return {
4735
5763
  dimension: row.dimension,
4736
5764
  ...dimensionKeys,
@@ -4765,16 +5793,19 @@ function sqliteAggregateAmount(row) {
4765
5793
  return (BigInt(required(row.amount_high)) * 1000000000000n + BigInt(required(row.amount_middle)) * 1000000n + BigInt(required(row.amount_low))).toString();
4766
5794
  }
4767
5795
  function accountingItemFromRow(row) {
4768
- const usage = usageFromRow(row, parseStringArray(row.requester_group_ids));
5796
+ const usage = usageFromRow2(row, parseStringArray(row.requester_group_ids));
4769
5797
  const cost = row.cost_status === null ? undefined : costFromJoinedRow(row);
4770
5798
  const attribution = row.attribution_kind === null ? undefined : row.attribution_kind === "agent" ? {
4771
5799
  kind: "agent",
4772
- agentId: required(row.attribution_agent_id),
4773
5800
  agentRunId: required(row.attribution_agent_run_id),
4774
5801
  threadId: required(row.attribution_thread_id)
5802
+ } : row.attribution_kind === "subagent" ? {
5803
+ kind: "subagent",
5804
+ subagentRunId: required(row.attribution_agent_run_id),
5805
+ parentRunId: required(row.attribution_parent_run_id)
4775
5806
  } : {
4776
5807
  kind: "workflowAgent",
4777
- agentId: required(row.attribution_agent_id),
5808
+ agentStepId: required(row.attribution_agent_step_id),
4778
5809
  nodeRunId: required(row.attribution_node_run_id),
4779
5810
  workflowId: required(row.attribution_workflow_id),
4780
5811
  workflowRunId: required(row.attribution_workflow_run_id)
@@ -4785,7 +5816,7 @@ function accountingItemFromRow(row) {
4785
5816
  ...cost ? { cost } : {}
4786
5817
  };
4787
5818
  }
4788
- function usageFromRow(row, requesterGroupIds) {
5819
+ function usageFromRow2(row, requesterGroupIds) {
4789
5820
  return {
4790
5821
  id: row.id,
4791
5822
  projectId: row.project_id,
@@ -4794,7 +5825,9 @@ function usageFromRow(row, requesterGroupIds) {
4794
5825
  callId: row.call_id,
4795
5826
  requesterGroupIds,
4796
5827
  providerId: row.provider_id,
5828
+ ...row.provider_ids === null ? {} : { providerIds: normalizeModelProviderIds2(JSON.parse(row.provider_ids)) },
4797
5829
  requestedModelId: row.requested_model_id,
5830
+ ...row.requested_reasoning === null ? {} : { requestedReasoning: requestedReasoningFromRow(row.requested_reasoning) },
4798
5831
  ...row.response_model_id === null ? {} : { responseModelId: row.response_model_id },
4799
5832
  responseId: row.response_id,
4800
5833
  usage: {
@@ -4843,14 +5876,12 @@ function costFromJoinedRow(row) {
4843
5876
  }
4844
5877
  function costRecord(input) {
4845
5878
  const details = parseAiModelCallCostDetails(input.details);
5879
+ const priceSource = details.priceSource === undefined ? undefined : { ...details.priceSource, observedAt: new Date(details.priceSource.observedAt) };
4846
5880
  const base = {
5881
+ ...details.estimate === undefined ? {} : { estimate: details.estimate },
4847
5882
  projectId: input.projectId,
4848
5883
  usageRecordId: input.usageId,
4849
5884
  pricingContext: details.pricingContext,
4850
- priceSource: {
4851
- ...details.priceSource,
4852
- observedAt: new Date(details.priceSource.observedAt)
4853
- },
4854
5885
  ratedAt: new Date(input.ratedAt)
4855
5886
  };
4856
5887
  if (input.status === "rated") {
@@ -4858,6 +5889,7 @@ function costRecord(input) {
4858
5889
  ...base,
4859
5890
  status: "rated",
4860
5891
  billingIdentity: { providerId: required(input.providerId), modelId: required(input.modelId) },
5892
+ priceSource: required(priceSource),
4861
5893
  money: { currency: required(input.currency), amountNanos: required(input.amountNanos) },
4862
5894
  components: required(details.components)
4863
5895
  });
@@ -4866,6 +5898,7 @@ function costRecord(input) {
4866
5898
  ...base,
4867
5899
  status: "unpriceable",
4868
5900
  ...input.providerId && input.modelId ? { billingIdentity: { providerId: input.providerId, modelId: input.modelId } } : {},
5901
+ ...priceSource === undefined ? {} : { priceSource },
4869
5902
  reason: required(input.reason),
4870
5903
  ...details.missingMeters ? { missingMeters: details.missingMeters } : {}
4871
5904
  });
@@ -4943,205 +5976,565 @@ function required(value) {
4943
5976
  return value;
4944
5977
  }
4945
5978
 
4946
- // src/ai-usage-storage.ts
5979
+ // src/ai-limit-storage.ts
4947
5980
  import {
4948
- AiUsageStorageError,
4949
- aggregateAiModelCallUsage,
4950
- assertAiUsageExecutionId,
4951
- normalizeAiModelCallRecord
4952
- } from "@sixb/core/storage";
4953
- class SqliteAiUsageStorage {
5981
+ aiLimitAmountKey,
5982
+ aiLimitQuantityFromAmount,
5983
+ aiLimitReservationBuckets,
5984
+ aiLimitReservationRequestKey,
5985
+ aiLimitReservationRequestMatches,
5986
+ aiLimitSubjectFromParts,
5987
+ aiLimitSubjectKey,
5988
+ aiLimitSubjectParts,
5989
+ aiLimitSubjectsFromAccountingEntry,
5990
+ assertAiLimitAccountingMatchesReservation,
5991
+ assertNonBlank as assertNonBlank2,
5992
+ cloneValidDate,
5993
+ normalizeAiLimitAmount,
5994
+ normalizeAiLimitQuantities,
5995
+ normalizeAiModelCallReservationIdentity,
5996
+ normalizeCreateAiLimitPolicy,
5997
+ normalizeReserveAiModelCall,
5998
+ normalizeUpdateAiLimitPolicy,
5999
+ resolveAiLimitActual
6000
+ } from "@sixb/core/internal/ai-limit-storage-provider";
6001
+ import { AiLimitStorageError, aiLimitCalendarMonth } from "@sixb/core/storage";
6002
+ class SqliteAiLimitStorage {
4954
6003
  connection;
4955
6004
  constructor(options = {}) {
4956
6005
  this.connection = openSqliteStoreConnection(options);
4957
- if (this.connection.installFreshSchema) {
6006
+ if (this.connection.installFreshSchema)
4958
6007
  installFreshSqliteSchema(this.connection.db);
4959
- }
4960
6008
  }
4961
- async recordModelCall(input) {
4962
- const record = normalizeAiModelCallRecord(input);
6009
+ async createPolicy(input) {
6010
+ const policy = normalizeCreateAiLimitPolicy(input);
4963
6011
  return runImmediateTransaction(this.connection.db, () => {
4964
- const existing = this.findByIdempotencyKey(record);
4965
- if (existing)
4966
- return { record: existing, created: false };
4967
- this.assertExecutionExists(record.projectId, record.executionId);
6012
+ const subject = aiLimitSubjectParts(policy.subject);
6013
+ const limit = normalizeAiLimitAmount(policy.limit);
4968
6014
  try {
4969
- this.insertRecord(record);
6015
+ this.connection.db.query(`
6016
+ INSERT INTO ai_usage_limits (
6017
+ project_id, id, subject_type, subject_id, meter, currency, limit_amount,
6018
+ period_kind, enabled, created_at, updated_at
6019
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6020
+ `).run(policy.projectId, policy.id, subject.type, subject.id, limit.meter, limit.currency, limit.amount.toString(), policy.period, policy.enabled ? 1 : 0, policy.createdAt.toISOString(), policy.updatedAt.toISOString());
4970
6021
  } catch (error) {
4971
- if (!isUniqueConstraintError2(error))
6022
+ if (!isConstraintError(error))
4972
6023
  throw error;
4973
- const concurrent = this.findByIdempotencyKey(record);
4974
- if (concurrent)
4975
- return { record: concurrent, created: false };
4976
- throw new AiUsageStorageError("duplicate_id", `[SixbSqlite] AI usage record '${record.id}' already exists for project '${record.projectId}'.`);
6024
+ throw duplicatePolicy(policy);
4977
6025
  }
4978
- for (const groupId of record.requesterGroupIds) {
4979
- this.connection.db.query(`
4980
- INSERT INTO ai_model_call_usage_groups (
4981
- project_id, usage_record_id, group_id, occurred_at
4982
- ) VALUES (?, ?, ?, ?)
4983
- `).run(record.projectId, record.id, groupId, record.occurredAt.toISOString());
6026
+ return this.requirePolicy(policy.projectId, policy.id);
6027
+ });
6028
+ }
6029
+ async updatePolicy(input) {
6030
+ const update = normalizeUpdateAiLimitPolicy(input);
6031
+ return runImmediateTransaction(this.connection.db, () => {
6032
+ const existing = this.requirePolicy(update.projectId, update.id);
6033
+ if (update.limit !== undefined)
6034
+ assertSameDimension(existing.limit, update.limit);
6035
+ const limit = normalizeAiLimitAmount(update.limit ?? existing.limit);
6036
+ this.connection.db.query(`
6037
+ UPDATE ai_usage_limits
6038
+ SET limit_amount = ?, enabled = ?, updated_at = ?
6039
+ WHERE project_id = ? AND id = ?
6040
+ `).run(limit.amount.toString(), update.enabled ?? existing.enabled ? 1 : 0, update.updatedAt.toISOString(), update.projectId, update.id);
6041
+ return this.requirePolicy(update.projectId, update.id);
6042
+ });
6043
+ }
6044
+ async deletePolicy(input) {
6045
+ validatePolicyIdentity(input);
6046
+ return runImmediateTransaction(this.connection.db, () => {
6047
+ const result = this.connection.db.query("DELETE FROM ai_usage_limits WHERE project_id = ? AND id = ?").run(input.projectId, input.id);
6048
+ return result.changes > 0;
6049
+ });
6050
+ }
6051
+ async getPolicy(input) {
6052
+ validatePolicyIdentity(input);
6053
+ const row = this.connection.db.query("SELECT * FROM ai_usage_limits WHERE project_id = ? AND id = ?").get(input.projectId, input.id);
6054
+ return row ? policyFromRow(row) : null;
6055
+ }
6056
+ async listPolicies(input) {
6057
+ assertNonBlank2(input.projectId, "projectId");
6058
+ return this.policyRows(input.projectId, input.includeDisabled ?? false).map(policyFromRow);
6059
+ }
6060
+ async listPolicyStatuses(input) {
6061
+ assertNonBlank2(input.projectId, "projectId");
6062
+ const period = aiLimitCalendarMonth(input.at ?? new Date);
6063
+ const existingGroups = input.existingGroupIds === undefined ? undefined : new Set(input.existingGroupIds);
6064
+ return runImmediateTransaction(this.connection.db, () => this.policyRows(input.projectId, input.includeDisabled ?? false).map((row) => this.statusForPolicy(policyFromRow(row), period, existingGroups)));
6065
+ }
6066
+ async reserveModelCall(input) {
6067
+ const request = normalizeReserveAiModelCall(input);
6068
+ return runImmediateTransaction(this.connection.db, () => {
6069
+ const existing = this.findReservation(request.identity);
6070
+ if (existing) {
6071
+ if (!aiLimitReservationRequestMatches(existing, request)) {
6072
+ throw new AiLimitStorageError("reservation_conflict", `[SixbSqlite] AI model-call reservation '${input.callId}' was replayed with different subjects, estimates, or period.`);
6073
+ }
6074
+ if (existing.state !== "active") {
6075
+ return { status: "terminal", reservation: existing, created: false };
6076
+ }
6077
+ return { status: "reserved", reservation: existing, created: false };
4984
6078
  }
4985
- return { record: this.requireById(record.projectId, record.id), created: true };
6079
+ this.assertExecutionExists(request.identity.projectId, request.identity.executionId);
6080
+ const subjectKeys = new Set(request.subjects.map(aiLimitSubjectKey));
6081
+ const estimates = new Map(request.estimates.map((quantity) => {
6082
+ const amount = normalizeAiLimitAmount(quantity);
6083
+ return [aiLimitAmountKey(amount), amount];
6084
+ }));
6085
+ const enabledPolicies = this.policyRows(request.identity.projectId, false).map(policyFromRow);
6086
+ const exhaustedPolicies = [];
6087
+ const unavailablePolicies = [];
6088
+ const unavailableReasons = new Set;
6089
+ for (const policy of enabledPolicies) {
6090
+ if (!subjectKeys.has(aiLimitSubjectKey(policy.subject)))
6091
+ continue;
6092
+ const limit = normalizeAiLimitAmount(policy.limit);
6093
+ const estimate = estimates.get(aiLimitAmountKey(limit));
6094
+ const status = this.statusForPolicy(policy, request.period);
6095
+ if (!estimate || status.accountingStatus === "unavailable") {
6096
+ unavailablePolicies.push(status);
6097
+ if (!estimate)
6098
+ unavailableReasons.add("missingEstimate");
6099
+ if (status.accountingStatus === "unavailable") {
6100
+ unavailableReasons.add("incompleteAccounting");
6101
+ }
6102
+ continue;
6103
+ }
6104
+ const consumption = status.consumption;
6105
+ const consumed = normalizeAiLimitAmount(consumption.actual).amount + normalizeAiLimitAmount(consumption.reserved).amount + normalizeAiLimitAmount(consumption.unknown).amount;
6106
+ if (consumed + estimate.amount > limit.amount)
6107
+ exhaustedPolicies.push(status);
6108
+ }
6109
+ if (unavailablePolicies.length > 0) {
6110
+ unavailablePolicies.sort((left, right) => left.policy.id.localeCompare(right.policy.id));
6111
+ return {
6112
+ status: "unavailable",
6113
+ unavailablePolicies,
6114
+ reasons: [...unavailableReasons].sort()
6115
+ };
6116
+ }
6117
+ if (exhaustedPolicies.length > 0) {
6118
+ exhaustedPolicies.sort((left, right) => left.policy.id.localeCompare(right.policy.id));
6119
+ return {
6120
+ status: "denied",
6121
+ exhaustedPolicies,
6122
+ resetAt: new Date(Math.min(...exhaustedPolicies.map((status) => status.period.resetAt.getTime())))
6123
+ };
6124
+ }
6125
+ const buckets = aiLimitReservationBuckets(enabledPolicies, request.subjects, request.estimates);
6126
+ if (buckets.length === 0)
6127
+ return { status: "notRequired" };
6128
+ for (const bucket of buckets) {
6129
+ const amount = normalizeAiLimitAmount(bucket.estimate);
6130
+ this.changeCounters(request.identity.projectId, bucket.subject, amount, request.period, { reserved: amount.amount }, request.reservedAt);
6131
+ }
6132
+ const reservation = {
6133
+ ...request.identity,
6134
+ buckets,
6135
+ requestKey: aiLimitReservationRequestKey(request),
6136
+ period: request.period,
6137
+ state: "active",
6138
+ reservedAt: request.reservedAt,
6139
+ updatedAt: new Date(request.reservedAt)
6140
+ };
6141
+ this.insertReservation(reservation);
6142
+ return { status: "reserved", reservation: structuredClone(reservation), created: true };
4986
6143
  });
4987
6144
  }
4988
- async summarizeExecution(input) {
4989
- const [summary] = await this.summarizeExecutions({
4990
- projectId: input.projectId,
4991
- executionIds: [input.executionId]
6145
+ async recordModelCallActuals(input) {
6146
+ assertNonBlank2(input.projectId, "projectId");
6147
+ assertNonBlank2(input.usageRecordId, "usageRecordId");
6148
+ const recordedAt = cloneValidDate(input.recordedAt ?? new Date, "recordedAt");
6149
+ runImmediateTransaction(this.connection.db, () => {
6150
+ const entry = this.requireAccountingEntry(input.projectId, input.usageRecordId);
6151
+ const period = aiLimitCalendarMonth(entry.occurredAt);
6152
+ for (const subjectValue of aiLimitSubjectsFromAccountingEntry(entry)) {
6153
+ const subject = aiLimitSubjectParts(subjectValue);
6154
+ const rows = this.connection.db.query(`
6155
+ SELECT meter, currency, actual_amount, accounting_status
6156
+ FROM ai_usage_limit_periods
6157
+ WHERE project_id = ? AND subject_type = ? AND subject_id = ?
6158
+ AND period_start = ?
6159
+ `).all(input.projectId, subject.type, subject.id, period.start.toISOString());
6160
+ for (const row of rows) {
6161
+ const actual = resolveAiLimitActual([entry], row);
6162
+ this.connection.db.query(`
6163
+ UPDATE ai_usage_limit_periods
6164
+ SET actual_amount = ?, accounting_status = ?, updated_at = ?
6165
+ WHERE project_id = ? AND subject_type = ? AND subject_id = ?
6166
+ AND meter = ? AND currency = ? AND period_start = ?
6167
+ `).run((BigInt(row.actual_amount) + actual.amount).toString(), row.accounting_status === "unavailable" || actual.accountingStatus === "unavailable" ? "unavailable" : "complete", recordedAt.toISOString(), input.projectId, subject.type, subject.id, row.meter, row.currency, period.start.toISOString());
6168
+ }
6169
+ }
4992
6170
  });
4993
- return summary;
4994
6171
  }
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;
6172
+ async reconcileModelCall(input) {
6173
+ const identity = normalizeAiModelCallReservationIdentity(input);
6174
+ assertNonBlank2(input.usageRecordId, "usageRecordId");
6175
+ const reconciledAt = cloneValidDate(input.reconciledAt ?? new Date, "reconciledAt");
6176
+ return runImmediateTransaction(this.connection.db, () => {
6177
+ const reservation = this.findReservation(identity);
6178
+ if (!reservation)
6179
+ throw missingReservation(identity);
6180
+ if (reservation.state === "reconciled") {
6181
+ if (reservation.usageRecordId === input.usageRecordId) {
6182
+ return reservation;
6183
+ }
6184
+ throw reconciliationConflict(identity.callId);
6185
+ }
6186
+ const accounting = this.requireAccountingEntry(identity.projectId, input.usageRecordId);
6187
+ assertAiLimitAccountingMatchesReservation(reservation, accounting);
6188
+ this.reconcileEstimateCounters(reservation, accounting, reconciledAt);
6189
+ const reconciled = {
6190
+ ...reservation,
6191
+ state: "reconciled",
6192
+ usageRecordId: input.usageRecordId,
6193
+ updatedAt: reconciledAt
6194
+ };
6195
+ this.updateReservation(reconciled);
6196
+ return structuredClone(reconciled);
6197
+ });
5005
6198
  }
5006
- async summarizeExecutions(input) {
5007
- assertNonBlankProjectId(input.projectId);
5008
- for (const executionId of input.executionIds)
5009
- assertAiUsageExecutionId(executionId);
5010
- if (input.executionIds.length === 0)
5011
- return [];
5012
- const rows = this.executionRows(input);
5013
- return aggregateExecutionRows(input.executionIds, rows);
6199
+ async markReservationUnknown(input) {
6200
+ const identity = normalizeAiModelCallReservationIdentity(input);
6201
+ const markedAt = cloneValidDate(input.markedAt ?? new Date, "markedAt");
6202
+ return runImmediateTransaction(this.connection.db, () => {
6203
+ const reservation = this.findReservation(identity);
6204
+ if (!reservation)
6205
+ throw missingReservation(identity);
6206
+ if (reservation.state === "unknown")
6207
+ return reservation;
6208
+ if (reservation.state !== "active")
6209
+ throw invalidState(reservation, "mark unknown");
6210
+ this.moveEstimateCounters(reservation, { reserved: -1n, unknown: 1n }, markedAt);
6211
+ const unknown = { ...reservation, state: "unknown", updatedAt: markedAt };
6212
+ this.updateReservation(unknown);
6213
+ return structuredClone(unknown);
6214
+ });
5014
6215
  }
5015
6216
  close() {
5016
6217
  closeSqliteStoreConnection(this.connection);
5017
6218
  }
5018
- insertRecord(record) {
6219
+ policyRows(projectId, includeDisabled) {
6220
+ return this.connection.db.query(`
6221
+ SELECT * FROM ai_usage_limits
6222
+ WHERE project_id = ? AND (? = 1 OR enabled = 1)
6223
+ ORDER BY id
6224
+ `).all(projectId, includeDisabled ? 1 : 0);
6225
+ }
6226
+ requirePolicy(projectId, id) {
6227
+ const row = this.connection.db.query("SELECT * FROM ai_usage_limits WHERE project_id = ? AND id = ?").get(projectId, id);
6228
+ if (row)
6229
+ return policyFromRow(row);
6230
+ throw new AiLimitStorageError("missing_policy", `[SixbSqlite] AI limit policy '${id}' does not exist in project '${projectId}'.`);
6231
+ }
6232
+ statusForPolicy(policy, period, existingGroups) {
6233
+ const subject = aiLimitSubjectParts(policy.subject);
6234
+ const limit = normalizeAiLimitAmount(policy.limit);
6235
+ let row = this.connection.db.query(`
6236
+ SELECT actual_amount, reserved_amount, unknown_amount, accounting_status
6237
+ FROM ai_usage_limit_periods
6238
+ WHERE project_id = ? AND subject_type = ? AND subject_id = ?
6239
+ AND meter = ? AND currency = ? AND period_start = ?
6240
+ `).get(policy.projectId, subject.type, subject.id, limit.meter, limit.currency, period.start.toISOString());
6241
+ if (!row || row.accounting_status === "unavailable") {
6242
+ const actual = resolveAiLimitActual(this.accountingEntries(policy, period), limit);
6243
+ row = {
6244
+ actual_amount: actual.amount.toString(),
6245
+ reserved_amount: row?.reserved_amount ?? "0",
6246
+ unknown_amount: row?.unknown_amount ?? "0",
6247
+ accounting_status: actual.accountingStatus
6248
+ };
6249
+ this.connection.db.query(`
6250
+ INSERT INTO ai_usage_limit_periods (
6251
+ project_id, subject_type, subject_id, meter, currency, period_start, period_end,
6252
+ actual_amount, reserved_amount, unknown_amount, accounting_status, updated_at
6253
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '0', '0', ?, ?)
6254
+ ON CONFLICT (project_id, subject_type, subject_id, meter, currency, period_start)
6255
+ DO UPDATE SET
6256
+ actual_amount = excluded.actual_amount,
6257
+ accounting_status = excluded.accounting_status,
6258
+ updated_at = excluded.updated_at
6259
+ `).run(policy.projectId, subject.type, subject.id, limit.meter, limit.currency, period.start.toISOString(), period.end.toISOString(), row.actual_amount, row.accounting_status, new Date().toISOString());
6260
+ }
6261
+ return policyStatus(policy, period, row, existingGroups);
6262
+ }
6263
+ changeCounters(projectId, subjectValue, amount, period, changes, updatedAt) {
6264
+ const subject = aiLimitSubjectParts(subjectValue);
6265
+ const row = this.connection.db.query(`
6266
+ SELECT actual_amount, reserved_amount, unknown_amount, accounting_status
6267
+ FROM ai_usage_limit_periods
6268
+ WHERE project_id = ? AND subject_type = ? AND subject_id = ?
6269
+ AND meter = ? AND currency = ? AND period_start = ?
6270
+ `).get(projectId, subject.type, subject.id, amount.meter, amount.currency, period.start.toISOString());
6271
+ const reserved = BigInt(row?.reserved_amount ?? "0") + (changes.reserved ?? 0n);
6272
+ const unknown = BigInt(row?.unknown_amount ?? "0") + (changes.unknown ?? 0n);
6273
+ if (reserved < 0n || unknown < 0n) {
6274
+ throw new Error("[SixbSqlite] AI limit period counter became negative.");
6275
+ }
6276
+ this.connection.db.query(`
6277
+ INSERT INTO ai_usage_limit_periods (
6278
+ project_id, subject_type, subject_id, meter, currency, period_start, period_end,
6279
+ actual_amount, reserved_amount, unknown_amount, accounting_status, updated_at
6280
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6281
+ ON CONFLICT (project_id, subject_type, subject_id, meter, currency, period_start)
6282
+ DO UPDATE SET
6283
+ period_end = excluded.period_end,
6284
+ actual_amount = excluded.actual_amount,
6285
+ reserved_amount = excluded.reserved_amount,
6286
+ unknown_amount = excluded.unknown_amount,
6287
+ accounting_status = excluded.accounting_status,
6288
+ updated_at = excluded.updated_at
6289
+ `).run(projectId, subject.type, subject.id, amount.meter, amount.currency, period.start.toISOString(), period.end.toISOString(), row?.actual_amount ?? "0", reserved.toString(), unknown.toString(), row?.accounting_status ?? "complete", updatedAt.toISOString());
6290
+ }
6291
+ moveEstimateCounters(reservation, directions, updatedAt) {
6292
+ for (const bucket of reservation.buckets) {
6293
+ const amount = normalizeAiLimitAmount(bucket.estimate);
6294
+ this.changeCounters(reservation.projectId, bucket.subject, amount, reservation.period, {
6295
+ ...directions.reserved === undefined ? {} : { reserved: directions.reserved * amount.amount },
6296
+ ...directions.unknown === undefined ? {} : { unknown: directions.unknown * amount.amount }
6297
+ }, updatedAt);
6298
+ }
6299
+ }
6300
+ reconcileEstimateCounters(reservation, entry, updatedAt) {
6301
+ for (const bucket of reservation.buckets) {
6302
+ const amount = normalizeAiLimitAmount(bucket.estimate);
6303
+ const actual = resolveAiLimitActual([entry], amount);
6304
+ if (actual.accountingStatus === "complete") {
6305
+ this.changeCounters(reservation.projectId, bucket.subject, amount, reservation.period, reservation.state === "active" ? { reserved: -amount.amount } : { unknown: -amount.amount }, updatedAt);
6306
+ } else if (reservation.state === "active") {
6307
+ this.changeCounters(reservation.projectId, bucket.subject, amount, reservation.period, { reserved: -amount.amount, unknown: amount.amount }, updatedAt);
6308
+ }
6309
+ }
6310
+ }
6311
+ insertReservation(reservation) {
6312
+ this.connection.db.query(`
6313
+ INSERT INTO ai_model_call_reservations (
6314
+ project_id, execution_id, attempt, call_id, buckets, request_key,
6315
+ period_start, period_end, state, usage_record_id, reserved_at, updated_at
6316
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6317
+ `).run(...reservationSqlValues(reservation));
6318
+ }
6319
+ updateReservation(reservation) {
5019
6320
  this.connection.db.query(`
5020
- INSERT INTO ai_model_call_usage (
5021
- project_id,
5022
- id,
5023
- execution_id,
5024
- attempt,
5025
- call_id,
5026
- provider_id,
5027
- requested_model_id,
5028
- response_model_id,
5029
- response_id,
5030
- input_tokens,
5031
- output_tokens,
5032
- total_tokens,
5033
- uncached_input_tokens,
5034
- cache_read_input_tokens,
5035
- cache_write_input_tokens,
5036
- text_output_tokens,
5037
- reasoning_output_tokens,
5038
- reporting_status,
5039
- raw_usage,
5040
- occurred_at,
5041
- recorded_at
5042
- ) VALUES (
5043
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
5044
- )
5045
- `).run(record.projectId, record.id, record.executionId, record.attempt, record.callId, record.providerId, record.requestedModelId, record.responseModelId ?? null, record.responseId, record.usage.inputTokens ?? null, record.usage.outputTokens ?? null, record.usage.totalTokens ?? null, record.usage.uncachedInputTokens ?? null, record.usage.cacheReadInputTokens ?? null, record.usage.cacheWriteInputTokens ?? null, record.usage.textOutputTokens ?? null, record.usage.reasoningOutputTokens ?? null, record.usage.reportingStatus, record.rawUsage === undefined ? null : JSON.stringify(record.rawUsage), record.occurredAt.toISOString(), record.recordedAt.toISOString());
6321
+ UPDATE ai_model_call_reservations
6322
+ SET state = ?, usage_record_id = ?, updated_at = ?
6323
+ WHERE project_id = ? AND execution_id = ? AND attempt = ? AND call_id = ?
6324
+ `).run(reservation.state, reservation.usageRecordId ?? null, reservation.updatedAt.toISOString(), reservation.projectId, reservation.executionId, reservation.attempt, reservation.callId);
5046
6325
  }
5047
- findByIdempotencyKey(record) {
6326
+ findReservation(input) {
5048
6327
  const row = this.connection.db.query(`
5049
- SELECT * FROM ai_model_call_usage
5050
- WHERE project_id = ?
5051
- AND execution_id = ?
5052
- AND attempt = ?
5053
- AND call_id = ?
5054
- AND response_id = ?
5055
- `).get(record.projectId, record.executionId, record.attempt, record.callId, record.responseId);
5056
- return row ? this.rowToRecord(row) : null;
5057
- }
5058
- requireById(projectId, id) {
5059
- const row = this.connection.db.query("SELECT * FROM ai_model_call_usage WHERE project_id = ? AND id = ?").get(projectId, id);
5060
- if (!row)
5061
- throw new Error(`[SixbSqlite] AI usage record '${id}' disappeared after insert.`);
5062
- return this.rowToRecord(row);
6328
+ SELECT * FROM ai_model_call_reservations
6329
+ WHERE project_id = ? AND execution_id = ? AND attempt = ? AND call_id = ?
6330
+ `).get(input.projectId, input.executionId, input.attempt, input.callId);
6331
+ return row ? reservationFromRow(row) : null;
5063
6332
  }
5064
- rowToRecord(row) {
5065
- const groupRows = this.connection.db.query(`
5066
- SELECT group_id FROM ai_model_call_usage_groups
5067
- WHERE project_id = ? AND usage_record_id = ?
5068
- ORDER BY group_id
5069
- `).all(row.project_id, row.id);
5070
- return {
5071
- id: row.id,
5072
- projectId: row.project_id,
5073
- executionId: row.execution_id,
5074
- attempt: row.attempt,
5075
- callId: row.call_id,
5076
- requesterGroupIds: groupRows.map((group) => group.group_id),
5077
- providerId: row.provider_id,
5078
- requestedModelId: row.requested_model_id,
5079
- ...row.response_model_id === null ? {} : { responseModelId: row.response_model_id },
5080
- responseId: row.response_id,
5081
- usage: {
5082
- ...usageFromRow2(row),
5083
- ...row.total_tokens === null ? {} : { totalTokens: row.total_tokens },
5084
- reportingStatus: row.reporting_status
5085
- },
5086
- ...row.raw_usage === null ? {} : { rawUsage: JSON.parse(row.raw_usage) },
5087
- occurredAt: new Date(row.occurred_at),
5088
- recordedAt: new Date(row.recorded_at)
5089
- };
6333
+ accountingEntries(policy, period) {
6334
+ const subject = aiLimitSubjectParts(policy.subject);
6335
+ const rows = this.connection.db.query(`
6336
+ SELECT usage.project_id, usage.id AS usage_record_id, usage.execution_id,
6337
+ usage.attempt, usage.call_id, usage.occurred_at,
6338
+ CAST(usage.total_tokens AS TEXT) AS total_tokens,
6339
+ executions.requested_by_user_id,
6340
+ executions.requested_by_service_account_id,
6341
+ valuations.status AS valuation_status,
6342
+ valuations.currency AS valuation_currency,
6343
+ CAST(valuations.amount_nanos AS TEXT) AS valuation_amount_nanos
6344
+ FROM ai_model_call_usage AS usage
6345
+ JOIN executions
6346
+ ON executions.project_id = usage.project_id AND executions.id = usage.execution_id
6347
+ LEFT JOIN ai_model_call_valuations AS valuations
6348
+ ON valuations.project_id = usage.project_id
6349
+ AND valuations.usage_record_id = usage.id
6350
+ WHERE usage.project_id = ? AND usage.occurred_at >= ? AND usage.occurred_at < ?
6351
+ AND (
6352
+ ? = 'project'
6353
+ OR (
6354
+ ? = 'group'
6355
+ AND EXISTS (
6356
+ SELECT 1 FROM ai_model_call_usage_groups AS usage_groups
6357
+ WHERE usage_groups.project_id = usage.project_id
6358
+ AND usage_groups.usage_record_id = usage.id
6359
+ AND usage_groups.group_id = ?
6360
+ )
6361
+ )
6362
+ OR (? = 'user' AND executions.requested_by_user_id = ?)
6363
+ OR (
6364
+ ? = 'serviceAccount'
6365
+ AND executions.requested_by_service_account_id = ?
6366
+ )
6367
+ )
6368
+ ORDER BY usage.occurred_at, usage.id
6369
+ `).all(policy.projectId, period.start.toISOString(), period.end.toISOString(), subject.type, subject.type, subject.id, subject.type, subject.id, subject.type, subject.id);
6370
+ return rows.map((row) => accountingEntryFromRow(row, policy.subject));
5090
6371
  }
5091
- executionRows(input) {
5092
- const requested = JSON.stringify([...new Set(input.executionIds)]);
5093
- return this.connection.db.query(`
5094
- WITH requested AS (
5095
- SELECT DISTINCT value AS execution_id
5096
- FROM json_each(?)
5097
- )
5098
- SELECT usage.* FROM requested
5099
- JOIN ai_model_call_usage AS usage
5100
- ON usage.project_id = ?
5101
- AND usage.execution_id = requested.execution_id
5102
- `).all(requested, input.projectId);
6372
+ requireAccountingEntry(projectId, usageRecordId) {
6373
+ const row = this.connection.db.query(`
6374
+ SELECT usage.project_id, usage.id AS usage_record_id, usage.execution_id,
6375
+ usage.attempt, usage.call_id, usage.occurred_at,
6376
+ CAST(usage.total_tokens AS TEXT) AS total_tokens,
6377
+ executions.requested_by_user_id,
6378
+ executions.requested_by_service_account_id,
6379
+ valuations.status AS valuation_status,
6380
+ valuations.currency AS valuation_currency,
6381
+ CAST(valuations.amount_nanos AS TEXT) AS valuation_amount_nanos
6382
+ FROM ai_model_call_usage AS usage
6383
+ JOIN executions
6384
+ ON executions.project_id = usage.project_id AND executions.id = usage.execution_id
6385
+ LEFT JOIN ai_model_call_valuations AS valuations
6386
+ ON valuations.project_id = usage.project_id
6387
+ AND valuations.usage_record_id = usage.id
6388
+ WHERE usage.project_id = ? AND usage.id = ?
6389
+ `).get(projectId, usageRecordId);
6390
+ if (row) {
6391
+ const groups = this.connection.db.query(`
6392
+ SELECT group_id FROM ai_model_call_usage_groups
6393
+ WHERE project_id = ? AND usage_record_id = ?
6394
+ ORDER BY group_id
6395
+ `).all(projectId, usageRecordId);
6396
+ return accountingEntryFromRow(row, undefined, groups.map((group) => group.group_id));
6397
+ }
6398
+ throw new AiLimitStorageError("missing_usage_record", `[SixbSqlite] AI usage record '${usageRecordId}' does not exist in project '${projectId}'.`);
5103
6399
  }
5104
6400
  assertExecutionExists(projectId, executionId) {
5105
- const execution = this.connection.db.query("SELECT 1 FROM executions WHERE project_id = ? AND id = ?").get(projectId, executionId);
5106
- if (execution)
6401
+ const row = this.connection.db.query("SELECT 1 FROM executions WHERE project_id = ? AND id = ?").get(projectId, executionId);
6402
+ if (row)
5107
6403
  return;
5108
- throw new AiUsageStorageError("missing_execution", `[SixbSqlite] AI usage execution '${executionId}' does not exist in project '${projectId}'.`);
6404
+ throw new AiLimitStorageError("missing_execution", `[SixbSqlite] AI limit execution '${executionId}' does not exist in project '${projectId}'.`);
5109
6405
  }
5110
6406
  }
5111
- function aggregateExecutionRows(executionIds, rows) {
5112
- const usageByExecutionId = new Map;
5113
- for (const executionId of executionIds)
5114
- usageByExecutionId.set(executionId, []);
5115
- for (const row of rows) {
5116
- usageByExecutionId.get(row.execution_id)?.push(usageFromRow2(row));
5117
- }
5118
- return executionIds.map((executionId) => {
5119
- const usages = usageByExecutionId.get(executionId) ?? [];
5120
- return {
5121
- modelCallCount: usages.length,
5122
- usage: aggregateAiModelCallUsage(usages)
5123
- };
5124
- });
6407
+ function policyFromRow(row) {
6408
+ return {
6409
+ id: row.id,
6410
+ projectId: row.project_id,
6411
+ subject: aiLimitSubjectFromParts(row.subject_type, row.subject_id),
6412
+ limit: aiLimitQuantityFromAmount({
6413
+ meter: row.meter,
6414
+ currency: row.currency,
6415
+ amount: BigInt(row.limit_amount)
6416
+ }),
6417
+ period: row.period_kind,
6418
+ enabled: row.enabled === 1,
6419
+ createdAt: new Date(row.created_at),
6420
+ updatedAt: new Date(row.updated_at)
6421
+ };
5125
6422
  }
5126
- function usageFromRow2(row) {
6423
+ function policyStatus(policy, period, row, existingGroups) {
6424
+ const limit = normalizeAiLimitAmount(policy.limit);
6425
+ const actual = BigInt(row.actual_amount);
6426
+ const reserved = BigInt(row.reserved_amount);
6427
+ const unknown = BigInt(row.unknown_amount);
6428
+ const consumed = actual + reserved + unknown;
6429
+ const quantity = (amount) => aiLimitQuantityFromAmount({ ...limit, amount });
5127
6430
  return {
5128
- ...row.input_tokens === null ? {} : { inputTokens: row.input_tokens },
5129
- ...row.output_tokens === null ? {} : { outputTokens: row.output_tokens },
5130
- ...row.uncached_input_tokens === null ? {} : { uncachedInputTokens: row.uncached_input_tokens },
5131
- ...row.cache_read_input_tokens === null ? {} : { cacheReadInputTokens: row.cache_read_input_tokens },
5132
- ...row.cache_write_input_tokens === null ? {} : { cacheWriteInputTokens: row.cache_write_input_tokens },
5133
- ...row.text_output_tokens === null ? {} : { textOutputTokens: row.text_output_tokens },
5134
- ...row.reasoning_output_tokens === null ? {} : { reasoningOutputTokens: row.reasoning_output_tokens }
6431
+ policy,
6432
+ period: structuredClone(period),
6433
+ consumption: {
6434
+ actual: quantity(actual),
6435
+ reserved: quantity(reserved),
6436
+ unknown: quantity(unknown),
6437
+ remaining: quantity(limit.amount > consumed ? limit.amount - consumed : 0n)
6438
+ },
6439
+ accountingStatus: row.accounting_status,
6440
+ exhausted: consumed >= limit.amount,
6441
+ orphaned: policy.subject.type === "group" && existingGroups !== undefined && !existingGroups.has(policy.subject.id)
5135
6442
  };
5136
6443
  }
5137
- function assertNonBlankProjectId(projectId) {
5138
- if (typeof projectId !== "string" || projectId.trim().length === 0) {
5139
- throw new TypeError("[Sixb] AI usage projectId must be nonblank.");
6444
+ function accountingEntryFromRow(row, subject, requesterGroupIds = subject?.type === "group" ? [subject.id] : []) {
6445
+ const totalTokens = row.total_tokens === null ? undefined : Number(row.total_tokens);
6446
+ const requester = row.requested_by_user_id ? { type: "user", id: row.requested_by_user_id } : row.requested_by_service_account_id ? { type: "serviceAccount", id: row.requested_by_service_account_id } : undefined;
6447
+ return {
6448
+ projectId: row.project_id,
6449
+ usageRecordId: row.usage_record_id,
6450
+ executionId: row.execution_id,
6451
+ attempt: row.attempt,
6452
+ callId: row.call_id,
6453
+ occurredAt: new Date(row.occurred_at),
6454
+ ...totalTokens !== undefined && Number.isSafeInteger(totalTokens) ? { totalTokens } : {},
6455
+ requesterGroupIds,
6456
+ ...requester ? { requester } : {},
6457
+ ...row.valuation_status === "rated" && row.valuation_currency !== null && row.valuation_amount_nanos !== null ? {
6458
+ valuation: {
6459
+ status: "rated",
6460
+ money: {
6461
+ currency: row.valuation_currency,
6462
+ amountNanos: row.valuation_amount_nanos
6463
+ }
6464
+ }
6465
+ } : row.valuation_status === "unpriceable" ? { valuation: { status: "unavailable" } } : {}
6466
+ };
6467
+ }
6468
+ function reservationFromRow(row) {
6469
+ return {
6470
+ projectId: row.project_id,
6471
+ executionId: row.execution_id,
6472
+ attempt: row.attempt,
6473
+ callId: row.call_id,
6474
+ buckets: normalizeBucketsJson(row.buckets),
6475
+ requestKey: row.request_key,
6476
+ period: {
6477
+ kind: "calendarMonth",
6478
+ start: new Date(row.period_start),
6479
+ end: new Date(row.period_end),
6480
+ resetAt: new Date(row.period_end)
6481
+ },
6482
+ state: row.state,
6483
+ ...row.usage_record_id === null ? {} : { usageRecordId: row.usage_record_id },
6484
+ reservedAt: new Date(row.reserved_at),
6485
+ updatedAt: new Date(row.updated_at)
6486
+ };
6487
+ }
6488
+ function normalizeBucketsJson(value) {
6489
+ const buckets = JSON.parse(value);
6490
+ return buckets.map((bucket) => ({
6491
+ subject: structuredClone(bucket.subject),
6492
+ estimate: normalizeAiLimitQuantities([bucket.estimate], "stored bucket")[0]
6493
+ }));
6494
+ }
6495
+ function reservationSqlValues(reservation) {
6496
+ return [
6497
+ reservation.projectId,
6498
+ reservation.executionId,
6499
+ reservation.attempt,
6500
+ reservation.callId,
6501
+ JSON.stringify(reservation.buckets),
6502
+ reservation.requestKey,
6503
+ reservation.period.start.toISOString(),
6504
+ reservation.period.end.toISOString(),
6505
+ reservation.state,
6506
+ reservation.usageRecordId ?? null,
6507
+ reservation.reservedAt.toISOString(),
6508
+ reservation.updatedAt.toISOString()
6509
+ ];
6510
+ }
6511
+ function assertSameDimension(previous, next) {
6512
+ if (aiLimitAmountKey(normalizeAiLimitAmount(previous)) !== aiLimitAmountKey(normalizeAiLimitAmount(next))) {
6513
+ throw new TypeError("[Sixb] AI limit policy meter and cost currency are immutable.");
5140
6514
  }
5141
6515
  }
6516
+ function validatePolicyIdentity(input) {
6517
+ assertNonBlank2(input.projectId, "projectId");
6518
+ assertNonBlank2(input.id, "policy id");
6519
+ }
6520
+ function duplicatePolicy(policy) {
6521
+ return new AiLimitStorageError("duplicate_policy", `[SixbSqlite] An AI limit policy already exists for policy ID or subject-meter dimension '${policy.id}'.`);
6522
+ }
6523
+ function missingReservation(input) {
6524
+ return new AiLimitStorageError("missing_reservation", `[SixbSqlite] AI model-call reservation '${input.callId}' does not exist for execution '${input.executionId}' attempt ${input.attempt}.`);
6525
+ }
6526
+ function reconciliationConflict(callId) {
6527
+ return new AiLimitStorageError("reconciliation_conflict", `[SixbSqlite] AI model-call reservation '${callId}' was reconciled with a different usage record.`);
6528
+ }
6529
+ function invalidState(reservation, operation) {
6530
+ return new AiLimitStorageError("invalid_reservation_state", `[SixbSqlite] Cannot ${operation} AI model-call reservation '${reservation.callId}' in state '${reservation.state}'.`);
6531
+ }
6532
+ function isConstraintError(error) {
6533
+ return error instanceof Error && /constraint/i.test(error.message);
6534
+ }
5142
6535
 
5143
6536
  // src/auth-storage/auth-storage.ts
5144
- import { AuthStorageError as AuthStorageError10 } from "@sixb/core/storage";
6537
+ import { AuthStorageError as AuthStorageError11 } from "@sixb/core/storage";
5145
6538
 
5146
6539
  // src/auth-storage/access-tokens.ts
5147
6540
  import { AuthStorageError as AuthStorageError2 } from "@sixb/core/storage";
@@ -5293,6 +6686,25 @@ function rowToOidcAuthorizationAttemptRecord(row) {
5293
6686
  consumedAt: row.consumed_at ? new Date(row.consumed_at) : undefined
5294
6687
  };
5295
6688
  }
6689
+ function rowToDeviceAuthorizationRecord(row) {
6690
+ return {
6691
+ id: row.id,
6692
+ projectId: row.project_id,
6693
+ deviceCodeHash: row.device_code_hash,
6694
+ userCode: row.user_code,
6695
+ clientName: row.client_name,
6696
+ tokenName: row.token_name,
6697
+ tokenExpiresAt: new Date(row.token_expires_at),
6698
+ status: row.status,
6699
+ approvedUserId: row.approved_user_id ?? undefined,
6700
+ approvedSessionId: row.approved_session_id ?? undefined,
6701
+ createdAt: new Date(row.created_at),
6702
+ expiresAt: new Date(row.expires_at),
6703
+ approvedAt: row.approved_at ? new Date(row.approved_at) : undefined,
6704
+ deniedAt: row.denied_at ? new Date(row.denied_at) : undefined,
6705
+ consumedAt: row.consumed_at ? new Date(row.consumed_at) : undefined
6706
+ };
6707
+ }
5296
6708
  function serializeOptionalRecord(value) {
5297
6709
  return value ? JSON.stringify(value) : null;
5298
6710
  }
@@ -5659,22 +7071,17 @@ function invitationRowToRecord(db, row) {
5659
7071
  }
5660
7072
 
5661
7073
  // src/auth-storage/access-tokens.ts
5662
- class SqliteAuthAccessTokenStore {
5663
- db;
5664
- constructor(db) {
5665
- this.db = db;
5666
- }
5667
- async create(input) {
5668
- const id = assertNonEmpty(input.id, "Access token id");
5669
- const projectId = assertNonEmpty(input.projectId, "Project id");
5670
- const name = assertNonEmpty(input.name, "Access token name");
5671
- const subjectId = assertNonEmpty(input.subjectId, "Access token subject id");
5672
- const tokenHash = assertNonEmpty(input.tokenHash, "Access token hash");
5673
- assertKindMatchesSubject(input);
5674
- this.assertSubjectExists(projectId, input.subjectType, subjectId);
5675
- const groupIds = input.groupIds === undefined ? undefined : normalizeGroupIds(input.groupIds);
5676
- try {
5677
- this.db.query(`
7074
+ function createAuthAccessToken(db, input) {
7075
+ const id = assertNonEmpty(input.id, "Access token id");
7076
+ const projectId = assertNonEmpty(input.projectId, "Project id");
7077
+ const name = assertNonEmpty(input.name, "Access token name");
7078
+ const subjectId = assertNonEmpty(input.subjectId, "Access token subject id");
7079
+ const tokenHash = assertNonEmpty(input.tokenHash, "Access token hash");
7080
+ assertKindMatchesSubject(input);
7081
+ assertSubjectExists(db, projectId, input.subjectType, subjectId);
7082
+ const groupIds = input.groupIds === undefined ? undefined : normalizeGroupIds(input.groupIds);
7083
+ try {
7084
+ db.query(`
5678
7085
  INSERT INTO auth_access_tokens (
5679
7086
  project_id,
5680
7087
  id,
@@ -5691,14 +7098,24 @@ class SqliteAuthAccessTokenStore {
5691
7098
  expires_at
5692
7099
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5693
7100
  `).run(projectId, id, name, input.kind, input.subjectType, subjectId, tokenHash, serializeOptionalStringArray(groupIds), input.createdByPrincipal?.type ?? null, input.createdByPrincipal?.id ?? null, input.createdBySessionId ?? null, toIso(input.createdAt), toIso(input.expiresAt));
5694
- } catch (error) {
5695
- mapUniqueConstraintError(error, "duplicate_access_token", `[Sixb] Access token '${id}' already exists for project '${projectId}'.`);
5696
- }
5697
- const token = await this.getById({ projectId, id });
5698
- if (!token) {
5699
- throw new AuthStorageError2("missing_access_token", `[Sixb] Access token '${id}' not found for project '${projectId}'.`);
5700
- }
5701
- return token;
7101
+ } catch (error) {
7102
+ mapUniqueConstraintError(error, "duplicate_access_token", `[Sixb] Access token '${id}' already exists for project '${projectId}'.`);
7103
+ }
7104
+ const row = db.query("SELECT * FROM auth_access_tokens WHERE project_id = ? AND id = ?").get(projectId, id);
7105
+ const token = row ? rowToAccessTokenRecord(row) : null;
7106
+ if (!token) {
7107
+ throw new AuthStorageError2("missing_access_token", `[Sixb] Access token '${id}' not found for project '${projectId}'.`);
7108
+ }
7109
+ return token;
7110
+ }
7111
+
7112
+ class SqliteAuthAccessTokenStore {
7113
+ db;
7114
+ constructor(db) {
7115
+ this.db = db;
7116
+ }
7117
+ async create(input) {
7118
+ return createAuthAccessToken(this.db, input);
5702
7119
  }
5703
7120
  async getById(params) {
5704
7121
  const row = this.db.query("SELECT * FROM auth_access_tokens WHERE project_id = ? AND id = ?").get(params.projectId, params.id);
@@ -5787,18 +7204,18 @@ class SqliteAuthAccessTokenStore {
5787
7204
  lastUsedIpAddress
5788
7205
  };
5789
7206
  }
5790
- assertSubjectExists(projectId, subjectType, subjectId) {
5791
- if (subjectType === "user") {
5792
- const row2 = this.db.query("SELECT * FROM auth_users WHERE project_id = ? AND id = ?").get(projectId, subjectId);
5793
- if (row2)
5794
- return;
5795
- throw new AuthStorageError2("missing_user", `[Sixb] User '${subjectId}' not found for project '${projectId}'.`);
5796
- }
5797
- const row = this.db.query("SELECT * FROM auth_service_accounts WHERE project_id = ? AND id = ?").get(projectId, subjectId);
5798
- if (row)
7207
+ }
7208
+ function assertSubjectExists(db, projectId, subjectType, subjectId) {
7209
+ if (subjectType === "user") {
7210
+ const row2 = db.query("SELECT * FROM auth_users WHERE project_id = ? AND id = ?").get(projectId, subjectId);
7211
+ if (row2)
5799
7212
  return;
5800
- throw new AuthStorageError2("missing_service_account", `[Sixb] Service account '${subjectId}' not found for project '${projectId}'.`);
7213
+ throw new AuthStorageError2("missing_user", `[Sixb] User '${subjectId}' not found for project '${projectId}'.`);
5801
7214
  }
7215
+ const row = db.query("SELECT * FROM auth_service_accounts WHERE project_id = ? AND id = ?").get(projectId, subjectId);
7216
+ if (row)
7217
+ return;
7218
+ throw new AuthStorageError2("missing_service_account", `[Sixb] Service account '${subjectId}' not found for project '${projectId}'.`);
5802
7219
  }
5803
7220
  function assertKindMatchesSubject(input) {
5804
7221
  if (input.kind === "personal" && input.subjectType === "user" || input.kind === "serviceAccount" && input.subjectType === "serviceAccount") {
@@ -5807,6 +7224,89 @@ function assertKindMatchesSubject(input) {
5807
7224
  throw new AuthStorageError2("invalid_input", `[Sixb] Access token kind '${input.kind}' cannot target subject type '${input.subjectType}'.`);
5808
7225
  }
5809
7226
 
7227
+ // src/auth-storage/device-authorizations.ts
7228
+ import { AuthStorageError as AuthStorageError3, MAX_PENDING_DEVICE_AUTHORIZATIONS } from "@sixb/core/storage";
7229
+ class SqliteAuthDeviceAuthorizationStore {
7230
+ db;
7231
+ constructor(db) {
7232
+ this.db = db;
7233
+ }
7234
+ async create(input) {
7235
+ return runImmediateTransaction(this.db, () => {
7236
+ this.db.query("DELETE FROM auth_device_authorizations WHERE project_id = ? AND expires_at <= ?").run(input.projectId, toIso(input.createdAt));
7237
+ const pending = this.db.query("SELECT COUNT(*) AS count FROM auth_device_authorizations WHERE project_id = ? AND status = 'pending'").get(input.projectId);
7238
+ if (pending.count >= MAX_PENDING_DEVICE_AUTHORIZATIONS) {
7239
+ throw new AuthStorageError3("device_authorization_limit_exceeded", `[Sixb] Project '${input.projectId}' has too many pending device authorizations.`);
7240
+ }
7241
+ try {
7242
+ this.db.query(`
7243
+ INSERT INTO auth_device_authorizations (
7244
+ project_id, id, device_code_hash, user_code, client_name, token_name,
7245
+ token_expires_at, status, created_at, expires_at
7246
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)
7247
+ `).run(assertNonEmpty(input.projectId, "Project id"), assertNonEmpty(input.id, "Device authorization id"), assertNonEmpty(input.deviceCodeHash, "Device code hash"), assertNonEmpty(input.userCode, "User code"), assertNonEmpty(input.clientName, "Client name"), assertNonEmpty(input.tokenName, "Token name"), toIso(input.tokenExpiresAt), toIso(input.createdAt), toIso(input.expiresAt));
7248
+ } catch (error) {
7249
+ mapUniqueConstraintError(error, "duplicate_device_authorization", `[Sixb] Device authorization '${input.id}' already exists for project '${input.projectId}'.`);
7250
+ }
7251
+ return requireById(this.db, input.projectId, input.id);
7252
+ });
7253
+ }
7254
+ async getById(params) {
7255
+ return getById(this.db, params.projectId, params.id);
7256
+ }
7257
+ async getByUserCode(params) {
7258
+ const row = this.db.query("SELECT * FROM auth_device_authorizations WHERE project_id = ? AND user_code = ?").get(params.projectId, params.userCode);
7259
+ return row ? rowToDeviceAuthorizationRecord(row) : null;
7260
+ }
7261
+ async approve(params) {
7262
+ return runImmediateTransaction(this.db, () => {
7263
+ const result = this.db.query(`
7264
+ UPDATE auth_device_authorizations
7265
+ SET status = 'approved', approved_user_id = ?, approved_session_id = ?, approved_at = ?
7266
+ WHERE project_id = ? AND id = ? AND status = 'pending' AND expires_at > ?
7267
+ AND EXISTS (
7268
+ SELECT 1 FROM auth_sessions
7269
+ WHERE project_id = ? AND id = ? AND user_id = ?
7270
+ AND revoked_at IS NULL AND expires_at > ?
7271
+ AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
7272
+ )
7273
+ `).run(params.userId, params.sessionId, toIso(params.approvedAt), params.projectId, params.id, toIso(params.approvedAt), params.projectId, params.sessionId, params.userId, toIso(params.approvedAt), toIso(params.approvedAt));
7274
+ if (result.changes !== 1)
7275
+ throw invalidTransition(params.id, params.projectId);
7276
+ return requireById(this.db, params.projectId, params.id);
7277
+ });
7278
+ }
7279
+ async deny(params) {
7280
+ return runImmediateTransaction(this.db, () => {
7281
+ const result = this.db.query(`
7282
+ UPDATE auth_device_authorizations
7283
+ SET status = 'denied', denied_at = ?
7284
+ WHERE project_id = ? AND id = ? AND status = 'pending' AND expires_at > ?
7285
+ `).run(toIso(params.deniedAt), params.projectId, params.id, toIso(params.deniedAt));
7286
+ if (result.changes !== 1)
7287
+ throw invalidTransition(params.id, params.projectId);
7288
+ return requireById(this.db, params.projectId, params.id);
7289
+ });
7290
+ }
7291
+ }
7292
+ function getDeviceAuthorizationById(db, projectId, id) {
7293
+ return getById(db, projectId, id);
7294
+ }
7295
+ function getById(db, projectId, id) {
7296
+ const row = db.query("SELECT * FROM auth_device_authorizations WHERE project_id = ? AND id = ?").get(projectId, id);
7297
+ return row ? rowToDeviceAuthorizationRecord(row) : null;
7298
+ }
7299
+ function requireById(db, projectId, id) {
7300
+ const record = getById(db, projectId, id);
7301
+ if (!record) {
7302
+ throw new AuthStorageError3("missing_device_authorization", `[Sixb] Device authorization '${id}' not found for project '${projectId}'.`);
7303
+ }
7304
+ return record;
7305
+ }
7306
+ function invalidTransition(id, projectId) {
7307
+ return new AuthStorageError3("invalid_device_authorization", `[Sixb] Device authorization '${id}' is no longer pending for project '${projectId}'.`);
7308
+ }
7309
+
5810
7310
  // src/auth-storage/group-memberships.ts
5811
7311
  class SqliteAuthGroupMembershipStore {
5812
7312
  db;
@@ -5895,7 +7395,7 @@ class SqliteAuthUserIdentityStore {
5895
7395
  }
5896
7396
 
5897
7397
  // src/auth-storage/invitations.ts
5898
- import { AuthStorageError as AuthStorageError3 } from "@sixb/core/storage";
7398
+ import { AuthStorageError as AuthStorageError4 } from "@sixb/core/storage";
5899
7399
  class SqliteAuthInvitationStore {
5900
7400
  db;
5901
7401
  constructor(db) {
@@ -5932,7 +7432,7 @@ class SqliteAuthInvitationStore {
5932
7432
  return requireInvitationById(this.db, { projectId, id: existingActive.id });
5933
7433
  }
5934
7434
  if (getInvitationById(this.db, { projectId, id })) {
5935
- throw new AuthStorageError3("duplicate_invitation", `[Sixb] Invitation '${id}' already exists but is not active for project '${projectId}'.`);
7435
+ throw new AuthStorageError4("duplicate_invitation", `[Sixb] Invitation '${id}' already exists but is not active for project '${projectId}'.`);
5936
7436
  }
5937
7437
  const createdAt = dateOrNow(input.createdAt);
5938
7438
  try {
@@ -6042,7 +7542,7 @@ class SqliteAuthInvitationStore {
6042
7542
 
6043
7543
  // src/auth-storage/magic-links.ts
6044
7544
  import { resolveAuthSessionAudience } from "@sixb/core";
6045
- import { AuthStorageError as AuthStorageError4 } from "@sixb/core/storage";
7545
+ import { AuthStorageError as AuthStorageError5 } from "@sixb/core/storage";
6046
7546
  class SqliteAuthMagicLinkStore {
6047
7547
  db;
6048
7548
  constructor(db) {
@@ -6057,7 +7557,7 @@ class SqliteAuthMagicLinkStore {
6057
7557
  const email = normalizeEmail(input.email);
6058
7558
  const tokenHash = assertNonEmpty(input.tokenHash, "Magic link token hash");
6059
7559
  if (getMagicLinkRowById(this.db, { projectId, id })) {
6060
- throw new AuthStorageError4("duplicate_magic_link", `[Sixb] Magic link '${id}' already exists for project '${projectId}'.`);
7560
+ throw new AuthStorageError5("duplicate_magic_link", `[Sixb] Magic link '${id}' already exists for project '${projectId}'.`);
6061
7561
  }
6062
7562
  revokeActiveMagicLinksForEmail(this.db, {
6063
7563
  projectId,
@@ -6121,7 +7621,7 @@ class SqliteAuthMagicLinkStore {
6121
7621
 
6122
7622
  // src/auth-storage/oidc-attempts.ts
6123
7623
  import { resolveAuthSessionAudience as resolveAuthSessionAudience2 } from "@sixb/core";
6124
- import { AuthStorageError as AuthStorageError5 } from "@sixb/core/storage";
7624
+ import { AuthStorageError as AuthStorageError6 } from "@sixb/core/storage";
6125
7625
  class SqliteAuthOidcAuthorizationAttemptStore {
6126
7626
  db;
6127
7627
  constructor(db) {
@@ -6137,7 +7637,7 @@ class SqliteAuthOidcAuthorizationAttemptStore {
6137
7637
  const nonceHash = assertNonEmpty(input.nonceHash, "OIDC nonce hash");
6138
7638
  const codeVerifier = assertNonEmpty(input.codeVerifier, "OIDC code verifier");
6139
7639
  if (getOidcAttemptRowById(this.db, { projectId, id })) {
6140
- throw new AuthStorageError5("duplicate_oidc_attempt", `[Sixb] OIDC authorization attempt '${id}' already exists for project '${projectId}'.`);
7640
+ throw new AuthStorageError6("duplicate_oidc_attempt", `[Sixb] OIDC authorization attempt '${id}' already exists for project '${projectId}'.`);
6141
7641
  }
6142
7642
  try {
6143
7643
  this.db.query(`
@@ -6180,7 +7680,7 @@ class SqliteAuthOidcAuthorizationAttemptStore {
6180
7680
  }
6181
7681
 
6182
7682
  // src/auth-storage/service-account-group-memberships.ts
6183
- import { AuthStorageError as AuthStorageError6 } from "@sixb/core/storage";
7683
+ import { AuthStorageError as AuthStorageError7 } from "@sixb/core/storage";
6184
7684
  class SqliteAuthServiceAccountGroupMembershipStore {
6185
7685
  db;
6186
7686
  constructor(db) {
@@ -6192,7 +7692,7 @@ class SqliteAuthServiceAccountGroupMembershipStore {
6192
7692
  const groupId = assertNonEmpty(input.groupId, "Group id");
6193
7693
  const serviceAccount = this.db.query("SELECT * FROM auth_service_accounts WHERE project_id = ? AND id = ?").get(projectId, serviceAccountId);
6194
7694
  if (!serviceAccount) {
6195
- throw new AuthStorageError6("missing_service_account", `[Sixb] Service account '${serviceAccountId}' not found for project '${projectId}'.`);
7695
+ throw new AuthStorageError7("missing_service_account", `[Sixb] Service account '${serviceAccountId}' not found for project '${projectId}'.`);
6196
7696
  }
6197
7697
  this.db.query(`
6198
7698
  INSERT OR IGNORE INTO auth_service_account_group_memberships (
@@ -6217,7 +7717,7 @@ class SqliteAuthServiceAccountGroupMembershipStore {
6217
7717
  const serviceAccountId = assertNonEmpty(input.serviceAccountId, "Service account id");
6218
7718
  const serviceAccount = this.db.query("SELECT * FROM auth_service_accounts WHERE project_id = ? AND id = ?").get(projectId, serviceAccountId);
6219
7719
  if (!serviceAccount) {
6220
- throw new AuthStorageError6("missing_service_account", `[Sixb] Service account '${serviceAccountId}' not found for project '${projectId}'.`);
7720
+ throw new AuthStorageError7("missing_service_account", `[Sixb] Service account '${serviceAccountId}' not found for project '${projectId}'.`);
6221
7721
  }
6222
7722
  const groupIds = normalizeGroupIds(input.groupIds);
6223
7723
  const desired = new Set(groupIds);
@@ -6272,7 +7772,7 @@ class SqliteAuthServiceAccountGroupMembershipStore {
6272
7772
  }
6273
7773
 
6274
7774
  // src/auth-storage/service-accounts.ts
6275
- import { AuthStorageError as AuthStorageError7 } from "@sixb/core/storage";
7775
+ import { AuthStorageError as AuthStorageError8 } from "@sixb/core/storage";
6276
7776
  class SqliteAuthServiceAccountStore {
6277
7777
  db;
6278
7778
  constructor(db) {
@@ -6334,7 +7834,7 @@ class SqliteAuthServiceAccountStore {
6334
7834
  `).run(name, input.description ?? null, input.status ?? null, toIso(updatedAt), projectId, id);
6335
7835
  const updated = await this.getById({ projectId, id });
6336
7836
  if (!updated) {
6337
- throw new AuthStorageError7("missing_service_account", `[Sixb] Service account '${id}' not found for project '${projectId}'.`);
7837
+ throw new AuthStorageError8("missing_service_account", `[Sixb] Service account '${id}' not found for project '${projectId}'.`);
6338
7838
  }
6339
7839
  return updated;
6340
7840
  }
@@ -6368,7 +7868,7 @@ class SqliteAuthServiceAccountStore {
6368
7868
  }
6369
7869
 
6370
7870
  // src/auth-storage/sessions.ts
6371
- import { AuthStorageError as AuthStorageError8 } from "@sixb/core/storage";
7871
+ import { AuthStorageError as AuthStorageError9 } from "@sixb/core/storage";
6372
7872
  class SqliteAuthSessionStore {
6373
7873
  db;
6374
7874
  constructor(db) {
@@ -6441,7 +7941,7 @@ class SqliteAuthSessionStore {
6441
7941
  return runImmediateTransaction(this.db, () => {
6442
7942
  const existing = getSessionRowById(this.db, params);
6443
7943
  if (!existing) {
6444
- throw new AuthStorageError8("missing_session", `[Sixb] Session '${params.id}' not found for project '${params.projectId}'.`);
7944
+ throw new AuthStorageError9("missing_session", `[Sixb] Session '${params.id}' not found for project '${params.projectId}'.`);
6445
7945
  }
6446
7946
  this.db.query(`
6447
7947
  UPDATE auth_sessions
@@ -6469,7 +7969,7 @@ class SqliteAuthSessionStore {
6469
7969
  `).run(toIso(params.lastSeenAt), params.projectId, params.id);
6470
7970
  const updated = getSessionById(this.db, params);
6471
7971
  if (!updated) {
6472
- throw new AuthStorageError8("missing_session", `[Sixb] Session '${params.id}' not found for project '${params.projectId}'.`);
7972
+ throw new AuthStorageError9("missing_session", `[Sixb] Session '${params.id}' not found for project '${params.projectId}'.`);
6473
7973
  }
6474
7974
  return updated;
6475
7975
  });
@@ -6477,7 +7977,7 @@ class SqliteAuthSessionStore {
6477
7977
  }
6478
7978
 
6479
7979
  // src/auth-storage/users.ts
6480
- import { AuthStorageError as AuthStorageError9 } from "@sixb/core/storage";
7980
+ import { AuthStorageError as AuthStorageError10 } from "@sixb/core/storage";
6481
7981
  class SqliteAuthUserStore {
6482
7982
  db;
6483
7983
  constructor(db) {
@@ -6488,10 +7988,10 @@ class SqliteAuthUserStore {
6488
7988
  const projectId = assertNonEmpty(input.projectId, "Project id");
6489
7989
  const email = normalizeEmail(input.email);
6490
7990
  if (getUserRowById(this.db, { projectId, id })) {
6491
- throw new AuthStorageError9("duplicate_user", `[Sixb] User '${id}' already exists for project '${projectId}'.`);
7991
+ throw new AuthStorageError10("duplicate_user", `[Sixb] User '${id}' already exists for project '${projectId}'.`);
6492
7992
  }
6493
7993
  if (getUserRowByEmail(this.db, { projectId, email })) {
6494
- throw new AuthStorageError9("duplicate_user", `[Sixb] User email '${email}' already exists for project '${projectId}'.`);
7994
+ throw new AuthStorageError10("duplicate_user", `[Sixb] User email '${email}' already exists for project '${projectId}'.`);
6495
7995
  }
6496
7996
  const createdAt = dateOrNow(input.createdAt);
6497
7997
  const updatedAt = input.updatedAt ? new Date(input.updatedAt) : createdAt;
@@ -6536,7 +8036,7 @@ class SqliteAuthUserStore {
6536
8036
  id: input.id
6537
8037
  });
6538
8038
  if (!existing) {
6539
- throw new AuthStorageError9("missing_user", `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`);
8039
+ throw new AuthStorageError10("missing_user", `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`);
6540
8040
  }
6541
8041
  const updatedAt = dateOrNow(input.updatedAt);
6542
8042
  this.db.query(`
@@ -6560,7 +8060,7 @@ class SqliteAuthUserStore {
6560
8060
  id: input.id
6561
8061
  });
6562
8062
  if (!existing) {
6563
- throw new AuthStorageError9("missing_user", `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`);
8063
+ throw new AuthStorageError10("missing_user", `[Sixb] User '${input.id}' not found for project '${input.projectId}'.`);
6564
8064
  }
6565
8065
  const updatedAt = dateOrNow(input.updatedAt);
6566
8066
  this.db.query(`
@@ -6619,6 +8119,7 @@ class SqliteAuthStorage {
6619
8119
  groupMemberships;
6620
8120
  magicLinks;
6621
8121
  oidcAuthorizationAttempts;
8122
+ deviceAuthorizations;
6622
8123
  constructor(options = {}) {
6623
8124
  this.connection = openSqliteStoreConnection(options);
6624
8125
  this.db = this.connection.db;
@@ -6635,6 +8136,30 @@ class SqliteAuthStorage {
6635
8136
  this.groupMemberships = new SqliteAuthGroupMembershipStore(this.db);
6636
8137
  this.magicLinks = new SqliteAuthMagicLinkStore(this.db);
6637
8138
  this.oidcAuthorizationAttempts = new SqliteAuthOidcAuthorizationAttemptStore(this.db);
8139
+ this.deviceAuthorizations = new SqliteAuthDeviceAuthorizationStore(this.db);
8140
+ }
8141
+ async completeDeviceAuthorization(input) {
8142
+ return runImmediateTransaction(this.db, () => {
8143
+ const authorization = getDeviceAuthorizationById(this.db, input.projectId, input.id);
8144
+ if (!authorization) {
8145
+ throw new AuthStorageError11("missing_device_authorization", `[Sixb] Device authorization '${input.id}' not found for project '${input.projectId}'.`);
8146
+ }
8147
+ assertCompletableDeviceAuthorization(authorization, input);
8148
+ const accessToken = createAuthAccessToken(this.db, input.accessToken);
8149
+ this.db.query(`
8150
+ UPDATE auth_device_authorizations
8151
+ SET status = 'consumed', consumed_at = ?
8152
+ WHERE project_id = ? AND id = ? AND status = 'approved'
8153
+ `).run(toIso(input.completedAt), input.projectId, input.id);
8154
+ return {
8155
+ authorization: {
8156
+ ...authorization,
8157
+ status: "consumed",
8158
+ consumedAt: new Date(input.completedAt)
8159
+ },
8160
+ accessToken
8161
+ };
8162
+ });
6638
8163
  }
6639
8164
  async completeMagicLinkSignIn(input) {
6640
8165
  const result = runImmediateTransaction(this.db, () => {
@@ -6664,7 +8189,7 @@ class SqliteAuthStorage {
6664
8189
  consumedAt: completedAt
6665
8190
  });
6666
8191
  return {
6667
- error: new AuthStorageError10("suspended_user", `[Sixb] User '${existingUserRow.id}' is suspended for project '${projectId}'.`)
8192
+ error: new AuthStorageError11("suspended_user", `[Sixb] User '${existingUserRow.id}' is suspended for project '${projectId}'.`)
6668
8193
  };
6669
8194
  }
6670
8195
  if (shouldCreateUser && !activeInvitation && !input.allowUserCreationWithoutInvitation) {
@@ -6675,7 +8200,7 @@ class SqliteAuthStorage {
6675
8200
  consumedAt: completedAt
6676
8201
  });
6677
8202
  return {
6678
- error: new AuthStorageError10("user_creation_not_allowed", `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`)
8203
+ error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`)
6679
8204
  };
6680
8205
  }
6681
8206
  if (shouldCreateUser && !activeInvitation && input.allowUserCreationWithoutInvitation && input.requireNoActiveUsersForUserCreation && hasActiveUsers(this.db, projectId)) {
@@ -6686,7 +8211,7 @@ class SqliteAuthStorage {
6686
8211
  consumedAt: completedAt
6687
8212
  });
6688
8213
  return {
6689
- error: new AuthStorageError10("user_creation_not_allowed", `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`)
8214
+ error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`)
6690
8215
  };
6691
8216
  }
6692
8217
  validateCompleteSessionInput(this.db, projectId, input.session);
@@ -6695,7 +8220,7 @@ class SqliteAuthStorage {
6695
8220
  if (shouldCreateUser) {
6696
8221
  newUserId = assertNonEmpty(input.newUserId, "User id");
6697
8222
  if (getUserRowById(this.db, { projectId, id: newUserId })) {
6698
- throw new AuthStorageError10("duplicate_user", `[Sixb] User '${newUserId}' already exists for project '${projectId}'.`);
8223
+ throw new AuthStorageError11("duplicate_user", `[Sixb] User '${newUserId}' already exists for project '${projectId}'.`);
6699
8224
  }
6700
8225
  }
6701
8226
  consumeMagicLink(this.db, {
@@ -6770,37 +8295,37 @@ class SqliteAuthStorage {
6770
8295
  if (identity && !userRow) {
6771
8296
  this.consumeOidcAttempt(input, completedAt, projectId);
6772
8297
  return {
6773
- error: new AuthStorageError10("missing_user", `[Sixb] User '${identity.user_id}' not found for linked OIDC identity.`)
8298
+ error: new AuthStorageError11("missing_user", `[Sixb] User '${identity.user_id}' not found for linked OIDC identity.`)
6774
8299
  };
6775
8300
  }
6776
8301
  if (!identity && userRow && (!input.autoLinkByVerifiedEmail || !input.emailVerified)) {
6777
8302
  this.consumeOidcAttempt(input, completedAt, projectId);
6778
8303
  return {
6779
- error: new AuthStorageError10("email_link_not_allowed", `[Sixb] OIDC identity cannot auto-link to user '${userRow.id}' for project '${projectId}'.`)
8304
+ error: new AuthStorageError11("email_link_not_allowed", `[Sixb] OIDC identity cannot auto-link to user '${userRow.id}' for project '${projectId}'.`)
6780
8305
  };
6781
8306
  }
6782
8307
  if (userRow?.status === "suspended") {
6783
8308
  this.consumeOidcAttempt(input, completedAt, projectId);
6784
8309
  return {
6785
- error: new AuthStorageError10("suspended_user", `[Sixb] User '${userRow.id}' is suspended for project '${projectId}'.`)
8310
+ error: new AuthStorageError11("suspended_user", `[Sixb] User '${userRow.id}' is suspended for project '${projectId}'.`)
6786
8311
  };
6787
8312
  }
6788
8313
  if (shouldCreateUser && !input.emailVerified) {
6789
8314
  this.consumeOidcAttempt(input, completedAt, projectId);
6790
8315
  return {
6791
- error: new AuthStorageError10("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
8316
+ error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
6792
8317
  };
6793
8318
  }
6794
8319
  if (shouldCreateUser && !activeInvitation && !input.allowUserCreationWithoutInvitation) {
6795
8320
  this.consumeOidcAttempt(input, completedAt, projectId);
6796
8321
  return {
6797
- error: new AuthStorageError10("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
8322
+ error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
6798
8323
  };
6799
8324
  }
6800
8325
  if (shouldCreateUser && !activeInvitation && input.allowUserCreationWithoutInvitation && input.requireNoActiveUsersForUserCreation && hasActiveUsers(this.db, projectId)) {
6801
8326
  this.consumeOidcAttempt(input, completedAt, projectId);
6802
8327
  return {
6803
- error: new AuthStorageError10("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
8328
+ error: new AuthStorageError11("user_creation_not_allowed", `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`)
6804
8329
  };
6805
8330
  }
6806
8331
  validateCompleteSessionInput(this.db, projectId, input.session);
@@ -6809,7 +8334,7 @@ class SqliteAuthStorage {
6809
8334
  if (shouldCreateUser) {
6810
8335
  newUserId = assertNonEmpty(input.newUserId, "User id");
6811
8336
  if (getUserRowById(this.db, { projectId, id: newUserId })) {
6812
- throw new AuthStorageError10("duplicate_user", `[Sixb] User '${newUserId}' already exists for project '${projectId}'.`);
8337
+ throw new AuthStorageError11("duplicate_user", `[Sixb] User '${newUserId}' already exists for project '${projectId}'.`);
6813
8338
  }
6814
8339
  }
6815
8340
  this.consumeOidcAttempt(input, completedAt, projectId);
@@ -6882,7 +8407,7 @@ class SqliteAuthStorage {
6882
8407
  id: input.userId
6883
8408
  });
6884
8409
  if (!existing) {
6885
- throw new AuthStorageError10("missing_user", `[Sixb] User '${input.userId}' not found for project '${input.projectId}'.`);
8410
+ throw new AuthStorageError11("missing_user", `[Sixb] User '${input.userId}' not found for project '${input.projectId}'.`);
6886
8411
  }
6887
8412
  this.db.query(`
6888
8413
  UPDATE auth_users
@@ -6908,24 +8433,24 @@ class SqliteAuthStorage {
6908
8433
  }
6909
8434
  assertMagicLinkUsable(projectId, id, tokenHash, completedAt, row) {
6910
8435
  if (!row) {
6911
- throw new AuthStorageError10("missing_magic_link", `[Sixb] Magic link '${id}' not found for project '${projectId}'.`);
8436
+ throw new AuthStorageError11("missing_magic_link", `[Sixb] Magic link '${id}' not found for project '${projectId}'.`);
6912
8437
  }
6913
8438
  if (row.token_hash !== tokenHash || row.consumed_at || row.revoked_at) {
6914
- throw new AuthStorageError10("invalid_magic_link", `[Sixb] Magic link '${id}' is not valid for project '${projectId}'.`);
8439
+ throw new AuthStorageError11("invalid_magic_link", `[Sixb] Magic link '${id}' is not valid for project '${projectId}'.`);
6915
8440
  }
6916
8441
  if (new Date(row.expires_at) <= completedAt) {
6917
- throw new AuthStorageError10("expired_magic_link", `[Sixb] Magic link '${id}' is expired for project '${projectId}'.`);
8442
+ throw new AuthStorageError11("expired_magic_link", `[Sixb] Magic link '${id}' is expired for project '${projectId}'.`);
6918
8443
  }
6919
8444
  }
6920
8445
  assertOidcAttemptUsable(projectId, id, stateHash, completedAt, row) {
6921
8446
  if (!row) {
6922
- throw new AuthStorageError10("missing_oidc_attempt", `[Sixb] OIDC authorization attempt '${id}' not found for project '${projectId}'.`);
8447
+ throw new AuthStorageError11("missing_oidc_attempt", `[Sixb] OIDC authorization attempt '${id}' not found for project '${projectId}'.`);
6923
8448
  }
6924
8449
  if (row.state_hash !== stateHash || row.consumed_at) {
6925
- throw new AuthStorageError10("invalid_oidc_attempt", `[Sixb] OIDC authorization attempt '${id}' is not valid for project '${projectId}'.`);
8450
+ throw new AuthStorageError11("invalid_oidc_attempt", `[Sixb] OIDC authorization attempt '${id}' is not valid for project '${projectId}'.`);
6926
8451
  }
6927
8452
  if (new Date(row.expires_at) <= completedAt) {
6928
- throw new AuthStorageError10("expired_oidc_attempt", `[Sixb] OIDC authorization attempt '${id}' is expired for project '${projectId}'.`);
8453
+ throw new AuthStorageError11("expired_oidc_attempt", `[Sixb] OIDC authorization attempt '${id}' is expired for project '${projectId}'.`);
6929
8454
  }
6930
8455
  }
6931
8456
  insertUser(input) {
@@ -7061,7 +8586,21 @@ function assertSignInSessionAudience(projectId, sessionAudience, storedAudience)
7061
8586
  if (sessionAudience === storedAudience) {
7062
8587
  return;
7063
8588
  }
7064
- throw new AuthStorageError10("invalid_input", `[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`);
8589
+ throw new AuthStorageError11("invalid_input", `[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`);
8590
+ }
8591
+ function assertCompletableDeviceAuthorization(authorization, input) {
8592
+ if (authorization.deviceCodeHash !== input.deviceCodeHash) {
8593
+ throw new AuthStorageError11("invalid_device_authorization", "[Sixb] Invalid device authorization.");
8594
+ }
8595
+ if (authorization.status === "pending") {
8596
+ throw new AuthStorageError11("pending_device_authorization", "[Sixb] Device authorization is pending.");
8597
+ }
8598
+ if (authorization.status === "denied") {
8599
+ throw new AuthStorageError11("device_authorization_denied", "[Sixb] Device authorization was denied.");
8600
+ }
8601
+ if (authorization.status !== "approved" || authorization.expiresAt <= input.completedAt || !authorization.approvedUserId || !authorization.approvedSessionId || input.accessToken.projectId !== input.projectId || input.accessToken.subjectType !== "user" || input.accessToken.subjectId !== authorization.approvedUserId || input.accessToken.createdBySessionId !== authorization.approvedSessionId) {
8602
+ throw new AuthStorageError11("invalid_device_authorization", "[Sixb] Device authorization is invalid.");
8603
+ }
7065
8604
  }
7066
8605
  // src/connector-connection-storage.ts
7067
8606
  import {
@@ -13521,15 +15060,15 @@ class SqliteWorkflowAgentNodeRunStorage {
13521
15060
  projectId: input.projectId,
13522
15061
  executionId: input.executionId,
13523
15062
  nodeRunId: input.nodeRunId,
13524
- agentId: input.agentId,
15063
+ actorId: input.actorId,
13525
15064
  workflowExecutionId: parent.execution_id
13526
15065
  });
13527
15066
  try {
13528
15067
  this.db.query(`
13529
15068
  INSERT INTO workflow_agent_node_runs (
13530
- project_id, node_run_id, execution_id, agent_id, status, prompt, attempt, created_at
15069
+ project_id, node_run_id, execution_id, actor_id, status, prompt, attempt, created_at
13531
15070
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
13532
- `).run(input.projectId, input.nodeRunId, input.executionId, input.agentId, "queued", input.prompt, 0, (input.createdAt ?? new Date).toISOString());
15071
+ `).run(input.projectId, input.nodeRunId, input.executionId, input.actorId, "queued", input.prompt, 0, (input.createdAt ?? new Date).toISOString());
13533
15072
  } catch (error) {
13534
15073
  if (isUniqueConstraintError2(error)) {
13535
15074
  throw new WorkflowRunError(`[SixbSqlite] Agent execution already exists for workflow node run '${input.nodeRunId}'.`);
@@ -13608,9 +15147,9 @@ class SqliteWorkflowAgentNodeRunStorage {
13608
15147
  return { runs: [], total: 0, hasMore: false };
13609
15148
  const whereClauses = ["project_id = ?"];
13610
15149
  const args = [input.projectId];
13611
- if (input.agentId) {
13612
- whereClauses.push("agent_id = ?");
13613
- args.push(input.agentId);
15150
+ if (input.actorId) {
15151
+ whereClauses.push("actor_id = ?");
15152
+ args.push(input.actorId);
13614
15153
  }
13615
15154
  appendRunListFilters(whereClauses, args, input);
13616
15155
  const result = queryRunList({
@@ -13860,7 +15399,7 @@ function rowToWorkflowAgentNodeRunRecord(row) {
13860
15399
  projectId: row.project_id,
13861
15400
  nodeRunId: row.node_run_id,
13862
15401
  executionId: row.execution_id,
13863
- agentId: row.agent_id,
15402
+ actorId: row.actor_id,
13864
15403
  status: row.status,
13865
15404
  prompt: row.prompt,
13866
15405
  ...row.model_id ? { modelId: row.model_id } : {},
@@ -13895,6 +15434,7 @@ class SqliteStorage {
13895
15434
  agents;
13896
15435
  aiUsage;
13897
15436
  aiCosts;
15437
+ aiLimits;
13898
15438
  actionRuns;
13899
15439
  pipelineRuns;
13900
15440
  syncRuns;
@@ -13942,6 +15482,7 @@ class SqliteStorage {
13942
15482
  this.agents = createAgentOperationScope(stores.agents, scope);
13943
15483
  this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope);
13944
15484
  this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope);
15485
+ this.aiLimits = createOperationScopedFacade(stores.aiLimits, scope);
13945
15486
  this.actionRuns = createOperationScopedFacade(stores.actionRuns, scope);
13946
15487
  this.pipelineRuns = createOperationScopedFacade(stores.pipelineRuns, scope);
13947
15488
  this.timeseries = createOperationScopedFacade(readTimeseries, readScope);
@@ -14062,6 +15603,7 @@ function createSqliteStores(connection, options) {
14062
15603
  agents: new SqliteAgentStorage({ connection, executions }),
14063
15604
  aiUsage: new SqliteAiUsageStorage({ connection }),
14064
15605
  aiCosts: new SqliteAiCostStorage(connection),
15606
+ aiLimits: new SqliteAiLimitStorage({ connection }),
14065
15607
  actionRuns: new SqliteActionRunStorage({ connection, executions }),
14066
15608
  pipelineRuns: new SqlitePipelineRunStorage({ connection, executions }),
14067
15609
  timeseries: new SqliteTimeseriesStorage({ connection }),
@@ -14079,4 +15621,4 @@ export {
14079
15621
  SqliteStorage
14080
15622
  };
14081
15623
 
14082
- //# debugId=65891EE349DAB6B764756E2164756E21
15624
+ //# debugId=CC3E88BEF909A1E364756E2164756E21