@gajae-code/coding-agent 0.13.2 → 0.13.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 (36) hide show
  1. package/CHANGELOG.md +33 -8
  2. package/dist/types/config/model-registry.d.ts +1 -0
  3. package/dist/types/config/settings-schema.d.ts +3 -3
  4. package/dist/types/main.d.ts +1 -0
  5. package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
  6. package/dist/types/modes/components/gajae-pet-widget.d.ts +5 -5
  7. package/dist/types/modes/components/pet-capability.d.ts +5 -5
  8. package/dist/types/modes/components/settings-selector.d.ts +5 -0
  9. package/dist/types/modes/theme/defaults/index.d.ts +118 -0
  10. package/dist/types/session/session-manager.d.ts +7 -0
  11. package/dist/types/tools/image-gen.d.ts +1 -0
  12. package/package.json +8 -8
  13. package/src/config/model-profile-activation.ts +3 -1
  14. package/src/config/model-profiles.ts +59 -2
  15. package/src/config/model-registry.ts +16 -1
  16. package/src/config/settings-schema.ts +1 -1
  17. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
  18. package/src/internal-urls/docs-index.generated.ts +3 -2
  19. package/src/main.ts +4 -0
  20. package/src/modes/components/chrome.ts +4 -3
  21. package/src/modes/components/dynamic-theme-text.ts +22 -0
  22. package/src/modes/components/gajae-pet-widget.ts +130 -42
  23. package/src/modes/components/model-selector.ts +1 -6
  24. package/src/modes/components/pet-capability.ts +10 -5
  25. package/src/modes/components/settings-selector.ts +20 -5
  26. package/src/modes/components/status-line/segments.ts +3 -3
  27. package/src/modes/controllers/selector-controller.ts +61 -5
  28. package/src/modes/theme/defaults/index.ts +2 -0
  29. package/src/modes/theme/defaults/ouroboros.json +118 -0
  30. package/src/session/agent-session.ts +39 -0
  31. package/src/session/internal/managed-session-storage.ts +26 -19
  32. package/src/session/session-manager.ts +28 -0
  33. package/src/slash-commands/builtin-registry.ts +19 -3
  34. package/src/tools/image-gen.ts +322 -126
  35. package/src/tools/todo-contract.ts +55 -3
  36. package/src/tools/todo-write.ts +4 -3
