approval-md 0.2.0 → 0.4.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/README.md +85 -877
- package/SPEC.md +150 -19
- package/cli.js +66 -7
- package/dist/src/adapters/smtp.d.ts +18 -0
- package/dist/src/adapters/smtp.js +24 -3
- package/dist/src/adapters/smtp.js.map +1 -1
- package/dist/src/channels/contract.d.ts +53 -1
- package/dist/src/channels/contract.js +304 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/relay-server.d.ts +90 -0
- package/dist/src/channels/relay-server.js +295 -0
- package/dist/src/channels/relay-server.js.map +1 -0
- package/dist/src/channels/relay.d.ts +273 -0
- package/dist/src/channels/relay.js +564 -0
- package/dist/src/channels/relay.js.map +1 -0
- package/dist/src/channels/tagging.js +6 -1
- package/dist/src/channels/tagging.js.map +1 -1
- package/dist/src/channels/telegram-webhook.d.ts +273 -0
- package/dist/src/channels/telegram-webhook.js +536 -0
- package/dist/src/channels/telegram-webhook.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +394 -12
- package/dist/src/channels/telegram.js +621 -68
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +9 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/amend.js +564 -34
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.d.ts +9 -0
- package/dist/src/cli/attest.js +177 -13
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/channel-relay.d.ts +108 -0
- package/dist/src/cli/channel-relay.js +334 -0
- package/dist/src/cli/channel-relay.js.map +1 -0
- package/dist/src/cli/channel-telegram-webhook.d.ts +273 -0
- package/dist/src/cli/channel-telegram-webhook.js +709 -0
- package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +347 -28
- package/dist/src/cli/channel-telegram.js +763 -28
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +10 -1
- package/dist/src/cli/channel.js +14 -1
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/codex-bridge.d.ts +819 -0
- package/dist/src/cli/codex-bridge.js +1607 -0
- package/dist/src/cli/codex-bridge.js.map +1 -0
- package/dist/src/cli/codex.d.ts +1 -1
- package/dist/src/cli/codex.js +304 -7
- package/dist/src/cli/codex.js.map +1 -1
- package/dist/src/cli/daemon.js +41 -6
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +63 -0
- package/dist/src/cli/doctor.js +1322 -122
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.d.ts +17 -1
- package/dist/src/cli/execute.js +366 -22
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/exit-codes.d.ts +12 -0
- package/dist/src/cli/exit-codes.js +13 -0
- package/dist/src/cli/exit-codes.js.map +1 -1
- package/dist/src/cli/gate.d.ts +2 -0
- package/dist/src/cli/gate.js +233 -2
- package/dist/src/cli/gate.js.map +1 -1
- package/dist/src/cli/help.d.ts +14 -2
- package/dist/src/cli/help.js +395 -82
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +49 -1
- package/dist/src/cli/hook-codex.js +60 -1
- package/dist/src/cli/hook-codex.js.map +1 -1
- package/dist/src/cli/hook.d.ts +828 -4
- package/dist/src/cli/hook.js +2972 -142
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/init.js +16 -7
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.js +24 -3
- package/dist/src/cli/instructions.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +55 -0
- package/dist/src/cli/log-anchor.js +199 -60
- package/dist/src/cli/log-anchor.js.map +1 -1
- package/dist/src/cli/main.d.ts +0 -12
- package/dist/src/cli/main.js +56 -3
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/muse.d.ts +2 -0
- package/dist/src/cli/muse.js +62 -0
- package/dist/src/cli/muse.js.map +1 -0
- package/dist/src/cli/payload.js +79 -1
- package/dist/src/cli/payload.js.map +1 -1
- package/dist/src/cli/policy-apply.d.ts +195 -0
- package/dist/src/cli/policy-apply.js +573 -0
- package/dist/src/cli/policy-apply.js.map +1 -0
- package/dist/src/cli/policy.js +14 -1
- package/dist/src/cli/policy.js.map +1 -1
- package/dist/src/cli/preflight.d.ts +151 -13
- package/dist/src/cli/preflight.js +398 -41
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +1 -1
- package/dist/src/cli/scaffold.js +7 -1
- package/dist/src/cli/scaffold.js.map +1 -1
- package/dist/src/cli/serve.d.ts +53 -0
- package/dist/src/cli/serve.js +270 -0
- package/dist/src/cli/serve.js.map +1 -0
- package/dist/src/cli/setup-channel.d.ts +9 -0
- package/dist/src/cli/setup-channel.js +28 -1
- package/dist/src/cli/setup-channel.js.map +1 -1
- package/dist/src/cli/setup-common.d.ts +3 -1
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup.d.ts +2 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/up.js +134 -51
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +2 -0
- package/dist/src/cli/verb-registry.js +510 -47
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/broker.d.ts +229 -0
- package/dist/src/codex/broker.js +548 -0
- package/dist/src/codex/broker.js.map +1 -0
- package/dist/src/codex/runner.d.ts +178 -0
- package/dist/src/codex/runner.js +231 -0
- package/dist/src/codex/runner.js.map +1 -0
- package/dist/src/codex/serve.d.ts +56 -0
- package/dist/src/codex/serve.js +98 -0
- package/dist/src/codex/serve.js.map +1 -0
- package/dist/src/codex/workspace-commit.d.ts +219 -0
- package/dist/src/codex/workspace-commit.js +549 -0
- package/dist/src/codex/workspace-commit.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +51 -0
- package/dist/src/core/advance-cycle.js +66 -2
- package/dist/src/core/advance-cycle.js.map +1 -1
- package/dist/src/core/agents-md.d.ts +20 -18
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/attest-bootstrap.d.ts +95 -0
- package/dist/src/core/attest-bootstrap.js +135 -0
- package/dist/src/core/attest-bootstrap.js.map +1 -0
- package/dist/src/core/attest.d.ts +234 -2
- package/dist/src/core/attest.js +342 -7
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +73 -3
- package/dist/src/core/audit.js +65 -5
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-lease.d.ts +322 -0
- package/dist/src/core/channel-lease.js +663 -0
- package/dist/src/core/channel-lease.js.map +1 -0
- package/dist/src/core/channel-owner.d.ts +213 -0
- package/dist/src/core/channel-owner.js +358 -0
- package/dist/src/core/channel-owner.js.map +1 -0
- package/dist/src/core/command-class.d.ts +205 -0
- package/dist/src/core/command-class.js +1433 -28
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/commit-guard.d.ts +272 -0
- package/dist/src/core/commit-guard.js +424 -0
- package/dist/src/core/commit-guard.js.map +1 -0
- package/dist/src/core/daemon-actor.d.ts +45 -0
- package/dist/src/core/daemon-actor.js +54 -0
- package/dist/src/core/daemon-actor.js.map +1 -0
- package/dist/src/core/daemon-host.d.ts +178 -0
- package/dist/src/core/daemon-host.js +228 -0
- package/dist/src/core/daemon-host.js.map +1 -0
- package/dist/src/core/daemon-identity.d.ts +238 -0
- package/dist/src/core/daemon-identity.js +252 -0
- package/dist/src/core/daemon-identity.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +109 -8
- package/dist/src/core/dark-session.js +266 -82
- package/dist/src/core/dark-session.js.map +1 -1
- package/dist/src/core/decision-refusal.d.ts +23 -2
- package/dist/src/core/decision-refusal.js +24 -2
- package/dist/src/core/decision-refusal.js.map +1 -1
- package/dist/src/core/env-file.d.ts +5 -0
- package/dist/src/core/env-file.js +60 -1
- package/dist/src/core/env-file.js.map +1 -1
- package/dist/src/core/execute.d.ts +15 -2
- package/dist/src/core/execute.js +15 -2
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/gate.d.ts +392 -6
- package/dist/src/core/gate.js +852 -24
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/gesture-refusal.d.ts +166 -0
- package/dist/src/core/gesture-refusal.js +188 -0
- package/dist/src/core/gesture-refusal.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +128 -4
- package/dist/src/core/harness-version.js +181 -12
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +82 -3
- package/dist/src/core/harness-wait.js +89 -3
- package/dist/src/core/harness-wait.js.map +1 -1
- package/dist/src/core/instance.d.ts +59 -2
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/log-subscribe.d.ts +21 -0
- package/dist/src/core/log-subscribe.js +16 -6
- package/dist/src/core/log-subscribe.js.map +1 -1
- package/dist/src/core/log-write-layer.d.ts +55 -0
- package/dist/src/core/log-write-layer.js +91 -0
- package/dist/src/core/log-write-layer.js.map +1 -0
- package/dist/src/core/log.d.ts +89 -22
- package/dist/src/core/log.js +197 -19
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/payload-census.d.ts +51 -0
- package/dist/src/core/payload-census.js +125 -2
- package/dist/src/core/payload-census.js.map +1 -1
- package/dist/src/core/payload-store.d.ts +31 -0
- package/dist/src/core/payload-store.js +113 -8
- package/dist/src/core/payload-store.js.map +1 -1
- package/dist/src/core/payload.d.ts +18 -3
- package/dist/src/core/payload.js +20 -4
- package/dist/src/core/payload.js.map +1 -1
- package/dist/src/core/policy-diff.d.ts +10 -0
- package/dist/src/core/policy-diff.js +10 -2
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +17 -1
- package/dist/src/core/policy-explain.js +48 -1
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +65 -1
- package/dist/src/core/policy-load.js +21 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +81 -0
- package/dist/src/core/policy-match.js +87 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +75 -0
- package/dist/src/core/policy-proposal.js +110 -4
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/protected-path-guard.d.ts +117 -4
- package/dist/src/core/protected-path-guard.js +362 -48
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/question-preempted.d.ts +141 -0
- package/dist/src/core/question-preempted.js +152 -0
- package/dist/src/core/question-preempted.js.map +1 -0
- package/dist/src/core/read-scope.d.ts +182 -0
- package/dist/src/core/read-scope.js +256 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/run-payload.d.ts +160 -0
- package/dist/src/core/run-payload.js +304 -0
- package/dist/src/core/run-payload.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +81 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/sender-identity.d.ts +501 -0
- package/dist/src/core/sender-identity.js +609 -0
- package/dist/src/core/sender-identity.js.map +1 -0
- package/dist/src/core/shlex.d.ts +102 -0
- package/dist/src/core/shlex.js +159 -0
- package/dist/src/core/shlex.js.map +1 -0
- package/dist/src/core/state.d.ts +52 -1
- package/dist/src/core/state.js +52 -6
- package/dist/src/core/state.js.map +1 -1
- package/dist/src/core/telegram-config.d.ts +79 -0
- package/dist/src/core/telegram-config.js +108 -0
- package/dist/src/core/telegram-config.js.map +1 -1
- package/dist/src/core/values.d.ts +18 -8
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/core/verify.d.ts +23 -1
- package/dist/src/core/verify.js +20 -2
- package/dist/src/core/verify.js.map +1 -1
- package/dist/src/daemon/advance.d.ts +10 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/audit.d.ts +80 -5
- package/dist/src/daemon/audit.js +126 -6
- package/dist/src/daemon/audit.js.map +1 -1
- package/dist/src/daemon/daemon.d.ts +174 -2
- package/dist/src/daemon/daemon.js +307 -7
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +13 -2
- package/dist/src/daemon/projection.js +39 -6
- package/dist/src/daemon/projection.js.map +1 -1
- package/dist/src/mcp/server.d.ts +39 -3
- package/dist/src/mcp/server.js +30 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/muse/server.d.ts +16 -0
- package/dist/src/muse/server.js +238 -0
- package/dist/src/muse/server.js.map +1 -0
- package/dist/src/serve/archive.d.ts +158 -0
- package/dist/src/serve/archive.js +354 -0
- package/dist/src/serve/archive.js.map +1 -0
- package/dist/src/serve/arguments.d.ts +93 -0
- package/dist/src/serve/arguments.js +228 -0
- package/dist/src/serve/arguments.js.map +1 -0
- package/dist/src/serve/credentials.d.ts +91 -0
- package/dist/src/serve/credentials.js +143 -0
- package/dist/src/serve/credentials.js.map +1 -0
- package/dist/src/serve/follow.d.ts +86 -0
- package/dist/src/serve/follow.js +101 -0
- package/dist/src/serve/follow.js.map +1 -0
- package/dist/src/serve/hook-thread.d.ts +134 -0
- package/dist/src/serve/hook-thread.js +333 -0
- package/dist/src/serve/hook-thread.js.map +1 -0
- package/dist/src/serve/hook-worker.d.ts +134 -0
- package/dist/src/serve/hook-worker.js +173 -0
- package/dist/src/serve/hook-worker.js.map +1 -0
- package/dist/src/serve/server.d.ts +412 -0
- package/dist/src/serve/server.js +1251 -0
- package/dist/src/serve/server.js.map +1 -0
- package/docs/README-extended.md +766 -0
- package/docs/cli-reference.md +2563 -73
- package/docs/codex-enforced-session.md +75 -2
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +6 -2
- package/schema/event.schema.json +595 -9
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
- package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
- package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
- package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
- package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
- package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
- package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
- package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
- package/schema/fixtures/event/valid/daemon-identity.json +15 -0
- package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
- package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
- package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
- package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
- package/schema/fixtures/policy/valid/canonical.json +1 -1
- package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
- package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
- package/schema/fixtures/policy-md/valid/canonical.md +1 -1
- package/schema/fixtures/policy-md/valid/with-values.md +5 -7
- package/schema/fixtures/values/invalid/class-shaped.json +1 -1
- package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
- package/schema/fixtures/values/invalid/non-string-item.json +1 -1
- package/schema/fixtures/values/invalid/over-cap.json +1 -1
- package/schema/fixtures/values/invalid/unknown-key.json +1 -1
- package/schema/fixtures/values/invalid/version-float.json +1 -0
- package/schema/fixtures/values/invalid/version-integer.json +1 -0
- package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +2 -3
- package/schema/fixtures/values/valid/full.json +5 -7
- package/schema/fixtures/values/valid/minimal.json +1 -1
- package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
- package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
- package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
- package/schema/fixtures/values-md/invalid/version-1.md +69 -0
- package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
- package/schema/fixtures/values-md/valid/absent.md +1 -1
- package/schema/fixtures/values-md/valid/with-values.md +5 -7
- package/schema/policy.schema.json +100 -4
- package/schema/values.schema.json +7 -11
- package/schema/fixtures/values/invalid/version-string.json +0 -1
|
@@ -92,6 +92,10 @@ import { watch } from "node:fs";
|
|
|
92
92
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
93
93
|
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
|
94
94
|
import { writeQueue } from "../channels/render-queue.js";
|
|
95
|
+
// APRV-403. The classifier is APRV-381's, shared rather than copied: one closed
|
|
96
|
+
// list of transient append codes means the drift scan and the sampling sweep
|
|
97
|
+
// cannot come to disagree about which failures a retry fixes.
|
|
98
|
+
import { isTransientAppendError } from "../core/audit.js";
|
|
95
99
|
import { tick as readClock } from "../core/clock.js";
|
|
96
100
|
import { parseFrontmatter, readTaskFile } from "../core/frontmatter.js";
|
|
97
101
|
import { expire } from "../core/gate.js";
|
|
@@ -104,6 +108,9 @@ import { processReadCache, readVerifiedRecords, } from "../core/state.js";
|
|
|
104
108
|
import { validate } from "../core/validate.js";
|
|
105
109
|
import { publishedState } from "../cli/log-advance.js";
|
|
106
110
|
import { isAdvanceBookkeeping } from "../core/advance-cycle.js";
|
|
111
|
+
import { markDaemonProcess } from "../core/daemon-actor.js";
|
|
112
|
+
import { declareDaemonIdentityFor, refreshDaemonAllowlist, } from "../core/daemon-host.js";
|
|
113
|
+
import { daemonIdentity } from "../core/daemon-identity.js";
|
|
107
114
|
import { repoRoot } from "../cli/git-scope.js";
|
|
108
115
|
import { checkLogAnchor, resolveAnchor } from "../cli/log-anchor.js";
|
|
109
116
|
import { checkLogCheckpoints, checkpointPolicyOf, } from "../core/checkpoint.js";
|
|
@@ -121,6 +128,61 @@ import { driftAlreadyLogged, lapsedRequests, latestRegistration, taskEnvelopeSta
|
|
|
121
128
|
export const DAEMON_ACTOR = "system:daemon";
|
|
122
129
|
/** Backlog.md's conventional task folder, relative to the working directory. */
|
|
123
130
|
export { DEFAULT_TASKS_DIR } from "../core/registration.js";
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// The drift scan's deferral memory (APRV-403)
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
/**
|
|
135
|
+
* `envelope.drift` records this PROCESS deferred and has not yet seen appended,
|
|
136
|
+
* keyed by log path, task and drift reason.
|
|
137
|
+
*
|
|
138
|
+
* Exactly the standing `daemon/audit.ts` gives its own deferral memory, and
|
|
139
|
+
* deliberately the same shape: process-lifetime, memory-only, and a
|
|
140
|
+
* de-duplicator for OUTPUT. Nothing here is consulted before an append, and
|
|
141
|
+
* nothing is gated or suppressed by it. The RETRY needs no memory to happen,
|
|
142
|
+
* because the drift scan re-derives the whole question from the verified log on
|
|
143
|
+
* every tick; what this buys is the word "retry" on one line, so an operator can
|
|
144
|
+
* pair the deferral they read with the record that resolved it.
|
|
145
|
+
*
|
|
146
|
+
* It is consulted in exactly one other place, and that one is not cosmetic: the
|
|
147
|
+
* write-back pass skips a file whose drift record this tick could not append, so
|
|
148
|
+
* the file is not repaired off the record (SPEC.md §6.3's fixed order). Losing
|
|
149
|
+
* the memory there costs a repair one tick, never a lost record.
|
|
150
|
+
*
|
|
151
|
+
* Keyed by log path as well as task, because one process can be pointed at more
|
|
152
|
+
* than one gate over its lifetime and a task id is only unique inside one.
|
|
153
|
+
* Bounded, so a long run cannot accumulate entries for a file that drifts, is
|
|
154
|
+
* deferred, and is then deleted.
|
|
155
|
+
*/
|
|
156
|
+
const driftDeferrals = new Set();
|
|
157
|
+
/** How many deferred drift records one process remembers for the retry wording. */
|
|
158
|
+
const DRIFT_DEFERRAL_LIMIT = 256;
|
|
159
|
+
function driftDeferralKey(logPath, task, reason) {
|
|
160
|
+
return JSON.stringify([logPath, task, reason]);
|
|
161
|
+
}
|
|
162
|
+
function rememberDriftDeferral(logPath, task, reason) {
|
|
163
|
+
const key = driftDeferralKey(logPath, task, reason);
|
|
164
|
+
if (driftDeferrals.has(key))
|
|
165
|
+
return;
|
|
166
|
+
// A `Set` iterates in insertion order, so "the oldest" needs no timestamp.
|
|
167
|
+
if (driftDeferrals.size >= DRIFT_DEFERRAL_LIMIT) {
|
|
168
|
+
const oldest = driftDeferrals.values().next();
|
|
169
|
+
if (!oldest.done)
|
|
170
|
+
driftDeferrals.delete(oldest.value);
|
|
171
|
+
}
|
|
172
|
+
driftDeferrals.add(key);
|
|
173
|
+
}
|
|
174
|
+
/** Whether this process deferred this record, consuming the memory of it. */
|
|
175
|
+
function takeDriftDeferral(logPath, task, reason) {
|
|
176
|
+
return driftDeferrals.delete(driftDeferralKey(logPath, task, reason));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Forget every deferral. Exported for tests, used nowhere else: the memory is
|
|
180
|
+
* output bookkeeping, so a suite that shares a process needs to be able to start
|
|
181
|
+
* from nothing and to exercise what a restarted daemon does.
|
|
182
|
+
*/
|
|
183
|
+
export function resetDriftDeferrals() {
|
|
184
|
+
driftDeferrals.clear();
|
|
185
|
+
}
|
|
124
186
|
/** How often the daemon looks, absent any watcher event. */
|
|
125
187
|
export const DEFAULT_INTERVAL_MS = 30_000;
|
|
126
188
|
/** How long a burst of watcher events is allowed to settle before a tick. */
|
|
@@ -236,6 +298,19 @@ export const DAEMON_WARNING_CODES = [
|
|
|
236
298
|
* repair, and the next tick asks again.
|
|
237
299
|
*/
|
|
238
300
|
"anchor-behind",
|
|
301
|
+
/**
|
|
302
|
+
* The working log changed between this tick's opening read and the anchor
|
|
303
|
+
* check's own read of the file, so the check read it again (APRV-389).
|
|
304
|
+
*
|
|
305
|
+
* A line rather than a stop, and rather than a silence. `approval log sync`
|
|
306
|
+
* and a records pull both rewrite `events.jsonl` under a running daemon
|
|
307
|
+
* (baseline, fast-forward, restore), and this loop reads without the append
|
|
308
|
+
* lock, so a tick can hold a view the file has already moved past. That is a
|
|
309
|
+
* fact about reads, never a fork: the anchor comparison is made again from the
|
|
310
|
+
* file and the verdict beside this line is the second read's. The line is the
|
|
311
|
+
* one an operator acts on when the writer was not a sync they ran.
|
|
312
|
+
*/
|
|
313
|
+
"anchor-reread",
|
|
239
314
|
/**
|
|
240
315
|
* `audit.checkpoint_every` says a human-signed checkpoint is due and the log
|
|
241
316
|
* carries none that recent (APRV-220). A WARNING and never a stop, at every
|
|
@@ -252,6 +327,53 @@ export const DAEMON_WARNING_CODES = [
|
|
|
252
327
|
* month of taps.
|
|
253
328
|
*/
|
|
254
329
|
"draw-unavailable",
|
|
330
|
+
/**
|
|
331
|
+
* This daemon's own id is not one the attested policy's `daemons` list admits,
|
|
332
|
+
* or the id it was launched with is not a usable id at all (APRV-383).
|
|
333
|
+
*
|
|
334
|
+
* A warning rather than a stop, and the distinction matters: the refusal itself
|
|
335
|
+
* happens at the write boundary, one append at a time, so the loop keeps
|
|
336
|
+
* reading, keeps rendering the queue and keeps reporting, and writes nothing.
|
|
337
|
+
* The line exists because an operator should learn that from the first tick
|
|
338
|
+
* rather than from a day of `append-refused` messages. The repair is a line in
|
|
339
|
+
* a policy a human re-attests, or a launch environment fixed, and neither is
|
|
340
|
+
* something this process can do for itself.
|
|
341
|
+
*/
|
|
342
|
+
"daemon-identity-refused",
|
|
343
|
+
/**
|
|
344
|
+
* An `audit.sampled` append met a held lock or a moved head, so the sample was
|
|
345
|
+
* postponed to the next tick (APRV-381). Appended to this union, so no existing
|
|
346
|
+
* entry changed meaning.
|
|
347
|
+
*
|
|
348
|
+
* Distinct from `append-refused` because the operator's next move is different:
|
|
349
|
+
* `append-refused` on a sample is a fact about the record or the file and wants
|
|
350
|
+
* a person, and this is contention between writers that the next sweep resolves
|
|
351
|
+
* by re-deriving the sample from the log. It is still a warning rather than a
|
|
352
|
+
* silence, because a tick that keeps deferring is a log under contention and
|
|
353
|
+
* that is a thing to know, and the `sampled` line that closes it carries
|
|
354
|
+
* `retry`.
|
|
355
|
+
*/
|
|
356
|
+
"sample-deferred",
|
|
357
|
+
/**
|
|
358
|
+
* An `envelope.drift` append met a held lock or a moved head, so the record was
|
|
359
|
+
* postponed to the next tick (APRV-403). Appended to this union, so no existing
|
|
360
|
+
* entry changed meaning.
|
|
361
|
+
*
|
|
362
|
+
* The same split APRV-381 made for `audit.sampled`, for the same reason and
|
|
363
|
+
* from the same closed list of transient codes. Found while landing that task:
|
|
364
|
+
* the end-to-end test that holds the append lockfile for a whole tick showed
|
|
365
|
+
* the drift scan printing `append-refused … (lock-timeout)`, which is true and
|
|
366
|
+
* reads as a lost record, while the next tick re-derived the drift from the
|
|
367
|
+
* verified log and appended it. An operator's next move differs between the
|
|
368
|
+
* two: `append-refused` on a drift record is a fact about the record or the
|
|
369
|
+
* file and wants a person, and this is contention between writers that the
|
|
370
|
+
* scan resolves by itself.
|
|
371
|
+
*
|
|
372
|
+
* Still a warning rather than a silence: a scan that keeps deferring is a log
|
|
373
|
+
* under contention, which is a thing to know. The `drift` line that closes it
|
|
374
|
+
* carries `retry`.
|
|
375
|
+
*/
|
|
376
|
+
"drift-deferred",
|
|
255
377
|
];
|
|
256
378
|
function errorMessage(cause) {
|
|
257
379
|
return cause instanceof Error ? cause.message : String(cause);
|
|
@@ -354,10 +476,37 @@ export class Daemon {
|
|
|
354
476
|
previousSelfWrites = new Set();
|
|
355
477
|
settle = null;
|
|
356
478
|
finished = false;
|
|
479
|
+
/**
|
|
480
|
+
* What this process resolved as its own daemon id (APRV-383), kept so the
|
|
481
|
+
* `started` line can name it and the startup check can say what is wrong with
|
|
482
|
+
* it. The id in FORCE lives in `core/daemon-identity.ts`'s process state, which
|
|
483
|
+
* is what the write boundary reads; this field is the resolution that produced
|
|
484
|
+
* it, and nothing reads it to decide anything.
|
|
485
|
+
*/
|
|
486
|
+
identity;
|
|
487
|
+
/** Has the unlisted-id warning already been said this run? */
|
|
488
|
+
reportedIdentityRefusal = false;
|
|
357
489
|
/** Whether {@link DaemonOptions.draw} actually bound (APRV-208). */
|
|
358
490
|
drawServing = false;
|
|
359
491
|
constructor(options) {
|
|
360
492
|
this.options = options;
|
|
493
|
+
// APRV-382. This process is the daemon, and the gated advance path asks
|
|
494
|
+
// `core/daemon-actor.ts` rather than being told: `log.advance.daemon` is
|
|
495
|
+
// autonomous in this repository's policy and `log.advance` is not, so which
|
|
496
|
+
// class a cycle asks under must not be a field any caller can set. Marked
|
|
497
|
+
// at CONSTRUCTION rather than in `run()` so that a shutdown flush, a
|
|
498
|
+
// `--once` tick and a test driving this object one tick at a time are all
|
|
499
|
+
// the same actor as the loop.
|
|
500
|
+
markDaemonProcess();
|
|
501
|
+
// APRV-383. WHICH daemon this process is, declared in the same breath and at
|
|
502
|
+
// the same moment for the same reason. The resolution is the launch
|
|
503
|
+
// environment's `APPROVAL_DAEMON_ID` or the id derived from this log's
|
|
504
|
+
// instance; an unusable declared id is declared as an identity with NO id,
|
|
505
|
+
// which refuses every append at the write boundary rather than leaving a
|
|
506
|
+
// daemon writing records nothing can attribute. The CLI verbs refuse to start
|
|
507
|
+
// on the same resolution, so this is the belt on that brace, and it is what
|
|
508
|
+
// covers a `Daemon` constructed directly by an embedder or a test.
|
|
509
|
+
this.identity = declareDaemonIdentityFor(this.options.logPath);
|
|
361
510
|
}
|
|
362
511
|
/** Run until stopped (or, with `once`, for exactly one tick). */
|
|
363
512
|
run() {
|
|
@@ -409,7 +558,19 @@ export class Daemon {
|
|
|
409
558
|
// would be a write nobody asked for yet. Seeded into `reportedDangling`
|
|
410
559
|
// so the tick that immediately follows does not repeat it.
|
|
411
560
|
dangling_advances: this.listDanglingAdvancesAtStartup(),
|
|
561
|
+
// APRV-383. Resolved in the constructor, printed here, and `null` for an
|
|
562
|
+
// id this process could not use at all.
|
|
563
|
+
daemon: this.identity.ok ? this.identity.id : null,
|
|
412
564
|
});
|
|
565
|
+
// APRV-383. Said once, before the first tick, when this run cannot write:
|
|
566
|
+
// a bad launch id is knowable without reading anything, and the allowlist
|
|
567
|
+
// is knowable from the attested policy the first tick resolves. An operator
|
|
568
|
+
// who reads it here does not have to infer it from a day of
|
|
569
|
+
// `append-refused` lines.
|
|
570
|
+
if (!this.identity.ok) {
|
|
571
|
+
this.reportedIdentityRefusal = true;
|
|
572
|
+
this.warn("daemon-identity-refused", `${this.identity.message}. Every append this run makes will be refused \`daemon-id-invalid\` until that is fixed; nothing is written and nothing is retried`);
|
|
573
|
+
}
|
|
413
574
|
const outcome = this.tick();
|
|
414
575
|
if (outcome !== null) {
|
|
415
576
|
this.finish(outcome);
|
|
@@ -720,6 +881,14 @@ export class Daemon {
|
|
|
720
881
|
const opening = this.read();
|
|
721
882
|
if (!opening.ok)
|
|
722
883
|
return this.fatal(opening);
|
|
884
|
+
// WHICH daemons this log's ATTESTED policy admits (APRV-383), put in force
|
|
885
|
+
// before anything below appends. Resolved every tick, from this tick's own
|
|
886
|
+
// verified read, so a `daemons` list a human adds and attests takes effect
|
|
887
|
+
// on the next tick rather than at the next restart; a resolution that FAILS
|
|
888
|
+
// (the policy will not load, or is not attested) leaves the previous one
|
|
889
|
+
// standing, so a policy that becomes unreadable is not a way out of the list
|
|
890
|
+
// it carried a moment ago.
|
|
891
|
+
this.refreshIdentity(opening.records);
|
|
723
892
|
// The anchor check (APRV-219), on the full re-proof cadence and on the
|
|
724
893
|
// first tick, which is always a cold walk. Placed immediately after the
|
|
725
894
|
// opening read and before any append this tick could make: a log that
|
|
@@ -731,6 +900,17 @@ export class Daemon {
|
|
|
731
900
|
if (anchor.status === "diverged") {
|
|
732
901
|
return { kind: "anchor-diverged", message: anchor.message };
|
|
733
902
|
}
|
|
903
|
+
// The log was rewritten between this tick's opening read and the
|
|
904
|
+
// check's own read of the file (APRV-389), so the check read it again
|
|
905
|
+
// and this verdict is the second read's. ONE line, and no stop: a
|
|
906
|
+
// `log sync` or a records pull replacing `events.jsonl` under this
|
|
907
|
+
// process is housekeeping on the container, the append lock keeps it
|
|
908
|
+
// from interleaving with any append, and the daemon's every other step
|
|
909
|
+
// re-reads the log for itself anyway. The line is here because a
|
|
910
|
+
// writer nobody expected is a thing to know, and because this is the
|
|
911
|
+
// read that used to be reported as a fork.
|
|
912
|
+
if (anchor.reread !== undefined)
|
|
913
|
+
this.warn("anchor-reread", anchor.reread.detail);
|
|
734
914
|
if (anchor.status === "behind") {
|
|
735
915
|
this.warn("anchor-behind", `${anchor.detail}. Nothing was appended onto the shorter chain by this check; run \`approval log sync\` before this daemon writes anything`);
|
|
736
916
|
}
|
|
@@ -789,14 +969,19 @@ export class Daemon {
|
|
|
789
969
|
...(this.options.schemaDir === undefined ? {} : { schemaDir: this.options.schemaDir }),
|
|
790
970
|
...(this.options.clock === undefined ? {} : { clock: this.options.clock }),
|
|
791
971
|
warn: (message) => this.warn("append-refused", message),
|
|
972
|
+
// APRV-381. Contention with another writer gets its own code, so the
|
|
973
|
+
// window an operator reads distinguishes "this sample is coming" from
|
|
974
|
+
// "an audit record was not written".
|
|
975
|
+
defer: (message) => this.warn("sample-deferred", message),
|
|
792
976
|
// One line per sample appended (APRV-57). The sweep names no event; it
|
|
793
977
|
// hands back what it wrote and the loop says it in the loop's own words.
|
|
794
|
-
sampled: (sample) => this.emit({
|
|
978
|
+
sampled: (sample, retry) => this.emit({
|
|
795
979
|
event: "sampled",
|
|
796
980
|
action_key: sample.candidate.actionKey,
|
|
797
981
|
task: sample.candidate.task,
|
|
798
982
|
seq: sample.record.seq,
|
|
799
983
|
subject_seq: sample.candidate.seq,
|
|
984
|
+
...(retry ? { retry: true } : {}),
|
|
800
985
|
}),
|
|
801
986
|
}));
|
|
802
987
|
// The dark-session sweep (APRV-192), on its own cadence. Placed with the
|
|
@@ -1370,14 +1555,45 @@ export class Daemon {
|
|
|
1370
1555
|
...(this.options.readProof === undefined ? {} : { readProof: this.options.readProof }),
|
|
1371
1556
|
});
|
|
1372
1557
|
}
|
|
1373
|
-
/**
|
|
1374
|
-
|
|
1558
|
+
/** Where this daemon's policy is, in `loadPolicy`'s own vocabulary. */
|
|
1559
|
+
policyWhere() {
|
|
1375
1560
|
const where = this.options.policy.file !== undefined
|
|
1376
1561
|
? { file: this.options.policy.file }
|
|
1377
1562
|
: { dir: this.options.policy.dir ?? this.options.cwd };
|
|
1378
1563
|
if (this.options.schemaDir !== undefined)
|
|
1379
1564
|
where.schemaDir = this.options.schemaDir;
|
|
1380
|
-
|
|
1565
|
+
return where;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Put the attested policy's `daemons` allowlist in force for this tick
|
|
1569
|
+
* (APRV-383).
|
|
1570
|
+
*
|
|
1571
|
+
* Called once per tick, immediately after the opening verified read and before
|
|
1572
|
+
* any sweep that appends. It never widens: a resolution that fails sets nothing,
|
|
1573
|
+
* so whatever was last resolved stays in force, and a run whose policy was never
|
|
1574
|
+
* attested is unrestricted exactly as every run before this key existed was.
|
|
1575
|
+
*
|
|
1576
|
+
* The warning is said ONCE per run. A line every thirty seconds forever stops
|
|
1577
|
+
* being read (the `reportedDangling` set above exists for the same reason), and
|
|
1578
|
+
* the write boundary is where the refusal actually happens: every refused append
|
|
1579
|
+
* carries its own `append-refused` line with the reason in it.
|
|
1580
|
+
*/
|
|
1581
|
+
refreshIdentity(records) {
|
|
1582
|
+
refreshDaemonAllowlist(records, loadPolicy(this.policyWhere()));
|
|
1583
|
+
if (this.reportedIdentityRefusal)
|
|
1584
|
+
return;
|
|
1585
|
+
const state = daemonIdentity();
|
|
1586
|
+
if (state === null || state.id === null)
|
|
1587
|
+
return;
|
|
1588
|
+
const allowed = state.allowed;
|
|
1589
|
+
if (allowed === null || allowed.includes(state.id))
|
|
1590
|
+
return;
|
|
1591
|
+
this.reportedIdentityRefusal = true;
|
|
1592
|
+
this.warn("daemon-identity-refused", `this daemon's id is ${state.id} and the attested policy's \`daemons\` list admits ${allowed.length === 0 ? "no daemon at all" : allowed.join(", ")}: every append this run makes is refused \`daemon-not-allowed\` and nothing is written. Add ${state.id} to \`daemons\` and re-attest the policy, or run the daemon whose id the list already names`);
|
|
1593
|
+
}
|
|
1594
|
+
/** The TTL in force right now, re-read every pass: policy files change. */
|
|
1595
|
+
ttlMs() {
|
|
1596
|
+
const load = loadPolicy(this.policyWhere());
|
|
1381
1597
|
// Fail closed exactly as the gate does: an unloadable policy declares no
|
|
1382
1598
|
// TTL, so nothing lapses and nothing is expired on its behalf.
|
|
1383
1599
|
return load.ok ? load.durations.approvalTtlMs : null;
|
|
@@ -1410,6 +1626,29 @@ export class Daemon {
|
|
|
1410
1626
|
* malformed file is not a *contradiction* of the log, it is a file the runtime
|
|
1411
1627
|
* cannot read a claim out of at all, and inventing a `declared_state` for it
|
|
1412
1628
|
* would put a fact in the log that nobody wrote.
|
|
1629
|
+
*
|
|
1630
|
+
* ## A deferred drift record is not a lost one (APRV-403)
|
|
1631
|
+
*
|
|
1632
|
+
* An append here can fail two ways that call for different things. A held
|
|
1633
|
+
* lockfile or a head that moved under the compare-and-append means the log was
|
|
1634
|
+
* not written and nothing about the file changed; the next tick re-reads the
|
|
1635
|
+
* folder, re-derives the disagreement from the verified log, and appends it.
|
|
1636
|
+
* A validation refusal, a canonicalization failure, a corrupt tail or an I/O
|
|
1637
|
+
* error is a fact about the record or the file that no retry repairs.
|
|
1638
|
+
*
|
|
1639
|
+
* Both used to read as `append-refused … was not appended`, which for the
|
|
1640
|
+
* first is true and reads as a loss. So the transient half now goes out as
|
|
1641
|
+
* {@link deferDrift}'s own `drift-deferred` line, naming the task and saying
|
|
1642
|
+
* that the scan retries, and the `drift` record that closes it carries
|
|
1643
|
+
* `retry`. Everything else keeps the form it had.
|
|
1644
|
+
*
|
|
1645
|
+
* Nothing about the APPEND changed, which is the part that matters for
|
|
1646
|
+
* SPEC.md §11.1 invariant 5: the record is still placed by `appendEvent`
|
|
1647
|
+
* against the `expectedHead` of a read taken immediately before it, and the
|
|
1648
|
+
* whole decision is still remade against that read. A `head-moved` here IS
|
|
1649
|
+
* compare-and-append working — another writer landed a record between this
|
|
1650
|
+
* scan's read and its append — and deferring is the only answer that does not
|
|
1651
|
+
* either write against a head this scan never saw or lose the observation.
|
|
1413
1652
|
*/
|
|
1414
1653
|
scanForDrift() {
|
|
1415
1654
|
// ONE verified read for the whole scan (APRV-211). The scan asks the same
|
|
@@ -1437,6 +1676,24 @@ export class Daemon {
|
|
|
1437
1676
|
}
|
|
1438
1677
|
return { appended, stop: null };
|
|
1439
1678
|
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Say that an `envelope.drift` append did not happen YET, and remember it
|
|
1681
|
+
* (APRV-403).
|
|
1682
|
+
*
|
|
1683
|
+
* Two things this line has to do that `append-refused` did not. It names the
|
|
1684
|
+
* TASK KEY, so an operator can match the deferral to the record that closes
|
|
1685
|
+
* it, and it says in words that the next tick retries — because the sentence
|
|
1686
|
+
* it replaced ended at "was not appended", which is true and reads as a loss.
|
|
1687
|
+
*
|
|
1688
|
+
* The claim is safe to make because the scan carries nothing between ticks:
|
|
1689
|
+
* the next one re-reads the folder, re-derives the state from the verified
|
|
1690
|
+
* log, and finds the same disagreement, so the retry is a property of how the
|
|
1691
|
+
* scan is written rather than a promise this method is keeping.
|
|
1692
|
+
*/
|
|
1693
|
+
deferDrift(task, reason, error) {
|
|
1694
|
+
rememberDriftDeferral(this.options.logPath, task, reason);
|
|
1695
|
+
this.warn("drift-deferred", `envelope.drift${reason === "state-mismatch" ? "" : ` (${reason})`} for ${task} was deferred (${error.code}): ${error.message} The record is NOT lost: the disagreement is still there in the log's own terms, and the scan re-derives it and retries on the next tick.`);
|
|
1696
|
+
}
|
|
1440
1697
|
/** Every `*.md` under the task folder, sorted, non-recursive. */
|
|
1441
1698
|
taskFiles() {
|
|
1442
1699
|
let entries;
|
|
@@ -1557,10 +1814,22 @@ export class Daemon {
|
|
|
1557
1814
|
expectedHead: records.head,
|
|
1558
1815
|
});
|
|
1559
1816
|
if (!result.ok) {
|
|
1560
|
-
|
|
1817
|
+
// APRV-403. A held lock or a moved head says the log was not written and
|
|
1818
|
+
// nothing about this file changed, which is a different fact from a record
|
|
1819
|
+
// the write boundary will never take; the scan re-derives and retries the
|
|
1820
|
+
// first, and only a person repairs the second. `isTransientAppendError`
|
|
1821
|
+
// and its closed list are `core/audit.ts`'s, shared rather than copied, so
|
|
1822
|
+
// the two sweeps cannot disagree about what transient means.
|
|
1823
|
+
if (isTransientAppendError(result.error)) {
|
|
1824
|
+
this.deferDrift(id, "state-mismatch", result.error);
|
|
1825
|
+
}
|
|
1826
|
+
else {
|
|
1827
|
+
this.warn("append-refused", `envelope.drift for ${id} was not appended (${result.error.code}): ${result.error.message}`);
|
|
1828
|
+
}
|
|
1561
1829
|
return { appended: false, stop: null };
|
|
1562
1830
|
}
|
|
1563
1831
|
this.drifts += 1;
|
|
1832
|
+
const retried = takeDriftDeferral(this.options.logPath, id, "state-mismatch");
|
|
1564
1833
|
this.emit({
|
|
1565
1834
|
event: "drift",
|
|
1566
1835
|
task: id,
|
|
@@ -1568,6 +1837,7 @@ export class Daemon {
|
|
|
1568
1837
|
declared_state: declaredState,
|
|
1569
1838
|
derived_state: facts.derivedState,
|
|
1570
1839
|
seq: result.record.seq,
|
|
1840
|
+
...(retried ? { retry: true } : {}),
|
|
1571
1841
|
});
|
|
1572
1842
|
return { appended: true, stop: null };
|
|
1573
1843
|
}
|
|
@@ -1638,10 +1908,20 @@ export class Daemon {
|
|
|
1638
1908
|
expectedHead: records.head,
|
|
1639
1909
|
});
|
|
1640
1910
|
if (!result.ok) {
|
|
1641
|
-
|
|
1911
|
+
// The same split as the mismatch path above (APRV-403). Both sites take
|
|
1912
|
+
// it, because contention does not care which reason the record carries and
|
|
1913
|
+
// an operator reading a deferral for one and a refusal for the other would
|
|
1914
|
+
// be reading two answers to one question.
|
|
1915
|
+
if (isTransientAppendError(result.error)) {
|
|
1916
|
+
this.deferDrift(task, "envelope-missing", result.error);
|
|
1917
|
+
}
|
|
1918
|
+
else {
|
|
1919
|
+
this.warn("append-refused", `envelope.drift (envelope-missing) for ${task} was not appended (${result.error.code}): ${result.error.message}`);
|
|
1920
|
+
}
|
|
1642
1921
|
return { appended: false, stop: null };
|
|
1643
1922
|
}
|
|
1644
1923
|
this.drifts += 1;
|
|
1924
|
+
const retried = takeDriftDeferral(this.options.logPath, task, "envelope-missing");
|
|
1645
1925
|
this.emit({
|
|
1646
1926
|
event: "drift",
|
|
1647
1927
|
task,
|
|
@@ -1650,6 +1930,7 @@ export class Daemon {
|
|
|
1650
1930
|
derived_state: facts.derivedState,
|
|
1651
1931
|
seq: result.record.seq,
|
|
1652
1932
|
reason: "envelope-missing",
|
|
1933
|
+
...(retried ? { retry: true } : {}),
|
|
1653
1934
|
});
|
|
1654
1935
|
return { appended: true, stop: null };
|
|
1655
1936
|
}
|
|
@@ -1764,7 +2045,7 @@ export class Daemon {
|
|
|
1764
2045
|
* log, and produces bytes only through `core/task-file.ts`. A file can no more
|
|
1765
2046
|
* teach the log a state than a screenshot can teach a database a row.
|
|
1766
2047
|
*
|
|
1767
|
-
*
|
|
2048
|
+
* Five rules, each of which is a way of not making things worse:
|
|
1768
2049
|
*
|
|
1769
2050
|
* 1. **Only files that already have an envelope.** `set-state` refuses
|
|
1770
2051
|
* `no-envelope`, and that refusal is honoured silently: a task with no
|
|
@@ -1786,6 +2067,11 @@ export class Daemon {
|
|
|
1786
2067
|
* envelope: each was warned about a few milliseconds ago by
|
|
1787
2068
|
* {@link scanForDrift} over the same folder. Repeating it here would double
|
|
1788
2069
|
* every line an operator reads without adding a fact.
|
|
2070
|
+
* 5. **No repair for a drift record that was deferred** (APRV-403). See the
|
|
2071
|
+
* check inside the loop: a file whose `envelope.drift` this tick could not
|
|
2072
|
+
* append is left exactly as it is, because repairing it would correct the
|
|
2073
|
+
* disagreement off the record and erase what the retry is supposed to
|
|
2074
|
+
* re-derive.
|
|
1789
2075
|
*
|
|
1790
2076
|
* Loop safety comes from the comparison, not from a remembered flag: the next
|
|
1791
2077
|
* tick derives the same state from the same log, finds the file already
|
|
@@ -1835,6 +2121,20 @@ export class Daemon {
|
|
|
1835
2121
|
const derived = taskEnvelopeState(records.records, id, ts, ttlMs).state;
|
|
1836
2122
|
if (declaredState === derived)
|
|
1837
2123
|
continue;
|
|
2124
|
+
// APRV-403, and it is rule 5 of this list. The drift scan earlier in this
|
|
2125
|
+
// tick DEFERRED the record for this task, so the disagreement has not been
|
|
2126
|
+
// written down yet. Repairing the file now would correct it off the record
|
|
2127
|
+
// and, worse, remove the evidence: the next tick would find the file
|
|
2128
|
+
// agreeing with the log, the retry would have nothing to re-derive, and
|
|
2129
|
+
// the deferral an operator was told about would resolve into silence.
|
|
2130
|
+
//
|
|
2131
|
+
// SPEC.md §6.3 fixes the order — the event is appended first and the file
|
|
2132
|
+
// is updated second, never the reverse — and a transient append failure is
|
|
2133
|
+
// exactly the case where "never the reverse" has to be enforced rather
|
|
2134
|
+
// than assumed. So the file waits for the record, and the tick that lands
|
|
2135
|
+
// the record repairs the file after it.
|
|
2136
|
+
if (driftDeferrals.has(driftDeferralKey(this.options.logPath, id, "state-mismatch")))
|
|
2137
|
+
continue;
|
|
1838
2138
|
const rewritten = rewriteTaskFile(text, { kind: "set-state", state: derived }, rewriteOptions);
|
|
1839
2139
|
if (!rewritten.ok) {
|
|
1840
2140
|
if (rewritten.code === "no-envelope")
|