@gajae-code/coding-agent 0.2.2 → 0.2.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 (96) hide show
  1. package/CHANGELOG.md +45 -8600
  2. package/dist/types/cli/setup-cli.d.ts +1 -0
  3. package/dist/types/cli/update-cli.d.ts +3 -0
  4. package/dist/types/commands/deep-interview.d.ts +41 -0
  5. package/dist/types/commands/setup.d.ts +3 -0
  6. package/dist/types/config/settings-schema.d.ts +56 -0
  7. package/dist/types/defaults/gjc-defaults.d.ts +19 -6
  8. package/dist/types/discovery/helpers.d.ts +2 -0
  9. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  10. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +18 -0
  11. package/dist/types/hooks/skill-state.d.ts +5 -0
  12. package/dist/types/memories/index.d.ts +1 -1
  13. package/dist/types/memory-backend/local-backend.d.ts +3 -3
  14. package/dist/types/modes/components/hook-selector.d.ts +7 -0
  15. package/dist/types/modes/components/settings-selector.d.ts +3 -1
  16. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  17. package/dist/types/modes/interactive-mode.d.ts +1 -0
  18. package/dist/types/modes/theme/defaults/index.d.ts +126 -0
  19. package/dist/types/modes/theme/theme.d.ts +5 -0
  20. package/dist/types/modes/types.d.ts +1 -0
  21. package/dist/types/modes/utils/context-usage.d.ts +6 -2
  22. package/dist/types/sdk.d.ts +6 -2
  23. package/dist/types/session/agent-session.d.ts +45 -1
  24. package/dist/types/session/session-manager.d.ts +3 -0
  25. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
  26. package/dist/types/setup/provider-onboarding.d.ts +29 -5
  27. package/dist/types/skill-state/active-state.d.ts +26 -1
  28. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  29. package/dist/types/skill-state/initial-phase.d.ts +12 -0
  30. package/dist/types/task/executor.d.ts +2 -0
  31. package/dist/types/task/types.d.ts +11 -0
  32. package/dist/types/tools/index.d.ts +20 -1
  33. package/dist/types/tools/skill.d.ts +47 -0
  34. package/dist/types/utils/changelog.d.ts +18 -2
  35. package/package.json +7 -7
  36. package/src/cli/setup-cli.ts +26 -12
  37. package/src/cli/update-cli.ts +67 -16
  38. package/src/cli.ts +1 -0
  39. package/src/commands/deep-interview.ts +25 -2
  40. package/src/commands/setup.ts +2 -0
  41. package/src/commands/state.ts +1 -0
  42. package/src/config/settings-schema.ts +63 -0
  43. package/src/defaults/gjc/skills/deep-interview/SKILL.md +58 -5
  44. package/src/defaults/gjc/skills/deep-interview/auto-answer-uncertain.md +37 -0
  45. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +42 -0
  46. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -0
  47. package/src/defaults/gjc/skills/team/SKILL.md +10 -0
  48. package/src/defaults/gjc/skills/ultragoal/SKILL.md +19 -6
  49. package/src/defaults/gjc-defaults.ts +68 -16
  50. package/src/discovery/helpers.ts +24 -1
  51. package/src/extensibility/extensions/types.ts +6 -0
  52. package/src/gjc-runtime/deep-interview-runtime.ts +312 -1
  53. package/src/gjc-runtime/state-runtime.ts +175 -5
  54. package/src/goals/tools/goal-tool.ts +5 -1
  55. package/src/hooks/skill-state.ts +8 -6
  56. package/src/internal-urls/docs-index.generated.ts +6 -4
  57. package/src/internal-urls/memory-protocol.ts +3 -2
  58. package/src/main.ts +2 -3
  59. package/src/memories/index.ts +6 -4
  60. package/src/memory-backend/local-backend.ts +14 -6
  61. package/src/modes/components/hook-selector.ts +156 -1
  62. package/src/modes/components/settings-selector.ts +16 -12
  63. package/src/modes/controllers/command-controller.ts +3 -4
  64. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  65. package/src/modes/controllers/selector-controller.ts +69 -9
  66. package/src/modes/interactive-mode.ts +14 -1
  67. package/src/modes/theme/defaults/blue-crab.json +126 -0
  68. package/src/modes/theme/defaults/index.ts +2 -0
  69. package/src/modes/theme/theme.ts +40 -1
  70. package/src/modes/types.ts +1 -0
  71. package/src/modes/utils/context-usage.ts +66 -17
  72. package/src/prompts/agents/architect.md +3 -0
  73. package/src/prompts/agents/executor.md +2 -0
  74. package/src/prompts/agents/frontmatter.md +1 -0
  75. package/src/prompts/memories/unavailable.md +9 -0
  76. package/src/prompts/system/subagent-system-prompt.md +6 -0
  77. package/src/prompts/tools/skill.md +28 -0
  78. package/src/prompts/tools/task.md +3 -0
  79. package/src/sdk.ts +54 -10
  80. package/src/session/agent-session.ts +204 -21
  81. package/src/session/session-manager.ts +9 -1
  82. package/src/setup/model-onboarding-guidance.ts +6 -3
  83. package/src/setup/provider-onboarding.ts +177 -16
  84. package/src/skill-state/active-state.ts +150 -25
  85. package/src/skill-state/deep-interview-mutation-guard.ts +11 -24
  86. package/src/skill-state/initial-phase.ts +17 -0
  87. package/src/slash-commands/builtin-registry.ts +62 -14
  88. package/src/slash-commands/helpers/context-report.ts +123 -13
  89. package/src/task/agents.ts +1 -0
  90. package/src/task/executor.ts +9 -1
  91. package/src/task/index.ts +91 -4
  92. package/src/task/types.ts +6 -0
  93. package/src/tools/ask.ts +2 -0
  94. package/src/tools/index.ts +23 -1
  95. package/src/tools/skill.ts +153 -0
  96. package/src/utils/changelog.ts +67 -44
