@gajae-code/coding-agent 0.11.11 → 0.12.0
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 +49 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +22 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +8 -0
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +15 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +44 -10
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +25 -13
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/skill-state/active-state.d.ts +0 -4
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/package.json +9 -13
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +8 -0
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/team.ts +1 -1
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +23 -1
- package/src/config/settings.ts +246 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/types.ts +28 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +742 -85
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +34 -5
- package/src/gjc-runtime/team-runtime.ts +970 -33
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
- package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
- package/src/gjc-runtime/workflow-manifest.ts +34 -179
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +8 -9
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +47 -0
- package/src/modes/acp/acp-agent.ts +43 -7
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +19 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +19 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +341 -38
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +13 -1
- package/src/session/agent-session.ts +466 -50
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +764 -17
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +0 -10
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +65 -7
- package/src/tools/ask.ts +89 -69
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1542 -310
- package/src/tools/resource-gc.ts +182 -18
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
package/src/config/settings.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import * as fs from "node:fs";
|
|
14
14
|
import * as os from "node:os";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
+
import * as util from "node:util";
|
|
16
17
|
import {
|
|
17
18
|
getAgentDbPath,
|
|
18
19
|
getAgentDir,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
atomicYamlPathHash,
|
|
43
44
|
type CasReceipt,
|
|
44
45
|
deleteByPath,
|
|
46
|
+
enqueueAtomicYamlOperation,
|
|
45
47
|
reserveAtomicYamlUpdateSlot,
|
|
46
48
|
setByPath,
|
|
47
49
|
} from "./atomic-yaml-patch";
|
|
@@ -374,6 +376,8 @@ export class Settings implements NotificationSettingsReader {
|
|
|
374
376
|
/** A newer config schema must never be rewritten by legacy migrations. */
|
|
375
377
|
#futureSchemaVersion = false;
|
|
376
378
|
#hasMalformedConfigRoot = false;
|
|
379
|
+
/** YAML syntax was unrecoverable, so the loaded defaults are read-only until config.yml is repaired. */
|
|
380
|
+
#hasRecoveredConfigSyntax = false;
|
|
377
381
|
#hasInvalidNotificationConfiguration = false;
|
|
378
382
|
#notificationValidationGeneration = 0;
|
|
379
383
|
/** Notification subtree fingerprint from the last raw durable config read. */
|
|
@@ -385,7 +389,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
385
389
|
private constructor(options: SettingsOptions = {}) {
|
|
386
390
|
this.#cwd = path.normalize(options.cwd ?? getProjectDir());
|
|
387
391
|
this.#agentDir = path.normalize(options.agentDir ?? getAgentDir());
|
|
388
|
-
this.#configPath = options.inMemory ? null : path.
|
|
392
|
+
this.#configPath = options.inMemory ? null : path.resolve(this.#agentDir, "config.yml");
|
|
389
393
|
this.#persist = !options.inMemory;
|
|
390
394
|
|
|
391
395
|
if (options.overrides) {
|
|
@@ -528,6 +532,11 @@ export class Settings implements NotificationSettingsReader {
|
|
|
528
532
|
return () => this.#changeListeners.delete(listener);
|
|
529
533
|
}
|
|
530
534
|
|
|
535
|
+
/** Whether durable settings mutations are permitted for the loaded configuration. */
|
|
536
|
+
canWriteDurableConfig(): boolean {
|
|
537
|
+
return !this.#persist || !this.#hasRecoveredConfigSyntax;
|
|
538
|
+
}
|
|
539
|
+
|
|
531
540
|
/**
|
|
532
541
|
* Set a setting value (sync).
|
|
533
542
|
* Updates global settings and reserves its background persistence slot before
|
|
@@ -538,6 +547,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
538
547
|
this.unset(path);
|
|
539
548
|
return;
|
|
540
549
|
}
|
|
550
|
+
this.#assertDurableConfigWritable();
|
|
541
551
|
this.#set(path, value, true);
|
|
542
552
|
}
|
|
543
553
|
|
|
@@ -569,6 +579,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
569
579
|
* `undefined` value. Defaults/project settings become visible immediately.
|
|
570
580
|
*/
|
|
571
581
|
unset<P extends SettingPath>(path: P): void {
|
|
582
|
+
this.#assertDurableConfigWritable();
|
|
572
583
|
const prev = this.get(path);
|
|
573
584
|
const patch: SettingsPatch = {
|
|
574
585
|
path,
|
|
@@ -594,6 +605,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
594
605
|
* {@link set}, canonical state and hooks change only after the rename succeeds.
|
|
595
606
|
*/
|
|
596
607
|
async commitAtomicBatch(patches: readonly SettingsAtomicPatch[]): Promise<CasReceipt> {
|
|
608
|
+
this.#assertDurableConfigWritable();
|
|
597
609
|
if (!this.#persist || !this.#configPath) {
|
|
598
610
|
const notificationValidationGuard = this.#notificationValidationRestoreGuard();
|
|
599
611
|
const changes = new Map<string, { before: unknown; beforeHash: string; afterHash: string }>();
|
|
@@ -694,13 +706,16 @@ export class Settings implements NotificationSettingsReader {
|
|
|
694
706
|
}));
|
|
695
707
|
for (const entry of revisions) this.#pathRevisions.set(entry.patch.path, entry.revision);
|
|
696
708
|
|
|
709
|
+
const commit = applyAtomicYamlPatches(this.#configPath, durablePatches, {
|
|
710
|
+
validateRoot: (root, currentPatches) =>
|
|
711
|
+
this.#rejectAtomicNotificationRepairForMalformedRoot(currentPatches, root),
|
|
712
|
+
onRestored: restoredPatches =>
|
|
713
|
+
this.#applyRestoredDurableBatch(revisions, restoredPatches, notificationValidationGuard),
|
|
714
|
+
});
|
|
715
|
+
const failureRefresh = this.#reserveAtomicFailureRefresh(commit);
|
|
697
716
|
try {
|
|
698
|
-
const receipt = await
|
|
699
|
-
|
|
700
|
-
this.#rejectAtomicNotificationRepairForMalformedRoot(currentPatches, root),
|
|
701
|
-
onRestored: restoredPatches =>
|
|
702
|
-
this.#applyRestoredDurableBatch(revisions, restoredPatches, notificationValidationGuard),
|
|
703
|
-
});
|
|
717
|
+
const receipt = await commit;
|
|
718
|
+
await failureRefresh;
|
|
704
719
|
const appliedNotificationMutation = this.#applyDurableBatch(revisions);
|
|
705
720
|
this.#recordNotificationValidationBatchApply(notificationValidationGuard, appliedNotificationMutation);
|
|
706
721
|
return receipt;
|
|
@@ -711,6 +726,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
711
726
|
else this.#pathRevisions.set(entry.patch.path, entry.previousRevision);
|
|
712
727
|
}
|
|
713
728
|
}
|
|
729
|
+
await failureRefresh;
|
|
714
730
|
if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
|
|
715
731
|
throw error;
|
|
716
732
|
}
|
|
@@ -722,6 +738,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
722
738
|
current: Readonly<RawSettings>,
|
|
723
739
|
) => Promise<readonly SettingsAtomicPatch[]> | readonly SettingsAtomicPatch[],
|
|
724
740
|
): Promise<CasReceipt> {
|
|
741
|
+
this.#assertDurableConfigWritable();
|
|
725
742
|
if (!this.#persist || !this.#configPath) {
|
|
726
743
|
const patches = await buildPatches(structuredClone(this.#global));
|
|
727
744
|
return this.commitAtomicBatch(patches);
|
|
@@ -730,38 +747,41 @@ export class Settings implements NotificationSettingsReader {
|
|
|
730
747
|
this.#releasePendingSaveSlot();
|
|
731
748
|
let revisions: DurableBatchRevision[] = [];
|
|
732
749
|
const notificationValidationGuard = this.#notificationValidationRestoreGuard();
|
|
750
|
+
const commit = applyAtomicYamlPatchesWithCurrent(
|
|
751
|
+
this.#configPath,
|
|
752
|
+
async current => {
|
|
753
|
+
const patches = await buildPatches(structuredClone(current));
|
|
754
|
+
const durablePatches: AtomicYamlPatch[] = patches.map(patch => {
|
|
755
|
+
if (!isAtomicSettingsPath(patch.path)) {
|
|
756
|
+
throw new Error(`Unknown setting path for atomic batch: ${patch.path}`);
|
|
757
|
+
}
|
|
758
|
+
if (patch.op === "unset") return { path: patch.path, op: "unset" };
|
|
759
|
+
if (patch.value === undefined) {
|
|
760
|
+
throw new TypeError(
|
|
761
|
+
`Settings set patch for ${patch.path} cannot carry undefined; use unset instead.`,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
return { path: patch.path, op: "set", value: structuredClone(patch.value) };
|
|
765
|
+
});
|
|
766
|
+
revisions = durablePatches.map(patch => ({
|
|
767
|
+
patch,
|
|
768
|
+
revision: ++this.#nextRevision,
|
|
769
|
+
previousRevision: this.#pathRevisions.get(patch.path),
|
|
770
|
+
}));
|
|
771
|
+
for (const entry of revisions) this.#pathRevisions.set(entry.patch.path, entry.revision);
|
|
772
|
+
return durablePatches;
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
validateRoot: (root, currentPatches) =>
|
|
776
|
+
this.#rejectAtomicNotificationRepairForMalformedRoot(currentPatches, root),
|
|
777
|
+
onRestored: restoredPatches =>
|
|
778
|
+
this.#applyRestoredDurableBatch(revisions, restoredPatches, notificationValidationGuard),
|
|
779
|
+
},
|
|
780
|
+
);
|
|
781
|
+
const failureRefresh = this.#reserveAtomicFailureRefresh(commit);
|
|
733
782
|
try {
|
|
734
|
-
const receipt = await
|
|
735
|
-
|
|
736
|
-
async current => {
|
|
737
|
-
const patches = await buildPatches(structuredClone(current));
|
|
738
|
-
const durablePatches: AtomicYamlPatch[] = patches.map(patch => {
|
|
739
|
-
if (!isAtomicSettingsPath(patch.path)) {
|
|
740
|
-
throw new Error(`Unknown setting path for atomic batch: ${patch.path}`);
|
|
741
|
-
}
|
|
742
|
-
if (patch.op === "unset") return { path: patch.path, op: "unset" };
|
|
743
|
-
if (patch.value === undefined) {
|
|
744
|
-
throw new TypeError(
|
|
745
|
-
`Settings set patch for ${patch.path} cannot carry undefined; use unset instead.`,
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
return { path: patch.path, op: "set", value: structuredClone(patch.value) };
|
|
749
|
-
});
|
|
750
|
-
revisions = durablePatches.map(patch => ({
|
|
751
|
-
patch,
|
|
752
|
-
revision: ++this.#nextRevision,
|
|
753
|
-
previousRevision: this.#pathRevisions.get(patch.path),
|
|
754
|
-
}));
|
|
755
|
-
for (const entry of revisions) this.#pathRevisions.set(entry.patch.path, entry.revision);
|
|
756
|
-
return durablePatches;
|
|
757
|
-
},
|
|
758
|
-
{
|
|
759
|
-
validateRoot: (root, currentPatches) =>
|
|
760
|
-
this.#rejectAtomicNotificationRepairForMalformedRoot(currentPatches, root),
|
|
761
|
-
onRestored: restoredPatches =>
|
|
762
|
-
this.#applyRestoredDurableBatch(revisions, restoredPatches, notificationValidationGuard),
|
|
763
|
-
},
|
|
764
|
-
);
|
|
783
|
+
const receipt = await commit;
|
|
784
|
+
await failureRefresh;
|
|
765
785
|
const appliedNotificationMutation = this.#applyDurableBatch(revisions);
|
|
766
786
|
this.#recordNotificationValidationBatchApply(notificationValidationGuard, appliedNotificationMutation);
|
|
767
787
|
return receipt;
|
|
@@ -772,6 +792,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
772
792
|
else this.#pathRevisions.set(entry.patch.path, entry.previousRevision);
|
|
773
793
|
}
|
|
774
794
|
}
|
|
795
|
+
await failureRefresh;
|
|
775
796
|
if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
|
|
776
797
|
throw error;
|
|
777
798
|
}
|
|
@@ -826,6 +847,15 @@ export class Settings implements NotificationSettingsReader {
|
|
|
826
847
|
}
|
|
827
848
|
}
|
|
828
849
|
await this.#refreshDurableSettings();
|
|
850
|
+
if (this.#modified.size > 0 && !this.#pendingSaveSlot) {
|
|
851
|
+
this.#queueSave();
|
|
852
|
+
this.#releasePendingSaveSlot();
|
|
853
|
+
try {
|
|
854
|
+
await this.#savePromise;
|
|
855
|
+
} catch {
|
|
856
|
+
// Keep dirty state for a later explicit flush or mutation.
|
|
857
|
+
}
|
|
858
|
+
}
|
|
829
859
|
}
|
|
830
860
|
|
|
831
861
|
/** Like {@link flush}, but reports a durable save failure to the caller. */
|
|
@@ -833,8 +863,20 @@ export class Settings implements NotificationSettingsReader {
|
|
|
833
863
|
this.#releasePendingSaveSlot();
|
|
834
864
|
if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
|
|
835
865
|
this.#releasePendingSaveSlot();
|
|
836
|
-
|
|
866
|
+
let saveError: unknown;
|
|
867
|
+
try {
|
|
868
|
+
await this.#savePromise;
|
|
869
|
+
} catch (error) {
|
|
870
|
+
saveError = error;
|
|
871
|
+
}
|
|
837
872
|
await this.#refreshDurableSettings();
|
|
873
|
+
if (this.#modified.size > 0 && !this.#pendingSaveSlot) {
|
|
874
|
+
this.#queueSave();
|
|
875
|
+
this.#releasePendingSaveSlot();
|
|
876
|
+
await this.#savePromise;
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (saveError !== undefined) throw saveError;
|
|
838
880
|
}
|
|
839
881
|
|
|
840
882
|
async cloneForCwd(cwd: string): Promise<Settings> {
|
|
@@ -848,15 +890,23 @@ export class Settings implements NotificationSettingsReader {
|
|
|
848
890
|
inMemory: !this.#persist,
|
|
849
891
|
});
|
|
850
892
|
cloned.#storage = this.#storage;
|
|
893
|
+
cloned.#schemaReport = structuredClone(this.#schemaReport);
|
|
894
|
+
cloned.#schemaMigrationPending = this.#schemaMigrationPending;
|
|
851
895
|
cloned.#futureSchemaVersion = this.#futureSchemaVersion;
|
|
852
|
-
|
|
896
|
+
cloned.#hasMalformedConfigRoot = this.#hasMalformedConfigRoot;
|
|
897
|
+
cloned.#hasRecoveredConfigSyntax = this.#hasRecoveredConfigSyntax;
|
|
898
|
+
cloned.#hasInvalidNotificationConfiguration = this.#hasInvalidNotificationConfiguration;
|
|
899
|
+
cloned.#notificationValidationGeneration = this.#notificationValidationGeneration;
|
|
853
900
|
cloned.#global = structuredClone(this.#global);
|
|
854
901
|
cloned.#rawNotificationConfig = structuredClone(this.#rawNotificationConfig);
|
|
855
902
|
cloned.#durableRawNotificationConfig = structuredClone(this.#durableRawNotificationConfig);
|
|
856
903
|
cloned.#durableNotificationFingerprint = this.#durableNotificationFingerprint;
|
|
857
904
|
cloned.#project = this.#persist ? await cloned.#loadProjectSettings() : structuredClone(this.#project);
|
|
858
905
|
cloned.#overrides = structuredClone(this.#overrides);
|
|
859
|
-
|
|
906
|
+
if (cloned.#hasRecoveredConfigSyntax) {
|
|
907
|
+
cloned.#sanitizeModelSelectorRecords();
|
|
908
|
+
cloned.#rebuildMerged();
|
|
909
|
+
} else await cloned.#normalizeAfterLoad();
|
|
860
910
|
cloned.#fireAllHooks();
|
|
861
911
|
return cloned;
|
|
862
912
|
}
|
|
@@ -948,6 +998,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
948
998
|
}
|
|
949
999
|
|
|
950
1000
|
setGlobalModelRole(role: ModelRole | string, modelId: ModelSelectorValue | undefined): void {
|
|
1001
|
+
this.#assertDurableConfigWritable();
|
|
951
1002
|
const revision = ++this.#nextRevision;
|
|
952
1003
|
const patch: SettingsPatch = {
|
|
953
1004
|
path: "modelRoles",
|
|
@@ -981,13 +1032,27 @@ export class Settings implements NotificationSettingsReader {
|
|
|
981
1032
|
}
|
|
982
1033
|
|
|
983
1034
|
#replaceGlobalWithDurable(current: RawSettings): void {
|
|
1035
|
+
const previous = new Map<SettingPath, unknown>();
|
|
1036
|
+
for (const settingPath of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
|
|
1037
|
+
previous.set(settingPath, structuredClone(this.get(settingPath)));
|
|
1038
|
+
}
|
|
984
1039
|
this.#global = current;
|
|
985
1040
|
for (const patch of this.#pendingPatchesInGenerationOrder()) {
|
|
986
1041
|
applySettingsPatch(this.#global, { ...patch, value: structuredClone(patch.value) });
|
|
987
|
-
this.#
|
|
1042
|
+
if (this.#rawNotificationConfig !== undefined) {
|
|
1043
|
+
this.#applyNotificationMutationToRaw(patch.path, patch.value);
|
|
1044
|
+
}
|
|
988
1045
|
}
|
|
989
1046
|
this.#rebuildMerged();
|
|
990
1047
|
this.#recomputeNotificationValidationFromRaw();
|
|
1048
|
+
for (const settingPath of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
|
|
1049
|
+
const previousValue = previous.get(settingPath);
|
|
1050
|
+
const nextValue = this.get(settingPath);
|
|
1051
|
+
if (util.isDeepStrictEqual(previousValue, nextValue)) continue;
|
|
1052
|
+
const hook = SETTING_HOOKS[settingPath];
|
|
1053
|
+
if (hook) hook(nextValue, previousValue);
|
|
1054
|
+
for (const listener of this.#changeListeners) listener(settingPath);
|
|
1055
|
+
}
|
|
991
1056
|
}
|
|
992
1057
|
/**
|
|
993
1058
|
* Set an agent model override while keeping any live runtime override aligned.
|
|
@@ -1081,60 +1146,97 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1081
1146
|
}
|
|
1082
1147
|
}
|
|
1083
1148
|
|
|
1084
|
-
|
|
1149
|
+
#resetYamlLoadState(): void {
|
|
1085
1150
|
this.#hasMalformedConfigRoot = false;
|
|
1151
|
+
this.#hasRecoveredConfigSyntax = false;
|
|
1086
1152
|
this.#hasInvalidNotificationConfiguration = false;
|
|
1153
|
+
this.#schemaReport = { issues: [], valid: true };
|
|
1154
|
+
this.#schemaMigrationPending = false;
|
|
1155
|
+
this.#futureSchemaVersion = false;
|
|
1087
1156
|
this.#captureRawNotificationConfig({});
|
|
1088
|
-
|
|
1089
|
-
const content = await Bun.file(filePath).text();
|
|
1090
|
-
const parsed = YAML.parse(content);
|
|
1091
|
-
if (parsed === undefined) return {};
|
|
1092
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1093
|
-
this.#hasMalformedConfigRoot = true;
|
|
1094
|
-
this.#captureRawNotificationConfig(undefined);
|
|
1095
|
-
return {};
|
|
1096
|
-
}
|
|
1097
|
-
const parsedRaw = parsed as RawSettings;
|
|
1098
|
-
if (filePath === this.#configPath) this.#captureRawNotificationConfig(parsedRaw);
|
|
1099
|
-
if (filePath === this.#configPath) {
|
|
1100
|
-
try {
|
|
1101
|
-
parseNotificationSettingsSnapshot(parsedRaw);
|
|
1102
|
-
} catch (error) {
|
|
1103
|
-
if (!(error instanceof Error) || error.message !== "gjc_notify_daemon_invalid_configuration")
|
|
1104
|
-
throw error;
|
|
1105
|
-
this.#hasInvalidNotificationConfiguration = true;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
this.#futureSchemaVersion =
|
|
1109
|
-
filePath === this.#configPath &&
|
|
1110
|
-
typeof parsedRaw.configSchemaVersion === "number" &&
|
|
1111
|
-
parsedRaw.configSchemaVersion > CONFIG_SCHEMA_VERSION;
|
|
1157
|
+
}
|
|
1112
1158
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
) {
|
|
1118
|
-
this.#schemaMigrationPending = true;
|
|
1119
|
-
}
|
|
1120
|
-
const migrated = this.#migrateRawSettings(parsedRaw);
|
|
1121
|
-
const reconciled = reconcileSettingsSchema(migrated);
|
|
1122
|
-
if (typeof configSchemaVersion === "number" && configSchemaVersion > CONFIG_SCHEMA_VERSION) {
|
|
1123
|
-
reconciled.report.issues.push({
|
|
1124
|
-
path: "configSchemaVersion",
|
|
1125
|
-
kind: "pending-migration",
|
|
1126
|
-
detail: `Configuration requires schema version ${configSchemaVersion}.`,
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
this.#schemaReport = reconciled.report;
|
|
1130
|
-
return reconciled.settings;
|
|
1159
|
+
async #loadYaml(filePath: string): Promise<RawSettings> {
|
|
1160
|
+
let content: string;
|
|
1161
|
+
try {
|
|
1162
|
+
content = await Bun.file(filePath).text();
|
|
1131
1163
|
} catch (error) {
|
|
1132
1164
|
if (isEnoent(error)) {
|
|
1133
|
-
this.#
|
|
1165
|
+
this.#resetYamlLoadState();
|
|
1134
1166
|
return {};
|
|
1135
1167
|
}
|
|
1136
1168
|
throw error;
|
|
1137
1169
|
}
|
|
1170
|
+
this.#resetYamlLoadState();
|
|
1171
|
+
if (content.trim() === "") return {};
|
|
1172
|
+
let parsed: unknown;
|
|
1173
|
+
try {
|
|
1174
|
+
parsed = YAML.parse(content);
|
|
1175
|
+
} catch {
|
|
1176
|
+
this.#hasRecoveredConfigSyntax = true;
|
|
1177
|
+
this.#hasMalformedConfigRoot = true;
|
|
1178
|
+
this.#schemaReport = {
|
|
1179
|
+
valid: false,
|
|
1180
|
+
issues: [
|
|
1181
|
+
{
|
|
1182
|
+
path: "config.yml",
|
|
1183
|
+
kind: "invalid",
|
|
1184
|
+
detail: "Configuration YAML syntax is invalid; repair config.yml before changing settings.",
|
|
1185
|
+
},
|
|
1186
|
+
],
|
|
1187
|
+
};
|
|
1188
|
+
this.#captureRawNotificationConfig(undefined);
|
|
1189
|
+
return {};
|
|
1190
|
+
}
|
|
1191
|
+
if (parsed === undefined) return {};
|
|
1192
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1193
|
+
this.#hasMalformedConfigRoot = true;
|
|
1194
|
+
this.#schemaReport = {
|
|
1195
|
+
valid: false,
|
|
1196
|
+
issues: [
|
|
1197
|
+
{
|
|
1198
|
+
path: "config.yml",
|
|
1199
|
+
kind: "invalid",
|
|
1200
|
+
detail: "Configuration root must be a YAML mapping.",
|
|
1201
|
+
},
|
|
1202
|
+
],
|
|
1203
|
+
};
|
|
1204
|
+
this.#captureRawNotificationConfig(undefined);
|
|
1205
|
+
return {};
|
|
1206
|
+
}
|
|
1207
|
+
const parsedRaw = parsed as RawSettings;
|
|
1208
|
+
if (filePath === this.#configPath) this.#captureRawNotificationConfig(parsedRaw);
|
|
1209
|
+
if (filePath === this.#configPath) {
|
|
1210
|
+
try {
|
|
1211
|
+
parseNotificationSettingsSnapshot(parsedRaw);
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
if (!(error instanceof Error) || error.message !== "gjc_notify_daemon_invalid_configuration") throw error;
|
|
1214
|
+
this.#hasInvalidNotificationConfiguration = true;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
this.#futureSchemaVersion =
|
|
1218
|
+
filePath === this.#configPath &&
|
|
1219
|
+
typeof parsedRaw.configSchemaVersion === "number" &&
|
|
1220
|
+
parsedRaw.configSchemaVersion > CONFIG_SCHEMA_VERSION;
|
|
1221
|
+
|
|
1222
|
+
const configSchemaVersion = parsedRaw.configSchemaVersion;
|
|
1223
|
+
if (
|
|
1224
|
+
filePath === this.#configPath &&
|
|
1225
|
+
(typeof configSchemaVersion !== "number" || configSchemaVersion < CONFIG_SCHEMA_VERSION)
|
|
1226
|
+
) {
|
|
1227
|
+
this.#schemaMigrationPending = true;
|
|
1228
|
+
}
|
|
1229
|
+
const migrated = this.#migrateRawSettings(parsedRaw);
|
|
1230
|
+
const reconciled = reconcileSettingsSchema(migrated);
|
|
1231
|
+
if (typeof configSchemaVersion === "number" && configSchemaVersion > CONFIG_SCHEMA_VERSION) {
|
|
1232
|
+
reconciled.report.issues.push({
|
|
1233
|
+
path: "configSchemaVersion",
|
|
1234
|
+
kind: "pending-migration",
|
|
1235
|
+
detail: `Configuration requires schema version ${configSchemaVersion}.`,
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
this.#schemaReport = reconciled.report;
|
|
1239
|
+
return reconciled.settings;
|
|
1138
1240
|
}
|
|
1139
1241
|
|
|
1140
1242
|
async #loadProjectSettings(): Promise<RawSettings> {
|
|
@@ -1527,7 +1629,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1527
1629
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1528
1630
|
|
|
1529
1631
|
#queueSave(): void {
|
|
1530
|
-
if (!this.#persist || !this.#configPath) return;
|
|
1632
|
+
if (!this.#persist || !this.#configPath || this.#hasRecoveredConfigSyntax) return;
|
|
1531
1633
|
|
|
1532
1634
|
const currentSlot = this.#pendingSaveSlot;
|
|
1533
1635
|
if (currentSlot && !currentSlot.captured && !currentSlot.released) {
|
|
@@ -1594,25 +1696,33 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1594
1696
|
this.#recomputeNotificationValidationFromRaw();
|
|
1595
1697
|
},
|
|
1596
1698
|
};
|
|
1597
|
-
})
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
}
|
|
1605
|
-
if (durableBeforeWrite) {
|
|
1606
|
-
this.#global = durableBeforeWrite;
|
|
1607
|
-
this.#captureRawNotificationConfig(durableBeforeWrite);
|
|
1608
|
-
for (const patch of this.#pendingPatchesInGenerationOrder()) {
|
|
1609
|
-
applySettingsPatch(this.#global, { ...patch, value: structuredClone(patch.value) });
|
|
1610
|
-
this.#applyNotificationMutationToRaw(patch.path, patch.value);
|
|
1699
|
+
})
|
|
1700
|
+
.then(() => undefined)
|
|
1701
|
+
.catch(async error => {
|
|
1702
|
+
logger.warn("Settings: background save failed", { error: String(error) });
|
|
1703
|
+
for (const patch of captured) {
|
|
1704
|
+
const key = settingsPatchKey(patch);
|
|
1705
|
+
if (this.#modified.get(key)?.generation === patch.generation) this.#modified.set(key, patch);
|
|
1611
1706
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1707
|
+
if (durableBeforeWrite) {
|
|
1708
|
+
this.#global = durableBeforeWrite;
|
|
1709
|
+
this.#captureRawNotificationConfig(durableBeforeWrite);
|
|
1710
|
+
for (const patch of this.#pendingPatchesInGenerationOrder()) {
|
|
1711
|
+
applySettingsPatch(this.#global, { ...patch, value: structuredClone(patch.value) });
|
|
1712
|
+
this.#applyNotificationMutationToRaw(patch.path, patch.value);
|
|
1713
|
+
}
|
|
1714
|
+
this.#rebuildMerged();
|
|
1715
|
+
this.#recomputeNotificationValidationFromRaw();
|
|
1716
|
+
}
|
|
1717
|
+
try {
|
|
1718
|
+
await this.#refreshDurableSettings();
|
|
1719
|
+
} catch (refreshError) {
|
|
1720
|
+
logger.warn("Settings: refresh after background save failure failed", { error: String(refreshError) });
|
|
1721
|
+
}
|
|
1722
|
+
throw error;
|
|
1723
|
+
});
|
|
1724
|
+
this.#savePromise = save;
|
|
1725
|
+
void save.catch(() => {});
|
|
1616
1726
|
this.#armSaveTimer(slot);
|
|
1617
1727
|
}
|
|
1618
1728
|
|
|
@@ -1716,13 +1826,41 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1716
1826
|
return applicable.some(patch => isNotificationSettingsPath(patch.path));
|
|
1717
1827
|
}
|
|
1718
1828
|
|
|
1719
|
-
|
|
1720
|
-
if (!this.#persist || !this.#configPath) return;
|
|
1829
|
+
#reserveAtomicFailureRefresh(commit: Promise<unknown>): Promise<void> {
|
|
1830
|
+
if (!this.#persist || !this.#configPath) return Promise.resolve();
|
|
1831
|
+
return enqueueAtomicYamlOperation(this.#configPath, async canonicalPath => {
|
|
1832
|
+
try {
|
|
1833
|
+
await commit;
|
|
1834
|
+
return;
|
|
1835
|
+
} catch {
|
|
1836
|
+
// The original commit error remains authoritative. Recovery failures
|
|
1837
|
+
// are diagnostic only and must not replace it.
|
|
1838
|
+
}
|
|
1839
|
+
try {
|
|
1840
|
+
await this.#refreshDurableSettingsUnderQueue(canonicalPath);
|
|
1841
|
+
} catch (refreshError) {
|
|
1842
|
+
logger.warn("Settings: refresh after atomic batch failure failed", { error: String(refreshError) });
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
async #refreshDurableSettingsUnderQueue(canonicalPath: string): Promise<void> {
|
|
1721
1847
|
const previousFingerprint = this.#durableNotificationFingerprint;
|
|
1722
|
-
const current = await this.#loadYaml(
|
|
1848
|
+
const current = await this.#loadYaml(canonicalPath);
|
|
1723
1849
|
if (previousFingerprint !== this.#durableNotificationFingerprint) this.#notificationValidationGeneration++;
|
|
1724
1850
|
this.#replaceGlobalWithDurable(current);
|
|
1725
1851
|
}
|
|
1852
|
+
async #refreshDurableSettings(): Promise<void> {
|
|
1853
|
+
if (!this.#persist || !this.#configPath) return;
|
|
1854
|
+
await enqueueAtomicYamlOperation(this.#configPath, canonicalPath =>
|
|
1855
|
+
this.#refreshDurableSettingsUnderQueue(canonicalPath),
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
#assertDurableConfigWritable(): void {
|
|
1859
|
+
if (this.canWriteDurableConfig()) return;
|
|
1860
|
+
throw new Error(
|
|
1861
|
+
"Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1726
1864
|
|
|
1727
1865
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1728
1866
|
// Utilities
|