jeopi-cli 16.4.0 → 16.4.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-cli",
4
- "version": "16.4.0",
4
+ "version": "16.4.2",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/akillness/jeopi",
7
7
  "author": "Can Boluk",
@@ -57,17 +57,17 @@
57
57
  "@agentclientprotocol/sdk": "0.25.0",
58
58
  "@babel/parser": "^7.29.7",
59
59
  "@mozilla/readability": "^0.6.0",
60
- "jeopi-hashline": "16.4.0",
61
- "jeopi-stats": "16.4.0",
62
- "jeopi-agent-core": "16.4.0",
63
- "jeopi-ai": "16.4.0",
64
- "jeopi-catalog": "16.4.0",
65
- "jeopi-mnemopi": "16.4.0",
66
- "jeopi-natives": "16.4.0",
67
- "jeopi-tui": "16.4.0",
68
- "jeopi-utils": "16.4.0",
69
- "jeopi-wire": "16.4.0",
70
- "jeopi-snapcompact": "16.4.0",
60
+ "jeopi-hashline": "16.4.2",
61
+ "jeopi-stats": "16.4.2",
62
+ "jeopi-agent-core": "16.4.2",
63
+ "jeopi-ai": "16.4.2",
64
+ "jeopi-catalog": "16.4.2",
65
+ "jeopi-mnemopi": "16.4.2",
66
+ "jeopi-natives": "16.4.2",
67
+ "jeopi-tui": "16.4.2",
68
+ "jeopi-utils": "16.4.2",
69
+ "jeopi-wire": "16.4.2",
70
+ "jeopi-snapcompact": "16.4.2",
71
71
  "@opentelemetry/api": "^1.9.1",
72
72
  "@opentelemetry/context-async-hooks": "^2.7.1",
73
73
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -109,6 +109,7 @@ import {
109
109
  serviceTierFamily,
110
110
  streamSimple,
111
111
  } from "jeopi-ai";
112
+ import { renderDemotedThinking } from "jeopi-ai/dialect";
112
113
  import * as AIError from "jeopi-ai/error";
113
114
  import { toolWireSchema } from "jeopi-ai/utils/schema";
114
115
  import { GeminiHeaderRunDetector, isGeminiThinkingModel } from "jeopi-ai/utils/thinking-loop";
@@ -521,6 +522,17 @@ const RETRY_BACKOFF_MAX_DELAY_MS = 8_000;
521
522
  const REFUSAL_BACKOFF_BASE_MS = 2_000;
522
523
  const REFUSAL_BACKOFF_MAX_MS = 30_000;
523
524
  const REFUSAL_BACKOFF_BUDGET_MS = 30 * 60_000;
525
+ /**
526
+ * Consecutive-use cap on the `reasoning_extraction` fast-degrade path (strip
527
+ * unsigned thinking, resend same-model, zero delay — see
528
+ * `#reasoningExtractionDegradeStreak` below). Bounds the worst case where a
529
+ * pathological classifier state (documented: 1,164 refusals over ~4 hours in
530
+ * one incident) refuses every turn's fresh thinking in a row — after this
531
+ * many back-to-back fast-degrades with no intervening success, fall through
532
+ * to the Rung-4 ladder above, which already wall-clock-bounds via
533
+ * {@link REFUSAL_BACKOFF_BUDGET_MS}.
534
+ */
535
+ const REASONING_EXTRACTION_DEGRADE_STREAK_CAP = 3;
524
536
 
