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.
- package/.turbo/turbo-test.log +333 -333
- package/CHANGELOG.md +62 -0
- package/examples/plugin-wrapper-template.ts +447 -33
- package/package.json +1 -1
- package/src/compaction-hook.test.ts +226 -280
- package/src/compaction-hook.ts +190 -42
- package/src/eval-capture.ts +5 -6
- package/src/index.ts +21 -1
- package/src/learning.integration.test.ts +0 -2
- package/src/schemas/task.ts +0 -1
- package/src/swarm-decompose.ts +1 -8
- package/src/swarm.integration.test.ts +0 -40
|
@@ -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
|
|
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
|
-
|
|
974
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
-
|
|
1139
|
-
|
|
1140
|
-
result.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
1484
|
-
|
|
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
|
-
|
|
1491
|
-
|
|
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
|
-
|
|
1496
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|