jeopi-cli 16.2.27 → 16.2.29

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.
@@ -19,6 +19,9 @@ export interface AdvisorAgent {
19
19
  readonly state: {
20
20
  messages: AgentMessage[];
21
21
  error?: string;
22
+ model?: {
23
+ id: string;
24
+ };
22
25
  };
23
26
  }
24
27
  export interface AdvisorRuntimeHost {
@@ -3,6 +3,18 @@ export interface HistoryFormatOptions {
3
3
  title?: string;
4
4
  /** Render assistant thinking blocks (default: elided). */
5
5
  includeThinking?: boolean;
6
+ /**
7
+ * When rendering thinking (requires `includeThinking`), disguise it through
8
+ * {@link renderDemotedThinking} for this target model id instead of the literal
9
+ * `_thinking:_ ` label. Anthropic's `reasoning_extraction` classifier can flag a
10
+ * transcript that replays a *different* model's prior thinking back at a
11
+ * Fable/Mythos model labeled as "thinking" — this is the same trigger
12
+ * `renderDemotedThinking` already avoids for cross-model/cross-turn replay in the
13
+ * wire layer. Omitted for the human-facing `history://` dump, where the explicit
14
+ * label is more useful than the disguise and there is no live model on the other
15
+ * end to refuse it.
16
+ */
17
+ demoteThinkingForModelId?: string;
6
18
  /** Render tool intent comment before tool call lines. */
7
19
  includeToolIntent?: boolean;
8
20
  /** Render watched-session roles as inline `**agent**:` / `**user**:` labels (collapsing consecutive same-role messages) instead of `## ` headings, so a primary transcript embedded inside an advisor turn stays visually distinct. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-cli",
4
- "version": "16.2.27",
4
+ "version": "16.2.29",
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",
@@ -43,6 +43,7 @@
43
43
  "gen:docs": "bun scripts/generate-docs-index.ts --generate",
44
44
  "gen:docs:reset": "bun scripts/generate-docs-index.ts --reset",
45
45
  "gen:tool-views": "bun --cwd=../collab-web run gen:tool-views",
46
+ "gen:share-viewer": "bun scripts/generate-share-viewer.ts",
46
47
  "gen:bundle": "bun scripts/bundle-dist.ts",
47
48
  "gen:mupdf": "bun scripts/embed-mupdf-wasm.ts --generate",
48
49
  "gen:mupdf:reset": "bun scripts/embed-mupdf-wasm.ts --reset",
@@ -56,17 +57,17 @@
56
57
  "@agentclientprotocol/sdk": "0.25.0",
57
58
  "@babel/parser": "^7.29.7",
58
59
  "@mozilla/readability": "^0.6.0",
59
- "jeopi-hashline": "16.2.27",
60
- "jeopi-stats": "16.2.27",
61
- "jeopi-agent-core": "16.2.27",
62
- "jeopi-ai": "16.2.27",
63
- "jeopi-catalog": "16.2.27",
64
- "jeopi-mnemopi": "16.2.27",
65
- "jeopi-natives": "16.2.27",
66
- "jeopi-tui": "16.2.27",
67
- "jeopi-utils": "16.2.27",
68
- "jeopi-wire": "16.2.27",
69
- "jeopi-snapcompact": "16.2.27",
60
+ "jeopi-hashline": "16.2.29",
61
+ "jeopi-stats": "16.2.29",
62
+ "jeopi-agent-core": "16.2.29",
63
+ "jeopi-ai": "16.2.29",
64
+ "jeopi-catalog": "16.2.29",
65
+ "jeopi-mnemopi": "16.2.29",
66
+ "jeopi-natives": "16.2.29",
67
+ "jeopi-tui": "16.2.29",
68
+ "jeopi-utils": "16.2.29",
69
+ "jeopi-wire": "16.2.29",
70
+ "jeopi-snapcompact": "16.2.29",
70
71
  "@opentelemetry/api": "^1.9.1",
71
72
  "@opentelemetry/context-async-hooks": "^2.7.1",
72
73
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -1381,6 +1381,49 @@ describe("advisor", () => {
1381
1381
  expect(strippedNotices).toHaveLength(1);
1382
1382
  });
1383
1383
 
1384
+ it("never sends the literal reasoning_extraction-triggering label when the advisor's own model is Fable/Mythos (no refusal, no degrade)", async () => {
1385
+ const promptInputs: string[] = [];
1386
+ const strippedNotices: number[] = [];
1387
+ const agent: AdvisorAgent = {
1388
+ prompt: async input => {
1389
+ promptInputs.push(input);
1390
+ },
1391
+ abort: () => {},
1392
+ reset: () => {},
1393
+ state: { messages: [], model: { id: "claude-fable-5" } },
1394
+ };
1395
+ const thinking = "secret chain of thought the classifier flags";
1396
+ const messages: AgentMessage[] = [
1397
+ { role: "user", content: "do the thing", timestamp: 1 } as AgentMessage,
1398
+ {
1399
+ role: "assistant",
1400
+ content: [
1401
+ { type: "thinking", thinking },
1402
+ { type: "text", text: "done" },
1403
+ ],
1404
+ timestamp: 2,
1405
+ } as AgentMessage,
1406
+ ];
1407
+ const host: AdvisorRuntimeHost = {
1408
+ snapshotMessages: () => messages,
1409
+ enqueueAdvice: () => {},
1410
+ notifyThinkingStripped: () => strippedNotices.push(1),
1411
+ };
1412
+ const runtime = new AdvisorRuntime(agent, host, 0);
1413
+
1414
+ runtime.onTurnEnd(messages);
1415
+ await runtime.waitForCatchup(5000, 1);
1416
+
1417
+ // Sent once, on the first attempt — the disguised form carries the
1418
+ // reasoning content but never the literal "_thinking:_" label, so
1419
+ // there is nothing for the classifier to catch and no degrade fires.
1420
+ expect(promptInputs).toHaveLength(1);
1421
+ expect(promptInputs[0]).not.toContain("_thinking:_");
1422
+ expect(promptInputs[0]).toContain(thinking);
1423
+ expect(strippedNotices).toHaveLength(0);
1424
+ expect(runtime.backlog).toBe(0);
1425
+ });
1426
+
1384
1427
  it("skips the degrade path when thinking is disabled at construction and falls through to the watchdog", async () => {
1385
1428
  const promptInputs: string[] = [];
1386
1429
  const strippedNotices: number[] = [];
@@ -21,7 +21,7 @@ export interface AdvisorAgent {
21
21
  * turn `Agent.#runLoop` records on its internal state.
22
22
  */
23
23
  rollbackTo?(count: number): void;
24
- readonly state: { messages: AgentMessage[]; error?: string };
24
+ readonly state: { messages: AgentMessage[]; error?: string; model?: { id: string } };
25
25
  }
26
26
 
27
27
  export interface AdvisorRuntimeHost {
@@ -116,7 +116,12 @@ export class AdvisorRuntime {
116
116
  * `false` exactly once, the first time a classifier refusal is caught — see
117
117
  * the `#drain` catch block. That single flip is sufficient to both change
118
118
  * future renders AND guard against re-triggering the degrade path, so no
119
- * separate one-shot flag is needed alongside it.
119
+ * separate one-shot flag is needed alongside it. When the render target is a
120
+ * Fable/Mythos model, {@link formatSessionHistoryMarkdown}'s
121
+ * `demoteThinkingForModelId` (wired from `agent.state.model.id` in
122
+ * {@link AdvisorRuntime.#renderDelta}) already disguises the rendered thinking
123
+ * before the first send, so this latch mainly guards other/unforeseen
124
+ * classifier triggers on the first turn.
120
125
  */
121
126
  #includeThinking: boolean;
122
127
 
@@ -243,6 +248,7 @@ export class AdvisorRuntime {
243
248
  const formattedDelta = obfuscator?.hasSecrets() ? obfuscateAdvisorDelta(obfuscator, delta) : delta;
244
249
  const md = formatSessionHistoryMarkdown(formattedDelta, {
245
250
  includeThinking: this.#includeThinking,
251
+ demoteThinkingForModelId: this.agent.state.model?.id,
246
252
  includeToolIntent: true,
247
253
  watchedRoles: true,
248
254
  expandPrimaryContext: true,
@@ -156,6 +156,17 @@ const AUTHORITATIVE_RUNTIME_CATALOG_PROVIDERS = new Set<string>(
156
156
  ),
157
157
  );
158
158
 
159
+ function isAuthoritativeLocalDiscovery(providerConfig: DiscoveryProviderConfig): boolean {
160
+ switch (providerConfig.discovery.type) {
161
+ case "ollama":
162
+ case "llama.cpp":
163
+ case "lm-studio":
164
+ return true;
165
+ default:
166
+ return false;
167
+ }
168
+ }
169
+
159
170
  function isAuthoritativeProjectCatalogModel(model: Model<Api>): boolean {
160
171
  return (
161
172
  model.provider === "google-vertex" &&
@@ -1334,7 +1345,21 @@ export class ModelRegistry {
1334
1345
  this.#discoverBuiltInProviderModels(strategy, providerFilter),
1335
1346
  ]);
1336
1347
  const discovered = [...configuredDiscovered, ...builtInDiscovery.models];
1337
- if (discovered.length === 0 && builtInDiscovery.authoritativeProviders.size === 0) {
1348
+ const successfulAuthoritativeDiscoverableProviders = new Set<string>();
1349
+ for (const providerConfig of selectedDiscoverableProviders) {
1350
+ if (!isAuthoritativeLocalDiscovery(providerConfig)) {
1351
+ continue;
1352
+ }
1353
+ const state = this.#providerDiscoveryStates.get(providerConfig.provider);
1354
+ if (state && (state.status === "ok" || state.status === "empty") && !state.stale) {
1355
+ successfulAuthoritativeDiscoverableProviders.add(providerConfig.provider);
1356
+ }
1357
+ }
1358
+ if (
1359
+ discovered.length === 0 &&
1360
+ builtInDiscovery.authoritativeProviders.size === 0 &&
1361
+ successfulAuthoritativeDiscoverableProviders.size === 0
1362
+ ) {
1338
1363
  return;
1339
1364
  }
1340
1365
  const discoveredModels = this.#applyHardcodedModelPolicies(
@@ -1350,6 +1375,9 @@ export class ModelRegistry {
1350
1375
  for (const provider of builtInDiscovery.authoritativeProviders) {
1351
1376
  authoritativeProviders.add(provider);
1352
1377
  }
1378
+ for (const provider of successfulAuthoritativeDiscoverableProviders) {
1379
+ authoritativeProviders.add(provider);
1380
+ }
1353
1381
  const baseModels =
1354
1382
  authoritativeProviders.size > 0 ? dropProviderModels(this.#models, authoritativeProviders) : this.#models;
1355
1383
  const resolved = this.#mergeResolvedModels(baseModels, discoveredModels);
@@ -1385,9 +1413,9 @@ export class ModelRegistry {
1385
1413
  const cacheProviderId = this.#configuredDiscoveryCacheProviderId(providerConfig);
1386
1414
  const cached = readModelCache<Api>(cacheProviderId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
1387
1415
  const cacheOlderThanConfig = cached !== null && this.#isDiscoveryCacheOlderThanModelsConfig(cached.updatedAt);
1388
- const bypassFreshCache = providerConfig.discovery.type === "llama.cpp" && strategy === "online-if-uncached";
1389
- const effectiveStrategy =
1390
- strategy === "online-if-uncached" && (cacheOlderThanConfig || bypassFreshCache) ? "online" : strategy;
1416
+ const shouldForceOnlineDiscovery =
1417
+ strategy === "online-if-uncached" && (cacheOlderThanConfig || isAuthoritativeLocalDiscovery(providerConfig));
1418
+ const effectiveStrategy = shouldForceOnlineDiscovery ? "online" : strategy;
1391
1419
  const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
1392
1420
  if (requiresAuth) {
1393
1421
  const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
@@ -1450,7 +1478,10 @@ export class ModelRegistry {
1450
1478
  provider: providerId,
1451
1479
  status,
1452
1480
  optional: providerConfig.optional ?? false,
1453
- stale: result.stale || status === "cached" || ((cacheOlderThanConfig || bypassFreshCache) && status !== "ok"),
1481
+ stale:
1482
+ result.stale ||
1483
+ status === "cached" ||
1484
+ (shouldForceOnlineDiscovery && status !== "ok" && status !== "empty"),
1454
1485
  fetchedAt: discoveryError ? cached?.updatedAt : Date.now(),
1455
1486
  models: result.models.map(model => model.id),
1456
1487
  error: discoveryError,
@@ -1627,7 +1658,9 @@ export class ModelRegistry {
1627
1658
  ): Promise<BuiltInDiscoveryResult> {
1628
1659
  try {
1629
1660
  const manager = createModelManager({ ...options, cacheDbPath: this.#cacheDbPath });
1630
- const result = await manager.refresh(strategy);
1661
+ const effectiveStrategy =
1662
+ strategy === "online-if-uncached" && options.providerId === "lm-studio" ? "online" : strategy;
1663
+ const result = await manager.refresh(effectiveStrategy);
1631
1664
  const models = result.models.map(model =>
1632
1665
  model.provider === options.providerId ? model : { ...model, provider: options.providerId },
1633
1666
  );