@gajae-code/coding-agent 0.5.0 → 0.5.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 (125) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/types/async/job-manager.d.ts +26 -0
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/list-models.d.ts +6 -0
  5. package/dist/types/commands/gc.d.ts +26 -0
  6. package/dist/types/config/file-lock-gc.d.ts +5 -0
  7. package/dist/types/config/file-lock.d.ts +7 -0
  8. package/dist/types/coordinator/contract.d.ts +1 -1
  9. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  10. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  11. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  12. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  13. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  14. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  15. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  16. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  19. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  20. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  21. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  22. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  23. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  25. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  26. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  27. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  28. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  29. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
  30. package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
  31. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  32. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  33. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  34. package/dist/types/harness-control-plane/owner.d.ts +7 -0
  35. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  36. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  37. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  38. package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
  39. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  40. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  41. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  42. package/dist/types/session/agent-session.d.ts +1 -1
  43. package/dist/types/session/blob-store.d.ts +39 -3
  44. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  45. package/dist/types/tools/ask.d.ts +15 -1
  46. package/dist/types/tools/subagent.d.ts +6 -0
  47. package/package.json +7 -7
  48. package/src/async/job-manager.ts +52 -0
  49. package/src/cli/args.ts +3 -0
  50. package/src/cli/auth-broker-cli.ts +1 -0
  51. package/src/cli/list-models.ts +13 -1
  52. package/src/cli.ts +1 -0
  53. package/src/commands/gc.ts +22 -0
  54. package/src/commands/harness.ts +7 -3
  55. package/src/config/file-lock-gc.ts +181 -0
  56. package/src/config/file-lock.ts +14 -0
  57. package/src/config/model-profiles.ts +24 -15
  58. package/src/coordinator/contract.ts +1 -0
  59. package/src/coordinator-mcp/server.ts +459 -3
  60. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  61. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  62. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  63. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  64. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  65. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  66. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  67. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  68. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  69. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  70. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  71. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  72. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  73. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  75. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  76. package/src/defaults/gjc-defaults.ts +7 -0
  77. package/src/defaults/gjc-grok-cli.ts +22 -0
  78. package/src/extensibility/extensions/index.ts +1 -0
  79. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  80. package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
  81. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  82. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  83. package/src/gjc-runtime/gc-render.ts +70 -0
  84. package/src/gjc-runtime/gc-runtime.ts +403 -0
  85. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  86. package/src/gjc-runtime/ralplan-runtime.ts +58 -7
  87. package/src/gjc-runtime/state-renderer.ts +12 -3
  88. package/src/gjc-runtime/state-runtime.ts +46 -29
  89. package/src/gjc-runtime/team-gc.ts +49 -0
  90. package/src/gjc-runtime/team-runtime.ts +179 -2
  91. package/src/gjc-runtime/tmux-common.ts +14 -0
  92. package/src/gjc-runtime/tmux-gc.ts +176 -0
  93. package/src/gjc-runtime/tmux-sessions.ts +49 -1
  94. package/src/gjc-runtime/ultragoal-runtime.ts +12 -0
  95. package/src/harness-control-plane/gc-adapter.ts +184 -0
  96. package/src/harness-control-plane/owner.ts +11 -0
  97. package/src/harness-control-plane/storage.ts +70 -0
  98. package/src/internal-urls/docs-index.generated.ts +14 -8
  99. package/src/main.ts +7 -2
  100. package/src/modes/components/hook-selector.ts +19 -0
  101. package/src/modes/components/model-selector.ts +25 -8
  102. package/src/modes/components/status-line/segments.ts +1 -1
  103. package/src/modes/controllers/command-controller.ts +25 -6
  104. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  105. package/src/modes/controllers/selector-controller.ts +1 -0
  106. package/src/modes/rpc/rpc-mode.ts +151 -33
  107. package/src/modes/shared/agent-wire/command-dispatch.ts +278 -261
  108. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  109. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  110. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  111. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  112. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  113. package/src/sdk.ts +17 -3
  114. package/src/session/agent-session.ts +77 -8
  115. package/src/session/blob-store.ts +59 -3
  116. package/src/session/session-manager.ts +4 -4
  117. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  118. package/src/skill-state/workflow-hud.ts +106 -10
  119. package/src/slash-commands/builtin-registry.ts +3 -2
  120. package/src/task/executor.ts +9 -0
  121. package/src/tools/ask.ts +56 -1
  122. package/src/tools/job.ts +3 -2
  123. package/src/tools/monitor.ts +36 -1
  124. package/src/tools/subagent-render.ts +9 -0
  125. package/src/tools/subagent.ts +26 -2
