opencode-swarm-plugin 0.20.0 → 0.22.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 (41) hide show
  1. package/.beads/issues.jsonl +213 -0
  2. package/INTEGRATION_EXAMPLE.md +66 -0
  3. package/README.md +352 -522
  4. package/dist/index.js +2046 -984
  5. package/dist/plugin.js +2051 -1017
  6. package/docs/analysis/subagent-coordination-patterns.md +2 -0
  7. package/docs/semantic-memory-cli-syntax.md +123 -0
  8. package/docs/swarm-mail-architecture.md +1147 -0
  9. package/evals/README.md +116 -0
  10. package/evals/evalite.config.ts +15 -0
  11. package/evals/example.eval.ts +32 -0
  12. package/evals/fixtures/decomposition-cases.ts +105 -0
  13. package/evals/lib/data-loader.test.ts +288 -0
  14. package/evals/lib/data-loader.ts +111 -0
  15. package/evals/lib/llm.ts +115 -0
  16. package/evals/scorers/index.ts +200 -0
  17. package/evals/scorers/outcome-scorers.test.ts +27 -0
  18. package/evals/scorers/outcome-scorers.ts +349 -0
  19. package/evals/swarm-decomposition.eval.ts +112 -0
  20. package/package.json +8 -1
  21. package/scripts/cleanup-test-memories.ts +346 -0
  22. package/src/beads.ts +49 -0
  23. package/src/eval-capture.ts +487 -0
  24. package/src/index.ts +45 -3
  25. package/src/learning.integration.test.ts +19 -4
  26. package/src/output-guardrails.test.ts +438 -0
  27. package/src/output-guardrails.ts +381 -0
  28. package/src/schemas/index.ts +18 -0
  29. package/src/schemas/swarm-context.ts +115 -0
  30. package/src/storage.ts +117 -5
  31. package/src/streams/events.test.ts +296 -0
  32. package/src/streams/events.ts +93 -0
  33. package/src/streams/migrations.test.ts +24 -20
  34. package/src/streams/migrations.ts +51 -0
  35. package/src/streams/projections.ts +187 -0
  36. package/src/streams/store.ts +275 -0
  37. package/src/swarm-orchestrate.ts +771 -189
  38. package/src/swarm-prompts.ts +84 -12
  39. package/src/swarm.integration.test.ts +124 -0
  40. package/vitest.integration.config.ts +6 -0
  41. package/vitest.integration.setup.ts +48 -0
package/dist/plugin.js CHANGED
@@ -12675,7 +12675,7 @@ var init_zod = __esm(() => {
12675
12675
  init_external();
12676
12676
  });
12677
12677
 
12678
- // node_modules/.pnpm/@opencode-ai+plugin@1.0.134/node_modules/@opencode-ai/plugin/dist/tool.js
12678
+ // node_modules/.pnpm/@opencode-ai+plugin@1.0.152/node_modules/@opencode-ai/plugin/dist/tool.js
12679
12679
  function tool(input) {
12680
12680
  return input;
12681
12681
  }
@@ -12684,7 +12684,7 @@ var init_tool = __esm(() => {
12684
12684
  tool.schema = exports_external;
12685
12685
  });
12686
12686
 
12687
- // node_modules/.pnpm/@opencode-ai+plugin@1.0.134/node_modules/@opencode-ai/plugin/dist/index.js
12687
+ // node_modules/.pnpm/@opencode-ai+plugin@1.0.152/node_modules/@opencode-ai/plugin/dist/index.js
12688
12688
  var init_dist = __esm(() => {
12689
12689
  init_tool();
12690
12690
  });
