@gajae-code/coding-agent 0.3.0 → 0.3.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 (175) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/types/async/job-manager.d.ts +7 -0
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/config/keybindings.d.ts +5 -0
  6. package/dist/types/config/settings-schema.d.ts +4 -4
  7. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  8. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  9. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  10. package/dist/types/eval/py/executor.d.ts +2 -0
  11. package/dist/types/eval/py/kernel.d.ts +2 -0
  12. package/dist/types/exec/bash-executor.d.ts +10 -0
  13. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  14. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  16. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  17. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  20. package/dist/types/hooks/skill-state.d.ts +21 -0
  21. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  22. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  23. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  24. package/dist/types/internal-urls/types.d.ts +4 -0
  25. package/dist/types/lsp/index.d.ts +10 -10
  26. package/dist/types/modes/bridge/auth.d.ts +12 -0
  27. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  29. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  30. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  32. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  33. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  34. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  35. package/dist/types/modes/components/status-line.d.ts +2 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  37. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  38. package/dist/types/modes/index.d.ts +1 -0
  39. package/dist/types/modes/interactive-mode.d.ts +1 -0
  40. package/dist/types/modes/jobs-observer.d.ts +57 -0
  41. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  42. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  43. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  44. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  46. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  47. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  48. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  49. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  50. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  51. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  52. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  53. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  54. package/dist/types/modes/types.d.ts +1 -0
  55. package/dist/types/sdk.d.ts +2 -0
  56. package/dist/types/session/agent-session.d.ts +11 -1
  57. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  58. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  59. package/dist/types/task/id.d.ts +7 -0
  60. package/dist/types/task/index.d.ts +5 -0
  61. package/dist/types/task/receipt.d.ts +85 -0
  62. package/dist/types/task/spawn-gate.d.ts +38 -0
  63. package/dist/types/task/types.d.ts +143 -11
  64. package/dist/types/tools/cron.d.ts +6 -0
  65. package/dist/types/tools/index.d.ts +2 -0
  66. package/dist/types/tools/path-utils.d.ts +1 -0
  67. package/dist/types/tools/subagent.d.ts +15 -0
  68. package/package.json +7 -7
  69. package/scripts/build-binary.ts +7 -0
  70. package/src/async/job-manager.ts +36 -0
  71. package/src/cli/args.ts +9 -2
  72. package/src/commands/deep-interview.ts +1 -0
  73. package/src/commands/harness.ts +289 -19
  74. package/src/commands/launch.ts +2 -2
  75. package/src/commands/state.ts +2 -1
  76. package/src/commands/team.ts +22 -4
  77. package/src/config/keybindings.ts +6 -0
  78. package/src/config/settings-schema.ts +6 -3
  79. package/src/dap/client.ts +17 -3
  80. package/src/debug/crash-diagnostics.ts +223 -0
  81. package/src/debug/runtime-gauges.ts +20 -0
  82. package/src/deep-interview/render-middleware.ts +6 -0
  83. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  84. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  85. package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
  86. package/src/eval/py/executor.ts +21 -1
  87. package/src/eval/py/kernel.ts +15 -0
  88. package/src/exec/bash-executor.ts +41 -0
  89. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  90. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  91. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  92. package/src/gjc-runtime/state-migrations.ts +54 -7
  93. package/src/gjc-runtime/state-runtime.ts +461 -64
  94. package/src/gjc-runtime/state-schema.ts +192 -0
  95. package/src/gjc-runtime/state-writer.ts +32 -1
  96. package/src/gjc-runtime/team-runtime.ts +177 -105
  97. package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
  98. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  99. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  100. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  101. package/src/harness-control-plane/control-endpoint.ts +19 -8
  102. package/src/harness-control-plane/owner.ts +57 -10
  103. package/src/harness-control-plane/state-machine.ts +2 -1
  104. package/src/hooks/skill-state.ts +176 -26
  105. package/src/internal-urls/agent-protocol.ts +68 -21
  106. package/src/internal-urls/artifact-protocol.ts +12 -17
  107. package/src/internal-urls/docs-index.generated.ts +3 -2
  108. package/src/internal-urls/registry-helpers.ts +19 -16
  109. package/src/internal-urls/types.ts +4 -0
  110. package/src/lsp/client.ts +18 -2
  111. package/src/main.ts +21 -5
  112. package/src/modes/bridge/auth.ts +41 -0
  113. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  114. package/src/modes/bridge/bridge-mode.ts +520 -0
  115. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  116. package/src/modes/bridge/event-stream.ts +70 -0
  117. package/src/modes/components/custom-editor.ts +101 -0
  118. package/src/modes/components/hook-selector.ts +61 -18
  119. package/src/modes/components/jobs-overlay-model.ts +109 -0
  120. package/src/modes/components/jobs-overlay.ts +172 -0
  121. package/src/modes/components/status-line/presets.ts +7 -5
  122. package/src/modes/components/status-line/segments.ts +25 -0
  123. package/src/modes/components/status-line/types.ts +2 -0
  124. package/src/modes/components/status-line.ts +9 -1
  125. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  126. package/src/modes/controllers/input-controller.ts +97 -9
  127. package/src/modes/controllers/selector-controller.ts +29 -0
  128. package/src/modes/index.ts +1 -0
  129. package/src/modes/interactive-mode.ts +27 -0
  130. package/src/modes/jobs-observer.ts +204 -0
  131. package/src/modes/rpc/host-tools.ts +1 -186
  132. package/src/modes/rpc/host-uris.ts +1 -235
  133. package/src/modes/rpc/rpc-client.ts +25 -10
  134. package/src/modes/rpc/rpc-mode.ts +12 -381
  135. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  136. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  137. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  138. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  139. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  140. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  141. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  142. package/src/modes/shared/agent-wire/responses.ts +17 -0
  143. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  144. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  145. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  146. package/src/modes/types.ts +1 -0
  147. package/src/prompts/tools/subagent.md +12 -7
  148. package/src/prompts/tools/task-summary.md +3 -9
  149. package/src/prompts/tools/task.md +5 -1
  150. package/src/sdk.ts +4 -0
  151. package/src/session/agent-session.ts +214 -38
  152. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  153. package/src/skill-state/workflow-state-contract.ts +7 -4
  154. package/src/skill-state/workflow-state-version.ts +3 -0
  155. package/src/slash-commands/builtin-registry.ts +8 -0
  156. package/src/task/executor.ts +29 -5
  157. package/src/task/id.ts +33 -0
  158. package/src/task/index.ts +257 -67
  159. package/src/task/output-manager.ts +5 -4
  160. package/src/task/receipt.ts +297 -0
  161. package/src/task/render.ts +48 -131
  162. package/src/task/spawn-gate.ts +132 -0
  163. package/src/task/types.ts +48 -7
  164. package/src/tools/ask.ts +73 -33
  165. package/src/tools/ast-edit.ts +1 -0
  166. package/src/tools/ast-grep.ts +1 -0
  167. package/src/tools/bash.ts +1 -1
  168. package/src/tools/cron.ts +48 -0
  169. package/src/tools/find.ts +4 -1
  170. package/src/tools/index.ts +2 -0
  171. package/src/tools/path-utils.ts +3 -2
  172. package/src/tools/read.ts +1 -0
  173. package/src/tools/search.ts +1 -0
  174. package/src/tools/skill.ts +6 -1
  175. package/src/tools/subagent.ts +237 -84
