@desplega.ai/agent-swarm 1.79.3 → 1.80.0

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 (49) hide show
  1. package/openapi.json +98 -19
  2. package/package.json +12 -6
  3. package/src/be/db.ts +101 -30
  4. package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
  5. package/src/be/pricing-normalize.ts +81 -0
  6. package/src/be/seed-pricing.ts +293 -0
  7. package/src/commands/claude-managed-setup.ts +19 -3
  8. package/src/commands/runner.ts +592 -237
  9. package/src/http/context.ts +6 -2
  10. package/src/http/index.ts +115 -68
  11. package/src/http/session-data.ts +74 -23
  12. package/src/otel-impl.ts +200 -0
  13. package/src/otel.ts +127 -0
  14. package/src/providers/claude-adapter.ts +30 -5
  15. package/src/providers/claude-managed-adapter.ts +43 -17
  16. package/src/providers/claude-managed-pricing.ts +34 -0
  17. package/src/providers/codex-adapter.ts +38 -27
  18. package/src/providers/codex-models.ts +22 -3
  19. package/src/providers/devin-adapter.ts +11 -0
  20. package/src/providers/opencode-adapter.ts +31 -7
  21. package/src/providers/pi-mono-adapter.ts +39 -7
  22. package/src/providers/pricing-sources.md +52 -0
  23. package/src/providers/swarm-events-shared.ts +8 -4
  24. package/src/providers/types.ts +33 -10
  25. package/src/server.ts +6 -0
  26. package/src/tests/claude-managed-adapter.test.ts +17 -3
  27. package/src/tests/claude-managed-setup.test.ts +10 -1
  28. package/src/tests/codex-adapter.test.ts +20 -19
  29. package/src/tests/context-snapshot.test.ts +2 -2
  30. package/src/tests/context-window.test.ts +65 -1
  31. package/src/tests/devin-adapter.test.ts +2 -0
  32. package/src/tests/http/context-routes.test.ts +161 -0
  33. package/src/tests/migration-063-schema-relax.test.ts +109 -0
  34. package/src/tests/opencode-adapter.test.ts +146 -1
  35. package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
  36. package/src/tests/pages-view-count.test.ts +30 -5
  37. package/src/tests/providers/codex-cost.test.ts +18 -0
  38. package/src/tests/providers/opencode-cost.test.ts +74 -0
  39. package/src/tests/providers/pi-cost.test.ts +128 -0
  40. package/src/tests/secret-scrubber.test.ts +19 -0
  41. package/src/tests/session-costs-codex-recompute.test.ts +35 -22
  42. package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
  43. package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
  44. package/src/tests/store-progress-cost.test.ts +6 -1
  45. package/src/tools/store-progress.ts +16 -60
  46. package/src/tools/utils.ts +65 -12
  47. package/src/types.ts +62 -9
  48. package/src/utils/context-window.ts +104 -4
  49. package/src/utils/secret-scrubber.ts +7 -0
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.79.3",
5
+ "version": "1.80.0",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
@@ -2147,7 +2147,8 @@
2147
2147
  "type": "string",
2148
2148
  "enum": [
2149
2149
  "auto",
2150
- "manual"
2150
+ "manual",
2151
+ "auto-inferred"
2151
2152
  ]
2152
2153
  },
2153
2154
  "preCompactTokens": {
@@ -2161,6 +2162,18 @@
2161
2162
  "cumulativeOutputTokens": {
2162
2163
  "type": "integer",
2163
2164
  "minimum": 0
2165
+ },
2166
+ "contextFormula": {
2167
+ "type": "string",
2168
+ "enum": [
2169
+ "input-cache-output",
2170
+ "input-cache-no-output",
2171
+ "input-output-no-cache",
2172
+ "peak-proxy",
2173
+ "pi-delegated",
2174
+ "harness-reported",
2175
+ "unknown"
2176
+ ]
2164
2177
  }
2165
2178
  },
2166
2179
  "required": [
@@ -5393,8 +5406,12 @@
5393
5406
  "type": "string",
5394
5407
  "enum": [
5395
5408
  "claude",
5409
+ "claude-managed",
5396
5410
  "codex",
5397
- "pi"
5411
+ "pi",
5412
+ "opencode",
5413
+ "devin",
5414
+ "gemini"
5398
5415
  ]
5399
5416
  },
