@kenkaiiii/ggcoder 5.68.1 → 5.70.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 (120) hide show
  1. package/dist/app-sidecar.js +37 -11
  2. package/dist/app-sidecar.js.map +1 -1
  3. package/dist/cli.js +17 -19
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/acceptance-checks.d.ts +48 -0
  6. package/dist/core/acceptance-checks.js +144 -0
  7. package/dist/core/acceptance-checks.js.map +1 -0
  8. package/dist/core/agent-session.d.ts +26 -0
  9. package/dist/core/agent-session.js +121 -5
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/agents.d.ts +6 -5
  12. package/dist/core/agents.js.map +1 -1
  13. package/dist/core/bundled-agents.js +1 -3
  14. package/dist/core/bundled-agents.js.map +1 -1
  15. package/dist/core/cache-diagnostics.d.ts +6 -0
  16. package/dist/core/cache-diagnostics.js +31 -0
  17. package/dist/core/cache-diagnostics.js.map +1 -1
  18. package/dist/core/cache-expiry.d.ts +87 -0
  19. package/dist/core/cache-expiry.js +111 -0
  20. package/dist/core/cache-expiry.js.map +1 -0
  21. package/dist/core/compaction/compactor.js +78 -48
  22. package/dist/core/compaction/compactor.js.map +1 -1
  23. package/dist/core/destructive-git-guard.d.ts +27 -0
  24. package/dist/core/destructive-git-guard.js +94 -54
  25. package/dist/core/destructive-git-guard.js.map +1 -1
  26. package/dist/core/injection-detect.d.ts +38 -0
  27. package/dist/core/injection-detect.js +232 -0
  28. package/dist/core/injection-detect.js.map +1 -0
  29. package/dist/core/mcp/client.d.ts +72 -0
  30. package/dist/core/mcp/client.js +264 -41
  31. package/dist/core/mcp/client.js.map +1 -1
  32. package/dist/core/mcp/store.d.ts +6 -1
  33. package/dist/core/mcp/store.js +12 -1
  34. package/dist/core/mcp/store.js.map +1 -1
  35. package/dist/core/mcp/types.d.ts +18 -0
  36. package/dist/core/model-unavailable.d.ts +14 -0
  37. package/dist/core/model-unavailable.js +23 -0
  38. package/dist/core/model-unavailable.js.map +1 -0
  39. package/dist/core/node-debugger.d.ts +148 -0
  40. package/dist/core/node-debugger.js +642 -0
  41. package/dist/core/node-debugger.js.map +1 -0
  42. package/dist/core/persistent-shell.d.ts +58 -6
  43. package/dist/core/persistent-shell.js +331 -49
  44. package/dist/core/persistent-shell.js.map +1 -1
  45. package/dist/core/process-manager.d.ts +14 -0
  46. package/dist/core/process-manager.js +61 -0
  47. package/dist/core/process-manager.js.map +1 -1
  48. package/dist/core/progress/git-xp.js +8 -14
  49. package/dist/core/progress/git-xp.js.map +1 -1
  50. package/dist/core/subagent-manager.d.ts +20 -5
  51. package/dist/core/subagent-manager.js +22 -8
  52. package/dist/core/subagent-manager.js.map +1 -1
  53. package/dist/core/subagent-receipt.d.ts +54 -0
  54. package/dist/core/subagent-receipt.js +276 -0
  55. package/dist/core/subagent-receipt.js.map +1 -0
  56. package/dist/core/subagent-turn-record.d.ts +2 -0
  57. package/dist/core/subagent-turn-record.js.map +1 -1
  58. package/dist/core/test-impact.d.ts +73 -0
  59. package/dist/core/test-impact.js +467 -0
  60. package/dist/core/test-impact.js.map +1 -0
  61. package/dist/core/thinking-level.d.ts +1 -1
  62. package/dist/core/thinking-level.js +1 -1
  63. package/dist/core/thinking-level.js.map +1 -1
  64. package/dist/core/verification-gate.d.ts +2 -0
  65. package/dist/core/verification-gate.js +4 -0
  66. package/dist/core/verification-gate.js.map +1 -1
  67. package/dist/core/verification-snapshot.js +3 -5
  68. package/dist/core/verification-snapshot.js.map +1 -1
  69. package/dist/core/workspace-guard.d.ts +18 -7
  70. package/dist/core/workspace-guard.js +227 -60
  71. package/dist/core/workspace-guard.js.map +1 -1
  72. package/dist/interactive.js +2 -1
  73. package/dist/interactive.js.map +1 -1
  74. package/dist/modes/subagent-worker-mode.js +34 -6
  75. package/dist/modes/subagent-worker-mode.js.map +1 -1
  76. package/dist/test-support/keep-alive.d.ts +14 -0
  77. package/dist/test-support/keep-alive.js +17 -0
  78. package/dist/test-support/keep-alive.js.map +1 -0
  79. package/dist/tools/bash.d.ts +10 -1
  80. package/dist/tools/bash.js +78 -7
  81. package/dist/tools/bash.js.map +1 -1
  82. package/dist/tools/debug.d.ts +54 -0
  83. package/dist/tools/debug.js +233 -0
  84. package/dist/tools/debug.js.map +1 -0
  85. package/dist/tools/index.d.ts +13 -2
  86. package/dist/tools/index.js +41 -6
  87. package/dist/tools/index.js.map +1 -1
  88. package/dist/tools/prompt-hints.js +2 -0
  89. package/dist/tools/prompt-hints.js.map +1 -1
  90. package/dist/tools/read.js +3 -2
  91. package/dist/tools/read.js.map +1 -1
  92. package/dist/tools/subagent-control.js +44 -8
  93. package/dist/tools/subagent-control.js.map +1 -1
  94. package/dist/tools/subagent-shared.d.ts +29 -8
  95. package/dist/tools/subagent-shared.js +45 -14
  96. package/dist/tools/subagent-shared.js.map +1 -1
  97. package/dist/tools/subagent.d.ts +8 -2
  98. package/dist/tools/subagent.js +28 -10
  99. package/dist/tools/subagent.js.map +1 -1
  100. package/dist/tools/task-output.js +3 -2
  101. package/dist/tools/task-output.js.map +1 -1
  102. package/dist/tools/tool-tiers.d.ts +2 -2
  103. package/dist/tools/tool-tiers.js +3 -2
  104. package/dist/tools/tool-tiers.js.map +1 -1
  105. package/dist/tools/truncate.d.ts +21 -0
  106. package/dist/tools/truncate.js +187 -0
  107. package/dist/tools/truncate.js.map +1 -1
  108. package/dist/tools/ui-adopt.js +2 -0
  109. package/dist/tools/ui-adopt.js.map +1 -1
  110. package/dist/utils/git.d.ts +77 -0
  111. package/dist/utils/git.js +285 -21
  112. package/dist/utils/git.js.map +1 -1
  113. package/dist/utils/github-ci.js +2 -1
  114. package/dist/utils/github-ci.js.map +1 -1
  115. package/dist/utils/github.js +11 -9
  116. package/dist/utils/github.js.map +1 -1
  117. package/dist/utils/process.d.ts +20 -0
  118. package/dist/utils/process.js +98 -0
  119. package/dist/utils/process.js.map +1 -1
  120. package/package.json +4 -4