@@ -0,0 +1,118 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/can1357/gajae-code/main/packages/coding-agent/theme-schema.json",
3
+ "name": "ouroboros",
4
+ "vars": {
5
+ "cyan": "#58d5c3",
6
+ "blue": "#7092be",
7
+ "green": "#78c878",
8
+ "red": "#ff667a",
9
+ "yellow": "#f2c94c",
10
+ "gray": "#a9bbb2",
11
+ "dimGray": "#5b756d",
12
+ "darkGray": "#29443f",
13
+ "accent": "#aee80e",
14
+ "selectedBg": "#1b3b3b",
15
+ "userMsgBg": "#102b30",
16
+ "toolPendingBg": "#0d252b",
17
+ "toolSuccessBg": "#163326",
18
+ "toolErrorBg": "#351923",
19
+ "customMsgBg": "#0d2130",
20
+ "abyss": "#050b18",
21
+ "deepNavy": "#0b1630",
22
+ "mantle": "#0d2130",
23
+ "surface": "#102b30",
24
+ "surfaceBright": "#1b3b3b",
25
+ "ouroTeal": "#38bfa7",
26
+ "serpentGreen": "#78c878",
27
+ "petLime": "#aee80e",
28
+ "ritualGold": "#f2c94c",
29
+ "eyeGold": "#ffe786",
30
+ "coolScale": "#7092be",
31
+ "foam": "#e7f1e9",
32
+ "mutedScale": "#a9bbb2",
33
+ "dimScale": "#5b756d",
34
+ "dangerRed": "#ff667a",
35
+ "diffRemovalRed": "#df5b6b"
36
+ },
37
+ "colors": {
38
+ "accent": "petLime",
39
+ "border": "ouroTeal",
40
+ "borderAccent": "ritualGold",
41
+ "borderMuted": "dimScale",
42
+ "success": "serpentGreen",
43
+ "error": "dangerRed",
44
+ "warning": "ritualGold",
45
+ "muted": "mutedScale",
46
+ "dim": "dimScale",
47
+ "text": "foam",
48
+ "thinkingText": "mutedScale",
49
+ "selectedBg": "surfaceBright",
50
+ "userMessageBg": "surface",
51
+ "userMessageText": "foam",
52
+ "customMessageBg": "mantle",
53
+ "customMessageText": "foam",
54
+ "customMessageLabel": "petLime",
55
+ "toolPendingBg": "mantle",
56
+ "toolSuccessBg": "#163326",
57
+ "toolErrorBg": "#351923",
58
+ "toolTitle": "foam",
59
+ "toolOutput": "mutedScale",
60
+ "mdHeading": "petLime",
61
+ "mdLink": "ouroTeal",
62
+ "mdLinkUrl": "dimScale",
63
+ "mdCode": "eyeGold",
64
+ "mdCodeBlock": "serpentGreen",
65
+ "mdCodeBlockBorder": "ouroTeal",
66
+ "mdQuote": "mutedScale",
67
+ "mdQuoteBorder": "ouroTeal",
68
+ "mdHr": "dimScale",
69
+ "mdListBullet": "ritualGold",
70
+ "toolDiffAdded": "serpentGreen",
71
+ "toolDiffRemoved": "diffRemovalRed",
72
+ "toolDiffContext": "mutedScale",
73
+ "syntaxComment": "dimScale",
74
+ "syntaxKeyword": "petLime",
75
+ "syntaxFunction": "eyeGold",
76
+ "syntaxVariable": "foam",
77
+ "syntaxString": "serpentGreen",
78
+ "syntaxNumber": "coolScale",
79
+ "syntaxType": "ouroTeal",
80
+ "syntaxOperator": "ritualGold",
81
+ "syntaxPunctuation": "mutedScale",
82
+ "thinkingOff": "dimScale",
83
+ "thinkingMinimal": "mutedScale",
84
+ "thinkingLow": "coolScale",
85
+ "thinkingMedium": "ouroTeal",
86
+ "thinkingHigh": "petLime",
87
+ "thinkingXhigh": "eyeGold",
88
+ "bashMode": "petLime",
89
+ "statusLineBg": "deepNavy",
90
+ "statusLineSep": "dimScale",
91
+ "statusLineModel": "petLime",
92
+ "statusLinePath": "ouroTeal",
93
+ "statusLineGitClean": "serpentGreen",
94
+ "statusLineGitDirty": "ritualGold",
95
+ "statusLineContext": "coolScale",
96
+ "statusLineSpend": "eyeGold",
97
+ "statusLineStaged": "serpentGreen",
98
+ "statusLineDirty": "ritualGold",
99
+ "statusLineUntracked": "diffRemovalRed",
100
+ "statusLineOutput": "foam",
101
+ "statusLineCost": "eyeGold",
102
+ "statusLineSubagents": "ouroTeal",
103
+ "pythonMode": "ritualGold"
104
+ },
105
+ "export": {
106
+ "pageBg": "#050b18",
107
+ "cardBg": "#0b1630",
108
+ "infoBg": "#102b30"
109
+ },
110
+ "symbols": {
111
+ "preset": "unicode",
112
+ "overrides": {
113
+ "icon.pi": "🐍",
114
+ "status.running": "∞",
115
+ "md.bullet": "◇"
116
+ }
117
+ }
118
+ }
@@ -954,6 +954,13 @@ type RetryErrorClassification =
954
954
 
955
955
  const BARE_DEFAULT_WATCHDOG_ERROR =
956
956
  /^(?:[A-Za-z][A-Za-z0-9-]*(?: [A-Za-z][A-Za-z0-9-]*){0,3} )stream (?:timed out while waiting for the first event|stalled while waiting for the next event)$/;
