@gajae-code/coding-agent 0.11.8 → 0.11.9

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 (57) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/dist/types/cli.d.ts +1 -0
  3. package/dist/types/config/model-profiles.d.ts +2 -3
  4. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
  5. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
  6. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
  7. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
  8. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
  9. package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
  10. package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
  11. package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
  12. package/dist/types/modes/components/welcome.d.ts +2 -0
  13. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  14. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
  15. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  16. package/dist/types/session/internal/managed-session-scope.d.ts +12 -2
  17. package/dist/types/session/internal/managed-session-storage.d.ts +8 -3
  18. package/dist/types/session/session-manager.d.ts +5 -1
  19. package/dist/types/session/session-storage.d.ts +13 -0
  20. package/dist/types/skill-state/active-state.d.ts +4 -0
  21. package/package.json +8 -8
  22. package/src/cli/mcp-cli.ts +4 -21
  23. package/src/cli.ts +32 -0
  24. package/src/commands/deep-interview.ts +9 -4
  25. package/src/commit/agentic/index.ts +3 -3
  26. package/src/commit/map-reduce/index.ts +2 -2
  27. package/src/config/model-profiles.ts +2 -3
  28. package/src/defaults/gjc/skills/deep-interview/SKILL.md +78 -163
  29. package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
  30. package/src/gjc-runtime/deep-interview-draft.ts +842 -0
  31. package/src/gjc-runtime/deep-interview-payload.ts +262 -0
  32. package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
  33. package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
  34. package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
  35. package/src/gjc-runtime/deep-interview-state.ts +916 -4
  36. package/src/gjc-runtime/ralplan-runtime.ts +3 -1
  37. package/src/gjc-runtime/state-writer.ts +166 -2
  38. package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
  39. package/src/gjc-runtime/workflow-manifest.ts +191 -5
  40. package/src/internal-urls/docs-index.generated.ts +3 -2
  41. package/src/modes/components/welcome.ts +10 -2
  42. package/src/modes/interactive-mode.ts +29 -9
  43. package/src/prompts/system/system-prompt.md +1 -0
  44. package/src/runtime-mcp/json-rpc.ts +7 -4
  45. package/src/runtime-mcp/redaction.ts +30 -0
  46. package/src/sdk/broker/session-index.ts +11 -1
  47. package/src/sdk/bus/chat-daemon-control.ts +5 -3
  48. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  49. package/src/sdk/bus/telegram-daemon.ts +18 -2
  50. package/src/session/agent-session.ts +86 -0
  51. package/src/session/internal/managed-session-scope.ts +206 -111
  52. package/src/session/internal/managed-session-storage.ts +17 -4
  53. package/src/session/session-manager.ts +96 -33
  54. package/src/session/session-storage.ts +23 -0
  55. package/src/skill-state/active-state.ts +10 -0
  56. package/src/task/executor.ts +13 -5
  57. package/src/tools/ask.ts +1 -0
@@ -547,6 +547,13 @@ export class SessionMigrationPolicyError extends Error {
547
547
  }
548
548
  }
549
549
 
550
+ export class SessionArtifactCapacityError extends Error {
551
+ constructor(message: string) {
552
+ super(message);
553
+ this.name = "SessionArtifactCapacityError";
554
+ }
555
+ }
556
+
550
557
  export type ResumeTailInspection = ResumeTailResumable | ResumeTailTerminal | ResumeTailError;