@@ -172,6 +172,7 @@ import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime"
172
172
  import { GoalRuntime } from "../goals/runtime";
173
173
  import type { Goal, GoalModeState } from "../goals/state";
174
174
  import type { HindsightSessionState } from "../hindsight/state";
175
+ import { ensureWorkflowSkillActivationState } from "../hooks/skill-state";
175
176
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
176
177
  import { resolveMemoryBackend } from "../memory-backend";
177
178
  import { getCurrentThemeName, theme } from "../modes/theme/theme";
@@ -297,7 +298,7 @@ function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
297
298
 
298
299
  /** Listener function for agent session events */
299
300
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
300
- export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
301
+ export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime" | "metadata">;
301
302
 
302
303
  export interface AsyncJobSnapshot {
303
304
  running: AsyncJobSnapshotItem[];
@@ -872,6 +873,7 @@ export class AgentSession {
872
873
  #activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
873
874
  // Todo completion reminder state
874
875
  #todoReminderCount = 0;
876
+ #lastGoalReminderAssistantTimestamp: number | undefined = undefined;
875
877
  #todoPhases: TodoPhase[] = [];
876
878
  #toolChoiceQueue = new ToolChoiceQueue();
877
879
 
@@ -972,6 +974,12 @@ export class AgentSession {
972
974
  * without producing an aborted message_end). */
973
975
  #planCompactAbortPending = false;
974
976
 
977
+ /** One-shot flag armed by `abort({ silent: true })` (e.g. Esc consuming a
978
+ * queued steer). Consumed in #handleAgentEvent to stamp `SILENT_ABORT_MARKER`
979
+ * on the resulting aborted assistant `message_end` so the interrupt does not
980
+ * surface a red "Operation aborted" line; cleared by a later non-silent abort
981
+ * or by `abort`'s safety net when no aborted message_end is produced. */
982
+ #silentAbortPending = false;
975
983
  /** Monotonic counter for `enqueueCustomMessageDisplay` tag generation;
976
984
  * combined with `Date.now()` so tags stay unique even across rapid
977
985
  * same-tick enqueues. */
@@ -1050,6 +1058,7 @@ export class AgentSession {
1050
1058
  this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
1051
1059
  if (this.#promptInFlightCount === 0) {
1052
1060
  this.#releasePowerAssertion();
1061
+ this.#flushPendingBackgroundExchanges();
1053
1062
  this.#flushPendingAgentEnd();
1054
1063
  }
1055
1064
  }
@@ -1057,6 +1066,7 @@ export class AgentSession {
1057
1066
  #resetInFlight(): void {
1058
1067
  this.#promptInFlightCount = 0;
1059
1068
  this.#releasePowerAssertion();
1069
+ this.#flushPendingBackgroundExchanges();
1060
1070
  this.#flushPendingAgentEnd();
1061
1071
  }
1062
1072
 
@@ -1485,6 +1495,10 @@ export class AgentSession {
1485
1495
  return tag;
1486
1496
  }
1487
1497
 
1498
+ getAgentId(): string | undefined {
1499
+ return this.#agentId;
1500
+ }
1501
+
1488
1502
  getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
1489
1503
  const manager = AsyncJobManager.instance();
1490
1504
  if (!manager) return null;
@@ -1495,6 +1509,7 @@ export class AgentSession {
1495
1509
  status: job.status,
1496
1510
  label: job.label,
1497
1511
  startTime: job.startTime,
1512
+ metadata: job.metadata,
1498
1513
  }));
