openhermes 1.12.1 → 2.5.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/README.md +126 -207
- package/autorecall.mjs +79 -12
- package/bootstrap.mjs +123 -24
- package/curator.mjs +4 -40
- package/harness/commands/harness-audit.md +1 -1
- package/harness/commands/learn.md +2 -2
- package/harness/commands/memory-search.md +2 -2
- package/harness/commands/ohc.md +13 -0
- package/harness/constitution/soul.md +16 -4
- package/harness/instructions/RUNTIME.md +6 -3
- package/harness/prompts/architect.txt +14 -0
- package/harness/prompts/build-cpp.md +15 -1
- package/harness/prompts/build-error-resolver.md +15 -9
- package/harness/prompts/build-go.md +14 -0
- package/harness/prompts/build-java.md +15 -1
- package/harness/prompts/build-kotlin.md +15 -1
- package/harness/prompts/build-rust.md +14 -0
- package/harness/prompts/code-reviewer.md +15 -9
- package/harness/prompts/doc-updater.md +13 -0
- package/harness/prompts/docs-lookup.md +11 -0
- package/harness/prompts/e2e-runner.txt +12 -0
- package/harness/prompts/explore.md +16 -4
- package/harness/prompts/harness-optimizer.md +12 -0
- package/harness/prompts/loop-operator.md +11 -0
- package/harness/prompts/planner.md +15 -9
- package/harness/prompts/refactor-cleaner.md +14 -0
- package/harness/prompts/review-cpp.md +14 -1
- package/harness/prompts/review-database.md +13 -0
- package/harness/prompts/review-go.md +13 -0
- package/harness/prompts/review-java.md +14 -1
- package/harness/prompts/review-kotlin.md +13 -0
- package/harness/prompts/review-python.md +14 -1
- package/harness/prompts/review-rust.md +13 -0
- package/harness/prompts/security-reviewer.md +15 -9
- package/harness/prompts/tdd-guide.md +14 -0
- package/harness/rules/audit.md +2 -2
- package/harness/rules/delegation.md +0 -2
- package/harness/rules/handoff.md +267 -0
- package/harness/rules/memory-management.md +4 -4
- package/harness/rules/precedence.md +1 -1
- package/harness/rules/retrieval.md +5 -5
- package/harness/rules/runtime-guards.md +1 -1
- package/harness/rules/self-heal.md +1 -1
- package/harness/rules/session-start.md +5 -5
- package/harness/rules/skills-management.md +2 -2
- package/harness/rules/verification.md +4 -4
- package/harness/scripts/sync-commands.mjs +259 -0
- package/index.mjs +6 -2
- package/lib/ambient-memory.mjs +167 -0
- package/lib/handoff.mjs +176 -0
- package/lib/hardening.mjs +13 -8
- package/lib/memory-tools-plugin.mjs +107 -54
- package/lib/ohc/block-sync.mjs +69 -0
- package/lib/ohc/compress/search.mjs +152 -0
- package/lib/ohc/compress/state.mjs +76 -0
- package/lib/ohc/config.mjs +172 -16
- package/lib/ohc/message-ids.mjs +168 -0
- package/lib/ohc/notify.mjs +150 -0
- package/lib/ohc/protected-patterns.mjs +54 -0
- package/lib/ohc/prune-apply.mjs +134 -0
- package/lib/ohc/pruner.mjs +406 -55
- package/lib/ohc/reaper.mjs +12 -3
- package/lib/ohc/state.mjs +246 -15
- package/lib/ohc/strategies/deduplication.mjs +72 -0
- package/lib/ohc/strategies/index.mjs +2 -0
- package/lib/ohc/strategies/purge-errors.mjs +43 -0
- package/lib/ohc/token-utils.mjs +26 -0
- package/lib/ohc/updater.mjs +36 -13
- package/lib/paths.mjs +0 -3
- package/lib/search.mjs +48 -0
- package/package.json +6 -2
- package/schemas/audit.schema.json +22 -1
- package/schemas/backlog.schema.json +23 -2
- package/schemas/checkpoint.schema.json +23 -2
- package/schemas/constraint.schema.json +23 -2
- package/schemas/decision.schema.json +23 -2
- package/schemas/instinct.schema.json +23 -2
- package/schemas/mistake.schema.json +23 -2
- package/schemas/verification_receipt.schema.json +23 -2
- package/skill-builder.mjs +12 -23
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { parseBoundaryId, formatBlockRef } from "../message-ids.mjs"
|
|
2
|
+
import { totalTokens } from "../reaper.mjs"
|
|
3
|
+
|
|
4
|
+
export function buildSearchContext(state, rawMessages) {
|
|
5
|
+
const rawMessagesById = new Map()
|
|
6
|
+
const rawIndexById = new Map()
|
|
7
|
+
|
|
8
|
+
for (const msg of rawMessages) {
|
|
9
|
+
if (msg.info?.id) rawMessagesById.set(msg.info.id, msg)
|
|
10
|
+
}
|
|
11
|
+
for (let i = 0; i < rawMessages.length; i++) {
|
|
12
|
+
const msg = rawMessages[i]
|
|
13
|
+
if (msg?.info?.id) rawIndexById.set(msg.info.id, i)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const summaryByBlockId = new Map()
|
|
17
|
+
const ms = state.prune?.messages
|
|
18
|
+
if (ms?.blocksById) {
|
|
19
|
+
for (const [blockId, block] of ms.blocksById) {
|
|
20
|
+
if (block.active) summaryByBlockId.set(blockId, block)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { rawMessages, rawMessagesById, rawIndexById, summaryByBlockId }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveBoundaryIds(context, state, startId, endId) {
|
|
28
|
+
const lookup = buildBoundaryLookup(context, state)
|
|
29
|
+
|
|
30
|
+
const parsedStart = parseBoundaryId(startId)
|
|
31
|
+
const parsedEnd = parseBoundaryId(endId)
|
|
32
|
+
|
|
33
|
+
if (!parsedStart) throw new Error(`startId "${startId}" is invalid. Use ohcNNNN (message) or bkNN (block).`)
|
|
34
|
+
if (!parsedEnd) throw new Error(`endId "${endId}" is invalid. Use ohcNNNN (message) or bkNN (block).`)
|
|
35
|
+
|
|
36
|
+
const startRef = lookup.get(parsedStart.ref)
|
|
37
|
+
const endRef = lookup.get(parsedEnd.ref)
|
|
38
|
+
|
|
39
|
+
if (!startRef) throw new Error(`startId ${parsedStart.ref} not found in context.`)
|
|
40
|
+
if (!endRef) throw new Error(`endId ${parsedEnd.ref} not found in context.`)
|
|
41
|
+
|
|
42
|
+
if (startRef.rawIndex > endRef.rawIndex) {
|
|
43
|
+
throw new Error(`startId ${parsedStart.ref} appears after endId ${parsedEnd.ref}. Start must come before end.`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { startReference: startRef, endReference: endRef }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildBoundaryLookup(context, state) {
|
|
50
|
+
const lookup = new Map()
|
|
51
|
+
|
|
52
|
+
for (const [msgRef, rawId] of state.messageIds.byRef) {
|
|
53
|
+
const rawMsg = context.rawMessagesById.get(rawId)
|
|
54
|
+
if (!rawMsg) continue
|
|
55
|
+
|
|
56
|
+
const rawIndex = context.rawIndexById.get(rawId)
|
|
57
|
+
if (rawIndex === undefined) continue
|
|
58
|
+
|
|
59
|
+
lookup.set(msgRef, { kind: "message", rawIndex, messageId: rawId })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const summaries = Array.from(context.summaryByBlockId.values()).sort((a, b) => a.blockId - b.blockId)
|
|
63
|
+
for (const summary of summaries) {
|
|
64
|
+
const anchorMsg = context.rawMessagesById.get(summary.anchorMessageId)
|
|
65
|
+
if (!anchorMsg) continue
|
|
66
|
+
|
|
67
|
+
const rawIndex = context.rawIndexById.get(summary.anchorMessageId)
|
|
68
|
+
if (rawIndex === undefined) continue
|
|
69
|
+
|
|
70
|
+
const bkRef = formatBlockRef(summary.blockId)
|
|
71
|
+
if (!lookup.has(bkRef)) {
|
|
72
|
+
lookup.set(bkRef, {
|
|
73
|
+
kind: "compressed-block",
|
|
74
|
+
rawIndex,
|
|
75
|
+
blockId: summary.blockId,
|
|
76
|
+
anchorMessageId: summary.anchorMessageId,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return lookup
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function resolveSelection(context, startReference, endReference) {
|
|
85
|
+
const messageIds = []
|
|
86
|
+
const messageSeen = new Set()
|
|
87
|
+
const toolIds = []
|
|
88
|
+
const toolSeen = new Set()
|
|
89
|
+
const requiredBlockIds = []
|
|
90
|
+
const requiredBlockSeen = new Set()
|
|
91
|
+
|
|
92
|
+
for (let i = startReference.rawIndex; i <= endReference.rawIndex; i++) {
|
|
93
|
+
const msg = context.rawMessages[i]
|
|
94
|
+
if (!msg) continue
|
|
95
|
+
|
|
96
|
+
const mid = msg.info?.id
|
|
97
|
+
if (mid && !messageSeen.has(mid)) {
|
|
98
|
+
messageSeen.add(mid)
|
|
99
|
+
messageIds.push(mid)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
103
|
+
for (const part of parts) {
|
|
104
|
+
if (part.type !== "tool" || !part.callID || toolSeen.has(part.callID)) continue
|
|
105
|
+
toolSeen.add(part.callID)
|
|
106
|
+
toolIds.push(part.callID)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const selectedIds = new Set(messageIds)
|
|
111
|
+
const summariesInRange = []
|
|
112
|
+
for (const block of context.summaryByBlockId.values()) {
|
|
113
|
+
if (!selectedIds.has(block.anchorMessageId)) continue
|
|
114
|
+
const idx = context.rawIndexById.get(block.anchorMessageId)
|
|
115
|
+
if (idx === undefined) continue
|
|
116
|
+
summariesInRange.push({ blockId: block.blockId, rawIndex: idx })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
summariesInRange.sort((a, b) => a.rawIndex - b.rawIndex || a.blockId - b.blockId)
|
|
120
|
+
for (const s of summariesInRange) {
|
|
121
|
+
if (!requiredBlockSeen.has(s.blockId)) {
|
|
122
|
+
requiredBlockSeen.add(s.blockId)
|
|
123
|
+
requiredBlockIds.push(s.blockId)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (messageIds.length === 0) {
|
|
128
|
+
throw new Error("No messages found in the specified range.")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { startReference, endReference, messageIds, toolIds, requiredBlockIds }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function resolveAnchorMessageId(startReference) {
|
|
135
|
+
if (startReference.kind === "compressed-block") {
|
|
136
|
+
if (!startReference.anchorMessageId) throw new Error("Compressed block has no anchor message ID")
|
|
137
|
+
return startReference.anchorMessageId
|
|
138
|
+
}
|
|
139
|
+
if (!startReference.messageId) throw new Error("No message ID in start reference")
|
|
140
|
+
return startReference.messageId
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function validateNonOverlapping(ranges) {
|
|
144
|
+
const sorted = [...ranges].sort((a, b) => a.selection.startReference.rawIndex - b.selection.startReference.rawIndex)
|
|
145
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
146
|
+
const prev = sorted[i - 1]
|
|
147
|
+
const curr = sorted[i]
|
|
148
|
+
if (curr.selection.startReference.rawIndex <= prev.selection.endReference.rawIndex) {
|
|
149
|
+
throw new Error(`content[${prev.index}] (${prev.entry.startId}..${prev.entry.endId}) overlaps content[${curr.index}] (${curr.entry.startId}..${curr.entry.endId}).`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function allocateBlockId(state) {
|
|
2
|
+
const ms = state.prune.messages
|
|
3
|
+
const id = ms.nextBlockId
|
|
4
|
+
ms.nextBlockId++
|
|
5
|
+
return id
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function allocateRunId(state) {
|
|
9
|
+
const ms = state.prune.messages
|
|
10
|
+
const id = ms.nextRunId
|
|
11
|
+
ms.nextRunId++
|
|
12
|
+
return id
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function wrapBlockSummary(blockId, summary) {
|
|
16
|
+
return `[OHC: Compressed bk${blockId}]\n\n${summary}\n<ohc-ref>bk${blockId}</ohc-ref>`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function applyCompressionState(state, input, selection, anchorMessageId, blockId, storedSummary, consumedBlockIds) {
|
|
20
|
+
const ms = state.prune.messages
|
|
21
|
+
|
|
22
|
+
const block = {
|
|
23
|
+
blockId,
|
|
24
|
+
active: true,
|
|
25
|
+
topic: input.topic,
|
|
26
|
+
batchTopic: input.batchTopic || input.topic,
|
|
27
|
+
mode: input.mode || "range",
|
|
28
|
+
runId: input.runId,
|
|
29
|
+
compressMessageId: input.compressMessageId,
|
|
30
|
+
compressCallId: input.compressCallId || null,
|
|
31
|
+
anchorMessageId,
|
|
32
|
+
startId: input.startId,
|
|
33
|
+
endId: input.endId,
|
|
34
|
+
summary: storedSummary,
|
|
35
|
+
summaryTokens: input.summaryTokens || 0,
|
|
36
|
+
compressedTokens: 0,
|
|
37
|
+
consumedBlockIds: Array.isArray(consumedBlockIds) ? consumedBlockIds : [],
|
|
38
|
+
deactivatedByBlockId: undefined,
|
|
39
|
+
deactivatedByUser: false,
|
|
40
|
+
deactivatedAt: undefined,
|
|
41
|
+
createdAt: Date.now(),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ms.blocksById.set(blockId, block)
|
|
45
|
+
ms.activeBlockIds.add(blockId)
|
|
46
|
+
|
|
47
|
+
if (anchorMessageId) {
|
|
48
|
+
ms.activeByAnchorMessageId.set(anchorMessageId, blockId)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const rawMessageId of selection.messageIds) {
|
|
52
|
+
let entry = ms.byMessageId.get(rawMessageId)
|
|
53
|
+
if (!entry) {
|
|
54
|
+
entry = { tokenCount: 0, allBlockIds: [], activeBlockIds: [] }
|
|
55
|
+
ms.byMessageId.set(rawMessageId, entry)
|
|
56
|
+
}
|
|
57
|
+
if (!entry.allBlockIds.includes(blockId)) {
|
|
58
|
+
entry.allBlockIds.push(blockId)
|
|
59
|
+
}
|
|
60
|
+
if (!entry.activeBlockIds.includes(blockId)) {
|
|
61
|
+
entry.activeBlockIds.push(blockId)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const cid of consumedBlockIds) {
|
|
66
|
+
const cb = ms.blocksById.get(cid)
|
|
67
|
+
if (cb) {
|
|
68
|
+
cb.active = false
|
|
69
|
+
cb.deactivatedAt = Date.now()
|
|
70
|
+
cb.deactivatedByBlockId = blockId
|
|
71
|
+
ms.activeBlockIds.delete(cid)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return block
|
|
76
|
+
}
|
package/lib/ohc/config.mjs
CHANGED
|
@@ -2,29 +2,185 @@ import fs from "node:fs"
|
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
import os from "node:os"
|
|
4
4
|
|
|
5
|
-
const
|
|
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")
|
|
6
8
|
|
|
7
|
-
const DEFAULTS = {
|
|
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
|
+
maxContextLimit: 150000,
|
|
20
|
+
minContextLimit: 50000,
|
|
21
|
+
nudgeFrequency: 5,
|
|
22
|
+
iterationNudgeThreshold: 15,
|
|
23
|
+
nudgeForce: "soft",
|
|
24
|
+
protectedTools: ["task", "skill", "todowrite", "todoread"],
|
|
25
|
+
protectUserMessages: false,
|
|
26
|
+
summaryBuffer: true,
|
|
27
|
+
},
|
|
28
|
+
strategies: {
|
|
29
|
+
deduplication: { enabled: true, protectedTools: [] },
|
|
30
|
+
purgeErrors: { enabled: true, turns: 4, protectedTools: [] },
|
|
31
|
+
},
|
|
32
|
+
}
|
|
8
33
|
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
34
|
+
function findOpencodeDir(startDir) {
|
|
35
|
+
let current = startDir
|
|
36
|
+
while (current && current.length > 3) {
|
|
37
|
+
const candidate = path.join(current, ".opencode")
|
|
38
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
39
|
+
return candidate
|
|
40
|
+
}
|
|
41
|
+
const parent = path.dirname(current)
|
|
42
|
+
if (parent === current) break
|
|
43
|
+
current = parent
|
|
44
|
+
}
|
|
45
|
+
return null
|
|
14
46
|
}
|
|
15
47
|
|
|
16
|
-
|
|
17
|
-
let raw = {}
|
|
48
|
+
function loadFile(filePath) {
|
|
18
49
|
try {
|
|
19
|
-
|
|
50
|
+
const content = fs.readFileSync(filePath, "utf8").trim()
|
|
51
|
+
if (!content) return null
|
|
52
|
+
if (filePath.endsWith(".jsonc")) {
|
|
53
|
+
const stripped = content
|
|
54
|
+
.replace(/"(?:[^"\\]|\\.)*"|\/\/.*/gm, m => m.startsWith('"') ? m : "")
|
|
55
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
56
|
+
return JSON.parse(stripped)
|
|
57
|
+
}
|
|
58
|
+
return JSON.parse(content)
|
|
20
59
|
} catch {
|
|
21
|
-
|
|
22
|
-
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function mergeDeep(base, override) {
|
|
65
|
+
if (!override || typeof override !== "object") return base
|
|
66
|
+
const result = { ...base }
|
|
67
|
+
for (const key of Object.keys(override)) {
|
|
68
|
+
if (override[key] === undefined) continue
|
|
69
|
+
if (typeof override[key] === "object" && !Array.isArray(override[key]) && override[key] !== null) {
|
|
70
|
+
result[key] = mergeDeep(base[key] || {}, override[key])
|
|
71
|
+
} else {
|
|
72
|
+
result[key] = override[key]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return result
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mergeArrays(base, override) {
|
|
79
|
+
if (!override || !Array.isArray(override)) return base
|
|
80
|
+
return [...new Set([...base, ...override])]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function mergeManualMode(base, override) {
|
|
84
|
+
if (!override || typeof override !== "object") return base
|
|
85
|
+
return {
|
|
86
|
+
enabled: override.enabled ?? base.enabled,
|
|
87
|
+
automaticStrategies: override.automaticStrategies ?? base.automaticStrategies,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function mergeTurnProtection(base, override) {
|
|
92
|
+
if (!override || typeof override !== "object") return base
|
|
93
|
+
return {
|
|
94
|
+
enabled: override.enabled ?? base.enabled,
|
|
95
|
+
turns: override.turns ?? base.turns,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function mergeCompress(base, override) {
|
|
100
|
+
if (!override || typeof override !== "object") return base
|
|
101
|
+
return {
|
|
102
|
+
maxContextLimit: override.maxContextLimit ?? base.maxContextLimit,
|
|
103
|
+
minContextLimit: override.minContextLimit ?? base.minContextLimit,
|
|
104
|
+
nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
|
|
105
|
+
iterationNudgeThreshold: override.iterationNudgeThreshold ?? base.iterationNudgeThreshold,
|
|
106
|
+
nudgeForce: override.nudgeForce ?? base.nudgeForce,
|
|
107
|
+
protectedTools: mergeArrays(base.protectedTools, override.protectedTools),
|
|
108
|
+
protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
|
|
109
|
+
summaryBuffer: override.summaryBuffer ?? base.summaryBuffer,
|
|
23
110
|
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function mergeStrategies(base, override) {
|
|
114
|
+
if (!override || typeof override !== "object") return base
|
|
115
|
+
return {
|
|
116
|
+
deduplication: {
|
|
117
|
+
enabled: override.deduplication?.enabled ?? base.deduplication.enabled,
|
|
118
|
+
protectedTools: mergeArrays(base.deduplication.protectedTools, override.deduplication?.protectedTools),
|
|
119
|
+
},
|
|
120
|
+
purgeErrors: {
|
|
121
|
+
enabled: override.purgeErrors?.enabled ?? base.purgeErrors.enabled,
|
|
122
|
+
turns: override.purgeErrors?.turns ?? base.purgeErrors.turns,
|
|
123
|
+
protectedTools: mergeArrays(base.purgeErrors.protectedTools, override.purgeErrors?.protectedTools),
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
}
|
|
24
127
|
|
|
128
|
+
function mergeLayer(base, data) {
|
|
129
|
+
if (!data) return base
|
|
25
130
|
return {
|
|
26
|
-
enabled:
|
|
27
|
-
|
|
28
|
-
|
|
131
|
+
enabled: data.enabled ?? base.enabled,
|
|
132
|
+
notification: data.notification ?? base.notification,
|
|
133
|
+
notificationMode: data.notificationMode ?? base.notificationMode,
|
|
134
|
+
max: data.max ?? base.max,
|
|
135
|
+
min: data.min ?? base.min,
|
|
136
|
+
manualMode: mergeManualMode(base.manualMode, data.manualMode),
|
|
137
|
+
turnProtection: mergeTurnProtection(base.turnProtection, data.turnProtection),
|
|
138
|
+
protectedFilePatterns: mergeArrays(base.protectedFilePatterns, data.protectedFilePatterns),
|
|
139
|
+
compress: mergeCompress(base.compress, data.compress),
|
|
140
|
+
strategies: mergeStrategies(base.strategies, data.strategies),
|
|
29
141
|
}
|
|
30
|
-
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function loadConfig(ctx) {
|
|
145
|
+
let config = { ...DEFAULTS }
|
|
146
|
+
|
|
147
|
+
const layers = [
|
|
148
|
+
{ path: fs.existsSync(GLOBAL_PATH_JSONC) ? GLOBAL_PATH_JSONC : (fs.existsSync(GLOBAL_PATH) ? GLOBAL_PATH : null), name: "global" },
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
const opencodeConfigDir = process.env.OPENCODE_CONFIG_DIR
|
|
152
|
+
if (opencodeConfigDir) {
|
|
153
|
+
const cdJsonc = path.join(opencodeConfigDir, "ohc.jsonc")
|
|
154
|
+
const cdJson = path.join(opencodeConfigDir, "ohc.json")
|
|
155
|
+
const cdPath = fs.existsSync(cdJsonc) ? cdJsonc : (fs.existsSync(cdJson) ? cdJson : null)
|
|
156
|
+
if (cdPath) layers.push({ path: cdPath, name: "configDir" })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (ctx?.directory) {
|
|
160
|
+
const opencodeDir = findOpencodeDir(ctx.directory)
|
|
161
|
+
if (opencodeDir) {
|
|
162
|
+
const pjJsonc = path.join(opencodeDir, "ohc.jsonc")
|
|
163
|
+
const pjJson = path.join(opencodeDir, "ohc.json")
|
|
164
|
+
const pjPath = fs.existsSync(pjJsonc) ? pjJsonc : (fs.existsSync(pjJson) ? pjJson : null)
|
|
165
|
+
if (pjPath) layers.push({ path: pjPath, name: "project" })
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const layer of layers) {
|
|
170
|
+
if (!layer.path) continue
|
|
171
|
+
const data = loadFile(layer.path)
|
|
172
|
+
if (data) config = mergeLayer(config, data)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
config.max = typeof config.max === "number" && config.max > 0 ? config.max : DEFAULTS.max
|
|
176
|
+
config.min = typeof config.min === "number" ? Math.max(10000, Math.min(config.min, config.max - 10000)) : DEFAULTS.min
|
|
177
|
+
|
|
178
|
+
return config
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function writeDefaults() {
|
|
182
|
+
fs.mkdirSync(GLOBAL_DIR, { recursive: true })
|
|
183
|
+
if (!fs.existsSync(GLOBAL_PATH) && !fs.existsSync(GLOBAL_PATH_JSONC)) {
|
|
184
|
+
fs.writeFileSync(GLOBAL_PATH, JSON.stringify(DEFAULTS, null, 2) + "\n", "utf8")
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
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 injectMessageIds(state, messages) {
|
|
118
|
+
for (const msg of messages) {
|
|
119
|
+
if (isIgnoredUserMessage(msg)) continue
|
|
120
|
+
const msgRef = state.messageIds.byRawId.get(msg.info?.id)
|
|
121
|
+
if (!msgRef) continue
|
|
122
|
+
const tag = formatOhcTag(msgRef)
|
|
123
|
+
|
|
124
|
+
if (msg.info.role === "user") {
|
|
125
|
+
let injected = false
|
|
126
|
+
for (const part of msg.parts) {
|
|
127
|
+
if (part.type === "text") {
|
|
128
|
+
part.text += tag
|
|
129
|
+
injected = true
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!injected) {
|
|
134
|
+
msg.parts.push({ type: "text", text: tag })
|
|
135
|
+
}
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (msg.info.role !== "assistant") continue
|
|
140
|
+
|
|
141
|
+
let hasContent = Array.isArray(msg.parts) && msg.parts.some(p => p.type === "text" || p.type === "tool")
|
|
142
|
+
if (!hasContent) continue
|
|
143
|
+
|
|
144
|
+
let injected = false
|
|
145
|
+
for (const part of msg.parts) {
|
|
146
|
+
if (part.type !== "tool" || typeof part.state?.output !== "string") continue
|
|
147
|
+
part.state.output = (part.state.output || "") + tag
|
|
148
|
+
injected = true
|
|
149
|
+
break
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (injected) continue
|
|
153
|
+
|
|
154
|
+
const lastText = [...msg.parts].reverse().find(p => p.type === "text")
|
|
155
|
+
if (lastText) {
|
|
156
|
+
lastText.text += tag
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const firstToolIdx = msg.parts.findIndex(p => p.type === "tool")
|
|
161
|
+
const synth = { type: "text", text: tag }
|
|
162
|
+
if (firstToolIdx === -1) {
|
|
163
|
+
msg.parts.push(synth)
|
|
164
|
+
} else {
|
|
165
|
+
msg.parts.splice(firstToolIdx, 0, synth)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|