@gajae-code/coding-agent 0.16.1 → 0.16.4

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 (146) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/types/cli/args.d.ts +2 -2
  3. package/dist/types/cli/root-flags.d.ts +4 -2
  4. package/dist/types/cli.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +1 -1
  6. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/model-resolver.d.ts +9 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  11. package/dist/types/discovery/helpers.d.ts +2 -0
  12. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  13. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  14. package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
  15. package/dist/types/extensibility/shared-events.d.ts +1 -1
  16. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  18. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  20. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  21. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  22. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -0
  25. package/dist/types/modes/types.d.ts +1 -0
  26. package/dist/types/runtime-mcp/client.d.ts +9 -1
  27. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  28. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  29. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  30. package/dist/types/runtime-mcp/types.d.ts +22 -0
  31. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  32. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  33. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  34. package/dist/types/sdk/cli/rows.d.ts +7 -0
  35. package/dist/types/sdk/host/events.d.ts +1 -0
  36. package/dist/types/sdk/host/host.d.ts +2 -0
  37. package/dist/types/sdk/host/session-runtime.d.ts +23 -0
  38. package/dist/types/sdk/session-api-key.d.ts +26 -0
  39. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  40. package/dist/types/session/agent-session.d.ts +9 -0
  41. package/dist/types/setup/paseo/announce.d.ts +111 -0
  42. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  43. package/dist/types/system-prompt.d.ts +2 -0
  44. package/dist/types/task/executor.d.ts +2 -1
  45. package/dist/types/tools/fetch.d.ts +7 -0
  46. package/dist/types/tools/index.d.ts +8 -2
  47. package/dist/types/tools/path-utils.d.ts +2 -0
  48. package/dist/types/tools/search.d.ts +6 -0
  49. package/dist/types/utils/external-editor.d.ts +5 -0
  50. package/package.json +7 -7
  51. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  52. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  53. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  54. package/src/cli/args.ts +20 -9
  55. package/src/cli/root-flags.ts +5 -3
  56. package/src/config/autorouting-tier-map.ts +1 -0
  57. package/src/config/file-lock.ts +181 -17
  58. package/src/config/model-preset-registry.ts +5 -3
  59. package/src/config/model-profiles.ts +40 -0
  60. package/src/config/model-registry.ts +57 -11
  61. package/src/config/model-resolver.ts +48 -0
  62. package/src/config/settings-schema.ts +33 -1
  63. package/src/config/settings.ts +1 -0
  64. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  65. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  66. package/src/discovery/builtin.ts +13 -4
  67. package/src/discovery/claude-plugins.ts +12 -4
  68. package/src/discovery/helpers.ts +10 -2
  69. package/src/extensibility/gjc-plugins/compiler.ts +208 -58
  70. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  71. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  72. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  73. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  74. package/src/extensibility/runtime-skill-discovery.ts +77 -151
  75. package/src/extensibility/shared-events.ts +1 -1
  76. package/src/extensibility/skills.ts +44 -16
  77. package/src/gjc-runtime/repository-binding.ts +1 -1
  78. package/src/gjc-runtime/session-state-lock.ts +94 -6
  79. package/src/internal-urls/docs-index.generated.ts +10 -10
  80. package/src/main.ts +72 -34
  81. package/src/modes/DESIGN.md +54 -0
  82. package/src/modes/acp/acp-agent.ts +29 -14
  83. package/src/modes/components/assistant-message.ts +22 -9
  84. package/src/modes/components/model-selector.ts +166 -33
  85. package/src/modes/components/settings-selector.ts +1075 -235
  86. package/src/modes/components/status-line/command.ts +190 -0
  87. package/src/modes/components/status-line/index.ts +1 -0
  88. package/src/modes/components/status-line/segments.ts +32 -5
  89. package/src/modes/components/status-line/types.ts +5 -0
  90. package/src/modes/components/tool-status-header.ts +393 -21
  91. package/src/modes/controllers/command-controller.ts +2 -0
  92. package/src/modes/controllers/event-controller.ts +148 -5
  93. package/src/modes/controllers/selector-controller.ts +21 -0
  94. package/src/modes/interactive-mode.ts +47 -1
  95. package/src/modes/types.ts +1 -0
  96. package/src/modes/utils/ui-helpers.ts +1 -0
  97. package/src/prompts/system/system-prompt.md +8 -0
  98. package/src/prompts/tools/bash.md +1 -1
  99. package/src/prompts/tools/job.md +1 -1
  100. package/src/prompts/tools/search.md +2 -0
  101. package/src/runtime-mcp/client.ts +60 -17
  102. package/src/runtime-mcp/manager.ts +171 -14
  103. package/src/runtime-mcp/pool.test.ts +209 -0
  104. package/src/runtime-mcp/pool.ts +224 -23
  105. package/src/runtime-mcp/transports/stdio.ts +158 -24
  106. package/src/runtime-mcp/types.ts +24 -0
  107. package/src/sdk/acp/adapter.ts +1 -1
  108. package/src/sdk/broker/broker.ts +5 -4
  109. package/src/sdk/bus/chat-daemon-control.ts +1 -1
  110. package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
  111. package/src/sdk/bus/slack-daemon.ts +92 -2
  112. package/src/sdk/bus/slack-live-provider.ts +12 -0
  113. package/src/sdk/bus/slack-provider.ts +5 -0
  114. package/src/sdk/cli/rows.ts +47 -2
  115. package/src/sdk/cli/session-cli.ts +47 -23
  116. package/src/sdk/host/events.ts +25 -2
  117. package/src/sdk/host/host.ts +4 -1
  118. package/src/sdk/host/session-runtime.test.ts +36 -25
  119. package/src/sdk/host/session-runtime.ts +279 -43
  120. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  121. package/src/sdk/router/session-router.ts +60 -37
  122. package/src/sdk/session-api-key.ts +94 -0
  123. package/src/sdk/session.ts +73 -30
  124. package/src/sdk/transport/serve-cli.ts +3 -3
  125. package/src/session/agent-session.ts +123 -38
  126. package/src/session/contribution-prep.ts +214 -21
  127. package/src/setup/paseo/announce.ts +450 -0
  128. package/src/setup/paseo/setup-deps.ts +15 -3
  129. package/src/slash-commands/builtin-registry.ts +1 -0
  130. package/src/system-prompt.ts +4 -0
  131. package/src/task/executor.ts +2 -1
  132. package/src/task/index.ts +17 -4
  133. package/src/tools/bash.ts +50 -21
  134. package/src/tools/cron.ts +1 -1
  135. package/src/tools/fetch.ts +29 -1
  136. package/src/tools/index.ts +8 -2
  137. package/src/tools/job.ts +8 -2
  138. package/src/tools/monitor.ts +8 -2
  139. package/src/tools/path-utils.ts +7 -0
  140. package/src/tools/search.ts +116 -58
  141. package/src/tools/skill.ts +22 -2
  142. package/src/tools/subagent.ts +3 -1
  143. package/src/tools/tool-catalog.generated.ts +9 -2
  144. package/src/utils/external-editor.ts +16 -2
  145. package/src/web/insane/bridge.ts +113 -23
  146. package/src/web/search/providers/perplexity.ts +102 -41
