siclaw 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/dist/agentbox/gateway-client.d.ts +4 -0
  2. package/dist/agentbox/gateway-client.js +9 -1
  3. package/dist/agentbox/gateway-client.js.map +1 -1
  4. package/dist/agentbox/http-server.js +25 -1
  5. package/dist/agentbox/http-server.js.map +1 -1
  6. package/dist/agentbox/session.d.ts +2 -0
  7. package/dist/agentbox/session.js +11 -7
  8. package/dist/agentbox/session.js.map +1 -1
  9. package/dist/agentbox-main.js +10 -0
  10. package/dist/agentbox-main.js.map +1 -1
  11. package/dist/cli-main.js +19 -3
  12. package/dist/cli-main.js.map +1 -1
  13. package/dist/core/agent-factory.d.ts +2 -0
  14. package/dist/core/agent-factory.js +87 -21
  15. package/dist/core/agent-factory.js.map +1 -1
  16. package/dist/core/compaction.d.ts +80 -0
  17. package/dist/core/compaction.js +442 -0
  18. package/dist/core/compaction.js.map +1 -0
  19. package/dist/core/config.d.ts +7 -0
  20. package/dist/core/config.js +27 -1
  21. package/dist/core/config.js.map +1 -1
  22. package/dist/core/extensions/compaction-safeguard.d.ts +2 -0
  23. package/dist/core/extensions/compaction-safeguard.js +681 -0
  24. package/dist/core/extensions/compaction-safeguard.js.map +1 -0
  25. package/dist/core/extensions/deep-investigation.js +47 -73
  26. package/dist/core/extensions/deep-investigation.js.map +1 -1
  27. package/dist/core/extensions/memory-flush.d.ts +2 -10
  28. package/dist/core/extensions/memory-flush.js +4 -86
  29. package/dist/core/extensions/memory-flush.js.map +1 -1
  30. package/dist/core/llm-proxy.js +25 -6
  31. package/dist/core/llm-proxy.js.map +1 -1
  32. package/dist/core/message-utils.d.ts +18 -0
  33. package/dist/core/message-utils.js +28 -0
  34. package/dist/core/message-utils.js.map +1 -0
  35. package/dist/core/prompt.js +4 -5
  36. package/dist/core/prompt.js.map +1 -1
  37. package/dist/core/session-tool-result-guard.d.ts +2 -0
  38. package/dist/core/session-tool-result-guard.js +159 -0
  39. package/dist/core/session-tool-result-guard.js.map +1 -0
  40. package/dist/core/stream-wrappers.d.ts +41 -0
  41. package/dist/core/stream-wrappers.js +369 -0
  42. package/dist/core/stream-wrappers.js.map +1 -0
  43. package/dist/core/thinking-blocks.d.ts +20 -0
  44. package/dist/core/thinking-blocks.js +45 -0
  45. package/dist/core/thinking-blocks.js.map +1 -0
  46. package/dist/core/tool-call-id.d.ts +22 -0
  47. package/dist/core/tool-call-id.js +226 -0
  48. package/dist/core/tool-call-id.js.map +1 -0
  49. package/dist/core/tool-call-repair.d.ts +18 -0
  50. package/dist/core/tool-call-repair.js +73 -0
  51. package/dist/core/tool-call-repair.js.map +1 -0
  52. package/dist/core/tool-result-context-guard.d.ts +36 -0
  53. package/dist/core/tool-result-context-guard.js +272 -0
  54. package/dist/core/tool-result-context-guard.js.map +1 -0
  55. package/dist/cron/cron-limits.d.ts +16 -0
  56. package/dist/cron/cron-limits.js +17 -0
  57. package/dist/cron/cron-limits.js.map +1 -0
  58. package/dist/cron/cron-matcher.d.ts +14 -0
  59. package/dist/cron/cron-matcher.js +29 -0
  60. package/dist/cron/cron-matcher.js.map +1 -1
  61. package/dist/gateway/agentbox/client.d.ts +0 -2
  62. package/dist/gateway/agentbox/client.js.map +1 -1
  63. package/dist/gateway/agentbox/k8s-spawner.d.ts +10 -10
  64. package/dist/gateway/agentbox/k8s-spawner.js +27 -55
  65. package/dist/gateway/agentbox/k8s-spawner.js.map +1 -1
  66. package/dist/gateway/agentbox/local-spawner.d.ts +5 -0
  67. package/dist/gateway/agentbox/local-spawner.js +10 -0
  68. package/dist/gateway/agentbox/local-spawner.js.map +1 -1
  69. package/dist/gateway/cron/cron-service.js +7 -0
  70. package/dist/gateway/cron/cron-service.js.map +1 -1
  71. package/dist/gateway/db/index.js +9 -1
  72. package/dist/gateway/db/index.js.map +1 -1
  73. package/dist/gateway/db/init-schema.js +65 -16
  74. package/dist/gateway/db/init-schema.js.map +1 -1
  75. package/dist/gateway/db/migrate-sqlite.js +73 -20
  76. package/dist/gateway/db/migrate-sqlite.js.map +1 -1
  77. package/dist/gateway/db/repositories/cluster-repo.d.ts +59 -0
  78. package/dist/gateway/db/repositories/cluster-repo.js +107 -0
  79. package/dist/gateway/db/repositories/cluster-repo.js.map +1 -0
  80. package/dist/gateway/db/repositories/config-repo.d.ts +4 -5
  81. package/dist/gateway/db/repositories/config-repo.js +17 -0
  82. package/dist/gateway/db/repositories/config-repo.js.map +1 -1
  83. package/dist/gateway/db/repositories/feedback-repo.d.ts +71 -0
  84. package/dist/gateway/db/repositories/feedback-repo.js +52 -0
  85. package/dist/gateway/db/repositories/feedback-repo.js.map +1 -0
  86. package/dist/gateway/db/repositories/knowledge-doc-repo.d.ts +37 -0
  87. package/dist/gateway/db/repositories/knowledge-doc-repo.js +48 -0
  88. package/dist/gateway/db/repositories/knowledge-doc-repo.js.map +1 -0
  89. package/dist/gateway/db/repositories/user-cluster-config-repo.d.ts +45 -0
  90. package/dist/gateway/db/repositories/user-cluster-config-repo.js +90 -0
  91. package/dist/gateway/db/repositories/user-cluster-config-repo.js.map +1 -0
  92. package/dist/gateway/db/repositories/workspace-repo.d.ts +2 -2
  93. package/dist/gateway/db/repositories/workspace-repo.js +12 -12
  94. package/dist/gateway/db/repositories/workspace-repo.js.map +1 -1
  95. package/dist/gateway/db/schema-mysql.d.ts +437 -44
  96. package/dist/gateway/db/schema-mysql.js +36 -9
  97. package/dist/gateway/db/schema-mysql.js.map +1 -1
  98. package/dist/gateway/db/schema-sqlite.d.ts +459 -46
  99. package/dist/gateway/db/schema-sqlite.js +36 -9
  100. package/dist/gateway/db/schema-sqlite.js.map +1 -1
  101. package/dist/gateway/db/schema.d.ts +435 -44
  102. package/dist/gateway/db/schema.js +1 -1
  103. package/dist/gateway/db/schema.js.map +1 -1
  104. package/dist/gateway/plugins/channel-bridge.js +1 -1
  105. package/dist/gateway/plugins/channel-bridge.js.map +1 -1
  106. package/dist/gateway/rpc-methods.d.ts +2 -1
  107. package/dist/gateway/rpc-methods.js +507 -172
  108. package/dist/gateway/rpc-methods.js.map +1 -1
  109. package/dist/gateway/server.js +191 -51
  110. package/dist/gateway/server.js.map +1 -1
  111. package/dist/gateway/web/dist/assets/index-DTD0P9j8.css +1 -0
  112. package/dist/gateway/web/dist/assets/index-DhqsS2E0.js +756 -0
  113. package/dist/gateway/web/dist/assets/index-DhqsS2E0.js.map +1 -0
  114. package/dist/gateway/web/dist/index.html +2 -2
  115. package/dist/gateway-main.js +1 -3
  116. package/dist/gateway-main.js.map +1 -1
  117. package/dist/memory/indexer.d.ts +13 -0
  118. package/dist/memory/indexer.js +91 -1
  119. package/dist/memory/indexer.js.map +1 -1
  120. package/dist/memory/knowledge-extractor.d.ts +47 -0
  121. package/dist/memory/knowledge-extractor.js +165 -0
  122. package/dist/memory/knowledge-extractor.js.map +1 -0
  123. package/dist/memory/overview-generator.d.ts +16 -0
  124. package/dist/memory/overview-generator.js +233 -0
  125. package/dist/memory/overview-generator.js.map +1 -0
  126. package/dist/memory/session-summarizer.d.ts +28 -0
  127. package/dist/memory/session-summarizer.js +20 -2
  128. package/dist/memory/session-summarizer.js.map +1 -1
  129. package/dist/memory/temporal-decay.js +2 -2
  130. package/dist/memory/temporal-decay.js.map +1 -1
  131. package/dist/memory/topic-consolidator.d.ts +52 -0
  132. package/dist/memory/topic-consolidator.js +197 -0
  133. package/dist/memory/topic-consolidator.js.map +1 -0
  134. package/dist/tools/cluster-info.d.ts +9 -0
  135. package/dist/tools/cluster-info.js +74 -0
  136. package/dist/tools/cluster-info.js.map +1 -0
  137. package/dist/tools/command-sets.js +15 -5
  138. package/dist/tools/command-sets.js.map +1 -1
  139. package/dist/tools/create-skill.js +1 -1
  140. package/dist/tools/create-skill.js.map +1 -1
  141. package/dist/tools/debug-pod.d.ts +217 -0
  142. package/dist/tools/debug-pod.js +603 -0
  143. package/dist/tools/debug-pod.js.map +1 -0
  144. package/dist/tools/deep-search/engine.d.ts +0 -5
  145. package/dist/tools/deep-search/engine.js +68 -28
  146. package/dist/tools/deep-search/engine.js.map +1 -1
  147. package/dist/tools/deep-search/format.d.ts +1 -1
  148. package/dist/tools/deep-search/format.js +1 -2
  149. package/dist/tools/deep-search/format.js.map +1 -1
  150. package/dist/tools/deep-search/prompts.d.ts +4 -1
  151. package/dist/tools/deep-search/prompts.js +47 -29
  152. package/dist/tools/deep-search/prompts.js.map +1 -1
  153. package/dist/tools/deep-search/quality-gate.d.ts +25 -0
  154. package/dist/tools/deep-search/quality-gate.js +81 -0
  155. package/dist/tools/deep-search/quality-gate.js.map +1 -0
  156. package/dist/tools/deep-search/schemas.d.ts +25 -0
  157. package/dist/tools/deep-search/schemas.js +26 -1
  158. package/dist/tools/deep-search/schemas.js.map +1 -1
  159. package/dist/tools/deep-search/sre-knowledge.d.ts +6 -10
  160. package/dist/tools/deep-search/sre-knowledge.js +21 -52
  161. package/dist/tools/deep-search/sre-knowledge.js.map +1 -1
  162. package/dist/tools/deep-search/sub-agent.js +24 -8
  163. package/dist/tools/deep-search/sub-agent.js.map +1 -1
  164. package/dist/tools/deep-search/tool.js +3 -6
  165. package/dist/tools/deep-search/tool.js.map +1 -1
  166. package/dist/tools/deep-search/types.d.ts +13 -0
  167. package/dist/tools/deep-search/types.js +4 -4
  168. package/dist/tools/deep-search/types.js.map +1 -1
  169. package/dist/tools/dp-tools.d.ts +9 -6
  170. package/dist/tools/dp-tools.js +26 -55
  171. package/dist/tools/dp-tools.js.map +1 -1
  172. package/dist/tools/exec-utils.d.ts +8 -21
  173. package/dist/tools/exec-utils.js +11 -95
  174. package/dist/tools/exec-utils.js.map +1 -1
  175. package/dist/tools/fork-skill.js +1 -1
  176. package/dist/tools/fork-skill.js.map +1 -1
  177. package/dist/tools/k8s-checks.d.ts +11 -5
  178. package/dist/tools/k8s-checks.js +28 -9
  179. package/dist/tools/k8s-checks.js.map +1 -1
  180. package/dist/tools/knowledge-search.d.ts +3 -0
  181. package/dist/tools/knowledge-search.js +115 -0
  182. package/dist/tools/knowledge-search.js.map +1 -0
  183. package/dist/tools/kubeconfig-resolver.d.ts +22 -0
  184. package/dist/tools/kubeconfig-resolver.js +98 -18
  185. package/dist/tools/kubeconfig-resolver.js.map +1 -1
  186. package/dist/tools/manage-schedule.js +23 -1
  187. package/dist/tools/manage-schedule.js.map +1 -1
  188. package/dist/tools/netns-script.d.ts +1 -1
  189. package/dist/tools/netns-script.js +19 -7
  190. package/dist/tools/netns-script.js.map +1 -1
  191. package/dist/tools/node-exec.d.ts +1 -1
  192. package/dist/tools/node-exec.js +19 -7
  193. package/dist/tools/node-exec.js.map +1 -1
  194. package/dist/tools/node-script.d.ts +1 -1
  195. package/dist/tools/node-script.js +19 -7
  196. package/dist/tools/node-script.js.map +1 -1
  197. package/dist/tools/pod-exec.js +12 -1
  198. package/dist/tools/pod-exec.js.map +1 -1
  199. package/dist/tools/pod-nsenter-exec.d.ts +1 -1
  200. package/dist/tools/pod-nsenter-exec.js +19 -7
  201. package/dist/tools/pod-nsenter-exec.js.map +1 -1
  202. package/dist/tools/pod-script.js +12 -1
  203. package/dist/tools/pod-script.js.map +1 -1
  204. package/dist/tools/restricted-bash.js +10 -3
  205. package/dist/tools/restricted-bash.js.map +1 -1
  206. package/dist/tools/run-skill.js +14 -2
  207. package/dist/tools/run-skill.js.map +1 -1
  208. package/dist/tools/save-feedback.d.ts +7 -0
  209. package/dist/tools/save-feedback.js +125 -0
  210. package/dist/tools/save-feedback.js.map +1 -0
  211. package/dist/tools/update-skill.js +1 -1
  212. package/dist/tools/update-skill.js.map +1 -1
  213. package/package.json +1 -1
  214. package/skills/core/deep-investigation/SKILL.md +11 -14
  215. package/skills/core/session-feedback/SKILL.md +146 -0
