@kenkaiiii/ggcoder 5.81.0 → 5.83.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 (73) hide show
  1. package/README.md +1 -1
  2. package/dist/app-sidecar/app-settings.js +11 -5
  3. package/dist/app-sidecar/app-settings.js.map +1 -1
  4. package/dist/app-sidecar/auth-routes.js +8 -3
  5. package/dist/app-sidecar/auth-routes.js.map +1 -1
  6. package/dist/app-sidecar/goal-routes.js +7 -0
  7. package/dist/app-sidecar/goal-routes.js.map +1 -1
  8. package/dist/app-sidecar/run-control-routes.js +31 -3
  9. package/dist/app-sidecar/run-control-routes.js.map +1 -1
  10. package/dist/app-sidecar.js +65 -11
  11. package/dist/app-sidecar.js.map +1 -1
  12. package/dist/cli/auth.js +22 -1
  13. package/dist/cli/auth.js.map +1 -1
  14. package/dist/cli.js +7 -7
  15. package/dist/cli.js.map +1 -1
  16. package/dist/config.js +4 -2
  17. package/dist/config.js.map +1 -1
  18. package/dist/core/agent-notifications.d.ts +18 -0
  19. package/dist/core/agent-notifications.js +35 -2
  20. package/dist/core/agent-notifications.js.map +1 -1
  21. package/dist/core/agent-session.d.ts +71 -1
  22. package/dist/core/agent-session.js +175 -16
  23. package/dist/core/agent-session.js.map +1 -1
  24. package/dist/core/compaction/policy.d.ts +2 -2
  25. package/dist/core/compaction/policy.js +86 -43
  26. package/dist/core/compaction/policy.js.map +1 -1
  27. package/dist/core/compaction/token-estimator.d.ts +4 -1
  28. package/dist/core/compaction/token-estimator.js +5 -2
  29. package/dist/core/compaction/token-estimator.js.map +1 -1
  30. package/dist/core/idle-wake.d.ts +62 -0
  31. package/dist/core/idle-wake.js +103 -0
  32. package/dist/core/idle-wake.js.map +1 -0
  33. package/dist/core/process-manager.d.ts +51 -11
  34. package/dist/core/process-manager.js +229 -54
  35. package/dist/core/process-manager.js.map +1 -1
  36. package/dist/core/slash-commands.d.ts +5 -1
  37. package/dist/core/slash-commands.js +24 -9
  38. package/dist/core/slash-commands.js.map +1 -1
  39. package/dist/core/steering.d.ts +7 -0
  40. package/dist/core/steering.js +13 -0
  41. package/dist/core/steering.js.map +1 -1
  42. package/dist/core/subagent-manager.js +5 -4
  43. package/dist/core/subagent-manager.js.map +1 -1
  44. package/dist/core/subagent-policy.js +4 -1
  45. package/dist/core/subagent-policy.js.map +1 -1
  46. package/dist/goals-agent/goals-agent.js +3 -2
  47. package/dist/goals-agent/goals-agent.js.map +1 -1
  48. package/dist/interactive.js +9 -3
  49. package/dist/interactive.js.map +1 -1
  50. package/dist/modes/acp-mode.js +8 -1
  51. package/dist/modes/acp-mode.js.map +1 -1
  52. package/dist/modes/agent-home-mode.js +13 -1
  53. package/dist/modes/agent-home-mode.js.map +1 -1
  54. package/dist/modes/rpc-mode.d.ts +2 -1
  55. package/dist/modes/rpc-mode.js +15 -3
  56. package/dist/modes/rpc-mode.js.map +1 -1
  57. package/dist/modes/serve-mode.js +13 -1
  58. package/dist/modes/serve-mode.js.map +1 -1
  59. package/dist/tools/background-watch-lint.d.ts +18 -0
  60. package/dist/tools/background-watch-lint.js +102 -0
  61. package/dist/tools/background-watch-lint.js.map +1 -0
  62. package/dist/tools/bash.js +5 -2
  63. package/dist/tools/bash.js.map +1 -1
  64. package/dist/tools/subagent-shared.js +4 -1
  65. package/dist/tools/subagent-shared.js.map +1 -1
  66. package/dist/tools/task-output.js +11 -1
  67. package/dist/tools/task-output.js.map +1 -1
  68. package/dist/ui/components/Footer.js +4 -0
  69. package/dist/ui/components/Footer.js.map +1 -1
  70. package/dist/ui/components/ModelSelector.d.ts +10 -0
  71. package/dist/ui/components/ModelSelector.js +51 -5
  72. package/dist/ui/components/ModelSelector.js.map +1 -1
  73. package/package.json +4 -4
