openhermes 2.8.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CONTEXT.md +18 -0
  2. package/ETHOS.md +15 -0
  3. package/README.md +135 -292
  4. package/bootstrap.mjs +174 -512
  5. package/harness/agents/openhermes.md +87 -0
  6. package/harness/codex/CONSTITUTION.md +70 -148
  7. package/harness/codex/ROUTING.md +126 -0
  8. package/harness/commands/oh-doctor.md +26 -0
  9. package/harness/instructions/CONVENTIONS.md +206 -206
  10. package/harness/instructions/RUNTIME.md +54 -31
  11. package/harness/skills/oh-builder/SKILL.md +98 -0
  12. package/harness/skills/oh-caveman/SKILL.md +33 -0
  13. package/harness/skills/oh-expert/SKILL.md +121 -0
  14. package/harness/skills/oh-freeze/SKILL.md +28 -0
  15. package/harness/skills/oh-gauntlet/SKILL.md +119 -0
  16. package/harness/skills/oh-grill/SKILL.md +77 -0
  17. package/harness/skills/oh-guard/SKILL.md +33 -0
  18. package/harness/skills/oh-handoff/SKILL.md +33 -0
  19. package/harness/skills/oh-health/SKILL.md +90 -0
  20. package/harness/skills/oh-init/SKILL.md +78 -0
  21. package/harness/skills/oh-investigate/SKILL.md +35 -0
  22. package/harness/skills/oh-issue/SKILL.md +36 -0
  23. package/harness/skills/oh-learn/SKILL.md +28 -0
  24. package/harness/skills/oh-manifest/SKILL.md +84 -0
  25. package/harness/skills/oh-plan-review/SKILL.md +128 -0
  26. package/harness/skills/oh-planner/SKILL.md +157 -0
  27. package/harness/skills/oh-prd/SKILL.md +35 -0
  28. package/harness/skills/oh-retro/SKILL.md +33 -0
  29. package/harness/skills/oh-review/SKILL.md +110 -0
  30. package/harness/skills/oh-security/SKILL.md +110 -0
  31. package/harness/skills/oh-ship/SKILL.md +39 -0
  32. package/harness/skills/oh-skill-craft/SKILL.md +107 -0
  33. package/harness/skills/oh-skills-link/SKILL.md +29 -0
  34. package/harness/skills/oh-skills-list/SKILL.md +31 -0
  35. package/harness/skills/oh-triage/SKILL.md +36 -0
  36. package/index.mjs +3 -60
  37. package/lib/harness-resolver.mjs +77 -0
  38. package/lib/logger.mjs +62 -0
  39. package/package.json +49 -53
  40. package/test/plugins-behavioral.test.mjs +64 -0
  41. package/test/plugins.test.mjs +62 -0
  42. package/autorecall.mjs +0 -237
  43. package/curator.mjs +0 -482
  44. package/harness/commands/build-fix.md +0 -60
  45. package/harness/commands/checkpoint.md +0 -68
  46. package/harness/commands/code-review.md +0 -71
  47. package/harness/commands/doctor.md +0 -42
  48. package/harness/commands/eval.md +0 -89
  49. package/harness/commands/go-build.md +0 -87
  50. package/harness/commands/go-review.md +0 -71
  51. package/harness/commands/harness-audit.md +0 -90
  52. package/harness/commands/learn.md +0 -37
  53. package/harness/commands/loop-start.md +0 -38
  54. package/harness/commands/loop-status.md +0 -30
  55. package/harness/commands/memory-search.md +0 -37
  56. package/harness/commands/model-route.md +0 -32
  57. package/harness/commands/ohc.md +0 -13
  58. package/harness/commands/orchestrate.md +0 -88
  59. package/harness/commands/plan.md +0 -53
  60. package/harness/commands/quality-gate.md +0 -35
  61. package/harness/commands/refactor-clean.md +0 -102
  62. package/harness/commands/rust-build.md +0 -78
  63. package/harness/commands/rust-review.md +0 -65
  64. package/harness/commands/security.md +0 -93
  65. package/harness/commands/setup-pm.md +0 -65
  66. package/harness/commands/skill-create.md +0 -99
  67. package/harness/commands/test-coverage.md +0 -80
  68. package/harness/commands/update-codemaps.md +0 -81
  69. package/harness/commands/update-docs.md +0 -67
  70. package/harness/commands/verify.md +0 -68
  71. package/harness/prompts/architect.txt +0 -189
  72. package/harness/prompts/build-cpp.md +0 -98
  73. package/harness/prompts/build-error-resolver.md +0 -44
  74. package/harness/prompts/build-go.md +0 -340
  75. package/harness/prompts/build-java.md +0 -140
  76. package/harness/prompts/build-kotlin.md +0 -137
  77. package/harness/prompts/build-rust.md +0 -108
  78. package/harness/prompts/code-reviewer.md +0 -40
  79. package/harness/prompts/doc-updater.md +0 -206
  80. package/harness/prompts/docs-lookup.md +0 -71
  81. package/harness/prompts/e2e-runner.txt +0 -317
  82. package/harness/prompts/explore.md +0 -42
  83. package/harness/prompts/harness-optimizer.md +0 -42
  84. package/harness/prompts/loop-operator.md +0 -53
  85. package/harness/prompts/planner.md +0 -37
  86. package/harness/prompts/refactor-cleaner.md +0 -256
  87. package/harness/prompts/review-cpp.md +0 -81
  88. package/harness/prompts/review-database.md +0 -261
  89. package/harness/prompts/review-go.md +0 -257
  90. package/harness/prompts/review-java.md +0 -113
  91. package/harness/prompts/review-kotlin.md +0 -143
  92. package/harness/prompts/review-python.md +0 -101
  93. package/harness/prompts/review-rust.md +0 -77
  94. package/harness/prompts/security-reviewer.md +0 -42
  95. package/harness/prompts/tdd-guide.md +0 -228
  96. package/harness/rules/audit.md +0 -84
  97. package/harness/rules/checkpointing.md +0 -75
  98. package/harness/rules/context-loading.md +0 -33
  99. package/harness/rules/credential-exposure.md +0 -0
  100. package/harness/rules/delegation.md +0 -80
  101. package/harness/rules/handoff.md +0 -267
  102. package/harness/rules/memory-management.md +0 -28
  103. package/harness/rules/precedence.md +0 -52
  104. package/harness/rules/promotion.md +0 -46
  105. package/harness/rules/ranking.md +0 -64
  106. package/harness/rules/retrieval.md +0 -94
  107. package/harness/rules/runtime-guards.md +0 -196
  108. package/harness/rules/self-heal.md +0 -79
  109. package/harness/rules/session-start.md +0 -34
  110. package/harness/rules/skills-management.md +0 -165
  111. package/harness/rules/state-drift.md +0 -192
  112. package/harness/rules/verification.md +0 -88
  113. package/harness/scripts/sync-commands.mjs +0 -259
  114. package/harness/skills/.bundled_manifest +0 -17
  115. package/harness/skills/.usage.json +0 -6
  116. package/harness/skills/api-design/SKILL.md +0 -523
  117. package/harness/skills/backend-patterns/SKILL.md +0 -598
  118. package/harness/skills/coding-standards/SKILL.md +0 -549
  119. package/harness/skills/e2e-testing/SKILL.md +0 -326
  120. package/harness/skills/frontend-patterns/SKILL.md +0 -642
  121. package/harness/skills/frontend-slides/SKILL.md +0 -184
  122. package/harness/skills/security-review/SKILL.md +0 -495
  123. package/harness/skills/strategic-compact/SKILL.md +0 -131
  124. package/harness/skills/tdd-workflow/SKILL.md +0 -463
  125. package/harness/skills/verification-loop/SKILL.md +0 -126
  126. package/lib/ambient-memory.mjs +0 -167
  127. package/lib/handoff.mjs +0 -171
  128. package/lib/hardening.mjs +0 -146
  129. package/lib/memory-tools-plugin.mjs +0 -368
  130. package/lib/ohc/block-sync.mjs +0 -69
  131. package/lib/ohc/compress/search.mjs +0 -152
  132. package/lib/ohc/compress/state.mjs +0 -76
  133. package/lib/ohc/config.mjs +0 -185
  134. package/lib/ohc/message-ids.mjs +0 -178
  135. package/lib/ohc/notify.mjs +0 -135
  136. package/lib/ohc/protected-patterns.mjs +0 -55
  137. package/lib/ohc/prune-apply.mjs +0 -134
  138. package/lib/ohc/pruner.mjs +0 -608
  139. package/lib/ohc/reaper.mjs +0 -70
  140. package/lib/ohc/state.mjs +0 -265
  141. package/lib/ohc/strategies/deduplication.mjs +0 -72
  142. package/lib/ohc/strategies/index.mjs +0 -2
  143. package/lib/ohc/strategies/purge-errors.mjs +0 -43
  144. package/lib/ohc/token-utils.mjs +0 -26
  145. package/lib/ohc/updater.mjs +0 -132
  146. package/lib/paths.mjs +0 -49
  147. package/lib/schema-validator.mjs +0 -79
  148. package/lib/search.mjs +0 -48
  149. package/schemas/audit.schema.json +0 -82
  150. package/schemas/backlog.schema.json +0 -63
  151. package/schemas/checkpoint.schema.json +0 -65
  152. package/schemas/constraint.schema.json +0 -62
  153. package/schemas/decision.schema.json +0 -63
  154. package/schemas/instinct.schema.json +0 -63
  155. package/schemas/loop-state.schema.json +0 -33
  156. package/schemas/mistake.schema.json +0 -64
  157. package/schemas/verification_receipt.schema.json +0 -88
  158. package/skill-builder.mjs +0 -88