@@ -21,6 +21,8 @@ interface RalplanHudState extends WorkflowGateHudState {
21
21
  stage?: string;
22
22
  waiting?: string;
23
23
  iteration?: number;
24
+ iterationFromIndex?: number;
25
+ stages?: string;
24
26
  verdict?: string;
25
27
  latestSummary?: string;
26
28
  pendingApproval?: boolean;
@@ -100,6 +102,95 @@ export function buildDeepInterviewHudSummary(state: DeepInterviewHudState): Work
100
102
  };
101
103
  }
102
104
 
105
+ export interface DeepInterviewHudDeriveOptions {
106
+ phase?: string;
107
+ specStatus?: string;
108
+ updatedAt?: string;
109
+ }
110
+
111
+ function diIsPlainObject(value: unknown): value is Record<string, unknown> {
112
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
113
+ }
114
+
115
+ function latestScoredAmbiguity(rounds: unknown): number | undefined {
116
+ if (!Array.isArray(rounds)) return undefined;
117
+ for (let index = rounds.length - 1; index >= 0; index--) {
118
+ const round = rounds[index];
119
+ if (diIsPlainObject(round) && round.lifecycle === "scored" && typeof round.ambiguity === "number") {
120
+ return round.ambiguity;
121
+ }
122
+ }
123
+ return undefined;
124
+ }
125
+
126
+ function weakestDimensionFromTopology(
127
+ topology: Record<string, unknown>,
128
+ targetComponent: string | undefined,
129
+ ): string | undefined {
130
+ if (!Array.isArray(topology.components)) return undefined;
131
+ const components = topology.components.filter(diIsPlainObject);
132
+ const dimensionOf = (component: Record<string, unknown>): string | undefined =>
133
+ typeof component.weakest_dimension === "string" && component.weakest_dimension.trim()
134
+ ? component.weakest_dimension
135
+ : undefined;
136
+ if (targetComponent) {
137
+ const targeted = components.find(component => component.id === targetComponent && dimensionOf(component));
138
+ if (targeted) return dimensionOf(targeted);
139
+ }
140
+ const active = components.find(component => component.status !== "deferred" && dimensionOf(component));
141
+ if (active) return dimensionOf(active);
142
+ const any = components.find(component => dimensionOf(component));
143
+ return any ? dimensionOf(any) : undefined;
144
+ }
145
+
146
+ /**
147
+ * Single source of deep-interview HUD derivation. Reads a complete (normalized)
148
+ * mode-state envelope so recorder, `gjc state write`, reconcile, seed, and handoff
149
+ * all produce identical chips. Topology-aware `target`/`weakest` come from
150
+ * `state.topology`; `legacy_missing` topology omits those chips (no synthetic values).
151
+ */
152
+ export function deriveDeepInterviewHud(
153
+ payload: Record<string, unknown>,
154
+ options: DeepInterviewHudDeriveOptions = {},
155
+ ): WorkflowHudSummary {
156
+ const stateField = diIsPlainObject(payload.state) ? payload.state : {};
157
+ const isNumber = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
158
+ const isArray = (value: unknown): value is unknown[] => Array.isArray(value);
159
+ const pick = <T>(key: string, guard: (value: unknown) => value is T): T | undefined => {
160
+ const value = stateField[key] ?? payload[key];
161
+ return guard(value) ? value : undefined;
162
+ };
163
+
164
+ const phase = options.phase ?? (typeof payload.current_phase === "string" ? payload.current_phase : undefined);
165
+ const rounds = pick("rounds", isArray);
166
+ const ambiguity = pick("current_ambiguity", isNumber) ?? latestScoredAmbiguity(rounds);
167
+ const threshold = pick("threshold", isNumber);
168
+ const rawTopology = diIsPlainObject(stateField.topology)
169
+ ? stateField.topology
170
+ : diIsPlainObject(payload.topology)
171
+ ? payload.topology
172
+ : undefined;
173
+ // `legacy_missing` topology was never confirmed: omit target/weakest even if stale fields linger.
174
+ const topology = rawTopology && rawTopology.status !== "legacy_missing" ? rawTopology : undefined;
175
+ const targetComponent =
176
+ topology && typeof topology.last_targeted_component_id === "string"
177
+ ? topology.last_targeted_component_id
178
+ : undefined;
179
+ const weakestDimension = topology ? weakestDimensionFromTopology(topology, targetComponent) : undefined;
180
+ const specStatus = options.specStatus ?? (typeof payload.spec_status === "string" ? payload.spec_status : undefined);
181
+
182
+ return buildDeepInterviewHudSummary({
183
+ phase,
184
+ ambiguity,
185
+ threshold,
186
+ roundCount: rounds?.length,
187
+ targetComponent,
188
+ weakestDimension,
189
+ specStatus,
190
+ updatedAt: options.updatedAt ?? new Date().toISOString(),
191
+ });
192
+ }
193
+
103
194
  export function buildRalplanHudSummary(state: RalplanHudState): WorkflowHudSummary {
104
195
  const verdict = state.verdict?.toUpperCase();
105
196
  const verdictSeverity =
@@ -118,7 +209,14 @@ export function buildRalplanHudSummary(state: RalplanHudState): WorkflowHudSumma
118
209
  ...gateChips(state, 6),
119
210
  chip("stage", state.stage, 10),
120
211
  chip("waiting", state.waiting, 20),
121
- chip("iter", state.iteration === undefined ? undefined : String(state.iteration), 30),
212
+ chip(
213
+ "iter",
214
+ (state.iterationFromIndex ?? state.iteration) === undefined
215
+ ? undefined
216
+ : String(state.iterationFromIndex ?? state.iteration),
217
+ 30,
218
+ ),
219
+ chip("stages", state.stages, 35),
122
220
  chip("verdict", verdict, 40, verdictSeverity),
123
221
  ]),