5400
5417
  "model": {
@@ -5405,7 +5422,10 @@
5405
5422
  "enum": [
5406
5423
  "input",
5407
5424
  "cached_input",
5408
- "output"
5425
+ "output",
5426
+ "cache_write",
5427
+ "runtime_hour",
5428
+ "acu"
5409
5429
  ]
5410
5430
  },
5411
5431
  "effectiveFrom": {
@@ -5462,8 +5482,12 @@
5462
5482
  "type": "string",
5463
5483
  "enum": [
5464
5484
  "claude",
5485
+ "claude-managed",
5465
5486
  "codex",
5466
- "pi"
5487
+ "pi",
5488
+ "opencode",
5489
+ "devin",
5490
+ "gemini"
5467
5491
  ]
5468
5492
  },
5469
5493
  "required": true,
@@ -5485,7 +5509,10 @@
5485
5509
  "enum": [
5486
5510
  "input",
5487
5511
  "cached_input",
5488
- "output"
5512
+ "output",
5513
+ "cache_write",
5514
+ "runtime_hour",
5515
+ "acu"
5489
5516
  ]
5490
5517
  },
5491
5518
  "required": true,
@@ -5515,8 +5542,12 @@
5515
5542
  "type": "string",
5516
5543
  "enum": [
5517
5544
  "claude",
5545
+ "claude-managed",
5518
5546
  "codex",
5519
- "pi"
5547
+ "pi",
5548
+ "opencode",
5549
+ "devin",
5550
+ "gemini"
5520
5551
  ]
5521
5552
  },
5522
5553
  "required": true,
@@ -5538,7 +5569,10 @@
5538
5569
  "enum": [
5539
5570
  "input",
5540
5571
  "cached_input",
5541
- "output"
5572
+ "output",
5573
+ "cache_write",
5574
+ "runtime_hour",
5575
+ "acu"
5542
5576
  ]
5543
5577
  },
5544
5578
  "required": true,
@@ -5580,8 +5614,12 @@
5580
5614
  "type": "string",
5581
5615
  "enum": [
5582
5616
  "claude",
5617
+ "claude-managed",
5583
5618
  "codex",
5584
- "pi"
5619
+ "pi",
5620
+ "opencode",
5621
+ "devin",
5622
+ "gemini"
5585
5623
  ]
5586
5624
  },
5587
5625
  "model": {
@@ -5592,7 +5630,10 @@
5592
5630
  "enum": [
5593
5631
  "input",
5594
5632
  "cached_input",
5595
- "output"
5633
+ "output",
5634
+ "cache_write",
5635
+ "runtime_hour",
5636
+ "acu"
5596
5637
  ]
5597
5638
  },
5598
5639
  "effectiveFrom": {
@@ -5649,8 +5690,12 @@
5649
5690
  "type": "string",
5650
5691
  "enum": [
5651
5692
  "claude",
5693
+ "claude-managed",
5652
5694
  "codex",
5653
- "pi"
5695
+ "pi",
5696
+ "opencode",
5697
+ "devin",
5698
+ "gemini"
5654
5699
  ]
5655
5700
  },
5656
5701
  "required": true,
@@ -5672,7 +5717,10 @@
5672
5717
  "enum": [
5673
5718
  "input",
5674
5719
  "cached_input",
5675
- "output"
5720
+ "output",
5721
+ "cache_write",
5722
+ "runtime_hour",
5723
+ "acu"
5676
5724
  ]
5677
5725
  },
5678
5726
  "required": true,
@@ -5692,8 +5740,12 @@
5692
5740
  "type": "string",
5693
5741
  "enum": [
5694
5742
  "claude",
5743
+ "claude-managed",
5695
5744
  "codex",
5696
- "pi"
5745
+ "pi",
5746
+ "opencode",
5747
+ "devin",
5748
+ "gemini"
5697
5749
  ]
5698
5750
  },
5699
5751
  "model": {
@@ -5704,7 +5756,10 @@
5704
5756
  "enum": [
5705
5757
  "input",
5706
5758
  "cached_input",
5707
- "output"
5759
+ "output",
5760
+ "cache_write",
5761
+ "runtime_hour",
5762
+ "acu"
5708
5763
  ]
5709
5764
  },
5710
5765
  "effectiveFrom": {
@@ -5758,8 +5813,12 @@
5758
5813
  "type": "string",
5759
5814
  "enum": [
5760
5815
  "claude",
5816
+ "claude-managed",
5761
5817
  "codex",
5762
- "pi"
5818
+ "pi",
5819
+ "opencode",
5820
+ "devin",
5821
+ "gemini"
5763
5822
  ]
5764
5823
  },
