jeopi-cli 16.4.2 → 16.4.3
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 +74 -0
- package/dist/cli.js +3179 -3140
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +7 -1
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -6
- package/dist/types/internal-urls/types.d.ts +7 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/auth-broker-config.d.ts +2 -2
- package/dist/types/tools/bash-skill-urls.d.ts +3 -2
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +22 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/config.ts +11 -8
- package/src/autolearn/controller.ts +14 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +63 -13
- package/src/cli/usage-error.ts +7 -0
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/config/model-resolver.ts +24 -6
- package/src/config/settings-schema.ts +3 -3
- package/src/edit/index.ts +10 -76
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
- package/src/eval/executor-base.ts +84 -7
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/memory-protocol.ts +13 -8
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/internal-urls/types.ts +7 -0
- package/src/irc/bus.ts +48 -18
- package/src/lsp/client.ts +7 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +73 -2
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/controllers/event-controller.ts +17 -30
- package/src/modes/controllers/input-controller.ts +9 -6
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/workflow.ts +5 -4
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/browser.md +5 -5
- package/src/sdk.ts +3 -0
- package/src/session/agent-session.ts +172 -25
- package/src/session/auth-broker-config.ts +4 -4
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/builtin-registry.ts +8 -8
- package/src/task/executor.ts +8 -0
- package/src/tools/bash-skill-urls.ts +7 -4
- package/src/tools/bash.ts +30 -22
- package/src/tools/browser/cmux/cmux-tab.ts +49 -50
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +44 -5
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +280 -133
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/glob.ts +20 -6
- package/src/tools/grep.ts +1 -0
- package/src/tools/image-gen.ts +3 -4
- package/src/tools/irc.ts +27 -13
- package/src/tools/read.ts +14 -7
- package/src/tools/write.ts +22 -5
- package/src/tools/yield.ts +29 -1
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +68 -7
- package/src/web/search/index.ts +14 -6
- package/src/web/search/providers/perplexity.ts +18 -2
package/src/modes/workflow.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import workflowNotice from "../prompts/system/workflow-notice.md" with { type: "text" };
|
|
2
2
|
import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-highlight";
|
|
3
|
+
import { magicKeywordRegex } from "./magic-keyword-boundary";
|
|
3
4
|
import { keywordInProse } from "./markdown-prose";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -14,15 +15,15 @@ import { keywordInProse } from "./markdown-prose";
|
|
|
14
15
|
* "workflowz.ts" never do.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
|
-
// Detection: lowercase keyword flanked by whitespace or a string edge.
|
|
18
|
-
const WORKFLOW_WORD =
|
|
18
|
+
// Detection: lowercase keyword flanked by prose punctuation, whitespace, or a string edge.
|
|
19
|
+
const WORKFLOW_WORD = magicKeywordRegex("workflowz");
|
|
19
20
|
|
|
20
21
|
/** Hidden system notice appended after a user message that mentions "workflowz". */
|
|
21
22
|
export const WORKFLOW_NOTICE: string = workflowNotice.trim();
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Whether `text` contains the standalone keyword "workflowz"
|
|
25
|
-
* (lowercase,
|
|
26
|
+
* (lowercase, prose-delimited) in prose — never inside a code block, inline
|
|
26
27
|
* code span, or XML/HTML section.
|
|
27
28
|
*/
|
|
28
29
|
export function containsWorkflow(text: string): boolean {
|
|
@@ -36,7 +37,7 @@ export function containsWorkflow(text: string): boolean {
|
|
|
36
37
|
*/
|
|
37
38
|
export const highlightWorkflow: KeywordHighlighter = createGradientHighlighter({
|
|
38
39
|
probe: /workflowz/,
|
|
39
|
-
highlight:
|
|
40
|
+
highlight: magicKeywordRegex("workflowz", "g"),
|
|
40
41
|
stops: 14,
|
|
41
42
|
hue: t => 30 + t * 120,
|
|
42
43
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<system-reminder>
|
|
2
2
|
Task delegation is enabled — subagents are the default for this request.
|
|
3
3
|
|
|
4
|
-
Explore and settle the approach FIRST. Once the design is settled, you MUST fan the work out to `{{toolRefs.task}}` subagents instead of implementing it yourself.{{#if taskBatch}} Batch independent slices into ONE parallel `{{toolRefs.task}}` call; never serialize work that can run concurrently.{{/if}}
|
|
4
|
+
Explore and settle the approach FIRST — scoping, top-level decomposition, and cross-slice contracts are YOUR job; NEVER spawn a subagent to produce the overall plan (per-slice design travels with its executor). Once the design is settled, you MUST fan the work out to `{{toolRefs.task}}` subagents instead of implementing it yourself.{{#if taskBatch}} Batch independent slices into ONE parallel `{{toolRefs.task}}` call; never serialize work that can run concurrently.{{/if}}
|
|
5
5
|
|
|
6
|
-
Work alone
|
|
6
|
+
Work alone for: a single-file edit under ~30 lines, a direct answer requiring no code changes, a command the user explicitly asked you to run, or when only ONE runnable slice exists — a lone subagent is a lossy handoff, not parallelism.
|
|
7
7
|
</system-reminder>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<system-notice cause="fork">
|
|
2
|
+
The conversation above belongs to your parent session.
|
|
3
|
+
You are a fork created solely to handle the user's request below.
|
|
4
|
+
|
|
5
|
+
Your parent agent is still working on the original task — that responsibility is
|
|
6
|
+
NOT yours. You have no obligations from the prior conversation.
|
|
7
|
+
|
|
8
|
+
- Focus EXCLUSIVELY on the user's immediate request. Nothing else.
|
|
9
|
+
- NEVER continue, follow up on, or intervene in anything discussed before this
|
|
10
|
+
message. Those belong to the parent session.
|
|
11
|
+
- Your parent is CONCURRENTLY editing this same working directory. Files may
|
|
12
|
+
change between your reads, look mid-refactor, or fail to compile. That is the
|
|
13
|
+
parent's live work — NEVER fix, audit, or build on it, even if it looks broken.
|
|
14
|
+
- Any todo list, plan, or unfinished checklist from the prior conversation is
|
|
15
|
+
the parent's. NEVER resume or update it.
|
|
16
|
+
- After addressing the user's request, STOP. Do not work on ANY OTHER TASK.
|
|
17
|
+
</system-notice>
|
|
@@ -5,7 +5,7 @@ Drives real Chromium tab; full puppeteer access via JS.
|
|
|
5
5
|
- Three actions:
|
|
6
6
|
- `open` — acquire/reuse named tab (`name` defaults `"main"`). Optional `url` (navigate once ready), `viewport`, `dialogs: "accept" | "dismiss"` (auto-handle `alert`/`confirm`/`beforeunload`; else page hangs till you wire `page.on('dialog', …)`).
|
|
7
7
|
- `close` — release tab by `name`, or all with `all: true`. `kill: true` also kills spawned-app process trees.
|
|
8
|
-
- `run` — execute JS in existing tab. `code` = async function body; `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. Return value JSON-stringified into result; `display(value)` accumulates text/images.
|
|
8
|
+
- `run` — execute JS in existing tab. `code` = async function body; `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. Return value JSON-stringified into result; `display(value)` accumulates text/images. `wait(ms)` sleeps; `wait(fn, { timeout?, interval? })` polls `fn` (sync or async) until truthy and resolves with that value (default 30s timeout, 100ms interval; named error on timeout) — use it instead of in-page polling Promises inside `tab.evaluate`.
|
|
9
9
|
- Tabs survive `run` calls and in-process subagents — open once, reuse.
|
|
10
10
|
- Browser kinds (`app` on `open`):
|
|
11
11
|
- default (no `app`) → headless Chromium with stealth patches.
|
|
@@ -17,9 +17,9 @@ Drives real Chromium tab; full puppeteer access via JS.
|
|
|
17
17
|
- `tab.observe({ includeAll?, viewportOnly? })` — accessibility snapshot: `{ url, title, viewport, scroll, elements: [{ id, role, name, value, states, … }] }`. Ids stable until next observe/goto.
|
|
18
18
|
- `tab.ariaSnapshot(selector?, { depth?, boxes? })` — Playwright-format ARIA-tree YAML (nested roles + accessible names + `/url`/`/placeholder`), scoped to `selector` or the whole document. Every node carries a `[ref=eN]` id; `[cursor=pointer]` flags clickables. Captures dense, hierarchical structure/text that `observe()`'s flat list flattens away. Refs renumber from e1 each call and stay valid until the next `ariaSnapshot()`.
|
|
19
19
|
- `tab.ref("e5")` — `[ref=eN]` from the last ariaSnapshot → element handle with the common action methods (`.click()`, `.type()`, `.fill()`, `.hover()`, `.evaluate()`, …); the primary way to act on a ref. For convenience `aria-ref=e5` also works inline in `tab.click`/`type`/`fill`/`waitFor`/`scrollIntoView` (e.g. `tab.click("aria-ref=e5")`).
|
|
20
|
-
- `tab.id(n)` — id from last observe →
|
|
20
|
+
- `tab.id(n)` — id from last observe → element handle with the same action methods (`.click()`, `.type()`, `.fill()`, …).
|
|
21
21
|
- `tab.click(selector)` / `tab.type(selector, text)` / `tab.fill(selector, value)` / `tab.press(key, { selector? })` / `tab.scroll(dx, dy)`.
|
|
22
|
-
- `tab.waitFor(selector, { timeout? })` / `tab.waitForSelector(selector, { timeout?, visible?, hidden? })` — wait until attached (optionally visible/hidden); returns
|
|
22
|
+
- `tab.waitFor(selector, { timeout? })` / `tab.waitForSelector(selector, { timeout?, visible?, hidden? })` — wait until attached (optionally visible/hidden); returns an action-method handle.
|
|
23
23
|
- `tab.drag(from, to)` — endpoints: selector (center-to-center) or `{ x, y }` viewport point (canvases, sliders).
|
|
24
24
|
- `tab.scrollIntoView(selector)` — center in viewport; before clicking off-screen elements.
|
|
25
25
|
- `tab.select(selector, …values)` — set `<select>` option(s); returns selection. `tab.fill` NEVER works for selects.
|
|
@@ -27,10 +27,10 @@ Drives real Chromium tab; full puppeteer access via JS.
|
|
|
27
27
|
- `tab.waitForUrl(pattern, { timeout? })` — substring or `RegExp` (matches SPA pushState nav); returns matched URL.
|
|
28
28
|
- `tab.waitForResponse(pattern, { timeout? })` — substring, `RegExp`, or `(response) => boolean`; returns puppeteer `HTTPResponse` (`.text()`/`.json()`/`.status()`/`.headers()`).
|
|
29
29
|
- `tab.waitForNavigation({ waitUntil?, timeout? })` — resolves on the next navigation. Start it BEFORE the click/submit that triggers it; after `tab.goto` (which already waits) use `tab.waitForUrl`/`tab.waitForSelector` instead.
|
|
30
|
-
- `tab.evaluate(fn, …args)` —
|
|
30
|
+
- `tab.evaluate(fn, …args)` — run ad-hoc code in the page's MAIN world. DOM and page-defined globals (`window.myFlag`) are visible; mutations affect the page.
|
|
31
31
|
- `tab.screenshot({ selector?, fullPage?, save?, silent? })` — capture + attach for viewing (`silent: true` skips). Pass `save` only when a later step needs the file.
|
|
32
32
|
- `tab.extract(format = "markdown")` — readable page content (`"markdown"` | `"text"`); throws when nothing readable.
|
|
33
|
-
- Selectors: CSS + puppeteer handlers `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`; also Playwright-style `p-aria/…`, `p-text/…`. Playwright-only engines/pseudos (`:has-text()`, `:visible`, …) are rejected — use `text/…` or `aria/…`. A stalled action/wait fails fast with a named `tab.<op
|
|
33
|
+
- Selectors: CSS + puppeteer handlers `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`; also Playwright-style `p-aria/…`, `p-text/…`. Playwright-only engines/pseudos (`:has-text()`, `:visible`, …) are rejected — use `text/…` or `aria/…`. A stalled action/wait fails fast with a named `tab.<op>` error carrying a match-count diagnosis, never the whole-cell timeout; a selector matching nothing fails in ~2s (pass an explicit `{ timeout }` to `waitFor`/`waitForSelector` to wait out slow-appearing elements).
|
|
34
34
|
</instruction>
|
|
35
35
|
|
|
36
36
|
<critical>
|
package/src/sdk.ts
CHANGED
|
@@ -847,6 +847,7 @@ function createCustomToolContext(ctx: ExtensionContext): CustomToolContext {
|
|
|
847
847
|
isIdle: ctx.isIdle,
|
|
848
848
|
hasQueuedMessages: ctx.hasPendingMessages,
|
|
849
849
|
abort: ctx.abort,
|
|
850
|
+
localProtocolOptions: ctx.localProtocolOptions,
|
|
850
851
|
};
|
|
851
852
|
}
|
|
852
853
|
|
|
@@ -2053,6 +2054,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2053
2054
|
modelRegistry,
|
|
2054
2055
|
() => (hasSession ? createSessionMemoryRuntimeContext(session, agentDir, cwd) : undefined),
|
|
2055
2056
|
settings,
|
|
2057
|
+
localProtocolOptions,
|
|
2056
2058
|
);
|
|
2057
2059
|
|
|
2058
2060
|
credentialDisabledTarget = extensionRunner;
|
|
@@ -2071,6 +2073,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2071
2073
|
session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
2072
2074
|
},
|
|
2073
2075
|
settings,
|
|
2076
|
+
localProtocolOptions,
|
|
2074
2077
|
autoApprove: options.autoApprove ?? false,
|
|
2075
2078
|
});
|
|
2076
2079
|
const toolContextStore = new ToolContextStore(getSessionContext);
|
|
@@ -173,7 +173,7 @@ import {
|
|
|
173
173
|
resolveModelOverride,
|
|
174
174
|
resolveModelRoleValue,
|
|
175
175
|
} from "../config/model-resolver";
|
|
176
|
-
import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
|
176
|
+
import { getKnownRoleIds, MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
|
177
177
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
178
178
|
import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
|
|
179
179
|
import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
|
|
@@ -387,6 +387,64 @@ const MID_RUN_TODO_NUDGE_MUTATING_TOOLS: Record<string, true> = {
|
|
|
387
387
|
write: true,
|
|
388
388
|
ast_edit: true,
|
|
389
389
|
};
|
|
390
|
+
const MARKDOWN_PROMPT_PREFIX_RE = /^(?:>\s*)?(?:(?:[-*+]|\d+[.)])\s+)*/;
|
|
391
|
+
const PROMPT_LABEL_RE = /^(?:q(?:uestion)?|ask)\s*\d*\s*[:.)-]\s*/i;
|
|
392
|
+
const QUESTION_PROMPT_RE =
|
|
393
|
+
/^(?:what|which|when|where|why|how|who|whom|whose|do|does|did|can|could|would|will|should|is|are|am|may|shall)\b/i;
|
|
394
|
+
const USER_DIRECTED_PROMPT_RE = /\b(?:you|your|we|our)\b/i;
|
|
395
|
+
const USER_RESPONSE_CUE_RE =
|
|
396
|
+
/^(?:please\s+)?(?:confirm|reply|choose|pick|decide|advise)\b|^(?:please\s+)?answer\b|^(?:please\s+)?(?:let\s+me\s+know|tell\s+me)\b/i;
|
|
397
|
+
|
|
398
|
+
function assistantText(message: AssistantMessage): string {
|
|
399
|
+
return message.content
|
|
400
|
+
.filter((content): content is TextContent => content.type === "text")
|
|
401
|
+
.map(content => content.text)
|
|
402
|
+
.join("\n")
|
|
403
|
+
.trim();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
interface PromptLine {
|
|
407
|
+
text: string;
|
|
408
|
+
hadPromptLabel: boolean;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function promptLine(line: string): PromptLine {
|
|
412
|
+
const withoutMarkdownPrefix = line.trim().replace(MARKDOWN_PROMPT_PREFIX_RE, "").trim();
|
|
413
|
+
const withoutPromptLabel = withoutMarkdownPrefix.replace(PROMPT_LABEL_RE, "").trim();
|
|
414
|
+
return {
|
|
415
|
+
text: withoutPromptLabel,
|
|
416
|
+
hadPromptLabel: withoutPromptLabel !== withoutMarkdownPrefix,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isQuestionPromptLine(line: string): boolean {
|
|
421
|
+
const candidate = promptLine(line);
|
|
422
|
+
if (!/[??]\s*$/.test(candidate.text)) return false;
|
|
423
|
+
return (
|
|
424
|
+
candidate.hadPromptLabel ||
|
|
425
|
+
QUESTION_PROMPT_RE.test(candidate.text) ||
|
|
426
|
+
USER_DIRECTED_PROMPT_RE.test(candidate.text)
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function isResponseCueLine(line: string): boolean {
|
|
431
|
+
const candidate = promptLine(line)
|
|
432
|
+
.text.replace(/[.!?。!?]+$/, "")
|
|
433
|
+
.trim();
|
|
434
|
+
return USER_RESPONSE_CUE_RE.test(candidate);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function isAwaitingUserAnswer(message: AssistantMessage): boolean {
|
|
438
|
+
const text = assistantText(message);
|
|
439
|
+
if (!text) return false;
|
|
440
|
+
const tailLines = text
|
|
441
|
+
.split(/\r?\n/)
|
|
442
|
+
.map(line => line.trim())
|
|
443
|
+
.filter(Boolean)
|
|
444
|
+
.slice(-6);
|
|
445
|
+
const lastLine = tailLines.at(-1);
|
|
446
|
+
return tailLines.some(isQuestionPromptLine) || (lastLine !== undefined && isResponseCueLine(lastLine));
|
|
447
|
+
}
|
|
390
448
|
/** `customType` for the hidden mid-run todo nudge; `display: false`, so it reaches
|
|
391
449
|
* the model but never renders in the TUI or transcript. */
|
|
392
450
|
const MID_RUN_TODO_NUDGE_MESSAGE_TYPE = "mid-run-todo-nudge";
|
|
@@ -2415,7 +2473,7 @@ export class AgentSession {
|
|
|
2415
2473
|
if (this.#advisorSharedInstructions) systemPrompt.push(this.#advisorSharedInstructions);
|
|
2416
2474
|
if (config.instructions?.trim()) systemPrompt.push(config.instructions.trim());
|
|
2417
2475
|
|
|
2418
|
-
const names = config.tools
|
|
2476
|
+
const names = config.tools === undefined ? ADVISOR_DEFAULT_TOOL_NAMES : new Set(config.tools);
|
|
2419
2477
|
const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
|
|
2420
2478
|
|
|
2421
2479
|
const advisorSessionId = this.#advisorSessionId(slug);
|
|
@@ -3946,6 +4004,17 @@ export class AgentSession {
|
|
|
3946
4004
|
await emitAgentEndNotification();
|
|
3947
4005
|
return;
|
|
3948
4006
|
}
|
|
4007
|
+
} else if (this.#isHardErrorFallbackEligible(msg)) {
|
|
4008
|
+
// A non-retryable hard error on a model covered by a configured
|
|
4009
|
+
// fallback chain: retrying the SAME model is pointless, but a
|
|
4010
|
+
// DIFFERENT model is a fresh chance — consult the chain before
|
|
4011
|
+
// surfacing the failure. #handleRetryableError bails out (no
|
|
4012
|
+
// backoff-retry of the failing model) when no switch happens.
|
|
4013
|
+
const didRetry = await this.#handleRetryableError(msg, { hardErrorFallback: true });
|
|
4014
|
+
if (didRetry) {
|
|
4015
|
+
await emitAgentEndNotification();
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
3949
4018
|
}
|
|
3950
4019
|
// Classifier refusals are persisted-skipped above; also prune the trailing
|
|
3951
4020
|
// stub from active context so the next turn's prompt does not replay it.
|
|
@@ -3990,7 +4059,7 @@ export class AgentSession {
|
|
|
3990
4059
|
await emitAgentEndNotification();
|
|
3991
4060
|
return;
|
|
3992
4061
|
}
|
|
3993
|
-
const todoContinuationScheduled = await this.#checkTodoCompletion();
|
|
4062
|
+
const todoContinuationScheduled = await this.#checkTodoCompletion(msg);
|
|
3994
4063
|
if (todoContinuationScheduled) {
|
|
3995
4064
|
await emitAgentEndNotification();
|
|
3996
4065
|
return;
|
|
@@ -6518,6 +6587,8 @@ export class AgentSession {
|
|
|
6518
6587
|
abort: () => {
|
|
6519
6588
|
this.agent.abort();
|
|
6520
6589
|
},
|
|
6590
|
+
settings: this.settings,
|
|
6591
|
+
localProtocolOptions: this.#localProtocolOptions(),
|
|
6521
6592
|
});
|
|
6522
6593
|
|
|
6523
6594
|
for (const customTool of mcpTools) {
|
|
@@ -7003,6 +7074,30 @@ export class AgentSession {
|
|
|
7003
7074
|
return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model);
|
|
7004
7075
|
}
|
|
7005
7076
|
|
|
7077
|
+
/**
|
|
7078
|
+
* Resolve the explicit thinking suffix that should apply when a temporary
|
|
7079
|
+
* picker selects a model already assigned to a configured role.
|
|
7080
|
+
*/
|
|
7081
|
+
resolveTemporaryModelThinkingLevel(model: Model): ConfiguredThinkingLevel | undefined {
|
|
7082
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
7083
|
+
if (availableModels.length === 0) return undefined;
|
|
7084
|
+
|
|
7085
|
+
const matchPreferences = getModelMatchPreferences(this.settings);
|
|
7086
|
+
for (const role of getKnownRoleIds(this.settings)) {
|
|
7087
|
+
const roleValue = this.settings.getModelRole(role);
|
|
7088
|
+
if (!roleValue) continue;
|
|
7089
|
+
|
|
7090
|
+
const resolved = resolveModelRoleValue(roleValue, availableModels, {
|
|
7091
|
+
settings: this.settings,
|
|
7092
|
+
matchPreferences,
|
|
7093
|
+
});
|
|
7094
|
+
if (!resolved.explicitThinkingLevel || resolved.thinkingLevel === undefined || !resolved.model) continue;
|
|
7095
|
+
if (modelsAreEqual(resolved.model, model)) return resolved.thinkingLevel;
|
|
7096
|
+
}
|
|
7097
|
+
|
|
7098
|
+
return undefined;
|
|
7099
|
+
}
|
|
7100
|
+
|
|
7006
7101
|
get promptTemplates(): ReadonlyArray<PromptTemplate> {
|
|
7007
7102
|
return this.#promptTemplates;
|
|
7008
7103
|
}
|
|
@@ -9263,6 +9358,11 @@ export class AgentSession {
|
|
|
9263
9358
|
* candidate is small or the session has been idle long enough that the
|
|
9264
9359
|
* provider prompt cache is cold), so it is cheap to run every turn. Gated
|
|
9265
9360
|
* on the `compaction.supersedeReads` and `compaction.dropUseless` settings.
|
|
9361
|
+
*
|
|
9362
|
+
* Persists via `rewriteEntries` like every other history rewrite — the
|
|
9363
|
+
* session file must match the live (pruned) context or file-based forks
|
|
9364
|
+
* (`/fork`, `/tan`) and resume rebuild a divergent prefix and cold-miss the
|
|
9365
|
+
* provider prompt cache.
|
|
9266
9366
|
*/
|
|
9267
9367
|
async #pruneStaleToolResults(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
9268
9368
|
const { supersedeReads, dropUseless } = this.settings.getGroup("compaction");
|
|
@@ -9285,6 +9385,7 @@ export class AgentSession {
|
|
|
9285
9385
|
return undefined;
|
|
9286
9386
|
}
|
|
9287
9387
|
|
|
9388
|
+
await this.sessionManager.rewriteEntries();
|
|
9288
9389
|
const sessionContext = this.buildDisplaySessionContext();
|
|
9289
9390
|
this.agent.replaceMessages(sessionContext.messages);
|
|
9290
9391
|
this.#resetAllAdvisorRuntimes();
|
|
@@ -10383,21 +10484,21 @@ export class AgentSession {
|
|
|
10383
10484
|
|
|
10384
10485
|
this.#emptyStopRetryCount++;
|
|
10385
10486
|
if (this.#emptyStopRetryCount > EMPTY_STOP_MAX_RETRIES) {
|
|
10386
|
-
|
|
10387
|
-
|
|
10487
|
+
const attempts = this.#emptyStopRetryCount - 1;
|
|
10488
|
+
const finalError = "Assistant returned empty stop after retry cap";
|
|
10489
|
+
logger.warn(finalError, {
|
|
10490
|
+
attempts,
|
|
10388
10491
|
model: assistantMessage.model,
|
|
10389
10492
|
provider: assistantMessage.provider,
|
|
10390
10493
|
});
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
this.#refusalBackoff = undefined;
|
|
10400
|
-
}
|
|
10494
|
+
await this.#emitSessionEvent({
|
|
10495
|
+
type: "auto_retry_end",
|
|
10496
|
+
success: false,
|
|
10497
|
+
attempt: this.#retryAttempt > 0 ? this.#retryAttempt : attempts,
|
|
10498
|
+
finalError,
|
|
10499
|
+
});
|
|
10500
|
+
this.#retryAttempt = 0;
|
|
10501
|
+
this.#refusalBackoff = undefined;
|
|
10401
10502
|
this.#resolveRetry();
|
|
10402
10503
|
// Tool-use orphans corrupt Anthropic message history (tool_result without
|
|
10403
10504
|
// matching tool_use). Always remove them even when the retry cap is hit.
|
|
@@ -10891,7 +10992,7 @@ export class AgentSession {
|
|
|
10891
10992
|
/**
|
|
10892
10993
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
10893
10994
|
*/
|
|
10894
|
-
async #checkTodoCompletion(): Promise<boolean> {
|
|
10995
|
+
async #checkTodoCompletion(message: AssistantMessage): Promise<boolean> {
|
|
10895
10996
|
// Skip todo reminders when the most recent turn was driven by an explicit user force —
|
|
10896
10997
|
// the user wanted exactly that tool, not a follow-up nag about incomplete todos.
|
|
10897
10998
|
const lastServedLabel = this.#toolChoiceQueue.consumeLastServedLabel();
|
|
@@ -10949,6 +11050,13 @@ export class AgentSession {
|
|
|
10949
11050
|
return false;
|
|
10950
11051
|
}
|
|
10951
11052
|
|
|
11053
|
+
if (isAwaitingUserAnswer(message)) {
|
|
11054
|
+
logger.debug("Todo completion: assistant is waiting for user input; skipping reminder", {
|
|
11055
|
+
incomplete: incomplete.length,
|
|
11056
|
+
});
|
|
11057
|
+
return false;
|
|
11058
|
+
}
|
|
11059
|
+
|
|
10952
11060
|
// Build reminder message
|
|
10953
11061
|
this.#todoReminderCount++;
|
|
10954
11062
|
const todoList = incompleteByPhase
|
|
@@ -13078,6 +13186,33 @@ export class AgentSession {
|
|
|
13078
13186
|
if (AIError.is(id, AIError.Flag.AuthFailed)) return false;
|
|
13079
13187
|
return this.#modelRegistry.find("fireworks", toFireworksBaseModelId(model.id)) !== undefined;
|
|
13080
13188
|
}
|
|
13189
|
+
/**
|
|
13190
|
+
* True when a turn failed with a hard (non-retryable) provider error but a
|
|
13191
|
+
* configured `retry.fallbackChains` entry covers the active model: the same
|
|
13192
|
+
* model is not worth retrying, yet a DIFFERENT model is a fresh chance, so
|
|
13193
|
+
* the chain is consulted before the error becomes final. Skips failures a
|
|
13194
|
+
* model switch cannot fix or must not replay: cancellations (abort-flavored
|
|
13195
|
+
* errors are not model faults), context overflow (compaction's job),
|
|
13196
|
+
* classifier refusals (chain consult is handled on the retryable path with
|
|
13197
|
+
* `pinFallback`), and turns that already emitted a tool call (replaying
|
|
13198
|
+
* could duplicate work).
|
|
13199
|
+
*/
|
|
13200
|
+
#isHardErrorFallbackEligible(message: AssistantMessage): boolean {
|
|
13201
|
+
if (message.stopReason !== "error") return false;
|
|
13202
|
+
const model = this.model;
|
|
13203
|
+
if (!model) return false;
|
|
13204
|
+
const retrySettings = this.settings.getGroup("retry");
|
|
13205
|
+
if (!retrySettings.enabled || !retrySettings.modelFallback) return false;
|
|
13206
|
+
if (this.#isClassifierRefusal(message)) return false;
|
|
13207
|
+
const id = this.#classifyRetryMessage(message);
|
|
13208
|
+
if (AIError.is(id, AIError.Flag.Abort) || AIError.is(id, AIError.Flag.UserInterrupt)) return false;
|
|
13209
|
+
if (AIError.isContextOverflow(message, model.contextWindow ?? 0)) return false;
|
|
13210
|
+
if (this.#hasReplayUnsafeToolOutput(message)) return false;
|
|
13211
|
+
const currentSelector = formatRetryFallbackSelector(model, this.thinkingLevel);
|
|
13212
|
+
const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
|
|
13213
|
+
if (!role) return false;
|
|
13214
|
+
return this.#findRetryFallbackCandidates(role, currentSelector).length > 0;
|
|
13215
|
+
}
|
|
13081
13216
|
|
|
13082
13217
|
/**
|
|
13083
13218
|
* Switch the active model from a Fireworks Fast (`-fast`) variant to its base
|
|
@@ -13208,12 +13343,16 @@ export class AgentSession {
|
|
|
13208
13343
|
}
|
|
13209
13344
|
|
|
13210
13345
|
/**
|
|
13211
|
-
* Handle retryable errors with exponential backoff
|
|
13346
|
+
* Handle retryable errors with exponential backoff, credential rotation, and
|
|
13347
|
+
* model-fallback chains. Also entered for NON-retryable errors when a switch
|
|
13348
|
+
* is the recovery (`fireworksFastFallback`, `hardErrorFallback`): then a
|
|
13349
|
+
* successful model switch retries immediately, and a failed switch surfaces
|
|
13350
|
+
* the error without a same-model backoff retry.
|
|
13212
13351
|
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
|
13213
13352
|
*/
|
|
13214
13353
|
async #handleRetryableError(
|
|
13215
13354
|
message: AssistantMessage,
|
|
13216
|
-
options?: { allowModelFallback?: boolean; fireworksFastFallback?: boolean },
|
|
13355
|
+
options?: { allowModelFallback?: boolean; fireworksFastFallback?: boolean; hardErrorFallback?: boolean },
|
|
13217
13356
|
): Promise<boolean> {
|
|
13218
13357
|
const retrySettings = this.settings.getGroup("retry");
|
|
13219
13358
|
// The Fireworks Fast→base degrade is an intrinsic model-selection safety net,
|
|
@@ -13399,11 +13538,16 @@ export class AgentSession {
|
|
|
13399
13538
|
this.#retryAttempt = Math.min(this.#retryAttempt, 1);
|
|
13400
13539
|
delayMs = refusalBackoffDelayMs(backoff.attempt);
|
|
13401
13540
|
}
|
|
13402
|
-
//
|
|
13403
|
-
//
|
|
13404
|
-
//
|
|
13405
|
-
// classifier wouldn't retry —
|
|
13406
|
-
|
|
13541
|
+
// A fallback switch was the whole reason we entered (Fast→base degrade or
|
|
13542
|
+
// a hard-error chain consult) but it could not happen (e.g. no candidate
|
|
13543
|
+
// has a credential). Don't fall through to backing-off and retrying the
|
|
13544
|
+
// failing model for an error the generic classifier wouldn't retry —
|
|
13545
|
+
// surface it instead.
|
|
13546
|
+
if (
|
|
13547
|
+
(options?.fireworksFastFallback || options?.hardErrorFallback) &&
|
|
13548
|
+
!switchedModel &&
|
|
13549
|
+
!this.#isRetryableError(message)
|
|
13550
|
+
) {
|
|
13407
13551
|
this.#retryAttempt = 0;
|
|
13408
13552
|
this.#resolveRetry();
|
|
13409
13553
|
return false;
|
|
@@ -14102,10 +14246,13 @@ export class AgentSession {
|
|
|
14102
14246
|
// Side-channel turns must not share OpenAI/Codex append-only
|
|
14103
14247
|
// conversation state with the main agent turn: IRC and /btw can run
|
|
14104
14248
|
// while the main turn is mid-tool-call. Keep the prompt-cache key
|
|
14105
|
-
// stable, but give provider routing a unique request lineage.
|
|
14249
|
+
// stable, but give provider routing a unique request lineage. The
|
|
14250
|
+
// shared provider state map is still required so Codex can allocate
|
|
14251
|
+
// websocket state under that side-channel session id.
|
|
14106
14252
|
sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
|
|
14107
14253
|
promptCacheKey: cacheSessionId,
|
|
14108
|
-
preferWebsockets:
|
|
14254
|
+
preferWebsockets: this.#preferWebsockets,
|
|
14255
|
+
providerSessionState: this.#providerSessionState,
|
|
14109
14256
|
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
14110
14257
|
disableReasoning: shouldDisableReasoning(this.thinkingLevel),
|
|
14111
14258
|
hideThinkingSummary: this.agent.hideThinkingSummary,
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
* Precedence (highest first):
|
|
10
10
|
* 1. `JEOPI_AUTH_BROKER_URL` / `JEOPI_AUTH_BROKER_TOKEN` env vars.
|
|
11
11
|
* 2. `auth.broker.url` / `auth.broker.token` in `~/.jeopi/agent/config.yml`
|
|
12
|
-
* (hidden from the settings UI; `!command` resolution supported).
|
|
12
|
+
* or `~/.jeopi/agent/config.yaml` (hidden from the settings UI; `!command` resolution supported).
|
|
13
13
|
* 3. Token file `~/.jeopi/auth-broker.token` (paired with URL from env or config).
|
|
14
14
|
*
|
|
15
15
|
* Returns null when no broker URL is configured — caller falls back to the
|
|
16
16
|
* local SQLite store.
|
|
17
17
|
*
|
|
18
|
-
* Reads config
|
|
18
|
+
* Reads config YAML directly (instead of going through `Settings.init`) because
|
|
19
19
|
* `discoverAuthStorage` runs before the settings singleton is initialized in
|
|
20
20
|
* `runRootCommand`, and we want hand-edited config entries to be honoured at
|
|
21
21
|
* boot without forcing a startup reorder.
|
|
@@ -36,10 +36,10 @@ export { type AuthBrokerClientConfig, getAuthBrokerTokenFilePath };
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Process-lifetime memo for {@link resolveAuthBrokerConfig}. Keyed on the env
|
|
39
|
-
* inputs (plus agent dir, which decides which config
|
|
39
|
+
* inputs (plus agent dir, which decides which config YAML is read) so tests
|
|
40
40
|
* that flip `JEOPI_AUTH_BROKER_*` between cases still observe the change, while
|
|
41
41
|
* repeated resolution within one CLI invocation (startup, subagent sessions)
|
|
42
|
-
* skips the config
|
|
42
|
+
* skips the config YAML read and any `!command` token resolution.
|
|
43
43
|
*/
|
|
44
44
|
let cachedConfigKey: string | null = null;
|
|
45
45
|
let cachedConfigPromise: Promise<AuthBrokerClientConfig | null> | null = null;
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
import { countTokens } from "jeopi-agent-core";
|
|
18
18
|
import type { Context, ImageContent, Model, TextContent, ToolResultMessage, UserMessage } from "jeopi-ai";
|
|
19
|
-
import { isPersonalGitHubCopilotBaseUrl } from "jeopi-catalog/wire/github-copilot";
|
|
20
19
|
import * as snapcompact from "jeopi-snapcompact";
|
|
21
20
|
import contextFramesNote from "../prompts/system/snapcompact-context-frames-note.md" with { type: "text" };
|
|
22
21
|
import contextStub from "../prompts/system/snapcompact-context-stub.md" with { type: "text" };
|
|
@@ -78,19 +77,6 @@ function passesSavingsGate(frames: number, shape: snapcompact.Shape, textTokens:
|
|
|
78
77
|
return frames * shape.frameTokenEstimate <= textTokens * SAVINGS_MARGIN;
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
/**
|
|
82
|
-
* The model is vision-capable for the endpoint we're actually about to hit.
|
|
83
|
-
* GitHub Copilot business and enterprise hosts respond `400 vision is not
|
|
84
|
-
* supported` on image inputs (issue #3387), so even if a stale cached spec
|
|
85
|
-
* still advertises `["text","image"]` we MUST not rasterize transcripts when
|
|
86
|
-
* the resolved `baseUrl` is non-personal.
|
|
87
|
-
*/
|
|
88
|
-
function canSendImages(model: Model): boolean {
|
|
89
|
-
if (!model.input.includes("image")) return false;
|
|
90
|
-
if (model.provider === "github-copilot" && !isPersonalGitHubCopilotBaseUrl(model.baseUrl)) return false;
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
80
|
interface SystemPromptImageTarget {
|
|
95
81
|
scope: Exclude<SnapcompactSystemPromptMode, "none">;
|
|
96
82
|
text: string;
|
|
@@ -291,7 +277,7 @@ export function estimateInlineSavings(input: {
|
|
|
291
277
|
messages: readonly InlineMessageView[];
|
|
292
278
|
}): SnapcompactSavingsEstimate {
|
|
293
279
|
const { options, model } = input;
|
|
294
|
-
if (!model
|
|
280
|
+
if (!model?.input.includes("image")) {
|
|
295
281
|
return { visionCapable: false, savedTokens: 0 };
|
|
296
282
|
}
|
|
297
283
|
|
|
@@ -430,10 +416,8 @@ export class SnapcompactInlineTransformer {
|
|
|
430
416
|
|
|
431
417
|
async transform(context: Context, model: Model): Promise<Context> {
|
|
432
418
|
// Vision gate: providers silently DROP images on text-only models —
|
|
433
|
-
// rendering would lose the content entirely.
|
|
434
|
-
|
|
435
|
-
// (issue #3387: Copilot business endpoint).
|
|
436
|
-
if (!canSendImages(model)) return context;
|
|
419
|
+
// rendering would lose the content entirely.
|
|
420
|
+
if (!model.input.includes("image")) return context;
|
|
437
421
|
|
|
438
422
|
const shape = snapcompact.resolveShape(model, this.options.shape);
|
|
439
423
|
const budget = snapcompact.providerImageBudget(model.provider) - countContextImages(context);
|
|
@@ -735,6 +735,7 @@ export class OutputSink {
|
|
|
735
735
|
#truncated = false;
|
|
736
736
|
#lastChunkTime = 0;
|
|
737
737
|
#pendingChunk = "";
|
|
738
|
+
#pendingChunkTimer: Timer | undefined;
|
|
738
739
|
|
|
739
740
|
// Per-line column cap streaming state (persists across `push` calls so a
|
|
740
741
|
// long line split across chunks still trips the same trigger).
|
|
@@ -806,20 +807,18 @@ export class OutputSink {
|
|
|
806
807
|
push(chunk: string): void {
|
|
807
808
|
chunk = sanitizeWithOptionalSixelPassthrough(chunk, sanitizeText);
|
|
808
809
|
|
|
809
|
-
// Throttled onChunk: coalesce chunks arriving inside the throttle window
|
|
810
|
-
//
|
|
811
|
-
// final
|
|
810
|
+
// Throttled onChunk: coalesce chunks arriving inside the throttle window.
|
|
811
|
+
// A timer flushes quiet tails at the throttle boundary; dump() catches a
|
|
812
|
+
// final pending chunk when the process exits before that timer fires.
|
|
812
813
|
// Live preview gets the raw (pre-cap) chunk so the TUI never lags behind
|
|
813
814
|
// what reached the sink — the column cap is for the persisted LLM view.
|
|
814
815
|
if (this.#onChunk) {
|
|
815
816
|
const now = Date.now();
|
|
816
817
|
if (now - this.#lastChunkTime >= this.#chunkThrottleMs) {
|
|
817
|
-
this.#
|
|
818
|
-
const merged = this.#pendingChunk + chunk;
|
|
819
|
-
this.#pendingChunk = "";
|
|
820
|
-
this.#onChunk(merged);
|
|
818
|
+
this.#emitPendingChunkWith(chunk, now);
|
|
821
819
|
} else {
|
|
822
820
|
this.#pendingChunk += chunk;
|
|
821
|
+
this.#schedulePendingChunkFlush();
|
|
823
822
|
}
|
|
824
823
|
}
|
|
825
824
|
|
|
@@ -1121,6 +1120,7 @@ export class OutputSink {
|
|
|
1121
1120
|
* branch in `dump()` against stale totals.
|
|
1122
1121
|
*/
|
|
1123
1122
|
replace(text: string): void {
|
|
1123
|
+
this.#clearPendingChunkTimer();
|
|
1124
1124
|
this.#buffer = text;
|
|
1125
1125
|
this.#bufferBytes = Buffer.byteLength(text, "utf-8");
|
|
1126
1126
|
this.#head = "";
|
|
@@ -1138,6 +1138,38 @@ export class OutputSink {
|
|
|
1138
1138
|
this.#pendingChunk = "";
|
|
1139
1139
|
}
|
|
1140
1140
|
|
|
1141
|
+
#clearPendingChunkTimer(): void {
|
|
1142
|
+
if (!this.#pendingChunkTimer) return;
|
|
1143
|
+
clearTimeout(this.#pendingChunkTimer);
|
|
1144
|
+
this.#pendingChunkTimer = undefined;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
#emitPendingChunkWith(chunk: string, now: number): void {
|
|
1148
|
+
this.#clearPendingChunkTimer();
|
|
1149
|
+
this.#lastChunkTime = now;
|
|
1150
|
+
const merged = this.#pendingChunk + chunk;
|
|
1151
|
+
this.#pendingChunk = "";
|
|
1152
|
+
this.#onChunk?.(merged);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
#flushPendingChunk(): void {
|
|
1156
|
+
if (this.#pendingChunk.length === 0) {
|
|
1157
|
+
this.#clearPendingChunkTimer();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
this.#emitPendingChunkWith("", Date.now());
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
#schedulePendingChunkFlush(): void {
|
|
1164
|
+
if (this.#chunkThrottleMs <= 0 || this.#pendingChunkTimer) return;
|
|
1165
|
+
const elapsed = Date.now() - this.#lastChunkTime;
|
|
1166
|
+
const delay = Math.max(0, this.#chunkThrottleMs - elapsed);
|
|
1167
|
+
this.#pendingChunkTimer = setTimeout(() => {
|
|
1168
|
+
this.#pendingChunkTimer = undefined;
|
|
1169
|
+
this.#flushPendingChunk();
|
|
1170
|
+
}, delay);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1141
1173
|
/**
|
|
1142
1174
|
* Replay the rolling tail ring back into the artifact sink. When bytes
|
|
1143
1175
|
* were actually dropped from the middle (the head budget was exhausted
|
|
@@ -1179,11 +1211,7 @@ export class OutputSink {
|
|
|
1179
1211
|
|
|
1180
1212
|
// Flush any chunk still held back by the throttle so the live preview
|
|
1181
1213
|
// ends with the complete stream.
|
|
1182
|
-
|
|
1183
|
-
const pending = this.#pendingChunk;
|
|
1184
|
-
this.#pendingChunk = "";
|
|
1185
|
-
this.#onChunk(pending);
|
|
1186
|
-
}
|
|
1214
|
+
this.#flushPendingChunk();
|
|
1187
1215
|
const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
|
|
1188
1216
|
|
|
1189
1217
|
if (this.#file) {
|
|
@@ -34,7 +34,12 @@ import { resolveResumableSession } from "../session/session-listing";
|
|
|
34
34
|
import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
|
|
35
35
|
import { expandTilde, resolveToCwd } from "../tools/path-utils";
|
|
36
36
|
import { urlHyperlinkAlways } from "../tui";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
getChangelogPath,
|
|
39
|
+
parseChangelog,
|
|
40
|
+
RECENT_CHANGELOG_ENTRY_LIMIT,
|
|
41
|
+
renderChangelogEntries,
|
|
42
|
+
} from "../utils/changelog";
|
|
38
43
|
import { copyToClipboard } from "../utils/clipboard";
|
|
39
44
|
import { CollabQrCodeComponent } from "./helpers/collab-qrcode";
|
|
40
45
|
import { buildContextReportText } from "./helpers/context-report";
|
|
@@ -1100,17 +1105,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1100
1105
|
const changelogPath = getChangelogPath();
|
|
1101
1106
|
const allEntries = await parseChangelog(changelogPath);
|
|
1102
1107
|
const showFull = command.args.trim().toLowerCase() === "full";
|
|
1103
|
-
const entriesToShow = showFull ? allEntries : allEntries.slice(0,
|
|
1108
|
+
const entriesToShow = showFull ? allEntries : allEntries.slice(0, RECENT_CHANGELOG_ENTRY_LIMIT);
|
|
1104
1109
|
if (entriesToShow.length === 0) {
|
|
1105
1110
|
await runtime.output("No changelog entries found.");
|
|
1106
1111
|
return commandConsumed();
|
|
1107
1112
|
}
|
|
1108
|
-
await runtime.output(
|
|
1109
|
-
[...entriesToShow]
|
|
1110
|
-
.reverse()
|
|
1111
|
-
.map(entry => entry.content)
|
|
1112
|
-
.join("\n\n"),
|
|
1113
|
-
);
|
|
1113
|
+
await runtime.output(renderChangelogEntries(entriesToShow).markdown);
|
|
1114
1114
|
return commandConsumed();
|
|
1115
1115
|
},
|
|
1116
1116
|
handleTui: async (command, runtime) => {
|