124
222
  ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),
@@ -136,17 +234,15 @@ export function buildUltragoalHudSummary(state: UltragoalHudState): WorkflowHudS
136
234
  chip("goals", `${complete}/${total}`, 10),
137
235
  chip("current", state.currentGoal ? `${state.currentGoal.id}:${state.currentGoal.title}` : state.status, 20),
138
236
  chip("status", state.status, 30, state.status === "complete" ? "success" : undefined),
237
+ chip(
238
+ "ledger",
239
+ state.latestLedgerEvent?.event
240
+ ? [state.latestLedgerEvent.event, state.latestLedgerEvent.goalId].filter(Boolean).join(":")
241
+ : undefined,
242
+ 35,
243
+ ),
139
244
  ...gateChips(state, 40),
140
245
  ]),
141
- details: state.latestLedgerEvent
142
- ? compactChips([
143
- chip(
144
- "ledger",
145
- [state.latestLedgerEvent.event, state.latestLedgerEvent.goalId].filter(Boolean).join(":"),
146
- 100,
147
- ),
148
- ])
149
- : undefined,
150
246
  ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),
151
247
  };
152
248
  }
@@ -4,6 +4,7 @@ import type { 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
6
  import { setProjectDir } from "@gajae-code/utils";
7
+ import { jobElapsedMs } from "../async";
7
8
  import {
8
9
  GJC_MODEL_ASSIGNMENT_TARGET_IDS,
9
10
  GJC_MODEL_ASSIGNMENT_TARGETS,
@@ -506,14 +507,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
506
507
  if (snapshot.running.length > 0) {
507
508
  lines.push("", "Running Jobs");
508
509
  for (const job of snapshot.running) {
509
- lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(now - job.startTime)}`);
510
+ lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(jobElapsedMs(job, now))}`);
510
511
  lines.push(` ${job.label}`);
511
512
  }
512
513
  }