1499
1514
  const recent = manager.getRecentJobs(options?.recentLimit ?? 5, ownerFilter).map(job => ({
1500
1515
  id: job.id,
@@ -1502,6 +1517,7 @@ export class AgentSession {
1502
1517
  status: job.status,
1503
1518
  label: job.label,
1504
1519
  startTime: job.startTime,
1520
+ metadata: job.metadata,
1505
1521
  }));
1506
1522
  const delivery = manager.getDeliveryState(ownerFilter);
1507
1523
  return { running, recent, delivery };
@@ -1644,10 +1660,11 @@ export class AgentSession {
1644
1660
  event.type === "message_end" &&
1645
1661
  event.message.role === "assistant" &&
1646
1662
  event.message.stopReason === "aborted" &&
1647
- this.#planCompactAbortPending
1663
+ (this.#planCompactAbortPending || this.#silentAbortPending)
1648
1664
  ) {
1649
1665
  (event.message as AssistantMessage).errorMessage = SILENT_ABORT_MARKER;
1650
1666
  this.#planCompactAbortPending = false;
1667
+ this.#silentAbortPending = false;
1651
1668
  }
1652
1669
 
1653
1670
  // Deobfuscate assistant message content for display emission — the LLM echoes back
@@ -2015,6 +2032,9 @@ export class AgentSession {
2015
2032
 
2016
2033
  if (this.#assistantEndedWithSuccessfulYield(msg)) {
2017
2034
  this.#lastSuccessfulYieldToolCallId = undefined;
2035
+ if (msg.stopReason !== "error" && msg.stopReason !== "aborted" && (await this.#checkGoalCompletion(msg))) {
2036
+ return;
2037
+ }
2018
2038
  return;
2019
2039
  }
2020
2040
  this.#lastSuccessfulYieldToolCallId = undefined;
@@ -2050,6 +2070,9 @@ export class AgentSession {
2050
2070
  if (this.#enforceRewindBeforeYield()) {
2051
2071
  return;
2052
2072
  }
2073
+ if (await this.#checkGoalCompletion(msg)) {
2074
+ return;
2075
+ }
2053
2076
  await this.#checkTodoCompletion();
2054
2077
  }
2055
2078
  }
@@ -2138,13 +2161,23 @@ export class AgentSession {
2138
2161
  delayMs?: number;
2139
2162
  generation?: number;
2140
2163
  shouldContinue?: () => boolean;
2141
- onSkip?: () => void;
2142
- onError?: () => void;
2164
+ onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained") => void;
2165
+ onError?: (error: unknown) => void;
2143
2166
  }): void {
2167
+ const scheduledGeneration = options?.generation;
2168
+ const signal = this.#postPromptTasksAbortController.signal;
2144
2169
  this.#schedulePostPromptTask(
2145
2170
  async () => {
2171
+ if (signal.aborted) {
2172
+ options?.onSkip?.("aborted_signal");
2173
+ return;
2174
+ }
2175
+ if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
2176
+ options?.onSkip?.("generation_changed");
2177
+ return;
2178
+ }
2146
2179
  if (options?.shouldContinue && !options.shouldContinue()) {
2147
- options.onSkip?.();
2180
+ options.onSkip?.("queue_drained");
2148
2181
  return;
2149
2182
  }
2150
2183
  try {
@@ -2154,17 +2187,45 @@ export class AgentSession {
2154
2187
  logger.warn("agent.continue failed after scheduling", {
2155
2188
  error: error instanceof Error ? error.message : String(error),
2156
2189
  });
2157
- options?.onError?.();
2190
+ options?.onError?.(error);
2158
2191
  }
2159
2192
  },
2160
- {
2161
- delayMs: options?.delayMs,
2162
- generation: options?.generation,
2163
- onSkip: options?.onSkip,
2164
- },
2193
+ { delayMs: options?.delayMs },
2165
2194
  );
2166
2195
  }
2167
2196
 
2197
+ #logCompactionContinuationSkipped(
2198
+ source: "auto_continue_prompt" | "queued_continue" | "overflow_retry",
2199
+ reason: string,
2200
+ ): void {
2201
+ logger.warn("Auto-compaction continuation skipped", { source, reason });
2202
+ }
2203
+
2204
+ #logCompactionContinuationError(
2205
+ source: "auto_continue_prompt" | "queued_continue" | "overflow_retry",
2206
+ error: unknown,
2207
+ ): void {
2208
+ logger.warn("Auto-compaction continuation failed", {
2209
+ source,
2210
+ reason: error instanceof Error && error.name === "AgentBusyError" ? "queue_drained" : "not_resumable_tail",
2211
+ error: error instanceof Error ? error.message : String(error),
2212
+ });
2213
+ }
2214
+
2215
+ #isResumableAgentTail(): boolean {
2216
+ const lastMsg = this.agent.state.messages.at(-1);
2217
+ return lastMsg !== undefined && lastMsg.role !== "assistant";
2218
+ }
2219
+
2220
+ #stripOverflowFailedTurnForRetry(): void {
2221
+ const messages = this.agent.state.messages;
2222
+ const lastMsg = messages.at(-1);
2223
+ const contextWindow = this.model?.contextWindow ?? 0;
2224
+ if (lastMsg?.role === "assistant" && isContextOverflow(lastMsg as AssistantMessage, contextWindow)) {
2225
+ this.agent.replaceMessages(messages.slice(0, -1));
2226
+ }
2227
+ }
2228
+
2168
2229
  #scheduleAutoContinuePrompt(generation: number): void {