package/src/sdk.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  type CredentialDisabledEvent,
13
13
  type Message,
14
14
  type Model,
15
+ type ProviderSessionState,
15
16
  type SimpleStreamOptions,
16
17
  streamSimple,
17
18
  } from "@gajae-code/ai";
@@ -82,7 +83,7 @@ import {
82
83
  obfuscateMessages,
83
84
  SecretObfuscator,
84
85
  } from "./secrets";
85
- import { AgentSession } from "./session/agent-session";
86
+ import { AgentSession, type ForkContextSeed } from "./session/agent-session";
86
87
  import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
87
88
  import { AuthBrokerClient, AuthStorage, RemoteAuthCredentialStore } from "./session/auth-storage";
88
89
  import { type CustomMessage, convertToLlm } from "./session/messages";
@@ -320,6 +321,10 @@ export interface CreateAgentSessionOptions {
320
321
  * `@opentelemetry/api` package returns a no-op tracer in that case.
321
322
  */
322
323
  telemetry?: AgentTelemetryConfig;
324
+ /** Optional fork-context seed used to initialize a child session before its first prompt. */
325
+ forkContextSeed?: ForkContextSeed;
326
+ /** Optional provider state override. Fork-context children should omit this by default. */
327
+ providerSessionState?: Map<string, ProviderSessionState>;
323
328
  }
324
329
 
325
330
  /** Result from createAgentSession */
@@ -861,7 +866,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
861
866
  logger.time("sessionManager", () =>
862
867
  SessionManager.create(cwd, SessionManager.getDefaultSessionDir(cwd, agentDir)),
863
868
  );
864
- const providerSessionId = options.providerSessionId ?? sessionManager.getSessionId();
869
+ const logicalSessionId = sessionManager.getSessionId();
870
+ const providerSessionId = options.providerSessionId ?? options.forkContextSeed?.cacheIdentity ?? logicalSessionId;
865
871
  const modelApiKeyAvailability = new Map<string, boolean>();
866
872
  const getModelAvailabilityKey = (candidate: Model): string =>
867
873
  `${candidate.provider}\u0000${candidate.baseUrl ?? ""}`;
@@ -1153,7 +1159,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1153
1159
  trackEvalExecution: (execution, abortController) =>
1154
1160
  session ? session.trackEvalExecution(execution, abortController) : execution,
1155
1161
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
1162
+ getActiveSkillState: () => session?.getActiveSkillState(),
1163
+ getActiveSkillPhase: () => session?.getActiveSkillPhase(),
1156
1164
  getHindsightSessionState: () => session?.getHindsightSessionState(),
1165
+ get model() {
1166
+ return agent?.state.model ?? model;
1167
+ },
1157
1168
  getAgentId: () => resolvedAgentId,
1158
1169
  getToolByName: name => session?.getToolByName(name),
1159
1170
  agentRegistry,
@@ -1195,6 +1206,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1195
1206
  attribution: "agent",
1196
1207
  timestamp: Date.now(),
1197
1208
  }),
1209
+ sendCustomMessage: (msg, opts) => session.sendCustomMessage(msg, opts),
1198
1210
  peekQueueInvoker: () => session.peekQueueInvoker(),
1199
1211
  peekStandingResolveHandler: () => session.peekStandingResolveHandler(),
1200
1212
  setStandingResolveHandler: handler => session.setStandingResolveHandler(handler),
@@ -1210,6 +1222,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1210
1222
  authStorage,
1211
1223
  modelRegistry,
1212
1224
  getTelemetry: () => agent?.telemetry,