513
514
  if (snapshot.recent.length > 0) {
514
515
  lines.push("", "Recent Jobs");
515
516
  for (const job of snapshot.recent) {
516
- lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(now - job.startTime)}`);
517
+ lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(jobElapsedMs(job, now))}`);
517
518
  lines.push(` ${job.label}`);
518
519
  }
519
520
  }
@@ -1154,6 +1154,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1154
1154
  resolvedModel: model.id,
1155
1155
  });
1156
1156
  }
1157
+ // Record which model the subagent actually runs on (and any auth fallback,
1158
+ // see #985) so the subagent panel can surface it to the user.
1159
+ if (model) {
1160
+ AsyncJobManager.instance()?.updateSubagentModel?.(options.subagentId ?? id, {
1161
+ requestedModel: modelSubstitutionWarning?.requested ?? resolvedModelString,
1162
+ effectiveModel: resolvedModelString,
1163
+ modelFellBack: authFallbackUsed === true,
1164
+ });
1165
+ }
1157
1166
  if (model?.contextWindow && model.contextWindow > 0) {
1158
1167
  progress.contextWindow = model.contextWindow;
1159
1168
  }
package/src/tools/ask.ts CHANGED
@@ -34,6 +34,8 @@ import {
34
34
  renderDeepInterviewAskQuestion,
35
35
  } from "../deep-interview/render-middleware";
36
36
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
37
+ import { appendOrMergeDeepInterviewRound } from "../gjc-runtime/deep-interview-recorder";
38
+ import { deepInterviewStatePath } from "../gjc-runtime/deep-interview-runtime";
37
39
  import { gateAnswerToResult, questionToGate } from "../modes/shared/agent-wire/deep-interview-gate";
38
40
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
39
41
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
@@ -50,15 +52,25 @@ const OptionItem = z.object({
50
52
  label: z.string().describe("display label"),
51
53
  });
52
54
 
55
+ /** Optional structured deep-interview round metadata; when present the round is recorded automatically. */
56
+ const DeepInterviewMeta = z.object({
57
+ round_id: z.string().describe("stable optional round identity").optional(),
58
+ round: z.number().int().nonnegative().describe("round number"),
59
+ component: z.string().min(1).describe("targeted topology component"),
60
+ dimension: z.string().min(1).describe("targeted clarity dimension"),
61
+ ambiguity: z.number().min(0).max(1).describe("ambiguity at ask time (0..1)"),
62
+ });
63
+
53
64
  const QuestionItem = z.object({
54
65
  id: z.string().describe("question id"),
55
66
  question: z.string().describe("question text"),
56
67
  options: z.array(OptionItem).describe("available options"),
57
68
  multi: z.boolean().describe("allow multiple selections").optional(),
58
69
  recommended: z.number().describe("recommended option index").optional(),
70
+ deepInterview: DeepInterviewMeta.describe("optional deep-interview round metadata").optional(),
59
71
  });
60
72
 