551
558
  export interface StrictSessionOpenSuccess {
552
559
  kind: "opened";
@@ -555,7 +562,8 @@ export interface StrictSessionOpenSuccess {
555
562
 
556
563
  export interface StrictSessionOpenFailure {
557
564
  kind: "error";
558
- reason: ResumeTailError["reason"] | "identity-mismatch" | "migration-required";
565
+ reason: ResumeTailError["reason"] | "identity-mismatch" | "migration-required" | "artifact_capacity_exceeded";
566
+ message?: string;
559
567
  }
560
568
 
561
569
  /** Exact transcript bytes and authority captured from one descriptor-bound read. */
@@ -2110,41 +2118,90 @@ export async function recoverOrphanedBackups(sessionDir: string, storage: Sessio
2110
2118
  }
2111
2119
 
2112
2120
  /**
2113
- * Reads all session files from the directory and returns them sorted by mtime (newest first).
2114
- * Uses low-level file I/O to efficiently read only the first 4KB of each file
2115
- * to extract the JSON header and first user message without loading entire session logs into memory.
2121
+ * Returns session metadata sorted by mtime (newest first).
2122
+ *
2123
+ * Directory entries are cheap to stat, but opening every transcript before applying a
2124
+ * small caller limit makes welcome-screen startup scale with the entire session history.
2125
+ * Rank paths first, then read bounded batches until the requested number of valid
2126
+ * sessions has been found. Invalid or future-version files do not prevent older valid
2127
+ * sessions from filling the result.
2116
2128
  */
2117
- async function getSortedSessions(sessionDir: string, storage: SessionStorage): Promise<RecentSessionInfo[]> {
2129
+ async function getSortedSessions(
2130
+ sessionDir: string,
2131
+ storage: SessionStorage,
2132
+ limit?: number,
2133
+ ): Promise<RecentSessionInfo[]> {
2118
2134
  await recoverOrphanedBackups(sessionDir, storage);
2119
2135
  try {
2120
- const files: string[] = storage.listFilesSync(sessionDir, "*.jsonl");
2136
+ let candidates: Array<{ path: string; mtime: number }> | undefined;
2137
+ if (storage.listFilesByMtime) {
2138
+ try {
2139
+ candidates = (await storage.listFilesByMtime(sessionDir, "*.jsonl")).map(candidate => ({
2140
+ path: candidate.path,
2141
+ mtime: candidate.mtimeMs,
2142
+ }));
2143
+ } catch (error) {
2144
+ logger.warn("Native session mtime listing failed; using JavaScript fallback", {
2145
+ sessionDir,
2146
+ error: toError(error).message,
2147
+ });
2148
+ }
2149
+ } else {
2150
+ logger.debug("Native session mtime listing unavailable; using JavaScript fallback", { sessionDir });
2151
+ }
2152
+ candidates ??= storage.listFilesSync(sessionDir, "*.jsonl").flatMap(path => {
2153
+ try {
2154
+ return [{ path, mtime: storage.statSync(path).mtimeMs }];
2155
+ } catch {
2156
+ return [];
2157
+ }
2158
+ });
2159
+ candidates.sort((left, right) => {
2160
+ if (left.mtime !== right.mtime) return right.mtime - left.mtime;
2161
+ return left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
2162
+ });
2163
+
2121
2164
  const sessions: RecentSessionInfo[] = [];
2122
- await Promise.all(
2123
- files.map(async (path: string) => {
2124
- try {
2125
- const buffer = Buffer.allocUnsafe(SESSION_LIST_PREFIX_BYTES);
2126
- const content = await readSessionListPrefix(path, storage, buffer);
2127
- const entries = parseJsonlLenient<Record<string, unknown>>(content);
2128
- if (entries.length === 0) return;
2129
- const header = entries[0] as Record<string, unknown>;
2130
- if (
2131
- header.type !== "session" ||
2132
- typeof header.id !== "string" ||
2133
- !isSupportedSessionVersion(header.version)
2134
- )
2135
- return;
2136
- if (typeof header.version === "number" && header.version >= 4) {
2137
- for (const patch of await readSessionListTrailingPatches(path, storage, buffer)) {
2138
- applySessionListHeaderPatch(header as unknown as SessionListHeader, patch);
2165
+ const batchSize = Math.max(32, limit ?? 0);
2166
+
2167
+ for (
2168
+ let offset = 0;
2169
+ offset < candidates.length && (limit === undefined || sessions.length < limit);
2170
+ offset += batchSize
2171
+ ) {
2172
+ const batch = candidates.slice(offset, offset + batchSize);
2173
+ const parsed = await Promise.all(
2174
+ batch.map(async candidate => {
2175
+ try {
2176
+ const buffer = Buffer.allocUnsafe(SESSION_LIST_PREFIX_BYTES);
2177
+ const content = await readSessionListPrefix(candidate.path, storage, buffer);
2178
+ const entries = parseJsonlLenient<Record<string, unknown>>(content);
2179
+ if (entries.length === 0) return undefined;
2180
+ const header = entries[0] as Record<string, unknown>;
2181
+ if (
2182
+ header.type !== "session" ||
2183
+ typeof header.id !== "string" ||
2184
+ !isSupportedSessionVersion(header.version)
2185
+ )
2186
+ return undefined;
2187
+ if (typeof header.version === "number" && header.version >= 4) {
2188
+ for (const patch of await readSessionListTrailingPatches(candidate.path, storage, buffer)) {
2189
+ applySessionListHeaderPatch(header as unknown as SessionListHeader, patch);
2190
+ }
2139
2191
  }
2192
+ const firstPrompt = header.title ? undefined : extractFirstUserPrompt(entries);
2193
+ return new RecentSessionInfo(candidate.path, candidate.mtime, header, firstPrompt);
2194
+ } catch {
2195
+ return undefined;
2140
2196
  }
2141
- const mtime = storage.statSync(path).mtimeMs;
2142
- const firstPrompt = header.title ? undefined : extractFirstUserPrompt(entries);
2143
- sessions.push(new RecentSessionInfo(path, mtime, header, firstPrompt));
2144
- } catch {}
2145
- }),
2146
- );
2147
- return sessions.sort((a, b) => b.mtime - a.mtime);
2197
+ }),
2198
+ );
2199
+ for (const session of parsed) {
2200
+ if (session) sessions.push(session);
2201
+ if (limit !== undefined && sessions.length >= limit) break;
2202
+ }
2203
+ }
2204
+ return sessions;
2148
2205
  } catch {
2149
2206
  return [];
2150
2207
  }
@@ -2155,7 +2212,7 @@ export async function findMostRecentSession(
2155
2212
  sessionDir: string,
2156
2213
  storage: SessionStorage = new FileSessionStorage(),
2157
2214
  ): Promise<string | null> {
2158
- const sessions = await getSortedSessions(sessionDir, storage);
2215
+ const sessions = await getSortedSessions(sessionDir, storage, 1);
2159
2216
  return sessions[0]?.path || null;
2160
2217
  }
2161
2218
 
@@ -3427,8 +3484,7 @@ export async function getRecentSessions(
3427
3484
  limit = DEFAULT_WELCOME_RECENT_SESSION_LIMIT,
3428
3485
  storage: SessionStorage = new FileSessionStorage(),
3429
3486
  ): Promise<RecentSessionInfo[]> {
3430
- const sessions = await getSortedSessions(sessionDir, storage);
3431
- return sessions.slice(0, limit);
3487
+ return getSortedSessions(sessionDir, storage, limit);
3432
3488
  }
3433
3489
 
3434
3490
  /**
@@ -7299,6 +7355,7 @@ export class SessionManager {
7299
7355
  if (opened.kind === "error") {
7300
7356
  managedDestinationStore.assertBound();
7301
7357
  if (opened.code === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
7358
+ if (opened.code === "artifact_capacity_exceeded") throw new SessionArtifactCapacityError(opened.message);
7302
7359
  throw new Error(`Could not open managed session: ${opened.message}`);
7303
7360
  }
7304
7361
  assertManagedDestinationBound();
@@ -7809,6 +7866,8 @@ export class SessionManager {
7809
7866
  } catch (error) {
7810
7867
  if (error instanceof SessionMigrationPolicyError)
7811
7868
  return { kind: "error", reason: "legacy_migration_disabled" };
7869
+ if (error instanceof SessionArtifactCapacityError)
7870
+ return { kind: "error", reason: "artifact_capacity_exceeded", message: error.message };
7812
7871
  if (
7813
7872
  error instanceof Error &&
7814
7873
  (error.message.includes("source_changed") || error.message.includes("changed before migration"))
@@ -7867,6 +7926,10 @@ export class SessionManager {
7867
7926
  );
7868
7927
  if (opened.kind === "error") {
7869
7928
  if (opened.reason === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
7929
+ if (opened.reason === "artifact_capacity_exceeded")
7930
+ throw new SessionArtifactCapacityError(
7931
+ opened.message ?? "Session artifacts exceed the migration capacity.",
7932
+ );
7870
7933
  return undefined;
7871
7934
  }
7872
7935
  return opened.manager;
@@ -167,6 +167,8 @@ export interface SessionStorage {
167
167
  readSnapshotSync?(path: string): SessionStorageSnapshot;
168
168
  statSync(path: string): SessionStorageStat;
169
169
  listFilesSync(dir: string, pattern: string): string[];
170
+ /** List matching files with mtimes without issuing one JavaScript stat call per path. */
171
+ listFilesByMtime?(dir: string, pattern: string): Promise<Array<{ path: string; mtimeMs: number }>>;
170
172
  /**
171
173
  * Strict directory scan that never suppresses scan/root errors. Used by strict
172
174
  * authorization inventory; the forgiving {@link listFilesSync} stays display-only.
@@ -705,6 +707,21 @@ export class FileSessionStorage implements SessionStorage {
705
707
  return [];
706
708
  }
707
709
  }
710
+ async listFilesByMtime(dir: string, pattern: string): Promise<Array<{ path: string; mtimeMs: number }>> {
711
+ const result = await native.glob({
712
+ path: dir,
713
+ pattern,
714
+ fileType: native.FileType.File,
715
+ recursive: false,
716
+ hidden: false,
717
+ gitignore: false,
718
+ sortByMtime: true,
719
+ });
720
+ return result.matches.map(match => ({
721
+ path: path.join(dir, match.path),
722
+ mtimeMs: match.mtime ?? 0,
723
+ }));
724
+ }
708
725
 
709
726
  listFilesStrictSync(dir: string, pattern: string): string[] {
710
727
  // Strict: never suppress scan/root errors. Authorization inventory depends on
@@ -1470,6 +1487,12 @@ export class MemorySessionStorage implements SessionStorage {
1470
1487
  }
1471
1488
  return files;
1472
1489
  }
1490
+ listFilesByMtime(dir: string, pattern: string): Promise<Array<{ path: string; mtimeMs: number }>> {
1491
+ const files = this.listFilesSync(dir, pattern)
1492
+ .map(path => ({ path, mtimeMs: this.statSync(path).mtimeMs }))
1493
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
1494
+ return Promise.resolve(files);
1495
+ }
1473
1496
  listFilesStrictSync(dir: string, pattern: string): string[] {
1474
1497
  // In-memory scan never suppresses; identical to the display scan.
1475
1498
  return this.listFilesSync(dir, pattern);
@@ -71,6 +71,8 @@ export interface SkillActiveEntry {
71
71
  handoff_at?: string;
72
72
  active_subskills?: ActiveSubskillEntry[];
73
73
  source_state_revision?: number;
74
+ /** Durable ordering token from the source workflow mode-state, not this cache file's revision. */
75
+ committed_mode_state_revision?: number;
74
76
  }
75
77
 
76
78
  export interface SkillActiveState {
@@ -114,6 +116,8 @@ export interface SyncSkillActiveStateOptions {
114
116
  handoff_at?: string;
115
117
  active_subskills?: ActiveSubskillEntry[];
116
118
  sourceRevision?: number;
119
+ /** Durable ordering token from the source workflow mode-state, not this cache file's revision. */
120
+ committedModeRevision?: number;
117
121
  }
118
122
 
119
123
  const HUD_TEXT_LIMIT = 80;
@@ -785,6 +789,9 @@ async function persistActiveEntry(
785
789
  await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
786
790
  cwd,
787
791
  audit: activeStateWriterAudit("write-active-entry", sessionScope),
792
+ ...(typeof entry.committed_mode_state_revision === "number"
793
+ ? { orderingRevision: entry.committed_mode_state_revision }
794
+ : {}),
788
795
  });
789
796
  }
790
797
  }
@@ -863,6 +870,9 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
863
870
  ? { active_subskills: preservedActiveSubskills }
864
871
  : {}),
865
872
  ...(typeof options.sourceRevision === "number" ? { source_state_revision: options.sourceRevision } : {}),
873
+ ...(typeof options.committedModeRevision === "number"
874
+ ? { committed_mode_state_revision: options.committedModeRevision }
875
+ : {}),
866
876
  };
867
877
  const sessionScope = { sessionId: options.sessionId };
868
878
  await removeSupersededPlanningPipelineEntries(options.cwd, sessionScope, entry);
@@ -486,11 +486,12 @@ function buildSchemaViolationOutcome(
486
486
  missing.length > 0
487
487
  ? `schema_violation: missing required fields: ${missing.join(", ")}`
488
488
  : `schema_violation: ${failure.message}`;
489
+ const dataPreview = previewOffendingData(data);
489
490
  const payload = {
490
491
  error: "schema_violation",
491
492
  message: failure.message,
492
493
  missingRequired: missing,
493
- data: previewOffendingData(data),
494
+ data,
494
495
  };
495
496
  let rawOutput: string;
496
497
  try {
@@ -498,7 +499,7 @@ function buildSchemaViolationOutcome(
498
499
  } catch {
499
500
  rawOutput = `{"error":"schema_violation","message":${JSON.stringify(headline)}}`;
500
501
  }
501
- return { rawOutput, stderr: headline, exitCode: 1 };
502
+ return { rawOutput, stderr: `${headline}. Offending data preview: ${dataPreview}`, exitCode: 1 };
502
503
  }
503
504
 
504
505
  function buildPlaceholderYieldOutcome(
@@ -541,9 +542,16 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
541
542
  const completeData = normalizeCompleteData(submitData, reportFindings);
542
543
  const { validator, error: schemaError } = buildOutputValidator(outputSchema);
543
544
  if (schemaError) {
544
- rawOutput = `{"error":"schema_violation","message":"invalid output schema: ${schemaError.replace(/"/g, '\\"')}"}`;
545
- stderr = `schema_violation: invalid output schema: ${schemaError}`;
546
- exitCode = 1;
545
+ const outcome = buildSchemaViolationOutcome(
546
+ {
547
+ message: `invalid output schema: ${schemaError}`,
548
+ missingRequired: [],
549
+ },
550
+ completeData,
551
+ );
552
+ rawOutput = outcome.rawOutput;
553
+ stderr = outcome.stderr;
554
+ exitCode = outcome.exitCode;
547
555
  } else {
548
556
  const placeholderPath = findPlaceholderYieldPath(completeData);
549
557
  if (placeholderPath) {
package/src/tools/ask.ts CHANGED
@@ -461,6 +461,7 @@ function recoverRoundZeroIntentContract(
461
461
  const deepInterview = question.deepInterview;
462
462
  const hasIntentContract = Object.hasOwn(deepInterview, "intent_contract");
463
463
  const hasIntentReview = Object.hasOwn(deepInterview, "intent_review");
464
+ if (hasIntentContract && hasIntentReview && stage !== "topology") return { outcome: "reject" };
464
465
  if (hasIntentContract !== hasIntentReview && askSchema.safeParse(normalizedArguments).success)
465
466
  return { outcome: "passthrough" };
466
467