957
+ /**
958
+ * Consecutive same-model retries allowed for `escaped_arguments_discarded`
959
+ * managed outcomes before the run terminates. Mirrors the agent loop's
960
+ * escaped-non-ASCII resample budget on dev; enforced session-side here because
961
+ * a managed retry re-enters the loop as a fresh run.
962
+ */
963
+ const MAX_ESCAPED_ARGUMENT_RETRIES = 2;
957
964
  const PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = "Provider stream timed out while waiting for the first event";
958
965
  const WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = `Error: ${PROVIDER_FIRST_EVENT_TIMEOUT_ERROR}`;
959
966
  const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream timed out while waiting for first event";
@@ -2030,6 +2037,7 @@ export class AgentSession {
2030
2037
  #retryPromise: Promise<void> | undefined = undefined;
2031
2038
  #retryResolve: (() => void) | undefined = undefined;
2032
2039
  #defaultFallbackController: FallbackChainController | undefined;
2040
+ #consecutiveEscapedArgumentRetries = 0;
2033
2041
  #overflowMaintenanceAttempts = 0;
2034
2042
  #defaultFallbackExhaustedLastTurn = false;
2035
2043
  #fallbackInvocationId = 0;
@@ -13690,6 +13698,7 @@ export class AgentSession {
13690
13698
  sessionMemory.metadataResidentBytes,
13691
13699
  tuiChatChildren: retainedMemory.tuiChatChildren ?? 0,
13692
13700
  tuiCachedRenderBytes: retainedMemory.tuiCachedRenderBytes ?? 0,
13701
+ transcriptFileBytes: this.sessionManager.getTranscriptFileBytes(),
13693
13702
  };
13694
13703
  }
13695
13704
 
@@ -15710,10 +15719,40 @@ export class AgentSession {
15710
15719
  }
15711
15720
 
15712
15721
  async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
15722
+ if (outcome.type !== "escaped_arguments_discarded") this.#consecutiveEscapedArgumentRetries = 0;
15713
15723
  if (outcome.type === "run_terminal") {
15714
15724
  this.#defaultFallbackChain().resetAttemptBudget();
15715
15725
  return { type: "terminal", terminal: { stopReason: outcome.reason } };
15716
15726
  }