61
- const askSchema = z.object({
73
+ export const askSchema = z.object({
62
74
  questions: z.array(QuestionItem).min(1).describe("questions to ask"),
63
75
  });
64
76
 
@@ -456,6 +468,45 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
456
468
  TERMINAL.sendNotification("Waiting for input");
457
469
  }
458
470
 
471
+ /**
472
+ * Record a resolved deep-interview round when the question carries structured
473
+ * metadata. The runtime owns durable record/merge semantics; this tool is only the
474
+ * caller. Best-effort: a state-write hiccup must not break the user's answer flow.
475
+ */
476
+ async #recordDeepInterviewRound(
477
+ q: AskParams["questions"][number],
478
+ selectedOptions: string[],
479
+ customInput: string | undefined,
480
+ ): Promise<void> {
481
+ const meta = q.deepInterview;
482
+ if (!meta) return;
483
+ try {
484
+ const cwd = this.session.cwd;
485
+ const sessionId = this.session.getSessionId?.() ?? undefined;
486
+ const statePath = deepInterviewStatePath(cwd, sessionId);
487
+ await appendOrMergeDeepInterviewRound(
488
+ cwd,
489
+ statePath,
490
+ {
491
+ round: meta.round,
492
+ round_id: meta.round_id,
493
+ questionId: q.id,
494
+ questionText: q.question,
495
+ component: meta.component,
496
+ dimension: meta.dimension,
497
+ ambiguity: meta.ambiguity,
498
+ selectedOptions,
499
+ customInput,
500
+ },
501
+ { sessionId },
502
+ );
503
+ } catch (error) {
504
+ console.warn(
505
+ `ask: deep-interview round recording failed: ${error instanceof Error ? error.message : String(error)}`,
506
+ );
507
+ }
508
+ }
509
+
459
510
  async execute(
460
511
  _toolCallId: string,
461
512
  params: AskParams,
@@ -515,6 +566,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
515
566
  options: q.options,
516
567
  multi: q.multi,
517
568
  recommended: q.recommended,
569
+ deepInterview: q.deepInterview,
518
570
  };
519
571
  const answer = await gateEmitter.emitGate(questionToGate(gateQuestion));
520
572
  const decoded = gateAnswerToResult(gateQuestion, answer);
@@ -582,6 +634,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
582
634
  context?.abort();
583
635
  throw new ToolAbortError("Ask tool was cancelled by the user");
584
636
  }
637
+ await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
585
638
  const details: AskToolDetails = {
586
639
  question: q.question,
587
640
  options: optionLabels,
@@ -644,6 +697,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
644
697
  customInput,
645
698
  };
646
699
 
700
+ await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
701
+
647
702
  if (navAction === "back") {
648
703
  questionIndex = Math.max(0, questionIndex - 1);
649
704
  continue;
package/src/tools/job.ts CHANGED
@@ -3,7 +3,7 @@ import type { Component } from "@gajae-code/tui";
3
3
  import { Text } from "@gajae-code/tui";
4
4
  import { prompt } from "@gajae-code/utils";
5
5
  import * as z from "zod/v4";
6
- import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
6
+ import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled, jobElapsedMs } from "../async";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { Theme } from "../modes/theme/theme";
9
9
  import jobDescription from "../prompts/tools/job.md" with { type: "text" };
@@ -257,6 +257,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
257
257
  status: string;
258
258
  label: string;
259
259
  startTime: number;
260
+ endTime?: number;
260
261
  resultText?: string;
261
262
  errorText?: string;
262
263
  }[],
@@ -270,7 +271,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
270
271
  type: latest.type,
271
272
  status: latest.status as JobSnapshot["status"],
272
273
  label: latest.label,
273
- durationMs: Math.max(0, now - latest.startTime),
274
+ durationMs: jobElapsedMs(latest, now),
274
275
  ...(latest.resultText ? { resultText: latest.resultText } : {}),
275
276
  ...(latest.errorText ? { errorText: latest.errorText } : {}),
276
277
  };
@@ -3,6 +3,7 @@ import { logger, prompt } from "@gajae-code/utils";
3
3
  import * as z from "zod/v4";
