pilotswarm-sdk 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/cms-migrations.d.ts.map +1 -1
  2. package/dist/cms-migrations.js +1090 -1
  3. package/dist/cms-migrations.js.map +1 -1
  4. package/dist/cms.d.ts +41 -1
  5. package/dist/cms.d.ts.map +1 -1
  6. package/dist/cms.js +51 -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 +36 -0
  14. package/dist/managed-session.d.ts.map +1 -1
  15. package/dist/managed-session.js +317 -21
  16. package/dist/managed-session.js.map +1 -1
  17. package/dist/management-client.d.ts +36 -2
  18. package/dist/management-client.d.ts.map +1 -1
  19. package/dist/management-client.js +114 -2
  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 +24 -0
  32. package/dist/orchestration/turn.d.ts.map +1 -1
  33. package/dist/orchestration/turn.js +378 -167
  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 +6 -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/orchestration_1_0_55/agents.d.ts +41 -0
  80. package/dist/orchestration_1_0_55/agents.d.ts.map +1 -0
  81. package/dist/orchestration_1_0_55/agents.js +758 -0
  82. package/dist/orchestration_1_0_55/agents.js.map +1 -0
  83. package/dist/orchestration_1_0_55/index.d.ts +24 -0
  84. package/dist/orchestration_1_0_55/index.d.ts.map +1 -0
  85. package/dist/orchestration_1_0_55/index.js +13 -0
  86. package/dist/orchestration_1_0_55/index.js.map +1 -0
  87. package/dist/orchestration_1_0_55/lifecycle.d.ts +41 -0
  88. package/dist/orchestration_1_0_55/lifecycle.d.ts.map +1 -0
  89. package/dist/orchestration_1_0_55/lifecycle.js +580 -0
  90. package/dist/orchestration_1_0_55/lifecycle.js.map +1 -0
  91. package/dist/orchestration_1_0_55/queue.d.ts +7 -0
  92. package/dist/orchestration_1_0_55/queue.d.ts.map +1 -0
  93. package/dist/orchestration_1_0_55/queue.js +644 -0
  94. package/dist/orchestration_1_0_55/queue.js.map +1 -0
  95. package/dist/orchestration_1_0_55/runtime.d.ts +29 -0
  96. package/dist/orchestration_1_0_55/runtime.d.ts.map +1 -0
  97. package/dist/orchestration_1_0_55/runtime.js +194 -0
  98. package/dist/orchestration_1_0_55/runtime.js.map +1 -0
  99. package/dist/orchestration_1_0_55/state.d.ts +126 -0
  100. package/dist/orchestration_1_0_55/state.d.ts.map +1 -0
  101. package/dist/orchestration_1_0_55/state.js +106 -0
  102. package/dist/orchestration_1_0_55/state.js.map +1 -0
  103. package/dist/orchestration_1_0_55/turn.d.ts +17 -0
  104. package/dist/orchestration_1_0_55/turn.d.ts.map +1 -0
  105. package/dist/orchestration_1_0_55/turn.js +921 -0
  106. package/dist/orchestration_1_0_55/turn.js.map +1 -0
  107. package/dist/orchestration_1_0_55/utils.d.ts +22 -0
  108. package/dist/orchestration_1_0_55/utils.d.ts.map +1 -0
  109. package/dist/orchestration_1_0_55/utils.js +226 -0
  110. package/dist/orchestration_1_0_55/utils.js.map +1 -0
  111. package/dist/session-manager.d.ts +24 -1
  112. package/dist/session-manager.d.ts.map +1 -1
  113. package/dist/session-manager.js +84 -0
  114. package/dist/session-manager.js.map +1 -1
  115. package/dist/session-proxy.d.ts +19 -0
  116. package/dist/session-proxy.d.ts.map +1 -1
  117. package/dist/session-proxy.js +172 -14
  118. package/dist/session-proxy.js.map +1 -1
  119. package/dist/sweeper-tools.d.ts +1 -1
  120. package/dist/sweeper-tools.d.ts.map +1 -1
  121. package/dist/sweeper-tools.js +20 -75
  122. package/dist/sweeper-tools.js.map +1 -1
  123. package/dist/system-agents.d.ts +2 -0
  124. package/dist/system-agents.d.ts.map +1 -1
  125. package/dist/system-agents.js +3 -0
  126. package/dist/system-agents.js.map +1 -1
  127. package/dist/types.d.ts +46 -1
  128. package/dist/types.d.ts.map +1 -1
  129. package/dist/types.js +10 -0
  130. package/dist/types.js.map +1 -1
  131. package/dist/worker.d.ts.map +1 -1
  132. package/dist/worker.js +10 -0
  133. package/dist/worker.js.map +1 -1
  134. package/package.json +3 -3
  135. package/plugins/mgmt/agents/agent-tuner.agent.md +36 -2
  136. package/plugins/mgmt/agents/sweeper.agent.md +7 -7
  137. package/plugins/system/agents/default.agent.md +3 -1
@@ -117,6 +117,21 @@ 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
+ },
130
+ {
131
+ version: "0024",
132
+ name: "stop_turn_active_turn_index",
133
+ sql: migration_0024_stop_turn_active_turn_index(schema),
134
+ },
120
135
  ];
121
136
  }
122
137
  // ─── Migration 0001: Baseline ────────────────────────────────────
@@ -2749,7 +2764,7 @@ BEGIN
2749
2764
  is_system = CASE WHEN p_updates ? 'isSystem' THEN (p_updates->>'isSystem')::BOOLEAN ELSE is_system END,
2750
2765
  agent_id = CASE WHEN p_updates ? 'agentId' THEN (p_updates->>'agentId') ELSE agent_id END,
2751
2766
  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,
2767
+ group_id = group_id,
2753
2768
  updated_at = now()
2754
2769
  WHERE session_id = p_session_id;
2755
2770
 
@@ -3384,4 +3399,1078 @@ END;
3384
3399
  $$ LANGUAGE plpgsql;