15727
+ if (outcome.type === "escaped_arguments_discarded") {
15728
+ // An escaped-non-ASCII wire defect is a sampling accident, not provider
15729
+ // evidence: never charge the attempt, advance the chain, or suppress the
15730
+ // selector. The loop already removed the defective turn from history, so
15731
+ // this decision just re-issues the same request on the same model.
15732
+ //
15733
+ // The retry bound lives HERE, not in the loop: a managed retry re-enters
15734
+ // the loop as a fresh run, so no loop-local budget survives it. After
15735
+ // MAX_ESCAPED_ARGUMENT_RETRIES consecutive escaped-argument discards the
15736
+ // run terminates with an error instead of ping-ponging forever against a
15737
+ // model that deterministically emits escaped arguments. The counter
15738
+ // resets on any other outcome.
15739
+ this.#defaultFallbackChain().discardStartedAttempt();
15740
+ this.#consecutiveEscapedArgumentRetries++;
15741
+ if (this.#consecutiveEscapedArgumentRetries > MAX_ESCAPED_ARGUMENT_RETRIES) {
15742
+ this.#consecutiveEscapedArgumentRetries = 0;
15743
+ return {
15744
+ type: "terminal",
15745
+ terminal: { stopReason: "error", messages: [outcome.message] },
15746
+ };
15747
+ }
15748
+ return {
15749
+ type: "retry",
15750
+ continuation: async ownership => {
15751
+ if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
15752
+ await this.agent.continue(this.#managedFallbackPromptOptions());
15753
+ },
15754
+ };
15755
+ }
15717
15756
  if (outcome.type === "context_overflow_discarded") {
15718
15757
  // The provider invocation happened, but overflow is context maintenance rather
15719
15758
  // than a fallback-policy failure. Keep the logical run owner and do not charge,
@@ -1336,25 +1336,32 @@ export class ManagedSessionDescendantStore {
1336
1336
  fsyncDirectory(this.#baseDir);
1337
1337
  return;
1338
1338
  }
1339
- if (outcome.reason === "destination_exists") {
1340
- const existing = captureManagedFilePrefixNoFollow(destination, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
1341
- if (existing.identity.sha256 !== pending.identity.sha256)
1342
- throw new Error("managed_replace_cleanup_receipt_invalid");
1343
- const removed = nativeSessionStorage().exactUnlink(receiptPath, {
1344
- dev: pending.identity.dev,
1345
- ino: pending.identity.ino,
1346
- nlink: pending.identity.nlink,
1347
- parentDev: this.#subtreeRoot.dev,
1348
- parentIno: this.#subtreeRoot.ino,
1349
- size: BigInt(pending.identity.size),
1350
- mtimeNs: pending.identity.mtimeNs,
1351
- sha256: pending.identity.sha256,
1352
- quarantineName: `.gjc-receipt-pending-remove-${pending.identity.dev.toString(16)}-${pending.identity.ino.toString(16)}`,
1353
- });
1354
- if (!exactUnlinkCompleted(removed) && removed.code !== "not_found")
1355
- throw new Error(`managed_replace_receipt_cleanup_pending:${removed.code ?? "unknown"}`);
1356
- fsyncDirectory(this.#baseDir);
1357
- return;
1339
+ if (outcome.reason === "destination_exists" || outcome.reason === "invalid_request") {
1340
+ const captureIfPresent = (pathname: string): ManagedFileSnapshot | undefined => {
1341
+ try {
1342
+ return captureManagedFilePrefixNoFollow(pathname, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
1343
+ } catch (error) {
1344
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
1345
+ throw error;
1346
+ }
1347
+ };
1348
+ const currentPending = captureIfPresent(receiptPath);
1349
+ const existing = captureIfPresent(destination);
1350
+ if (!currentPending && existing) {
1351
+ if (
1352
+ !sameReplacementIdentity(existing.identity, pending.identity) ||
1353
+ existing.identity.sha256 !== pending.identity.sha256
1354
+ )
1355
+ throw new Error("managed_replace_cleanup_receipt_invalid");
1356
+ fsyncDirectory(this.#baseDir);
1357
+ return;
1358
+ }
1359
+ if (currentPending && !existing) throw publishFailure(outcome);
1360
+ // Two names cannot both authoritatively represent the one receipt identity
1361
+ // encoded in the canonical filename. A byte-identical copy has a different
1362
+ // filesystem identity and would make the canonical receipt fail its next
1363
+ // reconciliation, while a hard link violates the single-link capture fence.
1364
+ throw new Error("managed_replace_cleanup_receipt_invalid");
1358
1365
  }
1359
1366
  throw publishFailure(outcome);
1360
1367
  }
@@ -14774,6 +14774,20 @@ export class SessionManager {
14774
14774
  getSessionFile(): string | undefined {
14775
14775
  return this.#sessionFile;
14776
14776
  }
14777
+ /**
14778
+ * On-disk transcript file size in bytes. Returns 0 when the file is
14779
+ * unavailable, unreadable, or no session file is set. The managed-storage
14780
+ * path reads through the descriptor (no full-file scan); the plain-file
14781
+ * path uses statSync.
14782
+ */
14783
+ getTranscriptFileBytes(): number {
14784
+ if (!this.#sessionFile) return 0;
14785
+ try {
14786
+ return this.#statSync(this.#sessionFile).size;
14787
+ } catch {
14788
+ return 0;
14789
+ }
14790
+ }
14777
14791
 
14778
14792
  getSessionMemoryStats(): SessionMemoryStats {
14779
14793
  const runtime = this.#sidecarRuntime;
@@ -15783,6 +15797,20 @@ export class SessionManager {
15783
15797
  if (publishResumeBreadcrumb && persisted) writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
15784
15798
  if (persisted) this.#readOnlyResume = false;
15785
15799
  } catch (err) {
15800
+ // content_too_large on the managed append hot path means the append-only
15801
+ // transcript file has reached the per-file storage limit (64 MiB). The
15802
+ // on-disk file is append-only and grew past the limit even though the
15803
+ // in-memory entry list may be much smaller (compaction evicts old
15804
+ // content but the append-only file never shrinks until a full rewrite).
15805
+ // Fall back to a full rewrite (replaceSync) which writes only the live
15806
+ // in-memory entries, shrinking the file below the limit. The entry has
15807
+ // already been added to #fileEntries by #appendEntryWithinPersistenceFence.
15808
+ if (err instanceof Error && err.message === "content_too_large") {
15809
+ this.#rewriteFileSync();
15810
+ if (publishResumeBreadcrumb) writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
15811
+ this.#readOnlyResume = false;
15812
+ return;
15813
+ }
15786
15814
  this.#recordPersistError(err);
15787
15815
  throw this.#persistError ?? toError(err);
15788
15816
  }
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { ThinkingLevel } from "@gajae-code/agent-core";
4
4
  import { type Model, modelsAreEqual } from "@gajae-code/ai/core";
5
5
  import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
6
- import { PET_SKINS, type PetMode, Spacer, Text } from "@gajae-code/tui";
6
+ import { PET_SKIN_IDS, PET_SKINS, type PetMode, Spacer, Text } from "@gajae-code/tui";
7
7
  import { setProjectDir } from "@gajae-code/utils";
8
8
  import { jobElapsedMs } from "../async";
9
9
  import { activateModelProfile, materializeActiveModelProfileAssignments } from "../config/model-profile-activation";
@@ -12,6 +12,7 @@ import {
12
12
  GJC_MODEL_ASSIGNMENT_TARGET_IDS,
13
13
  GJC_MODEL_ASSIGNMENT_TARGETS,
14
14
  type GjcModelAssignmentTargetId,
15
+ requiresExplicitThinkingChoice,
15
16
  } from "../config/model-registry";
16
17
 
17
18
  import {
@@ -70,8 +71,11 @@ function canClearComposer(runtime: BuiltinSlashCommandRuntime): boolean {
70
71
 
71
72
  const PET_COMMAND_OPTIONS: ReadonlyArray<{ name: string; mode: PetMode; description: string }> = [
72
73
  { name: "off", mode: "off", description: "Hide the pet" },
73
- { name: "RedGajae", mode: "red", description: PET_SKINS.red.description },
74
- { name: "BlueGajae", mode: "blue", description: PET_SKINS.blue.description },
74
+ ...PET_SKIN_IDS.map(id => ({
75
+ name: PET_SKINS[id].label,
76
+ mode: id,
77
+ description: PET_SKINS[id].description,
78
+ })),
75
79
  ];
76
80
  const PET_COMMAND_HINT = `[${PET_COMMAND_OPTIONS.map(option => option.name).join("|")}]`;
77
81
  /**
@@ -870,6 +874,18 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
870
874
  return usage(modelSelectionUsage(runtime, resolution.failure.message), runtime);
871
875
  }
872
876
  const { selection } = resolution;
877
+ if (
878
+ selection.thinkingLevel === undefined &&
879
+ targetIds.some(targetId => requiresExplicitThinkingChoice(selection.model, targetId))
880
+ ) {
881
+ return usage(
882
+ modelSelectionUsage(
883
+ runtime,
884
+ `Model ${selection.model.provider}/${selection.model.id} requires an explicit effort suffix.`,
885
+ ),
886
+ runtime,
887
+ );
888
+ }
873
889
  try {
874
890
  const includesDefault = targetIds.includes("default");
875
891
  const includesRoleAgent = targetIds.some(role => role !== "default");