@desplega.ai/agent-swarm 1.57.0 → 1.57.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.
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.56.5",
5
+ "version": "1.57.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": [
@@ -1646,6 +1646,37 @@
1646
1646
  }
1647
1647
  }
1648
1648
  },
1649
+ "/api/keys/costs": {
1650
+ "get": {
1651
+ "summary": "Get aggregated cost data per API key",
1652
+ "tags": [
1653
+ "API Keys"
1654
+ ],
1655
+ "security": [
1656
+ {
1657
+ "bearerAuth": []
1658
+ }
1659
+ ],
1660
+ "parameters": [
1661
+ {
1662
+ "schema": {
1663
+ "type": "string"
1664
+ },
1665
+ "required": false,
1666
+ "name": "keyType",
1667
+ "in": "query"
1668
+ }
1669
+ ],
1670
+ "responses": {
1671
+ "200": {
1672
+ "description": "Per-key cost aggregation"
1673
+ },
1674
+ "401": {
1675
+ "description": "Unauthorized"
1676
+ }
1677
+ }
1678
+ }
1679
+ },
1649
1680
  "/api/events": {
1650
1681
  "post": {
1651
1682
  "summary": "Store a single event",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.57.0",
3
+ "version": "1.57.2",
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>",
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Normalizes date strings from SQLite to ISO 8601 UTC format.
3
+ *
4
+ * SQLite's `datetime('now')` and `CURRENT_TIMESTAMP` produce bare format
5
+ * `YYYY-MM-DD HH:MM:SS` which browsers parse as local time. This utility
6
+ * converts them to `YYYY-MM-DDTHH:MM:SS.000Z` so they're unambiguously UTC.
7
+ */
8
+
9
+ const BARE_DATETIME_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
10
+
11
+ export function normalizeDate(date: string | null | undefined): string | null {
12
+ if (date == null) return null;
13
+ if (BARE_DATETIME_RE.test(date)) {
14
+ return `${date.replace(" ", "T")}.000Z`;
15
+ }
16
+ return date;
17
+ }
18
+
19
+ /**
20
+ * Non-null variant for required date fields.
21
+ * Returns the input unchanged if already ISO 8601, or converts bare format.
22
+ */
23
+ export function normalizeDateRequired(date: string): string {
24
+ if (BARE_DATETIME_RE.test(date)) {
25
+ return `${date.replace(" ", "T")}.000Z`;
26
+ }
27
+ return date;
28
+ }
@@ -1,12 +1,30 @@
1
1
  import type { OAuthApp, OAuthTokens } from "../../tracker/types";
2
+ import { normalizeDateRequired } from "../date-utils";
2
3
  import { getDb } from "../db";
3
4
 
4
5
  // ── OAuth Apps ──
5
6
 
7
+ function normalizeOAuthApp(row: OAuthApp): OAuthApp {
8
+ return {
9
+ ...row,
10
+ createdAt: normalizeDateRequired(row.createdAt),
11
+ updatedAt: normalizeDateRequired(row.updatedAt),
12
+ };
13
+ }
14
+
15
+ function normalizeOAuthTokens(row: OAuthTokens): OAuthTokens {
16
+ return {
17
+ ...row,
18
+ createdAt: normalizeDateRequired(row.createdAt),
19
+ updatedAt: normalizeDateRequired(row.updatedAt),
20
+ };
21
+ }
22
+
6
23
  export function getOAuthApp(provider: string): OAuthApp | null {
7
- return getDb()
24
+ const row = getDb()
8
25
  .query("SELECT * FROM oauth_apps WHERE provider = ?")
9
26
  .get(provider) as OAuthApp | null;
27
+ return row ? normalizeOAuthApp(row) : null;
10
28
  }
11
29
 
12
30
  export function upsertOAuthApp(
@@ -33,7 +51,7 @@ export function upsertOAuthApp(
33
51
  redirectUri = excluded.redirectUri,
34
52
  scopes = excluded.scopes,
35
53
  metadata = excluded.metadata,
36
- updatedAt = datetime('now')`,
54
+ updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`,
37
55
  )
38
56
  .run(
39
57
  provider,
@@ -50,9 +68,10 @@ export function upsertOAuthApp(
50
68
  // ── OAuth Tokens ──
51
69
 
52
70
  export function getOAuthTokens(provider: string): OAuthTokens | null {
53
- return getDb()
71
+ const row = getDb()
54
72
  .query("SELECT * FROM oauth_tokens WHERE provider = ?")
55
73
  .get(provider) as OAuthTokens | null;
74
+ return row ? normalizeOAuthTokens(row) : null;
56
75
  }
57
76
 
58
77
  export function storeOAuthTokens(
@@ -73,7 +92,7 @@ export function storeOAuthTokens(
73
92
  refreshToken = COALESCE(excluded.refreshToken, oauth_tokens.refreshToken),
74
93
  expiresAt = excluded.expiresAt,
75
94
  scope = COALESCE(excluded.scope, oauth_tokens.scope),
76
- updatedAt = datetime('now')`,
95
+ updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`,
77
96
  )
78
97
  .run(provider, data.accessToken, data.refreshToken ?? null, data.expiresAt, data.scope ?? null);
79
98
  }
@@ -1,6 +1,22 @@
1
1
  import type { TrackerAgentMapping, TrackerSync } from "../../tracker/types";
2
+ import { normalizeDateRequired } from "../date-utils";
2
3
  import { getDb } from "../db";
3
4
 
5
+ function normalizeTrackerSync(row: TrackerSync): TrackerSync {
6
+ return {
7
+ ...row,
8
+ lastSyncedAt: normalizeDateRequired(row.lastSyncedAt),
9
+ createdAt: normalizeDateRequired(row.createdAt),
10
+ };
11
+ }
12
+
13
+ function normalizeTrackerAgentMapping(row: TrackerAgentMapping): TrackerAgentMapping {
14
+ return {
15
+ ...row,
16
+ createdAt: normalizeDateRequired(row.createdAt),
17
+ };
18
+ }
19
+
4
20
  // ── Tracker Sync ──
5
21
 
6
22
  export function getTrackerSync(
@@ -8,9 +24,10 @@ export function getTrackerSync(
8
24
  entityType: "task",
9
25
  swarmId: string,
10
26
  ): TrackerSync | null {
11
- return getDb()
27
+ const row = getDb()
12
28
  .query("SELECT * FROM tracker_sync WHERE provider = ? AND entityType = ? AND swarmId = ?")
13
29
  .get(provider, entityType, swarmId) as TrackerSync | null;
30
+ return row ? normalizeTrackerSync(row) : null;
14
31
  }
15
32
 
16
33
  export function getTrackerSyncByExternalId(
@@ -18,9 +35,10 @@ export function getTrackerSyncByExternalId(
18
35
  entityType: "task",
19
36
  externalId: string,
20
37
  ): TrackerSync | null {
21
- return getDb()
38
+ const row = getDb()
22
39
  .query("SELECT * FROM tracker_sync WHERE provider = ? AND entityType = ? AND externalId = ?")
23
40
  .get(provider, entityType, externalId) as TrackerSync | null;
41
+ return row ? normalizeTrackerSync(row) : null;
24
42
  }
25
43
 
26
44
  export function createTrackerSync(data: {
@@ -53,7 +71,7 @@ export function createTrackerSync(data: {
53
71
  data.lastDeliveryId ?? null,
54
72
  data.syncDirection ?? "inbound",
55
73
  ) as TrackerSync;
56
- return result;
74
+ return normalizeTrackerSync(result);
57
75
  }
58
76
 
59
77
  export function updateTrackerSync(
@@ -124,9 +142,11 @@ export function getAllTrackerSyncs(provider?: string, entityType?: "task"): Trac
124
142
  }
125
143
 
126
144
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
127
- return getDb()
128
- .query(`SELECT * FROM tracker_sync ${where} ORDER BY createdAt DESC`)
129
- .all(...values) as TrackerSync[];
145
+ return (
146
+ getDb()
147
+ .query(`SELECT * FROM tracker_sync ${where} ORDER BY createdAt DESC`)
148
+ .all(...values) as TrackerSync[]
149
+ ).map(normalizeTrackerSync);
130
150
  }
131
151
 
132
152
  // ── Tracker Agent Mapping ──
@@ -135,18 +155,20 @@ export function getTrackerAgentMapping(
135
155
  provider: string,
136
156
  agentId: string,
137
157
  ): TrackerAgentMapping | null {
138
- return getDb()
158
+ const row = getDb()
139
159
  .query("SELECT * FROM tracker_agent_mapping WHERE provider = ? AND agentId = ?")
140
160
  .get(provider, agentId) as TrackerAgentMapping | null;
161
+ return row ? normalizeTrackerAgentMapping(row) : null;
141
162
  }
142
163
 
143
164
  export function getTrackerAgentMappingByExternalUser(
144
165
  provider: string,
145
166
  externalUserId: string,
146
167
  ): TrackerAgentMapping | null {
147
- return getDb()
168
+ const row = getDb()
148
169
  .query("SELECT * FROM tracker_agent_mapping WHERE provider = ? AND externalUserId = ?")
149
170
  .get(provider, externalUserId) as TrackerAgentMapping | null;
171
+ return row ? normalizeTrackerAgentMapping(row) : null;
150
172
  }
151
173
 
152
174
  export function createTrackerAgentMapping(data: {
@@ -155,13 +177,14 @@ export function createTrackerAgentMapping(data: {
155
177
  externalUserId: string;
156
178
  agentName: string;
157
179
  }): TrackerAgentMapping {
158
- return getDb()
180
+ const result = getDb()
159
181
  .query(
160
182
  `INSERT INTO tracker_agent_mapping (provider, agentId, externalUserId, agentName)
161
183
  VALUES (?, ?, ?, ?)
162
184
  RETURNING *`,
163
185
  )
164
186
  .get(data.provider, data.agentId, data.externalUserId, data.agentName) as TrackerAgentMapping;
187
+ return normalizeTrackerAgentMapping(result);
165
188
  }
166
189
 
167
190
  export function deleteTrackerAgentMapping(provider: string, agentId: string): void {
@@ -172,11 +195,15 @@ export function deleteTrackerAgentMapping(provider: string, agentId: string): vo
172
195
 
173
196
  export function getAllTrackerAgentMappings(provider?: string): TrackerAgentMapping[] {
174
197
  if (provider) {
175
- return getDb()
176
- .query("SELECT * FROM tracker_agent_mapping WHERE provider = ? ORDER BY createdAt DESC")
177
- .all(provider) as TrackerAgentMapping[];
198
+ return (
199
+ getDb()
200
+ .query("SELECT * FROM tracker_agent_mapping WHERE provider = ? ORDER BY createdAt DESC")
201
+ .all(provider) as TrackerAgentMapping[]
202
+ ).map(normalizeTrackerAgentMapping);
178
203
  }
179
- return getDb()
180
- .query("SELECT * FROM tracker_agent_mapping ORDER BY createdAt DESC")
181
- .all() as TrackerAgentMapping[];
204
+ return (
205
+ getDb()
206
+ .query("SELECT * FROM tracker_agent_mapping ORDER BY createdAt DESC")
207
+ .all() as TrackerAgentMapping[]
208
+ ).map(normalizeTrackerAgentMapping);
182
209
  }
package/src/be/db.ts CHANGED
@@ -55,6 +55,7 @@ import type {
55
55
  WorkflowSnapshot,
56
56
  WorkflowVersion,
57
57
  } from "../types";
58
+ import { normalizeDate, normalizeDateRequired } from "./date-utils";
58
59
  import { runMigrations } from "./migrations/runner";
59
60
  import { seedDefaultTemplates } from "./seed";
60
61
 
@@ -3960,17 +3961,17 @@ function rowToScheduledTask(row: ScheduledTaskRow): ScheduledTask {
3960
3961
  priority: row.priority,
3961
3962
  targetAgentId: row.targetAgentId ?? undefined,
3962
3963
  enabled: row.enabled === 1,
3963
- lastRunAt: row.lastRunAt ?? undefined,
3964
- nextRunAt: row.nextRunAt ?? undefined,
3964
+ lastRunAt: normalizeDate(row.lastRunAt) ?? undefined,
3965
+ nextRunAt: normalizeDate(row.nextRunAt) ?? undefined,
3965
3966
  createdByAgentId: row.createdByAgentId ?? undefined,
3966
3967
  timezone: row.timezone,
3967
3968
  consecutiveErrors: row.consecutiveErrors ?? 0,
3968
- lastErrorAt: row.lastErrorAt ?? undefined,
3969
+ lastErrorAt: normalizeDate(row.lastErrorAt) ?? undefined,
3969
3970
  lastErrorMessage: row.lastErrorMessage ?? undefined,
3970
3971
  model: (row.model as "haiku" | "sonnet" | "opus" | null) ?? undefined,
3971
3972
  scheduleType: row.scheduleType as "recurring" | "one_time",
3972
- createdAt: row.createdAt,
3973
- lastUpdatedAt: row.lastUpdatedAt,
3973
+ createdAt: normalizeDateRequired(row.createdAt),
3974
+ lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
3974
3975
  };
3975
3976
  }
3976
3977
 
@@ -5241,8 +5242,8 @@ function rowToWorkflow(row: WorkflowRow): Workflow {
5241
5242
  dir: row.dir ?? undefined,
5242
5243
  vcsRepo: row.vcs_repo ?? undefined,
5243
5244
  createdByAgentId: row.createdByAgentId ?? undefined,
5244
- createdAt: row.createdAt,
5245
- lastUpdatedAt: row.lastUpdatedAt,
5245
+ createdAt: normalizeDateRequired(row.createdAt),
5246
+ lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
5246
5247
  };
5247
5248
  }
5248
5249
 
@@ -5446,9 +5447,9 @@ function rowToWorkflowRun(row: WorkflowRunRow): WorkflowRun {
5446
5447
  triggerData: row.triggerData ? JSON.parse(row.triggerData) : undefined,
5447
5448
  context: row.context ? (JSON.parse(row.context) as Record<string, unknown>) : undefined,
5448
5449
  error: row.error ?? undefined,
5449
- startedAt: row.startedAt,
5450
- lastUpdatedAt: row.lastUpdatedAt,
5451
- finishedAt: row.finishedAt ?? undefined,
5450
+ startedAt: normalizeDateRequired(row.startedAt),
5451
+ lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
5452
+ finishedAt: normalizeDate(row.finishedAt) ?? undefined,
5452
5453
  };
5453
5454
  }
5454
5455
 
@@ -5555,11 +5556,11 @@ function rowToWorkflowRunStep(row: WorkflowRunStepRow): WorkflowRunStep {
5555
5556
  input: row.input ? JSON.parse(row.input) : undefined,
5556
5557
  output: row.output ? JSON.parse(row.output) : undefined,
5557
5558
  error: row.error ?? undefined,
5558
- startedAt: row.startedAt,
5559
- finishedAt: row.finishedAt ?? undefined,
5559
+ startedAt: normalizeDateRequired(row.startedAt),
5560
+ finishedAt: normalizeDate(row.finishedAt) ?? undefined,
5560
5561
  retryCount: row.retryCount,
5561
5562
  maxRetries: row.maxRetries,
5562
- nextRetryAt: row.nextRetryAt ?? undefined,
5563
+ nextRetryAt: normalizeDate(row.nextRetryAt) ?? undefined,
5563
5564
  idempotencyKey: row.idempotencyKey ?? undefined,
5564
5565
  diagnostics: row.diagnostics ?? undefined,
5565
5566
  nextPort: row.nextPort ?? undefined,
@@ -5806,7 +5807,7 @@ function rowToWorkflowVersion(row: WorkflowVersionRow): WorkflowVersion {
5806
5807
  version: row.version,
5807
5808
  snapshot: JSON.parse(row.snapshot) as WorkflowSnapshot,
5808
5809
  changedByAgentId: row.changedByAgentId ?? undefined,
5809
- createdAt: row.createdAt,
5810
+ createdAt: normalizeDateRequired(row.createdAt),
5810
5811
  };
5811
5812
  }
5812
5813
 
@@ -6315,7 +6316,7 @@ function rowToChannelActivityCursor(row: ChannelActivityCursorRow): ChannelActiv
6315
6316
  return {
6316
6317
  channelId: row.channelId,
6317
6318
  lastSeenTs: row.lastSeenTs,
6318
- updatedAt: row.updatedAt,
6319
+ updatedAt: normalizeDateRequired(row.updatedAt),
6319
6320
  };
6320
6321
  }
6321
6322
 
@@ -6339,7 +6340,7 @@ export function upsertChannelActivityCursor(channelId: string, lastSeenTs: strin
6339
6340
  getDb()
6340
6341
  .prepare(
6341
6342
  `INSERT INTO channel_activity_cursors (channelId, lastSeenTs, updatedAt)
6342
- VALUES (?, ?, datetime('now'))
6343
+ VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
6343
6344
  ON CONFLICT(channelId) DO UPDATE SET lastSeenTs = excluded.lastSeenTs, updatedAt = excluded.updatedAt`,
6344
6345
  )
6345
6346
  .run(channelId, lastSeenTs);
@@ -6399,12 +6400,12 @@ function rowToApprovalRequest(row: ApprovalRequestRow): ApprovalRequest {
6399
6400
  status: row.status as ApprovalRequest["status"],
6400
6401
  responses: row.responses ? JSON.parse(row.responses) : null,
6401
6402
  resolvedBy: row.resolvedBy,
6402
- resolvedAt: row.resolvedAt,
6403
+ resolvedAt: normalizeDate(row.resolvedAt),
6403
6404
  timeoutSeconds: row.timeoutSeconds,
6404
- expiresAt: row.expiresAt,
6405
+ expiresAt: normalizeDate(row.expiresAt),
6405
6406
  notificationChannels: row.notificationChannels ? JSON.parse(row.notificationChannels) : null,
6406
- createdAt: row.createdAt,
6407
- updatedAt: row.updatedAt,
6407
+ createdAt: normalizeDateRequired(row.createdAt),
6408
+ updatedAt: normalizeDateRequired(row.updatedAt),
6408
6409
  };
6409
6410
  }
6410
6411
 
@@ -6565,7 +6566,7 @@ export function getStuckApprovalRuns(): StuckApprovalRun[] {
6565
6566
  JOIN approval_requests ar ON ar.workflowRunStepId = wrs.id
6566
6567
  WHERE wr.status = 'waiting'
6567
6568
  AND (ar.status IN ('approved', 'rejected', 'timeout')
6568
- OR (ar.status = 'pending' AND ar.expiresAt IS NOT NULL AND ar.expiresAt < datetime('now')))`,
6569
+ OR (ar.status = 'pending' AND ar.expiresAt IS NOT NULL AND ar.expiresAt < strftime('%Y-%m-%dT%H:%M:%fZ', 'now')))`,
6569
6570
  )
6570
6571
  .all();
6571
6572
  }
@@ -6586,7 +6587,7 @@ export function getExpiredPendingApprovals(): ApprovalRequest[] {
6586
6587
  `SELECT * FROM approval_requests
6587
6588
  WHERE status = 'pending'
6588
6589
  AND expiresAt IS NOT NULL
6589
- AND expiresAt < datetime('now')`,
6590
+ AND expiresAt < strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`,
6590
6591
  )
6591
6592
  .all();
6592
6593
  return rows.map(rowToApprovalRequest);
@@ -0,0 +1,108 @@
1
+ -- Fix date storage format inconsistency: convert bare datetime format
2
+ -- (YYYY-MM-DD HH:MM:SS) to ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.000Z).
3
+ --
4
+ -- Affected tables use datetime('now') or CURRENT_TIMESTAMP defaults which
5
+ -- produce bare format. Browsers parse these as local time instead of UTC.
6
+
7
+ -- Update existing bare-format dates to ISO 8601 in all affected tables.
8
+ -- The pattern: replace space with T and append .000Z for dates matching bare format.
9
+
10
+ -- workflows
11
+ UPDATE workflows SET
12
+ createdAt = replace(createdAt, ' ', 'T') || '.000Z'
13
+ WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
14
+ UPDATE workflows SET
15
+ lastUpdatedAt = replace(lastUpdatedAt, ' ', 'T') || '.000Z'
16
+ WHERE lastUpdatedAt LIKE '____-__-__ __:__:__' AND lastUpdatedAt NOT LIKE '%T%';
17
+
18
+ -- workflow_runs
19
+ UPDATE workflow_runs SET
20
+ startedAt = replace(startedAt, ' ', 'T') || '.000Z'
21
+ WHERE startedAt LIKE '____-__-__ __:__:__' AND startedAt NOT LIKE '%T%';
22
+ UPDATE workflow_runs SET
23
+ lastUpdatedAt = replace(lastUpdatedAt, ' ', 'T') || '.000Z'
24
+ WHERE lastUpdatedAt LIKE '____-__-__ __:__:__' AND lastUpdatedAt NOT LIKE '%T%';
25
+ UPDATE workflow_runs SET
26
+ finishedAt = replace(finishedAt, ' ', 'T') || '.000Z'
27
+ WHERE finishedAt IS NOT NULL AND finishedAt LIKE '____-__-__ __:__:__' AND finishedAt NOT LIKE '%T%';
28
+
29
+ -- workflow_run_steps
30
+ UPDATE workflow_run_steps SET
31
+ startedAt = replace(startedAt, ' ', 'T') || '.000Z'
32
+ WHERE startedAt LIKE '____-__-__ __:__:__' AND startedAt NOT LIKE '%T%';
33
+ UPDATE workflow_run_steps SET
34
+ finishedAt = replace(finishedAt, ' ', 'T') || '.000Z'
35
+ WHERE finishedAt IS NOT NULL AND finishedAt LIKE '____-__-__ __:__:__' AND finishedAt NOT LIKE '%T%';
36
+ UPDATE workflow_run_steps SET
37
+ nextRetryAt = replace(nextRetryAt, ' ', 'T') || '.000Z'
38
+ WHERE nextRetryAt IS NOT NULL AND nextRetryAt LIKE '____-__-__ __:__:__' AND nextRetryAt NOT LIKE '%T%';
39
+
40
+ -- tracker_sync
41
+ UPDATE tracker_sync SET
42
+ lastSyncedAt = replace(lastSyncedAt, ' ', 'T') || '.000Z'
43
+ WHERE lastSyncedAt LIKE '____-__-__ __:__:__' AND lastSyncedAt NOT LIKE '%T%';
44
+ UPDATE tracker_sync SET
45
+ createdAt = replace(createdAt, ' ', 'T') || '.000Z'
46
+ WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
47
+
48
+ -- tracker_agent_mapping
49
+ UPDATE tracker_agent_mapping SET
50
+ createdAt = replace(createdAt, ' ', 'T') || '.000Z'
51
+ WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
52
+
53
+ -- approval_requests
54
+ UPDATE approval_requests SET
55
+ createdAt = replace(createdAt, ' ', 'T') || '.000Z'
56
+ WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
57
+ UPDATE approval_requests SET
58
+ updatedAt = replace(updatedAt, ' ', 'T') || '.000Z'
59
+ WHERE updatedAt LIKE '____-__-__ __:__:__' AND updatedAt NOT LIKE '%T%';
60
+ UPDATE approval_requests SET
61
+ resolvedAt = replace(resolvedAt, ' ', 'T') || '.000Z'
62
+ WHERE resolvedAt IS NOT NULL AND resolvedAt LIKE '____-__-__ __:__:__' AND resolvedAt NOT LIKE '%T%';
63
+ UPDATE approval_requests SET
64
+ expiresAt = replace(expiresAt, ' ', 'T') || '.000Z'
65
+ WHERE expiresAt IS NOT NULL AND expiresAt LIKE '____-__-__ __:__:__' AND expiresAt NOT LIKE '%T%';
66
+
67
+ -- channel_activity_cursors
68
+ UPDATE channel_activity_cursors SET
69
+ updatedAt = replace(updatedAt, ' ', 'T') || '.000Z'
70
+ WHERE updatedAt LIKE '____-__-__ __:__:__' AND updatedAt NOT LIKE '%T%';
71
+
72
+ -- oauth_apps
73
+ UPDATE oauth_apps SET
74
+ createdAt = replace(createdAt, ' ', 'T') || '.000Z'
75
+ WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
76
+ UPDATE oauth_apps SET
77
+ updatedAt = replace(updatedAt, ' ', 'T') || '.000Z'
78
+ WHERE updatedAt LIKE '____-__-__ __:__:__' AND updatedAt NOT LIKE '%T%';
79
+
80
+ -- oauth_tokens
81
+ UPDATE oauth_tokens SET
82
+ createdAt = replace(createdAt, ' ', 'T') || '.000Z'
83
+ WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
84
+ UPDATE oauth_tokens SET
85
+ updatedAt = replace(updatedAt, ' ', 'T') || '.000Z'
86
+ WHERE updatedAt LIKE '____-__-__ __:__:__' AND updatedAt NOT LIKE '%T%';
87
+
88
+ -- workflow_versions
89
+ UPDATE workflow_versions SET
90
+ createdAt = replace(createdAt, ' ', 'T') || '.000Z'
91
+ WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
92
+
93
+ -- scheduled_tasks (defense-in-depth — dates are written from JS toISOString() but normalize any legacy data)
94
+ UPDATE scheduled_tasks SET
95
+ createdAt = replace(createdAt, ' ', 'T') || '.000Z'
96
+ WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
97
+ UPDATE scheduled_tasks SET
98
+ lastUpdatedAt = replace(lastUpdatedAt, ' ', 'T') || '.000Z'
99
+ WHERE lastUpdatedAt LIKE '____-__-__ __:__:__' AND lastUpdatedAt NOT LIKE '%T%';
100
+ UPDATE scheduled_tasks SET
101
+ lastRunAt = replace(lastRunAt, ' ', 'T') || '.000Z'
102
+ WHERE lastRunAt IS NOT NULL AND lastRunAt LIKE '____-__-__ __:__:__' AND lastRunAt NOT LIKE '%T%';
103
+ UPDATE scheduled_tasks SET
104
+ nextRunAt = replace(nextRunAt, ' ', 'T') || '.000Z'
105
+ WHERE nextRunAt IS NOT NULL AND nextRunAt LIKE '____-__-__ __:__:__' AND nextRunAt NOT LIKE '%T%';
106
+ UPDATE scheduled_tasks SET
107
+ lastErrorAt = replace(lastErrorAt, ' ', 'T') || '.000Z'
108
+ WHERE lastErrorAt IS NOT NULL AND lastErrorAt LIKE '____-__-__ __:__:__' AND lastErrorAt NOT LIKE '%T%';
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { normalizeDate, normalizeDateRequired } from "../be/date-utils";
3
+
4
+ describe("normalizeDate", () => {
5
+ it("returns null for null input", () => {
6
+ expect(normalizeDate(null)).toBe(null);
7
+ });
8
+
9
+ it("returns null for undefined input", () => {
10
+ expect(normalizeDate(undefined)).toBe(null);
11
+ });
12
+
13
+ it("converts bare datetime format to ISO 8601", () => {
14
+ expect(normalizeDate("2026-03-31 14:30:00")).toBe("2026-03-31T14:30:00.000Z");
15
+ });
16
+
17
+ it("converts midnight bare datetime", () => {
18
+ expect(normalizeDate("2026-01-01 00:00:00")).toBe("2026-01-01T00:00:00.000Z");
19
+ });
20
+
21
+ it("passes through ISO 8601 with Z suffix unchanged", () => {
22
+ expect(normalizeDate("2026-03-31T14:30:00.000Z")).toBe("2026-03-31T14:30:00.000Z");
23
+ });
24
+
25
+ it("passes through ISO 8601 with milliseconds and Z", () => {
26
+ expect(normalizeDate("2026-03-31T14:30:00.123Z")).toBe("2026-03-31T14:30:00.123Z");
27
+ });
28
+
29
+ it("passes through toISOString() output unchanged", () => {
30
+ const iso = new Date().toISOString();
31
+ expect(normalizeDate(iso)).toBe(iso);
32
+ });
33
+
34
+ it("passes through strftime output unchanged", () => {
35
+ // strftime('%Y-%m-%dT%H:%M:%fZ', 'now') produces this format
36
+ expect(normalizeDate("2026-03-31T14:30:00.000Z")).toBe("2026-03-31T14:30:00.000Z");
37
+ });
38
+ });
39
+
40
+ describe("normalizeDateRequired", () => {
41
+ it("converts bare datetime format to ISO 8601", () => {
42
+ expect(normalizeDateRequired("2026-03-31 14:30:00")).toBe("2026-03-31T14:30:00.000Z");
43
+ });
44
+
45
+ it("passes through ISO 8601 unchanged", () => {
46
+ expect(normalizeDateRequired("2026-03-31T14:30:00.123Z")).toBe("2026-03-31T14:30:00.123Z");
47
+ });
48
+ });
@@ -68,7 +68,7 @@ describe("Heartbeat Triage", () => {
68
68
  createTaskExtended("Completed task", { agentId: agent.id });
69
69
  // Manually mark as completed
70
70
  getDb().run(
71
- "UPDATE agent_tasks SET status = 'completed', finishedAt = datetime('now') WHERE agentId = ?",
71
+ "UPDATE agent_tasks SET status = 'completed', finishedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE agentId = ?",
72
72
  [agent.id],
73
73
  );
74
74