@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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface CoordinatorDirectoryBarrierOptions {
|
|
5
|
+
platform?: NodeJS.Platform;
|
|
6
|
+
openDirectory?: (directory: string) => Promise<fs.FileHandle>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CoordinatorFileDurabilityOptions {
|
|
10
|
+
syncFile?: (handle: fs.FileHandle) => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CoordinatorAtomicWriteOptions
|
|
14
|
+
extends CoordinatorDirectoryBarrierOptions,
|
|
15
|
+
CoordinatorFileDurabilityOptions {
|
|
16
|
+
rename?: (source: string, destination: string) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The file was published, but its final directory barrier did not complete. */
|
|
20
|
+
export class CoordinatorPublicationUncertainError extends Error {
|
|
21
|
+
constructor(cause: unknown) {
|
|
22
|
+
super("coordinator publication outcome is uncertain", { cause });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function ensureCoordinatorDirectory(
|
|
27
|
+
directory: string,
|
|
28
|
+
options: CoordinatorDirectoryBarrierOptions = {},
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const missing: string[] = [];
|
|
31
|
+
for (let current = directory; ; current = path.dirname(current)) {
|
|
32
|
+
try {
|
|
33
|
+
const stat = await fs.stat(current);
|
|
34
|
+
if (!stat.isDirectory()) throw new Error(`coordinator directory is not a directory: ${current}`);
|
|
35
|
+
break;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
38
|
+
missing.push(current);
|
|
39
|
+
if (current === path.dirname(current)) throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const created of missing.reverse()) {
|
|
43
|
+
try {
|
|
44
|
+
await fs.mkdir(created, { mode: 0o700 });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
47
|
+
const stat = await fs.stat(created);
|
|
48
|
+
if (!stat.isDirectory()) throw error;
|
|
49
|
+
}
|
|
50
|
+
await syncCoordinatorDirectory(path.dirname(created), options);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Windows does not support fsync on directory handles. File contents must be
|
|
56
|
+
* synced before publication; this barrier only makes the renamed directory
|
|
57
|
+
* entry durable where the platform supports that operation.
|
|
58
|
+
*/
|
|
59
|
+
export function isUnsupportedWindowsDirectorySyncError(
|
|
60
|
+
error: unknown,
|
|
61
|
+
platform: NodeJS.Platform = process.platform,
|
|
62
|
+
): boolean {
|
|
63
|
+
if (platform !== "win32") return false;
|
|
64
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
65
|
+
// Windows directory-handle barriers use the established broker precedent:
|
|
66
|
+
// EPERM/EACCES are tolerated only for the directory open/sync operation.
|
|
67
|
+
return code === "EPERM" || code === "EACCES";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function syncCoordinatorDirectory(
|
|
71
|
+
directory: string,
|
|
72
|
+
options: CoordinatorDirectoryBarrierOptions = {},
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
let handle: fs.FileHandle;
|
|
75
|
+
try {
|
|
76
|
+
handle = await (options.openDirectory ?? (path => fs.open(path, "r")))(directory);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (!isUnsupportedWindowsDirectorySyncError(error, options.platform)) throw error;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let syncError: unknown;
|
|
82
|
+
try {
|
|
83
|
+
await handle.sync();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!isUnsupportedWindowsDirectorySyncError(error, options.platform)) syncError = error;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
await handle.close();
|
|
89
|
+
} catch (closeError) {
|
|
90
|
+
if (syncError) throw new AggregateError([syncError, closeError], "coordinator directory sync and close failed");
|
|
91
|
+
throw closeError;
|
|
92
|
+
}
|
|
93
|
+
if (syncError) throw syncError;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** File durability is never best-effort: callers must abort on every failure. */
|
|
97
|
+
export async function syncCoordinatorFile(
|
|
98
|
+
handle: fs.FileHandle,
|
|
99
|
+
options: CoordinatorFileDurabilityOptions = {},
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
await (options.syncFile ?? (file => file.sync()))(handle);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Append a durable coordinator journal or diagnostic record, then barrier its parent. */
|
|
105
|
+
export async function appendCoordinatorFile(
|
|
106
|
+
file: string,
|
|
107
|
+
contents: string,
|
|
108
|
+
options: CoordinatorDirectoryBarrierOptions & CoordinatorFileDurabilityOptions = {},
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
await ensureCoordinatorDirectory(path.dirname(file), options);
|
|
111
|
+
let originalSize = 0;
|
|
112
|
+
try {
|
|
113
|
+
originalSize = (await fs.stat(file)).size;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
116
|
+
}
|
|
117
|
+
const handle = await fs.open(file, "a", 0o600);
|
|
118
|
+
let writeError: unknown;
|
|
119
|
+
try {
|
|
120
|
+
await handle.writeFile(contents);
|
|
121
|
+
await syncCoordinatorFile(handle, options);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
writeError = error;
|
|
124
|
+
}
|
|
125
|
+
if (writeError) {
|
|
126
|
+
try {
|
|
127
|
+
await handle.truncate(originalSize);
|
|
128
|
+
await syncCoordinatorFile(handle, options);
|
|
129
|
+
} catch (rollbackError) {
|
|
130
|
+
writeError = new AggregateError([writeError, rollbackError], "coordinator append rollback failed");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
await handle.close();
|
|
135
|
+
} catch (closeError) {
|
|
136
|
+
if (writeError) {
|
|
137
|
+
const writeErrors = writeError instanceof AggregateError ? writeError.errors : [writeError];
|
|
138
|
+
throw new AggregateError([...writeErrors, closeError], "coordinator append and close failed");
|
|
139
|
+
}
|
|
140
|
+
throw closeError;
|
|
141
|
+
}
|
|
142
|
+
if (writeError) throw writeError;
|
|
143
|
+
try {
|
|
144
|
+
await syncCoordinatorDirectory(path.dirname(file), options);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw new CoordinatorPublicationUncertainError(error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Atomically publish a synced coordinator state file, then barrier its parent. */
|
|
151
|
+
export async function writeCoordinatorAtomic(
|
|
152
|
+
file: string,
|
|
153
|
+
contents: string,
|
|
154
|
+
options: CoordinatorAtomicWriteOptions = {},
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
await ensureCoordinatorDirectory(path.dirname(file), options);
|
|
157
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
158
|
+
let published = false;
|
|
159
|
+
try {
|
|
160
|
+
const handle = await fs.open(temporary, "wx", 0o600);
|
|
161
|
+
let writeError: unknown;
|
|
162
|
+
try {
|
|
163
|
+
await handle.writeFile(contents);
|
|
164
|
+
await syncCoordinatorFile(handle, options);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
writeError = error;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
await handle.close();
|
|
170
|
+
} catch (closeError) {
|
|
171
|
+
if (writeError) throw new AggregateError([writeError, closeError], "coordinator write and close failed");
|
|
172
|
+
throw closeError;
|
|
173
|
+
}
|
|
174
|
+
if (writeError) throw writeError;
|
|
175
|
+
await (options.rename ?? fs.rename)(temporary, file);
|
|
176
|
+
published = true;
|
|
177
|
+
await syncCoordinatorDirectory(path.dirname(file), options);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
let cleanupError: unknown;
|
|
180
|
+
try {
|
|
181
|
+
await fs.rm(temporary, { force: true });
|
|
182
|
+
} catch (cleanupFailure) {
|
|
183
|
+
cleanupError = cleanupFailure;
|
|
184
|
+
}
|
|
185
|
+
let cleanupBarrierError: unknown;
|
|
186
|
+
if (!cleanupError && !published) {
|
|
187
|
+
try {
|
|
188
|
+
await syncCoordinatorDirectory(path.dirname(file), options);
|
|
189
|
+
} catch (barrierError) {
|
|
190
|
+
cleanupBarrierError = barrierError;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const failures = [error, cleanupError, cleanupBarrierError].filter(
|
|
194
|
+
(value): value is unknown => value !== undefined,
|
|
195
|
+
);
|
|
196
|
+
if (published) throw new CoordinatorPublicationUncertainError(new AggregateError(failures));
|
|
197
|
+
if (failures.length === 1) throw failures[0];
|
|
198
|
+
throw new AggregateError(failures, "coordinator atomic write and cleanup failed");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { $credentialEnv } from "@gajae-code/utils/env";
|
|
4
|
+
import { withFileLock } from "../config/file-lock";
|
|
5
|
+
import { writeCoordinatorAtomic } from "./durability";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Opt-in webhook delivery of existing coordinator `watch_events` journal rows
|
|
9
|
+
* (issue #4706). Delivery is a durable outbox keyed by the journal row's stable
|
|
10
|
+
* `id`: each delivered row is recorded once, retried with bounded exponential
|
|
11
|
+
* backoff, and never blocks or rewrites the journal append path.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface EventWebhookConfig {
|
|
15
|
+
url: string;
|
|
16
|
+
tokenFile: string | null;
|
|
17
|
+
sessionIds: Set<string> | null;
|
|
18
|
+
timeoutMs: number;
|
|
19
|
+
maxAttempts: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface WebhookDeliveryRecord {
|
|
23
|
+
schema_version: 1;
|
|
24
|
+
event_id: string;
|
|
25
|
+
status: "pending" | "delivered" | "failed";
|
|
26
|
+
attempts: number;
|
|
27
|
+
created_at: string;
|
|
28
|
+
updated_at: string;
|
|
29
|
+
last_error: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isWebhookDeliveryRecord(value: unknown, eventId: string): value is WebhookDeliveryRecord {
|
|
33
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
34
|
+
const record = value as Record<string, unknown>;
|
|
35
|
+
return (
|
|
36
|
+
record.schema_version === 1 &&
|
|
37
|
+
record.event_id === eventId &&
|
|
38
|
+
(record.status === "pending" || record.status === "delivered" || record.status === "failed") &&
|
|
39
|
+
typeof record.attempts === "number" &&
|
|
40
|
+
Number.isSafeInteger(record.attempts) &&
|
|
41
|
+
record.attempts >= 0 &&
|
|
42
|
+
typeof record.created_at === "string" &&
|
|
43
|
+
typeof record.updated_at === "string" &&
|
|
44
|
+
(record.last_error === null || typeof record.last_error === "string")
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Result of one webhook POST attempt; 2xx is ok, anything else carries the reason. */
|
|
49
|
+
interface WebhookPostOutcome {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
status: number | null;
|
|
52
|
+
error: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WebhookDelivery {
|
|
56
|
+
/** Called with the exact JSON body to POST and per-attempt request options. */
|
|
57
|
+
post(
|
|
58
|
+
body: string,
|
|
59
|
+
options: { url: string; token: string | null; timeoutMs: number; signal: AbortSignal },
|
|
60
|
+
): Promise<WebhookPostOutcome>;
|
|
61
|
+
/** Test seam: delay between retries; default is `Bun.sleep`. */
|
|
62
|
+
sleep(ms: number): Promise<void>;
|
|
63
|
+
/** Monotonic-ish clock for deadline math; default is `Date.now`. */
|
|
64
|
+
now(): number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Default `WebhookDelivery` over `fetch`: POST, 2xx = ok, no redirects, abort on timeout. */
|
|
68
|
+
export function createDefaultEventWebhookDelivery(): WebhookDelivery {
|
|
69
|
+
return {
|
|
70
|
+
async post(body, options) {
|
|
71
|
+
const response = await fetch(options.url, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
redirect: "error",
|
|
74
|
+
signal: options.signal,
|
|
75
|
+
headers: {
|
|
76
|
+
"content-type": "application/json",
|
|
77
|
+
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
|
78
|
+
},
|
|
79
|
+
body,
|
|
80
|
+
});
|
|
81
|
+
// Drain the body so the socket is reusable, then treat non-2xx as failure.
|
|
82
|
+
await response.text().catch(() => "");
|
|
83
|
+
if (!response.ok) return { ok: false, status: response.status, error: `http_${response.status}` };
|
|
84
|
+
return { ok: true, status: response.status, error: null };
|
|
85
|
+
},
|
|
86
|
+
sleep: async ms => {
|
|
87
|
+
await Bun.sleep(ms);
|
|
88
|
+
},
|
|
89
|
+
now: () => Date.now(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const DEFAULT_EVENT_WEBHOOK_TIMEOUT_MS = 5_000;
|
|
94
|
+
export const MAX_EVENT_WEBHOOK_TIMEOUT_MS = 30_000;
|
|
95
|
+
export const DEFAULT_EVENT_WEBHOOK_MAX_ATTEMPTS = 5;
|
|
96
|
+
export const MAX_EVENT_WEBHOOK_MAX_ATTEMPTS = 10;
|
|
97
|
+
const WEBHOOK_BACKOFF_BASE_MS = 500;
|
|
98
|
+
const WEBHOOK_BACKOFF_CAP_MS = 15_000;
|
|
99
|
+
const WEBHOOK_ERROR_CAP = 240;
|
|
100
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "localhost"]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Trusted-source resolution for the five webhook variables. Egress destination
|
|
104
|
+
* and the token-file path must never come from the caller's cwd `.env` overlay:
|
|
105
|
+
* Bun loads that overlay into `process.env`, so a raw `process.env` read would
|
|
106
|
+
* let any repository select where coordinator journal rows are POSTed.
|
|
107
|
+
* `$credentialEnv` consults only the inherited environment and GJC/user-owned
|
|
108
|
+
* env files, exactly like the crash relay's DSN. Callers that own a fully
|
|
109
|
+
* controlled environment (tests, rendered hermes blocks) pass `explicitEnv`;
|
|
110
|
+
* production entry points must omit it and resolve through the trusted source.
|
|
111
|
+
*/
|
|
112
|
+
function webhookEnvValue(explicitEnv: NodeJS.ProcessEnv | undefined, name: string): string | undefined {
|
|
113
|
+
const explicit = explicitEnv?.[name]?.trim();
|
|
114
|
+
if (explicit) return explicit;
|
|
115
|
+
return $credentialEnv(name)?.trim() || undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function parseEventWebhookConfig(explicitEnv?: NodeJS.ProcessEnv): EventWebhookConfig | null {
|
|
119
|
+
const rawUrl = webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_URL");
|
|
120
|
+
if (!rawUrl) return null;
|
|
121
|
+
let parsed: URL;
|
|
122
|
+
try {
|
|
123
|
+
parsed = new URL(rawUrl);
|
|
124
|
+
} catch {
|
|
125
|
+
throw new Error("coordinator_event_webhook_url_invalid");
|
|
126
|
+
}
|
|
127
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
128
|
+
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && LOOPBACK_HOSTS.has(hostname)))
|
|
129
|
+
throw new Error("coordinator_event_webhook_url_not_allowed");
|
|
130
|
+
const rawSessions = webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_SESSION_IDS");
|
|
131
|
+
const sessionIds = rawSessions
|
|
132
|
+
? new Set(
|
|
133
|
+
rawSessions
|
|
134
|
+
.split(",")
|
|
135
|
+
.map(value => value.trim())
|
|
136
|
+
.filter(value => /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(value)),
|
|
137
|
+
)
|
|
138
|
+
: null;
|
|
139
|
+
const tokenFile = webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_TOKEN_FILE") ?? "";
|
|
140
|
+
if (tokenFile !== "" && !path.isAbsolute(tokenFile)) throw new Error("coordinator_event_webhook_token_file_invalid");
|
|
141
|
+
const timeoutRaw = Number.parseInt(
|
|
142
|
+
webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_TIMEOUT_MS") ?? "",
|
|
143
|
+
10,
|
|
144
|
+
);
|
|
145
|
+
const timeoutMs =
|
|
146
|
+
Number.isFinite(timeoutRaw) && timeoutRaw > 0
|
|
147
|
+
? Math.min(timeoutRaw, MAX_EVENT_WEBHOOK_TIMEOUT_MS)
|
|
148
|
+
: DEFAULT_EVENT_WEBHOOK_TIMEOUT_MS;
|
|
149
|
+
const attemptsRaw = Number.parseInt(
|
|
150
|
+
webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_MAX_ATTEMPTS") ?? "",
|
|
151
|
+
10,
|
|
152
|
+
);
|
|
153
|
+
const maxAttempts =
|
|
154
|
+
Number.isFinite(attemptsRaw) && attemptsRaw > 0
|
|
155
|
+
? Math.min(attemptsRaw, MAX_EVENT_WEBHOOK_MAX_ATTEMPTS)
|
|
156
|
+
: DEFAULT_EVENT_WEBHOOK_MAX_ATTEMPTS;
|
|
157
|
+
return { url: rawUrl, tokenFile: tokenFile || null, sessionIds, timeoutMs, maxAttempts };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function eventWebhookScopeApplies(config: EventWebhookConfig, event: { session_id?: string }): boolean {
|
|
161
|
+
return config.sessionIds === null || (event.session_id !== undefined && config.sessionIds.has(event.session_id));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function recordPath(namespaceDir: string, eventId: string): string {
|
|
165
|
+
if (!/^event-\d{12,}$/.test(eventId)) throw new Error("coordinator_event_webhook_event_id_invalid");
|
|
166
|
+
return path.join(namespaceDir, "webhook-outbox", `${eventId}.json`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function readRecord(file: string, eventId: string): Promise<WebhookDeliveryRecord | null> {
|
|
170
|
+
let source: string;
|
|
171
|
+
try {
|
|
172
|
+
source = await fs.readFile(file, "utf8");
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const parsed: unknown = JSON.parse(source);
|
|
179
|
+
return isWebhookDeliveryRecord(parsed, eventId) ? parsed : null;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error instanceof SyntaxError) return null;
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function writeRecord(file: string, record: WebhookDeliveryRecord): Promise<void> {
|
|
187
|
+
await writeCoordinatorAtomic(file, JSON.stringify(record));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function webhookErrorSummary(error: string | null): string | null {
|
|
191
|
+
if (error === null) return null;
|
|
192
|
+
const normalized = error.replace(/[\r\n]+/g, " ").trim();
|
|
193
|
+
return normalized.length > WEBHOOK_ERROR_CAP ? `${normalized.slice(0, WEBHOOK_ERROR_CAP - 3)}...` : normalized;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function backoffDelayMs(attempt: number): number {
|
|
197
|
+
return Math.min(WEBHOOK_BACKOFF_BASE_MS * 2 ** (attempt - 1), WEBHOOK_BACKOFF_CAP_MS);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function readWebhookToken(tokenFile: string | null): Promise<string | null> {
|
|
201
|
+
if (tokenFile === null) return null;
|
|
202
|
+
try {
|
|
203
|
+
return (await fs.readFile(tokenFile, "utf8")).trim();
|
|
204
|
+
} catch {
|
|
205
|
+
throw new Error("coordinator_event_webhook_token_file_unreadable");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Records the row in the durable outbox and, unless the row is already
|
|
211
|
+
* delivered or exhausted, performs one bounded delivery attempt series. The
|
|
212
|
+
* returned promise settles only after bounded attempts/backoff, so a dead sink
|
|
213
|
+
* can never wedge the caller's persistence; callers run it off the append path.
|
|
214
|
+
*/
|
|
215
|
+
export async function deliverEventWebhook(
|
|
216
|
+
namespaceDir: string,
|
|
217
|
+
config: EventWebhookConfig,
|
|
218
|
+
event: { id: string; session_id?: string },
|
|
219
|
+
renderBody: () => string,
|
|
220
|
+
delivery: WebhookDelivery,
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
if (!eventWebhookScopeApplies(config, event)) return;
|
|
223
|
+
const file = recordPath(namespaceDir, event.id);
|
|
224
|
+
return await withFileLock(file, async () => {
|
|
225
|
+
const now0 = delivery.now();
|
|
226
|
+
let record = await readRecord(file, event.id);
|
|
227
|
+
if (record === null) {
|
|
228
|
+
record = {
|
|
229
|
+
schema_version: 1,
|
|
230
|
+
event_id: event.id,
|
|
231
|
+
status: "pending",
|
|
232
|
+
attempts: 0,
|
|
233
|
+
created_at: new Date(now0).toISOString(),
|
|
234
|
+
updated_at: new Date(now0).toISOString(),
|
|
235
|
+
last_error: null,
|
|
236
|
+
};
|
|
237
|
+
await writeRecord(file, record);
|
|
238
|
+
} else if (record.status !== "pending") {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const token = await readWebhookToken(config.tokenFile);
|
|
242
|
+
const body = renderBody();
|
|
243
|
+
while (record.attempts < config.maxAttempts) {
|
|
244
|
+
record.attempts += 1;
|
|
245
|
+
const controller = new AbortController();
|
|
246
|
+
const timer = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
247
|
+
timer.unref?.();
|
|
248
|
+
let outcome: WebhookPostOutcome;
|
|
249
|
+
try {
|
|
250
|
+
outcome = await delivery.post(body, {
|
|
251
|
+
url: config.url,
|
|
252
|
+
token,
|
|
253
|
+
timeoutMs: config.timeoutMs,
|
|
254
|
+
signal: controller.signal,
|
|
255
|
+
});
|
|
256
|
+
} catch (error) {
|
|
257
|
+
outcome = { ok: false, status: null, error: error instanceof Error ? error.message : String(error) };
|
|
258
|
+
} finally {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
}
|
|
261
|
+
const ts = new Date(delivery.now()).toISOString();
|
|
262
|
+
record.updated_at = ts;
|
|
263
|
+
record.last_error = outcome.ok ? null : webhookErrorSummary(outcome.error);
|
|
264
|
+
if (outcome.ok) {
|
|
265
|
+
record.status = "delivered";
|
|
266
|
+
await writeRecord(file, record);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (record.attempts >= config.maxAttempts) {
|
|
270
|
+
record.status = "failed";
|
|
271
|
+
await writeRecord(file, record);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
await writeRecord(file, record);
|
|
275
|
+
await delivery.sleep(backoffDelayMs(record.attempts));
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|