@@ -1,185 +0,0 @@
1
- import fs from "node:fs"
2
- import path from "node:path"
3
- import os from "node:os"
4
-
5
- const GLOBAL_DIR = path.join(os.homedir(), ".config", "opencode")
6
- const GLOBAL_PATH = path.join(GLOBAL_DIR, "ohc.json")
7
- const GLOBAL_PATH_JSONC = path.join(GLOBAL_DIR, "ohc.jsonc")
8
-
9
- const DEFAULTS = {
10
- enabled: true,
11
- notification: "chat",
12
- notificationMode: "detailed",
13
- max: 150000,
14
- min: 50000,
15
- manualMode: { enabled: false, automaticStrategies: true },
16
- turnProtection: { enabled: false, turns: 4 },
17
- protectedFilePatterns: [],
18
- compress: {
19
-
20
- nudgeFrequency: 5,
21
- iterationNudgeThreshold: 15,
22
- nudgeForce: "soft",
23
- protectedTools: ["task", "skill", "todowrite", "todoread"],
24
- protectUserMessages: false,
25
- summaryBuffer: true,
26
- },
27
- strategies: {
28
- deduplication: { enabled: true, protectedTools: [] },
29
- purgeErrors: { enabled: true, turns: 4, protectedTools: [] },
30
- },
31
- }
32
-
33
- function findOpencodeDir(startDir) {
34
- let current = startDir
35
- while (current && current.length > 3) {
36
- const candidate = path.join(current, ".opencode")
37
- if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
38
- return candidate
39
- }
40
- const parent = path.dirname(current)
41
- if (parent === current) break
42
- current = parent
43
- }
44
- return null
45
- }
46
-
47
- function loadFile(filePath) {
48
- try {
49
- const content = fs.readFileSync(filePath, "utf8").trim()
50
- if (!content) return null
51
- if (filePath.endsWith(".jsonc")) {
52
- const stripped = content
53
- .replace(/"(?:[^"\\]|\\.)*"|\/\/.*/gm, m => m.startsWith('"') ? m : "")
54
- .replace(/\/\*[\s\S]*?\*\//g, "")
55
- return JSON.parse(stripped)
56
- }
57
- return JSON.parse(content)
58
- } catch {
59
- return null
60
- }
61
- }
62
-
63
- function mergeDeep(base, override) {
64
- if (!override || typeof override !== "object") return base
65
- const result = { ...base }
66
- for (const key of Object.keys(override)) {
67
- if (override[key] === undefined) continue
68
- if (typeof override[key] === "object" && !Array.isArray(override[key]) && override[key] !== null) {
69
- result[key] = mergeDeep(base[key] || {}, override[key])
70
- } else {
71
- result[key] = override[key]
72
- }
73
- }
74
- return result
75
- }
76
-
77
- function mergeArrays(base, override) {
78
- if (!override || !Array.isArray(override)) return base
79
- return [...new Set([...base, ...override])]
80
- }
81
-
82
- function mergeManualMode(base, override) {
83
- if (!override || typeof override !== "object") return base
84
- return {
85
- enabled: override.enabled ?? base.enabled,
86
- automaticStrategies: override.automaticStrategies ?? base.automaticStrategies,
87
- }
88
- }
89
-
90
- function mergeTurnProtection(base, override) {
91
- if (!override || typeof override !== "object") return base
92
- return {
93
- enabled: override.enabled ?? base.enabled,
94
- turns: override.turns ?? base.turns,
95
- }
96
- }
97
-
98
- function mergeCompress(base, override) {
99
- if (!override || typeof override !== "object") return base
100
- return {
101
- nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
102
- iterationNudgeThreshold: override.iterationNudgeThreshold ?? base.iterationNudgeThreshold,
103
- nudgeForce: override.nudgeForce ?? base.nudgeForce,
104
- protectedTools: mergeArrays(base.protectedTools, override.protectedTools),
105
- protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
106
- summaryBuffer: override.summaryBuffer ?? base.summaryBuffer,
107
- }
108
- }
109
-
110
- function mergeStrategies(base, override) {
111
- if (!override || typeof override !== "object") return base
112
- return {
113
- deduplication: {
114
- enabled: override.deduplication?.enabled ?? base.deduplication.enabled,
115
- protectedTools: mergeArrays(base.deduplication.protectedTools, override.deduplication?.protectedTools),
116
- },
117
- purgeErrors: {
118
- enabled: override.purgeErrors?.enabled ?? base.purgeErrors.enabled,
119
- turns: override.purgeErrors?.turns ?? base.purgeErrors.turns,
120
- protectedTools: mergeArrays(base.purgeErrors.protectedTools, override.purgeErrors?.protectedTools),
121
- },
122
- }
123
- }
124
-
125
- function mergeLayer(base, data) {
126
- if (!data) return base
127
- return {
128
- enabled: data.enabled ?? base.enabled,
129
- notification: data.notification ?? base.notification,
130
- notificationMode: data.notificationMode ?? base.notificationMode,
131
- max: data.max ?? base.max,
132
- min: data.min ?? base.min,
133
- manualMode: mergeManualMode(base.manualMode, data.manualMode),
134
- turnProtection: mergeTurnProtection(base.turnProtection, data.turnProtection),
135
- protectedFilePatterns: mergeArrays(base.protectedFilePatterns, data.protectedFilePatterns),
136
- compress: mergeCompress(base.compress, data.compress),
137
- strategies: mergeStrategies(base.strategies, data.strategies),
138
- }
139
- }
140
-
141
- export function loadConfig(ctx) {
142
- let config = { ...DEFAULTS }
143
-
144
- const layers = [
145
- { path: fs.existsSync(GLOBAL_PATH_JSONC) ? GLOBAL_PATH_JSONC : (fs.existsSync(GLOBAL_PATH) ? GLOBAL_PATH : null), name: "global" },
146
- ]
147
-
148
- const opencodeConfigDir = process.env.OPENCODE_CONFIG_DIR
149
- if (opencodeConfigDir) {
150
- const cdJsonc = path.join(opencodeConfigDir, "ohc.jsonc")
151
- const cdJson = path.join(opencodeConfigDir, "ohc.json")
152
- const cdPath = fs.existsSync(cdJsonc) ? cdJsonc : (fs.existsSync(cdJson) ? cdJson : null)
153
- if (cdPath) layers.push({ path: cdPath, name: "configDir" })
154
- }
155
-
156
- if (ctx?.directory) {
157
- const opencodeDir = findOpencodeDir(ctx.directory)
158
- if (opencodeDir) {
159
- const pjJsonc = path.join(opencodeDir, "ohc.jsonc")
160
- const pjJson = path.join(opencodeDir, "ohc.json")
161
- const pjPath = fs.existsSync(pjJsonc) ? pjJsonc : (fs.existsSync(pjJson) ? pjJson : null)
162
- if (pjPath) layers.push({ path: pjPath, name: "project" })
163
- }
164
- }
165
-
166
- for (const layer of layers) {
167
- if (!layer.path) continue
168
- const data = loadFile(layer.path)
169
- if (data) config = mergeLayer(config, data)
170
- }
171
-
172
- config.max = typeof config.max === "number" && config.max > 0 ? config.max : DEFAULTS.max
173
- config.min = typeof config.min === "number" ? Math.max(10000, Math.min(config.min, config.max - 10000)) : DEFAULTS.min
174
-
175
- try { writeDefaults() } catch { /* best-effort */ }
176
-
177
- return config
178
- }
179
-
180
- function writeDefaults() {
181
- fs.mkdirSync(GLOBAL_DIR, { recursive: true })
182
- if (!fs.existsSync(GLOBAL_PATH) && !fs.existsSync(GLOBAL_PATH_JSONC)) {
183
- fs.writeFileSync(GLOBAL_PATH, JSON.stringify(DEFAULTS, null, 2) + "\n", "utf8")
184
- }
185
- }
@@ -1,178 +0,0 @@
1
- const MESSAGE_REF_REGEX = /^ohc(\d{4})$/
2
- const BLOCK_REF_REGEX = /^bk([1-9]\d*)$/
3
- const OHCTAG = "ohc-ref"
4
-
5
- export function formatMessageRef(index) {
6
- if (!Number.isInteger(index) || index < 1 || index > 9999) {
7
- throw new Error(`OHC ref index out of bounds: ${index}`)
8
- }
9
- return `ohc${index.toString().padStart(4, "0")}`
10
- }
11
-
12
- export function formatBlockRef(blockId) {
13
- if (!Number.isInteger(blockId) || blockId < 1) {
14
- throw new Error(`Invalid block ID: ${blockId}`)
15
- }
16
- return `bk${blockId}`
17
- }
18
-
19
- export function parseMessageRef(ref) {
20
- const m = (ref || "").trim().toLowerCase().match(MESSAGE_REF_REGEX)
21
- if (!m) return null
22
- const idx = parseInt(m[1], 10)
23
- return Number.isInteger(idx) && idx >= 1 && idx <= 9999 ? idx : null
24
- }
25
-
26
- export function parseBlockRef(ref) {
27
- const m = (ref || "").trim().toLowerCase().match(BLOCK_REF_REGEX)
28
- if (!m) return null
29
- const id = parseInt(m[1], 10)
30
- return Number.isInteger(id) ? id : null
31
- }
32
-
33
- export function parseBoundaryId(id) {
34
- const norm = (id || "").trim().toLowerCase()
35
- const msgIdx = parseMessageRef(norm)
36
- if (msgIdx !== null) {
37
- return { kind: "message", ref: formatMessageRef(msgIdx), index: msgIdx }
38
- }
39
- const bkId = parseBlockRef(norm)
40
- if (bkId !== null) {
41
- return { kind: "compressed-block", ref: formatBlockRef(bkId), blockId: bkId }
42
- }
43
- return null
44
- }
45
-
46
- function escapeXml(s) {
47
- return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
48
- }
49
-
50
- export function formatOhcTag(ref, attrs) {
51
- const serialized = Object.entries(attrs || {})
52
- .filter(([, v]) => typeof v === "string" && v.length > 0)
53
- .sort(([a], [b]) => a.localeCompare(b))
54
- .map(([k, v]) => ` ${k}="${escapeXml(v)}"`)
55
- .join("")
56
- return `\n<${OHCTAG}${serialized}>${ref}</${OHCTAG}>`
57
- }
58
-
59
- export function isIgnoredUserMessage(message) {
60
- if (!message?.info || message.info.role !== "user") return false
61
- const parts = Array.isArray(message.parts) ? message.parts : []
62
- if (parts.length === 0) return true
63
- return parts.every(p => p.ignored)
64
- }
65
-
66
- export function assignMessageRefs(state, messages) {
67
- let assigned = 0
68
- let skippedFirstUser = false
69
-
70
- for (const msg of messages) {
71
- if (isIgnoredUserMessage(msg)) continue
72
-
73
- if (state.isSubAgent && !skippedFirstUser && msg.info.role === "user") {
74
- skippedFirstUser = true
75
- continue
76
- }
77
-
78
- const rawId = msg.info?.id
79
- if (typeof rawId !== "string" || !rawId) continue
80
-
81
- const existing = state.messageIds.byRawId.get(rawId)
82
- if (existing) {
83
- if (state.messageIds.byRef.get(existing) !== rawId) {
84
- state.messageIds.byRef.set(existing, rawId)
85
- }
86
- continue
87
- }
88
-
89
- const ref = allocateNextRef(state)
90
- state.messageIds.byRawId.set(rawId, ref)
91
- state.messageIds.byRef.set(ref, rawId)
92
- assigned++
93
- }
94
-
95
- return assigned
96
- }
97
-
98
- function allocateNextRef(state) {
99
- let candidate = Number.isInteger(state.messageIds.nextRef)
100
- ? Math.max(1, state.messageIds.nextRef)
101
- : 1
102
-
103
- for (let attempt = 0; attempt < 9999; attempt++) {
104
- if (candidate > 9999) candidate = 1
105
- const ref = formatMessageRef(candidate)
106
- if (!state.messageIds.byRef.has(ref)) {
107
- state.messageIds.nextRef = candidate + 1
108
- return ref
109
- }
110
- candidate++
111
- }
112
-
113
- state.messageIds.nextRef = 1
114
- return formatMessageRef(1)
115
- }
116
-
117
- export function cleanupMessageRefs(state, removedRawIds) {
118
- for (const rawId of removedRawIds) {
119
- const ref = state.messageIds.byRawId.get(rawId)
120
- if (ref) {
121
- state.messageIds.byRawId.delete(rawId)
122
- state.messageIds.byRef.delete(ref)
123
- }
124
- }
125
- }
126
-
127
- export function injectMessageIds(state, messages) {
128
- for (const msg of messages) {
129
- if (isIgnoredUserMessage(msg)) continue
130
- const msgRef = state.messageIds.byRawId.get(msg.info?.id)
131
- if (!msgRef) continue
132
- const tag = formatOhcTag(msgRef)
133
-
134
- if (msg.info.role === "user") {
135
- let injected = false
136
- for (const part of msg.parts) {
137
- if (part.type === "text") {
138
- part.text += tag
139
- injected = true
140
- break
141
- }
142
- }
143
- if (!injected) {
144
- msg.parts.push({ type: "text", text: tag })
145
- }
146
- continue
147
- }
148
-
149
- if (msg.info.role !== "assistant") continue
150
-
151
- let hasContent = Array.isArray(msg.parts) && msg.parts.some(p => p.type === "text" || p.type === "tool")
152
- if (!hasContent) continue
153
-
154
- let injected = false
155
- for (const part of msg.parts) {
156
- if (part.type !== "tool" || typeof part.state?.output !== "string") continue
157
- part.state.output = (part.state.output || "") + tag
158
- injected = true
159
- break
160
- }
161
-
162
- if (injected) continue
163
-
164
- const lastText = [...msg.parts].reverse().find(p => p.type === "text")
165
- if (lastText) {
166
- lastText.text += tag
167
- continue
168
- }
169
-
170
- const firstToolIdx = msg.parts.findIndex(p => p.type === "tool")
171
- const synth = { type: "text", text: tag }
172
- if (firstToolIdx === -1) {
173
- msg.parts.push(synth)
174
- } else {
175
- msg.parts.splice(firstToolIdx, 0, synth)
176
- }
177
- }
178
- }
@@ -1,135 +0,0 @@
1
- function formatTokenCount(tokens) {
2
- if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`.replace(".0M", "M")
3
- if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`.replace(".0K", "K")
4
- return String(tokens)
5
- }
6
-
7
- function truncate(str, max) {
8
- if (!str) return ""
9
- return str.length > max ? str.slice(0, max) + "\u2026" : str
10
- }
11
-
12
- function buildCompressionDetailed(count, savedTotal, blockCount, summary) {
13
- const parts = [
14
- `${formatTokenCount(savedTotal)} saved \u00b7 ${count} msg${count === 1 ? "" : "s"} removed \u00b7 #${blockCount} block${blockCount === 1 ? "" : "s"}`,
15
- ]
16
- if (summary) parts.push(truncate(summary, 48))
17
- return parts.join("\n")
18
- }
19
-
20
- function buildCompressionMinimal(count, savedTotal, blockCount) {
21
- return `${formatTokenCount(savedTotal)} saved \u00b7 ${blockCount} block${blockCount === 1 ? "" : "s"} \u00b7 ${count} msg${count === 1 ? "" : "s"} removed`
22
- }
23
-
24
- function buildMemoryMessage(action, cls, id, summary) {
25
- const idPart = id ? truncate(id, 28) : ""
26
- return [action, cls, idPart].filter(Boolean).join(" \u00b7 ")
27
- }
28
-
29
- export async function sendCompressNotification(client, sessionId, config, count, summary, tokensRemoved, ss, currentMessageCount) {
30
- if (count === 0) return false
31
-
32
- const savedTotal = ss?.totalTokensSaved || 0
33
- const blockCount = ss?.blockCount || 0
34
-
35
- const notifType = config.notification ?? "chat"
36
- const notifMode = config.notificationMode ?? "detailed"
37
-
38
- if (notifType === "off") return false
39
-
40
- if (notifType === "toast") {
41
- const message = notifMode === "minimal"
42
- ? buildCompressionMinimal(count, savedTotal, blockCount)
43
- : buildCompressionDetailed(count, savedTotal, blockCount, summary)
44
- try {
45
- await client.tui.showToast({
46
- body: {
47
- title: "OHC Compression",
48
- message,
49
- variant: "info",
50
- duration: 5000,
51
- },
52
- })
53
- } catch {}
54
- return true
55
- }
56
-
57
- const message = notifMode === "minimal"
58
- ? buildCompressionMinimal(count, savedTotal, blockCount)
59
- : buildCompressionDetailed(count, savedTotal, blockCount, summary)
60
- try {
61
- await client.session.prompt({
62
- path: { id: sessionId },
63
- body: {
64
- noReply: true,
65
- parts: [{ type: "text", text: `OHC: ${message}`, ignored: true }],
66
- },
67
- })
68
- } catch {}
69
- return true
70
- }
71
-
72
- export async function sendStrategyNotification(client, sessionId, config, strategy, count, detail) {
73
- if (count === 0) return false
74
- const notifType = config.notification ?? "chat"
75
- if (notifType === "off") return false
76
-
77
- const message = detail ? `${strategy} \u2014 ${count} pruned \u00b7 ${truncate(detail, 36)}` : `${strategy} \u2014 ${count} pruned`
78
-
79
- if (notifType === "toast") {
80
- try {
81
- await client.tui.showToast({
82
- body: {
83
- title: "OHC Strategy",
84
- message,
85
- variant: "info",
86
- duration: 3000,
87
- },
88
- })
89
- } catch {}
90
- return true
91
- }
92
-
93
- try {
94
- await client.session.prompt({
95
- path: { id: sessionId },
96
- body: {
97
- noReply: true,
98
- parts: [{ type: "text", text: `OHC: ${message}`, ignored: true }],
99
- },
100
- })
101
- } catch {}
102
- return true
103
- }
104
-
105
- export async function sendMemoryNotification(client, sessionId, config, action, cls, id, summary) {
106
- const notifType = config.notification ?? "toast"
107
- if (notifType === "off") return false
108
-
109
- const message = buildMemoryMessage(action, cls, id, summary)
110
-
111
- if (notifType === "toast") {
112
- try {
113
- await client.tui.showToast({
114
- body: {
115
- title: "OHC Memory",
116
- message,
117
- variant: "info",
118
- duration: 4000,
119
- },
120
- })
121
- } catch {}
122
- return true
123
- }
124
-
125
- try {
126
- await client.session.prompt({
127
- path: { id: sessionId },
128
- body: {
129
- noReply: true,
130
- parts: [{ type: "text", text: `OHC: ${message}`, ignored: true }],
131
- },
132
- })
133
- } catch {}
134
- return true
135
- }
@@ -1,55 +0,0 @@
1
- const DEFAULT_PROTECTED_TOOLS = new Set([
2
- "task", "skill", "todowrite", "todoread",
3
- "compress", "batch", "plan_enter", "plan_exit",
4
- "write", "edit", "bash", "webfetch",
5
- ])
6
-
7
- export function isToolNameProtected(toolName, extraProtected = []) {
8
- if (DEFAULT_PROTECTED_TOOLS.has(toolName)) return true
9
- return extraProtected.includes(toolName)
10
- }
11
-
12
- export function getFilePathsFromParameters(tool, params) {
13
- if (!params || typeof params !== "object") return []
14
- if (params.filePath) return [params.filePath]
15
- if (params.file_path) return [params.file_path]
16
- if (params.path) return [params.path]
17
- if (params.target) return [params.target]
18
- if (params.directory) return [params.directory]
19
- if (tool === "glob" && params.pattern) return [params.pattern]
20
- return []
21
- }
22
-
23
- export function isFilePathProtected(filePaths, protectedPatterns = []) {
24
- if (!protectedPatterns?.length) return false
25
- if (!filePaths?.length) return false
26
- for (const fp of filePaths) {
27
- for (const pattern of protectedPatterns) {
28
- if (globMatch(fp, pattern)) return true
29
- }
30
- }
31
- return false
32
- }
33
-
34
- function globMatch(filePath, pattern) {
35
- const regexStr = pattern
36
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
37
- .replace(/\*\*/g, ".*")
38
- .replace(/\*/g, "[^/]*")
39
- .replace(/\?/g, ".")
40
- try {
41
- return new RegExp(`^${regexStr}$`, "i").test(filePath)
42
- } catch {
43
- return filePath.toLowerCase() === pattern.toLowerCase()
44
- }
45
- }
46
-
47
- export function getTurnProtectionTags(state) {
48
- const config = state.protectedTurns || {}
49
- if (!config.enabled || !config.turns) return []
50
- const threshold = state.currentTurn - config.turns
51
- return [...state.toolIdList].filter(id => {
52
- const entry = state.toolParameters.get(id)
53
- return entry && entry.turn > threshold
54
- })
55
- }
@@ -1,134 +0,0 @@
1
- const PRUNED_OUTPUT_PLACEHOLDER = "[Output removed — information superseded or no longer needed]"
2
- const PRUNED_ERROR_INPUT_PLACEHOLDER = "[input removed — failed tool call]"
3
- const PRUNED_QUESTION_INPUT_PLACEHOLDER = "[questions removed — see output for answers]"
4
-
5
- export function applyPruneTools(state, messages) {
6
- if (!state.prune.tools.size) return 0
7
-
8
- let prunedCount = 0
9
-
10
- for (const msg of messages) {
11
- if (!Array.isArray(msg.parts)) continue
12
-
13
- for (const part of msg.parts) {
14
- if (part.type !== "tool" || !part.callID) continue
15
- if (!state.prune.tools.has(part.callID)) continue
16
-
17
- prunedCount++
18
-
19
- if (part.state?.status === "completed") {
20
- if (part.tool === "question") {
21
- if (part.state.input?.questions !== undefined) {
22
- part.state.input.questions = PRUNED_QUESTION_INPUT_PLACEHOLDER
23
- }
24
- } else if (part.tool !== "edit" && part.tool !== "write") {
25
- part.state = {
26
- ...part.state,
27
- output: PRUNED_OUTPUT_PLACEHOLDER,
28
- }
29
- }
30
- }
31
-
32
- if (part.state?.status === "error") {
33
- const input = part.state.input
34
- if (input && typeof input === "object") {
35
- for (const key of Object.keys(input)) {
36
- if (typeof input[key] === "string") {
37
- input[key] = PRUNED_ERROR_INPUT_PLACEHOLDER
38
- }
39
- }
40
- }
41
- }
42
- }
43
- }
44
-
45
- return prunedCount
46
- }
47
-
48
- export function filterCompressedBlocks(state, messages) {
49
- const ms = state.prune?.messages
50
- if (!ms || !ms.activeBlockIds?.size || !ms.blocksById?.size) return { removed: 0, injected: 0 }
51
-
52
- const coveredMessageIds = new Set()
53
- for (const [rawId, entry] of ms.byMessageId) {
54
- const hasActive = Array.isArray(entry.activeBlockIds) && entry.activeBlockIds.some(id => ms.activeBlockIds.has(id))
55
- if (hasActive) coveredMessageIds.add(rawId)
56
- }
57
-
58
- if (!coveredMessageIds.size) return { removed: 0, injected: 0 }
59
-
60
- const activeBlocks = [...ms.activeBlockIds].map(id => ms.blocksById.get(id)).filter(Boolean)
61
- const summaryByAnchor = new Map()
62
- for (const block of activeBlocks) {
63
- if (block.anchorMessageId && !summaryByAnchor.has(block.anchorMessageId)) {
64
- summaryByAnchor.set(block.anchorMessageId, block)
65
- }
66
- }
67
-
68
- let removed = 0
69
- let injected = 0
70
- const result = []
71
- const blocksDeployed = new Set()
72
-
73
- for (const msg of messages) {
74
- const mid = msg.info?.id
75
-
76
- if (mid && coveredMessageIds.has(mid)) {
77
- if (!blocksDeployed.has(mid)) {
78
- blocksDeployed.add(mid)
79
- const block = summaryByAnchor.get(mid)
80
- if (block) {
81
- result.push({
82
- parts: [{ type: "text", text: block.summary }],
83
- info: { role: "system" },
84
- })
85
- injected++
86
- }
87
- }
88
- removed++
89
- continue
90
- }
91
-
92
- result.push(msg)
93
- }
94
-
95
- messages.length = 0
96
- messages.push(...result)
97
-
98
- return { removed, injected }
99
- }
100
-
101
- export function applyFullToolRemoval(state, messages) {
102
- if (!state.prune.tools.size) return 0
103
-
104
- const removed = []
105
-
106
- for (const msg of messages) {
107
- if (!Array.isArray(msg.parts)) continue
108
-
109
- const toRemove = []
110
- for (const part of msg.parts) {
111
- if (part.type !== "tool" || !part.callID) continue
112
- if (!state.prune.tools.has(part.callID)) continue
113
- if (part.tool !== "edit" && part.tool !== "write") continue
114
- toRemove.push(part.callID)
115
- }
116
-
117
- if (!toRemove.length) continue
118
-
119
- const before = msg.parts.length
120
- msg.parts = msg.parts.filter(p => p.type !== "tool" || !toRemove.includes(p.callID))
121
- const after = msg.parts.length
122
- if (after === 0 && before > 0) {
123
- removed.push(msg.info.id)
124
- }
125
- }
126
-
127
- if (removed.length) {
128
- const result = messages.filter(m => !removed.includes(m.info.id))
129
- messages.length = 0
130
- messages.push(...result)
131
- }
132
-
133
- return removed.length
134
- }