2169
2230
  const continuePrompt = async () => {
2170
2231
  await this.#promptWithMessage(
@@ -2175,16 +2236,28 @@ export class AgentSession {
2175
2236
  timestamp: Date.now(),
2176
2237
  },
2177
2238
  autoContinuePrompt,
2178
- { skipPostPromptRecoveryWait: true },
2239
+ { skipPostPromptRecoveryWait: true, skipCompactionCheck: true },
2179
2240
  );
2180
2241
  };
2181
- this.#schedulePostPromptTask(
2182
- async signal => {
2242
+ const scheduledGeneration = generation;
2243
+ const signal = this.#postPromptTasksAbortController.signal;
2244
+ this.#trackPostPromptTask(
2245
+ (async () => {
2183
2246
  await Promise.resolve();
2184
- if (signal.aborted) return;
2185
- await continuePrompt();
2186
- },
2187
- { generation },
2247
+ if (signal.aborted) {
2248
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
2249
+ return;
2250
+ }
2251
+ if (this.#promptGeneration !== scheduledGeneration) {
2252
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
2253
+ return;
2254
+ }
2255
+ try {
2256
+ await continuePrompt();
2257
+ } catch (error) {
2258
+ this.#logCompactionContinuationError("auto_continue_prompt", error);
2259
+ }
2260
+ })(),
2188
2261
  );
2189
2262
  }
2190
2263
 
@@ -4327,12 +4400,17 @@ export class AgentSession {
4327
4400
  // Canonical GJC workflow skills (deep-interview, ralplan, ultragoal, team)
4328
4401
  // own their `.gjc/state/skill-active-state.json` row through the
4329
4402
  // `gjc state handoff` and `gjc state clear` runtime verbs. The prompt
4330
- // observer here used to overwrite the row with `phase: running` and
4331
- // later remove it with `active:false`, which clobbered handoff lineage
4332
- // (`handoff_from`/`handoff_at`) and made the HUD inconsistent with
4333
- // mode-state. The observational filesystem write is now skipped for
4334
- // canonical skills; the in-memory `#activeSkillState` tracking below
4335
- // keeps `getActiveSkillState` accurate for the chain guard.
4403
+ // observer must not overwrite an existing row (that clobbered handoff
4404
+ // lineage `handoff_from`/`handoff_at` and desynced the HUD). But a fresh
4405
+ // `/skill:<name>` invocation has no row yet, so seed `.gjc/state`
4406
+ // idempotently here: `ensureWorkflowSkillActivationState` writes the
4407
+ // initial mode-state + active row only when the skill is not already
4408
+ // active, so the mutation guard and Stop hook engage immediately instead
4409
+ // of relying on the skill prompt to run its own state-init steps.
4410
+ if (active) {
4411
+ await ensureWorkflowSkillActivationState({ cwd: this.sessionManager.getCwd(), skill, sessionId });
4412
+ }
4413
+ // In-memory tracking keeps `getActiveSkillState` accurate for the chain guard.
4336
4414
  this.#activeSkillState = active ? { skill, sessionId } : undefined;
4337
4415
  }
