jeopi-cli 16.2.25 → 16.2.27

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +2928 -2937
  4. package/dist/types/commands/install.d.ts +1 -1
  5. package/dist/types/discovery/helpers.d.ts +1 -1
  6. package/dist/types/export/html/index.d.ts +2 -2
  7. package/dist/types/export/html/web-palette.d.ts +2 -2
  8. package/dist/types/tiny/text.d.ts +9 -2
  9. package/dist/types/tools/report-tool-issue.d.ts +1 -1
  10. package/package.json +12 -12
  11. package/scripts/generate-share-viewer.ts +5 -5
  12. package/scripts/jeopi +4 -4
  13. package/scripts/{omp.ts → jeopi-preload.ts} +4 -4
  14. package/src/commands/install.ts +1 -1
  15. package/src/debug/report-bundle.ts +1 -1
  16. package/src/debug/system-info.ts +2 -2
  17. package/src/discovery/builtin.ts +4 -4
  18. package/src/discovery/helpers.ts +2 -2
  19. package/src/discovery/omp-plugins.ts +3 -3
  20. package/src/discovery/ssh.ts +1 -1
  21. package/src/export/html/index.ts +2 -2
  22. package/src/export/html/share-loader.js +5 -5
  23. package/src/export/html/template.css +2 -2
  24. package/src/export/html/template.js +5 -5
  25. package/src/export/html/tool-views.generated.js +2 -2
  26. package/src/export/html/web-palette.ts +2 -2
  27. package/src/export/share.ts +1 -1
  28. package/src/internal-urls/docs-index.generated.txt +1 -1
  29. package/src/memories/index.ts +23 -1
  30. package/src/modes/acp/acp-agent.ts +12 -6
  31. package/src/modes/controllers/selector-controller.ts +16 -2
  32. package/src/prompts/advisor/system.md +1 -1
  33. package/src/prompts/system/interrupted-thinking.md +2 -3
  34. package/src/session/agent-session.ts +78 -15
  35. package/src/tiny/text.ts +11 -8
  36. package/src/tools/image-gen.ts +2 -4
  37. package/src/tools/report-tool-issue.ts +1 -1
@@ -698,12 +698,34 @@ function extractPersistableMessages(payload: string): AgentMessage[] {
698
698
  if (!maybeMessage || typeof maybeMessage !== "object") continue;
699
699
  const message = maybeMessage as AgentMessage;
700
700
  if (shouldPersistResponseItemForMemories(message)) {
701
- messages.push(message);
701
+ messages.push(dropThinkingContentForMemories(message));
702
702
  }
703
703
  }
704
704
  return messages;
705
705
  }
706
706
 