@@ -1,5 +1,5 @@
1
1
  import { agentLoop, isAbortError, isUsageLimitError, repairToolPairingAdjacent, } from "@kenkaiiii/gg-agent";
2
- import { ProviderError, stream, } from "@kenkaiiii/gg-ai";
2
+ import { ProviderError, resolveToolSchema, stream, } from "@kenkaiiii/gg-ai";
3
3
  import { EventBus } from "./event-bus.js";
4
4
  import { flagUntrustedToolResult } from "./injection-detect.js";
5
5
  import { COMPLETION_REVIEW_STATE_KIND, } from "./completion-review.js";
@@ -53,7 +53,7 @@ import { buildLoopBreakMessage } from "./loop-breaker.js";
53
53
  import { buildRegroundingMessage, requestTextForRegrounding } from "./regrounding.js";
54
54
  import { buildSemanticLoopJudgePrompt, buildSemanticLoopMessage, parseSemanticLoopVerdict, SEMANTIC_LOOP_JUDGE_TIMEOUT_MS, withJudgeTimeout, } from "./semantic-loop-check.js";
55
55
  import { buildEnvDeltaMessage, buildLanguageUpdateMessage } from "./env-delta.js";
56
- import { wrapSteeringText, buildNotificationSteeringText, STEERING_PREFIX } from "./steering.js";
56
+ import { wrapSteeringText, buildNotificationSteeringText, buildIdleWakeNotificationText, STEERING_PREFIX, } from "./steering.js";
57
57
  import { AgentNotificationQueue } from "./agent-notifications.js";
58
58
  import { VerificationGate, VERIFICATION_STATE_KIND } from "./verification-gate.js";
59
59
  import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
@@ -68,6 +68,54 @@ import { RunLoopMonitor } from "./agent-session/run-loop-monitor.js";
68
68
  import { VerificationTracker } from "./agent-session/verification-tracker.js";
69
69
  import { McpToolRegistry } from "./agent-session/mcp-tools.js";
70
70
  import { TranscriptMarkers } from "./agent-session/transcript-markers.js";
71
+ // ── Output cap ─────────────────────────────────────────────
72
+ /**
73
+ * The output cap to send on a request. A model's output ceiling can equal its
74
+ * whole window (Kimi K2.7 Code: 262,144 of 262,144), and providers that count
75
+ * prompt + max_tokens against the window reject every request with a real
76
+ * prompt. Cap it at the room the model's TOTAL window leaves once the context
77
+ * reaches this session's compaction trigger — the largest prompt it lets build
78
+ * up. The total window, not the route's: the ChatGPT/Codex route's 272K
79
+ * window is an input budget, and GPT-6's 128K output sits outside it.
80
+ */
81
+ export function outputCapForRequest(options) {
82
+ const totalWindow = getContextWindow(options.model, { provider: options.provider });
83
+ const { targetTokens } = resolveCompactionPolicy({
84
+ provider: options.provider,
85
+ model: options.model,
86
+ contextWindow: getContextWindow(options.model, {
87
+ provider: options.provider,
88
+ accountId: options.accountId,
89
+ }),
90
+ threshold: options.threshold,
91
+ accountId: options.accountId,
92
+ });
93
+ const room = totalWindow - targetTokens;
94
+ return room > 0 ? Math.min(options.maxTokens, room) : options.maxTokens;
95
+ }
96
+ // ── Paid models ────────────────────────────────────────────
97
+ /**
98
+ * Reply for chat bridges (Telegram, agent home) that have no confirmation step.
99
+ * Generic on purpose: every paid model (Claude fast mode, MiMo UltraSpeed) is
100
+ * refused here, so the reason must not name one of them.
101
+ */
102
+ export function paidModelRefusal(modelName) {
103
+ return `${modelName} spends real money, so it can't be switched to from chat. Switch to it in the GG Coder app or CLI, where you confirm it.`;
104
+ }
105
+ /**
106
+ * Thrown by {@link AgentSession.switchModel} when the target model spends real
107
+ * money and the caller didn't pass the user's explicit confirmation.
108
+ */
109
+ export class PaidModelConfirmationError extends Error {
110
+ modelName;
111
+ detail;
112
+ constructor(modelName, detail) {
113
+ super(`${modelName} spends real money. ${detail} Confirm before switching to it.`);
114
+ this.name = "PaidModelConfirmationError";
115
+ this.modelName = modelName;
116
+ this.detail = detail;
117
+ }
118
+ }
71
119
  export { resolveSessionToolResultCharLimit, resolveSessionTurnToolResultCharLimit, } from "./agent-session/helpers.js";
