@gajae-code/coding-agent 0.12.11 → 0.12.13

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 (140) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/types/cli/marketplace-hint.d.ts +28 -0
  3. package/dist/types/cli/notify-cli.d.ts +42 -1
  4. package/dist/types/cli/update-cli.d.ts +9 -3
  5. package/dist/types/cli.d.ts +10 -0
  6. package/dist/types/commands/notify.d.ts +6 -0
  7. package/dist/types/config/file-lock-gc.d.ts +13 -1
  8. package/dist/types/config/file-lock.d.ts +3 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/models-config-schema.d.ts +99 -0
  11. package/dist/types/coordinator/contract.d.ts +1 -1
  12. package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
  13. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  14. package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
  15. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
  16. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  17. package/dist/types/gjc-runtime/deep-interview-state.d.ts +14 -0
  18. package/dist/types/gjc-runtime/gc-runtime.d.ts +11 -0
  19. package/dist/types/main.d.ts +6 -2
  20. package/dist/types/modes/acp/acp-agent.d.ts +24 -1
  21. package/dist/types/modes/components/session-selector.d.ts +1 -1
  22. package/dist/types/modes/utils/host-status.d.ts +23 -0
  23. package/dist/types/runtime-mcp/types.d.ts +2 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
  25. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
  26. package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  28. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
  29. package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
  30. package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
  31. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
  32. package/dist/types/sdk/bus/html-format.d.ts +10 -0
  33. package/dist/types/sdk/bus/notification-service.d.ts +10 -0
  34. package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
  35. package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
  36. package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
  37. package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
  38. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +28 -7
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -17
  41. package/dist/types/sdk/bus/telegram-reference.d.ts +20 -1
  42. package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
  43. package/dist/types/sdk/host/host.d.ts +52 -0
  44. package/dist/types/sdk/lifecycle-session.d.ts +6 -0
  45. package/dist/types/sdk/session-activation.d.ts +41 -0
  46. package/dist/types/sdk/session.d.ts +18 -0
  47. package/dist/types/sdk/startup-capability.d.ts +8 -1
  48. package/dist/types/session/agent-session.d.ts +7 -2
  49. package/dist/types/session/artifacts.d.ts +2 -2
  50. package/dist/types/session/session-manager.d.ts +40 -6
  51. package/dist/types/tools/index.d.ts +15 -0
  52. package/dist/types/utils/npm-registry.d.ts +71 -0
  53. package/examples/extensions/README.md +38 -4
  54. package/examples/extensions/ooo-bridge.ts +15 -0
  55. package/package.json +8 -8
  56. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  57. package/src/async/job-manager.ts +9 -2
  58. package/src/cli/fast-help.ts +1 -0
  59. package/src/cli/marketplace-hint.ts +39 -0
  60. package/src/cli/notify-cli.ts +226 -2
  61. package/src/cli/plugin-cli.ts +10 -0
  62. package/src/cli/update-cli.ts +90 -23
  63. package/src/cli.ts +23 -0
  64. package/src/commands/notify.ts +27 -3
  65. package/src/commands/sdk.ts +1 -0
  66. package/src/config/file-lock-gc.ts +65 -26
  67. package/src/config/file-lock.ts +61 -16
  68. package/src/config/model-profiles.ts +46 -30
  69. package/src/config/model-registry.ts +17 -5
  70. package/src/config/models-config-schema.ts +9 -4
  71. package/src/coordinator/contract.ts +1 -0
  72. package/src/coordinator-mcp/question-state.ts +2 -0
  73. package/src/coordinator-mcp/server.ts +358 -7
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  75. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  76. package/src/extensibility/extensions/index.ts +1 -0
  77. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
  78. package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
  79. package/src/extensibility/extensions/runner.ts +15 -2
  80. package/src/extensibility/extensions/types.ts +2 -0
  81. package/src/gjc-runtime/deep-interview-recorder.ts +9 -5
  82. package/src/gjc-runtime/deep-interview-state.ts +27 -3
  83. package/src/gjc-runtime/gc-render.ts +8 -1
  84. package/src/gjc-runtime/gc-runtime.ts +15 -1
  85. package/src/gjc-runtime/launch-tmux.ts +53 -1
  86. package/src/internal-urls/docs-index.generated.ts +14 -13
  87. package/src/main.ts +214 -70
  88. package/src/modes/DESIGN.md +8 -5
  89. package/src/modes/acp/acp-agent.ts +123 -7
  90. package/src/modes/components/footer.ts +10 -18
  91. package/src/modes/components/model-selector.ts +52 -5
  92. package/src/modes/components/session-selector.ts +26 -3
  93. package/src/modes/components/status-line/gh.ts +23 -2
  94. package/src/modes/controllers/event-controller.ts +3 -0
  95. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  96. package/src/modes/controllers/input-controller.ts +11 -0
  97. package/src/modes/controllers/selector-controller.ts +8 -2
  98. package/src/modes/interactive-mode.ts +7 -1
  99. package/src/modes/print-mode.ts +10 -0
  100. package/src/modes/utils/host-status.ts +38 -0
  101. package/src/prompts/system/project-prompt.md +19 -0
  102. package/src/runtime-mcp/types.ts +2 -0
  103. package/src/sdk/broker/lifecycle.ts +60 -3
  104. package/src/sdk/bus/chat-daemon-cli.ts +42 -0
  105. package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
  106. package/src/sdk/bus/chat-daemon-control.ts +16 -3
  107. package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
  108. package/src/sdk/bus/control-plane-frames.ts +51 -0
  109. package/src/sdk/bus/conversation-store.ts +29 -1
  110. package/src/sdk/bus/existing-thread-readiness.ts +155 -0
  111. package/src/sdk/bus/html-format.ts +19 -3
  112. package/src/sdk/bus/index.ts +91 -12
  113. package/src/sdk/bus/notification-service.ts +28 -1
  114. package/src/sdk/bus/rich-render.ts +3 -2
  115. package/src/sdk/bus/slack-daemon.ts +99 -4
  116. package/src/sdk/bus/slack-live-provider.ts +32 -2
  117. package/src/sdk/bus/slack-provider.ts +11 -0
  118. package/src/sdk/bus/slack-thread-binding.ts +641 -0
  119. package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
  120. package/src/sdk/bus/telegram-daemon-contract.ts +28 -7
  121. package/src/sdk/bus/telegram-daemon.ts +2007 -509
  122. package/src/sdk/bus/telegram-reference.ts +114 -15
  123. package/src/sdk/bus/topic-registry.ts +674 -181
  124. package/src/sdk/host/host.ts +180 -1
  125. package/src/sdk/lifecycle-session.ts +8 -2
  126. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  127. package/src/sdk/session-activation.ts +127 -0
  128. package/src/sdk/session.ts +144 -29
  129. package/src/sdk/startup-capability.ts +10 -1
  130. package/src/session/agent-session.ts +220 -13
  131. package/src/session/artifacts.ts +56 -13
  132. package/src/session/internal/managed-session-storage.ts +228 -7
  133. package/src/session/session-manager.ts +225 -60
  134. package/src/setup/provider-presets.json +3 -3
  135. package/src/task/index.ts +92 -44
  136. package/src/thinking-metadata.ts +2 -1
  137. package/src/tools/ask.ts +10 -0
  138. package/src/tools/index.ts +15 -0
  139. package/src/tools/todo-write.ts +4 -4
  140. package/src/utils/npm-registry.ts +660 -0
