@gajae-code/coding-agent 0.9.6 → 0.10.1

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.
Files changed (226) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/types/cli/classify-install-target.d.ts +0 -10
  3. package/dist/types/cli/daemon-cli.d.ts +2 -0
  4. package/dist/types/cli/initial-message.d.ts +1 -1
  5. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  6. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  7. package/dist/types/cli/notify-cli.d.ts +3 -1
  8. package/dist/types/cli/session-picker.d.ts +4 -3
  9. package/dist/types/cli/update-cli.d.ts +22 -3
  10. package/dist/types/cli.d.ts +5 -0
  11. package/dist/types/commands/daemon.d.ts +6 -2
  12. package/dist/types/commands/launch.d.ts +1 -0
  13. package/dist/types/commands/notify.d.ts +6 -0
  14. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  15. package/dist/types/commit/analysis/summary.d.ts +1 -1
  16. package/dist/types/commit/changelog/generate.d.ts +1 -1
  17. package/dist/types/commit/changelog/index.d.ts +2 -2
  18. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  19. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  20. package/dist/types/config/keybindings.d.ts +5 -0
  21. package/dist/types/config/model-profile-activation.d.ts +1 -0
  22. package/dist/types/config/settings-schema.d.ts +47 -7
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator/contract.d.ts +1 -1
  25. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  26. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  27. package/dist/types/coordinator-mcp/server.d.ts +79 -6
  28. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  29. package/dist/types/daemon/control-types.d.ts +10 -0
  30. package/dist/types/daemon/operator-contract.d.ts +47 -0
  31. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  32. package/dist/types/discovery/helpers.d.ts +1 -1
  33. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  34. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  35. package/dist/types/gjc-runtime/launch-tmux.d.ts +26 -7
  36. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +34 -3
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
  38. package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  39. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +309 -0
  40. package/dist/types/gjc-runtime/tmux-sessions.d.ts +49 -17
  41. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  42. package/dist/types/main.d.ts +66 -2
  43. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  44. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  45. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  46. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  47. package/dist/types/modes/components/gajae-pet-widget.d.ts +59 -0
  48. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  49. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  50. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  51. package/dist/types/modes/components/session-selector.d.ts +22 -9
  52. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  54. package/dist/types/modes/components/status-line.d.ts +0 -10
  55. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  56. package/dist/types/modes/components/user-message.d.ts +2 -1
  57. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  59. package/dist/types/modes/interactive-mode.d.ts +24 -2
  60. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  61. package/dist/types/modes/print-mode.d.ts +3 -4
  62. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  63. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  64. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  65. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  66. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  67. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  68. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  69. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  70. package/dist/types/modes/types.d.ts +28 -2
  71. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  72. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  73. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  74. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  75. package/dist/types/notifications/config.d.ts +34 -0
  76. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  77. package/dist/types/notifications/index.d.ts +27 -0
  78. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  79. package/dist/types/notifications/notification-service.d.ts +144 -0
  80. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  81. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  82. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  83. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  84. package/dist/types/notifications/topic-registry.d.ts +24 -4
  85. package/dist/types/registry/agent-registry.d.ts +2 -0
  86. package/dist/types/rlm/index.d.ts +1 -1
  87. package/dist/types/sdk.d.ts +2 -0
  88. package/dist/types/secrets/obfuscator.d.ts +1 -1
  89. package/dist/types/session/agent-session.d.ts +40 -3
  90. package/dist/types/session/cache-economics.d.ts +79 -0
  91. package/dist/types/session/context-estimation.d.ts +31 -0
  92. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  93. package/dist/types/session/session-manager.d.ts +124 -4
  94. package/dist/types/session/session-storage.d.ts +155 -13
  95. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  96. package/dist/types/task/executor.d.ts +3 -0
  97. package/dist/types/task/receipt.d.ts +3 -1
  98. package/dist/types/task/types.d.ts +8 -0
  99. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  100. package/dist/types/tools/ask.d.ts +25 -1
  101. package/dist/types/tools/bash.d.ts +1 -1
  102. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  103. package/dist/types/tools/index.d.ts +58 -6
  104. package/dist/types/tools/telegram-send.d.ts +7 -0
  105. package/dist/types/utils/external-editor.d.ts +2 -0
  106. package/package.json +9 -9
  107. package/scripts/g004-tmux-smoke.ts +196 -36
  108. package/src/cli/daemon-cli.ts +19 -29
  109. package/src/cli/malloc-env-guard.test.ts +104 -0
  110. package/src/cli/malloc-env-guard.ts +149 -0
  111. package/src/cli/notify-cli.ts +78 -14
  112. package/src/cli/session-picker.ts +23 -40
  113. package/src/cli/update-cli.ts +56 -37
  114. package/src/cli.ts +51 -10
  115. package/src/commands/daemon.ts +19 -5
  116. package/src/commands/harness.ts +515 -47
  117. package/src/commands/launch.ts +11 -3
  118. package/src/commands/notify.ts +5 -1
  119. package/src/commands/session.ts +6 -1
  120. package/src/config/keybindings.ts +5 -0
  121. package/src/config/model-profile-activation.ts +1 -1
  122. package/src/config/model-profiles.ts +14 -14
  123. package/src/config/settings-schema.ts +41 -1
  124. package/src/config/settings.ts +96 -45
  125. package/src/coordinator/contract.ts +1 -0
  126. package/src/coordinator-mcp/model-preset.ts +100 -0
  127. package/src/coordinator-mcp/policy.ts +30 -0
  128. package/src/coordinator-mcp/server.ts +2881 -425
  129. package/src/coordinator-mcp/session-reaper.ts +129 -0
  130. package/src/daemon/control-types.ts +11 -0
  131. package/src/daemon/operator-contract.ts +121 -0
  132. package/src/deep-interview/render-middleware.ts +67 -2
  133. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  134. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  135. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  136. package/src/extensibility/extensions/runner.ts +1 -1
  137. package/src/extensibility/extensions/types.ts +3 -1
  138. package/src/gjc-runtime/launch-tmux.ts +702 -135
  139. package/src/gjc-runtime/session-state-sidecar.ts +756 -135
  140. package/src/gjc-runtime/team-runtime.ts +6 -0
  141. package/src/gjc-runtime/tmux-common.ts +26 -7
  142. package/src/gjc-runtime/tmux-gc.ts +52 -9
  143. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +185 -0
  144. package/src/gjc-runtime/tmux-owner-isolation.ts +2060 -0
  145. package/src/gjc-runtime/tmux-sessions.ts +871 -68
  146. package/src/internal-urls/docs-index.generated.ts +12 -11
  147. package/src/internal-urls/local-protocol.ts +29 -10
  148. package/src/main.ts +301 -80
  149. package/src/modes/acp/acp-agent.ts +730 -100
  150. package/src/modes/acp/acp-mode.ts +29 -2
  151. package/src/modes/components/assistant-message.ts +32 -3
  152. package/src/modes/components/bash-execution.ts +43 -13
  153. package/src/modes/components/custom-editor.ts +5 -0
  154. package/src/modes/components/footer.ts +3 -3
  155. package/src/modes/components/gajae-pet-widget.ts +360 -0
  156. package/src/modes/components/hook-selector.ts +12 -0
  157. package/src/modes/components/irc-sidebar.ts +206 -0
  158. package/src/modes/components/pet-selector.ts +55 -0
  159. package/src/modes/components/session-selector.ts +222 -221
  160. package/src/modes/components/settings-selector.ts +10 -0
  161. package/src/modes/components/status-line/segments.ts +13 -6
  162. package/src/modes/components/status-line/types.ts +1 -1
  163. package/src/modes/components/status-line.ts +4 -171
  164. package/src/modes/components/tool-execution.ts +47 -6
  165. package/src/modes/components/user-message.ts +26 -9
  166. package/src/modes/controllers/command-controller.ts +28 -2
  167. package/src/modes/controllers/event-controller.ts +159 -36
  168. package/src/modes/controllers/extension-ui-controller.ts +37 -18
  169. package/src/modes/controllers/input-controller.ts +8 -2
  170. package/src/modes/controllers/selector-controller.ts +62 -15
  171. package/src/modes/interactive-mode.ts +199 -7
  172. package/src/modes/irc-observation-ledger.ts +127 -0
  173. package/src/modes/print-mode.ts +228 -92
  174. package/src/modes/rpc/rpc-client.ts +58 -1
  175. package/src/modes/rpc/rpc-mode.ts +286 -85
  176. package/src/modes/rpc/rpc-types.ts +15 -1
  177. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  178. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  179. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  180. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  181. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  182. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  183. package/src/modes/types.ts +34 -2
  184. package/src/modes/utils/context-usage.ts +56 -92
  185. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  186. package/src/modes/utils/irc-message.ts +138 -0
  187. package/src/modes/utils/ui-helpers.ts +216 -50
  188. package/src/notifications/config.ts +44 -0
  189. package/src/notifications/daemon-paths.ts +8 -0
  190. package/src/notifications/index.ts +535 -75
  191. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  192. package/src/notifications/notification-service.ts +710 -0
  193. package/src/notifications/rate-limit-pool.ts +46 -12
  194. package/src/notifications/telegram-daemon-cli.ts +2 -0
  195. package/src/notifications/telegram-daemon-control.ts +27 -2
  196. package/src/notifications/telegram-daemon.ts +635 -94
  197. package/src/notifications/telegram-reference.ts +73 -13
  198. package/src/notifications/topic-registry.ts +86 -9
  199. package/src/prompts/system/irc-peer-roster.md +1 -0
  200. package/src/prompts/system/subagent-system-prompt.md +1 -0
  201. package/src/prompts/system/system-prompt.md +1 -0
  202. package/src/prompts/tools/irc.md +1 -1
  203. package/src/registry/agent-registry.ts +3 -0
  204. package/src/sdk.ts +52 -1
  205. package/src/secrets/obfuscator.ts +4 -2
  206. package/src/session/agent-session.ts +670 -217
  207. package/src/session/cache-economics.ts +275 -0
  208. package/src/session/context-estimation.ts +91 -0
  209. package/src/session/provider-safety-stop.ts +9 -0
  210. package/src/session/session-dump-format.ts +6 -0
  211. package/src/session/session-manager.ts +809 -39
  212. package/src/session/session-storage.ts +582 -38
  213. package/src/slash-commands/builtin-registry.ts +153 -9
  214. package/src/slash-commands/helpers/context-report.ts +26 -4
  215. package/src/task/executor.ts +12 -3
  216. package/src/task/index.ts +8 -0
  217. package/src/task/receipt.ts +7 -2
  218. package/src/task/types.ts +26 -0
  219. package/src/tools/ask-answer-registry.ts +25 -0
  220. package/src/tools/ask.ts +355 -33
  221. package/src/tools/browser/tab-supervisor.ts +63 -6
  222. package/src/tools/browser.ts +8 -3
  223. package/src/tools/index.ts +81 -6
  224. package/src/tools/sqlite-reader.ts +8 -4
  225. package/src/tui/output-block.ts +18 -6
  226. package/src/utils/external-editor.ts +6 -1