1225
+ buildForkContextSeed: forkOptions => session.buildForkContextSeed(forkOptions),
1213
1226
  };
1214
1227
 
1215
1228
  // Wire process-wide internal URL singletons owned by their real classes.
@@ -1719,6 +1732,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1719
1732
  const preferOpenAICodexWebsockets =
1720
1733
  openaiWebsocketSetting === "on" ? true : openaiWebsocketSetting === "off" ? false : undefined;
1721
1734
  const serviceTierSetting = settings.get("serviceTier");
1735
+ const retrySettings = settings.getGroup("retry");
1722
1736
 
1723
1737
  const initialServiceTier = hasServiceTierEntry
1724
1738
  ? existingSession.serviceTier
@@ -1726,17 +1740,43 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1726
1740
  ? undefined
1727
1741
  : serviceTierSetting;
1728
1742
 
1743
+ const appendOnlyContext =
1744
+ model && resolveAppendOnlyMode(settings.get("provider.appendOnlyContext"), model.provider)
1745
+ ? new AppendOnlyContextManager()
1746
+ : undefined;
1747
+ if (appendOnlyContext && options.forkContextSeed && !hasExistingSession) {
1748
+ if (options.forkContextSeed.appendOnlyPrefixSnapshot) {
1749
+ (
1750
+ appendOnlyContext.prefix as typeof appendOnlyContext.prefix & {
1751
+ importSnapshot(
1752
+ snapshot: NonNullable<ForkContextSeed["appendOnlyPrefixSnapshot"]>,
1753
+ options: { intentTracing: boolean },
1754
+ ): void;
1755
+ }
1756
+ ).importSnapshot(options.forkContextSeed.appendOnlyPrefixSnapshot, { intentTracing: !!intentField });
1757
+ }
1758
+ (
1759
+ appendOnlyContext as AppendOnlyContextManager & {
1760
+ seedNormalizedMessages(messages: readonly Message[]): void;
1761
+ }
1762
+ ).seedNormalizedMessages(options.forkContextSeed.messages);
1763
+ }
1764
+
1729
1765
  agent = new Agent({
1730
1766
  initialState: {
1731
1767
  systemPrompt,
1732
1768
  model,
1733
1769
  thinkingLevel: toReasoningEffort(thinkingLevel),
1734
1770
  tools: initialTools,
1771
+ ...(options.forkContextSeed && !hasExistingSession
1772
+ ? { messages: options.forkContextSeed.agentMessages }
1773
+ : {}),
1735
1774
  },
1736
1775
  convertToLlm: convertToLlmFinal,
1737
1776
  onPayload,
1738
1777
  onResponse,
1739
- sessionId: providerSessionId,
1778
+ sessionId: logicalSessionId,
1779
+ providerSessionId,
1740
1780
  transformContext,
1741
1781
  steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
1742
1782
  followUpMode: settings.get("followUpMode") ?? "one-at-a-time",
@@ -1750,19 +1790,23 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1750
1790
  repetitionPenalty: settings.get("repetitionPenalty") >= 0 ? settings.get("repetitionPenalty") : undefined,
1751
1791
  serviceTier: initialServiceTier,
1752
1792
  hideThinkingSummary: settings.get("hideThinkingBlock"),
1793
+ maxRetryDelayMs: retrySettings.maxDelayMs,
1794
+ requestMaxRetries: retrySettings.requestMaxRetries,
1795
+ streamMaxRetries: retrySettings.streamMaxRetries,
1753
1796
  kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
1754
1797
  preferWebsockets: preferOpenAICodexWebsockets,
1755
1798
  getToolContext: tc => toolContextStore.getContext(tc),
1756
1799
  getApiKey: async provider => {
1757
1800
  // Read agent.sessionId at call time so credential selection stays aligned
1758
1801
  // with metadataResolver after /new, fork, resume, or branch switches.
1759
- const key = await modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
1802
+ const key = await modelRegistry.getApiKeyForProvider(provider, agent.providerSessionId ?? agent.sessionId);
1760
1803
  if (!key) {
1761
1804
  throw new Error(`No API key found for provider "${provider}"`);
1762
1805
  }
1763
1806
  return key;
1764
1807
  },
1765
- getAuthCredentialType: provider => modelRegistry.getSessionCredentialType(provider, agent.sessionId),
1808
+ getAuthCredentialType: provider =>
1809
+ modelRegistry.getSessionCredentialType(provider, agent.providerSessionId ?? agent.sessionId),
1766
1810
  streamFn: (streamModel, context, streamOptions) =>
1767
1811
  streamSimple(streamModel, context, {
1768
1812
  ...streamOptions,
@@ -1793,11 +1837,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1793
1837
  intentTracing: !!intentField,
1794
1838
  getToolChoice: () => session?.nextToolChoice(),
1795
1839
  telemetry: options.telemetry,
1796
- appendOnlyContext: model
1797
- ? resolveAppendOnlyMode(settings.get("provider.appendOnlyContext"), model.provider)
1798
- ? new AppendOnlyContextManager()
1799
- : undefined
1800
- : undefined,
1840
+ appendOnlyContext,
1801
1841
  });
1802
1842
 
1803
1843
  cursorEventEmitter = event => agent.emitExternalEvent(event);
@@ -1836,6 +1876,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1836
1876
  skillWarnings,
1837
1877
  skillsSettings: settings.getGroup("skills"),
1838
1878
  modelRegistry,
1879
+ taskDepth,
1839
1880
  toolRegistry,
1840
1881
  transformContext,
1841
1882
  onPayload,
@@ -1868,6 +1909,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1868
1909
  agentId: resolvedAgentId,
1869
1910
  agentRegistry,
1870
1911
  providerSessionId: options.providerSessionId,
1912
+ providerCacheSessionId: providerSessionId,
1913
+ forkContextSeed: options.forkContextSeed,
1914
+ providerSessionState: options.providerSessionState,
1871
1915
  });