@@ -12705,7 +12705,7 @@ function createEvent(type, data) {
12705
12705
  function isEventType(event, type) {
12706
12706
  return event.type === type;
12707
12707
  }
12708
- var BaseEventSchema, AgentRegisteredEventSchema, AgentActiveEventSchema, MessageSentEventSchema, MessageReadEventSchema, MessageAckedEventSchema, FileReservedEventSchema, FileReleasedEventSchema, TaskStartedEventSchema, TaskProgressEventSchema, TaskCompletedEventSchema, TaskBlockedEventSchema, AgentEventSchema;
12708
+ var BaseEventSchema, AgentRegisteredEventSchema, AgentActiveEventSchema, MessageSentEventSchema, MessageReadEventSchema, MessageAckedEventSchema, FileReservedEventSchema, FileReleasedEventSchema, TaskStartedEventSchema, TaskProgressEventSchema, TaskCompletedEventSchema, TaskBlockedEventSchema, DecompositionGeneratedEventSchema, SubtaskOutcomeEventSchema, HumanFeedbackEventSchema, SwarmCheckpointedEventSchema, SwarmRecoveredEventSchema, AgentEventSchema;
12709
12709
  var init_events = __esm(() => {
12710
12710
  init_zod();
12711
12711
  BaseEventSchema = exports_external.object({
@@ -12791,6 +12791,68 @@ var init_events = __esm(() => {
12791
12791
  bead_id: exports_external.string(),
12792
12792
  reason: exports_external.string()
12793
12793
  });
12794
+ DecompositionGeneratedEventSchema = BaseEventSchema.extend({
12795
+ type: exports_external.literal("decomposition_generated"),
12796
+ epic_id: exports_external.string(),
12797
+ task: exports_external.string(),
12798
+ context: exports_external.string().optional(),
12799
+ strategy: exports_external.enum(["file-based", "feature-based", "risk-based"]),
12800
+ epic_title: exports_external.string(),
12801
+ subtasks: exports_external.array(exports_external.object({
12802
+ title: exports_external.string(),
12803
+ files: exports_external.array(exports_external.string()),
12804
+ priority: exports_external.number().min(0).max(3).optional()
12805
+ })),
12806
+ recovery_context: exports_external.object({
12807
+ shared_context: exports_external.string().optional(),
12808
+ skills_to_load: exports_external.array(exports_external.string()).optional(),
12809
+ coordinator_notes: exports_external.string().optional()
12810
+ }).optional()
12811
+ });
12812
+ SubtaskOutcomeEventSchema = BaseEventSchema.extend({
12813
+ type: exports_external.literal("subtask_outcome"),
12814
+ epic_id: exports_external.string(),
12815
+ bead_id: exports_external.string(),
12816
+ planned_files: exports_external.array(exports_external.string()),
12817
+ actual_files: exports_external.array(exports_external.string()),
12818
+ duration_ms: exports_external.number().min(0),
12819
+ error_count: exports_external.number().min(0).default(0),
12820
+ retry_count: exports_external.number().min(0).default(0),
12821
+ success: exports_external.boolean()
12822
+ });
12823
+ HumanFeedbackEventSchema = BaseEventSchema.extend({
12824
+ type: exports_external.literal("human_feedback"),
12825
+ epic_id: exports_external.string(),
12826
+ accepted: exports_external.boolean(),
12827
+ modified: exports_external.boolean().default(false),
12828
+ notes: exports_external.string().optional()
12829
+ });
12830
+ SwarmCheckpointedEventSchema = BaseEventSchema.extend({
12831
+ type: exports_external.literal("swarm_checkpointed"),
12832
+ epic_id: exports_external.string(),
12833
+ bead_id: exports_external.string(),
12834
+ strategy: exports_external.enum(["file-based", "feature-based", "risk-based"]),
12835
+ files: exports_external.array(exports_external.string()),
12836
+ dependencies: exports_external.array(exports_external.string()),
12837
+ directives: exports_external.object({
12838
+ shared_context: exports_external.string().optional(),
12839
+ skills_to_load: exports_external.array(exports_external.string()).optional(),
12840
+ coordinator_notes: exports_external.string().optional()
12841
+ }),
12842
+ recovery: exports_external.object({
12843
+ last_checkpoint: exports_external.number(),
12844
+ files_modified: exports_external.array(exports_external.string()),
12845
+ progress_percent: exports_external.number().min(0).max(100),
12846
+ last_message: exports_external.string().optional(),
12847
+ error_context: exports_external.string().optional()
12848
+ })
12849
+ });
12850
+ SwarmRecoveredEventSchema = BaseEventSchema.extend({
12851
+ type: exports_external.literal("swarm_recovered"),
12852
+ epic_id: exports_external.string(),
12853
+ bead_id: exports_external.string(),
12854
+ recovered_from_checkpoint: exports_external.number()
12855
+ });
12794
12856
  AgentEventSchema = exports_external.discriminatedUnion("type", [
12795
12857
  AgentRegisteredEventSchema,
12796
12858
  AgentActiveEventSchema,
@@ -12802,609 +12864,206 @@ var init_events = __esm(() => {
12802
12864
  TaskStartedEventSchema,
12803
12865
  TaskProgressEventSchema,
12804
12866
  TaskCompletedEventSchema,
12805
- TaskBlockedEventSchema
12867
+ TaskBlockedEventSchema,
12868
+ DecompositionGeneratedEventSchema,
12869
+ SubtaskOutcomeEventSchema,
12870
+ HumanFeedbackEventSchema,
12871
+ SwarmCheckpointedEventSchema,
12872
+ SwarmRecoveredEventSchema
12806
12873
  ]);
12807
12874
  });
12808
12875
 
12809
- // src/streams/store.ts
12810
- function parseTimestamp(timestamp) {
12811
- const ts = parseInt(timestamp, 10);
12812
- if (Number.isNaN(ts)) {
12813
- throw new Error(`[SwarmMail] Invalid timestamp: ${timestamp}`);
12876
+ // node_modules/.pnpm/@isaacs+balanced-match@4.0.1/node_modules/@isaacs/balanced-match/dist/esm/index.js
12877
+ var balanced = (a, b, str2) => {
12878
+ const ma = a instanceof RegExp ? maybeMatch(a, str2) : a;
12879
+ const mb = b instanceof RegExp ? maybeMatch(b, str2) : b;
12880
+ const r = ma !== null && mb != null && range(ma, mb, str2);
12881
+ return r && {
12882
+ start: r[0],
12883
+ end: r[1],
12884
+ pre: str2.slice(0, r[0]),
12885
+ body: str2.slice(r[0] + ma.length, r[1]),
12886
+ post: str2.slice(r[1] + mb.length)
12887
+ };
12888
+ }, maybeMatch = (reg, str2) => {
12889
+ const m = str2.match(reg);
12890
+ return m ? m[0] : null;
12891
+ }, range = (a, b, str2) => {
12892
+ let begs, beg, left, right = undefined, result;
12893
+ let ai = str2.indexOf(a);
12894
+ let bi = str2.indexOf(b, ai + 1);
12895
+ let i = ai;
12896
+ if (ai >= 0 && bi > 0) {
12897
+ if (a === b) {
12898
+ return [ai, bi];
12899
+ }
12900
+ begs = [];
12901
+ left = str2.length;
12902
+ while (i >= 0 && !result) {
12903
+ if (i === ai) {
12904
+ begs.push(i);
12905
+ ai = str2.indexOf(a, i + 1);
12906
+ } else if (begs.length === 1) {
12907
+ const r = begs.pop();
12908
+ if (r !== undefined)
12909
+ result = [r, bi];
12910
+ } else {
12911
+ beg = begs.pop();
12912
+ if (beg !== undefined && beg < left) {
12913
+ left = beg;
12914
+ right = bi;
12915
+ }
12916
+ bi = str2.indexOf(b, i + 1);
12917
+ }
12918
+ i = ai < bi && ai >= 0 ? ai : bi;
12919
+ }
12920
+ if (begs.length && right !== undefined) {
12921
+ result = [left, right];
12922
+ }
12814
12923
  }
12815
- if (ts > Number.MAX_SAFE_INTEGER) {
12816
- console.warn(`[SwarmMail] Timestamp ${timestamp} exceeds MAX_SAFE_INTEGER (year 2286+), precision may be lost`);
12924
+ return result;
12925
+ };
12926
+
12927
+ // node_modules/.pnpm/@isaacs+brace-expansion@5.0.0/node_modules/@isaacs/brace-expansion/dist/esm/index.js
12928
+ function numeric(str2) {
12929
+ return !isNaN(str2) ? parseInt(str2, 10) : str2.charCodeAt(0);
12930
+ }
12931
+ function escapeBraces(str2) {
12932
+ return str2.replace(slashPattern, escSlash).replace(openPattern, escOpen).replace(closePattern, escClose).replace(commaPattern, escComma).replace(periodPattern, escPeriod);
12933
+ }
12934
+ function unescapeBraces(str2) {
12935
+ return str2.replace(escSlashPattern, "\\").replace(escOpenPattern, "{").replace(escClosePattern, "}").replace(escCommaPattern, ",").replace(escPeriodPattern, ".");
12936
+ }
12937
+ function parseCommaParts(str2) {
12938
+ if (!str2) {
12939
+ return [""];
12817
12940
  }
12818
- return ts;
12941
+ const parts = [];
12942
+ const m = balanced("{", "}", str2);
12943
+ if (!m) {
12944
+ return str2.split(",");
12945
+ }
12946
+ const { pre, body, post } = m;
12947
+ const p = pre.split(",");
12948
+ p[p.length - 1] += "{" + body + "}";
12949
+ const postParts = parseCommaParts(post);
12950
+ if (post.length) {
12951
+ p[p.length - 1] += postParts.shift();
12952
+ p.push.apply(p, postParts);
12953
+ }
12954
+ parts.push.apply(parts, p);
12955
+ return parts;
12819
12956
  }
12820
- async function appendEvent(event, projectPath) {
12821
- const db = await getDatabase(projectPath);
12822
- const { type, project_key, timestamp, ...rest } = event;
12823
- console.log("[SwarmMail] Appending event", {
12824
- type,
12825
- projectKey: project_key,
12826
- timestamp
12827
- });
12828
- const result = await db.query(`INSERT INTO events (type, project_key, timestamp, data)
12829
- VALUES ($1, $2, $3, $4)
12830
- RETURNING id, sequence`, [type, project_key, timestamp, JSON.stringify(rest)]);
12831
- const row = result.rows[0];
12832
- if (!row) {
12833
- throw new Error("Failed to insert event - no row returned");
12957
+ function expand(str2) {
12958
+ if (!str2) {
12959
+ return [];
12834
12960
  }
12835
- const { id, sequence } = row;
12836
- console.log("[SwarmMail] Event appended", {
12837
- type,
12838
- id,
12839
- sequence,
12840
- projectKey: project_key
12841
- });
12842
- console.debug("[SwarmMail] Updating materialized views", { type, id });
12843
- await updateMaterializedViews(db, { ...event, id, sequence });
12844
- return { ...event, id, sequence };
12961
+ if (str2.slice(0, 2) === "{}") {
12962
+ str2 = "\\{\\}" + str2.slice(2);
12963
+ }
12964
+ return expand_(escapeBraces(str2), true).map(unescapeBraces);
12845
12965
  }
12846
- async function appendEvents(events, projectPath) {
12847
- return withTiming("appendEvents", async () => {
12848
- const db = await getDatabase(projectPath);
12849
- const results = [];
12850
- await db.exec("BEGIN");
12851
- try {
12852
- for (const event of events) {
12853
- const { type, project_key, timestamp, ...rest } = event;
12854
- const result = await db.query(`INSERT INTO events (type, project_key, timestamp, data)
12855
- VALUES ($1, $2, $3, $4)
12856
- RETURNING id, sequence`, [type, project_key, timestamp, JSON.stringify(rest)]);
12857
- const row = result.rows[0];
12858
- if (!row) {
12859
- throw new Error("Failed to insert event - no row returned");
12966
+ function embrace(str2) {
12967
+ return "{" + str2 + "}";
12968
+ }
12969
+ function isPadded(el) {
12970
+ return /^-?0\d/.test(el);
12971
+ }
12972
+ function lte(i, y) {
12973
+ return i <= y;
12974
+ }
12975
+ function gte(i, y) {
12976
+ return i >= y;
12977
+ }
12978
+ function expand_(str2, isTop) {
12979
+ const expansions = [];
12980
+ const m = balanced("{", "}", str2);
12981
+ if (!m)
12982
+ return [str2];
12983
+ const pre = m.pre;
12984
+ const post = m.post.length ? expand_(m.post, false) : [""];
12985
+ if (/\$$/.test(m.pre)) {
12986
+ for (let k = 0;k < post.length; k++) {
12987
+ const expansion = pre + "{" + m.body + "}" + post[k];
12988
+ expansions.push(expansion);
12989
+ }
12990
+ } else {
12991
+ const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
12992
+ const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
12993
+ const isSequence = isNumericSequence || isAlphaSequence;
12994
+ const isOptions = m.body.indexOf(",") >= 0;
12995
+ if (!isSequence && !isOptions) {
12996
+ if (m.post.match(/,(?!,).*\}/)) {
12997
+ str2 = m.pre + "{" + m.body + escClose + m.post;
12998
+ return expand_(str2);
12999
+ }
13000
+ return [str2];
13001
+ }
13002
+ let n;
13003
+ if (isSequence) {
13004
+ n = m.body.split(/\.\./);
13005
+ } else {
13006
+ n = parseCommaParts(m.body);
13007
+ if (n.length === 1 && n[0] !== undefined) {
13008
+ n = expand_(n[0], false).map(embrace);
13009
+ if (n.length === 1) {
13010
+ return post.map((p) => m.pre + n[0] + p);
12860
13011
  }
12861
- const { id, sequence } = row;
12862
- const enrichedEvent = { ...event, id, sequence };
12863
- await updateMaterializedViews(db, enrichedEvent);
12864
- results.push(enrichedEvent);
12865
13012
  }
12866
- await db.exec("COMMIT");
12867
- } catch (e) {
12868
- let rollbackError = null;
12869
- try {
12870
- await db.exec("ROLLBACK");
12871
- } catch (rbErr) {
12872
- rollbackError = rbErr;
12873
- console.error("[SwarmMail] ROLLBACK failed:", rbErr);
13013
+ }
13014
+ let N;
13015
+ if (isSequence && n[0] !== undefined && n[1] !== undefined) {
13016
+ const x = numeric(n[0]);
13017
+ const y = numeric(n[1]);
13018
+ const width = Math.max(n[0].length, n[1].length);
13019
+ let incr = n.length === 3 && n[2] !== undefined ? Math.abs(numeric(n[2])) : 1;
13020
+ let test = lte;
13021
+ const reverse = y < x;
13022
+ if (reverse) {
13023
+ incr *= -1;
13024
+ test = gte;
12874
13025
  }
12875
- if (rollbackError) {
12876
- const compositeError = new Error(`Transaction failed: ${e instanceof Error ? e.message : String(e)}. ` + `ROLLBACK also failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}. ` + `Database may be in inconsistent state.`);
12877
- compositeError.originalError = e;
12878
- compositeError.rollbackError = rollbackError;
12879
- throw compositeError;
13026
+ const pad = n.some(isPadded);
13027
+ N = [];
13028
+ for (let i = x;test(i, y); i += incr) {
13029
+ let c;
13030
+ if (isAlphaSequence) {
13031
+ c = String.fromCharCode(i);
13032
+ if (c === "\\") {
13033
+ c = "";
13034
+ }
13035
+ } else {
13036
+ c = String(i);
13037
+ if (pad) {
13038
+ const need = width - c.length;
13039
+ if (need > 0) {
13040
+ const z = new Array(need + 1).join("0");
13041
+ if (i < 0) {
13042
+ c = "-" + z + c.slice(1);
13043
+ } else {
13044
+ c = z + c;
13045
+ }
13046
+ }
13047
+ }
13048
+ }
13049
+ N.push(c);
13050
+ }
13051
+ } else {
13052
+ N = [];
13053
+ for (let j = 0;j < n.length; j++) {
13054
+ N.push.apply(N, expand_(n[j], false));
12880
13055
  }
12881
- throw e;
12882
13056
  }
12883
- return results;
12884
- });
12885
- }
12886
- async function readEvents(options2 = {}, projectPath) {
12887
- return withTiming("readEvents", async () => {
12888
- const db = await getDatabase(projectPath);
12889
- const conditions = [];
12890
- const params = [];
12891
- let paramIndex = 1;
12892
- if (options2.projectKey) {
12893
- conditions.push(`project_key = $${paramIndex++}`);
12894
- params.push(options2.projectKey);
12895
- }
12896
- if (options2.types && options2.types.length > 0) {
12897
- conditions.push(`type = ANY($${paramIndex++})`);
12898
- params.push(options2.types);
12899
- }
12900
- if (options2.since !== undefined) {
12901
- conditions.push(`timestamp >= $${paramIndex++}`);
12902
- params.push(options2.since);
12903
- }
12904
- if (options2.until !== undefined) {
12905
- conditions.push(`timestamp <= $${paramIndex++}`);
12906
- params.push(options2.until);
12907
- }
12908
- if (options2.afterSequence !== undefined) {
12909
- conditions.push(`sequence > $${paramIndex++}`);
12910
- params.push(options2.afterSequence);
12911
- }
12912
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
12913
- let query = `
12914
- SELECT id, type, project_key, timestamp, sequence, data
12915
- FROM events
12916
- ${whereClause}
12917
- ORDER BY sequence ASC
12918
- `;
12919
- if (options2.limit) {
12920
- query += ` LIMIT $${paramIndex++}`;
12921
- params.push(options2.limit);
12922
- }
12923
- if (options2.offset) {
12924
- query += ` OFFSET $${paramIndex++}`;
12925
- params.push(options2.offset);
12926
- }
12927
- const result = await db.query(query, params);
12928
- return result.rows.map((row) => {
12929
- const data = typeof row.data === "string" ? JSON.parse(row.data) : row.data;
12930
- return {
12931
- id: row.id,
12932
- type: row.type,
12933
- project_key: row.project_key,
12934
- timestamp: parseTimestamp(row.timestamp),
12935
- sequence: row.sequence,
12936
- ...data
12937
- };
12938
- });
12939
- });
12940
- }
12941
- async function getLatestSequence(projectKey, projectPath) {
12942
- const db = await getDatabase(projectPath);
12943
- const query = projectKey ? "SELECT MAX(sequence) as seq FROM events WHERE project_key = $1" : "SELECT MAX(sequence) as seq FROM events";
12944
- const params = projectKey ? [projectKey] : [];
12945
- const result = await db.query(query, params);
12946
- return result.rows[0]?.seq ?? 0;
12947
- }
12948
- async function replayEvents(options2 = {}, projectPath) {
12949
- return withTiming("replayEvents", async () => {
12950
- const startTime = Date.now();
12951
- const db = await getDatabase(projectPath);
12952
- if (options2.clearViews) {
12953
- if (options2.projectKey) {
12954
- await db.query(`DELETE FROM message_recipients WHERE message_id IN (
12955
- SELECT id FROM messages WHERE project_key = $1
12956
- )`, [options2.projectKey]);
12957
- await db.query(`DELETE FROM messages WHERE project_key = $1`, [
12958
- options2.projectKey
12959
- ]);
12960
- await db.query(`DELETE FROM reservations WHERE project_key = $1`, [
12961
- options2.projectKey
12962
- ]);
12963
- await db.query(`DELETE FROM agents WHERE project_key = $1`, [
12964
- options2.projectKey
12965
- ]);
12966
- } else {
12967
- await db.exec(`
12968
- DELETE FROM message_recipients;
12969
- DELETE FROM messages;
12970
- DELETE FROM reservations;
12971
- DELETE FROM agents;
12972
- `);
12973
- }
12974
- }
12975
- const events = await readEvents({
12976
- projectKey: options2.projectKey,
12977
- afterSequence: options2.fromSequence
12978
- }, projectPath);
12979
- for (const event of events) {
12980
- await updateMaterializedViews(db, event);
12981
- }
12982
- return {
12983
- eventsReplayed: events.length,
12984
- duration: Date.now() - startTime
12985
- };
12986
- });
12987
- }
12988
- async function replayEventsBatched(projectKey, onBatch, options2 = {}, projectPath) {
12989
- return withTiming("replayEventsBatched", async () => {
12990
- const startTime = Date.now();
12991
- const batchSize = options2.batchSize ?? 1000;
12992
- const fromSequence = options2.fromSequence ?? 0;
12993
- const db = await getDatabase(projectPath);
12994
- if (options2.clearViews) {
12995
- await db.query(`DELETE FROM message_recipients WHERE message_id IN (
12996
- SELECT id FROM messages WHERE project_key = $1
12997
- )`, [projectKey]);
12998
- await db.query(`DELETE FROM messages WHERE project_key = $1`, [
12999
- projectKey
13000
- ]);
13001
- await db.query(`DELETE FROM reservations WHERE project_key = $1`, [
13002
- projectKey
13003
- ]);
13004
- await db.query(`DELETE FROM agents WHERE project_key = $1`, [projectKey]);
13005
- }
13006
- const countResult = await db.query(`SELECT COUNT(*) as count FROM events WHERE project_key = $1 AND sequence > $2`, [projectKey, fromSequence]);
13007
- const total = parseInt(countResult.rows[0]?.count ?? "0");
13008
- if (total === 0) {
13009
- return { eventsReplayed: 0, duration: Date.now() - startTime };
13010
- }
13011
- let processed = 0;
13012
- let offset = 0;
13013
- while (processed < total) {
13014
- const events = await readEvents({
13015
- projectKey,
13016
- afterSequence: fromSequence,
13017
- limit: batchSize,
13018
- offset
13019
- }, projectPath);
13020
- if (events.length === 0)
13021
- break;
13022
- for (const event of events) {
13023
- await updateMaterializedViews(db, event);
13024
- }
13025
- processed += events.length;
13026
- const percent = Math.round(processed / total * 100);
13027
- await onBatch(events, { processed, total, percent });
13028
- console.log(`[SwarmMail] Replaying events: ${processed}/${total} (${percent}%)`);
13029
- offset += batchSize;
13030
- }
13031
- return {
13032
- eventsReplayed: processed,
13033
- duration: Date.now() - startTime
13034
- };
13035
- });
13036
- }
13037
- async function updateMaterializedViews(db, event) {
13038
- try {
13039
- switch (event.type) {
13040
- case "agent_registered":
13041
- await handleAgentRegistered(db, event);
13042
- break;
13043
- case "agent_active":
13044
- await db.query(`UPDATE agents SET last_active_at = $1 WHERE project_key = $2 AND name = $3`, [event.timestamp, event.project_key, event.agent_name]);
13045
- break;
13046
- case "message_sent":
13047
- await handleMessageSent(db, event);
13048
- break;
13049
- case "message_read":
13050
- await db.query(`UPDATE message_recipients SET read_at = $1 WHERE message_id = $2 AND agent_name = $3`, [event.timestamp, event.message_id, event.agent_name]);
13051
- break;
13052
- case "message_acked":
13053
- await db.query(`UPDATE message_recipients SET acked_at = $1 WHERE message_id = $2 AND agent_name = $3`, [event.timestamp, event.message_id, event.agent_name]);
13054
- break;
13055
- case "file_reserved":
13056
- await handleFileReserved(db, event);
13057
- break;
13058
- case "file_released":
13059
- await handleFileReleased(db, event);
13060
- break;
13061
- case "task_started":
13062
- case "task_progress":
13063
- case "task_completed":
13064
- case "task_blocked":
13065
- break;
13066
- }
13067
- } catch (error45) {
13068
- console.error("[SwarmMail] Failed to update materialized views", {
13069
- eventType: event.type,
13070
- eventId: event.id,
13071
- error: error45
13072
- });
13073
- throw error45;
13074
- }
13075
- }
13076
- async function handleAgentRegistered(db, event) {
13077
- await db.query(`INSERT INTO agents (project_key, name, program, model, task_description, registered_at, last_active_at)
13078
- VALUES ($1, $2, $3, $4, $5, $6, $6)
13079
- ON CONFLICT (project_key, name) DO UPDATE SET
13080
- program = EXCLUDED.program,
13081
- model = EXCLUDED.model,
13082
- task_description = EXCLUDED.task_description,
13083
- last_active_at = EXCLUDED.last_active_at`, [
13084
- event.project_key,
13085
- event.agent_name,
13086
- event.program,
13087
- event.model,
13088
- event.task_description || null,
13089
- event.timestamp
13090
- ]);
13091
- }
13092
- async function handleMessageSent(db, event) {
13093
- console.log("[SwarmMail] Handling message sent event", {
13094
- from: event.from_agent,
13095
- to: event.to_agents,
13096
- subject: event.subject,
13097
- projectKey: event.project_key
13098
- });
13099
- const result = await db.query(`INSERT INTO messages (project_key, from_agent, subject, body, thread_id, importance, ack_required, created_at)
13100
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
13101
- RETURNING id`, [
13102
- event.project_key,
13103
- event.from_agent,
13104
- event.subject,
13105
- event.body,
13106
- event.thread_id || null,
13107
- event.importance,
13108
- event.ack_required,
13109
- event.timestamp
13110
- ]);
13111
- const msgRow = result.rows[0];
13112
- if (!msgRow) {
13113
- throw new Error("Failed to insert message - no row returned");
13114
- }
13115
- const messageId = msgRow.id;
13116
- if (event.to_agents.length > 0) {
13117
- const values = event.to_agents.map((_, i) => `($1, $${i + 2})`).join(", ");
13118
- const params = [messageId, ...event.to_agents];
13119
- await db.query(`INSERT INTO message_recipients (message_id, agent_name)
13120
- VALUES ${values}
13121
- ON CONFLICT DO NOTHING`, params);
13122
- console.log("[SwarmMail] Message recipients inserted", {
13123
- messageId,
13124
- recipientCount: event.to_agents.length
13125
- });
13126
- }
13127
- }
13128
- async function handleFileReserved(db, event) {
13129
- console.log("[SwarmMail] Handling file reservation event", {
13130
- agent: event.agent_name,
13131
- paths: event.paths,
13132
- exclusive: event.exclusive,
13133
- projectKey: event.project_key
13134
- });
13135
- if (event.paths.length > 0) {
13136
- const values = event.paths.map((_, i) => `($1, $2, $${i + 3}, $${event.paths.length + 3}, $${event.paths.length + 4}, $${event.paths.length + 5}, $${event.paths.length + 6})`).join(", ");
13137
- const params = [
13138
- event.project_key,
13139
- event.agent_name,
13140
- ...event.paths,
13141
- event.exclusive,
13142
- event.reason || null,
13143
- event.timestamp,
13144
- event.expires_at
13145
- ];
13146
- if (event.paths.length > 0) {
13147
- await db.query(`DELETE FROM reservations
13148
- WHERE project_key = $1
13149
- AND agent_name = $2
13150
- AND path_pattern = ANY($3)
13151
- AND released_at IS NULL`, [event.project_key, event.agent_name, event.paths]);
13152
- }
13153
- await db.query(`INSERT INTO reservations (project_key, agent_name, path_pattern, exclusive, reason, created_at, expires_at)
13154
- VALUES ${values}`, params);
13155
- console.log("[SwarmMail] File reservations inserted", {
13156
- agent: event.agent_name,
13157
- reservationCount: event.paths.length
13158
- });
13159
- }
13160
- }
13161
- async function handleFileReleased(db, event) {
13162
- if (event.type !== "file_released")
13163
- return;
13164
- if (event.reservation_ids && event.reservation_ids.length > 0) {
13165
- await db.query(`UPDATE reservations SET released_at = $1 WHERE id = ANY($2)`, [event.timestamp, event.reservation_ids]);
13166
- } else if (event.paths && event.paths.length > 0) {
13167
- await db.query(`UPDATE reservations SET released_at = $1
13168
- WHERE project_key = $2 AND agent_name = $3 AND path_pattern = ANY($4) AND released_at IS NULL`, [event.timestamp, event.project_key, event.agent_name, event.paths]);
13169
- } else {
13170
- await db.query(`UPDATE reservations SET released_at = $1
13171
- WHERE project_key = $2 AND agent_name = $3 AND released_at IS NULL`, [event.timestamp, event.project_key, event.agent_name]);
13172
- }
13173
- }
13174
- async function registerAgent(projectKey, agentName, options2 = {}, projectPath) {
13175
- const event = createEvent("agent_registered", {
13176
- project_key: projectKey,
13177
- agent_name: agentName,
13178
- program: options2.program || "opencode",
13179
- model: options2.model || "unknown",
13180
- task_description: options2.taskDescription
13181
- });
13182
- return appendEvent(event, projectPath);
13183
- }
13184
- async function sendMessage(projectKey, fromAgent, toAgents, subject, body, options2 = {}, projectPath) {
13185
- const event = createEvent("message_sent", {
13186
- project_key: projectKey,
13187
- from_agent: fromAgent,
13188
- to_agents: toAgents,
13189
- subject,
13190
- body,
13191
- thread_id: options2.threadId,
13192
- importance: options2.importance || "normal",
13193
- ack_required: options2.ackRequired || false
13194
- });
13195
- return appendEvent(event, projectPath);
13196
- }
13197
- async function reserveFiles(projectKey, agentName, paths, options2 = {}, projectPath) {
13198
- const ttlSeconds = options2.ttlSeconds || 3600;
13199
- const event = createEvent("file_reserved", {
13200
- project_key: projectKey,
13201
- agent_name: agentName,
13202
- paths,
13203
- reason: options2.reason,
13204
- exclusive: options2.exclusive ?? true,
13205
- ttl_seconds: ttlSeconds,
13206
- expires_at: Date.now() + ttlSeconds * 1000
13207
- });
13208
- return appendEvent(event, projectPath);
13209
- }
13210
- var TIMESTAMP_SAFE_UNTIL;
13211
- var init_store = __esm(() => {
13212
- init_streams();
13213
- init_events();
13214
- TIMESTAMP_SAFE_UNTIL = new Date("2286-01-01").getTime();
13215
- });
13216
-
13217
- // node_modules/.pnpm/@isaacs+balanced-match@4.0.1/node_modules/@isaacs/balanced-match/dist/esm/index.js
13218
- var balanced = (a, b, str2) => {
13219
- const ma = a instanceof RegExp ? maybeMatch(a, str2) : a;
13220
- const mb = b instanceof RegExp ? maybeMatch(b, str2) : b;
13221
- const r = ma !== null && mb != null && range(ma, mb, str2);
13222
- return r && {
13223
- start: r[0],
13224
- end: r[1],
13225
- pre: str2.slice(0, r[0]),
13226
- body: str2.slice(r[0] + ma.length, r[1]),
13227
- post: str2.slice(r[1] + mb.length)
13228
- };
13229
- }, maybeMatch = (reg, str2) => {
13230
- const m = str2.match(reg);
13231
- return m ? m[0] : null;
13232
- }, range = (a, b, str2) => {
13233
- let begs, beg, left, right = undefined, result;
13234
- let ai = str2.indexOf(a);
13235
- let bi = str2.indexOf(b, ai + 1);
13236
- let i = ai;
13237
- if (ai >= 0 && bi > 0) {
13238
- if (a === b) {
13239
- return [ai, bi];
13240
- }
13241
- begs = [];
13242
- left = str2.length;
13243
- while (i >= 0 && !result) {
13244
- if (i === ai) {
13245
- begs.push(i);
13246
- ai = str2.indexOf(a, i + 1);
13247
- } else if (begs.length === 1) {
13248
- const r = begs.pop();
13249
- if (r !== undefined)
13250
- result = [r, bi];
13251
- } else {
13252
- beg = begs.pop();
13253
- if (beg !== undefined && beg < left) {
13254
- left = beg;
13255
- right = bi;
13256
- }
13257
- bi = str2.indexOf(b, i + 1);
13258
- }
13259
- i = ai < bi && ai >= 0 ? ai : bi;
13260
- }
13261
- if (begs.length && right !== undefined) {
13262
- result = [left, right];
13263
- }
13264
- }
13265
- return result;
13266
- };
13267
-
13268
- // node_modules/.pnpm/@isaacs+brace-expansion@5.0.0/node_modules/@isaacs/brace-expansion/dist/esm/index.js
13269
- function numeric(str2) {
13270
- return !isNaN(str2) ? parseInt(str2, 10) : str2.charCodeAt(0);
13271
- }
13272
- function escapeBraces(str2) {
13273
- return str2.replace(slashPattern, escSlash).replace(openPattern, escOpen).replace(closePattern, escClose).replace(commaPattern, escComma).replace(periodPattern, escPeriod);
13274
- }
13275
- function unescapeBraces(str2) {
13276
- return str2.replace(escSlashPattern, "\\").replace(escOpenPattern, "{").replace(escClosePattern, "}").replace(escCommaPattern, ",").replace(escPeriodPattern, ".");
13277
- }
13278
- function parseCommaParts(str2) {
13279
- if (!str2) {
13280
- return [""];
13281
- }
13282
- const parts = [];
13283
- const m = balanced("{", "}", str2);
13284
- if (!m) {
13285
- return str2.split(",");
13286
- }
13287
- const { pre, body, post } = m;
13288
- const p = pre.split(",");
13289
- p[p.length - 1] += "{" + body + "}";
13290
- const postParts = parseCommaParts(post);
13291
- if (post.length) {
13292
- p[p.length - 1] += postParts.shift();
13293
- p.push.apply(p, postParts);
13294
- }
13295
- parts.push.apply(parts, p);
13296
- return parts;
13297
- }
13298
- function expand(str2) {
13299
- if (!str2) {
13300
- return [];
13301
- }
13302
- if (str2.slice(0, 2) === "{}") {
13303
- str2 = "\\{\\}" + str2.slice(2);
13304
- }
13305
- return expand_(escapeBraces(str2), true).map(unescapeBraces);
13306
- }
13307
- function embrace(str2) {
13308
- return "{" + str2 + "}";
13309
- }
13310
- function isPadded(el) {
13311
- return /^-?0\d/.test(el);
13312
- }
13313
- function lte(i, y) {
13314
- return i <= y;
13315
- }
13316
- function gte(i, y) {
13317
- return i >= y;
13318
- }
13319
- function expand_(str2, isTop) {
13320
- const expansions = [];
13321
- const m = balanced("{", "}", str2);
13322
- if (!m)
13323
- return [str2];
13324
- const pre = m.pre;
13325
- const post = m.post.length ? expand_(m.post, false) : [""];
13326
- if (/\$$/.test(m.pre)) {
13327
- for (let k = 0;k < post.length; k++) {
13328
- const expansion = pre + "{" + m.body + "}" + post[k];
13329
- expansions.push(expansion);
13330
- }
13331
- } else {
13332
- const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
13333
- const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
13334
- const isSequence = isNumericSequence || isAlphaSequence;
13335
- const isOptions = m.body.indexOf(",") >= 0;
13336
- if (!isSequence && !isOptions) {
13337
- if (m.post.match(/,(?!,).*\}/)) {
13338
- str2 = m.pre + "{" + m.body + escClose + m.post;
13339
- return expand_(str2);
13340
- }
13341
- return [str2];
13342
- }
13343
- let n;
13344
- if (isSequence) {
13345
- n = m.body.split(/\.\./);
13346
- } else {
13347
- n = parseCommaParts(m.body);
13348
- if (n.length === 1 && n[0] !== undefined) {
13349
- n = expand_(n[0], false).map(embrace);
13350
- if (n.length === 1) {
13351
- return post.map((p) => m.pre + n[0] + p);
13352
- }
13353
- }
13354
- }
13355
- let N;
13356
- if (isSequence && n[0] !== undefined && n[1] !== undefined) {
13357
- const x = numeric(n[0]);
13358
- const y = numeric(n[1]);
13359
- const width = Math.max(n[0].length, n[1].length);
13360
- let incr = n.length === 3 && n[2] !== undefined ? Math.abs(numeric(n[2])) : 1;
13361
- let test = lte;
13362
- const reverse = y < x;
13363
- if (reverse) {
13364
- incr *= -1;
13365
- test = gte;
13366
- }
13367
- const pad = n.some(isPadded);
13368
- N = [];
13369
- for (let i = x;test(i, y); i += incr) {
13370
- let c;
13371
- if (isAlphaSequence) {
13372
- c = String.fromCharCode(i);
13373
- if (c === "\\") {
13374
- c = "";
13375
- }
13376
- } else {
13377
- c = String(i);
13378
- if (pad) {
13379
- const need = width - c.length;
13380
- if (need > 0) {
13381
- const z = new Array(need + 1).join("0");
13382
- if (i < 0) {
13383
- c = "-" + z + c.slice(1);
13384
- } else {
13385
- c = z + c;
13386
- }
13387
- }
13388
- }
13389
- }
13390
- N.push(c);
13391
- }
13392
- } else {
13393
- N = [];
13394
- for (let j = 0;j < n.length; j++) {
13395
- N.push.apply(N, expand_(n[j], false));
13396
- }
13397
- }
13398
- for (let j = 0;j < N.length; j++) {
13399
- for (let k = 0;k < post.length; k++) {
13400
- const expansion = pre + N[j] + post[k];
13401
- if (!isTop || isSequence || expansion) {
13402
- expansions.push(expansion);
13403
- }
13404
- }
13405
- }
13406
- }
13407
- return expansions;
13057
+ for (let j = 0;j < N.length; j++) {
13058
+ for (let k = 0;k < post.length; k++) {
13059
+ const expansion = pre + N[j] + post[k];
13060
+ if (!isTop || isSequence || expansion) {
13061
+ expansions.push(expansion);
13062
+ }
13063
+ }
13064
+ }
13065
+ }
13066
+ return expansions;
13408
13067
  }
13409
13068
  var escSlash, escOpen, escClose, escComma, escPeriod, escSlashPattern, escOpenPattern, escClosePattern, escCommaPattern, escPeriodPattern, slashPattern, openPattern, closePattern, commaPattern, periodPattern;
13410
13069
  var init_esm = __esm(() => {
@@ -14753,6 +14412,79 @@ function pathMatches(path2, pattern) {
14753
14412
  }
14754
14413
  return minimatch(path2, pattern);
14755
14414
  }
14415
+ async function getEvalRecords(projectKey, options2, projectPath) {
14416
+ const db = await getDatabase(projectPath);
14417
+ const conditions = ["project_key = $1"];
14418
+ const params = [projectKey];
14419
+ let paramIndex = 2;
14420
+ if (options2?.strategy) {
14421
+ conditions.push(`strategy = $${paramIndex++}`);
14422
+ params.push(options2.strategy);
14423
+ }
14424
+ const whereClause = conditions.join(" AND ");
14425
+ let query = `
14426
+ SELECT id, project_key, task, context, strategy, epic_title, subtasks,
14427
+ outcomes, overall_success, total_duration_ms, total_errors,
14428
+ human_accepted, human_modified, human_notes,
14429
+ file_overlap_count, scope_accuracy, time_balance_ratio,
14430
+ created_at, updated_at
14431
+ FROM eval_records
14432
+ WHERE ${whereClause}
14433
+ ORDER BY created_at DESC
14434
+ `;
14435
+ if (options2?.limit) {
14436
+ query += ` LIMIT $${paramIndex}`;
14437
+ params.push(options2.limit);
14438
+ }
14439
+ const result = await db.query(query, params);
14440
+ return result.rows.map((row) => ({
14441
+ id: row.id,
14442
+ project_key: row.project_key,
14443
+ task: row.task,
14444
+ context: row.context,
14445
+ strategy: row.strategy,
14446
+ epic_title: row.epic_title,
14447
+ subtasks: typeof row.subtasks === "string" ? JSON.parse(row.subtasks) : row.subtasks,
14448
+ outcomes: row.outcomes ? typeof row.outcomes === "string" ? JSON.parse(row.outcomes) : row.outcomes : undefined,
14449
+ overall_success: row.overall_success,
14450
+ total_duration_ms: row.total_duration_ms,
14451
+ total_errors: row.total_errors,
14452
+ human_accepted: row.human_accepted,
14453
+ human_modified: row.human_modified,
14454
+ human_notes: row.human_notes,
14455
+ file_overlap_count: row.file_overlap_count,
14456
+ scope_accuracy: row.scope_accuracy,
14457
+ time_balance_ratio: row.time_balance_ratio,
14458
+ created_at: parseInt(row.created_at),
14459
+ updated_at: parseInt(row.updated_at)
14460
+ }));
14461
+ }
14462
+ async function getEvalStats(projectKey, projectPath) {
14463
+ const db = await getDatabase(projectPath);
14464
+ const overallResult = await db.query(`SELECT
14465
+ COUNT(*) as total_records,
14466
+ COUNT(*) FILTER (WHERE overall_success = true) as success_count,
14467
+ AVG(total_duration_ms) as avg_duration
14468
+ FROM eval_records
14469
+ WHERE project_key = $1`, [projectKey]);
14470
+ const totalRecords = parseInt(overallResult.rows[0]?.total_records || "0");
14471
+ const successCount = parseInt(overallResult.rows[0]?.success_count || "0");
14472
+ const avgDurationMs = parseFloat(overallResult.rows[0]?.avg_duration || "0");
14473
+ const strategyResult = await db.query(`SELECT strategy, COUNT(*) as count
14474
+ FROM eval_records
14475
+ WHERE project_key = $1
14476
+ GROUP BY strategy`, [projectKey]);
14477
+ const byStrategy = {};
14478
+ for (const row of strategyResult.rows) {
14479
+ byStrategy[row.strategy] = parseInt(row.count);
14480
+ }
14481
+ return {
14482
+ totalRecords,
14483
+ successRate: totalRecords > 0 ? successCount / totalRecords : 0,
14484
+ avgDurationMs,
14485
+ byStrategy
14486
+ };
14487
+ }
14756
14488
  var init_projections = __esm(() => {
14757
14489
  init_streams();
14758
14490
  init_esm2();
@@ -15438,45 +15170,314 @@ async function getPendingMigrations(db) {
15438
15170
  const currentVersion = await getCurrentVersion(db);
15439
15171
  return migrations.filter((m) => m.version > currentVersion).sort((a, b) => a.version - b.version);
15440
15172
  }
15441
- var migrations;
15442
- var init_migrations = __esm(() => {
15443
- migrations = [
15444
- {
15445
- version: 1,
15446
- description: "Add cursors table for DurableCursor",
15447
- up: `
15448
- CREATE TABLE IF NOT EXISTS cursors (
15449
- id SERIAL PRIMARY KEY,
15450
- stream TEXT NOT NULL,
15451
- checkpoint TEXT NOT NULL,
15452
- position BIGINT NOT NULL DEFAULT 0,
15453
- updated_at BIGINT NOT NULL,
15454
- UNIQUE(stream, checkpoint)
15455
- );
15456
- CREATE INDEX IF NOT EXISTS idx_cursors_checkpoint ON cursors(checkpoint);
15457
- CREATE INDEX IF NOT EXISTS idx_cursors_stream ON cursors(stream);
15458
- `,
15459
- down: `DROP TABLE IF EXISTS cursors;`
15460
- },
15461
- {
15462
- version: 2,
15463
- description: "Add deferred table for DurableDeferred",
15464
- up: `
15465
- CREATE TABLE IF NOT EXISTS deferred (
15466
- id SERIAL PRIMARY KEY,
15467
- url TEXT NOT NULL UNIQUE,
15468
- resolved BOOLEAN NOT NULL DEFAULT FALSE,
15469
- value JSONB,
15470
- error TEXT,
15471
- expires_at BIGINT NOT NULL,
15472
- created_at BIGINT NOT NULL
15473
- );
15474
- CREATE INDEX IF NOT EXISTS idx_deferred_url ON deferred(url);
15475
- CREATE INDEX IF NOT EXISTS idx_deferred_expires ON deferred(expires_at);
15476
- CREATE INDEX IF NOT EXISTS idx_deferred_resolved ON deferred(resolved);
15477
- `,
15478
- down: `DROP TABLE IF EXISTS deferred;`
15479
- }
15173
+ var migrations;
15174
+ var init_migrations = __esm(() => {
15175
+ migrations = [
15176
+ {
15177
+ version: 1,
15178
+ description: "Add cursors table for DurableCursor",
15179
+ up: `
15180
+ CREATE TABLE IF NOT EXISTS cursors (
15181
+ id SERIAL PRIMARY KEY,
15182
+ stream TEXT NOT NULL,
15183
+ checkpoint TEXT NOT NULL,
15184
+ position BIGINT NOT NULL DEFAULT 0,
15185
+ updated_at BIGINT NOT NULL,
15186
+ UNIQUE(stream, checkpoint)
15187
+ );
15188
+ CREATE INDEX IF NOT EXISTS idx_cursors_checkpoint ON cursors(checkpoint);
15189
+ CREATE INDEX IF NOT EXISTS idx_cursors_stream ON cursors(stream);
15190
+ `,
15191
+ down: `DROP TABLE IF EXISTS cursors;`
15192
+ },
15193
+ {
15194
+ version: 2,
15195
+ description: "Add deferred table for DurableDeferred",
15196
+ up: `
15197
+ CREATE TABLE IF NOT EXISTS deferred (
15198
+ id SERIAL PRIMARY KEY,
15199
+ url TEXT NOT NULL UNIQUE,
15200
+ resolved BOOLEAN NOT NULL DEFAULT FALSE,
15201
+ value JSONB,
15202
+ error TEXT,
15203
+ expires_at BIGINT NOT NULL,
15204
+ created_at BIGINT NOT NULL
15205
+ );
15206
+ CREATE INDEX IF NOT EXISTS idx_deferred_url ON deferred(url);
15207
+ CREATE INDEX IF NOT EXISTS idx_deferred_expires ON deferred(expires_at);
15208
+ CREATE INDEX IF NOT EXISTS idx_deferred_resolved ON deferred(resolved);
15209
+ `,
15210
+ down: `DROP TABLE IF EXISTS deferred;`
15211
+ },
15212
+ {
15213
+ version: 3,
15214
+ description: "Add eval_records table for learning system",
15215
+ up: `
15216
+ CREATE TABLE IF NOT EXISTS eval_records (
15217
+ id TEXT PRIMARY KEY,
15218
+ project_key TEXT NOT NULL,
15219
+ task TEXT NOT NULL,
15220
+ context TEXT,
15221
+ strategy TEXT NOT NULL,
15222
+ epic_title TEXT NOT NULL,
15223
+ subtasks JSONB NOT NULL,
15224
+ outcomes JSONB,
15225
+ overall_success BOOLEAN,
15226
+ total_duration_ms INTEGER,
15227
+ total_errors INTEGER,
15228
+ human_accepted BOOLEAN,
15229
+ human_modified BOOLEAN,
15230
+ human_notes TEXT,
15231
+ file_overlap_count INTEGER,
15232
+ scope_accuracy REAL,
15233
+ time_balance_ratio REAL,
15234
+ created_at BIGINT NOT NULL,
15235
+ updated_at BIGINT NOT NULL
15236
+ );
15237
+ CREATE INDEX IF NOT EXISTS idx_eval_records_project ON eval_records(project_key);
15238
+ CREATE INDEX IF NOT EXISTS idx_eval_records_strategy ON eval_records(strategy);
15239
+ `,
15240
+ down: `DROP TABLE IF EXISTS eval_records;`
15241
+ },
15242
+ {
15243
+ version: 4,
15244
+ description: "Add swarm_contexts table for context recovery",
15245
+ up: `
15246
+ CREATE TABLE IF NOT EXISTS swarm_contexts (
15247
+ id TEXT PRIMARY KEY,
15248
+ epic_id TEXT NOT NULL,
15249
+ bead_id TEXT NOT NULL,
15250
+ strategy TEXT NOT NULL,
15251
+ files JSONB NOT NULL,
15252
+ dependencies JSONB NOT NULL,
15253
+ directives JSONB NOT NULL,
15254
+ recovery JSONB NOT NULL,
15255
+ created_at BIGINT NOT NULL,
15256
+ updated_at BIGINT NOT NULL
15257
+ );
15258
+ CREATE INDEX IF NOT EXISTS idx_swarm_contexts_epic ON swarm_contexts(epic_id);
15259
+ CREATE INDEX IF NOT EXISTS idx_swarm_contexts_bead ON swarm_contexts(bead_id);
15260
+ `,
15261
+ down: `DROP TABLE IF EXISTS swarm_contexts;`
15262
+ }
15263
+ ];
15264
+ });
15265
+
15266
+ // src/streams/swarm-mail.ts
15267
+ function generateSwarmAgentName() {
15268
+ const adj = ADJECTIVES2[Math.floor(Math.random() * ADJECTIVES2.length)];
15269
+ const noun = NOUNS2[Math.floor(Math.random() * NOUNS2.length)];
15270
+ return `${adj}${noun}`;
15271
+ }
15272
+ async function initSwarmAgent(options2) {
15273
+ const {
15274
+ projectPath,
15275
+ agentName = generateSwarmAgentName(),
15276
+ program = "opencode",
15277
+ model = "unknown",
15278
+ taskDescription
15279
+ } = options2;
15280
+ await registerAgent(projectPath, agentName, { program, model, taskDescription }, projectPath);
15281
+ return {
15282
+ projectKey: projectPath,
15283
+ agentName
15284
+ };
15285
+ }
15286
+ async function sendSwarmMessage(options2) {
15287
+ const {
15288
+ projectPath,
15289
+ fromAgent,
15290
+ toAgents,
15291
+ subject,
15292
+ body,
15293
+ threadId,
15294
+ importance = "normal",
15295
+ ackRequired = false
15296
+ } = options2;
15297
+ await sendMessage(projectPath, fromAgent, toAgents, subject, body, { threadId, importance, ackRequired }, projectPath);
15298
+ const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_streams(), exports_streams));
15299
+ const db = await getDatabase2(projectPath);
15300
+ const result = await db.query(`SELECT id FROM messages
15301
+ WHERE project_key = $1 AND from_agent = $2 AND subject = $3
15302
+ ORDER BY created_at DESC LIMIT 1`, [projectPath, fromAgent, subject]);
15303
+ const messageId = result.rows[0]?.id ?? 0;
15304
+ return {
15305
+ success: true,
15306
+ messageId,
15307
+ threadId,
15308
+ recipientCount: toAgents.length
15309
+ };
15310
+ }
15311
+ async function getSwarmInbox(options2) {
15312
+ const {
15313
+ projectPath,
15314
+ agentName,
15315
+ limit = MAX_INBOX_LIMIT2,
15316
+ urgentOnly = false,
15317
+ unreadOnly = false,
15318
+ includeBodies = false
15319
+ } = options2;
15320
+ const effectiveLimit = Math.min(limit, MAX_INBOX_LIMIT2);
15321
+ const messages = await getInbox(projectPath, agentName, {
15322
+ limit: effectiveLimit,
15323
+ urgentOnly,
15324
+ unreadOnly,
15325
+ includeBodies
15326
+ }, projectPath);
15327
+ return {
15328
+ messages: messages.map((m) => ({
15329
+ id: m.id,
15330
+ from_agent: m.from_agent,
15331
+ subject: m.subject,
15332
+ body: includeBodies ? m.body : undefined,
15333
+ thread_id: m.thread_id,
15334
+ importance: m.importance,
15335
+ created_at: m.created_at
15336
+ })),
15337
+ total: messages.length
15338
+ };
15339
+ }
15340
+ async function readSwarmMessage(options2) {
15341
+ const { projectPath, messageId, agentName, markAsRead = false } = options2;
15342
+ const message = await getMessage(projectPath, messageId, projectPath);
15343
+ if (!message) {
15344
+ return null;
15345
+ }
15346
+ if (markAsRead && agentName) {
15347
+ await appendEvent(createEvent("message_read", {
15348
+ project_key: projectPath,
15349
+ message_id: messageId,
15350
+ agent_name: agentName
15351
+ }), projectPath);
15352
+ }
15353
+ return {
15354
+ id: message.id,
15355
+ from_agent: message.from_agent,
15356
+ subject: message.subject,
15357
+ body: message.body,
15358
+ thread_id: message.thread_id,
15359
+ importance: message.importance,
15360
+ created_at: message.created_at
15361
+ };
15362
+ }
15363
+ async function reserveSwarmFiles(options2) {
15364
+ const {
15365
+ projectPath,
15366
+ agentName,
15367
+ paths,
15368
+ reason,
15369
+ exclusive = true,
15370
+ ttlSeconds = DEFAULT_TTL_SECONDS2
15371
+ } = options2;
15372
+ const conflicts = await checkConflicts(projectPath, agentName, paths, projectPath);
15373
+ await reserveFiles(projectPath, agentName, paths, { reason, exclusive, ttlSeconds }, projectPath);
15374
+ const reservations = await getActiveReservations(projectPath, projectPath, agentName);
15375
+ const granted = reservations.filter((r) => paths.includes(r.path_pattern)).map((r) => ({
15376
+ id: r.id,
15377
+ path_pattern: r.path_pattern,
15378
+ exclusive: r.exclusive,
15379
+ expiresAt: r.expires_at
15380
+ }));
15381
+ return {
15382
+ granted,
15383
+ conflicts: conflicts.map((c) => ({
15384
+ path: c.path,
15385
+ holder: c.holder,
15386
+ pattern: c.pattern
15387
+ }))
15388
+ };
15389
+ }
15390
+ async function releaseSwarmFiles(options2) {
15391
+ const { projectPath, agentName, paths, reservationIds } = options2;
15392
+ const currentReservations = await getActiveReservations(projectPath, projectPath, agentName);
15393
+ let releaseCount = 0;
15394
+ if (paths && paths.length > 0) {
15395
+ releaseCount = currentReservations.filter((r) => paths.includes(r.path_pattern)).length;
15396
+ } else if (reservationIds && reservationIds.length > 0) {
15397
+ releaseCount = currentReservations.filter((r) => reservationIds.includes(r.id)).length;
15398
+ } else {
15399
+ releaseCount = currentReservations.length;
15400
+ }
15401
+ await appendEvent(createEvent("file_released", {
15402
+ project_key: projectPath,
15403
+ agent_name: agentName,
15404
+ paths,
15405
+ reservation_ids: reservationIds
15406
+ }), projectPath);
15407
+ return {
15408
+ released: releaseCount,
15409
+ releasedAt: Date.now()
15410
+ };
15411
+ }
15412
+ async function acknowledgeSwarmMessage(options2) {
15413
+ const { projectPath, messageId, agentName } = options2;
15414
+ const timestamp = Date.now();
15415
+ await appendEvent(createEvent("message_acked", {
15416
+ project_key: projectPath,
15417
+ message_id: messageId,
15418
+ agent_name: agentName
15419
+ }), projectPath);
15420
+ return {
15421
+ acknowledged: true,
15422
+ acknowledgedAt: new Date(timestamp).toISOString()
15423
+ };
15424
+ }
15425
+ async function checkSwarmHealth(projectPath) {
15426
+ const healthy = await isDatabaseHealthy(projectPath);
15427
+ if (!healthy) {
15428
+ return {
15429
+ healthy: false,
15430
+ database: "disconnected"
15431
+ };
15432
+ }
15433
+ const stats = await getDatabaseStats(projectPath);
15434
+ return {
15435
+ healthy: true,
15436
+ database: "connected",
15437
+ stats
15438
+ };
15439
+ }
15440
+ var MAX_INBOX_LIMIT2 = 5, DEFAULT_TTL_SECONDS2 = 3600, ADJECTIVES2, NOUNS2;
15441
+ var init_swarm_mail = __esm(() => {
15442
+ init_events();
15443
+ init_streams();
15444
+ init_projections();
15445
+ init_store();
15446
+ ADJECTIVES2 = [
15447
+ "Blue",
15448
+ "Red",
15449
+ "Green",
15450
+ "Gold",
15451
+ "Silver",
15452
+ "Swift",
15453
+ "Bright",
15454
+ "Dark",
15455
+ "Calm",
15456
+ "Bold",
15457
+ "Wise",
15458
+ "Quick",
15459
+ "Warm",
15460
+ "Cool",
15461
+ "Pure",
15462
+ "Wild"
15463
+ ];
15464
+ NOUNS2 = [
15465
+ "Lake",
15466
+ "Stone",
15467
+ "River",
15468
+ "Mountain",
15469
+ "Forest",
15470
+ "Ocean",
15471
+ "Star",
15472
+ "Moon",
15473
+ "Wind",
15474
+ "Fire",
15475
+ "Cloud",
15476
+ "Storm",
15477
+ "Dawn",
15478
+ "Dusk",
15479
+ "Hawk",
15480
+ "Wolf"
15480
15481
  ];
15481
15482
  });
15482
15483
 
@@ -15494,7 +15495,7 @@ __export(exports_streams, {
15494
15495
  reserveSwarmFiles: () => reserveSwarmFiles,
15495
15496
  reserveFiles: () => reserveFiles,
15496
15497
  reserveAgentFiles: () => reserveAgentFiles,
15497
- replayEventsBatched: () => replayEventsBatched,
15498
+ replayEventsBatched: () => replayEventsBatched2,
15498
15499
  replayEvents: () => replayEvents,
15499
15500
  releaseSwarmFiles: () => releaseSwarmFiles,
15500
15501
  releaseAgentFiles: () => releaseAgentFiles,
@@ -15516,6 +15517,8 @@ __export(exports_streams, {
15516
15517
  getLatestSequence: () => getLatestSequence,
15517
15518
  getInbox: () => getInbox,
15518
15519
  getEventTimeline: () => getEventTimeline,
15520
+ getEvalStats: () => getEvalStats,
15521
+ getEvalRecords: () => getEvalRecords,
15519
15522
  getDatabaseStats: () => getDatabaseStats,
15520
15523
  getDatabasePath: () => getDatabasePath,
15521
15524
  getDatabase: () => getDatabase,
@@ -15543,12 +15546,17 @@ __export(exports_streams, {
15543
15546
  TaskProgressEventSchema: () => TaskProgressEventSchema,
15544
15547
  TaskCompletedEventSchema: () => TaskCompletedEventSchema,
15545
15548
  TaskBlockedEventSchema: () => TaskBlockedEventSchema,
15549
+ SwarmRecoveredEventSchema: () => SwarmRecoveredEventSchema,
15550
+ SwarmCheckpointedEventSchema: () => SwarmCheckpointedEventSchema,
15551
+ SubtaskOutcomeEventSchema: () => SubtaskOutcomeEventSchema,
15546
15552
  PGlite: () => PGlite,
15547
15553
  MessageSentEventSchema: () => MessageSentEventSchema,
15548
15554
  MessageReadEventSchema: () => MessageReadEventSchema,
15549
15555
  MessageAckedEventSchema: () => MessageAckedEventSchema,
15556
+ HumanFeedbackEventSchema: () => HumanFeedbackEventSchema,
15550
15557
  FileReservedEventSchema: () => FileReservedEventSchema,
15551
15558
  FileReleasedEventSchema: () => FileReleasedEventSchema,
15559
+ DecompositionGeneratedEventSchema: () => DecompositionGeneratedEventSchema,
15552
15560
  BaseEventSchema: () => BaseEventSchema,
15553
15561
  AgentRegisteredEventSchema: () => AgentRegisteredEventSchema,
15554
15562
  AgentEventSchema: () => AgentEventSchema,
@@ -15829,271 +15837,627 @@ async function isDatabaseHealthy(projectPath) {
15829
15837
  return false;
15830
15838
  }
15831
15839
  }
15832
- async function getDatabaseStats(projectPath) {
15840
+ async function getDatabaseStats(projectPath) {
15841
+ const db = await getDatabase(projectPath);
15842
+ const [events2, agents, messages, reservations] = await Promise.all([
15843
+ db.query("SELECT COUNT(*) as count FROM events"),
15844
+ db.query("SELECT COUNT(*) as count FROM agents"),
15845
+ db.query("SELECT COUNT(*) as count FROM messages"),
15846
+ db.query("SELECT COUNT(*) as count FROM reservations WHERE released_at IS NULL")
15847
+ ]);
15848
+ return {
15849
+ events: parseInt(events2.rows[0]?.count || "0"),
15850
+ agents: parseInt(agents.rows[0]?.count || "0"),
15851
+ messages: parseInt(messages.rows[0]?.count || "0"),
15852
+ reservations: parseInt(reservations.rows[0]?.count || "0")
15853
+ };
15854
+ }
15855
+ function handleExit() {
15856
+ const dbsToClose = Array.from(instances.values());
15857
+ for (const db of dbsToClose) {
15858
+ try {
15859
+ db.close().catch(() => {});
15860
+ } catch {}
15861
+ }
15862
+ }
15863
+ var SLOW_QUERY_THRESHOLD_MS = 100, DEBUG_LOG_PATH, instances, pendingInstances, schemaInitialized, degradedInstances, lastAccess, MAX_CACHE_SIZE = 10;
15864
+ var init_streams = __esm(() => {
15865
+ init_agent_mail();
15866
+ init_debug();
15867
+ init_events();
15868
+ init_migrations();
15869
+ init_projections();
15870
+ init_store();
15871
+ init_swarm_mail();
15872
+ DEBUG_LOG_PATH = join(homedir(), ".opencode", "streams-debug.log");
15873
+ instances = new Map;
15874
+ pendingInstances = new Map;
15875
+ schemaInitialized = new Map;
15876
+ degradedInstances = new Map;
15877
+ lastAccess = new Map;
15878
+ process.on("exit", handleExit);
15879
+ process.on("SIGINT", () => {
15880
+ handleExit();
15881
+ process.exit(0);
15882
+ });
15883
+ process.on("SIGTERM", () => {
15884
+ handleExit();
15885
+ process.exit(0);
15886
+ });
15887
+ });
15888
+
15889
+ // src/streams/store.ts
15890
+ function parseTimestamp(timestamp) {
15891
+ const ts = parseInt(timestamp, 10);
15892
+ if (Number.isNaN(ts)) {
15893
+ throw new Error(`[SwarmMail] Invalid timestamp: ${timestamp}`);
15894
+ }
15895
+ if (ts > Number.MAX_SAFE_INTEGER) {
15896
+ console.warn(`[SwarmMail] Timestamp ${timestamp} exceeds MAX_SAFE_INTEGER (year 2286+), precision may be lost`);
15897
+ }
15898
+ return ts;
15899
+ }
15900
+ async function appendEvent(event, projectPath) {
15901
+ const db = await getDatabase(projectPath);
15902
+ const { type, project_key, timestamp, ...rest } = event;
15903
+ console.log("[SwarmMail] Appending event", {
15904
+ type,
15905
+ projectKey: project_key,
15906
+ timestamp
15907
+ });
15908
+ const result = await db.query(`INSERT INTO events (type, project_key, timestamp, data)
15909
+ VALUES ($1, $2, $3, $4)
15910
+ RETURNING id, sequence`, [type, project_key, timestamp, JSON.stringify(rest)]);
15911
+ const row = result.rows[0];
15912
+ if (!row) {
15913
+ throw new Error("Failed to insert event - no row returned");
15914
+ }
15915
+ const { id, sequence } = row;
15916
+ console.log("[SwarmMail] Event appended", {
15917
+ type,
15918
+ id,
15919
+ sequence,
15920
+ projectKey: project_key
15921
+ });
15922
+ console.debug("[SwarmMail] Updating materialized views", { type, id });
15923
+ await updateMaterializedViews(db, { ...event, id, sequence });
15924
+ return { ...event, id, sequence };
15925
+ }
15926
+ async function appendEvents(events2, projectPath) {
15927
+ return withTiming("appendEvents", async () => {
15928
+ const db = await getDatabase(projectPath);
15929
+ const results = [];
15930
+ await db.exec("BEGIN");
15931
+ try {
15932
+ for (const event of events2) {
15933
+ const { type, project_key, timestamp, ...rest } = event;
15934
+ const result = await db.query(`INSERT INTO events (type, project_key, timestamp, data)
15935
+ VALUES ($1, $2, $3, $4)
15936
+ RETURNING id, sequence`, [type, project_key, timestamp, JSON.stringify(rest)]);
15937
+ const row = result.rows[0];
15938
+ if (!row) {
15939
+ throw new Error("Failed to insert event - no row returned");
15940
+ }
15941
+ const { id, sequence } = row;
15942
+ const enrichedEvent = { ...event, id, sequence };
15943
+ await updateMaterializedViews(db, enrichedEvent);
15944
+ results.push(enrichedEvent);
15945
+ }
15946
+ await db.exec("COMMIT");
15947
+ } catch (e) {
15948
+ let rollbackError = null;
15949
+ try {
15950
+ await db.exec("ROLLBACK");
15951
+ } catch (rbErr) {
15952
+ rollbackError = rbErr;
15953
+ console.error("[SwarmMail] ROLLBACK failed:", rbErr);
15954
+ }
15955
+ if (rollbackError) {
15956
+ const compositeError = new Error(`Transaction failed: ${e instanceof Error ? e.message : String(e)}. ` + `ROLLBACK also failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}. ` + `Database may be in inconsistent state.`);
15957
+ compositeError.originalError = e;
15958
+ compositeError.rollbackError = rollbackError;
15959
+ throw compositeError;
15960
+ }
15961
+ throw e;
15962
+ }
15963
+ return results;
15964
+ });
15965
+ }
15966
+ async function readEvents(options2 = {}, projectPath) {
15967
+ return withTiming("readEvents", async () => {
15968
+ const db = await getDatabase(projectPath);
15969
+ const conditions = [];
15970
+ const params = [];
15971
+ let paramIndex = 1;
15972
+ if (options2.projectKey) {
15973
+ conditions.push(`project_key = $${paramIndex++}`);
15974
+ params.push(options2.projectKey);
15975
+ }
15976
+ if (options2.types && options2.types.length > 0) {
15977
+ conditions.push(`type = ANY($${paramIndex++})`);
15978
+ params.push(options2.types);
15979
+ }
15980
+ if (options2.since !== undefined) {
15981
+ conditions.push(`timestamp >= $${paramIndex++}`);
15982
+ params.push(options2.since);
15983
+ }
15984
+ if (options2.until !== undefined) {
15985
+ conditions.push(`timestamp <= $${paramIndex++}`);
15986
+ params.push(options2.until);
15987
+ }
15988
+ if (options2.afterSequence !== undefined) {
15989
+ conditions.push(`sequence > $${paramIndex++}`);
15990
+ params.push(options2.afterSequence);
15991
+ }
15992
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
15993
+ let query = `
15994
+ SELECT id, type, project_key, timestamp, sequence, data
15995
+ FROM events
15996
+ ${whereClause}
15997
+ ORDER BY sequence ASC
15998
+ `;
15999
+ if (options2.limit) {
16000
+ query += ` LIMIT $${paramIndex++}`;
16001
+ params.push(options2.limit);
16002
+ }
16003
+ if (options2.offset) {
16004
+ query += ` OFFSET $${paramIndex++}`;
16005
+ params.push(options2.offset);
16006
+ }
16007
+ const result = await db.query(query, params);
16008
+ return result.rows.map((row) => {
16009
+ const data = typeof row.data === "string" ? JSON.parse(row.data) : row.data;
16010
+ return {
16011
+ id: row.id,
16012
+ type: row.type,
16013
+ project_key: row.project_key,
16014
+ timestamp: parseTimestamp(row.timestamp),
16015
+ sequence: row.sequence,
16016
+ ...data
16017
+ };
16018
+ });
16019
+ });
16020
+ }
16021
+ async function getLatestSequence(projectKey, projectPath) {
15833
16022
  const db = await getDatabase(projectPath);
15834
- const [events2, agents, messages, reservations] = await Promise.all([
15835
- db.query("SELECT COUNT(*) as count FROM events"),
15836
- db.query("SELECT COUNT(*) as count FROM agents"),
15837
- db.query("SELECT COUNT(*) as count FROM messages"),
15838
- db.query("SELECT COUNT(*) as count FROM reservations WHERE released_at IS NULL")
15839
- ]);
15840
- return {
15841
- events: parseInt(events2.rows[0]?.count || "0"),
15842
- agents: parseInt(agents.rows[0]?.count || "0"),
15843
- messages: parseInt(messages.rows[0]?.count || "0"),
15844
- reservations: parseInt(reservations.rows[0]?.count || "0")
15845
- };
15846
- }
15847
- function handleExit() {
15848
- const dbsToClose = Array.from(instances.values());
15849
- for (const db of dbsToClose) {
15850
- try {
15851
- db.close().catch(() => {});
15852
- } catch {}
15853
- }
16023
+ const query = projectKey ? "SELECT MAX(sequence) as seq FROM events WHERE project_key = $1" : "SELECT MAX(sequence) as seq FROM events";
16024
+ const params = projectKey ? [projectKey] : [];
16025
+ const result = await db.query(query, params);
16026
+ return result.rows[0]?.seq ?? 0;
15854
16027
  }
15855
- var SLOW_QUERY_THRESHOLD_MS = 100, DEBUG_LOG_PATH, instances, pendingInstances, schemaInitialized, degradedInstances, lastAccess, MAX_CACHE_SIZE = 10;
15856
- var init_streams = __esm(() => {
15857
- init_agent_mail();
15858
- init_debug();
15859
- init_events();
15860
- init_migrations();
15861
- init_projections();
15862
- init_store();
15863
- init_swarm_mail();
15864
- DEBUG_LOG_PATH = join(homedir(), ".opencode", "streams-debug.log");
15865
- instances = new Map;
15866
- pendingInstances = new Map;
15867
- schemaInitialized = new Map;
15868
- degradedInstances = new Map;
15869
- lastAccess = new Map;
15870
- process.on("exit", handleExit);
15871
- process.on("SIGINT", () => {
15872
- handleExit();
15873
- process.exit(0);
15874
- });
15875
- process.on("SIGTERM", () => {
15876
- handleExit();
15877
- process.exit(0);
16028
+ async function replayEvents(options2 = {}, projectPath) {
16029
+ return withTiming("replayEvents", async () => {
16030
+ const startTime = Date.now();
16031
+ const db = await getDatabase(projectPath);
16032
+ if (options2.clearViews) {
16033
+ if (options2.projectKey) {
16034
+ await db.query(`DELETE FROM message_recipients WHERE message_id IN (
16035
+ SELECT id FROM messages WHERE project_key = $1
16036
+ )`, [options2.projectKey]);
16037
+ await db.query(`DELETE FROM messages WHERE project_key = $1`, [
16038
+ options2.projectKey
16039
+ ]);
16040
+ await db.query(`DELETE FROM reservations WHERE project_key = $1`, [
16041
+ options2.projectKey
16042
+ ]);
16043
+ await db.query(`DELETE FROM agents WHERE project_key = $1`, [
16044
+ options2.projectKey
16045
+ ]);
16046
+ } else {
16047
+ await db.exec(`
16048
+ DELETE FROM message_recipients;
16049
+ DELETE FROM messages;
16050
+ DELETE FROM reservations;
16051
+ DELETE FROM agents;
16052
+ `);
16053
+ }
16054
+ }
16055
+ const events2 = await readEvents({
16056
+ projectKey: options2.projectKey,
16057
+ afterSequence: options2.fromSequence
16058
+ }, projectPath);
16059
+ for (const event of events2) {
16060
+ await updateMaterializedViews(db, event);
16061
+ }
16062
+ return {
16063
+ eventsReplayed: events2.length,
16064
+ duration: Date.now() - startTime
16065
+ };
15878
16066
  });
15879
- });
15880
-
15881
- // src/streams/swarm-mail.ts
15882
- function generateSwarmAgentName() {
15883
- const adj = ADJECTIVES2[Math.floor(Math.random() * ADJECTIVES2.length)];
15884
- const noun = NOUNS2[Math.floor(Math.random() * NOUNS2.length)];
15885
- return `${adj}${noun}`;
15886
16067
  }
15887
- async function initSwarmAgent(options2) {
15888
- const {
15889
- projectPath,
15890
- agentName = generateSwarmAgentName(),
15891
- program = "opencode",
15892
- model = "unknown",
15893
- taskDescription
15894
- } = options2;
15895
- await registerAgent(projectPath, agentName, { program, model, taskDescription }, projectPath);
15896
- return {
15897
- projectKey: projectPath,
15898
- agentName
15899
- };
16068
+ async function replayEventsBatched2(projectKey, onBatch, options2 = {}, projectPath) {
16069
+ return withTiming("replayEventsBatched", async () => {
16070
+ const startTime = Date.now();
16071
+ const batchSize = options2.batchSize ?? 1000;
16072
+ const fromSequence = options2.fromSequence ?? 0;
16073
+ const db = await getDatabase(projectPath);
16074
+ if (options2.clearViews) {
16075
+ await db.query(`DELETE FROM message_recipients WHERE message_id IN (
16076
+ SELECT id FROM messages WHERE project_key = $1
16077
+ )`, [projectKey]);
16078
+ await db.query(`DELETE FROM messages WHERE project_key = $1`, [
16079
+ projectKey
16080
+ ]);
16081
+ await db.query(`DELETE FROM reservations WHERE project_key = $1`, [
16082
+ projectKey
16083
+ ]);
16084
+ await db.query(`DELETE FROM agents WHERE project_key = $1`, [projectKey]);
16085
+ }
16086
+ const countResult = await db.query(`SELECT COUNT(*) as count FROM events WHERE project_key = $1 AND sequence > $2`, [projectKey, fromSequence]);
16087
+ const total = parseInt(countResult.rows[0]?.count ?? "0");
16088
+ if (total === 0) {
16089
+ return { eventsReplayed: 0, duration: Date.now() - startTime };
16090
+ }
16091
+ let processed = 0;
16092
+ let offset = 0;
16093
+ while (processed < total) {
16094
+ const events2 = await readEvents({
16095
+ projectKey,
16096
+ afterSequence: fromSequence,
16097
+ limit: batchSize,
16098
+ offset
16099
+ }, projectPath);
16100
+ if (events2.length === 0)
16101
+ break;
16102
+ for (const event of events2) {
16103
+ await updateMaterializedViews(db, event);
16104
+ }
16105
+ processed += events2.length;
16106
+ const percent = Math.round(processed / total * 100);
16107
+ await onBatch(events2, { processed, total, percent });
16108
+ console.log(`[SwarmMail] Replaying events: ${processed}/${total} (${percent}%)`);
16109
+ offset += batchSize;
16110
+ }
16111
+ return {
16112
+ eventsReplayed: processed,
16113
+ duration: Date.now() - startTime
16114
+ };
16115
+ });
15900
16116
  }
15901
- async function sendSwarmMessage(options2) {
15902
- const {
15903
- projectPath,
15904
- fromAgent,
15905
- toAgents,
15906
- subject,
15907
- body,
15908
- threadId,
15909
- importance = "normal",
15910
- ackRequired = false
15911
- } = options2;
15912
- await sendMessage(projectPath, fromAgent, toAgents, subject, body, { threadId, importance, ackRequired }, projectPath);
15913
- const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_streams(), exports_streams));
15914
- const db = await getDatabase2(projectPath);
15915
- const result = await db.query(`SELECT id FROM messages
15916
- WHERE project_key = $1 AND from_agent = $2 AND subject = $3
15917
- ORDER BY created_at DESC LIMIT 1`, [projectPath, fromAgent, subject]);
15918
- const messageId = result.rows[0]?.id ?? 0;
15919
- return {
15920
- success: true,
15921
- messageId,
15922
- threadId,
15923
- recipientCount: toAgents.length
15924
- };
16117
+ async function updateMaterializedViews(db, event) {
16118
+ try {
16119
+ switch (event.type) {
16120
+ case "agent_registered":
16121
+ await handleAgentRegistered(db, event);
16122
+ break;
16123
+ case "agent_active":
16124
+ await db.query(`UPDATE agents SET last_active_at = $1 WHERE project_key = $2 AND name = $3`, [event.timestamp, event.project_key, event.agent_name]);
16125
+ break;
16126
+ case "message_sent":
16127
+ await handleMessageSent(db, event);
16128
+ break;
16129
+ case "message_read":
16130
+ await db.query(`UPDATE message_recipients SET read_at = $1 WHERE message_id = $2 AND agent_name = $3`, [event.timestamp, event.message_id, event.agent_name]);
16131
+ break;
16132
+ case "message_acked":
16133
+ await db.query(`UPDATE message_recipients SET acked_at = $1 WHERE message_id = $2 AND agent_name = $3`, [event.timestamp, event.message_id, event.agent_name]);
16134
+ break;
16135
+ case "file_reserved":
16136
+ await handleFileReserved(db, event);
16137
+ break;
16138
+ case "file_released":
16139
+ await handleFileReleased(db, event);
16140
+ break;
16141
+ case "task_started":
16142
+ case "task_progress":
16143
+ case "task_completed":
16144
+ case "task_blocked":
16145
+ break;
16146
+ case "decomposition_generated":
16147
+ await handleDecompositionGenerated(db, event);
16148
+ break;
16149
+ case "subtask_outcome":
16150
+ await handleSubtaskOutcome(db, event);
16151
+ break;
16152
+ case "human_feedback":
16153
+ await handleHumanFeedback(db, event);
16154
+ break;
16155
+ case "swarm_checkpointed":
16156
+ await handleSwarmCheckpointed(db, event);
16157
+ break;
16158
+ case "swarm_recovered":
16159
+ await handleSwarmRecovered(db, event);
16160
+ break;
16161
+ }
16162
+ } catch (error45) {
16163
+ console.error("[SwarmMail] Failed to update materialized views", {
16164
+ eventType: event.type,
16165
+ eventId: event.id,
16166
+ error: error45
16167
+ });
16168
+ throw error45;
16169
+ }
15925
16170
  }
15926
- async function getSwarmInbox(options2) {
15927
- const {
15928
- projectPath,
15929
- agentName,
15930
- limit = MAX_INBOX_LIMIT2,
15931
- urgentOnly = false,
15932
- unreadOnly = false,
15933
- includeBodies = false
15934
- } = options2;
15935
- const effectiveLimit = Math.min(limit, MAX_INBOX_LIMIT2);
15936
- const messages = await getInbox(projectPath, agentName, {
15937
- limit: effectiveLimit,
15938
- urgentOnly,
15939
- unreadOnly,
15940
- includeBodies
15941
- }, projectPath);
15942
- return {
15943
- messages: messages.map((m) => ({
15944
- id: m.id,
15945
- from_agent: m.from_agent,
15946
- subject: m.subject,
15947
- body: includeBodies ? m.body : undefined,
15948
- thread_id: m.thread_id,
15949
- importance: m.importance,
15950
- created_at: m.created_at
15951
- })),
15952
- total: messages.length
15953
- };
16171
+ async function handleAgentRegistered(db, event) {
16172
+ await db.query(`INSERT INTO agents (project_key, name, program, model, task_description, registered_at, last_active_at)
16173
+ VALUES ($1, $2, $3, $4, $5, $6, $6)
16174
+ ON CONFLICT (project_key, name) DO UPDATE SET
16175
+ program = EXCLUDED.program,
16176
+ model = EXCLUDED.model,
16177
+ task_description = EXCLUDED.task_description,
16178
+ last_active_at = EXCLUDED.last_active_at`, [
16179
+ event.project_key,
16180
+ event.agent_name,
16181
+ event.program,
16182
+ event.model,
16183
+ event.task_description || null,
16184
+ event.timestamp
16185
+ ]);
15954
16186
  }
15955
- async function readSwarmMessage(options2) {
15956
- const { projectPath, messageId, agentName, markAsRead = false } = options2;
15957
- const message = await getMessage(projectPath, messageId, projectPath);
15958
- if (!message) {
15959
- return null;
16187
+ async function handleMessageSent(db, event) {
16188
+ console.log("[SwarmMail] Handling message sent event", {
16189
+ from: event.from_agent,
16190
+ to: event.to_agents,
16191
+ subject: event.subject,
16192
+ projectKey: event.project_key
16193
+ });
16194
+ const result = await db.query(`INSERT INTO messages (project_key, from_agent, subject, body, thread_id, importance, ack_required, created_at)
16195
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
16196
+ RETURNING id`, [
16197
+ event.project_key,
16198
+ event.from_agent,
16199
+ event.subject,
16200
+ event.body,
16201
+ event.thread_id || null,
16202
+ event.importance,
16203
+ event.ack_required,
16204
+ event.timestamp
16205
+ ]);
16206
+ const msgRow = result.rows[0];
16207
+ if (!msgRow) {
16208
+ throw new Error("Failed to insert message - no row returned");
15960
16209
  }
15961
- if (markAsRead && agentName) {
15962
- await appendEvent(createEvent("message_read", {
15963
- project_key: projectPath,
15964
- message_id: messageId,
15965
- agent_name: agentName
15966
- }), projectPath);
16210
+ const messageId = msgRow.id;
16211
+ if (event.to_agents.length > 0) {
16212
+ const values = event.to_agents.map((_, i) => `($1, $${i + 2})`).join(", ");
16213
+ const params = [messageId, ...event.to_agents];
16214
+ await db.query(`INSERT INTO message_recipients (message_id, agent_name)
16215
+ VALUES ${values}
16216
+ ON CONFLICT DO NOTHING`, params);
16217
+ console.log("[SwarmMail] Message recipients inserted", {
16218
+ messageId,
16219
+ recipientCount: event.to_agents.length
16220
+ });
15967
16221
  }
15968
- return {
15969
- id: message.id,
15970
- from_agent: message.from_agent,
15971
- subject: message.subject,
15972
- body: message.body,
15973
- thread_id: message.thread_id,
15974
- importance: message.importance,
15975
- created_at: message.created_at
15976
- };
15977
16222
  }
15978
- async function reserveSwarmFiles(options2) {
15979
- const {
15980
- projectPath,
15981
- agentName,
15982
- paths,
15983
- reason,
15984
- exclusive = true,
15985
- ttlSeconds = DEFAULT_TTL_SECONDS2
15986
- } = options2;
15987
- const conflicts = await checkConflicts(projectPath, agentName, paths, projectPath);
15988
- await reserveFiles(projectPath, agentName, paths, { reason, exclusive, ttlSeconds }, projectPath);
15989
- const reservations = await getActiveReservations(projectPath, projectPath, agentName);
15990
- const granted = reservations.filter((r) => paths.includes(r.path_pattern)).map((r) => ({
15991
- id: r.id,
15992
- path_pattern: r.path_pattern,
15993
- exclusive: r.exclusive,
15994
- expiresAt: r.expires_at
15995
- }));
15996
- return {
15997
- granted,
15998
- conflicts: conflicts.map((c) => ({
15999
- path: c.path,
16000
- holder: c.holder,
16001
- pattern: c.pattern
16002
- }))
16003
- };
16223
+ async function handleFileReserved(db, event) {
16224
+ console.log("[SwarmMail] Handling file reservation event", {
16225
+ agent: event.agent_name,
16226
+ paths: event.paths,
16227
+ exclusive: event.exclusive,
16228
+ projectKey: event.project_key
16229
+ });
16230
+ if (event.paths.length > 0) {
16231
+ const values = event.paths.map((_, i) => `($1, $2, $${i + 3}, $${event.paths.length + 3}, $${event.paths.length + 4}, $${event.paths.length + 5}, $${event.paths.length + 6})`).join(", ");
16232
+ const params = [
16233
+ event.project_key,
16234
+ event.agent_name,
16235
+ ...event.paths,
16236
+ event.exclusive,
16237
+ event.reason || null,
16238
+ event.timestamp,
16239
+ event.expires_at
16240
+ ];
16241
+ if (event.paths.length > 0) {
16242
+ await db.query(`DELETE FROM reservations
16243
+ WHERE project_key = $1
16244
+ AND agent_name = $2
16245
+ AND path_pattern = ANY($3)
16246
+ AND released_at IS NULL`, [event.project_key, event.agent_name, event.paths]);
16247
+ }
16248
+ await db.query(`INSERT INTO reservations (project_key, agent_name, path_pattern, exclusive, reason, created_at, expires_at)
16249
+ VALUES ${values}`, params);
16250
+ console.log("[SwarmMail] File reservations inserted", {
16251
+ agent: event.agent_name,
16252
+ reservationCount: event.paths.length
16253
+ });
16254
+ }
16004
16255
  }
16005
- async function releaseSwarmFiles(options2) {
16006
- const { projectPath, agentName, paths, reservationIds } = options2;
16007
- const currentReservations = await getActiveReservations(projectPath, projectPath, agentName);
16008
- let releaseCount = 0;
16009
- if (paths && paths.length > 0) {
16010
- releaseCount = currentReservations.filter((r) => paths.includes(r.path_pattern)).length;
16011
- } else if (reservationIds && reservationIds.length > 0) {
16012
- releaseCount = currentReservations.filter((r) => reservationIds.includes(r.id)).length;
16256
+ async function handleFileReleased(db, event) {
16257
+ if (event.type !== "file_released")
16258
+ return;
16259
+ if (event.reservation_ids && event.reservation_ids.length > 0) {
16260
+ await db.query(`UPDATE reservations SET released_at = $1 WHERE id = ANY($2)`, [event.timestamp, event.reservation_ids]);
16261
+ } else if (event.paths && event.paths.length > 0) {
16262
+ await db.query(`UPDATE reservations SET released_at = $1
16263
+ WHERE project_key = $2 AND agent_name = $3 AND path_pattern = ANY($4) AND released_at IS NULL`, [event.timestamp, event.project_key, event.agent_name, event.paths]);
16013
16264
  } else {
16014
- releaseCount = currentReservations.length;
16265
+ await db.query(`UPDATE reservations SET released_at = $1
16266
+ WHERE project_key = $2 AND agent_name = $3 AND released_at IS NULL`, [event.timestamp, event.project_key, event.agent_name]);
16015
16267
  }
16016
- await appendEvent(createEvent("file_released", {
16017
- project_key: projectPath,
16018
- agent_name: agentName,
16019
- paths,
16020
- reservation_ids: reservationIds
16021
- }), projectPath);
16022
- return {
16023
- released: releaseCount,
16024
- releasedAt: Date.now()
16025
- };
16026
16268
  }
16027
- async function acknowledgeSwarmMessage(options2) {
16028
- const { projectPath, messageId, agentName } = options2;
16029
- const timestamp = Date.now();
16030
- await appendEvent(createEvent("message_acked", {
16031
- project_key: projectPath,
16032
- message_id: messageId,
16033
- agent_name: agentName
16034
- }), projectPath);
16035
- return {
16036
- acknowledged: true,
16037
- acknowledgedAt: new Date(timestamp).toISOString()
16038
- };
16269
+ async function handleDecompositionGenerated(db, event) {
16270
+ if (event.type !== "decomposition_generated")
16271
+ return;
16272
+ await db.query(`INSERT INTO eval_records (
16273
+ id, project_key, task, context, strategy, epic_title, subtasks,
16274
+ created_at, updated_at
16275
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8)
16276
+ ON CONFLICT (id) DO NOTHING`, [
16277
+ event.epic_id,
16278
+ event.project_key,
16279
+ event.task,
16280
+ event.context || null,
16281
+ event.strategy,
16282
+ event.epic_title,
16283
+ JSON.stringify(event.subtasks),
16284
+ event.timestamp
16285
+ ]);
16039
16286
  }
16040
- async function checkSwarmHealth(projectPath) {
16041
- const healthy = await isDatabaseHealthy(projectPath);
16042
- if (!healthy) {
16043
- return {
16044
- healthy: false,
16045
- database: "disconnected"
16046
- };
16287
+ async function handleSubtaskOutcome(db, event) {
16288
+ if (event.type !== "subtask_outcome")
16289
+ return;
16290
+ const result = await db.query(`SELECT outcomes, subtasks FROM eval_records WHERE id = $1`, [
16291
+ event.epic_id
16292
+ ]);
16293
+ if (!result.rows[0]) {
16294
+ console.warn(`[SwarmMail] No eval_record found for epic_id ${event.epic_id}`);
16295
+ return;
16047
16296
  }
16048
- const stats = await getDatabaseStats(projectPath);
16049
- return {
16050
- healthy: true,
16051
- database: "connected",
16052
- stats
16053
- };
16297
+ const row = result.rows[0];
16298
+ const subtasks = typeof row.subtasks === "string" ? JSON.parse(row.subtasks) : row.subtasks;
16299
+ const outcomes = row.outcomes ? typeof row.outcomes === "string" ? JSON.parse(row.outcomes) : row.outcomes : [];
16300
+ const newOutcome = {
16301
+ bead_id: event.bead_id,
16302
+ planned_files: event.planned_files,
16303
+ actual_files: event.actual_files,
16304
+ duration_ms: event.duration_ms,
16305
+ error_count: event.error_count,
16306
+ retry_count: event.retry_count,
16307
+ success: event.success
16308
+ };
16309
+ const updatedOutcomes = [...outcomes, newOutcome];
16310
+ const fileOverlapCount = computeFileOverlap(subtasks);
16311
+ const scopeAccuracy = computeScopeAccuracy(event.planned_files, event.actual_files);
16312
+ const timeBalanceRatio = computeTimeBalanceRatio(updatedOutcomes);
16313
+ const overallSuccess = updatedOutcomes.every((o) => o.success);
16314
+ const totalDurationMs = updatedOutcomes.reduce((sum, o) => sum + o.duration_ms, 0);
16315
+ const totalErrors = updatedOutcomes.reduce((sum, o) => sum + o.error_count, 0);
16316
+ await db.query(`UPDATE eval_records SET
16317
+ outcomes = $1,
16318
+ file_overlap_count = $2,
16319
+ scope_accuracy = $3,
16320
+ time_balance_ratio = $4,
16321
+ overall_success = $5,
16322
+ total_duration_ms = $6,
16323
+ total_errors = $7,
16324
+ updated_at = $8
16325
+ WHERE id = $9`, [
16326
+ JSON.stringify(updatedOutcomes),
16327
+ fileOverlapCount,
16328
+ scopeAccuracy,
16329
+ timeBalanceRatio,
16330
+ overallSuccess,
16331
+ totalDurationMs,
16332
+ totalErrors,
16333
+ event.timestamp,
16334
+ event.epic_id
16335
+ ]);
16054
16336
  }
16055
- var MAX_INBOX_LIMIT2 = 5, DEFAULT_TTL_SECONDS2 = 3600, ADJECTIVES2, NOUNS2;
16056
- var init_swarm_mail = __esm(() => {
16057
- init_events();
16058
- init_streams();
16059
- init_projections();
16060
- init_store();
16061
- ADJECTIVES2 = [
16062
- "Blue",
16063
- "Red",
16064
- "Green",
16065
- "Gold",
16066
- "Silver",
16067
- "Swift",
16068
- "Bright",
16069
- "Dark",
16070
- "Calm",
16071
- "Bold",
16072
- "Wise",
16073
- "Quick",
16074
- "Warm",
16075
- "Cool",
16076
- "Pure",
16077
- "Wild"
16078
- ];
16079
- NOUNS2 = [
16080
- "Lake",
16081
- "Stone",
16082
- "River",
16083
- "Mountain",
16084
- "Forest",
16085
- "Ocean",
16086
- "Star",
16087
- "Moon",
16088
- "Wind",
16089
- "Fire",
16090
- "Cloud",
16091
- "Storm",
16092
- "Dawn",
16093
- "Dusk",
16094
- "Hawk",
16095
- "Wolf"
16096
- ];
16337
+ async function handleHumanFeedback(db, event) {
16338
+ if (event.type !== "human_feedback")
16339
+ return;
16340
+ await db.query(`UPDATE eval_records SET
16341
+ human_accepted = $1,
16342
+ human_modified = $2,
16343
+ human_notes = $3,
16344
+ updated_at = $4
16345
+ WHERE id = $5`, [
16346
+ event.accepted,
16347
+ event.modified,
16348
+ event.notes || null,
16349
+ event.timestamp,
16350
+ event.epic_id
16351
+ ]);
16352
+ }
16353
+ async function handleSwarmCheckpointed(db, event) {
16354
+ if (event.type !== "swarm_checkpointed")
16355
+ return;
16356
+ await db.query(`INSERT INTO swarm_contexts (
16357
+ project_key, epic_id, bead_id, strategy, files, dependencies,
16358
+ directives, recovery, checkpointed_at, updated_at
16359
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9)
16360
+ ON CONFLICT (project_key, epic_id, bead_id) DO UPDATE SET
16361
+ strategy = EXCLUDED.strategy,
16362
+ files = EXCLUDED.files,
16363
+ dependencies = EXCLUDED.dependencies,
16364
+ directives = EXCLUDED.directives,
16365
+ recovery = EXCLUDED.recovery,
16366
+ checkpointed_at = EXCLUDED.checkpointed_at,
16367
+ updated_at = EXCLUDED.updated_at`, [
16368
+ event.project_key,
16369
+ event.epic_id,
16370
+ event.bead_id,
16371
+ event.strategy,
16372
+ JSON.stringify(event.files),
16373
+ JSON.stringify(event.dependencies),
16374
+ JSON.stringify(event.directives),
16375
+ JSON.stringify(event.recovery),
16376
+ event.timestamp
16377
+ ]);
16378
+ }
16379
+ async function handleSwarmRecovered(db, event) {
16380
+ if (event.type !== "swarm_recovered")
16381
+ return;
16382
+ await db.query(`UPDATE swarm_contexts SET
16383
+ recovered_at = $1,
16384
+ recovered_from_checkpoint = $2,
16385
+ updated_at = $1
16386
+ WHERE project_key = $3 AND epic_id = $4 AND bead_id = $5`, [
16387
+ event.timestamp,
16388
+ event.recovered_from_checkpoint,
16389
+ event.project_key,
16390
+ event.epic_id,
16391
+ event.bead_id
16392
+ ]);
16393
+ }
16394
+ function computeFileOverlap(subtasks) {
16395
+ const fileCount = new Map;
16396
+ for (const subtask of subtasks) {
16397
+ for (const file2 of subtask.files) {
16398
+ fileCount.set(file2, (fileCount.get(file2) || 0) + 1);
16399
+ }
16400
+ }
16401
+ return Array.from(fileCount.values()).filter((count) => count > 1).length;
16402
+ }
16403
+ function computeScopeAccuracy(planned, actual) {
16404
+ if (planned.length === 0)
16405
+ return 1;
16406
+ const plannedSet = new Set(planned);
16407
+ const intersection2 = actual.filter((file2) => plannedSet.has(file2));
16408
+ return intersection2.length / planned.length;
16409
+ }
16410
+ function computeTimeBalanceRatio(outcomes) {
16411
+ if (outcomes.length === 0)
16412
+ return null;
16413
+ const durations = outcomes.map((o) => o.duration_ms);
16414
+ const max = Math.max(...durations);
16415
+ const min = Math.min(...durations);
16416
+ if (min === 0)
16417
+ return null;
16418
+ return max / min;
16419
+ }
16420
+ async function registerAgent(projectKey, agentName, options2 = {}, projectPath) {
16421
+ const event = createEvent("agent_registered", {
16422
+ project_key: projectKey,
16423
+ agent_name: agentName,
16424
+ program: options2.program || "opencode",
16425
+ model: options2.model || "unknown",
16426
+ task_description: options2.taskDescription
16427
+ });
16428
+ return appendEvent(event, projectPath);
16429
+ }
16430
+ async function sendMessage(projectKey, fromAgent, toAgents, subject, body, options2 = {}, projectPath) {
16431
+ const event = createEvent("message_sent", {
16432
+ project_key: projectKey,
16433
+ from_agent: fromAgent,
16434
+ to_agents: toAgents,
16435
+ subject,
16436
+ body,
16437
+ thread_id: options2.threadId,
16438
+ importance: options2.importance || "normal",
16439
+ ack_required: options2.ackRequired || false
16440
+ });
16441
+ return appendEvent(event, projectPath);
16442
+ }
16443
+ async function reserveFiles(projectKey, agentName, paths, options2 = {}, projectPath) {
16444
+ const ttlSeconds = options2.ttlSeconds || 3600;
16445
+ const event = createEvent("file_reserved", {
16446
+ project_key: projectKey,
16447
+ agent_name: agentName,
16448
+ paths,
16449
+ reason: options2.reason,
16450
+ exclusive: options2.exclusive ?? true,
16451
+ ttl_seconds: ttlSeconds,
16452
+ expires_at: Date.now() + ttlSeconds * 1000
16453
+ });
16454
+ return appendEvent(event, projectPath);
16455
+ }
16456
+ var TIMESTAMP_SAFE_UNTIL;
16457
+ var init_store = __esm(() => {
16458
+ init_streams();
16459
+ init_events();
16460
+ TIMESTAMP_SAFE_UNTIL = new Date("2286-01-01").getTime();
16097
16461
  });
16098
16462
 
16099
16463
  // node_modules/.pnpm/@ioredis+commands@1.4.0/node_modules/@ioredis/commands/built/commands.json
@@ -30762,7 +31126,58 @@ var ScoreCalculationResultSchema = exports_external.object({
30762
31126
  score: MandateScoreSchema,
30763
31127
  status_changed: exports_external.boolean()
30764
31128
  });
31129
+ // src/schemas/swarm-context.ts
31130
+ init_zod();
31131
+ var SwarmStrategySchema = exports_external.enum([
31132
+ "file-based",
31133
+ "feature-based",
31134
+ "risk-based"
31135
+ ]);
31136
+ var SwarmDirectivesSchema = exports_external.object({
31137
+ shared_context: exports_external.string(),
31138
+ skills_to_load: exports_external.array(exports_external.string()).default([]),
31139
+ coordinator_notes: exports_external.string().default("")
31140
+ });
31141
+ var SwarmRecoverySchema = exports_external.object({
31142
+ last_checkpoint: exports_external.string(),
31143
+ files_modified: exports_external.array(exports_external.string()).default([]),
31144
+ progress_percent: exports_external.number().min(0).max(100).default(0),
31145
+ last_message: exports_external.string().default(""),
31146
+ error_context: exports_external.string().optional()
31147
+ });
31148
+ var SwarmBeadContextSchema = exports_external.object({
31149
+ id: exports_external.string(),
31150
+ epic_id: exports_external.string(),
31151
+ bead_id: exports_external.string(),
31152
+ strategy: SwarmStrategySchema,
31153
+ files: exports_external.array(exports_external.string()),
31154
+ dependencies: exports_external.array(exports_external.string()).default([]),
31155
+ directives: SwarmDirectivesSchema,
31156
+ recovery: SwarmRecoverySchema,
31157
+ created_at: exports_external.number().int().positive(),
31158
+ updated_at: exports_external.number().int().positive()
31159
+ });
31160
+ var CreateSwarmContextArgsSchema = SwarmBeadContextSchema.omit({
31161
+ id: true,
31162
+ created_at: true,
31163
+ updated_at: true
31164
+ });
31165
+ var UpdateSwarmContextArgsSchema = exports_external.object({
31166
+ id: exports_external.string(),
31167
+ recovery: SwarmRecoverySchema.partial().optional(),
31168
+ files: exports_external.array(exports_external.string()).optional(),
31169
+ dependencies: exports_external.array(exports_external.string()).optional(),
31170
+ directives: SwarmDirectivesSchema.partial().optional()
31171
+ });
31172
+ var QuerySwarmContextsArgsSchema = exports_external.object({
31173
+ epic_id: exports_external.string().optional(),
31174
+ bead_id: exports_external.string().optional(),
31175
+ strategy: SwarmStrategySchema.optional(),
31176
+ has_errors: exports_external.boolean().optional()
31177
+ });
30765
31178
  // src/beads.ts
31179
+ init_events();
31180
+ init_store();
30766
31181
  var beadsWorkingDirectory = null;
30767
31182
  function setBeadsWorkingDirectory(directory) {
30768
31183
  beadsWorkingDirectory = directory;
@@ -30907,7 +31322,15 @@ var beads_create_epic = tool({
30907
31322
  priority: tool.schema.number().min(0).max(3).optional(),
30908
31323
  files: tool.schema.array(tool.schema.string()).optional(),
30909
31324
  id_suffix: tool.schema.string().optional().describe("Custom ID suffix (e.g., 'e2e-test' becomes 'phase-0.e2e-test')")
30910
- })).describe("Subtasks to create under the epic")
31325
+ })).describe("Subtasks to create under the epic"),
31326
+ strategy: tool.schema.enum(["file-based", "feature-based", "risk-based"]).optional().describe("Decomposition strategy used (default: feature-based)"),
31327
+ task: tool.schema.string().optional().describe("Original task description that was decomposed"),
31328
+ project_key: tool.schema.string().optional().describe("Project path for event emission"),
31329
+ recovery_context: tool.schema.object({
31330
+ shared_context: tool.schema.string().optional(),
31331
+ skills_to_load: tool.schema.array(tool.schema.string()).optional(),
31332
+ coordinator_notes: tool.schema.string().optional()
31333
+ }).optional().describe("Recovery context from checkpoint compaction")
30911
31334
  },
30912
31335
  async execute(args, ctx) {
30913
31336
  const validated = EpicCreateArgsSchema.parse(args);
@@ -30950,6 +31373,27 @@ var beads_create_epic = tool({
30950
31373
  epic,
30951
31374
  subtasks: created.slice(1)
30952
31375
  };
31376
+ if (args.project_key) {
31377
+ try {
31378
+ const event = createEvent("decomposition_generated", {
31379
+ project_key: args.project_key,
31380
+ epic_id: epic.id,
31381
+ task: args.task || validated.epic_title,
31382
+ context: validated.epic_description,
31383
+ strategy: args.strategy || "feature-based",
31384
+ epic_title: validated.epic_title,
31385
+ subtasks: validated.subtasks.map((st) => ({
31386
+ title: st.title,
31387
+ files: st.files || [],
31388
+ priority: st.priority
31389
+ })),
31390
+ recovery_context: args.recovery_context
31391
+ });
31392
+ await appendEvent(event, args.project_key);
31393
+ } catch (error45) {
31394
+ console.warn("[beads_create_epic] Failed to emit DecompositionGeneratedEvent:", error45);
31395
+ }
31396
+ }
30953
31397
  return JSON.stringify(result, null, 2);
30954
31398
  } catch (error45) {
30955
31399
  const rollbackCommands = [];
@@ -31128,7 +31572,7 @@ var beads_sync = tool({
31128
31572
  async execute(args, ctx) {
31129
31573
  const autoPull = args.auto_pull ?? true;
31130
31574
  const TIMEOUT_MS = 30000;
31131
- const withTimeout = async (promise2, timeoutMs, operation) => {
31575
+ const withTimeout2 = async (promise2, timeoutMs, operation) => {
31132
31576
  let timeoutId;
31133
31577
  const timeoutPromise = new Promise((_, reject) => {
31134
31578
  timeoutId = setTimeout(() => reject(new BeadError(`Operation timed out after ${timeoutMs}ms`, operation)), timeoutMs);
@@ -31141,7 +31585,7 @@ var beads_sync = tool({
31141
31585
  }
31142
31586
  }
31143
31587
  };
31144
- const flushResult = await withTimeout(runBdCommand(["sync", "--flush-only"]), TIMEOUT_MS, "bd sync --flush-only");
31588
+ const flushResult = await withTimeout2(runBdCommand(["sync", "--flush-only"]), TIMEOUT_MS, "bd sync --flush-only");
31145
31589
  if (flushResult.exitCode !== 0) {
31146
31590
  throw new BeadError(`Failed to flush beads because bd sync failed: ${flushResult.stderr}. Try: Check if .beads/ directory is writable, verify no corrupted JSONL files, or run 'bd list' to test basic beads functionality.`, "bd sync --flush-only", flushResult.exitCode);
31147
31591
  }
@@ -31156,7 +31600,7 @@ var beads_sync = tool({
31156
31600
  if (addResult.exitCode !== 0) {
31157
31601
  throw new BeadError(`Failed to stage beads because git add failed: ${addResult.stderr}. Try: Check if .beads/ directory exists, verify git is initialized with 'git status', or check for .gitignore patterns blocking .beads/.`, "git add .beads/", addResult.exitCode);
31158
31602
  }
31159
- const commitResult = await withTimeout(runGitCommand(["commit", "-m", "chore: sync beads"]), TIMEOUT_MS, "git commit");
31603
+ const commitResult = await withTimeout2(runGitCommand(["commit", "-m", "chore: sync beads"]), TIMEOUT_MS, "git commit");
31160
31604
  if (commitResult.exitCode !== 0 && !commitResult.stdout.includes("nothing to commit")) {
31161
31605
  throw new BeadError(`Failed to commit beads because git commit failed: ${commitResult.stderr}. Try: Check git config (user.name, user.email) with 'git config --list', verify working tree is clean, or check for pre-commit hooks blocking commit.`, "git commit", commitResult.exitCode);
31162
31606
  }
@@ -31185,7 +31629,7 @@ var beads_sync = tool({
31185
31629
  console.warn(`[beads] Stash failed (${stashResult.stderr}), attempting pull anyway...`);
31186
31630
  }
31187
31631
  }
31188
- const pullResult = await withTimeout(runGitCommand(["pull", "--rebase"]), TIMEOUT_MS, "git pull --rebase");
31632
+ const pullResult = await withTimeout2(runGitCommand(["pull", "--rebase"]), TIMEOUT_MS, "git pull --rebase");
31189
31633
  if (didStash) {
31190
31634
  console.warn("[beads] Restoring stashed changes...");
31191
31635
  const unstashResult = await runGitCommand(["stash", "pop"]);
@@ -31199,12 +31643,12 @@ var beads_sync = tool({
31199
31643
  if (pullResult.exitCode !== 0) {
31200
31644
  throw new BeadError(`Failed to pull because git pull --rebase failed: ${pullResult.stderr}. Try: Resolve merge conflicts manually with 'git status', check if remote is accessible with 'git remote -v', or use skip_verification to bypass automatic pull.`, "git pull --rebase", pullResult.exitCode);
31201
31645
  }
31202
- const importResult = await withTimeout(runBdCommand(["sync", "--import-only"]), TIMEOUT_MS, "bd sync --import-only");
31646
+ const importResult = await withTimeout2(runBdCommand(["sync", "--import-only"]), TIMEOUT_MS, "bd sync --import-only");
31203
31647
  if (importResult.exitCode !== 0) {
31204
31648
  console.warn(`[beads] Import warning: ${importResult.stderr}`);
31205
31649
  }
31206
31650
  }
31207
- const pushResult = await withTimeout(runGitCommand(["push"]), TIMEOUT_MS, "git push");
31651
+ const pushResult = await withTimeout2(runGitCommand(["push"]), TIMEOUT_MS, "git push");
31208
31652
  if (pushResult.exitCode !== 0) {
31209
31653
  throw new BeadError(`Failed to push because git push failed: ${pushResult.stderr}. Try: Check if remote branch is up to date with 'git pull --rebase', verify push permissions, check remote URL with 'git remote -v', or force push with 'git push --force-with-lease' if safe.`, "git push", pushResult.exitCode);
31210
31654
  }
@@ -34018,20 +34462,29 @@ Only modify these files. Need others? Message the coordinator.
34018
34462
 
34019
34463
  {error_context}
34020
34464
 
34021
- ## [MANDATORY: SWARM MAIL]
34465
+ ## [MANDATORY: SWARM MAIL INITIALIZATION]
34022
34466
 
34023
- **YOU MUST USE SWARM MAIL FOR ALL COORDINATION.** This is non-negotiable.
34467
+ **CRITICAL: YOU MUST INITIALIZE SWARM MAIL BEFORE DOING ANY WORK.**
34024
34468
 
34025
- ### Initialize FIRST (before any work)
34026
- \`\`\`
34027
- swarmmail_init(project_path="$PWD", task_description="{subtask_title}")
34028
- \`\`\`
34469
+ This is your FIRST step - before reading files, before planning, before ANY other action.
34029
34470
 
34030
- ### Reserve Files (if not already reserved by coordinator)
34471
+ ### Step 1: Initialize (REQUIRED - DO THIS FIRST)
34031
34472
  \`\`\`
34032
- swarmmail_reserve(paths=[...files...], reason="{bead_id}: {subtask_title}")
34473
+ swarmmail_init(project_path="{project_path}", task_description="{bead_id}: {subtask_title}")
34033
34474
  \`\`\`
34034
34475
 
34476
+ **This registers you with the coordination system and enables:**
34477
+ - File reservation tracking
34478
+ - Inter-agent communication
34479
+ - Progress monitoring
34480
+ - Conflict detection
34481
+
34482
+ **If you skip this step, your work will not be tracked and swarm_complete will fail.**
34483
+
34484
+ ## [SWARM MAIL USAGE]
34485
+
34486
+ After initialization, use Swarm Mail for coordination:
34487
+
34035
34488
  ### Check Inbox Regularly
34036
34489
  \`\`\`
34037
34490
  swarmmail_inbox() # Check for coordinator messages
@@ -34085,14 +34538,17 @@ As you work, note reusable patterns, best practices, or domain insights:
34085
34538
  - Skills make swarms smarter over time
34086
34539
 
34087
34540
  ## [WORKFLOW]
34088
- 1. **swarmmail_init** - Initialize session FIRST
34541
+ 1. **swarmmail_init** - Initialize session (MANDATORY FIRST STEP)
34089
34542
  2. Read assigned files
34090
34543
  3. Implement changes
34091
34544
  4. **swarmmail_send** - Report progress to coordinator
34092
34545
  5. Verify (typecheck)
34093
34546
  6. **swarm_complete** - Mark done, release reservations
34094
34547
 
34095
- **CRITICAL: Never work silently. Send progress updates via swarmmail_send every significant milestone.**
34548
+ **CRITICAL REQUIREMENTS:**
34549
+ - Step 1 (swarmmail_init) is NON-NEGOTIABLE - do it before anything else
34550
+ - Never work silently - send progress updates via swarmmail_send every significant milestone
34551
+ - If you complete without initializing, swarm_complete will detect this and warn/fail
34096
34552
 
34097
34553
  Begin now.`;
34098
34554
  var EVALUATION_PROMPT = `Evaluate the work completed for this subtask.
@@ -34136,7 +34592,33 @@ function formatSubtaskPromptV2(params) {
34136
34592
  `) : "(no specific files - use judgment)";
34137
34593
  const compressedSection = params.compressed_context ? params.compressed_context : "";
34138
34594
  const errorSection = params.error_context ? params.error_context : "";
34139
- return SUBTASK_PROMPT_V2.replace(/{bead_id}/g, params.bead_id).replace(/{epic_id}/g, params.epic_id).replace("{subtask_title}", params.subtask_title).replace("{subtask_description}", params.subtask_description || "(see title)").replace("{file_list}", fileList).replace("{shared_context}", params.shared_context || "(none)").replace("{compressed_context}", compressedSection).replace("{error_context}", errorSection);
34595
+ let recoverySection = "";
34596
+ if (params.recovery_context) {
34597
+ const sections = [];
34598
+ if (params.recovery_context.shared_context) {
34599
+ sections.push(`### Recovery Context
34600
+ ${params.recovery_context.shared_context}`);
34601
+ }
34602
+ if (params.recovery_context.skills_to_load && params.recovery_context.skills_to_load.length > 0) {
34603
+ sections.push(`### Skills to Load
34604
+ Before starting work, load these skills for specialized guidance:
34605
+ ${params.recovery_context.skills_to_load.map((s) => `- skills_use(name="${s}")`).join(`
34606
+ `)}`);
34607
+ }
34608
+ if (params.recovery_context.coordinator_notes) {
34609
+ sections.push(`### Coordinator Notes
34610
+ ${params.recovery_context.coordinator_notes}`);
34611
+ }
34612
+ if (sections.length > 0) {
34613
+ recoverySection = `
34614
+ ## [RECOVERY CONTEXT]
34615
+
34616
+ ${sections.join(`
34617
+
34618
+ `)}`;
34619
+ }
34620
+ }
34621
+ return SUBTASK_PROMPT_V2.replace(/{bead_id}/g, params.bead_id).replace(/{epic_id}/g, params.epic_id).replace(/{project_path}/g, params.project_path || "$PWD").replace("{subtask_title}", params.subtask_title).replace("{subtask_description}", params.subtask_description || "(see title)").replace("{file_list}", fileList).replace("{shared_context}", params.shared_context || "(none)").replace("{compressed_context}", compressedSection).replace("{error_context}", errorSection + recoverySection);
34140
34622
  }
34141
34623
  function formatSubtaskPrompt(params) {
34142
34624
  const fileList = params.files.map((f) => `- \`${f}\``).join(`
@@ -34157,7 +34639,8 @@ var swarm_subtask_prompt = tool({
34157
34639
  subtask_title: tool.schema.string().describe("Subtask title"),
34158
34640
  subtask_description: tool.schema.string().optional().describe("Detailed subtask instructions"),
34159
34641
  files: tool.schema.array(tool.schema.string()).describe("Files assigned to this subtask"),
34160
- shared_context: tool.schema.string().optional().describe("Context shared across all agents")
34642
+ shared_context: tool.schema.string().optional().describe("Context shared across all agents"),
34643
+ project_path: tool.schema.string().optional().describe("Absolute project path for swarmmail_init")
34161
34644
  },
34162
34645
  async execute(args) {
34163
34646
  const prompt = formatSubtaskPrompt({
@@ -34173,14 +34656,20 @@ var swarm_subtask_prompt = tool({
34173
34656
  }
34174
34657
  });
34175
34658
  var swarm_spawn_subtask = tool({
34176
- description: "Prepare a subtask for spawning. Returns prompt with Agent Mail/beads instructions.",
34659
+ description: "Prepare a subtask for spawning. Returns prompt with Agent Mail/beads instructions. IMPORTANT: Pass project_path for swarmmail_init.",
34177
34660
  args: {
34178
34661
  bead_id: tool.schema.string().describe("Subtask bead ID"),
34179
34662
  epic_id: tool.schema.string().describe("Parent epic bead ID"),
34180
34663
  subtask_title: tool.schema.string().describe("Subtask title"),
34181
34664
  subtask_description: tool.schema.string().optional().describe("Detailed subtask instructions"),
34182
34665
  files: tool.schema.array(tool.schema.string()).describe("Files assigned to this subtask"),
34183
- shared_context: tool.schema.string().optional().describe("Context shared across all agents")
34666
+ shared_context: tool.schema.string().optional().describe("Context shared across all agents"),
34667
+ project_path: tool.schema.string().optional().describe("Absolute project path for swarmmail_init (REQUIRED for tracking)"),
34668
+ recovery_context: tool.schema.object({
34669
+ shared_context: tool.schema.string().optional(),
34670
+ skills_to_load: tool.schema.array(tool.schema.string()).optional(),
34671
+ coordinator_notes: tool.schema.string().optional()
34672
+ }).optional().describe("Recovery context from checkpoint compaction")
34184
34673
  },
34185
34674
  async execute(args) {
34186
34675
  const prompt = formatSubtaskPromptV2({
@@ -34189,13 +34678,17 @@ var swarm_spawn_subtask = tool({
34189
34678
  subtask_title: args.subtask_title,
34190
34679
  subtask_description: args.subtask_description || "",
34191
34680
  files: args.files,
34192
- shared_context: args.shared_context
34681
+ shared_context: args.shared_context,
34682
+ project_path: args.project_path,
34683
+ recovery_context: args.recovery_context
34193
34684
  });
34194
34685
  return JSON.stringify({
34195
34686
  prompt,
34196
34687
  bead_id: args.bead_id,
34197
34688
  epic_id: args.epic_id,
34198
- files: args.files
34689
+ files: args.files,
34690
+ project_path: args.project_path,
34691
+ recovery_context: args.recovery_context
34199
34692
  }, null, 2);
34200
34693
  }
34201
34694
  });
@@ -34317,6 +34810,9 @@ var promptTools = {
34317
34810
  init_dist();
34318
34811
  init_zod();
34319
34812
  init_swarm_mail();
34813
+ init_projections();
34814
+ init_events();
34815
+ init_store();
34320
34816
  init_learning();
34321
34817
  init_skills();
34322
34818
  async function queryEpicSubtasks(epicId) {
@@ -34746,7 +35242,57 @@ var swarm_progress = tool({
34746
35242
  threadId: epicId,
34747
35243
  importance: args.status === "blocked" ? "high" : "normal"
34748
35244
  });
34749
- return `Progress reported: ${args.status}${args.progress_percent !== undefined ? ` (${args.progress_percent}%)` : ""}`;
35245
+ let checkpointCreated = false;
35246
+ if (args.progress_percent !== undefined && args.files_touched && args.files_touched.length > 0) {
35247
+ const milestones = [25, 50, 75];
35248
+ if (milestones.includes(args.progress_percent)) {
35249
+ try {
35250
+ const checkpoint = {
35251
+ epic_id: epicId,
35252
+ bead_id: args.bead_id,
35253
+ strategy: "file-based",
35254
+ files: args.files_touched,
35255
+ dependencies: [],
35256
+ directives: {},
35257
+ recovery: {
35258
+ last_checkpoint: Date.now(),
35259
+ files_modified: args.files_touched,
35260
+ progress_percent: args.progress_percent,
35261
+ last_message: args.message
35262
+ }
35263
+ };
35264
+ const event = createEvent("swarm_checkpointed", {
35265
+ project_key: args.project_key,
35266
+ ...checkpoint
35267
+ });
35268
+ await appendEvent(event, args.project_key);
35269
+ const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_streams(), exports_streams));
35270
+ const db = await getDatabase2(args.project_key);
35271
+ const now = Date.now();
35272
+ await db.query(`INSERT INTO swarm_contexts (id, epic_id, bead_id, strategy, files, dependencies, directives, recovery, created_at, updated_at)
35273
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
35274
+ ON CONFLICT (id) DO UPDATE SET
35275
+ files = EXCLUDED.files,
35276
+ recovery = EXCLUDED.recovery,
35277
+ updated_at = EXCLUDED.updated_at`, [
35278
+ args.bead_id,
35279
+ epicId,
35280
+ args.bead_id,
35281
+ checkpoint.strategy,
35282
+ JSON.stringify(checkpoint.files),
35283
+ JSON.stringify(checkpoint.dependencies),
35284
+ JSON.stringify(checkpoint.directives),
35285
+ JSON.stringify(checkpoint.recovery),
35286
+ now,
35287
+ now
35288
+ ]);
35289
+ checkpointCreated = true;
35290
+ } catch (error45) {
35291
+ console.warn(`[swarm_progress] Auto-checkpoint failed at ${args.progress_percent}%:`, error45);
35292
+ }
35293
+ }
35294
+ }
35295
+ return `Progress reported: ${args.status}${args.progress_percent !== undefined ? ` (${args.progress_percent}%)` : ""}${checkpointCreated ? " [checkpoint created]" : ""}`;
34750
35296
  }
34751
35297
  });
34752
35298
  var swarm_broadcast = tool({
@@ -34806,125 +35352,205 @@ var swarm_complete = tool({
34806
35352
  evaluation: tool.schema.string().optional().describe("Self-evaluation JSON (Evaluation schema)"),
34807
35353
  files_touched: tool.schema.array(tool.schema.string()).optional().describe("Files modified - will be verified (UBS, typecheck, tests)"),
34808
35354
  skip_ubs_scan: tool.schema.boolean().optional().describe("Skip UBS bug scan (default: false)"),
34809
- skip_verification: tool.schema.boolean().optional().describe("Skip ALL verification (UBS, typecheck, tests). Use sparingly! (default: false)")
35355
+ skip_verification: tool.schema.boolean().optional().describe("Skip ALL verification (UBS, typecheck, tests). Use sparingly! (default: false)"),
35356
+ planned_files: tool.schema.array(tool.schema.string()).optional().describe("Files that were originally planned to be modified"),
35357
+ start_time: tool.schema.number().optional().describe("Task start timestamp (Unix ms) for duration calculation"),
35358
+ error_count: tool.schema.number().optional().describe("Number of errors encountered during task"),
35359
+ retry_count: tool.schema.number().optional().describe("Number of retry attempts during task")
34810
35360
  },
34811
35361
  async execute(args) {
34812
- let verificationResult = null;
34813
- if (!args.skip_verification && args.files_touched?.length) {
34814
- verificationResult = await runVerificationGate(args.files_touched, args.skip_ubs_scan ?? false);
34815
- if (!verificationResult.passed) {
34816
- return JSON.stringify({
34817
- success: false,
34818
- error: "Verification Gate FAILED - fix issues before completing",
34819
- verification: {
34820
- passed: false,
34821
- summary: verificationResult.summary,
34822
- blockers: verificationResult.blockers,
34823
- steps: verificationResult.steps.map((s) => ({
34824
- name: s.name,
34825
- passed: s.passed,
34826
- skipped: s.skipped,
34827
- skipReason: s.skipReason,
34828
- error: s.error?.slice(0, 200)
34829
- }))
34830
- },
34831
- hint: verificationResult.blockers.length > 0 ? `Fix these issues: ${verificationResult.blockers.map((b, i) => `${i + 1}. ${b}`).join(", ")}. Use skip_verification=true only as last resort.` : "Fix the failing checks and try again. Use skip_verification=true only as last resort.",
34832
- gate_function: "IDENTIFY → RUN → READ → VERIFY → CLAIM (you are at VERIFY, claim blocked)"
34833
- }, null, 2);
35362
+ const epicId = args.bead_id.includes(".") ? args.bead_id.split(".")[0] : args.bead_id;
35363
+ try {
35364
+ const projectKey = args.project_key.replace(/\//g, "-").replace(/\\/g, "-");
35365
+ let agentRegistered = false;
35366
+ let registrationWarning = "";
35367
+ try {
35368
+ const agent = await getAgent(projectKey, args.agent_name, args.project_key);
35369
+ agentRegistered = agent !== null;
35370
+ if (!agentRegistered) {
35371
+ registrationWarning = `⚠️ WARNING: Agent '${args.agent_name}' was NOT registered in swarm-mail for project '${projectKey}'.
35372
+
35373
+ This usually means you skipped the MANDATORY swarmmail_init step.
35374
+
35375
+ **Impact:**
35376
+ - Your work was not tracked in the coordination system
35377
+ - File reservations may not have been managed
35378
+ - Other agents couldn't coordinate with you
35379
+ - Learning/eval data may be incomplete
35380
+
35381
+ **Next time:** Run swarmmail_init(project_path="${args.project_key}", task_description="<task>") FIRST, before any other work.
35382
+
35383
+ Continuing with completion, but this should be fixed for future subtasks.`;
35384
+ console.warn(`[swarm_complete] ${registrationWarning}`);
35385
+ }
35386
+ } catch (error45) {
35387
+ console.warn(`[swarm_complete] Could not verify agent registration:`, error45);
35388
+ registrationWarning = `ℹ️ Could not verify swarm-mail registration (database may not be available). Consider running swarmmail_init next time.`;
34834
35389
  }
34835
- }
34836
- let ubsResult = null;
34837
- if (!args.skip_verification && !verificationResult && args.files_touched?.length && !args.skip_ubs_scan) {
34838
- ubsResult = await runUbsScan(args.files_touched);
34839
- if (ubsResult && ubsResult.summary.critical > 0) {
34840
- return JSON.stringify({
34841
- success: false,
34842
- error: `UBS found ${ubsResult.summary.critical} critical bug(s) that must be fixed before completing`,
34843
- ubs_scan: {
34844
- critical_count: ubsResult.summary.critical,
34845
- bugs: ubsResult.bugs.filter((b) => b.severity === "critical")
34846
- },
34847
- hint: `Fix these critical bugs: ${ubsResult.bugs.filter((b) => b.severity === "critical").map((b) => `${b.file}:${b.line} - ${b.message}`).slice(0, 3).join("; ")}. Try: Run 'ubs scan ${args.files_touched?.join(" ") || "."} --json' for full report, fix reported issues, or use skip_ubs_scan=true to bypass (not recommended).`
34848
- }, null, 2);
35390
+ let verificationResult = null;
35391
+ if (!args.skip_verification && args.files_touched?.length) {
35392
+ verificationResult = await runVerificationGate(args.files_touched, args.skip_ubs_scan ?? false);
35393
+ if (!verificationResult.passed) {
35394
+ return JSON.stringify({
35395
+ success: false,
35396
+ error: "Verification Gate FAILED - fix issues before completing",
35397
+ verification: {
35398
+ passed: false,
35399
+ summary: verificationResult.summary,
35400
+ blockers: verificationResult.blockers,
35401
+ steps: verificationResult.steps.map((s) => ({
35402
+ name: s.name,
35403
+ passed: s.passed,
35404
+ skipped: s.skipped,
35405
+ skipReason: s.skipReason,
35406
+ error: s.error?.slice(0, 200)
35407
+ }))
35408
+ },
35409
+ hint: verificationResult.blockers.length > 0 ? `Fix these issues: ${verificationResult.blockers.map((b, i) => `${i + 1}. ${b}`).join(", ")}. Use skip_verification=true only as last resort.` : "Fix the failing checks and try again. Use skip_verification=true only as last resort.",
35410
+ gate_function: "IDENTIFY → RUN → READ → VERIFY → CLAIM (you are at VERIFY, claim blocked)"
35411
+ }, null, 2);
35412
+ }
35413
+ }
35414
+ let ubsResult = null;
35415
+ if (!args.skip_verification && !verificationResult && args.files_touched?.length && !args.skip_ubs_scan) {
35416
+ ubsResult = await runUbsScan(args.files_touched);
35417
+ if (ubsResult && ubsResult.summary.critical > 0) {
35418
+ return JSON.stringify({
35419
+ success: false,
35420
+ error: `UBS found ${ubsResult.summary.critical} critical bug(s) that must be fixed before completing`,
35421
+ ubs_scan: {
35422
+ critical_count: ubsResult.summary.critical,
35423
+ bugs: ubsResult.bugs.filter((b) => b.severity === "critical")
35424
+ },
35425
+ hint: `Fix these critical bugs: ${ubsResult.bugs.filter((b) => b.severity === "critical").map((b) => `${b.file}:${b.line} - ${b.message}`).slice(0, 3).join("; ")}. Try: Run 'ubs scan ${args.files_touched?.join(" ") || "."} --json' for full report, fix reported issues, or use skip_ubs_scan=true to bypass (not recommended).`
35426
+ }, null, 2);
35427
+ }
35428
+ }
35429
+ let parsedEvaluation;
35430
+ if (args.evaluation) {
35431
+ try {
35432
+ parsedEvaluation = EvaluationSchema.parse(JSON.parse(args.evaluation));
35433
+ } catch (error45) {
35434
+ return JSON.stringify({
35435
+ success: false,
35436
+ error: "Invalid evaluation format",
35437
+ details: error45 instanceof exports_external.ZodError ? error45.issues : String(error45)
35438
+ }, null, 2);
35439
+ }
35440
+ if (!parsedEvaluation.passed) {
35441
+ return JSON.stringify({
35442
+ success: false,
35443
+ error: "Self-evaluation failed",
35444
+ retry_suggestion: parsedEvaluation.retry_suggestion,
35445
+ feedback: parsedEvaluation.overall_feedback
35446
+ }, null, 2);
35447
+ }
35448
+ }
35449
+ const closeResult = await Bun.$`bd close ${args.bead_id} --reason ${args.summary} --json`.quiet().nothrow();
35450
+ if (closeResult.exitCode !== 0) {
35451
+ throw new Error(`Failed to close bead because bd close command failed: ${closeResult.stderr.toString()}. Try: Verify bead exists and is not already closed with 'bd show ${args.bead_id}', check if bead ID is correct with 'beads_query()', or use beads_close tool directly.`);
34849
35452
  }
34850
- }
34851
- let parsedEvaluation;
34852
- if (args.evaluation) {
34853
35453
  try {
34854
- parsedEvaluation = EvaluationSchema.parse(JSON.parse(args.evaluation));
35454
+ const epicId3 = args.bead_id.includes(".") ? args.bead_id.split(".")[0] : args.bead_id;
35455
+ const durationMs2 = args.start_time ? Date.now() - args.start_time : 0;
35456
+ const event = createEvent("subtask_outcome", {
35457
+ project_key: args.project_key,
35458
+ epic_id: epicId3,
35459
+ bead_id: args.bead_id,
35460
+ planned_files: args.planned_files || [],
35461
+ actual_files: args.files_touched || [],
35462
+ duration_ms: durationMs2,
35463
+ error_count: args.error_count || 0,
35464
+ retry_count: args.retry_count || 0,
35465
+ success: true
35466
+ });
35467
+ await appendEvent(event, args.project_key);
34855
35468
  } catch (error45) {
34856
- return JSON.stringify({
34857
- success: false,
34858
- error: "Invalid evaluation format",
34859
- details: error45 instanceof exports_external.ZodError ? error45.issues : String(error45)
34860
- }, null, 2);
35469
+ console.warn("[swarm_complete] Failed to emit SubtaskOutcomeEvent:", error45);
34861
35470
  }
34862
- if (!parsedEvaluation.passed) {
34863
- return JSON.stringify({
34864
- success: false,
34865
- error: "Self-evaluation failed",
34866
- retry_suggestion: parsedEvaluation.retry_suggestion,
34867
- feedback: parsedEvaluation.overall_feedback
34868
- }, null, 2);
35471
+ let capturedStrategy;
35472
+ const durationMs = args.start_time ? Date.now() - args.start_time : 0;
35473
+ const memoryInfo = formatMemoryStoreOnSuccess(args.bead_id, args.summary, args.files_touched || [], capturedStrategy);
35474
+ let memoryStored = false;
35475
+ let memoryError;
35476
+ try {
35477
+ const memoryAvailable = await isToolAvailable("semantic-memory");
35478
+ if (memoryAvailable) {
35479
+ const storeResult = await Bun.$`semantic-memory store ${memoryInfo.information} --metadata ${memoryInfo.metadata}`.quiet().nothrow();
35480
+ if (storeResult.exitCode === 0) {
35481
+ memoryStored = true;
35482
+ console.log(`[swarm_complete] Stored learning for ${args.bead_id} in semantic-memory`);
35483
+ } else {
35484
+ memoryError = `semantic-memory store failed: ${storeResult.stderr.toString().slice(0, 200)}`;
35485
+ console.warn(`[swarm_complete] ${memoryError}`);
35486
+ }
35487
+ } else {
35488
+ memoryError = "semantic-memory not available - learning stored in-memory only";
35489
+ warnMissingTool("semantic-memory");
35490
+ }
35491
+ } catch (error45) {
35492
+ memoryError = `Failed to store memory: ${error45 instanceof Error ? error45.message : String(error45)}`;
35493
+ console.warn(`[swarm_complete] ${memoryError}`);
34869
35494
  }
34870
- }
34871
- const closeResult = await Bun.$`bd close ${args.bead_id} --reason ${args.summary} --json`.quiet().nothrow();
34872
- if (closeResult.exitCode !== 0) {
34873
- throw new Error(`Failed to close bead because bd close command failed: ${closeResult.stderr.toString()}. Try: Verify bead exists and is not already closed with 'bd show ${args.bead_id}', check if bead ID is correct with 'beads_query()', or use beads_close tool directly.`);
34874
- }
34875
- try {
34876
- await releaseSwarmFiles({
35495
+ try {
35496
+ await releaseSwarmFiles({
35497
+ projectPath: args.project_key,
35498
+ agentName: args.agent_name
35499
+ });
35500
+ } catch (error45) {
35501
+ console.warn(`[swarm] Failed to release file reservations for ${args.agent_name}:`, error45);
35502
+ }
35503
+ const epicId2 = args.bead_id.includes(".") ? args.bead_id.split(".")[0] : args.bead_id;
35504
+ const completionBody = [
35505
+ `## Subtask Complete: ${args.bead_id}`,
35506
+ "",
35507
+ `**Summary**: ${args.summary}`,
35508
+ "",
35509
+ parsedEvaluation ? `**Self-Evaluation**: ${parsedEvaluation.passed ? "PASSED" : "FAILED"}` : "",
35510
+ parsedEvaluation?.overall_feedback ? `**Feedback**: ${parsedEvaluation.overall_feedback}` : "",
35511
+ "",
35512
+ `**Memory Capture**: ${memoryStored ? "✓ Stored in semantic-memory" : `✗ ${memoryError || "Failed"}`}`
35513
+ ].filter(Boolean).join(`
35514
+ `);
35515
+ await sendSwarmMessage({
34877
35516
  projectPath: args.project_key,
34878
- agentName: args.agent_name
35517
+ fromAgent: args.agent_name,
35518
+ toAgents: [],
35519
+ subject: `Complete: ${args.bead_id}`,
35520
+ body: completionBody,
35521
+ threadId: epicId2,
35522
+ importance: "normal"
34879
35523
  });
34880
- } catch (error45) {
34881
- console.warn(`[swarm] Failed to release file reservations for ${args.agent_name}:`, error45);
34882
- }
34883
- const epicId = args.bead_id.includes(".") ? args.bead_id.split(".")[0] : args.bead_id;
34884
- const completionBody = [
34885
- `## Subtask Complete: ${args.bead_id}`,
34886
- "",
34887
- `**Summary**: ${args.summary}`,
34888
- "",
34889
- parsedEvaluation ? `**Self-Evaluation**: ${parsedEvaluation.passed ? "PASSED" : "FAILED"}` : "",
34890
- parsedEvaluation?.overall_feedback ? `**Feedback**: ${parsedEvaluation.overall_feedback}` : ""
34891
- ].filter(Boolean).join(`
34892
- `);
34893
- await sendSwarmMessage({
34894
- projectPath: args.project_key,
34895
- fromAgent: args.agent_name,
34896
- toAgents: [],
34897
- subject: `Complete: ${args.bead_id}`,
34898
- body: completionBody,
34899
- threadId: epicId,
34900
- importance: "normal"
34901
- });
34902
- const response = {
34903
- success: true,
34904
- bead_id: args.bead_id,
34905
- closed: true,
34906
- reservations_released: true,
34907
- message_sent: true,
34908
- verification_gate: verificationResult ? {
34909
- passed: true,
34910
- summary: verificationResult.summary,
34911
- steps: verificationResult.steps.map((s) => ({
34912
- name: s.name,
34913
- passed: s.passed,
34914
- skipped: s.skipped,
34915
- skipReason: s.skipReason
34916
- }))
34917
- } : args.skip_verification ? { skipped: true, reason: "skip_verification=true" } : { skipped: true, reason: "no files_touched provided" },
34918
- ubs_scan: ubsResult ? {
34919
- ran: true,
34920
- bugs_found: ubsResult.summary.total,
34921
- summary: ubsResult.summary,
34922
- warnings: ubsResult.bugs.filter((b) => b.severity !== "critical")
34923
- } : verificationResult ? { ran: true, included_in_verification_gate: true } : {
34924
- ran: false,
34925
- reason: args.skip_ubs_scan ? "skipped" : "no files or ubs unavailable"
34926
- },
34927
- learning_prompt: `## Reflection
35524
+ const response = {
35525
+ success: true,
35526
+ bead_id: args.bead_id,
35527
+ closed: true,
35528
+ reservations_released: true,
35529
+ message_sent: true,
35530
+ agent_registration: {
35531
+ verified: agentRegistered,
35532
+ warning: registrationWarning || undefined
35533
+ },
35534
+ verification_gate: verificationResult ? {
35535
+ passed: true,
35536
+ summary: verificationResult.summary,
35537
+ steps: verificationResult.steps.map((s) => ({
35538
+ name: s.name,
35539
+ passed: s.passed,
35540
+ skipped: s.skipped,
35541
+ skipReason: s.skipReason
35542
+ }))
35543
+ } : args.skip_verification ? { skipped: true, reason: "skip_verification=true" } : { skipped: true, reason: "no files_touched provided" },
35544
+ ubs_scan: ubsResult ? {
35545
+ ran: true,
35546
+ bugs_found: ubsResult.summary.total,
35547
+ summary: ubsResult.summary,
35548
+ warnings: ubsResult.bugs.filter((b) => b.severity !== "critical")
35549
+ } : verificationResult ? { ran: true, included_in_verification_gate: true } : {
35550
+ ran: false,
35551
+ reason: args.skip_ubs_scan ? "skipped" : "no files or ubs unavailable"
35552
+ },
35553
+ learning_prompt: `## Reflection
34928
35554
 
34929
35555
  Did you learn anything reusable during this subtask? Consider:
34930
35556
 
@@ -34936,9 +35562,81 @@ Did you learn anything reusable during this subtask? Consider:
34936
35562
  If you discovered something valuable, use \`swarm_learn\` or \`skills_create\` to preserve it as a skill for future swarms.
34937
35563
 
34938
35564
  Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
34939
- memory_store: formatMemoryStoreOnSuccess(args.bead_id, args.summary, args.files_touched || [])
34940
- };
34941
- return JSON.stringify(response, null, 2);
35565
+ memory_capture: {
35566
+ attempted: true,
35567
+ stored: memoryStored,
35568
+ error: memoryError,
35569
+ information: memoryInfo.information,
35570
+ metadata: memoryInfo.metadata,
35571
+ note: memoryStored ? "Learning automatically stored in semantic-memory" : `Failed to store: ${memoryError}. Learning lost unless semantic-memory is available.`
35572
+ }
35573
+ };
35574
+ return JSON.stringify(response, null, 2);
35575
+ } catch (error45) {
35576
+ const errorMessage = error45 instanceof Error ? error45.message : String(error45);
35577
+ const errorStack = error45 instanceof Error ? error45.stack : undefined;
35578
+ let failedStep = "unknown";
35579
+ if (errorMessage.includes("verification")) {
35580
+ failedStep = "Verification Gate (UBS/typecheck/tests)";
35581
+ } else if (errorMessage.includes("UBS") || errorMessage.includes("ubs")) {
35582
+ failedStep = "UBS scan";
35583
+ } else if (errorMessage.includes("evaluation")) {
35584
+ failedStep = "Self-evaluation parsing";
35585
+ } else if (errorMessage.includes("bead") || errorMessage.includes("close")) {
35586
+ failedStep = "Bead close";
35587
+ } else if (errorMessage.includes("memory") || errorMessage.includes("semantic")) {
35588
+ failedStep = "Memory storage (non-fatal)";
35589
+ } else if (errorMessage.includes("reservation") || errorMessage.includes("release")) {
35590
+ failedStep = "File reservation release";
35591
+ } else if (errorMessage.includes("message") || errorMessage.includes("mail")) {
35592
+ failedStep = "Swarm mail notification";
35593
+ }
35594
+ const errorBody = [
35595
+ `## ⚠️ SWARM_COMPLETE FAILED`,
35596
+ "",
35597
+ `**Bead**: ${args.bead_id}`,
35598
+ `**Agent**: ${args.agent_name}`,
35599
+ `**Failed Step**: ${failedStep}`,
35600
+ "",
35601
+ `### Error Message`,
35602
+ "```",
35603
+ errorMessage,
35604
+ "```",
35605
+ "",
35606
+ errorStack ? `### Stack Trace
35607
+ \`\`\`
35608
+ ${errorStack.slice(0, 1000)}
35609
+ \`\`\`
35610
+ ` : "",
35611
+ `### Context`,
35612
+ `- **Summary**: ${args.summary}`,
35613
+ `- **Files touched**: ${args.files_touched?.length ? args.files_touched.join(", ") : "none"}`,
35614
+ `- **Skip UBS**: ${args.skip_ubs_scan ?? false}`,
35615
+ `- **Skip verification**: ${args.skip_verification ?? false}`,
35616
+ "",
35617
+ `### Recovery Actions`,
35618
+ "1. Check error message for specific issue",
35619
+ "2. Review failed step (UBS scan, typecheck, bead close, etc.)",
35620
+ "3. Fix underlying issue or use skip flags if appropriate",
35621
+ "4. Retry swarm_complete after fixing"
35622
+ ].filter(Boolean).join(`
35623
+ `);
35624
+ try {
35625
+ await sendSwarmMessage({
35626
+ projectPath: args.project_key,
35627
+ fromAgent: args.agent_name,
35628
+ toAgents: [],
35629
+ subject: `FAILED: swarm_complete for ${args.bead_id}`,
35630
+ body: errorBody,
35631
+ threadId: epicId,
35632
+ importance: "urgent"
35633
+ });
35634
+ } catch (mailError) {
35635
+ console.error(`[swarm_complete] CRITICAL: Failed to notify coordinator of failure for ${args.bead_id}:`, mailError);
35636
+ console.error(`[swarm_complete] Original error:`, error45);
35637
+ }
35638
+ throw error45;
35639
+ }
34942
35640
  }
34943
35641
  });
34944
35642
  var swarm_record_outcome = tool({
@@ -35166,6 +35864,147 @@ var swarm_check_strikes = tool({
35166
35864
  }
35167
35865
  }
35168
35866
  });
35867
+ var swarm_checkpoint = tool({
35868
+ description: "Checkpoint swarm context for recovery. Records current state for crash recovery. Non-fatal errors.",
35869
+ args: {
35870
+ project_key: tool.schema.string().describe("Project path"),
35871
+ agent_name: tool.schema.string().describe("Agent name"),
35872
+ bead_id: tool.schema.string().describe("Subtask bead ID"),
35873
+ epic_id: tool.schema.string().describe("Epic bead ID"),
35874
+ files_modified: tool.schema.array(tool.schema.string()).describe("Files modified so far"),
35875
+ progress_percent: tool.schema.number().min(0).max(100).describe("Current progress"),
35876
+ directives: tool.schema.object({
35877
+ shared_context: tool.schema.string().optional(),
35878
+ skills_to_load: tool.schema.array(tool.schema.string()).optional(),
35879
+ coordinator_notes: tool.schema.string().optional()
35880
+ }).optional().describe("Coordinator directives for this subtask"),
35881
+ error_context: tool.schema.string().optional().describe("Error context if checkpoint is during error handling")
35882
+ },
35883
+ async execute(args) {
35884
+ try {
35885
+ const checkpoint = {
35886
+ epic_id: args.epic_id,
35887
+ bead_id: args.bead_id,
35888
+ strategy: "file-based",
35889
+ files: args.files_modified,
35890
+ dependencies: [],
35891
+ directives: args.directives || {},
35892
+ recovery: {
35893
+ last_checkpoint: Date.now(),
35894
+ files_modified: args.files_modified,
35895
+ progress_percent: args.progress_percent,
35896
+ error_context: args.error_context
35897
+ }
35898
+ };
35899
+ const event = createEvent("swarm_checkpointed", {
35900
+ project_key: args.project_key,
35901
+ epic_id: args.epic_id,
35902
+ bead_id: args.bead_id,
35903
+ strategy: checkpoint.strategy,
35904
+ files: checkpoint.files,
35905
+ dependencies: checkpoint.dependencies,
35906
+ directives: checkpoint.directives,
35907
+ recovery: checkpoint.recovery
35908
+ });
35909
+ await appendEvent(event, args.project_key);
35910
+ const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_streams(), exports_streams));
35911
+ const db = await getDatabase2(args.project_key);
35912
+ const now = Date.now();
35913
+ await db.query(`INSERT INTO swarm_contexts (id, epic_id, bead_id, strategy, files, dependencies, directives, recovery, created_at, updated_at)
35914
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
35915
+ ON CONFLICT (id) DO UPDATE SET
35916
+ files = EXCLUDED.files,
35917
+ recovery = EXCLUDED.recovery,
35918
+ updated_at = EXCLUDED.updated_at`, [
35919
+ args.bead_id,
35920
+ args.epic_id,
35921
+ args.bead_id,
35922
+ checkpoint.strategy,
35923
+ JSON.stringify(checkpoint.files),
35924
+ JSON.stringify(checkpoint.dependencies),
35925
+ JSON.stringify(checkpoint.directives),
35926
+ JSON.stringify(checkpoint.recovery),
35927
+ now,
35928
+ now
35929
+ ]);
35930
+ return JSON.stringify({
35931
+ success: true,
35932
+ checkpoint_timestamp: now,
35933
+ summary: `Checkpoint saved for ${args.bead_id} at ${args.progress_percent}%`,
35934
+ bead_id: args.bead_id,
35935
+ epic_id: args.epic_id,
35936
+ files_tracked: args.files_modified.length
35937
+ }, null, 2);
35938
+ } catch (error45) {
35939
+ console.warn(`[swarm_checkpoint] Failed to checkpoint ${args.bead_id}:`, error45);
35940
+ return JSON.stringify({
35941
+ success: false,
35942
+ warning: "Checkpoint failed but continuing",
35943
+ error: error45 instanceof Error ? error45.message : String(error45),
35944
+ bead_id: args.bead_id,
35945
+ note: "This is non-fatal. Work can continue without checkpoint."
35946
+ }, null, 2);
35947
+ }
35948
+ }
35949
+ });
35950
+ var swarm_recover = tool({
35951
+ description: "Recover swarm context from last checkpoint. Returns context or null if not found.",
35952
+ args: {
35953
+ project_key: tool.schema.string().describe("Project path"),
35954
+ epic_id: tool.schema.string().describe("Epic bead ID to recover")
35955
+ },
35956
+ async execute(args) {
35957
+ try {
35958
+ const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_streams(), exports_streams));
35959
+ const db = await getDatabase2(args.project_key);
35960
+ const result = await db.query(`SELECT * FROM swarm_contexts
35961
+ WHERE epic_id = $1
35962
+ ORDER BY updated_at DESC
35963
+ LIMIT 1`, [args.epic_id]);
35964
+ if (result.rows.length === 0) {
35965
+ return JSON.stringify({
35966
+ found: false,
35967
+ message: `No checkpoint found for epic ${args.epic_id}`,
35968
+ epic_id: args.epic_id
35969
+ }, null, 2);
35970
+ }
35971
+ const row = result.rows[0];
35972
+ const context = {
35973
+ id: row.id,
35974
+ epic_id: row.epic_id,
35975
+ bead_id: row.bead_id,
35976
+ strategy: row.strategy,
35977
+ files: JSON.parse(row.files),
35978
+ dependencies: JSON.parse(row.dependencies),
35979
+ directives: JSON.parse(row.directives),
35980
+ recovery: JSON.parse(row.recovery),
35981
+ created_at: row.created_at,
35982
+ updated_at: row.updated_at
35983
+ };
35984
+ const event = createEvent("swarm_recovered", {
35985
+ project_key: args.project_key,
35986
+ epic_id: args.epic_id,
35987
+ bead_id: context.bead_id,
35988
+ recovered_from_checkpoint: context.recovery.last_checkpoint
35989
+ });
35990
+ await appendEvent(event, args.project_key);
35991
+ return JSON.stringify({
35992
+ found: true,
35993
+ context,
35994
+ summary: `Recovered checkpoint from ${new Date(context.updated_at).toISOString()}`,
35995
+ age_seconds: Math.round((Date.now() - context.updated_at) / 1000)
35996
+ }, null, 2);
35997
+ } catch (error45) {
35998
+ console.warn(`[swarm_recover] Failed to recover context for ${args.epic_id}:`, error45);
35999
+ return JSON.stringify({
36000
+ found: false,
36001
+ error: error45 instanceof Error ? error45.message : String(error45),
36002
+ message: `Recovery failed for epic ${args.epic_id}`,
36003
+ epic_id: args.epic_id
36004
+ }, null, 2);
36005
+ }
36006
+ }
36007
+ });
35169
36008
  var swarm_learn = tool({
35170
36009
  description: `Analyze completed work and optionally create a skill from learned patterns.
35171
36010
 
@@ -35306,6 +36145,8 @@ var orchestrateTools = {
35306
36145
  swarm_get_error_context,
35307
36146
  swarm_resolve_error,
35308
36147
  swarm_check_strikes,
36148
+ swarm_checkpoint,
36149
+ swarm_recover,
35309
36150
  swarm_learn
35310
36151
  };
35311
36152
 
@@ -36354,6 +37195,164 @@ var mandateTools = {
36354
37195
  mandate_list,
36355
37196
  mandate_stats
36356
37197
  };
37198
+
37199
+ // src/output-guardrails.ts
37200
+ var DEFAULT_GUARDRAIL_CONFIG = {
37201
+ defaultMaxChars: 32000,
37202
+ toolLimits: {
37203
+ "repo-autopsy_file": 64000,
37204
+ "repo-autopsy_search": 64000,
37205
+ "repo-autopsy_exports_map": 64000,
37206
+ "context7_get-library-docs": 64000,
37207
+ cass_view: 64000,
37208
+ cass_search: 48000,
37209
+ skills_read: 48000,
37210
+ "repo-autopsy_structure": 24000,
37211
+ "repo-autopsy_stats": 16000,
37212
+ cass_stats: 8000
37213
+ },
37214
+ skipTools: [
37215
+ "beads_create",
37216
+ "beads_create_epic",
37217
+ "beads_query",
37218
+ "beads_update",
37219
+ "beads_close",
37220
+ "beads_start",
37221
+ "beads_ready",
37222
+ "beads_sync",
37223
+ "agentmail_init",
37224
+ "agentmail_send",
37225
+ "agentmail_inbox",
37226
+ "agentmail_read_message",
37227
+ "agentmail_summarize_thread",
37228
+ "agentmail_reserve",
37229
+ "agentmail_release",
37230
+ "agentmail_ack",
37231
+ "swarmmail_init",
37232
+ "swarmmail_send",
37233
+ "swarmmail_inbox",
37234
+ "swarmmail_read_message",
37235
+ "swarmmail_reserve",
37236
+ "swarmmail_release",
37237
+ "swarmmail_ack",
37238
+ "structured_extract_json",
37239
+ "structured_validate",
37240
+ "structured_parse_evaluation",
37241
+ "structured_parse_decomposition",
37242
+ "structured_parse_bead_tree",
37243
+ "swarm_select_strategy",
37244
+ "swarm_plan_prompt",
37245
+ "swarm_decompose",
37246
+ "swarm_validate_decomposition",
37247
+ "swarm_status",
37248
+ "swarm_progress",
37249
+ "swarm_complete",
37250
+ "swarm_record_outcome",
37251
+ "swarm_subtask_prompt",
37252
+ "swarm_spawn_subtask",
37253
+ "swarm_complete_subtask",
37254
+ "swarm_evaluation_prompt",
37255
+ "mandate_file",
37256
+ "mandate_vote",
37257
+ "mandate_query",
37258
+ "mandate_list",
37259
+ "mandate_stats"
37260
+ ]
37261
+ };
37262
+ function findMatchingBrace(text, startIdx) {
37263
+ const openChar = text[startIdx];
37264
+ const closeChar = openChar === "{" ? "}" : "]";
37265
+ let depth = 1;
37266
+ for (let i = startIdx + 1;i < text.length; i++) {
37267
+ if (text[i] === openChar) {
37268
+ depth++;
37269
+ } else if (text[i] === closeChar) {
37270
+ depth--;
37271
+ if (depth === 0) {
37272
+ return i;
37273
+ }
37274
+ }
37275
+ }
37276
+ return -1;
37277
+ }
37278
+ function truncateWithBoundaries(text, maxChars) {
37279
+ if (text.length <= maxChars) {
37280
+ return text;
37281
+ }
37282
+ let truncateAt = maxChars;
37283
+ const beforeTruncate = text.slice(0, maxChars);
37284
+ const lastOpenBrace = Math.max(beforeTruncate.lastIndexOf("{"), beforeTruncate.lastIndexOf("["));
37285
+ const lastCloseBrace = Math.max(beforeTruncate.lastIndexOf("}"), beforeTruncate.lastIndexOf("]"));
37286
+ if (lastOpenBrace > lastCloseBrace) {
37287
+ const matchingClose = findMatchingBrace(text, lastOpenBrace);
37288
+ if (matchingClose !== -1 && matchingClose < maxChars * 1.2) {
37289
+ truncateAt = matchingClose + 1;
37290
+ } else {
37291
+ truncateAt = lastOpenBrace;
37292
+ }
37293
+ }
37294
+ const codeBlockMarker = "```";
37295
+ const beforeTruncateForCode = text.slice(0, truncateAt);
37296
+ const codeBlockCount = (beforeTruncateForCode.match(/```/g) || []).length;
37297
+ if (codeBlockCount % 2 === 1) {
37298
+ const closeMarkerIdx = text.indexOf(codeBlockMarker, truncateAt);
37299
+ if (closeMarkerIdx !== -1 && closeMarkerIdx < maxChars * 1.2) {
37300
+ truncateAt = closeMarkerIdx + codeBlockMarker.length;
37301
+ } else {
37302
+ const lastOpenMarker = beforeTruncateForCode.lastIndexOf(codeBlockMarker);
37303
+ if (lastOpenMarker !== -1) {
37304
+ truncateAt = lastOpenMarker;
37305
+ }
37306
+ }
37307
+ }
37308
+ const headerMatch = text.slice(0, truncateAt).match(/\n#{1,6}\s/g);
37309
+ if (headerMatch && headerMatch.length > 0) {
37310
+ const lastHeaderIdx = beforeTruncateForCode.lastIndexOf(`
37311
+ ##`);
37312
+ if (lastHeaderIdx !== -1 && lastHeaderIdx > maxChars * 0.8) {
37313
+ truncateAt = lastHeaderIdx;
37314
+ }
37315
+ }
37316
+ while (truncateAt > 0 && !/\s/.test(text[truncateAt])) {
37317
+ truncateAt--;
37318
+ }
37319
+ const truncated = text.slice(0, truncateAt).trimEnd();
37320
+ const charsRemoved = text.length - truncated.length;
37321
+ return `${truncated}
37322
+
37323
+ [TRUNCATED - ${charsRemoved.toLocaleString()} chars removed]`;
37324
+ }
37325
+ function getToolLimit(toolName, config2 = DEFAULT_GUARDRAIL_CONFIG) {
37326
+ return config2.toolLimits[toolName] ?? config2.defaultMaxChars;
37327
+ }
37328
+ function guardrailOutput(toolName, output, config2 = DEFAULT_GUARDRAIL_CONFIG) {
37329
+ const originalLength = output.length;
37330
+ if (config2.skipTools.includes(toolName)) {
37331
+ return {
37332
+ output,
37333
+ truncated: false,
37334
+ originalLength,
37335
+ truncatedLength: originalLength
37336
+ };
37337
+ }
37338
+ const limit = getToolLimit(toolName, config2);
37339
+ if (originalLength <= limit) {
37340
+ return {
37341
+ output,
37342
+ truncated: false,
37343
+ originalLength,
37344
+ truncatedLength: originalLength
37345
+ };
37346
+ }
37347
+ const truncatedOutput = truncateWithBoundaries(output, limit);
37348
+ const truncatedLength = truncatedOutput.length;
37349
+ return {
37350
+ output: truncatedOutput,
37351
+ truncated: true,
37352
+ originalLength,
37353
+ truncatedLength
37354
+ };
37355
+ }
36357
37356
  // src/storage.ts
36358
37357
  init_learning();
36359
37358
 
@@ -36448,6 +37447,34 @@ class InMemoryMaturityStorage {
36448
37447
  }
36449
37448
  }
36450
37449
 
37450
+ // src/storage.ts
37451
+ function getCollectionNames() {
37452
+ const base = {
37453
+ feedback: "swarm-feedback",
37454
+ patterns: "swarm-patterns",
37455
+ maturity: "swarm-maturity"
37456
+ };
37457
+ if (process.env.TEST_MEMORY_COLLECTIONS === "true") {
37458
+ return {
37459
+ feedback: `${base.feedback}-test`,
37460
+ patterns: `${base.patterns}-test`,
37461
+ maturity: `${base.maturity}-test`
37462
+ };
37463
+ }
37464
+ return base;
37465
+ }
37466
+ var DEFAULT_STORAGE_CONFIG = {
37467
+ backend: "semantic-memory",
37468
+ collections: getCollectionNames(),
37469
+ useSemanticSearch: true
37470
+ };
37471
+ var sessionStats = {
37472
+ storesCount: 0,
37473
+ queriesCount: 0,
37474
+ sessionStart: Date.now(),
37475
+ lastAlertCheck: Date.now()
37476
+ };
37477
+
36451
37478
  // src/index.ts
36452
37479
  init_skills();
36453
37480
  var SwarmPlugin = async (input) => {
@@ -36503,6 +37530,13 @@ var SwarmPlugin = async (input) => {
36503
37530
  },
36504
37531
  "tool.execute.after": async (input2, output) => {
36505
37532
  const toolName = input2.tool;
37533
+ if (output.output && typeof output.output === "string") {
37534
+ const guardrailResult = guardrailOutput(toolName, output.output);
37535
+ if (guardrailResult.truncated) {
37536
+ output.output = guardrailResult.output;
37537
+ console.log(`[swarm-plugin] Guardrail truncated ${toolName}: ${guardrailResult.originalLength} → ${guardrailResult.truncatedLength} chars`);
37538
+ }
37539
+ }
36506
37540
  if (toolName === "agentmail_init" && output.output) {
36507
37541
  try {
36508
37542
  const result = JSON.parse(output.output);