@@ -1,6 +1,7 @@
1
1
  import { agentLoop, isAbortError, isUsageLimitError, repairToolPairingAdjacent, } from "@kenkaiiii/gg-agent";
2
2
  import { ProviderError, stream, } from "@kenkaiiii/gg-ai";
3
3
  import { EventBus } from "./event-bus.js";
4
+ import { flagUntrustedToolResult } from "./injection-detect.js";
4
5
  import { COMPLETION_REVIEW_STATE_KIND, } from "./completion-review.js";
5
6
  import { SlashCommandRegistry, createBuiltinCommands, } from "./slash-commands.js";
6
7
  import { PROMPT_COMMANDS, getPromptCommand } from "./prompt-commands.js";
@@ -23,6 +24,8 @@ import { ensureAppDirs } from "../config.js";
23
24
  import { buildSubAgentSystemPrompt, buildSystemPrompt, } from "../system-prompt.js";
24
25
  import { createTools, createWebSearchTool, } from "../tools/index.js";
25
26
  import { partitionToolsByTier } from "../tools/tool-tiers.js";
27
+ import { formatImpactForVerification } from "./test-impact.js";
28
+ import { autoBackgroundedId } from "../tools/bash.js";
26
29
  import { buildProcessCompletionFollowUp } from "./process-gate.js";
27
30
  import { buildSubAgentCompletionFollowUp, } from "./subagent-manager.js";
28
31
  import { applyAsyncSubagentPolicy } from "./subagent-policy.js";
@@ -37,6 +40,7 @@ import { createSessionStatsTool } from "../tools/session-stats.js";
37
40
  import { createDiagnoseCommand, isInternalDiagnosticsEnabled, SessionDiagnosticsRecorder, } from "./internal-diagnostics.js";
38
41
  import { log } from "./logger.js";
39
42
  import { CacheDiagnostics } from "./cache-diagnostics.js";
