opencode-codeops 1.10.1 → 1.10.3

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.3 — 2026-10-04
6
+
7
+ ### Fixes
8
+
9
+ - effort: prefer the model variant over the reasoning-support flag
10
+
11
+ ## 1.10.2 — 2026-10-04
12
+
13
+ ### Fixes
14
+
15
+ - effort: remember dispatch markers per session
16
+
5
17
  ## 1.10.1 — 2026-10-04
6
18
 
7
19
  ### Features
@@ -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:
@@ -309,9 +309,13 @@ export function modelSupportsReasoning(model) {
309
309
  * The runtime model carries a `variants` record whose entries are the exact
310
310
  * provider options for each level (for example `reasoningEffort`, or a nested
311
311
  * `reasoning.effort`). This function is the only place that mapping is
312
- * consumed, so the plugin never hardcodes a provider key. The function is
313
- * pure: it returns a new object when a change applies and the original object
314
- * reference otherwise.
312
+ * consumed, so the plugin never hardcodes a provider key. A matching variant
313
+ * is applied even when the model's reasoning capability flag is absent or
314
+ * false: the variant itself is the provider-known option set, and the host
315
+ * applies inherited variants the same way. Only when no variants record
316
+ * exists does the documented `reasoningEffort` passthrough require an
317
+ * explicit reasoning capability. The function is pure: it returns a new
318
+ * object when a change applies and the original object reference otherwise.
315
319
  *
316
320
  * @param options - Current provider options
317
321
  * @param level - Candidate level from {@link resolveEffort}
@@ -320,7 +324,6 @@ export function modelSupportsReasoning(model) {
320
324
  */
321
325
  export function applyEffort(options, level, model) {
322
326
  if (!isRoutingReasoning(level)) return options
323
- if (!modelSupportsReasoning(model)) return options
324
327
 
325
328
  const variants = extractModelVariants(model)
326
329
  if (variants !== undefined) {
@@ -330,6 +333,7 @@ export function applyEffort(options, level, model) {
330
333
  return deepMergePlain(options, variantOptions)
331
334
  }
332
335
 
336
+ if (!modelSupportsReasoning(model)) return options
333
337
  return { ...options, reasoningEffort: level }
334
338
  }
335
339
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codeops",
3
- "version": "1.10.1",
3
+ "version": "1.10.3",
4
4
  "description": "Specification-first engineering for complex systems — CodeOps plugin for OpenCode",
5
5
  "type": "module",
6
6
  "engines": {
package/plugin/index.ts CHANGED
@@ -12,10 +12,12 @@ import {
12
12
  import {
13
13
  appendEffortTrace,
14
14
  applyEffort,
15
+ extractModelVariants,
15
16
  findEffortMarker,
16
17
  isEffortLevel,
17
18
  isEffortTraceEnabled,
18
19
  isRoutingReasoning,
20
+ modelSupportsReasoning,
19
21
  readRoutingReasoning,
20
22
  readSessionEffort,
21
23
  resolveEffort,
@@ -196,9 +198,9 @@ function traceEffort(
196
198
  // ---------------------------------------------------------------------------
197
199
  export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
198
200
  // Reasoning-effort state lives for the lifetime of this plugin instance:
199
- // one entry per user message that carried a dispatch marker, plus a
200
- // deduplication set for unsupported-level warnings.
201
- const effortMarkers = new Map<string, { sessionID: string; level: EffortLevel }>()
201
+ // the latest dispatch-marker level per session, plus a deduplication set
202
+ // for unsupported-level warnings.
203
+ const effortMarkers = new Map<string, EffortLevel>()
202
204
  const warnedEffortLevels = new Set<string>()
203
205
  const effortTraceEnabled = isEffortTraceEnabled(process.env.CODEOPS_EFFORT_TRACE)
204
206
 
@@ -221,9 +223,7 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
221
223
  const info = (event.properties as { info: { id: string } }).info
222
224
  removeSessionTmpDir(info.id)
223
225
  try {
224
- for (const [messageID, entry] of effortMarkers) {
225
- if (entry.sessionID === info.id) effortMarkers.delete(messageID)
226
- }
226
+ effortMarkers.delete(info.id)
227
227
  } catch {
228
228
  await warnContentFree(client, "Could not clear captured reasoning-effort markers.")
229
229
  }
@@ -288,8 +288,9 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
288
288
 
289
289
  // -----------------------------------------------------------------------
290
290
  // Hook 6: capture a dispatch marker from an incoming user message. The
291
- // marker travels in the dispatch packet text; storing it by message id
292
- // lets the later chat.params hook apply it to the same request.
291
+ // marker travels in the dispatch packet text; it is remembered for the
292
+ // whole session, because the later chat.params hook receives the latest
293
+ // user message, which can differ from the message that carried it.
293
294
  // -----------------------------------------------------------------------
294
295
  "chat.message": async (input, output) => {
295
296
  try {
@@ -298,7 +299,7 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
298
299
  )
299
300
  const level = findEffortMarker(texts)
300
301
  if (level !== undefined) {
301
- effortMarkers.set(output.message.id, { sessionID: input.sessionID, level })
302
+ effortMarkers.set(input.sessionID, level)
302
303
  traceEffort(effortTraceEnabled, input.sessionID, {
303
304
  event: "capture",
304
305
  messageID: output.message.id,
@@ -317,8 +318,7 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
317
318
  // -----------------------------------------------------------------------
318
319
  "chat.params": async (input, output) => {
319
320
  try {
320
- const stored = effortMarkers.get(input.message.id)
321
- const marker = stored && stored.sessionID === input.sessionID ? stored.level : undefined
321
+ const marker = effortMarkers.get(input.sessionID)
322
322
  const session = readSessionEffort(input.sessionID)
323
323
  const routing = readRoutingReasoning(readRoutingConfig(directory), input.agent)
324
324
  const level = resolveEffort({ marker, session, routing })
@@ -350,6 +350,8 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
350
350
  level,
351
351
  source,
352
352
  applied: changed,
353
+ reasoningSupported: modelSupportsReasoning(input.model),
354
+ variantLevels: Object.keys(extractModelVariants(input.model) ?? {}),
353
355
  })
354
356
  if (!changed) {
355
357
  if (marker !== undefined) {