@gajae-code/coding-agent 0.11.10 → 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 +86 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +8 -1
- 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-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -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 +86 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +43 -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/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -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 +22 -9
- 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/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -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/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- 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/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +18 -2
- 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/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
- package/dist/types/sdk/host/control/operations.d.ts +2 -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/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +51 -19
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +59 -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/task/executor.d.ts +6 -0
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -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 +23 -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 +4 -0
- package/scripts/run-test-manifest.ts +6 -4
- 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/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +49 -2
- 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/sdk.ts +91 -7
- package/src/commands/team.ts +1 -1
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +24 -0
- 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 +84 -3
- package/src/config/settings.ts +254 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
- 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 -921
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +979 -71
- package/src/gjc-runtime/state-runtime.ts +50 -12
- 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 +10 -11
- package/src/internal-urls/local-protocol.ts +23 -4
- 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 +597 -67
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- 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 +76 -5
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- 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/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +62 -6
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +127 -29
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +20 -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/agents/executor.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/lifecycle.ts +107 -7
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +503 -60
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
- package/src/sdk/bus/telegram-daemon.ts +531 -53
- package/src/sdk/host/control/dispatch.ts +7 -1
- package/src/sdk/host/control/operations.ts +2 -1
- package/src/sdk/host/host.ts +25 -3
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +76 -0
- package/src/sdk/protocol/operation-registry.ts +8 -2
- package/src/sdk/session.ts +25 -2
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +720 -190
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +40 -10
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +1372 -42
- 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/task/executor.ts +11 -3
- package/src/task/index.ts +2 -0
- package/src/task/types.ts +22 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- 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 +665 -125
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -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";
|
|
@@ -363,6 +365,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
363
365
|
/** Pending debounced ordinary save; its queue slot is reserved immediately. */
|
|
364
366
|
#saveTimer?: NodeJS.Timeout;
|
|
365
367
|
#savePromise?: Promise<void>;
|
|
368
|
+
#changeListeners = new Set<(path: SettingPath) => void>();
|
|
366
369
|
#pendingSaveSlot?: PendingSaveSlot;
|
|
367
370
|
|
|
368
371
|
/** Legacy fallback migration warnings emitted once per settings instance. */
|
|
@@ -373,6 +376,8 @@ export class Settings implements NotificationSettingsReader {
|
|
|
373
376
|
/** A newer config schema must never be rewritten by legacy migrations. */
|
|
374
377
|
#futureSchemaVersion = false;
|
|
375
378
|
#hasMalformedConfigRoot = false;
|
|
379
|
+
/** YAML syntax was unrecoverable, so the loaded defaults are read-only until config.yml is repaired. */
|
|
380
|
+
#hasRecoveredConfigSyntax = false;
|
|
376
381
|
#hasInvalidNotificationConfiguration = false;
|
|
377
382
|
#notificationValidationGeneration = 0;
|
|
378
383
|
/** Notification subtree fingerprint from the last raw durable config read. */
|
|
@@ -384,7 +389,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
384
389
|
private constructor(options: SettingsOptions = {}) {
|
|
385
390
|
this.#cwd = path.normalize(options.cwd ?? getProjectDir());
|
|
386
391
|
this.#agentDir = path.normalize(options.agentDir ?? getAgentDir());
|
|
387
|
-
this.#configPath = options.inMemory ? null : path.
|
|
392
|
+
this.#configPath = options.inMemory ? null : path.resolve(this.#agentDir, "config.yml");
|
|
388
393
|
this.#persist = !options.inMemory;
|
|
389
394
|
|
|
390
395
|
if (options.overrides) {
|
|
@@ -522,6 +527,16 @@ export class Settings implements NotificationSettingsReader {
|
|
|
522
527
|
return structuredClone(this.#schemaReport);
|
|
523
528
|
}
|
|
524
529
|
|
|
530
|
+
onChanged(listener: (path: SettingPath) => void): () => void {
|
|
531
|
+
this.#changeListeners.add(listener);
|
|
532
|
+
return () => this.#changeListeners.delete(listener);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Whether durable settings mutations are permitted for the loaded configuration. */
|
|
536
|
+
canWriteDurableConfig(): boolean {
|
|
537
|
+
return !this.#persist || !this.#hasRecoveredConfigSyntax;
|
|
538
|
+
}
|
|
539
|
+
|
|
525
540
|
/**
|
|
526
541
|
* Set a setting value (sync).
|
|
527
542
|
* Updates global settings and reserves its background persistence slot before
|
|
@@ -532,6 +547,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
532
547
|
this.unset(path);
|
|
533
548
|
return;
|
|
534
549
|
}
|
|
550
|
+
this.#assertDurableConfigWritable();
|
|
535
551
|
this.#set(path, value, true);
|
|
536
552
|
}
|
|
537
553
|
|
|
@@ -555,6 +571,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
555
571
|
|
|
556
572
|
const hook = SETTING_HOOKS[path];
|
|
557
573
|
if (hook) hook(value, prev);
|
|
574
|
+
for (const listener of this.#changeListeners) listener(path);
|
|
558
575
|
}
|
|
559
576
|
|
|
560
577
|
/**
|
|
@@ -562,6 +579,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
562
579
|
* `undefined` value. Defaults/project settings become visible immediately.
|
|
563
580
|
*/
|
|
564
581
|
unset<P extends SettingPath>(path: P): void {
|
|
582
|
+
this.#assertDurableConfigWritable();
|
|
565
583
|
const prev = this.get(path);
|
|
566
584
|
const patch: SettingsPatch = {
|
|
567
585
|
path,
|
|
@@ -579,6 +597,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
579
597
|
|
|
580
598
|
const hook = SETTING_HOOKS[path];
|
|
581
599
|
if (hook) hook(this.get(path), prev);
|
|
600
|
+
for (const listener of this.#changeListeners) listener(path);
|
|
582
601
|
}
|
|
583
602
|
|
|
584
603
|
/**
|
|
@@ -586,6 +605,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
586
605
|
* {@link set}, canonical state and hooks change only after the rename succeeds.
|
|
587
606
|
*/
|
|
588
607
|
async commitAtomicBatch(patches: readonly SettingsAtomicPatch[]): Promise<CasReceipt> {
|
|
608
|
+
this.#assertDurableConfigWritable();
|
|
589
609
|
if (!this.#persist || !this.#configPath) {
|
|
590
610
|
const notificationValidationGuard = this.#notificationValidationRestoreGuard();
|
|
591
611
|
const changes = new Map<string, { before: unknown; beforeHash: string; afterHash: string }>();
|
|
@@ -686,13 +706,16 @@ export class Settings implements NotificationSettingsReader {
|
|
|
686
706
|
}));
|
|
687
707
|
for (const entry of revisions) this.#pathRevisions.set(entry.patch.path, entry.revision);
|
|
688
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);
|
|
689
716
|
try {
|
|
690
|
-
const receipt = await
|
|
691
|
-
|
|
692
|
-
this.#rejectAtomicNotificationRepairForMalformedRoot(currentPatches, root),
|
|
693
|
-
onRestored: restoredPatches =>
|
|
694
|
-
this.#applyRestoredDurableBatch(revisions, restoredPatches, notificationValidationGuard),
|
|
695
|
-
});
|
|
717
|
+
const receipt = await commit;
|
|
718
|
+
await failureRefresh;
|
|
696
719
|
const appliedNotificationMutation = this.#applyDurableBatch(revisions);
|
|
697
720
|
this.#recordNotificationValidationBatchApply(notificationValidationGuard, appliedNotificationMutation);
|
|
698
721
|
return receipt;
|
|
@@ -703,6 +726,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
703
726
|
else this.#pathRevisions.set(entry.patch.path, entry.previousRevision);
|
|
704
727
|
}
|
|
705
728
|
}
|
|
729
|
+
await failureRefresh;
|
|
706
730
|
if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
|
|
707
731
|
throw error;
|
|
708
732
|
}
|
|
@@ -714,6 +738,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
714
738
|
current: Readonly<RawSettings>,
|
|
715
739
|
) => Promise<readonly SettingsAtomicPatch[]> | readonly SettingsAtomicPatch[],
|
|
716
740
|
): Promise<CasReceipt> {
|
|
741
|
+
this.#assertDurableConfigWritable();
|
|
717
742
|
if (!this.#persist || !this.#configPath) {
|
|
718
743
|
const patches = await buildPatches(structuredClone(this.#global));
|
|
719
744
|
return this.commitAtomicBatch(patches);
|
|
@@ -722,38 +747,41 @@ export class Settings implements NotificationSettingsReader {
|
|
|
722
747
|
this.#releasePendingSaveSlot();
|
|
723
748
|
let revisions: DurableBatchRevision[] = [];
|
|
724
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);
|
|
725
782
|
try {
|
|
726
|
-
const receipt = await
|
|
727
|
-
|
|
728
|
-
async current => {
|
|
729
|
-
const patches = await buildPatches(structuredClone(current));
|
|
730
|
-
const durablePatches: AtomicYamlPatch[] = patches.map(patch => {
|
|
731
|
-
if (!isAtomicSettingsPath(patch.path)) {
|
|
732
|
-
throw new Error(`Unknown setting path for atomic batch: ${patch.path}`);
|
|
733
|
-
}
|
|
734
|
-
if (patch.op === "unset") return { path: patch.path, op: "unset" };
|
|
735
|
-
if (patch.value === undefined) {
|
|
736
|
-
throw new TypeError(
|
|
737
|
-
`Settings set patch for ${patch.path} cannot carry undefined; use unset instead.`,
|
|
738
|
-
);
|
|
739
|
-
}
|
|
740
|
-
return { path: patch.path, op: "set", value: structuredClone(patch.value) };
|
|
741
|
-
});
|
|
742
|
-
revisions = durablePatches.map(patch => ({
|
|
743
|
-
patch,
|
|
744
|
-
revision: ++this.#nextRevision,
|
|
745
|
-
previousRevision: this.#pathRevisions.get(patch.path),
|
|
746
|
-
}));
|
|
747
|
-
for (const entry of revisions) this.#pathRevisions.set(entry.patch.path, entry.revision);
|
|
748
|
-
return durablePatches;
|
|
749
|
-
},
|
|
750
|
-
{
|
|
751
|
-
validateRoot: (root, currentPatches) =>
|
|
752
|
-
this.#rejectAtomicNotificationRepairForMalformedRoot(currentPatches, root),
|
|
753
|
-
onRestored: restoredPatches =>
|
|
754
|
-
this.#applyRestoredDurableBatch(revisions, restoredPatches, notificationValidationGuard),
|
|
755
|
-
},
|
|
756
|
-
);
|
|
783
|
+
const receipt = await commit;
|
|
784
|
+
await failureRefresh;
|
|
757
785
|
const appliedNotificationMutation = this.#applyDurableBatch(revisions);
|
|
758
786
|
this.#recordNotificationValidationBatchApply(notificationValidationGuard, appliedNotificationMutation);
|
|
759
787
|
return receipt;
|
|
@@ -764,6 +792,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
764
792
|
else this.#pathRevisions.set(entry.patch.path, entry.previousRevision);
|
|
765
793
|
}
|
|
766
794
|
}
|
|
795
|
+
await failureRefresh;
|
|
767
796
|
if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
|
|
768
797
|
throw error;
|
|
769
798
|
}
|
|
@@ -818,6 +847,15 @@ export class Settings implements NotificationSettingsReader {
|
|
|
818
847
|
}
|
|
819
848
|
}
|
|
820
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
|
+
}
|
|
821
859
|
}
|
|
822
860
|
|
|
823
861
|
/** Like {@link flush}, but reports a durable save failure to the caller. */
|
|
@@ -825,8 +863,20 @@ export class Settings implements NotificationSettingsReader {
|
|
|
825
863
|
this.#releasePendingSaveSlot();
|
|
826
864
|
if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
|
|
827
865
|
this.#releasePendingSaveSlot();
|
|
828
|
-
|
|
866
|
+
let saveError: unknown;
|
|
867
|
+
try {
|
|
868
|
+
await this.#savePromise;
|
|
869
|
+
} catch (error) {
|
|
870
|
+
saveError = error;
|
|
871
|
+
}
|
|
829
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;
|
|
830
880
|
}
|
|
831
881
|
|
|
832
882
|
async cloneForCwd(cwd: string): Promise<Settings> {
|
|
@@ -840,15 +890,23 @@ export class Settings implements NotificationSettingsReader {
|
|
|
840
890
|
inMemory: !this.#persist,
|
|
841
891
|
});
|
|
842
892
|
cloned.#storage = this.#storage;
|
|
893
|
+
cloned.#schemaReport = structuredClone(this.#schemaReport);
|
|
894
|
+
cloned.#schemaMigrationPending = this.#schemaMigrationPending;
|
|
843
895
|
cloned.#futureSchemaVersion = this.#futureSchemaVersion;
|
|
844
|
-
|
|
896
|
+
cloned.#hasMalformedConfigRoot = this.#hasMalformedConfigRoot;
|
|
897
|
+
cloned.#hasRecoveredConfigSyntax = this.#hasRecoveredConfigSyntax;
|
|
898
|
+
cloned.#hasInvalidNotificationConfiguration = this.#hasInvalidNotificationConfiguration;
|
|
899
|
+
cloned.#notificationValidationGeneration = this.#notificationValidationGeneration;
|
|
845
900
|
cloned.#global = structuredClone(this.#global);
|
|
846
901
|
cloned.#rawNotificationConfig = structuredClone(this.#rawNotificationConfig);
|
|
847
902
|
cloned.#durableRawNotificationConfig = structuredClone(this.#durableRawNotificationConfig);
|
|
848
903
|
cloned.#durableNotificationFingerprint = this.#durableNotificationFingerprint;
|
|
849
904
|
cloned.#project = this.#persist ? await cloned.#loadProjectSettings() : structuredClone(this.#project);
|
|
850
905
|
cloned.#overrides = structuredClone(this.#overrides);
|
|
851
|
-
|
|
906
|
+
if (cloned.#hasRecoveredConfigSyntax) {
|
|
907
|
+
cloned.#sanitizeModelSelectorRecords();
|
|
908
|
+
cloned.#rebuildMerged();
|
|
909
|
+
} else await cloned.#normalizeAfterLoad();
|
|
852
910
|
cloned.#fireAllHooks();
|
|
853
911
|
return cloned;
|
|
854
912
|
}
|
|
@@ -940,6 +998,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
940
998
|
}
|
|
941
999
|
|
|
942
1000
|
setGlobalModelRole(role: ModelRole | string, modelId: ModelSelectorValue | undefined): void {
|
|
1001
|
+
this.#assertDurableConfigWritable();
|
|
943
1002
|
const revision = ++this.#nextRevision;
|
|
944
1003
|
const patch: SettingsPatch = {
|
|
945
1004
|
path: "modelRoles",
|
|
@@ -973,13 +1032,27 @@ export class Settings implements NotificationSettingsReader {
|
|
|
973
1032
|
}
|
|
974
1033
|
|
|
975
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
|
+
}
|
|
976
1039
|
this.#global = current;
|
|
977
1040
|
for (const patch of this.#pendingPatchesInGenerationOrder()) {
|
|
978
1041
|
applySettingsPatch(this.#global, { ...patch, value: structuredClone(patch.value) });
|
|
979
|
-
this.#
|
|
1042
|
+
if (this.#rawNotificationConfig !== undefined) {
|
|
1043
|
+
this.#applyNotificationMutationToRaw(patch.path, patch.value);
|
|
1044
|
+
}
|
|
980
1045
|
}
|
|
981
1046
|
this.#rebuildMerged();
|
|
982
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
|
+
}
|
|
983
1056
|
}
|
|
984
1057
|
/**
|
|
985
1058
|
* Set an agent model override while keeping any live runtime override aligned.
|
|
@@ -1073,60 +1146,97 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1073
1146
|
}
|
|
1074
1147
|
}
|
|
1075
1148
|
|
|
1076
|
-
|
|
1149
|
+
#resetYamlLoadState(): void {
|
|
1077
1150
|
this.#hasMalformedConfigRoot = false;
|
|
1151
|
+
this.#hasRecoveredConfigSyntax = false;
|
|
1078
1152
|
this.#hasInvalidNotificationConfiguration = false;
|
|
1153
|
+
this.#schemaReport = { issues: [], valid: true };
|
|
1154
|
+
this.#schemaMigrationPending = false;
|
|
1155
|
+
this.#futureSchemaVersion = false;
|
|
1079
1156
|
this.#captureRawNotificationConfig({});
|
|
1080
|
-
|
|
1081
|
-
const content = await Bun.file(filePath).text();
|
|
1082
|
-
const parsed = YAML.parse(content);
|
|
1083
|
-
if (parsed === undefined) return {};
|
|
1084
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1085
|
-
this.#hasMalformedConfigRoot = true;
|
|
1086
|
-
this.#captureRawNotificationConfig(undefined);
|
|
1087
|
-
return {};
|
|
1088
|
-
}
|
|
1089
|
-
const parsedRaw = parsed as RawSettings;
|
|
1090
|
-
if (filePath === this.#configPath) this.#captureRawNotificationConfig(parsedRaw);
|
|
1091
|
-
if (filePath === this.#configPath) {
|
|
1092
|
-
try {
|
|
1093
|
-
parseNotificationSettingsSnapshot(parsedRaw);
|
|
1094
|
-
} catch (error) {
|
|
1095
|
-
if (!(error instanceof Error) || error.message !== "gjc_notify_daemon_invalid_configuration")
|
|
1096
|
-
throw error;
|
|
1097
|
-
this.#hasInvalidNotificationConfiguration = true;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
this.#futureSchemaVersion =
|
|
1101
|
-
filePath === this.#configPath &&
|
|
1102
|
-
typeof parsedRaw.configSchemaVersion === "number" &&
|
|
1103
|
-
parsedRaw.configSchemaVersion > CONFIG_SCHEMA_VERSION;
|
|
1157
|
+
}
|
|
1104
1158
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
) {
|
|
1110
|
-
this.#schemaMigrationPending = true;
|
|
1111
|
-
}
|
|
1112
|
-
const migrated = this.#migrateRawSettings(parsedRaw);
|
|
1113
|
-
const reconciled = reconcileSettingsSchema(migrated);
|
|
1114
|
-
if (typeof configSchemaVersion === "number" && configSchemaVersion > CONFIG_SCHEMA_VERSION) {
|
|
1115
|
-
reconciled.report.issues.push({
|
|
1116
|
-
path: "configSchemaVersion",
|
|
1117
|
-
kind: "pending-migration",
|
|
1118
|
-
detail: `Configuration requires schema version ${configSchemaVersion}.`,
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
this.#schemaReport = reconciled.report;
|
|
1122
|
-
return reconciled.settings;
|
|
1159
|
+
async #loadYaml(filePath: string): Promise<RawSettings> {
|
|
1160
|
+
let content: string;
|
|
1161
|
+
try {
|
|
1162
|
+
content = await Bun.file(filePath).text();
|
|
1123
1163
|
} catch (error) {
|
|
1124
1164
|
if (isEnoent(error)) {
|
|
1125
|
-
this.#
|
|
1165
|
+
this.#resetYamlLoadState();
|
|
1126
1166
|
return {};
|
|
1127
1167
|
}
|
|
1128
1168
|
throw error;
|
|
1129
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;
|
|
1130
1240
|
}
|
|
1131
1241
|
|
|
1132
1242
|
async #loadProjectSettings(): Promise<RawSettings> {
|
|
@@ -1519,7 +1629,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1519
1629
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1520
1630
|
|
|
1521
1631
|
#queueSave(): void {
|
|
1522
|
-
if (!this.#persist || !this.#configPath) return;
|
|
1632
|
+
if (!this.#persist || !this.#configPath || this.#hasRecoveredConfigSyntax) return;
|
|
1523
1633
|
|
|
1524
1634
|
const currentSlot = this.#pendingSaveSlot;
|
|
1525
1635
|
if (currentSlot && !currentSlot.captured && !currentSlot.released) {
|
|
@@ -1586,25 +1696,33 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1586
1696
|
this.#recomputeNotificationValidationFromRaw();
|
|
1587
1697
|
},
|
|
1588
1698
|
};
|
|
1589
|
-
})
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
}
|
|
1597
|
-
if (durableBeforeWrite) {
|
|
1598
|
-
this.#global = durableBeforeWrite;
|
|
1599
|
-
this.#captureRawNotificationConfig(durableBeforeWrite);
|
|
1600
|
-
for (const patch of this.#pendingPatchesInGenerationOrder()) {
|
|
1601
|
-
applySettingsPatch(this.#global, { ...patch, value: structuredClone(patch.value) });
|
|
1602
|
-
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);
|
|
1603
1706
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
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(() => {});
|
|
1608
1726
|
this.#armSaveTimer(slot);
|
|
1609
1727
|
}
|
|
1610
1728
|
|
|
@@ -1708,13 +1826,41 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1708
1826
|
return applicable.some(patch => isNotificationSettingsPath(patch.path));
|
|
1709
1827
|
}
|
|
1710
1828
|
|
|
1711
|
-
|
|
1712
|
-
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> {
|
|
1713
1847
|
const previousFingerprint = this.#durableNotificationFingerprint;
|
|
1714
|
-
const current = await this.#loadYaml(
|
|
1848
|
+
const current = await this.#loadYaml(canonicalPath);
|
|
1715
1849
|
if (previousFingerprint !== this.#durableNotificationFingerprint) this.#notificationValidationGeneration++;
|
|
1716
1850
|
this.#replaceGlobalWithDurable(current);
|
|
1717
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
|
+
}
|
|
1718
1864
|
|
|
1719
1865
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1720
1866
|
// Utilities
|