@oh-my-pi/pi-coding-agent 17.2.4 → 17.2.6
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 +69 -0
- package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-gs76k6wc.md} +69 -0
- package/dist/cli.js +4438 -3962
- package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
- package/dist/types/cli/args.d.ts +4 -1
- package/dist/types/cli/browser-relay-cli.d.ts +15 -0
- package/dist/types/cli/command-help.d.ts +99 -0
- package/dist/types/cli/gc-cli.d.ts +1 -0
- package/dist/types/cli/help-extra.d.ts +2 -0
- package/dist/types/cli/thinking-levels.d.ts +5 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/commands/browser-relay.d.ts +38 -0
- package/dist/types/commands/commit.d.ts +3 -0
- package/dist/types/commands/complete.d.ts +1 -1
- package/dist/types/commands/completions.d.ts +6 -0
- package/dist/types/commands/grep.d.ts +3 -0
- package/dist/types/commands/join.d.ts +4 -0
- package/dist/types/commands/launch-help.d.ts +171 -0
- package/dist/types/commands/launch.d.ts +5 -1
- package/dist/types/commands/models.d.ts +3 -0
- package/dist/types/commands/say.d.ts +10 -0
- package/dist/types/commands/worktree.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/model-discovery.d.ts +2 -2
- package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/models-config.d.ts +1 -0
- package/dist/types/config/service-tier.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +69 -21
- package/dist/types/cursor-bridge-tools.d.ts +1 -1
- package/dist/types/cursor.d.ts +7 -4
- package/dist/types/edit/index.d.ts +3 -3
- package/dist/types/edit/modes/replace.d.ts +22 -15
- package/dist/types/edit/renderer.d.ts +1 -1
- package/dist/types/eval/py/kernel.d.ts +3 -1
- package/dist/types/eval/py/spawn-options.d.ts +12 -16
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
- package/dist/types/launch/broker.d.ts +1 -1
- package/dist/types/launch/client.d.ts +15 -4
- package/dist/types/launch/ensure.d.ts +8 -0
- package/dist/types/launch/paths.d.ts +1 -1
- package/dist/types/launch/protocol.d.ts +19 -2
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/mux/daemon.d.ts +15 -0
- package/dist/types/lsp/mux/protocol.d.ts +57 -0
- package/dist/types/lsp/mux/server.d.ts +18 -0
- package/dist/types/lsp/types.d.ts +22 -2
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mnemopi/embed-client.d.ts +1 -1
- package/dist/types/mnemopi/state.d.ts +0 -23
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/btw-panel.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/workflow.d.ts +2 -1
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/security/contracts/schemas.d.ts +405 -403
- package/dist/types/session/agent-session-types.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +26 -2
- package/dist/types/session/indexed-session-storage.d.ts +1 -0
- package/dist/types/session/launch-completion.d.ts +10 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-context.d.ts +2 -1
- package/dist/types/session/session-entries.d.ts +15 -1
- package/dist/types/session/session-maintenance.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +23 -11
- package/dist/types/session/session-stats.d.ts +10 -0
- package/dist/types/session/session-storage.d.ts +15 -3
- package/dist/types/session/yield-queue.d.ts +6 -1
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/spawn-policy.d.ts +5 -0
- package/dist/types/thinking.d.ts +1 -7
- package/dist/types/tools/ast-grep.d.ts +1 -1
- package/dist/types/tools/browser/registry.d.ts +2 -1
- package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
- package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
- package/dist/types/tools/browser/relay/kind.d.ts +19 -0
- package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
- package/dist/types/tools/browser/relay/server.d.ts +35 -0
- package/dist/types/tools/browser/render.d.ts +1 -0
- package/dist/types/tools/browser.d.ts +3 -0
- package/dist/types/tools/computer/exposure.d.ts +9 -7
- package/dist/types/tools/computer/protocol.d.ts +70 -17
- package/dist/types/tools/computer/supervisor.d.ts +23 -10
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +42 -7
- package/dist/types/tools/computer-renderer.d.ts +7 -5
- package/dist/types/tools/computer.d.ts +25 -44
- package/dist/types/tools/context.d.ts +3 -0
- package/dist/types/tools/glob.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +1 -1
- package/dist/types/tools/index.d.ts +9 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
- package/dist/types/tools/tool-timeouts.d.ts +5 -0
- package/dist/types/tools/xdev.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +2 -2
- package/dist/types/web/kagi.d.ts +1 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/providers/anthropic.d.ts +1 -0
- package/dist/types/web/search/providers/base.d.ts +2 -0
- package/dist/types/web/search/providers/brave.d.ts +1 -0
- package/dist/types/web/search/providers/browser-page.d.ts +1 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -0
- package/dist/types/web/search/providers/exa.d.ts +1 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/dist/types/web/search/providers/jina.d.ts +1 -0
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -0
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +1 -0
- package/dist/types/web/search/providers/searxng.d.ts +1 -0
- package/dist/types/web/search/providers/tavily.d.ts +1 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +6 -9
- package/dist/types/web/search/providers/zai.d.ts +1 -0
- package/dist/types/web/search/types.d.ts +4 -0
- package/package.json +12 -12
- package/src/cli/args.ts +7 -87
- package/src/cli/browser-relay-cli.ts +119 -0
- package/src/cli/command-help.ts +103 -0
- package/src/cli/completion-gen.ts +5 -5
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/gc-cli.ts +641 -21
- package/src/cli/help-extra.ts +89 -0
- package/src/cli/setup-cli.ts +13 -21
- package/src/cli/thinking-levels.ts +7 -0
- package/src/cli/update-cli.ts +26 -7
- package/src/cli-commands.ts +170 -33
- package/src/cli.ts +17 -5
- package/src/commands/acp.ts +3 -1
- package/src/commands/agents.ts +3 -2
- package/src/commands/auth-broker.ts +3 -2
- package/src/commands/auth-gateway.ts +3 -2
- package/src/commands/bench.ts +2 -3
- package/src/commands/browser-relay.ts +53 -0
- package/src/commands/cleanse.ts +2 -2
- package/src/commands/commit.ts +3 -2
- package/src/commands/complete.ts +2 -1
- package/src/commands/completions.ts +3 -2
- package/src/commands/config.ts +3 -2
- package/src/commands/dry-balance.ts +2 -2
- package/src/commands/gallery.ts +3 -2
- package/src/commands/gc.ts +3 -2
- package/src/commands/grep.ts +3 -2
- package/src/commands/grievances.ts +3 -2
- package/src/commands/install.ts +2 -2
- package/src/commands/join.ts +3 -2
- package/src/commands/launch-help.ts +116 -0
- package/src/commands/launch.ts +7 -191
- package/src/commands/models.ts +3 -2
- package/src/commands/plugin.ts +3 -2
- package/src/commands/read.ts +3 -2
- package/src/commands/say.ts +3 -2
- package/src/commands/setup.ts +3 -2
- package/src/commands/shell.ts +3 -2
- package/src/commands/ssh.ts +3 -2
- package/src/commands/stats.ts +3 -2
- package/src/commands/tiny-models.ts +2 -2
- package/src/commands/token.ts +2 -2
- package/src/commands/ttsr.ts +2 -2
- package/src/commands/update.ts +3 -2
- package/src/commands/usage.ts +3 -2
- package/src/commands/web-search.ts +3 -2
- package/src/commands/worktree.ts +3 -2
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-discovery.ts +44 -27
- package/src/config/model-registry.ts +19 -13
- package/src/config/models-config-schema-bundle.ts +9 -0
- package/src/config/resolve-config-value.ts +2 -1
- package/src/config/service-tier.ts +5 -0
- package/src/config/settings-schema.ts +72 -20
- package/src/config/settings.ts +1 -1
- package/src/cursor-bridge-tools.ts +1 -1
- package/src/cursor.ts +10 -14
- package/src/edit/diff.ts +17 -8
- package/src/edit/index.ts +20 -7
- package/src/edit/modes/replace.ts +93 -40
- package/src/edit/renderer.ts +1 -1
- package/src/edit/streaming.ts +14 -17
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/spawn-options.ts +24 -29
- package/src/export/html/index.ts +10 -3
- package/src/export/html/template.js +8 -26
- package/src/export/share.ts +4 -0
- package/src/extensibility/extensions/wrapper.ts +1 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
- package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
- package/src/extensibility/skills.ts +11 -0
- package/src/extensibility/utils.ts +10 -2
- package/src/launch/broker.ts +223 -7
- package/src/launch/client.ts +192 -21
- package/src/launch/ensure.ts +73 -0
- package/src/launch/paths.ts +1 -1
- package/src/launch/protocol.ts +54 -2
- package/src/lsp/client.ts +23 -2
- package/src/lsp/index.ts +8 -0
- package/src/lsp/mux/daemon.ts +348 -0
- package/src/lsp/mux/protocol.ts +96 -0
- package/src/lsp/mux/server.ts +797 -0
- package/src/lsp/types.ts +28 -2
- package/src/main.ts +20 -8
- package/src/mcp/config-writer.ts +1 -1
- package/src/mcp/oauth-flow.ts +11 -1
- package/src/mnemopi/embed-client.ts +49 -2
- package/src/mnemopi/state.ts +57 -23
- package/src/modes/components/assistant-message.ts +11 -0
- package/src/modes/components/btw-panel.ts +41 -4
- package/src/modes/components/chat-transcript-builder.ts +4 -0
- package/src/modes/components/custom-editor.ts +9 -0
- package/src/modes/components/read-tool-group.ts +10 -0
- package/src/modes/components/status-line/component.ts +1 -1
- package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
- package/src/modes/components/tool-execution.ts +8 -0
- package/src/modes/components/tree-selector.ts +46 -48
- package/src/modes/controllers/btw-controller.ts +55 -7
- package/src/modes/controllers/command-controller.ts +31 -0
- package/src/modes/controllers/event-controller.ts +17 -0
- package/src/modes/controllers/input-controller.ts +63 -8
- package/src/modes/controllers/mcp-command-controller.ts +11 -5
- package/src/modes/controllers/selector-controller.ts +20 -0
- package/src/modes/interactive-mode.ts +18 -2
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +6 -10
- package/src/modes/workflow.ts +8 -2
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/session/launch-completion.md +1 -0
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +26 -52
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/computer.md +21 -21
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/replace.md +5 -4
- package/src/prompts/tools/task.md +4 -4
- package/src/sdk.ts +49 -6
- package/src/secrets/index.ts +1 -1
- package/src/security/contracts/schemas.ts +205 -183
- package/src/security/contracts/validation.ts +5 -1
- package/src/security/store.ts +2 -2
- package/src/session/agent-session-types.ts +9 -1
- package/src/session/agent-session.ts +204 -15
- package/src/session/indexed-session-storage.ts +9 -0
- package/src/session/launch-completion.ts +37 -0
- package/src/session/prewalk.ts +24 -2
- package/src/session/session-advisors.ts +2 -2
- package/src/session/session-context.ts +42 -12
- package/src/session/session-dump-format.ts +1 -1
- package/src/session/session-entries.ts +17 -1
- package/src/session/session-maintenance.ts +38 -4
- package/src/session/session-manager.ts +123 -67
- package/src/session/session-paths.ts +118 -55
- package/src/session/session-stats.ts +48 -3
- package/src/session/session-storage.ts +30 -22
- package/src/session/stream-guards.ts +1 -1
- package/src/session/yield-queue.ts +121 -16
- package/src/slash-commands/builtin-registry.ts +37 -24
- package/src/system-prompt.ts +8 -3
- package/src/task/index.ts +23 -8
- package/src/task/spawn-policy.ts +14 -0
- package/src/thinking.ts +2 -8
- package/src/tools/ast-grep.ts +10 -4
- package/src/tools/browser/cmux/cmux-tab.ts +97 -14
- package/src/tools/browser/cmux/rpc.ts +2 -8
- package/src/tools/browser/registry.ts +53 -2
- package/src/tools/browser/relay/bridge.ts +945 -0
- package/src/tools/browser/relay/daemon.ts +117 -0
- package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
- package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
- package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
- package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
- package/src/tools/browser/relay/kind.ts +41 -0
- package/src/tools/browser/relay/protocol.ts +54 -0
- package/src/tools/browser/relay/server.ts +141 -0
- package/src/tools/browser/render.ts +4 -1
- package/src/tools/browser/shared-daemon.ts +7 -58
- package/src/tools/browser/tab-supervisor.ts +5 -4
- package/src/tools/browser/tab-worker.ts +33 -12
- package/src/tools/browser.ts +26 -1
- package/src/tools/computer/exposure.ts +11 -35
- package/src/tools/computer/protocol.ts +51 -10
- package/src/tools/computer/supervisor.ts +227 -124
- package/src/tools/computer/worker-entry.ts +11 -6
- package/src/tools/computer/worker.ts +695 -81
- package/src/tools/computer-renderer.ts +104 -65
- package/src/tools/computer.ts +134 -428
- package/src/tools/context.ts +3 -0
- package/src/tools/glob.ts +9 -2
- package/src/tools/grep.ts +12 -6
- package/src/tools/hub/launch.ts +122 -6
- package/src/tools/index.ts +9 -0
- package/src/tools/inspect-image.ts +32 -6
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/tools/xdev.ts +29 -2
- package/src/utils/clipboard.ts +40 -14
- package/src/utils/command-args.ts +26 -28
- package/src/utils/tool-choice.ts +0 -13
- package/src/web/kagi.ts +2 -1
- package/src/web/search/index.ts +18 -2
- package/src/web/search/providers/anthropic.ts +4 -1
- package/src/web/search/providers/base.ts +2 -0
- package/src/web/search/providers/brave.ts +3 -1
- package/src/web/search/providers/browser-page.ts +6 -4
- package/src/web/search/providers/codex.ts +4 -1
- package/src/web/search/providers/duckduckgo.ts +2 -1
- package/src/web/search/providers/ecosia.ts +2 -1
- package/src/web/search/providers/exa.ts +4 -2
- package/src/web/search/providers/firecrawl.ts +3 -1
- package/src/web/search/providers/gemini.ts +8 -2
- package/src/web/search/providers/google.ts +2 -1
- package/src/web/search/providers/jina.ts +12 -2
- package/src/web/search/providers/kagi.ts +3 -0
- package/src/web/search/providers/kimi.ts +5 -1
- package/src/web/search/providers/mojeek.ts +2 -1
- package/src/web/search/providers/parallel.ts +5 -1
- package/src/web/search/providers/perplexity.ts +6 -3
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/searxng.ts +11 -5
- package/src/web/search/providers/startpage.ts +10 -4
- package/src/web/search/providers/synthetic.ts +11 -5
- package/src/web/search/providers/tavily.ts +3 -1
- package/src/web/search/providers/tinyfish.ts +3 -1
- package/src/web/search/providers/utils.ts +12 -10
- package/src/web/search/providers/xai.ts +2 -2
- package/src/web/search/providers/zai.ts +18 -3
- package/src/web/search/types.ts +6 -0
- package/dist/types/config/file-lock.d.ts +0 -29
- package/src/config/file-lock.ts +0 -164
|
@@ -2,10 +2,17 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getTerminalId } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getSessionsDir,
|
|
7
|
+
getTerminalSessionsDir,
|
|
8
|
+
isEnoent,
|
|
9
|
+
isRecord,
|
|
10
|
+
logger,
|
|
11
|
+
resolveEquivalentPath,
|
|
12
|
+
} from "@oh-my-pi/pi-utils";
|
|
6
13
|
import type { SessionStorage } from "./session-storage";
|
|
7
14
|
|
|
8
|
-
const
|
|
15
|
+
const SESSION_HEADER_PREFIX_BYTES = 4096;
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* Merge or rename a legacy session directory into its canonical target.
|
|
@@ -35,12 +42,16 @@ function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
|
|
|
35
42
|
return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
function
|
|
45
|
+
function encodeLegacyRelativeSessionDirName(prefix: string, relative: string): string {
|
|
39
46
|
const encoded = relative.replace(/[/\\:]/g, "-");
|
|
40
47
|
return encoded ? (prefix.endsWith("-") ? `${prefix}${encoded}` : `${prefix}-${encoded}`) : prefix;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
function getDefaultSessionDirName(cwd: string): {
|
|
50
|
+
function getDefaultSessionDirName(cwd: string): {
|
|
51
|
+
encodedDirName: string;
|
|
52
|
+
legacyRelativeDirName: string | undefined;
|
|
53
|
+
resolvedCwd: string;
|
|
54
|
+
} {
|
|
44
55
|
const resolvedCwd = path.resolve(cwd);
|
|
45
56
|
const canonicalCwd = resolveEquivalentPath(resolvedCwd);
|
|
46
57
|
const home = os.homedir();
|
|
@@ -49,72 +60,111 @@ function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolv
|
|
|
49
60
|
const canonicalTempRoot = resolveEquivalentPath(tempRoot);
|
|
50
61
|
const homeRelative = path.relative(canonicalHome, canonicalCwd);
|
|
51
62
|
const tempRelative = path.relative(canonicalTempRoot, canonicalCwd);
|
|
52
|
-
const encodedDirName =
|
|
53
|
-
homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))
|
|
54
|
-
? encodeRelativeSessionDirName("-", homeRelative)
|
|
55
|
-
: tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))
|
|
56
|
-
? encodeRelativeSessionDirName("-tmp", tempRelative)
|
|
57
|
-
: encodeLegacyAbsoluteSessionDirName(canonicalCwd);
|
|
58
|
-
return { encodedDirName, resolvedCwd };
|
|
59
|
-
}
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
64
|
+
let scope: "home" | "tmp" | "abs";
|
|
65
|
+
let legacyRelativeDirName: string | undefined;
|
|
66
|
+
if (homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))) {
|
|
67
|
+
scope = "home";
|
|
68
|
+
legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-", homeRelative);
|
|
69
|
+
} else if (tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))) {
|
|
70
|
+
scope = "tmp";
|
|
71
|
+
legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-tmp", tempRelative);
|
|
72
|
+
} else {
|
|
73
|
+
scope = "abs";
|
|
74
|
+
}
|
|
68
75
|
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
const normalized = canonicalCwd.replaceAll("\\", "/");
|
|
77
|
+
const readable = path
|
|
78
|
+
.basename(canonicalCwd)
|
|
79
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
80
|
+
.replace(/^-+|-+$/g, "")
|
|
81
|
+
.slice(-80);
|
|
82
|
+
const digest = Bun.SHA256.hash(normalized, "hex");
|
|
83
|
+
const encodedDirName = `${scope}-${readable || "project"}-${digest}`;
|
|
84
|
+
return { encodedDirName, legacyRelativeDirName, resolvedCwd };
|
|
85
|
+
}
|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
function readSessionCwd(sessionFile: string, buffer: Buffer): string | undefined {
|
|
88
|
+
let descriptor: number | undefined;
|
|
75
89
|
try {
|
|
76
|
-
|
|
90
|
+
descriptor = fs.openSync(sessionFile, "r");
|
|
91
|
+
const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
92
|
+
const prefix = buffer.toString("utf8", 0, bytesRead);
|
|
93
|
+
for (const line of prefix.split(/\r?\n/, 3)) {
|
|
94
|
+
try {
|
|
95
|
+
const record: unknown = JSON.parse(line);
|
|
96
|
+
if (isRecord(record) && record.type === "session" && typeof record.cwd === "string") {
|
|
97
|
+
return record.cwd;
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Ignore title slots or truncated/corrupt headers.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
77
103
|
} catch {
|
|
78
|
-
|
|
104
|
+
// Best-effort migration leaves unreadable sessions in the current bucket.
|
|
105
|
+
} finally {
|
|
106
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
79
107
|
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
80
110
|
|
|
111
|
+
function moveSessionBundle(sourceDir: string, targetDir: string, sessionName: string, entries: string[]): void {
|
|
112
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
113
|
+
const artifactsName = path.basename(sessionName, ".jsonl");
|
|
81
114
|
for (const entry of entries) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
if (entry !== sessionName && entry !== artifactsName && !entry.startsWith(`${sessionName}.`)) continue;
|
|
116
|
+
const source = path.join(sourceDir, entry);
|
|
117
|
+
if (!fs.existsSync(source)) continue;
|
|
118
|
+
const target = path.join(targetDir, entry);
|
|
119
|
+
const existing = fs.statSync(target, { throwIfNoEntry: false });
|
|
120
|
+
if (!existing) {
|
|
121
|
+
fs.renameSync(source, target);
|
|
122
|
+
} else if (existing.isDirectory() && fs.statSync(source).isDirectory()) {
|
|
123
|
+
migrateSessionDirPath(source, target);
|
|
87
124
|
} else {
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const newName = remainder ? `-${remainder}` : "-";
|
|
92
|
-
const oldPath = path.join(sessionsRoot, entry);
|
|
93
|
-
const newPath = path.join(sessionsRoot, newName);
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
migrateSessionDirPath(oldPath, newPath);
|
|
97
|
-
} catch {
|
|
98
|
-
// Best effort
|
|
125
|
+
fs.rmSync(source, { recursive: true, force: true });
|
|
99
126
|
}
|
|
100
127
|
}
|
|
101
128
|
}
|
|
102
129
|
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
130
|
+
function rerouteCollidingSessions(
|
|
131
|
+
cwd: string,
|
|
132
|
+
legacyDir: string,
|
|
133
|
+
sessionsRoot: string,
|
|
134
|
+
kind: "relative" | "absolute",
|
|
135
|
+
): void {
|
|
136
|
+
const entries = fs.readdirSync(legacyDir);
|
|
137
|
+
const currentCwd = resolveEquivalentPath(path.resolve(cwd));
|
|
138
|
+
const buffer = Buffer.allocUnsafe(SESSION_HEADER_PREFIX_BYTES);
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
141
|
+
const recordedCwd = readSessionCwd(path.join(legacyDir, entry), buffer);
|
|
142
|
+
if (!recordedCwd) continue;
|
|
143
|
+
const recordedCanonical = resolveEquivalentPath(path.resolve(recordedCwd));
|
|
144
|
+
if (
|
|
145
|
+
recordedCanonical === currentCwd ||
|
|
146
|
+
!fs.statSync(recordedCanonical, { throwIfNoEntry: false })?.isDirectory()
|
|
147
|
+
) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const recordedNames = getDefaultSessionDirName(recordedCanonical);
|
|
151
|
+
const recordedLegacyName =
|
|
152
|
+
kind === "relative"
|
|
153
|
+
? recordedNames.legacyRelativeDirName
|
|
154
|
+
: encodeLegacyAbsoluteSessionDirName(recordedCanonical);
|
|
155
|
+
if (recordedLegacyName !== path.basename(legacyDir)) continue;
|
|
156
|
+
moveSessionBundle(legacyDir, path.join(sessionsRoot, recordedNames.encodedDirName), entry, entries);
|
|
111
157
|
}
|
|
112
158
|
}
|
|
113
159
|
|
|
114
160
|
export function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
|
|
115
161
|
const currentDirName = path.basename(sessionDir);
|
|
116
|
-
const { encodedDirName } = getDefaultSessionDirName(cwd);
|
|
117
|
-
if (
|
|
162
|
+
const { encodedDirName, legacyRelativeDirName } = getDefaultSessionDirName(cwd);
|
|
163
|
+
if (
|
|
164
|
+
currentDirName !== encodedDirName &&
|
|
165
|
+
currentDirName !== legacyRelativeDirName &&
|
|
166
|
+
currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)
|
|
167
|
+
) {
|
|
118
168
|
return undefined;
|
|
119
169
|
}
|
|
120
170
|
return path.dirname(sessionDir);
|
|
@@ -130,10 +180,23 @@ export function computeDefaultSessionDir(
|
|
|
130
180
|
storage: SessionStorage,
|
|
131
181
|
sessionsRoot: string = getSessionsDir(),
|
|
132
182
|
): string {
|
|
133
|
-
const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
|
|
134
|
-
migrateHomeSessionDirs(sessionsRoot);
|
|
183
|
+
const { encodedDirName, legacyRelativeDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
|
|
135
184
|
const sessionDir = path.join(sessionsRoot, encodedDirName);
|
|
136
|
-
|
|
185
|
+
const legacyDirs: Array<{ kind: "relative" | "absolute"; name: string | undefined }> = [
|
|
186
|
+
{ kind: "relative", name: legacyRelativeDirName },
|
|
187
|
+
{ kind: "absolute", name: encodeLegacyAbsoluteSessionDirName(resolvedCwd) },
|
|
188
|
+
];
|
|
189
|
+
for (const legacy of legacyDirs) {
|
|
190
|
+
if (!legacy.name) continue;
|
|
191
|
+
const legacyDir = path.join(sessionsRoot, legacy.name);
|
|
192
|
+
if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) continue;
|
|
193
|
+
try {
|
|
194
|
+
rerouteCollidingSessions(resolvedCwd, legacyDir, sessionsRoot, legacy.kind);
|
|
195
|
+
migrateSessionDirPath(legacyDir, sessionDir);
|
|
196
|
+
} catch {
|
|
197
|
+
// Best effort
|
|
198
|
+
}
|
|
199
|
+
}
|
|
137
200
|
storage.ensureDirSync(sessionDir);
|
|
138
201
|
return sessionDir;
|
|
139
202
|
}
|
|
@@ -34,6 +34,11 @@ export interface SessionStatsTrackerHost {
|
|
|
34
34
|
sessionId(): string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function correctedPromptTokens(assistant: AssistantMessage): number {
|
|
38
|
+
const providerPromptTokens = assistant.contextSnapshot?.promptTokens ?? calculatePromptTokens(assistant.usage);
|
|
39
|
+
return Math.max(0, providerPromptTokens - (assistant.contextSnapshot?.historyRewriteTokensRemoved ?? 0));
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
/** Computes session totals and tracks the in-flight context estimate. */
|
|
38
43
|
export class SessionStatsTracker {
|
|
39
44
|
readonly #host: SessionStatsTrackerHost;
|
|
@@ -160,8 +165,7 @@ export class SessionStatsTracker {
|
|
|
160
165
|
const useAnchor =
|
|
161
166
|
anchorAssistant !== undefined && anchorIndex !== -1 && (!pending || anchorIndex >= pending.cutoffCount);
|
|
162
167
|
if (useAnchor && anchorAssistant) {
|
|
163
|
-
const promptTokens =
|
|
164
|
-
anchorAssistant.contextSnapshot?.promptTokens ?? calculatePromptTokens(anchorAssistant.usage);
|
|
168
|
+
const promptTokens = correctedPromptTokens(anchorAssistant);
|
|
165
169
|
const nonMessageTokens =
|
|
166
170
|
anchorAssistant.contextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this.#host.session);
|
|
167
171
|
anchored = true;
|
|
@@ -199,7 +203,7 @@ export class SessionStatsTracker {
|
|
|
199
203
|
) {
|
|
200
204
|
continue;
|
|
201
205
|
}
|
|
202
|
-
const promptTokens =
|
|
206
|
+
const promptTokens = correctedPromptTokens(message);
|
|
203
207
|
const nonMessageTokens =
|
|
204
208
|
message.contextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this.#host.session);
|
|
205
209
|
let tailTokens = 0;
|
|
@@ -256,6 +260,47 @@ export class SessionStatsTracker {
|
|
|
256
260
|
return this.#pendingContextSnapshot?.nonMessageTokens;
|
|
257
261
|
}
|
|
258
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Apply an estimated prompt-prefix reduction to the current provider anchor.
|
|
265
|
+
*
|
|
266
|
+
* History after the anchor is estimated live by {@link getContextBreakdown};
|
|
267
|
+
* callers must pass only savings from entries already included in the
|
|
268
|
+
* anchor's provider-reported prompt. Persisting the correction on the
|
|
269
|
+
* assistant snapshot keeps reloads accurate, and the next successful
|
|
270
|
+
* assistant response naturally replaces it with a fresh provider anchor.
|
|
271
|
+
*/
|
|
272
|
+
recordAnchoredHistoryRewrite(tokensRemoved: number): void {
|
|
273
|
+
if (!Number.isFinite(tokensRemoved) || tokensRemoved <= 0) return;
|
|
274
|
+
|
|
275
|
+
const branchEntries = this.#host.sessionManager.getBranch();
|
|
276
|
+
const latestCompaction = getLatestCompactionEntry(branchEntries);
|
|
277
|
+
const compactionIndex = latestCompaction ? branchEntries.lastIndexOf(latestCompaction) : -1;
|
|
278
|
+
for (let index = branchEntries.length - 1; index > compactionIndex; index--) {
|
|
279
|
+
const entry = branchEntries[index];
|
|
280
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
281
|
+
const assistant = entry.message;
|
|
282
|
+
if (
|
|
283
|
+
assistant.stopReason === "aborted" ||
|
|
284
|
+
assistant.stopReason === "error" ||
|
|
285
|
+
!assistant.usage ||
|
|
286
|
+
!hasContextTokenUsage(assistant.usage)
|
|
287
|
+
) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!assistant.contextSnapshot) {
|
|
292
|
+
assistant.contextSnapshot = {
|
|
293
|
+
promptTokens: calculatePromptTokens(assistant.usage),
|
|
294
|
+
nonMessageTokens: computeNonMessageTokens(this.#host.session),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const snapshot = assistant.contextSnapshot;
|
|
298
|
+
snapshot.historyRewriteTokensRemoved = (snapshot.historyRewriteTokensRemoved ?? 0) + Math.floor(tokensRemoved);
|
|
299
|
+
this.#contextUsageRevision++;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
259
304
|
/** Sets or clears the in-flight context snapshot. */
|
|
260
305
|
setPendingSnapshot(snapshot: PendingContextSnapshot | undefined): void {
|
|
261
306
|
this.#pendingContextSnapshot = snapshot;
|
|
@@ -14,13 +14,25 @@ export interface SessionStorageStat {
|
|
|
14
14
|
|
|
15
15
|
export interface SessionStorageWriter {
|
|
16
16
|
/**
|
|
17
|
-
* Append one newline-terminated line.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Append one newline-terminated line.
|
|
18
|
+
*
|
|
19
|
+
* File and memory storage apply the line synchronously before the returned
|
|
20
|
+
* promise settles, so a software crash after `append` returns (or after a
|
|
21
|
+
* fire-and-forget call begins) still sees the entry on disk / in body. No
|
|
22
|
+
* `fsync` — power loss may still drop the last page. Indexed backends update
|
|
23
|
+
* the local index immediately and queue the remote publish in call order.
|
|
20
24
|
*
|
|
21
25
|
* `line` MUST include the trailing newline.
|
|
22
26
|
*/
|
|
23
27
|
append(line: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Synchronous append when the backend can apply the line before return.
|
|
30
|
+
* File and memory implement this so {@link SessionManager} can latch the
|
|
31
|
+
* first write failure before the appending call returns (surfaced by a later
|
|
32
|
+
* flushSync/close/next append — the turn loop does not throw from append).
|
|
33
|
+
* Indexed backends update the local index immediately and queue remote I/O.
|
|
34
|
+
*/
|
|
35
|
+
appendSync?(line: string): void;
|
|
24
36
|
/** Resolve once all queued appends complete. No fsync. */
|
|
25
37
|
flush(): Promise<void>;
|
|
26
38
|
/** Drain synchronously flushable queued work when the backend supports it. No fsync. */
|
|
@@ -93,8 +105,6 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
93
105
|
#closed = false;
|
|
94
106
|
#error: Error | undefined;
|
|
95
107
|
#onError: ((err: Error) => void) | undefined;
|
|
96
|
-
#pending = "";
|
|
97
|
-
#flushScheduled = false;
|
|
98
108
|
|
|
99
109
|
constructor(fpath: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
|
|
100
110
|
this.#onError = options?.onError;
|
|
@@ -129,34 +139,29 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
this.#
|
|
134
|
-
if (this.#
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
appendSync(line: string): void {
|
|
143
|
+
if (this.#closed) throw new Error("Writer closed");
|
|
144
|
+
if (this.#error) throw this.#error;
|
|
145
|
+
// Write in-body so software crash after the call still sees the entry.
|
|
146
|
+
// Microtask batching used to leave completed transcript lines only in
|
|
147
|
+
// memory until the next event-loop turn; process crash then lost every
|
|
148
|
+
// post-checkpoint event. flush/flushSync remain no-op drains (no fsync).
|
|
137
149
|
try {
|
|
138
|
-
this.#writeNow(
|
|
150
|
+
this.#writeNow(line);
|
|
139
151
|
} catch (err) {
|
|
140
|
-
this.#recordError(err);
|
|
152
|
+
throw this.#recordError(err);
|
|
141
153
|
}
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
async append(line: string): Promise<void> {
|
|
145
|
-
|
|
146
|
-
if (this.#error) throw this.#error;
|
|
147
|
-
this.#pending += line;
|
|
148
|
-
if (this.#flushScheduled) return;
|
|
149
|
-
this.#flushScheduled = true;
|
|
150
|
-
queueMicrotask(() => this.#flushPendingNow());
|
|
157
|
+
this.appendSync(line);
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
async flush(): Promise<void> {
|
|
154
|
-
this.#flushPendingNow();
|
|
155
161
|
if (this.#error) throw this.#error;
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
flushSync(): void {
|
|
159
|
-
this.#flushPendingNow();
|
|
160
165
|
if (this.#error) throw this.#error;
|
|
161
166
|
}
|
|
162
167
|
|
|
@@ -166,7 +171,6 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
166
171
|
|
|
167
172
|
async close(): Promise<void> {
|
|
168
173
|
if (this.#closed) return;
|
|
169
|
-
this.#flushPendingNow();
|
|
170
174
|
this.#closed = true;
|
|
171
175
|
// Unregister from finalization - we're closing properly
|
|
172
176
|
writerRegistry.unregister(this);
|
|
@@ -487,7 +491,7 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
487
491
|
return error;
|
|
488
492
|
}
|
|
489
493
|
|
|
490
|
-
|
|
494
|
+
appendSync(line: string): void {
|
|
491
495
|
if (this.#closed) throw new Error("Writer closed");
|
|
492
496
|
if (this.#error) throw this.#error;
|
|
493
497
|
try {
|
|
@@ -498,6 +502,10 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
498
502
|
}
|
|
499
503
|
}
|
|
500
504
|
|
|
505
|
+
async append(line: string): Promise<void> {
|
|
506
|
+
this.appendSync(line);
|
|
507
|
+
}
|
|
508
|
+
|
|
501
509
|
async flush(): Promise<void> {
|
|
502
510
|
if (this.#error) throw this.#error;
|
|
503
511
|
}
|
|
@@ -164,7 +164,7 @@ export class StreamingEditGuard {
|
|
|
164
164
|
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
165
165
|
if (toolCall.name !== "edit") return undefined;
|
|
166
166
|
const args = toolCall.arguments;
|
|
167
|
-
if (!args || typeof args !== "object" || Array.isArray(args) || "
|
|
167
|
+
if (!args || typeof args !== "object" || Array.isArray(args) || "old_string" in args || "new_string" in args) {
|
|
168
168
|
return undefined;
|
|
169
169
|
}
|
|
170
170
|
const filePath = typeof args.path === "string" ? args.path : undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentMessage, ASIDE_MESSAGE_COMMIT, ASIDE_MESSAGE_DISCARD } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
3
3
|
|
|
4
4
|
export interface YieldDispatcher<P> {
|
|
@@ -25,6 +25,17 @@ interface StoredDispatcher {
|
|
|
25
25
|
skipIdleFlush?: boolean;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
interface StoredEntry {
|
|
29
|
+
value: unknown;
|
|
30
|
+
resolve?: () => void;
|
|
31
|
+
reject?: (error: Error) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface BuiltMessage {
|
|
35
|
+
message: AgentMessage;
|
|
36
|
+
entries: StoredEntry[];
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
function formatError(error: unknown): string {
|
|
29
40
|
return error instanceof Error ? error.message : String(error);
|
|
30
41
|
}
|
|
@@ -32,7 +43,7 @@ function formatError(error: unknown): string {
|
|
|
32
43
|
export class YieldQueue {
|
|
33
44
|
readonly #options: YieldQueueOptions;
|
|
34
45
|
readonly #dispatchers = new Map<string, StoredDispatcher>();
|
|
35
|
-
readonly #entries = new Map<string,
|
|
46
|
+
readonly #entries = new Map<string, StoredEntry[]>();
|
|
36
47
|
#idleFlushPending = false;
|
|
37
48
|
|
|
38
49
|
constructor(options: YieldQueueOptions) {
|
|
@@ -49,14 +60,27 @@ export class YieldQueue {
|
|
|
49
60
|
return () => {
|
|
50
61
|
if (this.#dispatchers.get(kind) !== stored) return;
|
|
51
62
|
this.#dispatchers.delete(kind);
|
|
63
|
+
this.#rejectEntries(this.#entries.get(kind) ?? [], new Error(`Yield queue dispatcher removed: ${kind}`));
|
|
52
64
|
this.#entries.delete(kind);
|
|
53
65
|
};
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
enqueue<P>(kind: string, entry: P): void {
|
|
69
|
+
this.#enqueue(kind, { value: entry });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
enqueueWithReceipt<P>(kind: string, entry: P): Promise<void> {
|
|
73
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
74
|
+
if (!this.#enqueue(kind, { value: entry, resolve, reject })) {
|
|
75
|
+
reject(new Error(`Yield queue entry ignored for unregistered kind: ${kind}`));
|
|
76
|
+
}
|
|
77
|
+
return promise;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#enqueue(kind: string, entry: StoredEntry): boolean {
|
|
57
81
|
if (!this.#dispatchers.has(kind)) {
|
|
58
82
|
logger.warn("Yield queue entry ignored for unregistered kind", { kind });
|
|
59
|
-
return;
|
|
83
|
+
return false;
|
|
60
84
|
}
|
|
61
85
|
let entries = this.#entries.get(kind);
|
|
62
86
|
if (!entries) {
|
|
@@ -67,6 +91,7 @@ export class YieldQueue {
|
|
|
67
91
|
if (!this.#options.isStreaming() && !this.#dispatchers.get(kind)!.skipIdleFlush) {
|
|
68
92
|
this.#scheduleIdleFlush();
|
|
69
93
|
}
|
|
94
|
+
return true;
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
has(kind?: string): boolean {
|
|
@@ -77,30 +102,55 @@ export class YieldQueue {
|
|
|
77
102
|
return false;
|
|
78
103
|
}
|
|
79
104
|
|
|
105
|
+
/** Arrange an idle flush for entries queued near the end of a streaming run. */
|
|
106
|
+
requestIdleFlush(): void {
|
|
107
|
+
for (const [kind, dispatcher] of this.#dispatchers) {
|
|
108
|
+
if (!dispatcher.skipIdleFlush && this.has(kind)) {
|
|
109
|
+
this.#scheduleIdleFlush();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
async flush(mode: YieldFlushMode): Promise<void> {
|
|
81
116
|
if (mode === "idle") {
|
|
82
117
|
this.#idleFlushPending = false;
|
|
83
118
|
}
|
|
84
|
-
const idleMessages:
|
|
119
|
+
const idleMessages: BuiltMessage[] = [];
|
|
85
120
|
for (const [kind, dispatcher] of this.#dispatchers) {
|
|
121
|
+
if (mode === "idle" && dispatcher.skipIdleFlush) continue;
|
|
86
122
|
const entries = this.#drain(kind);
|
|
87
123
|
if (entries.length === 0) continue;
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
124
|
+
const built = this.#build(kind, dispatcher, entries);
|
|
125
|
+
if (!built) continue;
|
|
90
126
|
if (mode === "streaming") {
|
|
91
127
|
try {
|
|
92
|
-
this.#options.injectStreaming
|
|
128
|
+
if (!this.#options.injectStreaming) throw new Error("Streaming injection is unavailable");
|
|
129
|
+
this.#options.injectStreaming(built.message);
|
|
130
|
+
this.#resolveEntries(built.entries);
|
|
93
131
|
} catch (error) {
|
|
132
|
+
const dispatchError = error instanceof Error ? error : new Error(String(error));
|
|
133
|
+
this.#rejectEntries(built.entries, dispatchError);
|
|
94
134
|
logger.warn("Yield queue streaming dispatch failed", { kind, error: formatError(error) });
|
|
95
135
|
}
|
|
96
136
|
} else {
|
|
97
|
-
idleMessages.push(
|
|
137
|
+
idleMessages.push(built);
|
|
98
138
|
}
|
|
99
139
|
}
|
|
100
140
|
if (mode === "idle" && idleMessages.length > 0) {
|
|
141
|
+
for (const item of idleMessages) this.#attachEntrySettlement(item);
|
|
101
142
|
try {
|
|
102
|
-
await this.#options.injectIdle(idleMessages);
|
|
143
|
+
await this.#options.injectIdle(idleMessages.map(item => item.message));
|
|
144
|
+
for (const item of idleMessages) {
|
|
145
|
+
(item.message as AgentMessage & { [ASIDE_MESSAGE_COMMIT]?: () => void })[ASIDE_MESSAGE_COMMIT]?.();
|
|
146
|
+
}
|
|
103
147
|
} catch (error) {
|
|
148
|
+
const dispatchError = error instanceof Error ? error : new Error(String(error));
|
|
149
|
+
for (const item of idleMessages) {
|
|
150
|
+
(item.message as AgentMessage & { [ASIDE_MESSAGE_DISCARD]?: (error: Error) => void })[
|
|
151
|
+
ASIDE_MESSAGE_DISCARD
|
|
152
|
+
]?.(dispatchError);
|
|
153
|
+
}
|
|
104
154
|
logger.warn("Yield queue idle dispatch failed", { error: formatError(error) });
|
|
105
155
|
}
|
|
106
156
|
}
|
|
@@ -119,7 +169,12 @@ export class YieldQueue {
|
|
|
119
169
|
for (const [kind, dispatcher] of this.#dispatchers) {
|
|
120
170
|
const entries = this.#drain(kind);
|
|
121
171
|
if (entries.length === 0) continue;
|
|
122
|
-
thunks.push(() =>
|
|
172
|
+
thunks.push(() => {
|
|
173
|
+
const built = this.#build(kind, dispatcher, entries);
|
|
174
|
+
if (!built) return null;
|
|
175
|
+
this.#attachEntrySettlement(built);
|
|
176
|
+
return built.message;
|
|
177
|
+
});
|
|
123
178
|
}
|
|
124
179
|
return thunks;
|
|
125
180
|
}
|
|
@@ -127,14 +182,22 @@ export class YieldQueue {
|
|
|
127
182
|
/** Drop queued entries. With `kind`, drop only that kind's entries (leaving
|
|
128
183
|
* any pending idle-flush for other kinds intact); otherwise drop everything. */
|
|
129
184
|
clear(kind?: string): void {
|
|
185
|
+
const error = new Error("Yield queue entry cleared before dispatch");
|
|
130
186
|
if (kind !== undefined) {
|
|
187
|
+
this.#rejectEntries(this.#entries.get(kind) ?? [], error);
|
|
131
188
|
this.#entries.delete(kind);
|
|
132
189
|
return;
|
|
133
190
|
}
|
|
191
|
+
for (const entries of this.#entries.values()) this.#rejectEntries(entries, error);
|
|
134
192
|
this.#entries.clear();
|
|
135
193
|
this.#idleFlushPending = false;
|
|
136
194
|
}
|
|
137
195
|
|
|
196
|
+
/** Clear a scheduled-flush latch when its host task is cancelled before running. */
|
|
197
|
+
cancelIdleFlushScheduling(): void {
|
|
198
|
+
this.#idleFlushPending = false;
|
|
199
|
+
}
|
|
200
|
+
|
|
138
201
|
#scheduleIdleFlush(): void {
|
|
139
202
|
if (this.#idleFlushPending) return;
|
|
140
203
|
this.#idleFlushPending = true;
|
|
@@ -150,34 +213,76 @@ export class YieldQueue {
|
|
|
150
213
|
}
|
|
151
214
|
}
|
|
152
215
|
|
|
153
|
-
#drain(kind: string):
|
|
216
|
+
#drain(kind: string): StoredEntry[] {
|
|
154
217
|
const entries = this.#entries.get(kind);
|
|
155
218
|
if (!entries || entries.length === 0) return [];
|
|
156
219
|
this.#entries.delete(kind);
|
|
157
220
|
return entries;
|
|
158
221
|
}
|
|
159
222
|
|
|
160
|
-
#build(kind: string, dispatcher: StoredDispatcher, entries:
|
|
161
|
-
const survivors:
|
|
223
|
+
#build(kind: string, dispatcher: StoredDispatcher, entries: StoredEntry[]): BuiltMessage | null {
|
|
224
|
+
const survivors: StoredEntry[] = [];
|
|
162
225
|
for (const entry of entries) {
|
|
163
226
|
if (dispatcher.isStale) {
|
|
164
227
|
let stale: boolean;
|
|
165
228
|
try {
|
|
166
|
-
stale = dispatcher.isStale(entry);
|
|
229
|
+
stale = dispatcher.isStale(entry.value);
|
|
167
230
|
} catch (error) {
|
|
231
|
+
const staleError = error instanceof Error ? error : new Error(String(error));
|
|
232
|
+
entry.reject?.(staleError);
|
|
168
233
|
logger.warn("Yield queue stale check failed", { kind, error: formatError(error) });
|
|
169
234
|
continue;
|
|
170
235
|
}
|
|
171
|
-
if (stale)
|
|
236
|
+
if (stale) {
|
|
237
|
+
entry.reject?.(new Error(`Yield queue entry became stale: ${kind}`));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
172
240
|
}
|
|
173
241
|
survivors.push(entry);
|
|
174
242
|
}
|
|
175
243
|
if (survivors.length === 0) return null;
|
|
176
244
|
try {
|
|
177
|
-
|
|
245
|
+
const message = dispatcher.build(survivors.map(entry => entry.value));
|
|
246
|
+
if (!message) {
|
|
247
|
+
this.#rejectEntries(survivors, new Error(`Yield queue dispatcher skipped entry: ${kind}`));
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return { message, entries: survivors };
|
|
178
251
|
} catch (error) {
|
|
252
|
+
const buildError = error instanceof Error ? error : new Error(String(error));
|
|
253
|
+
this.#rejectEntries(survivors, buildError);
|
|
179
254
|
logger.warn("Yield queue build failed", { kind, error: formatError(error) });
|
|
180
255
|
return null;
|
|
181
256
|
}
|
|
182
257
|
}
|
|
258
|
+
|
|
259
|
+
#attachEntrySettlement(built: BuiltMessage): void {
|
|
260
|
+
let settled = false;
|
|
261
|
+
Object.defineProperties(built.message, {
|
|
262
|
+
[ASIDE_MESSAGE_COMMIT]: {
|
|
263
|
+
configurable: true,
|
|
264
|
+
value: () => {
|
|
265
|
+
if (settled) return;
|
|
266
|
+
settled = true;
|
|
267
|
+
this.#resolveEntries(built.entries);
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
[ASIDE_MESSAGE_DISCARD]: {
|
|
271
|
+
configurable: true,
|
|
272
|
+
value: (error: Error) => {
|
|
273
|
+
if (settled) return;
|
|
274
|
+
settled = true;
|
|
275
|
+
this.#rejectEntries(built.entries, error);
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#resolveEntries(entries: StoredEntry[]): void {
|
|
282
|
+
for (const entry of entries) entry.resolve?.();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#rejectEntries(entries: StoredEntry[], error: Error): void {
|
|
286
|
+
for (const entry of entries) entry.reject?.(error);
|
|
287
|
+
}
|
|
183
288
|
}
|