@gajae-code/coding-agent 0.9.6 → 0.10.0

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