@gajae-code/coding-agent 0.14.2 → 0.15.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 +120 -1
- package/dist/types/async/job-manager.d.ts +4 -1
- package/dist/types/autoresearch/dashboard.d.ts +29 -4
- package/dist/types/autoresearch/data-context.d.ts +25 -0
- package/dist/types/autoresearch/git.d.ts +65 -7
- package/dist/types/autoresearch/harness.d.ts +58 -0
- package/dist/types/autoresearch/prompts.d.ts +51 -0
- package/dist/types/autoresearch/runs.d.ts +173 -0
- package/dist/types/autoresearch/types.d.ts +9 -139
- package/dist/types/capability/types.d.ts +20 -0
- package/dist/types/cli/accounts-cli.d.ts +1 -0
- package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
- package/dist/types/config/autorouting-contract.d.ts +146 -0
- package/dist/types/config/autorouting-generator.d.ts +40 -0
- package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
- package/dist/types/config/autorouting.d.ts +35 -0
- package/dist/types/config/model-discovery-manager.d.ts +8 -1
- package/dist/types/config/model-registry.d.ts +30 -1
- package/dist/types/config/provider-selection-policy.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +61 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/durability.d.ts +29 -0
- package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +14 -0
- package/dist/types/crash/index-store.d.ts +3 -0
- package/dist/types/crash/record-loader.d.ts +2 -0
- package/dist/types/crash/upstream/relay.d.ts +5 -3
- package/dist/types/cursor.d.ts +12 -0
- package/dist/types/defaults/gjc-defaults.d.ts +35 -1
- package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/edit/index.d.ts +2 -1
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +32 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
- package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
- package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
- package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
- package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
- package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
- package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
- package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
- package/dist/types/hooks/skill-keywords.d.ts +1 -1
- package/dist/types/main.d.ts +0 -10
- package/dist/types/modes/acp/acp-agent.d.ts +13 -6
- package/dist/types/modes/components/model-selector.d.ts +14 -0
- package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
- package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +9 -0
- package/dist/types/rlm/artifacts.d.ts +4 -4
- package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
- package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
- package/dist/types/runtime-mcp/config.d.ts +6 -0
- package/dist/types/runtime-mcp/manager.d.ts +10 -0
- package/dist/types/sdk/acp/adapter.d.ts +0 -4
- package/dist/types/sdk/broker/broker.d.ts +1 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
- package/dist/types/sdk/broker/session-index.d.ts +15 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/index.d.ts +8 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
- package/dist/types/sdk/client/client.d.ts +39 -4
- package/dist/types/sdk/host/host.d.ts +1 -0
- package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
- package/dist/types/sdk/host/model-pin.d.ts +29 -0
- package/dist/types/sdk/host/session-runtime.d.ts +2 -1
- package/dist/types/sdk/lifecycle-session.d.ts +13 -0
- package/dist/types/sdk/router/session-router.d.ts +27 -1
- package/dist/types/sdk/session.d.ts +11 -1
- package/dist/types/session/agent-session.d.ts +36 -6
- package/dist/types/session/artifacts.d.ts +16 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/session-manager.d.ts +84 -2
- package/dist/types/session/session-staging-paths.d.ts +3 -0
- package/dist/types/skill-state/canonical-skills.d.ts +1 -1
- package/dist/types/skill-state/workflow-hud.d.ts +12 -21
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
- package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
- package/dist/types/system-prompt.d.ts +23 -0
- package/dist/types/task/executor.d.ts +35 -4
- package/dist/types/task/index.d.ts +17 -3
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +144 -0
- package/dist/types/tools/atomic-file-write.d.ts +72 -0
- package/dist/types/tools/descriptors.d.ts +1 -0
- package/dist/types/tools/index.d.ts +36 -1
- package/dist/types/tools/move-session.d.ts +40 -0
- package/dist/types/tools/python.d.ts +12 -0
- package/dist/types/tools/resource-gc.d.ts +0 -3
- package/dist/types/tools/search.d.ts +8 -1
- package/dist/types/tools/skill.d.ts +9 -4
- package/dist/types/tools/subagent.d.ts +33 -3
- package/dist/types/utils/xml-escape.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
- package/package.json +11 -12
- package/scripts/build-sdk-package-smoke.ts +7 -5
- package/scripts/check-autorouting-tier-map.ts +183 -0
- package/scripts/generate-sdk-operation-inventory.ts +12 -0
- package/scripts/generate-tool-catalog.ts +40 -5
- package/src/async/job-manager.ts +5 -2
- package/src/autoresearch/dashboard.ts +134 -314
- package/src/autoresearch/data-context.ts +35 -0
- package/src/autoresearch/git.ts +186 -26
- package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
- package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
- package/src/autoresearch/prompts/prompt.md +103 -0
- package/src/autoresearch/prompts.ts +126 -0
- package/src/autoresearch/runs.ts +708 -0
- package/src/autoresearch/types.ts +9 -150
- package/src/capability/index.ts +4 -4
- package/src/capability/types.ts +20 -0
- package/src/cli/accounts-cli.ts +21 -5
- package/src/cli/config-cli.ts +25 -1
- package/src/cli/fast-help.ts +1 -2
- package/src/cli/list-models.ts +5 -8
- package/src/cli/setup-cli.ts +8 -0
- package/src/cli.ts +2 -21
- package/src/commands/autoresearch.ts +33 -0
- package/src/commands/coordinator.ts +15 -0
- package/src/commands/launch.ts +2 -4
- package/src/commands/quick-lane.ts +1 -1
- package/src/commands/sdk.ts +24 -0
- package/src/commands/state.ts +1 -1
- package/src/config/autorouting-contract.ts +474 -0
- package/src/config/autorouting-generator.ts +238 -0
- package/src/config/autorouting-tier-map.ts +6108 -0
- package/src/config/autorouting.ts +120 -0
- package/src/config/model-discovery-manager.ts +42 -11
- package/src/config/model-registry.ts +348 -68
- package/src/config/provider-selection-policy.ts +60 -9
- package/src/config/settings-schema.ts +195 -6
- package/src/config/settings.ts +62 -1
- package/src/config.ts +2 -3
- package/src/coordinator/contract.ts +0 -1
- package/src/coordinator-mcp/codex-handoff.ts +68 -42
- package/src/coordinator-mcp/durability.ts +200 -0
- package/src/coordinator-mcp/event-webhook.ts +278 -0
- package/src/coordinator-mcp/question-state.ts +109 -46
- package/src/coordinator-mcp/server.ts +932 -504
- package/src/crash/index-store.ts +126 -22
- package/src/crash/record-loader.ts +7 -0
- package/src/crash/upstream/relay.ts +82 -34
- package/src/cursor.ts +84 -1
- package/src/debug/index.ts +26 -0
- package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
- package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
- package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
- package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
- package/src/defaults/gjc-defaults.ts +89 -2
- package/src/defaults/gjc-skills.generated.ts +24 -17
- package/src/discovery/builtin.ts +18 -5
- package/src/discovery/helpers.ts +6 -6
- package/src/edit/index.ts +5 -2
- package/src/eval/py/executor.ts +8 -0
- package/src/extensibility/extensions/loader.ts +135 -13
- package/src/extensibility/extensions/wrapper.ts +1 -2
- package/src/extensibility/gjc-plugins/schema.ts +1 -1
- package/src/extensibility/runtime-skill-discovery.ts +2 -2
- package/src/extensibility/skill-management.ts +2 -3
- package/src/extensibility/skills.ts +16 -8
- package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
- package/src/gjc-runtime/gc-render.ts +0 -1
- package/src/gjc-runtime/gc-runtime.ts +1 -11
- package/src/gjc-runtime/launch-tmux.ts +3 -3
- package/src/gjc-runtime/psmux-detect.ts +2 -2
- package/src/gjc-runtime/python-transcript.ts +58 -0
- package/src/gjc-runtime/ralplan-runtime.ts +6 -18
- package/src/gjc-runtime/session-layout.ts +18 -7
- package/src/gjc-runtime/session-state-sidecar.ts +58 -9
- package/src/gjc-runtime/state-renderer.ts +0 -38
- package/src/gjc-runtime/state-runtime.ts +43 -22
- package/src/gjc-runtime/state-schema.ts +1 -1
- package/src/gjc-runtime/tmux-common.ts +7 -7
- package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
- package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
- package/src/gjc-runtime/workflow-command-ref.ts +15 -11
- package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
- package/src/gjc-runtime/workflow-manifest.ts +43 -83
- package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
- package/src/hooks/skill-keywords.ts +3 -3
- package/src/hooks/skill-state.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +26 -24
- package/src/lsp/index.ts +25 -5
- package/src/main.ts +2 -27
- package/src/modes/acp/acp-agent.ts +200 -75
- package/src/modes/components/model-selector.ts +216 -13
- package/src/modes/components/oauth-selector.ts +5 -5
- package/src/modes/components/skill-hud/render.ts +173 -35
- package/src/modes/components/smart-routing-panel.ts +531 -0
- package/src/modes/components/tool-status-header.ts +6 -3
- package/src/modes/controllers/command-controller.ts +17 -7
- package/src/modes/controllers/input-controller.ts +44 -9
- package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +298 -65
- package/src/modes/interactive-mode.ts +8 -1
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
- package/src/modes/types.ts +9 -1
- package/src/prompts/agents/planner.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/volatile-project-context.md +2 -1
- package/src/prompts/tools/move-session.md +15 -0
- package/src/prompts/tools/python.md +12 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/prompts/tools/skill.md +2 -2
- package/src/prompts/tools/task-summary.md +1 -0
- package/src/prompts/tools/task.md +6 -0
- package/src/rlm/artifacts.ts +12 -10
- package/src/runtime/optional-runtime-services.ts +3 -2
- package/src/runtime/workspace-tree-service.ts +11 -3
- package/src/runtime-mcp/config.ts +8 -1
- package/src/runtime-mcp/manager.ts +33 -0
- package/src/sdk/acp/adapter.ts +48 -1
- package/src/sdk/broker/broker.ts +30 -2
- package/src/sdk/broker/discovery.ts +217 -2
- package/src/sdk/broker/lifecycle.ts +385 -59
- package/src/sdk/broker/session-index.ts +243 -48
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
- package/src/sdk/bus/index.ts +208 -11
- package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
- package/src/sdk/bus/reconciliation-store.ts +27 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
- package/src/sdk/client/client.ts +204 -22
- package/src/sdk/host/host.ts +16 -0
- package/src/sdk/host/internal-autorouting-state.ts +13 -0
- package/src/sdk/host/model-pin.ts +101 -0
- package/src/sdk/host/reverse-leases.ts +2 -1
- package/src/sdk/host/session-runtime.test.ts +224 -1
- package/src/sdk/host/session-runtime.ts +125 -37
- package/src/sdk/lifecycle/service.ts +3 -1
- package/src/sdk/lifecycle-session.ts +42 -1
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/router/session-router.ts +375 -36
- package/src/sdk/session.ts +547 -47
- package/src/session/account-inventory.ts +1 -1
- package/src/session/agent-session.ts +765 -189
- package/src/session/artifacts.ts +234 -4
- package/src/session/internal/managed-session-storage.ts +91 -4
- package/src/session/provider-safety-stop-hint.ts +3 -2
- package/src/session/session-manager.ts +775 -16
- package/src/session/session-staging-paths.ts +8 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
- package/src/setup/host-plugin-setup.ts +2 -2
- package/src/skill-state/active-state.ts +2 -2
- package/src/skill-state/canonical-skills.ts +1 -1
- package/src/skill-state/initial-phase.ts +1 -1
- package/src/skill-state/workflow-hud.ts +41 -41
- package/src/skill-state/workflow-mutation-guard.ts +26 -7
- package/src/slash-commands/builtin-registry.ts +77 -0
- package/src/slash-commands/helpers/autorouting-status.ts +55 -0
- package/src/system-prompt.ts +102 -5
- package/src/task/discovery.ts +2 -3
- package/src/task/executor.ts +617 -43
- package/src/task/index.ts +262 -15
- package/src/task/receipt.ts +30 -0
- package/src/task/render.ts +11 -1
- package/src/task/types.ts +316 -0
- package/src/tools/atomic-file-write.ts +594 -0
- package/src/tools/bisect.ts +3 -2
- package/src/tools/descriptors.test.ts +3 -0
- package/src/tools/descriptors.ts +24 -0
- package/src/tools/image-gen.ts +5 -1
- package/src/tools/index.ts +62 -5
- package/src/tools/move-session.ts +107 -0
- package/src/tools/python.ts +205 -0
- package/src/tools/read.ts +88 -15
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resource-gc.ts +8 -177
- package/src/tools/search.ts +30 -6
- package/src/tools/skill-discovery.ts +1 -0
- package/src/tools/skill.ts +37 -7
- package/src/tools/subagent-render.ts +119 -13
- package/src/tools/subagent.ts +91 -74
- package/src/tools/tool-catalog.generated.ts +64 -2
- package/src/tools/write.ts +16 -12
- package/src/utils/xml-escape.ts +14 -0
- package/src/web/search/providers/codex.ts +2 -2
- package/src/workflow/workflow-intent-diff.ts +13 -12
- package/src/workflow/workflow-intent-report-consumer.ts +6 -6
- package/dist/types/autoresearch/helpers.d.ts +0 -24
- package/dist/types/autoresearch/index.d.ts +0 -2
- package/dist/types/autoresearch/state.d.ts +0 -17
- package/dist/types/autoresearch/storage.d.ts +0 -142
- package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
- package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
- package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
- package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
- package/dist/types/commands/team.d.ts +0 -24
- package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
- package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
- package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
- package/dist/types/gjc-runtime/team-store.d.ts +0 -168
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
- package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
- package/dist/types/rlm/index.d.ts +0 -47
- package/dist/types/rlm/preset.d.ts +0 -23
- package/dist/types/rlm/python-tool.d.ts +0 -16
- package/src/autoresearch/command-resume.md +0 -14
- package/src/autoresearch/index.ts +0 -536
- package/src/autoresearch/prompt.md +0 -103
- package/src/autoresearch/resume-message.md +0 -10
- package/src/autoresearch/state.ts +0 -273
- package/src/autoresearch/storage.ts +0 -699
- package/src/autoresearch/tools/init-experiment.ts +0 -272
- package/src/autoresearch/tools/log-experiment.ts +0 -524
- package/src/autoresearch/tools/run-experiment.ts +0 -475
- package/src/autoresearch/tools/update-notes.ts +0 -109
- package/src/commands/rlm.ts +0 -19
- package/src/commands/team.ts +0 -238
- package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
- package/src/defaults/gjc/skills/team/SKILL.md +0 -467
- package/src/gjc-runtime/team-gc.ts +0 -49
- package/src/gjc-runtime/team-launch.ts +0 -404
- package/src/gjc-runtime/team-notify.ts +0 -326
- package/src/gjc-runtime/team-runtime.ts +0 -5923
- package/src/gjc-runtime/team-store.ts +0 -878
- package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
- package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
- package/src/gjc-runtime/team-workers.ts +0 -1123
- package/src/prompts/system/rlm-report-command.md +0 -1
- package/src/prompts/system/rlm-research.md +0 -23
- package/src/rlm/index.ts +0 -365
- package/src/rlm/preset.ts +0 -76
- package/src/rlm/python-tool.ts +0 -68
|
@@ -146,6 +146,74 @@ const dirFor = (agentDir: string) => path.join(agentDir, "sdk", "sessions");
|
|
|
146
146
|
const logFor = (agentDir: string) => path.join(dirFor(agentDir), "index.jsonl");
|
|
147
147
|
const snapshotFor = (agentDir: string) => path.join(dirFor(agentDir), "index.snapshot.json");
|
|
148
148
|
const auditFor = (agentDir: string) => path.join(dirFor(agentDir), "index-audit.jsonl");
|
|
149
|
+
/**
|
|
150
|
+
* Idle-poll change stamp over the two index files (#4689). A polling reader
|
|
151
|
+
* that has already replayed the index must be able to prove "nothing changed"
|
|
152
|
+
* with two stats instead of re-acquiring the machine-global lock and
|
|
153
|
+
* re-parsing the whole log every cycle.
|
|
154
|
+
*
|
|
155
|
+
* Timestamps alone are NOT a durable-equality proof: on a coarse-resolution
|
|
156
|
+
* filesystem a same-size write inside one tick reports identical `mtimeMs` and
|
|
157
|
+
* `ctimeMs` (measured: ~98% of back-to-back same-size rewrites collide on
|
|
158
|
+
* both). The stamp is therefore anchored on the two fields the cooperative
|
|
159
|
+
* writer protocol cannot leave untouched:
|
|
160
|
+
* - append (`appendSync`) always grows `size`;
|
|
161
|
+
* - rename-replace (`replaceAtomically`, used for snapshots and rotation)
|
|
162
|
+
* always installs a new inode, so `ino` changes even when the replacement
|
|
163
|
+
* has an identical byte length (measured: 0/3000 inode collisions).
|
|
164
|
+
* `mtimeMs`/`ctimeMs` are retained as an extra signal for foreign in-place
|
|
165
|
+
* edits that keep both size and inode, which the writer protocol never does.
|
|
166
|
+
*/
|
|
167
|
+
interface SessionIndexFileStamp {
|
|
168
|
+
readonly exists: boolean;
|
|
169
|
+
readonly size: number;
|
|
170
|
+
/**
|
|
171
|
+
* Kept as `bigint` so a 64-bit inode / Windows file id above
|
|
172
|
+
* `Number.MAX_SAFE_INTEGER` cannot collapse onto another id and make a real
|
|
173
|
+
* replacement read as unchanged (#4730 review).
|
|
174
|
+
*/
|
|
175
|
+
readonly ino: bigint;
|
|
176
|
+
readonly mtimeMs: number;
|
|
177
|
+
readonly ctimeMs: number;
|
|
178
|
+
}
|
|
179
|
+
interface SessionIndexChangeStamp {
|
|
180
|
+
readonly log: SessionIndexFileStamp;
|
|
181
|
+
readonly snapshot: SessionIndexFileStamp;
|
|
182
|
+
}
|
|
183
|
+
async function statIndexFile(file: string): Promise<SessionIndexFileStamp> {
|
|
184
|
+
try {
|
|
185
|
+
const stat = await fs.stat(file, { bigint: true });
|
|
186
|
+
return {
|
|
187
|
+
exists: true,
|
|
188
|
+
size: Number(stat.size),
|
|
189
|
+
ino: stat.ino,
|
|
190
|
+
mtimeMs: Number(stat.mtimeMs),
|
|
191
|
+
ctimeMs: Number(stat.ctimeMs),
|
|
192
|
+
};
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT")
|
|
195
|
+
return { exists: false, size: 0, ino: 0n, mtimeMs: 0, ctimeMs: 0 };
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function readIndexChangeStamp(agentDir: string): Promise<SessionIndexChangeStamp> {
|
|
200
|
+
const [log, snapshot] = await Promise.all([statIndexFile(logFor(agentDir)), statIndexFile(snapshotFor(agentDir))]);
|
|
201
|
+
return { log, snapshot };
|
|
202
|
+
}
|
|
203
|
+
function sameIndexChangeStamp(a: SessionIndexChangeStamp, b: SessionIndexChangeStamp): boolean {
|
|
204
|
+
return (
|
|
205
|
+
a.log.exists === b.log.exists &&
|
|
206
|
+
a.log.size === b.log.size &&
|
|
207
|
+
a.log.ino === b.log.ino &&
|
|
208
|
+
a.log.mtimeMs === b.log.mtimeMs &&
|
|
209
|
+
a.log.ctimeMs === b.log.ctimeMs &&
|
|
210
|
+
a.snapshot.exists === b.snapshot.exists &&
|
|
211
|
+
a.snapshot.size === b.snapshot.size &&
|
|
212
|
+
a.snapshot.ino === b.snapshot.ino &&
|
|
213
|
+
a.snapshot.mtimeMs === b.snapshot.mtimeMs &&
|
|
214
|
+
a.snapshot.ctimeMs === b.snapshot.ctimeMs
|
|
215
|
+
);
|
|
216
|
+
}
|
|
149
217
|
const ROTATE_BYTES = 4 * 1024 * 1024;
|
|
150
218
|
/** Coalesced heartbeat checkpoint rate cap (C2): at most one per session per minute. */
|
|
151
219
|
export const SESSION_HEARTBEAT_INTERVAL_MS = 60_000;
|
|
@@ -723,35 +791,60 @@ async function syncDirectory(file: string): Promise<void> {
|
|
|
723
791
|
if (process.platform === "win32" && (code === "EPERM" || code === "EACCES")) return;
|
|
724
792
|
throw error;
|
|
725
793
|
}
|
|
794
|
+
let syncError: unknown;
|
|
726
795
|
try {
|
|
727
796
|
await handle.sync();
|
|
728
797
|
} catch (error) {
|
|
729
798
|
const code = (error as NodeJS.ErrnoException).code;
|
|
730
|
-
if (process.platform !== "win32" || (code !== "EPERM" && code !== "EACCES"))
|
|
731
|
-
}
|
|
799
|
+
if (process.platform !== "win32" || (code !== "EPERM" && code !== "EACCES")) syncError = error;
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
732
802
|
await handle.close();
|
|
803
|
+
} catch (closeError) {
|
|
804
|
+
if (syncError) throw new AggregateError([syncError, closeError], "session-index directory sync and close failed");
|
|
805
|
+
throw closeError;
|
|
733
806
|
}
|
|
807
|
+
if (syncError) throw syncError;
|
|
734
808
|
}
|
|
735
809
|
|
|
736
810
|
async function writeAndSync(file: string, contents: Buffer | string): Promise<void> {
|
|
737
811
|
const handle = await fs.open(file, "w", 0o600);
|
|
812
|
+
let writeError: unknown;
|
|
738
813
|
try {
|
|
739
814
|
await handle.writeFile(contents);
|
|
740
815
|
await handle.sync();
|
|
741
|
-
}
|
|
816
|
+
} catch (error) {
|
|
817
|
+
writeError = error;
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
742
820
|
await handle.close();
|
|
821
|
+
} catch (closeError) {
|
|
822
|
+
if (writeError) throw new AggregateError([writeError, closeError], "session-index write and close failed");
|
|
823
|
+
throw closeError;
|
|
743
824
|
}
|
|
825
|
+
if (writeError) throw writeError;
|
|
744
826
|
}
|
|
745
827
|
|
|
746
828
|
async function replaceAtomically(file: string, contents: Buffer | string): Promise<void> {
|
|
747
829
|
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
830
|
+
let primaryError: unknown;
|
|
748
831
|
try {
|
|
749
832
|
await writeAndSync(temporary, contents);
|
|
750
833
|
await fs.rename(temporary, file);
|
|
751
834
|
await syncDirectory(file);
|
|
752
|
-
}
|
|
835
|
+
} catch (error) {
|
|
836
|
+
primaryError = error;
|
|
837
|
+
}
|
|
838
|
+
let cleanupError: unknown;
|
|
839
|
+
try {
|
|
753
840
|
await fs.rm(temporary, { force: true });
|
|
841
|
+
} catch (error) {
|
|
842
|
+
cleanupError = error;
|
|
754
843
|
}
|
|
844
|
+
if (primaryError && cleanupError)
|
|
845
|
+
throw new AggregateError([primaryError, cleanupError], "session-index replace and cleanup failed");
|
|
846
|
+
if (primaryError) throw primaryError;
|
|
847
|
+
if (cleanupError) throw cleanupError;
|
|
755
848
|
}
|
|
756
849
|
|
|
757
850
|
function alive(pid: number): boolean {
|
|
@@ -788,6 +881,11 @@ export class SessionIndex {
|
|
|
788
881
|
#warnings: string[] = [];
|
|
789
882
|
#logOffset = 0;
|
|
790
883
|
#corruptSuffix = false;
|
|
884
|
+
/**
|
|
885
|
+
* Last change stamp observed by a completed locked pass (#4689); lets the
|
|
886
|
+
* polling refresh path prove "index unchanged" with two stats.
|
|
887
|
+
*/
|
|
888
|
+
#changeStamp: SessionIndexChangeStamp | undefined;
|
|
791
889
|
/** indexSeqs already recorded in the durable audit; null until lazily seeded. */
|
|
792
890
|
#auditedSeq: Set<number> | null = null;
|
|
793
891
|
constructor(agentDir: string, policy: RetentionPolicy = {}) {
|
|
@@ -836,6 +934,77 @@ export class SessionIndex {
|
|
|
836
934
|
withSessionIndexLock("replay", this.#agentDir, () => this.#replayUnderLock()),
|
|
837
935
|
);
|
|
838
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Polling-path refresh (#4689): when both index files carry the exact change
|
|
939
|
+
* stamp of the last completed locked pass, the in-memory projection is
|
|
940
|
+
* already current and the locked rescan is skipped entirely — this is what
|
|
941
|
+
* keeps an idle SessionRouter reconcile (2s cadence) from re-parsing and
|
|
942
|
+
* re-checksumming the whole index forever. An append committed before the
|
|
943
|
+
* stat always changes the stamp, so a miss is impossible; a change landing
|
|
944
|
+
* after the stat is seen on the next poll, the same TOCTOU envelope a
|
|
945
|
+
* locked read has. A corrupt suffix never takes the fast path: re-scanning
|
|
946
|
+
* preserves the existing re-diagnosis behavior. Returns true when state was
|
|
947
|
+
* reloaded. Authority revalidation that needs the strongest available
|
|
948
|
+
* snapshot inside an already-locked write (append, unregister) keeps using
|
|
949
|
+
* the exact locked paths.
|
|
950
|
+
*/
|
|
951
|
+
async refreshIfChanged(): Promise<boolean> {
|
|
952
|
+
if (this.#changeStamp !== undefined && !this.#corruptSuffix) {
|
|
953
|
+
const stamp = await readIndexChangeStamp(this.#agentDir);
|
|
954
|
+
if (sameIndexChangeStamp(this.#changeStamp, stamp)) return false;
|
|
955
|
+
// A possible change is re-classified UNDER the lock (#4689 review):
|
|
956
|
+
// writers mutate these files only while holding it, so the locked
|
|
957
|
+
// observation is atomic with the tail/replay it selects. The
|
|
958
|
+
// unlocked stamp above is only the cheap "definitely unchanged" cut.
|
|
959
|
+
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
960
|
+
await SessionIndex.#enqueue(indexPath, () =>
|
|
961
|
+
withSessionIndexLock("poll-refresh", this.#agentDir, () => this.#refreshOrReplayUnderLock()),
|
|
962
|
+
);
|
|
963
|
+
return true;
|
|
964
|
+
}
|
|
965
|
+
await this.open();
|
|
966
|
+
await this.refresh();
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Locked change classification (#4689). Append-only log growth with an
|
|
971
|
+
* untouched snapshot tails from the last offset; anything else — in-place
|
|
972
|
+
* rewrite, truncation, rotation, snapshot replacement, existence
|
|
973
|
+
* transitions, or an append/compact interleaving that outran the unlocked
|
|
974
|
+
* pre-check — takes the full replay. Metadata cannot prove prefix identity
|
|
975
|
+
* against a writer that rewrites history in place while growing the file;
|
|
976
|
+
* that boundary is the cooperative locked-writer protocol, which never
|
|
977
|
+
* does that (appendSync, or rename-replace with a fresh snapshot).
|
|
978
|
+
*/
|
|
979
|
+
async #refreshOrReplayUnderLock(): Promise<void> {
|
|
980
|
+
const stamp = await readIndexChangeStamp(this.#agentDir);
|
|
981
|
+
const prior = this.#changeStamp;
|
|
982
|
+
// Append-only growth with an untouched snapshot tails from the last
|
|
983
|
+
// offset; anything else replays in full.
|
|
984
|
+
// The inode is load-bearing on BOTH files here (#4730 review): tailing is
|
|
985
|
+
// only sound if this is the SAME log file grown in place and the SAME
|
|
986
|
+
// snapshot file. A rename-over installs a new inode while size and
|
|
987
|
+
// timestamps can all still match, so without these two comparisons a log
|
|
988
|
+
// that grew while the snapshot was replaced by a same-size, same-timestamp
|
|
989
|
+
// file would be misclassified as append-only, and the reader would tail
|
|
990
|
+
// from its last offset while caching a stale compacted projection.
|
|
991
|
+
if (
|
|
992
|
+
prior?.log.exists &&
|
|
993
|
+
stamp.log.exists &&
|
|
994
|
+
stamp.log.ino === prior.log.ino &&
|
|
995
|
+
stamp.log.size > prior.log.size &&
|
|
996
|
+
stamp.snapshot.exists === prior.snapshot.exists &&
|
|
997
|
+
stamp.snapshot.ino === prior.snapshot.ino &&
|
|
998
|
+
stamp.snapshot.size === prior.snapshot.size &&
|
|
999
|
+
stamp.snapshot.mtimeMs === prior.snapshot.mtimeMs &&
|
|
1000
|
+
stamp.snapshot.ctimeMs === prior.snapshot.ctimeMs
|
|
1001
|
+
)
|
|
1002
|
+
await this.#tailUnderLock();
|
|
1003
|
+
else await this.#replayUnderLock();
|
|
1004
|
+
// #replayUnderLock recaptures the stamp itself; the tail path needs it
|
|
1005
|
+
// captured here (refresh() does this via #refreshUnderLock).
|
|
1006
|
+
this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
|
|
1007
|
+
}
|
|
839
1008
|
async #replayUnderLock(): Promise<void> {
|
|
840
1009
|
const scan = await this.#scan();
|
|
841
1010
|
if (scan.diagnosis.status === "unsupported") throw scan.unsupportedError!;
|
|
@@ -846,6 +1015,7 @@ export class SessionIndex {
|
|
|
846
1015
|
if (scan.diagnosis.reason === "invalid snapshot") this.#warnings.push("Invalid session index snapshot");
|
|
847
1016
|
if (this.#corruptSuffix) this.#warnings.push("Corrupt session index entry; replay truncated");
|
|
848
1017
|
await this.#writeAuditUnderLock();
|
|
1018
|
+
this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
|
|
849
1019
|
}
|
|
850
1020
|
/** Seed the audit dedupe set once, then append records for newly-rejected events. */
|
|
851
1021
|
async #writeAuditUnderLock(): Promise<void> {
|
|
@@ -1032,43 +1202,44 @@ export class SessionIndex {
|
|
|
1032
1202
|
const indexPath = path.resolve(logFor(this.#agentDir));
|
|
1033
1203
|
return await SessionIndex.#enqueue(indexPath, async () => {
|
|
1034
1204
|
await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
|
|
1035
|
-
return await withSessionIndexLock("repair", this.#agentDir, async () =>
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
// compaction), so the log must be truncated to match: leaving the original
|
|
1064
|
-
// events in place keeps every subsequent #scan() parsing them under the lock
|
|
1065
|
-
// — the same starvation the compaction above is meant to end. This mirrors
|
|
1066
|
-
// #rotate(), which writes an empty log after snapshotting.
|
|
1067
|
-
await replaceAtomically(logFor(this.#agentDir), "");
|
|
1068
|
-
await this.#replayUnderLock();
|
|
1069
|
-
return { ...scan.diagnosis, repaired: true, quarantinePath };
|
|
1070
|
-
});
|
|
1205
|
+
return await withSessionIndexLock("repair", this.#agentDir, async () => await this.#repairUnderLock());
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
async #repairUnderLock(): Promise<SessionIndexRepairResult> {
|
|
1209
|
+
const scan = await this.#scan();
|
|
1210
|
+
if (scan.diagnosis.status === "unsupported") return { ...scan.diagnosis, repaired: false };
|
|
1211
|
+
if (scan.diagnosis.status === "healthy") return { ...scan.diagnosis, repaired: false };
|
|
1212
|
+
const quarantineBase = path.join(dirFor(this.#agentDir), "quarantine");
|
|
1213
|
+
await fs.mkdir(quarantineBase, { recursive: true, mode: 0o700 });
|
|
1214
|
+
await syncDirectory(quarantineBase);
|
|
1215
|
+
const quarantinePath = path.join(quarantineBase, `repair-${Date.now()}-${process.pid}-${randomUUID()}`);
|
|
1216
|
+
await fs.mkdir(quarantinePath, { mode: 0o700 });
|
|
1217
|
+
await syncDirectory(quarantinePath);
|
|
1218
|
+
if (scan.snapshotContents)
|
|
1219
|
+
await writeAndSync(path.join(quarantinePath, "index.snapshot.json"), scan.snapshotContents);
|
|
1220
|
+
if (scan.logContents) await writeAndSync(path.join(quarantinePath, "index.jsonl"), scan.logContents);
|
|
1221
|
+
await syncDirectory(path.join(quarantinePath, "index.jsonl"));
|
|
1222
|
+
// Repair republishes the surviving history as the new snapshot, so it must
|
|
1223
|
+
// apply the same retention the ordinary snapshot path applies. Writing the
|
|
1224
|
+
// raw survivor set instead let one repair of a long-lived index restore an
|
|
1225
|
+
// unbounded snapshot, and every later locked transaction then re-parsed that
|
|
1226
|
+
// whole history while holding the index lock — the broker burns CPU and
|
|
1227
|
+
// unrelated launches time out waiting for the lock.
|
|
1228
|
+
const events = compactEvents([...scan.snapshotEvents, ...scan.validLogEvents], this.#policy);
|
|
1229
|
+
const snapshot = JSON.stringify({
|
|
1230
|
+
version: SESSION_INDEX_SNAPSHOT_VERSION,
|
|
1231
|
+
indexSeq: scan.diagnosis.validPrefixSeq,
|
|
1232
|
+
events,
|
|
1071
1233
|
});
|
|
1234
|
+
await replaceAtomically(snapshotFor(this.#agentDir), snapshot);
|
|
1235
|
+
// The republished snapshot already carries the full surviving history (after
|
|
1236
|
+
// compaction), so the log must be truncated to match: leaving the original
|
|
1237
|
+
// events in place keeps every subsequent #scan() parsing them under the lock
|
|
1238
|
+
// — the same starvation the compaction above is meant to end. This mirrors
|
|
1239
|
+
// #rotate(), which writes an empty log after snapshotting.
|
|
1240
|
+
await replaceAtomically(logFor(this.#agentDir), "");
|
|
1241
|
+
await this.#replayUnderLock();
|
|
1242
|
+
return { ...scan.diagnosis, repaired: true, quarantinePath };
|
|
1072
1243
|
}
|
|
1073
1244
|
async #tailUnderLock(snapshotSeq = this.indexSeq, allowResync = true): Promise<void> {
|
|
1074
1245
|
let data: Buffer;
|
|
@@ -1129,8 +1300,15 @@ export class SessionIndex {
|
|
|
1129
1300
|
);
|
|
1130
1301
|
}
|
|
1131
1302
|
async #refreshUnderLock(): Promise<void> {
|
|
1132
|
-
|
|
1303
|
+
// A newly constructed reader has no replay cursor. Refresh must still load
|
|
1304
|
+
// snapshot-carried authority before tailing the empty log; otherwise a cold
|
|
1305
|
+
// router starting after compaction projects no sessions until a later append.
|
|
1306
|
+
if (this.#events.length === 0 && this.#logOffset === 0) await this.#replayUnderLock();
|
|
1307
|
+
else await this.#tailUnderLock();
|
|
1133
1308
|
await this.#writeAuditUnderLock();
|
|
1309
|
+
// #tailUnderLock may delegate to #replayUnderLock (which re-captures);
|
|
1310
|
+
// capturing here covers the tail-only and missing-log paths.
|
|
1311
|
+
this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
|
|
1134
1312
|
}
|
|
1135
1313
|
get indexSeq(): number {
|
|
1136
1314
|
return this.#events.at(-1)?.indexSeq ?? 0;
|
|
@@ -1156,10 +1334,25 @@ export class SessionIndex {
|
|
|
1156
1334
|
ownIncarnation = input.processIncarnation ?? processIncarnation(input.pid);
|
|
1157
1335
|
return await withSessionIndexLock("append", this.#agentDir, async () => {
|
|
1158
1336
|
await this.#replayUnderLock();
|
|
1159
|
-
if (this.#corruptSuffix)
|
|
1160
|
-
|
|
1161
|
-
|
|
1337
|
+
if (this.#corruptSuffix) {
|
|
1338
|
+
// A corrupt suffix used to hard-fail every append until a human ran
|
|
1339
|
+
// `gjc gc --repair-session-index` — but the writers that corrupt the
|
|
1340
|
+
// log (typically a stale long-lived broker signing events against an
|
|
1341
|
+
// outdated in-memory indexSeq) keep appending, so the operator-run
|
|
1342
|
+
// repair was re-corrupted within minutes and delegated session
|
|
1343
|
+
// launches stayed dead indefinitely. Run the same quarantine-backed
|
|
1344
|
+
// repair inline instead: evidence is preserved under
|
|
1345
|
+
// sessions/quarantine, the valid prefix is republished, and this
|
|
1346
|
+
// append proceeds against the repaired index.
|
|
1347
|
+
const repair = await this.#repairUnderLock();
|
|
1348
|
+
if (this.#corruptSuffix)
|
|
1349
|
+
throw new Error(
|
|
1350
|
+
"Cannot append to corrupt session index log; automatic repair did not converge — run `gjc gc --repair-session-index` to quarantine evidence and retain the valid prefix",
|
|
1351
|
+
);
|
|
1352
|
+
logger.warn(
|
|
1353
|
+
`sdk broker: session index self-repaired before append (${repair.reason ?? "corrupt suffix"}); evidence quarantined at ${repair.quarantinePath ?? "unknown"}`,
|
|
1162
1354
|
);
|
|
1355
|
+
}
|
|
1163
1356
|
const unsigned: Omit<SessionIndexEvent, "checksum"> = {
|
|
1164
1357
|
...input,
|
|
1165
1358
|
version: SDK_STATE_VERSION,
|
|
@@ -1325,11 +1518,13 @@ export class SessionIndex {
|
|
|
1325
1518
|
async #rotate(): Promise<void> {
|
|
1326
1519
|
await this.#snapshotUnderLock();
|
|
1327
1520
|
const file = logFor(this.#agentDir);
|
|
1328
|
-
|
|
1329
|
-
await fs.writeFile(temporary, "", { mode: 0o600 });
|
|
1330
|
-
await fs.rename(temporary, file);
|
|
1331
|
-
await syncDirectory(file);
|
|
1521
|
+
await replaceAtomically(file, "");
|
|
1332
1522
|
this.#logOffset = 0;
|
|
1523
|
+
// Sync in-memory state with the compacted snapshot while the lock is
|
|
1524
|
+
// held: without this the instance keeps pre-compaction events and a
|
|
1525
|
+
// fresh change stamp, so later refreshIfChanged() polls would never
|
|
1526
|
+
// observe the compaction (#4689 review).
|
|
1527
|
+
await this.#replayUnderLock();
|
|
1333
1528
|
}
|
|
1334
1529
|
|
|
1335
1530
|
listSessions(): SessionList {
|
|
@@ -124,10 +124,16 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
124
124
|
* Discord generation 65 / Slack generation 68 fence the off-reconcile-tail
|
|
125
125
|
* initial attachment replay introduced by #4542 so pre-upgrade daemon owners
|
|
126
126
|
* cannot retain the earlier SessionRouter attachment contract.
|
|
127
|
+
* The Discord 67 / Slack 70 fence covers the SessionRouter idle-poll/change-stamp
|
|
128
|
+
* rollout (#4689). Both daemons construct a
|
|
129
|
+
* SessionRouter whose idle tick no longer re-acquires the machine-global
|
|
130
|
+
* session-index lock every 2s; staleness retirement moved to a 30s sweep and
|
|
131
|
+
* lease heartbeats no longer force an authority reconcile. A pre-upgrade owner
|
|
132
|
+
* would retain the old hot polling loop, so replacement is required.
|
|
127
133
|
*/
|
|
128
134
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
129
|
-
discord:
|
|
130
|
-
slack:
|
|
135
|
+
discord: 67,
|
|
136
|
+
slack: 70,
|
|
131
137
|
};
|
|
132
138
|
|
|
133
139
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -365,6 +365,11 @@ export class ChatDaemonRuntime {
|
|
|
365
365
|
return this.#transportHealthy?.() ?? false;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
/** Reconcile indexed attachments and optionally wait for their replay tails. */
|
|
369
|
+
async reconcile(options: { waitForReplay?: boolean } = {}): Promise<void> {
|
|
370
|
+
await this.#router.reconcile(options);
|
|
371
|
+
}
|
|
372
|
+
|
|
368
373
|
async stop(): Promise<void> {
|
|
369
374
|
const providerResults = await Promise.allSettled([this.#discord?.stop(), this.#slack?.stop()]);
|
|
370
375
|
const discordRestartBlocked = this.#discord?.restartBlocked() ?? false;
|