@gajae-code/coding-agent 0.13.0 → 0.13.2

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 (119) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/types/config/file-lock.d.ts +2 -0
  3. package/dist/types/config/keybindings.d.ts +2 -2
  4. package/dist/types/config/model-registry.d.ts +4 -0
  5. package/dist/types/config/models-config-schema.d.ts +6 -0
  6. package/dist/types/config/settings-schema.d.ts +17 -5
  7. package/dist/types/config/settings.d.ts +10 -1
  8. package/dist/types/edit/streaming.d.ts +5 -5
  9. package/dist/types/eval/py/kernel.d.ts +2 -2
  10. package/dist/types/eval/py/runtime.d.ts +5 -1
  11. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
  12. package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
  13. package/dist/types/modes/components/model-selector.d.ts +3 -2
  14. package/dist/types/runtime/process-lifecycle.d.ts +12 -31
  15. package/dist/types/sdk/broker/identity.d.ts +2 -1
  16. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
  17. package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
  18. package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
  19. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  20. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  21. package/dist/types/sdk/bus/index.d.ts +4 -0
  22. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
  23. package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -2
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
  26. package/dist/types/sdk/host/query/cursor.d.ts +4 -0
  27. package/dist/types/sdk/host/query/handlers.d.ts +6 -0
  28. package/dist/types/sdk/host/session-runtime.d.ts +7 -6
  29. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  30. package/dist/types/sdk/index.d.ts +1 -0
  31. package/dist/types/sdk/turn-result.d.ts +37 -0
  32. package/dist/types/session/agent-session.d.ts +8 -11
  33. package/dist/types/session/blob-store.d.ts +6 -3
  34. package/dist/types/session/internal/managed-session-storage.d.ts +13 -0
  35. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
  36. package/dist/types/task/executor.d.ts +4 -7
  37. package/dist/types/tools/index.d.ts +1 -1
  38. package/dist/types/tools/read.d.ts +1 -1
  39. package/dist/types/tools/search.d.ts +1 -1
  40. package/dist/types/utils/edit-mode.d.ts +69 -0
  41. package/package.json +8 -8
  42. package/scripts/generate-hotkeys-docs.ts +8 -6
  43. package/src/commands/sdk.ts +23 -2
  44. package/src/config/file-lock.ts +25 -5
  45. package/src/config/keybindings.ts +4 -2
  46. package/src/config/model-registry.ts +20 -0
  47. package/src/config/models-config-schema.ts +1 -0
  48. package/src/config/settings-schema.ts +29 -22
  49. package/src/config/settings.ts +42 -10
  50. package/src/coordinator-mcp/server.ts +87 -106
  51. package/src/edit/index.ts +5 -19
  52. package/src/edit/renderer.ts +3 -2
  53. package/src/edit/streaming.ts +28 -15
  54. package/src/eval/py/executor.ts +108 -10
  55. package/src/eval/py/kernel.ts +86 -12
  56. package/src/eval/py/runtime.ts +200 -16
  57. package/src/exec/bash-executor.ts +36 -37
  58. package/src/gjc-runtime/gc-runtime.ts +25 -3
  59. package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
  60. package/src/gjc-runtime/session-state-sidecar.ts +110 -4
  61. package/src/gjc-runtime/tmux-sessions.ts +34 -5
  62. package/src/internal-urls/docs-index.generated.ts +9 -9
  63. package/src/modes/acp/acp-agent.ts +10 -2
  64. package/src/modes/components/model-selector.ts +28 -8
  65. package/src/modes/controllers/plan-mode-controller.ts +3 -3
  66. package/src/modes/controllers/selector-controller.ts +4 -2
  67. package/src/modes/interactive-mode.ts +4 -0
  68. package/src/runtime/process-lifecycle.ts +126 -27
  69. package/src/runtime-mcp/transports/stdio.ts +11 -6
  70. package/src/sdk/acp/adapter.ts +18 -10
  71. package/src/sdk/broker/broker.ts +55 -4
  72. package/src/sdk/broker/ensure.ts +10 -7
  73. package/src/sdk/broker/identity.ts +10 -0
  74. package/src/sdk/broker/lifecycle-ledger.ts +56 -6
  75. package/src/sdk/broker/lifecycle.ts +53 -10
  76. package/src/sdk/broker/process-incarnation.ts +6 -1
  77. package/src/sdk/broker/session-index.ts +28 -11
  78. package/src/sdk/broker/startup-failure.ts +101 -0
  79. package/src/sdk/broker/transport.ts +1 -0
  80. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  81. package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
  82. package/src/sdk/bus/conversation-store.ts +171 -50
  83. package/src/sdk/bus/index.ts +101 -47
  84. package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
  85. package/src/sdk/bus/notification-service.ts +3 -3
  86. package/src/sdk/bus/reconciliation-store.ts +16 -0
  87. package/src/sdk/bus/telegram-daemon-contract.ts +13 -2
  88. package/src/sdk/bus/telegram-daemon.ts +151 -41
  89. package/src/sdk/bus/topic-registry.ts +38 -9
  90. package/src/sdk/cli/session-cli.ts +20 -7
  91. package/src/sdk/client/discovery.ts +23 -2
  92. package/src/sdk/host/query/cursor.ts +4 -0
  93. package/src/sdk/host/query/handlers.ts +99 -100
  94. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  95. package/src/sdk/host/session-runtime.test.ts +98 -1
  96. package/src/sdk/host/session-runtime.ts +57 -15
  97. package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
  98. package/src/sdk/host/websocket-transport.ts +54 -30
  99. package/src/sdk/index.ts +1 -0
  100. package/src/sdk/mcp/server.ts +19 -6
  101. package/src/sdk/protocol/operation-inventory.generated.json +1 -1
  102. package/src/sdk/protocol/operation-registry.ts +8 -12
  103. package/src/sdk/session.ts +5 -14
  104. package/src/sdk/turn-result.ts +53 -0
  105. package/src/session/agent-session.ts +112 -69
  106. package/src/session/blob-store.ts +91 -21
  107. package/src/session/internal/managed-session-storage.ts +136 -21
  108. package/src/session/session-manager.ts +408 -81
  109. package/src/session/session-storage.ts +68 -10
  110. package/src/slash-commands/builtin-registry.ts +43 -5
  111. package/src/slash-commands/helpers/fast-status-report.ts +24 -8
  112. package/src/task/executor.ts +25 -8
  113. package/src/task/index.ts +5 -4
  114. package/src/tools/bash.ts +11 -9
  115. package/src/tools/index.ts +1 -1
  116. package/src/tools/read.ts +12 -11
  117. package/src/tools/search.ts +7 -7
  118. package/src/utils/edit-mode.ts +180 -9
  119. package/src/web/search/providers/insane.ts +55 -1
