jeopi-cli 16.2.26 → 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.
@@ -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
@@ -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 {
@@ -15195,7 +15193,7 @@ export class AgentSession {
15195
15193
  * @returns Path to exported file
15196
15194
  */
15197
15195
  async exportToHtml(outputPath?: string): Promise<string> {
15198
- // Public HTML export ships in the omp brand palette (collab-web
15196
+ // Public HTML export ships in the jeopi brand palette (collab-web
15199
15197
  // pink/purple), matching my.omp.sh — not the host's terminal theme.
15200
15198
  // Callers who want a themed export can pass `palette: "theme"` with
15201
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