opencode-codeops 1.10.0 → 1.10.2
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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to CodeOps are recorded here.
|
|
4
4
|
|
|
5
|
+
## 1.10.2 — 2026-10-04
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- effort: remember dispatch markers per session
|
|
10
|
+
|
|
11
|
+
## 1.10.1 — 2026-10-04
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- effort: add opt-in applied-level trace
|
|
16
|
+
|
|
5
17
|
## 1.10.0 — 2026-10-04
|
|
6
18
|
|
|
7
19
|
### Documentation
|
|
@@ -43,6 +43,10 @@ A dispatch marker is one standalone line in a dispatch message:
|
|
|
43
43
|
For quality-agent packets the marker follows the dispatch header; for packet kinds without a
|
|
44
44
|
header it is the first line. `exec-plan` owns the composition rule.
|
|
45
45
|
|
|
46
|
+
A captured marker is remembered for the whole session: every request in that session uses it
|
|
47
|
+
until a newer marker replaces it or the session ends. This is what keeps the marker effective
|
|
48
|
+
when the host passes a later user message to the request hook.
|
|
49
|
+
|
|
46
50
|
## Precedence
|
|
47
51
|
|
|
48
52
|
The most specific source wins:
|
|
@@ -126,6 +130,15 @@ Semantics:
|
|
|
126
130
|
For `exec-plan`, a bare flag follows each phase's suggestion, while `--auto-effort=<level>`
|
|
127
131
|
forces that level for the whole run, including every dispatch marker composed during it.
|
|
128
132
|
|
|
133
|
+
## Optional tracing
|
|
134
|
+
|
|
135
|
+
Set `CODEOPS_EFFORT_TRACE=1` (or `true`) in the environment that starts OpenCode to record what
|
|
136
|
+
the plugin does. One content-free JSON line is appended per marker capture and per request to
|
|
137
|
+
`reasoning-effort-trace.jsonl` inside the session's scratch directory. Lines carry only a
|
|
138
|
+
timestamp, event name, message id, agent name, level, source, and applied flag — never prompt
|
|
139
|
+
text or file content. The trace lives with the rest of the session scratch and is removed with
|
|
140
|
+
it. Tracing is off by default and never affects a request.
|
|
141
|
+
|
|
129
142
|
## Suggestion-only guarantee
|
|
130
143
|
|
|
131
144
|
| Allowed | Forbidden |
|
|
@@ -57,6 +57,19 @@ export declare function readSessionEffort(
|
|
|
57
57
|
base?: string
|
|
58
58
|
): EffortLevel | undefined
|
|
59
59
|
|
|
60
|
+
/** Check whether the optional trace environment switch is on. */
|
|
61
|
+
export declare function isEffortTraceEnabled(value: unknown): boolean
|
|
62
|
+
|
|
63
|
+
/** Compute the trace-file path inside the session temp directory. */
|
|
64
|
+
export declare function sessionEffortTracePath(sessionID?: string, base?: string): string
|
|
65
|
+
|
|
66
|
+
/** Append one content-free trace entry to the session trace file. */
|
|
67
|
+
export declare function appendEffortTrace(
|
|
68
|
+
sessionID: string | undefined,
|
|
69
|
+
entry: unknown,
|
|
70
|
+
base?: string
|
|
71
|
+
): boolean
|
|
72
|
+
|
|
60
73
|
/** Read an agent's explicit reasoning entry from the project routing config. */
|
|
61
74
|
export declare function readRoutingReasoning(
|
|
62
75
|
config: unknown,
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
* @module lib/reasoning-effort
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { lstatSync, readFileSync } from "node:fs"
|
|
22
|
+
import { appendFileSync, lstatSync, readFileSync } from "node:fs"
|
|
23
23
|
import { join } from "node:path"
|
|
24
24
|
|
|
25
|
-
import { sessionTmpDir } from "./tmp-hygiene.mjs"
|
|
25
|
+
import { ensureSessionTmpDir, sessionTmpDir } from "./tmp-hygiene.mjs"
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* The four levels a plan or skill may suggest.
|
|
@@ -198,6 +198,58 @@ export function readSessionEffort(sessionID, base) {
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Check whether the optional trace environment switch is on.
|
|
203
|
+
*
|
|
204
|
+
* Tracing is opt-in and diagnostic: it is enabled only when the value is
|
|
205
|
+
* exactly `1` or `true`, compared case-insensitively. Every other value,
|
|
206
|
+
* including `undefined`, disables it.
|
|
207
|
+
*
|
|
208
|
+
* @param value - Raw `CODEOPS_EFFORT_TRACE` value
|
|
209
|
+
* @returns True only for `"1"` or `"true"` in any case
|
|
210
|
+
*/
|
|
211
|
+
export function isEffortTraceEnabled(value) {
|
|
212
|
+
if (typeof value !== "string") return false
|
|
213
|
+
const normalized = value.trim().toLowerCase()
|
|
214
|
+
return normalized === "1" || normalized === "true"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Compute the trace-file path inside the session temp directory.
|
|
219
|
+
*
|
|
220
|
+
* @param sessionID - Session identifier
|
|
221
|
+
* @param base - Base temp directory (injectable for tests)
|
|
222
|
+
* @returns Absolute path to `reasoning-effort-trace.jsonl`
|
|
223
|
+
*/
|
|
224
|
+
export function sessionEffortTracePath(sessionID, base) {
|
|
225
|
+
return join(sessionTmpDir(sessionID, base), "reasoning-effort-trace.jsonl")
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Append one content-free trace entry to the session trace file.
|
|
230
|
+
*
|
|
231
|
+
* The entry is serialized as one compact JSON line. Nothing here inspects or
|
|
232
|
+
* interprets the entry; callers must never pass prompt text, file content, or
|
|
233
|
+
* secrets. The session directory is created when missing. Any failure — an
|
|
234
|
+
* unwritable path, an unserializable entry — returns false instead of
|
|
235
|
+
* throwing, because tracing must never affect a request.
|
|
236
|
+
*
|
|
237
|
+
* @param sessionID - Session identifier
|
|
238
|
+
* @param entry - Plain, content-free record to append
|
|
239
|
+
* @param base - Base temp directory (injectable for tests)
|
|
240
|
+
* @returns True when the line was appended, false otherwise
|
|
241
|
+
*/
|
|
242
|
+
export function appendEffortTrace(sessionID, entry, base) {
|
|
243
|
+
try {
|
|
244
|
+
const line = `${JSON.stringify(entry)}\n`
|
|
245
|
+
ensureSessionTmpDir(sessionID, base)
|
|
246
|
+
appendFileSync(sessionEffortTracePath(sessionID, base), line, "utf-8")
|
|
247
|
+
return true
|
|
248
|
+
} catch {
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
201
253
|
/**
|
|
202
254
|
* Read an agent's explicit reasoning entry from the project routing config.
|
|
203
255
|
*
|
package/package.json
CHANGED
package/plugin/index.ts
CHANGED
|
@@ -10,8 +10,12 @@ import {
|
|
|
10
10
|
removeSessionTmpDir,
|
|
11
11
|
} from "../bin/lib/tmp-hygiene.mjs"
|
|
12
12
|
import {
|
|
13
|
+
appendEffortTrace,
|
|
13
14
|
applyEffort,
|
|
14
15
|
findEffortMarker,
|
|
16
|
+
isEffortLevel,
|
|
17
|
+
isEffortTraceEnabled,
|
|
18
|
+
isRoutingReasoning,
|
|
15
19
|
readRoutingReasoning,
|
|
16
20
|
readSessionEffort,
|
|
17
21
|
resolveEffort,
|
|
@@ -172,16 +176,31 @@ function readRoutingConfig(directory: string): unknown {
|
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Helper — append one content-free trace line when the optional
|
|
181
|
+
// CODEOPS_EFFORT_TRACE switch is on. Tracing is diagnostic only: it never
|
|
182
|
+
// affects a request and swallows its own failures.
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
function traceEffort(
|
|
185
|
+
enabled: boolean,
|
|
186
|
+
sessionID: string,
|
|
187
|
+
entry: Record<string, unknown>
|
|
188
|
+
): void {
|
|
189
|
+
if (!enabled) return
|
|
190
|
+
appendEffortTrace(sessionID, { ts: new Date().toISOString(), ...entry })
|
|
191
|
+
}
|
|
192
|
+
|
|
175
193
|
// ---------------------------------------------------------------------------
|
|
176
194
|
// CodeOps plugin for OpenCode
|
|
177
195
|
// Replaces: hooks/hooks.json + hook_session_context.sh + hook_marker_guard.sh
|
|
178
196
|
// ---------------------------------------------------------------------------
|
|
179
197
|
export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
|
|
180
198
|
// Reasoning-effort state lives for the lifetime of this plugin instance:
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
const effortMarkers = new Map<string,
|
|
199
|
+
// the latest dispatch-marker level per session, plus a deduplication set
|
|
200
|
+
// for unsupported-level warnings.
|
|
201
|
+
const effortMarkers = new Map<string, EffortLevel>()
|
|
184
202
|
const warnedEffortLevels = new Set<string>()
|
|
203
|
+
const effortTraceEnabled = isEffortTraceEnabled(process.env.CODEOPS_EFFORT_TRACE)
|
|
185
204
|
|
|
186
205
|
return {
|
|
187
206
|
// -----------------------------------------------------------------------
|
|
@@ -202,9 +221,7 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
|
|
|
202
221
|
const info = (event.properties as { info: { id: string } }).info
|
|
203
222
|
removeSessionTmpDir(info.id)
|
|
204
223
|
try {
|
|
205
|
-
|
|
206
|
-
if (entry.sessionID === info.id) effortMarkers.delete(messageID)
|
|
207
|
-
}
|
|
224
|
+
effortMarkers.delete(info.id)
|
|
208
225
|
} catch {
|
|
209
226
|
await warnContentFree(client, "Could not clear captured reasoning-effort markers.")
|
|
210
227
|
}
|
|
@@ -269,8 +286,9 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
|
|
|
269
286
|
|
|
270
287
|
// -----------------------------------------------------------------------
|
|
271
288
|
// Hook 6: capture a dispatch marker from an incoming user message. The
|
|
272
|
-
// marker travels in the dispatch packet text;
|
|
273
|
-
//
|
|
289
|
+
// marker travels in the dispatch packet text; it is remembered for the
|
|
290
|
+
// whole session, because the later chat.params hook receives the latest
|
|
291
|
+
// user message, which can differ from the message that carried it.
|
|
274
292
|
// -----------------------------------------------------------------------
|
|
275
293
|
"chat.message": async (input, output) => {
|
|
276
294
|
try {
|
|
@@ -279,7 +297,12 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
|
|
|
279
297
|
)
|
|
280
298
|
const level = findEffortMarker(texts)
|
|
281
299
|
if (level !== undefined) {
|
|
282
|
-
effortMarkers.set(
|
|
300
|
+
effortMarkers.set(input.sessionID, level)
|
|
301
|
+
traceEffort(effortTraceEnabled, input.sessionID, {
|
|
302
|
+
event: "capture",
|
|
303
|
+
messageID: output.message.id,
|
|
304
|
+
level,
|
|
305
|
+
})
|
|
283
306
|
}
|
|
284
307
|
} catch {
|
|
285
308
|
await warnContentFree(client, "Could not scan a message for a reasoning-effort marker.")
|
|
@@ -293,15 +316,40 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
|
|
|
293
316
|
// -----------------------------------------------------------------------
|
|
294
317
|
"chat.params": async (input, output) => {
|
|
295
318
|
try {
|
|
296
|
-
const
|
|
297
|
-
const marker = stored && stored.sessionID === input.sessionID ? stored.level : undefined
|
|
319
|
+
const marker = effortMarkers.get(input.sessionID)
|
|
298
320
|
const session = readSessionEffort(input.sessionID)
|
|
299
321
|
const routing = readRoutingReasoning(readRoutingConfig(directory), input.agent)
|
|
300
322
|
const level = resolveEffort({ marker, session, routing })
|
|
301
|
-
|
|
323
|
+
const source = isEffortLevel(marker)
|
|
324
|
+
? "marker"
|
|
325
|
+
: isEffortLevel(session)
|
|
326
|
+
? "session"
|
|
327
|
+
: isRoutingReasoning(routing)
|
|
328
|
+
? "routing"
|
|
329
|
+
: "none"
|
|
330
|
+
if (level === undefined) {
|
|
331
|
+
traceEffort(effortTraceEnabled, input.sessionID, {
|
|
332
|
+
event: "apply",
|
|
333
|
+
messageID: input.message.id,
|
|
334
|
+
agent: input.agent,
|
|
335
|
+
level: null,
|
|
336
|
+
source,
|
|
337
|
+
applied: false,
|
|
338
|
+
})
|
|
339
|
+
return
|
|
340
|
+
}
|
|
302
341
|
|
|
303
342
|
const applied = applyEffort(output.options, level, input.model)
|
|
304
|
-
|
|
343
|
+
const changed = applied !== output.options
|
|
344
|
+
traceEffort(effortTraceEnabled, input.sessionID, {
|
|
345
|
+
event: "apply",
|
|
346
|
+
messageID: input.message.id,
|
|
347
|
+
agent: input.agent,
|
|
348
|
+
level,
|
|
349
|
+
source,
|
|
350
|
+
applied: changed,
|
|
351
|
+
})
|
|
352
|
+
if (!changed) {
|
|
305
353
|
if (marker !== undefined) {
|
|
306
354
|
const warningKey = `${input.sessionID}:${marker}`
|
|
307
355
|
if (!warnedEffortLevels.has(warningKey)) {
|
|
Binary file
|