@gajae-code/coding-agent 0.17.6 → 0.17.7

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 (48) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/types/cli/public-command-errors.d.ts +2 -0
  3. package/dist/types/commands/sdk.d.ts +7 -1
  4. package/dist/types/config/model-profile-ownership.d.ts +74 -0
  5. package/dist/types/config/model-registry.d.ts +2 -1
  6. package/dist/types/config/settings-schema.d.ts +32 -0
  7. package/dist/types/sdk/broker/lifecycle.d.ts +4 -1
  8. package/dist/types/sdk/lifecycle/service.d.ts +2 -0
  9. package/dist/types/sdk/lifecycle-session.d.ts +5 -1
  10. package/dist/types/sdk/session-directory.d.ts +1 -0
  11. package/dist/types/sdk/session-list.d.ts +11 -0
  12. package/dist/types/sdk/startup-capability.d.ts +1 -1
  13. package/dist/types/session/agent-session.d.ts +6 -0
  14. package/dist/types/session/internal/managed-session-scope.d.ts +4 -0
  15. package/dist/types/session/session-manager.d.ts +6 -0
  16. package/dist/types/setup/model-onboarding-guidance.d.ts +18 -0
  17. package/dist/types/task/delegation-hint.d.ts +53 -0
  18. package/examples/README.md +3 -3
  19. package/examples/custom-tools/README.md +1 -1
  20. package/examples/hooks/README.md +1 -1
  21. package/package.json +7 -7
  22. package/scripts/generate-sdk-operation-inventory.ts +6 -0
  23. package/src/cli/config-cli.ts +2 -0
  24. package/src/cli/public-command-errors.ts +6 -0
  25. package/src/commands/sdk.ts +361 -117
  26. package/src/config/model-profile-ownership.ts +303 -0
  27. package/src/config/model-registry.ts +53 -14
  28. package/src/config/settings-schema.ts +81 -0
  29. package/src/internal-urls/docs-index.generated.ts +18 -17
  30. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  31. package/src/modes/controllers/selector-controller.ts +3 -29
  32. package/src/sdk/broker/broker.ts +120 -23
  33. package/src/sdk/broker/lifecycle.ts +89 -13
  34. package/src/sdk/cli/session-cli.ts +45 -9
  35. package/src/sdk/host/control/dispatch.ts +6 -0
  36. package/src/sdk/lifecycle/service.ts +186 -3
  37. package/src/sdk/lifecycle-session.ts +35 -8
  38. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  39. package/src/sdk/protocol/operation-registry.ts +5 -1
  40. package/src/sdk/session-directory.ts +1 -1
  41. package/src/sdk/session-list.ts +47 -0
  42. package/src/sdk/session.ts +290 -114
  43. package/src/sdk/startup-capability.ts +2 -1
  44. package/src/session/agent-session.ts +102 -4
  45. package/src/session/internal/managed-session-scope.ts +81 -17
  46. package/src/session/session-manager.ts +70 -11
  47. package/src/setup/model-onboarding-guidance.ts +26 -0
  48. package/src/task/delegation-hint.ts +578 -0
