pilotswarm-sdk 0.3.1 → 0.3.2

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 (101) hide show
  1. package/dist/cms-migrations.d.ts.map +1 -1
  2. package/dist/cms-migrations.js +840 -1
  3. package/dist/cms-migrations.js.map +1 -1
  4. package/dist/cms.d.ts +31 -1
  5. package/dist/cms.d.ts.map +1 -1
  6. package/dist/cms.js +46 -1
  7. package/dist/cms.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/inspect-tools.d.ts.map +1 -1
  11. package/dist/inspect-tools.js +27 -0
  12. package/dist/inspect-tools.js.map +1 -1
  13. package/dist/managed-session.d.ts +1 -0
  14. package/dist/managed-session.d.ts.map +1 -1
  15. package/dist/managed-session.js +243 -21
  16. package/dist/managed-session.js.map +1 -1
  17. package/dist/management-client.d.ts +14 -1
  18. package/dist/management-client.d.ts.map +1 -1
  19. package/dist/management-client.js +49 -1
  20. package/dist/management-client.js.map +1 -1
  21. package/dist/orchestration/index.d.ts +2 -2
  22. package/dist/orchestration/index.js +1 -1
  23. package/dist/orchestration/lifecycle.d.ts.map +1 -1
  24. package/dist/orchestration/lifecycle.js +74 -4
  25. package/dist/orchestration/lifecycle.js.map +1 -1
  26. package/dist/orchestration/runtime.d.ts +1 -1
  27. package/dist/orchestration/state.d.ts +1 -0
  28. package/dist/orchestration/state.d.ts.map +1 -1
  29. package/dist/orchestration/state.js +1 -0
  30. package/dist/orchestration/state.js.map +1 -1
  31. package/dist/orchestration/turn.d.ts +11 -0
  32. package/dist/orchestration/turn.d.ts.map +1 -1
  33. package/dist/orchestration/turn.js +61 -4
  34. package/dist/orchestration/turn.js.map +1 -1
  35. package/dist/orchestration-registry.d.ts.map +1 -1
  36. package/dist/orchestration-registry.js +4 -2
  37. package/dist/orchestration-registry.js.map +1 -1
  38. package/dist/orchestration-version.d.ts +1 -1
  39. package/dist/orchestration-version.js +1 -1
  40. package/dist/orchestration.d.ts +2 -2
  41. package/dist/orchestration.js +1 -1
  42. package/dist/orchestration_1_0_46.d.ts +1 -1
  43. package/dist/orchestration_1_0_47.d.ts +1 -1
  44. package/dist/orchestration_1_0_48.d.ts +1 -1
  45. package/dist/orchestration_1_0_49.d.ts +1 -1
  46. package/dist/orchestration_1_0_50.d.ts +1 -1
  47. package/dist/orchestration_1_0_54/agents.d.ts +41 -0
  48. package/dist/orchestration_1_0_54/agents.d.ts.map +1 -0
  49. package/dist/orchestration_1_0_54/agents.js +758 -0
  50. package/dist/orchestration_1_0_54/agents.js.map +1 -0
  51. package/dist/orchestration_1_0_54/index.d.ts +24 -0
  52. package/dist/orchestration_1_0_54/index.d.ts.map +1 -0
  53. package/dist/orchestration_1_0_54/index.js +13 -0
  54. package/dist/orchestration_1_0_54/index.js.map +1 -0
  55. package/dist/orchestration_1_0_54/lifecycle.d.ts +41 -0
  56. package/dist/orchestration_1_0_54/lifecycle.d.ts.map +1 -0
  57. package/dist/orchestration_1_0_54/lifecycle.js +510 -0
  58. package/dist/orchestration_1_0_54/lifecycle.js.map +1 -0
  59. package/dist/orchestration_1_0_54/queue.d.ts +7 -0
  60. package/dist/orchestration_1_0_54/queue.d.ts.map +1 -0
  61. package/dist/orchestration_1_0_54/queue.js +644 -0
  62. package/dist/orchestration_1_0_54/queue.js.map +1 -0
  63. package/dist/orchestration_1_0_54/runtime.d.ts +29 -0
  64. package/dist/orchestration_1_0_54/runtime.d.ts.map +1 -0
  65. package/dist/orchestration_1_0_54/runtime.js +194 -0
  66. package/dist/orchestration_1_0_54/runtime.js.map +1 -0
  67. package/dist/orchestration_1_0_54/state.d.ts +125 -0
  68. package/dist/orchestration_1_0_54/state.d.ts.map +1 -0
  69. package/dist/orchestration_1_0_54/state.js +105 -0
  70. package/dist/orchestration_1_0_54/state.js.map +1 -0
  71. package/dist/orchestration_1_0_54/turn.d.ts +6 -0
  72. package/dist/orchestration_1_0_54/turn.d.ts.map +1 -0
  73. package/dist/orchestration_1_0_54/turn.js +864 -0
  74. package/dist/orchestration_1_0_54/turn.js.map +1 -0
  75. package/dist/orchestration_1_0_54/utils.d.ts +22 -0
  76. package/dist/orchestration_1_0_54/utils.d.ts.map +1 -0
  77. package/dist/orchestration_1_0_54/utils.js +226 -0
  78. package/dist/orchestration_1_0_54/utils.js.map +1 -0
  79. package/dist/session-manager.d.ts +4 -0
  80. package/dist/session-manager.d.ts.map +1 -1
  81. package/dist/session-manager.js +22 -0
  82. package/dist/session-manager.js.map +1 -1
  83. package/dist/session-proxy.d.ts +12 -0
  84. package/dist/session-proxy.d.ts.map +1 -1
  85. package/dist/session-proxy.js +137 -13
  86. package/dist/session-proxy.js.map +1 -1
  87. package/dist/sweeper-tools.d.ts +1 -1
  88. package/dist/sweeper-tools.d.ts.map +1 -1
  89. package/dist/sweeper-tools.js +20 -75
  90. package/dist/sweeper-tools.js.map +1 -1
  91. package/dist/system-agents.d.ts +2 -0
  92. package/dist/system-agents.d.ts.map +1 -1
  93. package/dist/system-agents.js +3 -0
  94. package/dist/system-agents.js.map +1 -1
  95. package/dist/types.d.ts +14 -1
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/types.js.map +1 -1
  98. package/package.json +2 -2
  99. package/plugins/mgmt/agents/agent-tuner.agent.md +36 -2
  100. package/plugins/mgmt/agents/sweeper.agent.md +7 -7
  101. package/plugins/system/agents/default.agent.md +3 -1
@@ -117,6 +117,16 @@ export function CMS_MIGRATIONS(schema) {
117
117
  name: "retrieval_usage_procs",
118
118
  sql: migration_0021_retrieval_usage_procs(schema),
119
119
  },
120
+ {
121
+ version: "0022",
122
+ name: "turn_metrics_reasoning_effort",
123
+ sql: migration_0022_turn_metrics_reasoning_effort(schema),
124
+ },
125
+ {
126
+ version: "0023",
127
+ name: "turn_metrics_stats_fallbacks_and_group_owner_patch",
128
+ sql: migration_0023_turn_metrics_stats_fallbacks_and_group_owner_patch(schema),
129
+ },
120
130
  ];
121
131
  }
122
132
  // ─── Migration 0001: Baseline ────────────────────────────────────
@@ -2749,7 +2759,7 @@ BEGIN
2749
2759
  is_system = CASE WHEN p_updates ? 'isSystem' THEN (p_updates->>'isSystem')::BOOLEAN ELSE is_system END,