72
120
  // ── Agent Session ──────────────────────────────────────────
73
121
  export class AgentSession {
@@ -156,6 +204,8 @@ export class AgentSession {
156
204
  deferredCompactionPending = false;
157
205
  /** Latest provider count, anchored to the assistant response it measured. */
158
206
  providerContext = null;
207
+ /** See {@link toolDefinitionChars}. */
208
+ toolCharsCache = null;
159
209
  originalRequest = "";
160
210
  // Messages queued by the user while a run is in flight. Drained at the
161
211
  // mid-loop steering boundary (user steering wins over the hooks), mirroring
@@ -167,6 +217,8 @@ export class AgentSession {
167
217
  // different message (or past the end) by the time the cancel arrives.
168
218
  userQueue = [];
169
219
  queueSeq = 0;
220
+ /** Monotonic count of human-originated prompts; idle-wake budgets reset on it. */
221
+ humanPromptSeq = 0;
170
222
  /** Instant interrupt: the running loop's preempt listeners, fired on queueMessage. */
171
223
  steeringListeners = new Set();
172
224
  processManager;
@@ -296,6 +348,16 @@ export class AgentSession {
296
348
  }
297
349
  return modelInfo?.maxOutputTokens ?? 16384;
298
350
  }
351
+ /** {@link outputCapForRequest} for this session's live model and settings. */
352
+ requestMaxTokens(accountId) {
353
+ return outputCapForRequest({
354
+ provider: this.provider,
355
+ model: this.model,
356
+ maxTokens: this.maxTokens,
357
+ threshold: this.settingsManager.get("compactThreshold"),
358
+ accountId,
359
+ });
360
+ }
299
361
  async initialize() {
300
362
  // Set model for accurate token estimation
301
363
  setEstimatorModel(this.model);
@@ -812,6 +874,8 @@ export class AgentSession {
812
874
  visibility: "transcript",
813
875
  }, options = {}) {
814
876
  this.prewarmController?.abort();
877
+ if (provenance.source === "human")
878
+ this.humanPromptSeq += 1;
815
879
  await this.settlePostTurnCompaction();
816
880
  await this.adoptDeferredCheckpointBeforePrompt();
817
881
  const slash = await this.resolveSlashInput(content);
@@ -850,6 +914,7 @@ export class AgentSession {
850
914
  */
851
915
  async promptWithAttachments(text, attachments) {
852
916
  this.prewarmController?.abort();
917
+ this.humanPromptSeq += 1;
853
918
  await this.settlePostTurnCompaction();
854
919
  await this.adoptDeferredCheckpointBeforePrompt();
855
920
  const parts = this.buildAttachmentParts(text, attachments);
@@ -1499,7 +1564,10 @@ export class AgentSession {
1499
1564
  }
1500
1565
  // Reset self-correction hook state for this run; pin the latest user message
1501
1566
  // as the verbatim original request for post-compaction re-grounding.
1502
- const lastUser = [...this.messages].reverse().find((m) => m.role === "user");
1567
+ // A status update that started an idle-wake run is not the user's request.
1568
+ const lastUser = [...this.messages]
1569
+ .reverse()
1570
+ .find((m) => m.role === "user" && m.provenance?.kind !== "notification");
1503
1571
  this.resetHookState(requestTextForRegrounding(lastUser));
1504
1572
  // Resolve OAuth credentials and run agent loop.
1505
1573
  // On 401, force-refresh the token and retry once — the provider may have
@@ -1591,7 +1659,7 @@ export class AgentSession {
1591
1659
  model: this.model,
1592
1660
  tools: options.disableTools ? [] : this.tools,
1593
1661
  webSearch: !options.disableTools,
1594
- maxTokens: this.maxTokens,
1662
+ maxTokens: this.requestMaxTokens(accountId),
1595
1663
  maxTurns: this.opts.maxTurns,
1596
1664
  maxTurnExtensions: this.opts.maxTurnExtensions,
1597
1665
  // Plan mode caps effort at medium (Codex `plan_mode_reasoning_effort`
@@ -1697,8 +1765,12 @@ export class AgentSession {
1697
1765
  if (anchor?.role === "assistant") {
1698
1766
  this.providerContext = { usage: { ...transformOpts.usage }, anchor };
1699
1767
  // Feed the authoritative usage back into the token estimator so
1700
- // char-based estimates track this session's real tokenizer.
1701
- calibrateEstimatorFromUsage(messages.slice(0, anchorIndex), transformOpts.usage);
1768
+ // char-based estimates track this session's real tokenizer. The
1769
+ // provider's count includes the tool definitions sent with the
1770
+ // request, so their characters must be counted too — without
1771
+ // them a short history reads as ~2 chars/token and drags the
1772
+ // shared ratio down for every session in the process.
1773
+ calibrateEstimatorFromUsage(messages.slice(0, anchorIndex), transformOpts.usage, options.disableTools ? 0 : this.toolDefinitionChars());
1702
1774
  }
1703
1775
  }
1704
1776
  const force = transformOpts.force === true;
@@ -1749,12 +1821,13 @@ export class AgentSession {
1749
1821
  compactedToolCalls: String(pruneResult.compactedToolCalls),
1750
1822
  freedTokens: String(pruneResult.freedTokens),
1751
1823
  });
1752
- // The turn's own usage also counted the pruned content — estimate
1753
- // the (now smaller) history so the freed tokens actually defer
1754
- // the LLM compaction.
1755
- activeTokens = calculateActiveContextTokens(messages, {
1756
- pendingMessages: transformOpts.pendingMessages,
1757
- });
1824
+ // The measured size counted the pruned content, so take the
1825
+ // freed tokens off it. Never swap the provider's count for a
1826
+ // whole-history estimate here: that estimate rides a shared
1827
+ // chars-per-token ratio, and it once read a 234K-token MiMo
1828
+ // context (23% of 1M) as ~400K and compacted it against a 340K
1829
+ // trigger.
1830
+ activeTokens = Math.max(0, activeTokens - pruneResult.freedTokens);
1758
1831
  }
1759
1832
  }
1760
1833
  if (Date.now() < this.compactionRetryAfter)
@@ -1926,7 +1999,17 @@ export class AgentSession {
1926
1999
  // prompt. Detached by design; the pre-run path above remains the backstop.
1927
2000
  this.maybeCompactPostTurn(creds);
1928
2001
  }
1929
- async switchModel(provider, model) {
2002
+ /**
2003
+ * Switch the session's model. A model that spends real money (Claude fast
2004
+ * mode bills usage credits) is refused unless `confirmPaid` carries the
2005
+ * user's explicit yes, so no caller can switch onto one by accident: every
2006
+ * path (app, CLI, editors, chat bridges, RPC) fails closed by default.
2007
+ */
2008
+ async switchModel(provider, model, options = {}) {
2009
+ const target = getModel(model);
2010
+ if (target?.billing?.paid && options.confirmPaid !== true && model !== this.model) {
2011
+ throw new PaidModelConfirmationError(target.name, target.billing.detail);
2012
+ }
1930
2013
  // The model-switch note is appended to `this.messages`; settle any
1931
2014
  // background compaction first so the note cannot be swapped out.
1932
2015
  await this.settlePostTurnCompaction();
@@ -2713,6 +2796,74 @@ export class AgentSession {
2713
2796
  notify(id, text) {
2714
2797
  this.notifications.enqueue("goal", id, text, { terminal: true });
2715
2798
  }
2799
+ /**
2800
+ * Observe urgent background alerts — a background task failed or exited
2801
+ * non-zero, or a child agent finished — as they are queued. Hosts use this to
2802
+ * wake an idle session (see {@link continueFromNotifications}); a live run
2803
+ * already receives them at its next steering boundary. Returns unsubscribe.
2804
+ */
2805
+ onBackgroundAlert(listener) {
2806
+ return this.notifications.onUrgent(() => listener());
2807
+ }
2808
+ /**
2809
+ * Characters of the tool definitions sent with every request (name,
2810
+ * description, JSON schema), for estimator calibration. Cached by tool count:
2811
+ * the tool array is append-only.
2812
+ */
2813
+ toolDefinitionChars() {
2814
+ if (this.toolCharsCache?.count === this.tools.length)
2815
+ return this.toolCharsCache.chars;
2816
+ let chars = 0;
2817
+ for (const tool of this.tools) {
2818
+ chars += tool.name.length + tool.description.length;
2819
+ try {
2820
+ chars += JSON.stringify(resolveToolSchema(tool)).length;
2821
+ }
2822
+ catch {
2823
+ // A schema that cannot be serialized is not sent either.
2824
+ }
2825
+ }
2826
+ this.toolCharsCache = { count: this.tools.length, chars };
2827
+ return chars;
2828
+ }
2829
+ /** True while an urgent background alert is waiting to be delivered. */
2830
+ hasPendingBackgroundAlert() {
2831
+ return this.notifications.hasUrgent();
2832
+ }
2833
+ /** Monotonic count of human-originated prompts this session has run. */
2834
+ getHumanPromptSeq() {
2835
+ return this.humanPromptSeq;
2836
+ }
2837
+ /**
2838
+ * Start a run from pending background notifications, for a session that had
2839
+ * already finished its turn. The notifications become one hidden runtime
2840
+ * message (never shown as something the user typed), and the agent decides
2841
+ * whether they need action. Returns false, starting nothing, when no urgent
2842
+ * alert is pending — a live run may have delivered it in the meantime.
2843
+ */
2844
+ async continueFromNotifications() {
2845
+ if (!this.notifications.hasUrgent())
2846
+ return false;
2847
+ this.prewarmController?.abort();
2848
+ await this.settlePostTurnCompaction();
2849
+ await this.adoptDeferredCheckpointBeforePrompt();
2850
+ const notified = this.notifications.drain();
2851
+ if (notified.length === 0)
2852
+ return false;
2853
+ const message = {
2854
+ role: "user",
2855
+ content: buildIdleWakeNotificationText(notified.map((entry) => entry.text)),
2856
+ provenance: { source: "runtime", kind: "notification", visibility: "hidden" },
2857
+ };
2858
+ log("INFO", "notifications", "Starting a run from background alerts", {
2859
+ count: String(notified.length),
2860
+ });
2861
+ this.messages.push(message);
2862
+ await this.persistMessage(message);
2863
+ this.lastPersistedIndex = this.messages.length;
2864
+ await this.runLoop();
2865
+ return true;
2866
+ }
2716
2867
  /** This session's sub-agents (workers), newest-state, including ones restored after a restart. */
2717
2868
  listSubAgents() {
2718
2869
  return this.subAgentManager?.list() ?? [];
@@ -3242,10 +3393,18 @@ export class AgentSession {
3242
3393
  return { enhanced: text, segments: [{ kind: "text", text }] };
3243
3394
  // Keep credentials and settings on the model selected at click time, even
3244
3395
  // if the user switches models while credential resolution is pending.
3245
- const { provider, model, thinkingLevel, maxTokens, baseUrl } = this;
3396
+ const { provider, model, thinkingLevel, baseUrl } = this;
3397
+ const threshold = this.settingsManager.get("compactThreshold");
3246
3398
  const creds = await this.authStorage.resolveCredentials(provider, {
3247
3399
  storageKeys: this.currentAuthStorageKeys(),
3248
3400
  });
3401
+ const maxTokens = outputCapForRequest({
3402
+ provider,
3403
+ model,
3404
+ maxTokens: this.maxTokens,
3405
+ threshold,
3406
+ accountId: creds.accountId,
3407
+ });
3249
3408
  // Cheap, best-effort stack detection from the project root so terminology is
3250
3409
  // idiomatic to the user's stack. Never throws (returns "" on any failure).
3251
3410
  let stack = "";
@@ -3363,7 +3522,7 @@ export class AgentSession {
3363
3522
  messages,
3364
3523
  tools: this.tools,
3365
3524
  webSearch: true,
3366
- maxTokens: this.maxTokens,
3525
+ maxTokens: this.requestMaxTokens(creds.accountId),
3367
3526
  thinking: this.planModeRef.current
3368
3527
  ? clampThinkingForPlanMode(this.thinkingLevel)
3369
3528
  : this.thinkingLevel,
@@ -3675,7 +3834,7 @@ export class AgentSession {
3675
3834
  }
3676
3835
  createSlashCommandContext() {
3677
3836
  return {
3678
- switchModel: (provider, model) => this.switchModel(provider, model),
3837
+ switchModel: (provider, model, options) => this.switchModel(provider, model, options),
3679
3838
  compact: (focus) => this.compact(undefined, "manual", focus),
3680
3839
  newSession: () => this.newSession(),
3681
3840
  listSessions: async () => {