5765
5824
  "required": true,
@@ -5781,7 +5840,10 @@
5781
5840
  "enum": [
5782
5841
  "input",
5783
5842
  "cached_input",
5784
- "output"
5843
+ "output",
5844
+ "cache_write",
5845
+ "runtime_hour",
5846
+ "acu"
5785
5847
  ]
5786
5848
  },
5787
5849
  "required": true,
@@ -6951,13 +7013,27 @@
6951
7013
  "type": "integer"
6952
7014
  },
6953
7015
  "cacheWriteTokens": {
6954
- "type": "integer"
7016
+ "type": [
7017
+ "integer",
7018
+ "null"
7019
+ ]
7020
+ },
7021
+ "reasoningOutputTokens": {
7022
+ "type": "integer",
7023
+ "minimum": 0
7024
+ },
7025
+ "thinkingTokens": {
7026
+ "type": "integer",
7027
+ "minimum": 0
6955
7028
  },
6956
7029
  "durationMs": {
6957
7030
  "type": "integer"
6958
7031
  },
6959
7032
  "numTurns": {
6960
- "type": "integer"
7033
+ "type": [
7034
+ "integer",
7035
+ "null"
7036
+ ]
6961
7037
  },
6962
7038
  "model": {
6963
7039
  "type": "string"
@@ -6969,9 +7045,12 @@
6969
7045
  "type": "string",
6970
7046
  "enum": [
6971
7047
  "claude",
7048
+ "claude-managed",
6972
7049
  "codex",
6973
7050
  "pi",
6974
- "opencode"
7051
+ "opencode",
7052
+ "devin",
7053
+ "gemini"
6975
7054
  ]
6976
7055
  },
6977
7056
  "createdAt": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.79.3",
3
+ "version": "1.80.0",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -73,6 +73,7 @@
73
73
  "deploy:docker": "bun deploy/docker-push.ts",
74
74
  "e2e:workflows": "bun scripts/e2e-workflow-test.ts",
75
75
  "e2e:workflows:docker": "bun scripts/e2e-workflow-test.ts --with-docker",
76
+ "e2e:otel:jaeger": "bun scripts/e2e-otel-jaeger.ts",
76
77
  "docs:mcp": "bun scripts/generate-mcp-docs.ts",
77
78
  "docs:openapi": "bun scripts/generate-openapi.ts",
78
79
  "docs:business-use": "bun scripts/generate-business-use-docs.ts",
@@ -104,13 +105,18 @@
104
105
  "@desplega.ai/localtunnel": "^2.2.0",
105
106
  "@inkjs/ui": "^2.0.0",
106
107
  "@linear/sdk": "^77.0.0",
107
- "@earendil-works/pi-agent-core": "^0.74.0",
108
- "@earendil-works/pi-ai": "^0.74.0",
109
- "@earendil-works/pi-coding-agent": "^0.74.0",
108
+ "@earendil-works/pi-agent-core": "^0.75.3",
109
+ "@earendil-works/pi-ai": "^0.75.3",
110
+ "@earendil-works/pi-coding-agent": "^0.75.3",
110
111
  "@modelcontextprotocol/sdk": "^1.25.1",
111
- "@openai/codex-sdk": "^0.128.0",
112
- "@opencode-ai/sdk": "^1.14.30",
112
+ "@openai/codex-sdk": "^0.130.0",
113
+ "@opencode-ai/sdk": "^1.15.4",
113
114
  "@openfort/openfort-node": "^0.9.1",
115
+ "@opentelemetry/api": "^1.9.1",
116
+ "@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
117
+ "@opentelemetry/resources": "^2.7.1",
118
+ "@opentelemetry/sdk-node": "^0.218.0",
119
+ "@opentelemetry/semantic-conventions": "^1.41.1",
114
120
  "@slack/bolt": "^4.6.0",
115
121
  "@types/react": "^19.2.7",
116
122
  "@x402/core": "^2.5.0",
