@gajae-code/coding-agent 0.2.1 → 0.2.3

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 (153) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/dist/types/cli/setup-cli.d.ts +1 -0
  3. package/dist/types/commands/contribution-prep.d.ts +18 -0
  4. package/dist/types/commands/deep-interview.d.ts +41 -0
  5. package/dist/types/commands/session.d.ts +24 -0
  6. package/dist/types/commands/setup.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +2 -2
  8. package/dist/types/config/models-config-schema.d.ts +17 -9
  9. package/dist/types/config/settings-schema.d.ts +37 -24
  10. package/dist/types/discovery/helpers.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +33 -0
  13. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  16. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  18. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  19. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  20. package/dist/types/goals/runtime.d.ts +3 -9
  21. package/dist/types/goals/state.d.ts +3 -6
  22. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  23. package/dist/types/hooks/skill-state.d.ts +5 -0
  24. package/dist/types/memories/index.d.ts +1 -1
  25. package/dist/types/memory-backend/local-backend.d.ts +3 -3
  26. package/dist/types/modes/components/hook-selector.d.ts +7 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +0 -2
  28. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  29. package/dist/types/modes/components/status-line.d.ts +0 -3
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  31. package/dist/types/modes/interactive-mode.d.ts +1 -12
  32. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  33. package/dist/types/modes/theme/theme.d.ts +1 -2
  34. package/dist/types/modes/types.d.ts +1 -7
  35. package/dist/types/modes/utils/context-usage.d.ts +6 -2
  36. package/dist/types/sdk.d.ts +6 -2
  37. package/dist/types/session/agent-session.d.ts +47 -1
  38. package/dist/types/session/contribution-prep.d.ts +47 -0
  39. package/dist/types/session/session-manager.d.ts +3 -0
  40. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
  41. package/dist/types/setup/provider-onboarding.d.ts +29 -5
  42. package/dist/types/skill-state/active-state.d.ts +30 -1
  43. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  44. package/dist/types/skill-state/initial-phase.d.ts +12 -0
  45. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  46. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  47. package/dist/types/task/executor.d.ts +2 -0
  48. package/dist/types/task/types.d.ts +11 -0
  49. package/dist/types/tools/index.d.ts +20 -1
  50. package/dist/types/tools/skill.d.ts +47 -0
  51. package/dist/types/utils/changelog.d.ts +18 -2
  52. package/package.json +7 -7
  53. package/src/cli/args.ts +3 -2
  54. package/src/cli/setup-cli.ts +26 -12
  55. package/src/cli.ts +7 -1
  56. package/src/commands/contribution-prep.ts +41 -0
  57. package/src/commands/deep-interview.ts +30 -23
  58. package/src/commands/launch.ts +10 -1
  59. package/src/commands/ralplan.ts +10 -22
  60. package/src/commands/session.ts +150 -0
  61. package/src/commands/setup.ts +2 -0
  62. package/src/commands/state.ts +15 -4
  63. package/src/commands/team.ts +23 -3
  64. package/src/config/model-registry.ts +10 -2
  65. package/src/config/models-config-schema.ts +120 -102
  66. package/src/config/settings-schema.ts +42 -25
  67. package/src/config.ts +1 -1
  68. package/src/defaults/gjc/skills/deep-interview/SKILL.md +32 -13
  69. package/src/defaults/gjc/skills/ralplan/SKILL.md +22 -2
  70. package/src/defaults/gjc/skills/team/SKILL.md +39 -7
  71. package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -25
  72. package/src/discovery/helpers.ts +24 -1
  73. package/src/eval/py/prelude.py +1 -1
  74. package/src/extensibility/extensions/types.ts +6 -0
  75. package/src/gjc-runtime/deep-interview-runtime.ts +546 -0
  76. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  77. package/src/gjc-runtime/launch-tmux.ts +83 -43
  78. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  79. package/src/gjc-runtime/state-runtime.ts +731 -0
  80. package/src/gjc-runtime/team-runtime.ts +708 -52
  81. package/src/gjc-runtime/tmux-common.ts +119 -0
  82. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  83. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  84. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  85. package/src/goals/runtime.ts +38 -144
  86. package/src/goals/state.ts +36 -7
  87. package/src/goals/tools/goal-tool.ts +15 -172
  88. package/src/hooks/skill-state.ts +39 -18
  89. package/src/internal-urls/docs-index.generated.ts +5 -4
  90. package/src/internal-urls/memory-protocol.ts +3 -2
  91. package/src/main.ts +2 -3
  92. package/src/memories/index.ts +2 -1
  93. package/src/memory-backend/local-backend.ts +14 -6
  94. package/src/modes/components/hook-selector.ts +156 -1
  95. package/src/modes/components/settings-selector.ts +5 -12
  96. package/src/modes/components/skill-hud/render.ts +4 -0
  97. package/src/modes/components/status-line/segments.ts +5 -16
  98. package/src/modes/components/status-line/types.ts +0 -3
  99. package/src/modes/components/status-line.ts +0 -6
  100. package/src/modes/controllers/command-controller.ts +27 -4
  101. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  102. package/src/modes/controllers/input-controller.ts +0 -15
  103. package/src/modes/controllers/selector-controller.ts +4 -11
  104. package/src/modes/interactive-mode.ts +18 -219
  105. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  106. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  107. package/src/modes/theme/theme.ts +0 -6
  108. package/src/modes/types.ts +1 -7
  109. package/src/modes/utils/context-usage.ts +66 -17
  110. package/src/prompts/agents/architect.md +3 -0
  111. package/src/prompts/agents/executor.md +2 -0
  112. package/src/prompts/agents/frontmatter.md +1 -0
  113. package/src/prompts/goals/goal-continuation.md +1 -4
  114. package/src/prompts/goals/goal-mode-active.md +3 -5
  115. package/src/prompts/system/subagent-system-prompt.md +6 -0
  116. package/src/prompts/system/system-prompt.md +5 -7
  117. package/src/prompts/tools/goal.md +4 -4
  118. package/src/prompts/tools/skill.md +28 -0
  119. package/src/prompts/tools/task.md +3 -0
  120. package/src/sdk.ts +51 -11
  121. package/src/session/agent-session.ts +222 -21
  122. package/src/session/contribution-prep.ts +320 -0
  123. package/src/session/session-manager.ts +9 -1
  124. package/src/setup/model-onboarding-guidance.ts +6 -3
  125. package/src/setup/provider-onboarding.ts +177 -16
  126. package/src/skill-state/active-state.ts +188 -25
  127. package/src/skill-state/deep-interview-mutation-guard.ts +72 -21
  128. package/src/skill-state/initial-phase.ts +17 -0
  129. package/src/skill-state/workflow-hud.ts +23 -5
  130. package/src/skill-state/workflow-state-contract.ts +121 -0
  131. package/src/slash-commands/builtin-registry.ts +75 -25
  132. package/src/slash-commands/helpers/context-report.ts +123 -13
  133. package/src/task/agents.ts +1 -0
  134. package/src/task/commands.ts +1 -5
  135. package/src/task/executor.ts +9 -1
  136. package/src/task/index.ts +91 -4
  137. package/src/task/types.ts +6 -0
  138. package/src/tools/ask.ts +2 -0
  139. package/src/tools/gh.ts +212 -2
  140. package/src/tools/index.ts +25 -6
  141. package/src/tools/skill.ts +153 -0
  142. package/src/utils/changelog.ts +67 -44
  143. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  144. package/dist/types/commands/question.d.ts +0 -7
  145. package/dist/types/modes/loop-limit.d.ts +0 -22
  146. package/src/commands/gjc-runtime-bridge.ts +0 -227
  147. package/src/commands/question.ts +0 -12
  148. package/src/modes/loop-limit.ts +0 -140
  149. package/src/prompts/commands/orchestrate.md +0 -49
  150. package/src/prompts/goals/goal-budget-limit.md +0 -16
  151. package/src/prompts/tools/create-goal.md +0 -3
  152. package/src/prompts/tools/get-goal.md +0 -3
  153. package/src/prompts/tools/update-goal.md +0 -3