4338
4416
 
@@ -4978,6 +5056,13 @@ export class AgentSession {
4978
5056
  return this.#steeringMessages.length + this.#followUpMessages.length + this.#pendingNextTurnMessages.length;
4979
5057
  }
4980
5058
 
5059
+ /** Whether the agent has queued steering messages that a `user_interrupt`
5060
+ * abort would resume into (steer-on-interrupt). Drives the Esc-on-steer UX:
5061
+ * the first Esc consumes the steer and auto-continues, a second Esc aborts. */
5062
+ get hasQueuedSteering(): boolean {
5063
+ return this.agent.hasQueuedSteering();
5064
+ }
5065
+
4981
5066
  /** Get pending messages (read-only). Returns the public text-only view;
4982
5067
  * internal `{text, tag?}` records are mapped to `.text` so callers
4983
5068
  * (`updatePendingMessagesDisplay`, `restoreQueuedMessagesToEditor`) see
@@ -5074,7 +5159,17 @@ export class AgentSession {
5074
5159
  | "handoff"
5075
5160
  | "tool_abort"
5076
5161
  | "internal";
5162
+ /** Suppress the "Operation aborted" line on the resulting aborted message
5163
+ * by stamping `SILENT_ABORT_MARKER`. Used when Esc consumes a queued steer
5164
+ * and resumes via steer-on-interrupt, so the interrupt reads as a quiet
5165
+ * hand-off rather than a failure. */
5166
+ silent?: boolean;
5077
5167
  }): Promise<void> {
5168
+ if (options?.silent) {
5169
+ this.#silentAbortPending = true;
5170
+ } else {
5171
+ this.#silentAbortPending = false;
5172
+ }
5078
5173
  this.abortRetry();
5079
5174
  this.#promptGeneration++;
5080
5175
  this.#scheduledHiddenNextTurnGeneration = undefined;
@@ -5114,6 +5209,10 @@ export class AgentSession {
5114
5209
  // block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
5115
5210
  // a subsequent prompt() can incorrectly observe the session as busy after an abort.
5116
5211
  this.#resetInFlight();
5212
+ // Safety net: clear the silent-abort flag if it was never consumed (the
5213
+ // abort produced no aborted assistant message_end to stamp). Prevents the
5214
+ // marker from leaking onto a later, unrelated abort.
5215
+ this.#silentAbortPending = false;
5117
5216
  // Safety net: if the agent loop aborted without producing an assistant
5118
5217
  // message (e.g. failed before the first stream), the in-flight yield was
5119
5218
  // never resolved or rejected by the normal message_end path. Reject it now
@@ -5188,6 +5287,9 @@ export class AgentSession {
5188
5287
  this.#scheduledHiddenNextTurnGeneration = undefined;
5189
5288
 
5190
5289
  this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
5290
+ if (this.model) {
5291
+ this.sessionManager.appendModelChange(`${this.model.provider}/${this.model.id}`);
5292
+ }
5191
5293
  this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
5192
5294
  if (nextDiscoverySessionToolNames) {
5193
5295
  await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, { persistMCPSelection: false });
@@ -5979,6 +6081,11 @@ export class AgentSession {
5979
6081
  this.#pendingNextTurnMessages = [];
5980
6082
  this.#scheduledHiddenNextTurnGeneration = undefined;
5981
6083
  this.#todoReminderCount = 0;
6084
+ if (model) {
6085
+ this.sessionManager.appendModelChange(`${model.provider}/${model.id}`);
6086
+ }
6087
+ this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
6088
+ this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
5982
6089
 
5983
6090
  // Inject the handoff document as a custom message
5984
6091
  const handoffContent = createHandoffContext(handoffText);
@@ -6266,6 +6373,39 @@ export class AgentSession {
6266
6373
  toolChoice: todoWriteToolChoice,
6267
6374
  };
6268
6375
  }
6376
+
6377
+ async #checkGoalCompletion(assistantMessage: AssistantMessage): Promise<boolean> {
6378
+ const state = this.getGoalModeState();
6379
+ if (!state?.enabled || state.goal.status !== "active") {
6380
+ this.#lastGoalReminderAssistantTimestamp = undefined;
6381
+ return false;
6382
+ }
6383
+ if (this.#lastGoalReminderAssistantTimestamp === assistantMessage.timestamp) {
6384
+ return false;
6385
+ }
6386
+ this.#lastGoalReminderAssistantTimestamp = assistantMessage.timestamp;
6387
+
6388
+ const continuationPrompt = this.#goalRuntime.buildContinuationPrompt();
6389
+ if (!continuationPrompt) return false;
6390
+ const reminder = [
6391
+ "<system-reminder>",
6392
+ "You stopped while a goal is still active and uncleared.",
6393
+ "Continue working on the active goal until it is verified complete, paused, or dropped.",
6394
+ "",
6395
+ continuationPrompt,
6396
+ "</system-reminder>",
6397
+ ].join("\n");
6398
+
6399
+ logger.debug("Goal completion: sending active-goal reminder", { goalId: state.goal.id });
6400
+ this.agent.appendMessage({
6401
+ role: "developer",
6402
+ content: [{ type: "text", text: reminder }],
6403
+ attribution: "agent",
6404
+ timestamp: Date.now(),
6405
+ });
6406
+ this.#scheduleAgentContinue({ generation: this.#promptGeneration });
6407
+ return true;
6408
+ }
6269
6409
  /**
6270
6410
  * Check if agent stopped with incomplete todos and prompt to continue.
6271
6411
  */
@@ -6909,12 +7049,34 @@ export class AgentSession {
6909
7049
  willRetry: false,
6910
7050
  skipped: true,
6911
7051
  });
6912
- if (!willRetry && this.agent.hasQueuedMessages()) {
7052
+ if (willRetry) {
7053
+ this.#stripOverflowFailedTurnForRetry();
7054
+ if (this.#isResumableAgentTail()) {
7055
+ this.#scheduleAgentContinue({
7056
+ delayMs: 100,
7057
+ generation,
7058
+ onSkip: skipReason => this.#logCompactionContinuationSkipped("overflow_retry", skipReason),
7059
+ onError: error => this.#logCompactionContinuationError("overflow_retry", error),
7060
+ });
7061
+ } else {
7062
+ const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
7063
+ logger.warn("Auto-compaction continuation skipped", {
7064
+ source: "overflow_retry",
7065
+ reason: "not_resumable_tail",
7066
+ role: tail?.role,
7067
+ stopReason: tail?.stopReason,
7068
+ });
7069
+ }
7070
+ } else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
6913
7071
  this.#scheduleAgentContinue({