package/src/be/db.ts CHANGED
@@ -980,7 +980,7 @@ type AgentTaskRow = {
980
980
  progress: string | null;
981
981
  compactionCount: number | null;
982
982
  peakContextPercent: number | null;
983
- totalContextTokensUsed: number | null;
983
+ peakContextTokens: number | null;
984
984
  contextWindowSize: number | null;
985
985
  was_paused: number;
986
986
  credentialKeySuffix: string | null;
@@ -1036,7 +1036,7 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
1036
1036
  contextKey: row.contextKey ?? undefined,
1037
1037
  compactionCount: row.compactionCount ?? undefined,
1038
1038
  peakContextPercent: row.peakContextPercent ?? undefined,
1039
- totalContextTokensUsed: row.totalContextTokensUsed ?? undefined,
1039
+ peakContextTokens: row.peakContextTokens ?? undefined,
1040
1040
  contextWindowSize: row.contextWindowSize ?? undefined,
1041
1041
  createdAt: row.createdAt,
1042
1042
  lastUpdatedAt: row.lastUpdatedAt,
@@ -3761,8 +3761,11 @@ type SessionCostRow = {
3761
3761
  outputTokens: number;
3762
3762
  cacheReadTokens: number;
3763
3763
  cacheWriteTokens: number;
3764
+ // Migration 063 additions:
3765
+ reasoningOutputTokens: number;
3766
+ thinkingTokens: number;
3764
3767
  durationMs: number;
3765
- numTurns: number;
3768
+ numTurns: number | null;
3766
3769
  model: string;
3767
3770
  isError: number;
3768
3771
  costSource: string;
@@ -3780,6 +3783,8 @@ function rowToSessionCost(row: SessionCostRow): SessionCost {
3780
3783
  outputTokens: row.outputTokens,
3781
3784
  cacheReadTokens: row.cacheReadTokens,
3782
3785
  cacheWriteTokens: row.cacheWriteTokens,
3786
+ reasoningOutputTokens: row.reasoningOutputTokens ?? 0,
3787
+ thinkingTokens: row.thinkingTokens ?? 0,
3783
3788
  durationMs: row.durationMs,
3784
3789
  numTurns: row.numTurns,
3785
3790
  model: row.model,
@@ -3803,15 +3808,24 @@ const sessionCostQueries = {
3803
3808
  number,
3804
3809
  number,
3805
3810
  number,
3806
- number,
3807
- number,
3808
- string,
3809
- number,
3810
- string,
3811
+ number, // reasoningOutputTokens
3812
+ number, // thinkingTokens
3813
+ number, // durationMs
3814
+ number | null, // numTurns
3815
+ string, // model
3816
+ number, // isError
3817
+ string, // costSource
3811
3818
  ]
3812
3819
  >(
3813
- `INSERT INTO session_costs (id, sessionId, taskId, agentId, totalCostUsd, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, durationMs, numTurns, model, isError, costSource, createdAt)
3814
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,
3820
+ `INSERT INTO session_costs (
3821
+ id, sessionId, taskId, agentId,
3822
+ totalCostUsd, inputTokens, outputTokens,
3823
+ cacheReadTokens, cacheWriteTokens,
3824
+ reasoningOutputTokens, thinkingTokens,
3825
+ durationMs, numTurns, model, isError,
3826
+ costSource, createdAt
3827
+ )
3828
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,
3815
3829
  ),
3816
3830
 
3817
3831
  getByTaskId: () =>
@@ -3839,16 +3853,22 @@ export interface CreateSessionCostInput {
3839
3853
  outputTokens?: number;
3840
3854
  cacheReadTokens?: number;
3841
3855
  cacheWriteTokens?: number;
3856
+ // Migration 063 additions — adapters that have these numbers should pass
3857
+ // them; defaulting to 0 preserves the old write shape for callers that don't.
3858
+ reasoningOutputTokens?: number;
3859
+ thinkingTokens?: number;
3842
3860
  durationMs: number;
3843
- numTurns: number;
3861
+ // Nullable: some adapters (claude when num_turns is absent) can't honestly
3862
+ // report a turn count; we prefer null over a faked 1.
3863
+ numTurns: number | null;
3844
3864
  model: string;
3845
3865
  isError?: boolean;
3846
3866
  /**
3847
- * Phase 6: where the recorded `totalCostUsd` came from.
3867
+ * Phase 6 (migration 063 added 'unpriced'): where `totalCostUsd` came from.
3848
3868
  * - 'harness' — value reported by the harness as-is (default).
3849
- * - 'pricing-table' — value recomputed by the API from `pricing` rows
3850
- * (Codex when DB pricing rows exist for all three
3851
- * token classes).
3869
+ * - 'pricing-table' — value recomputed by the API from `pricing` rows.
3870
+ * - 'unpriced' — recompute attempted but no matching pricing rows;
3871
+ * `totalCostUsd` is whatever the worker submitted.
3852
3872
  */
3853
3873
  costSource?: SessionCostSource;
3854
3874
  }
@@ -3856,6 +3876,8 @@ export interface CreateSessionCostInput {
3856
3876
  export function createSessionCost(input: CreateSessionCostInput): SessionCost {
3857
3877
  const id = crypto.randomUUID();
3858
3878
  const costSource: SessionCostSource = input.costSource ?? "harness";
3879
+ const reasoningOutputTokens = input.reasoningOutputTokens ?? 0;
3880
+ const thinkingTokens = input.thinkingTokens ?? 0;
3859
3881
  sessionCostQueries
3860
3882
  .insert()
3861
3883
  .run(
@@ -3868,6 +3890,8 @@ export function createSessionCost(input: CreateSessionCostInput): SessionCost {
3868
3890
  input.outputTokens ?? 0,
3869
3891
  input.cacheReadTokens ?? 0,
3870
3892
  input.cacheWriteTokens ?? 0,
3893
+ reasoningOutputTokens,
3894
+ thinkingTokens,
3871
3895
  input.durationMs,
3872
3896
  input.numTurns,
3873
3897
  input.model,
@@ -3885,6 +3909,8 @@ export function createSessionCost(input: CreateSessionCostInput): SessionCost {
3885
3909
  outputTokens: input.outputTokens ?? 0,
3886
3910
  cacheReadTokens: input.cacheReadTokens ?? 0,
3887
3911
  cacheWriteTokens: input.cacheWriteTokens ?? 0,
3912
+ reasoningOutputTokens,
3913
+ thinkingTokens,
3888
3914
  durationMs: input.durationMs,
3889
3915
  numTurns: input.numTurns,
3890
3916
  model: input.model,
@@ -4110,16 +4136,33 @@ export interface DashboardCostSummary {
4110
4136
  }
4111
4137
 
4112
4138
  export function getDashboardCostSummary(): DashboardCostSummary {
4139
+ // Phase 13: compute the date boundaries in TS and pass them as ISO 8601
4140
+ // strings. `session_costs.createdAt` is a TEXT ISO 8601 column; lexicographic
4141
+ // comparison on ISO 8601 sorts correctly, so the comparison works as long
4142
+ // as both sides are the same shape. The old code compared an ISO string
4143
+ // (`2026-05-15T03:45:12.123Z`) against `date('now')` (which returns the
4144
+ // string `2026-05-15`) — lexicographically `2026-05-15T...` > `2026-05-15`,
4145
+ // so post-midnight rows correctly counted, BUT rows whose ISO began with
4146
+ // the EXACT bare-date string would fail the `>=` check inconsistently
4147
+ // depending on millisecond precision. Use a proper ISO-millisecond boundary
4148
+ // for both halves so the comparison is unambiguous.
4149
+ const now = new Date();
4150
+ const startOfDayUtc = new Date(
4151
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()),
4152
+ ).toISOString();
4153
+ const startOfMonthUtc = new Date(
4154
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1),
4155
+ ).toISOString();
4113
4156
  type CostRow = { costToday: number; costMtd: number };
4114
4157
  const row = getDb()
4115
- .prepare<CostRow, []>(
4158
+ .prepare<CostRow, [string, string]>(
4116
4159
  `SELECT
4117
- COALESCE(SUM(CASE WHEN createdAt >= date('now') THEN totalCostUsd ELSE 0 END), 0) as costToday,
4160
+ COALESCE(SUM(CASE WHEN createdAt >= ? THEN totalCostUsd ELSE 0 END), 0) as costToday,
4118
4161
  COALESCE(SUM(totalCostUsd), 0) as costMtd
4119
4162
  FROM session_costs
4120
- WHERE createdAt >= date('now', 'start of month')`,
4163
+ WHERE createdAt >= ?`,
4121
4164
  )
4122
- .get();
4165
+ .get(startOfDayUtc, startOfMonthUtc);
4123
4166
 
4124
4167
  return row ?? { costToday: 0, costMtd: 0 };
4125
4168
  }
@@ -8245,6 +8288,8 @@ type ContextSnapshotRow = {
8245
8288
  preCompactTokens: number | null;
8246
8289
  cumulativeInputTokens: number;
8247
8290
  cumulativeOutputTokens: number;
8291
+ // Migration 063 — see ContextFormulaSchema in src/types.ts for the value set.
8292
+ contextFormula: string | null;
8248
8293
  createdAt: string;
8249
8294
  };
8250
8295
 
@@ -8258,10 +8303,11 @@ function rowToContextSnapshot(row: ContextSnapshotRow): ContextSnapshot {
8258
8303
  contextTotalTokens: row.contextTotalTokens ?? undefined,
8259
8304
  contextPercent: row.contextPercent ?? undefined,
8260
8305
  eventType: row.eventType,
8261
- compactTrigger: (row.compactTrigger as "auto" | "manual" | null) ?? undefined,
8306
+ compactTrigger: (row.compactTrigger as "auto" | "manual" | "auto-inferred" | null) ?? undefined,
8262
8307
  preCompactTokens: row.preCompactTokens ?? undefined,
8263
8308
  cumulativeInputTokens: row.cumulativeInputTokens,
8264
8309
  cumulativeOutputTokens: row.cumulativeOutputTokens,
8310
+ contextFormula: (row.contextFormula as ContextSnapshot["contextFormula"]) ?? undefined,
8265
8311
  createdAt: row.createdAt,
8266
8312
  };
8267
8313
  }
@@ -8283,11 +8329,12 @@ const contextSnapshotQueries = {
8283
8329
  number | null,
8284
8330
  number,
8285
8331
  number,
8332
+ string | null, // contextFormula (migration 063)
8286
8333
  string,
8287
8334
  ]
8288
8335
  >(
8289
- `INSERT INTO task_context_snapshots (id, taskId, agentId, sessionId, contextUsedTokens, contextTotalTokens, contextPercent, eventType, compactTrigger, preCompactTokens, cumulativeInputTokens, cumulativeOutputTokens, createdAt)
8290
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
8336
+ `INSERT INTO task_context_snapshots (id, taskId, agentId, sessionId, contextUsedTokens, contextTotalTokens, contextPercent, eventType, compactTrigger, preCompactTokens, cumulativeInputTokens, cumulativeOutputTokens, contextFormula, createdAt)
8337
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
8291
8338
  ),
8292
8339
 
8293
8340
  getByTaskId: () =>
@@ -8309,10 +8356,12 @@ export interface CreateContextSnapshotInput {
8309
8356
  contextTotalTokens?: number;
8310
8357
  contextPercent?: number;
8311
8358
  eventType: ContextSnapshotEventType;
8312
- compactTrigger?: "auto" | "manual";
8359
+ compactTrigger?: "auto" | "manual" | "auto-inferred";
8313
8360
  preCompactTokens?: number;
8314
8361
  cumulativeInputTokens?: number;
8315
8362
  cumulativeOutputTokens?: number;
8363
+ // Migration 063 — adapter-supplied formula tag.
8364
+ contextFormula?: ContextSnapshot["contextFormula"];
8316
8365
  }
8317
8366
 
8318
8367
  export function createContextSnapshot(input: CreateContextSnapshotInput): ContextSnapshot {
@@ -8334,6 +8383,7 @@ export function createContextSnapshot(input: CreateContextSnapshotInput): Contex
8334
8383
  input.preCompactTokens ?? null,
8335
8384
  input.cumulativeInputTokens ?? 0,
8336
8385
  input.cumulativeOutputTokens ?? 0,
8386
+ input.contextFormula ?? null,
8337
8387
  now,
8338
8388
  );
8339
8389
 
@@ -8347,10 +8397,15 @@ export function createContextSnapshot(input: CreateContextSnapshotInput): Contex
8347
8397
  .run(input.contextPercent, input.taskId);
8348
8398
  }
8349
8399
 
8350
- // Keep totalContextTokensUsed up to date with the latest known value
8400
+ // Migration 063: peakContextTokens is monotonic-max across snapshots, not a
8401
+ // rolling latest. Mirrors Claude Code's status-line "peak context" semantic.
8351
8402
  if (input.contextUsedTokens != null) {
8352
8403
  getDb()
8353
- .prepare("UPDATE agent_tasks SET totalContextTokensUsed = ? WHERE id = ?")
8404
+ .prepare(
8405
+ `UPDATE agent_tasks
8406
+ SET peakContextTokens = MAX(COALESCE(peakContextTokens, 0), ?)
8407
+ WHERE id = ?`,
8408
+ )
8354
8409
  .run(input.contextUsedTokens, input.taskId);
8355
8410
  }
8356
8411
 
@@ -8362,9 +8417,17 @@ export function createContextSnapshot(input: CreateContextSnapshotInput): Contex
8362
8417
  .run(input.taskId);
8363
8418
  }
8364
8419
 
8365
- if (input.eventType === "completion" && input.contextTotalTokens != null) {
8420
+ // Phase 10: set contextWindowSize on the FIRST snapshot that carries one
8421
+ // (was previously gated on eventType === 'completion', meaning the UI saw
8422
+ // NULL throughout running tasks). Subsequent snapshots leave it alone — the
8423
+ // window doesn't change mid-session.
8424
+ if (input.contextTotalTokens != null) {
8366
8425
  getDb()
8367
- .prepare("UPDATE agent_tasks SET contextWindowSize = ? WHERE id = ?")
8426
+ .prepare(
8427
+ `UPDATE agent_tasks
8428
+ SET contextWindowSize = ?
8429
+ WHERE id = ? AND contextWindowSize IS NULL`,
8430
+ )
8368
8431
  .run(input.contextTotalTokens, input.taskId);
8369
8432
  }
8370
8433
 
@@ -8381,6 +8444,7 @@ export function createContextSnapshot(input: CreateContextSnapshotInput): Contex
8381
8444
  preCompactTokens: input.preCompactTokens,
8382
8445
  cumulativeInputTokens: input.cumulativeInputTokens ?? 0,
8383
8446
  cumulativeOutputTokens: input.cumulativeOutputTokens ?? 0,
8447
+ contextFormula: input.contextFormula,
8384
8448
  createdAt: now,
8385
8449
  };
8386
8450
  }
@@ -8396,7 +8460,8 @@ export function getContextSnapshotsBySessionId(sessionId: string, limit = 500):
8396
8460
  export interface ContextSummary {
8397
8461
  compactionCount: number;
8398
8462
  peakContextPercent: number | null;
8399
- totalContextTokensUsed: number | null;
8463
+ // Migration 063: renamed from totalContextTokensUsed.
8464
+ peakContextTokens: number | null;
8400
8465
  contextWindowSize: number | null;
8401
8466
  snapshotCount: number;
8402
8467
  }
@@ -8412,7 +8477,7 @@ export function getContextSummaryByTaskId(taskId: string): ContextSummary {
8412
8477
  return {
8413
8478
  compactionCount: task?.compactionCount ?? 0,
8414
8479
  peakContextPercent: task?.peakContextPercent ?? null,
8415
- totalContextTokensUsed: task?.totalContextTokensUsed ?? null,
8480
+ peakContextTokens: task?.peakContextTokens ?? null,
8416
8481
  contextWindowSize: task?.contextWindowSize ?? null,
8417
8482
  snapshotCount: countRow?.cnt ?? 0,
8418
8483
  };
@@ -8635,6 +8700,12 @@ export function getKeyCostSummary(keyType?: string): KeyCostSummary[] {
8635
8700
  }
8636
8701
 
8637
8702
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
8703
+ // Phase 13: INNER JOIN -> LEFT JOIN. The `WHERE t.credentialKeySuffix IS NOT NULL`
8704
+ // still filters out rows whose taskId doesn't link to a task with credentials,
8705
+ // but switching to LEFT JOIN means a future change that drops the WHERE
8706
+ // (or a debugging query that wants orphan rows visible) doesn't silently
8707
+ // disappear them. Equivalent for the current `WHERE … IS NOT NULL` filter;
8708
+ // makes the query's intent (cost rows owned by a credential) explicit.
8638
8709
  return db
8639
8710
  .prepare<KeyCostSummary, string[]>(
8640
8711
  `SELECT
@@ -8645,7 +8716,7 @@ export function getKeyCostSummary(keyType?: string): KeyCostSummary[] {
8645
8716
  COALESCE(SUM(sc.outputTokens), 0) as totalOutputTokens,
8646
8717
  COUNT(DISTINCT sc.taskId) as taskCount
8647
8718
  FROM session_costs sc
8648
- JOIN agent_tasks t ON sc.taskId = t.id
8719
+ LEFT JOIN agent_tasks t ON sc.taskId = t.id
8649
8720
  ${where}
8650
8721
  GROUP BY t.credentialKeyType, t.credentialKeySuffix`,
8651
8722
  )