2750
2760
  agent_id = CASE WHEN p_updates ? 'agentId' THEN (p_updates->>'agentId') ELSE agent_id END,
2751
2761
  splash = CASE WHEN p_updates ? 'splash' THEN (p_updates->>'splash') ELSE splash END,
2752
- group_id = CASE WHEN p_updates ? 'groupId' THEN NULLIF(BTRIM(p_updates->>'groupId'), '') ELSE group_id END,
2762
+ group_id = group_id,
2753
2763
  updated_at = now()
2754
2764
  WHERE session_id = p_session_id;
2755
2765
 
@@ -3384,4 +3394,833 @@ END;
3384
3394
  $$ LANGUAGE plpgsql;
3385
3395
  `;
3386
3396
  }
3397
+ // ─── Migration 0022: Turn Metrics Reasoning Effort ──────────────
3398
+ function migration_0022_turn_metrics_reasoning_effort(schema) {
3399
+ const s = `"${schema}"`;
3400
+ return `
3401
+ -- 0022_turn_metrics_reasoning_effort:
3402
+ -- - Add reasoning_effort to per-turn metrics so token attribution aligns with
3403
+ -- the session row model:effort convention and survives mid-session switches.
3404
+ -- - Add model+effort composite indexes for by-model aggregation.
3405
+ -- - Extend insert/read procs; add per-session by-model rollup with turn count.
3406
+
3407
+ DO $$
3408
+ BEGIN
3409
+ IF to_regclass('${schema}.session_metrics') IS NULL
3410
+ AND to_regclass('${schema}.session_metric_summaries') IS NOT NULL THEN
3411
+ ALTER TABLE ${s}.session_metric_summaries RENAME TO session_metrics;
3412
+ END IF;
3413
+ END;
3414
+ $$ LANGUAGE plpgsql;
3415
+
3416
+ DO $$
3417
+ BEGIN
3418
+ IF to_regclass('${schema}.session_metric_summaries') IS NULL
3419
+ AND to_regclass('${schema}.session_metrics') IS NOT NULL THEN
3420
+ EXECUTE 'CREATE VIEW ${s}.session_metric_summaries AS SELECT * FROM ${s}.session_metrics';
3421
+ END IF;
3422
+ END;
3423
+ $$ LANGUAGE plpgsql;
3424
+
3425
+ CREATE INDEX IF NOT EXISTS idx_${schema}_session_metrics_agent_model
3426
+ ON ${s}.session_metrics(agent_id, model);
3427
+ CREATE INDEX IF NOT EXISTS idx_${schema}_session_metrics_agent_model_effort
3428
+ ON ${s}.session_metrics(agent_id, model, reasoning_effort);
3429
+ CREATE INDEX IF NOT EXISTS idx_${schema}_session_metrics_parent
3430
+ ON ${s}.session_metrics(parent_session_id);
3431
+ CREATE INDEX IF NOT EXISTS idx_${schema}_session_metrics_updated
3432
+ ON ${s}.session_metrics(updated_at DESC);
3433
+
3434
+ DROP INDEX IF EXISTS ${s}.idx_${schema}_sms_agent_model;
3435
+ DROP INDEX IF EXISTS ${s}.idx_${schema}_sms_agent_model_reasoning;
3436
+ DROP INDEX IF EXISTS ${s}.idx_${schema}_sms_parent;
3437
+ DROP INDEX IF EXISTS ${s}.idx_${schema}_sms_updated;
3438
+
3439
+ CREATE OR REPLACE FUNCTION ${s}.cms_update_session_group(
3440
+ p_group_id TEXT,
3441
+ p_patch JSONB
3442
+ ) RETURNS VOID AS $$
3443
+ BEGIN
3444
+ UPDATE ${s}.session_groups
3445
+ SET title = CASE WHEN p_patch ? 'title' THEN NULLIF(BTRIM(p_patch->>'title'), '') ELSE title END,
3446
+ description = CASE WHEN p_patch ? 'description' THEN p_patch->>'description' ELSE description END,
3447
+ owner = CASE WHEN p_patch ? 'owner' THEN p_patch->'owner' ELSE owner END,
3448
+ metadata = CASE WHEN p_patch ? 'metadataPatch' THEN metadata || COALESCE(p_patch->'metadataPatch', '{}'::jsonb) ELSE metadata END,
3449
+ updated_at = now()
3450
+ WHERE group_id = p_group_id;
3451
+ END;
3452
+ $$ LANGUAGE plpgsql;
3453
+
3454
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_session_metric_summary(
3455
+ p_session_id TEXT
3456
+ ) RETURNS SETOF ${s}.session_metrics AS $$
3457
+ BEGIN
3458
+ RETURN QUERY
3459
+ SELECT * FROM ${s}.session_metrics
3460
+ WHERE session_id = p_session_id;
3461
+ END;
3462
+ $$ LANGUAGE plpgsql;
3463
+
3464
+ CREATE OR REPLACE FUNCTION ${s}.cms_create_session(
3465
+ p_session_id TEXT,
3466
+ p_model TEXT,
3467
+ p_reasoning_effort TEXT,
3468
+ p_parent_session_id TEXT,
3469
+ p_is_system BOOLEAN,
3470
+ p_agent_id TEXT,
3471
+ p_splash TEXT,
3472
+ p_group_id TEXT
3473
+ ) RETURNS VOID AS $$
3474
+ DECLARE
3475
+ v_reasoning_effort TEXT := NULLIF(BTRIM(p_reasoning_effort), '');
3476
+ v_group_id TEXT := NULLIF(BTRIM(p_group_id), '');
3477
+ BEGIN
3478
+ IF v_group_id IS NULL AND p_parent_session_id IS NOT NULL THEN
3479
+ SELECT group_id INTO v_group_id
3480
+ FROM ${s}.sessions
3481
+ WHERE session_id = p_parent_session_id;
3482
+ END IF;
3483
+
3484
+ INSERT INTO ${s}.sessions
3485
+ (session_id, model, reasoning_effort, parent_session_id, is_system, agent_id, splash, group_id)
3486
+ VALUES
3487
+ (p_session_id, p_model, v_reasoning_effort, p_parent_session_id, p_is_system, p_agent_id, p_splash, v_group_id)
3488
+ ON CONFLICT (session_id) DO UPDATE
3489
+ SET model = EXCLUDED.model,
3490
+ reasoning_effort = EXCLUDED.reasoning_effort,
3491
+ parent_session_id = EXCLUDED.parent_session_id,
3492
+ is_system = EXCLUDED.is_system,
3493
+ agent_id = EXCLUDED.agent_id,
3494
+ splash = EXCLUDED.splash,
3495
+ group_id = EXCLUDED.group_id,
3496
+ deleted_at = NULL,
3497
+ updated_at = now(),
3498
+ state = 'pending',
3499
+ orchestration_id = NULL,
3500
+ last_error = NULL,
3501
+ last_active_at = NULL,
3502
+ current_iteration = 0,
3503
+ wait_reason = NULL,
3504
+ title_locked = FALSE
3505
+ WHERE ${s}.sessions.deleted_at IS NOT NULL;
3506
+
3507
+ INSERT INTO ${s}.session_metrics
3508
+ (session_id, agent_id, model, reasoning_effort, parent_session_id)
3509
+ VALUES
3510
+ (p_session_id, p_agent_id, p_model, v_reasoning_effort, p_parent_session_id)
3511
+ ON CONFLICT (session_id) DO UPDATE
3512
+ SET agent_id = COALESCE(${s}.session_metrics.agent_id, EXCLUDED.agent_id),
3513
+ model = COALESCE(${s}.session_metrics.model, EXCLUDED.model),
3514
+ reasoning_effort = COALESCE(${s}.session_metrics.reasoning_effort, EXCLUDED.reasoning_effort),
3515
+ parent_session_id = COALESCE(${s}.session_metrics.parent_session_id, EXCLUDED.parent_session_id),
3516
+ updated_at = now();
3517
+ END;
3518
+ $$ LANGUAGE plpgsql;
3519
+
3520
+ CREATE OR REPLACE FUNCTION ${s}.cms_update_session(
3521
+ p_session_id TEXT,
3522
+ p_updates JSONB
3523
+ ) RETURNS VOID AS $$
3524
+ BEGIN
3525
+ UPDATE ${s}.sessions SET
3526
+ orchestration_id = CASE WHEN p_updates ? 'orchestrationId' THEN (p_updates->>'orchestrationId') ELSE orchestration_id END,
3527
+ title = CASE WHEN p_updates ? 'title' THEN (p_updates->>'title') ELSE title END,
3528
+ title_locked = CASE WHEN p_updates ? 'titleLocked' THEN (p_updates->>'titleLocked')::BOOLEAN ELSE title_locked END,
3529
+ state = CASE WHEN p_updates ? 'state' THEN (p_updates->>'state') ELSE state END,
3530
+ model = CASE WHEN p_updates ? 'model' THEN (p_updates->>'model') ELSE model END,
3531
+ reasoning_effort = CASE WHEN p_updates ? 'reasoningEffort' THEN NULLIF(BTRIM(p_updates->>'reasoningEffort'), '') ELSE reasoning_effort END,
3532
+ last_active_at = CASE WHEN p_updates ? 'lastActiveAt' THEN (p_updates->>'lastActiveAt')::TIMESTAMPTZ ELSE last_active_at END,
3533
+ current_iteration = CASE WHEN p_updates ? 'currentIteration' THEN (p_updates->>'currentIteration')::INT ELSE current_iteration END,
3534
+ last_error = CASE WHEN p_updates ? 'lastError' THEN (p_updates->>'lastError') ELSE last_error END,
3535
+ wait_reason = CASE WHEN p_updates ? 'waitReason' THEN (p_updates->>'waitReason') ELSE wait_reason END,
3536
+ is_system = CASE WHEN p_updates ? 'isSystem' THEN (p_updates->>'isSystem')::BOOLEAN ELSE is_system END,
3537
+ agent_id = CASE WHEN p_updates ? 'agentId' THEN (p_updates->>'agentId') ELSE agent_id END,
3538
+ splash = CASE WHEN p_updates ? 'splash' THEN (p_updates->>'splash') ELSE splash END,
3539
+ group_id = group_id,
3540
+ updated_at = now()
3541
+ WHERE session_id = p_session_id;
3542
+
3543
+ IF p_updates ? 'groupId' THEN
3544
+ PERFORM ${s}.cms_assign_session_group(p_session_id, p_updates->>'groupId');
3545
+ END IF;
3546
+
3547
+ UPDATE ${s}.session_metrics
3548
+ SET model = CASE WHEN p_updates ? 'model' THEN (p_updates->>'model') ELSE model END,
3549
+ reasoning_effort = CASE WHEN p_updates ? 'reasoningEffort' THEN NULLIF(BTRIM(p_updates->>'reasoningEffort'), '') ELSE reasoning_effort END,
3550
+ updated_at = CASE WHEN p_updates ? 'model' OR p_updates ? 'reasoningEffort' THEN now() ELSE updated_at END
3551
+ WHERE session_id = p_session_id
3552
+ AND (p_updates ? 'model' OR p_updates ? 'reasoningEffort');
3553
+ END;
3554
+ $$ LANGUAGE plpgsql;
3555
+
3556
+ CREATE OR REPLACE FUNCTION ${s}.cms_upsert_session_metric_summary(
3557
+ p_session_id TEXT,
3558
+ p_updates JSONB
3559
+ ) RETURNS VOID AS $$
3560
+ DECLARE
3561
+ v_snapshot BIGINT := COALESCE((p_updates->>'snapshotSizeBytes')::BIGINT, 0);
3562
+ v_dehydration INT := COALESCE((p_updates->>'dehydrationCountIncrement')::INT, 0);
3563
+ v_hydration INT := COALESCE((p_updates->>'hydrationCountIncrement')::INT, 0);
3564
+ v_lossy INT := COALESCE((p_updates->>'lossyHandoffCountIncrement')::INT, 0);
3565
+ v_tokens_in BIGINT := COALESCE((p_updates->>'tokensInputIncrement')::BIGINT, 0);
3566
+ v_tokens_out BIGINT := COALESCE((p_updates->>'tokensOutputIncrement')::BIGINT, 0);
3567
+ v_tokens_cread BIGINT := COALESCE((p_updates->>'tokensCacheReadIncrement')::BIGINT, 0);
3568
+ v_tokens_cwrite BIGINT := COALESCE((p_updates->>'tokensCacheWriteIncrement')::BIGINT, 0);
3569
+ v_set_dehydrated BOOLEAN := COALESCE((p_updates->>'lastDehydratedAt')::BOOLEAN, FALSE);
3570
+ v_set_hydrated BOOLEAN := COALESCE((p_updates->>'lastHydratedAt')::BOOLEAN, FALSE);
3571
+ v_set_checkpoint BOOLEAN := COALESCE((p_updates->>'lastCheckpointAt')::BOOLEAN, FALSE);
3572
+ BEGIN
3573
+ INSERT INTO ${s}.session_metrics (
3574
+ session_id, snapshot_size_bytes,
3575
+ dehydration_count, hydration_count, lossy_handoff_count,
3576
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write
3577
+ ) VALUES (
3578
+ p_session_id, v_snapshot,
3579
+ v_dehydration, v_hydration, v_lossy,
3580
+ v_tokens_in, v_tokens_out, v_tokens_cread, v_tokens_cwrite
3581
+ )
3582
+ ON CONFLICT (session_id) DO UPDATE SET
3583
+ snapshot_size_bytes = CASE
3584
+ WHEN p_updates ? 'snapshotSizeBytes'
3585
+ THEN v_snapshot
3586
+ ELSE ${s}.session_metrics.snapshot_size_bytes
3587
+ END,
3588
+ dehydration_count = ${s}.session_metrics.dehydration_count + v_dehydration,
3589
+ hydration_count = ${s}.session_metrics.hydration_count + v_hydration,
3590
+ lossy_handoff_count = ${s}.session_metrics.lossy_handoff_count + v_lossy,
3591
+ tokens_input = ${s}.session_metrics.tokens_input + v_tokens_in,
3592
+ tokens_output = ${s}.session_metrics.tokens_output + v_tokens_out,
3593
+ tokens_cache_read = ${s}.session_metrics.tokens_cache_read + v_tokens_cread,
3594
+ tokens_cache_write = ${s}.session_metrics.tokens_cache_write + v_tokens_cwrite,
3595
+ last_dehydrated_at = CASE WHEN v_set_dehydrated THEN now() ELSE ${s}.session_metrics.last_dehydrated_at END,
3596
+ last_hydrated_at = CASE WHEN v_set_hydrated THEN now() ELSE ${s}.session_metrics.last_hydrated_at END,
3597
+ last_checkpoint_at = CASE WHEN v_set_checkpoint THEN now() ELSE ${s}.session_metrics.last_checkpoint_at END,
3598
+ updated_at = now();
3599
+ END;
3600
+ $$ LANGUAGE plpgsql;
3601
+
3602
+ CREATE OR REPLACE FUNCTION ${s}.cms_prune_deleted_summaries(
3603
+ p_older_than TIMESTAMPTZ
3604
+ ) RETURNS BIGINT AS $$
3605
+ DECLARE
3606
+ deleted_count BIGINT;
3607
+ BEGIN
3608
+ DELETE FROM ${s}.session_metrics
3609
+ WHERE deleted_at IS NOT NULL AND deleted_at < p_older_than;
3610
+ GET DIAGNOSTICS deleted_count = ROW_COUNT;
3611
+ RETURN deleted_count;
3612
+ END;
3613
+ $$ LANGUAGE plpgsql;
3614
+
3615
+ ALTER TABLE ${s}.session_turn_metrics ADD COLUMN IF NOT EXISTS reasoning_effort TEXT;
3616
+
3617
+ INSERT INTO ${s}.session_turn_metrics (
3618
+ session_id, agent_id, model, reasoning_effort, turn_index,
3619
+ started_at, ended_at, duration_ms,
3620
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write,
3621
+ tool_calls, tool_errors, result_type, error_message, worker_node_id
3622
+ )
3623
+ SELECT
3624
+ m.session_id,
3625
+ m.agent_id,
3626
+ m.model,
3627
+ m.reasoning_effort,
3628
+ 0,
3629
+ COALESCE(m.created_at, now()),
3630
+ GREATEST(COALESCE(m.updated_at, m.created_at, now()), COALESCE(m.created_at, now())),
3631
+ GREATEST(0, FLOOR(EXTRACT(EPOCH FROM (GREATEST(COALESCE(m.updated_at, m.created_at, now()), COALESCE(m.created_at, now())) - COALESCE(m.created_at, now()))) * 1000)::INT),
3632
+ m.tokens_input,
3633
+ m.tokens_output,
3634
+ m.tokens_cache_read,
3635
+ m.tokens_cache_write,
3636
+ 0,
3637
+ 0,
3638
+ 'legacy_summary',
3639
+ NULL,
3640
+ NULL
3641
+ FROM ${s}.session_metrics m
3642
+ WHERE (COALESCE(m.tokens_input, 0) <> 0
3643
+ OR COALESCE(m.tokens_output, 0) <> 0
3644
+ OR COALESCE(m.tokens_cache_read, 0) <> 0
3645
+ OR COALESCE(m.tokens_cache_write, 0) <> 0)
3646
+ AND NOT EXISTS (
3647
+ SELECT 1
3648
+ FROM ${s}.session_turn_metrics t
3649
+ WHERE t.session_id = m.session_id
3650
+ );
3651
+
3652
+ CREATE INDEX IF NOT EXISTS idx_${schema}_turn_metrics_session_model
3653
+ ON ${s}.session_turn_metrics(session_id, model, reasoning_effort);
3654
+ CREATE INDEX IF NOT EXISTS idx_${schema}_turn_metrics_model_effort_started
3655
+ ON ${s}.session_turn_metrics(model, reasoning_effort, started_at DESC);
3656
+ CREATE INDEX IF NOT EXISTS idx_${schema}_turn_metrics_agent_model_started
3657
+ ON ${s}.session_turn_metrics(agent_id, model, reasoning_effort, started_at DESC);
3658
+
3659
+ -- Signature/return-type changes require drop-then-create.
3660
+ DROP FUNCTION IF EXISTS ${s}.cms_insert_turn_metric(
3661
+ TEXT, TEXT, TEXT, INTEGER, TIMESTAMPTZ, TIMESTAMPTZ, INTEGER,
3662
+ BIGINT, BIGINT, BIGINT, BIGINT, INTEGER, INTEGER, TEXT, TEXT, TEXT);
3663
+ DROP FUNCTION IF EXISTS ${s}.cms_get_session_turn_metrics(TEXT, TIMESTAMPTZ, INT);
3664
+
3665
+ CREATE OR REPLACE FUNCTION ${s}.cms_insert_turn_metric(
3666
+ p_session_id TEXT,
3667
+ p_agent_id TEXT,
3668
+ p_model TEXT,
3669
+ p_reasoning_effort TEXT,
3670
+ p_turn_index INTEGER,
3671
+ p_started_at TIMESTAMPTZ,
3672
+ p_ended_at TIMESTAMPTZ,
3673
+ p_duration_ms INTEGER,
3674
+ p_tokens_input BIGINT,
3675
+ p_tokens_output BIGINT,
3676
+ p_tokens_cache_read BIGINT,
3677
+ p_tokens_cache_write BIGINT,
3678
+ p_tool_calls INTEGER,
3679
+ p_tool_errors INTEGER,
3680
+ p_result_type TEXT,
3681
+ p_error_message TEXT,
3682
+ p_worker_node_id TEXT
3683
+ ) RETURNS VOID AS $$
3684
+ BEGIN
3685
+ INSERT INTO ${s}.session_turn_metrics (
3686
+ session_id, agent_id, model, reasoning_effort, turn_index,
3687
+ started_at, ended_at, duration_ms,
3688
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write,
3689
+ tool_calls, tool_errors, result_type, error_message, worker_node_id
3690
+ ) VALUES (
3691
+ p_session_id, p_agent_id, p_model, NULLIF(BTRIM(p_reasoning_effort), ''), p_turn_index,
3692
+ p_started_at, p_ended_at, p_duration_ms,
3693
+ p_tokens_input, p_tokens_output, p_tokens_cache_read, p_tokens_cache_write,
3694
+ p_tool_calls, p_tool_errors, p_result_type, p_error_message, p_worker_node_id
3695
+ );
3696
+ END;
3697
+ $$ LANGUAGE plpgsql;
3698
+
3699
+ CREATE OR REPLACE FUNCTION ${s}.cms_complete_turn_writeback(
3700
+ p_session_id TEXT,
3701
+ p_agent_id TEXT,
3702
+ p_model TEXT,
3703
+ p_reasoning_effort TEXT,
3704
+ p_turn_index INTEGER,
3705
+ p_started_at TIMESTAMPTZ,
3706
+ p_ended_at TIMESTAMPTZ,
3707
+ p_duration_ms INTEGER,
3708
+ p_tokens_input BIGINT,
3709
+ p_tokens_output BIGINT,
3710
+ p_tokens_cache_read BIGINT,
3711
+ p_tokens_cache_write BIGINT,
3712
+ p_tool_calls INTEGER,
3713
+ p_tool_errors INTEGER,
3714
+ p_tool_names TEXT[],
3715
+ p_result_type TEXT,
3716
+ p_error_message TEXT,
3717
+ p_worker_node_id TEXT,
3718
+ p_state TEXT,
3719
+ p_last_active_at TIMESTAMPTZ,
3720
+ p_last_error TEXT,
3721
+ p_wait_reason TEXT,
3722
+ p_current_iteration INTEGER
3723
+ ) RETURNS VOID AS $$
3724
+ DECLARE
3725
+ v_reasoning_effort TEXT := NULLIF(BTRIM(p_reasoning_effort), '');
3726
+ v_ended_at TIMESTAMPTZ := COALESCE(p_ended_at, now());
3727
+ v_started_at TIMESTAMPTZ := COALESCE(p_started_at, v_ended_at);
3728
+ v_duration_ms INTEGER := GREATEST(0, COALESCE(p_duration_ms, FLOOR(EXTRACT(EPOCH FROM (v_ended_at - v_started_at)) * 1000)::INT));
3729
+ BEGIN
3730
+ UPDATE ${s}.sessions
3731
+ SET state = COALESCE(p_state, state),
3732
+ last_active_at = COALESCE(p_last_active_at, v_ended_at),
3733
+ current_iteration = COALESCE(p_current_iteration, current_iteration),
3734
+ last_error = p_last_error,
3735
+ wait_reason = p_wait_reason,
3736
+ updated_at = now()
3737
+ WHERE session_id = p_session_id;
3738
+
3739
+ INSERT INTO ${s}.session_metrics (
3740
+ session_id, agent_id, model, reasoning_effort,
3741
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write
3742
+ ) VALUES (
3743
+ p_session_id, p_agent_id, p_model, v_reasoning_effort,
3744
+ COALESCE(p_tokens_input, 0), COALESCE(p_tokens_output, 0),
3745
+ COALESCE(p_tokens_cache_read, 0), COALESCE(p_tokens_cache_write, 0)
3746
+ )
3747
+ ON CONFLICT (session_id) DO UPDATE SET
3748
+ agent_id = COALESCE(${s}.session_metrics.agent_id, EXCLUDED.agent_id),
3749
+ model = COALESCE(EXCLUDED.model, ${s}.session_metrics.model),
3750
+ reasoning_effort = COALESCE(EXCLUDED.reasoning_effort, ${s}.session_metrics.reasoning_effort),
3751
+ tokens_input = ${s}.session_metrics.tokens_input + EXCLUDED.tokens_input,
3752
+ tokens_output = ${s}.session_metrics.tokens_output + EXCLUDED.tokens_output,
3753
+ tokens_cache_read = ${s}.session_metrics.tokens_cache_read + EXCLUDED.tokens_cache_read,
3754
+ tokens_cache_write = ${s}.session_metrics.tokens_cache_write + EXCLUDED.tokens_cache_write,
3755
+ updated_at = now();
3756
+
3757
+ INSERT INTO ${s}.session_turn_metrics (
3758
+ session_id, agent_id, model, reasoning_effort, turn_index,
3759
+ started_at, ended_at, duration_ms,
3760
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write,
3761
+ tool_calls, tool_errors, result_type, error_message, worker_node_id
3762
+ ) VALUES (
3763
+ p_session_id, p_agent_id, p_model, v_reasoning_effort, COALESCE(p_turn_index, 0),
3764
+ v_started_at, v_ended_at, v_duration_ms,
3765
+ COALESCE(p_tokens_input, 0), COALESCE(p_tokens_output, 0),
3766
+ COALESCE(p_tokens_cache_read, 0), COALESCE(p_tokens_cache_write, 0),
3767
+ COALESCE(p_tool_calls, 0), COALESCE(p_tool_errors, 0),
3768
+ p_result_type, p_error_message, p_worker_node_id
3769
+ );
3770
+
3771
+ INSERT INTO ${s}.session_events (session_id, event_type, data, worker_node_id)
3772
+ VALUES (
3773
+ p_session_id,
3774
+ 'session.turn_completed',
3775
+ jsonb_build_object(
3776
+ 'iteration', COALESCE(p_turn_index, 0),
3777
+ 'turnIndex', COALESCE(p_turn_index, 0),
3778
+ 'model', p_model,
3779
+ 'reasoningEffort', v_reasoning_effort,
3780
+ 'startedAt', v_started_at,
3781
+ 'endedAt', v_ended_at,
3782
+ 'durationMs', v_duration_ms,
3783
+ 'tokensInput', COALESCE(p_tokens_input, 0),
3784
+ 'tokensOutput', COALESCE(p_tokens_output, 0),
3785
+ 'tokensCacheRead', COALESCE(p_tokens_cache_read, 0),
3786
+ 'tokensCacheWrite', COALESCE(p_tokens_cache_write, 0),
3787
+ 'toolCalls', COALESCE(p_tool_calls, 0),
3788
+ 'toolErrors', COALESCE(p_tool_errors, 0),
3789
+ 'toolNames', to_jsonb(COALESCE(p_tool_names, ARRAY[]::TEXT[])),
3790
+ 'resultType', p_result_type,
3791
+ 'errorMessage', p_error_message,
3792
+ 'workerNodeId', p_worker_node_id
3793
+ ),
3794
+ p_worker_node_id
3795
+ );
3796
+ END;
3797
+ $$ LANGUAGE plpgsql;
3798
+
3799
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_session_turn_metrics(
3800
+ p_session_id TEXT,
3801
+ p_since TIMESTAMPTZ DEFAULT NULL,
3802
+ p_limit INT DEFAULT 200
3803
+ ) RETURNS TABLE (
3804
+ id BIGINT,
3805
+ session_id TEXT,
3806
+ agent_id TEXT,
3807
+ model TEXT,
3808
+ reasoning_effort TEXT,
3809
+ turn_index INT,
3810
+ started_at TIMESTAMPTZ,
3811
+ ended_at TIMESTAMPTZ,
3812
+ duration_ms INT,
3813
+ tokens_input BIGINT,
3814
+ tokens_output BIGINT,
3815
+ tokens_cache_read BIGINT,
3816
+ tokens_cache_write BIGINT,
3817
+ tool_calls INT,
3818
+ tool_errors INT,
3819
+ result_type TEXT,
3820
+ error_message TEXT,
3821
+ worker_node_id TEXT,
3822
+ created_at TIMESTAMPTZ
3823
+ ) AS $$
3824
+ DECLARE
3825
+ v_limit INT := GREATEST(1, LEAST(COALESCE(p_limit, 200), 500));
3826
+ BEGIN
3827
+ RETURN QUERY
3828
+ SELECT
3829
+ t.id, t.session_id, t.agent_id, t.model, t.reasoning_effort, t.turn_index,
3830
+ t.started_at, t.ended_at, t.duration_ms,
3831
+ t.tokens_input, t.tokens_output, t.tokens_cache_read, t.tokens_cache_write,
3832
+ t.tool_calls, t.tool_errors, t.result_type, t.error_message,
3833
+ t.worker_node_id, t.created_at
3834
+ FROM ${s}.session_turn_metrics t
3835
+ WHERE t.session_id = p_session_id
3836
+ AND (p_since IS NULL OR t.started_at >= p_since)
3837
+ ORDER BY t.turn_index DESC, t.id DESC
3838
+ LIMIT v_limit;
3839
+ END;
3840
+ $$ LANGUAGE plpgsql;
3841
+
3842
+ -- Per-session token totals grouped by model:effort label, with per-bucket turn count.
3843
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_session_tokens_by_model(
3844
+ p_session_id TEXT
3845
+ ) RETURNS TABLE (
3846
+ model TEXT,
3847
+ turn_count BIGINT,
3848
+ total_tokens_input BIGINT,
3849
+ total_tokens_output BIGINT,
3850
+ total_tokens_cache_read BIGINT,
3851
+ total_tokens_cache_write BIGINT
3852
+ ) AS $$
3853
+ BEGIN
3854
+ RETURN QUERY
3855
+ SELECT
3856
+ norm.label AS model,
3857
+ COUNT(*)::bigint AS turn_count,
3858
+ COALESCE(SUM(norm.tokens_input), 0)::bigint AS total_tokens_input,
3859
+ COALESCE(SUM(norm.tokens_output), 0)::bigint AS total_tokens_output,
3860
+ COALESCE(SUM(norm.tokens_cache_read), 0)::bigint AS total_tokens_cache_read,
3861
+ COALESCE(SUM(norm.tokens_cache_write), 0)::bigint AS total_tokens_cache_write
3862
+ FROM (
3863
+ SELECT
3864
+ CASE
3865
+ WHEN NULLIF(BTRIM(t.reasoning_effort), '') IS NULL
3866
+ THEN COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)')
3867
+ ELSE COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)') || ':' || BTRIM(t.reasoning_effort)
3868
+ END AS label,
3869
+ t.tokens_input, t.tokens_output, t.tokens_cache_read, t.tokens_cache_write
3870
+ FROM ${s}.session_turn_metrics t
3871
+ WHERE t.session_id = p_session_id
3872
+ ) norm
3873
+ GROUP BY norm.label
3874
+ ORDER BY COALESCE(SUM(norm.tokens_input), 0) DESC, norm.label;
3875
+ END;
3876
+ $$ LANGUAGE plpgsql;
3877
+
3878
+ DROP FUNCTION IF EXISTS ${s}.cms_get_session_tree_stats_by_model(TEXT);
3879
+ CREATE FUNCTION ${s}.cms_get_session_tree_stats_by_model(
3880
+ p_session_id TEXT
3881
+ ) RETURNS TABLE (
3882
+ model TEXT,
3883
+ session_count INT,
3884
+ turn_count BIGINT,
3885
+ total_tokens_input BIGINT,
3886
+ total_tokens_output BIGINT,
3887
+ total_tokens_cache_read BIGINT,
3888
+ total_tokens_cache_write BIGINT,
3889
+ total_snapshot_size_bytes BIGINT
3890
+ ) AS $$
3891
+ BEGIN
3892
+ RETURN QUERY
3893
+ WITH RECURSIVE tree AS (
3894
+ SELECT m.session_id FROM ${s}.session_metrics m
3895
+ WHERE m.session_id = p_session_id
3896
+ UNION ALL
3897
+ SELECT m.session_id FROM ${s}.session_metrics m
3898
+ INNER JOIN tree tr ON m.parent_session_id = tr.session_id
3899
+ ), token_rows AS (
3900
+ SELECT
3901
+ CASE
3902
+ WHEN NULLIF(BTRIM(t.reasoning_effort), '') IS NULL
3903
+ THEN COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)')
3904
+ ELSE COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)') || ':' || BTRIM(t.reasoning_effort)
3905
+ END AS model_label,
3906
+ t.session_id,
3907
+ t.tokens_input,
3908
+ t.tokens_output,
3909
+ t.tokens_cache_read,
3910
+ t.tokens_cache_write
3911
+ FROM ${s}.session_turn_metrics t
3912
+ INNER JOIN tree tr ON tr.session_id = t.session_id
3913
+ UNION ALL
3914
+ SELECT
3915
+ CASE
3916
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
3917
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
3918
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
3919
+ END AS model_label,
3920
+ m.session_id,
3921
+ m.tokens_input,
3922
+ m.tokens_output,
3923
+ m.tokens_cache_read,
3924
+ m.tokens_cache_write
3925
+ FROM ${s}.session_metrics m
3926
+ INNER JOIN tree tr ON tr.session_id = m.session_id
3927
+ WHERE NOT EXISTS (
3928
+ SELECT 1 FROM ${s}.session_turn_metrics existing
3929
+ WHERE existing.session_id = m.session_id
3930
+ )
3931
+ ), token_rollup AS (
3932
+ SELECT
3933
+ model_label,
3934
+ COUNT(DISTINCT session_id)::INT AS session_count,
3935
+ COUNT(*)::BIGINT AS turn_count,
3936
+ COALESCE(SUM(tokens_input), 0)::BIGINT AS total_tokens_input,
3937
+ COALESCE(SUM(tokens_output), 0)::BIGINT AS total_tokens_output,
3938
+ COALESCE(SUM(tokens_cache_read), 0)::BIGINT AS total_tokens_cache_read,
3939
+ COALESCE(SUM(tokens_cache_write), 0)::BIGINT AS total_tokens_cache_write
3940
+ FROM token_rows
3941
+ GROUP BY model_label
3942
+ ), metric_rollup AS (
3943
+ SELECT
3944
+ CASE
3945
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
3946
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
3947
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
3948
+ END AS model_label,
3949
+ COUNT(*)::INT AS session_count,
3950
+ COALESCE(SUM(m.snapshot_size_bytes), 0)::BIGINT AS total_snapshot_size_bytes
3951
+ FROM ${s}.session_metrics m
3952
+ INNER JOIN tree tr ON tr.session_id = m.session_id
3953
+ GROUP BY model_label
3954
+ )
3955
+ SELECT
3956
+ COALESCE(t.model_label, m.model_label) AS model,
3957
+ COALESCE(t.session_count, m.session_count, 0)::INT AS session_count,
3958
+ COALESCE(t.turn_count, 0)::BIGINT AS turn_count,
3959
+ COALESCE(t.total_tokens_input, 0)::BIGINT AS total_tokens_input,
3960
+ COALESCE(t.total_tokens_output, 0)::BIGINT AS total_tokens_output,
3961
+ COALESCE(t.total_tokens_cache_read, 0)::BIGINT AS total_tokens_cache_read,
3962
+ COALESCE(t.total_tokens_cache_write, 0)::BIGINT AS total_tokens_cache_write,
3963
+ COALESCE(m.total_snapshot_size_bytes, 0)::BIGINT AS total_snapshot_size_bytes
3964
+ FROM token_rollup t
3965
+ FULL OUTER JOIN metric_rollup m ON m.model_label = t.model_label
3966
+ ORDER BY COALESCE(t.total_tokens_input, 0) DESC, COALESCE(t.model_label, m.model_label);
3967
+ END;
3968
+ $$ LANGUAGE plpgsql;
3969
+
3970
+ DROP FUNCTION IF EXISTS ${s}.cms_get_fleet_stats_by_agent(BOOLEAN, TIMESTAMPTZ);
3971
+ CREATE FUNCTION ${s}.cms_get_fleet_stats_by_agent(
3972
+ p_include_deleted BOOLEAN,
3973
+ p_since TIMESTAMPTZ
3974
+ ) RETURNS TABLE (
3975
+ agent_id TEXT,
3976
+ model TEXT,
3977
+ session_count INT,
3978
+ turn_count BIGINT,
3979
+ total_snapshot_size_bytes BIGINT,
3980
+ total_dehydration_count INT,
3981
+ total_hydration_count INT,
3982
+ total_lossy_handoff_count INT,
3983
+ total_tokens_input BIGINT,
3984
+ total_tokens_output BIGINT,
3985
+ total_tokens_cache_read BIGINT,
3986
+ total_tokens_cache_write BIGINT
3987
+ ) AS $$
3988
+ BEGIN
3989
+ RETURN QUERY
3990
+ WITH token_rows AS (
3991
+ SELECT
3992
+ COALESCE(t.agent_id, m.agent_id, sess.agent_id) AS agent_id_value,
3993
+ CASE
3994
+ WHEN NULLIF(BTRIM(t.reasoning_effort), '') IS NULL
3995
+ THEN COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)')
3996
+ ELSE COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)') || ':' || BTRIM(t.reasoning_effort)
3997
+ END AS model_label,
3998
+ t.session_id,
3999
+ t.tokens_input,
4000
+ t.tokens_output,
4001
+ t.tokens_cache_read,
4002
+ t.tokens_cache_write
4003
+ FROM ${s}.session_turn_metrics t
4004
+ INNER JOIN ${s}.sessions sess ON sess.session_id = t.session_id
4005
+ LEFT JOIN ${s}.session_metrics m ON m.session_id = t.session_id
4006
+ WHERE (p_include_deleted OR sess.deleted_at IS NULL)
4007
+ AND (p_since IS NULL OR t.started_at >= p_since)
4008
+ UNION ALL
4009
+ SELECT
4010
+ COALESCE(m.agent_id, sess.agent_id) AS agent_id_value,
4011
+ CASE
4012
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
4013
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
4014
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
4015
+ END AS model_label,
4016
+ m.session_id,
4017
+ m.tokens_input,
4018
+ m.tokens_output,
4019
+ m.tokens_cache_read,
4020
+ m.tokens_cache_write
4021
+ FROM ${s}.session_metrics m
4022
+ INNER JOIN ${s}.sessions sess ON sess.session_id = m.session_id
4023
+ WHERE (p_include_deleted OR m.deleted_at IS NULL)
4024
+ AND (p_since IS NULL OR m.created_at >= p_since)
4025
+ AND NOT EXISTS (
4026
+ SELECT 1 FROM ${s}.session_turn_metrics existing
4027
+ WHERE existing.session_id = m.session_id
4028
+ )
4029
+ ), token_rollup AS (
4030
+ SELECT
4031
+ agent_id_value,
4032
+ model_label,
4033
+ COUNT(DISTINCT session_id)::INT AS session_count,
4034
+ COUNT(*)::BIGINT AS turn_count,
4035
+ COALESCE(SUM(tokens_input), 0)::BIGINT AS total_tokens_input,
4036
+ COALESCE(SUM(tokens_output), 0)::BIGINT AS total_tokens_output,
4037
+ COALESCE(SUM(tokens_cache_read), 0)::BIGINT AS total_tokens_cache_read,
4038
+ COALESCE(SUM(tokens_cache_write), 0)::BIGINT AS total_tokens_cache_write
4039
+ FROM token_rows
4040
+ GROUP BY agent_id_value, model_label
4041
+ ), metric_rollup AS (
4042
+ SELECT
4043
+ m.agent_id AS agent_id_value,
4044
+ CASE
4045
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
4046
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
4047
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
4048
+ END AS model_label,
4049
+ COUNT(*)::INT AS session_count,
4050
+ COALESCE(SUM(m.snapshot_size_bytes), 0)::BIGINT AS total_snapshot_size_bytes,
4051
+ COALESCE(SUM(m.dehydration_count), 0)::INT AS total_dehydration_count,
4052
+ COALESCE(SUM(m.hydration_count), 0)::INT AS total_hydration_count,
4053
+ COALESCE(SUM(m.lossy_handoff_count), 0)::INT AS total_lossy_handoff_count
4054
+ FROM ${s}.session_metrics m
4055
+ WHERE (p_include_deleted OR m.deleted_at IS NULL)
4056
+ AND (p_since IS NULL OR m.created_at >= p_since)
4057
+ GROUP BY m.agent_id, model_label
4058
+ )
4059
+ SELECT
4060
+ COALESCE(t.agent_id_value, m.agent_id_value) AS agent_id,
4061
+ COALESCE(t.model_label, m.model_label) AS model,
4062
+ COALESCE(t.session_count, m.session_count, 0)::INT AS session_count,
4063
+ COALESCE(t.turn_count, 0)::BIGINT AS turn_count,
4064
+ COALESCE(m.total_snapshot_size_bytes, 0)::BIGINT AS total_snapshot_size_bytes,
4065
+ COALESCE(m.total_dehydration_count, 0)::INT AS total_dehydration_count,
4066
+ COALESCE(m.total_hydration_count, 0)::INT AS total_hydration_count,
4067
+ COALESCE(m.total_lossy_handoff_count, 0)::INT AS total_lossy_handoff_count,
4068
+ COALESCE(t.total_tokens_input, 0)::BIGINT AS total_tokens_input,
4069
+ COALESCE(t.total_tokens_output, 0)::BIGINT AS total_tokens_output,
4070
+ COALESCE(t.total_tokens_cache_read, 0)::BIGINT AS total_tokens_cache_read,
4071
+ COALESCE(t.total_tokens_cache_write, 0)::BIGINT AS total_tokens_cache_write
4072
+ FROM token_rollup t
4073
+ FULL OUTER JOIN metric_rollup m
4074
+ ON m.agent_id_value IS NOT DISTINCT FROM t.agent_id_value
4075
+ AND m.model_label = t.model_label
4076
+ ORDER BY COALESCE(t.total_tokens_input, 0) DESC, COALESCE(t.model_label, m.model_label);
4077
+ END;
4078
+ $$ LANGUAGE plpgsql;
4079
+
4080
+ DROP FUNCTION IF EXISTS ${s}.cms_get_user_stats_by_model(BOOLEAN, TIMESTAMPTZ);
4081
+ CREATE FUNCTION ${s}.cms_get_user_stats_by_model(
4082
+ p_include_deleted BOOLEAN,
4083
+ p_since TIMESTAMPTZ
4084
+ ) RETURNS TABLE (
4085
+ owner_kind TEXT,
4086
+ owner_provider TEXT,
4087
+ owner_subject TEXT,
4088
+ owner_email TEXT,
4089
+ owner_display_name TEXT,
4090
+ model TEXT,
4091
+ session_ids TEXT[],
4092
+ session_count INT,
4093
+ turn_count BIGINT,
4094
+ total_snapshot_size_bytes BIGINT,
4095
+ total_dehydration_count INT,
4096
+ total_hydration_count INT,
4097
+ total_lossy_handoff_count INT,
4098
+ total_tokens_input BIGINT,
4099
+ total_tokens_output BIGINT,
4100
+ total_tokens_cache_read BIGINT,
4101
+ total_tokens_cache_write BIGINT,
4102
+ earliest_session_created_at TIMESTAMPTZ
4103
+ ) AS $$
4104
+ BEGIN
4105
+ RETURN QUERY
4106
+ WITH session_owner AS (
4107
+ SELECT
4108
+ sess.session_id,
4109
+ CASE
4110
+ WHEN sess.is_system THEN 'system'
4111
+ WHEN u.user_id IS NULL THEN 'unowned'
4112
+ ELSE 'user'
4113
+ END::TEXT AS owner_kind_value,
4114
+ u.provider AS owner_provider_value,
4115
+ u.subject AS owner_subject_value,
4116
+ u.email AS owner_email_value,
4117
+ u.display_name AS owner_display_name_value
4118
+ FROM ${s}.sessions sess
4119
+ LEFT JOIN ${s}.session_owners so ON so.session_id = sess.session_id
4120
+ LEFT JOIN ${s}.users u ON u.user_id = so.user_id
4121
+ ), token_rows AS (
4122
+ SELECT
4123
+ so.owner_kind_value, so.owner_provider_value, so.owner_subject_value, so.owner_email_value, so.owner_display_name_value,
4124
+ CASE
4125
+ WHEN NULLIF(BTRIM(t.reasoning_effort), '') IS NULL
4126
+ THEN COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)')
4127
+ ELSE COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)') || ':' || BTRIM(t.reasoning_effort)
4128
+ END AS model_label,
4129
+ t.session_id,
4130
+ t.tokens_input, t.tokens_output, t.tokens_cache_read, t.tokens_cache_write
4131
+ FROM ${s}.session_turn_metrics t
4132
+ INNER JOIN ${s}.sessions sess ON sess.session_id = t.session_id
4133
+ INNER JOIN session_owner so ON so.session_id = t.session_id
4134
+ WHERE (p_include_deleted OR sess.deleted_at IS NULL)
4135
+ AND (p_since IS NULL OR t.started_at >= p_since)
4136
+ UNION ALL
4137
+ SELECT
4138
+ so.owner_kind_value, so.owner_provider_value, so.owner_subject_value, so.owner_email_value, so.owner_display_name_value,
4139
+ CASE
4140
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
4141
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
4142
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
4143
+ END AS model_label,
4144
+ m.session_id,
4145
+ m.tokens_input, m.tokens_output, m.tokens_cache_read, m.tokens_cache_write
4146
+ FROM ${s}.session_metrics m
4147
+ INNER JOIN ${s}.sessions sess ON sess.session_id = m.session_id
4148
+ INNER JOIN session_owner so ON so.session_id = m.session_id
4149
+ WHERE (p_include_deleted OR m.deleted_at IS NULL)
4150
+ AND (p_since IS NULL OR m.created_at >= p_since)
4151
+ AND NOT EXISTS (
4152
+ SELECT 1 FROM ${s}.session_turn_metrics existing
4153
+ WHERE existing.session_id = m.session_id
4154
+ )
4155
+ ), token_rollup AS (
4156
+ SELECT
4157
+ owner_kind_value, owner_provider_value, owner_subject_value, owner_email_value, owner_display_name_value, model_label,
4158
+ ARRAY_AGG(DISTINCT session_id ORDER BY session_id) AS session_ids,
4159
+ COUNT(DISTINCT session_id)::INT AS session_count,
4160
+ COUNT(*)::BIGINT AS turn_count,
4161
+ COALESCE(SUM(tokens_input), 0)::BIGINT AS total_tokens_input,
4162
+ COALESCE(SUM(tokens_output), 0)::BIGINT AS total_tokens_output,
4163
+ COALESCE(SUM(tokens_cache_read), 0)::BIGINT AS total_tokens_cache_read,
4164
+ COALESCE(SUM(tokens_cache_write), 0)::BIGINT AS total_tokens_cache_write
4165
+ FROM token_rows
4166
+ GROUP BY owner_kind_value, owner_provider_value, owner_subject_value, owner_email_value, owner_display_name_value, model_label
4167
+ ), metric_rollup AS (
4168
+ SELECT
4169
+ so.owner_kind_value, so.owner_provider_value, so.owner_subject_value, so.owner_email_value, so.owner_display_name_value,
4170
+ CASE
4171
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
4172
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
4173
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
4174
+ END AS model_label,
4175
+ ARRAY_AGG(DISTINCT m.session_id ORDER BY m.session_id) AS session_ids,
4176
+ COUNT(*)::INT AS session_count,
4177
+ COALESCE(SUM(m.snapshot_size_bytes), 0)::BIGINT AS total_snapshot_size_bytes,
4178
+ COALESCE(SUM(m.dehydration_count), 0)::INT AS total_dehydration_count,
4179
+ COALESCE(SUM(m.hydration_count), 0)::INT AS total_hydration_count,
4180
+ COALESCE(SUM(m.lossy_handoff_count), 0)::INT AS total_lossy_handoff_count,
4181
+ MIN(m.created_at) AS earliest_session_created_at
4182
+ FROM ${s}.session_metrics m
4183
+ INNER JOIN ${s}.sessions sess ON sess.session_id = m.session_id
4184
+ INNER JOIN session_owner so ON so.session_id = m.session_id
4185
+ WHERE (p_include_deleted OR m.deleted_at IS NULL)
4186
+ AND (p_since IS NULL OR m.created_at >= p_since)
4187
+ GROUP BY so.owner_kind_value, so.owner_provider_value, so.owner_subject_value, so.owner_email_value, so.owner_display_name_value, model_label
4188
+ )
4189
+ SELECT
4190
+ COALESCE(t.owner_kind_value, m.owner_kind_value) AS owner_kind,
4191
+ COALESCE(t.owner_provider_value, m.owner_provider_value) AS owner_provider,
4192
+ COALESCE(t.owner_subject_value, m.owner_subject_value) AS owner_subject,
4193
+ COALESCE(t.owner_email_value, m.owner_email_value) AS owner_email,
4194
+ COALESCE(t.owner_display_name_value, m.owner_display_name_value) AS owner_display_name,
4195
+ COALESCE(t.model_label, m.model_label) AS model,
4196
+ ARRAY(SELECT DISTINCT unnest(COALESCE(t.session_ids, ARRAY[]::TEXT[]) || COALESCE(m.session_ids, ARRAY[]::TEXT[])) ORDER BY 1) AS session_ids,
4197
+ COALESCE(t.session_count, m.session_count, 0)::INT AS session_count,
4198
+ COALESCE(t.turn_count, 0)::BIGINT AS turn_count,
4199
+ COALESCE(m.total_snapshot_size_bytes, 0)::BIGINT AS total_snapshot_size_bytes,
4200
+ COALESCE(m.total_dehydration_count, 0)::INT AS total_dehydration_count,
4201
+ COALESCE(m.total_hydration_count, 0)::INT AS total_hydration_count,
4202
+ COALESCE(m.total_lossy_handoff_count, 0)::INT AS total_lossy_handoff_count,
4203
+ COALESCE(t.total_tokens_input, 0)::BIGINT AS total_tokens_input,
4204
+ COALESCE(t.total_tokens_output, 0)::BIGINT AS total_tokens_output,
4205
+ COALESCE(t.total_tokens_cache_read, 0)::BIGINT AS total_tokens_cache_read,
4206
+ COALESCE(t.total_tokens_cache_write, 0)::BIGINT AS total_tokens_cache_write,
4207
+ m.earliest_session_created_at AS earliest_session_created_at
4208
+ FROM token_rollup t
4209
+ FULL OUTER JOIN metric_rollup m
4210
+ ON m.owner_kind_value = t.owner_kind_value
4211
+ AND m.owner_provider_value IS NOT DISTINCT FROM t.owner_provider_value
4212
+ AND m.owner_subject_value IS NOT DISTINCT FROM t.owner_subject_value
4213
+ AND m.model_label = t.model_label
4214
+ ORDER BY COALESCE(t.total_tokens_input, 0) DESC, COALESCE(t.model_label, m.model_label);
4215
+ END;
4216
+ $$ LANGUAGE plpgsql;
4217
+ `;
4218
+ }
4219
+ // ─── Migration 0023: Turn Metrics Stats Fallbacks And Group Owner Patch ───
4220
+ function migration_0023_turn_metrics_stats_fallbacks_and_group_owner_patch(schema) {
4221
+ // Migration 0022 was deployed before these stored-procedure fixes landed.
4222
+ // Its SQL is idempotent, so reapplying the corrected procedure layer under
4223
+ // a new version updates already-migrated schemas without requiring a reset.
4224
+ return migration_0022_turn_metrics_reasoning_effort(schema);
4225
+ }
3387
4226
  //# sourceMappingURL=cms-migrations.js.map