@desplega.ai/agent-swarm 1.92.2 → 1.94.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.
- package/README.md +2 -2
- package/openapi.json +242 -3
- package/package.json +5 -5
- package/src/be/db.ts +152 -11
- package/src/be/memory/boot-reembed.ts +0 -1
- package/src/be/memory/providers/sqlite-store.ts +42 -25
- package/src/be/memory/raters/llm-client.ts +12 -5
- package/src/be/memory/types.ts +3 -0
- package/src/be/migrations/088_script_runs_list_indexes.sql +10 -0
- package/src/be/migrations/089_harness_variant.sql +2 -0
- package/src/be/migrations/090_model_tiers.sql +2 -0
- package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
- package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
- package/src/be/migrations/093_slack_message_tracking.sql +6 -0
- package/src/be/migrations/runner.ts +52 -0
- package/src/be/modelsdev-cache.json +3264 -1166
- package/src/be/scripts/boot-reembed.ts +74 -0
- package/src/be/scripts/db.ts +19 -3
- package/src/be/seed/index.ts +1 -1
- package/src/be/seed/registry.ts +2 -2
- package/src/be/seed/runner.ts +5 -5
- package/src/be/seed/types.ts +6 -1
- package/src/be/seed-pricing.ts +2 -0
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +863 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +506 -0
- package/src/be/seed-scripts/index.ts +8 -7
- package/src/be/skill-sync.ts +28 -179
- package/src/commands/runner.ts +197 -10
- package/src/http/api-keys.ts +42 -0
- package/src/http/index.ts +13 -2
- package/src/http/mcp-bridge.ts +1 -1
- package/src/http/memory.ts +23 -24
- package/src/http/metrics.ts +55 -6
- package/src/http/schedules.ts +16 -15
- package/src/http/script-runs.ts +7 -1
- package/src/http/scripts.ts +147 -1
- package/src/http/tasks.ts +17 -6
- package/src/model-tiers.ts +140 -0
- package/src/providers/claude-adapter.ts +33 -1
- package/src/providers/claude-managed-adapter.ts +3 -0
- package/src/providers/claude-managed-models.ts +16 -0
- package/src/providers/codex-adapter.ts +8 -1
- package/src/providers/codex-models.ts +1 -0
- package/src/providers/codex-oauth/auth-json.ts +1 -0
- package/src/providers/harness-version.ts +7 -0
- package/src/providers/opencode-adapter.ts +12 -4
- package/src/providers/pi-mono-adapter.ts +90 -8
- package/src/providers/types.ts +2 -0
- package/src/scheduler/scheduler.ts +22 -34
- package/src/scripts-runtime/egress-secrets.ts +83 -0
- package/src/scripts-runtime/eval-harness.ts +4 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/loader.ts +2 -0
- package/src/server-user.ts +8 -2
- package/src/slack/channel-join.ts +41 -0
- package/src/slack/responses.ts +39 -11
- package/src/slack/watcher.ts +121 -8
- package/src/tests/additive-buffer.test.ts +0 -1
- package/src/tests/agents-list-model-display.test.ts +13 -0
- package/src/tests/api-key-tracking.test.ts +113 -0
- package/src/tests/approval-requests.test.ts +0 -6
- package/src/tests/aws-error-classifier.test.ts +148 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -0
- package/src/tests/claude-managed-setup.test.ts +0 -4
- package/src/tests/codex-pool.test.ts +2 -6
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/http-api-integration.test.ts +23 -6
- package/src/tests/memory-edges.test.ts +0 -2
- package/src/tests/memory-rate-endpoint.test.ts +0 -2
- package/src/tests/memory-rater-e2e.test.ts +0 -2
- package/src/tests/memory-store.test.ts +19 -1
- package/src/tests/memory.test.ts +51 -0
- package/src/tests/metrics-http.test.ts +137 -3
- package/src/tests/migration-046-budgets.test.ts +33 -0
- package/src/tests/migration-runner-regressions.test.ts +69 -0
- package/src/tests/model-control.test.ts +162 -46
- package/src/tests/opencode-adapter.test.ts +9 -0
- package/src/tests/pi-mono-adapter.test.ts +319 -0
- package/src/tests/providers/pi-cost.test.ts +9 -0
- package/src/tests/reload-config.test.ts +33 -17
- package/src/tests/runner-fallback-output.test.ts +50 -0
- package/src/tests/runner-skills-refresh.test.ts +216 -46
- package/src/tests/script-runs-http.test.ts +7 -1
- package/src/tests/scripts-boot-reembed.test.ts +163 -0
- package/src/tests/scripts-embeddings.test.ts +90 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +129 -0
- package/src/tests/seed-scripts.test.ts +13 -1
- package/src/tests/seed.test.ts +26 -1
- package/src/tests/session-attach.test.ts +6 -6
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
- package/src/tests/skill-fs-writer.test.ts +250 -0
- package/src/tests/slack-attachments-block.test.ts +0 -1
- package/src/tests/slack-blocks.test.ts +0 -1
- package/src/tests/slack-channel-join.test.ts +80 -0
- package/src/tests/slack-identity-resolution.test.ts +0 -1
- package/src/tests/slack-watcher.test.ts +66 -0
- package/src/tests/structured-output.test.ts +0 -2
- package/src/tests/use-dismissible-card.test.ts +0 -4
- package/src/tests/workflow-agent-task.test.ts +5 -2
- package/src/tests/workflow-validation-port-routing.test.ts +181 -0
- package/src/tools/memory-get.ts +11 -0
- package/src/tools/memory-search.ts +18 -0
- package/src/tools/schedules/create-schedule.ts +71 -70
- package/src/tools/schedules/update-schedule.ts +43 -31
- package/src/tools/send-task.ts +16 -5
- package/src/tools/slack-post.ts +18 -15
- package/src/tools/slack-read.ts +9 -11
- package/src/tools/slack-reply.ts +18 -15
- package/src/tools/slack-start-thread.ts +17 -14
- package/src/tools/task-action.ts +11 -3
- package/src/types.ts +40 -0
- package/src/utils/aws-error-classifier.ts +97 -0
- package/src/utils/context-window.ts +5 -0
- package/src/utils/credentials.test.ts +68 -0
- package/src/utils/credentials.ts +66 -5
- package/src/utils/pretty-print.ts +25 -10
- package/src/utils/skill-fs-writer.ts +220 -0
- package/src/utils/skills-refresh.ts +123 -40
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/executors/agent-task.ts +3 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-listen backfill: embed scripts that are missing embeddings (e.g. after
|
|
3
|
+
* boot seeding with scriptEmbeddingMode: "skip"). Runs once per boot,
|
|
4
|
+
* async/non-blocking, idempotent, no-op when every non-scratch script already
|
|
5
|
+
* has an embedding row.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors the memory boot-reembed pattern (src/be/memory/boot-reembed.ts).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getDb } from "@/be/db";
|
|
11
|
+
import type { ScriptScope } from "@/types";
|
|
12
|
+
import { embedScript } from "./embeddings";
|
|
13
|
+
|
|
14
|
+
type ScriptMissingEmbedding = {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
scope: ScriptScope;
|
|
18
|
+
scopeId: string | null;
|
|
19
|
+
source: string;
|
|
20
|
+
description: string;
|
|
21
|
+
intent: string;
|
|
22
|
+
signatureJson: string;
|
|
23
|
+
argsJsonSchema: string | null;
|
|
24
|
+
contentHash: string;
|
|
25
|
+
version: number;
|
|
26
|
+
isScratch: number;
|
|
27
|
+
typeChecked: number;
|
|
28
|
+
fsMode: "none" | "workspace-rw";
|
|
29
|
+
createdByAgentId: string | null;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export async function runBootReembedScripts(): Promise<void> {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
|
|
37
|
+
const missing = db
|
|
38
|
+
.prepare<ScriptMissingEmbedding, []>(
|
|
39
|
+
`SELECT s.* FROM scripts s
|
|
40
|
+
LEFT JOIN script_embeddings e ON e.scriptId = s.id
|
|
41
|
+
WHERE s.isScratch = 0 AND e.scriptId IS NULL`,
|
|
42
|
+
)
|
|
43
|
+
.all();
|
|
44
|
+
|
|
45
|
+
if (missing.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`[boot-reembed-scripts] starting: ${missing.length} scripts missing embeddings`);
|
|
50
|
+
|
|
51
|
+
let embedded = 0;
|
|
52
|
+
let failed = 0;
|
|
53
|
+
|
|
54
|
+
for (const row of missing) {
|
|
55
|
+
try {
|
|
56
|
+
await embedScript({
|
|
57
|
+
...row,
|
|
58
|
+
scopeId: row.scopeId ?? null,
|
|
59
|
+
isScratch: row.isScratch === 1,
|
|
60
|
+
typeChecked: row.typeChecked === 1,
|
|
61
|
+
createdByAgentId: row.createdByAgentId ?? null,
|
|
62
|
+
});
|
|
63
|
+
embedded++;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
failed++;
|
|
66
|
+
console.error(
|
|
67
|
+
`[boot-reembed-scripts] failed to embed "${row.name}":`,
|
|
68
|
+
(err as Error).message,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`[boot-reembed-scripts] complete: embedded=${embedded} failed=${failed}`);
|
|
74
|
+
}
|
package/src/be/scripts/db.ts
CHANGED
|
@@ -26,6 +26,7 @@ type ScriptWriteArgs = ScriptIdentity & {
|
|
|
26
26
|
fsMode?: ScriptFsMode;
|
|
27
27
|
agentId?: string | null;
|
|
28
28
|
changeReason?: string | null;
|
|
29
|
+
embeddingMode?: "sync" | "skip";
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export type UpsertScriptResult = {
|
|
@@ -178,10 +179,11 @@ export function insertScript(args: ScriptWriteArgs): ScriptRecord {
|
|
|
178
179
|
* immediately consistent for authored/promoted scripts.
|
|
179
180
|
*/
|
|
180
181
|
export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertScriptResult> {
|
|
182
|
+
const shouldEmbed = args.embeddingMode !== "skip";
|
|
181
183
|
const existing = getScript(args);
|
|
182
184
|
if (!existing) {
|
|
183
185
|
const script = insertScript(args);
|
|
184
|
-
if (!script.isScratch) {
|
|
186
|
+
if (!script.isScratch && shouldEmbed) {
|
|
185
187
|
await embedScript(script);
|
|
186
188
|
}
|
|
187
189
|
return {
|
|
@@ -235,7 +237,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
|
|
|
235
237
|
|
|
236
238
|
if (!row) throw new Error("Failed to update script metadata");
|
|
237
239
|
const script = rowToScript(row);
|
|
238
|
-
if (!script.isScratch && (trackedMetadataChanged || promotedFromScratch)) {
|
|
240
|
+
if (!script.isScratch && shouldEmbed && (trackedMetadataChanged || promotedFromScratch)) {
|
|
239
241
|
await embedScript(script);
|
|
240
242
|
}
|
|
241
243
|
return {
|
|
@@ -318,7 +320,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
|
|
|
318
320
|
});
|
|
319
321
|
|
|
320
322
|
const script = txn();
|
|
321
|
-
if (!script.isScratch) {
|
|
323
|
+
if (!script.isScratch && shouldEmbed) {
|
|
322
324
|
await embedScript(script);
|
|
323
325
|
}
|
|
324
326
|
|
|
@@ -347,6 +349,11 @@ export function getScript(args: ScriptIdentity): ScriptRecord | null {
|
|
|
347
349
|
return row ? rowToScript(row) : null;
|
|
348
350
|
}
|
|
349
351
|
|
|
352
|
+
export function getScriptById(id: string): ScriptRecord | null {
|
|
353
|
+
const row = getDb().prepare<ScriptRow, [string]>("SELECT * FROM scripts WHERE id = ?").get(id);
|
|
354
|
+
return row ? rowToScript(row) : null;
|
|
355
|
+
}
|
|
356
|
+
|
|
350
357
|
export function getScriptVersion(args: {
|
|
351
358
|
scriptId: string;
|
|
352
359
|
version?: number;
|
|
@@ -408,6 +415,15 @@ export function listScripts(args?: {
|
|
|
408
415
|
.map(rowToScript);
|
|
409
416
|
}
|
|
410
417
|
|
|
418
|
+
export function listScriptVersions(scriptId: string): ScriptVersionRecord[] {
|
|
419
|
+
return getDb()
|
|
420
|
+
.prepare<ScriptVersionRow, [string]>(
|
|
421
|
+
"SELECT * FROM script_versions WHERE scriptId = ? ORDER BY version DESC",
|
|
422
|
+
)
|
|
423
|
+
.all(scriptId)
|
|
424
|
+
.map(rowToScriptVersion);
|
|
425
|
+
}
|
|
426
|
+
|
|
411
427
|
export function deleteScript(args: ScriptIdentity): boolean {
|
|
412
428
|
const existing = getScript(args);
|
|
413
429
|
if (!existing) return false;
|
package/src/be/seed/index.ts
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
export { runAllSeeders, SEEDERS } from "./registry";
|
|
7
7
|
export { runSeeder, runSeeders } from "./runner";
|
|
8
8
|
export { getSeedState, recordSeedState } from "./state-db";
|
|
9
|
-
export type { SeedAction, Seeder, SeederResult, SeedItem } from "./types";
|
|
9
|
+
export type { SeedAction, Seeder, SeederResult, SeederRunOptions, SeedItem } from "./types";
|
package/src/be/seed/registry.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
import { scriptsSeeder } from "../seed-scripts";
|
|
10
10
|
import { skillsSeeder } from "../seed-skills";
|
|
11
11
|
import { runSeeders } from "./runner";
|
|
12
|
-
import type { Seeder, SeederResult } from "./types";
|
|
12
|
+
import type { Seeder, SeederResult, SeederRunOptions } from "./types";
|
|
13
13
|
|
|
14
14
|
export const SEEDERS: Seeder[] = [scriptsSeeder, skillsSeeder];
|
|
15
15
|
|
|
16
16
|
/** Apply every registered seeder. Called at API boot and by the seed CLI. */
|
|
17
|
-
export function runAllSeeders(opts?:
|
|
17
|
+
export function runAllSeeders(opts?: SeederRunOptions): Promise<SeederResult[]> {
|
|
18
18
|
return runSeeders(SEEDERS, opts);
|
|
19
19
|
}
|
package/src/be/seed/runner.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getSeedState, recordSeedState } from "./state-db";
|
|
8
|
-
import type { Seeder, SeederResult } from "./types";
|
|
8
|
+
import type { Seeder, SeederResult, SeederRunOptions } from "./types";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Apply one seeder. Idempotent and version-aware:
|
|
@@ -14,7 +14,7 @@ import type { Seeder, SeederResult } from "./types";
|
|
|
14
14
|
* - upstream pristine, src same -> no-op
|
|
15
15
|
* - upstream user-modified -> preserve (never overwrite)
|
|
16
16
|
*/
|
|
17
|
-
export async function runSeeder(seeder: Seeder, opts?:
|
|
17
|
+
export async function runSeeder(seeder: Seeder, opts?: SeederRunOptions): Promise<SeederResult> {
|
|
18
18
|
const result: SeederResult = {
|
|
19
19
|
kind: seeder.kind,
|
|
20
20
|
created: 0,
|
|
@@ -31,7 +31,7 @@ export async function runSeeder(seeder: Seeder, opts?: { quiet?: boolean }): Pro
|
|
|
31
31
|
|
|
32
32
|
// Absent upstream -> create.
|
|
33
33
|
if (upstream === null) {
|
|
34
|
-
await seeder.apply(item, "create");
|
|
34
|
+
await seeder.apply(item, "create", opts);
|
|
35
35
|
recordSeedState(seeder.kind, item.key, item.contentHash);
|
|
36
36
|
result.created += 1;
|
|
37
37
|
continue;
|
|
@@ -60,7 +60,7 @@ export async function runSeeder(seeder: Seeder, opts?: { quiet?: boolean }): Pro
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Pristine upstream + changed source -> update to the new source version.
|
|
63
|
-
await seeder.apply(item, "update");
|
|
63
|
+
await seeder.apply(item, "update", opts);
|
|
64
64
|
recordSeedState(seeder.kind, item.key, item.contentHash);
|
|
65
65
|
result.updated += 1;
|
|
66
66
|
} catch (err) {
|
|
@@ -88,7 +88,7 @@ export async function runSeeder(seeder: Seeder, opts?: { quiet?: boolean }): Pro
|
|
|
88
88
|
/** Apply a list of seeders in order. */
|
|
89
89
|
export async function runSeeders(
|
|
90
90
|
seeders: Seeder[],
|
|
91
|
-
opts?:
|
|
91
|
+
opts?: SeederRunOptions,
|
|
92
92
|
): Promise<SeederResult[]> {
|
|
93
93
|
const results: SeederResult[] = [];
|
|
94
94
|
for (const seeder of seeders) {
|
package/src/be/seed/types.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface SeedItem {
|
|
|
35
35
|
readonly contentHash: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export type SeederRunOptions = {
|
|
39
|
+
quiet?: boolean;
|
|
40
|
+
scriptEmbeddingMode?: "sync" | "skip";
|
|
41
|
+
};
|
|
42
|
+
|
|
38
43
|
export interface Seeder<TItem extends SeedItem = SeedItem> {
|
|
39
44
|
/** Kind discriminator — namespaces this seeder's rows in `seed_state`. */
|
|
40
45
|
readonly kind: string;
|
|
@@ -46,7 +51,7 @@ export interface Seeder<TItem extends SeedItem = SeedItem> {
|
|
|
46
51
|
*/
|
|
47
52
|
upstreamHash(item: TItem): string | null | Promise<string | null>;
|
|
48
53
|
/** Create or update the upstream entity so it matches the source definition. */
|
|
49
|
-
apply(item: TItem, action: "create" | "update"): void | Promise<void>;
|
|
54
|
+
apply(item: TItem, action: "create" | "update", opts?: SeederRunOptions): void | Promise<void>;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
export type SeederResult = {
|
package/src/be/seed-pricing.ts
CHANGED
|
@@ -67,6 +67,8 @@ const MANUAL_PRICING_OVERRIDES: Array<{
|
|
|
67
67
|
* fields the models.dev snapshot doesn't index directly; we map them here.
|
|
68
68
|
*/
|
|
69
69
|
const ANTHROPIC_SHORTNAME_TO_MODELSDEV: Record<string, string> = {
|
|
70
|
+
fable: "claude-fable-5",
|
|
71
|
+
mythos: "claude-mythos-5",
|
|
70
72
|
opus: "claude-opus-4-8",
|
|
71
73
|
sonnet: "claude-sonnet-4-6",
|
|
72
74
|
haiku: "claude-haiku-4-5",
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const argsSchema = z.object({
|
|
4
|
+
nowIso: z.string().optional().describe("Triage clock override (default: current time)"),
|
|
5
|
+
failureLookbackMinutes: z
|
|
6
|
+
.number()
|
|
7
|
+
.int()
|
|
8
|
+
.positive()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("Look back this many minutes for real failures (default 60)"),
|
|
11
|
+
stuckMinutes: z
|
|
12
|
+
.number()
|
|
13
|
+
.int()
|
|
14
|
+
.positive()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Flag in-progress tasks older than this on offline agents (default 5)"),
|
|
17
|
+
deployWindowMinutes: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.positive()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Window around now for merged agent-swarm PRs (default 15)"),
|
|
23
|
+
repo: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Optional GitHub repository in 'owner/name' form for restart/deploy PR detection"),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const BENIGN_FAILURE_RE = /^(superseded_workflow_task|cancelled|reboot-sweep)$/i;
|
|
30
|
+
|
|
31
|
+
function rowsToObjects(res: any): any[] {
|
|
32
|
+
const p = res?.data ?? res;
|
|
33
|
+
const cols: string[] = p?.columns ?? [];
|
|
34
|
+
return (p?.rows ?? []).map((r: any) =>
|
|
35
|
+
Array.isArray(r) ? Object.fromEntries(cols.map((c, i) => [c, r[i]])) : r,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function query(ctx: any, sql: string, params?: unknown[]): Promise<any[]> {
|
|
40
|
+
try {
|
|
41
|
+
return rowsToObjects(await ctx.swarm.db_query({ sql, params }));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return [{ unavailable: error instanceof Error ? error.message : String(error) }];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function taskPreview(task: unknown): string {
|
|
48
|
+
return String(task || "").replace(/\s+/g, " ").trim().slice(0, 180);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function summarizeTask(row: any): any {
|
|
52
|
+
return {
|
|
53
|
+
id: row.id,
|
|
54
|
+
status: row.status,
|
|
55
|
+
taskType: row.taskType || null,
|
|
56
|
+
agentId: row.agentId || null,
|
|
57
|
+
agentName: row.agentName || null,
|
|
58
|
+
scheduleId: row.scheduleId || null,
|
|
59
|
+
parentTaskId: row.parentTaskId || null,
|
|
60
|
+
failureReason: row.failureReason || null,
|
|
61
|
+
createdAt: row.createdAt,
|
|
62
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
63
|
+
taskPreview: taskPreview(row.task),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function recentMergedPrs(
|
|
68
|
+
ctx: any,
|
|
69
|
+
repo: string | undefined,
|
|
70
|
+
nowMs: number,
|
|
71
|
+
windowMinutes: number,
|
|
72
|
+
): Promise<any> {
|
|
73
|
+
if (!repo) return { skipped: "repo not provided" };
|
|
74
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) return { error: "repo must be in 'owner/name' form" };
|
|
75
|
+
|
|
76
|
+
const windowMs = windowMinutes * 60 * 1000;
|
|
77
|
+
try {
|
|
78
|
+
const response = await ctx.stdlib.fetch(
|
|
79
|
+
"https://api.github.com/repos/" +
|
|
80
|
+
repo +
|
|
81
|
+
"/pulls?state=closed&sort=updated&direction=desc&per_page=20",
|
|
82
|
+
{
|
|
83
|
+
headers: {
|
|
84
|
+
Accept: "application/vnd.github+json",
|
|
85
|
+
"User-Agent": "agent-swarm-boot-triage",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
if (!response.ok) return { error: `GitHub API ${response.status}` };
|
|
90
|
+
const prs = (await response.json()) as any[];
|
|
91
|
+
return prs
|
|
92
|
+
.filter((pr) => pr?.merged_at)
|
|
93
|
+
.map((pr) => {
|
|
94
|
+
const mergedAtMs = Date.parse(pr.merged_at);
|
|
95
|
+
return {
|
|
96
|
+
repo,
|
|
97
|
+
number: pr.number,
|
|
98
|
+
title: pr.title,
|
|
99
|
+
url: pr.html_url,
|
|
100
|
+
mergedAt: pr.merged_at,
|
|
101
|
+
minutesFromRestart: Math.round((mergedAtMs - nowMs) / 60000),
|
|
102
|
+
};
|
|
103
|
+
})
|
|
104
|
+
.filter((pr) => Math.abs(pr.minutesFromRestart * 60000) <= windowMs);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Read-only post-restart triage snapshot for the heartbeat.boot-triage prompt. */
|
|
111
|
+
export default async function bootTriage(args: any, ctx: any) {
|
|
112
|
+
const parsed = argsSchema.safeParse(args || {});
|
|
113
|
+
if (!parsed.success) return { error: "invalid args: " + parsed.error.message };
|
|
114
|
+
|
|
115
|
+
const now = parsed.data.nowIso ? new Date(parsed.data.nowIso) : new Date();
|
|
116
|
+
const nowMs = now.getTime();
|
|
117
|
+
const failureLookbackMinutes = parsed.data.failureLookbackMinutes || 60;
|
|
118
|
+
const stuckMinutes = parsed.data.stuckMinutes || 5;
|
|
119
|
+
const deployWindowMinutes = parsed.data.deployWindowMinutes || 15;
|
|
120
|
+
const repo = parsed.data.repo;
|
|
121
|
+
|
|
122
|
+
const mergedPrs = await recentMergedPrs(ctx, repo, nowMs, deployWindowMinutes);
|
|
123
|
+
|
|
124
|
+
const recentFailureRows = await query(
|
|
125
|
+
ctx,
|
|
126
|
+
`SELECT t.id, t.task, t.status, t.taskType, t.agentId, a.name as agentName,
|
|
127
|
+
t.scheduleId, t.parentTaskId, t.failureReason, t.createdAt, t.lastUpdatedAt
|
|
128
|
+
FROM agent_tasks t
|
|
129
|
+
LEFT JOIN agents a ON a.id = t.agentId
|
|
130
|
+
WHERE t.status = 'failed'
|
|
131
|
+
AND datetime(t.lastUpdatedAt) >= datetime(?, ?)
|
|
132
|
+
ORDER BY datetime(t.lastUpdatedAt) DESC
|
|
133
|
+
LIMIT 50`,
|
|
134
|
+
[now.toISOString(), `-${failureLookbackMinutes} minutes`],
|
|
135
|
+
);
|
|
136
|
+
const recentlyFailedTasks = recentFailureRows
|
|
137
|
+
.filter((row) => !row.unavailable)
|
|
138
|
+
.filter((row) => !BENIGN_FAILURE_RE.test(String(row.failureReason || "")))
|
|
139
|
+
.map(summarizeTask);
|
|
140
|
+
|
|
141
|
+
const stuckOfflineRows = await query(
|
|
142
|
+
ctx,
|
|
143
|
+
`SELECT t.id, t.task, t.status, t.taskType, t.agentId, a.name as agentName,
|
|
144
|
+
t.scheduleId, t.parentTaskId, t.failureReason, t.createdAt, t.lastUpdatedAt
|
|
145
|
+
FROM agent_tasks t
|
|
146
|
+
JOIN agents a ON a.id = t.agentId
|
|
147
|
+
WHERE t.status = 'in_progress'
|
|
148
|
+
AND a.status = 'offline'
|
|
149
|
+
AND datetime(t.lastUpdatedAt) <= datetime(?, ?)
|
|
150
|
+
ORDER BY datetime(t.lastUpdatedAt) ASC
|
|
151
|
+
LIMIT 50`,
|
|
152
|
+
[now.toISOString(), `-${stuckMinutes} minutes`],
|
|
153
|
+
);
|
|
154
|
+
const stuckInProgressOnOfflineAgents = stuckOfflineRows
|
|
155
|
+
.filter((row) => !row.unavailable)
|
|
156
|
+
.map(summarizeTask);
|
|
157
|
+
|
|
158
|
+
const orphanRows = await query(
|
|
159
|
+
ctx,
|
|
160
|
+
`SELECT t.id, t.task, t.status, t.taskType, t.agentId, a.name as agentName,
|
|
161
|
+
t.scheduleId, t.parentTaskId, t.failureReason, t.createdAt, t.lastUpdatedAt
|
|
162
|
+
FROM agent_tasks t
|
|
163
|
+
JOIN agents a ON a.id = t.agentId
|
|
164
|
+
WHERE t.status IN ('pending', 'offered')
|
|
165
|
+
AND a.status = 'offline'
|
|
166
|
+
ORDER BY datetime(t.lastUpdatedAt) ASC
|
|
167
|
+
LIMIT 50`,
|
|
168
|
+
);
|
|
169
|
+
const orphanedPendingOrOfferedOnOfflineWorkers = orphanRows
|
|
170
|
+
.filter((row) => !row.unavailable)
|
|
171
|
+
.map(summarizeTask);
|
|
172
|
+
|
|
173
|
+
const supersededRows = await query(
|
|
174
|
+
ctx,
|
|
175
|
+
`SELECT p.id, p.task, p.status, p.taskType, p.agentId, a.name as agentName,
|
|
176
|
+
p.scheduleId, p.parentTaskId, p.failureReason, p.createdAt, p.lastUpdatedAt
|
|
177
|
+
FROM agent_tasks p
|
|
178
|
+
LEFT JOIN agents a ON a.id = p.agentId
|
|
179
|
+
WHERE p.status = 'superseded'
|
|
180
|
+
AND datetime(p.lastUpdatedAt) >= datetime(?, ?)
|
|
181
|
+
AND NOT EXISTS (
|
|
182
|
+
SELECT 1
|
|
183
|
+
FROM agent_tasks c
|
|
184
|
+
WHERE c.parentTaskId = p.id
|
|
185
|
+
AND c.taskType = 'resume'
|
|
186
|
+
AND c.status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
187
|
+
)
|
|
188
|
+
ORDER BY datetime(p.lastUpdatedAt) DESC
|
|
189
|
+
LIMIT 50`,
|
|
190
|
+
[now.toISOString(), `-${failureLookbackMinutes} minutes`],
|
|
191
|
+
);
|
|
192
|
+
const supersededTasksMissingResumeChild = supersededRows
|
|
193
|
+
.filter((row) => !row.unavailable)
|
|
194
|
+
.map(summarizeTask);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
generatedAt: now.toISOString(),
|
|
198
|
+
windows: {
|
|
199
|
+
failureLookbackMinutes,
|
|
200
|
+
stuckMinutes,
|
|
201
|
+
deployWindowMinutes,
|
|
202
|
+
},
|
|
203
|
+
deployRestartDetection: {
|
|
204
|
+
source: repo ? "github:" + repo : null,
|
|
205
|
+
mergedPrsWithinWindow: Array.isArray(mergedPrs) ? mergedPrs : [],
|
|
206
|
+
skipped: Array.isArray(mergedPrs) ? null : mergedPrs.skipped || null,
|
|
207
|
+
error: Array.isArray(mergedPrs) ? null : mergedPrs.error,
|
|
208
|
+
},
|
|
209
|
+
recentlyFailedTasks,
|
|
210
|
+
stuckInProgressOnOfflineAgents,
|
|
211
|
+
orphanedPendingOrOfferedOnOfflineWorkers,
|
|
212
|
+
supersededTasksMissingResumeChild,
|
|
213
|
+
summary: {
|
|
214
|
+
mergedPrsWithinWindow: Array.isArray(mergedPrs) ? mergedPrs.length : 0,
|
|
215
|
+
recentlyFailedTasks: recentlyFailedTasks.length,
|
|
216
|
+
stuckInProgressOnOfflineAgents: stuckInProgressOnOfflineAgents.length,
|
|
217
|
+
orphanedPendingOrOfferedOnOfflineWorkers: orphanedPendingOrOfferedOnOfflineWorkers.length,
|
|
218
|
+
supersededTasksMissingResumeChild: supersededTasksMissingResumeChild.length,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|