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.
- package/CONTEXT.md +18 -0
- package/ETHOS.md +15 -0
- package/README.md +135 -292
- package/bootstrap.mjs +174 -512
- package/harness/agents/openhermes.md +87 -0
- package/harness/codex/CONSTITUTION.md +70 -148
- package/harness/codex/ROUTING.md +126 -0
- package/harness/commands/oh-doctor.md +26 -0
- package/harness/instructions/CONVENTIONS.md +206 -206
- package/harness/instructions/RUNTIME.md +54 -31
- package/harness/skills/oh-builder/SKILL.md +98 -0
- package/harness/skills/oh-caveman/SKILL.md +33 -0
- package/harness/skills/oh-expert/SKILL.md +121 -0
- package/harness/skills/oh-freeze/SKILL.md +28 -0
- package/harness/skills/oh-gauntlet/SKILL.md +119 -0
- package/harness/skills/oh-grill/SKILL.md +77 -0
- package/harness/skills/oh-guard/SKILL.md +33 -0
- package/harness/skills/oh-handoff/SKILL.md +33 -0
- package/harness/skills/oh-health/SKILL.md +90 -0
- package/harness/skills/oh-init/SKILL.md +78 -0
- package/harness/skills/oh-investigate/SKILL.md +35 -0
- package/harness/skills/oh-issue/SKILL.md +36 -0
- package/harness/skills/oh-learn/SKILL.md +28 -0
- package/harness/skills/oh-manifest/SKILL.md +84 -0
- package/harness/skills/oh-plan-review/SKILL.md +128 -0
- package/harness/skills/oh-planner/SKILL.md +157 -0
- package/harness/skills/oh-prd/SKILL.md +35 -0
- package/harness/skills/oh-retro/SKILL.md +33 -0
- package/harness/skills/oh-review/SKILL.md +110 -0
- package/harness/skills/oh-security/SKILL.md +110 -0
- package/harness/skills/oh-ship/SKILL.md +39 -0
- package/harness/skills/oh-skill-craft/SKILL.md +107 -0
- package/harness/skills/oh-skills-link/SKILL.md +29 -0
- package/harness/skills/oh-skills-list/SKILL.md +31 -0
- package/harness/skills/oh-triage/SKILL.md +36 -0
- package/index.mjs +3 -60
- package/lib/harness-resolver.mjs +77 -0
- package/lib/logger.mjs +62 -0
- package/package.json +49 -53
- package/test/plugins-behavioral.test.mjs +64 -0
- package/test/plugins.test.mjs +62 -0
- package/autorecall.mjs +0 -237
- package/curator.mjs +0 -482
- package/harness/commands/build-fix.md +0 -60
- package/harness/commands/checkpoint.md +0 -68
- package/harness/commands/code-review.md +0 -71
- package/harness/commands/doctor.md +0 -42
- package/harness/commands/eval.md +0 -89
- package/harness/commands/go-build.md +0 -87
- package/harness/commands/go-review.md +0 -71
- package/harness/commands/harness-audit.md +0 -90
- package/harness/commands/learn.md +0 -37
- package/harness/commands/loop-start.md +0 -38
- package/harness/commands/loop-status.md +0 -30
- package/harness/commands/memory-search.md +0 -37
- package/harness/commands/model-route.md +0 -32
- package/harness/commands/ohc.md +0 -13
- package/harness/commands/orchestrate.md +0 -88
- package/harness/commands/plan.md +0 -53
- package/harness/commands/quality-gate.md +0 -35
- package/harness/commands/refactor-clean.md +0 -102
- package/harness/commands/rust-build.md +0 -78
- package/harness/commands/rust-review.md +0 -65
- package/harness/commands/security.md +0 -93
- package/harness/commands/setup-pm.md +0 -65
- package/harness/commands/skill-create.md +0 -99
- package/harness/commands/test-coverage.md +0 -80
- package/harness/commands/update-codemaps.md +0 -81
- package/harness/commands/update-docs.md +0 -67
- package/harness/commands/verify.md +0 -68
- package/harness/prompts/architect.txt +0 -189
- package/harness/prompts/build-cpp.md +0 -98
- package/harness/prompts/build-error-resolver.md +0 -44
- package/harness/prompts/build-go.md +0 -340
- package/harness/prompts/build-java.md +0 -140
- package/harness/prompts/build-kotlin.md +0 -137
- package/harness/prompts/build-rust.md +0 -108
- package/harness/prompts/code-reviewer.md +0 -40
- package/harness/prompts/doc-updater.md +0 -206
- package/harness/prompts/docs-lookup.md +0 -71
- package/harness/prompts/e2e-runner.txt +0 -317
- package/harness/prompts/explore.md +0 -42
- package/harness/prompts/harness-optimizer.md +0 -42
- package/harness/prompts/loop-operator.md +0 -53
- package/harness/prompts/planner.md +0 -37
- package/harness/prompts/refactor-cleaner.md +0 -256
- package/harness/prompts/review-cpp.md +0 -81
- package/harness/prompts/review-database.md +0 -261
- package/harness/prompts/review-go.md +0 -257
- package/harness/prompts/review-java.md +0 -113
- package/harness/prompts/review-kotlin.md +0 -143
- package/harness/prompts/review-python.md +0 -101
- package/harness/prompts/review-rust.md +0 -77
- package/harness/prompts/security-reviewer.md +0 -42
- package/harness/prompts/tdd-guide.md +0 -228
- package/harness/rules/audit.md +0 -84
- package/harness/rules/checkpointing.md +0 -75
- package/harness/rules/context-loading.md +0 -33
- package/harness/rules/credential-exposure.md +0 -0
- package/harness/rules/delegation.md +0 -80
- package/harness/rules/handoff.md +0 -267
- package/harness/rules/memory-management.md +0 -28
- package/harness/rules/precedence.md +0 -52
- package/harness/rules/promotion.md +0 -46
- package/harness/rules/ranking.md +0 -64
- package/harness/rules/retrieval.md +0 -94
- package/harness/rules/runtime-guards.md +0 -196
- package/harness/rules/self-heal.md +0 -79
- package/harness/rules/session-start.md +0 -34
- package/harness/rules/skills-management.md +0 -165
- package/harness/rules/state-drift.md +0 -192
- package/harness/rules/verification.md +0 -88
- package/harness/scripts/sync-commands.mjs +0 -259
- package/harness/skills/.bundled_manifest +0 -17
- package/harness/skills/.usage.json +0 -6
- package/harness/skills/api-design/SKILL.md +0 -523
- package/harness/skills/backend-patterns/SKILL.md +0 -598
- package/harness/skills/coding-standards/SKILL.md +0 -549
- package/harness/skills/e2e-testing/SKILL.md +0 -326
- package/harness/skills/frontend-patterns/SKILL.md +0 -642
- package/harness/skills/frontend-slides/SKILL.md +0 -184
- package/harness/skills/security-review/SKILL.md +0 -495
- package/harness/skills/strategic-compact/SKILL.md +0 -131
- package/harness/skills/tdd-workflow/SKILL.md +0 -463
- package/harness/skills/verification-loop/SKILL.md +0 -126
- package/lib/ambient-memory.mjs +0 -167
- package/lib/handoff.mjs +0 -171
- package/lib/hardening.mjs +0 -146
- package/lib/memory-tools-plugin.mjs +0 -368
- package/lib/ohc/block-sync.mjs +0 -69
- package/lib/ohc/compress/search.mjs +0 -152
- package/lib/ohc/compress/state.mjs +0 -76
- package/lib/ohc/config.mjs +0 -185
- package/lib/ohc/message-ids.mjs +0 -178
- package/lib/ohc/notify.mjs +0 -135
- package/lib/ohc/protected-patterns.mjs +0 -55
- package/lib/ohc/prune-apply.mjs +0 -134
- package/lib/ohc/pruner.mjs +0 -608
- package/lib/ohc/reaper.mjs +0 -70
- package/lib/ohc/state.mjs +0 -265
- package/lib/ohc/strategies/deduplication.mjs +0 -72
- package/lib/ohc/strategies/index.mjs +0 -2
- package/lib/ohc/strategies/purge-errors.mjs +0 -43
- package/lib/ohc/token-utils.mjs +0 -26
- package/lib/ohc/updater.mjs +0 -132
- package/lib/paths.mjs +0 -49
- package/lib/schema-validator.mjs +0 -79
- package/lib/search.mjs +0 -48
- package/schemas/audit.schema.json +0 -82
- package/schemas/backlog.schema.json +0 -63
- package/schemas/checkpoint.schema.json +0 -65
- package/schemas/constraint.schema.json +0 -62
- package/schemas/decision.schema.json +0 -63
- package/schemas/instinct.schema.json +0 -63
- package/schemas/loop-state.schema.json +0 -33
- package/schemas/mistake.schema.json +0 -64
- package/schemas/verification_receipt.schema.json +0 -88
- package/skill-builder.mjs +0 -88
package/lib/ohc/config.mjs
DELETED
|
@@ -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
|
-
}
|
package/lib/ohc/message-ids.mjs
DELETED
|
@@ -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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">")
|
|
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
|
-
}
|
package/lib/ohc/notify.mjs
DELETED
|
@@ -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
|
-
}
|
package/lib/ohc/prune-apply.mjs
DELETED
|
@@ -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
|
-
}
|