707
+ /**
708
+ * Drop `thinking`/`redactedThinking` content blocks from an assistant message
709
+ * before it is JSON-serialized into the stage-one extraction prompt. The raw
710
+ * message object (not just its extracted text) is embedded verbatim as
711
+ * `response_items_json` in `stage_one_input.md`, so any reasoning block left
712
+ * in `content` would be replayed as plaintext JSON back to the model — a
713
+ * documented Anthropic `reasoning_extraction` refusal trigger. Dropped, not
714
+ * reformatted (mirrors `hindsight/transcript.ts` and `compaction/utils.ts`).
715
+ */
716
+ function dropThinkingContentForMemories(message: AgentMessage): AgentMessage {
717
+ const role = (message as { role?: string }).role;
718
+ if (role !== "assistant") return message;
719
+ const content = (message as { content?: unknown }).content;
720
+ if (!Array.isArray(content)) return message;
721
+ const filtered = content.filter(block => {
722
+ const type = (block as { type?: string }).type;
723
+ return type !== "thinking" && type !== "redactedThinking";
724
+ });
725
+ if (filtered.length === content.length) return message;
726
+ return { ...message, content: filtered } as AgentMessage;
727
+ }
728
+
707
729
  async function runStage1Job(options: {
708
730
  claim: Stage1Claim;
709
731
  model: Model;
@@ -177,6 +177,8 @@ type ReplayableMessage = {
177
177
  toolName?: string;
178
178
  details?: unknown;
179
179
  isError?: boolean;
180
+ /** `custom`/`hookMessage` visibility flag; absent (e.g. `user`/`developer`) means no gate. */
181
+ display?: boolean;
180
182
  };
181
183
 
182
184
  type ReplayableToolItem = {
@@ -1906,12 +1908,16 @@ export class AcpAgent implements Agent {
1906
1908
  if (message.role === "assistant") {
1907
1909
  return this.#replayAssistantMessage(sessionId, message, cwd, replayedToolCallIds, replayedToolCallArgs);
1908
1910
  }
1909
- if (
1910
- message.role === "user" ||
1911
- message.role === "developer" ||
1912
- message.role === "custom" ||
1913
- message.role === "hookMessage"
1914
- ) {
1911
+ // `developer` carries jeopi-internal reminders/nudges (e.g. `<system-reminder>`
1912
+ // todo nudges) — the TUI never renders these (blanked to empty text in both the
1913
+ // live and history-rebuild paths; see chat-transcript-builder.ts / ui-helpers.ts),
1914
+ // so ACP replay must not surface them to the client either. `custom`/`hookMessage`
1915
+ // carry the same `display` gate the TUI checks (`#appendCustomMessage` /
1916
+ // ui-helpers.ts's `if (message.display)`) — a hidden one (e.g. the resolve-preview
1917
+ // reminder, the todo-error reminder) must stay hidden on replay too.
1918
+ const replayableAsUserChunk =
1919
+ message.role === "user" || ((message.role === "custom" || message.role === "hookMessage") && message.display);
1920
+ if (replayableAsUserChunk) {
1915
1921
  return this.#wrapReplayContent(
1916
1922
  sessionId,
1917
1923
  this.#extractReplayContent(message.content, undefined),
@@ -4,7 +4,7 @@ import { getOAuthProviders } from "jeopi-ai/oauth";
4
4
  import type { OAuthProvider } from "jeopi-ai/oauth/types";
5
5
  import type { Component, OverlayHandle } from "jeopi-tui";
6
6
  import { Input, Loader, Spacer, setTuiTight, Text } from "jeopi-tui";
7
- import { getAgentDbPath, getAgentDir, getProjectDir, normalizePathForComparison } from "jeopi-utils";
7
+ import { formatNumber, getAgentDbPath, getAgentDir, getProjectDir, normalizePathForComparison } from "jeopi-utils";
8
8
  import {
9
9
  type AdvisorConfigScope,
10
10
  discoverAdvisorConfigs,
@@ -633,8 +633,22 @@ export class SelectorController {
633
633
  if (switched) {
634
634
  this.ctx.statusLine.invalidate();
635
635
  this.ctx.updateEditorBorderColor();
636
+ this.ctx.showStatus(`Default model: ${selector ?? model.id}`);
637
+ } else {
638
+ // `setModel` deliberately skips the live switch (while still
639
+ // persisting the role) when the session's current context token
640
+ // count already exceeds the candidate's context window — sending
641
+ // on that model would just fail with a context-overflow error.
642
+ // This is exactly the moment a user hits a usage/token limit and
643
+ // reaches for /model to escape it, so the picker must say the
644
+ // active model did NOT change instead of the generic success
645
+ // status — otherwise the switch looks silently ignored.
646
+ this.ctx.showError(
647
+ `Default model set to ${selector ?? model.id} for future sessions, but NOT applied now: ` +
648
+ `current context (${formatNumber(currentContextTokens)} tokens) exceeds its ` +
649
+ `${formatNumber(model.contextWindow ?? 0)}-token window. Compact (/compact) or start a new session to switch now.`,
650
+ );
636
651
  }
637
- this.ctx.showStatus(`Default model: ${selector ?? model.id}`);
638
652
  // Don't call done() - selector stays open for role assignment
639
653
  } else {
640
654
  // Other roles (smol, slow): just update settings, not current model
@@ -13,7 +13,7 @@ Look where the agent is NOT — bring the angle they skipped, NEVER re-run reaso
13
13
  Offer that view before they sink work into the wrong direction.
14
14
 
15
15
  <workflow>
16
- You receive the agent's transcript incrementally, including their thoughts.
16
+ You receive the agent's transcript incrementally — their messages, tool calls, and results.
17
17
  Use the tools this session grants you to verify suspicions — by default read-only lookup (`read`, `grep`, `glob`); operators may extend the grant via `WATCHDOG.yml`. Advising is your primary channel; touch mutating tools (when granted) only when a verify step genuinely needs them.
18
18
  Keep exploration lean:
19
19
  - 2–3 tool calls per advise.
@@ -1,7 +1,6 @@
1
1
  <system-notice type="interrupted-thinking">
2
- Your previous turn was interrupted while you were thinking.
3
- - You MUST treat the preserved reasoning as internal continuity context.
4
- - You MUST continue the user's task from the relevant unfinished point.
2
+ Your previous turn was cut off before you finished. Below is your unfinished internal work state — use it silently to resume; do not quote, repeat, or restate it back.
3
+ - Continue the user's task from the relevant unfinished point.
5
4
  ------
6
5
  {{reasoning}}
7
6
  </system-notice>
@@ -1472,17 +1472,6 @@ function textFromContent(content: unknown): string {
1472
1472
  return parts.join("\n\n");
1473
1473
  }
1474
1474
 
1475
- function thinkingFromContent(content: unknown): string {
1476
- if (!Array.isArray(content)) return "";
1477
- const parts: string[] = [];
1478
- for (const block of content) {
1479
- if (!isRecord(block) || block.type !== "thinking" || typeof block.thinking !== "string") continue;
1480
- const thinking = block.thinking.trim();
1481
- if (thinking) parts.push(thinking);
1482
- }
1483
- return parts.join("\n\n");
1484
- }
1485
-
1486
1475
  function toolCallOpFromMessage(message: AgentMessage, toolCallId: string): string | undefined {
1487
1476
  if (message.role !== "assistant" || !Array.isArray(message.content)) return undefined;
1488
1477
  for (const block of message.content) {
@@ -1492,12 +1481,21 @@ function toolCallOpFromMessage(message: AgentMessage, toolCallId: string): strin
1492
1481
  return undefined;
1493
1482
  }
1494
1483
 
1484
+ /**
1485
+ * Title context turns carry `text` only — no `thinking`. The replan-refresh
1486
+ * title model resolves through the `tiny`/`commit`/`smol` roles but falls back
1487
+ * to the session's own current model (`getTitleModel` in title-generator.ts)
1488
+ * when no such role is configured, which can be the same Anthropic model the
1489
+ * primary turn used. Replaying prior assistant thinking as plaintext back to
1490
+ * that model is a `reasoning_extraction` classifier-refusal trigger (see the
1491
+ * intentional thinking-drop in compaction/utils.ts, memories/index.ts, and the
1492
+ * hindsight/mnemopi transcript extractor) — dropped here for the same reason.
1493
+ */
1495
1494
  function titleConversationTurnFromMessage(message: AgentMessage): TitleConversationTurn | undefined {
1496
1495
  if (message.role !== "user" && message.role !== "assistant") return undefined;
1497
1496
  const text = textFromContent(message.content);
1498
- const thinking = message.role === "assistant" ? thinkingFromContent(message.content) : undefined;
1499
- if (!text && !thinking) return undefined;
1500
- return { role: message.role, ...(text ? { text } : {}), ...(thinking ? { thinking } : {}) };
1497
+ if (!text) return undefined;
1498
+ return { role: message.role, text };
1501
1499
  }
1502
1500
 
1503
1501
  export class AgentSession {
@@ -1593,6 +1591,14 @@ export class AgentSession {
1593
1591
  #retryAttempt = 0;
1594
1592
  /** Rung-4 classifier-refusal resend streak (post fallback-ladder). */
1595
1593
  #refusalBackoff: { startedAt: number; attempt: number } | undefined = undefined;
1594
+ /**
1595
+ * One-shot latch: `reasoning_extraction` classifier refusals are payload-shape-bound
1596
+ * (see advisor/runtime.ts `#includeThinking` degrade) — resending the identical batch
1597
+ * predictably re-refuses. Set the first time `#handleRetryableError` strips unsigned
1598
+ * thinking/redactedThinking from active context and resends on the same model; never
1599
+ * reset, mirroring the advisor's permanent-for-the-session degrade.
1600
+ */
1601
+ #reasoningExtractionDegraded = false;
1596
1602
  #retryPromise: Promise<void> | undefined = undefined;
1597
1603
  #retryResolve: (() => void) | undefined = undefined;
1598
1604
  #activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
@@ -12681,6 +12687,36 @@ export class AgentSession {
12681
12687
  const stopType = message.stopDetails?.type;
12682
12688
  return stopType === "refusal" || stopType === "sensitive";
12683
12689
  }
12690
+ /**
12691
+ * Strip unsigned `thinking` and every `redactedThinking` block from assistant
12692
+ * messages. Used once as a `reasoning_extraction` classifier-refusal degrade:
12693
+ * unlike {@link renderDemotedThinking}'s plain-text fallback (still a plaintext
12694
+ * reasoning replay for dialects outside the Fable/Mythos carve-out), dropping the
12695
+ * block outright removes the trigger instead of reformatting it. Signed thinking
12696
+ * is left untouched — it replays natively via the provider's structured thinking
12697
+ * block and is never demoted to plaintext.
12698
+ * @returns a rewritten array, or `undefined` if no message needed stripping.
12699
+ */
12700
+ #stripUnsignedThinkingForRefusalDegrade(messages: readonly AgentMessage[]): AgentMessage[] | undefined {
12701
+ let changed = false;
12702
+ const next: AgentMessage[] = [];
12703
+ for (const message of messages) {
12704
+ if (message.role !== "assistant") {
12705
+ next.push(message);
12706
+ continue;
12707
+ }
12708
+ const content = message.content.filter(
12709
+ block => block.type !== "redactedThinking" && (block.type !== "thinking" || !!block.thinkingSignature),
12710
+ );
12711
+ if (content.length === message.content.length) {
12712
+ next.push(message);
12713
+ continue;
12714
+ }
12715
+ changed = true;
12716
+ if (content.length > 0) next.push({ ...message, content });
12717
+ }
12718
+ return changed ? next : undefined;
12719
+ }
12684
12720
 
12685
12721
  #getRetryFallbackChains(): RetryFallbackChains {
12686
12722
  const configuredChains = this.settings.get("retry.fallbackChains");
@@ -13099,6 +13135,33 @@ export class AgentSession {
13099
13135
  }
13100
13136
 
13101
13137
  const errorMessage = message.errorMessage || "Unknown error";
13138
+ // `reasoning_extraction` is payload-shape-bound (see advisor/runtime.ts): resending
13139
+ // the identical batch predictably re-refuses, unlike probabilistic classifier flags
13140
+ // where backoff/fallback genuinely help. Degrade once — strip unsigned
13141
+ // thinking/redactedThinking from active context and resend immediately on the same
13142
+ // model — before falling through to the generic fallback-pin / backoff ladder below,
13143
+ // which would otherwise retry the unchanged, re-refusing payload.
13144
+ if (
13145
+ classifierRefusal &&
13146
+ message.stopDetails?.category === "reasoning_extraction" &&
13147
+ !this.#reasoningExtractionDegraded
13148
+ ) {
13149
+ this.#reasoningExtractionDegraded = true;
13150
+ const stripped = this.#stripUnsignedThinkingForRefusalDegrade(this.agent.state.messages);
13151
+ if (stripped) this.agent.replaceMessages(stripped);
13152
+ this.#removeAssistantMessageFromActiveContext(message, "reasoning-extraction-degrade");
13153
+ await this.#emitSessionEvent({
13154
+ type: "auto_retry_start",
13155
+ attempt: this.#retryAttempt,
13156
+ maxAttempts: retrySettings.maxRetries,
13157
+ delayMs: 0,
13158
+ errorMessage,
13159
+ errorId: message.errorId,
13160
+ });
13161
+ this.#scheduleAgentContinue({ delayMs: 1, generation });
13162
+ return true;
13163
+ }
13164
+
13102
13165
  const id = this.#classifyRetryMessage(message);
13103
13166
  const staleOpenAIResponsesReplayError = AIError.is(id, AIError.Flag.StaleResponsesItem);
13104
13167
  const parsedRetryAfterMs = this.#parseRetryAfterMsFromError(errorMessage);
@@ -15130,7 +15193,7 @@ export class AgentSession {
15130
15193
  * @returns Path to exported file
15131
15194
  */
15132
15195
  async exportToHtml(outputPath?: string): Promise<string> {
15133
- // Public HTML export ships in the omp brand palette (collab-web
15196
+ // Public HTML export ships in the jeopi brand palette (collab-web
15134
15197
  // pink/purple), matching my.omp.sh — not the host's terminal theme.
15135
15198
  // Callers who want a themed export can pass `palette: "theme"` with
15136
15199
  // `themeName` directly to `exportSessionToHtml`.
package/src/tiny/text.ts CHANGED
@@ -47,20 +47,23 @@ export function formatTitleUserMessage(message: string): string {
47
47
  export interface TitleConversationTurn {
48
48
  role: "user" | "assistant";
49
49
  text?: string;
50
- thinking?: string;
51
50
  }
52
51
 
53
- /** Format recent user/assistant context for title generation after a todo replan. */
52
+ /**
53
+ * Format recent user/assistant context for title generation after a todo
54
+ * replan. Assistant `thinking` is intentionally never included: the
55
+ * replan-refresh title model falls back to the session's own current model
56
+ * when no `tiny`/`commit`/`smol` role is configured, so replaying prior
57
+ * reasoning as plaintext here is a `reasoning_extraction` classifier-refusal
58
+ * trigger on Anthropic targets (mirrors the thinking-drop in compaction,
59
+ * memories, and the hindsight transcript extractor).
60
+ */
54
61
  export function formatTitleConversationContext(turns: readonly TitleConversationTurn[]): string {
55
62
  const formattedTurns: string[] = [];
56
63
  for (const turn of turns) {
57
- const sections: string[] = [];
58
64
  const text = turn.text?.trim();
59
- if (text) sections.push(text);
60
- const thinking = turn.role === "assistant" ? turn.thinking?.trim() : undefined;
61
- if (thinking) sections.push(`<thinking>\n${thinking}\n</thinking>`);
62
- if (sections.length === 0) continue;
63
- formattedTurns.push(`<${turn.role}>\n${sections.join("\n\n")}\n</${turn.role}>`);
65
+ if (!text) continue;
66
+ formattedTurns.push(`<${turn.role}>\n${text}\n</${turn.role}>`);
64
67
  }
65
68
  if (formattedTurns.length === 0) return "";
66
69
  return prepareTitleInput(`<conversation>\n${formattedTurns.join("\n\n")}\n</conversation>`);
@@ -1,7 +1,7 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
3
  import { type } from "arktype";
4
- import { type ApiKey, type FetchImpl, getEnvApiKey, type Model, withAuth } from "jeopi-ai";
4
+ import { type ApiKey, type FetchImpl, getEnvApiKey, getOpenRouterHeaders, type Model, withAuth } from "jeopi-ai";
5
5
  import { ProviderHttpError } from "jeopi-ai/error";
6
6
  import {
7
7
  CODEX_BASE_URL,
@@ -1394,9 +1394,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1394
1394
  headers: {
1395
1395
  "Content-Type": "application/json",
1396
1396
  Authorization: `Bearer ${key}`,
1397
- "HTTP-Referer": "https://omp.sh/",
1398
- "X-OpenRouter-Title": "Oh-My-Pi",
1399
- "X-OpenRouter-Categories": "cli-agent",
1397
+ ...getOpenRouterHeaders(),
1400
1398
  },
1401
1399
  body: JSON.stringify(requestBody),
1402
1400
  signal: requestSignal,
@@ -13,7 +13,7 @@
13
13
  * (including from subagents) read the cached decision without prompting.
14
14
  *
15
15
  * When the user grants consent, push is automatically active against the
16
- * bundled endpoint (`dev.autoqaPush.endpoint`, default `qa.omp.sh`). Each
16
+ * bundled endpoint (`dev.autoqaPush.endpoint`, default `qa.jeopi.sh`). Each
17
17
  * insert schedules a background flush that POSTs pending rows and deletes
18
18
  * them on HTTP 2xx. `PI_AUTO_QA_PUSH=1` forces push in non-interactive
19
19
  * environments where the consent dialog never fires. Tool execution is