3385
3400
  `;
3386
3401
  }
3402
+ // ─── Migration 0022: Turn Metrics Reasoning Effort ──────────────
3403
+ function migration_0022_turn_metrics_reasoning_effort(schema) {
3404
+ const s = `"${schema}"`;
3405
+ return `
3406
+ -- 0022_turn_metrics_reasoning_effort:
3407
+ -- - Add reasoning_effort to per-turn metrics so token attribution aligns with
3408
+ -- the session row model:effort convention and survives mid-session switches.
3409
+ -- - Add model+effort composite indexes for by-model aggregation.
3410
+ -- - Extend insert/read procs; add per-session by-model rollup with turn count.
3411
+
3412
+ DO $$
3413
+ BEGIN
3414
+ IF to_regclass('${schema}.session_metrics') IS NULL
3415
+ AND to_regclass('${schema}.session_metric_summaries') IS NOT NULL THEN
3416
+ ALTER TABLE ${s}.session_metric_summaries RENAME TO session_metrics;
3417
+ END IF;
3418
+ END;
3419
+ $$ LANGUAGE plpgsql;
3420
+
3421
+ DO $$
3422
+ BEGIN
3423
+ IF to_regclass('${schema}.session_metric_summaries') IS NULL
3424
+ AND to_regclass('${schema}.session_metrics') IS NOT NULL THEN
3425
+ EXECUTE 'CREATE VIEW ${s}.session_metric_summaries AS SELECT * FROM ${s}.session_metrics';
3426
+ END IF;
3427
+ END;
3428
+ $$ LANGUAGE plpgsql;
3429
+
3430
+ CREATE INDEX IF NOT EXISTS idx_${schema}_session_metrics_agent_model
3431
+ ON ${s}.session_metrics(agent_id, model);
3432
+ CREATE INDEX IF NOT EXISTS idx_${schema}_session_metrics_agent_model_effort
3433
+ ON ${s}.session_metrics(agent_id, model, reasoning_effort);
3434
+ CREATE INDEX IF NOT EXISTS idx_${schema}_session_metrics_parent
3435
+ ON ${s}.session_metrics(parent_session_id);
3436
+ CREATE INDEX IF NOT EXISTS idx_${schema}_session_metrics_updated
3437
+ ON ${s}.session_metrics(updated_at DESC);
3438
+
3439
+ DROP INDEX IF EXISTS ${s}.idx_${schema}_sms_agent_model;
3440
+ DROP INDEX IF EXISTS ${s}.idx_${schema}_sms_agent_model_reasoning;
3441
+ DROP INDEX IF EXISTS ${s}.idx_${schema}_sms_parent;
3442
+ DROP INDEX IF EXISTS ${s}.idx_${schema}_sms_updated;
3443
+
3444
+ CREATE OR REPLACE FUNCTION ${s}.cms_update_session_group(
3445
+ p_group_id TEXT,
3446
+ p_patch JSONB
3447
+ ) RETURNS VOID AS $$
3448
+ BEGIN
3449
+ UPDATE ${s}.session_groups
3450
+ SET title = CASE WHEN p_patch ? 'title' THEN NULLIF(BTRIM(p_patch->>'title'), '') ELSE title END,
3451
+ description = CASE WHEN p_patch ? 'description' THEN p_patch->>'description' ELSE description END,
3452
+ owner = CASE WHEN p_patch ? 'owner' THEN p_patch->'owner' ELSE owner END,
3453
+ metadata = CASE WHEN p_patch ? 'metadataPatch' THEN metadata || COALESCE(p_patch->'metadataPatch', '{}'::jsonb) ELSE metadata END,
3454
+ updated_at = now()
3455
+ WHERE group_id = p_group_id;
3456
+ END;
3457
+ $$ LANGUAGE plpgsql;
3458
+
3459
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_session_metric_summary(
3460
+ p_session_id TEXT
3461
+ ) RETURNS SETOF ${s}.session_metrics AS $$
3462
+ BEGIN
3463
+ RETURN QUERY
3464
+ SELECT * FROM ${s}.session_metrics
3465
+ WHERE session_id = p_session_id;
3466
+ END;
3467
+ $$ LANGUAGE plpgsql;
3468
+
3469
+ CREATE OR REPLACE FUNCTION ${s}.cms_create_session(
3470
+ p_session_id TEXT,
3471
+ p_model TEXT,
3472
+ p_reasoning_effort TEXT,
3473
+ p_parent_session_id TEXT,
3474
+ p_is_system BOOLEAN,
3475
+ p_agent_id TEXT,
3476
+ p_splash TEXT,
3477
+ p_group_id TEXT
3478
+ ) RETURNS VOID AS $$
3479
+ DECLARE
3480
+ v_reasoning_effort TEXT := NULLIF(BTRIM(p_reasoning_effort), '');
3481
+ v_group_id TEXT := NULLIF(BTRIM(p_group_id), '');
3482
+ BEGIN
3483
+ IF v_group_id IS NULL AND p_parent_session_id IS NOT NULL THEN
3484
+ SELECT group_id INTO v_group_id
3485
+ FROM ${s}.sessions
3486
+ WHERE session_id = p_parent_session_id;
3487
+ END IF;
3488
+
3489
+ INSERT INTO ${s}.sessions
3490
+ (session_id, model, reasoning_effort, parent_session_id, is_system, agent_id, splash, group_id)
3491
+ VALUES
3492
+ (p_session_id, p_model, v_reasoning_effort, p_parent_session_id, p_is_system, p_agent_id, p_splash, v_group_id)
3493
+ ON CONFLICT (session_id) DO UPDATE
3494
+ SET model = EXCLUDED.model,
3495
+ reasoning_effort = EXCLUDED.reasoning_effort,
3496
+ parent_session_id = EXCLUDED.parent_session_id,
3497
+ is_system = EXCLUDED.is_system,
3498
+ agent_id = EXCLUDED.agent_id,
3499
+ splash = EXCLUDED.splash,
3500
+ group_id = EXCLUDED.group_id,
3501
+ deleted_at = NULL,
3502
+ updated_at = now(),
3503
+ state = 'pending',
3504
+ orchestration_id = NULL,
3505
+ last_error = NULL,
3506
+ last_active_at = NULL,
3507
+ current_iteration = 0,
3508
+ wait_reason = NULL,
3509
+ title_locked = FALSE
3510
+ WHERE ${s}.sessions.deleted_at IS NOT NULL;
3511
+
3512
+ INSERT INTO ${s}.session_metrics
3513
+ (session_id, agent_id, model, reasoning_effort, parent_session_id)
3514
+ VALUES
3515
+ (p_session_id, p_agent_id, p_model, v_reasoning_effort, p_parent_session_id)
3516
+ ON CONFLICT (session_id) DO UPDATE
3517
+ SET agent_id = COALESCE(${s}.session_metrics.agent_id, EXCLUDED.agent_id),
3518
+ model = COALESCE(${s}.session_metrics.model, EXCLUDED.model),
3519
+ reasoning_effort = COALESCE(${s}.session_metrics.reasoning_effort, EXCLUDED.reasoning_effort),
3520
+ parent_session_id = COALESCE(${s}.session_metrics.parent_session_id, EXCLUDED.parent_session_id),
3521
+ updated_at = now();
3522
+ END;
3523
+ $$ LANGUAGE plpgsql;
3524
+
3525
+ CREATE OR REPLACE FUNCTION ${s}.cms_update_session(
3526
+ p_session_id TEXT,
3527
+ p_updates JSONB
3528
+ ) RETURNS VOID AS $$
3529
+ BEGIN
3530
+ UPDATE ${s}.sessions SET
3531
+ orchestration_id = CASE WHEN p_updates ? 'orchestrationId' THEN (p_updates->>'orchestrationId') ELSE orchestration_id END,
3532
+ title = CASE WHEN p_updates ? 'title' THEN (p_updates->>'title') ELSE title END,
3533
+ title_locked = CASE WHEN p_updates ? 'titleLocked' THEN (p_updates->>'titleLocked')::BOOLEAN ELSE title_locked END,
3534
+ state = CASE WHEN p_updates ? 'state' THEN (p_updates->>'state') ELSE state END,
3535
+ model = CASE WHEN p_updates ? 'model' THEN (p_updates->>'model') ELSE model END,
3536
+ reasoning_effort = CASE WHEN p_updates ? 'reasoningEffort' THEN NULLIF(BTRIM(p_updates->>'reasoningEffort'), '') ELSE reasoning_effort END,
3537
+ last_active_at = CASE WHEN p_updates ? 'lastActiveAt' THEN (p_updates->>'lastActiveAt')::TIMESTAMPTZ ELSE last_active_at END,
3538
+ current_iteration = CASE WHEN p_updates ? 'currentIteration' THEN (p_updates->>'currentIteration')::INT ELSE current_iteration END,
3539
+ last_error = CASE WHEN p_updates ? 'lastError' THEN (p_updates->>'lastError') ELSE last_error END,
3540
+ wait_reason = CASE WHEN p_updates ? 'waitReason' THEN (p_updates->>'waitReason') ELSE wait_reason END,
3541
+ is_system = CASE WHEN p_updates ? 'isSystem' THEN (p_updates->>'isSystem')::BOOLEAN ELSE is_system END,
3542
+ agent_id = CASE WHEN p_updates ? 'agentId' THEN (p_updates->>'agentId') ELSE agent_id END,
3543
+ splash = CASE WHEN p_updates ? 'splash' THEN (p_updates->>'splash') ELSE splash END,
3544
+ group_id = group_id,
3545
+ updated_at = now()
3546
+ WHERE session_id = p_session_id;
3547
+
3548
+ IF p_updates ? 'groupId' THEN
3549
+ PERFORM ${s}.cms_assign_session_group(p_session_id, p_updates->>'groupId');
3550
+ END IF;
3551
+
3552
+ UPDATE ${s}.session_metrics
3553
+ SET model = CASE WHEN p_updates ? 'model' THEN (p_updates->>'model') ELSE model END,
3554
+ reasoning_effort = CASE WHEN p_updates ? 'reasoningEffort' THEN NULLIF(BTRIM(p_updates->>'reasoningEffort'), '') ELSE reasoning_effort END,
3555
+ updated_at = CASE WHEN p_updates ? 'model' OR p_updates ? 'reasoningEffort' THEN now() ELSE updated_at END
3556
+ WHERE session_id = p_session_id
3557
+ AND (p_updates ? 'model' OR p_updates ? 'reasoningEffort');
3558
+ END;
3559
+ $$ LANGUAGE plpgsql;
3560
+
3561
+ CREATE OR REPLACE FUNCTION ${s}.cms_upsert_session_metric_summary(
3562
+ p_session_id TEXT,
3563
+ p_updates JSONB
3564
+ ) RETURNS VOID AS $$
3565
+ DECLARE
3566
+ v_snapshot BIGINT := COALESCE((p_updates->>'snapshotSizeBytes')::BIGINT, 0);
3567
+ v_dehydration INT := COALESCE((p_updates->>'dehydrationCountIncrement')::INT, 0);
3568
+ v_hydration INT := COALESCE((p_updates->>'hydrationCountIncrement')::INT, 0);
3569
+ v_lossy INT := COALESCE((p_updates->>'lossyHandoffCountIncrement')::INT, 0);
3570
+ v_tokens_in BIGINT := COALESCE((p_updates->>'tokensInputIncrement')::BIGINT, 0);
3571
+ v_tokens_out BIGINT := COALESCE((p_updates->>'tokensOutputIncrement')::BIGINT, 0);
3572
+ v_tokens_cread BIGINT := COALESCE((p_updates->>'tokensCacheReadIncrement')::BIGINT, 0);
3573
+ v_tokens_cwrite BIGINT := COALESCE((p_updates->>'tokensCacheWriteIncrement')::BIGINT, 0);
3574
+ v_set_dehydrated BOOLEAN := COALESCE((p_updates->>'lastDehydratedAt')::BOOLEAN, FALSE);
3575
+ v_set_hydrated BOOLEAN := COALESCE((p_updates->>'lastHydratedAt')::BOOLEAN, FALSE);
3576
+ v_set_checkpoint BOOLEAN := COALESCE((p_updates->>'lastCheckpointAt')::BOOLEAN, FALSE);
3577
+ BEGIN
3578
+ INSERT INTO ${s}.session_metrics (
3579
+ session_id, snapshot_size_bytes,
3580
+ dehydration_count, hydration_count, lossy_handoff_count,
3581
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write
3582
+ ) VALUES (
3583
+ p_session_id, v_snapshot,
3584
+ v_dehydration, v_hydration, v_lossy,
3585
+ v_tokens_in, v_tokens_out, v_tokens_cread, v_tokens_cwrite
3586
+ )
3587
+ ON CONFLICT (session_id) DO UPDATE SET
3588
+ snapshot_size_bytes = CASE
3589
+ WHEN p_updates ? 'snapshotSizeBytes'
3590
+ THEN v_snapshot
3591
+ ELSE ${s}.session_metrics.snapshot_size_bytes
3592
+ END,
3593
+ dehydration_count = ${s}.session_metrics.dehydration_count + v_dehydration,
3594
+ hydration_count = ${s}.session_metrics.hydration_count + v_hydration,
3595
+ lossy_handoff_count = ${s}.session_metrics.lossy_handoff_count + v_lossy,
3596
+ tokens_input = ${s}.session_metrics.tokens_input + v_tokens_in,
3597
+ tokens_output = ${s}.session_metrics.tokens_output + v_tokens_out,
3598
+ tokens_cache_read = ${s}.session_metrics.tokens_cache_read + v_tokens_cread,
3599
+ tokens_cache_write = ${s}.session_metrics.tokens_cache_write + v_tokens_cwrite,
3600
+ last_dehydrated_at = CASE WHEN v_set_dehydrated THEN now() ELSE ${s}.session_metrics.last_dehydrated_at END,
3601
+ last_hydrated_at = CASE WHEN v_set_hydrated THEN now() ELSE ${s}.session_metrics.last_hydrated_at END,
3602
+ last_checkpoint_at = CASE WHEN v_set_checkpoint THEN now() ELSE ${s}.session_metrics.last_checkpoint_at END,
3603
+ updated_at = now();
3604
+ END;
3605
+ $$ LANGUAGE plpgsql;
3606
+
3607
+ CREATE OR REPLACE FUNCTION ${s}.cms_prune_deleted_summaries(
3608
+ p_older_than TIMESTAMPTZ
3609
+ ) RETURNS BIGINT AS $$
3610
+ DECLARE
3611
+ deleted_count BIGINT;
3612
+ BEGIN
3613
+ DELETE FROM ${s}.session_metrics
3614
+ WHERE deleted_at IS NOT NULL AND deleted_at < p_older_than;
3615
+ GET DIAGNOSTICS deleted_count = ROW_COUNT;
3616
+ RETURN deleted_count;
3617
+ END;
3618
+ $$ LANGUAGE plpgsql;
3619
+
3620
+ ALTER TABLE ${s}.session_turn_metrics ADD COLUMN IF NOT EXISTS reasoning_effort TEXT;
3621
+
3622
+ INSERT INTO ${s}.session_turn_metrics (
3623
+ session_id, agent_id, model, reasoning_effort, turn_index,
3624
+ started_at, ended_at, duration_ms,
3625
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write,
3626
+ tool_calls, tool_errors, result_type, error_message, worker_node_id
3627
+ )
3628
+ SELECT
3629
+ m.session_id,
3630
+ m.agent_id,
3631
+ m.model,
3632
+ m.reasoning_effort,
3633
+ 0,
3634
+ COALESCE(m.created_at, now()),
3635
+ GREATEST(COALESCE(m.updated_at, m.created_at, now()), COALESCE(m.created_at, now())),
3636
+ 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),
3637
+ m.tokens_input,
3638
+ m.tokens_output,
3639
+ m.tokens_cache_read,
3640
+ m.tokens_cache_write,
3641
+ 0,
3642
+ 0,
3643
+ 'legacy_summary',
3644
+ NULL,
3645
+ NULL
3646
+ FROM ${s}.session_metrics m
3647
+ WHERE (COALESCE(m.tokens_input, 0) <> 0
3648
+ OR COALESCE(m.tokens_output, 0) <> 0
3649
+ OR COALESCE(m.tokens_cache_read, 0) <> 0
3650
+ OR COALESCE(m.tokens_cache_write, 0) <> 0)
3651
+ AND NOT EXISTS (
3652
+ SELECT 1
3653
+ FROM ${s}.session_turn_metrics t
3654
+ WHERE t.session_id = m.session_id
3655
+ );
3656
+
3657
+ CREATE INDEX IF NOT EXISTS idx_${schema}_turn_metrics_session_model
3658
+ ON ${s}.session_turn_metrics(session_id, model, reasoning_effort);
3659
+ CREATE INDEX IF NOT EXISTS idx_${schema}_turn_metrics_model_effort_started
3660
+ ON ${s}.session_turn_metrics(model, reasoning_effort, started_at DESC);
3661
+ CREATE INDEX IF NOT EXISTS idx_${schema}_turn_metrics_agent_model_started
3662
+ ON ${s}.session_turn_metrics(agent_id, model, reasoning_effort, started_at DESC);
3663
+
3664
+ -- Signature/return-type changes require drop-then-create.
3665
+ DROP FUNCTION IF EXISTS ${s}.cms_insert_turn_metric(
3666
+ TEXT, TEXT, TEXT, INTEGER, TIMESTAMPTZ, TIMESTAMPTZ, INTEGER,
3667
+ BIGINT, BIGINT, BIGINT, BIGINT, INTEGER, INTEGER, TEXT, TEXT, TEXT);
3668
+ DROP FUNCTION IF EXISTS ${s}.cms_get_session_turn_metrics(TEXT, TIMESTAMPTZ, INT);
3669
+
3670
+ CREATE OR REPLACE FUNCTION ${s}.cms_insert_turn_metric(
3671
+ p_session_id TEXT,
3672
+ p_agent_id TEXT,
3673
+ p_model TEXT,
3674
+ p_reasoning_effort TEXT,
3675
+ p_turn_index INTEGER,
3676
+ p_started_at TIMESTAMPTZ,
3677
+ p_ended_at TIMESTAMPTZ,
3678
+ p_duration_ms INTEGER,
3679
+ p_tokens_input BIGINT,
3680
+ p_tokens_output BIGINT,
3681
+ p_tokens_cache_read BIGINT,
3682
+ p_tokens_cache_write BIGINT,
3683
+ p_tool_calls INTEGER,
3684
+ p_tool_errors INTEGER,
3685
+ p_result_type TEXT,
3686
+ p_error_message TEXT,
3687
+ p_worker_node_id TEXT
3688
+ ) RETURNS VOID AS $$
3689
+ BEGIN
3690
+ INSERT INTO ${s}.session_turn_metrics (
3691
+ session_id, agent_id, model, reasoning_effort, turn_index,
3692
+ started_at, ended_at, duration_ms,
3693
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write,
3694
+ tool_calls, tool_errors, result_type, error_message, worker_node_id
3695
+ ) VALUES (
3696
+ p_session_id, p_agent_id, p_model, NULLIF(BTRIM(p_reasoning_effort), ''), p_turn_index,
3697
+ p_started_at, p_ended_at, p_duration_ms,
3698
+ p_tokens_input, p_tokens_output, p_tokens_cache_read, p_tokens_cache_write,
3699
+ p_tool_calls, p_tool_errors, p_result_type, p_error_message, p_worker_node_id
3700
+ );
3701
+ END;
3702
+ $$ LANGUAGE plpgsql;
3703
+
3704
+ CREATE OR REPLACE FUNCTION ${s}.cms_complete_turn_writeback(
3705
+ p_session_id TEXT,
3706
+ p_agent_id TEXT,
3707
+ p_model TEXT,
3708
+ p_reasoning_effort TEXT,
3709
+ p_turn_index INTEGER,
3710
+ p_started_at TIMESTAMPTZ,
3711
+ p_ended_at TIMESTAMPTZ,
3712
+ p_duration_ms INTEGER,
3713
+ p_tokens_input BIGINT,
3714
+ p_tokens_output BIGINT,
3715
+ p_tokens_cache_read BIGINT,
3716
+ p_tokens_cache_write BIGINT,
3717
+ p_tool_calls INTEGER,
3718
+ p_tool_errors INTEGER,
3719
+ p_tool_names TEXT[],
3720
+ p_result_type TEXT,
3721
+ p_error_message TEXT,
3722
+ p_worker_node_id TEXT,
3723
+ p_state TEXT,
3724
+ p_last_active_at TIMESTAMPTZ,
3725
+ p_last_error TEXT,
3726
+ p_wait_reason TEXT,
3727
+ p_current_iteration INTEGER
3728
+ ) RETURNS VOID AS $$
3729
+ DECLARE
3730
+ v_reasoning_effort TEXT := NULLIF(BTRIM(p_reasoning_effort), '');
3731
+ v_ended_at TIMESTAMPTZ := COALESCE(p_ended_at, now());
3732
+ v_started_at TIMESTAMPTZ := COALESCE(p_started_at, v_ended_at);
3733
+ v_duration_ms INTEGER := GREATEST(0, COALESCE(p_duration_ms, FLOOR(EXTRACT(EPOCH FROM (v_ended_at - v_started_at)) * 1000)::INT));
3734
+ BEGIN
3735
+ UPDATE ${s}.sessions
3736
+ SET state = COALESCE(p_state, state),
3737
+ last_active_at = COALESCE(p_last_active_at, v_ended_at),
3738
+ current_iteration = COALESCE(p_current_iteration, current_iteration),
3739
+ last_error = p_last_error,
3740
+ wait_reason = p_wait_reason,
3741
+ updated_at = now()
3742
+ WHERE session_id = p_session_id;
3743
+
3744
+ INSERT INTO ${s}.session_metrics (
3745
+ session_id, agent_id, model, reasoning_effort,
3746
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write
3747
+ ) VALUES (
3748
+ p_session_id, p_agent_id, p_model, v_reasoning_effort,
3749
+ COALESCE(p_tokens_input, 0), COALESCE(p_tokens_output, 0),
3750
+ COALESCE(p_tokens_cache_read, 0), COALESCE(p_tokens_cache_write, 0)
3751
+ )
3752
+ ON CONFLICT (session_id) DO UPDATE SET
3753
+ agent_id = COALESCE(${s}.session_metrics.agent_id, EXCLUDED.agent_id),
3754
+ model = COALESCE(EXCLUDED.model, ${s}.session_metrics.model),
3755
+ reasoning_effort = COALESCE(EXCLUDED.reasoning_effort, ${s}.session_metrics.reasoning_effort),
3756
+ tokens_input = ${s}.session_metrics.tokens_input + EXCLUDED.tokens_input,
3757
+ tokens_output = ${s}.session_metrics.tokens_output + EXCLUDED.tokens_output,
3758
+ tokens_cache_read = ${s}.session_metrics.tokens_cache_read + EXCLUDED.tokens_cache_read,
3759
+ tokens_cache_write = ${s}.session_metrics.tokens_cache_write + EXCLUDED.tokens_cache_write,
3760
+ updated_at = now();
3761
+
3762
+ INSERT INTO ${s}.session_turn_metrics (
3763
+ session_id, agent_id, model, reasoning_effort, turn_index,
3764
+ started_at, ended_at, duration_ms,
3765
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write,
3766
+ tool_calls, tool_errors, result_type, error_message, worker_node_id
3767
+ ) VALUES (
3768
+ p_session_id, p_agent_id, p_model, v_reasoning_effort, COALESCE(p_turn_index, 0),
3769
+ v_started_at, v_ended_at, v_duration_ms,
3770
+ COALESCE(p_tokens_input, 0), COALESCE(p_tokens_output, 0),
3771
+ COALESCE(p_tokens_cache_read, 0), COALESCE(p_tokens_cache_write, 0),
3772
+ COALESCE(p_tool_calls, 0), COALESCE(p_tool_errors, 0),
3773
+ p_result_type, p_error_message, p_worker_node_id
3774
+ );
3775
+
3776
+ INSERT INTO ${s}.session_events (session_id, event_type, data, worker_node_id)
3777
+ VALUES (
3778
+ p_session_id,
3779
+ 'session.turn_completed',
3780
+ jsonb_build_object(
3781
+ 'iteration', COALESCE(p_turn_index, 0),
3782
+ 'turnIndex', COALESCE(p_turn_index, 0),
3783
+ 'model', p_model,
3784
+ 'reasoningEffort', v_reasoning_effort,
3785
+ 'startedAt', v_started_at,
3786
+ 'endedAt', v_ended_at,
3787
+ 'durationMs', v_duration_ms,
3788
+ 'tokensInput', COALESCE(p_tokens_input, 0),
3789
+ 'tokensOutput', COALESCE(p_tokens_output, 0),
3790
+ 'tokensCacheRead', COALESCE(p_tokens_cache_read, 0),
3791
+ 'tokensCacheWrite', COALESCE(p_tokens_cache_write, 0),
3792
+ 'toolCalls', COALESCE(p_tool_calls, 0),
3793
+ 'toolErrors', COALESCE(p_tool_errors, 0),
3794
+ 'toolNames', to_jsonb(COALESCE(p_tool_names, ARRAY[]::TEXT[])),
3795
+ 'resultType', p_result_type,
3796
+ 'errorMessage', p_error_message,
3797
+ 'workerNodeId', p_worker_node_id
3798
+ ),
3799
+ p_worker_node_id
3800
+ );
3801
+ END;
3802
+ $$ LANGUAGE plpgsql;
3803
+
3804
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_session_turn_metrics(
3805
+ p_session_id TEXT,
3806
+ p_since TIMESTAMPTZ DEFAULT NULL,
3807
+ p_limit INT DEFAULT 200
3808
+ ) RETURNS TABLE (
3809
+ id BIGINT,
3810
+ session_id TEXT,
3811
+ agent_id TEXT,
3812
+ model TEXT,
3813
+ reasoning_effort TEXT,
3814
+ turn_index INT,
3815
+ started_at TIMESTAMPTZ,
3816
+ ended_at TIMESTAMPTZ,
3817
+ duration_ms INT,
3818
+ tokens_input BIGINT,
3819
+ tokens_output BIGINT,
3820
+ tokens_cache_read BIGINT,
3821
+ tokens_cache_write BIGINT,
3822
+ tool_calls INT,
3823
+ tool_errors INT,
3824
+ result_type TEXT,
3825
+ error_message TEXT,
3826
+ worker_node_id TEXT,
3827
+ created_at TIMESTAMPTZ
3828
+ ) AS $$
3829
+ DECLARE
3830
+ v_limit INT := GREATEST(1, LEAST(COALESCE(p_limit, 200), 500));
3831
+ BEGIN
3832
+ RETURN QUERY
3833
+ SELECT
3834
+ t.id, t.session_id, t.agent_id, t.model, t.reasoning_effort, t.turn_index,
3835
+ t.started_at, t.ended_at, t.duration_ms,
3836
+ t.tokens_input, t.tokens_output, t.tokens_cache_read, t.tokens_cache_write,
3837
+ t.tool_calls, t.tool_errors, t.result_type, t.error_message,
3838
+ t.worker_node_id, t.created_at
3839
+ FROM ${s}.session_turn_metrics t
3840
+ WHERE t.session_id = p_session_id
3841
+ AND (p_since IS NULL OR t.started_at >= p_since)
3842
+ ORDER BY t.turn_index DESC, t.id DESC
3843
+ LIMIT v_limit;
3844
+ END;
3845
+ $$ LANGUAGE plpgsql;
3846
+
3847
+ -- Per-session token totals grouped by model:effort label, with per-bucket turn count.
3848
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_session_tokens_by_model(
3849
+ p_session_id TEXT
3850
+ ) RETURNS TABLE (
3851
+ model TEXT,
3852
+ turn_count BIGINT,
3853
+ total_tokens_input BIGINT,
3854
+ total_tokens_output BIGINT,
3855
+ total_tokens_cache_read BIGINT,
3856
+ total_tokens_cache_write BIGINT
3857
+ ) AS $$
3858
+ BEGIN
3859
+ RETURN QUERY
3860
+ SELECT
3861
+ norm.label AS model,
3862
+ COUNT(*)::bigint AS turn_count,
3863
+ COALESCE(SUM(norm.tokens_input), 0)::bigint AS total_tokens_input,
3864
+ COALESCE(SUM(norm.tokens_output), 0)::bigint AS total_tokens_output,
3865
+ COALESCE(SUM(norm.tokens_cache_read), 0)::bigint AS total_tokens_cache_read,
3866
+ COALESCE(SUM(norm.tokens_cache_write), 0)::bigint AS total_tokens_cache_write
3867
+ FROM (
3868
+ SELECT
3869
+ CASE
3870
+ WHEN NULLIF(BTRIM(t.reasoning_effort), '') IS NULL
3871
+ THEN COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)')
3872
+ ELSE COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)') || ':' || BTRIM(t.reasoning_effort)
3873
+ END AS label,
3874
+ t.tokens_input, t.tokens_output, t.tokens_cache_read, t.tokens_cache_write
3875
+ FROM ${s}.session_turn_metrics t
3876
+ WHERE t.session_id = p_session_id
3877
+ ) norm
3878
+ GROUP BY norm.label
3879
+ ORDER BY COALESCE(SUM(norm.tokens_input), 0) DESC, norm.label;
3880
+ END;
3881
+ $$ LANGUAGE plpgsql;
3882
+
3883
+ DROP FUNCTION IF EXISTS ${s}.cms_get_session_tree_stats_by_model(TEXT);
3884
+ CREATE FUNCTION ${s}.cms_get_session_tree_stats_by_model(
3885
+ p_session_id TEXT
3886
+ ) RETURNS TABLE (
3887
+ model TEXT,
3888
+ session_count INT,
3889
+ turn_count BIGINT,
3890
+ total_tokens_input BIGINT,
3891
+ total_tokens_output BIGINT,
3892
+ total_tokens_cache_read BIGINT,
3893
+ total_tokens_cache_write BIGINT,
3894
+ total_snapshot_size_bytes BIGINT
3895
+ ) AS $$
3896
+ BEGIN
3897
+ RETURN QUERY
3898
+ WITH RECURSIVE tree AS (
3899
+ SELECT m.session_id FROM ${s}.session_metrics m
3900
+ WHERE m.session_id = p_session_id
3901
+ UNION ALL
3902
+ SELECT m.session_id FROM ${s}.session_metrics m
3903
+ INNER JOIN tree tr ON m.parent_session_id = tr.session_id
3904
+ ), token_rows AS (
3905
+ SELECT
3906
+ CASE
3907
+ WHEN NULLIF(BTRIM(t.reasoning_effort), '') IS NULL
3908
+ THEN COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)')
3909
+ ELSE COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)') || ':' || BTRIM(t.reasoning_effort)
3910
+ END AS model_label,
3911
+ t.session_id,
3912
+ t.tokens_input,
3913
+ t.tokens_output,
3914
+ t.tokens_cache_read,
3915
+ t.tokens_cache_write
3916
+ FROM ${s}.session_turn_metrics t
3917
+ INNER JOIN tree tr ON tr.session_id = t.session_id
3918
+ UNION ALL
3919
+ SELECT
3920
+ CASE
3921
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
3922
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
3923
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
3924
+ END AS model_label,
3925
+ m.session_id,
3926
+ m.tokens_input,
3927
+ m.tokens_output,
3928
+ m.tokens_cache_read,
3929
+ m.tokens_cache_write
3930
+ FROM ${s}.session_metrics m
3931
+ INNER JOIN tree tr ON tr.session_id = m.session_id
3932
+ WHERE NOT EXISTS (
3933
+ SELECT 1 FROM ${s}.session_turn_metrics existing
3934
+ WHERE existing.session_id = m.session_id
3935
+ )
3936
+ ), token_rollup AS (
3937
+ SELECT
3938
+ model_label,
3939
+ COUNT(DISTINCT session_id)::INT AS session_count,
3940
+ COUNT(*)::BIGINT AS turn_count,
3941
+ COALESCE(SUM(tokens_input), 0)::BIGINT AS total_tokens_input,
3942
+ COALESCE(SUM(tokens_output), 0)::BIGINT AS total_tokens_output,
3943
+ COALESCE(SUM(tokens_cache_read), 0)::BIGINT AS total_tokens_cache_read,
3944
+ COALESCE(SUM(tokens_cache_write), 0)::BIGINT AS total_tokens_cache_write
3945
+ FROM token_rows
3946
+ GROUP BY model_label
3947
+ ), metric_rollup AS (
3948
+ SELECT
3949
+ CASE
3950
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
3951
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
3952
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
3953
+ END AS model_label,
3954
+ COUNT(*)::INT AS session_count,
3955
+ COALESCE(SUM(m.snapshot_size_bytes), 0)::BIGINT AS total_snapshot_size_bytes
3956
+ FROM ${s}.session_metrics m
3957
+ INNER JOIN tree tr ON tr.session_id = m.session_id
3958
+ GROUP BY model_label
3959
+ )
3960
+ SELECT
3961
+ COALESCE(t.model_label, m.model_label) AS model,
3962
+ COALESCE(t.session_count, m.session_count, 0)::INT AS session_count,
3963
+ COALESCE(t.turn_count, 0)::BIGINT AS turn_count,
3964
+ COALESCE(t.total_tokens_input, 0)::BIGINT AS total_tokens_input,
3965
+ COALESCE(t.total_tokens_output, 0)::BIGINT AS total_tokens_output,
3966
+ COALESCE(t.total_tokens_cache_read, 0)::BIGINT AS total_tokens_cache_read,
3967
+ COALESCE(t.total_tokens_cache_write, 0)::BIGINT AS total_tokens_cache_write,
3968
+ COALESCE(m.total_snapshot_size_bytes, 0)::BIGINT AS total_snapshot_size_bytes
3969
+ FROM token_rollup t
3970
+ FULL OUTER JOIN metric_rollup m ON m.model_label = t.model_label
3971
+ ORDER BY COALESCE(t.total_tokens_input, 0) DESC, COALESCE(t.model_label, m.model_label);
3972
+ END;
3973
+ $$ LANGUAGE plpgsql;
3974
+
3975
+ DROP FUNCTION IF EXISTS ${s}.cms_get_fleet_stats_by_agent(BOOLEAN, TIMESTAMPTZ);
3976
+ CREATE FUNCTION ${s}.cms_get_fleet_stats_by_agent(
3977
+ p_include_deleted BOOLEAN,
3978
+ p_since TIMESTAMPTZ
3979
+ ) RETURNS TABLE (
3980
+ agent_id TEXT,
3981
+ model TEXT,
3982
+ session_count INT,
3983
+ turn_count BIGINT,
3984
+ total_snapshot_size_bytes BIGINT,
3985
+ total_dehydration_count INT,
3986
+ total_hydration_count INT,
3987
+ total_lossy_handoff_count INT,
3988
+ total_tokens_input BIGINT,
3989
+ total_tokens_output BIGINT,
3990
+ total_tokens_cache_read BIGINT,
3991
+ total_tokens_cache_write BIGINT
3992
+ ) AS $$
3993
+ BEGIN
3994
+ RETURN QUERY
3995
+ WITH token_rows AS (
3996
+ SELECT
3997
+ COALESCE(t.agent_id, m.agent_id, sess.agent_id) AS agent_id_value,
3998
+ CASE
3999
+ WHEN NULLIF(BTRIM(t.reasoning_effort), '') IS NULL
4000
+ THEN COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)')
4001
+ ELSE COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)') || ':' || BTRIM(t.reasoning_effort)
4002
+ END AS model_label,
4003
+ t.session_id,
4004
+ t.tokens_input,
4005
+ t.tokens_output,
4006
+ t.tokens_cache_read,
4007
+ t.tokens_cache_write
4008
+ FROM ${s}.session_turn_metrics t
4009
+ INNER JOIN ${s}.sessions sess ON sess.session_id = t.session_id
4010
+ LEFT JOIN ${s}.session_metrics m ON m.session_id = t.session_id
4011
+ WHERE (p_include_deleted OR sess.deleted_at IS NULL)
4012
+ AND (p_since IS NULL OR t.started_at >= p_since)
4013
+ UNION ALL
4014
+ SELECT
4015
+ COALESCE(m.agent_id, sess.agent_id) AS agent_id_value,
4016
+ CASE
4017
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
4018
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
4019
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
4020
+ END AS model_label,
4021
+ m.session_id,
4022
+ m.tokens_input,
4023
+ m.tokens_output,
4024
+ m.tokens_cache_read,
4025
+ m.tokens_cache_write
4026
+ FROM ${s}.session_metrics m
4027
+ INNER JOIN ${s}.sessions sess ON sess.session_id = m.session_id
4028
+ WHERE (p_include_deleted OR m.deleted_at IS NULL)
4029
+ AND (p_since IS NULL OR m.created_at >= p_since)
4030
+ AND NOT EXISTS (
4031
+ SELECT 1 FROM ${s}.session_turn_metrics existing
4032
+ WHERE existing.session_id = m.session_id
4033
+ )
4034
+ ), token_rollup AS (
4035
+ SELECT
4036
+ agent_id_value,
4037
+ model_label,
4038
+ COUNT(DISTINCT session_id)::INT AS session_count,
4039
+ COUNT(*)::BIGINT AS turn_count,
4040
+ COALESCE(SUM(tokens_input), 0)::BIGINT AS total_tokens_input,
4041
+ COALESCE(SUM(tokens_output), 0)::BIGINT AS total_tokens_output,
4042
+ COALESCE(SUM(tokens_cache_read), 0)::BIGINT AS total_tokens_cache_read,
4043
+ COALESCE(SUM(tokens_cache_write), 0)::BIGINT AS total_tokens_cache_write
4044
+ FROM token_rows
4045
+ GROUP BY agent_id_value, model_label
4046
+ ), metric_rollup AS (
4047
+ SELECT
4048
+ m.agent_id AS agent_id_value,
4049
+ CASE
4050
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
4051
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
4052
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
4053
+ END AS model_label,
4054
+ COUNT(*)::INT AS session_count,
4055
+ COALESCE(SUM(m.snapshot_size_bytes), 0)::BIGINT AS total_snapshot_size_bytes,
4056
+ COALESCE(SUM(m.dehydration_count), 0)::INT AS total_dehydration_count,
4057
+ COALESCE(SUM(m.hydration_count), 0)::INT AS total_hydration_count,
4058
+ COALESCE(SUM(m.lossy_handoff_count), 0)::INT AS total_lossy_handoff_count
4059
+ FROM ${s}.session_metrics m
4060
+ WHERE (p_include_deleted OR m.deleted_at IS NULL)
4061
+ AND (p_since IS NULL OR m.created_at >= p_since)
4062
+ GROUP BY m.agent_id, model_label
4063
+ )
4064
+ SELECT
4065
+ COALESCE(t.agent_id_value, m.agent_id_value) AS agent_id,
4066
+ COALESCE(t.model_label, m.model_label) AS model,
4067
+ COALESCE(t.session_count, m.session_count, 0)::INT AS session_count,
4068
+ COALESCE(t.turn_count, 0)::BIGINT AS turn_count,
4069
+ COALESCE(m.total_snapshot_size_bytes, 0)::BIGINT AS total_snapshot_size_bytes,
4070
+ COALESCE(m.total_dehydration_count, 0)::INT AS total_dehydration_count,
4071
+ COALESCE(m.total_hydration_count, 0)::INT AS total_hydration_count,
4072
+ COALESCE(m.total_lossy_handoff_count, 0)::INT AS total_lossy_handoff_count,
4073
+ COALESCE(t.total_tokens_input, 0)::BIGINT AS total_tokens_input,
4074
+ COALESCE(t.total_tokens_output, 0)::BIGINT AS total_tokens_output,
4075
+ COALESCE(t.total_tokens_cache_read, 0)::BIGINT AS total_tokens_cache_read,
4076
+ COALESCE(t.total_tokens_cache_write, 0)::BIGINT AS total_tokens_cache_write
4077
+ FROM token_rollup t
4078
+ FULL OUTER JOIN metric_rollup m
4079
+ ON m.agent_id_value IS NOT DISTINCT FROM t.agent_id_value
4080
+ AND m.model_label = t.model_label
4081
+ ORDER BY COALESCE(t.total_tokens_input, 0) DESC, COALESCE(t.model_label, m.model_label);
4082
+ END;
4083
+ $$ LANGUAGE plpgsql;
4084
+
4085
+ DROP FUNCTION IF EXISTS ${s}.cms_get_user_stats_by_model(BOOLEAN, TIMESTAMPTZ);
4086
+ CREATE FUNCTION ${s}.cms_get_user_stats_by_model(
4087
+ p_include_deleted BOOLEAN,
4088
+ p_since TIMESTAMPTZ
4089
+ ) RETURNS TABLE (
4090
+ owner_kind TEXT,
4091
+ owner_provider TEXT,
4092
+ owner_subject TEXT,
4093
+ owner_email TEXT,
4094
+ owner_display_name TEXT,
4095
+ model TEXT,
4096
+ session_ids TEXT[],
4097
+ session_count INT,
4098
+ turn_count BIGINT,
4099
+ total_snapshot_size_bytes BIGINT,
4100
+ total_dehydration_count INT,
4101
+ total_hydration_count INT,
4102
+ total_lossy_handoff_count INT,
4103
+ total_tokens_input BIGINT,
4104
+ total_tokens_output BIGINT,
4105
+ total_tokens_cache_read BIGINT,
4106
+ total_tokens_cache_write BIGINT,
4107
+ earliest_session_created_at TIMESTAMPTZ
4108
+ ) AS $$
4109
+ BEGIN
4110
+ RETURN QUERY
4111
+ WITH session_owner AS (
4112
+ SELECT
4113
+ sess.session_id,
4114
+ CASE
4115
+ WHEN sess.is_system THEN 'system'
4116
+ WHEN u.user_id IS NULL THEN 'unowned'
4117
+ ELSE 'user'
4118
+ END::TEXT AS owner_kind_value,
4119
+ u.provider AS owner_provider_value,
4120
+ u.subject AS owner_subject_value,
4121
+ u.email AS owner_email_value,
4122
+ u.display_name AS owner_display_name_value
4123
+ FROM ${s}.sessions sess
4124
+ LEFT JOIN ${s}.session_owners so ON so.session_id = sess.session_id
4125
+ LEFT JOIN ${s}.users u ON u.user_id = so.user_id
4126
+ ), token_rows AS (
4127
+ SELECT
4128
+ so.owner_kind_value, so.owner_provider_value, so.owner_subject_value, so.owner_email_value, so.owner_display_name_value,
4129
+ CASE
4130
+ WHEN NULLIF(BTRIM(t.reasoning_effort), '') IS NULL
4131
+ THEN COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)')
4132
+ ELSE COALESCE(NULLIF(BTRIM(t.model), ''), '(unknown)') || ':' || BTRIM(t.reasoning_effort)
4133
+ END AS model_label,
4134
+ t.session_id,
4135
+ t.tokens_input, t.tokens_output, t.tokens_cache_read, t.tokens_cache_write
4136
+ FROM ${s}.session_turn_metrics t
4137
+ INNER JOIN ${s}.sessions sess ON sess.session_id = t.session_id
4138
+ INNER JOIN session_owner so ON so.session_id = t.session_id
4139
+ WHERE (p_include_deleted OR sess.deleted_at IS NULL)
4140
+ AND (p_since IS NULL OR t.started_at >= p_since)
4141
+ UNION ALL
4142
+ SELECT
4143
+ so.owner_kind_value, so.owner_provider_value, so.owner_subject_value, so.owner_email_value, so.owner_display_name_value,
4144
+ CASE
4145
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
4146
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
4147
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
4148
+ END AS model_label,
4149
+ m.session_id,
4150
+ m.tokens_input, m.tokens_output, m.tokens_cache_read, m.tokens_cache_write
4151
+ FROM ${s}.session_metrics m
4152
+ INNER JOIN ${s}.sessions sess ON sess.session_id = m.session_id
4153
+ INNER JOIN session_owner so ON so.session_id = m.session_id
4154
+ WHERE (p_include_deleted OR m.deleted_at IS NULL)
4155
+ AND (p_since IS NULL OR m.created_at >= p_since)
4156
+ AND NOT EXISTS (
4157
+ SELECT 1 FROM ${s}.session_turn_metrics existing
4158
+ WHERE existing.session_id = m.session_id
4159
+ )
4160
+ ), token_rollup AS (
4161
+ SELECT
4162
+ owner_kind_value, owner_provider_value, owner_subject_value, owner_email_value, owner_display_name_value, model_label,
4163
+ ARRAY_AGG(DISTINCT session_id ORDER BY session_id) AS session_ids,
4164
+ COUNT(DISTINCT session_id)::INT AS session_count,
4165
+ COUNT(*)::BIGINT AS turn_count,
4166
+ COALESCE(SUM(tokens_input), 0)::BIGINT AS total_tokens_input,
4167
+ COALESCE(SUM(tokens_output), 0)::BIGINT AS total_tokens_output,
4168
+ COALESCE(SUM(tokens_cache_read), 0)::BIGINT AS total_tokens_cache_read,
4169
+ COALESCE(SUM(tokens_cache_write), 0)::BIGINT AS total_tokens_cache_write
4170
+ FROM token_rows
4171
+ GROUP BY owner_kind_value, owner_provider_value, owner_subject_value, owner_email_value, owner_display_name_value, model_label
4172
+ ), metric_rollup AS (
4173
+ SELECT
4174
+ so.owner_kind_value, so.owner_provider_value, so.owner_subject_value, so.owner_email_value, so.owner_display_name_value,
4175
+ CASE
4176
+ WHEN NULLIF(BTRIM(m.reasoning_effort), '') IS NULL
4177
+ THEN COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)')
4178
+ ELSE COALESCE(NULLIF(BTRIM(m.model), ''), '(unknown)') || ':' || BTRIM(m.reasoning_effort)
4179
+ END AS model_label,
4180
+ ARRAY_AGG(DISTINCT m.session_id ORDER BY m.session_id) AS session_ids,
4181
+ COUNT(*)::INT AS session_count,
4182
+ COALESCE(SUM(m.snapshot_size_bytes), 0)::BIGINT AS total_snapshot_size_bytes,
4183
+ COALESCE(SUM(m.dehydration_count), 0)::INT AS total_dehydration_count,
4184
+ COALESCE(SUM(m.hydration_count), 0)::INT AS total_hydration_count,
4185
+ COALESCE(SUM(m.lossy_handoff_count), 0)::INT AS total_lossy_handoff_count,
4186
+ MIN(m.created_at) AS earliest_session_created_at
4187
+ FROM ${s}.session_metrics m
4188
+ INNER JOIN ${s}.sessions sess ON sess.session_id = m.session_id
4189
+ INNER JOIN session_owner so ON so.session_id = m.session_id
4190
+ WHERE (p_include_deleted OR m.deleted_at IS NULL)
4191
+ AND (p_since IS NULL OR m.created_at >= p_since)
4192
+ 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
4193
+ )
4194
+ SELECT
4195
+ COALESCE(t.owner_kind_value, m.owner_kind_value) AS owner_kind,
4196
+ COALESCE(t.owner_provider_value, m.owner_provider_value) AS owner_provider,
4197
+ COALESCE(t.owner_subject_value, m.owner_subject_value) AS owner_subject,
4198
+ COALESCE(t.owner_email_value, m.owner_email_value) AS owner_email,
4199
+ COALESCE(t.owner_display_name_value, m.owner_display_name_value) AS owner_display_name,
4200
+ COALESCE(t.model_label, m.model_label) AS model,
4201
+ ARRAY(SELECT DISTINCT unnest(COALESCE(t.session_ids, ARRAY[]::TEXT[]) || COALESCE(m.session_ids, ARRAY[]::TEXT[])) ORDER BY 1) AS session_ids,
4202
+ COALESCE(t.session_count, m.session_count, 0)::INT AS session_count,
4203
+ COALESCE(t.turn_count, 0)::BIGINT AS turn_count,
4204
+ COALESCE(m.total_snapshot_size_bytes, 0)::BIGINT AS total_snapshot_size_bytes,
4205
+ COALESCE(m.total_dehydration_count, 0)::INT AS total_dehydration_count,
4206
+ COALESCE(m.total_hydration_count, 0)::INT AS total_hydration_count,
4207
+ COALESCE(m.total_lossy_handoff_count, 0)::INT AS total_lossy_handoff_count,
4208
+ COALESCE(t.total_tokens_input, 0)::BIGINT AS total_tokens_input,
4209
+ COALESCE(t.total_tokens_output, 0)::BIGINT AS total_tokens_output,
4210
+ COALESCE(t.total_tokens_cache_read, 0)::BIGINT AS total_tokens_cache_read,
4211
+ COALESCE(t.total_tokens_cache_write, 0)::BIGINT AS total_tokens_cache_write,
4212
+ m.earliest_session_created_at AS earliest_session_created_at
4213
+ FROM token_rollup t
4214
+ FULL OUTER JOIN metric_rollup m
4215
+ ON m.owner_kind_value = t.owner_kind_value
4216
+ AND m.owner_provider_value IS NOT DISTINCT FROM t.owner_provider_value
4217
+ AND m.owner_subject_value IS NOT DISTINCT FROM t.owner_subject_value
4218
+ AND m.model_label = t.model_label
4219
+ ORDER BY COALESCE(t.total_tokens_input, 0) DESC, COALESCE(t.model_label, m.model_label);
4220
+ END;
4221
+ $$ LANGUAGE plpgsql;
4222
+ `;
4223
+ }
4224
+ // ─── Migration 0023: Turn Metrics Stats Fallbacks And Group Owner Patch ───
4225
+ function migration_0023_turn_metrics_stats_fallbacks_and_group_owner_patch(schema) {
4226
+ // Migration 0022 was deployed before these stored-procedure fixes landed.
4227
+ // Its SQL is idempotent, so reapplying the corrected procedure layer under
4228
+ // a new version updates already-migrated schemas without requiring a reset.
4229
+ return migration_0022_turn_metrics_reasoning_effort(schema);
4230
+ }
4231
+ // ─── Migration 0024: Stop Turn Active Turn Index ──────────────────
4232
+ function migration_0024_stop_turn_active_turn_index(schema) {
4233
+ const s = `"${schema}"`;
4234
+ return `
4235
+ -- 0024_stop_turn_active_turn_index: track the in-flight turn index on the
4236
+ -- sessions row so stopSessionTurn() can address the turn-scoped stop queue
4237
+ -- (stopTurn.<turnIndex>). Written by the runTurn activity's pre-turn
4238
+ -- writeback; cleared by the post-turn writeback and by any state transition
4239
+ -- away from 'running' (stop-turn plan, docs/proposals-impl/stop-button-turn-abort-plan.md).
4240
+
4241
+ ALTER TABLE ${s}.sessions ADD COLUMN IF NOT EXISTS active_turn_index INTEGER;
4242
+
4243
+ -- ── cms_set_active_turn_index ─────────────────────────────────────
4244
+ -- Pre-turn: publish the in-flight turn index.
4245
+ CREATE OR REPLACE FUNCTION ${s}.cms_set_active_turn_index(
4246
+ p_session_id TEXT,
4247
+ p_turn_index INTEGER
4248
+ ) RETURNS VOID AS $$
4249
+ BEGIN
4250
+ UPDATE ${s}.sessions
4251
+ SET active_turn_index = p_turn_index,
4252
+ updated_at = now()
4253
+ WHERE session_id = p_session_id;
4254
+ END;
4255
+ $$ LANGUAGE plpgsql;
4256
+
4257
+ -- ── cms_get_session ──────────────────────────────────────────────
4258
+ -- Same as the owner-join version, plus active_turn_index in the returned
4259
+ -- column set (RETURNS TABLE is a fixed list — new table columns are not
4260
+ -- returned automatically). stopSessionTurn() reads it for the pre-check and
4261
+ -- the turn-scoped stop queue name.
4262
+ DROP FUNCTION IF EXISTS ${s}.cms_get_session(TEXT);
4263
+ CREATE FUNCTION ${s}.cms_get_session(
4264
+ p_session_id TEXT
4265
+ ) RETURNS TABLE (
4266
+ session_id TEXT,
4267
+ orchestration_id TEXT,
4268
+ title TEXT,
4269
+ title_locked BOOLEAN,
4270
+ state TEXT,
4271
+ model TEXT,
4272
+ reasoning_effort TEXT,
4273
+ group_id TEXT,
4274
+ short_summary TEXT,
4275
+ summary_state JSONB,
4276
+ summary_updated_at TIMESTAMPTZ,
4277
+ created_at TIMESTAMPTZ,
4278
+ updated_at TIMESTAMPTZ,
4279
+ last_active_at TIMESTAMPTZ,
4280
+ deleted_at TIMESTAMPTZ,
4281
+ current_iteration INTEGER,
4282
+ last_error TEXT,
4283
+ parent_session_id TEXT,
4284
+ wait_reason TEXT,
4285
+ is_system BOOLEAN,
4286
+ agent_id TEXT,
4287
+ splash TEXT,
4288
+ owner_provider TEXT,
4289
+ owner_subject TEXT,
4290
+ owner_email TEXT,
4291
+ owner_display_name TEXT,
4292
+ active_turn_index INTEGER
4293
+ ) AS $$
4294
+ BEGIN
4295
+ RETURN QUERY
4296
+ SELECT
4297
+ sess.session_id,
4298
+ sess.orchestration_id,
4299
+ sess.title,
4300
+ sess.title_locked,
4301
+ sess.state,
4302
+ sess.model,
4303
+ sess.reasoning_effort,
4304
+ sess.group_id,
4305
+ sess.short_summary,
4306
+ sess.summary_state,
4307
+ sess.summary_updated_at,
4308
+ sess.created_at,
4309
+ sess.updated_at,
4310
+ sess.last_active_at,
4311
+ sess.deleted_at,
4312
+ sess.current_iteration,
4313
+ sess.last_error,
4314
+ sess.parent_session_id,
4315
+ sess.wait_reason,
4316
+ sess.is_system,
4317
+ sess.agent_id,
4318
+ sess.splash,
4319
+ u.provider AS owner_provider,
4320
+ u.subject AS owner_subject,
4321
+ u.email AS owner_email,
4322
+ u.display_name AS owner_display_name,
4323
+ sess.active_turn_index
4324
+ FROM ${s}.sessions sess
4325
+ LEFT JOIN ${s}.session_owners so ON so.session_id = sess.session_id
4326
+ LEFT JOIN ${s}.users u ON u.user_id = so.user_id
4327
+ WHERE sess.session_id = p_session_id AND sess.deleted_at IS NULL;
4328
+ END;
4329
+ $$ LANGUAGE plpgsql;
4330
+
4331
+ -- ── cms_update_session ───────────────────────────────────────────
4332
+ -- Same as 0022/0023, plus: any state transition away from 'running' clears
4333
+ -- active_turn_index, so the orchestration's authoritative stop bookkeeping
4334
+ -- (updateCmsState idle) and error transitions retire the stop-queue target.
4335
+ CREATE OR REPLACE FUNCTION ${s}.cms_update_session(
4336
+ p_session_id TEXT,
4337
+ p_updates JSONB
4338
+ ) RETURNS VOID AS $$
4339
+ BEGIN
4340
+ UPDATE ${s}.sessions SET
4341
+ orchestration_id = CASE WHEN p_updates ? 'orchestrationId' THEN (p_updates->>'orchestrationId') ELSE orchestration_id END,
4342
+ title = CASE WHEN p_updates ? 'title' THEN (p_updates->>'title') ELSE title END,
4343
+ title_locked = CASE WHEN p_updates ? 'titleLocked' THEN (p_updates->>'titleLocked')::BOOLEAN ELSE title_locked END,
4344
+ state = CASE WHEN p_updates ? 'state' THEN (p_updates->>'state') ELSE state END,
4345
+ model = CASE WHEN p_updates ? 'model' THEN (p_updates->>'model') ELSE model END,
4346
+ reasoning_effort = CASE WHEN p_updates ? 'reasoningEffort' THEN NULLIF(BTRIM(p_updates->>'reasoningEffort'), '') ELSE reasoning_effort END,
4347
+ last_active_at = CASE WHEN p_updates ? 'lastActiveAt' THEN (p_updates->>'lastActiveAt')::TIMESTAMPTZ ELSE last_active_at END,
4348
+ current_iteration = CASE WHEN p_updates ? 'currentIteration' THEN (p_updates->>'currentIteration')::INT ELSE current_iteration END,
4349
+ last_error = CASE WHEN p_updates ? 'lastError' THEN (p_updates->>'lastError') ELSE last_error END,
4350
+ wait_reason = CASE WHEN p_updates ? 'waitReason' THEN (p_updates->>'waitReason') ELSE wait_reason END,
4351
+ is_system = CASE WHEN p_updates ? 'isSystem' THEN (p_updates->>'isSystem')::BOOLEAN ELSE is_system END,
4352
+ agent_id = CASE WHEN p_updates ? 'agentId' THEN (p_updates->>'agentId') ELSE agent_id END,
4353
+ splash = CASE WHEN p_updates ? 'splash' THEN (p_updates->>'splash') ELSE splash END,
4354
+ active_turn_index = CASE WHEN (p_updates ? 'state') AND (p_updates->>'state') <> 'running' THEN NULL ELSE active_turn_index END,
4355
+ group_id = group_id,
4356
+ updated_at = now()
4357
+ WHERE session_id = p_session_id;
4358
+
4359
+ IF p_updates ? 'groupId' THEN
4360
+ PERFORM ${s}.cms_assign_session_group(p_session_id, p_updates->>'groupId');
4361
+ END IF;
4362
+
4363
+ UPDATE ${s}.session_metrics
4364
+ SET model = CASE WHEN p_updates ? 'model' THEN (p_updates->>'model') ELSE model END,
4365
+ reasoning_effort = CASE WHEN p_updates ? 'reasoningEffort' THEN NULLIF(BTRIM(p_updates->>'reasoningEffort'), '') ELSE reasoning_effort END,
4366
+ updated_at = CASE WHEN p_updates ? 'model' OR p_updates ? 'reasoningEffort' THEN now() ELSE updated_at END
4367
+ WHERE session_id = p_session_id
4368
+ AND (p_updates ? 'model' OR p_updates ? 'reasoningEffort');
4369
+ END;
4370
+ $$ LANGUAGE plpgsql;
4371
+
4372
+ -- ── cms_complete_turn_writeback ───────────────────────────────────
4373
+ -- Same as 0022/0023, plus: the turn ended, so always clear active_turn_index.
4374
+ CREATE OR REPLACE FUNCTION ${s}.cms_complete_turn_writeback(
4375
+ p_session_id TEXT,
4376
+ p_agent_id TEXT,
4377
+ p_model TEXT,
4378
+ p_reasoning_effort TEXT,
4379
+ p_turn_index INTEGER,
4380
+ p_started_at TIMESTAMPTZ,
4381
+ p_ended_at TIMESTAMPTZ,
4382
+ p_duration_ms INTEGER,
4383
+ p_tokens_input BIGINT,
4384
+ p_tokens_output BIGINT,
4385
+ p_tokens_cache_read BIGINT,
4386
+ p_tokens_cache_write BIGINT,
4387
+ p_tool_calls INTEGER,
4388
+ p_tool_errors INTEGER,
4389
+ p_tool_names TEXT[],
4390
+ p_result_type TEXT,
4391
+ p_error_message TEXT,
4392
+ p_worker_node_id TEXT,
4393
+ p_state TEXT,
4394
+ p_last_active_at TIMESTAMPTZ,
4395
+ p_last_error TEXT,
4396
+ p_wait_reason TEXT,
4397
+ p_current_iteration INTEGER
4398
+ ) RETURNS VOID AS $$
4399
+ DECLARE
4400
+ v_reasoning_effort TEXT := NULLIF(BTRIM(p_reasoning_effort), '');
4401
+ v_ended_at TIMESTAMPTZ := COALESCE(p_ended_at, now());
4402
+ v_started_at TIMESTAMPTZ := COALESCE(p_started_at, v_ended_at);
4403
+ v_duration_ms INTEGER := GREATEST(0, COALESCE(p_duration_ms, FLOOR(EXTRACT(EPOCH FROM (v_ended_at - v_started_at)) * 1000)::INT));
4404
+ BEGIN
4405
+ UPDATE ${s}.sessions
4406
+ SET state = COALESCE(p_state, state),
4407
+ last_active_at = COALESCE(p_last_active_at, v_ended_at),
4408
+ current_iteration = COALESCE(p_current_iteration, current_iteration),
4409
+ last_error = p_last_error,
4410
+ wait_reason = p_wait_reason,
4411
+ active_turn_index = NULL,
4412
+ updated_at = now()
4413
+ WHERE session_id = p_session_id;
4414
+
4415
+ INSERT INTO ${s}.session_metrics (
4416
+ session_id, agent_id, model, reasoning_effort,
4417
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write
4418
+ ) VALUES (
4419
+ p_session_id, p_agent_id, p_model, v_reasoning_effort,
4420
+ COALESCE(p_tokens_input, 0), COALESCE(p_tokens_output, 0),
4421
+ COALESCE(p_tokens_cache_read, 0), COALESCE(p_tokens_cache_write, 0)
4422
+ )
4423
+ ON CONFLICT (session_id) DO UPDATE SET
4424
+ agent_id = COALESCE(${s}.session_metrics.agent_id, EXCLUDED.agent_id),
4425
+ model = COALESCE(EXCLUDED.model, ${s}.session_metrics.model),
4426
+ reasoning_effort = COALESCE(EXCLUDED.reasoning_effort, ${s}.session_metrics.reasoning_effort),
4427
+ tokens_input = ${s}.session_metrics.tokens_input + EXCLUDED.tokens_input,
4428
+ tokens_output = ${s}.session_metrics.tokens_output + EXCLUDED.tokens_output,
4429
+ tokens_cache_read = ${s}.session_metrics.tokens_cache_read + EXCLUDED.tokens_cache_read,
4430
+ tokens_cache_write = ${s}.session_metrics.tokens_cache_write + EXCLUDED.tokens_cache_write,
4431
+ updated_at = now();
4432
+
4433
+ INSERT INTO ${s}.session_turn_metrics (
4434
+ session_id, agent_id, model, reasoning_effort, turn_index,
4435
+ started_at, ended_at, duration_ms,
4436
+ tokens_input, tokens_output, tokens_cache_read, tokens_cache_write,
4437
+ tool_calls, tool_errors, result_type, error_message, worker_node_id
4438
+ ) VALUES (
4439
+ p_session_id, p_agent_id, p_model, v_reasoning_effort, COALESCE(p_turn_index, 0),
4440
+ v_started_at, v_ended_at, v_duration_ms,
4441
+ COALESCE(p_tokens_input, 0), COALESCE(p_tokens_output, 0),
4442
+ COALESCE(p_tokens_cache_read, 0), COALESCE(p_tokens_cache_write, 0),
4443
+ COALESCE(p_tool_calls, 0), COALESCE(p_tool_errors, 0),
4444
+ p_result_type, p_error_message, p_worker_node_id
4445
+ );
4446
+
4447
+ INSERT INTO ${s}.session_events (session_id, event_type, data, worker_node_id)
4448
+ VALUES (
4449
+ p_session_id,
4450
+ 'session.turn_completed',
4451
+ jsonb_build_object(
4452
+ 'iteration', COALESCE(p_turn_index, 0),
4453
+ 'turnIndex', COALESCE(p_turn_index, 0),
4454
+ 'model', p_model,
4455
+ 'reasoningEffort', v_reasoning_effort,
4456
+ 'startedAt', v_started_at,
4457
+ 'endedAt', v_ended_at,
4458
+ 'durationMs', v_duration_ms,
4459
+ 'tokensInput', COALESCE(p_tokens_input, 0),
4460
+ 'tokensOutput', COALESCE(p_tokens_output, 0),
4461
+ 'tokensCacheRead', COALESCE(p_tokens_cache_read, 0),
4462
+ 'tokensCacheWrite', COALESCE(p_tokens_cache_write, 0),
4463
+ 'toolCalls', COALESCE(p_tool_calls, 0),
4464
+ 'toolErrors', COALESCE(p_tool_errors, 0),
4465
+ 'toolNames', to_jsonb(COALESCE(p_tool_names, ARRAY[]::TEXT[])),
4466
+ 'resultType', p_result_type,
4467
+ 'errorMessage', p_error_message,
4468
+ 'workerNodeId', p_worker_node_id
4469
+ ),
4470
+ p_worker_node_id
4471
+ );
4472
+ END;
4473
+ $$ LANGUAGE plpgsql;
4474
+ `;
4475
+ }
3387
4476
  //# sourceMappingURL=cms-migrations.js.map