@@ -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";
@@ -192,6 +221,11 @@ import { extractFileMentions, generateFileMentionMessages } from "../utils/file-
192
221
  import { buildNamedToolChoice } from "../utils/tool-choice";
193
222
  import type { AuthStorage } from "./auth-storage";
194
223
  import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
224
+ import {
225
+ type ContributionPrepOptions,
226
+ type ContributionPrepResult,
227
+ prepareContributionPrep,
228
+ } from "./contribution-prep";
195
229
  import {
196
230
  type BashExecutionMessage,
197
231
  type CompactionSummaryMessage,
@@ -241,6 +275,13 @@ export type AgentSessionEvent =
241
275
  | { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
242
276
  | { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
243
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
+
244
285
  /** Listener function for agent session events */
245
286
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
246
287
  export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
@@ -278,6 +319,8 @@ export interface AgentSessionConfig {
278
319
  skillsSettings?: SkillsSettings;
279
320
  /** Model registry for API key resolution and model discovery */
280
321
  modelRegistry: ModelRegistry;
322
+ /** Task recursion depth for nested sessions. Top-level sessions use 0. */
323
+ taskDepth?: number;
281
324
  /** Tool registry for LSP and settings */
282
325
  toolRegistry?: Map<string, AgentTool>;
283
326
  /** Current session pre-LLM message transform pipeline */
@@ -326,6 +369,10 @@ export interface AgentSessionConfig {
326
369
  * **MUST NOT** dispose it on their own teardown.
327
370
  */
328
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>;
329
376
  /** Agent identity (registry id like "0-Main" or "3-Alice") used for IRC routing. */
330
377
  agentId?: string;
331
378
  /** Shared agent registry (for forwarding IRC observations to the main session UI). */
@@ -337,6 +384,8 @@ export interface AgentSessionConfig {
337
384
  * so that credential sticky selection is consistent with the session's streaming calls.
338
385
  */
339
386
  providerSessionId?: string;
387
+ /** Optional provider-facing cache identity, distinct from logical session identity. */
388
+ providerCacheSessionId?: string;
340
389
  }
341
390
 
342
391
  /** Options for AgentSession.prompt() */
@@ -744,6 +793,7 @@ export class AgentSession {
744
793
  readonly agent: Agent;
745
794
  readonly sessionManager: SessionManager;
746
795
  readonly settings: Settings;
796
+ readonly taskDepth: number;
747
797
  readonly yieldQueue: YieldQueue;
748
798
 
749
799
  #powerAssertion: MacOSPowerAssertion | undefined;
@@ -831,6 +881,7 @@ export class AgentSession {
831
881
  #agentId: string | undefined;
832
882
  #agentRegistry: AgentRegistry | undefined;
833
883
  #providerSessionId: string | undefined;
884
+ #providerCacheSessionId: string | undefined;
834
885
  #isDisposed = false;
835
886
  // Extension system
836
887
  #extensionRunner: ExtensionRunner | undefined = undefined;
@@ -1002,6 +1053,7 @@ export class AgentSession {
1002
1053
  this.agent = config.agent;
1003
1054
  this.sessionManager = config.sessionManager;
1004
1055
  this.settings = config.settings;
1056
+ this.taskDepth = config.taskDepth ?? 0;
1005
1057
  // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
1006
1058
  this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
1007
1059
  this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
@@ -1015,6 +1067,9 @@ export class AgentSession {
1015
1067
  this.#customCommands = config.customCommands ?? [];
1016
1068
  this.#skillsSettings = config.skillsSettings;
1017
1069
  this.#modelRegistry = config.modelRegistry;
1070
+ if (config.providerSessionState) {
1071
+ this.#providerSessionState = config.providerSessionState;
1072
+ }
1018
1073
  this.#validateRetryFallbackChains();
1019
1074
  this.#toolRegistry = config.toolRegistry ?? new Map();
1020
1075
  this.#requestedToolNames = config.requestedToolNames;
@@ -1094,6 +1149,7 @@ export class AgentSession {
1094
1149
  this.#agentId = config.agentId;
1095
1150
  this.#agentRegistry = config.agentRegistry;
1096
1151
  this.#providerSessionId = config.providerSessionId;
1152
+ this.#providerCacheSessionId = config.providerCacheSessionId;
1097
1153
  this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
1098
1154
  const event: AgentEvent = {
1099
1155
  type: "message_update",
@@ -1191,6 +1247,46 @@ export class AgentSession {
1191
1247
  return this.#toolChoiceQueue;
1192
1248
  }
1193
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
+
1194
1290
  /** Peek the in-flight directive's invocation handler for use by the resolve tool. */
1195
1291
  peekQueueInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined {
1196
1292
  return this.#toolChoiceQueue.peekInFlightInvoker();
@@ -1214,6 +1310,100 @@ export class AgentSession {
1214
1310
  return this.#providerSessionState;
1215
1311
  }
1216
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
+
1217
1407
  getHindsightSessionState(): HindsightSessionState | undefined {
1218
1408
  return this.#hindsightSessionState;
1219
1409
  }
@@ -1550,11 +1740,7 @@ export class AgentSession {
1550
1740
  }
1551
1741
 
1552
1742
  const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
1553
- if (
1554
- !this.#ttsrAbortPending ||
1555
- this.#promptGeneration !== generation ||
1556
- targetAssistantIndex === -1
1557
- ) {
1743
+ if (!this.#ttsrAbortPending || this.#promptGeneration !== generation) {
1558
1744
  this.#ttsrAbortPending = false;
1559
1745
  this.#pendingTtsrInjections = [];
1560
1746
  this.#perToolTtsrInjections.clear();
@@ -1564,8 +1750,8 @@ export class AgentSession {
1564
1750
  this.#ttsrAbortPending = false;
1565
1751
  this.#perToolTtsrInjections.clear();
1566
1752
  const ttsrSettings = this.#ttsrManager?.getSettings();
1567
- if (ttsrSettings?.contextMode === "discard") {
1568
- // 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.
1569
1755
  this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
1570
1756
  }
1571
1757
  // Inject TTSR rules as system reminder before retry
@@ -2752,6 +2938,7 @@ export class AgentSession {
2752
2938
  #syncAgentSessionId(sessionId?: string): void {
2753
2939
  const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
2754
2940
  this.agent.sessionId = sid;
2941
+ this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
2755
2942
  this.agent.setMetadataResolver((provider: string) =>
2756
2943
  buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
2757
2944
  );
@@ -4087,18 +4274,19 @@ export class AgentSession {
4087
4274
  const details = message.details;
4088
4275
  if (!details || typeof details !== "object") return;
4089
4276
  const name = (details as { name?: unknown }).name;
4090
- if (typeof name !== "string" || !isCanonicalGjcWorkflowSkill(name)) return;
4091
- const cwd = this.sessionManager.getCwd();
4277
+ if (typeof name !== "string" || !name.trim()) return;
4278
+ const skill = name.trim();
4092
4279
  const sessionId = this.sessionManager.getSessionId();
4093
- await syncSkillActiveState({
4094
- cwd,
4095
- skill: name,
4096
- active,
4097
- phase: active ? "running" : "complete",
4098
- sessionId,
4099
- source: "skill-prompt",
4100
- });
4101
- 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;
4102
4290
  }
4103
4291
 
4104
4292
  async #syncSkillPromptActiveStateSafely(
@@ -5762,6 +5950,19 @@ export class AgentSession {
5762
5950
  }
5763
5951
  }
5764
5952
 
5953
+ async prepareContributionPrep(options: ContributionPrepOptions = {}): Promise<ContributionPrepResult> {
5954
+ return prepareContributionPrep(
5955
+ {
5956
+ sessionId: this.sessionId,
5957
+ cwd: this.sessionManager.getCwd(),
5958
+ sessionFile: this.sessionFile,
5959
+ messages: this.agent.state.messages,
5960
+ customInstructions: options.customInstructions,
5961
+ },
5962
+ options,
5963
+ );
5964
+ }
5965
+
5765
5966
  /**
5766
5967
  * Check if context maintenance or promotion is needed and run it.
5767
5968
  * Called after agent_end and before prompt submission.
@@ -6129,7 +6330,7 @@ export class AgentSession {
6129
6330
 
6130
6331
  #closeCodexProviderSessionsForHistoryRewrite(): void {
6131
6332
  const currentModel = this.model;
6132
- if (!currentModel || currentModel.api !== "openai-codex-responses") return;
6333
+ if (currentModel?.api !== "openai-codex-responses") return;
6133
6334
  this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
6134
6335
  }
6135
6336
 
@@ -8210,7 +8411,7 @@ export class AgentSession {
8210
8411
  const previousSessionFile = this.sessionFile;
8211
8412
  const selectedEntry = this.sessionManager.getEntry(entryId);
8212
8413
 
8213
- if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
8414
+ if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
8214
8415
  throw new Error("Invalid entry ID for branching");
8215
8416
  }
8216
8417