@@ -3,7 +3,15 @@ import * as path from "node:path";
3
3
  import { ThinkingLevel } from "@gajae-code/agent-core";
4
4
  import { type Model, modelsAreEqual } from "@gajae-code/ai";
5
5
  import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
6
- import { Spacer, Text } from "@gajae-code/tui";
6
+ import {
7
+ isPetMode,
8
+ isUnderTerminalMultiplexer,
9
+ PET_MODE_IDS,
10
+ PET_SKIN_IDS,
11
+ PET_SKINS,
12
+ Spacer,
13
+ Text,
14
+ } from "@gajae-code/tui";
7
15
  import { setProjectDir } from "@gajae-code/utils";
8
16
  import { jobElapsedMs } from "../async";
9
17
  import { materializeActiveModelProfileAssignments } from "../config/model-profile-activation";
@@ -22,8 +30,20 @@ import {
22
30
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../discovery/helpers.js";
23
31
  import { resolveMemoryBackend } from "../memory-backend";
24
32
  import { DynamicBorder } from "../modes/components/dynamic-border";
33
+ import { GajaePetWidget } from "../modes/components/gajae-pet-widget";
25
34
  import { theme } from "../modes/theme/theme";
26
35
  import type { InteractiveModeContext } from "../modes/types";
36
+ import {
37
+ buildNotificationStatusReport,
38
+ checkNotificationHealth,
39
+ formatNotificationHealthReport,
40
+ formatNotificationRecoveryReport,
41
+ formatNotificationStatusReport,
42
+ formatNotificationTestResult,
43
+ recoverNotifications,
44
+ sendNotificationTest,
45
+ } from "../notifications/notification-service";
46
+ import { computeCacheMissCostSummary, formatCacheMissSummaryLines } from "../session/cache-economics";
27
47
  import { formatModelOnboardingGuidance } from "../setup/model-onboarding-guidance";
28
48
  import {
29
49
  addApiCompatibleProvider,
@@ -36,7 +56,7 @@ import { getDisplayChangelogEntries } from "../utils/changelog";
36
56
  import { buildContextReportText } from "./helpers/context-report";
37
57
  import { buildFastStatusReport } from "./helpers/fast-status-report";
38
58
  import { formatDuration } from "./helpers/format";
39
- import { commandConsumed, errorMessage, parseSlashCommand, usage } from "./helpers/parse";
59
+ import { commandConsumed, errorMessage, parseSlashCommand, parseSubcommand, usage } from "./helpers/parse";
40
60
  import { handleSshAcp } from "./helpers/ssh";
41
61
  import { buildUsageReportText } from "./helpers/usage-report";
42
62
  import type {
@@ -470,6 +490,66 @@ const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashComma
470
490
  };
471
491
 
472
492
  const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
493
+ {
494
+ name: "notify",
495
+ priority: 30,
496
+ description: "Notification status, health, test, recovery, and session on/off",
497
+ acpDescription: "Notification status, health, test, recovery, and session on/off",
498
+ subcommands: [
499
+ { name: "on", description: "Enable notifications for this session" },
500
+ { name: "off", description: "Disable notifications for this session" },
501
+ { name: "status", description: "Show notification configuration (no secrets)" },
502
+ { name: "health", description: "Config, daemon-ownership and endpoint health" },
503
+ { name: "test", description: "Send a test notification", usage: "[message]" },
504
+ { name: "recovery", description: "Clear dead-owner locks and stale endpoint files" },
505
+ { name: "setup", description: "How to pair a Telegram bot (run in a terminal)" },
506
+ ],
507
+ inlineHint: "[on|off|status|health|test|recovery|setup]",
508
+ acpInputHint: "[on|off|status|health|test|recovery|setup]",
509
+ allowArgs: true,
510
+ handle: async (command, runtime) => {
511
+ const { verb, rest } = parseSubcommand(command.args);
512
+ const action = verb || "status";
513
+ // `on`/`off` are session-local runtime controls owned by the
514
+ // notifications extension command (`api.registerCommand("notify")`),
515
+ // which holds the live per-session server/disable state. Pass them
516
+ // through untouched — never consume them — so this builtin cannot
517
+ // shadow that control. Everything below is config/service diagnostics
518
+ // the extension does not implement, so the builtin owns them exclusively
519
+ // (and the extension therefore never consumes them).
520
+ if (action === "on" || action === "off") {
521
+ return { prompt: command.text };
522
+ }
523
+ const stateRoot = path.join(runtime.cwd, ".gjc", "state");
524
+ switch (action) {
525
+ case "status":
526
+ await runtime.output(formatNotificationStatusReport(buildNotificationStatusReport(runtime.settings)));
527
+ return commandConsumed();
528
+ case "health": {
529
+ const report = await checkNotificationHealth({ settings: runtime.settings, stateRoot });
530
+ await runtime.output(formatNotificationHealthReport(report));
531
+ return commandConsumed();
532
+ }
533
+ case "test": {
534
+ const result = await sendNotificationTest({ settings: runtime.settings, text: rest || undefined });
535
+ await runtime.output(formatNotificationTestResult(result));
536
+ return commandConsumed();
537
+ }
538
+ case "recovery": {
539
+ const report = await recoverNotifications({ settings: runtime.settings, stateRoot });
540
+ await runtime.output(formatNotificationRecoveryReport(report));
541
+ return commandConsumed();
542
+ }
543
+ case "setup":
544
+ return usage(
545
+ "Run `gjc notify setup` in a terminal to pair a Telegram bot token with a private chat (interactive; requires a TTY).",
546
+ runtime,
547
+ );
548
+ default:
549
+ return usage(`Usage: /notify [on|off|status|health|test|recovery|setup] (got "${action}")`, runtime);
550
+ }
551
+ },
552
+ },
473
553
  {
474
554
  name: "settings",
475
555
  priority: 40,
@@ -487,6 +567,41 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
487
567
  runtime.ctx.editor.setText("");
488
568
  },
489
569
  },
570
+ {
571
+ name: "pet",
572
+ description: "Gajae pet living beside the composer",
573
+ subcommands: [
574
+ { name: "off", description: "Hide the pet" },
575
+ ...PET_SKIN_IDS.map(id => ({ name: id, description: PET_SKINS[id].description })),
576
+ ],
577
+ inlineHint: `[${PET_MODE_IDS.join("|")}]`,
578
+ allowArgs: true,
579
+ handleTui: (command, runtime) => {
580
+ const ctx = runtime.ctx;
581
+ const raw = command.args?.trim().toLowerCase() ?? "";
582
+ const arg = raw === "on" ? "red" : raw;
583
+ if (!arg) {
584
+ ctx.showPetSelector();
585
+ ctx.editor.setText("");
586
+ return;
587
+ }
588
+ if (arg !== "off" && !GajaePetWidget.pixelProtocol()) {
589
+ ctx.showStatus(
590
+ isUnderTerminalMultiplexer()
591
+ ? "Gajae pet: graphics are suppressed inside tmux/screen/zellij — escapes are not forwarded end-to-end. Run gjc outside the multiplexer in a sixel/kitty-graphics terminal, or set PI_FORCE_IMAGE_PROTOCOL=sixel when your multiplexer+client chain renders sixel."
592
+ : "Gajae pet needs a sixel/kitty-graphics terminal (Windows Terminal 1.22+, kitty, Ghostty, WezTerm)",
593
+ { dim: true },
594
+ );
595
+ } else if (isPetMode(arg)) {
596
+ ctx.setPetMode(arg);
597
+ const name = arg === "off" ? "Gajae pet hidden" : `${PET_SKINS[arg].label} is here`;
598
+ ctx.showStatus(name);
599
+ } else {
600
+ ctx.showStatus(`Usage: /pet [${PET_MODE_IDS.join("|")}]`, { dim: true });
601
+ }
602
+ ctx.editor.setText("");
603
+ },
604
+ },
490
605
  {
491
606
  name: "goal",
492
607
  priority: 84,
@@ -820,13 +935,42 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
820
935
  allowArgs: true,
821
936
  handle: async (command, runtime) => {
822
937
  if (!command.args || command.args === "info") {
823
- await runtime.output(
824
- [
825
- `Session: ${runtime.session.sessionId}`,
826
- `Title: ${runtime.session.sessionName}`,
827
- `CWD: ${runtime.cwd}`,
828
- ].join("\n"),
829
- );
938
+ const stats = runtime.session.getSessionStats();
939
+ const lines = [
940
+ `Session: ${runtime.session.sessionId}`,
941
+ `Title: ${runtime.session.sessionName}`,
942
+ `CWD: ${runtime.cwd}`,
943
+ "",
944
+ "Tokens",
945
+ `Input: ${stats.tokens.input.toLocaleString()}`,
946
+ `Output: ${stats.tokens.output.toLocaleString()}`,
947
+ ];
948
+ if (stats.tokens.cacheRead > 0) {
949
+ lines.push(`Cache Read: ${stats.tokens.cacheRead.toLocaleString()}`);
950
+ }
951
+ if (stats.tokens.cacheWrite > 0) {
952
+ lines.push(`Cache Write: ${stats.tokens.cacheWrite.toLocaleString()}`);
953
+ }
954
+ lines.push(`Total: ${stats.tokens.total.toLocaleString()}`);
955
+ if (stats.cost > 0 || stats.premiumRequests > 0) {
956
+ lines.push("", "Cost");
957
+ if (stats.cost > 0) {
958
+ lines.push(`Total: ${stats.cost.toFixed(4)}`);
959
+ }
960
+ if (stats.premiumRequests > 0) {
961
+ lines.push(`Premium Requests: ${stats.premiumRequests.toLocaleString()}`);
962
+ }
963
+ }
964
+ const cacheMissSummary = stats.costBreakdown
965
+ ? computeCacheMissCostSummary(stats.tokens, {
966
+ kind: "persisted-aggregate",
967
+ costBreakdown: stats.costBreakdown,
968
+ })
969
+ : undefined;
970
+ if (cacheMissSummary) {
971
+ lines.push("", "Cache Miss Cost", ...formatCacheMissSummaryLines(cacheMissSummary));
972
+ }
973
+ await runtime.output(lines.join("\n"));
830
974
  return commandConsumed();
831
975
  }
832
976
  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 usedPct = (breakdown.usedTokens / breakdown.contextWindow) * 100;
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
- `Active context: ${breakdown.usedTokens.toLocaleString()} / ${breakdown.contextWindow.toLocaleString()} tokens (${usedPct.toFixed(1)}% used)`,
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(formatTokenLine("Free", breakdown.freeTokens, breakdown.contextWindow));
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 || fallback.tokens === undefined ? "unknown" : fallback.tokens.toLocaleString()}`,
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");
@@ -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 = AgentRegistry.global()
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,
@@ -1,5 +1,4 @@
1
- import type { SingleResult, TaskToolDetails } from "./types";
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
+ }