@gajae-code/coding-agent 0.13.0 → 0.13.1
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.
- package/CHANGELOG.md +18 -0
- package/dist/types/config/file-lock.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +17 -5
- package/dist/types/config/settings.d.ts +10 -1
- package/dist/types/edit/streaming.d.ts +5 -5
- package/dist/types/eval/py/kernel.d.ts +2 -2
- package/dist/types/eval/py/runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
- package/dist/types/modes/components/model-selector.d.ts +3 -2
- package/dist/types/runtime/process-lifecycle.d.ts +12 -31
- package/dist/types/sdk/broker/identity.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
- package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
- package/dist/types/sdk/host/query/cursor.d.ts +4 -0
- package/dist/types/sdk/host/query/handlers.d.ts +6 -0
- package/dist/types/sdk/host/session-runtime.d.ts +7 -6
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/turn-result.d.ts +37 -0
- package/dist/types/session/agent-session.d.ts +8 -11
- package/dist/types/session/blob-store.d.ts +6 -3
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
- package/dist/types/task/executor.d.ts +4 -7
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/utils/edit-mode.d.ts +69 -0
- package/package.json +8 -8
- package/scripts/generate-hotkeys-docs.ts +8 -6
- package/src/commands/sdk.ts +23 -2
- package/src/config/file-lock.ts +25 -5
- package/src/config/keybindings.ts +4 -2
- package/src/config/model-registry.ts +20 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +29 -22
- package/src/config/settings.ts +42 -10
- package/src/coordinator-mcp/server.ts +84 -106
- package/src/edit/index.ts +5 -19
- package/src/edit/renderer.ts +3 -2
- package/src/edit/streaming.ts +28 -15
- package/src/eval/py/executor.ts +108 -10
- package/src/eval/py/kernel.ts +86 -12
- package/src/eval/py/runtime.ts +200 -16
- package/src/exec/bash-executor.ts +36 -37
- package/src/gjc-runtime/gc-runtime.ts +25 -3
- package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
- package/src/gjc-runtime/session-state-sidecar.ts +110 -4
- package/src/gjc-runtime/tmux-sessions.ts +34 -5
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/modes/acp/acp-agent.ts +10 -2
- package/src/modes/components/model-selector.ts +28 -8
- package/src/modes/controllers/plan-mode-controller.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +4 -2
- package/src/runtime/process-lifecycle.ts +126 -27
- package/src/runtime-mcp/transports/stdio.ts +11 -6
- package/src/sdk/acp/adapter.ts +18 -10
- package/src/sdk/broker/broker.ts +55 -4
- package/src/sdk/broker/ensure.ts +10 -7
- package/src/sdk/broker/identity.ts +10 -0
- package/src/sdk/broker/lifecycle-ledger.ts +56 -6
- package/src/sdk/broker/lifecycle.ts +53 -10
- package/src/sdk/broker/session-index.ts +28 -11
- package/src/sdk/broker/startup-failure.ts +101 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
- package/src/sdk/bus/conversation-store.ts +171 -50
- package/src/sdk/bus/index.ts +101 -47
- package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
- package/src/sdk/bus/notification-service.ts +3 -3
- package/src/sdk/bus/reconciliation-store.ts +16 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -2
- package/src/sdk/bus/telegram-daemon.ts +131 -29
- package/src/sdk/bus/topic-registry.ts +38 -9
- package/src/sdk/cli/session-cli.ts +20 -7
- package/src/sdk/client/discovery.ts +23 -2
- package/src/sdk/host/query/cursor.ts +4 -0
- package/src/sdk/host/query/handlers.ts +99 -100
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +98 -1
- package/src/sdk/host/session-runtime.ts +57 -15
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
- package/src/sdk/host/websocket-transport.ts +54 -30
- package/src/sdk/index.ts +1 -0
- package/src/sdk/mcp/server.ts +19 -6
- package/src/sdk/protocol/operation-inventory.generated.json +1 -1
- package/src/sdk/protocol/operation-registry.ts +8 -12
- package/src/sdk/session.ts +5 -14
- package/src/sdk/turn-result.ts +53 -0
- package/src/session/agent-session.ts +112 -69
- package/src/session/blob-store.ts +91 -21
- package/src/session/session-manager.ts +408 -81
- package/src/session/session-storage.ts +68 -10
- package/src/slash-commands/helpers/fast-status-report.ts +24 -8
- package/src/task/executor.ts +25 -8
- package/src/task/index.ts +0 -2
- package/src/tools/index.ts +1 -1
- package/src/tools/read.ts +12 -11
- package/src/tools/search.ts +7 -7
- package/src/utils/edit-mode.ts +180 -9
- package/src/web/search/providers/insane.ts +55 -1
|
@@ -75,8 +75,10 @@ export function defaultMessageQueueKeysForPlatform(platform: NodeJS.Platform = p
|
|
|
75
75
|
return platform === "win32" || platform === "darwin" ? "alt+q" : "alt+enter";
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
export function defaultClipboardPasteImageKeysForPlatform(platform: NodeJS.Platform = process.platform): KeyId {
|
|
79
|
-
|
|
78
|
+
export function defaultClipboardPasteImageKeysForPlatform(platform: NodeJS.Platform = process.platform): KeyId[] {
|
|
79
|
+
if (platform === "darwin") return ["ctrl+v", "super+v"];
|
|
80
|
+
if (platform === "win32") return ["ctrl+v", "alt+v"];
|
|
81
|
+
return ["ctrl+v"];
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
/**
|
|
@@ -1223,6 +1223,7 @@ interface ModelManagerDiscoveryOptions {
|
|
|
1223
1223
|
*/
|
|
1224
1224
|
export class ModelRegistry {
|
|
1225
1225
|
#models: Model<Api>[] = [];
|
|
1226
|
+
#catalogChangeListeners: Set<() => void> = new Set();
|
|
1226
1227
|
#canonicalIndex: CanonicalModelIndex = {
|
|
1227
1228
|
records: [],
|
|
1228
1229
|
byId: new Map(),
|
|
@@ -1301,6 +1302,13 @@ export class ModelRegistry {
|
|
|
1301
1302
|
this.#loadModels();
|
|
1302
1303
|
}
|
|
1303
1304
|
|
|
1305
|
+
onCatalogChanged(listener: () => void): () => void {
|
|
1306
|
+
this.#catalogChangeListeners.add(listener);
|
|
1307
|
+
return () => {
|
|
1308
|
+
this.#catalogChangeListeners.delete(listener);
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1304
1312
|
/**
|
|
1305
1313
|
* Reload models from disk (built-in + custom from models.json).
|
|
1306
1314
|
*/
|
|
@@ -3250,6 +3258,7 @@ export class ModelRegistry {
|
|
|
3250
3258
|
}
|
|
3251
3259
|
this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig);
|
|
3252
3260
|
this.#invalidateAvailableModels();
|
|
3261
|
+
this.#notifyCatalogChanged();
|
|
3253
3262
|
this.#rebuildPending = false;
|
|
3254
3263
|
}
|
|
3255
3264
|
|
|
@@ -3259,6 +3268,16 @@ export class ModelRegistry {
|
|
|
3259
3268
|
this.#availableModelsEnvFingerprint = undefined;
|
|
3260
3269
|
}
|
|
3261
3270
|
|
|
3271
|
+
#notifyCatalogChanged(): void {
|
|
3272
|
+
for (const listener of [...this.#catalogChangeListeners]) {
|
|
3273
|
+
try {
|
|
3274
|
+
listener();
|
|
3275
|
+
} catch (error) {
|
|
3276
|
+
logger.debug("ModelRegistry catalog listener failed", { error: String(error) });
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3262
3281
|
#suspendRebuild(): void {
|
|
3263
3282
|
this.#rebuildSuspended += 1;
|
|
3264
3283
|
}
|
|
@@ -3271,6 +3290,7 @@ export class ModelRegistry {
|
|
|
3271
3290
|
this.#rebuildPending = false;
|
|
3272
3291
|
this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig);
|
|
3273
3292
|
this.#invalidateAvailableModels();
|
|
3293
|
+
this.#notifyCatalogChanged();
|
|
3274
3294
|
}
|
|
3275
3295
|
}
|
|
3276
3296
|
|
|
@@ -25,6 +25,7 @@ export const ModelCompatSchema = z.object({
|
|
|
25
25
|
supportsDeveloperRole: z.boolean().optional(),
|
|
26
26
|
sendSessionHeaders: z.boolean().optional(),
|
|
27
27
|
supportsResponsesSessionAffinity: z.boolean().optional(),
|
|
28
|
+
supportsServiceTier: z.boolean().optional(),
|
|
28
29
|
supportsMultipleSystemMessages: z.boolean().optional(),
|
|
29
30
|
supportsReasoningEffort: z.boolean().optional(),
|
|
30
31
|
reasoningEffortMap: ReasoningEffortMapSchema.optional(),
|
|
@@ -2,7 +2,7 @@ import type { Effort } from "@gajae-code/ai/model-thinking";
|
|
|
2
2
|
import { PET_MODE_IDS, PET_SKIN_IDS, PET_SKINS } from "@gajae-code/tui/components/gajae-pet";
|
|
3
3
|
import { TASK_SIMPLE_MODES } from "../task/simple-mode";
|
|
4
4
|
import { getThinkingLevelMetadata } from "../thinking-metadata";
|
|
5
|
-
import { EDIT_MODES } from "../utils/edit-mode";
|
|
5
|
+
import { DEFAULT_EDIT_MODE_SETTING, EDIT_MODE_SETTINGS, EDIT_MODES, type EditMode } from "../utils/edit-mode";
|
|
6
6
|
import { CONFIGURABLE_SEARCH_PROVIDER_IDS } from "../web/search/types";
|
|
7
7
|
import type { ModelSelectorValue } from "./model-selector-value";
|
|
8
8
|
import { UPDATE_CHANNELS } from "./update-channel";
|
|
@@ -181,9 +181,7 @@ interface ArrayDef<T> {
|
|
|
181
181
|
ui?: UiBase;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
type: "model-selector-value";
|
|
186
|
-
}
|
|
184
|
+
type RecordValueDef = { type: "model-selector-value" } | { type: "string-enum"; values: readonly string[] };
|
|
187
185
|
|
|
188
186
|
interface RecordDef<T> {
|
|
189
187
|
type: "record";
|
|
@@ -218,6 +216,7 @@ export interface ModelTagsSettings {
|
|
|
218
216
|
const EMPTY_STRING_ARRAY: string[] = [];
|
|
219
217
|
const EMPTY_MODEL_SELECTOR_RECORD: Record<string, ModelSelectorValue> = {};
|
|
220
218
|
const MODEL_SELECTOR_VALUE_SCHEMA = { type: "model-selector-value" } as const;
|
|
219
|
+
const EDIT_MODE_VALUE_SCHEMA = { type: "string-enum", values: EDIT_MODES } as const;
|
|
221
220
|
const DEFAULT_CYCLE_ORDER: string[] = ["default"];
|
|
222
221
|
const EMPTY_MODEL_TAGS_RECORD: ModelTagsSettings = {};
|
|
223
222
|
const HINDSIGHT_RECALL_TYPES_DEFAULT: string[] = ["world", "experience"];
|
|
@@ -2141,14 +2140,20 @@ export const SETTINGS_SCHEMA = {
|
|
|
2141
2140
|
// Edit tool
|
|
2142
2141
|
"edit.mode": {
|
|
2143
2142
|
type: "enum",
|
|
2144
|
-
values:
|
|
2145
|
-
default:
|
|
2143
|
+
values: EDIT_MODE_SETTINGS,
|
|
2144
|
+
default: DEFAULT_EDIT_MODE_SETTING,
|
|
2146
2145
|
ui: {
|
|
2147
2146
|
tab: "editing",
|
|
2148
2147
|
label: "Edit Mode",
|
|
2149
|
-
description:
|
|
2148
|
+
description:
|
|
2149
|
+
"Select the edit tool variant (auto routes by model family; replace, patch, hashline, vim, or apply_patch)",
|
|
2150
2150
|
},
|
|
2151
2151
|
},
|
|
2152
|
+
"edit.modelVariants": {
|
|
2153
|
+
type: "record",
|
|
2154
|
+
default: {} as Record<string, EditMode>,
|
|
2155
|
+
valueSchema: EDIT_MODE_VALUE_SCHEMA,
|
|
2156
|
+
},
|
|
2152
2157
|
|
|
2153
2158
|
"edit.fuzzyMatch": {
|
|
2154
2159
|
type: "boolean",
|
|
@@ -3857,6 +3862,13 @@ function validArraySettingValue(value: unknown, allowedValues: readonly string[]
|
|
|
3857
3862
|
return !allowedValues || value.every(item => typeof item === "string" && allowedValues.includes(item));
|
|
3858
3863
|
}
|
|
3859
3864
|
|
|
3865
|
+
function validRecordValue(valueSchema: RecordValueDef, value: unknown): boolean {
|
|
3866
|
+
if (valueSchema.type === "model-selector-value") {
|
|
3867
|
+
return typeof value === "string" || (Array.isArray(value) && value.every(item => typeof item === "string"));
|
|
3868
|
+
}
|
|
3869
|
+
return typeof value === "string" && valueSchema.values.includes(value);
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3860
3872
|
function validSettingValue(definition: (typeof SETTINGS_SCHEMA)[SettingPath], value: unknown): boolean {
|
|
3861
3873
|
return (
|
|
3862
3874
|
(definition.type === "boolean" && typeof value === "boolean") ||
|
|
@@ -3897,11 +3909,8 @@ export function validateSettingPatch(patch: Record<string, unknown>): Array<{ pa
|
|
|
3897
3909
|
issues.push({ path, detail: "Setting is not a valid record sub-path." });
|
|
3898
3910
|
continue;
|
|
3899
3911
|
}
|
|
3900
|
-
if (
|
|
3901
|
-
parentDef.valueSchema.type
|
|
3902
|
-
!(typeof value === "string" || (Array.isArray(value) && value.every(item => typeof item === "string")))
|
|
3903
|
-
) {
|
|
3904
|
-
issues.push({ path, detail: "Expected model-selector-value." });
|
|
3912
|
+
if (!validRecordValue(parentDef.valueSchema, value)) {
|
|
3913
|
+
issues.push({ path, detail: `Expected ${parentDef.valueSchema.type}.` });
|
|
3905
3914
|
}
|
|
3906
3915
|
continue;
|
|
3907
3916
|
}
|
|
@@ -3923,11 +3932,8 @@ export function validateSettingPatch(patch: Record<string, unknown>): Array<{ pa
|
|
|
3923
3932
|
}
|
|
3924
3933
|
if (definition.type === "record" && "valueSchema" in definition && definition.valueSchema) {
|
|
3925
3934
|
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
3926
|
-
if (
|
|
3927
|
-
definition.valueSchema.type
|
|
3928
|
-
!(typeof entry === "string" || (Array.isArray(entry) && entry.every(item => typeof item === "string")))
|
|
3929
|
-
) {
|
|
3930
|
-
issues.push({ path: `${path}.${key}`, detail: "Expected model-selector-value." });
|
|
3935
|
+
if (!validRecordValue(definition.valueSchema, entry)) {
|
|
3936
|
+
issues.push({ path: `${path}.${key}`, detail: `Expected ${definition.valueSchema.type}.` });
|
|
3931
3937
|
}
|
|
3932
3938
|
}
|
|
3933
3939
|
}
|
|
@@ -3984,11 +3990,12 @@ export function reconcileSettingsSchema(raw: Record<string, unknown>): {
|
|
|
3984
3990
|
validSettingValue(definition, next)
|
|
3985
3991
|
) {
|
|
3986
3992
|
for (const [key, entry] of Object.entries(next as Record<string, unknown>)) {
|
|
3987
|
-
if (
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3993
|
+
if (!validRecordValue(definition.valueSchema, entry)) {
|
|
3994
|
+
issues.push({
|
|
3995
|
+
path: `${path}.${key}`,
|
|
3996
|
+
kind: "invalid",
|
|
3997
|
+
detail: `Expected ${definition.valueSchema.type}.`,
|
|
3998
|
+
});
|
|
3992
3999
|
}
|
|
3993
4000
|
}
|
|
3994
4001
|
}
|
package/src/config/settings.ts
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
parseNotificationSettingsSnapshot,
|
|
38
38
|
} from "../sdk/bus/config";
|
|
39
39
|
import { AgentStorage } from "../session/agent-storage";
|
|
40
|
-
import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
|
|
40
|
+
import { type EditMode, type EditVariantMatch, normalizeEditMode } from "../utils/edit-mode";
|
|
41
41
|
import {
|
|
42
42
|
type AtomicYamlPatch,
|
|
43
43
|
applyAtomicYamlPatches,
|
|
@@ -77,6 +77,8 @@ export interface RawSettings {
|
|
|
77
77
|
[key: string]: unknown;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
const UNSAFE_EDIT_VARIANT_PATTERNS = new Set(["__proto__", "constructor", "prototype"]);
|
|
81
|
+
|
|
80
82
|
type SettingsPatch = {
|
|
81
83
|
readonly path: string;
|
|
82
84
|
readonly value: unknown | undefined;
|
|
@@ -964,14 +966,17 @@ export class Settings implements NotificationSettingsReader {
|
|
|
964
966
|
/**
|
|
965
967
|
* Get the edit variant for a specific model.
|
|
966
968
|
* Returns "patch", "replace", "hashline", "vim", "apply_patch", or null (use global default).
|
|
969
|
+
* Skips invalid values; prefer `matchEditVariantForModel` when invalid
|
|
970
|
+
* matches must fail closed instead of falling through.
|
|
967
971
|
*/
|
|
968
972
|
getEditVariantForModel(model: string | undefined): EditMode | null {
|
|
969
973
|
if (!model) return null;
|
|
970
|
-
const variants =
|
|
974
|
+
const variants = this.#editModelVariants();
|
|
971
975
|
if (!variants) return null;
|
|
972
|
-
for (const pattern
|
|
976
|
+
for (const [pattern, rawValue] of Object.entries(variants)) {
|
|
977
|
+
if (UNSAFE_EDIT_VARIANT_PATTERNS.has(pattern)) continue;
|
|
973
978
|
if (model.includes(pattern)) {
|
|
974
|
-
const value = normalizeEditMode(
|
|
979
|
+
const value = normalizeEditMode(rawValue);
|
|
975
980
|
if (value) {
|
|
976
981
|
return value;
|
|
977
982
|
}
|
|
@@ -980,6 +985,35 @@ export class Settings implements NotificationSettingsReader {
|
|
|
980
985
|
return null;
|
|
981
986
|
}
|
|
982
987
|
|
|
988
|
+
/**
|
|
989
|
+
* First matching `edit.modelVariants` rule for a model, with its raw
|
|
990
|
+
* (unvalidated) value. The edit-mode resolver uses this discriminated
|
|
991
|
+
* result so a matched-but-invalid value fails closed with a diagnostic
|
|
992
|
+
* rather than silently resolving to another mode.
|
|
993
|
+
*/
|
|
994
|
+
matchEditVariantForModel(model: string | undefined): EditVariantMatch | null {
|
|
995
|
+
if (!model) return null;
|
|
996
|
+
const variants = this.#editModelVariants();
|
|
997
|
+
if (!variants) return null;
|
|
998
|
+
for (const [pattern, value] of Object.entries(variants)) {
|
|
999
|
+
if (UNSAFE_EDIT_VARIANT_PATTERNS.has(pattern)) continue;
|
|
1000
|
+
if (model.includes(pattern)) {
|
|
1001
|
+
return { pattern, value: String(value) };
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
#editModelVariants(): Record<string, string> | undefined {
|
|
1008
|
+
const variants = (this.#merged.edit as { modelVariants?: unknown })?.modelVariants;
|
|
1009
|
+
return typeof variants === "object" &&
|
|
1010
|
+
variants !== null &&
|
|
1011
|
+
!Array.isArray(variants) &&
|
|
1012
|
+
(Object.getPrototypeOf(variants) === Object.prototype || Object.getPrototypeOf(variants) === null)
|
|
1013
|
+
? (variants as Record<string, string>)
|
|
1014
|
+
: undefined;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
983
1017
|
/**
|
|
984
1018
|
* Get bash interceptor rules (typed accessor for complex array config).
|
|
985
1019
|
*/
|
|
@@ -2075,18 +2109,16 @@ export class Settings implements NotificationSettingsReader {
|
|
|
2075
2109
|
|
|
2076
2110
|
if (override === undefined) continue;
|
|
2077
2111
|
|
|
2078
|
-
|
|
2112
|
+
const value =
|
|
2079
2113
|
typeof override === "object" &&
|
|
2080
2114
|
override !== null &&
|
|
2081
2115
|
!Array.isArray(override) &&
|
|
2082
2116
|
typeof baseVal === "object" &&
|
|
2083
2117
|
baseVal !== null &&
|
|
2084
2118
|
!Array.isArray(baseVal)
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
result[key] = override;
|
|
2089
|
-
}
|
|
2119
|
+
? this.#deepMerge(baseVal as RawSettings, override as RawSettings)
|
|
2120
|
+
: override;
|
|
2121
|
+
Object.defineProperty(result, key, { configurable: true, enumerable: true, value, writable: true });
|
|
2090
2122
|
}
|
|
2091
2123
|
return result;
|
|
2092
2124
|
}
|
|
@@ -5,20 +5,20 @@ import * as path from "node:path";
|
|
|
5
5
|
import { getAgentDir, isKnownSinkPeerClosedError } from "@gajae-code/utils";
|
|
6
6
|
import { normalizePathForComparison, VERSION } from "@gajae-code/utils/dirs";
|
|
7
7
|
|
|
8
|
+
import { withFileLock } from "../config/file-lock";
|
|
8
9
|
import {
|
|
9
10
|
COORDINATOR_MCP_PROTOCOL_VERSION,
|
|
10
11
|
COORDINATOR_MCP_SERVER_NAME,
|
|
11
12
|
COORDINATOR_MCP_TOOL_NAMES,
|
|
12
13
|
type CoordinatorToolName,
|
|
13
14
|
} from "../coordinator/contract";
|
|
14
|
-
import { readLinuxProcStartTimeSync } from "../gjc-runtime/linux-proc";
|
|
15
15
|
import { listMcpDelegateHostContexts } from "../hooks/mcp-delegate-host-context";
|
|
16
16
|
import type { WorkflowGate, WorkflowGateQueryRecord } from "../modes/shared/agent-wire/workflow-gate-types";
|
|
17
17
|
import type { BrokerDiscovery } from "../sdk/broker/discovery";
|
|
18
18
|
import { type EnsureBrokerSettings, ensureBroker } from "../sdk/broker/ensure";
|
|
19
19
|
import { lifecycleRequestTimeoutMs } from "../sdk/broker/startup-budget";
|
|
20
20
|
import { UnsupportedStateVersionError } from "../sdk/broker/state-version";
|
|
21
|
-
import { SdkClient, SdkClientError } from "../sdk/client/client";
|
|
21
|
+
import { SdkClient, SdkClientError, type SdkSentRecord } from "../sdk/client/client";
|
|
22
22
|
import { readSdkBrokerDiscovery } from "../sdk/client/discovery";
|
|
23
23
|
import {
|
|
24
24
|
type ActivatedPreparedSession,
|
|
@@ -111,6 +111,16 @@ interface JsonRpcResponse {
|
|
|
111
111
|
error?: { code: number; message: string; data?: unknown };
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
function isLifecycleOperation(operation: string): boolean {
|
|
115
|
+
return (
|
|
116
|
+
operation === "session.create" ||
|
|
117
|
+
operation === "session.fork" ||
|
|
118
|
+
operation === "session.resume" ||
|
|
119
|
+
operation === "session.close" ||
|
|
120
|
+
operation === "session.delete"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
function sinkErrorCode(error: unknown): string | undefined {
|
|
115
125
|
if (error === null || (typeof error !== "object" && typeof error !== "function")) return undefined;
|
|
116
126
|
try {
|
|
@@ -270,6 +280,10 @@ interface CoordinatorSessionState {
|
|
|
270
280
|
source: "coordinator" | "agent_session_event";
|
|
271
281
|
live: boolean | null;
|
|
272
282
|
reason: string | null;
|
|
283
|
+
last_activity_at?: string;
|
|
284
|
+
activity?: unknown;
|
|
285
|
+
active_tools?: unknown;
|
|
286
|
+
active_tool_calls?: unknown;
|
|
273
287
|
}
|
|
274
288
|
|
|
275
289
|
type CoordinatorEventKind =
|
|
@@ -1227,7 +1241,7 @@ function publicLifecycleReceipt(result: Record<string, unknown>, sessionId: stri
|
|
|
1227
1241
|
|
|
1228
1242
|
function publicCoordinatorSessionState(state: CoordinatorSessionState | null): Record<string, unknown> | null {
|
|
1229
1243
|
if (!state) return null;
|
|
1230
|
-
|
|
1244
|
+
const result: Record<string, unknown> = {
|
|
1231
1245
|
session_id: state.session_id,
|
|
1232
1246
|
state: state.state,
|
|
1233
1247
|
ready_for_input: state.ready_for_input,
|
|
@@ -1236,6 +1250,55 @@ function publicCoordinatorSessionState(state: CoordinatorSessionState | null): R
|
|
|
1236
1250
|
updated_at: state.updated_at,
|
|
1237
1251
|
...(typeof state.live === "boolean" ? { live: state.live } : {}),
|
|
1238
1252
|
};
|
|
1253
|
+
const activity = asRecord(state.activity);
|
|
1254
|
+
if (
|
|
1255
|
+
typeof state.last_activity_at === "string" &&
|
|
1256
|
+
Number.isFinite(Date.parse(state.last_activity_at)) &&
|
|
1257
|
+
activity &&
|
|
1258
|
+
typeof activity.sequence === "number" &&
|
|
1259
|
+
Number.isSafeInteger(activity.sequence) &&
|
|
1260
|
+
activity.sequence > 0 &&
|
|
1261
|
+
activity.kind === "tool_execution" &&
|
|
1262
|
+
(activity.phase === "started" || activity.phase === "finished") &&
|
|
1263
|
+
typeof activity.tool_name === "string" &&
|
|
1264
|
+
/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(activity.tool_name) &&
|
|
1265
|
+
typeof activity.observed_at === "string" &&
|
|
1266
|
+
Number.isFinite(Date.parse(activity.observed_at)) &&
|
|
1267
|
+
typeof activity.active_tool_count === "number" &&
|
|
1268
|
+
Number.isSafeInteger(activity.active_tool_count) &&
|
|
1269
|
+
activity.active_tool_count >= 0
|
|
1270
|
+
) {
|
|
1271
|
+
result.last_activity_at = state.last_activity_at;
|
|
1272
|
+
result.activity = {
|
|
1273
|
+
sequence: activity.sequence,
|
|
1274
|
+
kind: activity.kind,
|
|
1275
|
+
phase: activity.phase,
|
|
1276
|
+
tool_name: activity.tool_name,
|
|
1277
|
+
observed_at: activity.observed_at,
|
|
1278
|
+
...(typeof activity.started_at === "string" && Number.isFinite(Date.parse(activity.started_at))
|
|
1279
|
+
? { started_at: activity.started_at }
|
|
1280
|
+
: {}),
|
|
1281
|
+
...(typeof activity.elapsed_ms === "number" &&
|
|
1282
|
+
Number.isSafeInteger(activity.elapsed_ms) &&
|
|
1283
|
+
activity.elapsed_ms >= 0
|
|
1284
|
+
? { elapsed_ms: activity.elapsed_ms }
|
|
1285
|
+
: {}),
|
|
1286
|
+
...(activity.outcome === "succeeded" || activity.outcome === "failed" ? { outcome: activity.outcome } : {}),
|
|
1287
|
+
active_tool_count: activity.active_tool_count,
|
|
1288
|
+
};
|
|
1289
|
+
if (Array.isArray(state.active_tools))
|
|
1290
|
+
result.active_tools = state.active_tools.slice(0, 8).flatMap(value => {
|
|
1291
|
+
const tool = asRecord(value);
|
|
1292
|
+
return tool &&
|
|
1293
|
+
typeof tool.tool_name === "string" &&
|
|
1294
|
+
/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(tool.tool_name) &&
|
|
1295
|
+
typeof tool.started_at === "string" &&
|
|
1296
|
+
Number.isFinite(Date.parse(tool.started_at))
|
|
1297
|
+
? [{ tool_name: tool.tool_name, started_at: tool.started_at }]
|
|
1298
|
+
: [];
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
return result;
|
|
1239
1302
|
}
|
|
1240
1303
|
|
|
1241
1304
|
function eventTimestamp(record: Record<string, unknown>): string | null {
|
|
@@ -1828,7 +1891,8 @@ async function writeSessionStateUnlocked(
|
|
|
1828
1891
|
overwrite?: boolean;
|
|
1829
1892
|
} = {},
|
|
1830
1893
|
): Promise<CoordinatorSessionState> {
|
|
1831
|
-
const
|
|
1894
|
+
const existing = await readSessionState(namespaceDir, sessionId);
|
|
1895
|
+
const previous = options.overwrite ? null : existing;
|
|
1832
1896
|
const hasCurrentTurn = Object.hasOwn(options, "currentTurnId");
|
|
1833
1897
|
const hasLastTurn = Object.hasOwn(options, "lastTurnId");
|
|
1834
1898
|
const hasLive = Object.hasOwn(options, "live");
|
|
@@ -1847,6 +1911,12 @@ async function writeSessionStateUnlocked(
|
|
|
1847
1911
|
source: options.source ?? "coordinator",
|
|
1848
1912
|
live: hasLive ? (options.live ?? null) : (previous?.live ?? null),
|
|
1849
1913
|
reason: options.reason ?? null,
|
|
1914
|
+
...(typeof existing?.last_activity_at === "string" ? { last_activity_at: existing.last_activity_at } : {}),
|
|
1915
|
+
...(existing?.activity && typeof existing.activity === "object" ? { activity: existing.activity } : {}),
|
|
1916
|
+
...(Array.isArray(existing?.active_tools) ? { active_tools: existing.active_tools } : {}),
|
|
1917
|
+
...(existing?.active_tool_calls && typeof existing.active_tool_calls === "object"
|
|
1918
|
+
? { active_tool_calls: existing.active_tool_calls }
|
|
1919
|
+
: {}),
|
|
1850
1920
|
};
|
|
1851
1921
|
await writeJsonFile(sessionStateFile(namespaceDir, sessionId), payload);
|
|
1852
1922
|
if (
|
|
@@ -1874,108 +1944,8 @@ async function writeSessionStateUnlocked(
|
|
|
1874
1944
|
return payload;
|
|
1875
1945
|
}
|
|
1876
1946
|
|
|
1877
|
-
interface SessionStateLockOwner {
|
|
1878
|
-
pid: number;
|
|
1879
|
-
start_time: string;
|
|
1880
|
-
token: string;
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
function processStartTime(pid: number): string | null {
|
|
1884
|
-
return readLinuxProcStartTimeSync(pid);
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
function validLockOwner(value: unknown): value is SessionStateLockOwner {
|
|
1888
|
-
if (!value || typeof value !== "object") return false;
|
|
1889
|
-
const owner = value as Partial<SessionStateLockOwner>;
|
|
1890
|
-
return (
|
|
1891
|
-
typeof owner.pid === "number" &&
|
|
1892
|
-
Number.isSafeInteger(owner.pid) &&
|
|
1893
|
-
owner.pid > 0 &&
|
|
1894
|
-
typeof owner.start_time === "string" &&
|
|
1895
|
-
typeof owner.token === "string" &&
|
|
1896
|
-
owner.token.length > 0
|
|
1897
|
-
);
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
function lockOwnerIsAlive(value: unknown): boolean {
|
|
1901
|
-
if (!validLockOwner(value)) return false;
|
|
1902
|
-
const owner = value;
|
|
1903
|
-
try {
|
|
1904
|
-
process.kill(owner.pid, 0);
|
|
1905
|
-
} catch (error) {
|
|
1906
|
-
if ((error as NodeJS.ErrnoException).code === "ESRCH") return false;
|
|
1907
|
-
return true;
|
|
1908
|
-
}
|
|
1909
|
-
const currentStartTime = processStartTime(owner.pid);
|
|
1910
|
-
return currentStartTime === null || currentStartTime === owner.start_time;
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
async function reclaimStaleSessionStateLock(lockFile: string): Promise<void> {
|
|
1914
|
-
let raw: string;
|
|
1915
|
-
try {
|
|
1916
|
-
raw = await fs.readFile(lockFile, "utf8");
|
|
1917
|
-
} catch (error) {
|
|
1918
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
1919
|
-
throw error;
|
|
1920
|
-
}
|
|
1921
|
-
let owner: unknown;
|
|
1922
|
-
try {
|
|
1923
|
-
owner = JSON.parse(raw);
|
|
1924
|
-
} catch {
|
|
1925
|
-
owner = null;
|
|
1926
|
-
}
|
|
1927
|
-
if (!validLockOwner(owner)) {
|
|
1928
|
-
const stat = await fs.stat(lockFile);
|
|
1929
|
-
if (Date.now() - stat.mtimeMs < 30_000) return;
|
|
1930
|
-
} else if (lockOwnerIsAlive(owner)) return;
|
|
1931
|
-
try {
|
|
1932
|
-
if ((await fs.readFile(lockFile, "utf8")) === raw) await fs.rm(lockFile);
|
|
1933
|
-
} catch (error) {
|
|
1934
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
1947
|
async function withSessionStateLock<T>(stateFile: string, operation: () => Promise<T>): Promise<T> {
|
|
1939
|
-
|
|
1940
|
-
const owner: SessionStateLockOwner = {
|
|
1941
|
-
pid: process.pid,
|
|
1942
|
-
start_time: processStartTime(process.pid) ?? "unknown",
|
|
1943
|
-
token: randomUUID(),
|
|
1944
|
-
};
|
|
1945
|
-
await ensureDir(path.dirname(stateFile));
|
|
1946
|
-
for (let attempt = 0; attempt < 12_000; attempt++) {
|
|
1947
|
-
let handle: fs.FileHandle | undefined;
|
|
1948
|
-
try {
|
|
1949
|
-
handle = await fs.open(lockFile, "wx");
|
|
1950
|
-
try {
|
|
1951
|
-
await handle.writeFile(JSON.stringify(owner));
|
|
1952
|
-
} catch (error) {
|
|
1953
|
-
await handle.close().catch(() => undefined);
|
|
1954
|
-
handle = undefined;
|
|
1955
|
-
await fs.rm(lockFile, { force: true }).catch(() => undefined);
|
|
1956
|
-
throw error;
|
|
1957
|
-
}
|
|
1958
|
-
const outcome = await operation().then(
|
|
1959
|
-
value => ({ ok: true as const, value }),
|
|
1960
|
-
error => ({ ok: false as const, error }),
|
|
1961
|
-
);
|
|
1962
|
-
await handle.close();
|
|
1963
|
-
try {
|
|
1964
|
-
if ((await fs.readFile(lockFile, "utf8")) === JSON.stringify(owner)) await fs.rm(lockFile);
|
|
1965
|
-
} catch (error) {
|
|
1966
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1967
|
-
}
|
|
1968
|
-
if (!outcome.ok) throw outcome.error;
|
|
1969
|
-
return outcome.value;
|
|
1970
|
-
} catch (error) {
|
|
1971
|
-
if (handle) throw error;
|
|
1972
|
-
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw new Error("coordinator_state_unreadable");
|
|
1973
|
-
|
|
1974
|
-
await reclaimStaleSessionStateLock(lockFile);
|
|
1975
|
-
await Bun.sleep(5);
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
throw new Error("coordinator_state_unreadable");
|
|
1948
|
+
return await withFileLock(stateFile, operation, { staleMs: 30_000, retries: 12_000, retryDelayMs: 5 });
|
|
1979
1949
|
}
|
|
1980
1950
|
|
|
1981
1951
|
async function writeSessionState(
|
|
@@ -3191,7 +3161,15 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3191
3161
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
3192
3162
|
});
|
|
3193
3163
|
} catch (error) {
|
|
3194
|
-
|
|
3164
|
+
const mapped = toCoordinatorBrokerError("request", error);
|
|
3165
|
+
if (
|
|
3166
|
+
isLifecycleOperation(operation) &&
|
|
3167
|
+
mapped.code === "uncertain_after_send" &&
|
|
3168
|
+
mapped.details &&
|
|
3169
|
+
typeof mapped.details === "object"
|
|
3170
|
+
)
|
|
3171
|
+
result = await client.lookupLifecycle(mapped.details as SdkSentRecord);
|
|
3172
|
+
else requestError = mapped;
|
|
3195
3173
|
}
|
|
3196
3174
|
try {
|
|
3197
3175
|
await client.close();
|
package/src/edit/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import { prompt } from "@gajae-code/utils";
|
|
3
3
|
import type * as z from "zod/v4";
|
|
4
4
|
import {
|
|
5
5
|
executeHashlineSingle,
|
|
@@ -22,7 +22,7 @@ import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
|
22
22
|
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
|
|
23
23
|
import type { ToolSession } from "../tools";
|
|
24
24
|
import { VimTool, vimSchema } from "../tools/vim";
|
|
25
|
-
import { type EditMode,
|
|
25
|
+
import { type EditMode, resolveEditMode, resolveForcedEnvEditMode } from "../utils/edit-mode";
|
|
26
26
|
import type { VimToolDetails } from "../vim/types";
|
|
27
27
|
import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
|
|
28
28
|
import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
|
|
@@ -132,19 +132,6 @@ type EditModeDefinition = {
|
|
|
132
132
|
) => Promise<AgentToolResult<EditToolResultDetails, TInput>>;
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
-
function resolveConfiguredEditMode(rawEditMode: string): EditMode | undefined {
|
|
136
|
-
if (!rawEditMode || rawEditMode === "auto") {
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const editMode = normalizeEditMode(rawEditMode);
|
|
141
|
-
if (!editMode) {
|
|
142
|
-
throw new Error(`Invalid PI_EDIT_VARIANT: ${rawEditMode}`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return editMode;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
135
|
function resolveAllowFuzzy(session: ToolSession, rawValue: string): boolean {
|
|
149
136
|
switch (rawValue) {
|
|
150
137
|
case "true":
|
|
@@ -347,15 +334,15 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
347
334
|
readonly #allowFuzzy: boolean;
|
|
348
335
|
readonly #fuzzyThreshold: number;
|
|
349
336
|
readonly #writethrough: WritethroughCallback;
|
|
350
|
-
readonly #editMode?: EditMode;
|
|
351
337
|
readonly #vimTool: VimTool;
|
|
352
338
|
readonly #pendingDeferredFetches = new Map<string, AbortController>();
|
|
353
339
|
|
|
354
340
|
constructor(private readonly session: ToolSession) {
|
|
355
341
|
const { PI_EDIT_FUZZY: editFuzzy = "auto", PI_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto" } = Bun.env;
|
|
356
|
-
const envEditVariant = $pickenv("GJC_EDIT_VARIANT", "PI_EDIT_VARIANT") ?? "auto";
|
|
357
342
|
|
|
358
|
-
|
|
343
|
+
// Fail fast on an invalid forced env variant; the shared resolver owns
|
|
344
|
+
// env precedence at resolution time (no separate cache here).
|
|
345
|
+
resolveForcedEnvEditMode();
|
|
359
346
|
this.#allowFuzzy = resolveAllowFuzzy(session, editFuzzy);
|
|
360
347
|
this.#fuzzyThreshold = resolveFuzzyThreshold(session, editFuzzyThreshold);
|
|
361
348
|
this.#writethrough = createEditWritethrough(session);
|
|
@@ -363,7 +350,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
363
350
|
}
|
|
364
351
|
|
|
365
352
|
get mode(): EditMode {
|
|
366
|
-
if (this.#editMode) return this.#editMode;
|
|
367
353
|
return resolveEditMode(this.session);
|
|
368
354
|
}
|
|
369
355
|
|
package/src/edit/renderer.ts
CHANGED
|
@@ -356,8 +356,8 @@ export const editToolRenderer = {
|
|
|
356
356
|
});
|
|
357
357
|
const firstEdit = Array.isArray(editArgs.edits) && editArgs.edits.length > 0 ? editArgs.edits[0] : undefined;
|
|
358
358
|
const rawPath = inventory.paths[0] ?? "";
|
|
359
|
-
const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move;
|
|
360
|
-
const op = editArgs.op || firstEdit?.op || inventory.
|
|
359
|
+
const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move || inventory.rename;
|
|
360
|
+
const op = editArgs.op || firstEdit?.op || inventory.op;
|
|
361
361
|
const { description } = formatEditDescription(rawPath, uiTheme, options.expanded ? { rename } : undefined);
|
|
362
362
|
const spinner =
|
|
363
363
|
options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
|
|
@@ -416,6 +416,7 @@ function resolveCompletedEditIdentity(
|
|
|
416
416
|
const firstEdit = args?.edits?.[0];
|
|
417
417
|
const inventory = getEditRequestTargetInventory(args, editMode, { isPartial });
|
|
418
418
|
const detailsPath = details && "path" in details ? details.path : undefined;
|
|
419
|
+
|
|
419
420
|
return {
|
|
420
421
|
path: detailsPath ?? args?.file_path ?? args?.path ?? firstEdit?.path ?? inventory.paths[0] ?? "",
|
|
421
422
|
op: details?.op ?? args?.op ?? firstEdit?.op,
|