@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 +32 -1
- package/package.json +1 -1
- package/src/be/date-utils.ts +28 -0
- package/src/be/db-queries/oauth.ts +23 -4
- package/src/be/db-queries/tracker.ts +42 -15
- package/src/be/db.ts +23 -22
- package/src/be/migrations/030_iso8601_date_consistency.sql +108 -0
- package/src/tests/date-utils.test.ts +48 -0
- package/src/tests/heartbeat.test.ts +1 -1
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.
|
|
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
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
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 (?, ?,
|
|
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 <
|
|
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 <
|
|
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 =
|
|
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
|
|