opencode-swarm-plugin 0.35.0 → 0.36.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.
@@ -16,9 +16,54 @@
16
16
  import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin";
17
17
  import { tool } from "@opencode-ai/plugin";
18
18
  import { spawn } from "child_process";
19
+ import { appendFileSync, mkdirSync, existsSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { homedir } from "node:os";
19
22
 
20
23
  const SWARM_CLI = "swarm";
21
24
 
25
+ // =============================================================================
26
+ // File-based Logging (writes to ~/.config/swarm-tools/logs/)
27
+ // =============================================================================
28
+
29
+ const LOG_DIR = join(homedir(), ".config", "swarm-tools", "logs");
30
+ const COMPACTION_LOG = join(LOG_DIR, "compaction.log");
31
+
32
+ /**
33
+ * Ensure log directory exists
34
+ */
35
+ function ensureLogDir(): void {
36
+ if (!existsSync(LOG_DIR)) {
37
+ mkdirSync(LOG_DIR, { recursive: true });
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Log a compaction event to file (JSON lines format, compatible with `swarm log`)
43
+ *
44
+ * @param level - Log level (info, debug, warn, error)
45
+ * @param msg - Log message
46
+ * @param data - Additional structured data
47
+ */
48
+ function logCompaction(
49
+ level: "info" | "debug" | "warn" | "error",
50
+ msg: string,
51
+ data?: Record<string, unknown>,
52
+ ): void {
53
+ try {
54
+ ensureLogDir();
55
+ const entry = JSON.stringify({
56
+ time: new Date().toISOString(),
57
+ level,
58
+ msg,
59
+ ...data,
60
+ });
61
+ appendFileSync(COMPACTION_LOG, entry + "\n");
62
+ } catch {
63
+ // Silently fail - logging should never break the plugin
64
+ }
65
+ }
66
+
22
67
  // Module-level project directory - set during plugin initialization
23
68
  // This is CRITICAL: without it, the CLI uses process.cwd() which may be wrong
24
69
  let projectDirectory: string = process.cwd();
@@ -952,26 +997,69 @@ interface SwarmStateSnapshot {
952
997
  * Shells out to swarm CLI to get real data.
953
998
  */
954
999
  async function querySwarmState(sessionID: string): Promise<SwarmStateSnapshot> {
1000
+ const startTime = Date.now();
1001
+
1002
+ logCompaction("debug", "query_swarm_state_start", {
1003
+ session_id: sessionID,
1004
+ project_directory: projectDirectory,
1005
+ });
1006
+
955
1007
  try {
956
1008
  // Query cells via swarm CLI
957
- const cellsResult = await new Promise<{ exitCode: number; stdout: string }>(
1009
+ const cliStart = Date.now();
1010
+ const cellsResult = await new Promise<{ exitCode: number; stdout: string; stderr: string }>(
958
1011
  (resolve) => {
959
1012
  const proc = spawn(SWARM_CLI, ["tool", "hive_query"], {
960
1013
  cwd: projectDirectory,
961
1014
  stdio: ["ignore", "pipe", "pipe"],
962
1015
  });
963
1016
  let stdout = "";
1017
+ let stderr = "";
964
1018
  proc.stdout.on("data", (d) => {
965
1019
  stdout += d;
966
1020
  });
1021
+ proc.stderr.on("data", (d) => {
1022
+ stderr += d;
1023
+ });
967
1024
  proc.on("close", (exitCode) =>
968
- resolve({ exitCode: exitCode ?? 1, stdout }),
1025
+ resolve({ exitCode: exitCode ?? 1, stdout, stderr }),
969
1026
  );
970
1027
  },
971
1028
  );
1029
+ const cliDuration = Date.now() - cliStart;
1030
+
1031
+ logCompaction("debug", "query_swarm_state_cli_complete", {
1032
+ session_id: sessionID,
1033
+ duration_ms: cliDuration,
1034
+ exit_code: cellsResult.exitCode,
1035
+ stdout_length: cellsResult.stdout.length,
1036
+ stderr_length: cellsResult.stderr.length,
1037
+ });
972
1038
 
973
- const cells =
974
- cellsResult.exitCode === 0 ? JSON.parse(cellsResult.stdout) : [];
1039
+ let cells: any[] = [];
1040
+ if (cellsResult.exitCode === 0) {
1041
+ try {
1042
+ cells = JSON.parse(cellsResult.stdout);
1043
+ } catch (parseErr) {
1044
+ logCompaction("error", "query_swarm_state_parse_failed", {
1045
+ session_id: sessionID,
1046
+ error: parseErr instanceof Error ? parseErr.message : String(parseErr),
1047
+ stdout_preview: cellsResult.stdout.substring(0, 500),
1048
+ });
1049
+ }
1050
+ }
1051
+
1052
+ logCompaction("debug", "query_swarm_state_cells_parsed", {
1053
+ session_id: sessionID,
1054
+ cell_count: cells.length,
1055
+ cells: cells.map((c: any) => ({
1056
+ id: c.id,
1057
+ title: c.title,
1058
+ type: c.type,
1059
+ status: c.status,
1060
+ parent_id: c.parent_id,
1061
+ })),
1062
+ });
975
1063
 
976
1064
  // Find active epic (first unclosed epic with subtasks)
977
1065
  const openEpics = cells.filter(
@@ -980,6 +1068,12 @@ async function querySwarmState(sessionID: string): Promise<SwarmStateSnapshot> {
980
1068
  );
981
1069
  const epic = openEpics[0];
982
1070
 
1071
+ logCompaction("debug", "query_swarm_state_epics", {
1072
+ session_id: sessionID,
1073
+ open_epic_count: openEpics.length,
1074
+ selected_epic: epic ? { id: epic.id, title: epic.title, status: epic.status } : null,
1075
+ });
1076
+
983
1077
  // Get subtasks if we have an epic
984
1078
  const subtasks =
985
1079
  epic && epic.id
@@ -988,15 +1082,26 @@ async function querySwarmState(sessionID: string): Promise<SwarmStateSnapshot> {
988
1082
  )
989
1083
  : [];
990
1084
 
1085
+ logCompaction("debug", "query_swarm_state_subtasks", {
1086
+ session_id: sessionID,
1087
+ subtask_count: subtasks.length,
1088
+ subtasks: subtasks.map((s: any) => ({
1089
+ id: s.id,
1090
+ title: s.title,
1091
+ status: s.status,
1092
+ files: s.files,
1093
+ })),
1094
+ });
1095
+
991
1096
  // TODO: Query swarm mail for messages and reservations
992
1097
  // For MVP, use empty arrays - the fallback chain handles this
993
1098
  const messages: SwarmStateSnapshot["messages"] = [];
994
1099
  const reservations: SwarmStateSnapshot["reservations"] = [];
995
1100
 
996
- // Run detection for confidence
1101
+ // Run detection for confidence (already logged internally)
997
1102
  const detection = await detectSwarm();
998
1103
 
999
- return {
1104
+ const snapshot: SwarmStateSnapshot = {
1000
1105
  sessionID,
1001
1106
  detection: {
1002
1107
  confidence: detection.confidence,
@@ -1023,7 +1128,27 @@ async function querySwarmState(sessionID: string): Promise<SwarmStateSnapshot> {
1023
1128
  messages,
1024
1129
  reservations,
1025
1130
  };
1131
+
1132
+ const totalDuration = Date.now() - startTime;
1133
+ logCompaction("debug", "query_swarm_state_complete", {
1134
+ session_id: sessionID,
1135
+ duration_ms: totalDuration,
1136
+ has_epic: !!snapshot.epic,
1137
+ epic_id: snapshot.epic?.id,
1138
+ subtask_count: snapshot.epic?.subtasks?.length ?? 0,
1139
+ message_count: snapshot.messages.length,
1140
+ reservation_count: snapshot.reservations.length,
1141
+ });
1142
+
1143
+ return snapshot;
1026
1144
  } catch (err) {
1145
+ logCompaction("error", "query_swarm_state_exception", {
1146
+ session_id: sessionID,
1147
+ error: err instanceof Error ? err.message : String(err),
1148
+ stack: err instanceof Error ? err.stack : undefined,
1149
+ duration_ms: Date.now() - startTime,
1150
+ });
1151
+
1027
1152
  // If query fails, return minimal snapshot
1028
1153
  const detection = await detectSwarm();
1029
1154
  return {
@@ -1049,10 +1174,19 @@ async function querySwarmState(sessionID: string): Promise<SwarmStateSnapshot> {
1049
1174
  async function generateCompactionPrompt(
1050
1175
  snapshot: SwarmStateSnapshot,
1051
1176
  ): Promise<string | null> {
1052
- try {
1053
- const liteModel =
1054
- process.env.OPENCODE_LITE_MODEL || "claude-3-5-haiku-20241022";
1177
+ const startTime = Date.now();
1178
+ const liteModel = process.env.OPENCODE_LITE_MODEL || "claude-3-5-haiku-20241022";
1179
+
1180
+ logCompaction("debug", "generate_compaction_prompt_start", {
1181
+ session_id: snapshot.sessionID,
1182
+ lite_model: liteModel,
1183
+ has_epic: !!snapshot.epic,
1184
+ epic_id: snapshot.epic?.id,
1185
+ subtask_count: snapshot.epic?.subtasks?.length ?? 0,
1186
+ snapshot_size: JSON.stringify(snapshot).length,
1187
+ });
1055
1188
 
1189
+ try {
1056
1190
  const promptText = `You are generating a continuation prompt for a compacted swarm coordination session.
1057
1191
 
1058
1192
  Analyze this swarm state and generate a structured markdown prompt that will be given to the resumed session:
@@ -1100,6 +1234,14 @@ You are resuming coordination of an active swarm that was interrupted by context
1100
1234
 
1101
1235
  Keep the prompt concise but actionable. Use actual data from the snapshot, not placeholders.`;
1102
1236
 
1237
+ logCompaction("debug", "generate_compaction_prompt_calling_llm", {
1238
+ session_id: snapshot.sessionID,
1239
+ prompt_length: promptText.length,
1240
+ model: liteModel,
1241
+ command: `opencode run -m ${liteModel} -- <prompt>`,
1242
+ });
1243
+
1244
+ const llmStart = Date.now();
1103
1245
  const result = await new Promise<{ exitCode: number; stdout: string; stderr: string }>(
1104
1246
  (resolve, reject) => {
1105
1247
  const proc = spawn("opencode", ["run", "-m", liteModel, "--", promptText], {
@@ -1133,20 +1275,51 @@ Keep the prompt concise but actionable. Use actual data from the snapshot, not p
1133
1275
  }, 30000);
1134
1276
  },
1135
1277
  );
1278
+ const llmDuration = Date.now() - llmStart;
1279
+
1280
+ logCompaction("debug", "generate_compaction_prompt_llm_complete", {
1281
+ session_id: snapshot.sessionID,
1282
+ duration_ms: llmDuration,
1283
+ exit_code: result.exitCode,
1284
+ stdout_length: result.stdout.length,
1285
+ stderr_length: result.stderr.length,
1286
+ stderr_preview: result.stderr.substring(0, 500),
1287
+ stdout_preview: result.stdout.substring(0, 500),
1288
+ });
1136
1289
 
1137
1290
  if (result.exitCode !== 0) {
1138
- console.error(
1139
- "[Swarm Compaction] opencode run failed:",
1140
- result.stderr,
1141
- );
1291
+ logCompaction("error", "generate_compaction_prompt_llm_failed", {
1292
+ session_id: snapshot.sessionID,
1293
+ exit_code: result.exitCode,
1294
+ stderr: result.stderr,
1295
+ stdout: result.stdout,
1296
+ duration_ms: llmDuration,
1297
+ });
1142
1298
  return null;
1143
1299
  }
1144
1300
 
1145
1301
  // Extract the prompt from stdout (LLM may wrap in markdown)
1146
1302
  const prompt = result.stdout.trim();
1303
+
1304
+ const totalDuration = Date.now() - startTime;
1305
+ logCompaction("debug", "generate_compaction_prompt_success", {
1306
+ session_id: snapshot.sessionID,
1307
+ total_duration_ms: totalDuration,
1308
+ llm_duration_ms: llmDuration,
1309
+ prompt_length: prompt.length,
1310
+ prompt_preview: prompt.substring(0, 500),
1311
+ prompt_has_content: prompt.length > 0,
1312
+ });
1313
+
1147
1314
  return prompt.length > 0 ? prompt : null;
1148
1315
  } catch (err) {
1149
- console.error("[Swarm Compaction] LLM generation failed:", err);
1316
+ const totalDuration = Date.now() - startTime;
1317
+ logCompaction("error", "generate_compaction_prompt_exception", {
1318
+ session_id: snapshot.sessionID,
1319
+ error: err instanceof Error ? err.message : String(err),
1320
+ stack: err instanceof Error ? err.stack : undefined,
1321
+ duration_ms: totalDuration,
1322
+ });
1150
1323
  return null;
1151
1324
  }
1152
1325
  }
@@ -1164,37 +1337,90 @@ Keep the prompt concise but actionable. Use actual data from the snapshot, not p
1164
1337
  * False negative = lost swarm (high cost)
1165
1338
  */
1166
1339
  async function detectSwarm(): Promise<SwarmDetection> {
1340
+ const startTime = Date.now();
1167
1341
  const reasons: string[] = [];
1168
1342
  let highConfidence = false;
1169
1343
  let mediumConfidence = false;
1170
1344
  let lowConfidence = false;
1171
1345
 
1346
+ logCompaction("debug", "detect_swarm_start", {
1347
+ project_directory: projectDirectory,
1348
+ cwd: process.cwd(),
1349
+ });
1350
+
1172
1351
  try {
1173
- const result = await new Promise<{ exitCode: number; stdout: string }>(
1352
+ const cliStart = Date.now();
1353
+ const result = await new Promise<{ exitCode: number; stdout: string; stderr: string }>(
1174
1354
  (resolve) => {
1175
1355
  // Use swarm tool to query beads
1176
1356
  const proc = spawn(SWARM_CLI, ["tool", "hive_query"], {
1357
+ cwd: projectDirectory,
1177
1358
  stdio: ["ignore", "pipe", "pipe"],
1178
1359
  });
1179
1360
  let stdout = "";
1361
+ let stderr = "";
1180
1362
  proc.stdout.on("data", (d) => {
1181
1363
  stdout += d;
1182
1364
  });
1365
+ proc.stderr.on("data", (d) => {
1366
+ stderr += d;
1367
+ });
1183
1368
  proc.on("close", (exitCode) =>
1184
- resolve({ exitCode: exitCode ?? 1, stdout }),
1369
+ resolve({ exitCode: exitCode ?? 1, stdout, stderr }),
1185
1370
  );
1186
1371
  },
1187
1372
  );
1373
+ const cliDuration = Date.now() - cliStart;
1374
+
1375
+ logCompaction("debug", "detect_swarm_cli_complete", {
1376
+ duration_ms: cliDuration,
1377
+ exit_code: result.exitCode,
1378
+ stdout_length: result.stdout.length,
1379
+ stderr_length: result.stderr.length,
1380
+ stderr_preview: result.stderr.substring(0, 200),
1381
+ });
1188
1382
 
1189
1383
  if (result.exitCode !== 0) {
1384
+ logCompaction("warn", "detect_swarm_cli_failed", {
1385
+ exit_code: result.exitCode,
1386
+ stderr: result.stderr,
1387
+ });
1190
1388
  return { detected: false, confidence: "none", reasons: ["hive_query failed"] };
1191
1389
  }
1192
1390
 
1193
- const cells = JSON.parse(result.stdout);
1391
+ let cells: any[];
1392
+ try {
1393
+ cells = JSON.parse(result.stdout);
1394
+ } catch (parseErr) {
1395
+ logCompaction("error", "detect_swarm_parse_failed", {
1396
+ error: parseErr instanceof Error ? parseErr.message : String(parseErr),
1397
+ stdout_preview: result.stdout.substring(0, 500),
1398
+ });
1399
+ return { detected: false, confidence: "none", reasons: ["hive_query parse failed"] };
1400
+ }
1401
+
1194
1402
  if (!Array.isArray(cells) || cells.length === 0) {
1403
+ logCompaction("debug", "detect_swarm_no_cells", {
1404
+ is_array: Array.isArray(cells),
1405
+ length: cells?.length ?? 0,
1406
+ });
1195
1407
  return { detected: false, confidence: "none", reasons: ["no cells found"] };
1196
1408
  }
1197
1409
 
1410
+ // Log ALL cells for debugging
1411
+ logCompaction("debug", "detect_swarm_cells_found", {
1412
+ total_cells: cells.length,
1413
+ cells: cells.map((c: any) => ({
1414
+ id: c.id,
1415
+ title: c.title,
1416
+ type: c.type,
1417
+ status: c.status,
1418
+ parent_id: c.parent_id,
1419
+ updated_at: c.updated_at,
1420
+ created_at: c.created_at,
1421
+ })),
1422
+ });
1423
+
1198
1424
  // HIGH: Any in_progress cells
1199
1425
  const inProgress = cells.filter(
1200
1426
  (c: { status: string }) => c.status === "in_progress"
@@ -1202,6 +1428,10 @@ async function detectSwarm(): Promise<SwarmDetection> {
1202
1428
  if (inProgress.length > 0) {
1203
1429
  highConfidence = true;
1204
1430
  reasons.push(`${inProgress.length} cells in_progress`);
1431
+ logCompaction("debug", "detect_swarm_in_progress", {
1432
+ count: inProgress.length,
1433
+ cells: inProgress.map((c: any) => ({ id: c.id, title: c.title })),
1434
+ });
1205
1435
  }
1206
1436
 
1207
1437
  // MEDIUM: Open subtasks (cells with parent_id)
@@ -1212,6 +1442,10 @@ async function detectSwarm(): Promise<SwarmDetection> {
1212
1442
  if (subtasks.length > 0) {
1213
1443
  mediumConfidence = true;
1214
1444
  reasons.push(`${subtasks.length} open subtasks`);
1445
+ logCompaction("debug", "detect_swarm_open_subtasks", {
1446
+ count: subtasks.length,
1447
+ cells: subtasks.map((c: any) => ({ id: c.id, title: c.title, parent_id: c.parent_id })),
1448
+ });
1215
1449
  }
1216
1450
 
1217
1451
  // MEDIUM: Unclosed epics
@@ -1222,6 +1456,10 @@ async function detectSwarm(): Promise<SwarmDetection> {
1222
1456
  if (openEpics.length > 0) {
1223
1457
  mediumConfidence = true;
1224
1458
  reasons.push(`${openEpics.length} unclosed epics`);
1459
+ logCompaction("debug", "detect_swarm_open_epics", {
1460
+ count: openEpics.length,
1461
+ cells: openEpics.map((c: any) => ({ id: c.id, title: c.title, status: c.status })),
1462
+ });
1225
1463
  }
1226
1464
 
1227
1465
  // MEDIUM: Recently updated cells (last hour)
@@ -1232,6 +1470,16 @@ async function detectSwarm(): Promise<SwarmDetection> {
1232
1470
  if (recentCells.length > 0) {
1233
1471
  mediumConfidence = true;
1234
1472
  reasons.push(`${recentCells.length} cells updated in last hour`);
1473
+ logCompaction("debug", "detect_swarm_recent_cells", {
1474
+ count: recentCells.length,
1475
+ one_hour_ago: oneHourAgo,
1476
+ cells: recentCells.map((c: any) => ({
1477
+ id: c.id,
1478
+ title: c.title,
1479
+ updated_at: c.updated_at,
1480
+ age_minutes: Math.round((Date.now() - c.updated_at) / 60000),
1481
+ })),
1482
+ });
1235
1483
  }
1236
1484
 
1237
1485
  // LOW: Any cells exist at all
@@ -1239,10 +1487,14 @@ async function detectSwarm(): Promise<SwarmDetection> {
1239
1487
  lowConfidence = true;
1240
1488
  reasons.push(`${cells.length} total cells in hive`);
1241
1489
  }
1242
- } catch {
1490
+ } catch (err) {
1243
1491
  // Detection failed, use fallback
1244
1492
  lowConfidence = true;
1245
1493
  reasons.push("Detection error, using fallback");
1494
+ logCompaction("error", "detect_swarm_exception", {
1495
+ error: err instanceof Error ? err.message : String(err),
1496
+ stack: err instanceof Error ? err.stack : undefined,
1497
+ });
1246
1498
  }
1247
1499
 
1248
1500
  // Determine overall confidence
@@ -1257,6 +1509,18 @@ async function detectSwarm(): Promise<SwarmDetection> {
1257
1509
  confidence = "none";
1258
1510
  }
1259
1511
 
1512
+ const totalDuration = Date.now() - startTime;
1513
+ logCompaction("debug", "detect_swarm_complete", {
1514
+ duration_ms: totalDuration,
1515
+ confidence,
1516
+ detected: confidence !== "none",
1517
+ reason_count: reasons.length,
1518
+ reasons,
1519
+ high_confidence: highConfidence,
1520
+ medium_confidence: mediumConfidence,
1521
+ low_confidence: lowConfidence,
1522
+ });
1523
+
1260
1524
  return {
1261
1525
  detected: confidence !== "none",
1262
1526
  confidence,
@@ -1458,55 +1722,205 @@ export const SwarmPlugin: Plugin = async (
1458
1722
  input: { sessionID: string },
1459
1723
  output: CompactionOutput,
1460
1724
  ) => {
1725
+ const startTime = Date.now();
1726
+
1727
+ // =======================================================================
1728
+ // LOG: Compaction hook invoked - capture EVERYTHING we receive
1729
+ // =======================================================================
1730
+ logCompaction("info", "compaction_hook_invoked", {
1731
+ session_id: input.sessionID,
1732
+ project_directory: projectDirectory,
1733
+ input_keys: Object.keys(input),
1734
+ input_full: JSON.parse(JSON.stringify(input)), // Deep clone for logging
1735
+ output_keys: Object.keys(output),
1736
+ output_context_count: output.context?.length ?? 0,
1737
+ output_has_prompt_field: "prompt" in output,
1738
+ output_initial_state: {
1739
+ context: output.context,
1740
+ prompt: (output as any).prompt,
1741
+ },
1742
+ env: {
1743
+ OPENCODE_SESSION_ID: process.env.OPENCODE_SESSION_ID,
1744
+ OPENCODE_MESSAGE_ID: process.env.OPENCODE_MESSAGE_ID,
1745
+ OPENCODE_AGENT: process.env.OPENCODE_AGENT,
1746
+ OPENCODE_LITE_MODEL: process.env.OPENCODE_LITE_MODEL,
1747
+ SWARM_PROJECT_DIR: process.env.SWARM_PROJECT_DIR,
1748
+ },
1749
+ cwd: process.cwd(),
1750
+ timestamp: new Date().toISOString(),
1751
+ });
1752
+
1753
+ // =======================================================================
1754
+ // STEP 1: Detect swarm state from hive
1755
+ // =======================================================================
1756
+ const detectionStart = Date.now();
1461
1757
  const detection = await detectSwarm();
1758
+ const detectionDuration = Date.now() - detectionStart;
1759
+
1760
+ logCompaction("info", "swarm_detection_complete", {
1761
+ session_id: input.sessionID,
1762
+ duration_ms: detectionDuration,
1763
+ detected: detection.detected,
1764
+ confidence: detection.confidence,
1765
+ reasons: detection.reasons,
1766
+ reason_count: detection.reasons.length,
1767
+ });
1462
1768
 
1463
1769
  if (detection.confidence === "high" || detection.confidence === "medium") {
1464
1770
  // Definite or probable swarm - try LLM-powered compaction
1771
+ logCompaction("info", "swarm_detected_attempting_llm", {
1772
+ session_id: input.sessionID,
1773
+ confidence: detection.confidence,
1774
+ reasons: detection.reasons,
1775
+ });
1776
+
1465
1777
  try {
1466
1778
  // Level 1: Query actual state
1779
+ const queryStart = Date.now();
1467
1780
  const snapshot = await querySwarmState(input.sessionID);
1781
+ const queryDuration = Date.now() - queryStart;
1782
+
1783
+ logCompaction("info", "swarm_state_queried", {
1784
+ session_id: input.sessionID,
1785
+ duration_ms: queryDuration,
1786
+ has_epic: !!snapshot.epic,
1787
+ epic_id: snapshot.epic?.id,
1788
+ epic_title: snapshot.epic?.title,
1789
+ epic_status: snapshot.epic?.status,
1790
+ subtask_count: snapshot.epic?.subtasks?.length ?? 0,
1791
+ subtasks: snapshot.epic?.subtasks?.map(s => ({
1792
+ id: s.id,
1793
+ title: s.title,
1794
+ status: s.status,
1795
+ file_count: s.files?.length ?? 0,
1796
+ })),
1797
+ message_count: snapshot.messages?.length ?? 0,
1798
+ reservation_count: snapshot.reservations?.length ?? 0,
1799
+ detection_confidence: snapshot.detection.confidence,
1800
+ detection_reasons: snapshot.detection.reasons,
1801
+ full_snapshot: snapshot, // Log the entire snapshot
1802
+ });
1468
1803
 
1469
1804
  // Level 2: Generate prompt with LLM
1805
+ const llmStart = Date.now();
1470
1806
  const llmPrompt = await generateCompactionPrompt(snapshot);
1807
+ const llmDuration = Date.now() - llmStart;
1808
+
1809
+ logCompaction("info", "llm_generation_complete", {
1810
+ session_id: input.sessionID,
1811
+ duration_ms: llmDuration,
1812
+ success: !!llmPrompt,
1813
+ prompt_length: llmPrompt?.length ?? 0,
1814
+ prompt_preview: llmPrompt?.substring(0, 500),
1815
+ });
1471
1816
 
1472
1817
  if (llmPrompt) {
1473
1818
  // SUCCESS: Use LLM-generated prompt
1474
1819
  const header = `[Swarm compaction: LLM-generated, ${detection.reasons.join(", ")}]\n\n`;
1820
+ const fullContent = header + llmPrompt;
1475
1821
 
1476
1822
  // Progressive enhancement: use new API if available
1477
1823
  if ("prompt" in output) {
1478
- output.prompt = header + llmPrompt;
1824
+ output.prompt = fullContent;
1825
+ logCompaction("info", "context_injected_via_prompt_api", {
1826
+ session_id: input.sessionID,
1827
+ content_length: fullContent.length,
1828
+ method: "output.prompt",
1829
+ });
1479
1830
  } else {
1480
- output.context.push(header + llmPrompt);
1831
+ output.context.push(fullContent);
1832
+ logCompaction("info", "context_injected_via_context_array", {
1833
+ session_id: input.sessionID,
1834
+ content_length: fullContent.length,
1835
+ method: "output.context.push",
1836
+ context_count_after: output.context.length,
1837
+ });
1481
1838
  }
1482
1839
 
1483
- console.log(
1484
- "[Swarm Compaction] Using LLM-generated continuation prompt",
1485
- );
1840
+ const totalDuration = Date.now() - startTime;
1841
+ logCompaction("info", "compaction_complete_llm_success", {
1842
+ session_id: input.sessionID,
1843
+ total_duration_ms: totalDuration,
1844
+ detection_duration_ms: detectionDuration,
1845
+ query_duration_ms: queryDuration,
1846
+ llm_duration_ms: llmDuration,
1847
+ confidence: detection.confidence,
1848
+ context_type: "llm_generated",
1849
+ content_length: fullContent.length,
1850
+ });
1486
1851
  return;
1487
1852
  }
1488
1853
 
1489
1854
  // LLM failed, fall through to static prompt
1490
- console.log(
1491
- "[Swarm Compaction] LLM generation returned null, using static prompt",
1492
- );
1855
+ logCompaction("warn", "llm_generation_returned_null", {
1856
+ session_id: input.sessionID,
1857
+ llm_duration_ms: llmDuration,
1858
+ falling_back_to: "static_prompt",
1859
+ });
1493
1860
  } catch (err) {
1494
1861
  // LLM failed, fall through to static prompt
1495
- console.error(
1496
- "[Swarm Compaction] LLM generation failed, using static prompt:",
1497
- err,
1498
- );
1862
+ logCompaction("error", "llm_generation_failed", {
1863
+ session_id: input.sessionID,
1864
+ error: err instanceof Error ? err.message : String(err),
1865
+ error_stack: err instanceof Error ? err.stack : undefined,
1866
+ falling_back_to: "static_prompt",
1867
+ });
1499
1868
  }
1500
1869
 
1501
1870
  // Level 3: Fall back to static context
1502
1871
  const header = `[Swarm detected: ${detection.reasons.join(", ")}]\n\n`;
1503
- output.context.push(header + SWARM_COMPACTION_CONTEXT);
1872
+ const staticContent = header + SWARM_COMPACTION_CONTEXT;
1873
+ output.context.push(staticContent);
1874
+
1875
+ const totalDuration = Date.now() - startTime;
1876
+ logCompaction("info", "compaction_complete_static_fallback", {
1877
+ session_id: input.sessionID,
1878
+ total_duration_ms: totalDuration,
1879
+ confidence: detection.confidence,
1880
+ context_type: "static_swarm_context",
1881
+ content_length: staticContent.length,
1882
+ context_count_after: output.context.length,
1883
+ });
1504
1884
  } else if (detection.confidence === "low") {
1505
1885
  // Level 4: Possible swarm - inject fallback detection prompt
1506
1886
  const header = `[Possible swarm: ${detection.reasons.join(", ")}]\n\n`;
1507
- output.context.push(header + SWARM_DETECTION_FALLBACK);
1887
+ const fallbackContent = header + SWARM_DETECTION_FALLBACK;
1888
+ output.context.push(fallbackContent);
1889
+
1890
+ const totalDuration = Date.now() - startTime;
1891
+ logCompaction("info", "compaction_complete_detection_fallback", {
1892
+ session_id: input.sessionID,
1893
+ total_duration_ms: totalDuration,
1894
+ confidence: detection.confidence,
1895
+ context_type: "detection_fallback",
1896
+ content_length: fallbackContent.length,
1897
+ context_count_after: output.context.length,
1898
+ reasons: detection.reasons,
1899
+ });
1900
+ } else {
1901
+ // Level 5: confidence === "none" - no injection, probably not a swarm
1902
+ const totalDuration = Date.now() - startTime;
1903
+ logCompaction("info", "compaction_complete_no_swarm", {
1904
+ session_id: input.sessionID,
1905
+ total_duration_ms: totalDuration,
1906
+ confidence: detection.confidence,
1907
+ context_type: "none",
1908
+ reasons: detection.reasons,
1909
+ context_count_unchanged: output.context.length,
1910
+ });
1508
1911
  }
1509
- // Level 5: confidence === "none" - no injection, probably not a swarm
1912
+
1913
+ // =======================================================================
1914
+ // LOG: Final output state
1915
+ // =======================================================================
1916
+ logCompaction("debug", "compaction_hook_complete_final_state", {
1917
+ session_id: input.sessionID,
1918
+ output_context_count: output.context?.length ?? 0,
1919
+ output_context_lengths: output.context?.map(c => c.length) ?? [],
1920
+ output_has_prompt: !!(output as any).prompt,
1921
+ output_prompt_length: (output as any).prompt?.length ?? 0,
1922
+ total_duration_ms: Date.now() - startTime,
1923
+ });
1510
1924
  },
1511
1925
  };
1512
1926
  };