@sayknow-cli/coding-agent 0.3.12 → 0.3.15
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/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/daemon-cli.d.ts +2 -0
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/malloc-env-guard.d.ts +65 -0
- package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +3 -1
- package/dist/types/cli/session-picker.d.ts +4 -3
- package/dist/types/cli/update-cli.d.ts +22 -3
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/commands/daemon.d.ts +6 -2
- package/dist/types/commands/launch.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/commands/web-search.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
- package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
- package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +3 -3
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +9 -9
- package/dist/types/config/keybindings.d.ts +134 -129
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +35 -35
- package/dist/types/config/models-config-schema.d.ts +111 -111
- package/dist/types/config/settings-schema.d.ts +56 -16
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
- package/dist/types/coordinator-mcp/policy.d.ts +3 -0
- package/dist/types/coordinator-mcp/server.d.ts +79 -6
- package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
- package/dist/types/daemon/control-types.d.ts +10 -0
- package/dist/types/daemon/operator-contract.d.ts +47 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/edit/modes/patch.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -1
- package/dist/types/goals/tools/goal-tool.d.ts +6 -6
- package/dist/types/i18n/messages/en.d.ts +8 -8
- package/dist/types/internal-urls/local-protocol.d.ts +13 -9
- package/dist/types/lsp/index.d.ts +9 -9
- package/dist/types/lsp/types.d.ts +9 -9
- package/dist/types/main.d.ts +66 -2
- package/dist/types/modes/acp/acp-agent.d.ts +19 -2
- package/dist/types/modes/acp/acp-mode.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
- package/dist/types/modes/components/pet-selector.d.ts +11 -0
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
- package/dist/types/modes/components/session-selector.d.ts +22 -9
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +0 -10
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +4 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +24 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
- package/dist/types/modes/print-mode.d.ts +3 -4
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
- package/dist/types/modes/theme/defaults/index.d.ts +568 -568
- package/dist/types/modes/types.d.ts +28 -2
- package/dist/types/modes/utils/abort-message.d.ts +1 -1
- package/dist/types/modes/utils/context-usage.d.ts +4 -19
- package/dist/types/modes/utils/irc-message.d.ts +38 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
- package/dist/types/notifications/config.d.ts +34 -0
- package/dist/types/notifications/daemon-paths.d.ts +7 -0
- package/dist/types/notifications/index.d.ts +27 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
- package/dist/types/notifications/notification-service.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
- package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
- package/dist/types/notifications/telegram-daemon.d.ts +130 -3
- package/dist/types/notifications/telegram-reference.d.ts +19 -2
- package/dist/types/notifications/topic-registry.d.ts +24 -4
- package/dist/types/registry/agent-registry.d.ts +2 -0
- package/dist/types/rlm/index.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +40 -3
- package/dist/types/session/cache-economics.d.ts +79 -0
- package/dist/types/session/context-estimation.d.ts +31 -0
- package/dist/types/session/provider-safety-stop.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +124 -4
- package/dist/types/session/session-storage.d.ts +155 -13
- package/dist/types/setup/credential-auto-import.d.ts +2 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
- package/dist/types/skc-runtime/state-schema.d.ts +19 -19
- package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
- package/dist/types/task/agents.d.ts +2 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +35 -27
- package/dist/types/tools/ask-answer-registry.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
- package/dist/types/tools/browser.d.ts +28 -28
- package/dist/types/tools/computer.d.ts +20 -20
- package/dist/types/tools/cron.d.ts +2 -2
- package/dist/types/tools/debug.d.ts +40 -40
- package/dist/types/tools/gh.d.ts +10 -10
- package/dist/types/tools/image-gen.d.ts +2 -2
- package/dist/types/tools/index.d.ts +58 -6
- package/dist/types/tools/monitor.d.ts +6 -6
- package/dist/types/tools/skill-discovery.d.ts +4 -4
- package/dist/types/tools/subagent.d.ts +12 -12
- package/dist/types/tools/telegram-send.d.ts +7 -0
- package/dist/types/tools/todo-write.d.ts +8 -8
- package/dist/types/utils/external-editor.d.ts +2 -0
- package/dist/types/web/insane/bridge.d.ts +9 -9
- package/dist/types/web/search/index.d.ts +4 -4
- package/package.json +8 -8
- package/scripts/g004-tmux-smoke.ts +196 -36
- package/src/cli/daemon-cli.ts +19 -29
- package/src/cli/malloc-env-guard.test.ts +104 -0
- package/src/cli/malloc-env-guard.ts +149 -0
- package/src/cli/notify-cli.ts +78 -14
- package/src/cli/session-picker.ts +23 -40
- package/src/cli/update-cli.ts +56 -37
- package/src/cli.ts +51 -10
- package/src/commands/daemon.ts +19 -5
- package/src/commands/harness.ts +515 -47
- package/src/commands/launch.ts +11 -3
- package/src/commands/notify.ts +5 -1
- package/src/commands/session.ts +6 -1
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-profile-activation.ts +1 -1
- package/src/config/model-profiles.ts +14 -14
- package/src/config/settings-schema.ts +41 -1
- package/src/config/settings.ts +96 -45
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/model-preset.ts +100 -0
- package/src/coordinator-mcp/policy.ts +30 -0
- package/src/coordinator-mcp/server.ts +2958 -430
- package/src/coordinator-mcp/session-reaper.ts +129 -0
- package/src/daemon/control-types.ts +11 -0
- package/src/daemon/operator-contract.ts +121 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
- package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/internal-urls/local-protocol.ts +29 -10
- package/src/main.ts +301 -80
- package/src/modes/acp/acp-agent.ts +730 -100
- package/src/modes/acp/acp-mode.ts +29 -2
- package/src/modes/components/assistant-message.ts +32 -3
- package/src/modes/components/bash-execution.ts +43 -13
- package/src/modes/components/custom-editor.ts +5 -0
- package/src/modes/components/footer.ts +3 -3
- package/src/modes/components/hook-selector.ts +12 -0
- package/src/modes/components/irc-sidebar.ts +206 -0
- package/src/modes/components/pet-selector.ts +55 -0
- package/src/modes/components/sayknow-pet-widget.ts +360 -0
- package/src/modes/components/session-selector.ts +222 -221
- package/src/modes/components/settings-selector.ts +10 -0
- package/src/modes/components/status-line/segments.ts +14 -7
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +4 -171
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/components/user-message.ts +26 -9
- package/src/modes/controllers/command-controller.ts +28 -2
- package/src/modes/controllers/event-controller.ts +159 -36
- package/src/modes/controllers/extension-ui-controller.ts +37 -18
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +62 -15
- package/src/modes/interactive-mode.ts +199 -7
- package/src/modes/irc-observation-ledger.ts +127 -0
- package/src/modes/print-mode.ts +228 -92
- package/src/modes/rpc/rpc-client.ts +58 -1
- package/src/modes/rpc/rpc-mode.ts +286 -85
- package/src/modes/rpc/rpc-types.ts +15 -1
- package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
- package/src/modes/shared/agent-wire/command-validation.ts +10 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
- package/src/modes/types.ts +34 -2
- package/src/modes/utils/context-usage.ts +56 -92
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/irc-message.ts +138 -0
- package/src/modes/utils/ui-helpers.ts +216 -50
- package/src/notifications/config.ts +44 -0
- package/src/notifications/daemon-paths.ts +8 -0
- package/src/notifications/index.ts +535 -75
- package/src/notifications/lifecycle-control-runtime.ts +930 -77
- package/src/notifications/notification-service.ts +710 -0
- package/src/notifications/rate-limit-pool.ts +46 -12
- package/src/notifications/telegram-daemon-control.ts +27 -2
- package/src/notifications/telegram-daemon.ts +635 -94
- package/src/notifications/telegram-reference.ts +73 -13
- package/src/notifications/topic-registry.ts +86 -9
- package/src/prompts/system/irc-peer-roster.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/registry/agent-registry.ts +3 -0
- package/src/sdk.ts +52 -1
- package/src/secrets/obfuscator.ts +4 -2
- package/src/session/agent-session.ts +670 -217
- package/src/session/cache-economics.ts +275 -0
- package/src/session/context-estimation.ts +91 -0
- package/src/session/provider-safety-stop.ts +9 -0
- package/src/session/session-dump-format.ts +6 -0
- package/src/session/session-manager.ts +809 -39
- package/src/session/session-storage.ts +582 -38
- package/src/skc-runtime/session-state-sidecar.ts +756 -135
- package/src/skc-runtime/tmux-common.ts +26 -7
- package/src/skc-runtime/tmux-gc.ts +52 -9
- package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
- package/src/skc-runtime/tmux-sessions.ts +871 -68
- package/src/slash-commands/builtin-registry.ts +153 -9
- package/src/slash-commands/helpers/context-report.ts +26 -4
- package/src/task/executor.ts +12 -3
- package/src/task/index.ts +8 -0
- package/src/task/receipt.ts +7 -2
- package/src/task/types.ts +26 -0
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/browser/tab-supervisor.ts +63 -6
- package/src/tools/browser.ts +8 -3
- package/src/tools/index.ts +81 -6
- package/src/tools/sqlite-reader.ts +8 -4
- package/src/tui/output-block.ts +18 -6
- package/src/utils/external-editor.ts +6 -1
|
@@ -3,7 +3,15 @@ import * as path from "node:path";
|
|
|
3
3
|
import { ThinkingLevel } from "@sayknow-cli/agent-core";
|
|
4
4
|
import { type Model, modelsAreEqual } from "@sayknow-cli/ai";
|
|
5
5
|
import { getOAuthProviders } from "@sayknow-cli/ai/utils/oauth";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
isPetMode,
|
|
8
|
+
isUnderTerminalMultiplexer,
|
|
9
|
+
PET_MODE_IDS,
|
|
10
|
+
PET_SKIN_IDS,
|
|
11
|
+
PET_SKINS,
|
|
12
|
+
Spacer,
|
|
13
|
+
Text,
|
|
14
|
+
} from "@sayknow-cli/tui";
|
|
7
15
|
import { setProjectDir } from "@sayknow-cli/utils";
|
|
8
16
|
import { jobElapsedMs } from "../async";
|
|
9
17
|
import { materializeActiveModelProfileAssignments } from "../config/model-profile-activation";
|
|
@@ -23,8 +31,20 @@ import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../
|
|
|
23
31
|
import { td } from "../i18n";
|
|
24
32
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
25
33
|
import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
34
|
+
import { SayknowPetWidget } from "../modes/components/sayknow-pet-widget";
|
|
26
35
|
import { theme } from "../modes/theme/theme";
|
|
27
36
|
import type { InteractiveModeContext } from "../modes/types";
|
|
37
|
+
import {
|
|
38
|
+
buildNotificationStatusReport,
|
|
39
|
+
checkNotificationHealth,
|
|
40
|
+
formatNotificationHealthReport,
|
|
41
|
+
formatNotificationRecoveryReport,
|
|
42
|
+
formatNotificationStatusReport,
|
|
43
|
+
formatNotificationTestResult,
|
|
44
|
+
recoverNotifications,
|
|
45
|
+
sendNotificationTest,
|
|
46
|
+
} from "../notifications/notification-service";
|
|
47
|
+
import { computeCacheMissCostSummary, formatCacheMissSummaryLines } from "../session/cache-economics";
|
|
28
48
|
import { formatModelOnboardingGuidance } from "../setup/model-onboarding-guidance";
|
|
29
49
|
import {
|
|
30
50
|
addApiCompatibleProvider,
|
|
@@ -37,7 +57,7 @@ import { getDisplayChangelogEntries } from "../utils/changelog";
|
|
|
37
57
|
import { buildContextReportText } from "./helpers/context-report";
|
|
38
58
|
import { buildFastStatusReport } from "./helpers/fast-status-report";
|
|
39
59
|
import { formatDuration } from "./helpers/format";
|
|
40
|
-
import { commandConsumed, errorMessage, parseSlashCommand, usage } from "./helpers/parse";
|
|
60
|
+
import { commandConsumed, errorMessage, parseSlashCommand, parseSubcommand, usage } from "./helpers/parse";
|
|
41
61
|
import { handleSshAcp } from "./helpers/ssh";
|
|
42
62
|
import { buildUsageReportText } from "./helpers/usage-report";
|
|
43
63
|
import type {
|
|
@@ -471,6 +491,66 @@ const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashComma
|
|
|
471
491
|
};
|
|
472
492
|
|
|
473
493
|
const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
494
|
+
{
|
|
495
|
+
name: "notify",
|
|
496
|
+
priority: 30,
|
|
497
|
+
description: "Notification status, health, test, recovery, and session on/off",
|
|
498
|
+
acpDescription: "Notification status, health, test, recovery, and session on/off",
|
|
499
|
+
subcommands: [
|
|
500
|
+
{ name: "on", description: "Enable notifications for this session" },
|
|
501
|
+
{ name: "off", description: "Disable notifications for this session" },
|
|
502
|
+
{ name: "status", description: "Show notification configuration (no secrets)" },
|
|
503
|
+
{ name: "health", description: "Config, daemon-ownership and endpoint health" },
|
|
504
|
+
{ name: "test", description: "Send a test notification", usage: "[message]" },
|
|
505
|
+
{ name: "recovery", description: "Clear dead-owner locks and stale endpoint files" },
|
|
506
|
+
{ name: "setup", description: "How to pair a Telegram bot (run in a terminal)" },
|
|
507
|
+
],
|
|
508
|
+
inlineHint: "[on|off|status|health|test|recovery|setup]",
|
|
509
|
+
acpInputHint: "[on|off|status|health|test|recovery|setup]",
|
|
510
|
+
allowArgs: true,
|
|
511
|
+
handle: async (command, runtime) => {
|
|
512
|
+
const { verb, rest } = parseSubcommand(command.args);
|
|
513
|
+
const action = verb || "status";
|
|
514
|
+
// `on`/`off` are session-local runtime controls owned by the
|
|
515
|
+
// notifications extension command (`api.registerCommand("notify")`),
|
|
516
|
+
// which holds the live per-session server/disable state. Pass them
|
|
517
|
+
// through untouched — never consume them — so this builtin cannot
|
|
518
|
+
// shadow that control. Everything below is config/service diagnostics
|
|
519
|
+
// the extension does not implement, so the builtin owns them exclusively
|
|
520
|
+
// (and the extension therefore never consumes them).
|
|
521
|
+
if (action === "on" || action === "off") {
|
|
522
|
+
return { prompt: command.text };
|
|
523
|
+
}
|
|
524
|
+
const stateRoot = path.join(runtime.cwd, ".skc", "state");
|
|
525
|
+
switch (action) {
|
|
526
|
+
case "status":
|
|
527
|
+
await runtime.output(formatNotificationStatusReport(buildNotificationStatusReport(runtime.settings)));
|
|
528
|
+
return commandConsumed();
|
|
529
|
+
case "health": {
|
|
530
|
+
const report = await checkNotificationHealth({ settings: runtime.settings, stateRoot });
|
|
531
|
+
await runtime.output(formatNotificationHealthReport(report));
|
|
532
|
+
return commandConsumed();
|
|
533
|
+
}
|
|
534
|
+
case "test": {
|
|
535
|
+
const result = await sendNotificationTest({ settings: runtime.settings, text: rest || undefined });
|
|
536
|
+
await runtime.output(formatNotificationTestResult(result));
|
|
537
|
+
return commandConsumed();
|
|
538
|
+
}
|
|
539
|
+
case "recovery": {
|
|
540
|
+
const report = await recoverNotifications({ settings: runtime.settings, stateRoot });
|
|
541
|
+
await runtime.output(formatNotificationRecoveryReport(report));
|
|
542
|
+
return commandConsumed();
|
|
543
|
+
}
|
|
544
|
+
case "setup":
|
|
545
|
+
return usage(
|
|
546
|
+
"Run `skc notify setup` in a terminal to pair a Telegram bot token with a private chat (interactive; requires a TTY).",
|
|
547
|
+
runtime,
|
|
548
|
+
);
|
|
549
|
+
default:
|
|
550
|
+
return usage(`Usage: /notify [on|off|status|health|test|recovery|setup] (got "${action}")`, runtime);
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
},
|
|
474
554
|
{
|
|
475
555
|
name: "settings",
|
|
476
556
|
priority: 40,
|
|
@@ -488,6 +568,41 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
488
568
|
runtime.ctx.editor.setText("");
|
|
489
569
|
},
|
|
490
570
|
},
|
|
571
|
+
{
|
|
572
|
+
name: "pet",
|
|
573
|
+
description: "Sayknow pet living beside the composer",
|
|
574
|
+
subcommands: [
|
|
575
|
+
{ name: "off", description: "Hide the pet" },
|
|
576
|
+
...PET_SKIN_IDS.map(id => ({ name: id, description: PET_SKINS[id].description })),
|
|
577
|
+
],
|
|
578
|
+
inlineHint: `[${PET_MODE_IDS.join("|")}]`,
|
|
579
|
+
allowArgs: true,
|
|
580
|
+
handleTui: (command, runtime) => {
|
|
581
|
+
const ctx = runtime.ctx;
|
|
582
|
+
const raw = command.args?.trim().toLowerCase() ?? "";
|
|
583
|
+
const arg = raw === "on" ? "red" : raw;
|
|
584
|
+
if (!arg) {
|
|
585
|
+
ctx.showPetSelector();
|
|
586
|
+
ctx.editor.setText("");
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (arg !== "off" && !SayknowPetWidget.pixelProtocol()) {
|
|
590
|
+
ctx.showStatus(
|
|
591
|
+
isUnderTerminalMultiplexer()
|
|
592
|
+
? "Sayknow pet: graphics are suppressed inside tmux/screen/zellij — escapes are not forwarded end-to-end. Run skc outside the multiplexer in a sixel/kitty-graphics terminal, or set PI_FORCE_IMAGE_PROTOCOL=sixel when your multiplexer+client chain renders sixel."
|
|
593
|
+
: "Sayknow pet needs a sixel/kitty-graphics terminal (Windows Terminal 1.22+, kitty, Ghostty, WezTerm)",
|
|
594
|
+
{ dim: true },
|
|
595
|
+
);
|
|
596
|
+
} else if (isPetMode(arg)) {
|
|
597
|
+
ctx.setPetMode(arg);
|
|
598
|
+
const name = arg === "off" ? "Sayknow pet hidden" : `${PET_SKINS[arg].label} is here`;
|
|
599
|
+
ctx.showStatus(name);
|
|
600
|
+
} else {
|
|
601
|
+
ctx.showStatus(`Usage: /pet [${PET_MODE_IDS.join("|")}]`, { dim: true });
|
|
602
|
+
}
|
|
603
|
+
ctx.editor.setText("");
|
|
604
|
+
},
|
|
605
|
+
},
|
|
491
606
|
{
|
|
492
607
|
name: "goal",
|
|
493
608
|
priority: 84,
|
|
@@ -821,13 +936,42 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
821
936
|
allowArgs: true,
|
|
822
937
|
handle: async (command, runtime) => {
|
|
823
938
|
if (!command.args || command.args === "info") {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
939
|
+
const stats = runtime.session.getSessionStats();
|
|
940
|
+
const lines = [
|
|
941
|
+
`Session: ${runtime.session.sessionId}`,
|
|
942
|
+
`Title: ${runtime.session.sessionName}`,
|
|
943
|
+
`CWD: ${runtime.cwd}`,
|
|
944
|
+
"",
|
|
945
|
+
"Tokens",
|
|
946
|
+
`Input: ${stats.tokens.input.toLocaleString()}`,
|
|
947
|
+
`Output: ${stats.tokens.output.toLocaleString()}`,
|
|
948
|
+
];
|
|
949
|
+
if (stats.tokens.cacheRead > 0) {
|
|
950
|
+
lines.push(`Cache Read: ${stats.tokens.cacheRead.toLocaleString()}`);
|
|
951
|
+
}
|
|
952
|
+
if (stats.tokens.cacheWrite > 0) {
|
|
953
|
+
lines.push(`Cache Write: ${stats.tokens.cacheWrite.toLocaleString()}`);
|
|
954
|
+
}
|
|
955
|
+
lines.push(`Total: ${stats.tokens.total.toLocaleString()}`);
|
|
956
|
+
if (stats.cost > 0 || stats.premiumRequests > 0) {
|
|
957
|
+
lines.push("", "Cost");
|
|
958
|
+
if (stats.cost > 0) {
|
|
959
|
+
lines.push(`Total: ${stats.cost.toFixed(4)}`);
|
|
960
|
+
}
|
|
961
|
+
if (stats.premiumRequests > 0) {
|
|
962
|
+
lines.push(`Premium Requests: ${stats.premiumRequests.toLocaleString()}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const cacheMissSummary = stats.costBreakdown
|
|
966
|
+
? computeCacheMissCostSummary(stats.tokens, {
|
|
967
|
+
kind: "persisted-aggregate",
|
|
968
|
+
costBreakdown: stats.costBreakdown,
|
|
969
|
+
})
|
|
970
|
+
: undefined;
|
|
971
|
+
if (cacheMissSummary) {
|
|
972
|
+
lines.push("", "Cache Miss Cost", ...formatCacheMissSummaryLines(cacheMissSummary));
|
|
973
|
+
}
|
|
974
|
+
await runtime.output(lines.join("\n"));
|
|
831
975
|
return commandConsumed();
|
|
832
976
|
}
|
|
833
977
|
if (command.args === "delete") {
|
|
@@ -83,6 +83,12 @@ function formatReserveText(runtime: SlashCommandRuntime, contextWindow: number,
|
|
|
83
83
|
return "unknown";
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function contextUsageSourceLabel(source: "provider_anchor" | "heuristic" | "unknown"): string {
|
|
87
|
+
if (source === "provider_anchor") return "provider-reported";
|
|
88
|
+
if (source === "unknown") return "estimated; exact count unknown until next response";
|
|
89
|
+
return "estimated";
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
/**
|
|
87
93
|
* Build the `/context` ACP-mode text. Tries the rich breakdown first
|
|
88
94
|
* (categories + auto-compact buffer + free slack) and falls back to the
|
|
@@ -96,11 +102,21 @@ export function buildContextReportText(runtime: SlashCommandRuntime): string {
|
|
|
96
102
|
return "Context usage is unavailable: no model is selected for this session.";
|
|
97
103
|
}
|
|
98
104
|
const promptUsage = findLastAssistantUsage(history.activeMessages);
|
|
99
|
-
const
|
|
105
|
+
const activeContext =
|
|
106
|
+
breakdown.usedTokens === null
|
|
107
|
+
? `Active context: unknown / ${breakdown.contextWindow.toLocaleString()} tokens (exact count unknown until next response) (${contextUsageSourceLabel(breakdown.source)})`
|
|
108
|
+
: `Active context: ${breakdown.usedTokens.toLocaleString()} / ${breakdown.contextWindow.toLocaleString()} tokens (${((breakdown.usedTokens / breakdown.contextWindow) * 100).toFixed(1)}% used) (${contextUsageSourceLabel(breakdown.source)})`;
|
|
100
109
|
const lines = [
|
|
101
110
|
"Context usage",
|
|
102
111
|
`Model: ${breakdown.model?.provider ?? "unknown"}/${breakdown.model?.id ?? "unknown"}`,
|
|
103
|
-
|
|
112
|
+
activeContext,
|
|
113
|
+
...(breakdown.source !== "unknown" &&
|
|
114
|
+
breakdown.usedTokens !== null &&
|
|
115
|
+
breakdown.estimatedCategoryTotal !== breakdown.usedTokens
|
|
116
|
+
? [
|
|
117
|
+
`Estimated category total: ${breakdown.estimatedCategoryTotal.toLocaleString()} tokens (composition below is estimated)`,
|
|
118
|
+
]
|
|
119
|
+
: []),
|
|
104
120
|
`Reserve: ${formatReserveText(runtime, breakdown.contextWindow, breakdown.autoCompactBufferTokens)}`,
|
|
105
121
|
"",
|
|
106
122
|
"Active context breakdown (estimated)",
|
|
@@ -111,7 +127,13 @@ export function buildContextReportText(runtime: SlashCommandRuntime): string {
|
|
|
111
127
|
if (breakdown.autoCompactBufferTokens > 0) {
|
|
112
128
|
lines.push(formatTokenLine("Reserve", breakdown.autoCompactBufferTokens, breakdown.contextWindow));
|
|
113
129
|
}
|
|
114
|
-
lines.push(
|
|
130
|
+
lines.push(
|
|
131
|
+
formatTokenLine(
|
|
132
|
+
breakdown.usedTokens === null ? "Free (estimated)" : "Free",
|
|
133
|
+
breakdown.freeTokens,
|
|
134
|
+
breakdown.contextWindow,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
115
137
|
lines.push(
|
|
116
138
|
"",
|
|
117
139
|
"History",
|
|
@@ -141,7 +163,7 @@ export function buildContextReportText(runtime: SlashCommandRuntime): string {
|
|
|
141
163
|
if (!fallback) return "Context usage is unavailable.";
|
|
142
164
|
return [
|
|
143
165
|
"Context usage",
|
|
144
|
-
`Active context: ${fallback.tokens === null
|
|
166
|
+
`Active context: ${fallback.tokens === null ? "unknown" : fallback.tokens.toLocaleString()} (${contextUsageSourceLabel(fallback.source)})`,
|
|
145
167
|
`Context window: ${fallback.contextWindow.toLocaleString()}`,
|
|
146
168
|
"Breakdown: unknown",
|
|
147
169
|
].join("\n");
|
package/src/task/executor.ts
CHANGED
|
@@ -47,6 +47,7 @@ import { persistTaskTokenLog, taskTokenLogFromUsage } from "./token-log";
|
|
|
47
47
|
import {
|
|
48
48
|
type AgentDefinition,
|
|
49
49
|
type AgentProgress,
|
|
50
|
+
hasCompleteUsageCostBreakdown,
|
|
50
51
|
MAX_OUTPUT_BYTES,
|
|
51
52
|
MAX_OUTPUT_LINES,
|
|
52
53
|
type ModelSubstitutionWarning,
|
|
@@ -86,8 +87,8 @@ function normalizeModelPatterns(value: string | string[] | undefined): string[]
|
|
|
86
87
|
.filter(Boolean);
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
function renderIrcPeerRoster(selfId: string): string {
|
|
90
|
-
const peers =
|
|
90
|
+
function renderIrcPeerRoster(agentRegistry: AgentRegistry, selfId: string): string {
|
|
91
|
+
const peers = agentRegistry
|
|
91
92
|
.list()
|
|
92
93
|
.filter(ref => ref.id !== selfId && (ref.status === "running" || ref.status === "idle"));
|
|
93
94
|
if (peers.length === 0) return "- (no other live agents)";
|
|
@@ -151,6 +152,8 @@ export interface ExecutorOptions {
|
|
|
151
152
|
authStorage?: AuthStorage;
|
|
152
153
|
modelRegistry?: ModelRegistry;
|
|
153
154
|
settings?: Settings;
|
|
155
|
+
/** Parent session's registry; shared by child sessions for IRC routing and roster visibility. */
|
|
156
|
+
agentRegistry?: AgentRegistry;
|
|
154
157
|
/**
|
|
155
158
|
* Live service-tier intent of the parent session (`AgentSession.serviceTier`),
|
|
156
159
|
* used as the inherited tier when `task.serviceTier === "inherit"`. Passing the
|
|
@@ -705,6 +708,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
705
708
|
: agent.spawns.join(",");
|
|
706
709
|
|
|
707
710
|
const lspEnabled = enableLsp ?? true;
|
|
711
|
+
const agentRegistry = options.agentRegistry ?? AgentRegistry.global();
|
|
708
712
|
const ircEnabled = options.ircAvailable === true;
|
|
709
713
|
const contextFileForPrompt = ircEnabled ? undefined : options.contextFile;
|
|
710
714
|
const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("eval");
|
|
@@ -743,6 +747,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
743
747
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
744
748
|
};
|
|
745
749
|
let hasUsage = false;
|
|
750
|
+
let usageCostBreakdownComplete = true;
|
|
746
751
|
|
|
747
752
|
const requestAbort = (reason: AbortReason) => {
|
|
748
753
|
if (reason === "timeout") {
|
|
@@ -1109,6 +1114,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1109
1114
|
const usageRecord = messageUsage as Record<string, unknown>;
|
|
1110
1115
|
const costRecord = (messageUsage as { cost?: Record<string, unknown> }).cost;
|
|
1111
1116
|
hasUsage = true;
|
|
1117
|
+
usageCostBreakdownComplete &&= hasCompleteUsageCostBreakdown(usageRecord);
|
|
1112
1118
|
accumulatedUsage.input += getNumberField(usageRecord, "input") ?? 0;
|
|
1113
1119
|
accumulatedUsage.output += getNumberField(usageRecord, "output") ?? 0;
|
|
1114
1120
|
accumulatedUsage.cacheRead += getNumberField(usageRecord, "cacheRead") ?? 0;
|
|
@@ -1382,7 +1388,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1382
1388
|
worktree: worktree ?? "",
|
|
1383
1389
|
outputSchema: normalizedOutputSchema,
|
|
1384
1390
|
contextFile: contextFileForPrompt,
|
|
1385
|
-
ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
|
|
1391
|
+
ircPeers: ircEnabled ? renderIrcPeerRoster(agentRegistry, id) : "",
|
|
1386
1392
|
ircSelfId: ircEnabled ? id : "",
|
|
1387
1393
|
forkContext: forkContextNotice,
|
|
1388
1394
|
});
|
|
@@ -1411,6 +1417,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1411
1417
|
parentTaskPrefix: id,
|
|
1412
1418
|
agentId: id,
|
|
1413
1419
|
agentDisplayName: agent.name,
|
|
1420
|
+
agentRosterLabel: options.description,
|
|
1414
1421
|
bashAllowedPrefixes: agent.bashAllowedPrefixes,
|
|
1415
1422
|
enableLsp: lspEnabled,
|
|
1416
1423
|
skipPythonPreflight,
|
|
@@ -1418,6 +1425,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1418
1425
|
localProtocolOptions: options.localProtocolOptions,
|
|
1419
1426
|
telemetry: subagentTelemetry,
|
|
1420
1427
|
forkContextSeed: options.forkContextSeed,
|
|
1428
|
+
agentRegistry,
|
|
1421
1429
|
shouldPause: () => pauseRequested,
|
|
1422
1430
|
}),
|
|
1423
1431
|
);
|
|
@@ -1866,6 +1874,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1866
1874
|
abortReason: finalAbortReason,
|
|
1867
1875
|
paused,
|
|
1868
1876
|
usage: hasUsage ? accumulatedUsage : undefined,
|
|
1877
|
+
usageCostBreakdownComplete: hasUsage && usageCostBreakdownComplete ? true : undefined,
|
|
1869
1878
|
outputPath,
|
|
1870
1879
|
extractedToolData: progress.extractedToolData,
|
|
1871
1880
|
retryFailure: progress.retryFailure,
|
package/src/task/index.ts
CHANGED
|
@@ -34,12 +34,14 @@ import {
|
|
|
34
34
|
type ForkContextMode,
|
|
35
35
|
type ForkContextPolicy,
|
|
36
36
|
getTaskSchema,
|
|
37
|
+
hasCompleteAggregateUsageCostBreakdown,
|
|
37
38
|
type SingleResult,
|
|
38
39
|
type TaskItem,
|
|
39
40
|
type TaskParams,
|
|
40
41
|
type TaskToolDetails,
|
|
41
42
|
type TaskToolSchemaInstance,
|
|
42
43
|
} from "./types";
|
|
44
|
+
|
|
43
45
|
// Import review tools for side effects (registers subagent tool handlers)
|
|
44
46
|
import "../tools/review";
|
|
45
47
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
@@ -53,6 +55,7 @@ import { getTaskIdValidationError, validateAllocatedTaskId } from "./id";
|
|
|
53
55
|
import { AgentOutputManager } from "./output-manager";
|
|
54
56
|
import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
|
|
55
57
|
import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary } from "./receipt";
|
|
58
|
+
|
|
56
59
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
57
60
|
import { reconcileSpawnRoi } from "./roi-reconciliation";
|
|
58
61
|
import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
|
|
@@ -1316,6 +1319,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1316
1319
|
},
|
|
1317
1320
|
authStorage: this.session.authStorage,
|
|
1318
1321
|
modelRegistry: this.session.modelRegistry,
|
|
1322
|
+
agentRegistry: this.session.agentRegistry,
|
|
1319
1323
|
settings: this.session.settings,
|
|
1320
1324
|
inheritedServiceTier: this.session.serviceTier,
|
|
1321
1325
|
contextFiles,
|
|
@@ -1379,6 +1383,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1379
1383
|
},
|
|
1380
1384
|
authStorage: this.session.authStorage,
|
|
1381
1385
|
modelRegistry: this.session.modelRegistry,
|
|
1386
|
+
agentRegistry: this.session.agentRegistry,
|
|
1382
1387
|
settings: this.session.settings,
|
|
1383
1388
|
inheritedServiceTier: this.session.serviceTier,
|
|
1384
1389
|
contextFiles,
|
|
@@ -1529,6 +1534,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1529
1534
|
// Aggregate usage from executor results (already accumulated incrementally)
|
|
1530
1535
|
const aggregatedUsage = createUsageTotals();
|
|
1531
1536
|
let hasAggregatedUsage = false;
|
|
1537
|
+
|
|
1532
1538
|
for (const result of results) {
|
|
1533
1539
|
if (result.usage) {
|
|
1534
1540
|
addUsageTotals(aggregatedUsage, result.usage);
|
|
@@ -1734,6 +1740,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1734
1740
|
results: receipts,
|
|
1735
1741
|
totalDurationMs: totalDuration,
|
|
1736
1742
|
usage: hasAggregatedUsage ? aggregatedUsage : undefined,
|
|
1743
|
+
usageCostBreakdownComplete:
|
|
1744
|
+
hasAggregatedUsage && hasCompleteAggregateUsageCostBreakdown(results) ? true : undefined,
|
|
1737
1745
|
forkContextClonedTokens: forkContextClonedTokens > 0 ? forkContextClonedTokens : undefined,
|
|
1738
1746
|
roiSummary,
|
|
1739
1747
|
roiReconciliation,
|
package/src/task/receipt.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
1
|
+
import { hasCompleteUsageCostBreakdown, type SingleResult, type TaskToolDetails } from "./types";
|
|
3
2
|
export interface TaskRoi {
|
|
4
3
|
tokens: number;
|
|
5
4
|
contextTokens?: number;
|
|
@@ -32,6 +31,7 @@ export interface TaskResultReceipt {
|
|
|
32
31
|
modelSubstitutionWarning?: SingleResult["modelSubstitutionWarning"];
|
|
33
32
|
usage?: SingleResult["usage"];
|
|
34
33
|
cost?: number;
|
|
34
|
+
usageCostBreakdownComplete?: true;
|
|
35
35
|
branchName?: string;
|
|
36
36
|
retryFailure?: { attempt: number; errorSummary: string };
|
|
37
37
|
errorSummary?: string;
|
|
@@ -246,6 +246,8 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
|
|
|
246
246
|
modelSubstitutionWarning: raw.modelSubstitutionWarning,
|
|
247
247
|
usage: raw.usage,
|
|
248
248
|
cost: raw.usage?.cost.total,
|
|
249
|
+
usageCostBreakdownComplete:
|
|
250
|
+
raw.usageCostBreakdownComplete === true && hasCompleteUsageCostBreakdown(raw.usage) ? true : undefined,
|
|
249
251
|
branchName: raw.branchName,
|
|
250
252
|
retryFailure: raw.retryFailure
|
|
251
253
|
? { attempt: raw.retryFailure.attempt, errorSummary: "Retry failure recorded." }
|
|
@@ -274,6 +276,7 @@ export interface RawTaskToolDetails {
|
|
|
274
276
|
results: SingleResult[];
|
|
275
277
|
totalDurationMs: number;
|
|
276
278
|
usage?: TaskToolDetails["usage"];
|
|
279
|
+
usageCostBreakdownComplete?: TaskToolDetails["usageCostBreakdownComplete"];
|
|
277
280
|
async?: TaskToolDetails["async"];
|
|
278
281
|
forkContextClonedTokens?: number;
|
|
279
282
|
roiSummary?: TaskToolDetails["roiSummary"];
|
|
@@ -286,6 +289,8 @@ export function sanitizeTaskToolDetails(raw: RawTaskToolDetails): TaskToolDetail
|
|
|
286
289
|
results: raw.results.map(buildTaskReceipt),
|
|
287
290
|
totalDurationMs: raw.totalDurationMs,
|
|
288
291
|
usage: raw.usage,
|
|
292
|
+
usageCostBreakdownComplete:
|
|
293
|
+
raw.usageCostBreakdownComplete === true && hasCompleteUsageCostBreakdown(raw.usage) ? true : undefined,
|
|
289
294
|
forkContextClonedTokens: raw.forkContextClonedTokens,
|
|
290
295
|
roiSummary: raw.roiSummary ?? buildTaskRoiSummary(raw.results.map(buildTaskReceipt)),
|
|
291
296
|
async: raw.async,
|
package/src/task/types.ts
CHANGED
|
@@ -320,6 +320,8 @@ export interface SingleResult {
|
|
|
320
320
|
paused?: boolean;
|
|
321
321
|
/** Aggregated usage from the subprocess, accumulated incrementally from message_end events. */
|
|
322
322
|
usage?: Usage;
|
|
323
|
+
/** True only when every usage-contributing assistant supplied a complete, non-negative raw cost breakdown. */
|
|
324
|
+
usageCostBreakdownComplete?: true;
|
|
323
325
|
/** Output path for the task result */
|
|
324
326
|
outputPath?: string;
|
|
325
327
|
/** Patch path for isolated worktree output */
|
|
@@ -353,6 +355,28 @@ export interface SingleResult {
|
|
|
353
355
|
forkContextAdvisory?: { recommendedMode: ForkContextMode; reasons: string[] };
|
|
354
356
|
}
|
|
355
357
|
|
|
358
|
+
/** True only for complete, factual five-bucket cost accounting. */
|
|
359
|
+
export function hasCompleteUsageCostBreakdown(usage: unknown): boolean {
|
|
360
|
+
if (!usage || typeof usage !== "object") return false;
|
|
361
|
+
const cost = (usage as { cost?: unknown }).cost;
|
|
362
|
+
if (!cost || typeof cost !== "object") return false;
|
|
363
|
+
return ["input", "output", "cacheRead", "cacheWrite", "total"].every(key => {
|
|
364
|
+
const value = (cost as Record<string, unknown>)[key];
|
|
365
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** True only when every usage-contributing result has explicit, complete cost provenance. */
|
|
370
|
+
export function hasCompleteAggregateUsageCostBreakdown(
|
|
371
|
+
results: readonly Pick<SingleResult, "usage" | "usageCostBreakdownComplete">[],
|
|
372
|
+
): boolean {
|
|
373
|
+
return results.every(
|
|
374
|
+
result =>
|
|
375
|
+
result.usage === undefined ||
|
|
376
|
+
(result.usageCostBreakdownComplete === true && hasCompleteUsageCostBreakdown(result.usage)),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
356
380
|
/** Tool details for TUI rendering */
|
|
357
381
|
export interface TaskToolDetails {
|
|
358
382
|
projectAgentsDir: string | null;
|
|
@@ -360,6 +384,8 @@ export interface TaskToolDetails {
|
|
|
360
384
|
totalDurationMs: number;
|
|
361
385
|
/** Aggregated usage across all subagents. */
|
|
362
386
|
usage?: Usage;
|
|
387
|
+
/** True only when every usage-contributing subagent supplied a complete raw cost breakdown. */
|
|
388
|
+
usageCostBreakdownComplete?: true;
|
|
363
389
|
/** Aggregate cloned tokens copied into fork-context seeds across subagents. */
|
|
364
390
|
forkContextClonedTokens?: number;
|
|
365
391
|
roiSummary?: {
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* source; registering returns a disposer.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { WorkflowGateEmitter } from "../modes/shared/agent-wire/unattended-session";
|
|
10
11
|
import type { AskAnswerSource } from "./index";
|
|
11
12
|
|
|
12
13
|
const sources = new Map<string, AskAnswerSource>();
|
|
14
|
+
const workflowGateEmitters = new Map<string, WorkflowGateEmitter>();
|
|
15
|
+
const workflowGateListeners = new Map<string, Set<(emitter: WorkflowGateEmitter | undefined) => void>>();
|
|
13
16
|
|
|
14
17
|
/** Register `source` for `sessionId`. Returns a disposer that clears it. */
|
|
15
18
|
export function registerAskAnswerSource(sessionId: string, source: AskAnswerSource): () => void {
|
|
@@ -23,3 +26,25 @@ export function registerAskAnswerSource(sessionId: string, source: AskAnswerSour
|
|
|
23
26
|
export function getAskAnswerSource(sessionId: string): AskAnswerSource | undefined {
|
|
24
27
|
return sources.get(sessionId);
|
|
25
28
|
}
|
|
29
|
+
|
|
30
|
+
/** Publish a session's current workflow-gate emitter after mode initialization. */
|
|
31
|
+
export function notifyWorkflowGateEmitterChanged(sessionId: string, emitter: WorkflowGateEmitter | undefined): void {
|
|
32
|
+
if (emitter) workflowGateEmitters.set(sessionId, emitter);
|
|
33
|
+
else workflowGateEmitters.delete(sessionId);
|
|
34
|
+
for (const listener of workflowGateListeners.get(sessionId) ?? []) listener(emitter);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Observe workflow-gate emitter installation even when it occurs after session_start. */
|
|
38
|
+
export function registerWorkflowGateEmitterListener(
|
|
39
|
+
sessionId: string,
|
|
40
|
+
listener: (emitter: WorkflowGateEmitter | undefined) => void,
|
|
41
|
+
): () => void {
|
|
42
|
+
const listeners = workflowGateListeners.get(sessionId) ?? new Set();
|
|
43
|
+
listeners.add(listener);
|
|
44
|
+
workflowGateListeners.set(sessionId, listeners);
|
|
45
|
+
listener(workflowGateEmitters.get(sessionId));
|
|
46
|
+
return () => {
|
|
47
|
+
listeners.delete(listener);
|
|
48
|
+
if (listeners.size === 0) workflowGateListeners.delete(sessionId);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -89,11 +89,57 @@ export interface RunInTabOptions {
|
|
|
89
89
|
|
|
90
90
|
export interface ReleaseTabOptions {
|
|
91
91
|
kill?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Absolute end-to-end deadline (`Date.now()`-based, ms) for the whole teardown chain.
|
|
94
|
+
* Shared across a `releaseAllTabs` loop so close/close-all honors ONE aggregate budget.
|
|
95
|
+
* Omitted (the GC/session-teardown callers) keeps the original unbounded behavior.
|
|
96
|
+
*/
|
|
97
|
+
deadlineAt?: number;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
const tabs = new Map<string, TabSession>();
|
|
95
101
|
const GRACE_MS = 750;
|
|
96
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Remaining time (ms) until an absolute teardown deadline, or +Infinity when no deadline
|
|
105
|
+
* is set (the GC / session-teardown callers). A non-positive result means the shared
|
|
106
|
+
* close budget is already spent.
|
|
107
|
+
*/
|
|
108
|
+
function remainingBudget(deadlineAt: number | undefined): number {
|
|
109
|
+
if (deadlineAt === undefined) return Number.POSITIVE_INFINITY;
|
|
110
|
+
return deadlineAt - Date.now();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Await `op` but never longer than `remainingMs` (#2027). On timeout — or when the budget
|
|
115
|
+
* is already exhausted — the operation is detached and kept alive best-effort with its
|
|
116
|
+
* rejection swallowed, so a dying CDP target cannot wedge the close teardown or leak an
|
|
117
|
+
* unhandled rejection. Returns true when `op` settled within budget, false when detached.
|
|
118
|
+
*/
|
|
119
|
+
async function awaitWithinBudget(op: Promise<unknown>, remainingMs: number, label: string): Promise<boolean> {
|
|
120
|
+
const settled = Promise.resolve(op).then(
|
|
121
|
+
() => true,
|
|
122
|
+
() => true,
|
|
123
|
+
);
|
|
124
|
+
if (remainingMs === Number.POSITIVE_INFINITY) return await settled;
|
|
125
|
+
if (remainingMs <= 0) {
|
|
126
|
+
void settled;
|
|
127
|
+
logger.debug("close teardown budget already spent; detaching step", { label });
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
131
|
+
const timedOut = new Promise<boolean>(resolve => {
|
|
132
|
+
timer = setTimeout(() => resolve(false), remainingMs);
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
const ok = await Promise.race([settled, timedOut]);
|
|
136
|
+
if (!ok) logger.debug("close teardown step exceeded deadline; detached", { label });
|
|
137
|
+
return ok;
|
|
138
|
+
} finally {
|
|
139
|
+
if (timer) clearTimeout(timer);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
97
143
|
export function getTab(name: string): TabSession | undefined {
|
|
98
144
|
return tabs.get(name);
|
|
99
145
|
}
|
|
@@ -316,17 +362,24 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
316
362
|
}
|
|
317
363
|
tab.pending.clear();
|
|
318
364
|
let forced = false;
|
|
365
|
+
const deadlineAt = opts.deadlineAt;
|
|
319
366
|
if (wasAlive) {
|
|
320
367
|
try {
|
|
321
368
|
tab.worker.send({ type: "close" });
|
|
322
|
-
await waitForClosed(tab);
|
|
369
|
+
await waitForClosed(tab, remainingBudget(deadlineAt));
|
|
323
370
|
} catch {
|
|
324
371
|
forced = true;
|
|
325
372
|
}
|
|
326
373
|
}
|
|
327
|
-
await tab.worker.terminate()
|
|
328
|
-
if (forced && tab.kindTag === "headless")
|
|
329
|
-
|
|
374
|
+
await awaitWithinBudget(tab.worker.terminate(), remainingBudget(deadlineAt), "worker.terminate");
|
|
375
|
+
if (forced && tab.kindTag === "headless") {
|
|
376
|
+
await awaitWithinBudget(closeOrphanTarget(tab), remainingBudget(deadlineAt), "closeOrphanTarget");
|
|
377
|
+
}
|
|
378
|
+
await awaitWithinBudget(
|
|
379
|
+
releaseBrowser(tab.browser, { kill: opts.kill ?? false }),
|
|
380
|
+
remainingBudget(deadlineAt),
|
|
381
|
+
"releaseBrowser",
|
|
382
|
+
);
|
|
330
383
|
// Only delete if the map still holds THIS tab: a same-name reacquire during our async
|
|
331
384
|
// teardown may have installed a fresh tab that we must not evict.
|
|
332
385
|
if (tabs.get(name) === tab) tabs.delete(name);
|
|
@@ -495,13 +548,17 @@ async function closeOrphanTarget(tab: TabSession): Promise<void> {
|
|
|
495
548
|
}
|
|
496
549
|
}
|
|
497
550
|
|
|
498
|
-
async function waitForClosed(tab: TabSession): Promise<void> {
|
|
551
|
+
async function waitForClosed(tab: TabSession, remainingMs: number = Number.POSITIVE_INFINITY): Promise<void> {
|
|
499
552
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
500
553
|
const unsubscribe = tab.worker.onMessage(msg => {
|
|
501
554
|
if (msg.type === "closed") resolve();
|
|
502
555
|
});
|
|
503
556
|
try {
|
|
504
|
-
await raceWithTimeout(
|
|
557
|
+
await raceWithTimeout(
|
|
558
|
+
promise,
|
|
559
|
+
Math.max(0, Math.min(GRACE_MS, remainingMs)),
|
|
560
|
+
"Timed out closing browser tab worker",
|
|
561
|
+
);
|
|
505
562
|
} finally {
|
|
506
563
|
unsubscribe();
|
|
507
564
|
}
|
package/src/tools/browser.ts
CHANGED
|
@@ -187,7 +187,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
187
187
|
case "open":
|
|
188
188
|
return await this.#open(name, params, details, timeoutMs, signal);
|
|
189
189
|
case "close":
|
|
190
|
-
return await this.#close(name, params, details, signal);
|
|
190
|
+
return await this.#close(name, params, details, timeoutMs, signal);
|
|
191
191
|
case "run":
|
|
192
192
|
return await this.#run(name, params, details, timeoutMs, signal);
|
|
193
193
|
case "act":
|
|
@@ -274,15 +274,20 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
274
274
|
name: string,
|
|
275
275
|
params: BrowserParams,
|
|
276
276
|
details: BrowserToolDetails,
|
|
277
|
+
timeoutMs: number,
|
|
277
278
|
signal?: AbortSignal,
|
|
278
279
|
): Promise<AgentToolResult<BrowserToolDetails>> {
|
|
279
280
|
const kill = !!params.kill;
|
|
281
|
+
// One end-to-end budget across the whole close/close-all teardown chain (#2027):
|
|
282
|
+
// releaseAllTabs → waitForClosed → worker.terminate → releaseBrowser stay bounded so
|
|
283
|
+
// a CDP target dying mid-teardown cannot hang the tool call or the agent turn.
|
|
284
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
280
285
|
if (params.all) {
|
|
281
|
-
const count = await untilAborted(signal, () => releaseAllTabs({ kill }));
|
|
286
|
+
const count = await untilAborted(signal, () => releaseAllTabs({ kill, deadlineAt }));
|
|
282
287
|
details.result = `Closed ${count} tab(s)`;
|
|
283
288
|
return toolResult(details).text(details.result).done();
|
|
284
289
|
}
|
|
285
|
-
const closed = await untilAborted(signal, () => releaseTab(name, { kill }));
|
|
290
|
+
const closed = await untilAborted(signal, () => releaseTab(name, { kill, deadlineAt }));
|
|
286
291
|
details.result = closed ? `Closed tab ${JSON.stringify(name)}` : `No tab named ${JSON.stringify(name)}`;
|
|
287
292
|
return toolResult(details).text(details.result).done();
|
|
288
293
|
}
|