@@ -0,0 +1,303 @@
1
+ import * as util from "node:util";
2
+ import { resolveModelProfileName } from "./model-profile-contract";
3
+ import type { SettingsAtomicPatch } from "./settings";
4
+
5
+ const OWNERSHIP_PROJECTION_PATHS = new Set(["modelProfile.ownership", "modelProfile.default"]);
6
+
7
+ export type ModelProfileOwnershipMarker =
8
+ | { kind: "inherit" }
9
+ | { kind: "cleared" }
10
+ | { kind: "profile"; profile: string };
11
+
12
+ export interface DurableModelProfileOwnership {
13
+ schemaVersion: 1;
14
+ version: number;
15
+ marker: ModelProfileOwnershipMarker;
16
+ }
17
+
18
+ export interface ModelProfileOwnershipSettings {
19
+ getGlobal(path: "modelProfile.ownership" | "modelProfile.default"): unknown;
20
+ }
21
+
22
+ export interface DurableModelProfileOwnershipCommit {
23
+ ownership: DurableModelProfileOwnership;
24
+ wrote: boolean;
25
+ }
26
+
27
+ export interface DurableModelProfileOwnershipStore extends ModelProfileOwnershipSettings {
28
+ commitAtomicBatchWithCurrent(
29
+ buildPatches: (
30
+ current: Readonly<Record<string, unknown>>,
31
+ ) => Promise<readonly SettingsAtomicPatch[]> | readonly SettingsAtomicPatch[],
32
+ ): Promise<unknown>;
33
+ }
34
+
35
+ export const MODEL_PROFILE_OWNERSHIP_ENTRY = "model_profile_ownership";
36
+
37
+ export function validateModelProfileOwnershipMarker(value: unknown): ModelProfileOwnershipMarker | undefined {
38
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
39
+ const marker = value as Record<string, unknown>;
40
+ if (marker.kind === "inherit" || marker.kind === "cleared") {
41
+ return Object.keys(marker).length === 1 ? { kind: marker.kind } : undefined;
42
+ }
43
+ if (marker.kind === "profile" && typeof marker.profile === "string" && marker.profile.trim() !== "") {
44
+ return Object.keys(marker).length === 2 ? { kind: "profile", profile: marker.profile } : undefined;
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ export function validateDurableModelProfileOwnership(value: unknown): DurableModelProfileOwnership | undefined {
50
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
51
+ const record = value as Record<string, unknown>;
52
+ const marker = validateModelProfileOwnershipMarker(record.marker);
53
+ if (
54
+ record.schemaVersion !== 1 ||
55
+ !Number.isSafeInteger(record.version) ||
56
+ (record.version as number) < 0 ||
57
+ !marker ||
58
+ Object.keys(record).some(key => !["schemaVersion", "version", "marker"].includes(key))
59
+ ) {
60
+ return undefined;
61
+ }
62
+ return { schemaVersion: 1, version: record.version as number, marker };
63
+ }
64
+
65
+ function markersEqual(left: ModelProfileOwnershipMarker, right: ModelProfileOwnershipMarker): boolean {
66
+ if (left.kind !== right.kind) return false;
67
+ return left.kind !== "profile" || (right.kind === "profile" && left.profile === right.profile);
68
+ }
69
+
70
+ function rawValueAtPath(raw: Readonly<Record<string, unknown>>, path: string): unknown {
71
+ const dotted = raw[path];
72
+ if (dotted !== undefined) return dotted;
73
+ let current: unknown = raw;
74
+ for (const segment of path.split(".")) {
75
+ if (!current || typeof current !== "object" || Array.isArray(current)) return undefined;
76
+ current = (current as Record<string, unknown>)[segment];
77
+ }
78
+ return current;
79
+ }
80
+
81
+ function extraPatchesAlreadyApplied(
82
+ current: Readonly<Record<string, unknown>>,
83
+ patches: readonly SettingsAtomicPatch[],
84
+ ): boolean {
85
+ return patches.every(patch => {
86
+ const actual = rawValueAtPath(current, patch.path);
87
+ return patch.op === "unset" ? actual === undefined : util.isDeepStrictEqual(actual, patch.value);
88
+ });
89
+ }
90
+
91
+ function assertLegacyDefaultMatchesOwnership(marker: ModelProfileOwnershipMarker, legacyProfile: unknown): void {
92
+ const expected = marker.kind === "profile" ? marker.profile : undefined;
93
+ if (legacyProfile !== expected) throw new InvalidModelProfileOwnershipError();
94
+ }
95
+
96
+ export class InvalidModelProfileOwnershipError extends Error {
97
+ readonly code = "model_profile_ownership_invalid";
98
+
99
+ constructor() {
100
+ super("Persisted model-profile ownership state is invalid; refusing to infer an owner.");
101
+ this.name = "InvalidModelProfileOwnershipError";
102
+ }
103
+ }
104
+
105
+ export class UnresolvedModelProfileOwnershipError extends Error {
106
+ readonly code = "model_profile_unresolved";
107
+
108
+ constructor(readonly profileName: string) {
109
+ super(
110
+ `Model profile "${profileName}" is referenced by this session but no longer exists. Choose a replacement profile explicitly.`,
111
+ );
112
+ this.name = "UnresolvedModelProfileOwnershipError";
113
+ }
114
+ }
115
+
116
+ export class ModelProfileReplacementRequiredError extends Error {
117
+ readonly code = "model_profile_replacement_required";
118
+
119
+ constructor(readonly profileName: string) {
120
+ super(`Choose a replacement profile before deleting the active profile "${profileName}".`);
121
+ this.name = "ModelProfileReplacementRequiredError";
122
+ }
123
+ }
124
+
125
+ export class ModelProfileOwnershipConflictError extends Error {
126
+ readonly code = "model_profile_ownership_conflict";
127
+
128
+ constructor(
129
+ readonly expectedVersion: number,
130
+ readonly actualVersion: number,
131
+ readonly actualMarker?: ModelProfileOwnershipMarker,
132
+ ) {
133
+ super(
134
+ `Model-profile ownership changed concurrently (expected version ${expectedVersion}, found ${actualVersion}).`,
135
+ );
136
+ this.name = "ModelProfileOwnershipConflictError";
137
+ }
138
+ }
139
+
140
+ export class ModelProfileApplyCommittedError extends Error {
141
+ readonly code = "model_profile_apply_committed";
142
+
143
+ constructor(
144
+ readonly profileName: string,
145
+ readonly committedVersion: number,
146
+ cause: unknown,
147
+ ) {
148
+ super(
149
+ `Model-profile ownership for "${profileName}" was durably committed at version ${committedVersion}, but could not be applied. The requesting session is fail-closed; the durable version was not compensated.`,
150
+ { cause },
151
+ );
152
+ this.name = "ModelProfileApplyCommittedError";
153
+ }
154
+ }
155
+
156
+ export interface ProfileOwnershipChangedEvent {
157
+ type: "profile_ownership_changed";
158
+ transitionId: string;
159
+ source: "session" | "durable" | "recovery";
160
+ oldMarker: ModelProfileOwnershipMarker;
161
+ newMarker: ModelProfileOwnershipMarker;
162
+ oldSessionId: string;
163
+ sessionId: string;
164
+ observedDurableVersion: number;
165
+ committedDurableVersion?: number;
166
+ outcome: "committed" | "failed" | "reconciled";
167
+ }
168
+
169
+ export function readDurableModelProfileOwnership(
170
+ settings: ModelProfileOwnershipSettings,
171
+ ): DurableModelProfileOwnership {
172
+ const configured = settings.getGlobal("modelProfile.ownership");
173
+ if (configured !== undefined) {
174
+ const validated = validateDurableModelProfileOwnership(configured);
175
+ if (!validated) throw new InvalidModelProfileOwnershipError();
176
+ assertLegacyDefaultMatchesOwnership(validated.marker, settings.getGlobal("modelProfile.default"));
177
+ return validated;
178
+ }
179
+ const legacyProfile = settings.getGlobal("modelProfile.default");
180
+ return {
181
+ schemaVersion: 1,
182
+ version: 0,
183
+ marker:
184
+ typeof legacyProfile === "string" && legacyProfile.trim() !== ""
185
+ ? { kind: "profile", profile: legacyProfile }
186
+ : { kind: "inherit" },
187
+ };
188
+ }
189
+
190
+ export function readDurableModelProfileOwnershipFromRaw(
191
+ raw: Readonly<Record<string, unknown>>,
192
+ ): DurableModelProfileOwnership {
193
+ const configured = rawValueAtPath(raw, "modelProfile.ownership");
194
+ if (configured !== undefined) {
195
+ const validated = validateDurableModelProfileOwnership(configured);
196
+ if (!validated) throw new InvalidModelProfileOwnershipError();
197
+ assertLegacyDefaultMatchesOwnership(validated.marker, rawValueAtPath(raw, "modelProfile.default"));
198
+ return validated;
199
+ }
200
+ const legacyProfile = rawValueAtPath(raw, "modelProfile.default");
201
+ return {
202
+ schemaVersion: 1,
203
+ version: 0,
204
+ marker:
205
+ typeof legacyProfile === "string" && legacyProfile.trim() !== ""
206
+ ? { kind: "profile", profile: legacyProfile }
207
+ : { kind: "inherit" },
208
+ };
209
+ }
210
+
211
+ export function resolveEffectiveModelProfileMarker(
212
+ sessionMarker: ModelProfileOwnershipMarker | undefined,
213
+ durable: DurableModelProfileOwnership,
214
+ ): ModelProfileOwnershipMarker {
215
+ const session = sessionMarker ?? { kind: "inherit" as const };
216
+ return session.kind === "inherit" ? durable.marker : session;
217
+ }
218
+
219
+ export function resolveOwnedModelProfileName(
220
+ marker: ModelProfileOwnershipMarker,
221
+ profiles: ReadonlyMap<string, unknown>,
222
+ ): string | undefined {
223
+ if (marker.kind !== "profile") return undefined;
224
+ const resolvedName = resolveModelProfileName(marker.profile, profiles);
225
+ if (!profiles.has(resolvedName)) throw new UnresolvedModelProfileOwnershipError(marker.profile);
226
+ return resolvedName;
227
+ }
228
+
229
+ export function nextDurableModelProfileOwnership(
230
+ current: DurableModelProfileOwnership,
231
+ marker: ModelProfileOwnershipMarker,
232
+ ): DurableModelProfileOwnership {
233
+ if (current.version === Number.MAX_SAFE_INTEGER)
234
+ throw new RangeError("Model-profile ownership version is exhausted.");
235
+ return { schemaVersion: 1, version: current.version + 1, marker };
236
+ }
237
+
238
+ export async function commitDurableModelProfileOwnershipWithResult(
239
+ settings: DurableModelProfileOwnershipStore,
240
+ marker: ModelProfileOwnershipMarker,
241
+ extraPatches: readonly SettingsAtomicPatch[] = [],
242
+ validateCurrent?: () => Promise<void> | void,
243
+ observedOwnership?: DurableModelProfileOwnership,
244
+ ): Promise<DurableModelProfileOwnershipCommit> {
245
+ const requestedMarker = validateModelProfileOwnershipMarker(marker);
246
+ if (!requestedMarker) throw new InvalidModelProfileOwnershipError();
247
+ if (extraPatches.some(patch => OWNERSHIP_PROJECTION_PATHS.has(patch.path))) {
248
+ throw new Error("Durable ownership extra patches cannot target modelProfile.ownership or modelProfile.default.");
249
+ }
250
+ const expected = observedOwnership ?? readDurableModelProfileOwnership(settings);
251
+ let result = expected;
252
+ let wrote = false;
253
+ await settings.commitAtomicBatchWithCurrent(async current => {
254
+ const actual = readDurableModelProfileOwnershipFromRaw(current);
255
+ if (actual.version !== expected.version || !markersEqual(actual.marker, expected.marker)) {
256
+ throw new ModelProfileOwnershipConflictError(expected.version, actual.version, actual.marker);
257
+ }
258
+ await validateCurrent?.();
259
+ if (
260
+ actual.version > 0 &&
261
+ markersEqual(actual.marker, requestedMarker) &&
262
+ extraPatchesAlreadyApplied(current, extraPatches)
263
+ ) {
264
+ result = actual;
265
+ return [];
266
+ }
267
+ result = nextDurableModelProfileOwnership(actual, requestedMarker);
268
+ wrote = true;
269
+ return [
270
+ { path: "modelProfile.ownership", op: "set", value: result },
271
+ requestedMarker.kind === "profile"
272
+ ? { path: "modelProfile.default", op: "set", value: requestedMarker.profile }
273
+ : { path: "modelProfile.default", op: "unset" },
274
+ ...extraPatches,
275
+ ];
276
+ });
277
+ return { ownership: result, wrote };
278
+ }
279
+
280
+ export async function commitDurableModelProfileOwnership(
281
+ settings: DurableModelProfileOwnershipStore,
282
+ marker: ModelProfileOwnershipMarker,
283
+ extraPatches: readonly SettingsAtomicPatch[] = [],
284
+ validateCurrent?: () => Promise<void> | void,
285
+ observedOwnership?: DurableModelProfileOwnership,
286
+ ): Promise<DurableModelProfileOwnership> {
287
+ return (
288
+ await commitDurableModelProfileOwnershipWithResult(
289
+ settings,
290
+ marker,
291
+ extraPatches,
292
+ validateCurrent,
293
+ observedOwnership,
294
+ )
295
+ ).ownership;
296
+ }
297
+
298
+ export function modelProfileOwnershipMarkersEqual(
299
+ left: ModelProfileOwnershipMarker | undefined,
300
+ right: ModelProfileOwnershipMarker | undefined,
301
+ ): boolean {
302
+ return markersEqual(left ?? { kind: "inherit" }, right ?? { kind: "inherit" });
303
+ }
@@ -87,6 +87,11 @@ import {
87
87
  refreshModelPresetRegistry,
88
88
  refreshModelPresetRegistryInBackground,
89
89
  } from "./model-preset-registry";
90
+ import {
91
+ ModelProfileReplacementRequiredError,
92
+ readDurableModelProfileOwnershipFromRaw,
93
+ UnresolvedModelProfileOwnershipError,
94
+ } from "./model-profile-ownership";
90
95
 
91
96
  export type { ProviderDiscoveryState, ProviderDiscoveryStatus } from "./model-discovery-manager";
92
97
 
@@ -1747,6 +1752,7 @@ export class ModelRegistry {
1747
1752
  #configError: ConfigError | undefined = undefined;
1748
1753
  #modelsConfigFile: ConfigFile<ModelsConfig>;
1749
1754
  #settings: Pick<Settings, "get" | "getGlobal">;
1755
+ #ownershipSettings: Pick<Settings, "commitAtomicBatchWithCurrent"> | undefined;
1750
1756
  readonly #authStorageConfigOwner: object = {};
1751
1757
  #disposeAuthStorageFallbackResolver: (() => void) | undefined;
1752
1758
  #lastStaticLoadMtime: number | null = null;
@@ -1795,10 +1801,15 @@ export class ModelRegistry {
1795
1801
  constructor(
1796
1802
  readonly authStorage: AuthStorage,
1797
1803
  modelsPath?: string,
1798
- registrySettings?: Pick<Settings, "get" | "getGlobal">,
1804
+ registrySettings?: Pick<Settings, "get" | "getGlobal"> & Partial<Pick<Settings, "commitAtomicBatchWithCurrent">>,
1799
1805
  modelPresetRegistryDependencies: ModelPresetRegistryDependencies = {},
1800
1806
  ) {
1801
1807
  this.#settings = registrySettings ?? settings;
1808
+ this.#ownershipSettings = registrySettings
1809
+ ? typeof registrySettings.commitAtomicBatchWithCurrent === "function"
1810
+ ? (registrySettings as Pick<Settings, "commitAtomicBatchWithCurrent">)
1811
+ : undefined
1812
+ : settings;
1802
1813
  const configuredAgentDir = path.resolve(modelPresetRegistryDependencies.agentDir ?? getAgentDir());
1803
1814
  this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
1804
1815
  this.#modelPresetRegistryAgentDir = modelPresetRegistryDependencies.agentDir
@@ -3005,6 +3016,18 @@ export class ModelRegistry {
3005
3016
  return new Map(this.#modelProfiles);
3006
3017
  }
3007
3018
 
3019
+ assertCurrentModelProfileExists(name: string): void {
3020
+ const loaded = this.#modelsConfigFile.tryLoad();
3021
+ if (loaded.status === "error") {
3022
+ throw new Error(
3023
+ `Cannot validate durable model-profile ownership because ${this.#modelsConfigFile.path()} is invalid.`,
3024
+ );
3025
+ }
3026
+ if (mergeModelProfiles(loaded.value?.profiles).has(name)) return;
3027
+ if (this.#modelProfiles.get(name)?.source === "registry") return;
3028
+ throw new UnresolvedModelProfileOwnershipError(name);
3029
+ }
3030
+
3008
3031
  getModelProfile(name: string): ModelProfileDefinition | undefined {
3009
3032
  return this.#modelProfiles.get(name);
3010
3033
  }
@@ -3090,20 +3113,36 @@ export class ModelRegistry {
3090
3113
  async deleteCustomModelProfile(name: string): Promise<ModelProfileConfig> {
3091
3114
  const normalizedName = name.trim();
3092
3115
  if (!normalizedName) throw new Error("Profile name is required.");
3093
- const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "delete");
3094
- const nextProfiles = { ...(current.profiles ?? {}) };
3095
- delete nextProfiles[normalizedName];
3096
- const checkedConfig = ModelsConfigSchema.safeParse({
3097
- ...current,
3098
- profiles: Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined,
3116
+ const ownershipSettings = this.#ownershipSettings;
3117
+ if (!ownershipSettings) {
3118
+ throw new Error("Profile deletion requires a settings store with ownership transaction support.");
3119
+ }
3120
+ let deletedProfile: ModelProfileConfig | undefined;
3121
+ await ownershipSettings.commitAtomicBatchWithCurrent(async currentSettings => {
3122
+ const durableOwnership = readDurableModelProfileOwnershipFromRaw(currentSettings);
3123
+ if (durableOwnership.marker.kind === "profile" && durableOwnership.marker.profile === normalizedName) {
3124
+ throw new ModelProfileReplacementRequiredError(normalizedName);
3125
+ }
3126
+ const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "delete");
3127
+ const nextProfiles = { ...(current.profiles ?? {}) };
3128
+ delete nextProfiles[normalizedName];
3129
+ const checkedConfig = ModelsConfigSchema.safeParse({
3130
+ ...current,
3131
+ profiles: Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined,
3132
+ });
3133
+ if (!checkedConfig.success) {
3134
+ const first = checkedConfig.error.issues[0];
3135
+ const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
3136
+ throw new Error(
3137
+ `Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`,
3138
+ );
3139
+ }
3140
+ await this.#writeCheckedModelsConfig(checkedConfig.data);
3141
+ deletedProfile = profile;
3142
+ return [];
3099
3143
  });
3100
- if (!checkedConfig.success) {
3101
- const first = checkedConfig.error.issues[0];
3102
- const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
3103
- throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
3104
- }
3105
- await this.#writeCheckedModelsConfig(checkedConfig.data);
3106
- return profile;
3144
+ if (!deletedProfile) throw new Error(`Model profile "${normalizedName}" was not deleted.`);
3145
+ return deletedProfile;
3107
3146
  }
3108
3147
 
3109
3148
  #loadCustomProfileForMutation(
@@ -16,6 +16,7 @@ import {
16
16
  validateAutoroutingProvenance,
17
17
  validateAutoroutingSetup,
18
18
  } from "./autorouting-contract";
19
+ import { type DurableModelProfileOwnership, validateDurableModelProfileOwnership } from "./model-profile-ownership";
19
20
  import type { ModelSelectorValue } from "./model-selector-value";
20
21
  import { UPDATE_CHANNELS } from "./update-channel";
21
22
 
@@ -369,6 +370,42 @@ const AUTOROUTING_PROVENANCE_JSON_SCHEMA: JsonSchemaObject = {
369
370
  required: ["schema", "source", "declarationFingerprint", "tiersFingerprint"],
370
371
  };
371
372
 
373
+ const MODEL_PROFILE_OWNERSHIP_JSON_SCHEMA: JsonSchemaObject = {
374
+ type: "object",
375
+ properties: {
376
+ schemaVersion: { type: "integer", const: 1 },
377
+ version: { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
378
+ marker: {
379
+ type: "object",
380
+ oneOf: [
381
+ {
382
+ type: "object",
383
+ properties: { kind: { type: "string", const: "inherit" } },
384
+ required: ["kind"],
385
+ additionalProperties: false,
386
+ },
387
+ {
388
+ type: "object",
389
+ properties: { kind: { type: "string", const: "cleared" } },
390
+ required: ["kind"],
391
+ additionalProperties: false,
392
+ },
393
+ {
394
+ type: "object",
395
+ properties: {
396
+ kind: { type: "string", const: "profile" },
397
+ profile: { type: "string", minLength: 1 },
398
+ },
399
+ required: ["kind", "profile"],
400
+ additionalProperties: false,
401
+ },
402
+ ],
403
+ },
404
+ },
405
+ required: ["schemaVersion", "version", "marker"],
406
+ additionalProperties: false,
407
+ };
408
+
372
409
  // SDK prompt deadline defaults. The `sdk.promptDeadlineMs` /
373
410
  // `sdk.promptMaxRuntimeMs` schema entries below and the SDK bus/host lease
374
411
  // fallbacks read these same constants, so a Settings lookup that misses — no
@@ -684,6 +721,23 @@ export const SETTINGS_SCHEMA = {
684
721
  options: "runtime",
685
722
  },
686
723
  },
724
+ "modelProfile.ownership": {
725
+ type: "optional-object",
726
+ default: undefined,
727
+ jsonSchema: MODEL_PROFILE_OWNERSHIP_JSON_SCHEMA,
728
+ validate: (value: unknown) =>
729
+ validateDurableModelProfileOwnership(value)
730
+ ? []
731
+ : [
732
+ {
733
+ path: "modelProfile.ownership",
734
+ code: "config_invalid",
735
+ reason: "config_invalid",
736
+ detail: "Expected a versioned ownership record with a valid inherit, cleared, or profile marker.",
737
+ },
738
+ ],
739
+ _value: undefined as unknown as DurableModelProfileOwnership,
740
+ },
687
741
  "modelProfile.proxyProvider": {
688
742
  type: "string",
689
743
  default: undefined,
@@ -3636,6 +3690,25 @@ export const SETTINGS_SCHEMA = {
3636
3690
  "Encourage the agent to delegate work to subagents unless changes are trivial (on by default when executor/planner run on a different provider than the default role)",
3637
3691
  },
3638
3692
  },
3693
+ "task.delegationHint.mode": {
3694
+ type: "enum",
3695
+ values: ["off", "hint"] as const,
3696
+ default: "off",
3697
+ ui: {
3698
+ tab: "tasks",
3699
+ label: "Delegation Decision Hint",
3700
+ description:
3701
+ "Opt in to transient local-only Kev advice after observable edit activity. Notices never alter prompts, task arguments, or saved sessions.",
3702
+ options: [
3703
+ { value: "off", label: "Off", description: "Do not request decision hints (default)" },
3704
+ {
3705
+ value: "hint",
3706
+ label: "Hint",
3707
+ description: "Show ephemeral user-facing advice from the local Kev service",
3708
+ },
3709
+ ],
3710
+ },
3711
+ },
3639
3712
 
3640
3713
  "task.simple": {
3641
3714
  type: "enum",
@@ -4303,6 +4376,14 @@ export function validateSettingPatch(patch: Record<string, unknown>): Array<{ pa
4303
4376
  const issues: Array<{ path: string; detail: string }> = [];
4304
4377
  const knownPaths = new Set(Object.keys(SETTINGS_SCHEMA));
4305
4378
  for (const [path, value] of Object.entries(patch)) {
4379
+ // Keep the legacy projection writable until every durable writer uses ownership CAS.
4380
+ if (path === "modelProfile.ownership") {
4381
+ issues.push({
4382
+ path,
4383
+ detail: "The versioned model-profile ownership record is internal and cannot be patched generically.",
4384
+ });
4385
+ continue;
4386
+ }
4306
4387
  const definition = SETTINGS_SCHEMA[path as SettingPath];
4307
4388
  if (!definition) {
4308
4389
  const recordParent = [...knownPaths].find(known => known !== path && path.startsWith(`${known}.`));