@mono-agent/agent-app 0.19.1 → 0.20.1
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 +307 -32
- package/dist/acp-bridge.d.ts.map +1 -1
- package/dist/acp-bridge.js +113 -54
- package/dist/acp-bridge.js.map +1 -1
- package/dist/acp-session-store.d.ts +12 -0
- package/dist/acp-session-store.d.ts.map +1 -0
- package/dist/acp-session-store.js +74 -0
- package/dist/acp-session-store.js.map +1 -0
- package/dist/agent-artifact-paths.d.ts +8 -0
- package/dist/agent-artifact-paths.d.ts.map +1 -0
- package/dist/agent-artifact-paths.js +11 -0
- package/dist/agent-artifact-paths.js.map +1 -0
- package/dist/agent-root-coordinator.d.ts +101 -0
- package/dist/agent-root-coordinator.d.ts.map +1 -0
- package/dist/agent-root-coordinator.js +470 -0
- package/dist/agent-root-coordinator.js.map +1 -0
- package/dist/app-controller-channels.d.ts +13 -1
- package/dist/app-controller-channels.d.ts.map +1 -1
- package/dist/app-controller-channels.js +152 -20
- package/dist/app-controller-channels.js.map +1 -1
- package/dist/app-controller-continuation.d.ts +2 -0
- package/dist/app-controller-continuation.d.ts.map +1 -1
- package/dist/app-controller-continuation.js.map +1 -1
- package/dist/app-controller-lifecycle.d.ts +5 -0
- package/dist/app-controller-lifecycle.d.ts.map +1 -1
- package/dist/app-controller-lifecycle.js +24 -5
- package/dist/app-controller-lifecycle.js.map +1 -1
- package/dist/app-controller-maintenance.d.ts +4 -0
- package/dist/app-controller-maintenance.d.ts.map +1 -1
- package/dist/app-controller-maintenance.js +34 -2
- package/dist/app-controller-maintenance.js.map +1 -1
- package/dist/app-controller-memory.d.ts +2 -0
- package/dist/app-controller-memory.d.ts.map +1 -1
- package/dist/app-controller-memory.js +6 -3
- package/dist/app-controller-memory.js.map +1 -1
- package/dist/app-controller-process-jobs.d.ts +49 -0
- package/dist/app-controller-process-jobs.d.ts.map +1 -0
- package/dist/app-controller-process-jobs.js +294 -0
- package/dist/app-controller-process-jobs.js.map +1 -0
- package/dist/app-controller-responder.d.ts +17 -0
- package/dist/app-controller-responder.d.ts.map +1 -1
- package/dist/app-controller-responder.js +164 -13
- package/dist/app-controller-responder.js.map +1 -1
- package/dist/app-controller-traceability.d.ts +8 -0
- package/dist/app-controller-traceability.d.ts.map +1 -1
- package/dist/app-controller-traceability.js +22 -1
- package/dist/app-controller-traceability.js.map +1 -1
- package/dist/app-controller-types.d.ts +1 -1
- package/dist/app-controller-types.d.ts.map +1 -1
- package/dist/app-controller-utils.d.ts +8 -0
- package/dist/app-controller-utils.d.ts.map +1 -1
- package/dist/app-controller-utils.js +24 -1
- package/dist/app-controller-utils.js.map +1 -1
- package/dist/app-controller.d.ts +45 -1
- package/dist/app-controller.d.ts.map +1 -1
- package/dist/app-controller.js +135 -39
- package/dist/app-controller.js.map +1 -1
- package/dist/background-log-maintenance.d.ts +85 -2
- package/dist/background-log-maintenance.d.ts.map +1 -1
- package/dist/background-log-maintenance.js +407 -136
- package/dist/background-log-maintenance.js.map +1 -1
- package/dist/background-runtime.d.ts.map +1 -1
- package/dist/background-runtime.js +22 -41
- package/dist/background-runtime.js.map +1 -1
- package/dist/background.d.ts +18 -26
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +105 -138
- package/dist/background.js.map +1 -1
- package/dist/channel-drivers/cron.d.ts +12 -1
- package/dist/channel-drivers/cron.d.ts.map +1 -1
- package/dist/channel-drivers/cron.js +121 -18
- package/dist/channel-drivers/cron.js.map +1 -1
- package/dist/channel-drivers/native-notify.d.ts +3 -0
- package/dist/channel-drivers/native-notify.d.ts.map +1 -1
- package/dist/channel-drivers/native-notify.js +2 -1
- package/dist/channel-drivers/native-notify.js.map +1 -1
- package/dist/channel-drivers/slack.d.ts.map +1 -1
- package/dist/channel-drivers/slack.js +41 -4
- package/dist/channel-drivers/slack.js.map +1 -1
- package/dist/channel-drivers/telegram.d.ts.map +1 -1
- package/dist/channel-drivers/telegram.js +46 -3
- package/dist/channel-drivers/telegram.js.map +1 -1
- package/dist/channel-drivers/tui.d.ts +13 -2
- package/dist/channel-drivers/tui.d.ts.map +1 -1
- package/dist/channel-drivers/tui.js +112 -3
- package/dist/channel-drivers/tui.js.map +1 -1
- package/dist/channels.d.ts +1 -1
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +4 -2
- package/dist/channels.js.map +1 -1
- package/dist/cli-args.d.ts +15 -1
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +82 -12
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-background-command.d.ts +9 -2
- package/dist/cli-background-command.d.ts.map +1 -1
- package/dist/cli-background-command.js +179 -66
- package/dist/cli-background-command.js.map +1 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +50 -12
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +21 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +105 -2
- package/dist/config-reference.js.map +1 -1
- package/dist/configured-agent.d.ts +64 -21
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +840 -156
- package/dist/configured-agent.js.map +1 -1
- package/dist/conversation-state-roots.d.ts +59 -0
- package/dist/conversation-state-roots.d.ts.map +1 -0
- package/dist/conversation-state-roots.js +248 -0
- package/dist/conversation-state-roots.js.map +1 -0
- package/dist/cron-control-store.d.ts +91 -0
- package/dist/cron-control-store.d.ts.map +1 -0
- package/dist/cron-control-store.js +1251 -0
- package/dist/cron-control-store.js.map +1 -0
- package/dist/cron-operator-service.d.ts +54 -0
- package/dist/cron-operator-service.d.ts.map +1 -0
- package/dist/cron-operator-service.js +352 -0
- package/dist/cron-operator-service.js.map +1 -0
- package/dist/doctor-session-history.d.ts +9 -0
- package/dist/doctor-session-history.d.ts.map +1 -0
- package/dist/doctor-session-history.js +313 -0
- package/dist/doctor-session-history.js.map +1 -0
- package/dist/doctor.d.ts +2 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +356 -9
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interaction-bridge.d.ts +1 -0
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +54 -6
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/jobs-command.d.ts +17 -0
- package/dist/jobs-command.d.ts.map +1 -0
- package/dist/jobs-command.js +207 -0
- package/dist/jobs-command.js.map +1 -0
- package/dist/launchd-lifecycle-lock.d.ts +36 -0
- package/dist/launchd-lifecycle-lock.d.ts.map +1 -0
- package/dist/launchd-lifecycle-lock.js +124 -0
- package/dist/launchd-lifecycle-lock.js.map +1 -0
- package/dist/launchd-log-monitor-status.d.ts +7 -0
- package/dist/launchd-log-monitor-status.d.ts.map +1 -0
- package/dist/launchd-log-monitor-status.js +224 -0
- package/dist/launchd-log-monitor-status.js.map +1 -0
- package/dist/launchd-logs.d.ts +9 -1
- package/dist/launchd-logs.d.ts.map +1 -1
- package/dist/launchd-logs.js +69 -16
- package/dist/launchd-logs.js.map +1 -1
- package/dist/launchd-maintenance-command.d.ts +10 -0
- package/dist/launchd-maintenance-command.d.ts.map +1 -0
- package/dist/launchd-maintenance-command.js +2 -0
- package/dist/launchd-maintenance-command.js.map +1 -0
- package/dist/launchd-maintenance-entry.d.ts +30 -0
- package/dist/launchd-maintenance-entry.d.ts.map +1 -0
- package/dist/launchd-maintenance-entry.js +206 -0
- package/dist/launchd-maintenance-entry.js.map +1 -0
- package/dist/launchd-maintenance-gate.d.ts +28 -0
- package/dist/launchd-maintenance-gate.d.ts.map +1 -0
- package/dist/launchd-maintenance-gate.js +80 -0
- package/dist/launchd-maintenance-gate.js.map +1 -0
- package/dist/launchd-private-files.d.ts +3 -0
- package/dist/launchd-private-files.d.ts.map +1 -0
- package/dist/launchd-private-files.js +58 -0
- package/dist/launchd-private-files.js.map +1 -0
- package/dist/launchd.d.ts +56 -2
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +210 -5
- package/dist/launchd.js.map +1 -1
- package/dist/local-configuration.d.ts.map +1 -1
- package/dist/local-configuration.js +34 -7
- package/dist/local-configuration.js.map +1 -1
- package/dist/managed-launchd-maintenance-environment.d.ts +3 -0
- package/dist/managed-launchd-maintenance-environment.d.ts.map +1 -0
- package/dist/managed-launchd-maintenance-environment.js +12 -0
- package/dist/managed-launchd-maintenance-environment.js.map +1 -0
- package/dist/managed-runtime-launch-proof.d.ts +11 -0
- package/dist/managed-runtime-launch-proof.d.ts.map +1 -0
- package/dist/managed-runtime-launch-proof.js +59 -0
- package/dist/managed-runtime-launch-proof.js.map +1 -0
- package/dist/managed-runtime-maintenance-entry.d.ts +10 -0
- package/dist/managed-runtime-maintenance-entry.d.ts.map +1 -0
- package/dist/managed-runtime-maintenance-entry.js +244 -0
- package/dist/managed-runtime-maintenance-entry.js.map +1 -0
- package/dist/managed-web-logs.d.ts +29 -11
- package/dist/managed-web-logs.d.ts.map +1 -1
- package/dist/managed-web-logs.js +244 -121
- package/dist/managed-web-logs.js.map +1 -1
- package/dist/managed-web-maintenance-environment.d.ts +6 -0
- package/dist/managed-web-maintenance-environment.d.ts.map +1 -0
- package/dist/managed-web-maintenance-environment.js +19 -0
- package/dist/managed-web-maintenance-environment.js.map +1 -0
- package/dist/mcp-apps.d.ts +37 -0
- package/dist/mcp-apps.d.ts.map +1 -0
- package/dist/mcp-apps.js +1753 -0
- package/dist/mcp-apps.js.map +1 -0
- package/dist/memory-command.d.ts +12 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +510 -1
- package/dist/memory-command.js.map +1 -1
- package/dist/modules/known-tools.d.ts +1 -1
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +2 -0
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/posted-reply-history.d.ts.map +1 -1
- package/dist/posted-reply-history.js +12 -0
- package/dist/posted-reply-history.js.map +1 -1
- package/dist/proactive-notify.d.ts +4 -1
- package/dist/proactive-notify.d.ts.map +1 -1
- package/dist/proactive-notify.js +43 -5
- package/dist/proactive-notify.js.map +1 -1
- package/dist/process-jobs-config.d.ts +84 -0
- package/dist/process-jobs-config.d.ts.map +1 -0
- package/dist/process-jobs-config.js +427 -0
- package/dist/process-jobs-config.js.map +1 -0
- package/dist/process-jobs-context.d.ts +24 -0
- package/dist/process-jobs-context.d.ts.map +1 -0
- package/dist/process-jobs-context.js +135 -0
- package/dist/process-jobs-context.js.map +1 -0
- package/dist/process-jobs-protection.d.ts +33 -0
- package/dist/process-jobs-protection.d.ts.map +1 -0
- package/dist/process-jobs-protection.js +74 -0
- package/dist/process-jobs-protection.js.map +1 -0
- package/dist/process-jobs-root-registry.d.ts +115 -0
- package/dist/process-jobs-root-registry.d.ts.map +1 -0
- package/dist/process-jobs-root-registry.js +978 -0
- package/dist/process-jobs-root-registry.js.map +1 -0
- package/dist/process-jobs-runtime.d.ts +38 -0
- package/dist/process-jobs-runtime.d.ts.map +1 -0
- package/dist/process-jobs-runtime.js +178 -0
- package/dist/process-jobs-runtime.js.map +1 -0
- package/dist/process-jobs-service.d.ts +70 -0
- package/dist/process-jobs-service.d.ts.map +1 -0
- package/dist/process-jobs-service.js +1989 -0
- package/dist/process-jobs-service.js.map +1 -0
- package/dist/process-jobs-store.d.ts +151 -0
- package/dist/process-jobs-store.d.ts.map +1 -0
- package/dist/process-jobs-store.js +1290 -0
- package/dist/process-jobs-store.js.map +1 -0
- package/dist/reply-artifacts.d.ts +65 -0
- package/dist/reply-artifacts.d.ts.map +1 -0
- package/dist/reply-artifacts.js +1147 -0
- package/dist/reply-artifacts.js.map +1 -0
- package/dist/reply-part-budget.d.ts +16 -0
- package/dist/reply-part-budget.d.ts.map +1 -0
- package/dist/reply-part-budget.js +57 -0
- package/dist/reply-part-budget.js.map +1 -0
- package/dist/request-model-override.d.ts +19 -0
- package/dist/request-model-override.d.ts.map +1 -1
- package/dist/request-model-override.js +54 -1
- package/dist/request-model-override.js.map +1 -1
- package/dist/request-scoped-mcp.d.ts +31 -0
- package/dist/request-scoped-mcp.d.ts.map +1 -0
- package/dist/request-scoped-mcp.js +197 -0
- package/dist/request-scoped-mcp.js.map +1 -0
- package/dist/run-history.d.ts.map +1 -1
- package/dist/run-history.js +50 -254
- package/dist/run-history.js.map +1 -1
- package/dist/runtime-option-extensions.d.ts +23 -0
- package/dist/runtime-option-extensions.d.ts.map +1 -1
- package/dist/runtime-option-extensions.js +55 -0
- package/dist/runtime-option-extensions.js.map +1 -1
- package/dist/runtime-routes.d.ts +7 -1
- package/dist/runtime-routes.d.ts.map +1 -1
- package/dist/runtime-routes.js +25 -0
- package/dist/runtime-routes.js.map +1 -1
- package/dist/secure-file-replace.d.ts +3 -0
- package/dist/secure-file-replace.d.ts.map +1 -1
- package/dist/secure-file-replace.js +31 -7
- package/dist/secure-file-replace.js.map +1 -1
- package/dist/session-history.d.ts +146 -0
- package/dist/session-history.d.ts.map +1 -0
- package/dist/session-history.js +347 -0
- package/dist/session-history.js.map +1 -0
- package/dist/sessions.d.ts +45 -2
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +849 -30
- package/dist/sessions.js.map +1 -1
- package/dist/web-command.d.ts +12 -11
- package/dist/web-command.d.ts.map +1 -1
- package/dist/web-command.js +554 -80
- package/dist/web-command.js.map +1 -1
- package/dist/web-log-maintenance-command.d.ts +7 -0
- package/dist/web-log-maintenance-command.d.ts.map +1 -0
- package/dist/web-log-maintenance-command.js +21 -0
- package/dist/web-log-maintenance-command.js.map +1 -0
- package/dist/web-log-maintenance-status.d.ts +26 -0
- package/dist/web-log-maintenance-status.d.ts.map +1 -0
- package/dist/web-log-maintenance-status.js +171 -0
- package/dist/web-log-maintenance-status.js.map +1 -0
- package/dist/web-log-maintenance.d.ts +32 -0
- package/dist/web-log-maintenance.d.ts.map +1 -0
- package/dist/web-log-maintenance.js +163 -0
- package/dist/web-log-maintenance.js.map +1 -0
- package/dist/web-maintenance-paths.d.ts +12 -0
- package/dist/web-maintenance-paths.d.ts.map +1 -0
- package/dist/web-maintenance-paths.js +24 -0
- package/dist/web-maintenance-paths.js.map +1 -0
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +1 -0
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.js +1 -1
- package/dist/wizard/run.js.map +1 -1
- package/package.json +15 -15
- package/schema/mono-agent.config.schema.json +106 -1
- package/skills/mono-agent-composer/references/config-blueprint.md +27 -2
- package/skills/mono-agent-composer/references/discovery-questions.md +2 -2
- package/skills/mono-agent-composer/references/feature-coverage.md +6 -4
- package/skills/mono-agent-composer/references/package-map.md +19 -3
- package/skills/mono-agent-composer/references/playbooks.md +3 -3
- package/skills/mono-agent-composer/references/validation.md +4 -2
|
@@ -0,0 +1,1290 @@
|
|
|
1
|
+
import { createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, lstat, mkdir, opendir, readdir, realpath, rename, rm, } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { isProcessJobErrorCode, isProcessJobState, processJobPublicError, } from "@mono-agent/agent-contracts";
|
|
6
|
+
import { PROCESS_JOBS_CAPS } from "./process-jobs-config.js";
|
|
7
|
+
import { processIncarnationFromJson, } from "./process-incarnation.js";
|
|
8
|
+
import { secureFileReplace } from "./secure-file-replace.js";
|
|
9
|
+
import { readBoundedOwnerOnlyFile, syncDirectory, } from "./continuation-store-fs.js";
|
|
10
|
+
export const PROCESS_JOB_RECORD_SCHEMA = 1;
|
|
11
|
+
export const PROCESS_JOB_RECORDS_DIRECTORY = "records-v1";
|
|
12
|
+
export const PROCESS_JOB_ARTIFACTS_DIRECTORY = "artifacts";
|
|
13
|
+
export const PROCESS_JOB_MANIFEST_FILE = "process-jobs-store-v1.json";
|
|
14
|
+
export const PROCESS_JOB_TRANSACTION_FILE = "process-jobs-transaction-v1.json";
|
|
15
|
+
export const PROCESS_JOB_QUARANTINE_DIRECTORY = "quarantine-v1";
|
|
16
|
+
export const PROCESS_JOB_SECRET_FILE = "process-jobs-secret";
|
|
17
|
+
export const PROCESS_JOB_HEALTH_FILE = "process-jobs-health-v1.json";
|
|
18
|
+
export const PROCESS_JOB_ROLLBACK_GUARD = "PROCESS-JOBS-STORE-V1";
|
|
19
|
+
export const PROCESS_JOB_ROLLBACK_GUARD_CONTENT = "This state directory uses mono-agent process-job records v1. Older runtimes must not open it.\n";
|
|
20
|
+
export const PROCESS_JOB_ORPHAN_RECONCILIATION_INTERVAL = 64;
|
|
21
|
+
export const PROCESS_JOB_DESTINATION_UNAVAILABLE_ATTEMPTS = 3;
|
|
22
|
+
export const PROCESS_JOB_CONVERSATION_BUSY_ATTEMPT_COUNTER_MAX = 65_535;
|
|
23
|
+
export const PROCESS_JOB_CONVERSATION_BUSY_MAX_AGE_MS = 5 * 60 * 1_000;
|
|
24
|
+
/** Maximum simultaneously retained records plus pending wake obligations. */
|
|
25
|
+
export const PROCESS_JOB_STORE_MAX_RECORD_ENTRIES = (2 * PROCESS_JOBS_CAPS.retention.maxRecords)
|
|
26
|
+
+ PROCESS_JOBS_CAPS.maxConcurrent
|
|
27
|
+
+ PROCESS_JOBS_CAPS.maxQueued;
|
|
28
|
+
const MAX_RECORD_BYTES = 128 * 1024;
|
|
29
|
+
const MAX_TRANSACTION_BYTES = 256 * 1024;
|
|
30
|
+
const MAX_MANIFEST_BYTES = 64 * 1024;
|
|
31
|
+
const MAX_GUARD_BYTES = 4 * 1024;
|
|
32
|
+
export const MAX_PROCESS_JOB_HEALTH_BYTES = 4 * 1024;
|
|
33
|
+
export const PROCESS_JOB_ENV_KEYS_CAPS = Object.freeze({
|
|
34
|
+
maxItems: 128,
|
|
35
|
+
maxItemBytes: 256,
|
|
36
|
+
maxTotalBytes: 8 * 1024,
|
|
37
|
+
});
|
|
38
|
+
const MAX_QUARANTINED_TRANSACTIONS = 10_000;
|
|
39
|
+
const MAX_RECORD_TEMP_ENTRIES = 1;
|
|
40
|
+
const RECORD_CAPACITY_ERROR = "Process-job durable record capacity is exceeded.";
|
|
41
|
+
class ProcessJobStoreMutationError extends Error {
|
|
42
|
+
constructor(message, options) {
|
|
43
|
+
super(message, options);
|
|
44
|
+
this.name = "ProcessJobStoreMutationError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
class UnreplayableProcessJobTransactionError extends Error {
|
|
48
|
+
constructor(message, options) {
|
|
49
|
+
super(message, options);
|
|
50
|
+
this.name = "UnreplayableProcessJobTransactionError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Transactional copy-on-read Map that never exposes store-owned record objects. */
|
|
54
|
+
class ProcessJobMutationDraft extends Map {
|
|
55
|
+
#source;
|
|
56
|
+
#overrides = new Map();
|
|
57
|
+
#deleted = new Set();
|
|
58
|
+
#reinserted = new Set();
|
|
59
|
+
#workCounter;
|
|
60
|
+
constructor(source, workCounter) {
|
|
61
|
+
super();
|
|
62
|
+
this.#source = source;
|
|
63
|
+
this.#workCounter = workCounter;
|
|
64
|
+
}
|
|
65
|
+
get size() {
|
|
66
|
+
let added = 0;
|
|
67
|
+
for (const key of this.#overrides.keys()) {
|
|
68
|
+
if (!this.#source.has(key))
|
|
69
|
+
added += 1;
|
|
70
|
+
}
|
|
71
|
+
return this.#source.size - this.#deleted.size + added;
|
|
72
|
+
}
|
|
73
|
+
get(key) {
|
|
74
|
+
if (this.#deleted.has(key))
|
|
75
|
+
return undefined;
|
|
76
|
+
const overridden = this.#overrides.get(key);
|
|
77
|
+
if (overridden !== undefined)
|
|
78
|
+
return overridden;
|
|
79
|
+
const current = this.#source.get(key);
|
|
80
|
+
if (current === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
const owned = structuredClone(current);
|
|
83
|
+
incrementWork(this.#workCounter, "mutationEntriesExamined");
|
|
84
|
+
this.#overrides.set(key, owned);
|
|
85
|
+
return owned;
|
|
86
|
+
}
|
|
87
|
+
has(key) {
|
|
88
|
+
return !this.#deleted.has(key) && (this.#overrides.has(key) || this.#source.has(key));
|
|
89
|
+
}
|
|
90
|
+
set(key, value) {
|
|
91
|
+
if (this.#deleted.delete(key) && this.#source.has(key))
|
|
92
|
+
this.#reinserted.add(key);
|
|
93
|
+
if (!this.#overrides.has(key))
|
|
94
|
+
incrementWork(this.#workCounter, "mutationEntriesExamined");
|
|
95
|
+
this.#overrides.set(key, value);
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
delete(key) {
|
|
99
|
+
if (!this.has(key))
|
|
100
|
+
return false;
|
|
101
|
+
this.#overrides.delete(key);
|
|
102
|
+
this.#reinserted.delete(key);
|
|
103
|
+
if (this.#source.has(key))
|
|
104
|
+
this.#deleted.add(key);
|
|
105
|
+
incrementWork(this.#workCounter, "mutationEntriesExamined");
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
clear() {
|
|
109
|
+
if (this.size === 0)
|
|
110
|
+
return;
|
|
111
|
+
this.#overrides.clear();
|
|
112
|
+
this.#reinserted.clear();
|
|
113
|
+
for (const key of this.#source.keys())
|
|
114
|
+
this.#deleted.add(key);
|
|
115
|
+
if (this.#workCounter !== undefined) {
|
|
116
|
+
this.#workCounter.mutationEntriesExamined += this.#deleted.size;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
keys() {
|
|
120
|
+
return this.#iterateKeys();
|
|
121
|
+
}
|
|
122
|
+
values() {
|
|
123
|
+
return this.#iterateValues();
|
|
124
|
+
}
|
|
125
|
+
entries() {
|
|
126
|
+
return this.#iterateEntries();
|
|
127
|
+
}
|
|
128
|
+
[Symbol.iterator]() {
|
|
129
|
+
return this.entries();
|
|
130
|
+
}
|
|
131
|
+
forEach(callbackfn, thisArg) {
|
|
132
|
+
for (const [key, value] of this.entries())
|
|
133
|
+
callbackfn.call(thisArg, value, key, this);
|
|
134
|
+
}
|
|
135
|
+
candidateEntries() {
|
|
136
|
+
return this.#overrides.entries();
|
|
137
|
+
}
|
|
138
|
+
deletedKeys() {
|
|
139
|
+
return this.#deleted.values();
|
|
140
|
+
}
|
|
141
|
+
*#iterateKeys() {
|
|
142
|
+
for (const key of this.#source.keys()) {
|
|
143
|
+
if (!this.#deleted.has(key) && !this.#reinserted.has(key))
|
|
144
|
+
yield key;
|
|
145
|
+
}
|
|
146
|
+
for (const key of this.#overrides.keys()) {
|
|
147
|
+
if (!this.#source.has(key) || this.#reinserted.has(key))
|
|
148
|
+
yield key;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
*#iterateValues() {
|
|
152
|
+
for (const key of this.#iterateKeys()) {
|
|
153
|
+
const value = this.get(key);
|
|
154
|
+
if (value !== undefined)
|
|
155
|
+
yield value;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
*#iterateEntries() {
|
|
159
|
+
for (const key of this.#iterateKeys()) {
|
|
160
|
+
const value = this.get(key);
|
|
161
|
+
if (value !== undefined)
|
|
162
|
+
yield [key, value];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Open and recover the independent per-record v1 process-job store. */
|
|
167
|
+
export async function openProcessJobStore(agentRoot, stateDir, options = {}) {
|
|
168
|
+
const maxRecordEntries = options.maxRecordEntries ?? PROCESS_JOB_STORE_MAX_RECORD_ENTRIES;
|
|
169
|
+
if (!Number.isInteger(maxRecordEntries)
|
|
170
|
+
|| maxRecordEntries < 1
|
|
171
|
+
|| maxRecordEntries > PROCESS_JOB_STORE_MAX_RECORD_ENTRIES) {
|
|
172
|
+
throw new Error("Process-job store record-entry limit is invalid.");
|
|
173
|
+
}
|
|
174
|
+
const { root, confined } = await resolveConfinedStateDirectory(agentRoot, stateDir);
|
|
175
|
+
await ensureConfinedPrivateDirectory(root, confined);
|
|
176
|
+
const recordsDir = join(confined, PROCESS_JOB_RECORDS_DIRECTORY);
|
|
177
|
+
const artifactsDir = join(confined, PROCESS_JOB_ARTIFACTS_DIRECTORY);
|
|
178
|
+
const quarantineDir = join(confined, PROCESS_JOB_QUARANTINE_DIRECTORY);
|
|
179
|
+
await ensureConfinedPrivateDirectory(root, recordsDir);
|
|
180
|
+
await ensureConfinedPrivateDirectory(root, artifactsDir);
|
|
181
|
+
await ensureConfinedPrivateDirectory(root, quarantineDir);
|
|
182
|
+
const manifestPath = join(confined, PROCESS_JOB_MANIFEST_FILE);
|
|
183
|
+
const transactionPath = join(confined, PROCESS_JOB_TRANSACTION_FILE);
|
|
184
|
+
const guardPath = join(confined, PROCESS_JOB_ROLLBACK_GUARD);
|
|
185
|
+
await ensureRollbackGuard(guardPath);
|
|
186
|
+
let manifest = await readManifest(manifestPath);
|
|
187
|
+
if (manifest !== undefined && manifest.records > maxRecordEntries) {
|
|
188
|
+
throw new Error(RECORD_CAPACITY_ERROR);
|
|
189
|
+
}
|
|
190
|
+
let transaction;
|
|
191
|
+
try {
|
|
192
|
+
transaction = await readTransaction(transactionPath);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (!(error instanceof UnreplayableProcessJobTransactionError))
|
|
196
|
+
throw error;
|
|
197
|
+
await quarantineUnreplayableTransaction(confined, quarantineDir, transactionPath);
|
|
198
|
+
}
|
|
199
|
+
let directory = await inspectRecordDirectory(recordsDir, maxRecordEntries, options.workCounter);
|
|
200
|
+
if (transaction !== undefined) {
|
|
201
|
+
if (transaction.write !== null
|
|
202
|
+
&& !directory.jobIds.has(transaction.write.jobId)
|
|
203
|
+
&& directory.recordCount >= maxRecordEntries) {
|
|
204
|
+
throw new Error(RECORD_CAPACITY_ERROR);
|
|
205
|
+
}
|
|
206
|
+
await applyTransaction(recordsDir, transaction);
|
|
207
|
+
directory = await inspectRecordDirectory(recordsDir, maxRecordEntries, options.workCounter);
|
|
208
|
+
manifest = await persistManifest(manifestPath, transaction.generation, directory.recordCount);
|
|
209
|
+
await rm(transactionPath, { force: true });
|
|
210
|
+
await syncDirectory(confined);
|
|
211
|
+
}
|
|
212
|
+
const records = await loadRecords(recordsDir, maxRecordEntries);
|
|
213
|
+
if (manifest === undefined) {
|
|
214
|
+
manifest = await persistManifest(manifestPath, randomUUID(), records.size);
|
|
215
|
+
}
|
|
216
|
+
else if (manifest.records !== records.size) {
|
|
217
|
+
throw new Error("Process-job manifest record count does not match the durable record set.");
|
|
218
|
+
}
|
|
219
|
+
const workCounter = options.workCounter;
|
|
220
|
+
let artifactBytesByJob = await removeOrphanArtifacts(artifactsDir, records, workCounter);
|
|
221
|
+
let retainedArtifactByteTotal = retainedArtifactBytesFor(records, artifactBytesByJob);
|
|
222
|
+
let retentionApplications = 0;
|
|
223
|
+
const quarantinedTransactions = await inspectQuarantinedTransactions(quarantineDir);
|
|
224
|
+
let tail = Promise.resolve();
|
|
225
|
+
let poisoned;
|
|
226
|
+
const withLock = async (operation) => {
|
|
227
|
+
const previous = tail;
|
|
228
|
+
let release;
|
|
229
|
+
tail = new Promise((resolvePromise) => { release = resolvePromise; });
|
|
230
|
+
await previous;
|
|
231
|
+
try {
|
|
232
|
+
if (poisoned !== undefined)
|
|
233
|
+
throw poisoned;
|
|
234
|
+
return await operation();
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
release();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const setArtifactBytes = (jobId, bytes) => {
|
|
241
|
+
const record = records.get(jobId);
|
|
242
|
+
const before = retainedArtifactContribution(record, artifactBytesByJob.get(jobId));
|
|
243
|
+
if (bytes === undefined)
|
|
244
|
+
artifactBytesByJob.delete(jobId);
|
|
245
|
+
else
|
|
246
|
+
artifactBytesByJob.set(jobId, bytes);
|
|
247
|
+
retainedArtifactByteTotal += retainedArtifactContribution(record, bytes) - before;
|
|
248
|
+
};
|
|
249
|
+
const commitChanges = (changes) => {
|
|
250
|
+
for (const change of changes) {
|
|
251
|
+
const jobId = change.write?.jobId ?? change.delete;
|
|
252
|
+
if (jobId === null)
|
|
253
|
+
continue;
|
|
254
|
+
const before = retainedArtifactContribution(records.get(jobId), artifactBytesByJob.get(jobId));
|
|
255
|
+
if (change.write === null)
|
|
256
|
+
records.delete(jobId);
|
|
257
|
+
else
|
|
258
|
+
records.set(jobId, structuredClone(change.write));
|
|
259
|
+
retainedArtifactByteTotal += retainedArtifactContribution(records.get(jobId), artifactBytesByJob.get(jobId)) - before;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const store = {
|
|
263
|
+
stateDir: confined,
|
|
264
|
+
recordsDir,
|
|
265
|
+
artifactsDir,
|
|
266
|
+
health: {
|
|
267
|
+
state: quarantinedTransactions === 0 ? "ok" : "degraded",
|
|
268
|
+
quarantinedTransactions,
|
|
269
|
+
},
|
|
270
|
+
async get(jobId) {
|
|
271
|
+
return await withLock(async () => clone(records.get(jobId)));
|
|
272
|
+
},
|
|
273
|
+
async list() {
|
|
274
|
+
return await withLock(async () => [...records.values()].map((record) => structuredClone(record)));
|
|
275
|
+
},
|
|
276
|
+
async mutate(operation) {
|
|
277
|
+
return await withLock(async () => {
|
|
278
|
+
const draft = new ProcessJobMutationDraft(records, workCounter);
|
|
279
|
+
const result = await operation(draft);
|
|
280
|
+
if (draft.size > maxRecordEntries)
|
|
281
|
+
throw new ProcessJobStoreMutationError(RECORD_CAPACITY_ERROR);
|
|
282
|
+
let changes;
|
|
283
|
+
try {
|
|
284
|
+
changes = await persistDiff(confined, recordsDir, transactionPath, manifestPath, records, draft, workCounter);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
if (!(error instanceof ProcessJobStoreMutationError))
|
|
288
|
+
poisoned = error;
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
commitChanges(changes);
|
|
292
|
+
return result;
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
async ensureArtifacts(jobId) {
|
|
296
|
+
return await withLock(async () => {
|
|
297
|
+
assertJobId(jobId);
|
|
298
|
+
const directory = join(artifactsDir, jobId);
|
|
299
|
+
let existed = true;
|
|
300
|
+
try {
|
|
301
|
+
await lstat(directory);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (!isErrno(error, "ENOENT"))
|
|
305
|
+
throw error;
|
|
306
|
+
existed = false;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
await ensureConfinedPrivateDirectory(root, directory);
|
|
310
|
+
const stdoutRef = `${PROCESS_JOB_ARTIFACTS_DIRECTORY}/${jobId}/stdout.log`;
|
|
311
|
+
const stderrRef = `${PROCESS_JOB_ARTIFACTS_DIRECTORY}/${jobId}/stderr.log`;
|
|
312
|
+
for (const path of [join(directory, "stdout.log"), join(directory, "stderr.log")]) {
|
|
313
|
+
try {
|
|
314
|
+
assertPrivateFile(await lstat(path), path, 0o600);
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
if (!isErrno(error, "ENOENT"))
|
|
318
|
+
throw error;
|
|
319
|
+
await replacePrivateFile(path, "", 0o600);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
setArtifactBytes(jobId, await inspectArtifactDirectoryBytes(directory, workCounter));
|
|
323
|
+
return { stdoutRef, stderrRef };
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
// A rejected admission must not accumulate half-created artifact
|
|
327
|
+
// directories. Never remove a directory that predated this unique id;
|
|
328
|
+
// open-time orphan recovery remains the crash-only fallback.
|
|
329
|
+
if (!existed) {
|
|
330
|
+
try {
|
|
331
|
+
await rm(directory, { recursive: true, force: true });
|
|
332
|
+
setArtifactBytes(jobId, undefined);
|
|
333
|
+
await syncDirectory(artifactsDir);
|
|
334
|
+
}
|
|
335
|
+
catch (cleanupError) {
|
|
336
|
+
throw new AggregateError([error, cleanupError], "Process-job artifact admission and rejected-artifact cleanup both failed.");
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
async discardArtifacts(jobId) {
|
|
344
|
+
await withLock(async () => {
|
|
345
|
+
assertJobId(jobId);
|
|
346
|
+
await rm(join(artifactsDir, jobId), { recursive: true, force: true });
|
|
347
|
+
setArtifactBytes(jobId, undefined);
|
|
348
|
+
await syncDirectory(artifactsDir);
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
async writeArtifact(jobId, stream, contents) {
|
|
352
|
+
await withLock(async () => {
|
|
353
|
+
assertJobId(jobId);
|
|
354
|
+
const path = join(artifactsDir, jobId, `${stream}.log`);
|
|
355
|
+
await replacePrivateFile(path, contents, 0o600);
|
|
356
|
+
await syncDirectory(dirname(path));
|
|
357
|
+
setArtifactBytes(jobId, await inspectArtifactDirectoryBytes(dirname(path), workCounter));
|
|
358
|
+
});
|
|
359
|
+
},
|
|
360
|
+
async applyRetention(settings, now = new Date()) {
|
|
361
|
+
await withLock(async () => {
|
|
362
|
+
const terminal = [...records.values()]
|
|
363
|
+
.filter((record) => isTerminalProcessJobState(record.state))
|
|
364
|
+
.sort((left, right) => terminalTime(left) - terminalTime(right)
|
|
365
|
+
|| left.jobId.localeCompare(right.jobId));
|
|
366
|
+
// A pending wake is still live state. Retiring its record or artifacts
|
|
367
|
+
// would erase the durable delivery obligation or evidence it references.
|
|
368
|
+
const retireable = terminal.filter((record) => record.wake.state !== "pending");
|
|
369
|
+
const cutoff = now.getTime() - settings.retention.maxAgeMs;
|
|
370
|
+
const remove = new Set(retireable.filter((record) => terminalTime(record) < cutoff).map((record) => record.jobId));
|
|
371
|
+
const retained = retireable.filter((record) => !remove.has(record.jobId));
|
|
372
|
+
while (retained.length > settings.retention.maxRecords) {
|
|
373
|
+
const next = retained.shift();
|
|
374
|
+
if (next === undefined)
|
|
375
|
+
break;
|
|
376
|
+
remove.add(next.jobId);
|
|
377
|
+
}
|
|
378
|
+
const draft = new ProcessJobMutationDraft(records, workCounter);
|
|
379
|
+
for (const jobId of remove)
|
|
380
|
+
draft.delete(jobId);
|
|
381
|
+
const artifactRemovals = new Set(remove);
|
|
382
|
+
const artifactRecords = terminal.filter((record) => !remove.has(record.jobId));
|
|
383
|
+
let artifactBytes = retainedArtifactByteTotal;
|
|
384
|
+
for (const jobId of remove)
|
|
385
|
+
artifactBytes -= artifactBytesByJob.get(jobId) ?? 0;
|
|
386
|
+
for (const record of artifactRecords) {
|
|
387
|
+
if (artifactBytes <= settings.retention.artifactMaxBytes)
|
|
388
|
+
break;
|
|
389
|
+
if (record.wake.state === "pending")
|
|
390
|
+
continue;
|
|
391
|
+
artifactBytes -= artifactBytesByJob.get(record.jobId) ?? 0;
|
|
392
|
+
artifactRemovals.add(record.jobId);
|
|
393
|
+
const mutable = draft.get(record.jobId);
|
|
394
|
+
if (mutable !== undefined) {
|
|
395
|
+
mutable.stdoutRef = null;
|
|
396
|
+
mutable.stderrRef = null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
let changes;
|
|
400
|
+
try {
|
|
401
|
+
changes = await persistDiff(confined, recordsDir, transactionPath, manifestPath, records, draft, workCounter);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
if (!(error instanceof ProcessJobStoreMutationError))
|
|
405
|
+
poisoned = error;
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
commitChanges(changes);
|
|
409
|
+
// Records and ref removals reach durable storage first. A crash after
|
|
410
|
+
// that point can leave only orphan artifacts, which open-time recovery
|
|
411
|
+
// removes; it can never leave a retained record pointing at output that
|
|
412
|
+
// was deleted before its transaction committed.
|
|
413
|
+
for (const jobId of artifactRemovals) {
|
|
414
|
+
await rm(join(artifactsDir, jobId), { recursive: true, force: true });
|
|
415
|
+
setArtifactBytes(jobId, undefined);
|
|
416
|
+
}
|
|
417
|
+
if (artifactRemovals.size > 0)
|
|
418
|
+
await syncDirectory(artifactsDir);
|
|
419
|
+
retentionApplications = (retentionApplications + 1) % PROCESS_JOB_ORPHAN_RECONCILIATION_INTERVAL;
|
|
420
|
+
if (retentionApplications === 0) {
|
|
421
|
+
incrementWork(workCounter, "orphanReconciliations");
|
|
422
|
+
artifactBytesByJob = await removeOrphanArtifacts(artifactsDir, records, workCounter);
|
|
423
|
+
retainedArtifactByteTotal = retainedArtifactBytesFor(records, artifactBytesByJob);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
return store;
|
|
429
|
+
}
|
|
430
|
+
/** Prepare only the confined private state root, before acquiring its live-owner lock. */
|
|
431
|
+
export async function prepareProcessJobStateDirectory(agentRoot, stateDir) {
|
|
432
|
+
const { root, confined } = await resolveConfinedStateDirectory(agentRoot, stateDir);
|
|
433
|
+
await ensureConfinedPrivateDirectory(root, confined);
|
|
434
|
+
return confined;
|
|
435
|
+
}
|
|
436
|
+
/** Read-only migration probe for one exact configured/default durable store. */
|
|
437
|
+
export async function hasExactProcessJobStateMarkers(agentRoot, stateDir) {
|
|
438
|
+
const { confined } = await resolveConfinedStateDirectory(agentRoot, stateDir);
|
|
439
|
+
let details;
|
|
440
|
+
try {
|
|
441
|
+
details = await lstat(confined);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
if (isErrno(error, "ENOENT"))
|
|
445
|
+
return false;
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
if (!details.isDirectory() || details.isSymbolicLink()
|
|
449
|
+
|| (typeof process.getuid === "function" && details.uid !== process.getuid())
|
|
450
|
+
|| (process.platform !== "win32" && (details.mode & 0o077) !== 0)) {
|
|
451
|
+
throw new Error(`Process-job state marker root is unsafe: ${confined}`);
|
|
452
|
+
}
|
|
453
|
+
let guard;
|
|
454
|
+
try {
|
|
455
|
+
guard = await readBoundedOwnerOnlyFile(join(confined, PROCESS_JOB_ROLLBACK_GUARD), MAX_GUARD_BYTES, "Process-job rollback guard");
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
if (isErrno(error, "ENOENT"))
|
|
459
|
+
return false;
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
if (guard !== PROCESS_JOB_ROLLBACK_GUARD_CONTENT) {
|
|
463
|
+
throw new Error(`Process-job rollback guard contents are invalid: ${confined}`);
|
|
464
|
+
}
|
|
465
|
+
return await readManifest(join(confined, PROCESS_JOB_MANIFEST_FILE)) !== undefined;
|
|
466
|
+
}
|
|
467
|
+
export async function loadOrCreateProcessJobSecret(stateDir) {
|
|
468
|
+
const path = join(stateDir, PROCESS_JOB_SECRET_FILE);
|
|
469
|
+
try {
|
|
470
|
+
const encoded = (await readBoundedOwnerOnlyFile(path, 256, "Process-job operator secret")).trim();
|
|
471
|
+
const secret = Buffer.from(encoded, "base64url");
|
|
472
|
+
if (secret.length !== 32 || secret.toString("base64url") !== encoded) {
|
|
473
|
+
throw new Error(`Process-job operator secret has invalid contents: ${path}`);
|
|
474
|
+
}
|
|
475
|
+
return secret;
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
if (!isErrno(error, "ENOENT"))
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
const secret = randomBytes(32);
|
|
482
|
+
await replacePrivateFile(path, `${secret.toString("base64url")}\n`, 0o600);
|
|
483
|
+
await syncDirectory(stateDir);
|
|
484
|
+
return secret;
|
|
485
|
+
}
|
|
486
|
+
/** Persist only a bounded, secret-free health fact for detached status/doctor readers. */
|
|
487
|
+
export async function recordProcessJobHealthIncident(stateDir, operation, detectedAt = new Date()) {
|
|
488
|
+
const normalizedOperation = /^[a-z][a-z0-9_.-]{0,63}$/u.test(operation)
|
|
489
|
+
? operation
|
|
490
|
+
: "unknown";
|
|
491
|
+
const incident = {
|
|
492
|
+
schemaVersion: 1,
|
|
493
|
+
state: "degraded",
|
|
494
|
+
operation: normalizedOperation,
|
|
495
|
+
detectedAt: detectedAt.toISOString(),
|
|
496
|
+
};
|
|
497
|
+
await writeBoundedJson(join(stateDir, PROCESS_JOB_HEALTH_FILE), incident, MAX_PROCESS_JOB_HEALTH_BYTES);
|
|
498
|
+
}
|
|
499
|
+
/** Clear a prior runtime incident only after startup recovery and retention succeed. */
|
|
500
|
+
export async function clearProcessJobHealthIncident(stateDir) {
|
|
501
|
+
await rm(join(stateDir, PROCESS_JOB_HEALTH_FILE), { force: true });
|
|
502
|
+
await syncDirectory(stateDir);
|
|
503
|
+
}
|
|
504
|
+
export function processJobOperatorToken(secret) {
|
|
505
|
+
if (secret.byteLength !== 32)
|
|
506
|
+
throw new Error("Process-job operator secret must contain exactly 32 bytes.");
|
|
507
|
+
return createHmac("sha256", secret).update("mono-agent-process-job-operator-v1").digest("base64url");
|
|
508
|
+
}
|
|
509
|
+
export function projectProcessJob(record) {
|
|
510
|
+
return {
|
|
511
|
+
schema: "mono-agent.process-job-projection.v1",
|
|
512
|
+
jobId: record.jobId,
|
|
513
|
+
tool: record.tool,
|
|
514
|
+
state: record.state,
|
|
515
|
+
summary: record.summary,
|
|
516
|
+
origin: {
|
|
517
|
+
conversationId: record.origin.conversationId,
|
|
518
|
+
channel: record.origin.channel,
|
|
519
|
+
runId: record.origin.runId,
|
|
520
|
+
historyBoundary: record.origin.historyBoundary,
|
|
521
|
+
bucket: record.origin.bucket,
|
|
522
|
+
},
|
|
523
|
+
timestamps: {
|
|
524
|
+
admittedAt: record.admittedAt,
|
|
525
|
+
queueDeadlineAt: record.queueDeadlineAt,
|
|
526
|
+
startedAt: record.startedAt,
|
|
527
|
+
runtimeDeadlineAt: record.runtimeDeadlineAt,
|
|
528
|
+
completedAt: record.completedAt,
|
|
529
|
+
},
|
|
530
|
+
limits: {
|
|
531
|
+
maxRuntimeMs: record.maxRuntimeMs,
|
|
532
|
+
maxOutputBytes: record.maxOutputBytes,
|
|
533
|
+
previewChars: record.previewChars,
|
|
534
|
+
chainDepth: record.chainDepth,
|
|
535
|
+
},
|
|
536
|
+
output: {
|
|
537
|
+
stdoutBytes: record.stdoutBytes,
|
|
538
|
+
stderrBytes: record.stderrBytes,
|
|
539
|
+
truncated: record.truncated,
|
|
540
|
+
preview: record.preview,
|
|
541
|
+
stdoutRef: record.stdoutRef,
|
|
542
|
+
stderrRef: record.stderrRef,
|
|
543
|
+
},
|
|
544
|
+
wake: {
|
|
545
|
+
state: record.wake.state,
|
|
546
|
+
attempts: record.wake.attempts,
|
|
547
|
+
deliveryKey: record.wake.deliveryKey,
|
|
548
|
+
lastAttemptAt: record.wake.lastAttemptAt,
|
|
549
|
+
},
|
|
550
|
+
exitCode: record.exitCode,
|
|
551
|
+
signal: record.signal,
|
|
552
|
+
durationMs: record.durationMs,
|
|
553
|
+
cancelRequested: record.cancelRequested,
|
|
554
|
+
lastError: record.lastError === null ? null : processJobPublicError(record.lastError.code),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
export function isTerminalProcessJobState(state) {
|
|
558
|
+
return state === "succeeded"
|
|
559
|
+
|| state === "failed"
|
|
560
|
+
|| state === "timed_out"
|
|
561
|
+
|| state === "cancelled"
|
|
562
|
+
|| state === "spawn_failed"
|
|
563
|
+
|| state === "queue_expired"
|
|
564
|
+
|| state === "interrupted";
|
|
565
|
+
}
|
|
566
|
+
async function persistDiff(stateDir, recordsDir, transactionPath, manifestPath, before, after, workCounter) {
|
|
567
|
+
const changes = [];
|
|
568
|
+
// Existing records were validated when loaded or committed. Validate every
|
|
569
|
+
// changed/new record before publishing any transaction marker, while leaving
|
|
570
|
+
// untouched retained records out of mutation work entirely.
|
|
571
|
+
for (const [jobId, record] of after.candidateEntries()) {
|
|
572
|
+
const previous = before.get(jobId);
|
|
573
|
+
if (previous !== undefined && isDeepStrictEqual(previous, record))
|
|
574
|
+
continue;
|
|
575
|
+
try {
|
|
576
|
+
const publicRecord = publicDurableRecord(record);
|
|
577
|
+
assertJobId(jobId);
|
|
578
|
+
assertDurableRecord(publicRecord);
|
|
579
|
+
if (publicRecord.jobId !== jobId)
|
|
580
|
+
throw new Error("Process-job record key does not match its job id.");
|
|
581
|
+
assertBoundedJson(join(recordsDir, `${jobId}.json`), publicRecord, MAX_RECORD_BYTES);
|
|
582
|
+
incrementWork(workCounter, "mutationRecordsValidated");
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
const reason = error instanceof Error ? ` ${error.message}` : "";
|
|
586
|
+
throw new ProcessJobStoreMutationError(`Process-job mutation produced an invalid durable record.${reason}`, { cause: error });
|
|
587
|
+
}
|
|
588
|
+
changes.push({ write: publicDurableRecord(record), delete: null, countDelta: previous === undefined ? 1 : 0 });
|
|
589
|
+
}
|
|
590
|
+
for (const jobId of after.deletedKeys())
|
|
591
|
+
changes.push({ write: null, delete: jobId, countDelta: -1 });
|
|
592
|
+
let durableCount = before.size;
|
|
593
|
+
for (const change of changes) {
|
|
594
|
+
const generation = randomUUID();
|
|
595
|
+
if (change.write !== null)
|
|
596
|
+
change.write.generation = generation;
|
|
597
|
+
const transaction = {
|
|
598
|
+
schemaVersion: 1,
|
|
599
|
+
generation,
|
|
600
|
+
createdAt: new Date().toISOString(),
|
|
601
|
+
write: change.write,
|
|
602
|
+
delete: change.delete,
|
|
603
|
+
};
|
|
604
|
+
await writeBoundedJson(transactionPath, transaction, MAX_TRANSACTION_BYTES);
|
|
605
|
+
await applyTransaction(recordsDir, transaction);
|
|
606
|
+
durableCount += change.countDelta;
|
|
607
|
+
// Each transaction is independently reopenable. A crash between changes
|
|
608
|
+
// may expose a safe prefix, but never a manifest whose count describes
|
|
609
|
+
// records that have not reached disk yet; the idempotent caller can retry.
|
|
610
|
+
await persistManifest(manifestPath, generation, durableCount);
|
|
611
|
+
await rm(transactionPath, { force: true });
|
|
612
|
+
await syncDirectory(stateDir);
|
|
613
|
+
incrementWork(workCounter, "mutationEntriesPersisted");
|
|
614
|
+
}
|
|
615
|
+
if (durableCount !== after.size)
|
|
616
|
+
throw new Error("Process-job durable count diverged during mutation.");
|
|
617
|
+
return changes;
|
|
618
|
+
}
|
|
619
|
+
async function applyTransaction(recordsDir, transaction) {
|
|
620
|
+
if (transaction.write !== null) {
|
|
621
|
+
const record = { ...transaction.write, generation: transaction.generation };
|
|
622
|
+
assertDurableRecord(record);
|
|
623
|
+
await writeBoundedJson(join(recordsDir, `${record.jobId}.json`), record, MAX_RECORD_BYTES);
|
|
624
|
+
}
|
|
625
|
+
else if (transaction.delete !== null) {
|
|
626
|
+
assertJobId(transaction.delete);
|
|
627
|
+
await rm(join(recordsDir, `${transaction.delete}.json`), { force: true });
|
|
628
|
+
}
|
|
629
|
+
await syncDirectory(recordsDir);
|
|
630
|
+
}
|
|
631
|
+
async function loadRecords(recordsDir, maxRecordEntries) {
|
|
632
|
+
const records = new Map();
|
|
633
|
+
let entriesExamined = 0;
|
|
634
|
+
let recordCount = 0;
|
|
635
|
+
let tempCount = 0;
|
|
636
|
+
const directory = await opendir(recordsDir);
|
|
637
|
+
for await (const entry of directory) {
|
|
638
|
+
entriesExamined += 1;
|
|
639
|
+
if (entriesExamined > maxRecordEntries + MAX_RECORD_TEMP_ENTRIES) {
|
|
640
|
+
throw new Error(RECORD_CAPACITY_ERROR);
|
|
641
|
+
}
|
|
642
|
+
if (entry.name.startsWith(".") && entry.name.endsWith(".tmp")) {
|
|
643
|
+
tempCount += 1;
|
|
644
|
+
if (!entry.isFile() || tempCount > MAX_RECORD_TEMP_ENTRIES) {
|
|
645
|
+
throw new Error("Process-job record directory contains unsupported temporary entries.");
|
|
646
|
+
}
|
|
647
|
+
await rm(join(recordsDir, entry.name), { force: true });
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
651
|
+
throw new Error("Process-job record directory contains an unsupported entry.");
|
|
652
|
+
}
|
|
653
|
+
recordCount += 1;
|
|
654
|
+
if (recordCount > maxRecordEntries)
|
|
655
|
+
throw new Error(RECORD_CAPACITY_ERROR);
|
|
656
|
+
const path = join(recordsDir, entry.name);
|
|
657
|
+
const value = parseJson(await readBoundedOwnerOnlyFile(path, MAX_RECORD_BYTES, "Process-job record"), path);
|
|
658
|
+
assertDurableRecord(value);
|
|
659
|
+
if (entry.name !== `${value.jobId}.json` || records.has(value.jobId)) {
|
|
660
|
+
throw new Error(`Process-job record filename is invalid or duplicated: ${entry.name}`);
|
|
661
|
+
}
|
|
662
|
+
records.set(value.jobId, structuredClone(value));
|
|
663
|
+
}
|
|
664
|
+
return records;
|
|
665
|
+
}
|
|
666
|
+
async function inspectRecordDirectory(recordsDir, maxRecordEntries, workCounter) {
|
|
667
|
+
let recordCount = 0;
|
|
668
|
+
let tempCount = 0;
|
|
669
|
+
const jobIds = new Set();
|
|
670
|
+
const directory = await opendir(recordsDir);
|
|
671
|
+
for await (const entry of directory) {
|
|
672
|
+
if (workCounter !== undefined) {
|
|
673
|
+
workCounter.recordEntriesExaminedAtOpen = (workCounter.recordEntriesExaminedAtOpen ?? 0) + 1;
|
|
674
|
+
}
|
|
675
|
+
if (entry.name.startsWith(".") && entry.name.endsWith(".tmp")) {
|
|
676
|
+
tempCount += 1;
|
|
677
|
+
if (!entry.isFile() || tempCount > MAX_RECORD_TEMP_ENTRIES) {
|
|
678
|
+
throw new Error("Process-job record directory contains unsupported temporary entries.");
|
|
679
|
+
}
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
683
|
+
throw new Error("Process-job record directory contains an unsupported entry.");
|
|
684
|
+
}
|
|
685
|
+
recordCount += 1;
|
|
686
|
+
if (recordCount > maxRecordEntries)
|
|
687
|
+
throw new Error(RECORD_CAPACITY_ERROR);
|
|
688
|
+
jobIds.add(entry.name.slice(0, -".json".length));
|
|
689
|
+
}
|
|
690
|
+
return { recordCount, jobIds };
|
|
691
|
+
}
|
|
692
|
+
async function readTransaction(path) {
|
|
693
|
+
let text;
|
|
694
|
+
try {
|
|
695
|
+
text = await readBoundedOwnerOnlyFile(path, MAX_TRANSACTION_BYTES, "Process-job transaction");
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
if (isErrno(error, "ENOENT"))
|
|
699
|
+
return undefined;
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
const value = parseJson(text, path);
|
|
704
|
+
if (!isRecord(value)
|
|
705
|
+
|| !hasExactKeys(value, ["schemaVersion", "generation", "createdAt", "write", "delete"])
|
|
706
|
+
|| value.schemaVersion !== 1
|
|
707
|
+
|| !isUuid(value.generation)
|
|
708
|
+
|| !isIso(value.createdAt)
|
|
709
|
+
|| !((value.write === null && typeof value.delete === "string")
|
|
710
|
+
|| (value.delete === null && isRecord(value.write)))) {
|
|
711
|
+
throw new Error(`Process-job transaction has a malformed schema: ${path}`);
|
|
712
|
+
}
|
|
713
|
+
if (value.write !== null) {
|
|
714
|
+
assertDurableRecord(value.write);
|
|
715
|
+
assertBoundedJson(`${path}#record`, value.write, MAX_RECORD_BYTES);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
assertJobId(value.delete);
|
|
719
|
+
}
|
|
720
|
+
return value;
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
throw new UnreplayableProcessJobTransactionError("Process-job transaction is permanently unreplayable and must be quarantined.", { cause: error });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function readManifest(path) {
|
|
727
|
+
let text;
|
|
728
|
+
try {
|
|
729
|
+
text = await readBoundedOwnerOnlyFile(path, MAX_MANIFEST_BYTES, "Process-job manifest");
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
if (isErrno(error, "ENOENT"))
|
|
733
|
+
return undefined;
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
const value = parseJson(text, path);
|
|
737
|
+
if (!isRecord(value)
|
|
738
|
+
|| !hasExactKeys(value, ["schemaVersion", "generation", "updatedAt", "rollbackGuardRequired", "records"])
|
|
739
|
+
|| value.schemaVersion !== 1
|
|
740
|
+
|| !isUuid(value.generation)
|
|
741
|
+
|| !isIso(value.updatedAt)
|
|
742
|
+
|| value.rollbackGuardRequired !== true
|
|
743
|
+
|| !nonNegativeInteger(value.records)) {
|
|
744
|
+
throw new Error(`Process-job manifest has a malformed schema: ${path}`);
|
|
745
|
+
}
|
|
746
|
+
return value;
|
|
747
|
+
}
|
|
748
|
+
async function persistManifest(path, generation, records) {
|
|
749
|
+
const manifest = {
|
|
750
|
+
schemaVersion: 1,
|
|
751
|
+
generation,
|
|
752
|
+
updatedAt: new Date().toISOString(),
|
|
753
|
+
rollbackGuardRequired: true,
|
|
754
|
+
records,
|
|
755
|
+
};
|
|
756
|
+
await writeBoundedJson(path, manifest, MAX_MANIFEST_BYTES);
|
|
757
|
+
return manifest;
|
|
758
|
+
}
|
|
759
|
+
async function ensureRollbackGuard(path) {
|
|
760
|
+
try {
|
|
761
|
+
const current = await readBoundedOwnerOnlyFile(path, MAX_GUARD_BYTES, "Process-job rollback guard");
|
|
762
|
+
if (current !== PROCESS_JOB_ROLLBACK_GUARD_CONTENT) {
|
|
763
|
+
throw new Error(`Process-job rollback guard contents are invalid: ${path}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
if (!isErrno(error, "ENOENT"))
|
|
768
|
+
throw error;
|
|
769
|
+
await replacePrivateFile(path, PROCESS_JOB_ROLLBACK_GUARD_CONTENT, 0o600);
|
|
770
|
+
await syncDirectory(dirname(path));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function writeBoundedJson(path, value, maxBytes) {
|
|
774
|
+
const contents = assertBoundedJson(path, value, maxBytes);
|
|
775
|
+
await replacePrivateFile(path, contents, 0o600);
|
|
776
|
+
await syncDirectory(dirname(path));
|
|
777
|
+
}
|
|
778
|
+
function assertBoundedJson(path, value, maxBytes) {
|
|
779
|
+
const contents = `${JSON.stringify(value, null, 2)}\n`;
|
|
780
|
+
if (Buffer.byteLength(contents, "utf8") > maxBytes) {
|
|
781
|
+
throw new ProcessJobStoreMutationError(`Process-job durable file exceeds its ${String(maxBytes)} byte limit: ${path}`);
|
|
782
|
+
}
|
|
783
|
+
return contents;
|
|
784
|
+
}
|
|
785
|
+
async function quarantineUnreplayableTransaction(stateDir, quarantineDir, transactionPath) {
|
|
786
|
+
const name = `transaction-${new Date().toISOString().replaceAll(":", "-")}-${randomUUID()}.json`;
|
|
787
|
+
const quarantinePath = join(quarantineDir, name);
|
|
788
|
+
await rename(transactionPath, quarantinePath);
|
|
789
|
+
assertPrivateFile(await lstat(quarantinePath), quarantinePath, 0o600);
|
|
790
|
+
await syncDirectory(quarantineDir);
|
|
791
|
+
await syncDirectory(stateDir);
|
|
792
|
+
}
|
|
793
|
+
async function inspectQuarantinedTransactions(quarantineDir) {
|
|
794
|
+
const entries = await readdir(quarantineDir, { withFileTypes: true });
|
|
795
|
+
if (entries.length > MAX_QUARANTINED_TRANSACTIONS) {
|
|
796
|
+
throw new Error(`Process-job quarantined transaction count exceeds ${String(MAX_QUARANTINED_TRANSACTIONS)}.`);
|
|
797
|
+
}
|
|
798
|
+
for (const entry of entries) {
|
|
799
|
+
if (!entry.isFile()
|
|
800
|
+
|| entry.isSymbolicLink()
|
|
801
|
+
|| !/^transaction-\d{4}-\d{2}-\d{2}T[0-9.-]+Z-[0-9a-f-]{36}\.json$/iu.test(entry.name)) {
|
|
802
|
+
throw new Error(`Process-job quarantine contains an unsupported entry: ${entry.name}`);
|
|
803
|
+
}
|
|
804
|
+
const path = join(quarantineDir, entry.name);
|
|
805
|
+
const info = await lstat(path);
|
|
806
|
+
assertPrivateFile(info, path, 0o600);
|
|
807
|
+
if (info.size > MAX_TRANSACTION_BYTES) {
|
|
808
|
+
throw new Error(`Process-job quarantined transaction exceeds its durable bound: ${entry.name}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return entries.length;
|
|
812
|
+
}
|
|
813
|
+
async function replacePrivateFile(path, contents, mode) {
|
|
814
|
+
let expected;
|
|
815
|
+
try {
|
|
816
|
+
expected = await lstat(path);
|
|
817
|
+
assertPrivateFile(expected, path, mode);
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
if (!isErrno(error, "ENOENT"))
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
await secureFileReplace({
|
|
824
|
+
path,
|
|
825
|
+
contents,
|
|
826
|
+
mode,
|
|
827
|
+
target: {
|
|
828
|
+
expected: expected === undefined
|
|
829
|
+
? { kind: "missing" }
|
|
830
|
+
: {
|
|
831
|
+
kind: "present",
|
|
832
|
+
validate: async (candidate) => {
|
|
833
|
+
try {
|
|
834
|
+
const current = await lstat(candidate);
|
|
835
|
+
assertPrivateFile(current, candidate, mode);
|
|
836
|
+
return current.dev === expected.dev && current.ino === expected.ino;
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
invalidError: () => new Error(`Process-job private file changed before replacement: ${path}`),
|
|
843
|
+
},
|
|
844
|
+
recovery: "restore-previous",
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
async function ensureConfinedPrivateDirectory(root, path) {
|
|
849
|
+
assertLexicallyInside(root, path);
|
|
850
|
+
const rel = relative(root, path);
|
|
851
|
+
let current = root;
|
|
852
|
+
await assertBaseDirectory(current);
|
|
853
|
+
for (const part of rel.split(sep).filter(Boolean)) {
|
|
854
|
+
current = join(current, part);
|
|
855
|
+
try {
|
|
856
|
+
await mkdir(current, { mode: 0o700 });
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
if (!isErrno(error, "EEXIST"))
|
|
860
|
+
throw error;
|
|
861
|
+
}
|
|
862
|
+
await assertPrivateDirectory(current);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function assertBaseDirectory(path) {
|
|
866
|
+
const info = await lstat(path);
|
|
867
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
868
|
+
throw new Error(`Agent root is not a real directory: ${path}`);
|
|
869
|
+
}
|
|
870
|
+
if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
|
|
871
|
+
throw new Error(`Agent root is not owned by the current user: ${path}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
async function assertPrivateDirectory(path) {
|
|
875
|
+
const info = await lstat(path);
|
|
876
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
877
|
+
throw new Error(`Process-job state path is not a real directory: ${path}`);
|
|
878
|
+
}
|
|
879
|
+
if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
|
|
880
|
+
throw new Error(`Process-job state directory is not owned by the current user: ${path}`);
|
|
881
|
+
}
|
|
882
|
+
if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
|
|
883
|
+
await chmod(path, 0o700);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function assertPrivateFile(info, path, mode) {
|
|
887
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) {
|
|
888
|
+
throw new Error(`Process-job durable path is not a single-link regular file: ${path}`);
|
|
889
|
+
}
|
|
890
|
+
if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
|
|
891
|
+
throw new Error(`Process-job durable file is not owned by the current user: ${path}`);
|
|
892
|
+
}
|
|
893
|
+
if (process.platform !== "win32" && (info.mode & 0o777) !== mode) {
|
|
894
|
+
throw new Error(`Process-job durable file permissions are not owner-only: ${path}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function assertDurableRecord(value) {
|
|
898
|
+
if (!isRecord(value)
|
|
899
|
+
|| !hasExactKeys(value, [
|
|
900
|
+
"schemaVersion", "generation", "jobId", "tool", "state", "summary", "agentIncarnation",
|
|
901
|
+
...(Object.prototype.hasOwnProperty.call(value, "processIncarnation") ? ["processIncarnation"] : []),
|
|
902
|
+
"pid", "pgid", "sandboxSettingsPath", "argvSummary", "cwd", "envKeys", "origin", "chainDepth",
|
|
903
|
+
"maxRuntimeMs", "maxOutputBytes", "previewChars", "admittedAt", "queueDeadlineAt", "startedAt",
|
|
904
|
+
"runtimeDeadlineAt", "completedAt", "exitCode", "signal", "durationMs", "stdoutBytes", "stderrBytes",
|
|
905
|
+
"truncated", "preview", "stdoutRef", "stderrRef", "cancelRequested", "wake", "lastError",
|
|
906
|
+
])
|
|
907
|
+
|| value.schemaVersion !== 1
|
|
908
|
+
|| !isUuid(value.generation)
|
|
909
|
+
|| !isJobId(value.jobId)
|
|
910
|
+
|| (value.tool !== "Exec" && value.tool !== "Bash")
|
|
911
|
+
|| !isProcessJobState(value.state)
|
|
912
|
+
|| !boundedString(value.summary, 8_000)
|
|
913
|
+
|| processIncarnationFromJson(value.agentIncarnation) === undefined
|
|
914
|
+
|| (value.processIncarnation !== undefined && processIncarnationFromJson(value.processIncarnation) === undefined)
|
|
915
|
+
|| !nullablePositiveInteger(value.pid)
|
|
916
|
+
|| !nullablePositiveInteger(value.pgid)
|
|
917
|
+
|| (value.pgid !== null && value.pgid !== value.pid)
|
|
918
|
+
|| (value.state === "running"
|
|
919
|
+
&& (value.pid === null || value.pgid !== value.pid || value.processIncarnation === undefined))
|
|
920
|
+
|| !nullableSandboxSettingsPath(value.sandboxSettingsPath)
|
|
921
|
+
|| !boundedString(value.argvSummary, 8_000)
|
|
922
|
+
|| !boundedString(value.cwd, 16_384)
|
|
923
|
+
|| !stringArray(value.envKeys, PROCESS_JOB_ENV_KEYS_CAPS.maxItems, PROCESS_JOB_ENV_KEYS_CAPS.maxItemBytes, PROCESS_JOB_ENV_KEYS_CAPS.maxTotalBytes)
|
|
924
|
+
|| !isProcessJobOriginRecord(value.origin)
|
|
925
|
+
|| !nonNegativeInteger(value.chainDepth)
|
|
926
|
+
|| !boundedPositiveInteger(value.maxRuntimeMs, PROCESS_JOBS_CAPS.maxRuntimeMs)
|
|
927
|
+
|| !boundedPositiveInteger(value.maxOutputBytes, PROCESS_JOBS_CAPS.maxOutputBytes)
|
|
928
|
+
|| !boundedPositiveInteger(value.previewChars, PROCESS_JOBS_CAPS.previewChars)
|
|
929
|
+
|| Number(value.chainDepth) > PROCESS_JOBS_CAPS.maxChainDepth
|
|
930
|
+
|| !isIso(value.admittedAt)
|
|
931
|
+
|| !isIso(value.queueDeadlineAt)
|
|
932
|
+
|| !nullableIso(value.startedAt)
|
|
933
|
+
|| !nullableIso(value.runtimeDeadlineAt)
|
|
934
|
+
|| !nullableIso(value.completedAt)
|
|
935
|
+
|| !nullableInteger(value.exitCode)
|
|
936
|
+
|| !nullableString(value.signal, 128)
|
|
937
|
+
|| !nullableNonNegativeInteger(value.durationMs)
|
|
938
|
+
|| !nonNegativeInteger(value.stdoutBytes)
|
|
939
|
+
|| !nonNegativeInteger(value.stderrBytes)
|
|
940
|
+
|| typeof value.truncated !== "boolean"
|
|
941
|
+
|| !boundedCharacters(value.preview, PROCESS_JOBS_CAPS.previewChars)
|
|
942
|
+
|| (typeof value.preview === "string" && value.preview.length > Number(value.previewChars))
|
|
943
|
+
|| !nullableArtifactRef(value.stdoutRef, value.jobId, "stdout.log")
|
|
944
|
+
|| !nullableArtifactRef(value.stderrRef, value.jobId, "stderr.log")
|
|
945
|
+
|| typeof value.cancelRequested !== "boolean"
|
|
946
|
+
|| !validWake(value.wake)
|
|
947
|
+
|| !validLastError(value.lastError)
|
|
948
|
+
|| !validDurableLifecycle(value)) {
|
|
949
|
+
throw new Error("Process-job record has a malformed schema.");
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function validDurableLifecycle(value) {
|
|
953
|
+
const noProcessOwner = value.pid === null
|
|
954
|
+
&& value.pgid === null
|
|
955
|
+
&& value.processIncarnation === undefined
|
|
956
|
+
&& value.startedAt === null
|
|
957
|
+
&& value.runtimeDeadlineAt === null;
|
|
958
|
+
const completeProcessOwner = typeof value.pid === "number"
|
|
959
|
+
&& value.pgid === value.pid
|
|
960
|
+
&& value.processIncarnation !== undefined
|
|
961
|
+
&& typeof value.startedAt === "string"
|
|
962
|
+
&& typeof value.runtimeDeadlineAt === "string";
|
|
963
|
+
if (value.state === "queued" && !noProcessOwner)
|
|
964
|
+
return false;
|
|
965
|
+
if (value.state === "starting" && !noProcessOwner && !completeProcessOwner)
|
|
966
|
+
return false;
|
|
967
|
+
if (value.state === "running" && !completeProcessOwner)
|
|
968
|
+
return false;
|
|
969
|
+
const terminal = isProcessJobState(value.state) && isTerminalProcessJobState(value.state);
|
|
970
|
+
if (terminal !== (value.completedAt !== null))
|
|
971
|
+
return false;
|
|
972
|
+
if (!terminal) {
|
|
973
|
+
const wake = value.wake;
|
|
974
|
+
if (wake.state !== "pending"
|
|
975
|
+
|| wake.attempts !== 0
|
|
976
|
+
|| wake.lastAttemptAt !== null
|
|
977
|
+
|| wake.retrySafe === true
|
|
978
|
+
|| (wake.destinationUnavailableAttempts ?? 0) !== 0
|
|
979
|
+
|| (wake.conversationBusyAttempts ?? 0) !== 0
|
|
980
|
+
|| (wake.conversationBusySinceAt !== undefined && wake.conversationBusySinceAt !== null))
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
/** Validate the exact durable wake identity, including its normalized base. */
|
|
986
|
+
export function isProcessJobOriginRecord(value) {
|
|
987
|
+
if (!isRecord(value)
|
|
988
|
+
|| !hasExactKeys(value, [
|
|
989
|
+
"conversationId", "baseConversationId", "bucket", "replyToConversationId",
|
|
990
|
+
"normalizedReplyTarget", "runId", "historyBoundary", "channel",
|
|
991
|
+
])
|
|
992
|
+
|| !boundedNonEmptyString(value.conversationId, 2_048)
|
|
993
|
+
|| !boundedNonEmptyString(value.baseConversationId, 2_048)
|
|
994
|
+
|| !nullableString(value.bucket, 512)
|
|
995
|
+
|| !boundedNonEmptyString(value.replyToConversationId, 2_048)
|
|
996
|
+
|| !boundedNonEmptyString(value.normalizedReplyTarget, 2_048)
|
|
997
|
+
|| !boundedNonEmptyString(value.runId, 512)
|
|
998
|
+
|| !boundedNonEmptyString(value.historyBoundary, 512)
|
|
999
|
+
|| (value.channel !== "slack" && value.channel !== "telegram" && value.channel !== "web")) {
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
const conversation = splitProcessJobConversationId(value.conversationId);
|
|
1003
|
+
return conversation !== undefined
|
|
1004
|
+
&& conversation.baseConversationId === value.baseConversationId
|
|
1005
|
+
&& conversation.bucket === value.bucket
|
|
1006
|
+
&& isCanonicalProcessJobBase(value.baseConversationId, value.channel)
|
|
1007
|
+
&& value.replyToConversationId === value.baseConversationId
|
|
1008
|
+
&& value.normalizedReplyTarget === value.baseConversationId
|
|
1009
|
+
&& value.historyBoundary === value.runId;
|
|
1010
|
+
}
|
|
1011
|
+
function splitProcessJobConversationId(value) {
|
|
1012
|
+
if (value !== value.trim())
|
|
1013
|
+
return undefined;
|
|
1014
|
+
const hash = value.indexOf("#");
|
|
1015
|
+
if (hash < 0)
|
|
1016
|
+
return { baseConversationId: value, bucket: null };
|
|
1017
|
+
if (hash === 0 || hash === value.length - 1 || value.indexOf("#", hash + 1) >= 0)
|
|
1018
|
+
return undefined;
|
|
1019
|
+
const baseConversationId = value.slice(0, hash);
|
|
1020
|
+
const bucket = value.slice(hash + 1);
|
|
1021
|
+
if (bucket !== bucket.trim())
|
|
1022
|
+
return undefined;
|
|
1023
|
+
return { baseConversationId, bucket };
|
|
1024
|
+
}
|
|
1025
|
+
function isCanonicalProcessJobBase(value, channel) {
|
|
1026
|
+
const prefix = `${channel}:`;
|
|
1027
|
+
if (!value.startsWith(prefix))
|
|
1028
|
+
return false;
|
|
1029
|
+
const destination = value.slice(prefix.length);
|
|
1030
|
+
if (channel === "telegram") {
|
|
1031
|
+
const chatId = Number(destination);
|
|
1032
|
+
return /^-?\d+$/u.test(destination)
|
|
1033
|
+
&& Number.isSafeInteger(chatId)
|
|
1034
|
+
&& String(chatId) === destination;
|
|
1035
|
+
}
|
|
1036
|
+
if (channel === "web")
|
|
1037
|
+
return destination !== "new" && /^[^\s:#]+$/u.test(destination);
|
|
1038
|
+
const parts = destination.split(":");
|
|
1039
|
+
const channelId = parts[0];
|
|
1040
|
+
const threadTs = parts[1];
|
|
1041
|
+
return (parts.length === 1 || parts.length === 2)
|
|
1042
|
+
&& typeof channelId === "string"
|
|
1043
|
+
&& /^(?:C|D|G)[A-Z0-9]+$/u.test(channelId)
|
|
1044
|
+
&& (threadTs === undefined || /^\d+\.\d+$/u.test(threadTs));
|
|
1045
|
+
}
|
|
1046
|
+
function nullableSandboxSettingsPath(value) {
|
|
1047
|
+
if (value === null)
|
|
1048
|
+
return true;
|
|
1049
|
+
if (!boundedNonEmptyString(value, 16_384) || !isAbsolute(value) || resolve(value) !== value)
|
|
1050
|
+
return false;
|
|
1051
|
+
return basename(value) === "settings.json"
|
|
1052
|
+
&& /^mono-agent-srt-settings-[A-Za-z0-9_-]{6,}$/u.test(basename(dirname(value)));
|
|
1053
|
+
}
|
|
1054
|
+
function validWake(value) {
|
|
1055
|
+
return isRecord(value)
|
|
1056
|
+
&& hasExactKeys(value, [
|
|
1057
|
+
"state", "attempts", "deliveryKey", "lastAttemptAt",
|
|
1058
|
+
...(Object.prototype.hasOwnProperty.call(value, "retrySafe") ? ["retrySafe"] : []),
|
|
1059
|
+
...(Object.prototype.hasOwnProperty.call(value, "destinationUnavailableAttempts")
|
|
1060
|
+
? ["destinationUnavailableAttempts"]
|
|
1061
|
+
: []),
|
|
1062
|
+
...(Object.prototype.hasOwnProperty.call(value, "conversationBusyAttempts")
|
|
1063
|
+
? ["conversationBusyAttempts"]
|
|
1064
|
+
: []),
|
|
1065
|
+
...(Object.prototype.hasOwnProperty.call(value, "conversationBusySinceAt")
|
|
1066
|
+
? ["conversationBusySinceAt"]
|
|
1067
|
+
: []),
|
|
1068
|
+
])
|
|
1069
|
+
&& (value.state === "pending" || value.state === "delivered" || value.state === "failed")
|
|
1070
|
+
&& nonNegativeInteger(value.attempts)
|
|
1071
|
+
&& boundedNonEmptyString(value.deliveryKey, 512)
|
|
1072
|
+
&& nullableIso(value.lastAttemptAt)
|
|
1073
|
+
&& (value.retrySafe === undefined || typeof value.retrySafe === "boolean")
|
|
1074
|
+
&& (value.destinationUnavailableAttempts === undefined
|
|
1075
|
+
|| (nonNegativeInteger(value.destinationUnavailableAttempts)
|
|
1076
|
+
&& Number(value.destinationUnavailableAttempts) <= PROCESS_JOB_DESTINATION_UNAVAILABLE_ATTEMPTS))
|
|
1077
|
+
&& (value.conversationBusyAttempts === undefined
|
|
1078
|
+
|| (nonNegativeInteger(value.conversationBusyAttempts)
|
|
1079
|
+
&& Number(value.conversationBusyAttempts) <= PROCESS_JOB_CONVERSATION_BUSY_ATTEMPT_COUNTER_MAX))
|
|
1080
|
+
&& (value.conversationBusySinceAt === undefined || nullableIso(value.conversationBusySinceAt))
|
|
1081
|
+
&& ((Number(value.conversationBusyAttempts ?? 0) === 0)
|
|
1082
|
+
=== (value.conversationBusySinceAt === undefined || value.conversationBusySinceAt === null));
|
|
1083
|
+
}
|
|
1084
|
+
function publicDurableRecord(record) {
|
|
1085
|
+
const owned = structuredClone(record);
|
|
1086
|
+
if (owned.lastError !== null)
|
|
1087
|
+
owned.lastError = processJobPublicError(owned.lastError.code);
|
|
1088
|
+
return owned;
|
|
1089
|
+
}
|
|
1090
|
+
function validLastError(value) {
|
|
1091
|
+
return value === null || (isRecord(value)
|
|
1092
|
+
&& hasExactKeys(value, ["code", "message"])
|
|
1093
|
+
&& isProcessJobErrorCode(value.code)
|
|
1094
|
+
&& boundedNonEmptyString(value.message, 8_000));
|
|
1095
|
+
}
|
|
1096
|
+
async function removeOrphanArtifacts(artifactsDir, records, workCounter) {
|
|
1097
|
+
const artifactBytesByJob = new Map();
|
|
1098
|
+
let removed = false;
|
|
1099
|
+
for (const entry of await readdir(artifactsDir, { withFileTypes: true })) {
|
|
1100
|
+
incrementWork(workCounter, "artifactDirectoriesInspected");
|
|
1101
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !isJobId(entry.name)) {
|
|
1102
|
+
throw new Error(`Process-job artifact directory contains an unsupported entry: ${entry.name}`);
|
|
1103
|
+
}
|
|
1104
|
+
const path = join(artifactsDir, entry.name);
|
|
1105
|
+
const record = records.get(entry.name);
|
|
1106
|
+
if (record === undefined || (record.stdoutRef === null && record.stderrRef === null)) {
|
|
1107
|
+
await rm(path, { recursive: true, force: true });
|
|
1108
|
+
removed = true;
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
await assertPrivateDirectory(path);
|
|
1112
|
+
artifactBytesByJob.set(entry.name, await artifactDirectoryBytes(path));
|
|
1113
|
+
}
|
|
1114
|
+
if (removed)
|
|
1115
|
+
await syncDirectory(artifactsDir);
|
|
1116
|
+
for (const record of records.values()) {
|
|
1117
|
+
if (record.stdoutRef !== null) {
|
|
1118
|
+
await assertReferencedArtifact(join(artifactsDir, record.jobId, "stdout.log"));
|
|
1119
|
+
}
|
|
1120
|
+
if (record.stderrRef !== null) {
|
|
1121
|
+
await assertReferencedArtifact(join(artifactsDir, record.jobId, "stderr.log"));
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return artifactBytesByJob;
|
|
1125
|
+
}
|
|
1126
|
+
async function assertReferencedArtifact(path) {
|
|
1127
|
+
let info;
|
|
1128
|
+
try {
|
|
1129
|
+
info = await lstat(path);
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
if (isErrno(error, "ENOENT")) {
|
|
1133
|
+
throw new Error(`Process-job referenced artifact is missing: ${path}`);
|
|
1134
|
+
}
|
|
1135
|
+
throw error;
|
|
1136
|
+
}
|
|
1137
|
+
assertPrivateFile(info, path, 0o600);
|
|
1138
|
+
}
|
|
1139
|
+
function retainedArtifactBytesFor(records, artifactBytesByJob) {
|
|
1140
|
+
let total = 0;
|
|
1141
|
+
for (const record of records.values()) {
|
|
1142
|
+
total += retainedArtifactContribution(record, artifactBytesByJob.get(record.jobId));
|
|
1143
|
+
}
|
|
1144
|
+
return total;
|
|
1145
|
+
}
|
|
1146
|
+
function retainedArtifactContribution(record, bytes) {
|
|
1147
|
+
return record !== undefined
|
|
1148
|
+
&& isTerminalProcessJobState(record.state)
|
|
1149
|
+
&& (record.stdoutRef !== null || record.stderrRef !== null)
|
|
1150
|
+
? bytes ?? 0
|
|
1151
|
+
: 0;
|
|
1152
|
+
}
|
|
1153
|
+
async function inspectArtifactDirectoryBytes(path, workCounter) {
|
|
1154
|
+
incrementWork(workCounter, "artifactDirectoriesInspected");
|
|
1155
|
+
return await artifactDirectoryBytes(path);
|
|
1156
|
+
}
|
|
1157
|
+
async function artifactDirectoryBytes(path) {
|
|
1158
|
+
let total = 0;
|
|
1159
|
+
try {
|
|
1160
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
1161
|
+
if (entry.name.startsWith(".") && entry.name.endsWith(".tmp") && entry.isFile() && !entry.isSymbolicLink()) {
|
|
1162
|
+
await rm(join(path, entry.name), { force: true });
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (!entry.isFile() || entry.isSymbolicLink() || (entry.name !== "stdout.log" && entry.name !== "stderr.log")) {
|
|
1166
|
+
throw new Error(`Process-job artifact directory contains an unsupported entry: ${entry.name}`);
|
|
1167
|
+
}
|
|
1168
|
+
const artifactPath = join(path, entry.name);
|
|
1169
|
+
const info = await lstat(artifactPath);
|
|
1170
|
+
assertPrivateFile(info, artifactPath, 0o600);
|
|
1171
|
+
total += info.size;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
catch (error) {
|
|
1175
|
+
if (!isErrno(error, "ENOENT"))
|
|
1176
|
+
throw error;
|
|
1177
|
+
}
|
|
1178
|
+
return total;
|
|
1179
|
+
}
|
|
1180
|
+
function terminalTime(record) {
|
|
1181
|
+
return Date.parse(record.completedAt ?? record.startedAt ?? record.admittedAt);
|
|
1182
|
+
}
|
|
1183
|
+
function clone(record) {
|
|
1184
|
+
return record === undefined ? undefined : structuredClone(record);
|
|
1185
|
+
}
|
|
1186
|
+
function incrementWork(workCounter, key) {
|
|
1187
|
+
if (workCounter !== undefined)
|
|
1188
|
+
workCounter[key] += 1;
|
|
1189
|
+
}
|
|
1190
|
+
function parseJson(text, path) {
|
|
1191
|
+
try {
|
|
1192
|
+
return JSON.parse(text);
|
|
1193
|
+
}
|
|
1194
|
+
catch (error) {
|
|
1195
|
+
throw new Error(`Process-job durable file contains invalid JSON: ${path}`, { cause: error });
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function assertLexicallyInside(root, path) {
|
|
1199
|
+
const rel = relative(root, path);
|
|
1200
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || rel.startsWith("/") || rel.startsWith("\\")) {
|
|
1201
|
+
throw new Error(`Process-job state directory escapes the agent root: ${path}`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
async function resolveConfinedStateDirectory(agentRoot, stateDir) {
|
|
1205
|
+
const lexicalRoot = resolve(agentRoot);
|
|
1206
|
+
const root = await realpath(lexicalRoot).catch(() => lexicalRoot);
|
|
1207
|
+
const candidate = resolve(stateDir);
|
|
1208
|
+
// Callers loaded through process-jobs-config already pass a canonical path.
|
|
1209
|
+
// Direct store callers may retain a platform alias for the same root (for
|
|
1210
|
+
// example macOS /var versus /private/var) or a symlink spelling of cwd. Map
|
|
1211
|
+
// only a lexically confined suffix onto the attested real root; every child
|
|
1212
|
+
// component is still lstat-checked before it is used.
|
|
1213
|
+
const canonicalRelative = relative(root, candidate);
|
|
1214
|
+
if (canonicalRelative.length > 0 && isConfinedRelative(canonicalRelative)) {
|
|
1215
|
+
return { root, confined: candidate };
|
|
1216
|
+
}
|
|
1217
|
+
const lexicalRelative = relative(lexicalRoot, candidate);
|
|
1218
|
+
if (lexicalRelative.length > 0 && isConfinedRelative(lexicalRelative)) {
|
|
1219
|
+
return { root, confined: resolve(root, lexicalRelative) };
|
|
1220
|
+
}
|
|
1221
|
+
throw new Error(`Process-job state directory escapes the agent root: ${candidate}`);
|
|
1222
|
+
}
|
|
1223
|
+
function isConfinedRelative(path) {
|
|
1224
|
+
return path !== ".." && !path.startsWith(`..${sep}`) && !path.startsWith("/") && !path.startsWith("\\");
|
|
1225
|
+
}
|
|
1226
|
+
function assertJobId(value) {
|
|
1227
|
+
if (!isJobId(value))
|
|
1228
|
+
throw new Error("Process-job id is invalid.");
|
|
1229
|
+
}
|
|
1230
|
+
function isJobId(value) {
|
|
1231
|
+
return isUuid(value);
|
|
1232
|
+
}
|
|
1233
|
+
function isUuid(value) {
|
|
1234
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value);
|
|
1235
|
+
}
|
|
1236
|
+
function isRecord(value) {
|
|
1237
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1238
|
+
}
|
|
1239
|
+
function hasExactKeys(value, keys) {
|
|
1240
|
+
const actual = Object.keys(value);
|
|
1241
|
+
return actual.length === keys.length && actual.every((key) => keys.includes(key));
|
|
1242
|
+
}
|
|
1243
|
+
function boundedString(value, max) {
|
|
1244
|
+
return typeof value === "string" && Buffer.byteLength(value, "utf8") <= max;
|
|
1245
|
+
}
|
|
1246
|
+
function boundedCharacters(value, max) {
|
|
1247
|
+
return typeof value === "string" && value.length <= max;
|
|
1248
|
+
}
|
|
1249
|
+
function boundedNonEmptyString(value, max) {
|
|
1250
|
+
return boundedString(value, max) && value.trim().length > 0;
|
|
1251
|
+
}
|
|
1252
|
+
function nullableString(value, max) {
|
|
1253
|
+
return value === null || boundedString(value, max);
|
|
1254
|
+
}
|
|
1255
|
+
function stringArray(value, maxItems, maxBytes, maxTotalBytes) {
|
|
1256
|
+
return Array.isArray(value) && value.length <= maxItems
|
|
1257
|
+
&& value.every((item) => boundedNonEmptyString(item, maxBytes))
|
|
1258
|
+
&& value.reduce((total, item) => total + (typeof item === "string" ? Buffer.byteLength(item, "utf8") : 0), 0) <= maxTotalBytes;
|
|
1259
|
+
}
|
|
1260
|
+
function positiveInteger(value) {
|
|
1261
|
+
return Number.isSafeInteger(value) && Number(value) > 0;
|
|
1262
|
+
}
|
|
1263
|
+
function boundedPositiveInteger(value, max) {
|
|
1264
|
+
return positiveInteger(value) && Number(value) <= max;
|
|
1265
|
+
}
|
|
1266
|
+
function nonNegativeInteger(value) {
|
|
1267
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
1268
|
+
}
|
|
1269
|
+
function nullablePositiveInteger(value) {
|
|
1270
|
+
return value === null || positiveInteger(value);
|
|
1271
|
+
}
|
|
1272
|
+
function nullableInteger(value) {
|
|
1273
|
+
return value === null || Number.isSafeInteger(value);
|
|
1274
|
+
}
|
|
1275
|
+
function nullableNonNegativeInteger(value) {
|
|
1276
|
+
return value === null || nonNegativeInteger(value);
|
|
1277
|
+
}
|
|
1278
|
+
function isIso(value) {
|
|
1279
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
1280
|
+
}
|
|
1281
|
+
function nullableIso(value) {
|
|
1282
|
+
return value === null || isIso(value);
|
|
1283
|
+
}
|
|
1284
|
+
function nullableArtifactRef(value, jobId, name) {
|
|
1285
|
+
return value === null || value === `${PROCESS_JOB_ARTIFACTS_DIRECTORY}/${jobId}/${name}`;
|
|
1286
|
+
}
|
|
1287
|
+
function isErrno(error, code) {
|
|
1288
|
+
return error?.code === code;
|
|
1289
|
+
}
|
|
1290
|
+
//# sourceMappingURL=process-jobs-store.js.map
|