@@ -0,0 +1,681 @@
1
+ import { EXACT_IDENTIFIERS_HEADING, SUMMARIZATION_OVERHEAD_TOKENS, SAFETY_MARGIN, computeAdaptiveChunkRatio, estimateMessagesTokens, extractToolCallsFromAssistant, extractToolResultId, pruneHistoryForContextShare, repairToolUseResultPairing, resolveContextWindowTokens, summarizeInStages, } from "../compaction.js";
2
+ // ── Constants ────────────────────────────────────────────────────────────
3
+ const MAX_TOOL_FAILURES = 8;
4
+ const MAX_TOOL_FAILURE_CHARS = 240;
5
+ const DEFAULT_RECENT_TURNS_PRESERVE = 3;
6
+ const DEFAULT_QUALITY_GUARD_MAX_RETRIES = 1;
7
+ const MAX_RECENT_TURNS_PRESERVE = 12;
8
+ const MAX_RECENT_TURN_TEXT_CHARS = 600;
9
+ const MAX_EXTRACTED_IDENTIFIERS = 12;
10
+ const REQUIRED_SUMMARY_SECTIONS = [
11
+ "## Decisions",
12
+ "## Open TODOs",
13
+ "## Constraints/Rules",
14
+ "## Pending user asks",
15
+ EXACT_IDENTIFIERS_HEADING,
16
+ ];
17
+ const STRICT_EXACT_IDENTIFIERS_INSTRUCTION = "For ## Exact identifiers, preserve literal values exactly as seen (IDs, URLs, file paths, ports, hashes, dates, times).";
18
+ const TURN_PREFIX_INSTRUCTIONS = "This summary covers the prefix of a split turn. Focus on the original request," +
19
+ " early progress, and any details needed to understand the retained suffix.";
20
+ const DEFAULT_COMPACTION_INSTRUCTIONS = "Write the summary body in the primary language used in the conversation.\n" +
21
+ "Focus on factual content: what was discussed, decisions made, and current state.\n" +
22
+ "Keep the required summary structure and section headers unchanged.\n" +
23
+ "Do not translate or alter code, file paths, identifiers, or error messages.";
24
+ const MAX_INSTRUCTION_LENGTH = 800;
25
+ // ── Small helpers ────────────────────────────────────────────────────────
26
+ function clampNonNegativeInt(value, fallback) {
27
+ const normalized = typeof value === "number" && Number.isFinite(value) ? value : fallback;
28
+ return Math.max(0, Math.floor(normalized));
29
+ }
30
+ function truncateUnicodeSafe(s, maxCodePoints) {
31
+ const chars = Array.from(s);
32
+ if (chars.length <= maxCodePoints)
33
+ return s;
34
+ return chars.slice(0, maxCodePoints).join("");
35
+ }
36
+ function resolveCompactionInstructions(eventInstructions) {
37
+ const trimmed = eventInstructions?.trim();
38
+ const resolved = trimmed && trimmed.length > 0 ? trimmed : DEFAULT_COMPACTION_INSTRUCTIONS;
39
+ return truncateUnicodeSafe(resolved, MAX_INSTRUCTION_LENGTH);
40
+ }
41
+ // ── Message inspection ───────────────────────────────────────────────────
42
+ function isRealConversationMessage(message) {
43
+ return message.role === "user" || message.role === "assistant" || message.role === "toolResult";
44
+ }
45
+ function extractMessageText(message) {
46
+ const content = message.content;
47
+ if (typeof content === "string")
48
+ return content.trim();
49
+ if (!Array.isArray(content))
50
+ return "";
51
+ const parts = [];
52
+ for (const block of content) {
53
+ if (!block || typeof block !== "object")
54
+ continue;
55
+ const text = block.text;
56
+ if (typeof text === "string" && text.trim().length > 0) {
57
+ parts.push(text.trim());
58
+ }
59
+ }
60
+ return parts.join("\n").trim();
61
+ }
62
+ function collectTextContentBlocks(content) {
63
+ if (!Array.isArray(content))
64
+ return [];
65
+ const parts = [];
66
+ for (const block of content) {
67
+ if (!block || typeof block !== "object")
68
+ continue;
69
+ const rec = block;
70
+ if (rec.type === "text" && typeof rec.text === "string") {
71
+ parts.push(rec.text);
72
+ }
73
+ }
74
+ return parts;
75
+ }
76
+ // ── Tool failure collection ──────────────────────────────────────────────
77
+ function normalizeFailureText(text) {
78
+ return text.replace(/\s+/g, " ").trim();
79
+ }
80
+ function truncateFailureText(text, maxChars) {
81
+ if (text.length <= maxChars)
82
+ return text;
83
+ return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
84
+ }
85
+ function formatToolFailureMeta(details) {
86
+ if (!details || typeof details !== "object")
87
+ return undefined;
88
+ const record = details;
89
+ const status = typeof record.status === "string" ? record.status : undefined;
90
+ const exitCode = typeof record.exitCode === "number" && Number.isFinite(record.exitCode)
91
+ ? record.exitCode
92
+ : undefined;
93
+ const parts = [];
94
+ if (status)
95
+ parts.push(`status=${status}`);
96
+ if (exitCode !== undefined)
97
+ parts.push(`exitCode=${exitCode}`);
98
+ return parts.length > 0 ? parts.join(" ") : undefined;
99
+ }
100
+ function collectToolFailures(messages) {
101
+ const failures = [];
102
+ const seen = new Set();
103
+ for (const message of messages) {
104
+ if (!message || typeof message !== "object")
105
+ continue;
106
+ const role = message.role;
107
+ if (role !== "toolResult")
108
+ continue;
109
+ const toolResult = message;
110
+ if (toolResult.isError !== true)
111
+ continue;
112
+ const toolCallId = typeof toolResult.toolCallId === "string" ? toolResult.toolCallId : "";
113
+ if (!toolCallId || seen.has(toolCallId))
114
+ continue;
115
+ seen.add(toolCallId);
116
+ const toolName = typeof toolResult.toolName === "string" && toolResult.toolName.trim()
117
+ ? toolResult.toolName
118
+ : "tool";
119
+ const rawText = collectTextContentBlocks(toolResult.content).join("\n");
120
+ const meta = formatToolFailureMeta(toolResult.details);
121
+ const normalized = normalizeFailureText(rawText);
122
+ const summary = truncateFailureText(normalized || (meta ? "failed" : "failed (no output)"), MAX_TOOL_FAILURE_CHARS);
123
+ failures.push({ toolCallId, toolName, summary, meta });
124
+ }
125
+ return failures;
126
+ }
127
+ function formatToolFailuresSection(failures) {
128
+ if (failures.length === 0)
129
+ return "";
130
+ const lines = failures.slice(0, MAX_TOOL_FAILURES).map((failure) => {
131
+ const meta = failure.meta ? ` (${failure.meta})` : "";
132
+ return `- ${failure.toolName}${meta}: ${failure.summary}`;
133
+ });
134
+ if (failures.length > MAX_TOOL_FAILURES) {
135
+ lines.push(`- ...and ${failures.length - MAX_TOOL_FAILURES} more`);
136
+ }
137
+ return `\n\n## Tool Failures\n${lines.join("\n")}`;
138
+ }
139
+ // ── File operations formatting ───────────────────────────────────────────
140
+ function computeFileLists(fileOps) {
141
+ const modified = new Set([...fileOps.edited, ...fileOps.written]);
142
+ const readFiles = [...fileOps.read].filter((f) => !modified.has(f)).sort();
143
+ const modifiedFiles = [...modified].sort();
144
+ return { readFiles, modifiedFiles };
145
+ }
146
+ function formatFileOperations(readFiles, modifiedFiles) {
147
+ const sections = [];
148
+ if (readFiles.length > 0) {
149
+ sections.push(`<read-files>\n${readFiles.join("\n")}\n</read-files>`);
150
+ }
151
+ if (modifiedFiles.length > 0) {
152
+ sections.push(`<modified-files>\n${modifiedFiles.join("\n")}\n</modified-files>`);
153
+ }
154
+ if (sections.length === 0)
155
+ return "";
156
+ return `\n\n${sections.join("\n\n")}`;
157
+ }
158
+ // ── Recent turns preservation ────────────────────────────────────────────
159
+ function splitPreservedRecentTurns(params) {
160
+ const preserveTurns = Math.min(MAX_RECENT_TURNS_PRESERVE, clampNonNegativeInt(params.recentTurnsPreserve, 0));
161
+ if (preserveTurns <= 0) {
162
+ return { summarizableMessages: params.messages, preservedMessages: [] };
163
+ }
164
+ const conversationIndexes = [];
165
+ const userIndexes = [];
166
+ for (let i = 0; i < params.messages.length; i += 1) {
167
+ const role = params.messages[i].role;
168
+ if (role === "user" || role === "assistant") {
169
+ conversationIndexes.push(i);
170
+ if (role === "user")
171
+ userIndexes.push(i);
172
+ }
173
+ }
174
+ if (conversationIndexes.length === 0) {
175
+ return { summarizableMessages: params.messages, preservedMessages: [] };
176
+ }
177
+ const preservedIndexSet = new Set();
178
+ if (userIndexes.length >= preserveTurns) {
179
+ const boundaryStartIndex = userIndexes[userIndexes.length - preserveTurns] ?? -1;
180
+ if (boundaryStartIndex >= 0) {
181
+ for (const index of conversationIndexes) {
182
+ if (index >= boundaryStartIndex)
183
+ preservedIndexSet.add(index);
184
+ }
185
+ }
186
+ }
187
+ else {
188
+ const fallbackMessageCount = preserveTurns * 2;
189
+ for (const userIndex of userIndexes)
190
+ preservedIndexSet.add(userIndex);
191
+ for (let i = conversationIndexes.length - 1; i >= 0; i -= 1) {
192
+ const index = conversationIndexes[i];
193
+ if (index === undefined)
194
+ continue;
195
+ preservedIndexSet.add(index);
196
+ if (preservedIndexSet.size >= fallbackMessageCount)
197
+ break;
198
+ }
199
+ }
200
+ if (preservedIndexSet.size === 0) {
201
+ return { summarizableMessages: params.messages, preservedMessages: [] };
202
+ }
203
+ // Collect tool call IDs from preserved assistant messages
204
+ const preservedToolCallIds = new Set();
205
+ for (let i = 0; i < params.messages.length; i += 1) {
206
+ if (!preservedIndexSet.has(i))
207
+ continue;
208
+ const message = params.messages[i];
209
+ if (message.role !== "assistant")
210
+ continue;
211
+ const toolCalls = extractToolCallsFromAssistant(message);
212
+ for (const toolCall of toolCalls)
213
+ preservedToolCallIds.add(toolCall.id);
214
+ }
215
+ // Include matching toolResult messages in the preserved set
216
+ if (preservedToolCallIds.size > 0) {
217
+ let preservedStartIndex = -1;
218
+ for (let i = 0; i < params.messages.length; i += 1) {
219
+ if (preservedIndexSet.has(i)) {
220
+ preservedStartIndex = i;
221
+ break;
222
+ }
223
+ }
224
+ if (preservedStartIndex >= 0) {
225
+ for (let i = preservedStartIndex; i < params.messages.length; i += 1) {
226
+ const message = params.messages[i];
227
+ if (message.role !== "toolResult")
228
+ continue;
229
+ const toolResultId = extractToolResultId(message);
230
+ if (toolResultId && preservedToolCallIds.has(toolResultId)) {
231
+ preservedIndexSet.add(i);
232
+ }
233
+ }
234
+ }
235
+ }
236
+ const summarizableMessages = params.messages.filter((_, idx) => !preservedIndexSet.has(idx));
237
+ const repairedSummarizableMessages = repairToolUseResultPairing(summarizableMessages).messages;
238
+ const preservedMessages = params.messages
239
+ .filter((_, idx) => preservedIndexSet.has(idx))
240
+ .filter((msg) => {
241
+ const role = msg.role;
242
+ return role === "user" || role === "assistant" || role === "toolResult";
243
+ });
244
+ return { summarizableMessages: repairedSummarizableMessages, preservedMessages };
245
+ }
246
+ function formatNonTextPlaceholder(content) {
247
+ if (content === null || content === undefined)
248
+ return null;
249
+ if (typeof content === "string")
250
+ return null;
251
+ if (!Array.isArray(content))
252
+ return "[non-text content]";
253
+ const typeCounts = new Map();
254
+ for (const block of content) {
255
+ if (!block || typeof block !== "object")
256
+ continue;
257
+ const typeRaw = block.type;
258
+ const type = typeof typeRaw === "string" && typeRaw.trim().length > 0 ? typeRaw : "unknown";
259
+ if (type === "text")
260
+ continue;
261
+ typeCounts.set(type, (typeCounts.get(type) ?? 0) + 1);
262
+ }
263
+ if (typeCounts.size === 0)
264
+ return null;
265
+ const parts = [...typeCounts.entries()].map(([type, count]) => count > 1 ? `${type} x${count}` : type);
266
+ return `[non-text content: ${parts.join(", ")}]`;
267
+ }
268
+ function formatPreservedTurnsSection(messages) {
269
+ if (messages.length === 0)
270
+ return "";
271
+ const lines = messages
272
+ .map((message) => {
273
+ let roleLabel;
274
+ if (message.role === "assistant") {
275
+ roleLabel = "Assistant";
276
+ }
277
+ else if (message.role === "user") {
278
+ roleLabel = "User";
279
+ }
280
+ else if (message.role === "toolResult") {
281
+ const toolName = message.toolName;
282
+ const safeToolName = typeof toolName === "string" && toolName.trim() ? toolName : "tool";
283
+ roleLabel = `Tool result (${safeToolName})`;
284
+ }
285
+ else {
286
+ return null;
287
+ }
288
+ const text = extractMessageText(message);
289
+ const nonTextPlaceholder = formatNonTextPlaceholder(message.content);
290
+ const rendered = text && nonTextPlaceholder ? `${text}\n${nonTextPlaceholder}` : text || nonTextPlaceholder;
291
+ if (!rendered)
292
+ return null;
293
+ const trimmed = rendered.length > MAX_RECENT_TURN_TEXT_CHARS
294
+ ? `${rendered.slice(0, MAX_RECENT_TURN_TEXT_CHARS)}...`
295
+ : rendered;
296
+ return `- ${roleLabel}: ${trimmed}`;
297
+ })
298
+ .filter((line) => Boolean(line));
299
+ if (lines.length === 0)
300
+ return "";
301
+ return `\n\n## Recent turns preserved verbatim\n${lines.join("\n")}`;
302
+ }
303
+ // ── Structured summary instructions ──────────────────────────────────────
304
+ function buildCompactionStructureInstructions(customInstructions) {
305
+ const sectionsTemplate = [
306
+ "Produce a compact, factual summary with these exact section headings:",
307
+ ...REQUIRED_SUMMARY_SECTIONS,
308
+ STRICT_EXACT_IDENTIFIERS_INSTRUCTION,
309
+ "Do not omit unresolved asks from the user.",
310
+ ].join("\n");
311
+ const custom = customInstructions?.trim();
312
+ if (!custom)
313
+ return sectionsTemplate;
314
+ return `${sectionsTemplate}\n\nAdditional context:\n${custom}`;
315
+ }
316
+ // ── Fallback summary ─────────────────────────────────────────────────────
317
+ function buildStructuredFallbackSummary(previousSummary) {
318
+ const trimmed = previousSummary?.trim() ?? "";
319
+ if (trimmed && hasRequiredSummarySections(trimmed))
320
+ return trimmed;
321
+ return [
322
+ "## Decisions",
323
+ trimmed || "No prior history.",
324
+ "",
325
+ "## Open TODOs",
326
+ "None.",
327
+ "",
328
+ "## Constraints/Rules",
329
+ "None.",
330
+ "",
331
+ "## Pending user asks",
332
+ "None.",
333
+ "",
334
+ "## Exact identifiers",
335
+ "None captured.",
336
+ ].join("\n");
337
+ }
338
+ // ── Quality auditing ─────────────────────────────────────────────────────
339
+ function normalizedSummaryLines(summary) {
340
+ return summary
341
+ .split(/\r?\n/u)
342
+ .map((line) => line.trim())
343
+ .filter((line) => line.length > 0);
344
+ }
345
+ function hasRequiredSummarySections(summary) {
346
+ const lines = normalizedSummaryLines(summary);
347
+ let cursor = 0;
348
+ for (const heading of REQUIRED_SUMMARY_SECTIONS) {
349
+ const index = lines.findIndex((line, lineIndex) => lineIndex >= cursor && line === heading);
350
+ if (index < 0)
351
+ return false;
352
+ cursor = index + 1;
353
+ }
354
+ return true;
355
+ }
356
+ function extractOpaqueIdentifiers(text) {
357
+ const matches = text.match(/([A-Fa-f0-9]{8,}|https?:\/\/\S+|\/[\w.-]{2,}(?:\/[\w.-]+)+|[A-Za-z]:\\[\w\\.-]+|[A-Za-z0-9._-]+\.[A-Za-z0-9._/-]+:\d{1,5}|\b\d{6,}\b)/g) ?? [];
358
+ const sanitize = (value) => value.trim().replace(/^[("'`[{<]+/, "").replace(/[)\]"'`,;:.!?<>]+$/, "");
359
+ const normalize = (value) => /^[A-Fa-f0-9]{8,}$/.test(value) ? value.toUpperCase() : value;
360
+ return Array.from(new Set(matches
361
+ .map((v) => sanitize(v))
362
+ .map((v) => normalize(v))
363
+ .filter((v) => v.length >= 4))).slice(0, MAX_EXTRACTED_IDENTIFIERS);
364
+ }
365
+ function extractLatestUserAsk(messages) {
366
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
367
+ if (messages[i].role !== "user")
368
+ continue;
369
+ const text = extractMessageText(messages[i]);
370
+ if (text)
371
+ return text;
372
+ }
373
+ return null;
374
+ }
375
+ function summaryIncludesIdentifier(summary, identifier) {
376
+ if (/^[A-Fa-f0-9]{8,}$/.test(identifier)) {
377
+ return summary.toUpperCase().includes(identifier.toUpperCase());
378
+ }
379
+ return summary.includes(identifier);
380
+ }
381
+ // Common stopwords that inflate overlap counts without carrying meaning.
382
+ // Covers English + high-frequency Chinese function words.
383
+ const OVERLAP_STOP_WORDS = new Set([
384
+ // English
385
+ "the", "is", "are", "was", "were", "be", "been", "being",
386
+ "have", "has", "had", "do", "does", "did", "will", "would",
387
+ "could", "should", "may", "might", "shall", "can",
388
+ "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
389
+ "of", "with", "by", "from", "as", "into", "through", "during",
390
+ "it", "its", "this", "that", "these", "those", "what", "which",
391
+ "who", "whom", "how", "when", "where", "why",
392
+ "not", "no", "nor", "so", "if", "then", "than", "too", "very",
393
+ "just", "about", "up", "out", "all", "also", "get", "got",
394
+ "help", "need", "want", "thing", "stuff", "some", "any",
395
+ // Chinese function words
396
+ "的", "了", "在", "是", "我", "有", "和", "就", "不", "人",
397
+ "都", "一", "个", "上", "也", "很", "到", "说", "要", "去",
398
+ "你", "会", "着", "没有", "看", "好", "这", "那", "吗", "呢",
399
+ ]);
400
+ function tokenizeOverlapText(text) {
401
+ const normalized = text.toLocaleLowerCase().normalize("NFKC").trim();
402
+ if (!normalized)
403
+ return [];
404
+ return normalized
405
+ .split(/[^\p{L}\p{N}]+/u)
406
+ .map((token) => token.trim())
407
+ .filter((token) => token.length > 0);
408
+ }
409
+ /** Check if a token is a CJK character (single-char tokens from CJK splitting). */
410
+ function isCjkChar(ch) {
411
+ if (ch.length !== 1)
412
+ return false;
413
+ const code = ch.codePointAt(0);
414
+ // CJK Unified Ideographs + Extension A
415
+ return (code >= 0x4E00 && code <= 0x9FFF) || (code >= 0x3400 && code <= 0x4DBF);
416
+ }
417
+ function hasAskOverlap(summary, latestAsk) {
418
+ if (!latestAsk)
419
+ return true;
420
+ const askTokens = Array.from(new Set(tokenizeOverlapText(latestAsk))).slice(0, 12);
421
+ if (askTokens.length === 0)
422
+ return true;
423
+ // Filter out stopwords so only meaningful terms drive the overlap check
424
+ const meaningfulTokens = askTokens.filter((t) => t.length > 1 && !OVERLAP_STOP_WORDS.has(t));
425
+ const tokensToCheck = meaningfulTokens.length > 0 ? meaningfulTokens : askTokens;
426
+ if (tokensToCheck.length === 0)
427
+ return true;
428
+ const summaryLower = summary.toLocaleLowerCase().normalize("NFKC");
429
+ const summaryTokens = new Set(tokenizeOverlapText(summary));
430
+ let overlapCount = 0;
431
+ for (const token of tokensToCheck) {
432
+ // For single CJK characters, use substring matching instead of exact token match
433
+ // since Unicode word-boundary splitting yields individual characters that rarely
434
+ // match as standalone tokens in the summary.
435
+ if (isCjkChar(token)) {
436
+ if (summaryLower.includes(token))
437
+ overlapCount += 1;
438
+ }
439
+ else {
440
+ if (summaryTokens.has(token))
441
+ overlapCount += 1;
442
+ }
443
+ }
444
+ const requiredMatches = tokensToCheck.length >= 3 ? 2 : 1;
445
+ return overlapCount >= requiredMatches;
446
+ }
447
+ function auditSummaryQuality(params) {
448
+ const reasons = [];
449
+ const lines = new Set(normalizedSummaryLines(params.summary));
450
+ for (const section of REQUIRED_SUMMARY_SECTIONS) {
451
+ if (!lines.has(section))
452
+ reasons.push(`missing_section:${section}`);
453
+ }
454
+ const missingIdentifiers = params.identifiers.filter((id) => !summaryIncludesIdentifier(params.summary, id));
455
+ if (missingIdentifiers.length > 0) {
456
+ reasons.push(`missing_identifiers:${missingIdentifiers.slice(0, 3).join(",")}`);
457
+ }
458
+ if (!hasAskOverlap(params.summary, params.latestAsk)) {
459
+ reasons.push("latest_user_ask_not_reflected");
460
+ }
461
+ return { ok: reasons.length === 0, reasons };
462
+ }
463
+ // ── Summary assembly helpers ─────────────────────────────────────────────
464
+ function appendSummarySection(summary, section) {
465
+ if (!section)
466
+ return summary;
467
+ if (!summary.trim())
468
+ return section.trimStart();
469
+ return `${summary}${section}`;
470
+ }
471
+ // ── Extension entry point ────────────────────────────────────────────────
472
+ export default function compactionSafeguardExtension(api) {
473
+ api.on("session_before_compact", async (event, ctx) => {
474
+ const { preparation, customInstructions: eventInstructions, signal } = event;
475
+ // ── Empty conversation guard ──
476
+ const hasRealSummarizable = preparation.messagesToSummarize.some(isRealConversationMessage);
477
+ const hasRealTurnPrefix = preparation.turnPrefixMessages.some(isRealConversationMessage);
478
+ if (!hasRealSummarizable && !hasRealTurnPrefix) {
479
+ console.log("[compaction-safeguard] No real conversation messages to summarize; writing compaction boundary to suppress re-trigger loop.");
480
+ const fallbackSummary = buildStructuredFallbackSummary(preparation.previousSummary);
481
+ return {
482
+ compaction: {
483
+ summary: fallbackSummary,
484
+ firstKeptEntryId: preparation.firstKeptEntryId,
485
+ tokensBefore: preparation.tokensBefore,
486
+ },
487
+ };
488
+ }
489
+ // ── Extract file ops & tool failures ──
490
+ const { readFiles, modifiedFiles } = computeFileLists(preparation.fileOps);
491
+ const fileOpsSummary = formatFileOperations(readFiles, modifiedFiles);
492
+ const toolFailures = collectToolFailures([
493
+ ...preparation.messagesToSummarize,
494
+ ...preparation.turnPrefixMessages,
495
+ ]);
496
+ const toolFailureSection = formatToolFailuresSection(toolFailures);
497
+ // ── Resolve model + API key ──
498
+ const customInstructions = resolveCompactionInstructions(eventInstructions);
499
+ const model = ctx.model;
500
+ if (!model) {
501
+ console.warn("[compaction-safeguard] ctx.model is undefined; cancelling compaction to preserve history.");
502
+ return { cancel: true };
503
+ }
504
+ const apiKey = await ctx.modelRegistry.getApiKey(model);
505
+ if (!apiKey) {
506
+ console.warn("[compaction-safeguard] No API key available; cancelling compaction to preserve history.");
507
+ return { cancel: true };
508
+ }
509
+ try {
510
+ const contextWindowTokens = resolveContextWindowTokens(model);
511
+ const turnPrefixMessages = preparation.turnPrefixMessages ?? [];
512
+ let messagesToSummarize = preparation.messagesToSummarize;
513
+ const recentTurnsPreserve = DEFAULT_RECENT_TURNS_PRESERVE;
514
+ const qualityGuardMaxRetries = DEFAULT_QUALITY_GUARD_MAX_RETRIES;
515
+ const structuredInstructions = buildCompactionStructureInstructions(customInstructions);
516
+ const maxHistoryShare = 0.5;
517
+ const tokensBefore = typeof preparation.tokensBefore === "number" && Number.isFinite(preparation.tokensBefore)
518
+ ? preparation.tokensBefore
519
+ : undefined;
520
+ let droppedSummary;
521
+ // ── Token budget check ──
522
+ if (tokensBefore !== undefined) {
523
+ const summarizableTokens = estimateMessagesTokens(messagesToSummarize) + estimateMessagesTokens(turnPrefixMessages);
524
+ const newContentTokens = Math.max(0, Math.floor(tokensBefore - summarizableTokens));
525
+ const maxHistoryTokens = Math.floor(contextWindowTokens * maxHistoryShare * SAFETY_MARGIN);
526
+ if (newContentTokens > maxHistoryTokens) {
527
+ const pruned = pruneHistoryForContextShare({
528
+ messages: messagesToSummarize,
529
+ maxContextTokens: contextWindowTokens,
530
+ maxHistoryShare,
531
+ parts: 2,
532
+ });
533
+ if (pruned.droppedChunks > 0) {
534
+ const newContentRatio = (newContentTokens / contextWindowTokens) * 100;
535
+ console.warn(`[compaction-safeguard] New content uses ${newContentRatio.toFixed(1)}% of context; dropped ${pruned.droppedChunks} older chunk(s) ` +
536
+ `(${pruned.droppedMessages} messages) to fit history budget.`);
537
+ messagesToSummarize = pruned.messages;
538
+ if (pruned.droppedMessagesList.length > 0) {
539
+ try {
540
+ const droppedChunkRatio = computeAdaptiveChunkRatio(pruned.droppedMessagesList, contextWindowTokens);
541
+ const droppedMaxChunkTokens = Math.max(1, Math.floor(contextWindowTokens * droppedChunkRatio) -
542
+ SUMMARIZATION_OVERHEAD_TOKENS);
543
+ droppedSummary = await summarizeInStages({
544
+ messages: pruned.droppedMessagesList,
545
+ model,
546
+ apiKey,
547
+ signal,
548
+ reserveTokens: Math.max(1, Math.floor(preparation.settings.reserveTokens)),
549
+ maxChunkTokens: droppedMaxChunkTokens,
550
+ contextWindow: contextWindowTokens,
551
+ customInstructions: structuredInstructions,
552
+ previousSummary: preparation.previousSummary,
553
+ });
554
+ }
555
+ catch (droppedError) {
556
+ console.warn(`[compaction-safeguard] Failed to summarize dropped messages: ${droppedError instanceof Error ? droppedError.message : String(droppedError)}`);
557
+ }
558
+ }
559
+ }
560
+ }
561
+ }
562
+ // ── Recent turns preservation ──
563
+ // Extract latestUserAsk BEFORE splitting so it reflects the actual most recent ask,
564
+ // not an older one left after preserved turns are removed.
565
+ const latestUserAsk = extractLatestUserAsk([...messagesToSummarize, ...turnPrefixMessages]);
566
+ const { summarizableMessages: summaryTargetMessages, preservedMessages: preservedRecentMessages, } = splitPreservedRecentTurns({
567
+ messages: messagesToSummarize,
568
+ recentTurnsPreserve,
569
+ });
570
+ messagesToSummarize = summaryTargetMessages;
571
+ const preservedTurnsSection = formatPreservedTurnsSection(preservedRecentMessages);
572
+ const identifierSeedText = [...messagesToSummarize, ...turnPrefixMessages]
573
+ .slice(-10)
574
+ .map((message) => extractMessageText(message))
575
+ .filter(Boolean)
576
+ .join("\n");
577
+ const identifiers = extractOpaqueIdentifiers(identifierSeedText);
578
+ // ── Summarize with adaptive chunk ratio ──
579
+ const allMessages = [...messagesToSummarize, ...turnPrefixMessages];
580
+ const adaptiveRatio = computeAdaptiveChunkRatio(allMessages, contextWindowTokens);
581
+ const maxChunkTokens = Math.max(1, Math.floor(contextWindowTokens * adaptiveRatio) - SUMMARIZATION_OVERHEAD_TOKENS);
582
+ const reserveTokens = Math.max(1, Math.floor(preparation.settings.reserveTokens));
583
+ const effectivePreviousSummary = droppedSummary ?? preparation.previousSummary;
584
+ let summary = "";
585
+ let currentInstructions = structuredInstructions;
586
+ const totalAttempts = qualityGuardMaxRetries + 1;
587
+ let lastSuccessfulSummary = null;
588
+ // ── Quality guard retry loop ──
589
+ for (let attempt = 0; attempt < totalAttempts; attempt += 1) {
590
+ let summaryWithoutPreservedTurns = "";
591
+ let summaryWithPreservedTurns = "";
592
+ try {
593
+ const historySummary = messagesToSummarize.length > 0
594
+ ? await summarizeInStages({
595
+ messages: messagesToSummarize,
596
+ model,
597
+ apiKey,
598
+ signal,
599
+ reserveTokens,
600
+ maxChunkTokens,
601
+ contextWindow: contextWindowTokens,
602
+ customInstructions: currentInstructions,
603
+ previousSummary: effectivePreviousSummary,
604
+ })
605
+ : buildStructuredFallbackSummary(effectivePreviousSummary);
606
+ summaryWithoutPreservedTurns = historySummary;
607
+ // Handle split turn prefix
608
+ if (preparation.isSplitTurn && turnPrefixMessages.length > 0) {
609
+ const composedTurnInstructions = [
610
+ TURN_PREFIX_INSTRUCTIONS,
611
+ "Additional requirements:",
612
+ currentInstructions,
613
+ ].join("\n\n");
614
+ const prefixSummary = await summarizeInStages({
615
+ messages: turnPrefixMessages,
616
+ model,
617
+ apiKey,
618
+ signal,
619
+ reserveTokens,
620
+ maxChunkTokens,
621
+ contextWindow: contextWindowTokens,
622
+ customInstructions: composedTurnInstructions,
623
+ previousSummary: undefined,
624
+ });
625
+ const splitTurnSection = `**Turn Context (split turn):**\n\n${prefixSummary}`;
626
+ summaryWithoutPreservedTurns = historySummary.trim()
627
+ ? `${historySummary}\n\n---\n\n${splitTurnSection}`
628
+ : splitTurnSection;
629
+ }
630
+ summaryWithPreservedTurns = appendSummarySection(summaryWithoutPreservedTurns, preservedTurnsSection);
631
+ }
632
+ catch (attemptError) {
633
+ if (lastSuccessfulSummary && attempt > 0) {
634
+ console.warn(`[compaction-safeguard] Quality retry failed on attempt ${attempt + 1}; ` +
635
+ `keeping last successful summary: ${attemptError instanceof Error ? attemptError.message : String(attemptError)}`);
636
+ summary = lastSuccessfulSummary;
637
+ break;
638
+ }
639
+ throw attemptError;
640
+ }
641
+ lastSuccessfulSummary = summaryWithPreservedTurns;
642
+ const canRegenerate = messagesToSummarize.length > 0 ||
643
+ (preparation.isSplitTurn && turnPrefixMessages.length > 0);
644
+ if (!canRegenerate) {
645
+ summary = summaryWithPreservedTurns;
646
+ break;
647
+ }
648
+ const quality = auditSummaryQuality({
649
+ summary: summaryWithoutPreservedTurns,
650
+ identifiers,
651
+ latestAsk: latestUserAsk,
652
+ });
653
+ summary = summaryWithPreservedTurns;
654
+ if (quality.ok || attempt >= totalAttempts - 1)
655
+ break;
656
+ const reasons = quality.reasons.join(", ");
657
+ console.log(`[compaction-safeguard] Quality audit failed (attempt ${attempt + 1}): ${reasons}`);
658
+ currentInstructions =
659
+ `${structuredInstructions}\n\n` +
660
+ `Fix all issues and include every required section with exact identifiers preserved.\n\n` +
661
+ `Previous summary failed quality checks (${reasons}).`;
662
+ }
663
+ // ── Assemble final summary ──
664
+ summary = appendSummarySection(summary, toolFailureSection);
665
+ summary = appendSummarySection(summary, fileOpsSummary);
666
+ return {
667
+ compaction: {
668
+ summary,
669
+ firstKeptEntryId: preparation.firstKeptEntryId,
670
+ tokensBefore: preparation.tokensBefore,
671
+ details: { readFiles, modifiedFiles },
672
+ },
673
+ };
674
+ }
675
+ catch (error) {
676
+ console.warn(`[compaction-safeguard] Summarization failed; cancelling compaction to preserve history: ${error instanceof Error ? error.message : String(error)}`);
677
+ return { cancel: true };
678
+ }
679
+ });
680
+ }
681
+ //# sourceMappingURL=compaction-safeguard.js.map