6914
7072
  delayMs: 100,
6915
7073
  generation,
6916
7074
  shouldContinue: () => this.agent.hasQueuedMessages(),
7075
+ onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
7076
+ onError: error => this.#logCompactionContinuationError("queued_continue", error),
6917
7077
  });
7078
+ } else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
7079
+ this.#scheduleAutoContinuePrompt(generation);
6918
7080
  }
6919
7081
  return;
6920
7082
  }
@@ -7116,26 +7278,36 @@ export class AgentSession {
7116
7278
  };
7117
7279
  await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
7118
7280
 
7119
- if (!willRetry && reason !== "idle" && compactionSettings.autoContinue !== false) {
7120
- this.#scheduleAutoContinuePrompt(generation);
7121
- }
7122
-
7123
7281
  if (willRetry) {
7124
- const messages = this.agent.state.messages;
7125
- const lastMsg = messages[messages.length - 1];
7126
- if (lastMsg?.role === "assistant" && (lastMsg as AssistantMessage).stopReason === "error") {
7127
- this.agent.replaceMessages(messages.slice(0, -1));
7282
+ this.#stripOverflowFailedTurnForRetry();
7283
+ if (!this.#isResumableAgentTail()) {
7284
+ const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
7285
+ logger.warn("Auto-compaction continuation skipped", {
7286
+ source: "overflow_retry",
7287
+ reason: "not_resumable_tail",
7288
+ role: tail?.role,
7289
+ stopReason: tail?.stopReason,
7290
+ });
7291
+ } else {
7292
+ this.#scheduleAgentContinue({
7293
+ delayMs: 100,
7294
+ generation,
7295
+ onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
7296
+ onError: error => this.#logCompactionContinuationError("overflow_retry", error),
7297
+ });
7128
7298
  }
7129
-
7130
- this.#scheduleAgentContinue({ delayMs: 100, generation });
7131
- } else if (this.agent.hasQueuedMessages()) {
7299
+ } else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
7132
7300
  // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
7133
7301
  // Kick the loop so queued messages are actually delivered.
7134
7302
  this.#scheduleAgentContinue({
7135
7303
  delayMs: 100,
7136
7304
  generation,
7137
7305
  shouldContinue: () => this.agent.hasQueuedMessages(),
7306
+ onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
7307
+ onError: error => this.#logCompactionContinuationError("queued_continue", error),
7138
7308
  });
7309
+ } else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
7310
+ this.#scheduleAutoContinuePrompt(generation);
7139
7311
  }
7140
7312
  } catch (error) {
7141
7313
  if (autoCompactionSignal.aborted) {
@@ -8377,13 +8549,17 @@ export class AgentSession {
8377
8549
  return;
8378
8550
  }
8379
8551
  if (this.isStreaming) {
8380
- setTimeout(attempt, 50);
8552
+ // Re-poll while streaming, but do not let this housekeeping timer
8553
+ // keep the event loop alive on its own (CPU-7).
8554
+ const pollTimer = setTimeout(attempt, 50);
8555
+ pollTimer.unref?.();
8381
8556
  return;
8382
8557
  }
8383
8558
  this.#scheduledBackgroundExchangeFlush = false;
8384
8559
  this.#flushPendingBackgroundExchanges();
8385
8560
  };
8386
- setTimeout(attempt, 0);
8561
+ const kickoff = setTimeout(attempt, 0);
8562
+ kickoff.unref?.();
8387
8563
  }