1872
1916
  hasSession = true;
1873
1917
  if (asyncJobManager) {
@@ -28,8 +28,10 @@ import {
28
28
  type AgentTool,
29
29
  AppendOnlyContextManager,
30
30
  resolveTelemetry,
31
+ type StablePrefixSnapshot,
31
32
  ThinkingLevel,
32
33
  } from "@gajae-code/agent-core";
34
+ import { normalizeMessagesForProvider } from "@gajae-code/agent-core/agent-loop";
33
35
  import {
34
36
  AUTO_HANDOFF_THRESHOLD_FOCUS,
35
37
  CompactionCancelledError,
@@ -75,6 +77,33 @@ import {
75
77
  resolveServiceTier,
76
78
  streamSimple,
77
79
  } from "@gajae-code/ai";
80
+
81
+ export interface ForkContextSeedMetadata {
82
+ sourceSessionId: string;
83
+ parentMessageCount: number;
84
+ includedMessages: number;
85
+ skippedMessages: number;
86
+ approximateTokens: number;
87
+ maxMessages: number;
88
+ maxTokens: number;
89
+ skippedReasons: Record<string, number>;
90
+ }
91
+
92
+ export interface ForkContextSeed {
93
+ messages: Message[];
94
+ agentMessages: AgentMessage[];
95
+ metadata: ForkContextSeedMetadata;
96
+ cacheIdentity?: string;
97
+ appendOnlyPrefixSnapshot?: StablePrefixSnapshot;
98
+ }
99
+
100
+ export interface ForkContextSeedOptions {
101
+ maxMessages: number;
102
+ maxTokens: number;
103
+ cacheIdentity?: string;
104
+ signal?: AbortSignal;
105
+ }
106
+
78
107
  import { MacOSPowerAssertion } from "@gajae-code/natives";
79
108
  import {
80
109
  extractRetryHint,
@@ -168,7 +197,7 @@ import {
168
197
  } from "../runtime-mcp/discoverable-tool-metadata";
169
198
  import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
170
199
  import { formatNoCredentialOnboardingError, formatNoModelOnboardingError } from "../setup/model-onboarding-guidance";
171
- import { isCanonicalGjcWorkflowSkill, syncSkillActiveState } from "../skill-state/active-state";
200
+ import { isCanonicalGjcWorkflowSkill } from "../skill-state/active-state";
172
201
  import { assertDeepInterviewMutationAllowed } from "../skill-state/deep-interview-mutation-guard";
173
202
  import { invalidateHostMetadata } from "../ssh/connection-manager";
174
203
  import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
@@ -246,6 +275,13 @@ export type AgentSessionEvent =
246
275
  | { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
247
276
  | { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
248
277
 
278
+ /**
279
+ * Safe path component pattern used to validate session-id segments before
280
+ * joining them into `.gjc/state` paths. Mirrors the regex used by the
281
+ * `gjc state` runtime selector resolver.
282
+ */
283
+ const SAFE_PATH_COMPONENT = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
284
+
249
285
  /** Listener function for agent session events */
250
286
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
251
287
  export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
@@ -283,6 +319,8 @@ export interface AgentSessionConfig {
283
319
  skillsSettings?: SkillsSettings;
284
320
  /** Model registry for API key resolution and model discovery */
285
321
  modelRegistry: ModelRegistry;
322
+ /** Task recursion depth for nested sessions. Top-level sessions use 0. */
323
+ taskDepth?: number;
286
324
  /** Tool registry for LSP and settings */
287
325
  toolRegistry?: Map<string, AgentTool>;
288
326
  /** Current session pre-LLM message transform pipeline */
@@ -331,6 +369,10 @@ export interface AgentSessionConfig {
331
369
  * **MUST NOT** dispose it on their own teardown.
332
370
  */
333
371
  ownedAsyncJobManager?: AsyncJobManager;
372
+ /** Optional fork-context seed used to initialize a child session before its first prompt. */
373
+ forkContextSeed?: ForkContextSeed;
374
+ /** Optional provider state override. Fork-context children should omit this by default. */
375
+ providerSessionState?: Map<string, ProviderSessionState>;
334
376
  /** Agent identity (registry id like "0-Main" or "3-Alice") used for IRC routing. */
335
377
  agentId?: string;
336
378
  /** Shared agent registry (for forwarding IRC observations to the main session UI). */
@@ -342,6 +384,8 @@ export interface AgentSessionConfig {
342
384
  * so that credential sticky selection is consistent with the session's streaming calls.
343
385
  */
344
386
  providerSessionId?: string;
387
+ /** Optional provider-facing cache identity, distinct from logical session identity. */
388
+ providerCacheSessionId?: string;
345
389
  }
346
390
 
347
391
  /** Options for AgentSession.prompt() */
@@ -749,6 +793,7 @@ export class AgentSession {
749
793
  readonly agent: Agent;
750
794
  readonly sessionManager: SessionManager;
751
795
  readonly settings: Settings;
796
+ readonly taskDepth: number;
752
797
  readonly yieldQueue: YieldQueue;
753
798
 
754
799
  #powerAssertion: MacOSPowerAssertion | undefined;
@@ -836,6 +881,7 @@ export class AgentSession {
836
881
  #agentId: string | undefined;
837
882
  #agentRegistry: AgentRegistry | undefined;
838
883
  #providerSessionId: string | undefined;
884
+ #providerCacheSessionId: string | undefined;
839
885
  #isDisposed = false;
840
886
  // Extension system
841
887
  #extensionRunner: ExtensionRunner | undefined = undefined;
@@ -1007,6 +1053,7 @@ export class AgentSession {
1007
1053
  this.agent = config.agent;
1008
1054
  this.sessionManager = config.sessionManager;
1009
1055
  this.settings = config.settings;
1056
+ this.taskDepth = config.taskDepth ?? 0;
1010
1057
  // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
1011
1058
  this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
1012
1059
  this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
@@ -1020,6 +1067,9 @@ export class AgentSession {
1020
1067
  this.#customCommands = config.customCommands ?? [];
1021
1068
  this.#skillsSettings = config.skillsSettings;
1022
1069
  this.#modelRegistry = config.modelRegistry;
1070
+ if (config.providerSessionState) {
1071
+ this.#providerSessionState = config.providerSessionState;
1072
+ }
1023
1073
  this.#validateRetryFallbackChains();
1024
1074
  this.#toolRegistry = config.toolRegistry ?? new Map();
1025
1075
  this.#requestedToolNames = config.requestedToolNames;
@@ -1099,6 +1149,7 @@ export class AgentSession {
1099
1149
  this.#agentId = config.agentId;
1100
1150
  this.#agentRegistry = config.agentRegistry;
1101
1151
  this.#providerSessionId = config.providerSessionId;
1152
+ this.#providerCacheSessionId = config.providerCacheSessionId;
1102
1153
  this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
1103
1154
  const event: AgentEvent = {
1104
1155
  type: "message_update",
@@ -1196,6 +1247,46 @@ export class AgentSession {
1196
1247
  return this.#toolChoiceQueue;
1197
1248
  }
1198
1249
 
1250
+ /** Current skill prompt executing in this session, if any. */
1251
+ getActiveSkillState(): { skill: string; session_id?: string } | undefined {
1252
+ if (!this.#activeSkillState) return undefined;
1253
+ return {
1254
+ skill: this.#activeSkillState.skill,
1255
+ ...(this.#activeSkillState.sessionId ? { session_id: this.#activeSkillState.sessionId } : {}),
1256
+ };
1257
+ }
1258
+
1259
+ /** Best-effort accessor for the active skill's `current_phase` field from
1260
+ * its persisted mode-state file. Used by the `skill` tool to enforce the
1261
+ * terminal-phase chain guard. Returns undefined when no active skill is
1262
+ * recorded or the mode-state file is missing/unreadable; callers should
1263
+ * treat undefined as a non-terminal phase (refuses to chain). */
1264
+ getActiveSkillPhase(): string | undefined {
1265
+ const active = this.#activeSkillState;
1266
+ if (!active) return undefined;
1267
+ // Path safety: refuse to read mode-state files when the skill or
1268
+ // session-id are not safe path components. The `skill` tool
1269
+ // interprets undefined as a non-terminal phase, so chaining is
1270
+ // refused — there is no risk of bypassing the guard via a custom
1271
+ // skill name with `..` or a session-id with separators.
1272
+ if (!isCanonicalGjcWorkflowSkill(active.skill)) return undefined;
1273
+ if (active.sessionId !== undefined && !SAFE_PATH_COMPONENT.test(active.sessionId)) {
1274
+ return undefined;
1275
+ }
1276
+ try {
1277
+ const stateDir = path.join(this.sessionManager.getCwd(), ".gjc", "state");
1278
+ const segments = active.sessionId
1279
+ ? [stateDir, "sessions", encodeURIComponent(active.sessionId).replaceAll(".", "%2E")]
1280
+ : [stateDir];
1281
+ const filePath = path.join(...segments, `${active.skill}-state.json`);
1282
+ const raw = fs.readFileSync(filePath, "utf-8");
1283
+ const parsed = JSON.parse(raw) as { current_phase?: unknown };
1284
+ return typeof parsed.current_phase === "string" ? parsed.current_phase : undefined;
1285
+ } catch {
1286
+ return undefined;
1287
+ }
1288
+ }
1289
+
1199
1290
  /** Peek the in-flight directive's invocation handler for use by the resolve tool. */
1200
1291
  peekQueueInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined {
1201
1292
  return this.#toolChoiceQueue.peekInFlightInvoker();
@@ -1219,6 +1310,100 @@ export class AgentSession {
1219
1310
  return this.#providerSessionState;
1220
1311
  }
1221
1312
 
1313
+ async buildForkContextSeed(options: ForkContextSeedOptions): Promise<ForkContextSeed> {
1314
+ const transformedMessages = await this.#transformContext([...this.messages], options.signal);
1315
+ const convertedMessages = await this.#convertToLlm(transformedMessages);
1316
+ const providerMessages = this.model
1317
+ ? normalizeMessagesForProvider(convertedMessages, this.model)
1318
+ : convertedMessages;
1319
+ const maxMessages = Math.min(500, Math.max(0, Math.trunc(options.maxMessages)));
1320
+ const maxTokens = Math.max(0, Math.trunc(options.maxTokens));
1321
+ const selected: Message[] = [];
1322
+ const skippedReasons: Record<string, number> = {};
1323
+ let skippedMessages = 0;
1324
+ let approximateTokens = 0;
1325
+
1326
+ const recordSkip = (reason: string) => {
1327
+ skippedMessages++;
1328
+ skippedReasons[reason] = (skippedReasons[reason] ?? 0) + 1;
1329
+ };
1330
+
1331
+ const sanitizeMessage = (message: Message): Message | undefined => {
1332
+ if (message.role === "developer") {
1333
+ recordSkip("developer-role");
1334
+ return undefined;
1335
+ }
1336
+ if (message.role === "toolResult") {
1337
+ recordSkip("tool-result-role");
1338
+ return undefined;
1339
+ }
1340
+ if (message.role !== "user" && message.role !== "assistant") {
1341
+ recordSkip("unsupported-role");
1342
+ return undefined;
1343
+ }
1344
+ const cloned = structuredClone(message) as Message;
1345
+ if ("providerPayload" in cloned) {
1346
+ delete (cloned as { providerPayload?: unknown }).providerPayload;
1347
+ }
1348
+ if (Array.isArray(cloned.content)) {
1349
+ const sanitizedContent: TextContent[] = [];
1350
+ for (const block of cloned.content) {
1351
+ if (block.type === "text") {
1352
+ sanitizedContent.push(block);
1353
+ } else if (block.type === "image") {
1354
+ sanitizedContent.push({ type: "text", text: "[Image omitted from fork-context seed]" });
1355
+ } else if (block.type !== "thinking") {
1356
+ recordSkip(`unsupported-content-${block.type}`);
1357
+ }
1358
+ }
1359
+ return { ...cloned, content: sanitizedContent } as Message;
1360
+ }
1361
+ return cloned;
1362
+ };
1363
+
1364
+ for (let i = providerMessages.length - 1; i >= 0; i--) {
1365
+ if (selected.length >= maxMessages) {
1366
+ recordSkip("message-limit");
1367
+ continue;
1368
+ }
1369
+ const sanitized = sanitizeMessage(providerMessages[i]!);
1370
+ if (!sanitized) continue;
1371
+ const messageTokens = estimateTokens(sanitized);
1372
+ if (maxTokens > 0 && approximateTokens + messageTokens > maxTokens) {
1373
+ recordSkip("token-limit");
1374
+ continue;
1375
+ }
1376
+ selected.unshift(sanitized);
1377
+ approximateTokens += messageTokens;
1378
+ }
1379
+
1380
+ const messages = selected;
1381
+ let appendOnlyPrefixSnapshot: StablePrefixSnapshot | undefined;
1382
+ const appendOnly = this.agent.appendOnlyContext;
1383
+ if (appendOnly) {
1384
+ if (!appendOnly.prefix.built) {
1385
+ appendOnly.prefix.build(this.agent.state, { intentTracing: this.agent.intentTracing });
1386
+ }
1387
+ appendOnlyPrefixSnapshot = appendOnly.prefix.exportSnapshot() ?? undefined;
1388
+ }
1389
+ return {
1390
+ messages,
1391
+ agentMessages: messages.map(message => structuredClone(message) as AgentMessage),
1392
+ metadata: {
1393
+ sourceSessionId: this.sessionId,
1394
+ parentMessageCount: providerMessages.length,
1395
+ includedMessages: messages.length,
1396
+ skippedMessages,
1397
+ approximateTokens,
1398
+ maxMessages,
1399
+ maxTokens,
1400
+ skippedReasons,
1401
+ },
1402
+ cacheIdentity: options.cacheIdentity ?? this.sessionId,
1403
+ appendOnlyPrefixSnapshot,
1404
+ };
1405
+ }
1406
+
1222
1407
  getHindsightSessionState(): HindsightSessionState | undefined {
1223
1408
  return this.#hindsightSessionState;
1224
1409
  }
@@ -1555,11 +1740,7 @@ export class AgentSession {
1555
1740
  }
1556
1741
 
1557
1742
  const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
1558
- if (
1559
- !this.#ttsrAbortPending ||
1560
- this.#promptGeneration !== generation ||
1561
- targetAssistantIndex === -1
1562
- ) {
1743
+ if (!this.#ttsrAbortPending || this.#promptGeneration !== generation) {
1563
1744
  this.#ttsrAbortPending = false;
1564
1745
  this.#pendingTtsrInjections = [];
1565
1746
  this.#perToolTtsrInjections.clear();
@@ -1569,8 +1750,8 @@ export class AgentSession {
1569
1750
  this.#ttsrAbortPending = false;
1570
1751
  this.#perToolTtsrInjections.clear();
1571
1752
  const ttsrSettings = this.#ttsrManager?.getSettings();
1572
- if (ttsrSettings?.contextMode === "discard") {
1573
- // Remove the partial/aborted assistant turn from agent state
1753
+ if (ttsrSettings?.contextMode === "discard" && targetAssistantIndex !== -1) {
1754
+ // Remove the partial/aborted assistant turn from agent state when it was persisted.
1574
1755
  this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
1575
1756
  }
1576
1757
  // Inject TTSR rules as system reminder before retry
@@ -2757,6 +2938,7 @@ export class AgentSession {
2757
2938
  #syncAgentSessionId(sessionId?: string): void {
2758
2939
  const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
2759
2940
  this.agent.sessionId = sid;
2941
+ this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
2760
2942
  this.agent.setMetadataResolver((provider: string) =>
2761
2943
  buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
2762
2944
  );
@@ -4092,18 +4274,19 @@ export class AgentSession {
4092
4274
  const details = message.details;
4093
4275
  if (!details || typeof details !== "object") return;
4094
4276
  const name = (details as { name?: unknown }).name;
4095
- if (typeof name !== "string" || !isCanonicalGjcWorkflowSkill(name)) return;
4096
- const cwd = this.sessionManager.getCwd();
4277
+ if (typeof name !== "string" || !name.trim()) return;
4278
+ const skill = name.trim();
4097
4279
  const sessionId = this.sessionManager.getSessionId();
4098
- await syncSkillActiveState({
4099
- cwd,
4100
- skill: name,
4101
- active,
4102
- phase: active ? "running" : "complete",
4103
- sessionId,
4104
- source: "skill-prompt",
4105
- });
4106
- this.#activeSkillState = active ? { skill: name, sessionId } : undefined;
4280
+ // Canonical GJC workflow skills (deep-interview, ralplan, ultragoal, team)
4281
+ // own their `.gjc/state/skill-active-state.json` row through the
4282
+ // `gjc state handoff` and `gjc state clear` runtime verbs. The prompt
4283
+ // observer here used to overwrite the row with `phase: running` and
4284
+ // later remove it with `active:false`, which clobbered handoff lineage
4285
+ // (`handoff_from`/`handoff_at`) and made the HUD inconsistent with
4286
+ // mode-state. The observational filesystem write is now skipped for
4287
+ // canonical skills; the in-memory `#activeSkillState` tracking below
4288
+ // keeps `getActiveSkillState` accurate for the chain guard.
4289
+ this.#activeSkillState = active ? { skill, sessionId } : undefined;
4107
4290
  }
4108
4291
 
4109
4292
  async #syncSkillPromptActiveStateSafely(
@@ -6147,7 +6330,7 @@ export class AgentSession {
6147
6330
 
6148
6331
  #closeCodexProviderSessionsForHistoryRewrite(): void {
6149
6332
  const currentModel = this.model;
6150
- if (!currentModel || currentModel.api !== "openai-codex-responses") return;
6333
+ if (currentModel?.api !== "openai-codex-responses") return;
6151
6334
  this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
6152
6335
  }
6153
6336
 
@@ -8228,7 +8411,7 @@ export class AgentSession {
8228
8411
  const previousSessionFile = this.sessionFile;
8229
8412
  const selectedEntry = this.sessionManager.getEntry(entryId);
8230
8413
 
8231
- if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
8414
+ if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
8232
8415
  throw new Error("Invalid entry ID for branching");
8233
8416
  }
8234
8417
 
@@ -176,6 +176,8 @@ export interface SessionInitEntry extends SessionEntryBase {
176
176
  tools: string[];
177
177
  /** Output schema if structured output was requested */
178
178
  outputSchema?: unknown;
179
+ /** Fork-context seed metadata for subagent debugging/replay. */
180
+ forkContext?: unknown;
179
181
  }
180
182
 
181
183
  /** Mode change entry - tracks agent mode transitions (e.g. plan mode). */
@@ -2714,7 +2716,13 @@ export class SessionManager {
2714
2716
  }
2715
2717
 
2716
2718
  /** Append session init metadata (for subagent debugging/replay). Returns entry id. */
2717
- appendSessionInit(init: { systemPrompt: string; task: string; tools: string[]; outputSchema?: unknown }): string {
2719
+ appendSessionInit(init: {
2720
+ systemPrompt: string;
2721
+ task: string;
2722
+ tools: string[];
2723
+ outputSchema?: unknown;
2724
+ forkContext?: unknown;
2725
+ }): string {
2718
2726
  const entry: SessionInitEntry = {
2719
2727
  type: "session_init",
2720
2728
  id: generateId(this.#byId),
@@ -1,5 +1,6 @@
1
1
  export const MODEL_ONBOARDING_API_PROVIDER_COMMAND =
2
2
  "/provider add --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <model>";
3
+ export const MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND = "/provider add --preset <minimax|minimax-cn|glm>";
3
4
 
4
5
  export const MODEL_ONBOARDING_SETUP_COMMAND = "gjc setup provider";
5
6
  export const MODEL_ONBOARDING_OAUTH_COMMAND = "/provider login [provider-id] or /login [provider-id]";
@@ -9,14 +10,15 @@ export function formatModelOnboardingGuidance(): string {
9
10
  "Model selection only shows configured providers.",
10
11
  "Assignment targets are DEFAULT plus the GJC role agents: EXECUTOR, ARCHITECT, PLANNER, and CRITIC.",
11
12
  "Legacy model-role aliases are compatibility-only and are not shown as assignment targets.",
12
- `API-compatible providers: ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}).`,
13
+ `Provider presets: ${MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND} --preset <preset>).`,
14
+ `API-compatible custom providers: ${MODEL_ONBOARDING_API_PROVIDER_COMMAND}.`,
13
15
  `OAuth/subscription providers: ${MODEL_ONBOARDING_OAUTH_COMMAND}.`,
14
16
  "Then run /model to select a configured model or assign it to a target.",
15
17
  ].join("\n");
16
18
  }
17
19
 
18
20
  export function formatModelOnboardingInlineHint(): string {
19
- return `Add API-compatible providers with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}); OAuth/subscription with ${MODEL_ONBOARDING_OAUTH_COMMAND}; then run /model for DEFAULT, EXECUTOR, ARCHITECT, PLANNER, and CRITIC.`;
21
+ return `Add MiniMax/GLM presets with ${MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND}; custom API providers with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}); OAuth/subscription with ${MODEL_ONBOARDING_OAUTH_COMMAND}; then run /model for DEFAULT, EXECUTOR, ARCHITECT, PLANNER, and CRITIC.`;
20
22
  }
21
23
 
22
24
  export function formatNoModelOnboardingError(): string {
@@ -27,7 +29,8 @@ export function formatNoCredentialOnboardingError(providerId: string): string {
27
29
  return [
28
30
  `No credentials found for ${providerId}.`,
29
31
  "",
30
- `For API-compatible providers, configure credentials with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}).`,
32
+ `For MiniMax/GLM presets, configure credentials with ${MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND} --preset <preset>).`,
33
+ `For custom API-compatible providers, use ${MODEL_ONBOARDING_API_PROVIDER_COMMAND}.`,
31
34
  `For OAuth/subscription providers, use ${MODEL_ONBOARDING_OAUTH_COMMAND}.`,
32
35
  "Then run /model to select a configured model or assign it to DEFAULT, EXECUTOR, ARCHITECT, PLANNER, or CRITIC.",
33
36
  ].join("\n");