@@ -36,6 +36,7 @@ import {
36
36
  canContinuePersistedHistory,
37
37
  dispatchedToolIdentity,
38
38
  getAgentTerminalOwnerContext,
39
+ isContinuingMidRunMaintenanceOutcome,
39
40
  isNonDispatchedToolEvent,
40
41
  type ManagedAttemptContinuationOwnership,
41
42
  type ManagedAttemptDecision,
@@ -882,6 +883,10 @@ export interface AgentSessionConfig {
882
883
  skills?: Skill[];
883
884
  /** Skill loading warnings (already captured by SDK) */
884
885
  skillWarnings?: SkillWarning[];
886
+ /** Re-enumerate filesystem and marketplace skills after plugin lifecycle changes. */
887
+ reloadSkills?: (cwd: string) => Promise<{ skills: Skill[]; warnings: SkillWarning[] }>;
888
+ /** Publish a successfully committed skill catalog to SDK/process snapshots. */
889
+ onSkillsReloaded?: (skills: Skill[], warnings: SkillWarning[]) => void;
885
890
  /** Custom commands (TypeScript slash commands) */
886
891
  customCommands?: LoadedCustomCommand[];
887
892
  skillsSettings?: SkillsSettings;
@@ -1044,7 +1049,7 @@ type AutoCompactionTerminalStatus =
1044
1049
  | { kind: "compacted"; continuationScheduled?: boolean }
1045
1050
  | { kind: "aborted"; source: "signal" | "hook" }
1046
1051
  | { kind: "skipped"; continuationScheduled?: boolean }
1047
- | { kind: "failed" };
1052
+ | { kind: "failed"; errorMessage: string };
1048
1053
 
1049
1054
  type ToolOutputPruneResult = {
1050
1055
  prunedCount: number;
@@ -3124,6 +3129,8 @@ export class AgentSession {
3124
3129
 
3125
3130
  #skills: Skill[];
3126
3131
  #skillWarnings: SkillWarning[];
3132
+ #reloadSkills: AgentSessionConfig["reloadSkills"];
3133
+ #onSkillsReloaded: AgentSessionConfig["onSkillsReloaded"];
3127
3134
 
3128
3135
  // Custom commands (TypeScript slash commands)
3129
3136
  #customCommands: LoadedCustomCommand[] = [];
@@ -4556,6 +4563,8 @@ export class AgentSession {
4556
4563
  this.agent.setMainAttemptScopeObserver(scope => this.#bindAttemptScope(scope));
4557
4564
  this.#skills = config.skills ?? [];
4558
4565
  this.#skillWarnings = config.skillWarnings ?? [];
4566
+ this.#reloadSkills = config.reloadSkills;
4567
+ this.#onSkillsReloaded = config.onSkillsReloaded;
4559
4568
  this.#customCommands = config.customCommands ?? [];
4560
4569
  this.#skillsSettings = config.skillsSettings;
4561
4570
  this.#modelRegistry = config.modelRegistry;
@@ -5638,6 +5647,27 @@ export class AgentSession {
5638
5647
  if (failures.length > 0) throw new AggregateError(failures, "Tool session cleanup failed.");
5639
5648
  }
5640
5649
 
5650
+ #claimToolSessionTransitionCleanups(): Promise<void> {
5651
+ const cleanups = Array.from(this.#toolSessionTransitionCleanups);
5652
+ this.#toolSessionTransitionCleanups.clear();
5653
+ return this.#settleClaimedToolSessionCleanups(cleanups, "Tool session transition cleanup failed.");
5654
+ }
5655
+
5656
+ #claimToolSessionCleanups(): Promise<void> {
5657
+ const cleanups = Array.from(this.#toolSessionCleanups);
5658
+ this.#toolSessionCleanups.clear();
5659
+ return this.#settleClaimedToolSessionCleanups(cleanups, "Tool session cleanup failed.");
5660
+ }
5661
+
5662
+ async #settleClaimedToolSessionCleanups(
5663
+ cleanups: readonly (() => Promise<void> | void)[],
5664
+ message: string,
5665
+ ): Promise<void> {
5666
+ const results = await Promise.allSettled(cleanups.map(async cleanup => await cleanup()));
5667
+ const failures = results.flatMap(result => (result.status === "rejected" ? [result.reason] : []));
5668
+ if (failures.length > 0) throw new AggregateError(failures, message);
5669
+ }
5670
+
5641
5671
  getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
5642
5672
  // The session's OWNED manager first: /jobs, the status header, and the
5643
5673
  // active-background count must reflect THIS session's jobs — with
@@ -5918,7 +5948,9 @@ export class AgentSession {
5918
5948
  const canonicalAdmission = this.#reserveCanonicalMessageAdmission(event);
5919
5949
  const terminalOwner = event.type === "agent_end" ? getAgentTerminalOwnerContext(event) : undefined;
5920
5950
  const maintenanceCheckpoint =
5921
- event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted";
5951
+ event.type === "agent_end" &&
5952
+ event.stopReason === "maintenance" &&
5953
+ isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome);
5922
5954
  const terminalClaim =
5923
5955
  maintenanceCheckpoint || !terminalOwner
5924
5956
  ? undefined
@@ -6196,9 +6228,13 @@ export class AgentSession {
6196
6228
  this.#requestWorkerIntegrationAttempt();
6197
6229
  }
6198
6230
  // A maintenance agent_end is an internal checkpoint only while another
6199
- // continuation will follow. An aborted maintenance run is its terminal
6200
- // settlement and must reach public subscribers.
6201
- if (event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted")
6231
+ // continuation will follow. Failed or aborted maintenance is terminal and
6232
+ // must reach public subscribers.
6233
+ if (
6234
+ event.type === "agent_end" &&
6235
+ event.stopReason === "maintenance" &&
6236
+ isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome)
6237
+ )
6202
6238
  return;
6203
6239
 
6204
6240
  // Hold agent_end until the prompt's finally and all earlier async event work
@@ -6983,7 +7019,7 @@ export class AgentSession {
6983
7019
 
6984
7020
  if (
6985
7021
  event.type === "agent_end" &&
6986
- !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted")
7022
+ !(event.stopReason === "maintenance" && isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome))
6987
7023
  ) {
6988
7024
  this.#releaseDeferredSdkFollowUps();
6989
7025
  }
@@ -6996,28 +7032,33 @@ export class AgentSession {
6996
7032
  // the same run on the rewritten context (no synthetic prompt), skipping
6997
7033
  // the goal-runtime / skill-state / retry / compaction finalization a
6998
7034
  // normal agent_end runs — none of that applies to an in-progress run.
6999
- // "aborted" settles without resuming; the generation guard drops the
7000
- // continuation if a newer prompt/abort has moved the run on.
7035
+ // Failed and aborted outcomes fall through to terminal handling. The
7036
+ // generation guard drops a successful continuation if a newer prompt/abort
7037
+ // has moved the run on.
7001
7038
  if (event.stopReason === "maintenance") {
7002
- this.#lastAssistantMessage = undefined;
7003
- const outcome = event.maintenanceOutcome;
7004
- if (
7005
- outcome &&
7006
- outcome !== "aborted" &&
7007
- !maintenanceWasDisposed &&
7008
- !this.#isDisposed &&
7009
- maintenanceGeneration !== undefined &&
7010
- this.#promptGeneration === maintenanceGeneration
7011
- ) {
7012
- this.#scheduleAgentContinue({
7013
- generation: maintenanceGeneration,
7014
- skipCompactionCheck: true,
7015
- resourceRunId: activePromptHandle,
7016
- maintenanceContinuation: true,
7017
- sdkRunToken: attemptScope ? this.#sdkRunTokensByAttemptScope.get(attemptScope) : undefined,
7018
- });
7039
+ if (isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome)) {
7040
+ this.#lastAssistantMessage = undefined;
7041
+ if (
7042
+ !maintenanceWasDisposed &&
7043
+ !this.#isDisposed &&
7044
+ maintenanceGeneration !== undefined &&
7045
+ this.#promptGeneration === maintenanceGeneration
7046
+ ) {
7047
+ this.#scheduleAgentContinue({
7048
+ generation: maintenanceGeneration,
7049
+ skipCompactionCheck: true,
7050
+ resourceRunId: activePromptHandle,
7051
+ maintenanceContinuation: true,
7052
+ sdkRunToken: attemptScope ? this.#sdkRunTokensByAttemptScope.get(attemptScope) : undefined,
7053
+ });
7054
+ }
7055
+ return;
7019
7056
  }
7020
- return;
7057
+ // Cancellation already owns teardown and terminal publication. Running
7058
+ // normal post-turn finalization here can deadlock abort/dispose while the
7059
+ // maintenance barrier is unwinding. Failed maintenance alone carries the
7060
+ // synthetic assistant error that must flow through terminal handling.
7061
+ if (event.maintenanceOutcome === "aborted") return;
7021
7062
  }
7022
7063
  const usage = this.getSessionStats().tokens;
7023
7064
  await this.#goalRuntime.onAgentEnd({
@@ -7083,6 +7124,13 @@ export class AgentSession {
7083
7124
  return;
7084
7125
  }
7085
7126
  this.#lastSuccessfulYieldToolCallId = undefined;
7127
+ if (event.stopReason === "maintenance" && event.maintenanceOutcome === "failed") {
7128
+ // The maintenance error already terminalized the run. Retrying this
7129
+ // synthetic assistant failure would resubmit the unchanged context that
7130
+ // maintenance failed to rewrite.
7131
+ this.#resolveRetry();
7132
+ return;
7133
+ }
7086
7134
 
7087
7135
  // Check for retryable errors first (overloaded, rate limit, server errors)
7088
7136
  if (this.#isRetryableError(msg)) {
@@ -8580,7 +8628,8 @@ export class AgentSession {
8580
8628
  ? this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope)
8581
8629
  : undefined;
8582
8630
  const isTerminalAgentEnd =
8583
- event.type === "agent_end" && !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted");
8631
+ event.type === "agent_end" &&
8632
+ !(event.stopReason === "maintenance" && isContinuingMidRunMaintenanceOutcome(event.maintenanceOutcome));
8584
8633
  const finishAttempt = () => {
8585
8634
  if (!isTerminalAgentEnd) return;
8586
8635
  // A merged wake batch whose flush fired while still streaming cleared its
@@ -9371,10 +9420,10 @@ export class AgentSession {
9371
9420
  // one here left signal exit orphaning the subprocess. Each runner clears
9372
9421
  // its set, so a graceful dispose running first makes this a no-op rather
9373
9422
  // than a double free.
9374
- this.#runToolSessionCleanups().catch((error: unknown) =>
9423
+ this.#claimToolSessionCleanups().catch((error: unknown) =>
9375
9424
  logger.warn("signal teardown: tool session cleanups failed", { error }),
9376
9425
  ),
9377
- this.#runToolSessionTransitionCleanups().catch((error: unknown) =>
9426
+ this.#claimToolSessionTransitionCleanups().catch((error: unknown) =>
9378
9427
  logger.warn("signal teardown: tool session transition cleanups failed", { error }),
9379
9428
  ),
9380
9429
  ]);
@@ -14200,6 +14249,25 @@ export class AgentSession {
14200
14249
  await this.refreshBaseSystemPrompt();
14201
14250
  }
14202
14251
 
14252
+ /** Refresh the live registered-skill catalog after install/remove/reload. */
14253
+ async reloadSkills(cwd = this.sessionManager.getCwd()): Promise<void> {
14254
+ if (!this.#reloadSkills) return;
14255
+ const reloaded = await this.#reloadSkills(cwd);
14256
+ const previousSkills = this.#skills;
14257
+ const previousWarnings = this.#skillWarnings;
14258
+ this.#skills = reloaded.skills;
14259
+ this.#skillWarnings = reloaded.warnings;
14260
+ try {
14261
+ await this.refreshBaseSystemPrompt();
14262
+ } catch (error) {
14263
+ this.#skills = previousSkills;
14264
+ this.#skillWarnings = previousWarnings;
14265
+ await this.refreshBaseSystemPrompt().catch(() => {});
14266
+ throw error;
14267
+ }
14268
+ this.#onSkillsReloaded?.(reloaded.skills, reloaded.warnings);
14269
+ }
14270
+
14203
14271
  /**
14204
14272
  * Retire the cached workspace tree after a rescope so the next turn re-scans
14205
14273
  * at the new cwd instead of re-presenting the abandoned launcher root.
@@ -18407,18 +18475,23 @@ export class AgentSession {
18407
18475
  * prune → promote → compact machinery and returns an explicit outcome.
18408
18476
  *
18409
18477
  * A non-"not-needed" outcome ends the current run losslessly
18410
- * (`agent_end.stopReason === "maintenance"`); the `agent_end` handler is the
18411
- * single continuation owner that resumes the run with no synthetic prompt.
18412
- * This method never self-continues, runs goal-runtime hooks, clears skill
18413
- * state, or emits a user-facing pause.
18478
+ * (`agent_end.stopReason === "maintenance"`). The `agent_end` handler resumes
18479
+ * committed maintenance with no synthetic prompt and terminalizes failed or
18480
+ * aborted maintenance. This method never self-continues, runs goal-runtime
18481
+ * hooks, clears skill state, or emits a user-facing pause.
18414
18482
  */
18415
18483
  async #runMidRunMaintenance(
18416
18484
  context: AgentContext,
18417
18485
  lifecycle: MidRunMaintenanceLifecycle,
18418
18486
  ): Promise<ContextMaintenanceResult> {
18419
- const result = (outcome: MidRunMaintenanceOutcome, releaseCurrentContext = false): ContextMaintenanceResult => ({
18487
+ const result = (
18488
+ outcome: MidRunMaintenanceOutcome,
18489
+ releaseCurrentContext = false,
18490
+ errorMessage?: string,
18491
+ ): ContextMaintenanceResult => ({
18420
18492
  outcome,
18421
18493
  ...(releaseCurrentContext ? { releaseCurrentContext: true } : {}),
18494
+ ...(errorMessage ? { errorMessage } : {}),
18422
18495
  });
18423
18496
  if (this.#isDisposed) return { outcome: "aborted" };
18424
18497
  const invocationController = new AbortController();
@@ -18529,7 +18602,13 @@ export class AgentSession {
18529
18602
  if (compactionStatus.kind === "aborted") {
18530
18603
  return compactionStatus.source === "hook" ? result("not-needed") : result("aborted");
18531
18604
  }
18532
- return result("failed");
18605
+ return result(
18606
+ "failed",
18607
+ false,
18608
+ compactionStatus.kind === "failed"
18609
+ ? compactionStatus.errorMessage
18610
+ : "Context maintenance could not commit a compaction before the next model request.",
18611
+ );
18533
18612
  } finally {
18534
18613
  this.#activeMidRunBarrierControllers.delete(invocationController);
18535
18614
  }
@@ -19643,12 +19722,15 @@ export class AgentSession {
19643
19722
  */
19644
19723
  #maintenanceProviderTransport(): {
19645
19724
  sessionId: string | undefined;
19725
+ providerSessionId: string | undefined;
19646
19726
  providerSessionState: Map<string, ProviderSessionState>;
19647
19727
  preferWebsockets: boolean | undefined;
19648
19728
  remoteCompactionFallbackHealth: RemoteCompactionFallbackHealthHooks;
19649
19729
  } {
19730
+ const providerSessionId = this.agent.providerSessionId ?? this.agent.sessionId;
19650
19731
  return {
19651
- sessionId: this.agent.providerSessionId ?? this.agent.sessionId,
19732
+ sessionId: providerSessionId,
19733
+ providerSessionId,
19652
19734
  providerSessionState: this.#providerSessionState,
19653
19735
  preferWebsockets: this.agent.preferWebsockets,
19654
19736
  remoteCompactionFallbackHealth: this.#remoteCompactionFallbackHealth,
@@ -20286,7 +20368,7 @@ export class AgentSession {
20286
20368
  ? `Context overflow recovery failed: ${errorMessage}`
20287
20369
  : `Auto-compaction failed: ${errorMessage}`,
20288
20370
  });
20289
- return { kind: "failed" };
20371
+ return { kind: "failed", errorMessage };
20290
20372
  } finally {
20291
20373
  if (this.#autoCompactionAbortController === autoCompactionAbortController) {
20292
20374
  this.#autoCompactionAbortController = undefined;
@@ -22350,7 +22432,10 @@ export class AgentSession {
22350
22432
  onChunk,
22351
22433
  signal: abortController.signal,
22352
22434
  });
22353
- this.recordPythonResult(code, result, options);
22435
+ // A retained kernel can finish after strict session close has fenced
22436
+ // persistence. The caller still receives its completed result, but it
22437
+ // must not reopen a closing session to append history.
22438
+ if (!this.#evalExecutionDisposing) this.recordPythonResult(code, result, options);
22354
22439
  return result;
22355
22440
  })();
22356
22441
  return await this.trackEvalExecution(execution, abortController);
@@ -3,7 +3,6 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import type { AgentMessage } from "@gajae-code/agent-core";
5
5
  import type { AssistantMessage, ToolResultMessage, UserMessage } from "@gajae-code/ai/core";
6
- import { $ } from "bun";
7
6
  import { resolveGjcCommand } from "../task/gjc-command";
8
7
  import { shortenPath } from "../tools/render-utils";
9
8
 
@@ -12,6 +11,12 @@ export const CONTRIBUTION_PREP_SCHEMA_VERSION = 1;
12
11
  const MAX_TRANSCRIPT_MESSAGES = 20;
13
12
  const MAX_TEXT_CHARS = 12000;
14
13
  const MAX_GIT_OUTPUT_CHARS = 60000;
14
+ const MAX_REDACTION_INPUT_CHARS = 1_000_000;
15
+ // Keep whole Git captures within the redactor's input budget; discard an overrun
16
+ // instead of emitting a prefix.
17
+ const MAX_GIT_RAW_OUTPUT_BYTES = MAX_REDACTION_INPUT_CHARS;
18
+ const MAX_JSON_TOKENS = 20_000;
19
+ const MAX_JSON_REPLACEMENTS = 10_000;
15
20
 
16
21
  export interface ContributionPrepArtifact {
17
22
  path: string;
@@ -72,12 +77,133 @@ function replaceRegex(text: string, regex: RegExp, replacement: string, state: R
72
77
  return text.replace(regex, replacement);
73
78
  }
74
79
 
80
+ function redactAwsAccessKeyIds(text: string, state: RedactionState): string {
81
+ return replaceRegex(
82
+ text,
83
+ /(^|[^0-9A-Za-z])(?:AKIA|ASIA)[0-9A-Z]{16}(?![0-9A-Za-z])/g,
84
+ "$1[REDACTED_AWS_KEY_ID]",
85
+ state,
86
+ "aws_keys",
87
+ );
88
+ }
89
+
90
+ function isAwsSecretField(value: string): boolean {
91
+ const normalized = value.toLowerCase().replaceAll(/[_-]/g, "");
92
+ return (
93
+ normalized === "secretaccesskey" ||
94
+ normalized === "sessiontoken" ||
95
+ normalized === "awssecretaccesskey" ||
96
+ normalized === "awssessiontoken" ||
97
+ normalized === "xamzsecuritytoken"
98
+ );
99
+ }
100
+
101
+ function redactAwsLabeledValues(text: string, state: RedactionState): string {
102
+ let redacted = redactAwsAccessKeyIds(text, state);
103
+ redacted = replaceRegex(
104
+ redacted,
105
+ // STS XML responses are commonly pretty-printed, so the value sits on its own
106
+ // line between the tags. `[^<]` still refuses to cross into another element,
107
+ // while the call-wide input bound caps work without imposing a credential-size
108
+ // assumption. The lower bound leaves empty or whitespace-only bodies alone.
109
+ /(<((?:[A-Za-z_][\w.-]*:)?(?:SecretAccessKey|SessionToken))>)[^<]{8,1000000}(<\/\2>)/gi,
110
+ "$1[REDACTED_SECRET]$3",
111
+ state,
112
+ "aws_keys",
113
+ );
114
+ redacted = replaceRegex(
115
+ redacted,
116
+ /(^|[^A-Za-z0-9_-])(["']?(?:(?:aws[_-]?)?secret[_-]?access[_-]?key|(?:aws[_-]?)?session[_-]?token|x[_-]?amz[_-]?security[_-]?token)["']?\s*[=:]\s*)(\$?)(["'`])([^"'`\r\n]{8,})\4/gi,
117
+ "$1$2$3$4[REDACTED_SECRET]$4",
118
+ state,
119
+ "aws_keys",
120
+ );
121
+ return replaceRegex(
122
+ redacted,
123
+ /(^|[^A-Za-z0-9_-])(["']?(?:(?:aws[_-]?)?secret[_-]?access[_-]?key|(?:aws[_-]?)?session[_-]?token|x[_-]?amz[_-]?security[_-]?token)["']?\s*[=:]\s*)[^\s"'`,;{}[\]()&<>#]{8,}/gi,
124
+ "$1$2[REDACTED_SECRET]",
125
+ state,
126
+ "aws_keys",
127
+ );
128
+ }
129
+
130
+ function redactAwsJsonStrings(text: string, state: RedactionState, depth = 0): string {
131
+ const tokens = [...text.matchAll(/"(?:\\(?:["\\/bfnrt]|u[0-9A-Fa-f]{4})|[^"\\\r\n])*"/g)].map(match => ({
132
+ start: match.index,
133
+ end: match.index + match[0].length,
134
+ raw: match[0],
135
+ }));
136
+ if (tokens.length > MAX_JSON_TOKENS) {
137
+ state.labels.add("oversized_content");
138
+ return "[REDACTED_OVERSIZED_CONTENT]";
139
+ }
140
+ const replacements = new Map<number, { end: number; value: string }>();
141
+
142
+ for (const [index, token] of tokens.entries()) {
143
+ let decoded: string;
144
+ try {
145
+ decoded = JSON.parse(token.raw) as string;
146
+ } catch {
147
+ continue;
148
+ }
149
+
150
+ if (isAwsSecretField(decoded)) {
151
+ const separator = /^\s*:\s*/.exec(text.slice(token.end));
152
+ const valueToken = tokens[index + 1];
153
+ if (separator && valueToken?.start === token.end + separator[0].length) {
154
+ let value: string;
155
+ try {
156
+ value = JSON.parse(valueToken.raw) as string;
157
+ } catch {
158
+ value = "";
159
+ }
160
+ if (value.length >= 8)
161
+ replacements.set(valueToken.start, { end: valueToken.end, value: JSON.stringify("[REDACTED_SECRET]") });
162
+ }
163
+ }
164
+
165
+ const nestedRedacted = depth >= 3 ? "[REDACTED_NESTED_CONTENT]" : redactAwsJsonStrings(decoded, state, depth + 1);
166
+ const redacted = redactAwsLabeledValues(nestedRedacted, state);
167
+ if (redacted !== decoded && !replacements.has(token.start)) {
168
+ replacements.set(token.start, { end: token.end, value: JSON.stringify(redacted) });
169
+ }
170
+ }
171
+
172
+ if (replacements.size === 0) return text;
173
+ if (replacements.size > MAX_JSON_REPLACEMENTS) {
174
+ state.labels.add("oversized_content");
175
+ return "[REDACTED_OVERSIZED_CONTENT]";
176
+ }
177
+ state.labels.add("aws_keys");
178
+ const chunks: string[] = [];
179
+ let cursor = 0;
180
+ for (const [start, replacement] of [...replacements.entries()].sort(([left], [right]) => left - right)) {
181
+ chunks.push(text.slice(cursor, start), replacement.value);
182
+ cursor = replacement.end;
183
+ }
184
+ chunks.push(text.slice(cursor));
185
+ return chunks.join("");
186
+ }
187
+
188
+ function assertSafeArtifactPath(artifactDir: string): void {
189
+ const state: RedactionState = { labels: new Set() };
190
+ redactContributionPrepText(artifactDir, path.join(artifactDir, "__gjc_path_probe__"), state);
191
+ if (["tokens", "aws_keys", "provider_keys", "auth_headers", "cookies"].some(label => state.labels.has(label))) {
192
+ throw new Error("Contribution prep artifact path contains credential-like material.");
193
+ }
194
+ }
195
+
75
196
  export function redactContributionPrepText(
76
197
  text: string,
77
198
  cwd: string,
78
199
  state: RedactionState = { labels: new Set() },
79
200
  ): string {
80
- let redacted = text;
201
+ if (text.length > MAX_REDACTION_INPUT_CHARS) {
202
+ state.labels.add("oversized_content");
203
+ return "[REDACTED_OVERSIZED_CONTENT]";
204
+ }
205
+ let redacted = redactAwsJsonStrings(text, state);
206
+ redacted = redactAwsLabeledValues(redacted, state);
81
207
  redacted = replaceRegex(
82
208
  redacted,
83
209
  /\b(?:sk|pk|rk|xox[baprs])-[-_A-Za-z0-9]{12,}\b/g,
@@ -150,17 +276,73 @@ function formatMessage(message: AgentMessage): string {
150
276
  return `## ${message.role}\n\n${JSON.stringify(message)}\n`;
151
277
  }
152
278
 
153
- async function gitOutput(cwd: string, args: string[], maxChars = MAX_GIT_OUTPUT_CHARS): Promise<string> {
279
+ interface BoundedGitOutput {
280
+ text: string;
281
+ oversized: boolean;
282
+ }
283
+
284
+ async function readBoundedGitOutput(stream: ReadableStream<Uint8Array>, maxBytes: number): Promise<BoundedGitOutput> {
285
+ const reader = stream.getReader();
286
+ const chunks: Uint8Array[] = [];
287
+ let totalBytes = 0;
288
+ try {
289
+ for (;;) {
290
+ const { done, value } = await reader.read();
291
+ if (done) break;
292
+ totalBytes += value.byteLength;
293
+ if (totalBytes > maxBytes) return { text: "", oversized: true };
294
+ chunks.push(value);
295
+ }
296
+ } finally {
297
+ reader.releaseLock();
298
+ }
299
+
300
+ const bytes = new Uint8Array(totalBytes);
301
+ let offset = 0;
302
+ for (const chunk of chunks) {
303
+ bytes.set(chunk, offset);
304
+ offset += chunk.byteLength;
305
+ }
306
+ return { text: new TextDecoder().decode(bytes), oversized: false };
307
+ }
308
+
309
+ async function gitOutput(
310
+ cwd: string,
311
+ args: string[],
312
+ state: RedactionState,
313
+ maxChars = MAX_GIT_OUTPUT_CHARS,
314
+ ): Promise<string> {
154
315
  try {
155
- const output = await $`git ${args}`.cwd(cwd).quiet().text();
156
- return limitText(output.trim(), maxChars);
316
+ const proc = Bun.spawn(["git", ...args], { cwd, stdin: "ignore", stdout: "pipe", stderr: "ignore" });
317
+ const stop = (): void => {
318
+ try {
319
+ proc.kill();
320
+ } catch {
321
+ // The process already exited.
322
+ }
323
+ };
324
+ try {
325
+ const output = await readBoundedGitOutput(proc.stdout, MAX_GIT_RAW_OUTPUT_BYTES);
326
+ if (output.oversized) {
327
+ stop();
328
+ await proc.exited;
329
+ state.labels.add("oversized_content");
330
+ return "[REDACTED_OVERSIZED_CONTENT]";
331
+ }
332
+ if ((await proc.exited) !== 0) return "";
333
+ return limitText(redactContributionPrepText(output.text.trim(), cwd, state), maxChars);
334
+ } catch {
335
+ stop();
336
+ await proc.exited;
337
+ return "";
338
+ }
157
339
  } catch {
158
340
  return "";
159
341
  }
160
342
  }
161
343
 
162
- async function changedFiles(cwd: string): Promise<string[]> {
163
- const output = await gitOutput(cwd, ["status", "--short"]);
344
+ async function changedFiles(cwd: string, state: RedactionState): Promise<string[]> {
345
+ const output = await gitOutput(cwd, ["status", "--short"], state);
164
346
  return output
165
347
  .split("\n")
166
348
  .map(line => line.trim())
@@ -180,15 +362,23 @@ async function writeArtifact(
180
362
  }
181
363
 
182
364
  export function buildContributionPrepWorkerPrompt(manifestPath: string): string {
365
+ const manifestName = path.basename(manifestPath);
183
366
  return [
184
367
  "Prepare a maintainer-friendly contribution draft from the redacted context dump.",
185
- "Read the manifest and referenced artifact file pointers. Do not assume transcript context was inlined here.",
186
- `Manifest: ${manifestPath}`,
368
+ "Read the manifest beside this worker prompt and its relative artifact file pointers. Do not assume transcript context was inlined here.",
369
+ `Manifest: ${manifestName}`,
187
370
  "Produce structured markdown with: title, problem summary, reproduction/context, proposed fix or implementation plan, affected files, tests to run, and uncertainty/remaining risks.",
188
371
  "Do not create GitHub issues, open PRs, push branches, or perform remote writes unless the user explicitly confirms that action in this fresh session.",
189
372
  ].join("\n");
190
373
  }
191
374
 
375
+ function safeWorkspaceLabel(cwd: string): string {
376
+ const resolved = path.resolve(cwd);
377
+ const home = os.homedir();
378
+ if (home && (resolved === home || resolved.startsWith(`${home}${path.sep}`))) return shortenPath(resolved);
379
+ return path.basename(resolved) || ".";
380
+ }
381
+
192
382
  export async function prepareContributionPrep(
193
383
  context: ContributionPrepContext,
194
384
  options: ContributionPrepOptions = {},
@@ -199,12 +389,14 @@ export async function prepareContributionPrep(
199
389
  options.artifactRoot ?? path.join(context.cwd, ".gjc", "contribution-prep"),
200
390
  safeTimestamp,
201
391
  );
392
+ assertSafeArtifactPath(artifactDir);
202
393
  await fs.mkdir(artifactDir, { recursive: true });
203
394
 
204
395
  const redactions: RedactionState = { labels: new Set() };
205
396
  const recentMessages = context.messages.slice(-MAX_TRANSCRIPT_MESSAGES);
206
397
  const artifacts: ContributionPrepArtifact[] = [];
207
398
  const redact = (text: string) => redactContributionPrepText(text, context.cwd, redactions);
399
+ const workspaceLabel = safeWorkspaceLabel(context.cwd);
208
400
 
209
401
  artifacts.push(
210
402
  await writeArtifact(
@@ -223,8 +415,8 @@ export async function prepareContributionPrep(
223
415
  [
224
416
  `# Contribution prep context`,
225
417
  `Source session: ${context.sessionId}`,
226
- `Session file: ${context.sessionFile ?? "(none)"}`,
227
- `Working directory: ${context.cwd}`,
418
+ `Session file: ${context.sessionFile ? path.basename(context.sessionFile) : "(none)"}`,
419
+ `Working directory: ${workspaceLabel}`,
228
420
  options.customInstructions || context.customInstructions
229
421
  ? `Custom instructions: ${options.customInstructions ?? context.customInstructions}`
230
422
  : "Custom instructions: (none)",
@@ -233,8 +425,8 @@ export async function prepareContributionPrep(
233
425
  ),
234
426
  );
235
427
 
236
- const gitHead = (await gitOutput(context.cwd, ["rev-parse", "HEAD"])) || null;
237
- const files = await changedFiles(context.cwd);
428
+ const gitHead = (await gitOutput(context.cwd, ["rev-parse", "HEAD"], redactions)) || null;
429
+ const files = await changedFiles(context.cwd, redactions);
238
430
  artifacts.push(
239
431
  await writeArtifact(artifactDir, "changed-files.txt", "Changed files from git status", redact(files.join("\n"))),
240
432
  );
@@ -243,7 +435,7 @@ export async function prepareContributionPrep(
243
435
  artifactDir,
244
436
  "git-diff.patch",
245
437
  "Bounded redacted git diff",
246
- redact(await gitOutput(context.cwd, ["diff", "--no-ext-diff"])),
438
+ await gitOutput(context.cwd, ["diff", "--no-ext-diff"], redactions),
247
439
  ),
248
440
  );
249
441
  artifacts.push(
@@ -253,7 +445,7 @@ export async function prepareContributionPrep(
253
445
  "Redacted environment and reproduction metadata",
254
446
  redact(
255
447
  [
256
- `cwd: ${context.cwd}`,
448
+ `cwd: ${workspaceLabel}`,
257
449
  `git_head: ${gitHead ?? "unknown"}`,
258
450
  `platform: ${process.platform}`,
259
451
  `arch: ${process.arch}`,
@@ -266,15 +458,16 @@ export async function prepareContributionPrep(
266
458
  const manifestPath = path.join(artifactDir, "manifest.json");
267
459
  const workerPromptPath = path.join(artifactDir, "worker-prompt.md");
268
460
  await Bun.write(workerPromptPath, `${buildContributionPrepWorkerPrompt(manifestPath)}\n`);
461
+ const manifestArtifacts = artifacts.map(artifact => ({ ...artifact, path: path.basename(artifact.path) }));
269
462
 
270
463
  const manifest: ContributionPrepManifest = {
271
464
  schema_version: CONTRIBUTION_PREP_SCHEMA_VERSION,
272
- source_session_id: context.sessionId,
465
+ source_session_id: redact(context.sessionId),
273
466
  created_at: createdAt,
274
- cwd: redact(context.cwd),
467
+ cwd: redact(workspaceLabel),
275
468
  git_head: gitHead,
276
469
  changed_files: files.map(redact),
277
- artifacts,
470
+ artifacts: manifestArtifacts,
278
471
  redactions: [...redactions.labels].sort(),
279
472
  recommended_output: [
280
473
  "title",
@@ -285,7 +478,7 @@ export async function prepareContributionPrep(
285
478
  "tests to run",
286
479
  "uncertainty / remaining risks",
287
480
  ],
288
- worker_prompt_path: workerPromptPath,
481
+ worker_prompt_path: path.basename(workerPromptPath),
289
482
  };
290
483
  await Bun.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
291
484
 
@@ -309,8 +502,8 @@ export async function prepareContributionPrep(
309
502
  });
310
503
  const command = resolveGjcCommand();
311
504
  await spawn(
312
- [command.cmd, ...command.args, "--no-skills", "--", `@${workerPromptPath}`],
313
- context.cwd,
505
+ [command.cmd, ...command.args, "--no-skills", "--", `@${path.basename(workerPromptPath)}`],
506
+ artifactDir,
314
507
  command.shell,
315
508
  );
316
509
  spawned = true;