525
537
  function positiveEnvMs(name: string): number | undefined {
526
538
  const raw = process.env[name];
@@ -1610,13 +1622,31 @@ export class AgentSession {
1610
1622
  /** Rung-4 classifier-refusal resend streak (post fallback-ladder). */
1611
1623
  #refusalBackoff: { startedAt: number; attempt: number } | undefined = undefined;
1612
1624
  /**
1613
- * One-shot latch: `reasoning_extraction` classifier refusals are payload-shape-bound
1614
- * (see advisor/runtime.ts `#includeThinking` degrade) — resending the identical batch
1615
- * predictably re-refuses. Set the first time `#handleRetryableError` strips unsigned
1616
- * thinking/redactedThinking from active context and resends on the same model; never
1617
- * reset, mirroring the advisor's permanent-for-the-session degrade.
1618
- */
1619
- #reasoningExtractionDegraded = false;
1625
+ * `reasoning_extraction` classifier refusals are payload-shape-bound (see
1626
+ * advisor/runtime.ts `#includeThinking` degrade): resending the IDENTICAL
1627
+ * batch predictably re-refuses, so a bare retry can't help. But this is a
1628
+ * point-in-time strip of the CURRENT active context, not a persistent
1629
+ * setting — unlike the advisor's permanent-for-the-session
1630
+ * `includeThinking` flag, it does not prevent NEW unsigned thinking from a
1631
+ * LATER turn (a fresh advisor exchange, another interrupt, plain
1632
+ * continued conversation) from independently triggering its own,
1633
+ * genuinely-different refusal. A one-shot latch here previously left every
1634
+ * occurrence after the first to fall through to the generic Rung-4
1635
+ * backoff ladder unstripped — resending the same unstripped payload until
1636
+ * the wall-clock budget expired and the turn failed visibly.
1637
+ *
1638
+ * Fixed by gating on whether {@link #stripUnsignedThinkingForRefusalDegrade}
1639
+ * actually found something to strip THIS time (it already returns
1640
+ * `undefined` when there is nothing new to remove, which is itself proof
1641
+ * the resend would be payload-identical and degrading again cannot help)
1642
+ * rather than a permanent flag — each distinct occurrence gets its own
1643
+ * degrade attempt. Bounded by {@link REASONING_EXTRACTION_DEGRADE_STREAK_CAP}
1644
+ * consecutive uses (reset on any turn that actually succeeds) so a
1645
+ * pathological classifier state that refuses every turn's fresh thinking
1646
+ * in a row still falls through to the wall-clock-bounded ladder instead
1647
+ * of resending at zero delay indefinitely.
1648
+ */
1649
+ #reasoningExtractionDegradeStreak = 0;
1620
1650
  #retryPromise: Promise<void> | undefined = undefined;
1621
1651
  #retryResolve: (() => void) | undefined = undefined;
1622
1652
  #activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
@@ -3327,6 +3357,15 @@ export class AgentSession {
3327
3357
  * so live render, reload, and Ctrl+L rebuilds keep showing it; `convertToLlm`
3328
3358
  * strips the run from the provider request (incomplete/unsigned thinking is
3329
3359
  * rejected on resend) when this continuity message follows the assistant turn.
3360
+ *
3361
+ * The reasoning text is rendered through {@link renderDemotedThinking} (keyed
3362
+ * off `message.model`, the model resuming the turn in the overwhelming
3363
+ * majority of cases) before it reaches the template — same disguise every
3364
+ * other thinking-replay site in this codebase already applies before
3365
+ * sending a model's own prior reasoning back to it as plaintext. Raw,
3366
+ * undisguised reasoning wrapped in a "your unfinished internal work state"
3367
+ * frame is exactly the shape Anthropic's `reasoning_extraction` classifier
3368
+ * flags; this was the one remaining site that still shipped it undisguised.
3330
3369
  */
3331
3370
  #demoteInterruptedThinkingOnUserInterrupt(
3332
3371
  message: AssistantMessage,
@@ -3335,10 +3374,11 @@ export class AgentSession {
3335
3374
  const demoted = demoteInterruptedThinking(message);
3336
3375
  if (!demoted) return undefined;
3337
3376
  const interruptedAt = Date.now();
3377
+ const disguisedReasoning = renderDemotedThinking(message.model, demoted.reasoning).trimEnd();
3338
3378
  return {
3339
3379
  role: "custom",
3340
3380
  customType: INTERRUPTED_THINKING_MESSAGE_TYPE,
3341
- content: prompt.render(interruptedThinkingTemplate, { reasoning: demoted.reasoning }),
3381
+ content: prompt.render(interruptedThinkingTemplate, { reasoning: disguisedReasoning }),
3342
3382
  display: false,
3343
3383
  details: {
3344
3384
  interruptedAt,
@@ -3663,6 +3703,7 @@ export class AgentSession {
3663
3703
  });
3664
3704
  this.#retryAttempt = 0;
3665
3705
  this.#refusalBackoff = undefined;
3706
+ this.#reasoningExtractionDegradeStreak = 0;
3666
3707
  }
3667
3708
  if (assistantMsg.provider === "opencode-go") {
3668
3709
  this.#modelRegistry.authStorage.recordUsageCost(assistantMsg.provider, assistantMsg.usage.cost.total, {
@@ -13213,29 +13254,39 @@ export class AgentSession {
13213
13254
  const errorMessage = message.errorMessage || "Unknown error";
13214
13255
  // `reasoning_extraction` is payload-shape-bound (see advisor/runtime.ts): resending
13215
13256
  // the identical batch predictably re-refuses, unlike probabilistic classifier flags
13216
- // where backoff/fallback genuinely help. Degrade once — strip unsigned
13217
- // thinking/redactedThinking from active context and resend immediately on the same
13218
- // model — before falling through to the generic fallback-pin / backoff ladder below,
13219
- // which would otherwise retry the unchanged, re-refusing payload.
13257
+ // where backoff/fallback genuinely help. Strip unsigned thinking/redactedThinking
13258
+ // from active context and resend immediately on the same model — before falling
13259
+ // through to the generic fallback-pin / backoff ladder below, which would otherwise
13260
+ // retry the unchanged, re-refusing payload. Gated on the strip itself finding
13261
+ // something to remove THIS time (not a one-shot flag): a later turn can
13262
+ // independently accumulate fresh unsigned thinking and trigger its own genuinely
13263
+ // different refusal, and `#stripUnsignedThinkingForRefusalDegrade` already reports
13264
+ // "nothing to strip" via `undefined` when the payload truly is unchanged — which is
13265
+ // itself the correct signal to fall through instead of degrading again. Bounded by
13266
+ // `REASONING_EXTRACTION_DEGRADE_STREAK_CAP` consecutive uses so a pathological
13267
+ // classifier state refusing every turn's fresh thinking in a row still reaches the
13268
+ // wall-clock-bounded ladder instead of resending at zero delay indefinitely.
13220
13269
  if (
13221
13270
  classifierRefusal &&
13222
13271
  message.stopDetails?.category === "reasoning_extraction" &&
13223
- !this.#reasoningExtractionDegraded
13272
+ this.#reasoningExtractionDegradeStreak < REASONING_EXTRACTION_DEGRADE_STREAK_CAP
13224
13273
  ) {
13225
- this.#reasoningExtractionDegraded = true;
13226
13274
  const stripped = this.#stripUnsignedThinkingForRefusalDegrade(this.agent.state.messages);
13227
- if (stripped) this.agent.replaceMessages(stripped);
13228
- this.#removeAssistantMessageFromActiveContext(message, "reasoning-extraction-degrade");
13229
- await this.#emitSessionEvent({
13230
- type: "auto_retry_start",
13231
- attempt: this.#retryAttempt,
13232
- maxAttempts: retrySettings.maxRetries,
13233
- delayMs: 0,
13234
- errorMessage,
13235
- errorId: message.errorId,
13236
- });
13237
- this.#scheduleAgentContinue({ delayMs: 1, generation });
13238
- return true;
13275
+ if (stripped) {
13276
+ this.#reasoningExtractionDegradeStreak++;
13277
+ this.agent.replaceMessages(stripped);
13278
+ this.#removeAssistantMessageFromActiveContext(message, "reasoning-extraction-degrade");
13279
+ await this.#emitSessionEvent({
13280
+ type: "auto_retry_start",
13281
+ attempt: this.#retryAttempt,
13282
+ maxAttempts: retrySettings.maxRetries,
13283
+ delayMs: 0,
13284
+ errorMessage,
13285
+ errorId: message.errorId,
13286
+ });
13287
+ this.#scheduleAgentContinue({ delayMs: 1, generation });
13288
+ return true;
13289
+ }
13239
13290
  }
13240
13291
 
13241
13292
  const id = this.#classifyRetryMessage(message);