@@ -76,6 +76,7 @@ import {
76
76
  assertManagedDirectoryRoot,
77
77
  captureManagedFileNoFollow,
78
78
  fsyncManagedArtifactTree,
79
+ MANAGED_ARTIFACT_MAX_FILE_BYTES,
79
80
  type ManagedDirectoryRoot,
80
81
  type ManagedFileSnapshot,
81
82
  ManagedSessionDescendantStore,
@@ -915,12 +916,25 @@ export interface ResumeTailTerminal {
915
916
 
916
917
  export interface ResumeTailError {
917
918
  kind: "error";
918
- reason: "missing" | "malformed" | "unstable" | "read-failed" | "legacy_migration_disabled";
919
+ reason: "missing" | "malformed" | "unstable" | "read-failed" | "legacy_migration_disabled" | "oversized";
920
+ size?: number;
919
921
  }
920
922
 
921
923
  export type SessionDirectoryMigrationPolicy = ManagedMigrationPolicy;
922
924
  export type SessionAppendPersistenceFailurePhase = "current_append" | "prior_failure";
923
925
 
926
+ /**
927
+ * Safety bound for resuming a single transcript file. Matches the managed-storage
928
+ * per-file artifact limit (`MANAGED_ARTIFACT_MAX_FILE_BYTES`): transcripts at or above
929
+ * this size are not loaded into memory wholesale because the decode + per-line JSON
930
+ * parse + identity hash would OOM or stall the process (#3851). The bound is a
931
+ * load-time guard only; it does not raise the on-disk managed-storage limit.
932
+ */
933
+ export const RESUME_TRANSCRIPT_MAX_BYTES = MANAGED_ARTIFACT_MAX_FILE_BYTES;
934
+
935
+ export const SESSION_OVERSIZED_RECOVERY_MESSAGE =
936
+ "The selected session transcript is too large to resume safely. Use `gjc export <session-file>` to export its content into a new session, or remove/archive it after confirming its content is no longer needed.";
937
+
924
938
  export class SessionAppendPersistenceError extends Error {
925
939
  readonly phase: SessionAppendPersistenceFailurePhase;
926
940
  readonly entryId: string;
@@ -964,6 +978,17 @@ export class SessionArtifactCapacityError extends Error {
964
978
  }
965
979
  }
966
980
 
981
+ export class SessionTranscriptOversizedError extends Error {
982
+ readonly code = "oversized";
983
+ readonly size: number;
984
+
985
+ constructor(size: number) {
986
+ super(SESSION_OVERSIZED_RECOVERY_MESSAGE);
987
+ this.name = "SessionTranscriptOversizedError";
988
+ this.size = size;
989
+ }
990
+ }
991
+
967
992
  export type ResumeTailInspection = ResumeTailResumable | ResumeTailTerminal | ResumeTailError;
968
993
  export interface StrictSessionOpenSuccess {
969
994
  kind: "opened";
@@ -979,6 +1004,7 @@ export interface StrictSessionOpenFailure {
979
1004
  | "artifact_capacity_exceeded"
980
1005
  | "migration_busy";
981
1006
  message?: string;
1007
+ size?: number;
982
1008
  }
983
1009
 
984
1010
  /** Exact transcript bytes and authority captured from one descriptor-bound read. */
@@ -2346,6 +2372,12 @@ function inspectResumeSessionFile(
2346
2372
  if (!before.isFile || !storage.readSnapshotSync) {
2347
2373
  return { kind: "error", reason: "read-failed" };
2348
2374
  }
2375
+ // Fail closed on oversized transcripts before the full read/decode/parse
2376
+ // path that would otherwise OOM or stall the process (#3851). The stat is
2377
+ // already in hand; this bound matches the managed-storage per-file limit.
2378
+ if (before.size > RESUME_TRANSCRIPT_MAX_BYTES) {
2379
+ return { kind: "error", reason: "oversized", size: before.size };
2380
+ }
2349
2381
 
2350
2382
  let bytes: Uint8Array;
2351
2383
  let snapshot: SessionStorageStat;
@@ -4378,12 +4410,6 @@ function extractTextFromContent(content: Message["content"]): string {
4378
4410
 
4379
4411
  const SESSION_LIST_PREFIX_BYTES = 4096;
4380
4412
  const SESSION_LIST_TRAILING_PATCH_BYTES = 4096;
4381
- // Hard cap on how many trailing bytes the listers may scan for header patches.
4382
- // moveTo folds cwd patches into line 1 via a full atomic rewrite, so only a
4383
- // recent title patch can live unfolded near EOF; anything buried deeper is
4384
- // folded on the next natural full rewrite. Keeping this bounded preserves the
4385
- // O(SESSION_LIST_PREFIX_BYTES) listing cost that SESSION_LIST_PREFIX_BYTES exists for.
4386
- const SESSION_LIST_TRAILING_PATCH_SCAN_CAP = SESSION_LIST_TRAILING_PATCH_BYTES * 4;
4387
4413
  const SESSION_NAME_MAX_CHARS = 1_000;
4388
4414
  const SESSION_LIST_PARALLEL_THRESHOLD = 64;
4389
4415
  const SESSION_LIST_MAX_WORKERS = 16;
@@ -4419,12 +4445,17 @@ async function readSessionListTrailingPatches(
4419
4445
  if (size <= SESSION_LIST_PREFIX_BYTES) return [];
4420
4446
  const latest: HeaderPatchRecord["patch"] = {};
4421
4447
  let position = size;
4422
- const scanFloor = Math.max(0, size - SESSION_LIST_TRAILING_PATCH_SCAN_CAP);
4448
+ // Reverse-scan from EOF in fixed chunks. Stop once both cwd and title
4449
+ // resolve so recent patches stay cheap. Continue past the historical 16 KiB
4450
+ // window when a field is still missing so a buried but canonically valid
4451
+ // header_patch remains listable without a full sequential JSONL parse (#3633).
4452
+ // Chunks that cannot contain a header_patch marker skip JSON parsing.
4423
4453
  let trailingFragment = Buffer.alloc(0);
4424
4454
  const chunkSize = Math.min(buffer.byteLength, SESSION_LIST_TRAILING_PATCH_BYTES);
4455
+ const headerPatchMarker = Buffer.from("header_patch");
4425
4456
  const handle = await fs.promises.open(file, "r");
4426
4457
  try {
4427
- while (position > scanFloor && (latest.cwd === undefined || latest.title === undefined)) {
4458
+ while (position > 0 && (latest.cwd === undefined || latest.title === undefined)) {
4428
4459
  const start = Math.max(0, position - chunkSize);
4429
4460
  const length = position - start;
4430
4461
  const { bytesRead } = await handle.read(buffer, 0, length, start);
@@ -4440,18 +4471,26 @@ async function readSessionListTrailingPatches(
4440
4471
  trailingFragment = combined.subarray(0, firstNewline);
4441
4472
  complete = combined.subarray(firstNewline + 1);
4442
4473
  }
4443
- const lines = complete.toString("utf8").split("\n");
4444
- for (let index = lines.length - 1; index >= 0; index--) {
4445
- const line = lines[index];
4446
- if (!line) continue;
4447
- try {
4448
- const record = JSON.parse(line) as SessionPatchRecord;
4449
- if (!isHeaderPatchRecord(record)) continue;
4450
- if (latest.cwd === undefined && typeof record.patch.cwd === "string") latest.cwd = record.patch.cwd;
4451
- if (latest.title === undefined && typeof record.patch.title === "string")
4452
- latest.title = record.patch.title;
4453
- } catch {
4454
- // Ignore malformed or partial records exactly as the canonical loader does.
4474
+ // Marker may straddle the discarded partial first line; still parse when
4475
+ // the raw chunk or carried fragment could hold a header_patch record.
4476
+ const mayContainPatch =
4477
+ complete.includes(headerPatchMarker) ||
4478
+ buffer.subarray(0, bytesRead).includes(headerPatchMarker) ||
4479
+ trailingFragment.includes(headerPatchMarker);
4480
+ if (mayContainPatch) {
4481
+ const lines = complete.toString("utf8").split("\n");
4482
+ for (let index = lines.length - 1; index >= 0; index--) {
4483
+ const line = lines[index];
4484
+ if (!line?.includes("header_patch")) continue;
4485
+ try {
4486
+ const record = JSON.parse(line) as SessionPatchRecord;
4487
+ if (!isHeaderPatchRecord(record)) continue;
4488
+ if (latest.cwd === undefined && typeof record.patch.cwd === "string") latest.cwd = record.patch.cwd;
4489
+ if (latest.title === undefined && typeof record.patch.title === "string")
4490
+ latest.title = record.patch.title;
4491
+ } catch {
4492
+ // Ignore malformed or partial records exactly as the canonical loader does.
4493
+ }
4455
4494
  }
4456
4495
  }
4457
4496
  position = start;
@@ -4765,6 +4804,7 @@ interface SessionManagerStateSnapshot {
4765
4804
  needsFullRewriteOnNextPersist: boolean;
4766
4805
  fileEntries: FileEntry[];
4767
4806
  materializedFileEntries: FileEntry[];
4807
+ adoptedArtifactManager: ArtifactManager | null;
4768
4808
  }
4769
4809
 
4770
4810
  /** Benchmark-derived cap for strong materialized session snapshots. */
@@ -4775,6 +4815,7 @@ export const SessionManagerTestHooks: {
4775
4815
  materializedCacheMaxBytesOverride?: number;
4776
4816
  beforeResidentTransitionIndexBuild?: () => void;
4777
4817
  afterForkSnapshot?: () => void | Promise<void>;
4818
+ beforeEphemeralArtifactManagerInstall?: (dir: string) => void | Promise<void>;
4778
4819
  } = {};
4779
4820
 
4780
4821
  function materializedCacheMaxBytes(): number {
@@ -4841,10 +4882,13 @@ export class SessionManager {
4841
4882
  // Subagents adopt the parent's manager so artifact IDs are unique across the
4842
4883
  // whole agent tree and all files land in the parent's artifacts dir.
4843
4884
  #adoptedArtifactManager: ArtifactManager | null = null;
4844
- // In-memory artifact fallback for non-persistent sessions (persist=false).
4845
- // Keyed by sequential numeric ID string; mirrors the file-based ArtifactManager ID scheme.
4846
- #inMemoryArtifacts: Map<string, string> | null = null;
4847
- #inMemoryArtifactCounter = 0;
4885
+ // Filesystem-backed artifact fallback for non-persistent sessions (persist=false).
4886
+ // The directory is created lazily on first save so artifact content is read back
4887
+ // from disk on demand instead of being retained in memory for the session lifetime.
4888
+ #ephemeralArtifactManager: ArtifactManager | null = null;
4889
+ #ephemeralArtifactDir: string | null = null;
4890
+ #ephemeralArtifactInit: Promise<ArtifactManager | null> | null = null;
4891
+ #ephemeralArtifactCleanups = new Set<Promise<void>>();
4848
4892
  readonly #blobStore: BlobStore;
4849
4893
  #residentTextBlobStore: BlobStore = new MemoryBlobStore();
4850
4894
  #residentImageBlobStore: BlobStore;
@@ -5360,6 +5404,7 @@ export class SessionManager {
5360
5404
  // Rollback snapshots must own resident data before another session reset disposes
5361
5405
  // the ephemeral store backing the resident sentinels above.
5362
5406
  materializedFileEntries,
5407
+ adoptedArtifactManager: this.#adoptedArtifactManager,
5363
5408
  };
5364
5409
  }
5365
5410
 
@@ -5397,7 +5442,7 @@ export class SessionManager {
5397
5442
  this.#persistErrorReported = false;
5398
5443
  this.#artifactManager = null;
5399
5444
  this.#artifactManagerSessionFile = null;
5400
- this.#adoptedArtifactManager = null;
5445
+ this.#adoptedArtifactManager = snapshot.adoptedArtifactManager;
5401
5446
  this.#commitResidentTextStoreTransition(prepared);
5402
5447
  if (this.#sessionFile) writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
5403
5448
  }
@@ -5438,8 +5483,7 @@ export class SessionManager {
5438
5483
  this.#flushed = false;
5439
5484
  this.#needsFullRewriteOnNextPersist = false;
5440
5485
  this.#ensuredOnDisk = false;
5441
- this.#inMemoryArtifacts = null;
5442
- this.#inMemoryArtifactCounter = 0;
5486
+
5443
5487
  this.#artifactManager = null;
5444
5488
  this.#artifactManagerSessionFile = null;
5445
5489
  this.#adoptedArtifactManager = null;
@@ -5471,6 +5515,7 @@ export class SessionManager {
5471
5515
  const prepared = this.#prepareFreshSessionTransition(fresh, "memory-fallback");
5472
5516
  this.#applyFreshSessionMetadata(fresh);
5473
5517
  this.#commitResidentTextStoreTransition(prepared);
5518
+ this.#retireEphemeralArtifacts();
5474
5519
  writeTerminalBreadcrumb(this.cwd, resolvedSessionFile);
5475
5520
  await this.#rewriteFile();
5476
5521
  this.#flushed = true;
@@ -5544,7 +5589,7 @@ export class SessionManager {
5544
5589
  }
5545
5590
 
5546
5591
  /** Switch to a different session file (used for resume and branching). */
5547
- async setSessionFile(sessionFile: string): Promise<void> {
5592
+ async setSessionFile(sessionFile: string, options?: { deferEphemeralArtifactRetirement?: boolean }): Promise<void> {
5548
5593
  this.#assertRecoveryHydrationWritable();
5549
5594
  const resolvedSessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
5550
5595
  const strictAdoption = this.#pendingStrictAdoption;
@@ -5641,6 +5686,7 @@ export class SessionManager {
5641
5686
  prepared.dispose();
5642
5687
  throw error;
5643
5688
  }
5689
+ if (!options?.deferEphemeralArtifactRetirement) this.#retireEphemeralArtifacts();
5644
5690
  managedTransition?.settle();
5645
5691
  this.#pendingStrictAdoption = undefined;
5646
5692
  this.#flushed = true;
@@ -5668,8 +5714,6 @@ export class SessionManager {
5668
5714
  flushed: this.#flushed,
5669
5715
  needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
5670
5716
  ensuredOnDisk: this.#ensuredOnDisk,
5671
- inMemoryArtifacts: this.#inMemoryArtifacts,
5672
- inMemoryArtifactCounter: this.#inMemoryArtifactCounter,
5673
5717
  artifactManager: this.#artifactManager,
5674
5718
  artifactManagerSessionFile: this.#artifactManagerSessionFile,
5675
5719
  adoptedArtifactManager: this.#adoptedArtifactManager,
@@ -5679,6 +5723,7 @@ export class SessionManager {
5679
5723
  managedTransition?.adopt();
5680
5724
  writeTerminalBreadcrumb(this.cwd, resolvedSessionFile);
5681
5725
  this.#commitResidentTextStoreTransition(prepared);
5726
+ if (!options?.deferEphemeralArtifactRetirement) this.#retireEphemeralArtifacts();
5682
5727
  } catch (error) {
5683
5728
  managedTransition?.rollback();
5684
5729
  this.#lifecycleIdAdopted = previous.lifecycleIdAdopted;
@@ -5692,8 +5737,6 @@ export class SessionManager {
5692
5737
  this.#flushed = previous.flushed;
5693
5738
  this.#needsFullRewriteOnNextPersist = previous.needsFullRewriteOnNextPersist;
5694
5739
  this.#ensuredOnDisk = previous.ensuredOnDisk;
5695
- this.#inMemoryArtifacts = previous.inMemoryArtifacts;
5696
- this.#inMemoryArtifactCounter = previous.inMemoryArtifactCounter;
5697
5740
  this.#artifactManager = previous.artifactManager;
5698
5741
  this.#artifactManagerSessionFile = previous.artifactManagerSessionFile;
5699
5742
  this.#adoptedArtifactManager = previous.adoptedArtifactManager;
@@ -6139,12 +6182,12 @@ export class SessionManager {
6139
6182
  this.#flushed = stage.flushed;
6140
6183
  this.#needsFullRewriteOnNextPersist = false;
6141
6184
  this.#ensuredOnDisk = stage.flushed;
6142
- this.#inMemoryArtifacts = null;
6143
- this.#inMemoryArtifactCounter = 0;
6185
+
6144
6186
  this.#artifactManager = null;
6145
6187
  this.#artifactManagerSessionFile = null;
6146
6188
  this.#adoptedArtifactManager = null;
6147
6189
  this.#commitResidentTextStoreTransition(transition);
6190
+ this.#retireEphemeralArtifacts();
6148
6191
  stage.committed = true;
6149
6192
  this.#preparedNewSessions.delete(stage);
6150
6193
  }
@@ -6955,6 +6998,7 @@ export class SessionManager {
6955
6998
  const prepared = this.#prepareFreshSessionTransition(fresh, "memory-fallback");
6956
6999
  this.#applyFreshSessionMetadata(fresh);
6957
7000
  this.#commitResidentTextStoreTransition(prepared);
7001
+ this.#retireEphemeralArtifacts();
6958
7002
  if (writeBreadcrumb && fresh.sessionFile) writeTerminalBreadcrumb(this.cwd, fresh.sessionFile);
6959
7003
  return fresh.sessionFile;
6960
7004
  }
@@ -7677,6 +7721,8 @@ export class SessionManager {
7677
7721
  this.#flushed = true;
7678
7722
  }
7679
7723
  });
7724
+ this.#retireEphemeralArtifacts();
7725
+ await this.#drainEphemeralArtifactCleanups();
7680
7726
  this.#releaseResidentTextStore();
7681
7727
  this.#releaseOwnedManagedAuthority();
7682
7728
  if (this.#persistError) throw this.#persistError;
@@ -7736,6 +7782,12 @@ export class SessionManager {
7736
7782
  // close leaves the session live for a genuine retry.
7737
7783
  if (!this.#persistWriter) {
7738
7784
  this.#releaseResidentTextStore();
7785
+ this.#retireEphemeralArtifacts();
7786
+ try {
7787
+ await this.#drainEphemeralArtifactCleanups();
7788
+ } catch (error) {
7789
+ outcome = { kind: "close_unknown", error: toError(error) };
7790
+ }
7739
7791
  }
7740
7792
  return outcome;
7741
7793
  }
@@ -8024,14 +8076,40 @@ export class SessionManager {
8024
8076
  this.#adoptedArtifactManager = manager;
8025
8077
  }
8026
8078
 
8079
+ /** Release only the matching externally adopted manager. */
8080
+ releaseArtifactManager(manager: ArtifactManager): void {
8081
+ if (this.#adoptedArtifactManager === manager) this.#adoptedArtifactManager = null;
8082
+ }
8083
+
8084
+ /** Temporarily release adopted authority while an outer transition validates its successor. */
8085
+ stageAdoptedArtifactManagerForTransition(): void {
8086
+ this.#adoptedArtifactManager = null;
8087
+ }
8088
+
8089
+ /** Prove manager authority by exact object identity, never by pathname shape. */
8090
+ isArtifactManagerAuthorized(manager: ArtifactManager): boolean {
8091
+ return (
8092
+ manager === this.#adoptedArtifactManager ||
8093
+ manager === this.#artifactManager ||
8094
+ manager === this.#ephemeralArtifactManager
8095
+ );
8096
+ }
8097
+
8027
8098
  /**
8028
8099
  * Returns the ArtifactManager this session writes through. Lazily creates
8029
8100
  * one bound to the current session file unless an external manager was
8030
- * adopted via `adoptArtifactManager`. Returns null only for non-persistent
8031
- * sessions with no adopted manager.
8101
+ * adopted via `adoptArtifactManager`. Falls back to the lazily created
8102
+ * ephemeral filesystem store once a non-persistent session has saved an
8103
+ * artifact, so `artifact://` stays resolvable. Returns null only when no
8104
+ * store has been established yet.
8032
8105
  */
8033
8106
  getArtifactManager(): ArtifactManager | null {
8034
- return this.#getOrCreateArtifactManager();
8107
+ return this.#getOrCreateArtifactManager() ?? this.#ephemeralArtifactManager;
8108
+ }
8109
+
8110
+ /** Linearizably establish this session's persistent or ephemeral artifact manager. */
8111
+ async ensureArtifactManager(): Promise<ArtifactManager | null> {
8112
+ return this.#getOrCreateArtifactManager() ?? (await this.#ensureEphemeralArtifactManager());
8035
8113
  }
8036
8114
 
8037
8115
  /**
@@ -8074,28 +8152,89 @@ export class SessionManager {
8074
8152
 
8075
8153
  /**
8076
8154
  * Save artifact content under the current session and return artifact ID.
8077
- * Returns an artifact ID for all sessions (file-backed for persistent, in-memory fallback otherwise).
8155
+ * Persistent sessions write into the session artifact directory; non-persistent
8156
+ * sessions write into a lazily created temporary directory so the content is
8157
+ * read back from the filesystem instead of being retained in memory.
8078
8158
  */
8079
8159
  async saveArtifact(content: string, toolType: string): Promise<string | undefined> {
8080
- const manager = this.#getOrCreateArtifactManager();
8081
- if (manager) return manager.save(content, toolType);
8082
- // Non-persistent session: store in memory so spill truncation can proceed.
8083
- if (!this.#inMemoryArtifacts) this.#inMemoryArtifacts = new Map();
8084
- const id = String(this.#inMemoryArtifactCounter++);
8085
- this.#inMemoryArtifacts.set(id, content);
8086
- return id;
8160
+ const manager = this.#getOrCreateArtifactManager() ?? (await this.#ensureEphemeralArtifactManager());
8161
+ return manager ? manager.save(content, toolType) : undefined;
8087
8162
  }
8088
8163
 
8089
8164
  /**
8090
8165
  * Resolve an artifact ID to an on-disk path for the current session.
8091
- * Returns null when missing or when the session is not persisted.
8166
+ * Returns null when the artifact is missing.
8092
8167
  */
8093
8168
  async getArtifactPath(id: string): Promise<string | null> {
8094
- const manager = this.#getOrCreateArtifactManager();
8169
+ const manager = this.getArtifactManager();
8095
8170
  if (!manager) return null;
8096
8171
  return manager.getPath(id);
8097
8172
  }
8098
8173
 
8174
+ /**
8175
+ * Create the non-persistent session's temporary artifact directory on first use.
8176
+ * Returns null when the directory cannot be created; callers then report no artifact.
8177
+ */
8178
+ async #ensureEphemeralArtifactManager(): Promise<ArtifactManager | null> {
8179
+ if (!this.#ephemeralArtifactInit) {
8180
+ let init: Promise<ArtifactManager | null>;
8181
+ init = fs.promises
8182
+ .mkdtemp(path.join(os.tmpdir(), "gjc-session-artifacts-"))
8183
+ .then(async dir => {
8184
+ try {
8185
+ await SessionManagerTestHooks.beforeEphemeralArtifactManagerInstall?.(dir);
8186
+ } catch (error) {
8187
+ await fs.promises.rm(dir, { recursive: true, force: true });
8188
+ throw error;
8189
+ }
8190
+ const manager = new ArtifactManager(dir);
8191
+ if (this.#ephemeralArtifactInit !== init) {
8192
+ await fs.promises.rm(dir, { recursive: true, force: true });
8193
+ return null;
8194
+ }
8195
+ this.#ephemeralArtifactDir = dir;
8196
+ this.#ephemeralArtifactManager = manager;
8197
+ return manager;
8198
+ })
8199
+ .catch(() => null);
8200
+ this.#ephemeralArtifactInit = init;
8201
+ }
8202
+ const init = this.#ephemeralArtifactInit;
8203
+ const manager = await init;
8204
+ if (!manager && this.#ephemeralArtifactInit === init) this.#ephemeralArtifactInit = null;
8205
+ return manager;
8206
+ }
8207
+
8208
+ /** Unbind the active ephemeral root only after a successor transition commits. */
8209
+ #retireEphemeralArtifacts(): void {
8210
+ const dir = this.#ephemeralArtifactDir;
8211
+ const init = this.#ephemeralArtifactInit;
8212
+ if (!dir && !init) return;
8213
+ this.#ephemeralArtifactManager = null;
8214
+ this.#ephemeralArtifactDir = null;
8215
+ this.#ephemeralArtifactInit = null;
8216
+ const cleanup = (async () => {
8217
+ const initialized = await init;
8218
+ const cleanupDir = dir ?? initialized?.dir;
8219
+ if (cleanupDir) await fs.promises.rm(cleanupDir, { recursive: true, force: true });
8220
+ })();
8221
+ void cleanup.catch(() => {});
8222
+ this.#ephemeralArtifactCleanups.add(cleanup);
8223
+ }
8224
+
8225
+ /** Retire predecessor ephemeral artifacts after an outer logical transition commits. */
8226
+ retireEphemeralArtifactsAfterTransition(): void {
8227
+ this.#retireEphemeralArtifacts();
8228
+ }
8229
+
8230
+ async #drainEphemeralArtifactCleanups(): Promise<void> {
8231
+ while (this.#ephemeralArtifactCleanups.size > 0) {
8232
+ const pending = Array.from(this.#ephemeralArtifactCleanups);
8233
+ this.#ephemeralArtifactCleanups.clear();
8234
+ await Promise.all(pending);
8235
+ }
8236
+ }
8237
+
8099
8238
  /**
8100
8239
  * Path to the unsent-input draft sidecar for an explicit session. It lives
8101
8240
  * inside the artifacts directory so `dropSession` removes it with its owner.
@@ -8229,20 +8368,26 @@ export class SessionManager {
8229
8368
  await this.#persistPatch({ type: "header_patch", patch });
8230
8369
  }
8231
8370
 
8232
- #appendManagedRecordSync(record: FileEntry | SessionPatchRecord): void {
8371
+ #appendManagedRecordsSync(records: readonly (FileEntry | SessionPatchRecord)[]): void {
8233
8372
  if (!this.#sessionFile) throw new Error("Managed transcript path is unavailable");
8234
8373
  const store = this.#managedTranscriptStore();
8235
8374
  const relativePath = path.basename(this.#sessionFile);
8236
- store.appendSync(relativePath, Buffer.from(`${JSON.stringify(record)}\n`, "utf8"));
8375
+ const bytes = Buffer.from(`${records.map(record => JSON.stringify(record)).join("\n")}\n`, "utf8");
8376
+ store.appendSync(relativePath, bytes);
8237
8377
  }
8238
8378
 
8239
8379
  async #persistPatch(record: SessionPatchRecord): Promise<void> {
8240
- if (!this.persist || !this.#sessionFile || !this.storage.existsSync(this.#sessionFile)) return;
8380
+ await this.#persistPatches([record]);
8381
+ }
8382
+
8383
+ async #persistPatches(records: readonly SessionPatchRecord[]): Promise<void> {
8384
+ if (records.length === 0 || !this.persist || !this.#sessionFile || !this.storage.existsSync(this.#sessionFile))
8385
+ return;
8241
8386
  await this.#queuePersistTask(async () => {
8242
8387
  const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
8243
8388
  if (this.#needsFullRewriteOnNextPersist || !this.#flushed || (header?.version ?? 1) < CURRENT_SESSION_VERSION)
8244
8389
  return this.#rewriteFileContents();
8245
- const persistedRecord =
8390
+ const persistedRecords = records.map(record =>
8246
8391
  record.type === "entry_patch"
8247
8392
  ? (prepareEntryForPersistenceSync(
8248
8393
  materializeResidentEntryForPersistenceSync(
@@ -8252,14 +8397,17 @@ export class SessionManager {
8252
8397
  ),
8253
8398
  this.#blobStore,
8254
8399
  ) as unknown as SessionPatchRecord)
8255
- : record;
8400
+ : record,
8401
+ );
8256
8402
  if (this.destination.kind === "managed") {
8257
- this.#appendManagedRecordSync(persistedRecord);
8403
+ this.#appendManagedRecordsSync(persistedRecords);
8258
8404
  return;
8259
8405
  }
8260
8406
  const writer = this.#ensurePersistWriter();
8261
8407
  if (!writer) return this.#rewriteFileContents();
8262
- await writer.write(persistedRecord);
8408
+ for (const persistedRecord of persistedRecords) {
8409
+ await writer.write(persistedRecord);
8410
+ }
8263
8411
  });
8264
8412
  }
8265
8413
  _persist(entry: SessionEntry): void {
@@ -8308,7 +8456,7 @@ export class SessionManager {
8308
8456
  new Map(),
8309
8457
  );
8310
8458
  const persistedEntry = prepareEntryForPersistenceSync(materializedEntry, this.#blobStore);
8311
- this.#appendManagedRecordSync(persistedEntry);
8459
+ this.#appendManagedRecordsSync([persistedEntry]);
8312
8460
  return;
8313
8461
  }
8314
8462
  const writer = this.#ensurePersistWriter();
@@ -9228,9 +9376,7 @@ export class SessionManager {
9228
9376
 
9229
9377
  async #sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist(): Promise<boolean> {
9230
9378
  const patches = this.#sanitizeLoadedOpenAIResponsesReplayMetadata();
9231
- for (const patch of patches) {
9232
- await this.#persistPatch(patch);
9233
- }
9379
+ await this.#persistPatches(patches);
9234
9380
  return patches.length > 0;
9235
9381
  }
9236
9382
 
@@ -9803,6 +9949,7 @@ export class SessionManager {
9803
9949
  );
9804
9950
  manager.#applyFreshSessionMetadata(fresh);
9805
9951
  manager.#commitResidentTextStoreTransition(transition);
9952
+ manager.#retireEphemeralArtifacts();
9806
9953
  if (fresh.sessionFile) writeTerminalBreadcrumb(manager.cwd, fresh.sessionFile);
9807
9954
  manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
9808
9955
  await manager.#rewriteFile();
@@ -9830,6 +9977,16 @@ export class SessionManager {
9830
9977
  // owner-only and reparse guards accept.
9831
9978
  if (storage instanceof FileSessionStorage) filePath = canonicalizeTrustedPath(filePath);
9832
9979
  if (destination.kind === "explicit" || !(storage instanceof FileSessionStorage)) {
9980
+ // Fail closed on oversized transcripts before the full read in the explicit path too (#3851).
9981
+ // A missing file falls through to loadEntriesFromFile, which returns [] and creates
9982
+ // a fresh session — matching the pre-existing behavior.
9983
+ try {
9984
+ const preStat = storage.statSync(filePath);
9985
+ if (preStat.size > RESUME_TRANSCRIPT_MAX_BYTES) throw new SessionTranscriptOversizedError(preStat.size);
9986
+ } catch (error) {
9987
+ if (error instanceof SessionTranscriptOversizedError) throw error;
9988
+ if (!isEnoent(error)) throw error;
9989
+ }
9833
9990
  const entries = await loadEntriesFromFile(filePath, storage);
9834
9991
  const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
9835
9992
  const manager = new SessionManager(
@@ -9850,6 +10007,7 @@ export class SessionManager {
9850
10007
  await manager.#initSessionFile(filePath);
9851
10008
  return manager;
9852
10009
  }
10010
+ if (inspected.reason === "oversized") throw new SessionTranscriptOversizedError(inspected.size ?? 0);
9853
10011
  throw new Error(`Could not open session: ${inspected.reason}`);
9854
10012
  }
9855
10013
  const opened = await SessionManager.openExistingStrict(inspected.identity, destination, storage, migrationPolicy);
@@ -9859,6 +10017,7 @@ export class SessionManager {
9859
10017
  throw new SessionArtifactCapacityError(
9860
10018
  opened.message ?? "Session artifacts exceed the migration capacity.",
9861
10019
  );
10020
+ if (opened.reason === "oversized") throw new SessionTranscriptOversizedError(opened.size ?? 0);
9862
10021
  if (opened.reason === "migration_busy") throw new SessionMigrationBusyError();
9863
10022
  throw new Error(`Could not open session: ${opened.reason}`);
9864
10023
  }
@@ -9905,6 +10064,7 @@ export class SessionManager {
9905
10064
  const transition = manager.#prepareFreshSessionTransition(fresh, "memory-fallback");
9906
10065
  manager.#applyFreshSessionMetadata(fresh);
9907
10066
  manager.#commitResidentTextStoreTransition(transition);
10067
+ manager.#retireEphemeralArtifacts();
9908
10068
  writeTerminalBreadcrumb(manager.cwd, resolved);
9909
10069
  await manager.#rewriteFile();
9910
10070
  manager.#flushed = true;
@@ -10096,6 +10256,7 @@ export class SessionManager {
10096
10256
  );
10097
10257
  manager.#applyFreshSessionMetadata(fresh);
10098
10258
  manager.#commitResidentTextStoreTransition(transition);
10259
+ manager.#retireEphemeralArtifacts();
10099
10260
  manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
10100
10261
  const beforeWrite = inspectResumeSessionFile(snapshot.sourcePath, snapshot.storage);
10101
10262
  if ("kind" in beforeWrite) {
@@ -10595,7 +10756,10 @@ export class SessionManager {
10595
10756
  const dir = destination.directory;
10596
10757
  const openSelectedStrictly = async (selectedPath: string): Promise<SessionManager | undefined> => {
10597
10758
  const inspected = await SessionManager.inspectSessionTailReadOnly(selectedPath, storage);
10598
- if (inspected.kind === "error") return undefined;
10759
+ if (inspected.kind === "error") {
10760
+ if (inspected.reason === "oversized") throw new SessionTranscriptOversizedError(inspected.size ?? 0);
10761
+ return undefined;
10762
+ }
10599
10763
  const opened = await SessionManager.openExistingStrict(
10600
10764
  inspected.identity,
10601
10765
  destination,
@@ -10608,6 +10772,7 @@ export class SessionManager {
10608
10772
  throw new SessionArtifactCapacityError(
10609
10773
  opened.message ?? "Session artifacts exceed the migration capacity.",
10610
10774
  );
10775
+ if (opened.reason === "oversized") throw new SessionTranscriptOversizedError(opened.size ?? 0);
10611
10776
  if (opened.reason === "migration_busy") throw new SessionMigrationBusyError();
10612
10777
  return undefined;
10613
10778
  }
@@ -1,6 +1,6 @@
1
1
  [
2
- {"id":"minimax","aliases":["minimax-code"],"name":"MiniMax Coding Plan","description":"OpenAI-compatible MiniMax Coding Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"minimax-code","baseUrl":"https://api.minimax.io/v1","apiKeyEnv":"MINIMAX_CODE_API_KEY","models":["minimax-m3"],"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}},
3
- {"id":"minimax-cn","aliases":["minimax-code-cn","minimaxi"],"name":"MiniMax Coding Plan (China)","description":"OpenAI-compatible MiniMax China endpoint","compatibility":"openai","api":"openai-completions","providerId":"minimax-code-cn","baseUrl":"https://api.minimaxi.com/v1","apiKeyEnv":"MINIMAX_CODE_CN_API_KEY","models":["minimax-m3"],"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}},
2
+ {"id":"minimax","aliases":["minimax-code"],"name":"MiniMax Coding Plan","description":"OpenAI-compatible MiniMax Coding Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"minimax-code","baseUrl":"https://api.minimax.io/v1","apiKeyEnv":"MINIMAX_CODE_API_KEY","models":["MiniMax-M3"],"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}},
3
+ {"id":"minimax-cn","aliases":["minimax-code-cn","minimaxi"],"name":"MiniMax Coding Plan (China)","description":"OpenAI-compatible MiniMax China endpoint","compatibility":"openai","api":"openai-completions","providerId":"minimax-code-cn","baseUrl":"https://api.minimaxi.com/v1","apiKeyEnv":"MINIMAX_CODE_CN_API_KEY","models":["MiniMax-M3"],"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}},
4
4
  {"id":"glm","aliases":["zai","z-ai","bigmodel"],"name":"GLM / zAI","description":"OpenAI-compatible GLM endpoint from zAI/BigModel","compatibility":"openai","api":"openai-completions","providerId":"glm-proxy","baseUrl":"https://api.z.ai/api/paas/v4","apiKeyEnv":"ZAI_API_KEY","models":["glm-4.6"],"compat":{"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","reasoningContentField":"reasoning_content"}},
5
- {"id":"alibaba-token-plan","aliases":["alibaba","token-plan"],"name":"Alibaba Token Plan","description":"OpenAI-compatible Alibaba Token Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"alibaba-token-plan","baseUrl":"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1","apiKeyEnv":"ALIBABA_TOKEN_PLAN_API_KEY","models":["qwen3.8-max-preview","glm-5.2","deepseek-v4-pro"],"modelApi":{"qwen3.8-max-preview":"openai-responses","glm-5.2":"openai-completions","deepseek-v4-pro":"openai-completions"},"compat":{"supportsDeveloperRole":false}}
5
+ {"id":"alibaba-token-plan","aliases":["alibaba","token-plan"],"name":"Alibaba Token Plan","description":"OpenAI-compatible Alibaba Token Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"alibaba-token-plan","baseUrl":"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1","apiKeyEnv":"ALIBABA_TOKEN_PLAN_API_KEY","models":["qwen3.8-max-preview","qwen3.8-max","glm-5.2","deepseek-v4-pro"],"modelApi":{"qwen3.8-max-preview":"openai-responses","qwen3.8-max":"openai-responses","glm-5.2":"openai-completions","deepseek-v4-pro":"openai-completions"},"compat":{"supportsDeveloperRole":false}}
6
6
  ]