8388
8564
 
8389
8565
  #flushPendingBackgroundExchanges(): void {
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import type { AgentTool } from "@gajae-code/agent-core";
3
3
  import { expandApplyPatchToEntries } from "../edit/modes/apply-patch";
4
+ import { ModeStateSchema } from "../gjc-runtime/state-schema";
4
5
  import { LocalProtocolHandler, resolveLocalUrlToPath } from "../internal-urls/local-protocol";
5
6
  import { resolveToCwd } from "../tools/path-utils";
6
7
  import { ToolError } from "../tools/tool-errors";
@@ -76,19 +77,37 @@ function modeStatePath(cwd: string, skill: string, sessionId?: string): string {
76
77
  return path.join(stateDir, fileName);
77
78
  }
78
79
 
79
- async function readJsonFile<T>(filePath: string): Promise<T | null> {
80
+ function warnInvalidModeState(filePath: string, error: string): void {
81
+ console.warn(`gjc skill-state: invalid mode-state at ${filePath}: ${error}`);
82
+ }
83
+
84
+ async function readValidatedModeState(filePath: string): Promise<ModeState | null> {
85
+ let raw: string;
80
86
  try {
81
- return JSON.parse(await Bun.file(filePath).text()) as T;
87
+ raw = await Bun.file(filePath).text();
82
88
  } catch {
83
89
  return null;
84
90
  }
91
+ let state: ModeState;
92
+ try {
93
+ state = JSON.parse(raw) as ModeState;
94
+ } catch (error) {
95
+ warnInvalidModeState(filePath, `invalid JSON: ${(error as Error).message}`);
96
+ return null;
97
+ }
98
+ const parsed = ModeStateSchema.safeParse(state);
99
+ if (!parsed.success) {
100
+ warnInvalidModeState(filePath, parsed.error.message);
101
+ return null;
102
+ }
103
+ return state;
85
104
  }
86
105
  async function readVisibleModeState(cwd: string, skill: string, sessionId?: string): Promise<ModeState | null> {
87
106
  if (sessionId) {
88
- const sessionState = await readJsonFile<ModeState>(modeStatePath(cwd, skill, sessionId));
107
+ const sessionState = await readValidatedModeState(modeStatePath(cwd, skill, sessionId));
89
108
  if (sessionState) return sessionState;
90
109
  }
91
- return await readJsonFile<ModeState>(modeStatePath(cwd, skill));
110
+ return await readValidatedModeState(modeStatePath(cwd, skill));
92
111
  }
93
112
 
94
113
  function isTerminalModeState(state: ModeState | null): boolean {
@@ -1,11 +1,14 @@
1
1
  import * as path from "node:path";
2
2
  import { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill, SKILL_ACTIVE_STATE_FILE } from "./active-state";
3
+ import { WORKFLOW_STATE_RECEIPT_FRESH_MS, WORKFLOW_STATE_RECEIPT_VERSION } from "./workflow-state-version";
3
4
 
4
- export type { CanonicalGjcWorkflowSkill };
5
-
6
- export const WORKFLOW_STATE_RECEIPT_VERSION = 1;
7
- export const WORKFLOW_STATE_RECEIPT_FRESH_MS = 30 * 60 * 1000;
5
+ export {
6
+ WORKFLOW_STATE_RECEIPT_FRESH_MS,
7
+ WORKFLOW_STATE_RECEIPT_VERSION,
8
+ WORKFLOW_STATE_VERSION,
9
+ } from "./workflow-state-version";
8
10
 
11
+ export type { CanonicalGjcWorkflowSkill };
9
12
  export type WorkflowStateMutationOwner = "gjc-state-cli" | "gjc-runtime" | "gjc-hook";
10
13
  export type WorkflowStateReceiptStatus = "fresh" | "stale";
11
14
 
@@ -0,0 +1,3 @@
1
+ export const WORKFLOW_STATE_RECEIPT_VERSION = 1;
2
+ export const WORKFLOW_STATE_VERSION = 2;
3
+ export const WORKFLOW_STATE_RECEIPT_FRESH_MS = 30 * 60 * 1000;
@@ -586,6 +586,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
586
586
  runtime.ctx.editor.setText("");
587
587
  },
588
588
  },