4
4
  import { AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
5
5
  import monitorDescription from "../prompts/tools/monitor.md" with { type: "text" };
6
+ import { truncateTail } from "../session/streaming-output";
6
7
  import { BashTool } from "./bash";
7
8
  import type { ToolSession } from "./index";
8
9
  import { ToolError } from "./tool-errors";
@@ -48,6 +49,8 @@ export interface MonitorToolDetails {
48
49
 
49
50
  const MONITOR_LABEL_MAX = 120;
50
51
  const MAX_PENDING_MONITOR_NOTIFICATIONS = 3;
52
+ const MONITOR_NOTIFICATION_LINE_MAX_BYTES = 16 * 1024;
53
+ const MONITOR_NOTIFICATION_LINE_MAX_LINES = 20;
51
54
 
52
55
  function buildMonitorLabel(params: MonitorParams): string {
53
56
  const base = `[monitor:${params.kind}] ${params.description}`;
@@ -55,6 +58,34 @@ function buildMonitorLabel(params: MonitorParams): string {
55
58
  return `${base.slice(0, MONITOR_LABEL_MAX - 3)}...`;
56
59
  }
57
60
 
61
+ function formatMonitorNotificationLine(line: string): {
62
+ content: string;
63
+ truncated: boolean;
64
+ totalBytes: number;
65
+ outputBytes: number;
66
+ } {
67
+ const truncation = truncateTail(line, {
68
+ maxBytes: MONITOR_NOTIFICATION_LINE_MAX_BYTES,
69
+ maxLines: MONITOR_NOTIFICATION_LINE_MAX_LINES,
70
+ });
71
+ const outputBytes = truncation.outputBytes ?? truncation.totalBytes;
72
+ if (!truncation.truncated) {
73
+ return {
74
+ content: truncation.content,
75
+ truncated: false,
76
+ totalBytes: truncation.totalBytes,
77
+ outputBytes,
78
+ };
79
+ }
80
+ const notice = `[Monitor output truncated: showing last ${outputBytes} of ${truncation.totalBytes} bytes]`;
81
+ return {
82
+ content: `${truncation.content}\n${notice}`,
83
+ truncated: true,
84
+ totalBytes: truncation.totalBytes,
85
+ outputBytes,
86
+ };
87
+ }
88
+
58
89
  export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolDetails> {
59
90
  readonly name = "monitor";
60
91
  readonly label = "Monitor";
@@ -130,7 +161,8 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
130
161
  if (controller.closed) return;
131
162
  const notificationId = `${jobId}:${sequence}`;
132
163
  const suffix = count > 0 ? `\n(+${count} earlier lines)` : "";
133
- const content = `<task-notification>\nMonitor task ${jobId} (${params.kind}: ${params.description}) emitted latest state:\n${line}${suffix}\n</task-notification>`;
164
+ const notificationLine = formatMonitorNotificationLine(line);
165
+ const content = `<task-notification>\nMonitor task ${jobId} (${params.kind}: ${params.description}) emitted latest state:\n${notificationLine.content}${suffix}\n</task-notification>`;
134
166
  const details = {
135
167
  taskId: jobId,
136
168
  kind: params.kind,
@@ -139,6 +171,9 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
139
171
  notificationId,
140
172
  sequence,
141
173
  coalescedCount: count,
174
+ outputTruncated: notificationLine.truncated,
175
+ outputTotalBytes: notificationLine.totalBytes,
176
+ outputBytes: notificationLine.outputBytes,
142
177
  };
143
178
  pendingNotifications += 1;
144
179
  if (pendingNotifications > MAX_PENDING_MONITOR_NOTIFICATIONS) {
@@ -66,6 +66,15 @@ function renderSubagentSnapshot(
66
66
  if (snapshot.agent && snapshot.agent !== "unknown") {
67
67
  lines.push(` ${theme.fg("dim", `Agent: ${snapshot.agent} (${snapshot.agentSource})`)}`);
68
68
  }
69
+ if (snapshot.effectiveModel) {
70
+ if (snapshot.modelFellBack && snapshot.requestedModel) {
71
+ lines.push(
72
+ ` ${theme.fg("warning", `Model: ${snapshot.effectiveModel} (requested ${snapshot.requestedModel}, fell back — no credentials)`)}`,
73
+ );
74
+ } else {
75
+ lines.push(` ${theme.fg("dim", `Model: ${snapshot.effectiveModel}`)}`);
76
+ }
77
+ }
69
78
  if (snapshot.description) lines.push(` ${theme.fg("dim", `Description: ${snapshot.description}`)}`);
70
79
  if (snapshot.outputRef) lines.push(` ${theme.fg("dim", `Output: ${snapshot.outputRef}`)}`);
71
80
  if (snapshot.assignment) {
@@ -2,7 +2,7 @@ import * as path from "node:path";
2
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
3
3
  import { prompt } from "@gajae-code/utils";
4
4
  import * as z from "zod/v4";
5
- import { type AsyncJob, AsyncJobManager, type SubagentRecord } from "../async";
5
+ import { type AsyncJob, AsyncJobManager, jobElapsedMs, type SubagentRecord } from "../async";
6
6
  import subagentDescription from "../prompts/tools/subagent.md" with { type: "text" };
7
7
  import type { AgentProgress, AgentSource } from "../task/types";
8
8
  import { Ellipsis, truncateToWidth } from "../tui";
@@ -67,6 +67,12 @@ export interface SubagentSnapshot {
67
67
  progress?: AgentProgress;
68
68
  /** True when a live in-session progress producer exists for this subagent. */
69
69
  liveProgressAvailable?: boolean;
70
+ /** Model the subagent actually runs on (after any auth fallback). */
71
+ effectiveModel?: string;
72
+ /** Model originally requested via role/preset mapping; differs from effective on fallback. */
73
+ requestedModel?: string;
74
+ /** True when the requested model lacked credentials and fell back to the parent model. */
75
+ modelFellBack?: boolean;
70
76
  }
71
77
 
72
78
  export interface SubagentToolDetails {
@@ -508,6 +514,13 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
508
514
  lines.push(`### ${snapshot.id} — ${snapshot.status}`);
509
515
  if (snapshot.jobId !== snapshot.id) lines.push(`Job: ${snapshot.jobId}`);
510
516
  if (snapshot.agent) lines.push(`Agent: ${snapshot.agent} (${snapshot.agentSource})`);
517
+ if (snapshot.effectiveModel) {
518
+ lines.push(
519
+ snapshot.modelFellBack && snapshot.requestedModel
520
+ ? `Model: ${snapshot.effectiveModel} (requested ${snapshot.requestedModel}, fell back — no credentials)`
521
+ : `Model: ${snapshot.effectiveModel}`,
522
+ );
523
+ }
511
524
  if (snapshot.description) lines.push(`Description: ${snapshot.description}`);
512
525
  if (snapshot.outputRef) lines.push(`Output: ${snapshot.outputRef}`);
513
526
  if (snapshot.assignment) lines.push("Assignment:", "```", snapshot.assignment, "```");
@@ -584,9 +597,19 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
584
597
  durationMs: 0,
585
598
  ...(verifiedOutputIds.has(record.subagentId) ? { outputRef: `agent://${record.subagentId}` } : {}),
586
599
  ...liveFields,
600
+ ...this.#modelFields(record),
587
601
  };
588
602
  }
589
603
 
604
+ #modelFields(record?: SubagentRecord): Partial<SubagentSnapshot> {
605
+ if (!record) return {};
606
+ const fields: Partial<SubagentSnapshot> = {};
607
+ if (record.effectiveModel) fields.effectiveModel = record.effectiveModel;
608
+ if (record.requestedModel) fields.requestedModel = record.requestedModel;
609
+ if (record.modelFellBack) fields.modelFellBack = true;
610
+ return fields;
611
+ }
612
+
590
613
  #snapshot(
591
614
  job: AsyncJob,
592
615
  timedOut = false,
@@ -608,7 +631,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
608
631
  label: sanitizeText(job.label, RECEIPT_PREVIEW_WIDTH),
609
632
  agent: subagent?.agent ?? "unknown",
610
633
  agentSource: subagent?.agentSource ?? "bundled",
611
- durationMs: Math.max(0, Date.now() - job.startTime),
634
+ durationMs: jobElapsedMs(job),
612
635
  ...(subagent?.description ? { description: sanitizeText(subagent.description, RECEIPT_PREVIEW_WIDTH) } : {}),
613
636
  ...(verbosity === "full" && subagent?.assignment
614
637
  ? { assignment: sanitizeText(subagent.assignment, FULL_PREVIEW_WIDTH) }
@@ -622,6 +645,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
622
645
  : {}),
623
646
  ...(outputRef ? { outputRef } : {}),
624
647
  ...(runningTimeoutGuidance ? { guidance: runningTimeoutGuidance } : {}),
648
+ ...this.#modelFields(record),
625
649
  };
626
650
  }
627
651