opencode-swarm-plugin 0.35.0 → 0.36.1
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/.hive/issues.jsonl +4 -4
- package/.hive/memories.jsonl +274 -1
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +307 -307
- package/CHANGELOG.md +133 -0
- package/bin/swarm.ts +234 -179
- package/dist/compaction-hook.d.ts +54 -4
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/eval-capture.d.ts +122 -17
- package/dist/eval-capture.d.ts.map +1 -1
- package/dist/index.d.ts +1 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1278 -619
- package/dist/planning-guardrails.d.ts +121 -0
- package/dist/planning-guardrails.d.ts.map +1 -1
- package/dist/plugin.d.ts +9 -9
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +1283 -329
- package/dist/schemas/task.d.ts +0 -1
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/swarm-decompose.d.ts +0 -8
- package/dist/swarm-decompose.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +0 -4
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-review.d.ts.map +1 -1
- package/dist/swarm.d.ts +0 -6
- package/dist/swarm.d.ts.map +1 -1
- package/evals/README.md +38 -0
- package/evals/coordinator-session.eval.ts +154 -0
- package/evals/fixtures/coordinator-sessions.ts +328 -0
- package/evals/lib/data-loader.ts +69 -0
- package/evals/scorers/coordinator-discipline.evalite-test.ts +536 -0
- package/evals/scorers/coordinator-discipline.ts +315 -0
- package/evals/scorers/index.ts +12 -0
- package/examples/plugin-wrapper-template.ts +747 -34
- package/package.json +2 -2
- package/src/compaction-hook.test.ts +234 -281
- package/src/compaction-hook.ts +221 -63
- package/src/eval-capture.test.ts +390 -0
- package/src/eval-capture.ts +168 -10
- package/src/index.ts +89 -2
- package/src/learning.integration.test.ts +0 -2
- package/src/planning-guardrails.test.ts +387 -2
- package/src/planning-guardrails.ts +289 -0
- package/src/plugin.ts +10 -10
- package/src/schemas/task.ts +0 -1
- package/src/swarm-decompose.ts +21 -8
- package/src/swarm-orchestrate.ts +44 -0
- package/src/swarm-prompts.ts +20 -0
- package/src/swarm-review.ts +41 -0
- package/src/swarm.integration.test.ts +0 -40
|
@@ -14,15 +14,65 @@
|
|
|
14
14
|
* - SWARM_PROJECT_DIR: Project directory (critical for database path)
|
|
15
15
|
*/
|
|
16
16
|
import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin";
|
|
17
|
+
import type { ToolPart } from "@opencode-ai/sdk";
|
|
17
18
|
import { tool } from "@opencode-ai/plugin";
|
|
18
19
|
import { spawn } from "child_process";
|
|
20
|
+
import { appendFileSync, mkdirSync, existsSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
19
23
|
|
|
20
24
|
const SWARM_CLI = "swarm";
|
|
21
25
|
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// File-based Logging (writes to ~/.config/swarm-tools/logs/)
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
const LOG_DIR = join(homedir(), ".config", "swarm-tools", "logs");
|
|
31
|
+
const COMPACTION_LOG = join(LOG_DIR, "compaction.log");
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ensure log directory exists
|
|
35
|
+
*/
|
|
36
|
+
function ensureLogDir(): void {
|
|
37
|
+
if (!existsSync(LOG_DIR)) {
|
|
38
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Log a compaction event to file (JSON lines format, compatible with `swarm log`)
|
|
44
|
+
*
|
|
45
|
+
* @param level - Log level (info, debug, warn, error)
|
|
46
|
+
* @param msg - Log message
|
|
47
|
+
* @param data - Additional structured data
|
|
48
|
+
*/
|
|
49
|
+
function logCompaction(
|
|
50
|
+
level: "info" | "debug" | "warn" | "error",
|
|
51
|
+
msg: string,
|
|
52
|
+
data?: Record<string, unknown>,
|
|
53
|
+
): void {
|
|
54
|
+
try {
|
|
55
|
+
ensureLogDir();
|
|
56
|
+
const entry = JSON.stringify({
|
|
57
|
+
time: new Date().toISOString(),
|
|
58
|
+
level,
|
|
59
|
+
msg,
|
|
60
|
+
...data,
|
|
61
|
+
});
|
|
62
|
+
appendFileSync(COMPACTION_LOG, entry + "\n");
|
|
63
|
+
} catch {
|
|
64
|
+
// Silently fail - logging should never break the plugin
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
22
68
|
// Module-level project directory - set during plugin initialization
|
|
23
69
|
// This is CRITICAL: without it, the CLI uses process.cwd() which may be wrong
|
|
24
70
|
let projectDirectory: string = process.cwd();
|
|
25
71
|
|
|
72
|
+
// Module-level SDK client - set during plugin initialization
|
|
73
|
+
// Used for scanning session messages during compaction
|
|
74
|
+
let sdkClient: any = null;
|
|
75
|
+
|
|
26
76
|
// =============================================================================
|
|
27
77
|
// CLI Execution Helper
|
|
28
78
|
// =============================================================================
|
|
@@ -952,26 +1002,71 @@ interface SwarmStateSnapshot {
|
|
|
952
1002
|
* Shells out to swarm CLI to get real data.
|
|
953
1003
|
*/
|
|
954
1004
|
async function querySwarmState(sessionID: string): Promise<SwarmStateSnapshot> {
|
|
1005
|
+
const startTime = Date.now();
|
|
1006
|
+
|
|
1007
|
+
logCompaction("debug", "query_swarm_state_start", {
|
|
1008
|
+
session_id: sessionID,
|
|
1009
|
+
project_directory: projectDirectory,
|
|
1010
|
+
});
|
|
1011
|
+
|
|
955
1012
|
try {
|
|
956
1013
|
// Query cells via swarm CLI
|
|
957
|
-
const
|
|
1014
|
+
const cliStart = Date.now();
|
|
1015
|
+
const cellsResult = await new Promise<{ exitCode: number; stdout: string; stderr: string }>(
|
|
958
1016
|
(resolve) => {
|
|
959
1017
|
const proc = spawn(SWARM_CLI, ["tool", "hive_query"], {
|
|
960
1018
|
cwd: projectDirectory,
|
|
961
1019
|
stdio: ["ignore", "pipe", "pipe"],
|
|
962
1020
|
});
|
|
963
1021
|
let stdout = "";
|
|
1022
|
+
let stderr = "";
|
|
964
1023
|
proc.stdout.on("data", (d) => {
|
|
965
1024
|
stdout += d;
|
|
966
1025
|
});
|
|
1026
|
+
proc.stderr.on("data", (d) => {
|
|
1027
|
+
stderr += d;
|
|
1028
|
+
});
|
|
967
1029
|
proc.on("close", (exitCode) =>
|
|
968
|
-
resolve({ exitCode: exitCode ?? 1, stdout }),
|
|
1030
|
+
resolve({ exitCode: exitCode ?? 1, stdout, stderr }),
|
|
969
1031
|
);
|
|
970
1032
|
},
|
|
971
1033
|
);
|
|
1034
|
+
const cliDuration = Date.now() - cliStart;
|
|
1035
|
+
|
|
1036
|
+
logCompaction("debug", "query_swarm_state_cli_complete", {
|
|
1037
|
+
session_id: sessionID,
|
|
1038
|
+
duration_ms: cliDuration,
|
|
1039
|
+
exit_code: cellsResult.exitCode,
|
|
1040
|
+
stdout_length: cellsResult.stdout.length,
|
|
1041
|
+
stderr_length: cellsResult.stderr.length,
|
|
1042
|
+
});
|
|
972
1043
|
|
|
973
|
-
|
|
974
|
-
|
|
1044
|
+
let cells: any[] = [];
|
|
1045
|
+
if (cellsResult.exitCode === 0) {
|
|
1046
|
+
try {
|
|
1047
|
+
const parsed = JSON.parse(cellsResult.stdout);
|
|
1048
|
+
// Handle wrapped response: { success: true, data: [...] }
|
|
1049
|
+
cells = Array.isArray(parsed) ? parsed : (parsed?.data ?? []);
|
|
1050
|
+
} catch (parseErr) {
|
|
1051
|
+
logCompaction("error", "query_swarm_state_parse_failed", {
|
|
1052
|
+
session_id: sessionID,
|
|
1053
|
+
error: parseErr instanceof Error ? parseErr.message : String(parseErr),
|
|
1054
|
+
stdout_preview: cellsResult.stdout.substring(0, 500),
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
logCompaction("debug", "query_swarm_state_cells_parsed", {
|
|
1060
|
+
session_id: sessionID,
|
|
1061
|
+
cell_count: cells.length,
|
|
1062
|
+
cells: cells.map((c: any) => ({
|
|
1063
|
+
id: c.id,
|
|
1064
|
+
title: c.title,
|
|
1065
|
+
type: c.type,
|
|
1066
|
+
status: c.status,
|
|
1067
|
+
parent_id: c.parent_id,
|
|
1068
|
+
})),
|
|
1069
|
+
});
|
|
975
1070
|
|
|
976
1071
|
// Find active epic (first unclosed epic with subtasks)
|
|
977
1072
|
const openEpics = cells.filter(
|
|
@@ -980,6 +1075,12 @@ async function querySwarmState(sessionID: string): Promise<SwarmStateSnapshot> {
|
|
|
980
1075
|
);
|
|
981
1076
|
const epic = openEpics[0];
|
|
982
1077
|
|
|
1078
|
+
logCompaction("debug", "query_swarm_state_epics", {
|
|
1079
|
+
session_id: sessionID,
|
|
1080
|
+
open_epic_count: openEpics.length,
|
|
1081
|
+
selected_epic: epic ? { id: epic.id, title: epic.title, status: epic.status } : null,
|
|
1082
|
+
});
|
|
1083
|
+
|
|
983
1084
|
// Get subtasks if we have an epic
|
|
984
1085
|
const subtasks =
|
|
985
1086
|
epic && epic.id
|
|
@@ -988,15 +1089,26 @@ async function querySwarmState(sessionID: string): Promise<SwarmStateSnapshot> {
|
|
|
988
1089
|
)
|
|
989
1090
|
: [];
|
|
990
1091
|
|
|
1092
|
+
logCompaction("debug", "query_swarm_state_subtasks", {
|
|
1093
|
+
session_id: sessionID,
|
|
1094
|
+
subtask_count: subtasks.length,
|
|
1095
|
+
subtasks: subtasks.map((s: any) => ({
|
|
1096
|
+
id: s.id,
|
|
1097
|
+
title: s.title,
|
|
1098
|
+
status: s.status,
|
|
1099
|
+
files: s.files,
|
|
1100
|
+
})),
|
|
1101
|
+
});
|
|
1102
|
+
|
|
991
1103
|
// TODO: Query swarm mail for messages and reservations
|
|
992
1104
|
// For MVP, use empty arrays - the fallback chain handles this
|
|
993
1105
|
const messages: SwarmStateSnapshot["messages"] = [];
|
|
994
1106
|
const reservations: SwarmStateSnapshot["reservations"] = [];
|
|
995
1107
|
|
|
996
|
-
// Run detection for confidence
|
|
1108
|
+
// Run detection for confidence (already logged internally)
|
|
997
1109
|
const detection = await detectSwarm();
|
|
998
1110
|
|
|
999
|
-
|
|
1111
|
+
const snapshot: SwarmStateSnapshot = {
|
|
1000
1112
|
sessionID,
|
|
1001
1113
|
detection: {
|
|
1002
1114
|
confidence: detection.confidence,
|
|
@@ -1023,7 +1135,27 @@ async function querySwarmState(sessionID: string): Promise<SwarmStateSnapshot> {
|
|
|
1023
1135
|
messages,
|
|
1024
1136
|
reservations,
|
|
1025
1137
|
};
|
|
1138
|
+
|
|
1139
|
+
const totalDuration = Date.now() - startTime;
|
|
1140
|
+
logCompaction("debug", "query_swarm_state_complete", {
|
|
1141
|
+
session_id: sessionID,
|
|
1142
|
+
duration_ms: totalDuration,
|
|
1143
|
+
has_epic: !!snapshot.epic,
|
|
1144
|
+
epic_id: snapshot.epic?.id,
|
|
1145
|
+
subtask_count: snapshot.epic?.subtasks?.length ?? 0,
|
|
1146
|
+
message_count: snapshot.messages.length,
|
|
1147
|
+
reservation_count: snapshot.reservations.length,
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
return snapshot;
|
|
1026
1151
|
} catch (err) {
|
|
1152
|
+
logCompaction("error", "query_swarm_state_exception", {
|
|
1153
|
+
session_id: sessionID,
|
|
1154
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1155
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
1156
|
+
duration_ms: Date.now() - startTime,
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1027
1159
|
// If query fails, return minimal snapshot
|
|
1028
1160
|
const detection = await detectSwarm();
|
|
1029
1161
|
return {
|
|
@@ -1049,10 +1181,19 @@ async function querySwarmState(sessionID: string): Promise<SwarmStateSnapshot> {
|
|
|
1049
1181
|
async function generateCompactionPrompt(
|
|
1050
1182
|
snapshot: SwarmStateSnapshot,
|
|
1051
1183
|
): Promise<string | null> {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1184
|
+
const startTime = Date.now();
|
|
1185
|
+
const liteModel = process.env.OPENCODE_LITE_MODEL || "__SWARM_LITE_MODEL__";
|
|
1186
|
+
|
|
1187
|
+
logCompaction("debug", "generate_compaction_prompt_start", {
|
|
1188
|
+
session_id: snapshot.sessionID,
|
|
1189
|
+
lite_model: liteModel,
|
|
1190
|
+
has_epic: !!snapshot.epic,
|
|
1191
|
+
epic_id: snapshot.epic?.id,
|
|
1192
|
+
subtask_count: snapshot.epic?.subtasks?.length ?? 0,
|
|
1193
|
+
snapshot_size: JSON.stringify(snapshot).length,
|
|
1194
|
+
});
|
|
1055
1195
|
|
|
1196
|
+
try {
|
|
1056
1197
|
const promptText = `You are generating a continuation prompt for a compacted swarm coordination session.
|
|
1057
1198
|
|
|
1058
1199
|
Analyze this swarm state and generate a structured markdown prompt that will be given to the resumed session:
|
|
@@ -1100,6 +1241,14 @@ You are resuming coordination of an active swarm that was interrupted by context
|
|
|
1100
1241
|
|
|
1101
1242
|
Keep the prompt concise but actionable. Use actual data from the snapshot, not placeholders.`;
|
|
1102
1243
|
|
|
1244
|
+
logCompaction("debug", "generate_compaction_prompt_calling_llm", {
|
|
1245
|
+
session_id: snapshot.sessionID,
|
|
1246
|
+
prompt_length: promptText.length,
|
|
1247
|
+
model: liteModel,
|
|
1248
|
+
command: `opencode run -m ${liteModel} -- <prompt>`,
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
const llmStart = Date.now();
|
|
1103
1252
|
const result = await new Promise<{ exitCode: number; stdout: string; stderr: string }>(
|
|
1104
1253
|
(resolve, reject) => {
|
|
1105
1254
|
const proc = spawn("opencode", ["run", "-m", liteModel, "--", promptText], {
|
|
@@ -1133,24 +1282,275 @@ Keep the prompt concise but actionable. Use actual data from the snapshot, not p
|
|
|
1133
1282
|
}, 30000);
|
|
1134
1283
|
},
|
|
1135
1284
|
);
|
|
1285
|
+
const llmDuration = Date.now() - llmStart;
|
|
1286
|
+
|
|
1287
|
+
logCompaction("debug", "generate_compaction_prompt_llm_complete", {
|
|
1288
|
+
session_id: snapshot.sessionID,
|
|
1289
|
+
duration_ms: llmDuration,
|
|
1290
|
+
exit_code: result.exitCode,
|
|
1291
|
+
stdout_length: result.stdout.length,
|
|
1292
|
+
stderr_length: result.stderr.length,
|
|
1293
|
+
stderr_preview: result.stderr.substring(0, 500),
|
|
1294
|
+
stdout_preview: result.stdout.substring(0, 500),
|
|
1295
|
+
});
|
|
1136
1296
|
|
|
1137
1297
|
if (result.exitCode !== 0) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
result.
|
|
1141
|
-
|
|
1298
|
+
logCompaction("error", "generate_compaction_prompt_llm_failed", {
|
|
1299
|
+
session_id: snapshot.sessionID,
|
|
1300
|
+
exit_code: result.exitCode,
|
|
1301
|
+
stderr: result.stderr,
|
|
1302
|
+
stdout: result.stdout,
|
|
1303
|
+
duration_ms: llmDuration,
|
|
1304
|
+
});
|
|
1142
1305
|
return null;
|
|
1143
1306
|
}
|
|
1144
1307
|
|
|
1145
1308
|
// Extract the prompt from stdout (LLM may wrap in markdown)
|
|
1146
1309
|
const prompt = result.stdout.trim();
|
|
1310
|
+
|
|
1311
|
+
const totalDuration = Date.now() - startTime;
|
|
1312
|
+
logCompaction("debug", "generate_compaction_prompt_success", {
|
|
1313
|
+
session_id: snapshot.sessionID,
|
|
1314
|
+
total_duration_ms: totalDuration,
|
|
1315
|
+
llm_duration_ms: llmDuration,
|
|
1316
|
+
prompt_length: prompt.length,
|
|
1317
|
+
prompt_preview: prompt.substring(0, 500),
|
|
1318
|
+
prompt_has_content: prompt.length > 0,
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1147
1321
|
return prompt.length > 0 ? prompt : null;
|
|
1148
1322
|
} catch (err) {
|
|
1149
|
-
|
|
1323
|
+
const totalDuration = Date.now() - startTime;
|
|
1324
|
+
logCompaction("error", "generate_compaction_prompt_exception", {
|
|
1325
|
+
session_id: snapshot.sessionID,
|
|
1326
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1327
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
1328
|
+
duration_ms: totalDuration,
|
|
1329
|
+
});
|
|
1150
1330
|
return null;
|
|
1151
1331
|
}
|
|
1152
1332
|
}
|
|
1153
1333
|
|
|
1334
|
+
/**
|
|
1335
|
+
* Session message scan result
|
|
1336
|
+
*/
|
|
1337
|
+
interface SessionScanResult {
|
|
1338
|
+
messageCount: number;
|
|
1339
|
+
toolCalls: Array<{
|
|
1340
|
+
toolName: string;
|
|
1341
|
+
args: Record<string, unknown>;
|
|
1342
|
+
output?: string;
|
|
1343
|
+
}>;
|
|
1344
|
+
swarmDetected: boolean;
|
|
1345
|
+
reasons: string[];
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Scan session messages for swarm tool calls
|
|
1350
|
+
*
|
|
1351
|
+
* Uses SDK client to fetch messages and look for swarm activity.
|
|
1352
|
+
* This can detect swarm work even if no cells exist yet.
|
|
1353
|
+
*/
|
|
1354
|
+
async function scanSessionMessages(sessionID: string): Promise<SessionScanResult> {
|
|
1355
|
+
const startTime = Date.now();
|
|
1356
|
+
const result: SessionScanResult = {
|
|
1357
|
+
messageCount: 0,
|
|
1358
|
+
toolCalls: [],
|
|
1359
|
+
swarmDetected: false,
|
|
1360
|
+
reasons: [],
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
logCompaction("debug", "session_scan_start", {
|
|
1364
|
+
session_id: sessionID,
|
|
1365
|
+
has_sdk_client: !!sdkClient,
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
if (!sdkClient) {
|
|
1369
|
+
logCompaction("warn", "session_scan_no_sdk_client", {
|
|
1370
|
+
session_id: sessionID,
|
|
1371
|
+
});
|
|
1372
|
+
return result;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
try {
|
|
1376
|
+
// Fetch session messages
|
|
1377
|
+
const messagesStart = Date.now();
|
|
1378
|
+
const rawResponse = await sdkClient.session.messages({ path: { id: sessionID } });
|
|
1379
|
+
const messagesDuration = Date.now() - messagesStart;
|
|
1380
|
+
|
|
1381
|
+
// Log the RAW response to understand its shape
|
|
1382
|
+
logCompaction("debug", "session_scan_raw_response", {
|
|
1383
|
+
session_id: sessionID,
|
|
1384
|
+
response_type: typeof rawResponse,
|
|
1385
|
+
is_array: Array.isArray(rawResponse),
|
|
1386
|
+
is_null: rawResponse === null,
|
|
1387
|
+
is_undefined: rawResponse === undefined,
|
|
1388
|
+
keys: rawResponse && typeof rawResponse === 'object' ? Object.keys(rawResponse) : [],
|
|
1389
|
+
raw_preview: JSON.stringify(rawResponse)?.slice(0, 500),
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
// The response might be wrapped - check common patterns
|
|
1393
|
+
const messages = Array.isArray(rawResponse)
|
|
1394
|
+
? rawResponse
|
|
1395
|
+
: rawResponse?.data
|
|
1396
|
+
? rawResponse.data
|
|
1397
|
+
: rawResponse?.messages
|
|
1398
|
+
? rawResponse.messages
|
|
1399
|
+
: rawResponse?.items
|
|
1400
|
+
? rawResponse.items
|
|
1401
|
+
: [];
|
|
1402
|
+
|
|
1403
|
+
result.messageCount = messages?.length ?? 0;
|
|
1404
|
+
|
|
1405
|
+
logCompaction("debug", "session_scan_messages_fetched", {
|
|
1406
|
+
session_id: sessionID,
|
|
1407
|
+
duration_ms: messagesDuration,
|
|
1408
|
+
message_count: result.messageCount,
|
|
1409
|
+
extraction_method: Array.isArray(rawResponse) ? 'direct_array' : rawResponse?.data ? 'data_field' : rawResponse?.messages ? 'messages_field' : rawResponse?.items ? 'items_field' : 'fallback_empty',
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
1413
|
+
logCompaction("debug", "session_scan_no_messages", {
|
|
1414
|
+
session_id: sessionID,
|
|
1415
|
+
});
|
|
1416
|
+
return result;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Swarm-related tool patterns
|
|
1420
|
+
const swarmTools = [
|
|
1421
|
+
// High confidence - active swarm coordination
|
|
1422
|
+
"hive_create_epic",
|
|
1423
|
+
"swarm_decompose",
|
|
1424
|
+
"swarm_spawn_subtask",
|
|
1425
|
+
"swarm_complete",
|
|
1426
|
+
"swarmmail_init",
|
|
1427
|
+
"swarmmail_reserve",
|
|
1428
|
+
// Medium confidence - swarm activity
|
|
1429
|
+
"hive_start",
|
|
1430
|
+
"hive_close",
|
|
1431
|
+
"swarm_status",
|
|
1432
|
+
"swarm_progress",
|
|
1433
|
+
"swarmmail_send",
|
|
1434
|
+
// Low confidence - possible swarm
|
|
1435
|
+
"hive_create",
|
|
1436
|
+
"hive_query",
|
|
1437
|
+
];
|
|
1438
|
+
|
|
1439
|
+
const highConfidenceTools = new Set([
|
|
1440
|
+
"hive_create_epic",
|
|
1441
|
+
"swarm_decompose",
|
|
1442
|
+
"swarm_spawn_subtask",
|
|
1443
|
+
"swarmmail_init",
|
|
1444
|
+
"swarmmail_reserve",
|
|
1445
|
+
]);
|
|
1446
|
+
|
|
1447
|
+
// Scan messages for tool calls
|
|
1448
|
+
let swarmToolCount = 0;
|
|
1449
|
+
let highConfidenceCount = 0;
|
|
1450
|
+
|
|
1451
|
+
// Debug: collect part types to understand message structure
|
|
1452
|
+
const partTypeCounts: Record<string, number> = {};
|
|
1453
|
+
let messagesWithParts = 0;
|
|
1454
|
+
let messagesWithoutParts = 0;
|
|
1455
|
+
let samplePartTypes: string[] = [];
|
|
1456
|
+
|
|
1457
|
+
for (const message of messages) {
|
|
1458
|
+
if (!message.parts || !Array.isArray(message.parts)) {
|
|
1459
|
+
messagesWithoutParts++;
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
messagesWithParts++;
|
|
1463
|
+
|
|
1464
|
+
for (const part of message.parts) {
|
|
1465
|
+
const partType = part.type || "unknown";
|
|
1466
|
+
partTypeCounts[partType] = (partTypeCounts[partType] || 0) + 1;
|
|
1467
|
+
|
|
1468
|
+
// Collect first 10 unique part types for debugging
|
|
1469
|
+
if (samplePartTypes.length < 10 && !samplePartTypes.includes(partType)) {
|
|
1470
|
+
samplePartTypes.push(partType);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Check if this is a tool call part
|
|
1474
|
+
// OpenCode SDK: ToolPart has type="tool", tool=<string name>, state={...}
|
|
1475
|
+
if (part.type === "tool") {
|
|
1476
|
+
const toolPart = part as ToolPart;
|
|
1477
|
+
const toolName = toolPart.tool; // tool name is a string directly
|
|
1478
|
+
|
|
1479
|
+
if (toolName && swarmTools.includes(toolName)) {
|
|
1480
|
+
swarmToolCount++;
|
|
1481
|
+
|
|
1482
|
+
if (highConfidenceTools.has(toolName)) {
|
|
1483
|
+
highConfidenceCount++;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Extract args/output from state if available
|
|
1487
|
+
const state = toolPart.state;
|
|
1488
|
+
const args = state && "input" in state ? state.input : {};
|
|
1489
|
+
const output = state && "output" in state ? state.output : undefined;
|
|
1490
|
+
|
|
1491
|
+
result.toolCalls.push({
|
|
1492
|
+
toolName,
|
|
1493
|
+
args,
|
|
1494
|
+
output,
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
logCompaction("debug", "session_scan_tool_found", {
|
|
1498
|
+
session_id: sessionID,
|
|
1499
|
+
tool_name: toolName,
|
|
1500
|
+
is_high_confidence: highConfidenceTools.has(toolName),
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// Determine if swarm detected based on tool calls
|
|
1508
|
+
if (highConfidenceCount > 0) {
|
|
1509
|
+
result.swarmDetected = true;
|
|
1510
|
+
result.reasons.push(`${highConfidenceCount} high-confidence swarm tools (${Array.from(new Set(result.toolCalls.filter(tc => highConfidenceTools.has(tc.toolName)).map(tc => tc.toolName))).join(", ")})`);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (swarmToolCount > 0 && !result.swarmDetected) {
|
|
1514
|
+
result.swarmDetected = true;
|
|
1515
|
+
result.reasons.push(`${swarmToolCount} swarm-related tools used`);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const totalDuration = Date.now() - startTime;
|
|
1519
|
+
|
|
1520
|
+
// Debug: log part type distribution to understand message structure
|
|
1521
|
+
logCompaction("debug", "session_scan_part_types", {
|
|
1522
|
+
session_id: sessionID,
|
|
1523
|
+
messages_with_parts: messagesWithParts,
|
|
1524
|
+
messages_without_parts: messagesWithoutParts,
|
|
1525
|
+
part_type_counts: partTypeCounts,
|
|
1526
|
+
sample_part_types: samplePartTypes,
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
logCompaction("info", "session_scan_complete", {
|
|
1530
|
+
session_id: sessionID,
|
|
1531
|
+
duration_ms: totalDuration,
|
|
1532
|
+
message_count: result.messageCount,
|
|
1533
|
+
tool_call_count: result.toolCalls.length,
|
|
1534
|
+
swarm_tool_count: swarmToolCount,
|
|
1535
|
+
high_confidence_count: highConfidenceCount,
|
|
1536
|
+
swarm_detected: result.swarmDetected,
|
|
1537
|
+
reasons: result.reasons,
|
|
1538
|
+
unique_tools: Array.from(new Set(result.toolCalls.map(tc => tc.toolName))),
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
return result;
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
const totalDuration = Date.now() - startTime;
|
|
1544
|
+
logCompaction("error", "session_scan_exception", {
|
|
1545
|
+
session_id: sessionID,
|
|
1546
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1547
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
1548
|
+
duration_ms: totalDuration,
|
|
1549
|
+
});
|
|
1550
|
+
return result;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1154
1554
|
/**
|
|
1155
1555
|
* Check for swarm sign - evidence a swarm passed through
|
|
1156
1556
|
*
|
|
@@ -1164,37 +1564,90 @@ Keep the prompt concise but actionable. Use actual data from the snapshot, not p
|
|
|
1164
1564
|
* False negative = lost swarm (high cost)
|
|
1165
1565
|
*/
|
|
1166
1566
|
async function detectSwarm(): Promise<SwarmDetection> {
|
|
1567
|
+
const startTime = Date.now();
|
|
1167
1568
|
const reasons: string[] = [];
|
|
1168
1569
|
let highConfidence = false;
|
|
1169
1570
|
let mediumConfidence = false;
|
|
1170
1571
|
let lowConfidence = false;
|
|
1171
1572
|
|
|
1573
|
+
logCompaction("debug", "detect_swarm_start", {
|
|
1574
|
+
project_directory: projectDirectory,
|
|
1575
|
+
cwd: process.cwd(),
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1172
1578
|
try {
|
|
1173
|
-
const
|
|
1579
|
+
const cliStart = Date.now();
|
|
1580
|
+
const result = await new Promise<{ exitCode: number; stdout: string; stderr: string }>(
|
|
1174
1581
|
(resolve) => {
|
|
1175
1582
|
// Use swarm tool to query beads
|
|
1176
1583
|
const proc = spawn(SWARM_CLI, ["tool", "hive_query"], {
|
|
1584
|
+
cwd: projectDirectory,
|
|
1177
1585
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1178
1586
|
});
|
|
1179
1587
|
let stdout = "";
|
|
1588
|
+
let stderr = "";
|
|
1180
1589
|
proc.stdout.on("data", (d) => {
|
|
1181
1590
|
stdout += d;
|
|
1182
1591
|
});
|
|
1592
|
+
proc.stderr.on("data", (d) => {
|
|
1593
|
+
stderr += d;
|
|
1594
|
+
});
|
|
1183
1595
|
proc.on("close", (exitCode) =>
|
|
1184
|
-
resolve({ exitCode: exitCode ?? 1, stdout }),
|
|
1596
|
+
resolve({ exitCode: exitCode ?? 1, stdout, stderr }),
|
|
1185
1597
|
);
|
|
1186
1598
|
},
|
|
1187
1599
|
);
|
|
1600
|
+
const cliDuration = Date.now() - cliStart;
|
|
1601
|
+
|
|
1602
|
+
logCompaction("debug", "detect_swarm_cli_complete", {
|
|
1603
|
+
duration_ms: cliDuration,
|
|
1604
|
+
exit_code: result.exitCode,
|
|
1605
|
+
stdout_length: result.stdout.length,
|
|
1606
|
+
stderr_length: result.stderr.length,
|
|
1607
|
+
stderr_preview: result.stderr.substring(0, 200),
|
|
1608
|
+
});
|
|
1188
1609
|
|
|
1189
1610
|
if (result.exitCode !== 0) {
|
|
1611
|
+
logCompaction("warn", "detect_swarm_cli_failed", {
|
|
1612
|
+
exit_code: result.exitCode,
|
|
1613
|
+
stderr: result.stderr,
|
|
1614
|
+
});
|
|
1190
1615
|
return { detected: false, confidence: "none", reasons: ["hive_query failed"] };
|
|
1191
1616
|
}
|
|
1192
1617
|
|
|
1193
|
-
|
|
1618
|
+
let cells: any[];
|
|
1619
|
+
try {
|
|
1620
|
+
cells = JSON.parse(result.stdout);
|
|
1621
|
+
} catch (parseErr) {
|
|
1622
|
+
logCompaction("error", "detect_swarm_parse_failed", {
|
|
1623
|
+
error: parseErr instanceof Error ? parseErr.message : String(parseErr),
|
|
1624
|
+
stdout_preview: result.stdout.substring(0, 500),
|
|
1625
|
+
});
|
|
1626
|
+
return { detected: false, confidence: "none", reasons: ["hive_query parse failed"] };
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1194
1629
|
if (!Array.isArray(cells) || cells.length === 0) {
|
|
1630
|
+
logCompaction("debug", "detect_swarm_no_cells", {
|
|
1631
|
+
is_array: Array.isArray(cells),
|
|
1632
|
+
length: cells?.length ?? 0,
|
|
1633
|
+
});
|
|
1195
1634
|
return { detected: false, confidence: "none", reasons: ["no cells found"] };
|
|
1196
1635
|
}
|
|
1197
1636
|
|
|
1637
|
+
// Log ALL cells for debugging
|
|
1638
|
+
logCompaction("debug", "detect_swarm_cells_found", {
|
|
1639
|
+
total_cells: cells.length,
|
|
1640
|
+
cells: cells.map((c: any) => ({
|
|
1641
|
+
id: c.id,
|
|
1642
|
+
title: c.title,
|
|
1643
|
+
type: c.type,
|
|
1644
|
+
status: c.status,
|
|
1645
|
+
parent_id: c.parent_id,
|
|
1646
|
+
updated_at: c.updated_at,
|
|
1647
|
+
created_at: c.created_at,
|
|
1648
|
+
})),
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1198
1651
|
// HIGH: Any in_progress cells
|
|
1199
1652
|
const inProgress = cells.filter(
|
|
1200
1653
|
(c: { status: string }) => c.status === "in_progress"
|
|
@@ -1202,6 +1655,10 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
1202
1655
|
if (inProgress.length > 0) {
|
|
1203
1656
|
highConfidence = true;
|
|
1204
1657
|
reasons.push(`${inProgress.length} cells in_progress`);
|
|
1658
|
+
logCompaction("debug", "detect_swarm_in_progress", {
|
|
1659
|
+
count: inProgress.length,
|
|
1660
|
+
cells: inProgress.map((c: any) => ({ id: c.id, title: c.title })),
|
|
1661
|
+
});
|
|
1205
1662
|
}
|
|
1206
1663
|
|
|
1207
1664
|
// MEDIUM: Open subtasks (cells with parent_id)
|
|
@@ -1212,6 +1669,10 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
1212
1669
|
if (subtasks.length > 0) {
|
|
1213
1670
|
mediumConfidence = true;
|
|
1214
1671
|
reasons.push(`${subtasks.length} open subtasks`);
|
|
1672
|
+
logCompaction("debug", "detect_swarm_open_subtasks", {
|
|
1673
|
+
count: subtasks.length,
|
|
1674
|
+
cells: subtasks.map((c: any) => ({ id: c.id, title: c.title, parent_id: c.parent_id })),
|
|
1675
|
+
});
|
|
1215
1676
|
}
|
|
1216
1677
|
|
|
1217
1678
|
// MEDIUM: Unclosed epics
|
|
@@ -1222,6 +1683,10 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
1222
1683
|
if (openEpics.length > 0) {
|
|
1223
1684
|
mediumConfidence = true;
|
|
1224
1685
|
reasons.push(`${openEpics.length} unclosed epics`);
|
|
1686
|
+
logCompaction("debug", "detect_swarm_open_epics", {
|
|
1687
|
+
count: openEpics.length,
|
|
1688
|
+
cells: openEpics.map((c: any) => ({ id: c.id, title: c.title, status: c.status })),
|
|
1689
|
+
});
|
|
1225
1690
|
}
|
|
1226
1691
|
|
|
1227
1692
|
// MEDIUM: Recently updated cells (last hour)
|
|
@@ -1232,6 +1697,16 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
1232
1697
|
if (recentCells.length > 0) {
|
|
1233
1698
|
mediumConfidence = true;
|
|
1234
1699
|
reasons.push(`${recentCells.length} cells updated in last hour`);
|
|
1700
|
+
logCompaction("debug", "detect_swarm_recent_cells", {
|
|
1701
|
+
count: recentCells.length,
|
|
1702
|
+
one_hour_ago: oneHourAgo,
|
|
1703
|
+
cells: recentCells.map((c: any) => ({
|
|
1704
|
+
id: c.id,
|
|
1705
|
+
title: c.title,
|
|
1706
|
+
updated_at: c.updated_at,
|
|
1707
|
+
age_minutes: Math.round((Date.now() - c.updated_at) / 60000),
|
|
1708
|
+
})),
|
|
1709
|
+
});
|
|
1235
1710
|
}
|
|
1236
1711
|
|
|
1237
1712
|
// LOW: Any cells exist at all
|
|
@@ -1239,10 +1714,14 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
1239
1714
|
lowConfidence = true;
|
|
1240
1715
|
reasons.push(`${cells.length} total cells in hive`);
|
|
1241
1716
|
}
|
|
1242
|
-
} catch {
|
|
1717
|
+
} catch (err) {
|
|
1243
1718
|
// Detection failed, use fallback
|
|
1244
1719
|
lowConfidence = true;
|
|
1245
1720
|
reasons.push("Detection error, using fallback");
|
|
1721
|
+
logCompaction("error", "detect_swarm_exception", {
|
|
1722
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1723
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
1724
|
+
});
|
|
1246
1725
|
}
|
|
1247
1726
|
|
|
1248
1727
|
// Determine overall confidence
|
|
@@ -1257,6 +1736,18 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
1257
1736
|
confidence = "none";
|
|
1258
1737
|
}
|
|
1259
1738
|
|
|
1739
|
+
const totalDuration = Date.now() - startTime;
|
|
1740
|
+
logCompaction("debug", "detect_swarm_complete", {
|
|
1741
|
+
duration_ms: totalDuration,
|
|
1742
|
+
confidence,
|
|
1743
|
+
detected: confidence !== "none",
|
|
1744
|
+
reason_count: reasons.length,
|
|
1745
|
+
reasons,
|
|
1746
|
+
high_confidence: highConfidence,
|
|
1747
|
+
medium_confidence: mediumConfidence,
|
|
1748
|
+
low_confidence: lowConfidence,
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1260
1751
|
return {
|
|
1261
1752
|
detected: confidence !== "none",
|
|
1262
1753
|
confidence,
|
|
@@ -1383,13 +1874,18 @@ type ExtendedHooks = Hooks & {
|
|
|
1383
1874
|
) => Promise<void>;
|
|
1384
1875
|
};
|
|
1385
1876
|
|
|
1386
|
-
export
|
|
1877
|
+
// NOTE: Only default export - named exports cause double registration!
|
|
1878
|
+
// OpenCode's plugin loader calls ALL exports as functions.
|
|
1879
|
+
const SwarmPlugin: Plugin = async (
|
|
1387
1880
|
input: PluginInput,
|
|
1388
1881
|
): Promise<ExtendedHooks> => {
|
|
1389
1882
|
// CRITICAL: Set project directory from OpenCode input
|
|
1390
1883
|
// Without this, CLI uses wrong database path
|
|
1391
1884
|
projectDirectory = input.directory;
|
|
1392
1885
|
|
|
1886
|
+
// Store SDK client for session message scanning during compaction
|
|
1887
|
+
sdkClient = input.client;
|
|
1888
|
+
|
|
1393
1889
|
return {
|
|
1394
1890
|
tool: {
|
|
1395
1891
|
// Beads
|
|
@@ -1458,55 +1954,272 @@ export const SwarmPlugin: Plugin = async (
|
|
|
1458
1954
|
input: { sessionID: string },
|
|
1459
1955
|
output: CompactionOutput,
|
|
1460
1956
|
) => {
|
|
1957
|
+
const startTime = Date.now();
|
|
1958
|
+
|
|
1959
|
+
// =======================================================================
|
|
1960
|
+
// LOG: Compaction hook invoked - capture EVERYTHING we receive
|
|
1961
|
+
// =======================================================================
|
|
1962
|
+
logCompaction("info", "compaction_hook_invoked", {
|
|
1963
|
+
session_id: input.sessionID,
|
|
1964
|
+
project_directory: projectDirectory,
|
|
1965
|
+
input_keys: Object.keys(input),
|
|
1966
|
+
input_full: JSON.parse(JSON.stringify(input)), // Deep clone for logging
|
|
1967
|
+
output_keys: Object.keys(output),
|
|
1968
|
+
output_context_count: output.context?.length ?? 0,
|
|
1969
|
+
output_has_prompt_field: "prompt" in output,
|
|
1970
|
+
output_initial_state: {
|
|
1971
|
+
context: output.context,
|
|
1972
|
+
prompt: (output as any).prompt,
|
|
1973
|
+
},
|
|
1974
|
+
env: {
|
|
1975
|
+
OPENCODE_SESSION_ID: process.env.OPENCODE_SESSION_ID,
|
|
1976
|
+
OPENCODE_MESSAGE_ID: process.env.OPENCODE_MESSAGE_ID,
|
|
1977
|
+
OPENCODE_AGENT: process.env.OPENCODE_AGENT,
|
|
1978
|
+
OPENCODE_LITE_MODEL: process.env.OPENCODE_LITE_MODEL,
|
|
1979
|
+
SWARM_PROJECT_DIR: process.env.SWARM_PROJECT_DIR,
|
|
1980
|
+
},
|
|
1981
|
+
cwd: process.cwd(),
|
|
1982
|
+
timestamp: new Date().toISOString(),
|
|
1983
|
+
});
|
|
1984
|
+
|
|
1985
|
+
// =======================================================================
|
|
1986
|
+
// STEP 1: Scan session messages for swarm tool calls
|
|
1987
|
+
// =======================================================================
|
|
1988
|
+
const sessionScanStart = Date.now();
|
|
1989
|
+
const sessionScan = await scanSessionMessages(input.sessionID);
|
|
1990
|
+
const sessionScanDuration = Date.now() - sessionScanStart;
|
|
1991
|
+
|
|
1992
|
+
logCompaction("info", "session_scan_results", {
|
|
1993
|
+
session_id: input.sessionID,
|
|
1994
|
+
duration_ms: sessionScanDuration,
|
|
1995
|
+
message_count: sessionScan.messageCount,
|
|
1996
|
+
tool_call_count: sessionScan.toolCalls.length,
|
|
1997
|
+
swarm_detected_from_messages: sessionScan.swarmDetected,
|
|
1998
|
+
reasons: sessionScan.reasons,
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
// =======================================================================
|
|
2002
|
+
// STEP 2: Detect swarm state from hive cells
|
|
2003
|
+
// =======================================================================
|
|
2004
|
+
const detectionStart = Date.now();
|
|
1461
2005
|
const detection = await detectSwarm();
|
|
2006
|
+
const detectionDuration = Date.now() - detectionStart;
|
|
2007
|
+
|
|
2008
|
+
logCompaction("info", "swarm_detection_complete", {
|
|
2009
|
+
session_id: input.sessionID,
|
|
2010
|
+
duration_ms: detectionDuration,
|
|
2011
|
+
detected: detection.detected,
|
|
2012
|
+
confidence: detection.confidence,
|
|
2013
|
+
reasons: detection.reasons,
|
|
2014
|
+
reason_count: detection.reasons.length,
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
// =======================================================================
|
|
2018
|
+
// STEP 3: Merge session scan with hive detection for final confidence
|
|
2019
|
+
// =======================================================================
|
|
2020
|
+
// If session messages show high-confidence swarm tools, boost confidence
|
|
2021
|
+
if (sessionScan.swarmDetected && sessionScan.reasons.some(r => r.includes("high-confidence"))) {
|
|
2022
|
+
if (detection.confidence === "none" || detection.confidence === "low") {
|
|
2023
|
+
detection.confidence = "high";
|
|
2024
|
+
detection.detected = true;
|
|
2025
|
+
detection.reasons.push(...sessionScan.reasons);
|
|
2026
|
+
|
|
2027
|
+
logCompaction("info", "confidence_boost_from_session_scan", {
|
|
2028
|
+
session_id: input.sessionID,
|
|
2029
|
+
original_confidence: detection.confidence,
|
|
2030
|
+
boosted_to: "high",
|
|
2031
|
+
session_reasons: sessionScan.reasons,
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
} else if (sessionScan.swarmDetected) {
|
|
2035
|
+
// Medium boost for any swarm tools found
|
|
2036
|
+
if (detection.confidence === "none") {
|
|
2037
|
+
detection.confidence = "medium";
|
|
2038
|
+
detection.detected = true;
|
|
2039
|
+
detection.reasons.push(...sessionScan.reasons);
|
|
2040
|
+
|
|
2041
|
+
logCompaction("info", "confidence_boost_from_session_scan", {
|
|
2042
|
+
session_id: input.sessionID,
|
|
2043
|
+
original_confidence: "none",
|
|
2044
|
+
boosted_to: "medium",
|
|
2045
|
+
session_reasons: sessionScan.reasons,
|
|
2046
|
+
});
|
|
2047
|
+
} else if (detection.confidence === "low") {
|
|
2048
|
+
detection.confidence = "medium";
|
|
2049
|
+
detection.reasons.push(...sessionScan.reasons);
|
|
2050
|
+
|
|
2051
|
+
logCompaction("info", "confidence_boost_from_session_scan", {
|
|
2052
|
+
session_id: input.sessionID,
|
|
2053
|
+
original_confidence: "low",
|
|
2054
|
+
boosted_to: "medium",
|
|
2055
|
+
session_reasons: sessionScan.reasons,
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
logCompaction("info", "final_swarm_detection", {
|
|
2061
|
+
session_id: input.sessionID,
|
|
2062
|
+
confidence: detection.confidence,
|
|
2063
|
+
detected: detection.detected,
|
|
2064
|
+
combined_reasons: detection.reasons,
|
|
2065
|
+
message_scan_contributed: sessionScan.swarmDetected,
|
|
2066
|
+
});
|
|
1462
2067
|
|
|
1463
2068
|
if (detection.confidence === "high" || detection.confidence === "medium") {
|
|
1464
2069
|
// Definite or probable swarm - try LLM-powered compaction
|
|
2070
|
+
logCompaction("info", "swarm_detected_attempting_llm", {
|
|
2071
|
+
session_id: input.sessionID,
|
|
2072
|
+
confidence: detection.confidence,
|
|
2073
|
+
reasons: detection.reasons,
|
|
2074
|
+
});
|
|
2075
|
+
|
|
1465
2076
|
try {
|
|
1466
2077
|
// Level 1: Query actual state
|
|
2078
|
+
const queryStart = Date.now();
|
|
1467
2079
|
const snapshot = await querySwarmState(input.sessionID);
|
|
2080
|
+
const queryDuration = Date.now() - queryStart;
|
|
2081
|
+
|
|
2082
|
+
logCompaction("info", "swarm_state_queried", {
|
|
2083
|
+
session_id: input.sessionID,
|
|
2084
|
+
duration_ms: queryDuration,
|
|
2085
|
+
has_epic: !!snapshot.epic,
|
|
2086
|
+
epic_id: snapshot.epic?.id,
|
|
2087
|
+
epic_title: snapshot.epic?.title,
|
|
2088
|
+
epic_status: snapshot.epic?.status,
|
|
2089
|
+
subtask_count: snapshot.epic?.subtasks?.length ?? 0,
|
|
2090
|
+
subtasks: snapshot.epic?.subtasks?.map(s => ({
|
|
2091
|
+
id: s.id,
|
|
2092
|
+
title: s.title,
|
|
2093
|
+
status: s.status,
|
|
2094
|
+
file_count: s.files?.length ?? 0,
|
|
2095
|
+
})),
|
|
2096
|
+
message_count: snapshot.messages?.length ?? 0,
|
|
2097
|
+
reservation_count: snapshot.reservations?.length ?? 0,
|
|
2098
|
+
detection_confidence: snapshot.detection.confidence,
|
|
2099
|
+
detection_reasons: snapshot.detection.reasons,
|
|
2100
|
+
full_snapshot: snapshot, // Log the entire snapshot
|
|
2101
|
+
});
|
|
1468
2102
|
|
|
1469
2103
|
// Level 2: Generate prompt with LLM
|
|
2104
|
+
const llmStart = Date.now();
|
|
1470
2105
|
const llmPrompt = await generateCompactionPrompt(snapshot);
|
|
2106
|
+
const llmDuration = Date.now() - llmStart;
|
|
2107
|
+
|
|
2108
|
+
logCompaction("info", "llm_generation_complete", {
|
|
2109
|
+
session_id: input.sessionID,
|
|
2110
|
+
duration_ms: llmDuration,
|
|
2111
|
+
success: !!llmPrompt,
|
|
2112
|
+
prompt_length: llmPrompt?.length ?? 0,
|
|
2113
|
+
prompt_preview: llmPrompt?.substring(0, 500),
|
|
2114
|
+
});
|
|
1471
2115
|
|
|
1472
2116
|
if (llmPrompt) {
|
|
1473
2117
|
// SUCCESS: Use LLM-generated prompt
|
|
1474
2118
|
const header = `[Swarm compaction: LLM-generated, ${detection.reasons.join(", ")}]\n\n`;
|
|
2119
|
+
const fullContent = header + llmPrompt;
|
|
1475
2120
|
|
|
1476
2121
|
// Progressive enhancement: use new API if available
|
|
1477
2122
|
if ("prompt" in output) {
|
|
1478
|
-
output.prompt =
|
|
2123
|
+
output.prompt = fullContent;
|
|
2124
|
+
logCompaction("info", "context_injected_via_prompt_api", {
|
|
2125
|
+
session_id: input.sessionID,
|
|
2126
|
+
content_length: fullContent.length,
|
|
2127
|
+
method: "output.prompt",
|
|
2128
|
+
});
|
|
1479
2129
|
} else {
|
|
1480
|
-
output.context.push(
|
|
2130
|
+
output.context.push(fullContent);
|
|
2131
|
+
logCompaction("info", "context_injected_via_context_array", {
|
|
2132
|
+
session_id: input.sessionID,
|
|
2133
|
+
content_length: fullContent.length,
|
|
2134
|
+
method: "output.context.push",
|
|
2135
|
+
context_count_after: output.context.length,
|
|
2136
|
+
});
|
|
1481
2137
|
}
|
|
1482
2138
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
2139
|
+
const totalDuration = Date.now() - startTime;
|
|
2140
|
+
logCompaction("info", "compaction_complete_llm_success", {
|
|
2141
|
+
session_id: input.sessionID,
|
|
2142
|
+
total_duration_ms: totalDuration,
|
|
2143
|
+
detection_duration_ms: detectionDuration,
|
|
2144
|
+
query_duration_ms: queryDuration,
|
|
2145
|
+
llm_duration_ms: llmDuration,
|
|
2146
|
+
confidence: detection.confidence,
|
|
2147
|
+
context_type: "llm_generated",
|
|
2148
|
+
content_length: fullContent.length,
|
|
2149
|
+
});
|
|
1486
2150
|
return;
|
|
1487
2151
|
}
|
|
1488
2152
|
|
|
1489
2153
|
// LLM failed, fall through to static prompt
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
2154
|
+
logCompaction("warn", "llm_generation_returned_null", {
|
|
2155
|
+
session_id: input.sessionID,
|
|
2156
|
+
llm_duration_ms: llmDuration,
|
|
2157
|
+
falling_back_to: "static_prompt",
|
|
2158
|
+
});
|
|
1493
2159
|
} catch (err) {
|
|
1494
2160
|
// LLM failed, fall through to static prompt
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
err,
|
|
1498
|
-
|
|
2161
|
+
logCompaction("error", "llm_generation_failed", {
|
|
2162
|
+
session_id: input.sessionID,
|
|
2163
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2164
|
+
error_stack: err instanceof Error ? err.stack : undefined,
|
|
2165
|
+
falling_back_to: "static_prompt",
|
|
2166
|
+
});
|
|
1499
2167
|
}
|
|
1500
2168
|
|
|
1501
2169
|
// Level 3: Fall back to static context
|
|
1502
2170
|
const header = `[Swarm detected: ${detection.reasons.join(", ")}]\n\n`;
|
|
1503
|
-
|
|
2171
|
+
const staticContent = header + SWARM_COMPACTION_CONTEXT;
|
|
2172
|
+
output.context.push(staticContent);
|
|
2173
|
+
|
|
2174
|
+
const totalDuration = Date.now() - startTime;
|
|
2175
|
+
logCompaction("info", "compaction_complete_static_fallback", {
|
|
2176
|
+
session_id: input.sessionID,
|
|
2177
|
+
total_duration_ms: totalDuration,
|
|
2178
|
+
confidence: detection.confidence,
|
|
2179
|
+
context_type: "static_swarm_context",
|
|
2180
|
+
content_length: staticContent.length,
|
|
2181
|
+
context_count_after: output.context.length,
|
|
2182
|
+
});
|
|
1504
2183
|
} else if (detection.confidence === "low") {
|
|
1505
2184
|
// Level 4: Possible swarm - inject fallback detection prompt
|
|
1506
2185
|
const header = `[Possible swarm: ${detection.reasons.join(", ")}]\n\n`;
|
|
1507
|
-
|
|
2186
|
+
const fallbackContent = header + SWARM_DETECTION_FALLBACK;
|
|
2187
|
+
output.context.push(fallbackContent);
|
|
2188
|
+
|
|
2189
|
+
const totalDuration = Date.now() - startTime;
|
|
2190
|
+
logCompaction("info", "compaction_complete_detection_fallback", {
|
|
2191
|
+
session_id: input.sessionID,
|
|
2192
|
+
total_duration_ms: totalDuration,
|
|
2193
|
+
confidence: detection.confidence,
|
|
2194
|
+
context_type: "detection_fallback",
|
|
2195
|
+
content_length: fallbackContent.length,
|
|
2196
|
+
context_count_after: output.context.length,
|
|
2197
|
+
reasons: detection.reasons,
|
|
2198
|
+
});
|
|
2199
|
+
} else {
|
|
2200
|
+
// Level 5: confidence === "none" - no injection, probably not a swarm
|
|
2201
|
+
const totalDuration = Date.now() - startTime;
|
|
2202
|
+
logCompaction("info", "compaction_complete_no_swarm", {
|
|
2203
|
+
session_id: input.sessionID,
|
|
2204
|
+
total_duration_ms: totalDuration,
|
|
2205
|
+
confidence: detection.confidence,
|
|
2206
|
+
context_type: "none",
|
|
2207
|
+
reasons: detection.reasons,
|
|
2208
|
+
context_count_unchanged: output.context.length,
|
|
2209
|
+
});
|
|
1508
2210
|
}
|
|
1509
|
-
|
|
2211
|
+
|
|
2212
|
+
// =======================================================================
|
|
2213
|
+
// LOG: Final output state
|
|
2214
|
+
// =======================================================================
|
|
2215
|
+
logCompaction("debug", "compaction_hook_complete_final_state", {
|
|
2216
|
+
session_id: input.sessionID,
|
|
2217
|
+
output_context_count: output.context?.length ?? 0,
|
|
2218
|
+
output_context_lengths: output.context?.map(c => c.length) ?? [],
|
|
2219
|
+
output_has_prompt: !!(output as any).prompt,
|
|
2220
|
+
output_prompt_length: (output as any).prompt?.length ?? 0,
|
|
2221
|
+
total_duration_ms: Date.now() - startTime,
|
|
2222
|
+
});
|
|
1510
2223
|
},
|
|
1511
2224
|
};
|
|
1512
2225
|
};
|