@@ -2,8 +2,15 @@ import { $pickenv } from "@gajae-code/utils/env";
2
2
 
3
3
  export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch";
4
4
 
5
+ /** Setting value for `edit.mode`: an executable mode or `auto` (model-family routing). */
6
+ export type EditModeSetting = "auto" | EditMode;
7
+
8
+ /** Fallback executable mode used when automatic routing cannot identify the model. */
5
9
  export const DEFAULT_EDIT_MODE: EditMode = "hashline";
6
10
 
11
+ /** Default `edit.mode` setting value. */
12
+ export const DEFAULT_EDIT_MODE_SETTING: EditModeSetting = "auto";
13
+
7
14
  const EDIT_MODE_IDS = {
8
15
  apply_patch: "apply_patch",
9
16
  hashline: "hashline",
@@ -14,29 +21,193 @@ const EDIT_MODE_IDS = {
14
21
 
15
22
  export const EDIT_MODES = Object.keys(EDIT_MODE_IDS) as EditMode[];
16
23
 
24
+ export const EDIT_MODE_SETTINGS: EditModeSetting[] = ["auto", ...EDIT_MODES];
25
+
17
26
  export function normalizeEditMode(mode?: string | null): EditMode | undefined {
18
27
  if (!mode) return undefined;
19
- return EDIT_MODE_IDS[mode as keyof typeof EDIT_MODE_IDS];
28
+ return Object.hasOwn(EDIT_MODE_IDS, mode) ? EDIT_MODE_IDS[mode as keyof typeof EDIT_MODE_IDS] : undefined;
29
+ }
30
+
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ // Model-family detection (automatic routing)
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+
35
+ export type ModelEditFamily = "gpt" | "codex" | "claude" | "deepseek" | "qwen" | "minimax" | "glm" | "kimi" | "unknown";
36
+
37
+ /**
38
+ * Built-in family → edit mode mapping used by `edit.mode: auto`.
39
+ *
40
+ * GPT/Codex models are trained on the OpenAI patch grammar; Claude, DeepSeek,
41
+ * and Qwen perform best with exact search/replace; MiniMax, GLM, and
42
+ * Kimi/Moonshot benchmark best with hash-anchored edits. Unknown models fall
43
+ * back to hashline, the safest fail-closed default.
44
+ */
45
+ const FAMILY_EDIT_MODES: Record<Exclude<ModelEditFamily, "unknown">, EditMode> = {
46
+ gpt: "apply_patch",
47
+ codex: "apply_patch",
48
+ claude: "replace",
49
+ deepseek: "replace",
50
+ qwen: "replace",
51
+ minimax: "hashline",
52
+ glm: "hashline",
53
+ kimi: "hashline",
54
+ };
55
+
56
+ /**
57
+ * Detect the model family from a model ID, ignoring the provider prefix so
58
+ * equivalent models served through different providers route identically
59
+ * (`openai/gpt-5.4`, `openrouter/openai/gpt-5.4`, `company-gateway/gpt-5.4`).
60
+ *
61
+ * Matching only examines the final path segment. A direct family prefix or a
62
+ * known provider-qualified family name is accepted, while arbitrary token
63
+ * substrings return `unknown` instead of guessing.
64
+ */
65
+ export function detectModelEditFamily(modelId: string | undefined): ModelEditFamily {
66
+ if (!modelId) return "unknown";
67
+ const normalized = modelId.trim().toLowerCase();
68
+ const segments = normalized.split("/").filter(Boolean);
69
+ const model = segments.at(-1) ?? normalized;
70
+
71
+ if (/^codex(?:$|[-_.\d])|^(?:gpt|(?:duo-chat|openai)-gpt)-\d+(?:[-_.][\w]+)*[-_.]codex(?:$|[-_.\d])/.test(model))
72
+ return "codex";
73
+ if (
74
+ /^(?:gpt-\d+(?:$|[-_.])|gpt-oss(?:$|[-_.\d])|chatgpt(?:$|[-_.]\d))|(?:^|\.)openai\.(?:gpt|chatgpt)(?:$|[-_.\d])/.test(
75
+ model,
76
+ )
77
+ )
78
+ return "gpt";
79
+ if (/^claude(?:$|[-_.\d])|(?:^|\.)anthropic\.claude(?:$|[-_.\d])/.test(model)) return "claude";
80
+ if (/^deepseek(?:$|[-_.\d])|(?:^|\.)deepseek(?:$|[-_.\d])/.test(model)) return "deepseek";
81
+ if (/^qwen(?:$|[-_.\d])|(?:^|\.)qwen\.qwen(?:$|[-_.\d])/.test(model)) return "qwen";
82
+ if (/^minimax(?:$|[-_.\d])/.test(model)) return "minimax";
83
+ if (/^glm(?:$|[-_.\d])/.test(model)) return "glm";
84
+ if (/^(?:kimi|moonshot)(?:$|[-_.\d])/.test(model)) return "kimi";
85
+
86
+ return "unknown";
20
87
  }
21
88
 
89
+ /** Built-in mode for a detected family; `undefined` for `unknown`. */
90
+ export function builtinEditModeForFamily(family: ModelEditFamily): EditMode | undefined {
91
+ if (family === "unknown") return undefined;
92
+ return FAMILY_EDIT_MODES[family];
93
+ }
94
+
95
+ // ─────────────────────────────────────────────────────────────────────────────
96
+ // Resolution
97
+ // ─────────────────────────────────────────────────────────────────────────────
98
+
99
+ export type EditModeSource = "environment" | "model-override" | "setting" | "catalog" | "builtin-family" | "fallback";
100
+
101
+ export type ResolvedEditModeDetails = {
102
+ mode: EditMode;
103
+ source: EditModeSource;
104
+ modelId?: string;
105
+ family?: ModelEditFamily;
106
+ matchedRule?: string;
107
+ };
108
+
109
+ /** Raw `edit.modelVariants` match: pattern plus the configured (unvalidated) value. */
110
+ export type EditVariantMatch = {
111
+ pattern: string;
112
+ value: string;
113
+ };
114
+
22
115
  export interface EditModeSettingsLike {
23
116
  get(key: "edit.mode"): unknown;
117
+ /** Legacy accessor: first *valid* matching `edit.modelVariants` value. */
24
118
  getEditVariantForModel?(model: string | undefined): EditMode | null;
119
+ /**
120
+ * Discriminated accessor: first matching `edit.modelVariants` rule with its
121
+ * raw value. Lets the resolver fail closed on invalid matched values
122
+ * instead of silently falling through to another mode.
123
+ */
124
+ matchEditVariantForModel?(model: string | undefined): EditVariantMatch | null;
25
125
  }
26
126
 
27
127
  export interface EditModeSessionLike {
28
128
  settings: EditModeSettingsLike;
29
129
  getActiveModelString?: () => string | undefined;
130
+ /**
131
+ * Optional model-catalog edit recommendation; beats the built-in family
132
+ * mapping when present.
133
+ *
134
+ * Extension point: production sessions do not wire this yet — the model
135
+ * catalog does not expose an edit-protocol recommendation (`applyPatchToolType`
136
+ * is the OpenAI wire representation of an already-selected `apply_patch`
137
+ * mode, not a mode selector). Catalog metadata generation is deliberately
138
+ * deferred until the family-based path is proven for arbitrary custom-provider
139
+ * IDs; when it lands, sessions can expose this hook without a resolver change.
140
+ */
141
+ getCatalogEditMode?: () => EditMode | undefined;
30
142
  }
31
143
 
32
- export function resolveEditMode(session: EditModeSessionLike): EditMode {
33
- const activeModel = session.getActiveModelString?.();
34
- const modelVariant = session.settings.getEditVariantForModel?.(activeModel);
35
- if (modelVariant) return modelVariant;
144
+ /**
145
+ * Read the forced edit mode from `GJC_EDIT_VARIANT`/`PI_EDIT_VARIANT`.
146
+ * The environment force is the emergency kill switch and beats every other
147
+ * source. Invalid values fail fast; empty and `auto` mean "not forced".
148
+ */
149
+ export function resolveForcedEnvEditMode(): EditMode | undefined {
150
+ const raw = $pickenv("GJC_EDIT_VARIANT", "PI_EDIT_VARIANT");
151
+ if (!raw || raw === "auto") return undefined;
152
+ const mode = normalizeEditMode(raw);
153
+ if (!mode) {
154
+ throw new Error(`Invalid GJC_EDIT_VARIANT: ${raw}`);
155
+ }
156
+ return mode;
157
+ }
158
+
159
+ /**
160
+ * Resolve the active edit mode with provenance.
161
+ *
162
+ * Precedence:
163
+ * 1. `GJC_EDIT_VARIANT`/`PI_EDIT_VARIANT` environment force (invalid → throw).
164
+ * 2. Matching user `edit.modelVariants` rule (matched invalid → throw).
165
+ * 3. Explicit `edit.mode` setting when it is not `auto`.
166
+ * 4. Model-catalog edit recommendation, when the session exposes one.
167
+ * 5. Built-in model-family mapping.
168
+ * 6. `hashline` fallback.
169
+ */
170
+ export function resolveEditModeDetails(session: EditModeSessionLike): ResolvedEditModeDetails {
171
+ const modelId = session.getActiveModelString?.();
172
+
173
+ const envMode = resolveForcedEnvEditMode();
174
+ if (envMode) return { mode: envMode, source: "environment", modelId };
175
+
176
+ const settings = session.settings;
177
+ if (settings.matchEditVariantForModel) {
178
+ const match = settings.matchEditVariantForModel(modelId);
179
+ if (match) {
180
+ const mode = normalizeEditMode(match.value);
181
+ if (!mode) {
182
+ throw new Error(
183
+ `Invalid edit.modelVariants value "${match.value}" for pattern "${match.pattern}" (expected one of: ${EDIT_MODES.join(", ")})`,
184
+ );
185
+ }
186
+ return { mode, source: "model-override", modelId, matchedRule: match.pattern };
187
+ }
188
+ } else {
189
+ const legacyVariant = settings.getEditVariantForModel?.(modelId);
190
+ if (legacyVariant) return { mode: legacyVariant, source: "model-override", modelId };
191
+ }
36
192
 
37
- const envMode = normalizeEditMode($pickenv("GJC_EDIT_VARIANT", "PI_EDIT_VARIANT"));
38
- if (envMode) return envMode;
193
+ const rawSetting = settings.get("edit.mode");
194
+ const settingValue = typeof rawSetting === "string" ? rawSetting : undefined;
195
+ if (settingValue && settingValue !== "auto") {
196
+ const settingsMode = normalizeEditMode(settingValue);
197
+ if (settingsMode) return { mode: settingsMode, source: "setting", modelId };
198
+ }
39
199
 
40
- const settingsMode = normalizeEditMode(String(session.settings.get("edit.mode") ?? ""));
41
- return settingsMode ?? DEFAULT_EDIT_MODE;
200
+ const family = detectModelEditFamily(modelId);
201
+
202
+ const catalogMode = session.getCatalogEditMode?.();
203
+ if (catalogMode) return { mode: catalogMode, source: "catalog", modelId, family };
204
+
205
+ const builtinMode = builtinEditModeForFamily(family);
206
+ if (builtinMode) return { mode: builtinMode, source: "builtin-family", modelId, family };
207
+
208
+ return { mode: DEFAULT_EDIT_MODE, source: "fallback", modelId, family };
209
+ }
210
+
211
+ export function resolveEditMode(session: EditModeSessionLike): EditMode {
212
+ return resolveEditModeDetails(session).mode;
42
213
  }
@@ -25,6 +25,7 @@ const DEFAULT_NUM_RESULTS = 10;
25
25
  const MAX_NUM_RESULTS = 20;
26
26
  const PUBLIC_ROUTE_TIMEOUT_MS = 15_000;
27
27
  const DISCOVERY_LIMIT = 8;
28
+ const MAX_PUBLIC_ROUTE_RESPONSE_BYTES = 1024 * 1024;
28
29
 
29
30
  const USER_AGENT = "Gajae-Code insane-search safe-public-routes/1.0 (+https://github.com/Yeachan-Heo/gajae-code)";
30
31
 
@@ -146,6 +147,59 @@ function attempt(
146
147
  return { platform, route, ok, status, bytes: new TextEncoder().encode(body).byteLength, note };
147
148
  }
148
149
 
150
+ class ResponseTooLargeError extends Error {
151
+ override readonly name = "response_too_large";
152
+ }
153
+
154
+ async function cancelBody(body: ReadableStream<Uint8Array> | null): Promise<void> {
155
+ if (!body) return;
156
+ try {
157
+ await body.cancel();
158
+ } catch {
159
+ // Best-effort teardown; preserve the stable size-limit diagnostic.
160
+ }
161
+ }
162
+
163
+ async function readResponseText(response: Response): Promise<string> {
164
+ const contentLength = response.headers.get("content-length");
165
+ if (contentLength !== null) {
166
+ const declaredBytes = Number.parseInt(contentLength, 10);
167
+ if (Number.isFinite(declaredBytes) && declaredBytes > MAX_PUBLIC_ROUTE_RESPONSE_BYTES) {
168
+ await cancelBody(response.body);
169
+ throw new ResponseTooLargeError();
170
+ }
171
+ }
172
+
173
+ const reader = response.body?.getReader();
174
+ if (!reader) return "";
175
+
176
+ const decoder = new TextDecoder();
177
+ let totalBytes = 0;
178
+ let text = "";
179
+ try {
180
+ while (true) {
181
+ const { done, value } = await reader.read();
182
+ if (done) break;
183
+ if (!value || value.byteLength === 0) continue;
184
+
185
+ totalBytes += value.byteLength;
186
+ if (totalBytes > MAX_PUBLIC_ROUTE_RESPONSE_BYTES) {
187
+ try {
188
+ await reader.cancel();
189
+ } catch {
190
+ // Best-effort teardown; preserve the stable size-limit diagnostic.
191
+ }
192
+ throw new ResponseTooLargeError();
193
+ }
194
+ text += decoder.decode(value, { stream: true });
195
+ }
196
+ text += decoder.decode();
197
+ return text;
198
+ } finally {
199
+ reader.releaseLock();
200
+ }
201
+ }
202
+
149
203
  async function fetchText(
150
204
  url: string,
151
205
  signal?: AbortSignal,
@@ -158,7 +212,7 @@ async function fetchText(
158
212
  redirect: "follow",
159
213
  signal: withHardTimeout(signal, PUBLIC_ROUTE_TIMEOUT_MS),
160
214
  });
161
- const text = await response.text();
215
+ const text = await readResponseText(response);
162
216
  return { status: response.status, text, contentType: response.headers.get("content-type") ?? "" };
163
217
  }
164
218