589
+ {
590
+ name: "monitors",
591
+ description: "Open the monitor/cron jobs overlay",
592
+ handleTui: (_command, runtime) => {
593
+ runtime.ctx.showJobsOverlay();
594
+ runtime.ctx.editor.setText("");
595
+ },
596
+ },
589
597
  {
590
598
  name: "tree",
591
599
  description: "Navigate session tree (switch branches)",
@@ -4,6 +4,8 @@
4
4
  * Runs each subagent on the main thread and forwards AgentEvents for progress tracking.
5
5
  */
6
6
 
7
+ import { createHash } from "node:crypto";
8
+ import * as fs from "node:fs/promises";
7
9
  import path from "node:path";
8
10
  import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@gajae-code/agent-core";
9
11
  import { recordHandoff, resolveTelemetry } from "@gajae-code/agent-core";
@@ -1189,6 +1191,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1189
1191
  hasUI: false,
1190
1192
  spawns: spawnsEnv,
1191
1193
  taskDepth: childDepth,
1194
+ currentAgentType: agent.name,
1192
1195
  parentHindsightSessionState: options.parentHindsightSessionState,
1193
1196
  parentTaskPrefix: id,
1194
1197
  agentId: id,
@@ -1536,18 +1539,39 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1536
1539
 
1537
1540
  // Write output artifact (input and jsonl already written in real-time)
1538
1541
  // Compute output metadata for agent:// URL integration
1539
- let outputMeta: { lineCount: number; charCount: number } | undefined;
1542
+ let outputMeta: { lineCount: number; charCount: number; byteSize?: number; sha256?: string } | undefined;
1540
1543
  let outputPath: string | undefined;
1541
1544
  if (options.artifactsDir) {
1542
- outputPath = path.join(options.artifactsDir, `${id}.md`);
1545
+ const candidateOutputPath = path.join(options.artifactsDir, `${id}.md`);
1543
1546
  try {
1544
- await Bun.write(outputPath, rawOutput);
1547
+ await Bun.write(candidateOutputPath, rawOutput);
1548
+ const byteSize = Buffer.byteLength(rawOutput, "utf8");
1549
+ const lineCount = rawOutput.split("\n").length;
1550
+ const sha256 = createHash("sha256").update(rawOutput).digest("hex");
1551
+ const createdAt = new Date().toISOString();
1552
+ await Bun.write(
1553
+ `${candidateOutputPath}.meta.json`,
1554
+ JSON.stringify({ id, kind: "agent-output", sizeBytes: byteSize, lineCount, sha256, createdAt }, null, 2),
1555
+ );
1556
+ outputPath = candidateOutputPath;
1545
1557
  outputMeta = {
1546
- lineCount: rawOutput.split("\n").length,
1558
+ lineCount,
1547
1559
  charCount: rawOutput.length,
1560
+ byteSize,
1561
+ sha256,
1548
1562
  };
1549
1563
  } catch {
1550
- // Non-fatal
1564
+ // Output or metadata write failed: never advertise an unverifiable
1565
+ // artifact. Best-effort remove any orphaned `.md`/sidecar so a later
1566
+ // agent:// read cannot serve unverified content. Non-fatal.
1567
+ outputPath = undefined;
1568
+ outputMeta = undefined;
1569
+ try {
1570
+ await fs.rm(candidateOutputPath, { force: true });
1571
+ await fs.rm(`${candidateOutputPath}.meta.json`, { force: true });
1572
+ } catch {
1573
+ // best-effort cleanup; ignore
1574
+ }
1551
1575
  }
1552
1576
  }
1553
1577
 
package/src/task/id.ts ADDED
@@ -0,0 +1,33 @@
1
+ export const TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/;
2
+ export const TASK_ID_DESCRIPTION = "filesystem-safe identifier matching ^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$";
3
+
4
+ const ALLOCATED_TASK_ID_PATTERN = new RegExp(
5
+ `^\\d+-${TASK_ID_PATTERN.source.slice(1, -1)}(?:\\.\\d+-${TASK_ID_PATTERN.source.slice(1, -1)})*$`,
6
+ );
7
+
8
+ export function isValidTaskId(id: string): boolean {
9
+ return TASK_ID_PATTERN.test(id);
10
+ }
11
+
12
+ export function getTaskIdValidationError(id: unknown): string | undefined {
13
+ if (typeof id !== "string") return "Task id must be a string.";
14
+ if (isValidTaskId(id)) return undefined;
15
+ return `Task id ${JSON.stringify(id)} is invalid. Use ${TASK_ID_DESCRIPTION}.`;
16
+ }
17
+
18
+ export function validateTaskId(id: string): string {
19
+ const error = getTaskIdValidationError(id);
20
+ if (error) throw new Error(error);
21
+ return id;
22
+ }
23
+
24
+ export function isValidAllocatedTaskId(id: string): boolean {
25
+ return ALLOCATED_TASK_ID_PATTERN.test(id);
26
+ }
27
+
28
+ export function validateAllocatedTaskId(id: string): string {
29
+ if (!isValidAllocatedTaskId(id)) {
30
+ throw new Error(`Allocated task id ${JSON.stringify(id)} is invalid for filesystem artifact paths.`);
31
+ }
32
+ return id;
33
+ }