43
+ import { assessCacheExpiry, resolveCacheTtl } from "./cache-expiry.js";
40
44
  import { setEstimatorModel, calibrateEstimatorFromUsage, estimateConversationTokens, } from "./compaction/token-estimator.js";
41
45
  import { calculateActiveContextTokens } from "./compaction/active-context.js";
42
46
  import { resolveCompactionPolicy } from "./compaction/policy.js";
@@ -256,8 +260,12 @@ export class AgentSession {
256
260
  // different message (or past the end) by the time the cancel arrives.
257
261
  userQueue = [];
258
262
  queueSeq = 0;
263
+ /** Instant interrupt: the running loop's preempt listeners, fired on queueMessage. */
264
+ steeringListeners = new Set();
259
265
  processManager;
260
266
  lspManager;
267
+ testImpact;
268
+ debugManager;
261
269
  subAgentManager;
262
270
  /**
263
271
  * Out-of-band push notifications (finished children, background-process
@@ -427,7 +435,7 @@ export class AgentSession {
427
435
  : this.opts.globalSubagents
428
436
  ? await discoverAgents({ globalAgentsDir: paths.agentsDir })
429
437
  : [];
430
- const { tools: builtInTools, processManager, rebuildReadTool, clearReadTracker, lspManager, subAgentManager, } = await createTools(this.cwd, {
438
+ const { tools: builtInTools, processManager, rebuildReadTool, clearReadTracker, lspManager, testImpact, debugManager, subAgentManager, } = await createTools(this.cwd, {
431
439
  agents,
432
440
  skills: this.skills,
433
441
  contextLimits: this.contextLimits,
@@ -468,7 +476,6 @@ export class AgentSession {
468
476
  // sub-agent spawns read the current parent state at execution time.
469
477
  getProvider: () => this.provider,
470
478
  getModel: () => this.model,
471
- getThinkingLevel: () => this.thinkingLevel,
472
479
  getBaseUrl: () => this.baseUrl,
473
480
  getCacheKey: () => this.getPromptCacheKey(),
474
481
  getMaxPerModel: () => this.settingsManager.get("subagentMaxPerModel"),
@@ -514,12 +521,15 @@ export class AgentSession {
514
521
  this.mcpCatalog ??= new DeferredToolCatalog(this.contextLimits);
515
522
  this.mcpCatalog.add(deferred);
516
523
  this.ensureToolSearchTool();
524
+ this.promoteWaitAgentAfterSpawn();
517
525
  }
518
526
  }
519
527
  this.rebuildReadTool = rebuildReadTool;
520
528
  this.clearReadTracker = clearReadTracker;
521
529
  this.processManager = processManager;
522
530
  this.lspManager = lspManager;
531
+ this.testImpact = testImpact;
532
+ this.debugManager = debugManager;
523
533
  this.subAgentManager = subAgentManager;
524
534
  this.bindManagerCancellation(this.opts.signal);
525
535
  // Connect MCP servers. The connect attempt itself can block for up to the
@@ -808,6 +818,29 @@ export class AgentSession {
808
818
  : { serverName, ok: false, error: outcome.error };
809
819
  }, this.contextLimits));
810
820
  }
821
+ /**
822
+ * `wait_agent` is deferred, yet nearly every `spawn_agent` is followed by it,
823
+ * so the model spent a whole turn on `tool_search` just to load it (bench 41:
824
+ * ~6 s per fan-out). Promote it as soon as a spawn succeeds instead: the tool
825
+ * list grows exactly as it would after that `tool_search`, one turn earlier,
826
+ * and sessions that never spawn keep the smaller prefix.
827
+ */
828
+ promoteWaitAgentAfterSpawn() {
829
+ const index = this.tools.findIndex((t) => t.name === "spawn_agent");
830
+ const spawn = index >= 0 ? this.tools[index] : undefined;
831
+ if (!spawn)
832
+ return;
833
+ this.tools[index] = {
834
+ ...spawn,
835
+ execute: async (args, context) => {
836
+ const result = await spawn.execute(args, context);
837
+ if (!this.tools.some((t) => t.name === "wait_agent")) {
838
+ this.tools.push(...(this.mcpCatalog?.promote(["wait_agent"]) ?? []));
839
+ }
840
+ return result;
841
+ },
842
+ };
843
+ }
811
844
  /** Append tools, replacing any same-named entry (cached stub → live tool). */
812
845
  replaceOrPushTools(tools) {
813
846
  for (const tool of tools) {
@@ -1255,8 +1288,13 @@ export class AgentSession {
1255
1288
  const command = typeof args.command === "string" ? args.command : "";
1256
1289
  const classification = classifyVerificationCommand(command);
1257
1290
  if (classification.accepted || classification.snapshotEligible) {
1258
- if (args.run_in_background === true && !event.isError && args.persist !== true) {
1259
- const id = /^ID:\s*(\S+)/m.exec(event.result)?.[1];
1291
+ // A foreground check that outlived the default budget was moved to
1292
+ // the background, not failed: track it to its real exit the same way.
1293
+ const autoBackgroundId = event.isError ? undefined : autoBackgroundedId(event.result);
1294
+ if ((autoBackgroundId !== undefined || args.run_in_background === true) &&
1295
+ !event.isError &&
1296
+ args.persist !== true) {
1297
+ const id = autoBackgroundId ?? /^ID:\s*(\S+)/m.exec(event.result)?.[1];
1260
1298
  // No parseable ID means the check cannot be tracked to a real exit
1261
1299
  // code — no evidence either way. Recording a FAILURE here made
1262
1300
  // every later green run of a different spelling look owed.
@@ -1907,8 +1945,23 @@ export class AgentSession {
1907
1945
  this.settingsManager.get("verificationGateEnabled") &&
1908
1946
  (!this.opts.allowedTools || this.opts.allowedTools.includes("bash"))) {
1909
1947
  const verificationReason = this.verificationGate.pendingReason();
1948
+ const pendingFiles = this.verificationGate.pendingFiles();
1910
1949
  const verificationFollowUp = this.verificationGate.followUp();
1911
1950
  if (verificationFollowUp) {
1951
+ // Name the tests that actually reach the unverified files, with the
1952
+ // command that runs exactly those, so the check is targeted rather
1953
+ // than guessed. Best-effort: no index or no runner leaves it unchanged.
1954
+ if (this.testImpact &&
1955
+ (verificationReason === "initial" || verificationReason === "recheck")) {
1956
+ const impactLine = await this.testImpact
1957
+ .impactFor(pendingFiles)
1958
+ .then(formatImpactForVerification)
1959
+ .catch(() => "");
1960
+ const first = verificationFollowUp[0];
1961
+ if (impactLine && first?.role === "user" && typeof first.content === "string") {
1962
+ verificationFollowUp[0] = { ...first, content: first.content + impactLine };
1963
+ }
1964
+ }
1912
1965
  log("INFO", "verification-gate", "Injecting verification follow-up", {});
1913
1966
  // Announce, THEN disarm: clients release held text on disarm, so the
1914
1967
  // reverse order paints the draft and immediately deletes it — the exact
@@ -2262,9 +2315,15 @@ export class AgentSession {
2262
2315
  maxToolResultChars: resolveSessionToolResultCharLimit(this.model, this.provider, accountId),
2263
2316
  // Aggregate per-turn budget across parallel tool results (fan-out guard).
2264
2317
  maxTurnToolResultChars: resolveSessionTurnToolResultCharLimit(this.model, this.provider, accountId),
2318
+ // Warn when web/MCP output contains instruction-like text (see injection-detect.ts).
2319
+ transformToolResult: flagUntrustedToolResult,
2265
2320
  // Self-correction hooks (same as the TUI): loop-break + re-grounding are
2266
2321
  // polled mid-loop; the ideal review is polled when the agent would stop.
2267
2322
  getSteeringMessages: () => this.getHookSteeringMessages(),
2323
+ onSteeringAvailable: (listener) => {
2324
+ this.steeringListeners.add(listener);
2325
+ return () => this.steeringListeners.delete(listener);
2326
+ },
2268
2327
  getFollowUpMessages: () => this.getHookFollowUpMessages(),
2269
2328
  onTurnBudgetExhausted: (ctx) => this.shouldExtendTurnBudget(ctx),
2270
2329
  // Check authoritative provider usage before every model/tool step.
@@ -3201,6 +3260,31 @@ export class AgentSession {
3201
3260
  : undefined;
3202
3261
  return costUsd === undefined ? { used, size } : { used, size, costUsd };
3203
3262
  }
3263
+ /**
3264
+ * Whether the provider's prompt cache has likely lapsed since the last
3265
+ * successful request, and how many tokens the next message would re-read at
3266
+ * full price. Null when the route has no known TTL or the chat is empty.
3267
+ */
3268
+ getCacheExpiryStatus(now = Date.now()) {
3269
+ const status = assessCacheExpiry({
3270
+ current: {
3271
+ provider: this.provider,
3272
+ model: this.model,
3273
+ policy: resolveCacheTtl({
3274
+ provider: this.provider,
3275
+ model: this.model,
3276
+ cacheRetention: this.isSpeedOptimized() ? "long" : "short",
3277
+ baseUrl: this.baseUrl,
3278
+ accountId: this.lastAccountId,
3279
+ }),
3280
+ },
3281
+ lastTouch: this.cacheDiagnostics.lastCacheTouch(),
3282
+ now,
3283
+ prefixTokens: this.getContextUsage().used,
3284
+ hasHistory: this.messages.some((m) => m.role === "user"),
3285
+ });
3286
+ return status && { ...status, sessionId: this.sessionId || this.transportSessionId };
3287
+ }
3204
3288
  getPlanMode() {
3205
3289
  return this.planModeRef.current;
3206
3290
  }
@@ -3225,6 +3309,9 @@ export class AgentSession {
3225
3309
  queueMessage(text, attachments = []) {
3226
3310
  this.queueSeq += 1;
3227
3311
  this.userQueue.push({ id: `q${this.queueSeq}`, text, attachments });
3312
+ // Instant interrupt: preempt running tools so the steer lands right away.
3313
+ for (const listener of [...this.steeringListeners])
3314
+ listener();
3228
3315
  return this.userQueue.length;
3229
3316
  }
3230
3317
  /** Pending queued messages (id + text), oldest first, for client display. */
@@ -3277,6 +3364,15 @@ export class AgentSession {
3277
3364
  return `No background process with id "${id}"`;
3278
3365
  return this.processManager.stop(id);
3279
3366
  }
3367
+ /**
3368
+ * Force-stop every background process tree, synchronously. Background
3369
+ * commands run in their own process group, so the daemon's group kill on
3370
+ * quit never reaches them: this is the only thing that does. Callers on a
3371
+ * shutdown deadline run it before awaiting anything that can hang.
3372
+ */
3373
+ stopBackgroundProcesses() {
3374
+ this.processManager?.shutdownAll();
3375
+ }
3280
3376
  /** Replace a host-owned system prompt in place without resetting conversation history. */
3281
3377
  setCustomSystemPrompt(systemPrompt, promptCacheKeyPrefix) {
3282
3378
  this.customSystemPrompt = systemPrompt;
@@ -4026,6 +4122,21 @@ export class AgentSession {
4026
4122
  return { ok: false, reason: "thinking_budget_incompatible" };
4027
4123
  }
4028
4124
  this.lastPrewarmAt = Date.now();
4125
+ const warmedPolicy = resolveCacheTtl({
4126
+ provider: this.provider,
4127
+ model: this.model,
4128
+ cacheRetention,
4129
+ baseUrl: this.baseUrl ?? creds.baseUrl,
4130
+ accountId: creds.accountId,
4131
+ });
4132
+ if (warmedPolicy) {
4133
+ this.cacheDiagnostics.noteCacheTouch({
4134
+ at: started,
4135
+ provider: this.provider,
4136
+ model: this.model,
4137
+ policy: warmedPolicy,
4138
+ });
4139
+ }
4029
4140
  log("INFO", "prewarm", "Cache prewarm complete", {
4030
4141
  tokens: String(tokens),
4031
4142
  cacheRead: String(usage.cacheRead ?? 0),
@@ -4074,6 +4185,9 @@ export class AgentSession {
4074
4185
  return this.getPromptCacheKey();
4075
4186
  }
4076
4187
  async dispose() {
4188
+ // First and synchronous: nothing below may delay this, or a hung teardown
4189
+ // step leaves background commands running after the app has quit.
4190
+ this.stopBackgroundProcesses();
4077
4191
  // Quiesce any in-flight post-turn compaction BEFORE tearing down state:
4078
4192
  // the background compact() snapshots and replaces `this.messages`, so
4079
4193
  // letting it run past this point would checkpoint a near-empty history
@@ -4083,8 +4197,10 @@ export class AgentSession {
4083
4197
  this.cacheDiagnostics.reset();
4084
4198
  this.diagnosticsRecorder?.finalize();
4085
4199
  this.managerAbortSignal?.removeEventListener("abort", this.managerAbortHandler);
4086
- this.processManager?.shutdownAll();
4200
+ // Again, in case a turn racing teardown started one while we awaited.
4201
+ this.stopBackgroundProcesses();
4087
4202
  this.lspManager?.shutdownAll();
4203
+ this.debugManager?.shutdown();
4088
4204
  await Promise.all([this.subAgentManager?.shutdownAll(), this.mcpManager?.dispose()]);
4089
4205
  await this.extensionLoader.deactivateAll();
4090
4206
  this.setSessionPath("");