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.
- package/CHANGELOG.md +23 -0
- package/README.md +1 -1
- package/dist/cli.js +2928 -2937
- package/dist/types/commands/install.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/export/html/index.d.ts +2 -2
- package/dist/types/export/html/web-palette.d.ts +2 -2
- package/dist/types/tiny/text.d.ts +9 -2
- package/dist/types/tools/report-tool-issue.d.ts +1 -1
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +5 -5
- package/scripts/jeopi +4 -4
- package/scripts/{omp.ts → jeopi-preload.ts} +4 -4
- package/src/commands/install.ts +1 -1
- package/src/debug/report-bundle.ts +1 -1
- package/src/debug/system-info.ts +2 -2
- package/src/discovery/builtin.ts +4 -4
- package/src/discovery/helpers.ts +2 -2
- package/src/discovery/omp-plugins.ts +3 -3
- package/src/discovery/ssh.ts +1 -1
- package/src/export/html/index.ts +2 -2
- package/src/export/html/share-loader.js +5 -5
- package/src/export/html/template.css +2 -2
- package/src/export/html/template.js +5 -5
- package/src/export/html/tool-views.generated.js +2 -2
- package/src/export/html/web-palette.ts +2 -2
- package/src/export/share.ts +1 -1
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/memories/index.ts +23 -1
- package/src/modes/acp/acp-agent.ts +12 -6
- package/src/modes/controllers/selector-controller.ts +16 -2
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/system/interrupted-thinking.md +2 -3
- package/src/session/agent-session.ts +78 -15
- package/src/tiny/text.ts +11 -8
- package/src/tools/image-gen.ts +2 -4
- package/src/tools/report-tool-issue.ts +1 -1
package/src/memories/index.ts
CHANGED
|
@@ -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
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
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
|
|
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
|
|
3
|
-
-
|
|
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
|
-
|
|
1499
|
-
|
|
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
|
|
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
|
-
/**
|
|
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)
|
|
60
|
-
|
|
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>`);
|
package/src/tools/image-gen.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|