clinkx 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +72 -0
- package/clinkx-workflows/dist/chairman-knobs.d.ts +24 -0
- package/clinkx-workflows/dist/chairman-knobs.js +27 -0
- package/clinkx-workflows/dist/chairman-knobs.js.map +1 -0
- package/clinkx-workflows/dist/clink-client.d.ts +10 -0
- package/clinkx-workflows/dist/clink-client.js +39 -1
- package/clinkx-workflows/dist/clink-client.js.map +1 -1
- package/clinkx-workflows/dist/config.d.ts +29 -1
- package/clinkx-workflows/dist/config.js +35 -2
- package/clinkx-workflows/dist/config.js.map +1 -1
- package/clinkx-workflows/dist/context-preflight.d.ts +5 -0
- package/clinkx-workflows/dist/context-preflight.js.map +1 -1
- package/clinkx-workflows/dist/council-execution-policy.d.ts +68 -0
- package/clinkx-workflows/dist/council-execution-policy.js +110 -0
- package/clinkx-workflows/dist/council-execution-policy.js.map +1 -0
- package/clinkx-workflows/dist/council-expander.d.ts +9 -8
- package/clinkx-workflows/dist/council-expander.js +18 -9
- package/clinkx-workflows/dist/council-expander.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +5557 -4
- package/clinkx-workflows/dist/dashboard/activity-contract.js +55 -3
- package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +9 -5
- package/clinkx-workflows/dist/dashboard/activity-reader.js +15 -2
- package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/api-routes.d.ts +4 -0
- package/clinkx-workflows/dist/dashboard/api-routes.js +133 -25
- package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.d.ts +3865 -0
- package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js +69 -0
- package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +36 -2
- package/clinkx-workflows/dist/dashboard/context-run-reader.js +142 -19
- package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/contracts.d.ts +20027 -480
- package/clinkx-workflows/dist/dashboard/contracts.js +133 -6
- package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/index.d.ts +4 -0
- package/clinkx-workflows/dist/dashboard/index.js +4 -0
- package/clinkx-workflows/dist/dashboard/index.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/invocation-contract.d.ts +786 -0
- package/clinkx-workflows/dist/dashboard/invocation-contract.js +201 -0
- package/clinkx-workflows/dist/dashboard/invocation-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +14 -1
- package/clinkx-workflows/dist/dashboard/lazy-content.js +53 -2
- package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/native-transcript/contract.d.ts +90 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/contract.js +31 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/projection.d.ts +12 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/projection.js +286 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/projection.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +147 -4
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js +11 -11
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js +4 -56
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +2 -1
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +6 -5
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.d.ts +194 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js +89 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.d.ts +26 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js +102 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js +4 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/scanner.d.ts +3 -0
- package/clinkx-workflows/dist/dashboard/scanner.js +52 -8
- package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/server-factory.d.ts +14 -1
- package/clinkx-workflows/dist/dashboard/server-factory.js +73 -21
- package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +223 -56
- package/clinkx-workflows/dist/dashboard/session-log-contract.js +26 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/session-log-journal.js +10 -3
- package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +9 -1
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js +793 -59
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -1
- package/clinkx-workflows/dist/dashboard-ui/assets/index-JPHQVtn9.js +106 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/style-DOMHk-j6.css +1 -0
- package/clinkx-workflows/dist/dashboard-ui/index.html +2 -2
- package/clinkx-workflows/dist/delegate-finalizer-policy.d.ts +26 -0
- package/clinkx-workflows/dist/delegate-finalizer-policy.js +72 -0
- package/clinkx-workflows/dist/delegate-finalizer-policy.js.map +1 -0
- package/clinkx-workflows/dist/delegate-signature.d.ts +14 -0
- package/clinkx-workflows/dist/delegate-signature.js +14 -0
- package/clinkx-workflows/dist/delegate-signature.js.map +1 -0
- package/clinkx-workflows/dist/delegate-validate.d.ts +32 -2
- package/clinkx-workflows/dist/delegate-validate.js +51 -0
- package/clinkx-workflows/dist/delegate-validate.js.map +1 -1
- package/clinkx-workflows/dist/engine.d.ts +113 -3
- package/clinkx-workflows/dist/engine.js +476 -80
- package/clinkx-workflows/dist/engine.js.map +1 -1
- package/clinkx-workflows/dist/errors.d.ts +22 -2
- package/clinkx-workflows/dist/errors.js +30 -2
- package/clinkx-workflows/dist/errors.js.map +1 -1
- package/clinkx-workflows/dist/fidelity.d.ts +47 -1
- package/clinkx-workflows/dist/fidelity.js +54 -1
- package/clinkx-workflows/dist/fidelity.js.map +1 -1
- package/clinkx-workflows/dist/fingerprint.d.ts +24 -6
- package/clinkx-workflows/dist/fingerprint.js +32 -9
- package/clinkx-workflows/dist/fingerprint.js.map +1 -1
- package/clinkx-workflows/dist/index.js +53 -1
- package/clinkx-workflows/dist/index.js.map +1 -1
- package/clinkx-workflows/dist/logger.d.ts +1 -1
- package/clinkx-workflows/dist/logger.js +17 -3
- package/clinkx-workflows/dist/logger.js.map +1 -1
- package/clinkx-workflows/dist/monotonic-clock.d.ts +19 -0
- package/clinkx-workflows/dist/monotonic-clock.js +22 -0
- package/clinkx-workflows/dist/monotonic-clock.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/contract.d.ts +52 -52
- package/clinkx-workflows/dist/plan-review/publication.d.ts +4 -4
- package/clinkx-workflows/dist/plan-review/run-state.d.ts +16 -16
- package/clinkx-workflows/dist/roster-contract.d.ts +889 -0
- package/clinkx-workflows/dist/roster-contract.js +180 -0
- package/clinkx-workflows/dist/roster-contract.js.map +1 -0
- package/clinkx-workflows/dist/rosters.d.ts +14 -0
- package/clinkx-workflows/dist/rosters.js +169 -0
- package/clinkx-workflows/dist/rosters.js.map +1 -0
- package/clinkx-workflows/dist/run-health.d.ts +7 -0
- package/clinkx-workflows/dist/run-health.js +25 -0
- package/clinkx-workflows/dist/run-health.js.map +1 -0
- package/clinkx-workflows/dist/schema.d.ts +64 -0
- package/clinkx-workflows/dist/schema.js +24 -0
- package/clinkx-workflows/dist/schema.js.map +1 -1
- package/clinkx-workflows/dist/server.js +2 -0
- package/clinkx-workflows/dist/server.js.map +1 -1
- package/clinkx-workflows/dist/shutdown.d.ts +15 -2
- package/clinkx-workflows/dist/shutdown.js +60 -16
- package/clinkx-workflows/dist/shutdown.js.map +1 -1
- package/clinkx-workflows/dist/state-schema.d.ts +23 -0
- package/clinkx-workflows/dist/state.d.ts +5 -0
- package/clinkx-workflows/dist/state.js +128 -21
- package/clinkx-workflows/dist/state.js.map +1 -1
- package/clinkx-workflows/dist/stdio-watchdog.d.ts +52 -0
- package/clinkx-workflows/dist/stdio-watchdog.js +212 -0
- package/clinkx-workflows/dist/stdio-watchdog.js.map +1 -0
- package/clinkx-workflows/dist/types.d.ts +6 -0
- package/clinkx-workflows/dist/validators/council.d.ts +50 -50
- package/clinkx-workflows/dist/validators/plan-review.d.ts +3 -3
- package/clinkx-workflows/dist/workflow-tools.d.ts +70 -0
- package/clinkx-workflows/dist/workflow-tools.js +760 -168
- package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
- package/clinkx-workflows/templates/council/answer-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/answer-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/answer-specialist.txt +3 -0
- package/clinkx-workflows/templates/council/chairman.txt +3 -0
- package/clinkx-workflows/templates/council/code-review-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/code-review-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/debug-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/debug-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/debug-debugger.txt +3 -0
- package/clinkx-workflows/templates/council/discover-analyst.txt +3 -0
- package/clinkx-workflows/templates/council/discover-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/discover-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/member.txt +3 -0
- package/clinkx-workflows/templates/council/plan-review-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/plan-review-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/reviewer.txt +3 -0
- package/clinkx-workflows/workflows/council-answer.yaml +3 -0
- package/clinkx-workflows/workflows/council-code-review.yaml +3 -0
- package/clinkx-workflows/workflows/council-debug.yaml +3 -0
- package/clinkx-workflows/workflows/council-default.yaml +7 -0
- package/clinkx-workflows/workflows/council-discover.yaml +3 -0
- package/clinkx-workflows/workflows/council-plan-review.yaml +3 -0
- package/conf/adapters/agy.json +9 -2
- package/conf/adapters/claude.json +7 -0
- package/conf/adapters/codex.json +7 -0
- package/conf/adapters/glm.json +19 -3
- package/conf/adapters/grok.json +9 -2
- package/conf/adapters/hapi/agy.json +9 -2
- package/conf/adapters/hapi/claude.json +7 -0
- package/conf/adapters/hapi/codex.json +7 -0
- package/conf/adapters/hapi/glm.json +20 -4
- package/conf/rosters/default-council.json +21 -0
- package/dist/activity-journal.d.ts +17 -3
- package/dist/activity-journal.js +404 -79
- package/dist/activity-journal.js.map +1 -1
- package/dist/codex/agent-tools.js +36 -0
- package/dist/codex/agent-tools.js.map +1 -1
- package/dist/codex/app-server-client.d.ts +3 -0
- package/dist/codex/app-server-client.js +11 -0
- package/dist/codex/app-server-client.js.map +1 -1
- package/dist/codex/app-server-registry.d.ts +18 -0
- package/dist/codex/app-server-registry.js +88 -2
- package/dist/codex/app-server-registry.js.map +1 -1
- package/dist/codex/client-types.d.ts +7 -1
- package/dist/codex/protocol.d.ts +4 -4
- package/dist/config.d.ts +220 -23
- package/dist/config.js +104 -1
- package/dist/config.js.map +1 -1
- package/dist/context-builder/bundle.d.ts +13 -0
- package/dist/context-builder/bundle.js +46 -0
- package/dist/context-builder/bundle.js.map +1 -1
- package/dist/context-builder/delegate-finalizer-effective.d.ts +33 -0
- package/dist/context-builder/delegate-finalizer-effective.js +82 -0
- package/dist/context-builder/delegate-finalizer-effective.js.map +1 -0
- package/dist/context-builder/delegate-packet.d.ts +18 -0
- package/dist/context-builder/delegate-packet.js +73 -9
- package/dist/context-builder/delegate-packet.js.map +1 -1
- package/dist/context-builder/delegate-reconstruction.d.ts +29 -0
- package/dist/context-builder/delegate-reconstruction.js +110 -41
- package/dist/context-builder/delegate-reconstruction.js.map +1 -1
- package/dist/context-builder/delegate-validation.d.ts +31 -0
- package/dist/context-builder/delegate-validation.js +87 -10
- package/dist/context-builder/delegate-validation.js.map +1 -1
- package/dist/context-builder/delegate.d.ts +9 -0
- package/dist/context-builder/delegate.js +210 -59
- package/dist/context-builder/delegate.js.map +1 -1
- package/dist/context-builder/diagnostics.d.ts +1 -1
- package/dist/context-builder/diagnostics.js.map +1 -1
- package/dist/context-builder/discovery-progress.d.ts +25 -0
- package/dist/context-builder/discovery-progress.js +64 -0
- package/dist/context-builder/discovery-progress.js.map +1 -0
- package/dist/context-builder/discovery-watchdog.d.ts +15 -0
- package/dist/context-builder/discovery-watchdog.js +39 -0
- package/dist/context-builder/discovery-watchdog.js.map +1 -0
- package/dist/context-builder/discovery.d.ts +7 -0
- package/dist/context-builder/discovery.js +119 -7
- package/dist/context-builder/discovery.js.map +1 -1
- package/dist/context-builder/external-gaps.d.ts +73 -0
- package/dist/context-builder/external-gaps.js +248 -0
- package/dist/context-builder/external-gaps.js.map +1 -0
- package/dist/context-builder/globs.d.ts +1 -0
- package/dist/context-builder/globs.js +15 -0
- package/dist/context-builder/globs.js.map +1 -1
- package/dist/context-builder/handler.d.ts +42 -1
- package/dist/context-builder/handler.js +690 -62
- package/dist/context-builder/handler.js.map +1 -1
- package/dist/context-builder/invocation-sidecar.d.ts +41 -0
- package/dist/context-builder/invocation-sidecar.js +189 -0
- package/dist/context-builder/invocation-sidecar.js.map +1 -0
- package/dist/context-builder/lifecycle-contract.d.ts +51 -51
- package/dist/context-builder/mutation-lock.d.ts +32 -0
- package/dist/context-builder/mutation-lock.js +321 -0
- package/dist/context-builder/mutation-lock.js.map +1 -0
- package/dist/context-builder/review-coverage.d.ts +32 -0
- package/dist/context-builder/review-coverage.js +130 -23
- package/dist/context-builder/review-coverage.js.map +1 -1
- package/dist/context-builder/schema.d.ts +76 -1
- package/dist/context-builder/schema.js +21 -1
- package/dist/context-builder/schema.js.map +1 -1
- package/dist/context-builder/secrets.d.ts +31 -0
- package/dist/context-builder/secrets.js +865 -83
- package/dist/context-builder/secrets.js.map +1 -1
- package/dist/context-builder/status.d.ts +1 -1
- package/dist/context-builder/status.js +1 -0
- package/dist/context-builder/status.js.map +1 -1
- package/dist/context-builder/storage.d.ts +29 -5
- package/dist/context-builder/storage.js +311 -236
- package/dist/context-builder/storage.js.map +1 -1
- package/dist/context-builder/versions.d.ts +19 -4
- package/dist/context-builder/versions.js +19 -4
- package/dist/context-builder/versions.js.map +1 -1
- package/dist/context-builder/workflow-runner.d.ts +6 -0
- package/dist/context-builder/workflow-runner.js +363 -173
- package/dist/context-builder/workflow-runner.js.map +1 -1
- package/dist/dashboard-adapter-catalog.js +7 -0
- package/dist/dashboard-adapter-catalog.js.map +1 -1
- package/dist/dashboard-cli.d.ts +2 -0
- package/dist/dashboard-cli.js +40 -2
- package/dist/dashboard-cli.js.map +1 -1
- package/dist/dashboard-companion/client.d.ts +1 -0
- package/dist/dashboard-companion/client.js +22 -3
- package/dist/dashboard-companion/client.js.map +1 -1
- package/dist/dashboard-companion/protocol.js +4 -1
- package/dist/dashboard-companion/protocol.js.map +1 -1
- package/dist/dashboard-gateway/index.d.ts +1 -0
- package/dist/dashboard-gateway/index.js +33 -2
- package/dist/dashboard-gateway/index.js.map +1 -1
- package/dist/handler.js +5 -1
- package/dist/handler.js.map +1 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +145 -7
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +18 -3
- package/dist/logger.js.map +1 -1
- package/dist/parsers/agy-stream-json.d.ts +14 -0
- package/dist/parsers/agy-stream-json.js +172 -0
- package/dist/parsers/agy-stream-json.js.map +1 -0
- package/dist/parsers/extract.js +5 -1
- package/dist/parsers/extract.js.map +1 -1
- package/dist/parsers/observed-invocation.d.ts +20 -0
- package/dist/parsers/observed-invocation.js +42 -0
- package/dist/parsers/observed-invocation.js.map +1 -0
- package/dist/pipeline.d.ts +13 -0
- package/dist/pipeline.js +60 -10
- package/dist/pipeline.js.map +1 -1
- package/dist/progress-report/lock.d.ts +74 -0
- package/dist/progress-report/lock.js +343 -0
- package/dist/progress-report/lock.js.map +1 -0
- package/dist/progress-report/schema.d.ts +808 -0
- package/dist/progress-report/schema.js +119 -0
- package/dist/progress-report/schema.js.map +1 -0
- package/dist/progress-report/store.d.ts +66 -0
- package/dist/progress-report/store.js +766 -0
- package/dist/progress-report/store.js.map +1 -0
- package/dist/progress-report/tools.d.ts +20 -0
- package/dist/progress-report/tools.js +141 -0
- package/dist/progress-report/tools.js.map +1 -0
- package/dist/prompt-mode.d.ts +1 -1
- package/dist/prompt-mode.js +7 -2
- package/dist/prompt-mode.js.map +1 -1
- package/dist/runner.d.ts +7 -0
- package/dist/runner.js +10 -3
- package/dist/runner.js.map +1 -1
- package/dist/safety.d.ts +50 -0
- package/dist/safety.js +182 -1
- package/dist/safety.js.map +1 -1
- package/dist/schema.d.ts +8 -0
- package/dist/schema.js +20 -0
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +6 -1
- package/dist/server.js +94 -8
- package/dist/server.js.map +1 -1
- package/dist/session-log-stream.d.ts +1 -1
- package/dist/session-log-stream.js +280 -33
- package/dist/session-log-stream.js.map +1 -1
- package/dist/stdio-activity.d.ts +28 -0
- package/dist/stdio-activity.js +72 -0
- package/dist/stdio-activity.js.map +1 -0
- package/package.json +2 -1
- package/clinkx-workflows/dist/dashboard-ui/assets/index-DOkMXTfB.js +0 -25
- package/clinkx-workflows/dist/dashboard-ui/assets/style-ChJsn0KD.css +0 -1
package/dist/activity-journal.js
CHANGED
|
@@ -1,50 +1,171 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { chmod, lstat, mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { ACTIVITY_RESULT_FILENAME, ACTIVITY_STATE_FILENAME, ActivityIdSchema, ActivityStateReadSchema, ActivityStateV1Schema, ActivityStateV2Schema, ActivityStateV3Schema, DEFAULT_ACTIVITY_HEARTBEAT_INTERVAL_MS, DEFAULT_ACTIVITY_MAX_AGE_MS, DEFAULT_MAX_ACTIVITY_ENTRIES, DEFAULT_MAX_ACTIVITY_RESULT_BYTES, DEFAULT_MAX_ACTIVITY_TOTAL_BYTES, MAX_ACTIVITY_STATE_BYTES, } from "../clinkx-workflows/dist/dashboard/activity-contract.js";
|
|
5
6
|
import { createDashboardActivityReader } from "../clinkx-workflows/dist/dashboard/activity-reader.js";
|
|
7
|
+
import { deriveActivityGeneration } from "../clinkx-workflows/dist/dashboard/generation.js";
|
|
6
8
|
import { removeSessionLogGeneration } from "../clinkx-workflows/dist/dashboard/session-log-retention.js";
|
|
7
9
|
import { createSessionLogGeneration, readSessionLogGeneration, } from "../clinkx-workflows/dist/dashboard/session-log-identity.js";
|
|
10
|
+
import { invocationAuditV2PersistenceEnabled, observedInvocationPersistenceEnabled, } from "../clinkx-workflows/dist/config.js";
|
|
11
|
+
import { resolveOpenedDescriptorPath } from "../clinkx-workflows/dist/contained-file.js";
|
|
8
12
|
import { logger } from "./logger.js";
|
|
9
|
-
export { ACTIVITY_RESULT_FILENAME, ACTIVITY_STATE_FILENAME, ActivityIdSchema, ActivityKindSchema, ActivityOutcomeKindSchema, ActivityPhaseSchema, ActivityResultReferenceSchema, ActivityStateV1Schema, ActivityTerminalStatusSchema, DEFAULT_ACTIVITY_HEARTBEAT_INTERVAL_MS, DEFAULT_ACTIVITY_MAX_AGE_MS, DEFAULT_MAX_ACTIVITY_ENTRIES, DEFAULT_MAX_ACTIVITY_RESULT_BYTES, DEFAULT_MAX_ACTIVITY_TOTAL_BYTES, MAX_ACTIVITY_STATE_BYTES, } from "../clinkx-workflows/dist/dashboard/activity-contract.js";
|
|
13
|
+
export { ACTIVITY_RESULT_FILENAME, ACTIVITY_STATE_FILENAME, ActivityIdSchema, ActivityKindSchema, ActivityOutcomeKindSchema, ActivityPhaseSchema, ActivityResultReferenceSchema, ActivityStateReadSchema, ActivityStateV1Schema, ActivityStateV2Schema, ActivityStateV3Schema, ActivityTerminalStatusSchema, DEFAULT_ACTIVITY_HEARTBEAT_INTERVAL_MS, DEFAULT_ACTIVITY_MAX_AGE_MS, DEFAULT_MAX_ACTIVITY_ENTRIES, DEFAULT_MAX_ACTIVITY_RESULT_BYTES, DEFAULT_MAX_ACTIVITY_TOTAL_BYTES, MAX_ACTIVITY_STATE_BYTES, } from "../clinkx-workflows/dist/dashboard/activity-contract.js";
|
|
10
14
|
export const ACTIVITY_RETAIN_RESULT_ENV = "CLINKX_DASHBOARD_ACTIVITY_RETAIN_RESULT";
|
|
11
15
|
const SESSION_LOG_GENERATION_FILE_BYTES = 44;
|
|
12
16
|
const NOOP_OBSERVATION = Object.freeze({
|
|
13
17
|
async transition() { },
|
|
18
|
+
async observe() { },
|
|
14
19
|
async complete() { },
|
|
15
20
|
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
class UnsafeActivityDirectoryError extends Error {
|
|
22
|
+
}
|
|
23
|
+
async function inspectActivityDirectory(activityDir, activityId) {
|
|
24
|
+
let linked;
|
|
25
|
+
try {
|
|
26
|
+
linked = await lstat(activityDir);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (isFileSystemError(error, "ENOENT"))
|
|
30
|
+
return undefined;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
if (linked.isSymbolicLink() || !linked.isDirectory()) {
|
|
34
|
+
throw new UnsafeActivityDirectoryError();
|
|
35
|
+
}
|
|
36
|
+
let handle;
|
|
19
37
|
try {
|
|
20
|
-
await
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
handle = await open(activityDir, fsConstants.O_RDONLY |
|
|
39
|
+
(fsConstants.O_DIRECTORY ?? 0) |
|
|
40
|
+
(fsConstants.O_NOFOLLOW ?? 0));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new UnsafeActivityDirectoryError();
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const opened = await handle.stat({ bigint: true });
|
|
47
|
+
const canonicalPath = await resolveOpenedDescriptorPath(handle.fd, activityDir);
|
|
48
|
+
if (!opened.isDirectory() ||
|
|
49
|
+
opened.nlink === 0n ||
|
|
50
|
+
BigInt(linked.dev) !== opened.dev ||
|
|
51
|
+
BigInt(linked.ino) !== opened.ino ||
|
|
52
|
+
canonicalPath === undefined) {
|
|
53
|
+
throw new UnsafeActivityDirectoryError();
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
activityId,
|
|
57
|
+
path: canonicalPath,
|
|
58
|
+
handle,
|
|
59
|
+
identity: { dev: opened.dev, ino: opened.ino },
|
|
60
|
+
localGeneration: deriveActivityGeneration(activityId, linked),
|
|
61
|
+
};
|
|
25
62
|
}
|
|
26
63
|
catch (error) {
|
|
27
64
|
await handle.close().catch(() => undefined);
|
|
28
|
-
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
29
65
|
throw error;
|
|
30
66
|
}
|
|
31
67
|
}
|
|
68
|
+
async function verifyActivityDirectory(path, admitted) {
|
|
69
|
+
const linked = await lstat(path).catch(() => undefined);
|
|
70
|
+
const opened = await admitted.handle.stat({ bigint: true }).catch(() => undefined);
|
|
71
|
+
if (linked === undefined ||
|
|
72
|
+
opened === undefined ||
|
|
73
|
+
linked.isSymbolicLink() ||
|
|
74
|
+
!linked.isDirectory() ||
|
|
75
|
+
!opened.isDirectory() ||
|
|
76
|
+
opened.nlink === 0n ||
|
|
77
|
+
BigInt(linked.dev) !== admitted.identity.dev ||
|
|
78
|
+
BigInt(linked.ino) !== admitted.identity.ino ||
|
|
79
|
+
opened.dev !== admitted.identity.dev ||
|
|
80
|
+
opened.ino !== admitted.identity.ino ||
|
|
81
|
+
deriveActivityGeneration(admitted.activityId, linked) !==
|
|
82
|
+
admitted.localGeneration ||
|
|
83
|
+
await resolveOpenedDescriptorPath(admitted.handle.fd, path) !== path) {
|
|
84
|
+
throw new UnsafeActivityDirectoryError();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function captureActivityDirectoryIncarnation(admitted) {
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
activityId: admitted.activityId,
|
|
90
|
+
path: admitted.path,
|
|
91
|
+
identity: admitted.identity,
|
|
92
|
+
localGeneration: admitted.localGeneration,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async function reopenActivityDirectory(incarnation) {
|
|
96
|
+
const admitted = await inspectActivityDirectory(incarnation.path, incarnation.activityId);
|
|
97
|
+
if (admitted === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
if (admitted.path !== incarnation.path ||
|
|
100
|
+
admitted.identity.dev !== incarnation.identity.dev ||
|
|
101
|
+
admitted.identity.ino !== incarnation.identity.ino ||
|
|
102
|
+
admitted.localGeneration !== incarnation.localGeneration) {
|
|
103
|
+
await admitted.handle.close().catch(() => undefined);
|
|
104
|
+
throw new UnsafeActivityDirectoryError();
|
|
105
|
+
}
|
|
106
|
+
return admitted;
|
|
107
|
+
}
|
|
108
|
+
async function writeActivityFileAtomic(directory, filename, content) {
|
|
109
|
+
const admitted = await reopenActivityDirectory(directory);
|
|
110
|
+
if (admitted === undefined)
|
|
111
|
+
throw new UnsafeActivityDirectoryError();
|
|
112
|
+
try {
|
|
113
|
+
const filePath = join(admitted.path, filename);
|
|
114
|
+
const temporaryPath = `${filePath}.${randomUUID()}.tmp`;
|
|
115
|
+
const handle = await open(temporaryPath, "wx", 0o600);
|
|
116
|
+
try {
|
|
117
|
+
await verifyActivityDirectory(admitted.path, admitted);
|
|
118
|
+
await handle.writeFile(content);
|
|
119
|
+
await handle.sync();
|
|
120
|
+
await handle.chmod(0o600);
|
|
121
|
+
// Bind the commit, not merely the temp creation, to the short-lived
|
|
122
|
+
// descriptor re-admitted for this exact incarnation.
|
|
123
|
+
await verifyActivityDirectory(admitted.path, admitted);
|
|
124
|
+
await rename(temporaryPath, filePath);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await handle.close().catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await admitted.handle.close().catch(() => undefined);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function isFileSystemError(error, code) {
|
|
139
|
+
return typeof error === "object" && error !== null &&
|
|
140
|
+
"code" in error && error.code === code;
|
|
141
|
+
}
|
|
32
142
|
function serializedState(state) {
|
|
33
|
-
const
|
|
143
|
+
const parsed = state.schemaVersion === "clinkx-dashboard-activity-v3"
|
|
144
|
+
? ActivityStateV3Schema.parse(state)
|
|
145
|
+
: state.schemaVersion === "clinkx-dashboard-activity-v2"
|
|
146
|
+
? ActivityStateV2Schema.parse(state)
|
|
147
|
+
: ActivityStateV1Schema.parse(state);
|
|
148
|
+
const serialized = `${JSON.stringify(parsed, null, 2)}\n`;
|
|
34
149
|
if (Buffer.byteLength(serialized, "utf8") > MAX_ACTIVITY_STATE_BYTES) {
|
|
35
150
|
throw new Error("activity state exceeds the producer byte cap");
|
|
36
151
|
}
|
|
37
152
|
return serialized;
|
|
38
153
|
}
|
|
39
|
-
async function pruneActivities(rootDir, sessionLogRootDir,
|
|
154
|
+
async function pruneActivities(rootDir, sessionLogRootDir, now, maxAgeMs, maxEntries, maxTotalBytes, reservedBytes, testHooks) {
|
|
40
155
|
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
41
156
|
await Promise.all(entries.map(async (entry) => {
|
|
42
157
|
if (!entry.isDirectory() || !ActivityIdSchema.safeParse(entry.name).success) {
|
|
43
158
|
return;
|
|
44
159
|
}
|
|
45
160
|
const activityDir = join(rootDir, entry.name);
|
|
161
|
+
let admitted;
|
|
46
162
|
try {
|
|
47
|
-
|
|
163
|
+
admitted = await inspectActivityDirectory(activityDir, entry.name);
|
|
164
|
+
if (admitted === undefined)
|
|
165
|
+
return;
|
|
166
|
+
const incarnation = captureActivityDirectoryIncarnation(admitted);
|
|
167
|
+
const parsed = ActivityStateReadSchema.safeParse(JSON.parse(await readFile(join(admitted.path, ACTIVITY_STATE_FILENAME), "utf8")));
|
|
168
|
+
await verifyActivityDirectory(admitted.path, admitted);
|
|
48
169
|
if (!parsed.success ||
|
|
49
170
|
parsed.data.phase !== "terminal" ||
|
|
50
171
|
parsed.data.completedAt === undefined) {
|
|
@@ -52,11 +173,10 @@ async function pruneActivities(rootDir, sessionLogRootDir, activityIdentityReade
|
|
|
52
173
|
}
|
|
53
174
|
if (now.getTime() - Date.parse(parsed.data.completedAt) > maxAgeMs) {
|
|
54
175
|
await pruneActivityDirectory({
|
|
55
|
-
rootDir,
|
|
56
176
|
sessionLogRootDir,
|
|
57
177
|
activityId: entry.name,
|
|
58
|
-
|
|
59
|
-
|
|
178
|
+
incarnation,
|
|
179
|
+
testHooks,
|
|
60
180
|
});
|
|
61
181
|
}
|
|
62
182
|
}
|
|
@@ -68,6 +188,9 @@ async function pruneActivities(rootDir, sessionLogRootDir, activityIdentityReade
|
|
|
68
188
|
}, "activity journal could not inspect a retention candidate");
|
|
69
189
|
}
|
|
70
190
|
}
|
|
191
|
+
finally {
|
|
192
|
+
await admitted?.handle.close().catch(() => undefined);
|
|
193
|
+
}
|
|
71
194
|
}));
|
|
72
195
|
const survivors = await readdir(rootDir, { withFileTypes: true });
|
|
73
196
|
const terminal = [];
|
|
@@ -76,35 +199,69 @@ async function pruneActivities(rootDir, sessionLogRootDir, activityIdentityReade
|
|
|
76
199
|
if (!entry.isDirectory())
|
|
77
200
|
continue;
|
|
78
201
|
const activityDir = join(rootDir, entry.name);
|
|
202
|
+
let admitted;
|
|
79
203
|
let activityBytes = 0;
|
|
204
|
+
let bytesBoundToIncarnation = false;
|
|
205
|
+
if (!ActivityIdSchema.safeParse(entry.name).success) {
|
|
206
|
+
try {
|
|
207
|
+
for (const child of await readdir(activityDir, { withFileTypes: true })) {
|
|
208
|
+
if (!child.isFile())
|
|
209
|
+
continue;
|
|
210
|
+
activityBytes += (await stat(join(activityDir, child.name))).size;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// An unreadable non-activity directory still occupies entry capacity.
|
|
215
|
+
}
|
|
216
|
+
totalBytes += activityBytes;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
80
219
|
try {
|
|
81
|
-
|
|
220
|
+
admitted = await inspectActivityDirectory(activityDir, entry.name);
|
|
221
|
+
if (admitted === undefined)
|
|
222
|
+
continue;
|
|
223
|
+
const incarnation = captureActivityDirectoryIncarnation(admitted);
|
|
224
|
+
for (const child of await readdir(admitted.path, { withFileTypes: true })) {
|
|
82
225
|
if (!child.isFile())
|
|
83
226
|
continue;
|
|
84
|
-
activityBytes += (await stat(join(
|
|
227
|
+
activityBytes += (await stat(join(admitted.path, child.name))).size;
|
|
85
228
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (!ActivityIdSchema.safeParse(entry.name).success)
|
|
92
|
-
continue;
|
|
93
|
-
try {
|
|
94
|
-
const parsed = ActivityStateV1Schema.safeParse(JSON.parse(await readFile(join(activityDir, ACTIVITY_STATE_FILENAME), "utf8")));
|
|
229
|
+
await verifyActivityDirectory(admitted.path, admitted);
|
|
230
|
+
totalBytes += activityBytes;
|
|
231
|
+
bytesBoundToIncarnation = true;
|
|
232
|
+
const parsed = ActivityStateReadSchema.safeParse(JSON.parse(await readFile(join(admitted.path, ACTIVITY_STATE_FILENAME), "utf8")));
|
|
233
|
+
await verifyActivityDirectory(admitted.path, admitted);
|
|
95
234
|
if (parsed.success &&
|
|
96
235
|
parsed.data.phase === "terminal" &&
|
|
97
236
|
parsed.data.completedAt !== undefined) {
|
|
98
237
|
terminal.push({
|
|
99
238
|
activityId: entry.name,
|
|
100
|
-
path: activityDir,
|
|
101
239
|
completedAtMs: Date.parse(parsed.data.completedAt),
|
|
102
240
|
bytes: activityBytes,
|
|
241
|
+
incarnation,
|
|
103
242
|
});
|
|
104
243
|
}
|
|
105
244
|
}
|
|
106
245
|
catch {
|
|
107
|
-
// Unreadable entries are not producer-owned retention candidates.
|
|
246
|
+
// Unreadable entries are not producer-owned retention candidates. If
|
|
247
|
+
// identity churn prevented a bound scan, account the current path only.
|
|
248
|
+
if (!bytesBoundToIncarnation) {
|
|
249
|
+
activityBytes = 0;
|
|
250
|
+
try {
|
|
251
|
+
for (const child of await readdir(activityDir, { withFileTypes: true })) {
|
|
252
|
+
if (!child.isFile())
|
|
253
|
+
continue;
|
|
254
|
+
activityBytes += (await stat(join(activityDir, child.name))).size;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Entry capacity still prevents unsafe admission at the count cap.
|
|
259
|
+
}
|
|
260
|
+
totalBytes += activityBytes;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
await admitted?.handle.close().catch(() => undefined);
|
|
108
265
|
}
|
|
109
266
|
}
|
|
110
267
|
terminal.sort((left, right) => left.completedAtMs - right.completedAtMs);
|
|
@@ -112,11 +269,10 @@ async function pruneActivities(rootDir, sessionLogRootDir, activityIdentityReade
|
|
|
112
269
|
while (entryCount >= maxEntries && terminal.length > 0) {
|
|
113
270
|
const victim = terminal.shift();
|
|
114
271
|
if (await pruneActivityDirectory({
|
|
115
|
-
rootDir,
|
|
116
272
|
sessionLogRootDir,
|
|
117
273
|
activityId: victim.activityId,
|
|
118
|
-
|
|
119
|
-
|
|
274
|
+
incarnation: victim.incarnation,
|
|
275
|
+
testHooks,
|
|
120
276
|
})) {
|
|
121
277
|
entryCount -= 1;
|
|
122
278
|
totalBytes -= victim.bytes;
|
|
@@ -126,34 +282,99 @@ async function pruneActivities(rootDir, sessionLogRootDir, activityIdentityReade
|
|
|
126
282
|
terminal.length > 0) {
|
|
127
283
|
const victim = terminal.shift();
|
|
128
284
|
if (await pruneActivityDirectory({
|
|
129
|
-
rootDir,
|
|
130
285
|
sessionLogRootDir,
|
|
131
286
|
activityId: victim.activityId,
|
|
132
|
-
|
|
133
|
-
|
|
287
|
+
incarnation: victim.incarnation,
|
|
288
|
+
testHooks,
|
|
134
289
|
})) {
|
|
135
290
|
entryCount -= 1;
|
|
136
291
|
totalBytes -= victim.bytes;
|
|
137
292
|
}
|
|
138
293
|
}
|
|
139
|
-
return entryCount < maxEntries &&
|
|
294
|
+
return entryCount < maxEntries &&
|
|
295
|
+
totalBytes + reservedBytes <= maxTotalBytes;
|
|
140
296
|
}
|
|
141
297
|
async function pruneActivityDirectory(input) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
298
|
+
const removed = await removeActivityDirectoryIncarnation({
|
|
299
|
+
activityId: input.activityId,
|
|
300
|
+
incarnation: input.incarnation,
|
|
301
|
+
reason: "retention",
|
|
302
|
+
testHooks: input.testHooks,
|
|
303
|
+
});
|
|
304
|
+
if (removed.kind !== "removed") {
|
|
305
|
+
logger.debug({ activity_id: input.activityId, removal: removed.kind }, "activity retention skipped because exact generation was unavailable");
|
|
145
306
|
return false;
|
|
146
307
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
:
|
|
151
|
-
|
|
308
|
+
await cleanupActivitySessionLog({
|
|
309
|
+
sessionLogRootDir: input.sessionLogRootDir,
|
|
310
|
+
activityId: input.activityId,
|
|
311
|
+
localGeneration: input.incarnation.localGeneration,
|
|
312
|
+
sessionLogIdentity: removed.sessionLogIdentity,
|
|
313
|
+
});
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
async function removeActivityDirectoryIncarnation(input) {
|
|
317
|
+
let admitted;
|
|
318
|
+
try {
|
|
319
|
+
admitted = await reopenActivityDirectory(input.incarnation);
|
|
320
|
+
if (admitted === undefined)
|
|
321
|
+
return { kind: "missing" };
|
|
322
|
+
const activityDir = admitted.path;
|
|
323
|
+
const quarantinePath = join(dirname(activityDir), `.retired-${input.activityId}-${randomUUID()}`);
|
|
324
|
+
await verifyActivityDirectory(activityDir, admitted);
|
|
325
|
+
await input.testHooks.beforeActivityDirectoryQuarantine?.(input.activityId, input.reason);
|
|
326
|
+
try {
|
|
327
|
+
await rename(activityDir, quarantinePath);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
return isFileSystemError(error, "ENOENT")
|
|
331
|
+
? { kind: "missing" }
|
|
332
|
+
: { kind: "unavailable" };
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
await verifyActivityDirectory(quarantinePath, admitted);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
// A same-ID replacement was moved instead of the admitted incarnation.
|
|
339
|
+
// Restore it only while the public name remains free; never delete it.
|
|
340
|
+
if (await lstat(activityDir).catch(() => undefined) === undefined) {
|
|
341
|
+
await rename(quarantinePath, activityDir).catch(() => undefined);
|
|
342
|
+
}
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
const sessionLogIdentity = await readSessionLogGeneration(quarantinePath);
|
|
346
|
+
await verifyActivityDirectory(quarantinePath, admitted);
|
|
347
|
+
try {
|
|
348
|
+
await rm(quarantinePath, { recursive: true, force: false });
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (await lstat(activityDir).catch(() => undefined) === undefined) {
|
|
352
|
+
await rename(quarantinePath, activityDir).catch(() => undefined);
|
|
353
|
+
}
|
|
354
|
+
return isFileSystemError(error, "ENOENT")
|
|
355
|
+
? { kind: "unsafe" }
|
|
356
|
+
: { kind: "unavailable" };
|
|
357
|
+
}
|
|
358
|
+
return { kind: "removed", sessionLogIdentity };
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
return error instanceof UnsafeActivityDirectoryError
|
|
362
|
+
? { kind: "unsafe" }
|
|
363
|
+
: { kind: "unavailable" };
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
await admitted?.handle.close().catch(() => undefined);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function cleanupActivitySessionLog(input) {
|
|
370
|
+
const sessionLogGeneration = input.sessionLogIdentity.kind === "available"
|
|
371
|
+
? input.sessionLogIdentity.sessionLogGeneration
|
|
372
|
+
: input.sessionLogIdentity.kind === "legacy"
|
|
373
|
+
? input.localGeneration
|
|
152
374
|
: undefined;
|
|
153
|
-
await rm(input.activityDir, { recursive: true, force: true });
|
|
154
375
|
if (sessionLogGeneration === undefined) {
|
|
155
376
|
logger.warn({ activity_id: input.activityId }, "activity parent pruned but portable session-log identity was unsafe");
|
|
156
|
-
return
|
|
377
|
+
return;
|
|
157
378
|
}
|
|
158
379
|
try {
|
|
159
380
|
const cleanup = await removeSessionLogGeneration({
|
|
@@ -168,7 +389,6 @@ async function pruneActivityDirectory(input) {
|
|
|
168
389
|
catch {
|
|
169
390
|
logger.warn({ activity_id: input.activityId }, "activity parent pruned but exact session-log cleanup failed");
|
|
170
391
|
}
|
|
171
|
-
return true;
|
|
172
392
|
}
|
|
173
393
|
function truncateUtf8(text, maxBytes) {
|
|
174
394
|
const source = Buffer.from(text, "utf8");
|
|
@@ -190,15 +410,15 @@ function truncateUtf8(text, maxBytes) {
|
|
|
190
410
|
return { bytes: Buffer.alloc(0), truncated: true };
|
|
191
411
|
}
|
|
192
412
|
class FileActivityObservation {
|
|
193
|
-
|
|
413
|
+
activityDirectory;
|
|
194
414
|
options;
|
|
195
415
|
identity;
|
|
196
416
|
state;
|
|
197
417
|
writeChain = Promise.resolve();
|
|
198
418
|
terminal = false;
|
|
199
419
|
heartbeat;
|
|
200
|
-
constructor(
|
|
201
|
-
this.
|
|
420
|
+
constructor(activityDirectory, options, initialState, identity) {
|
|
421
|
+
this.activityDirectory = activityDirectory;
|
|
202
422
|
this.options = options;
|
|
203
423
|
this.identity = identity;
|
|
204
424
|
this.state = initialState;
|
|
@@ -233,6 +453,27 @@ class FileActivityObservation {
|
|
|
233
453
|
await this.persistState();
|
|
234
454
|
});
|
|
235
455
|
}
|
|
456
|
+
async observe(observed) {
|
|
457
|
+
await this.enqueue(async () => {
|
|
458
|
+
if (this.terminal)
|
|
459
|
+
return;
|
|
460
|
+
if (!observedInvocationPersistenceEnabled())
|
|
461
|
+
return;
|
|
462
|
+
// The first observation is sticky; later conflicting ones are ignored.
|
|
463
|
+
if (this.observedInvocationRecorded())
|
|
464
|
+
return;
|
|
465
|
+
try {
|
|
466
|
+
this.state = this.withObservedInvocation(this.state, observed);
|
|
467
|
+
await this.persistState();
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
logger.warn({
|
|
471
|
+
activity_id: this.state.activityId,
|
|
472
|
+
err: error instanceof Error ? error.message : String(error),
|
|
473
|
+
}, "activity observed-invocation persistence failed; clink execution remains authoritative");
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
236
477
|
async complete(completion) {
|
|
237
478
|
await this.enqueue(async () => {
|
|
238
479
|
if (this.terminal)
|
|
@@ -253,7 +494,7 @@ class FileActivityObservation {
|
|
|
253
494
|
}, "activity result persistence failed; committing terminal metadata without content");
|
|
254
495
|
}
|
|
255
496
|
const timestamp = this.options.now().toISOString();
|
|
256
|
-
|
|
497
|
+
let nextState = {
|
|
257
498
|
...this.state,
|
|
258
499
|
phase: "terminal",
|
|
259
500
|
status: completion.status,
|
|
@@ -262,9 +503,39 @@ class FileActivityObservation {
|
|
|
262
503
|
outcome: { kind: completion.outcomeKind },
|
|
263
504
|
...(result === undefined ? {} : { result }),
|
|
264
505
|
};
|
|
506
|
+
if (completion.observedInvocation !== undefined &&
|
|
507
|
+
observedInvocationPersistenceEnabled() &&
|
|
508
|
+
!this.observedInvocationRecorded()) {
|
|
509
|
+
try {
|
|
510
|
+
nextState = this.withObservedInvocation(nextState, completion.observedInvocation);
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
logger.warn({
|
|
514
|
+
activity_id: this.state.activityId,
|
|
515
|
+
err: error instanceof Error ? error.message : String(error),
|
|
516
|
+
}, "activity observed-invocation persistence failed; committing terminal metadata without it");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
this.state = nextState;
|
|
265
520
|
await this.persistState();
|
|
266
521
|
});
|
|
267
522
|
}
|
|
523
|
+
observedInvocationRecorded() {
|
|
524
|
+
return this.state.schemaVersion === "clinkx-dashboard-activity-v3" &&
|
|
525
|
+
this.state.observedInvocation !== undefined;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Best-effort V3 upgrade carrying any existing pre-dispatch audit (a V2
|
|
529
|
+
* begin) plus the runtime observation; a V1 begin upgrades with the
|
|
530
|
+
* observation alone.
|
|
531
|
+
*/
|
|
532
|
+
withObservedInvocation(state, observed) {
|
|
533
|
+
return ActivityStateV3Schema.parse({
|
|
534
|
+
...state,
|
|
535
|
+
schemaVersion: "clinkx-dashboard-activity-v3",
|
|
536
|
+
observedInvocation: observed,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
268
539
|
async enqueue(task) {
|
|
269
540
|
const next = this.writeChain.then(task, task).catch((error) => {
|
|
270
541
|
logger.warn({
|
|
@@ -279,7 +550,7 @@ class FileActivityObservation {
|
|
|
279
550
|
if (!this.options.retainResult || resultText === undefined)
|
|
280
551
|
return undefined;
|
|
281
552
|
const { bytes, truncated } = truncateUtf8(resultText, this.options.maxResultBytes);
|
|
282
|
-
await
|
|
553
|
+
await writeActivityFileAtomic(this.activityDirectory, ACTIVITY_RESULT_FILENAME, bytes);
|
|
283
554
|
return {
|
|
284
555
|
path: ACTIVITY_RESULT_FILENAME,
|
|
285
556
|
bytes: bytes.byteLength,
|
|
@@ -288,7 +559,7 @@ class FileActivityObservation {
|
|
|
288
559
|
};
|
|
289
560
|
}
|
|
290
561
|
async persistState() {
|
|
291
|
-
await
|
|
562
|
+
await writeActivityFileAtomic(this.activityDirectory, ACTIVITY_STATE_FILENAME, serializedState(this.state));
|
|
292
563
|
}
|
|
293
564
|
}
|
|
294
565
|
export function createActivityJournal(options) {
|
|
@@ -296,6 +567,8 @@ export function createActivityJournal(options) {
|
|
|
296
567
|
const sessionLogRootDir = options.sessionLogRootDir ??
|
|
297
568
|
join(options.rootDir, "..", "session-logs");
|
|
298
569
|
const now = options.now ?? (() => new Date());
|
|
570
|
+
const persistInvocationAuditV2 = invocationAuditV2PersistenceEnabled();
|
|
571
|
+
const testHooks = options.testHooks ?? {};
|
|
299
572
|
const maxResultBytes = Number.isSafeInteger(options.maxResultBytes) &&
|
|
300
573
|
(options.maxResultBytes ?? 0) > 0
|
|
301
574
|
? Math.min(options.maxResultBytes, DEFAULT_MAX_ACTIVITY_RESULT_BYTES)
|
|
@@ -314,12 +587,43 @@ export function createActivityJournal(options) {
|
|
|
314
587
|
(normalized.retainResult ? normalized.maxResultBytes : 0);
|
|
315
588
|
let admissionChain = Promise.resolve();
|
|
316
589
|
async function beginOne(input) {
|
|
590
|
+
let claimedDirectory;
|
|
317
591
|
try {
|
|
318
592
|
const parsed = ActivityIdSchema.parse(input.activityId);
|
|
593
|
+
const admissionTime = now();
|
|
594
|
+
const timestamp = admissionTime.toISOString();
|
|
595
|
+
const commonState = {
|
|
596
|
+
activityId: parsed,
|
|
597
|
+
kind: input.kind,
|
|
598
|
+
phase: "resolving",
|
|
599
|
+
status: "running",
|
|
600
|
+
adapter: input.adapter,
|
|
601
|
+
role: input.role,
|
|
602
|
+
startedAt: timestamp,
|
|
603
|
+
stateUpdatedAt: timestamp,
|
|
604
|
+
leaseUpdatedAt: timestamp,
|
|
605
|
+
...(input.projectFolders === undefined || input.projectFolders.length === 0
|
|
606
|
+
? {}
|
|
607
|
+
: { projectFolders: [...input.projectFolders] }),
|
|
608
|
+
};
|
|
609
|
+
const initialState = !persistInvocationAuditV2 || input.invocation === undefined
|
|
610
|
+
? ActivityStateV1Schema.parse({
|
|
611
|
+
schemaVersion: "clinkx-dashboard-activity-v1",
|
|
612
|
+
...commonState,
|
|
613
|
+
})
|
|
614
|
+
: ActivityStateV2Schema.parse({
|
|
615
|
+
schemaVersion: "clinkx-dashboard-activity-v2",
|
|
616
|
+
...commonState,
|
|
617
|
+
invocation: input.invocation,
|
|
618
|
+
});
|
|
619
|
+
// Admission is transactional from this point forward: validate both the
|
|
620
|
+
// complete schema and the producer byte cap before mkdir or retention can
|
|
621
|
+
// mutate observable state.
|
|
622
|
+
const initialStateBytes = serializedState(initialState);
|
|
319
623
|
await mkdir(options.rootDir, { recursive: true, mode: 0o700 });
|
|
320
624
|
await chmod(options.rootDir, 0o700);
|
|
321
625
|
try {
|
|
322
|
-
const admitted = await pruneActivities(options.rootDir, sessionLogRootDir,
|
|
626
|
+
const admitted = await pruneActivities(options.rootDir, sessionLogRootDir, admissionTime, maxAgeMs, maxEntries, maxTotalBytes, reservedBytes, testHooks);
|
|
323
627
|
if (!admitted) {
|
|
324
628
|
logger.warn({ activity_id: input.activityId }, "activity journal capacity is occupied by live or unreadable records; observation disabled");
|
|
325
629
|
return NOOP_OBSERVATION;
|
|
@@ -334,35 +638,56 @@ export function createActivityJournal(options) {
|
|
|
334
638
|
}
|
|
335
639
|
const activityDir = join(options.rootDir, parsed);
|
|
336
640
|
await mkdir(activityDir, { mode: 0o700 });
|
|
337
|
-
await
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
await writeFileAtomic(join(activityDir, ACTIVITY_STATE_FILENAME), serializedState(initialState));
|
|
641
|
+
const admittedDirectory = await inspectActivityDirectory(activityDir, parsed);
|
|
642
|
+
if (admittedDirectory === undefined) {
|
|
643
|
+
throw new Error("activity journal directory identity unavailable");
|
|
644
|
+
}
|
|
645
|
+
claimedDirectory = captureActivityDirectoryIncarnation(admittedDirectory);
|
|
646
|
+
const createdSessionLogGeneration = await (async () => {
|
|
647
|
+
try {
|
|
648
|
+
await admittedDirectory.handle.chmod(0o700);
|
|
649
|
+
await verifyActivityDirectory(admittedDirectory.path, admittedDirectory);
|
|
650
|
+
const generation = await createSessionLogGeneration(admittedDirectory.path);
|
|
651
|
+
await verifyActivityDirectory(admittedDirectory.path, admittedDirectory);
|
|
652
|
+
return generation;
|
|
653
|
+
}
|
|
654
|
+
finally {
|
|
655
|
+
await admittedDirectory.handle.close().catch(() => undefined);
|
|
656
|
+
}
|
|
657
|
+
})();
|
|
658
|
+
await writeActivityFileAtomic(claimedDirectory, ACTIVITY_STATE_FILENAME, initialStateBytes);
|
|
356
659
|
const identity = await activityIdentityReader.resolveCurrent(options.rootDir, parsed);
|
|
357
|
-
if (identity.kind !== "resolved"
|
|
660
|
+
if (identity.kind !== "resolved" ||
|
|
661
|
+
identity.activityGeneration !== claimedDirectory.localGeneration) {
|
|
358
662
|
throw new Error("activity journal directory identity unavailable");
|
|
359
663
|
}
|
|
360
|
-
return new FileActivityObservation(
|
|
664
|
+
return new FileActivityObservation(claimedDirectory, normalized, initialState, Object.freeze({
|
|
361
665
|
activityId: parsed,
|
|
362
|
-
activityGeneration:
|
|
666
|
+
activityGeneration: createdSessionLogGeneration,
|
|
363
667
|
}));
|
|
364
668
|
}
|
|
365
669
|
catch (error) {
|
|
670
|
+
if (claimedDirectory !== undefined) {
|
|
671
|
+
try {
|
|
672
|
+
const removed = await removeActivityDirectoryIncarnation({
|
|
673
|
+
activityId: claimedDirectory.activityId,
|
|
674
|
+
incarnation: claimedDirectory,
|
|
675
|
+
reason: "admission_cleanup",
|
|
676
|
+
testHooks,
|
|
677
|
+
});
|
|
678
|
+
if (removed.kind === "removed") {
|
|
679
|
+
await cleanupActivitySessionLog({
|
|
680
|
+
sessionLogRootDir,
|
|
681
|
+
activityId: claimedDirectory.activityId,
|
|
682
|
+
localGeneration: claimedDirectory.localGeneration,
|
|
683
|
+
sessionLogIdentity: removed.sessionLogIdentity,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
// Cleanup is best-effort, but never widens beyond the exact incarnation.
|
|
689
|
+
}
|
|
690
|
+
}
|
|
366
691
|
logger.warn({
|
|
367
692
|
activity_id: input.activityId,
|
|
368
693
|
err: error instanceof Error ? error.message : String(error),
|