@ornexus/neocortex 4.60.71 → 4.60.73
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/dist/release-source.json +1 -1
- package/dist/sbom.cdx.json +1211 -1217
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +2 -2
- package/packages/client/dist/adapters/adapter-registry.js +108 -1
- package/packages/client/dist/adapters/antigravity-adapter.js +77 -2
- package/packages/client/dist/adapters/claude-code-adapter.js +79 -3
- package/packages/client/dist/adapters/codex-adapter.js +80 -2
- package/packages/client/dist/adapters/cursor-adapter.js +115 -4
- package/packages/client/dist/adapters/gemini-adapter.js +71 -2
- package/packages/client/dist/adapters/grok-adapter.js +42 -2
- package/packages/client/dist/adapters/index.js +22 -1
- package/packages/client/dist/adapters/platform-detector.js +212 -1
- package/packages/client/dist/adapters/target-adapter.js +12 -0
- package/packages/client/dist/adapters/vscode-adapter.js +72 -2
- package/packages/client/dist/agent/refresh-stubs.js +417 -2
- package/packages/client/dist/agent/update-agent-yaml.js +102 -1
- package/packages/client/dist/agent/update-description.js +251 -1
- package/packages/client/dist/cache/crypto-utils.js +78 -1
- package/packages/client/dist/cache/encrypted-cache.js +108 -1
- package/packages/client/dist/cache/in-memory-asset-cache.js +73 -1
- package/packages/client/dist/cache/index.js +13 -1
- package/packages/client/dist/cache/protected-pi-boundary.js +44 -1
- package/packages/client/dist/checkpoint/checkpoint-client-reader.js +127 -2
- package/packages/client/dist/checkpoint/index.js +11 -1
- package/packages/client/dist/checkpoint/shared-checkpoint-types.js +32 -1
- package/packages/client/dist/cli.js +358 -5
- package/packages/client/dist/commands/activate.js +394 -8
- package/packages/client/dist/commands/architecture-answers.js +274 -7
- package/packages/client/dist/commands/architecture-memory-terminal.js +253 -4
- package/packages/client/dist/commands/architecture-non-ui-continuation.js +100 -1
- package/packages/client/dist/commands/architecture-root-binding.js +51 -1
- package/packages/client/dist/commands/cache-status.js +112 -2
- package/packages/client/dist/commands/config.js +88 -1
- package/packages/client/dist/commands/doctor.js +635 -2
- package/packages/client/dist/commands/invoke.js +7200 -143
- package/packages/client/dist/commands/legacy-epic-terminal.js +71 -1
- package/packages/client/dist/commands/legacy-story-branch.js +96 -1
- package/packages/client/dist/commands/legacy-story-pr.js +276 -1
- package/packages/client/dist/commands/mutation-qa-checkpoint.js +285 -5
- package/packages/client/dist/commands/mutation-qa-gate.js +54 -1
- package/packages/client/dist/commands/mutation-qa-git.js +41 -1
- package/packages/client/dist/commands/mutation-qa-policy.js +102 -1
- package/packages/client/dist/commands/mutation-qa-run.js +219 -3
- package/packages/client/dist/commands/mutation-qa-verdict.js +149 -1
- package/packages/client/dist/commands/mutation-qa.js +220 -1
- package/packages/client/dist/commands/native-agent.js +136 -2
- package/packages/client/dist/commands/planning-diagnosis.js +43 -1
- package/packages/client/dist/commands/planning-generation.js +53 -1
- package/packages/client/dist/commands/planning-memory.js +40 -1
- package/packages/client/dist/commands/planning-research.js +42 -1
- package/packages/client/dist/commands/private-session.js +75 -1
- package/packages/client/dist/commands/property-qa.js +139 -1
- package/packages/client/dist/commands/qa-control.js +16 -1
- package/packages/client/dist/commands/qa-scan-browser-launch.js +55 -1
- package/packages/client/dist/commands/qa-scan-browser-route.js +28 -1
- package/packages/client/dist/commands/qa-scan-capture.js +663 -5
- package/packages/client/dist/commands/qa-scan-discovery.js +376 -3
- package/packages/client/dist/commands/qa-scan-review.js +826 -5
- package/packages/client/dist/commands/qa-synthetic-backend-capture.js +266 -1
- package/packages/client/dist/commands/qa-synthetic-backend-review.js +297 -1
- package/packages/client/dist/commands/qa-synthetic-backend.js +340 -2
- package/packages/client/dist/commands/qa-synthetic-broker-port.js +85 -1
- package/packages/client/dist/commands/qa-synthetic-browser.js +39 -1
- package/packages/client/dist/commands/refactor-characterization.js +671 -21
- package/packages/client/dist/commands/refactor-commit.js +399 -13
- package/packages/client/dist/commands/refactor-control.js +142 -3
- package/packages/client/dist/commands/refactor-cpp-assertions.js +93 -3
- package/packages/client/dist/commands/refactor-review.js +283 -5
- package/packages/client/dist/commands/refactor-root.js +588 -7
- package/packages/client/dist/commands/refresh-memory.js +156 -3
- package/packages/client/dist/commands/shared-architecture-non-ui.js +24 -1
- package/packages/client/dist/commands/story-checkpoint.js +171 -1
- package/packages/client/dist/config/profile-directory.js +13 -1
- package/packages/client/dist/config/resolver-selection.js +278 -1
- package/packages/client/dist/config/secure-config.js +281 -12
- package/packages/client/dist/config.js +223 -2
- package/packages/client/dist/constants.js +25 -1
- package/packages/client/dist/context/context-collector.js +222 -2
- package/packages/client/dist/context/context-sanitizer.js +145 -1
- package/packages/client/dist/continuity/autonomous-governance.js +285 -1
- package/packages/client/dist/continuity/continuity-client-state-store.js +1587 -1
- package/packages/client/dist/continuity/invoke-hooks.js +129 -1
- package/packages/client/dist/continuity/migrations/001-initial-schema.js +12 -2
- package/packages/client/dist/continuity/migrations/002-processing-output-summary-indexes.js +12 -2
- package/packages/client/dist/continuity/migrations/003-headless-execution-attempts.js +12 -2
- package/packages/client/dist/continuity/migrations/004-outbox-delivery-state.js +12 -2
- package/packages/client/dist/continuity/migrations/005-bounded-headless-queue.js +12 -2
- package/packages/client/dist/continuity/migrations/006-native-agent-dispatches.js +10 -2
- package/packages/client/dist/continuity/native-agent-store.js +69 -1
- package/packages/client/dist/continuity/native-agent-types.js +90 -1
- package/packages/client/dist/continuity/outbox.js +117 -1
- package/packages/client/dist/continuity/runner-bootstrap-policy.js +172 -1
- package/packages/client/dist/continuity/sqlite-store.d.ts +22 -1
- package/packages/client/dist/continuity/sqlite-store.js +2612 -81
- package/packages/client/dist/effects/invoke-effect-gateways.js +62 -1
- package/packages/client/dist/errors/error-messages.js +228 -2
- package/packages/client/dist/errors/frontmatter-diagnostics.js +125 -1
- package/packages/client/dist/evidence/collector.js +347 -1
- package/packages/client/dist/evidence/index.js +1 -1
- package/packages/client/dist/evidence/types.js +2 -0
- package/packages/client/dist/fs/atomic-state-replace.js +152 -1
- package/packages/client/dist/generated/command-registry.generated.js +4076 -1
- package/packages/client/dist/generated/public-projections.generated.js +149 -1
- package/packages/client/dist/git/direct-yolo-c01-retry.js +138 -4
- package/packages/client/dist/git/direct-yolo-checkpoint.js +1234 -4
- package/packages/client/dist/git/epic-operational-state.d.ts +198 -0
- package/packages/client/dist/git/epic-operational-state.js +672 -0
- package/packages/client/dist/git/git-lifecycle-step-ids.js +14 -1
- package/packages/client/dist/git/git-lifecycle-store.d.ts +9 -1
- package/packages/client/dist/git/git-lifecycle-store.js +1981 -9
- package/packages/client/dist/git/legacy-story-branch-checkpoint.js +259 -9
- package/packages/client/dist/git/legacy-story-branch-setup.js +348 -4
- package/packages/client/dist/git/legacy-story-pr.js +3538 -70
- package/packages/client/dist/git/secure-filesystem-boundary.js +182 -5
- package/packages/client/dist/graph-retrieval/pre-command-hook.js +284 -1
- package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +237 -1
- package/packages/client/dist/i18n/first-run.js +60 -2
- package/packages/client/dist/index.js +47 -1
- package/packages/client/dist/invocation/invoke-input.js +24 -1
- package/packages/client/dist/license/index.js +5 -1
- package/packages/client/dist/license/license-client.js +304 -1
- package/packages/client/dist/machine/fingerprint.js +163 -2
- package/packages/client/dist/machine/index.js +5 -1
- package/packages/client/dist/memory/project-memory-path-scope.js +62 -1
- package/packages/client/dist/memory/project-memory-writer.js +1007 -40
- package/packages/client/dist/memory/shared-project-memory-types.js +253 -2
- package/packages/client/dist/migrations/epic-lifecycle-backfill.d.ts +97 -0
- package/packages/client/dist/migrations/epic-lifecycle-backfill.js +1060 -0
- package/packages/client/dist/migrations/project-identity-capability.js +50 -1
- package/packages/client/dist/migrations/project-identity.js +493 -7
- package/packages/client/dist/migrations/state-json.d.ts +22 -0
- package/packages/client/dist/migrations/state-json.js +393 -4
- package/packages/client/dist/policy/architecture-policy.js +184 -2
- package/packages/client/dist/policy/index.js +7 -1
- package/packages/client/dist/policy/shared-policy-types.js +38 -0
- package/packages/client/dist/policy/subagent-approval-policy.js +55 -1
- package/packages/client/dist/private-execution/clean-baseline.js +279 -2
- package/packages/client/dist/private-execution/codex-executor.js +574 -4
- package/packages/client/dist/private-execution/codex-host-runtime.js +137 -1
- package/packages/client/dist/private-execution/codex-lifecycle-guard.js +97 -1
- package/packages/client/dist/private-execution/codex-scoped-permissions.js +128 -1
- package/packages/client/dist/private-execution/delivery.js +410 -13
- package/packages/client/dist/private-execution/diagnosis-root-review.js +294 -2
- package/packages/client/dist/private-execution/frozen-handoff.js +19 -6
- package/packages/client/dist/private-execution/implementation-validation.js +106 -2
- package/packages/client/dist/private-execution/isolated-admission.js +1432 -23
- package/packages/client/dist/private-execution/isolated-session.js +725 -5
- package/packages/client/dist/private-execution/isolated-workspace.js +122 -1
- package/packages/client/dist/private-execution/pact-consumer-runtime.js +409 -3
- package/packages/client/dist/private-execution/pact-test-source.js +258 -1
- package/packages/client/dist/private-execution/pact-toolchain.js +290 -3
- package/packages/client/dist/private-execution/pact-write-set.js +172 -1
- package/packages/client/dist/private-execution/planning-diagnosis-checkpoint.js +406 -5
- package/packages/client/dist/private-execution/planning-generation-checkpoint.js +462 -5
- package/packages/client/dist/private-execution/planning-memory-checkpoint.js +139 -3
- package/packages/client/dist/private-execution/planning-research-checkpoint.js +325 -4
- package/packages/client/dist/private-execution/private-report-store.js +42 -1
- package/packages/client/dist/private-execution/property-test-candidate.js +103 -1
- package/packages/client/dist/private-execution/property-test-choices.js +147 -3
- package/packages/client/dist/private-execution/property-test-plan.js +271 -7
- package/packages/client/dist/private-execution/property-test-review.js +258 -4
- package/packages/client/dist/private-execution/property-test-runtime.js +372 -9
- package/packages/client/dist/private-execution/property-test-source.js +139 -1
- package/packages/client/dist/private-execution/root-checkpoint.js +80 -1
- package/packages/client/dist/private-execution/root-memory-git-evidence.js +135 -3
- package/packages/client/dist/private-execution/root-memory-recovery.js +93 -1
- package/packages/client/dist/private-execution/root-memory-request.js +29 -1
- package/packages/client/dist/private-execution/root-memory-scope.js +178 -1
- package/packages/client/dist/private-execution/root-memory-writer.js +102 -1
- package/packages/client/dist/private-execution/session-lease.js +63 -1
- package/packages/client/dist/private-execution/shared-private-admission-types.js +50 -1
- package/packages/client/dist/private-execution/shared-project-validation.js +66 -1
- package/packages/client/dist/private-execution/shared-root-memory-types.js +61 -1
- package/packages/client/dist/private-execution/ux-source-validation.js +372 -1
- package/packages/client/dist/private-execution/ux-write-set.js +191 -2
- package/packages/client/dist/registry/registry-reconciliation.js +234 -1
- package/packages/client/dist/resilience/circuit-breaker.js +170 -1
- package/packages/client/dist/resilience/degradation-manager.js +164 -1
- package/packages/client/dist/resilience/freshness-indicator.js +100 -1
- package/packages/client/dist/resilience/index.js +8 -1
- package/packages/client/dist/resilience/recovery-detector.js +74 -1
- package/packages/client/dist/resolvers/asset-resolver.js +13 -0
- package/packages/client/dist/resolvers/local-resolver.js +255 -8
- package/packages/client/dist/resolvers/remote-resolver.js +233 -1
- package/packages/client/dist/runner/binary.js +404 -1
- package/packages/client/dist/runner/cli.js +1571 -17
- package/packages/client/dist/runner/headless/adapters.js +49 -1
- package/packages/client/dist/runner/headless/claude-code-adapter.js +79 -1
- package/packages/client/dist/runner/headless/continuous-story-workspace.js +1628 -11
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +69 -1
- package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +110 -2
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +1236 -8
- package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +318 -2
- package/packages/client/dist/runner/headless/fake-adapter.js +62 -1
- package/packages/client/dist/runner/headless/managed-profile.js +132 -1
- package/packages/client/dist/runner/headless/opencode-adapter.js +145 -2
- package/packages/client/dist/runner/headless/prepared-story-bundle.js +43 -1
- package/packages/client/dist/runner/headless/resolver.js +231 -1
- package/packages/client/dist/runner/headless/run-yolo.js +885 -1
- package/packages/client/dist/runner/headless/run-yoloop.js +2574 -2
- package/packages/client/dist/runner/headless/types.js +574 -2
- package/packages/client/dist/runner/local-console.js +396 -13
- package/packages/client/dist/runner/portal-sync-worker.js +1487 -1
- package/packages/client/dist/runner/project-identity.js +106 -1
- package/packages/client/dist/runner/scheduler.js +995 -7
- package/packages/client/dist/runner-cli.js +18 -2
- package/packages/client/dist/state/invocation-shape.js +13 -1
- package/packages/client/dist/state/project-state-snapshot.d.ts +8 -0
- package/packages/client/dist/state/project-state-snapshot.js +1731 -1
- package/packages/client/dist/state/state-json-repair.js +785 -3
- package/packages/client/dist/state/structural-dag-projection.js +118 -1
- package/packages/client/dist/telemetry/index.js +5 -1
- package/packages/client/dist/telemetry/offline-queue.js +131 -1
- package/packages/client/dist/tier/index.js +5 -1
- package/packages/client/dist/tier/tier-aware-client.js +405 -1
- package/packages/client/dist/types/index.js +38 -1
- package/packages/client/dist/version.js +27 -1
- package/packages/client/dist/yolo/change-evidence-collector.js +126 -1
- package/packages/client/dist/yolo/invoke-envelope-capability.js +62 -1
- package/packages/client/dist/yolo/spec-envelope-collector.js +184 -5
- package/packages/client/dist/yoloop/discovery-hook.js +300 -2
- package/packages/client/dist/yoloop/index.js +8 -1
- package/packages/client/dist/yoloop/invoke-hooks.js +669 -12
- package/packages/client/dist/yoloop/shared-discover-epics.js +1007 -1
- package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +65 -1
- package/packages/client/dist/yoloop/shared-yoloop-types.js +33 -1
- package/packages/client/dist/yoloop/target-normalization.js +180 -1
- package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +8 -0
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1920 -1
- package/packages/shared/dist/yolo/spec-envelope.js +3 -6
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/skill/SKILL.md +1 -1
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +2 -2
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +2 -2
- package/targets-stubs/codex/AGENTS.md +1 -1
- package/targets-stubs/cursor/agent.md +2 -2
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/opencode/neocortex-root.md +1 -1
- package/targets-stubs/vscode/neocortex.agent.md +2 -2
|
@@ -1,19 +1,1364 @@
|
|
|
1
|
-
import{createHash as W,randomBytes as Ee}from"node:crypto";import{resolve as G}from"node:path";import{CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE as X,ContinuitySqliteStoreError as Ie,openContinuitySqliteStore as Z}from"../continuity/sqlite-store.js";import{invoke as ye}from"../commands/invoke.js";import{checkRunnerBinaryTrust as P}from"./binary.js";import{runRunnerSchedulerCommand as ee}from"./scheduler.js";import{runYoloExecutionPhase as Se}from"./headless/run-yolo.js";import{runYoloopExecutionPhase as Re}from"./headless/run-yoloop.js";import{DEFAULT_RUNNER_HEARTBEAT_BACKOFF_MS as _e,DEFAULT_RUNNER_HEARTBEAT_INTERVAL_MS as ke,DEFAULT_RUNNER_HEARTBEAT_JITTER_RATIO as ge,DEFAULT_RUNNER_HEARTBEAT_TIMEOUT_MS as Ae,runRunnerPortalHeartbeat as V,runRunnerPortalHeartbeatLoop as ve,runRunnerPortalSync as te,runRunnerPortalSyncLoop as we}from"./portal-sync-worker.js";import{RUNNER_BINARY_REASON_CODES as Tt,checkRunnerBinaryTrust as jt,runnerTrustRelativePath as Nt}from"./binary.js";import{LOCAL_JOBS_CONSOLE_REASON_CODES as Dt,createLocalJobConsoleServer as Ot,isLocalJobsConsoleArgs as Mt,localJobConsoleSocketPath as xt,parseLocalJobsConsoleArgs as Ft,runLocalJobsConsoleCommand as Ht}from"./local-console.js";const d=Object.freeze({OK:"continuity-ok",LEASE_CONFLICT:"lease-conflict",LEASE_EXPIRED:"lease-expired",RUNNER_BINARY_UNTRUSTED:"runner-binary-untrusted",RUNNER_DIRECTIVE_UNTRUSTED:"runner-directive-untrusted",RUNNER_SERVER_INVOKE_FAILED:"runner-server-invoke-failed",SQLITE_ADAPTER_UNAVAILABLE:"sqlite-adapter-unavailable",SQLITE_INTEGRITY_FAILED:"sqlite-integrity-failed",BACKOFF_SCHEDULED:"backoff-scheduled",MISSED_RUN_CATCHUP:"missed-run-catchup",HEADLESS_RUNTIME_UNRESOLVED:"headless-runtime-unresolved",HEADLESS_RUNTIME_UNTRUSTED:"headless-runtime-untrusted",HEADLESS_ADAPTER_UNAVAILABLE:"headless-adapter-unavailable",HEADLESS_RUNTIME_AUTH_REQUIRED:"headless-runtime-auth-required",HEADLESS_EXECUTION_BACKOFF:"headless-execution-backoff",HEADLESS_EXECUTION_TIMEOUT:"headless-execution-timeout",HEADLESS_EXECUTION_FAILED:"headless-execution-failed",HEADLESS_RECEIPT_REDACTED:"headless-receipt-redacted",AUTONOMOUS_GOVERNANCE_BYPASS_BLOCKED:"headless-directive-rejected"}),Le=300*1e3,Ce=30*1e3,Te=30*1e3,je=1,Ne=10,Ue=new Set(["append_event","append_receipt","update_job_status","schedule_next_tick","release_lease","noop"]),De=new Set(["goal-tick","loop-tick","jobs","runner"]),Oe=new Set(["accepted","applied","ignored","failed","deferred"]),Me=new Set(["draft","planned","active","running","waiting_external","waiting_human","backoff","paused","blocked","completed","aborted","cancelled"]),xe=/(raw[_-]?logs?|prompt(?:[_-]?(?:body|text))?|private[_-]?urls?|secret|token|password|protected(?:[_-]?(?:field|body|internals))?|workflow(?:[_-]?body)?|step(?:[_-]?body)?|customer(?:[_-]?(?:pii|data))?|authorization|api[_-]?key|license[_-]?key)/i,q=/(https?:\/\/\S+|ghp_[A-Za-z0-9_]+|sk-[A-Za-z0-9_-]+|-----BEGIN [A-Z ]*PRIVATE KEY-----|authorization\s*:|password\s*=|token\s*=|api[_-]?key\s*=|license[_-]?key\s*=|raw[_-]?log|transcript|stack trace|stderr:|stdout:|system prompt|workflow body|step body|PROTECTED_|PRIVATE_INTERNALS|CUSTOMER_PII|VENDOR_CORPUS|P180[_-]?SYNTHETIC[_-]?CONTINUITY|P187[_-]?SYNTHETIC[_-]?(?:CONTINUITY|RUNNER|WATCH))/i,re=new Set([d.HEADLESS_RUNTIME_UNRESOLVED,d.HEADLESS_RUNTIME_UNTRUSTED,"headless-model-rejected","headless-directive-rejected","headless-prompt-rejected","headless-yoloop-metadata-invalid","headless-yoloop-isolation-violation"]),oe=new Set([d.HEADLESS_ADAPTER_UNAVAILABLE]),ae=new Set([d.HEADLESS_RUNTIME_AUTH_REQUIRED]),se=new Set([d.HEADLESS_EXECUTION_BACKOFF]),ne=new Set([d.HEADLESS_EXECUTION_TIMEOUT]),ce=new Set([d.HEADLESS_EXECUTION_FAILED]);async function Fe(e){const t=await ye({args:e.args,projectRoot:e.projectRoot,format:"json",platformTarget:"runner",serverUrl:e.serverUrl,strictReadOnly:e.strictReadOnly});return{success:t.success,instructions:t.instructions,metadata:t.metadata,error:t.error,exitCode:t.exitCode}}async function He(e={}){const t=G(e.projectRoot??process.cwd()),r=Y(e.runnerId??nt(t)),o=e.now??(()=>new Date().toISOString()),a=F(e.dueJobLimit??je,1,Ne),s=F(e.leaseMs??Le,1e3,3600*1e3),l=F(e.staleLeaseAfterMs??Ce,0,1440*60*1e3),c=e.idFactory??ct,I=he(e.processingSummarySource??(e.tickMode==="scheduled"?"scheduled_tick":"manual_tick")),y=[],R=[],i=[],u=[];if(e.trustCheck!==!1){const A=P(e.executablePath??process.argv[1]);if(!A.trusted)return{ok:!1,runnerId:r,processedJobCount:0,dueJobCount:0,recoveredLeaseIds:[],receipts:[],reasonCodes:[A.reasonCode??d.RUNNER_BINARY_UNTRUSTED],publicSafe:!0}}const m=await Z({projectRoot:t,adapter:e.adapter,adapterLoader:e.adapterLoader,now:o}),L=Be({options:e,projectRoot:t,runnerId:r,now:o});let C;try{const A=m.recoverStaleLeases({nowIso:o(),staleAfterMs:l,limit:a*2});if(A.recoveredLeaseIds.length>0){y.push(d.LEASE_EXPIRED);for(const E of A.recoveredLeaseIds);}const T=m.listDueJobs({nowIso:o(),limit:a});for(const E of T){const g=await Ve({store:m,job:E,projectRoot:t,runnerId:r,leaseMs:s,now:o,idFactory:c,processingSummarySource:I,serverUrl:e.serverUrl,serverInvoker:e.serverInvoker??Fe});R.push(g.receipt),i.push(...g.processingSummaries),g.executionMetadata&&u.push(g.executionMetadata),y.push(g.receipt.reasonCode)}const b=await Se({store:m,projectRoot:t,runnerId:r,now:o,metadata:u,adapterRegistry:e.headlessAdapterRegistry,adapterOptions:e.headlessAdapterOptions,runtimeDefaults:e.headlessRuntimeDefaults,runtimeFallback:e.headlessRuntimeFallback,limit:e.headlessExecutionLimit,leaseMs:e.headlessExecutionLeaseMs??s,staleLeaseAfterMs:e.headlessExecutionStaleLeaseAfterMs??l,timeoutMs:e.headlessExecutionTimeoutMs,backoffMs:e.headlessExecutionBackoffMs,maxAttempts:e.headlessExecutionMaxAttempts,queueCap:e.headlessExecutionQueueCap,idFactory:e.headlessExecutionIdFactory});y.push(...b.reasonCodes);const k=await Re({store:m,projectRoot:t,runnerId:r,now:o,metadata:u,adapterRegistry:e.headlessAdapterRegistry,adapterOptions:e.headlessAdapterOptions,runtimeDefaults:e.headlessRuntimeDefaults,runtimeFallback:e.headlessRuntimeFallback,limit:e.headlessExecutionLimit,leaseMs:e.headlessExecutionLeaseMs??s,staleLeaseAfterMs:e.headlessExecutionStaleLeaseAfterMs??l,timeoutMs:e.headlessExecutionTimeoutMs,backoffMs:e.headlessExecutionBackoffMs,maxAttempts:e.headlessExecutionMaxAttempts,queueCap:e.headlessExecutionQueueCap,maxIterations:e.headlessYoloopMaxIterations,maxDurationMs:e.headlessYoloopMaxDurationMs,localConcurrencyCap:e.headlessYoloopLocalConcurrencyCap,yoloopInvoker:e.headlessYoloopInvoker,serverUrl:e.serverUrl,idFactory:e.headlessExecutionIdFactory});y.push(...k.reasonCodes),C={ok:R.every(E=>E.outcome!=="failed")&&b.ok&&k.ok,runnerId:r,processedJobCount:R.length,dueJobCount:T.length,recoveredLeaseIds:A.recoveredLeaseIds,receipts:R,...i.length>0?{processingSummaries:i}:{},headlessExecution:b,headlessYoloopExecution:k,reasonCodes:J(y.length>0?y:[d.OK]),publicSafe:!0}}finally{await L.stop(),m.close()}if(!C)throw new Error("runner-tick-failed");return await Pe(C,{options:e,projectRoot:t,runnerId:r,localStateChanged:$e(C)})}async function Pe(e,t){const r=t.options.portalSync;if(r===!1||r===void 0)return e;if(!t.localStateChanged){const a=await V({...r,projectRoot:r.projectRoot??t.projectRoot,runnerId:r.runnerId??t.runnerId,sessionId:r.sessionId??t.options.portalHeartbeatSessionId,serverUrl:r.serverUrl??t.options.serverUrl,adapter:r.adapter??t.options.adapter,adapterLoader:r.adapterLoader??t.options.adapterLoader,now:r.now??t.options.now,mode:r.mode??(t.options.tickMode==="scheduled"?"scheduled":"background"),timeoutMs:r.heartbeatTimeoutMs??t.options.portalHeartbeatTimeoutMs});return{...e,portalHeartbeat:a}}const o=await te({...r,projectRoot:r.projectRoot??t.projectRoot,runnerId:r.runnerId??t.runnerId,sessionId:r.sessionId??t.options.portalHeartbeatSessionId,serverUrl:r.serverUrl??t.options.serverUrl,adapter:r.adapter??t.options.adapter,adapterLoader:r.adapterLoader??t.options.adapterLoader,now:r.now??t.options.now,mode:r.mode??(t.options.tickMode==="scheduled"?"scheduled":"background")});return{...e,portalSync:o}}function $e(e){return e.recoveredLeaseIds.length>0||e.receipts.length>0||(e.processingSummaries?.length??0)>0||(e.headlessExecution?.processedAttemptCount??0)>0||(e.headlessExecution?.recoveredAttemptIds.length??0)>0||(e.headlessYoloopExecution?.processedAttemptCount??0)>0||(e.headlessYoloopExecution?.recoveredAttemptIds.length??0)>0}function Be(e){const t=e.options.portalSync;if(t===!1||t===void 0)return{stop:async()=>{}};const r=F(e.options.portalHeartbeatIntervalMs??ke,0,1440*60*1e3);if(r<=0)return{stop:async()=>{}};const o=F(e.options.portalHeartbeatBackoffMs??_e,1e3,1800*1e3),a=pe(e.options.portalHeartbeatJitterRatio??ge,0,.5),s=F(e.options.portalHeartbeatTimeoutMs??t.heartbeatTimeoutMs??Ae,1e3,6e4);let l=!1,c,I,y=me(r,a,Math.random);const R=u=>new Promise(m=>{I=m,c=setTimeout(m,u),typeof c.unref=="function"&&c.unref()}),i=(async()=>{for(;!l&&(y>0&&await R(y),!l);){const u=await V({...t,projectRoot:t.projectRoot??e.projectRoot,runnerId:t.runnerId??e.runnerId,sessionId:t.sessionId??e.options.portalHeartbeatSessionId,serverUrl:t.serverUrl??e.options.serverUrl,adapter:t.adapter??e.options.adapter,adapterLoader:t.adapterLoader??e.options.adapterLoader,now:t.now??e.now,mode:t.mode??(e.options.tickMode==="scheduled"?"scheduled":"background"),timeoutMs:s}).catch(()=>({ok:!1,runnerId:e.runnerId,workspaceId:"workspace-default",workspaceLabel:"Workspace workspace-default",projectId:"project-default",projectLabel:"Project project-default",mode:e.options.tickMode==="scheduled"?"scheduled":"background",generatedAt:e.now(),serverAccessToLocalhost:!1,serverHeldStream:!1,localhostCallback:!1,posted:{heartbeat:!1},counts:{redactions:0},reasonCodes:[d.BACKOFF_SCHEDULED],redactionEvidence:[],publicSafe:!0}));y=me(u.ok?r:o,a,Math.random)}})();return{stop:async()=>{l=!0,c&&clearTimeout(c),I?.(),await i.catch(()=>{})}}}async function Ke(e={}){const t=G(e.projectRoot??process.cwd()),r=e.now??(()=>new Date().toISOString()),o=r(),a={};let s=null,l=null;try{l=await Z({projectRoot:t,adapter:e.adapter,adapterLoader:e.adapterLoader,now:r}),s=l.collectSnapshotRows({nowIso:o,jobLimit:10,staleLeaseLimit:10,receiptLimit:10,eventLimit:10,snapshotLimit:5});const f=String(l.pragmaValue("journal_mode")??"").toLowerCase();a["db-integrity"]=w("db-integrity","DB integrity","ok",[d.OK],"Continuity SQLite integrity check completed.",{jobs:_(s.counts,"jobs"),receipts:_(s.counts,"receipts"),events:_(s.counts,"events")}),a["sqlite-wal"]=w("sqlite-wal","SQLite WAL",f==="wal"?"ok":"blocked",f==="wal"?[d.OK]:[d.SQLITE_INTEGRITY_FAILED],f==="wal"?"SQLite journal mode is WAL.":"SQLite journal mode is not WAL."),a.migrations=w("migrations","Migrations",s.schemaVersion>=1?"ok":"blocked",s.schemaVersion>=1?[d.OK]:[d.SQLITE_INTEGRITY_FAILED],`Continuity schema version ${s.schemaVersion} is readable.`)}catch(f){const p=ue(f);a["db-integrity"]=w("db-integrity","DB integrity","blocked",[p],"Continuity SQLite could not be opened safely."),a["sqlite-wal"]=w("sqlite-wal","SQLite WAL","blocked",[p],"SQLite WAL could not be verified."),a.migrations=w("migrations","Migrations","blocked",[p],"Continuity migrations could not be verified.")}finally{if(l)try{l.close()}catch{}}const c=s?_(s.counts,"staleLeases"):0,I=s?_(s.counts,"staleHeadlessExecutionAttempts"):0;a.leases=w("leases","Lease health",c+I>0?"degraded":"ok",c+I>0?[d.LEASE_EXPIRED]:[d.OK],c+I>0?"Stale job leases or headless execution attempts are eligible for bounded recovery.":"No stale leases or headless attempts reported.",{staleLeases:c,staleHeadlessExecutionAttempts:I});const y=s?.headlessExecutionAttempts??[],R=J(y.map(f=>f.reason_code).filter(f=>typeof f=="string"&&f.length>0)),i=ft(y,R),u=new Set([d.HEADLESS_RUNTIME_UNRESOLVED,d.HEADLESS_RUNTIME_UNTRUSTED,d.HEADLESS_ADAPTER_UNAVAILABLE,d.HEADLESS_RUNTIME_AUTH_REQUIRED,"headless-model-rejected","headless-directive-rejected","headless-prompt-rejected","headless-yoloop-metadata-invalid","headless-yoloop-isolation-violation"]),m=s?_(s.counts,"headlessExecutionAttempts"):0,L=R.some(f=>u.has(f)),C=R.includes(d.HEADLESS_EXECUTION_BACKOFF)||y.some(f=>f.status==="backoff"),A=R.includes(d.HEADLESS_RECEIPT_REDACTED),T=i.authRequired>0,b=i.trustFailures>0||i.adapterUnavailable>0,k=I>0||C||A||i.timeout>0||i.genericFailed>0||i.redactions>0;a["headless-execution"]=w("headless-execution","Headless execution",L?"blocked":k?"degraded":m>0?"ok":"delegated",R.length>0?R:m>0?[d.OK]:[d.HEADLESS_RUNTIME_UNRESOLVED],L?T&&!b?"Complete local runtime login/auth setup before starting a new headless attempt; default retry/backoff is not scheduled for auth-required.":"Resolve public runtime/model trust, adapter availability or protected directive hard-stop reason codes before executing headless attempts.":k?"Headless attempts need bounded recovery, retry/backoff, timeout/generic failure follow-up or redaction review.":m>0?"Headless attempts are represented by public status, runtime/model and reason-code evidence.":"Headless resolver checks are delegated until durable headless work is queued.",{attempts:m,queuedBackoff:s?_(s.counts,"headlessQueuedAttempts"):0,running:s?_(s.counts,"headlessRunningAttempts"):0,staleHeadlessExecutionAttempts:I,trustFailures:i.trustFailures,adapterUnavailable:i.adapterUnavailable,authRequired:i.authRequired,retryBackoff:i.retryBackoff,timeout:i.timeout,genericFailed:i.genericFailed,redactions:i.redactions});const E=s?.outboxCounts??{},g=_(E,"failed")+_(E,"dead_letter");a.outbox=w("outbox","Outbox state",g>0?"degraded":"ok",g>0?[d.BACKOFF_SCHEDULED]:[d.OK],g>0?"Failed/dead-letter outbox entries can be retried with dedupe.":"Outbox has no failed/dead-letter entries.",{pending:_(E,"pending"),failed:_(E,"failed"),deadLetter:_(E,"dead_letter")}),a.metering=w("metering","Metering receipts","ok",[d.OK],"Metering evidence is represented by public job receipts and counts only.",{receipts:s?_(s.counts,"receipts"):0}),a["portal-sync"]=w("portal-sync","Portal sync",g>0?"degraded":"ok",g>0?[d.BACKOFF_SCHEDULED]:[d.OK],g>0?"Portal sync worker is enabled but has retryable outbox debt; outbound sync remains client-owned.":"Portal sync worker is enabled for outbound heartbeat, jobs sync, command polling and acknowledgement only.",{pending:_(E,"pending"),failed:_(E,"failed"),deadLetter:_(E,"dead_letter")});const N=e.trustCheck===!1?{trusted:!0,publicSafe:!0}:P(e.executablePath??process.argv[1]);a.runner=w("runner","Runner health",N.trusted?"ok":"blocked",N.trusted?[d.OK]:[N.reasonCode??d.RUNNER_BINARY_UNTRUSTED],N.trusted?"Runner binary trust check passed.":"Runner binary trust check failed.");try{const f=await ee({command:"doctor",projectRoot:t,executablePath:e.scheduler?.executablePath??e.executablePath??process.argv[1],platform:e.scheduler?.platform,env:e.scheduler?.env,dryRun:!0,intervalSeconds:e.scheduler?.intervalSeconds,strategy:e.scheduler?.strategy,allowCronFallback:e.scheduler?.allowCronFallback,trustCheck:e.scheduler?.trustCheck??e.trustCheck,uid:e.scheduler?.uid,commandAvailable:e.scheduler?.commandAvailable,commandRunner:e.scheduler?.commandRunner,trustChecker:e.scheduler?.trustChecker??P}),p=f.reasonCodes.includes(d.RUNNER_BINARY_UNTRUSTED)||f.reasonCodes.some(h=>h.startsWith("runner-binary-"))||f.reasonCodes.includes("scheduler-install-unsafe");a.scheduler=w("scheduler","Scheduler install",p?"blocked":f.ok?"ok":"degraded",f.reasonCodes,f.ok?"Scheduler doctor selected a user-scope strategy.":"Scheduler doctor found public guidance or degraded capability.",{manifests:f.manifestPaths.length,controlCommands:f.controlCommands.length,legacyEntries:f.legacyEntries.length})}catch(f){a.scheduler=w("scheduler","Scheduler install","degraded",["scheduler-command-failed"],x(f))}const v=a,H=J(Object.values(v).flatMap(f=>f.reasonCodes));return{ok:Object.values(v).every(f=>f.status!=="blocked"),generatedAt:o,checks:v,reasonCodes:H,publicSafe:!0}}function de(e){if(!e)return[];const t=[e.continuityRunnerDirectives,e.runnerDirectives,e.continuityDirectives];for(const r of t)if(Array.isArray(r))return r.filter(j).map(it).filter(lt);return[]}function Je(e,t){if(!j(e))throw new U("Directive is not a public record.");if(e.publicSafe!==!0)throw new U("Directive is not marked public-safe.");const r=typeof e.kind=="string"?e.kind:typeof e.type=="string"?e.type:"";if(!Ue.has(r))throw new U("Directive kind is not allowlisted.");const o=typeof e.commandClass=="string"?e.commandClass:"runner";if(!De.has(o))throw new U("Directive command class is not allowlisted.");if((typeof e.jobId=="string"?e.jobId:t)!==t)throw new U("Directive job ID does not match the leased job.");Q(e,"directive")}async function wt(e,t={}){const r=tt(e);if(r.command==="help")return process.stdout.write(`${ot}
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
`),1}}async function Ve(e){const{store:t,job:r,projectRoot:o,runnerId:a,leaseMs:s,now:l,idFactory:c,processingSummarySource:I,serverUrl:y,serverInvoker:R}=e,i=r.job_id,u=c("tick",i),m=c("lease",i),L=l(),C=new Date(Date.parse(L)+s).toISOString();let A=!1,T=!1,b=d.OK,k="applied",E=[];if(!t.acquireLease({leaseId:m,jobId:i,runnerId:a,acquiredAt:L,expiresAt:C,operationId:`${u}:acquire`,idempotencyKey:`${u}:acquire`,metadata:M({jobId:i,operationId:u,runnerId:a,leaseId:m,status:"running",reasonCode:d.OK})}).inserted)return{receipt:K(t,{jobId:i,tickId:u,leaseId:m,status:"ignored",reasonCode:d.LEASE_CONFLICT,createdAt:l(),redactionSummary:E}),processingSummaries:[]};A=!0;try{t.appendEvent({eventId:`${u}:started`,jobId:i,operationId:`${u}:event:started`,idempotencyKey:`${u}:event:started`,type:"runner_tick_started",summary:`Runner tick started for ${i}.`,reasonCode:d.OK,createdAt:L,metadata:M({jobId:i,operationId:u,runnerId:a,leaseId:m,status:"running",reasonCode:d.OK})});const N=r.kind==="loop"?`*loop tick @${i} --tick-id ${u} --lease-id ${m} --runner-id ${a}`:r.kind==="goal"?`*goal tick @${i} --tick-id ${u} --lease-id ${m} --runner-id ${a}`:`*runner tick --job-id ${i} --tick-id ${u} --lease-id ${m} --runner-id ${a}`,v=await R({projectRoot:o,args:N,jobId:i,tickId:u,leaseId:m,runnerId:a,serverUrl:y});if(!v.success){b=d.RUNNER_SERVER_INVOKE_FAILED,k="failed";const O=l();return t.updateJobStatus({jobId:i,status:"backoff",updatedAt:O,reasonCode:d.BACKOFF_SCHEDULED,nextRunAt:dt(O,Te)}),t.appendEvent({eventId:`${u}:server-failed`,jobId:i,operationId:`${u}:event:server-failed`,idempotencyKey:`${u}:event:server-failed`,type:"runner_tick_failed",summary:"Runner server tick invocation failed with public reason code.",reasonCode:b,createdAt:O,metadata:M({jobId:i,operationId:u,runnerId:a,leaseId:m,status:"failed",reasonCode:b})}),{receipt:K(t,{jobId:i,tickId:u,leaseId:m,status:k,reasonCode:b,createdAt:l(),redactionSummary:E}),processingSummaries:[]}}const f=de(v.metadata).filter(O=>!ie(O));let p={appliedCount:0,releasedLease:!1,changedStatus:!1,redactionSummary:[]};try{p=qe(t,f,{jobId:i,tickId:u,leaseId:m,runnerId:a,now:l}),T=p.releasedLease,E=[...p.redactionSummary],b=et(v.metadata)??b}catch(O){return b=d.RUNNER_DIRECTIVE_UNTRUSTED,k="failed",E=[O instanceof Error?O.message:"Directive rejected."],t.updateJobStatus({jobId:i,status:"blocked",updatedAt:l(),reasonCode:b}),t.appendEvent({eventId:`${u}:directive-rejected`,jobId:i,operationId:`${u}:event:directive-rejected`,idempotencyKey:`${u}:event:directive-rejected`,type:"runner_directive_rejected",summary:"Runner rejected an untrusted continuity directive.",reasonCode:b,createdAt:l(),metadata:M({jobId:i,operationId:u,runnerId:a,leaseId:m,status:"failed",reasonCode:b,redactionSummary:E})}),{receipt:K(t,{jobId:i,tickId:u,leaseId:m,status:k,reasonCode:b,createdAt:l(),redactionSummary:E}),processingSummaries:[]}}const h=Ye(t,v.metadata,{jobId:i,tickId:u,leaseId:m,runnerId:a,source:I,createdAt:l()}),S=!!v.metadata?.continuityStateUpdate;return!p.changedStatus&&!S&&t.updateJobStatus({jobId:i,status:"active",updatedAt:l(),reasonCode:d.OK}),t.appendEvent({eventId:`${u}:completed`,jobId:i,operationId:`${u}:event:completed`,idempotencyKey:`${u}:event:completed`,type:"runner_tick_completed",summary:`Runner tick completed with ${p.appliedCount} public directive(s).`,reasonCode:b,createdAt:l(),metadata:M({jobId:i,operationId:u,runnerId:a,leaseId:m,status:"applied",reasonCode:b})}),{receipt:K(t,{jobId:i,tickId:u,leaseId:m,status:k,reasonCode:b,createdAt:l(),redactionSummary:E}),processingSummaries:h,...v.metadata?{executionMetadata:v.metadata}:{}}}finally{if(A&&!T)try{t.releaseLease({leaseId:m,releasedAt:l(),reasonCode:b})}catch{}}}function qe(e,t,r){let o=0,a=!1,s=!1;const l=[];for(const c of t){switch(Je(c,r.jobId),c.kind){case"noop":o+=1;break;case"append_event":e.appendEvent({eventId:n(c,"eventId")??`${r.tickId}:directive:${o}:event`,jobId:r.jobId,operationId:n(c,"operationId")??`${r.tickId}:directive:${o}:event`,idempotencyKey:n(c,"idempotencyKey")??`${r.tickId}:directive:${o}:event`,type:n(c,"eventType")??"runner_directive_event",summary:n(c,"summary")??"Runner applied public continuity event directive.",reasonCode:n(c,"reasonCode")??d.OK,createdAt:n(c,"createdAt")??r.now(),metadata:fe(c)}),o+=1;break;case"append_receipt":e.appendReceipt({receiptId:n(c,"receiptId")??`${r.tickId}:directive:${o}:receipt`,jobId:r.jobId,operationId:n(c,"operationId")??`${r.tickId}:directive:${o}:receipt`,idempotencyKey:n(c,"idempotencyKey")??`${r.tickId}:directive:${o}:receipt`,status:pt(n(c,"status")),reasonCode:n(c,"reasonCode")??d.OK,createdAt:n(c,"createdAt")??r.now(),metadata:fe(c)}),o+=1;break;case"update_job_status":e.updateJobStatus({jobId:r.jobId,status:mt(n(c,"status")),updatedAt:n(c,"updatedAt")??r.now(),reasonCode:n(c,"reasonCode")??d.OK,nextRunAt:n(c,"nextRunAt")}),o+=1,s=!0;break;case"schedule_next_tick":e.scheduleNextTick({jobId:r.jobId,scheduledAt:ut(c,"scheduledAt"),updatedAt:n(c,"updatedAt")??r.now(),reasonCode:n(c,"reasonCode")??d.OK}),o+=1,s=!0;break;case"release_lease":e.releaseLease({leaseId:n(c,"leaseId")??r.leaseId,releasedAt:n(c,"releasedAt")??r.now(),reasonCode:n(c,"reasonCode")??d.OK}),o+=1,a=!0;break}const I=c.redactionSummary;Array.isArray(I)&&l.push(...I.filter(y=>typeof y=="string").slice(0,5))}return{appliedCount:o,releasedLease:a,changedStatus:s,redactionSummary:l}}function Ye(e,t,r){const o=[],a=new Set;for(const s of Qe(t)){const l=ze(s,r);if(!(!l||a.has(l.executionId))){a.add(l.executionId);try{e.recordProcessingOutputSummary(l),o.push(Xe(l))}catch(c){if(c instanceof Ie&&c.code==="sqlite-public-safety-rejected")continue;throw c}}}return o}function Qe(e){if(!e)return[];const t=[],r=D(e,"continuityStateUpdate"),o=$(r,"operations");for(const a of o){if(!j(a))continue;const s=D(a,"event");!s||n(s,"type")!==X||t.push({operation:a,event:s,summary:D(s,"processingOutputSummary")??s})}for(const a of de(e))ie(a)&&t.push({directive:a,event:a,summary:D(a,"processingOutputSummary")??a});for(const a of[...$(e,"processingSummaries"),...$(e,"processingOutputSummaries"),...$(e,"continuityProcessingSummaries"),...$(D(e,"continuity"),"processingSummaries")])j(a)&&t.push({event:a,summary:a});return t}function ze(e,t){if(!We(e))return null;const r=e.event??{},o=e.summary??r,a=e.operation??{},s=e.directive??{},l=n(o,"executionId")??n(r,"executionId")??n(a,"operationId")??n(s,"operationId")??t.tickId,c=n(o,"jobId")??n(r,"jobId")??n(a,"jobId")??n(s,"jobId")??t.jobId;if(c!==t.jobId)return null;const I=n(o,"publicSummary")??n(r,"publicSummary")??n(r,"summary")??n(s,"summary");if(!l||!I)return null;const y=Ge(D(a,"metadata"),D(r,"metadata"),D(o,"metadata"),D(s,"metadata"),{schemaVersion:1,contractVersion:"continuity-v1",executionId:l,jobId:c,operationId:l,runnerId:t.runnerId,leaseId:t.leaseId,processingOutputSource:t.source,publicSummary:I});return{eventId:n(r,"eventId")??n(o,"eventId")??n(s,"eventId"),executionId:l,jobId:c,idempotencyKey:n(a,"idempotencyKey")??n(s,"idempotencyKey")??l,goalId:n(o,"goalId")??n(r,"goalId")??n(a,"goalId"),loopId:n(o,"loopId")??n(r,"loopId")??n(a,"loopId"),publicSummary:I,reasonCode:n(o,"reasonCode")??n(r,"reasonCode")??n(a,"reasonCode")??n(s,"reasonCode"),createdAt:n(o,"createdAt")??n(r,"createdAt")??n(a,"createdAt")??n(s,"createdAt")??t.createdAt,source:t.source,metadata:y}}function ie(e){return e.kind==="append_event"&&(n(e,"eventType")??n(e,"type"))===X}function We(e){const t=[e.operation,e.event,e.summary,e.directive].filter(r=>!!r).map(r=>r.publicSafe);return t.some(r=>r===!1)?!1:t.some(r=>r===!0)}function Ge(...e){const t={};for(const r of e)r&&Object.assign(t,r);return t}function Xe(e){return{eventId:e.eventId??Ze(e.executionId),executionId:e.executionId,jobId:e.jobId,publicSummary:e.publicSummary,...e.reasonCode?{reasonCode:e.reasonCode}:{},createdAt:e.createdAt,source:he(e.source),publicSafe:!0}}function Ze(e){const t=e.replace(/[^a-z0-9_.:-]+/gi,"-").replace(/^-+|-+$/g,"");return t.length>0?`event-processing-summary-${t.slice(0,160)}`:`event-processing-summary-${W("sha256").update(e).digest("hex").slice(0,32)}`}function K(e,t){const r={receiptId:`receipt-${Y(t.tickId)}`,jobId:t.jobId,operationId:t.tickId,idempotencyKey:t.tickId,status:t.status,reasonCode:t.reasonCode,appliedOperationIds:t.status==="applied"?[t.tickId]:void 0,ignoredOperationIds:t.status==="ignored"?[t.tickId]:void 0,createdAt:t.createdAt,metadata:M({jobId:t.jobId,operationId:t.tickId,leaseId:t.leaseId,status:t.status,reasonCode:t.reasonCode,redactionSummary:t.redactionSummary})};return e.appendReceipt(r),{jobId:t.jobId,tickId:t.tickId,leaseId:t.leaseId,outcome:t.status,reasonCode:t.reasonCode,timestamp:t.createdAt,redactionSummary:t.redactionSummary,publicSafe:!0}}function M(e){return{schemaVersion:1,contractVersion:"continuity-v1",jobId:e.jobId,operationId:e.operationId,...e.runnerId?{runnerId:e.runnerId}:{},...e.leaseId?{leaseId:e.leaseId}:{},status:e.status,reasonCode:e.reasonCode,redactionEvidence:(e.redactionSummary??[]).slice(0,20).map(t=>({field:z(t,80),reason:"unsafe_key",evidence:"operation-denied",publicSafe:!0}))}}function et(e){if(!e)return null;const t=j(e.continuity)?e.continuity:void 0,r=t&&j(t.loop)?t.loop:void 0,o=t&&j(t.goal)?t.goal:void 0,a=[r?.reasonCode,o?.reasonCode,t?.reasonCode];for(const s of a)if(typeof s=="string"&&/^[a-z0-9][a-z0-9-]{0,79}$/.test(s)&&!q.test(s))return s;return null}function tt(e){const t=e[0];if(!t||t==="--help"||t==="-h"||t==="help")return{command:"help"};if(t==="--version"||t==="-v"||t==="version")return{command:"version"};if(t!=="tick"&&t!=="sync"&&t!=="heartbeat"&&!le(t))return{command:"unknown"};let r,o,a,s,l,c,I,y,R,i,u,m,L,C,A,T,b,k,E,g,N,v,H,f;for(let p=1;p<e.length;p++){const h=e[p],S=e[p+1];if(h==="--project-root"){r=S,p+=1;continue}if(h==="--runner-id"){o=S,p+=1;continue}if(h==="--server-url"){a=S,p+=1;continue}if(h==="--limit"){s=Number(S),p+=1;continue}if(h==="--lease-ms"){l=Number(S),p+=1;continue}if(h==="--stale-lease-after-ms"){c=Number(S),p+=1;continue}if(h==="--dry-run"){I=!0;continue}if(h==="--interval-seconds"){y=Number(S),p+=1;continue}if(h==="--interval-ms"){R=Number(S),p+=1;continue}if(h==="--max-runs"){i=Number(S),p+=1;continue}if(h==="--backoff-ms"){u=Number(S),p+=1;continue}if(h==="--heartbeat-timeout-ms"||h==="--timeout-ms"){m=Number(S),p+=1;continue}if(h==="--jitter-ratio"){L=Number(S),p+=1;continue}if(h==="--session-id"){C=S,p+=1;continue}if(h==="--strategy"&&at(S)){A=S,p+=1;continue}if(h==="--allow-cron-fallback"){T=!0;continue}if(h==="--no-cron-fallback"){T=!1;continue}if(h==="--scheduled"){b=!0;continue}if(h==="--manual"){b=!1;continue}if(h==="--loop"||h==="--background"){k=!0;continue}if(h==="--once"){k=!1;continue}if(h==="--workspace-id"){E=S,p+=1;continue}if(h==="--workspace-label"){g=S,p+=1;continue}if(h==="--project-id"){N=S,p+=1;continue}if(h==="--project-label"){v=S,p+=1;continue}if(h==="--sync-cursor"){H=S,p+=1;continue}if(h==="--yoloop-local-cap"){f=Number(S),p+=1;continue}}return{command:t,projectRoot:r,runnerId:o,serverUrl:a,limit:s,leaseMs:l,staleLeaseAfterMs:c,dryRun:I,intervalSeconds:y,intervalMs:R,maxRuns:i,heartbeatBackoffMs:u,heartbeatTimeoutMs:m,heartbeatJitterRatio:L,sessionId:C,strategy:A,allowCronFallback:T,scheduled:b,loop:k,workspaceId:E,workspaceLabel:g,projectId:N,projectLabel:v,syncCursor:H,yoloopLocalCap:f}}function rt(e,t,r){return t.portalSync===!1?!1:{...t.portalSync??{},...e.projectRoot??t.projectRoot?{projectRoot:e.projectRoot??t.projectRoot}:{},...e.runnerId??t.runnerId?{runnerId:e.runnerId??t.runnerId}:{},...e.sessionId??t.portalHeartbeatSessionId?{sessionId:e.sessionId??t.portalHeartbeatSessionId}:{},...e.serverUrl??t.serverUrl?{serverUrl:e.serverUrl??t.serverUrl}:{},...e.workspaceId?{workspaceId:e.workspaceId}:{},...e.workspaceLabel?{workspaceLabel:e.workspaceLabel}:{},...e.projectId?{projectId:e.projectId}:{},...e.projectLabel?{projectLabel:e.projectLabel}:{},...e.heartbeatTimeoutMs??t.portalHeartbeatTimeoutMs?{heartbeatTimeoutMs:e.heartbeatTimeoutMs??t.portalHeartbeatTimeoutMs}:{},mode:r}}const ot=`
|
|
1
|
+
/**
|
|
2
|
+
* @license FSL-1.1
|
|
3
|
+
* Copyright (c) 2026 OrNexus AI
|
|
4
|
+
*
|
|
5
|
+
* P180.06 -- neocortex-runner core tick loop, leases, receipts and recovery.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE, ContinuitySqliteStoreError, openContinuitySqliteStore, } from '../continuity/sqlite-store.js';
|
|
10
|
+
import { invoke } from '../commands/invoke.js';
|
|
11
|
+
import { checkRunnerBinaryTrust, } from './binary.js';
|
|
12
|
+
import { runRunnerSchedulerCommand, } from './scheduler.js';
|
|
13
|
+
import { runYoloExecutionPhase, } from './headless/run-yolo.js';
|
|
14
|
+
import { runYoloopExecutionPhase, } from './headless/run-yoloop.js';
|
|
15
|
+
import { DEFAULT_RUNNER_HEARTBEAT_BACKOFF_MS, DEFAULT_RUNNER_HEARTBEAT_INTERVAL_MS, DEFAULT_RUNNER_HEARTBEAT_JITTER_RATIO, DEFAULT_RUNNER_HEARTBEAT_TIMEOUT_MS, runRunnerPortalHeartbeat, runRunnerPortalHeartbeatLoop, runRunnerPortalSync, runRunnerPortalSyncLoop, } from './portal-sync-worker.js';
|
|
16
|
+
export { RUNNER_BINARY_REASON_CODES, checkRunnerBinaryTrust, runnerTrustRelativePath } from './binary.js';
|
|
17
|
+
export { LOCAL_JOBS_CONSOLE_REASON_CODES, createLocalJobConsoleServer, isLocalJobsConsoleArgs, localJobConsoleSocketPath, parseLocalJobsConsoleArgs, runLocalJobsConsoleCommand, } from './local-console.js';
|
|
18
|
+
export const RUNNER_REASON_CODES = Object.freeze({
|
|
19
|
+
OK: 'continuity-ok',
|
|
20
|
+
LEASE_CONFLICT: 'lease-conflict',
|
|
21
|
+
LEASE_EXPIRED: 'lease-expired',
|
|
22
|
+
RUNNER_BINARY_UNTRUSTED: 'runner-binary-untrusted',
|
|
23
|
+
RUNNER_DIRECTIVE_UNTRUSTED: 'runner-directive-untrusted',
|
|
24
|
+
RUNNER_SERVER_INVOKE_FAILED: 'runner-server-invoke-failed',
|
|
25
|
+
SQLITE_ADAPTER_UNAVAILABLE: 'sqlite-adapter-unavailable',
|
|
26
|
+
SQLITE_INTEGRITY_FAILED: 'sqlite-integrity-failed',
|
|
27
|
+
BACKOFF_SCHEDULED: 'backoff-scheduled',
|
|
28
|
+
MISSED_RUN_CATCHUP: 'missed-run-catchup',
|
|
29
|
+
HEADLESS_RUNTIME_UNRESOLVED: 'headless-runtime-unresolved',
|
|
30
|
+
HEADLESS_RUNTIME_UNTRUSTED: 'headless-runtime-untrusted',
|
|
31
|
+
HEADLESS_ADAPTER_UNAVAILABLE: 'headless-adapter-unavailable',
|
|
32
|
+
HEADLESS_RUNTIME_AUTH_REQUIRED: 'headless-runtime-auth-required',
|
|
33
|
+
HEADLESS_EXECUTION_BACKOFF: 'headless-execution-backoff',
|
|
34
|
+
HEADLESS_EXECUTION_TIMEOUT: 'headless-execution-timeout',
|
|
35
|
+
HEADLESS_EXECUTION_FAILED: 'headless-execution-failed',
|
|
36
|
+
HEADLESS_RECEIPT_REDACTED: 'headless-receipt-redacted',
|
|
37
|
+
AUTONOMOUS_GOVERNANCE_BYPASS_BLOCKED: 'headless-directive-rejected',
|
|
38
|
+
});
|
|
39
|
+
export const DEFAULT_RUNNER_LEASE_MS = 5 * 60 * 1000;
|
|
40
|
+
export const DEFAULT_STALE_LEASE_AFTER_MS = 30 * 1000;
|
|
41
|
+
export const DEFAULT_RUNNER_BACKOFF_MS = 30 * 1000;
|
|
42
|
+
export const DEFAULT_DUE_JOB_LIMIT = 1;
|
|
43
|
+
export const MAX_DUE_JOB_LIMIT = 10;
|
|
44
|
+
const ALLOWED_DIRECTIVE_KINDS = new Set([
|
|
45
|
+
'append_event',
|
|
46
|
+
'append_receipt',
|
|
47
|
+
'update_job_status',
|
|
48
|
+
'schedule_next_tick',
|
|
49
|
+
'release_lease',
|
|
50
|
+
'noop',
|
|
51
|
+
]);
|
|
52
|
+
const ALLOWED_COMMAND_CLASSES = new Set(['goal-tick', 'loop-tick', 'jobs', 'runner']);
|
|
53
|
+
const RECEIPT_STATUSES = new Set(['accepted', 'applied', 'ignored', 'failed', 'deferred']);
|
|
54
|
+
const JOB_STATUSES = new Set([
|
|
55
|
+
'draft',
|
|
56
|
+
'planned',
|
|
57
|
+
'active',
|
|
58
|
+
'running',
|
|
59
|
+
'waiting_external',
|
|
60
|
+
'waiting_human',
|
|
61
|
+
'backoff',
|
|
62
|
+
'paused',
|
|
63
|
+
'blocked',
|
|
64
|
+
'completed',
|
|
65
|
+
'aborted',
|
|
66
|
+
'cancelled',
|
|
67
|
+
]);
|
|
68
|
+
const UNSAFE_KEY_PATTERN = /(raw[_-]?logs?|prompt(?:[_-]?(?:body|text))?|private[_-]?urls?|secret|token|password|protected(?:[_-]?(?:field|body|internals))?|workflow(?:[_-]?body)?|step(?:[_-]?body)?|customer(?:[_-]?(?:pii|data))?|authorization|api[_-]?key|license[_-]?key)/i;
|
|
69
|
+
const UNSAFE_VALUE_PATTERN = /(https?:\/\/\S+|ghp_[A-Za-z0-9_]+|sk-[A-Za-z0-9_-]+|-----BEGIN [A-Z ]*PRIVATE KEY-----|authorization\s*:|password\s*=|token\s*=|api[_-]?key\s*=|license[_-]?key\s*=|raw[_-]?log|transcript|stack trace|stderr:|stdout:|system prompt|workflow body|step body|PROTECTED_|PRIVATE_INTERNALS|CUSTOMER_PII|VENDOR_CORPUS|P180[_-]?SYNTHETIC[_-]?CONTINUITY|P187[_-]?SYNTHETIC[_-]?(?:CONTINUITY|RUNNER|WATCH))/i;
|
|
70
|
+
const RUNNER_HEADLESS_TRUST_FAILURE_REASON_CODES = new Set([
|
|
71
|
+
RUNNER_REASON_CODES.HEADLESS_RUNTIME_UNRESOLVED,
|
|
72
|
+
RUNNER_REASON_CODES.HEADLESS_RUNTIME_UNTRUSTED,
|
|
73
|
+
'headless-model-rejected',
|
|
74
|
+
'headless-directive-rejected',
|
|
75
|
+
'headless-prompt-rejected',
|
|
76
|
+
'headless-yoloop-metadata-invalid',
|
|
77
|
+
'headless-yoloop-isolation-violation',
|
|
78
|
+
]);
|
|
79
|
+
const RUNNER_HEADLESS_ADAPTER_UNAVAILABLE_REASON_CODES = new Set([
|
|
80
|
+
RUNNER_REASON_CODES.HEADLESS_ADAPTER_UNAVAILABLE,
|
|
81
|
+
]);
|
|
82
|
+
const RUNNER_HEADLESS_AUTH_REQUIRED_REASON_CODES = new Set([
|
|
83
|
+
RUNNER_REASON_CODES.HEADLESS_RUNTIME_AUTH_REQUIRED,
|
|
84
|
+
]);
|
|
85
|
+
const RUNNER_HEADLESS_BACKOFF_REASON_CODES = new Set([
|
|
86
|
+
RUNNER_REASON_CODES.HEADLESS_EXECUTION_BACKOFF,
|
|
87
|
+
]);
|
|
88
|
+
const RUNNER_HEADLESS_TIMEOUT_REASON_CODES = new Set([
|
|
89
|
+
RUNNER_REASON_CODES.HEADLESS_EXECUTION_TIMEOUT,
|
|
90
|
+
]);
|
|
91
|
+
const RUNNER_HEADLESS_GENERIC_FAILURE_REASON_CODES = new Set([
|
|
92
|
+
RUNNER_REASON_CODES.HEADLESS_EXECUTION_FAILED,
|
|
93
|
+
]);
|
|
94
|
+
export async function defaultRunnerServerInvoker(input) {
|
|
95
|
+
const result = await invoke({
|
|
96
|
+
args: input.args,
|
|
97
|
+
projectRoot: input.projectRoot,
|
|
98
|
+
format: 'json',
|
|
99
|
+
platformTarget: 'runner',
|
|
100
|
+
serverUrl: input.serverUrl,
|
|
101
|
+
strictReadOnly: input.strictReadOnly,
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
success: result.success,
|
|
105
|
+
instructions: result.instructions,
|
|
106
|
+
metadata: result.metadata,
|
|
107
|
+
error: result.error,
|
|
108
|
+
exitCode: result.exitCode,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export async function runRunnerTick(options = {}) {
|
|
112
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
113
|
+
const runnerId = sanitizeId(options.runnerId ?? defaultRunnerId(projectRoot));
|
|
114
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
115
|
+
const dueJobLimit = boundedInteger(options.dueJobLimit ?? DEFAULT_DUE_JOB_LIMIT, 1, MAX_DUE_JOB_LIMIT);
|
|
116
|
+
const leaseMs = boundedInteger(options.leaseMs ?? DEFAULT_RUNNER_LEASE_MS, 1_000, 60 * 60 * 1000);
|
|
117
|
+
const staleLeaseAfterMs = boundedInteger(options.staleLeaseAfterMs ?? DEFAULT_STALE_LEASE_AFTER_MS, 0, 24 * 60 * 60 * 1000);
|
|
118
|
+
const idFactory = options.idFactory ?? defaultIdFactory;
|
|
119
|
+
const processingSummarySource = normalizeRunnerProcessingSummarySource(options.processingSummarySource ?? (options.tickMode === 'scheduled' ? 'scheduled_tick' : 'manual_tick'));
|
|
120
|
+
const reasonCodes = [];
|
|
121
|
+
const receipts = [];
|
|
122
|
+
const processingSummaries = [];
|
|
123
|
+
const executionMetadata = [];
|
|
124
|
+
if (options.trustCheck !== false) {
|
|
125
|
+
const trust = checkRunnerBinaryTrust(options.executablePath ?? process.argv[1]);
|
|
126
|
+
if (!trust.trusted) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
runnerId,
|
|
130
|
+
processedJobCount: 0,
|
|
131
|
+
dueJobCount: 0,
|
|
132
|
+
recoveredLeaseIds: [],
|
|
133
|
+
receipts: [],
|
|
134
|
+
reasonCodes: [trust.reasonCode ?? RUNNER_REASON_CODES.RUNNER_BINARY_UNTRUSTED],
|
|
135
|
+
publicSafe: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const store = await openContinuitySqliteStore({
|
|
140
|
+
projectRoot,
|
|
141
|
+
adapter: options.adapter,
|
|
142
|
+
adapterLoader: options.adapterLoader,
|
|
143
|
+
now,
|
|
144
|
+
});
|
|
145
|
+
const heartbeatLoop = startRunnerTickHeartbeatLoop({ options, projectRoot, runnerId, now });
|
|
146
|
+
let summary;
|
|
147
|
+
try {
|
|
148
|
+
const recovered = store.recoverStaleLeases({
|
|
149
|
+
nowIso: now(),
|
|
150
|
+
staleAfterMs: staleLeaseAfterMs,
|
|
151
|
+
limit: dueJobLimit * 2,
|
|
152
|
+
});
|
|
153
|
+
if (recovered.recoveredLeaseIds.length > 0) {
|
|
154
|
+
reasonCodes.push(RUNNER_REASON_CODES.LEASE_EXPIRED);
|
|
155
|
+
for (const leaseId of recovered.recoveredLeaseIds) {
|
|
156
|
+
// Recovery evidence is intentionally compact and public. It is not tied to
|
|
157
|
+
// a synthetic job receipt because the lease row already carries job ID.
|
|
158
|
+
void leaseId;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const dueJobs = store.listDueJobs({ nowIso: now(), limit: dueJobLimit });
|
|
162
|
+
for (const job of dueJobs) {
|
|
163
|
+
const result = await processDueJob({
|
|
164
|
+
store,
|
|
165
|
+
job,
|
|
166
|
+
projectRoot,
|
|
167
|
+
runnerId,
|
|
168
|
+
leaseMs,
|
|
169
|
+
now,
|
|
170
|
+
idFactory,
|
|
171
|
+
processingSummarySource,
|
|
172
|
+
serverUrl: options.serverUrl,
|
|
173
|
+
serverInvoker: options.serverInvoker ?? defaultRunnerServerInvoker,
|
|
174
|
+
});
|
|
175
|
+
receipts.push(result.receipt);
|
|
176
|
+
processingSummaries.push(...result.processingSummaries);
|
|
177
|
+
if (result.executionMetadata)
|
|
178
|
+
executionMetadata.push(result.executionMetadata);
|
|
179
|
+
reasonCodes.push(result.receipt.reasonCode);
|
|
180
|
+
}
|
|
181
|
+
const headlessExecution = await runYoloExecutionPhase({
|
|
182
|
+
store,
|
|
183
|
+
projectRoot,
|
|
184
|
+
runnerId,
|
|
185
|
+
now,
|
|
186
|
+
metadata: executionMetadata,
|
|
187
|
+
adapterRegistry: options.headlessAdapterRegistry,
|
|
188
|
+
adapterOptions: options.headlessAdapterOptions,
|
|
189
|
+
runtimeDefaults: options.headlessRuntimeDefaults,
|
|
190
|
+
runtimeFallback: options.headlessRuntimeFallback,
|
|
191
|
+
limit: options.headlessExecutionLimit,
|
|
192
|
+
leaseMs: options.headlessExecutionLeaseMs ?? leaseMs,
|
|
193
|
+
staleLeaseAfterMs: options.headlessExecutionStaleLeaseAfterMs ?? staleLeaseAfterMs,
|
|
194
|
+
timeoutMs: options.headlessExecutionTimeoutMs,
|
|
195
|
+
backoffMs: options.headlessExecutionBackoffMs,
|
|
196
|
+
maxAttempts: options.headlessExecutionMaxAttempts,
|
|
197
|
+
queueCap: options.headlessExecutionQueueCap,
|
|
198
|
+
idFactory: options.headlessExecutionIdFactory,
|
|
199
|
+
});
|
|
200
|
+
reasonCodes.push(...headlessExecution.reasonCodes);
|
|
201
|
+
const headlessYoloopExecution = await runYoloopExecutionPhase({
|
|
202
|
+
store,
|
|
203
|
+
projectRoot,
|
|
204
|
+
runnerId,
|
|
205
|
+
now,
|
|
206
|
+
metadata: executionMetadata,
|
|
207
|
+
adapterRegistry: options.headlessAdapterRegistry,
|
|
208
|
+
adapterOptions: options.headlessAdapterOptions,
|
|
209
|
+
runtimeDefaults: options.headlessRuntimeDefaults,
|
|
210
|
+
runtimeFallback: options.headlessRuntimeFallback,
|
|
211
|
+
limit: options.headlessExecutionLimit,
|
|
212
|
+
leaseMs: options.headlessExecutionLeaseMs ?? leaseMs,
|
|
213
|
+
staleLeaseAfterMs: options.headlessExecutionStaleLeaseAfterMs ?? staleLeaseAfterMs,
|
|
214
|
+
timeoutMs: options.headlessExecutionTimeoutMs,
|
|
215
|
+
backoffMs: options.headlessExecutionBackoffMs,
|
|
216
|
+
maxAttempts: options.headlessExecutionMaxAttempts,
|
|
217
|
+
queueCap: options.headlessExecutionQueueCap,
|
|
218
|
+
maxIterations: options.headlessYoloopMaxIterations,
|
|
219
|
+
maxDurationMs: options.headlessYoloopMaxDurationMs,
|
|
220
|
+
localConcurrencyCap: options.headlessYoloopLocalConcurrencyCap,
|
|
221
|
+
yoloopInvoker: options.headlessYoloopInvoker,
|
|
222
|
+
serverUrl: options.serverUrl,
|
|
223
|
+
idFactory: options.headlessExecutionIdFactory,
|
|
224
|
+
});
|
|
225
|
+
reasonCodes.push(...headlessYoloopExecution.reasonCodes);
|
|
226
|
+
summary = {
|
|
227
|
+
ok: receipts.every((receipt) => receipt.outcome !== 'failed') && headlessExecution.ok && headlessYoloopExecution.ok,
|
|
228
|
+
runnerId,
|
|
229
|
+
processedJobCount: receipts.length,
|
|
230
|
+
dueJobCount: dueJobs.length,
|
|
231
|
+
recoveredLeaseIds: recovered.recoveredLeaseIds,
|
|
232
|
+
receipts,
|
|
233
|
+
...(processingSummaries.length > 0 ? { processingSummaries } : {}),
|
|
234
|
+
headlessExecution,
|
|
235
|
+
headlessYoloopExecution,
|
|
236
|
+
reasonCodes: unique(reasonCodes.length > 0 ? reasonCodes : [RUNNER_REASON_CODES.OK]),
|
|
237
|
+
publicSafe: true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
await heartbeatLoop.stop();
|
|
242
|
+
store.close();
|
|
243
|
+
}
|
|
244
|
+
if (!summary)
|
|
245
|
+
throw new Error('runner-tick-failed');
|
|
246
|
+
return await withPostTickPortalSync(summary, {
|
|
247
|
+
options,
|
|
248
|
+
projectRoot,
|
|
249
|
+
runnerId,
|
|
250
|
+
localStateChanged: runnerTickChangedLocalState(summary),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async function withPostTickPortalSync(summary, input) {
|
|
254
|
+
const portalSync = input.options.portalSync;
|
|
255
|
+
if (portalSync === false || portalSync === undefined)
|
|
256
|
+
return summary;
|
|
257
|
+
if (!input.localStateChanged) {
|
|
258
|
+
const heartbeat = await runRunnerPortalHeartbeat({
|
|
259
|
+
...portalSync,
|
|
260
|
+
projectRoot: portalSync.projectRoot ?? input.projectRoot,
|
|
261
|
+
runnerId: portalSync.runnerId ?? input.runnerId,
|
|
262
|
+
sessionId: portalSync.sessionId ?? input.options.portalHeartbeatSessionId,
|
|
263
|
+
serverUrl: portalSync.serverUrl ?? input.options.serverUrl,
|
|
264
|
+
adapter: portalSync.adapter ?? input.options.adapter,
|
|
265
|
+
adapterLoader: portalSync.adapterLoader ?? input.options.adapterLoader,
|
|
266
|
+
now: portalSync.now ?? input.options.now,
|
|
267
|
+
mode: portalSync.mode ?? (input.options.tickMode === 'scheduled' ? 'scheduled' : 'background'),
|
|
268
|
+
timeoutMs: portalSync.heartbeatTimeoutMs ?? input.options.portalHeartbeatTimeoutMs,
|
|
269
|
+
});
|
|
270
|
+
return { ...summary, portalHeartbeat: heartbeat };
|
|
271
|
+
}
|
|
272
|
+
const sync = await runRunnerPortalSync({
|
|
273
|
+
...portalSync,
|
|
274
|
+
projectRoot: portalSync.projectRoot ?? input.projectRoot,
|
|
275
|
+
runnerId: portalSync.runnerId ?? input.runnerId,
|
|
276
|
+
sessionId: portalSync.sessionId ?? input.options.portalHeartbeatSessionId,
|
|
277
|
+
serverUrl: portalSync.serverUrl ?? input.options.serverUrl,
|
|
278
|
+
adapter: portalSync.adapter ?? input.options.adapter,
|
|
279
|
+
adapterLoader: portalSync.adapterLoader ?? input.options.adapterLoader,
|
|
280
|
+
now: portalSync.now ?? input.options.now,
|
|
281
|
+
mode: portalSync.mode ?? (input.options.tickMode === 'scheduled' ? 'scheduled' : 'background'),
|
|
282
|
+
});
|
|
283
|
+
return { ...summary, portalSync: sync };
|
|
284
|
+
}
|
|
285
|
+
function runnerTickChangedLocalState(summary) {
|
|
286
|
+
return summary.recoveredLeaseIds.length > 0
|
|
287
|
+
|| summary.receipts.length > 0
|
|
288
|
+
|| (summary.processingSummaries?.length ?? 0) > 0
|
|
289
|
+
|| (summary.headlessExecution?.processedAttemptCount ?? 0) > 0
|
|
290
|
+
|| (summary.headlessExecution?.recoveredAttemptIds.length ?? 0) > 0
|
|
291
|
+
|| (summary.headlessYoloopExecution?.processedAttemptCount ?? 0) > 0
|
|
292
|
+
|| (summary.headlessYoloopExecution?.recoveredAttemptIds.length ?? 0) > 0;
|
|
293
|
+
}
|
|
294
|
+
function startRunnerTickHeartbeatLoop(input) {
|
|
295
|
+
const portalSync = input.options.portalSync;
|
|
296
|
+
if (portalSync === false || portalSync === undefined)
|
|
297
|
+
return { stop: async () => undefined };
|
|
298
|
+
const intervalMs = boundedInteger(input.options.portalHeartbeatIntervalMs ?? DEFAULT_RUNNER_HEARTBEAT_INTERVAL_MS, 0, 24 * 60 * 60 * 1000);
|
|
299
|
+
if (intervalMs <= 0)
|
|
300
|
+
return { stop: async () => undefined };
|
|
301
|
+
const backoffMs = boundedInteger(input.options.portalHeartbeatBackoffMs ?? DEFAULT_RUNNER_HEARTBEAT_BACKOFF_MS, 1_000, 30 * 60 * 1000);
|
|
302
|
+
const jitterRatio = boundedNumber(input.options.portalHeartbeatJitterRatio ?? DEFAULT_RUNNER_HEARTBEAT_JITTER_RATIO, 0, 0.5);
|
|
303
|
+
const timeoutMs = boundedInteger(input.options.portalHeartbeatTimeoutMs ?? portalSync.heartbeatTimeoutMs ?? DEFAULT_RUNNER_HEARTBEAT_TIMEOUT_MS, 1_000, 60_000);
|
|
304
|
+
let stopped = false;
|
|
305
|
+
let timer;
|
|
306
|
+
let wake;
|
|
307
|
+
let nextDelayMs = jitteredDelayMs(intervalMs, jitterRatio, Math.random);
|
|
308
|
+
const waitForDelayOrStop = (delayMs) => new Promise((resolvePromise) => {
|
|
309
|
+
wake = resolvePromise;
|
|
310
|
+
timer = setTimeout(resolvePromise, delayMs);
|
|
311
|
+
if (typeof timer.unref === 'function')
|
|
312
|
+
timer.unref();
|
|
313
|
+
});
|
|
314
|
+
const loop = (async () => {
|
|
315
|
+
while (!stopped) {
|
|
316
|
+
if (nextDelayMs > 0)
|
|
317
|
+
await waitForDelayOrStop(nextDelayMs);
|
|
318
|
+
if (stopped)
|
|
319
|
+
break;
|
|
320
|
+
const heartbeat = await runRunnerPortalHeartbeat({
|
|
321
|
+
...portalSync,
|
|
322
|
+
projectRoot: portalSync.projectRoot ?? input.projectRoot,
|
|
323
|
+
runnerId: portalSync.runnerId ?? input.runnerId,
|
|
324
|
+
sessionId: portalSync.sessionId ?? input.options.portalHeartbeatSessionId,
|
|
325
|
+
serverUrl: portalSync.serverUrl ?? input.options.serverUrl,
|
|
326
|
+
adapter: portalSync.adapter ?? input.options.adapter,
|
|
327
|
+
adapterLoader: portalSync.adapterLoader ?? input.options.adapterLoader,
|
|
328
|
+
now: portalSync.now ?? input.now,
|
|
329
|
+
mode: portalSync.mode ?? (input.options.tickMode === 'scheduled' ? 'scheduled' : 'background'),
|
|
330
|
+
timeoutMs,
|
|
331
|
+
}).catch(() => ({
|
|
332
|
+
ok: false,
|
|
333
|
+
runnerId: input.runnerId,
|
|
334
|
+
workspaceId: 'workspace-default',
|
|
335
|
+
workspaceLabel: 'Workspace workspace-default',
|
|
336
|
+
projectId: 'project-default',
|
|
337
|
+
projectLabel: 'Project project-default',
|
|
338
|
+
mode: input.options.tickMode === 'scheduled' ? 'scheduled' : 'background',
|
|
339
|
+
generatedAt: input.now(),
|
|
340
|
+
serverAccessToLocalhost: false,
|
|
341
|
+
serverHeldStream: false,
|
|
342
|
+
localhostCallback: false,
|
|
343
|
+
posted: { heartbeat: false },
|
|
344
|
+
counts: { redactions: 0 },
|
|
345
|
+
reasonCodes: [RUNNER_REASON_CODES.BACKOFF_SCHEDULED],
|
|
346
|
+
redactionEvidence: [],
|
|
347
|
+
publicSafe: true,
|
|
348
|
+
}));
|
|
349
|
+
nextDelayMs = jitteredDelayMs(heartbeat.ok ? intervalMs : backoffMs, jitterRatio, Math.random);
|
|
350
|
+
}
|
|
351
|
+
})();
|
|
352
|
+
return {
|
|
353
|
+
stop: async () => {
|
|
354
|
+
stopped = true;
|
|
355
|
+
if (timer)
|
|
356
|
+
clearTimeout(timer);
|
|
357
|
+
wake?.();
|
|
358
|
+
await loop.catch(() => undefined);
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
export async function runRunnerDoctor(options = {}) {
|
|
363
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
364
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
365
|
+
const generatedAt = now();
|
|
366
|
+
const checks = {};
|
|
367
|
+
let snapshotRows = null;
|
|
368
|
+
let store = null;
|
|
369
|
+
try {
|
|
370
|
+
store = await openContinuitySqliteStore({
|
|
371
|
+
projectRoot,
|
|
372
|
+
adapter: options.adapter,
|
|
373
|
+
adapterLoader: options.adapterLoader,
|
|
374
|
+
now,
|
|
375
|
+
});
|
|
376
|
+
snapshotRows = store.collectSnapshotRows({
|
|
377
|
+
nowIso: generatedAt,
|
|
378
|
+
jobLimit: 10,
|
|
379
|
+
staleLeaseLimit: 10,
|
|
380
|
+
receiptLimit: 10,
|
|
381
|
+
eventLimit: 10,
|
|
382
|
+
snapshotLimit: 5,
|
|
383
|
+
});
|
|
384
|
+
const journalMode = String(store.pragmaValue('journal_mode') ?? '').toLowerCase();
|
|
385
|
+
checks['db-integrity'] = doctorCheck('db-integrity', 'DB integrity', 'ok', [RUNNER_REASON_CODES.OK], 'Continuity SQLite integrity check completed.', {
|
|
386
|
+
jobs: count(snapshotRows.counts, 'jobs'),
|
|
387
|
+
receipts: count(snapshotRows.counts, 'receipts'),
|
|
388
|
+
events: count(snapshotRows.counts, 'events'),
|
|
389
|
+
});
|
|
390
|
+
checks['sqlite-wal'] = doctorCheck('sqlite-wal', 'SQLite WAL', journalMode === 'wal' ? 'ok' : 'blocked', journalMode === 'wal' ? [RUNNER_REASON_CODES.OK] : [RUNNER_REASON_CODES.SQLITE_INTEGRITY_FAILED], journalMode === 'wal' ? 'SQLite journal mode is WAL.' : 'SQLite journal mode is not WAL.');
|
|
391
|
+
checks.migrations = doctorCheck('migrations', 'Migrations', snapshotRows.schemaVersion >= 1 ? 'ok' : 'blocked', snapshotRows.schemaVersion >= 1 ? [RUNNER_REASON_CODES.OK] : [RUNNER_REASON_CODES.SQLITE_INTEGRITY_FAILED], `Continuity schema version ${snapshotRows.schemaVersion} is readable.`);
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
const reasonCode = classifyRunnerError(error);
|
|
395
|
+
checks['db-integrity'] = doctorCheck('db-integrity', 'DB integrity', 'blocked', [reasonCode], 'Continuity SQLite could not be opened safely.');
|
|
396
|
+
checks['sqlite-wal'] = doctorCheck('sqlite-wal', 'SQLite WAL', 'blocked', [reasonCode], 'SQLite WAL could not be verified.');
|
|
397
|
+
checks.migrations = doctorCheck('migrations', 'Migrations', 'blocked', [reasonCode], 'Continuity migrations could not be verified.');
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
if (store) {
|
|
401
|
+
try {
|
|
402
|
+
store.close();
|
|
403
|
+
}
|
|
404
|
+
catch { /* ignore close failures for doctor output */ }
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const staleLeases = snapshotRows ? count(snapshotRows.counts, 'staleLeases') : 0;
|
|
408
|
+
const staleHeadlessExecutionAttempts = snapshotRows ? count(snapshotRows.counts, 'staleHeadlessExecutionAttempts') : 0;
|
|
409
|
+
checks.leases = doctorCheck('leases', 'Lease health', staleLeases + staleHeadlessExecutionAttempts > 0 ? 'degraded' : 'ok', staleLeases + staleHeadlessExecutionAttempts > 0 ? [RUNNER_REASON_CODES.LEASE_EXPIRED] : [RUNNER_REASON_CODES.OK], staleLeases + staleHeadlessExecutionAttempts > 0 ? 'Stale job leases or headless execution attempts are eligible for bounded recovery.' : 'No stale leases or headless attempts reported.', { staleLeases, staleHeadlessExecutionAttempts });
|
|
410
|
+
const headlessAttempts = snapshotRows?.headlessExecutionAttempts ?? [];
|
|
411
|
+
const headlessReasonCodes = unique(headlessAttempts
|
|
412
|
+
.map((attempt) => attempt.reason_code)
|
|
413
|
+
.filter((reasonCode) => typeof reasonCode === 'string' && reasonCode.length > 0));
|
|
414
|
+
const headlessDiagnostics = summarizeRunnerHeadlessDiagnostics(headlessAttempts, headlessReasonCodes);
|
|
415
|
+
const headlessBlockingReasons = new Set([
|
|
416
|
+
RUNNER_REASON_CODES.HEADLESS_RUNTIME_UNRESOLVED,
|
|
417
|
+
RUNNER_REASON_CODES.HEADLESS_RUNTIME_UNTRUSTED,
|
|
418
|
+
RUNNER_REASON_CODES.HEADLESS_ADAPTER_UNAVAILABLE,
|
|
419
|
+
RUNNER_REASON_CODES.HEADLESS_RUNTIME_AUTH_REQUIRED,
|
|
420
|
+
'headless-model-rejected',
|
|
421
|
+
'headless-directive-rejected',
|
|
422
|
+
'headless-prompt-rejected',
|
|
423
|
+
'headless-yoloop-metadata-invalid',
|
|
424
|
+
'headless-yoloop-isolation-violation',
|
|
425
|
+
]);
|
|
426
|
+
const headlessAttemptCount = snapshotRows ? count(snapshotRows.counts, 'headlessExecutionAttempts') : 0;
|
|
427
|
+
const headlessBlocked = headlessReasonCodes.some((reasonCode) => headlessBlockingReasons.has(reasonCode));
|
|
428
|
+
const headlessBackoff = headlessReasonCodes.includes(RUNNER_REASON_CODES.HEADLESS_EXECUTION_BACKOFF)
|
|
429
|
+
|| headlessAttempts.some((attempt) => attempt.status === 'backoff');
|
|
430
|
+
const headlessRedacted = headlessReasonCodes.includes(RUNNER_REASON_CODES.HEADLESS_RECEIPT_REDACTED);
|
|
431
|
+
const headlessAuthRequired = headlessDiagnostics.authRequired > 0;
|
|
432
|
+
const headlessTrustOrAdapterBlocked = headlessDiagnostics.trustFailures > 0 || headlessDiagnostics.adapterUnavailable > 0;
|
|
433
|
+
const headlessDegraded = staleHeadlessExecutionAttempts > 0
|
|
434
|
+
|| headlessBackoff
|
|
435
|
+
|| headlessRedacted
|
|
436
|
+
|| headlessDiagnostics.timeout > 0
|
|
437
|
+
|| headlessDiagnostics.genericFailed > 0
|
|
438
|
+
|| headlessDiagnostics.redactions > 0;
|
|
439
|
+
checks['headless-execution'] = doctorCheck('headless-execution', 'Headless execution', headlessBlocked ? 'blocked' : headlessDegraded ? 'degraded' : headlessAttemptCount > 0 ? 'ok' : 'delegated', headlessReasonCodes.length > 0
|
|
440
|
+
? headlessReasonCodes
|
|
441
|
+
: headlessAttemptCount > 0
|
|
442
|
+
? [RUNNER_REASON_CODES.OK]
|
|
443
|
+
: [RUNNER_REASON_CODES.HEADLESS_RUNTIME_UNRESOLVED], headlessBlocked
|
|
444
|
+
? headlessAuthRequired && !headlessTrustOrAdapterBlocked
|
|
445
|
+
? 'Complete local runtime login/auth setup before starting a new headless attempt; default retry/backoff is not scheduled for auth-required.'
|
|
446
|
+
: 'Resolve public runtime/model trust, adapter availability or protected directive hard-stop reason codes before executing headless attempts.'
|
|
447
|
+
: headlessDegraded
|
|
448
|
+
? 'Headless attempts need bounded recovery, retry/backoff, timeout/generic failure follow-up or redaction review.'
|
|
449
|
+
: headlessAttemptCount > 0
|
|
450
|
+
? 'Headless attempts are represented by public status, runtime/model and reason-code evidence.'
|
|
451
|
+
: 'Headless resolver checks are delegated until durable headless work is queued.', {
|
|
452
|
+
attempts: headlessAttemptCount,
|
|
453
|
+
queuedBackoff: snapshotRows ? count(snapshotRows.counts, 'headlessQueuedAttempts') : 0,
|
|
454
|
+
running: snapshotRows ? count(snapshotRows.counts, 'headlessRunningAttempts') : 0,
|
|
455
|
+
staleHeadlessExecutionAttempts,
|
|
456
|
+
trustFailures: headlessDiagnostics.trustFailures,
|
|
457
|
+
adapterUnavailable: headlessDiagnostics.adapterUnavailable,
|
|
458
|
+
authRequired: headlessDiagnostics.authRequired,
|
|
459
|
+
retryBackoff: headlessDiagnostics.retryBackoff,
|
|
460
|
+
timeout: headlessDiagnostics.timeout,
|
|
461
|
+
genericFailed: headlessDiagnostics.genericFailed,
|
|
462
|
+
redactions: headlessDiagnostics.redactions,
|
|
463
|
+
});
|
|
464
|
+
const outboxCounts = snapshotRows?.outboxCounts ?? {};
|
|
465
|
+
const failedOutbox = count(outboxCounts, 'failed') + count(outboxCounts, 'dead_letter');
|
|
466
|
+
checks.outbox = doctorCheck('outbox', 'Outbox state', failedOutbox > 0 ? 'degraded' : 'ok', failedOutbox > 0 ? [RUNNER_REASON_CODES.BACKOFF_SCHEDULED] : [RUNNER_REASON_CODES.OK], failedOutbox > 0 ? 'Failed/dead-letter outbox entries can be retried with dedupe.' : 'Outbox has no failed/dead-letter entries.', {
|
|
467
|
+
pending: count(outboxCounts, 'pending'),
|
|
468
|
+
failed: count(outboxCounts, 'failed'),
|
|
469
|
+
deadLetter: count(outboxCounts, 'dead_letter'),
|
|
470
|
+
});
|
|
471
|
+
checks.metering = doctorCheck('metering', 'Metering receipts', 'ok', [RUNNER_REASON_CODES.OK], 'Metering evidence is represented by public job receipts and counts only.', { receipts: snapshotRows ? count(snapshotRows.counts, 'receipts') : 0 });
|
|
472
|
+
checks['portal-sync'] = doctorCheck('portal-sync', 'Portal sync', failedOutbox > 0 ? 'degraded' : 'ok', failedOutbox > 0 ? [RUNNER_REASON_CODES.BACKOFF_SCHEDULED] : [RUNNER_REASON_CODES.OK], failedOutbox > 0
|
|
473
|
+
? 'Portal sync worker is enabled but has retryable outbox debt; outbound sync remains client-owned.'
|
|
474
|
+
: 'Portal sync worker is enabled for outbound heartbeat, jobs sync, command polling and acknowledgement only.', {
|
|
475
|
+
pending: count(outboxCounts, 'pending'),
|
|
476
|
+
failed: count(outboxCounts, 'failed'),
|
|
477
|
+
deadLetter: count(outboxCounts, 'dead_letter'),
|
|
478
|
+
});
|
|
479
|
+
const trust = options.trustCheck === false
|
|
480
|
+
? { trusted: true, publicSafe: true }
|
|
481
|
+
: checkRunnerBinaryTrust(options.executablePath ?? process.argv[1]);
|
|
482
|
+
checks.runner = doctorCheck('runner', 'Runner health', trust.trusted ? 'ok' : 'blocked', trust.trusted ? [RUNNER_REASON_CODES.OK] : [trust.reasonCode ?? RUNNER_REASON_CODES.RUNNER_BINARY_UNTRUSTED], trust.trusted ? 'Runner binary trust check passed.' : 'Runner binary trust check failed.');
|
|
483
|
+
try {
|
|
484
|
+
const scheduler = await runRunnerSchedulerCommand({
|
|
485
|
+
command: 'doctor',
|
|
486
|
+
projectRoot,
|
|
487
|
+
executablePath: options.scheduler?.executablePath ?? options.executablePath ?? process.argv[1],
|
|
488
|
+
platform: options.scheduler?.platform,
|
|
489
|
+
env: options.scheduler?.env,
|
|
490
|
+
dryRun: true,
|
|
491
|
+
intervalSeconds: options.scheduler?.intervalSeconds,
|
|
492
|
+
strategy: options.scheduler?.strategy,
|
|
493
|
+
allowCronFallback: options.scheduler?.allowCronFallback,
|
|
494
|
+
trustCheck: options.scheduler?.trustCheck ?? options.trustCheck,
|
|
495
|
+
uid: options.scheduler?.uid,
|
|
496
|
+
commandAvailable: options.scheduler?.commandAvailable,
|
|
497
|
+
commandRunner: options.scheduler?.commandRunner,
|
|
498
|
+
trustChecker: options.scheduler?.trustChecker ?? checkRunnerBinaryTrust,
|
|
499
|
+
});
|
|
500
|
+
const schedulerBlocked = scheduler.reasonCodes.includes(RUNNER_REASON_CODES.RUNNER_BINARY_UNTRUSTED)
|
|
501
|
+
|| scheduler.reasonCodes.some((reasonCode) => reasonCode.startsWith('runner-binary-'))
|
|
502
|
+
|| scheduler.reasonCodes.includes('scheduler-install-unsafe');
|
|
503
|
+
checks.scheduler = doctorCheck('scheduler', 'Scheduler install', schedulerBlocked ? 'blocked' : scheduler.ok ? 'ok' : 'degraded', scheduler.reasonCodes, scheduler.ok ? 'Scheduler doctor selected a user-scope strategy.' : 'Scheduler doctor found public guidance or degraded capability.', { manifests: scheduler.manifestPaths.length, controlCommands: scheduler.controlCommands.length, legacyEntries: scheduler.legacyEntries.length });
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
checks.scheduler = doctorCheck('scheduler', 'Scheduler install', 'degraded', ['scheduler-command-failed'], publicErrorMessage(error));
|
|
507
|
+
}
|
|
508
|
+
const completeChecks = checks;
|
|
509
|
+
const reasonCodes = unique(Object.values(completeChecks).flatMap((check) => check.reasonCodes));
|
|
510
|
+
return {
|
|
511
|
+
ok: Object.values(completeChecks).every((check) => check.status !== 'blocked'),
|
|
512
|
+
generatedAt,
|
|
513
|
+
checks: completeChecks,
|
|
514
|
+
reasonCodes,
|
|
515
|
+
publicSafe: true,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
export function extractRunnerDirectives(metadata) {
|
|
519
|
+
if (!metadata)
|
|
520
|
+
return [];
|
|
521
|
+
const candidates = [
|
|
522
|
+
metadata.continuityRunnerDirectives,
|
|
523
|
+
metadata.runnerDirectives,
|
|
524
|
+
metadata.continuityDirectives,
|
|
525
|
+
];
|
|
526
|
+
for (const candidate of candidates) {
|
|
527
|
+
if (Array.isArray(candidate))
|
|
528
|
+
return candidate.filter(isRecord).map(normalizeDirective).filter(isRunnerDirective);
|
|
529
|
+
}
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
export function assertRunnerDirectiveTrusted(directive, jobId) {
|
|
533
|
+
if (!isRecord(directive))
|
|
534
|
+
throw new RunnerDirectiveError('Directive is not a public record.');
|
|
535
|
+
if (directive.publicSafe !== true)
|
|
536
|
+
throw new RunnerDirectiveError('Directive is not marked public-safe.');
|
|
537
|
+
const kind = typeof directive.kind === 'string' ? directive.kind : typeof directive.type === 'string' ? directive.type : '';
|
|
538
|
+
if (!ALLOWED_DIRECTIVE_KINDS.has(kind))
|
|
539
|
+
throw new RunnerDirectiveError('Directive kind is not allowlisted.');
|
|
540
|
+
const commandClass = typeof directive.commandClass === 'string' ? directive.commandClass : 'runner';
|
|
541
|
+
if (!ALLOWED_COMMAND_CLASSES.has(commandClass))
|
|
542
|
+
throw new RunnerDirectiveError('Directive command class is not allowlisted.');
|
|
543
|
+
const directiveJobId = typeof directive.jobId === 'string' ? directive.jobId : jobId;
|
|
544
|
+
if (directiveJobId !== jobId)
|
|
545
|
+
throw new RunnerDirectiveError('Directive job ID does not match the leased job.');
|
|
546
|
+
assertPublicSafeDirectiveValue(directive, 'directive');
|
|
547
|
+
}
|
|
548
|
+
export async function runRunnerCli(argv, options = {}) {
|
|
549
|
+
const parsed = parseRunnerArgs(argv);
|
|
550
|
+
if (parsed.command === 'help') {
|
|
551
|
+
process.stdout.write(`${RUNNER_HELP}\n`);
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
if (parsed.command === 'version') {
|
|
555
|
+
const trust = checkRunnerBinaryTrust(options.executablePath ?? process.argv[1]);
|
|
556
|
+
process.stdout.write(`${trust.packageVersion ?? '0.0.0'}\n`);
|
|
557
|
+
return trust.trusted ? 0 : 1;
|
|
558
|
+
}
|
|
559
|
+
if (parsed.command === 'doctor') {
|
|
560
|
+
try {
|
|
561
|
+
const summary = await runRunnerDoctor({
|
|
562
|
+
...options,
|
|
563
|
+
projectRoot: parsed.projectRoot ?? options.projectRoot,
|
|
564
|
+
executablePath: options.executablePath ?? process.argv[1],
|
|
565
|
+
scheduler: {
|
|
566
|
+
...options.scheduler,
|
|
567
|
+
strategy: parsed.strategy ?? options.scheduler?.strategy,
|
|
568
|
+
allowCronFallback: parsed.allowCronFallback ?? options.scheduler?.allowCronFallback,
|
|
569
|
+
executablePath: options.scheduler?.executablePath ?? options.executablePath ?? process.argv[1],
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
process.stdout.write(JSON.stringify(summary) + '\n');
|
|
573
|
+
return summary.ok ? 0 : 1;
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
process.stderr.write(JSON.stringify({ error_code: 'runner-doctor-failed', message: publicErrorMessage(error) }) + '\n');
|
|
577
|
+
return 1;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (isSchedulerCommand(parsed.command)) {
|
|
581
|
+
try {
|
|
582
|
+
const summary = await runRunnerSchedulerCommand({
|
|
583
|
+
command: parsed.command,
|
|
584
|
+
projectRoot: parsed.projectRoot ?? options.projectRoot,
|
|
585
|
+
executablePath: options.scheduler?.executablePath ?? options.executablePath ?? process.argv[1],
|
|
586
|
+
platform: options.scheduler?.platform,
|
|
587
|
+
env: options.scheduler?.env,
|
|
588
|
+
dryRun: parsed.dryRun ?? options.scheduler?.dryRun,
|
|
589
|
+
intervalSeconds: parsed.intervalSeconds ?? options.scheduler?.intervalSeconds,
|
|
590
|
+
strategy: parsed.strategy ?? options.scheduler?.strategy,
|
|
591
|
+
allowCronFallback: parsed.allowCronFallback ?? options.scheduler?.allowCronFallback,
|
|
592
|
+
trustCheck: options.scheduler?.trustCheck,
|
|
593
|
+
uid: options.scheduler?.uid,
|
|
594
|
+
commandAvailable: options.scheduler?.commandAvailable,
|
|
595
|
+
commandRunner: options.scheduler?.commandRunner,
|
|
596
|
+
trustChecker: options.scheduler?.trustChecker ?? checkRunnerBinaryTrust,
|
|
597
|
+
});
|
|
598
|
+
process.stdout.write(JSON.stringify(publicRunnerSchedulerCommandResult(summary)) + '\n');
|
|
599
|
+
return summary.ok ? 0 : 1;
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
process.stderr.write(JSON.stringify({ error_code: 'scheduler-command-failed', message: publicErrorMessage(error) }) + '\n');
|
|
603
|
+
return 1;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (parsed.command === 'heartbeat') {
|
|
607
|
+
try {
|
|
608
|
+
if (parsed.loop) {
|
|
609
|
+
const summary = await runRunnerPortalHeartbeatLoop({
|
|
610
|
+
...options,
|
|
611
|
+
projectRoot: parsed.projectRoot ?? options.projectRoot,
|
|
612
|
+
runnerId: parsed.runnerId ?? options.runnerId,
|
|
613
|
+
sessionId: parsed.sessionId ?? options.portalHeartbeatSessionId,
|
|
614
|
+
serverUrl: parsed.serverUrl ?? options.serverUrl,
|
|
615
|
+
workspaceId: parsed.workspaceId,
|
|
616
|
+
workspaceLabel: parsed.workspaceLabel,
|
|
617
|
+
projectId: parsed.projectId,
|
|
618
|
+
projectLabel: parsed.projectLabel,
|
|
619
|
+
mode: parsed.scheduled ? 'scheduled' : 'background',
|
|
620
|
+
intervalMs: parsed.intervalMs ?? options.portalHeartbeatIntervalMs,
|
|
621
|
+
maxRuns: parsed.maxRuns ?? options.portalSyncMaxRuns,
|
|
622
|
+
backoffMs: parsed.heartbeatBackoffMs ?? options.portalHeartbeatBackoffMs,
|
|
623
|
+
timeoutMs: parsed.heartbeatTimeoutMs ?? options.portalHeartbeatTimeoutMs,
|
|
624
|
+
jitterRatio: parsed.heartbeatJitterRatio ?? options.portalHeartbeatJitterRatio,
|
|
625
|
+
});
|
|
626
|
+
process.stdout.write(JSON.stringify(summary) + '\n');
|
|
627
|
+
return summary.ok ? 0 : 1;
|
|
628
|
+
}
|
|
629
|
+
const summary = await runRunnerPortalHeartbeat({
|
|
630
|
+
...options,
|
|
631
|
+
projectRoot: parsed.projectRoot ?? options.projectRoot,
|
|
632
|
+
runnerId: parsed.runnerId ?? options.runnerId,
|
|
633
|
+
sessionId: parsed.sessionId ?? options.portalHeartbeatSessionId,
|
|
634
|
+
serverUrl: parsed.serverUrl ?? options.serverUrl,
|
|
635
|
+
workspaceId: parsed.workspaceId,
|
|
636
|
+
workspaceLabel: parsed.workspaceLabel,
|
|
637
|
+
projectId: parsed.projectId,
|
|
638
|
+
projectLabel: parsed.projectLabel,
|
|
639
|
+
mode: parsed.scheduled ? 'scheduled' : 'manual',
|
|
640
|
+
timeoutMs: parsed.heartbeatTimeoutMs ?? options.portalHeartbeatTimeoutMs,
|
|
641
|
+
});
|
|
642
|
+
process.stdout.write(JSON.stringify(summary) + '\n');
|
|
643
|
+
return summary.ok ? 0 : 1;
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
process.stderr.write(JSON.stringify({ error_code: 'portal-heartbeat-failed', message: publicErrorMessage(error) }) + '\n');
|
|
647
|
+
return 1;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (parsed.command === 'sync') {
|
|
651
|
+
try {
|
|
652
|
+
if (parsed.loop) {
|
|
653
|
+
const summary = await runRunnerPortalSyncLoop({
|
|
654
|
+
...options,
|
|
655
|
+
projectRoot: parsed.projectRoot ?? options.projectRoot,
|
|
656
|
+
runnerId: parsed.runnerId ?? options.runnerId,
|
|
657
|
+
serverUrl: parsed.serverUrl ?? options.serverUrl,
|
|
658
|
+
workspaceId: parsed.workspaceId,
|
|
659
|
+
workspaceLabel: parsed.workspaceLabel,
|
|
660
|
+
projectId: parsed.projectId,
|
|
661
|
+
projectLabel: parsed.projectLabel,
|
|
662
|
+
syncCursor: parsed.syncCursor,
|
|
663
|
+
mode: parsed.scheduled ? 'scheduled' : 'background',
|
|
664
|
+
intervalMs: parsed.intervalMs ?? options.portalSyncIntervalMs,
|
|
665
|
+
maxRuns: parsed.maxRuns ?? options.portalSyncMaxRuns,
|
|
666
|
+
});
|
|
667
|
+
process.stdout.write(JSON.stringify(summary) + '\n');
|
|
668
|
+
return summary.ok ? 0 : 1;
|
|
669
|
+
}
|
|
670
|
+
const summary = await runRunnerPortalSync({
|
|
671
|
+
...options,
|
|
672
|
+
projectRoot: parsed.projectRoot ?? options.projectRoot,
|
|
673
|
+
runnerId: parsed.runnerId ?? options.runnerId,
|
|
674
|
+
serverUrl: parsed.serverUrl ?? options.serverUrl,
|
|
675
|
+
workspaceId: parsed.workspaceId,
|
|
676
|
+
workspaceLabel: parsed.workspaceLabel,
|
|
677
|
+
projectId: parsed.projectId,
|
|
678
|
+
projectLabel: parsed.projectLabel,
|
|
679
|
+
syncCursor: parsed.syncCursor,
|
|
680
|
+
mode: parsed.scheduled ? 'scheduled' : 'manual',
|
|
681
|
+
});
|
|
682
|
+
process.stdout.write(JSON.stringify(summary) + '\n');
|
|
683
|
+
return summary.ok ? 0 : 1;
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
process.stderr.write(JSON.stringify({ error_code: 'portal-sync-failed', message: publicErrorMessage(error) }) + '\n');
|
|
687
|
+
return 1;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (parsed.command !== 'tick') {
|
|
691
|
+
process.stderr.write(JSON.stringify({ error_code: 'runner-command-unknown', message: 'Unknown neocortex-runner command.' }) + '\n');
|
|
692
|
+
return 1;
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
const summary = await runRunnerTick({
|
|
696
|
+
...options,
|
|
697
|
+
projectRoot: parsed.projectRoot ?? options.projectRoot,
|
|
698
|
+
runnerId: parsed.runnerId ?? options.runnerId,
|
|
699
|
+
dueJobLimit: parsed.limit ?? options.dueJobLimit,
|
|
700
|
+
leaseMs: parsed.leaseMs ?? options.leaseMs,
|
|
701
|
+
staleLeaseAfterMs: parsed.staleLeaseAfterMs ?? options.staleLeaseAfterMs,
|
|
702
|
+
headlessYoloopLocalConcurrencyCap: parsed.yoloopLocalCap ?? options.headlessYoloopLocalConcurrencyCap,
|
|
703
|
+
tickMode: parsed.scheduled ? 'scheduled' : options.tickMode,
|
|
704
|
+
processingSummarySource: parsed.scheduled ? 'scheduled_tick' : options.processingSummarySource,
|
|
705
|
+
serverUrl: parsed.serverUrl ?? options.serverUrl,
|
|
706
|
+
portalSync: runnerCliPortalSyncOptions(parsed, options, parsed.scheduled ? 'scheduled' : 'background'),
|
|
707
|
+
});
|
|
708
|
+
process.stdout.write(JSON.stringify(summary) + '\n');
|
|
709
|
+
return summary.ok ? 0 : 1;
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
const reasonCode = classifyRunnerError(error);
|
|
713
|
+
process.stderr.write(JSON.stringify({ error_code: reasonCode, message: publicErrorMessage(error) }) + '\n');
|
|
714
|
+
return 1;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async function processDueJob(input) {
|
|
718
|
+
const { store, job, projectRoot, runnerId, leaseMs, now, idFactory, processingSummarySource, serverUrl, serverInvoker } = input;
|
|
719
|
+
const jobId = job.job_id;
|
|
720
|
+
const tickId = idFactory('tick', jobId);
|
|
721
|
+
const leaseId = idFactory('lease', jobId);
|
|
722
|
+
const acquiredAt = now();
|
|
723
|
+
const expiresAt = new Date(Date.parse(acquiredAt) + leaseMs).toISOString();
|
|
724
|
+
let leaseHeld = false;
|
|
725
|
+
let leaseReleasedByDirective = false;
|
|
726
|
+
let finalReasonCode = RUNNER_REASON_CODES.OK;
|
|
727
|
+
let finalOutcome = 'applied';
|
|
728
|
+
let redactionSummary = [];
|
|
729
|
+
const acquired = store.acquireLease({
|
|
730
|
+
leaseId,
|
|
731
|
+
jobId,
|
|
732
|
+
runnerId,
|
|
733
|
+
acquiredAt,
|
|
734
|
+
expiresAt,
|
|
735
|
+
operationId: `${tickId}:acquire`,
|
|
736
|
+
idempotencyKey: `${tickId}:acquire`,
|
|
737
|
+
metadata: receiptMetadata({ jobId, operationId: tickId, runnerId, leaseId, status: 'running', reasonCode: RUNNER_REASON_CODES.OK }),
|
|
738
|
+
});
|
|
739
|
+
if (!acquired.inserted) {
|
|
740
|
+
const receipt = appendTickReceipt(store, {
|
|
741
|
+
jobId,
|
|
742
|
+
tickId,
|
|
743
|
+
leaseId,
|
|
744
|
+
status: 'ignored',
|
|
745
|
+
reasonCode: RUNNER_REASON_CODES.LEASE_CONFLICT,
|
|
746
|
+
createdAt: now(),
|
|
747
|
+
redactionSummary,
|
|
748
|
+
});
|
|
749
|
+
return { receipt, processingSummaries: [] };
|
|
750
|
+
}
|
|
751
|
+
leaseHeld = true;
|
|
752
|
+
try {
|
|
753
|
+
store.appendEvent({
|
|
754
|
+
eventId: `${tickId}:started`,
|
|
755
|
+
jobId,
|
|
756
|
+
operationId: `${tickId}:event:started`,
|
|
757
|
+
idempotencyKey: `${tickId}:event:started`,
|
|
758
|
+
type: 'runner_tick_started',
|
|
759
|
+
summary: `Runner tick started for ${jobId}.`,
|
|
760
|
+
reasonCode: RUNNER_REASON_CODES.OK,
|
|
761
|
+
createdAt: acquiredAt,
|
|
762
|
+
metadata: receiptMetadata({ jobId, operationId: tickId, runnerId, leaseId, status: 'running', reasonCode: RUNNER_REASON_CODES.OK }),
|
|
763
|
+
});
|
|
764
|
+
const args = job.kind === 'loop'
|
|
765
|
+
? `*loop tick @${jobId} --tick-id ${tickId} --lease-id ${leaseId} --runner-id ${runnerId}`
|
|
766
|
+
: job.kind === 'goal'
|
|
767
|
+
? `*goal tick @${jobId} --tick-id ${tickId} --lease-id ${leaseId} --runner-id ${runnerId}`
|
|
768
|
+
: `*runner tick --job-id ${jobId} --tick-id ${tickId} --lease-id ${leaseId} --runner-id ${runnerId}`;
|
|
769
|
+
const serverResult = await serverInvoker({ projectRoot, args, jobId, tickId, leaseId, runnerId, serverUrl });
|
|
770
|
+
if (!serverResult.success) {
|
|
771
|
+
finalReasonCode = RUNNER_REASON_CODES.RUNNER_SERVER_INVOKE_FAILED;
|
|
772
|
+
finalOutcome = 'failed';
|
|
773
|
+
const failedAt = now();
|
|
774
|
+
store.updateJobStatus({
|
|
775
|
+
jobId,
|
|
776
|
+
status: 'backoff',
|
|
777
|
+
updatedAt: failedAt,
|
|
778
|
+
reasonCode: RUNNER_REASON_CODES.BACKOFF_SCHEDULED,
|
|
779
|
+
nextRunAt: addMilliseconds(failedAt, DEFAULT_RUNNER_BACKOFF_MS),
|
|
780
|
+
});
|
|
781
|
+
store.appendEvent({
|
|
782
|
+
eventId: `${tickId}:server-failed`,
|
|
783
|
+
jobId,
|
|
784
|
+
operationId: `${tickId}:event:server-failed`,
|
|
785
|
+
idempotencyKey: `${tickId}:event:server-failed`,
|
|
786
|
+
type: 'runner_tick_failed',
|
|
787
|
+
summary: 'Runner server tick invocation failed with public reason code.',
|
|
788
|
+
reasonCode: finalReasonCode,
|
|
789
|
+
createdAt: failedAt,
|
|
790
|
+
metadata: receiptMetadata({ jobId, operationId: tickId, runnerId, leaseId, status: 'failed', reasonCode: finalReasonCode }),
|
|
791
|
+
});
|
|
792
|
+
const receipt = appendTickReceipt(store, { jobId, tickId, leaseId, status: finalOutcome, reasonCode: finalReasonCode, createdAt: now(), redactionSummary });
|
|
793
|
+
return { receipt, processingSummaries: [] };
|
|
794
|
+
}
|
|
795
|
+
const directives = extractRunnerDirectives(serverResult.metadata);
|
|
796
|
+
const mutationDirectives = directives.filter((directive) => !isProcessingSummaryDirective(directive));
|
|
797
|
+
let directiveResult = { appliedCount: 0, releasedLease: false, changedStatus: false, redactionSummary: [] };
|
|
798
|
+
try {
|
|
799
|
+
directiveResult = executeRunnerDirectives(store, mutationDirectives, { jobId, tickId, leaseId, runnerId, now });
|
|
800
|
+
leaseReleasedByDirective = directiveResult.releasedLease;
|
|
801
|
+
redactionSummary = [...directiveResult.redactionSummary];
|
|
802
|
+
finalReasonCode = extractPublicRunnerReasonCode(serverResult.metadata) ?? finalReasonCode;
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
finalReasonCode = RUNNER_REASON_CODES.RUNNER_DIRECTIVE_UNTRUSTED;
|
|
806
|
+
finalOutcome = 'failed';
|
|
807
|
+
redactionSummary = [error instanceof Error ? error.message : 'Directive rejected.'];
|
|
808
|
+
store.updateJobStatus({ jobId, status: 'blocked', updatedAt: now(), reasonCode: finalReasonCode });
|
|
809
|
+
store.appendEvent({
|
|
810
|
+
eventId: `${tickId}:directive-rejected`,
|
|
811
|
+
jobId,
|
|
812
|
+
operationId: `${tickId}:event:directive-rejected`,
|
|
813
|
+
idempotencyKey: `${tickId}:event:directive-rejected`,
|
|
814
|
+
type: 'runner_directive_rejected',
|
|
815
|
+
summary: 'Runner rejected an untrusted continuity directive.',
|
|
816
|
+
reasonCode: finalReasonCode,
|
|
817
|
+
createdAt: now(),
|
|
818
|
+
metadata: receiptMetadata({
|
|
819
|
+
jobId,
|
|
820
|
+
operationId: tickId,
|
|
821
|
+
runnerId,
|
|
822
|
+
leaseId,
|
|
823
|
+
status: 'failed',
|
|
824
|
+
reasonCode: finalReasonCode,
|
|
825
|
+
redactionSummary,
|
|
826
|
+
}),
|
|
827
|
+
});
|
|
828
|
+
const receipt = appendTickReceipt(store, { jobId, tickId, leaseId, status: finalOutcome, reasonCode: finalReasonCode, createdAt: now(), redactionSummary });
|
|
829
|
+
return { receipt, processingSummaries: [] };
|
|
830
|
+
}
|
|
831
|
+
const processingSummaries = persistRunnerProcessingSummaries(store, serverResult.metadata, {
|
|
832
|
+
jobId,
|
|
833
|
+
tickId,
|
|
834
|
+
leaseId,
|
|
835
|
+
runnerId,
|
|
836
|
+
source: processingSummarySource,
|
|
837
|
+
createdAt: now(),
|
|
838
|
+
});
|
|
839
|
+
const serverAlreadyMutated = Boolean(serverResult.metadata?.continuityStateUpdate);
|
|
840
|
+
if (!directiveResult.changedStatus && !serverAlreadyMutated) {
|
|
841
|
+
store.updateJobStatus({ jobId, status: 'active', updatedAt: now(), reasonCode: RUNNER_REASON_CODES.OK });
|
|
842
|
+
}
|
|
843
|
+
store.appendEvent({
|
|
844
|
+
eventId: `${tickId}:completed`,
|
|
845
|
+
jobId,
|
|
846
|
+
operationId: `${tickId}:event:completed`,
|
|
847
|
+
idempotencyKey: `${tickId}:event:completed`,
|
|
848
|
+
type: 'runner_tick_completed',
|
|
849
|
+
summary: `Runner tick completed with ${directiveResult.appliedCount} public directive(s).`,
|
|
850
|
+
reasonCode: finalReasonCode,
|
|
851
|
+
createdAt: now(),
|
|
852
|
+
metadata: receiptMetadata({ jobId, operationId: tickId, runnerId, leaseId, status: 'applied', reasonCode: finalReasonCode }),
|
|
853
|
+
});
|
|
854
|
+
const receipt = appendTickReceipt(store, { jobId, tickId, leaseId, status: finalOutcome, reasonCode: finalReasonCode, createdAt: now(), redactionSummary });
|
|
855
|
+
return { receipt, processingSummaries, ...(serverResult.metadata ? { executionMetadata: serverResult.metadata } : {}) };
|
|
856
|
+
}
|
|
857
|
+
finally {
|
|
858
|
+
if (leaseHeld && !leaseReleasedByDirective) {
|
|
859
|
+
try {
|
|
860
|
+
store.releaseLease({ leaseId, releasedAt: now(), reasonCode: finalReasonCode });
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
// The tick receipt above remains the durable public outcome. A stale
|
|
864
|
+
// recovery pass can expire the lease if this release races or crashes.
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function executeRunnerDirectives(store, directives, ctx) {
|
|
870
|
+
let appliedCount = 0;
|
|
871
|
+
let releasedLease = false;
|
|
872
|
+
let changedStatus = false;
|
|
873
|
+
const redactionSummary = [];
|
|
874
|
+
for (const directive of directives) {
|
|
875
|
+
assertRunnerDirectiveTrusted(directive, ctx.jobId);
|
|
876
|
+
switch (directive.kind) {
|
|
877
|
+
case 'noop':
|
|
878
|
+
appliedCount += 1;
|
|
879
|
+
break;
|
|
880
|
+
case 'append_event':
|
|
881
|
+
store.appendEvent({
|
|
882
|
+
eventId: stringField(directive, 'eventId') ?? `${ctx.tickId}:directive:${appliedCount}:event`,
|
|
883
|
+
jobId: ctx.jobId,
|
|
884
|
+
operationId: stringField(directive, 'operationId') ?? `${ctx.tickId}:directive:${appliedCount}:event`,
|
|
885
|
+
idempotencyKey: stringField(directive, 'idempotencyKey') ?? `${ctx.tickId}:directive:${appliedCount}:event`,
|
|
886
|
+
type: stringField(directive, 'eventType') ?? 'runner_directive_event',
|
|
887
|
+
summary: stringField(directive, 'summary') ?? 'Runner applied public continuity event directive.',
|
|
888
|
+
reasonCode: stringField(directive, 'reasonCode') ?? RUNNER_REASON_CODES.OK,
|
|
889
|
+
createdAt: stringField(directive, 'createdAt') ?? ctx.now(),
|
|
890
|
+
metadata: metadataField(directive),
|
|
891
|
+
});
|
|
892
|
+
appliedCount += 1;
|
|
893
|
+
break;
|
|
894
|
+
case 'append_receipt':
|
|
895
|
+
store.appendReceipt({
|
|
896
|
+
receiptId: stringField(directive, 'receiptId') ?? `${ctx.tickId}:directive:${appliedCount}:receipt`,
|
|
897
|
+
jobId: ctx.jobId,
|
|
898
|
+
operationId: stringField(directive, 'operationId') ?? `${ctx.tickId}:directive:${appliedCount}:receipt`,
|
|
899
|
+
idempotencyKey: stringField(directive, 'idempotencyKey') ?? `${ctx.tickId}:directive:${appliedCount}:receipt`,
|
|
900
|
+
status: receiptStatus(stringField(directive, 'status')),
|
|
901
|
+
reasonCode: stringField(directive, 'reasonCode') ?? RUNNER_REASON_CODES.OK,
|
|
902
|
+
createdAt: stringField(directive, 'createdAt') ?? ctx.now(),
|
|
903
|
+
metadata: metadataField(directive),
|
|
904
|
+
});
|
|
905
|
+
appliedCount += 1;
|
|
906
|
+
break;
|
|
907
|
+
case 'update_job_status':
|
|
908
|
+
store.updateJobStatus({
|
|
909
|
+
jobId: ctx.jobId,
|
|
910
|
+
status: jobStatus(stringField(directive, 'status')),
|
|
911
|
+
updatedAt: stringField(directive, 'updatedAt') ?? ctx.now(),
|
|
912
|
+
reasonCode: stringField(directive, 'reasonCode') ?? RUNNER_REASON_CODES.OK,
|
|
913
|
+
nextRunAt: stringField(directive, 'nextRunAt'),
|
|
914
|
+
});
|
|
915
|
+
appliedCount += 1;
|
|
916
|
+
changedStatus = true;
|
|
917
|
+
break;
|
|
918
|
+
case 'schedule_next_tick':
|
|
919
|
+
store.scheduleNextTick({
|
|
920
|
+
jobId: ctx.jobId,
|
|
921
|
+
scheduledAt: requiredDirectiveString(directive, 'scheduledAt'),
|
|
922
|
+
updatedAt: stringField(directive, 'updatedAt') ?? ctx.now(),
|
|
923
|
+
reasonCode: stringField(directive, 'reasonCode') ?? RUNNER_REASON_CODES.OK,
|
|
924
|
+
});
|
|
925
|
+
appliedCount += 1;
|
|
926
|
+
changedStatus = true;
|
|
927
|
+
break;
|
|
928
|
+
case 'release_lease':
|
|
929
|
+
store.releaseLease({
|
|
930
|
+
leaseId: stringField(directive, 'leaseId') ?? ctx.leaseId,
|
|
931
|
+
releasedAt: stringField(directive, 'releasedAt') ?? ctx.now(),
|
|
932
|
+
reasonCode: stringField(directive, 'reasonCode') ?? RUNNER_REASON_CODES.OK,
|
|
933
|
+
});
|
|
934
|
+
appliedCount += 1;
|
|
935
|
+
releasedLease = true;
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
const redactions = directive.redactionSummary;
|
|
939
|
+
if (Array.isArray(redactions)) {
|
|
940
|
+
redactionSummary.push(...redactions.filter((item) => typeof item === 'string').slice(0, 5));
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return { appliedCount, releasedLease, changedStatus, redactionSummary };
|
|
944
|
+
}
|
|
945
|
+
function persistRunnerProcessingSummaries(store, metadata, ctx) {
|
|
946
|
+
const persisted = [];
|
|
947
|
+
const seenExecutionIds = new Set();
|
|
948
|
+
for (const candidate of extractRunnerProcessingSummaryCandidates(metadata)) {
|
|
949
|
+
const input = normalizeRunnerProcessingSummaryCandidate(candidate, ctx);
|
|
950
|
+
if (!input || seenExecutionIds.has(input.executionId))
|
|
951
|
+
continue;
|
|
952
|
+
seenExecutionIds.add(input.executionId);
|
|
953
|
+
try {
|
|
954
|
+
store.recordProcessingOutputSummary(input);
|
|
955
|
+
persisted.push(publicRunnerProcessingSummary(input));
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
if (error instanceof ContinuitySqliteStoreError && error.code === 'sqlite-public-safety-rejected')
|
|
959
|
+
continue;
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return persisted;
|
|
964
|
+
}
|
|
965
|
+
function extractRunnerProcessingSummaryCandidates(metadata) {
|
|
966
|
+
if (!metadata)
|
|
967
|
+
return [];
|
|
968
|
+
const candidates = [];
|
|
969
|
+
const stateUpdate = recordField(metadata, 'continuityStateUpdate');
|
|
970
|
+
const operations = arrayField(stateUpdate, 'operations');
|
|
971
|
+
for (const operation of operations) {
|
|
972
|
+
if (!isRecord(operation))
|
|
973
|
+
continue;
|
|
974
|
+
const event = recordField(operation, 'event');
|
|
975
|
+
if (!event || stringField(event, 'type') !== CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE)
|
|
976
|
+
continue;
|
|
977
|
+
candidates.push({ operation, event, summary: recordField(event, 'processingOutputSummary') ?? event });
|
|
978
|
+
}
|
|
979
|
+
for (const directive of extractRunnerDirectives(metadata)) {
|
|
980
|
+
if (!isProcessingSummaryDirective(directive))
|
|
981
|
+
continue;
|
|
982
|
+
candidates.push({ directive, event: directive, summary: recordField(directive, 'processingOutputSummary') ?? directive });
|
|
983
|
+
}
|
|
984
|
+
for (const summary of [
|
|
985
|
+
...arrayField(metadata, 'processingSummaries'),
|
|
986
|
+
...arrayField(metadata, 'processingOutputSummaries'),
|
|
987
|
+
...arrayField(metadata, 'continuityProcessingSummaries'),
|
|
988
|
+
...arrayField(recordField(metadata, 'continuity'), 'processingSummaries'),
|
|
989
|
+
]) {
|
|
990
|
+
if (isRecord(summary))
|
|
991
|
+
candidates.push({ event: summary, summary });
|
|
992
|
+
}
|
|
993
|
+
return candidates;
|
|
994
|
+
}
|
|
995
|
+
function normalizeRunnerProcessingSummaryCandidate(candidate, ctx) {
|
|
996
|
+
if (!candidateMarkedPublicSafe(candidate))
|
|
997
|
+
return null;
|
|
998
|
+
const event = candidate.event ?? {};
|
|
999
|
+
const summary = candidate.summary ?? event;
|
|
1000
|
+
const operation = candidate.operation ?? {};
|
|
1001
|
+
const directive = candidate.directive ?? {};
|
|
1002
|
+
const executionId = stringField(summary, 'executionId')
|
|
1003
|
+
?? stringField(event, 'executionId')
|
|
1004
|
+
?? stringField(operation, 'operationId')
|
|
1005
|
+
?? stringField(directive, 'operationId')
|
|
1006
|
+
?? ctx.tickId;
|
|
1007
|
+
const jobId = stringField(summary, 'jobId')
|
|
1008
|
+
?? stringField(event, 'jobId')
|
|
1009
|
+
?? stringField(operation, 'jobId')
|
|
1010
|
+
?? stringField(directive, 'jobId')
|
|
1011
|
+
?? ctx.jobId;
|
|
1012
|
+
if (jobId !== ctx.jobId)
|
|
1013
|
+
return null;
|
|
1014
|
+
const publicSummary = stringField(summary, 'publicSummary')
|
|
1015
|
+
?? stringField(event, 'publicSummary')
|
|
1016
|
+
?? stringField(event, 'summary')
|
|
1017
|
+
?? stringField(directive, 'summary');
|
|
1018
|
+
if (!executionId || !publicSummary)
|
|
1019
|
+
return null;
|
|
1020
|
+
const metadata = mergePublicSummaryMetadata(recordField(operation, 'metadata'), recordField(event, 'metadata'), recordField(summary, 'metadata'), recordField(directive, 'metadata'), {
|
|
1021
|
+
schemaVersion: 1,
|
|
1022
|
+
contractVersion: 'continuity-v1',
|
|
1023
|
+
executionId,
|
|
1024
|
+
jobId,
|
|
1025
|
+
operationId: executionId,
|
|
1026
|
+
runnerId: ctx.runnerId,
|
|
1027
|
+
leaseId: ctx.leaseId,
|
|
1028
|
+
processingOutputSource: ctx.source,
|
|
1029
|
+
publicSummary,
|
|
1030
|
+
});
|
|
1031
|
+
return {
|
|
1032
|
+
eventId: stringField(event, 'eventId') ?? stringField(summary, 'eventId') ?? stringField(directive, 'eventId'),
|
|
1033
|
+
executionId,
|
|
1034
|
+
jobId,
|
|
1035
|
+
idempotencyKey: stringField(operation, 'idempotencyKey') ?? stringField(directive, 'idempotencyKey') ?? executionId,
|
|
1036
|
+
goalId: stringField(summary, 'goalId') ?? stringField(event, 'goalId') ?? stringField(operation, 'goalId'),
|
|
1037
|
+
loopId: stringField(summary, 'loopId') ?? stringField(event, 'loopId') ?? stringField(operation, 'loopId'),
|
|
1038
|
+
publicSummary,
|
|
1039
|
+
reasonCode: stringField(summary, 'reasonCode') ?? stringField(event, 'reasonCode') ?? stringField(operation, 'reasonCode') ?? stringField(directive, 'reasonCode'),
|
|
1040
|
+
createdAt: stringField(summary, 'createdAt') ?? stringField(event, 'createdAt') ?? stringField(operation, 'createdAt') ?? stringField(directive, 'createdAt') ?? ctx.createdAt,
|
|
1041
|
+
source: ctx.source,
|
|
1042
|
+
metadata,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function isProcessingSummaryDirective(directive) {
|
|
1046
|
+
return directive.kind === 'append_event'
|
|
1047
|
+
&& (stringField(directive, 'eventType') ?? stringField(directive, 'type')) === CONTINUITY_PROCESSING_OUTPUT_SUMMARY_EVENT_TYPE;
|
|
1048
|
+
}
|
|
1049
|
+
function candidateMarkedPublicSafe(candidate) {
|
|
1050
|
+
const markers = [candidate.operation, candidate.event, candidate.summary, candidate.directive]
|
|
1051
|
+
.filter((item) => Boolean(item))
|
|
1052
|
+
.map((item) => item.publicSafe);
|
|
1053
|
+
if (markers.some((marker) => marker === false))
|
|
1054
|
+
return false;
|
|
1055
|
+
return markers.some((marker) => marker === true);
|
|
1056
|
+
}
|
|
1057
|
+
function mergePublicSummaryMetadata(...records) {
|
|
1058
|
+
const output = {};
|
|
1059
|
+
for (const record of records) {
|
|
1060
|
+
if (record)
|
|
1061
|
+
Object.assign(output, record);
|
|
1062
|
+
}
|
|
1063
|
+
return output;
|
|
1064
|
+
}
|
|
1065
|
+
function publicRunnerProcessingSummary(input) {
|
|
1066
|
+
return {
|
|
1067
|
+
eventId: input.eventId ?? processingSummaryOutputEventId(input.executionId),
|
|
1068
|
+
executionId: input.executionId,
|
|
1069
|
+
jobId: input.jobId,
|
|
1070
|
+
publicSummary: input.publicSummary,
|
|
1071
|
+
...(input.reasonCode ? { reasonCode: input.reasonCode } : {}),
|
|
1072
|
+
createdAt: input.createdAt,
|
|
1073
|
+
source: normalizeRunnerProcessingSummarySource(input.source),
|
|
1074
|
+
publicSafe: true,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function processingSummaryOutputEventId(executionId) {
|
|
1078
|
+
const safe = executionId.replace(/[^a-z0-9_.:-]+/gi, '-').replace(/^-+|-+$/g, '');
|
|
1079
|
+
if (safe.length > 0)
|
|
1080
|
+
return `event-processing-summary-${safe.slice(0, 160)}`;
|
|
1081
|
+
return `event-processing-summary-${createHash('sha256').update(executionId).digest('hex').slice(0, 32)}`;
|
|
1082
|
+
}
|
|
1083
|
+
function appendTickReceipt(store, input) {
|
|
1084
|
+
const receipt = {
|
|
1085
|
+
receiptId: `receipt-${sanitizeId(input.tickId)}`,
|
|
1086
|
+
jobId: input.jobId,
|
|
1087
|
+
operationId: input.tickId,
|
|
1088
|
+
idempotencyKey: input.tickId,
|
|
1089
|
+
status: input.status,
|
|
1090
|
+
reasonCode: input.reasonCode,
|
|
1091
|
+
appliedOperationIds: input.status === 'applied' ? [input.tickId] : undefined,
|
|
1092
|
+
ignoredOperationIds: input.status === 'ignored' ? [input.tickId] : undefined,
|
|
1093
|
+
createdAt: input.createdAt,
|
|
1094
|
+
metadata: receiptMetadata({
|
|
1095
|
+
jobId: input.jobId,
|
|
1096
|
+
operationId: input.tickId,
|
|
1097
|
+
leaseId: input.leaseId,
|
|
1098
|
+
status: input.status,
|
|
1099
|
+
reasonCode: input.reasonCode,
|
|
1100
|
+
redactionSummary: input.redactionSummary,
|
|
1101
|
+
}),
|
|
1102
|
+
};
|
|
1103
|
+
store.appendReceipt(receipt);
|
|
1104
|
+
return {
|
|
1105
|
+
jobId: input.jobId,
|
|
1106
|
+
tickId: input.tickId,
|
|
1107
|
+
leaseId: input.leaseId,
|
|
1108
|
+
outcome: input.status,
|
|
1109
|
+
reasonCode: input.reasonCode,
|
|
1110
|
+
timestamp: input.createdAt,
|
|
1111
|
+
redactionSummary: input.redactionSummary,
|
|
1112
|
+
publicSafe: true,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function receiptMetadata(input) {
|
|
1116
|
+
return {
|
|
1117
|
+
schemaVersion: 1,
|
|
1118
|
+
contractVersion: 'continuity-v1',
|
|
1119
|
+
jobId: input.jobId,
|
|
1120
|
+
operationId: input.operationId,
|
|
1121
|
+
...(input.runnerId ? { runnerId: input.runnerId } : {}),
|
|
1122
|
+
...(input.leaseId ? { leaseId: input.leaseId } : {}),
|
|
1123
|
+
status: input.status,
|
|
1124
|
+
reasonCode: input.reasonCode,
|
|
1125
|
+
redactionEvidence: (input.redactionSummary ?? []).slice(0, 20).map((summary) => ({
|
|
1126
|
+
field: boundPublicString(summary, 80),
|
|
1127
|
+
reason: 'unsafe_key',
|
|
1128
|
+
evidence: 'operation-denied',
|
|
1129
|
+
publicSafe: true,
|
|
1130
|
+
})),
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
function extractPublicRunnerReasonCode(metadata) {
|
|
1134
|
+
if (!metadata)
|
|
1135
|
+
return null;
|
|
1136
|
+
const continuity = isRecord(metadata.continuity) ? metadata.continuity : undefined;
|
|
1137
|
+
const loop = continuity && isRecord(continuity.loop) ? continuity.loop : undefined;
|
|
1138
|
+
const goal = continuity && isRecord(continuity.goal) ? continuity.goal : undefined;
|
|
1139
|
+
const candidates = [
|
|
1140
|
+
loop?.reasonCode,
|
|
1141
|
+
goal?.reasonCode,
|
|
1142
|
+
continuity?.reasonCode,
|
|
1143
|
+
];
|
|
1144
|
+
for (const candidate of candidates) {
|
|
1145
|
+
if (typeof candidate === 'string' && /^[a-z0-9][a-z0-9-]{0,79}$/.test(candidate) && !UNSAFE_VALUE_PATTERN.test(candidate)) {
|
|
1146
|
+
return candidate;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
function parseRunnerArgs(argv) {
|
|
1152
|
+
const first = argv[0];
|
|
1153
|
+
if (!first || first === '--help' || first === '-h' || first === 'help')
|
|
1154
|
+
return { command: 'help' };
|
|
1155
|
+
if (first === '--version' || first === '-v' || first === 'version')
|
|
1156
|
+
return { command: 'version' };
|
|
1157
|
+
if (first !== 'tick' && first !== 'sync' && first !== 'heartbeat' && !isSchedulerCommand(first))
|
|
1158
|
+
return { command: 'unknown' };
|
|
1159
|
+
let projectRoot;
|
|
1160
|
+
let runnerId;
|
|
1161
|
+
let serverUrl;
|
|
1162
|
+
let limit;
|
|
1163
|
+
let leaseMs;
|
|
1164
|
+
let staleLeaseAfterMs;
|
|
1165
|
+
let dryRun;
|
|
1166
|
+
let intervalSeconds;
|
|
1167
|
+
let intervalMs;
|
|
1168
|
+
let maxRuns;
|
|
1169
|
+
let heartbeatBackoffMs;
|
|
1170
|
+
let heartbeatTimeoutMs;
|
|
1171
|
+
let heartbeatJitterRatio;
|
|
1172
|
+
let sessionId;
|
|
1173
|
+
let strategy;
|
|
1174
|
+
let allowCronFallback;
|
|
1175
|
+
let scheduled;
|
|
1176
|
+
let loop;
|
|
1177
|
+
let workspaceId;
|
|
1178
|
+
let workspaceLabel;
|
|
1179
|
+
let projectId;
|
|
1180
|
+
let projectLabel;
|
|
1181
|
+
let syncCursor;
|
|
1182
|
+
let yoloopLocalCap;
|
|
1183
|
+
for (let index = 1; index < argv.length; index++) {
|
|
1184
|
+
const arg = argv[index];
|
|
1185
|
+
const next = argv[index + 1];
|
|
1186
|
+
if (arg === '--project-root') {
|
|
1187
|
+
projectRoot = next;
|
|
1188
|
+
index += 1;
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
if (arg === '--runner-id') {
|
|
1192
|
+
runnerId = next;
|
|
1193
|
+
index += 1;
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
if (arg === '--server-url') {
|
|
1197
|
+
serverUrl = next;
|
|
1198
|
+
index += 1;
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (arg === '--limit') {
|
|
1202
|
+
limit = Number(next);
|
|
1203
|
+
index += 1;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (arg === '--lease-ms') {
|
|
1207
|
+
leaseMs = Number(next);
|
|
1208
|
+
index += 1;
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
if (arg === '--stale-lease-after-ms') {
|
|
1212
|
+
staleLeaseAfterMs = Number(next);
|
|
1213
|
+
index += 1;
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
if (arg === '--dry-run') {
|
|
1217
|
+
dryRun = true;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if (arg === '--interval-seconds') {
|
|
1221
|
+
intervalSeconds = Number(next);
|
|
1222
|
+
index += 1;
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
if (arg === '--interval-ms') {
|
|
1226
|
+
intervalMs = Number(next);
|
|
1227
|
+
index += 1;
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
if (arg === '--max-runs') {
|
|
1231
|
+
maxRuns = Number(next);
|
|
1232
|
+
index += 1;
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
if (arg === '--backoff-ms') {
|
|
1236
|
+
heartbeatBackoffMs = Number(next);
|
|
1237
|
+
index += 1;
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
if (arg === '--heartbeat-timeout-ms' || arg === '--timeout-ms') {
|
|
1241
|
+
heartbeatTimeoutMs = Number(next);
|
|
1242
|
+
index += 1;
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (arg === '--jitter-ratio') {
|
|
1246
|
+
heartbeatJitterRatio = Number(next);
|
|
1247
|
+
index += 1;
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (arg === '--session-id') {
|
|
1251
|
+
sessionId = next;
|
|
1252
|
+
index += 1;
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
if (arg === '--strategy' && isSchedulerStrategy(next)) {
|
|
1256
|
+
strategy = next;
|
|
1257
|
+
index += 1;
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (arg === '--allow-cron-fallback') {
|
|
1261
|
+
allowCronFallback = true;
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
if (arg === '--no-cron-fallback') {
|
|
1265
|
+
allowCronFallback = false;
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
if (arg === '--scheduled') {
|
|
1269
|
+
scheduled = true;
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
if (arg === '--manual') {
|
|
1273
|
+
scheduled = false;
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
if (arg === '--loop' || arg === '--background') {
|
|
1277
|
+
loop = true;
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
if (arg === '--once') {
|
|
1281
|
+
loop = false;
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (arg === '--workspace-id') {
|
|
1285
|
+
workspaceId = next;
|
|
1286
|
+
index += 1;
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
if (arg === '--workspace-label') {
|
|
1290
|
+
workspaceLabel = next;
|
|
1291
|
+
index += 1;
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
if (arg === '--project-id') {
|
|
1295
|
+
projectId = next;
|
|
1296
|
+
index += 1;
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
if (arg === '--project-label') {
|
|
1300
|
+
projectLabel = next;
|
|
1301
|
+
index += 1;
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
if (arg === '--sync-cursor') {
|
|
1305
|
+
syncCursor = next;
|
|
1306
|
+
index += 1;
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
if (arg === '--yoloop-local-cap') {
|
|
1310
|
+
yoloopLocalCap = Number(next);
|
|
1311
|
+
index += 1;
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return {
|
|
1316
|
+
command: first,
|
|
1317
|
+
projectRoot,
|
|
1318
|
+
runnerId,
|
|
1319
|
+
serverUrl,
|
|
1320
|
+
limit,
|
|
1321
|
+
leaseMs,
|
|
1322
|
+
staleLeaseAfterMs,
|
|
1323
|
+
dryRun,
|
|
1324
|
+
intervalSeconds,
|
|
1325
|
+
intervalMs,
|
|
1326
|
+
maxRuns,
|
|
1327
|
+
heartbeatBackoffMs,
|
|
1328
|
+
heartbeatTimeoutMs,
|
|
1329
|
+
heartbeatJitterRatio,
|
|
1330
|
+
sessionId,
|
|
1331
|
+
strategy,
|
|
1332
|
+
allowCronFallback,
|
|
1333
|
+
scheduled,
|
|
1334
|
+
loop,
|
|
1335
|
+
workspaceId,
|
|
1336
|
+
workspaceLabel,
|
|
1337
|
+
projectId,
|
|
1338
|
+
projectLabel,
|
|
1339
|
+
syncCursor,
|
|
1340
|
+
yoloopLocalCap,
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function runnerCliPortalSyncOptions(parsed, options, mode) {
|
|
1344
|
+
if (options.portalSync === false)
|
|
1345
|
+
return false;
|
|
1346
|
+
const base = options.portalSync ?? {};
|
|
1347
|
+
return {
|
|
1348
|
+
...base,
|
|
1349
|
+
...(parsed.projectRoot ?? options.projectRoot ? { projectRoot: parsed.projectRoot ?? options.projectRoot } : {}),
|
|
1350
|
+
...(parsed.runnerId ?? options.runnerId ? { runnerId: parsed.runnerId ?? options.runnerId } : {}),
|
|
1351
|
+
...(parsed.sessionId ?? options.portalHeartbeatSessionId ? { sessionId: parsed.sessionId ?? options.portalHeartbeatSessionId } : {}),
|
|
1352
|
+
...(parsed.serverUrl ?? options.serverUrl ? { serverUrl: parsed.serverUrl ?? options.serverUrl } : {}),
|
|
1353
|
+
...(parsed.workspaceId ? { workspaceId: parsed.workspaceId } : {}),
|
|
1354
|
+
...(parsed.workspaceLabel ? { workspaceLabel: parsed.workspaceLabel } : {}),
|
|
1355
|
+
...(parsed.projectId ? { projectId: parsed.projectId } : {}),
|
|
1356
|
+
...(parsed.projectLabel ? { projectLabel: parsed.projectLabel } : {}),
|
|
1357
|
+
...(parsed.heartbeatTimeoutMs ?? options.portalHeartbeatTimeoutMs ? { heartbeatTimeoutMs: parsed.heartbeatTimeoutMs ?? options.portalHeartbeatTimeoutMs } : {}),
|
|
1358
|
+
mode,
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
const RUNNER_HELP = `
|
|
17
1362
|
neocortex-runner - Enterprise continuity local runner
|
|
18
1363
|
|
|
19
1364
|
Usage:
|
|
@@ -67,4 +1412,213 @@ Headless doctor evidence:
|
|
|
67
1412
|
only bounded attempt status, runtime/model IDs, counts, reason codes, cursors and
|
|
68
1413
|
redaction counts; raw adapter stdout/stderr, prompts, transcripts, private URLs,
|
|
69
1414
|
local absolute paths, secrets and vendor payloads are never printed.
|
|
70
|
-
`.trim();
|
|
1415
|
+
`.trim();
|
|
1416
|
+
function isSchedulerCommand(value) {
|
|
1417
|
+
return value === 'install' || value === 'start' || value === 'stop' || value === 'status' || value === 'doctor';
|
|
1418
|
+
}
|
|
1419
|
+
function isSchedulerStrategy(value) {
|
|
1420
|
+
return value === 'systemd-user' || value === 'launchd-user' || value === 'windows-task-scheduler' || value === 'cron-fallback';
|
|
1421
|
+
}
|
|
1422
|
+
class RunnerDirectiveError extends Error {
|
|
1423
|
+
constructor(message) {
|
|
1424
|
+
super(message);
|
|
1425
|
+
this.name = 'RunnerDirectiveError';
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
function classifyRunnerError(error) {
|
|
1429
|
+
if (isRecord(error)) {
|
|
1430
|
+
const code = typeof error.reasonCode === 'string' ? error.reasonCode : typeof error.code === 'string' ? error.code : undefined;
|
|
1431
|
+
if (code === RUNNER_REASON_CODES.SQLITE_ADAPTER_UNAVAILABLE || code === RUNNER_REASON_CODES.SQLITE_INTEGRITY_FAILED)
|
|
1432
|
+
return code;
|
|
1433
|
+
}
|
|
1434
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1435
|
+
if (/sqlite-adapter-unavailable/i.test(message))
|
|
1436
|
+
return RUNNER_REASON_CODES.SQLITE_ADAPTER_UNAVAILABLE;
|
|
1437
|
+
if (/sqlite-integrity-failed|not a database|file is not a database|database disk image is malformed|malformed/i.test(message))
|
|
1438
|
+
return RUNNER_REASON_CODES.SQLITE_INTEGRITY_FAILED;
|
|
1439
|
+
return 'runner-tick-failed';
|
|
1440
|
+
}
|
|
1441
|
+
function publicErrorMessage(error) {
|
|
1442
|
+
return boundPublicString(error instanceof Error ? error.message : String(error), 240);
|
|
1443
|
+
}
|
|
1444
|
+
function publicRunnerSchedulerCommandResult(summary) {
|
|
1445
|
+
if (summary.manifests.length === 0)
|
|
1446
|
+
return summary;
|
|
1447
|
+
return {
|
|
1448
|
+
...summary,
|
|
1449
|
+
manifests: summary.manifests.map((manifest) => ({
|
|
1450
|
+
...manifest,
|
|
1451
|
+
content: `[redacted ${manifest.kind} for project ${summary.identity.projectKey}]`,
|
|
1452
|
+
})),
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
function defaultRunnerId(projectRoot) {
|
|
1456
|
+
const hash = createHash('sha256').update(projectRoot).update(process.pid.toString()).digest('hex').slice(0, 12);
|
|
1457
|
+
return `runner-${hash}`;
|
|
1458
|
+
}
|
|
1459
|
+
function defaultIdFactory(prefix, jobId) {
|
|
1460
|
+
const entropy = randomBytes(4).toString('hex');
|
|
1461
|
+
return `${prefix}-${sanitizeId(jobId)}-${Date.now()}-${entropy}`;
|
|
1462
|
+
}
|
|
1463
|
+
function sanitizeId(value) {
|
|
1464
|
+
return value.replace(/[^a-z0-9_.:-]+/gi, '-').slice(0, 120);
|
|
1465
|
+
}
|
|
1466
|
+
function boundedInteger(value, min, max) {
|
|
1467
|
+
if (!Number.isFinite(value))
|
|
1468
|
+
return min;
|
|
1469
|
+
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
1470
|
+
}
|
|
1471
|
+
function boundedNumber(value, min, max) {
|
|
1472
|
+
if (!Number.isFinite(value))
|
|
1473
|
+
return min;
|
|
1474
|
+
return Math.max(min, Math.min(max, value));
|
|
1475
|
+
}
|
|
1476
|
+
function jitteredDelayMs(baseDelayMs, jitterRatio, random) {
|
|
1477
|
+
if (baseDelayMs <= 0)
|
|
1478
|
+
return 0;
|
|
1479
|
+
if (jitterRatio <= 0)
|
|
1480
|
+
return baseDelayMs;
|
|
1481
|
+
const boundedRandom = boundedNumber(random(), 0, 1);
|
|
1482
|
+
return Math.max(0, baseDelayMs + Math.floor(baseDelayMs * jitterRatio * boundedRandom));
|
|
1483
|
+
}
|
|
1484
|
+
function addMilliseconds(iso, ms) {
|
|
1485
|
+
const base = Date.parse(iso);
|
|
1486
|
+
return new Date((Number.isFinite(base) ? base : Date.now()) + ms).toISOString();
|
|
1487
|
+
}
|
|
1488
|
+
function normalizeDirective(value) {
|
|
1489
|
+
return typeof value.kind === 'string' ? value : { ...value, kind: value.type };
|
|
1490
|
+
}
|
|
1491
|
+
function isRunnerDirective(value) {
|
|
1492
|
+
return typeof value.kind === 'string' && value.publicSafe === true;
|
|
1493
|
+
}
|
|
1494
|
+
function assertPublicSafeDirectiveValue(value, path) {
|
|
1495
|
+
if (typeof value === 'string') {
|
|
1496
|
+
if (UNSAFE_VALUE_PATTERN.test(value))
|
|
1497
|
+
throw new RunnerDirectiveError(`Unsafe directive value at ${path}.`);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (value === null || typeof value === 'number' || typeof value === 'boolean')
|
|
1501
|
+
return;
|
|
1502
|
+
if (Array.isArray(value)) {
|
|
1503
|
+
value.forEach((item, index) => assertPublicSafeDirectiveValue(item, `${path}[${index}]`));
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
if (isRecord(value)) {
|
|
1507
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1508
|
+
if (UNSAFE_KEY_PATTERN.test(key))
|
|
1509
|
+
throw new RunnerDirectiveError(`Unsafe directive key at ${path}.${key}.`);
|
|
1510
|
+
assertPublicSafeDirectiveValue(child, `${path}.${key}`);
|
|
1511
|
+
}
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
throw new RunnerDirectiveError(`Unsupported directive value at ${path}.`);
|
|
1515
|
+
}
|
|
1516
|
+
function stringField(input, key) {
|
|
1517
|
+
const value = input[key];
|
|
1518
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
1519
|
+
}
|
|
1520
|
+
function requiredDirectiveString(input, key) {
|
|
1521
|
+
const value = stringField(input, key);
|
|
1522
|
+
if (!value)
|
|
1523
|
+
throw new RunnerDirectiveError(`Directive missing ${key}.`);
|
|
1524
|
+
return value;
|
|
1525
|
+
}
|
|
1526
|
+
function metadataField(input) {
|
|
1527
|
+
const value = input.metadata;
|
|
1528
|
+
return isRecord(value) ? value : undefined;
|
|
1529
|
+
}
|
|
1530
|
+
function recordField(input, key) {
|
|
1531
|
+
if (!input)
|
|
1532
|
+
return undefined;
|
|
1533
|
+
const value = input[key];
|
|
1534
|
+
return isRecord(value) ? value : undefined;
|
|
1535
|
+
}
|
|
1536
|
+
function arrayField(input, key) {
|
|
1537
|
+
if (!input)
|
|
1538
|
+
return [];
|
|
1539
|
+
const value = input[key];
|
|
1540
|
+
return Array.isArray(value) ? value : [];
|
|
1541
|
+
}
|
|
1542
|
+
function receiptStatus(value) {
|
|
1543
|
+
return value && RECEIPT_STATUSES.has(value) ? value : 'applied';
|
|
1544
|
+
}
|
|
1545
|
+
function jobStatus(value) {
|
|
1546
|
+
if (!value || !JOB_STATUSES.has(value))
|
|
1547
|
+
throw new RunnerDirectiveError('Directive status is not allowlisted.');
|
|
1548
|
+
return value;
|
|
1549
|
+
}
|
|
1550
|
+
function normalizeRunnerProcessingSummarySource(value) {
|
|
1551
|
+
return value === 'scheduled_tick' || value === 'runner_tick' || value === 'manual_tick' ? value : 'manual_tick';
|
|
1552
|
+
}
|
|
1553
|
+
function isRecord(value) {
|
|
1554
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1555
|
+
}
|
|
1556
|
+
function unique(values) {
|
|
1557
|
+
return Array.from(new Set(values));
|
|
1558
|
+
}
|
|
1559
|
+
function count(input, key) {
|
|
1560
|
+
return typeof input[key] === 'number' ? input[key] : 0;
|
|
1561
|
+
}
|
|
1562
|
+
function summarizeRunnerHeadlessDiagnostics(attempts, reasonCodes) {
|
|
1563
|
+
return {
|
|
1564
|
+
trustFailures: countRunnerHeadlessSignals(attempts, reasonCodes, RUNNER_HEADLESS_TRUST_FAILURE_REASON_CODES),
|
|
1565
|
+
adapterUnavailable: countRunnerHeadlessSignals(attempts, reasonCodes, RUNNER_HEADLESS_ADAPTER_UNAVAILABLE_REASON_CODES),
|
|
1566
|
+
authRequired: countRunnerHeadlessSignals(attempts, reasonCodes, RUNNER_HEADLESS_AUTH_REQUIRED_REASON_CODES),
|
|
1567
|
+
retryBackoff: countRunnerHeadlessSignals(attempts, reasonCodes, RUNNER_HEADLESS_BACKOFF_REASON_CODES, new Set(['backoff'])),
|
|
1568
|
+
timeout: countRunnerHeadlessSignals(attempts, reasonCodes, RUNNER_HEADLESS_TIMEOUT_REASON_CODES, new Set(['timed_out'])),
|
|
1569
|
+
genericFailed: countRunnerGenericHeadlessFailures(attempts, reasonCodes),
|
|
1570
|
+
redactions: attempts.reduce((total, attempt) => total + runnerHeadlessRedactionCount(attempt), 0),
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function countRunnerHeadlessSignals(attempts, snapshotReasonCodes, reasonCodes, statuses = new Set()) {
|
|
1574
|
+
const attemptCount = attempts.filter((attempt) => {
|
|
1575
|
+
const reasonMatch = attempt.reason_code ? reasonCodes.has(attempt.reason_code) : false;
|
|
1576
|
+
return reasonMatch || statuses.has(attempt.status);
|
|
1577
|
+
}).length;
|
|
1578
|
+
if (attemptCount > 0)
|
|
1579
|
+
return attemptCount;
|
|
1580
|
+
return snapshotReasonCodes.some((reasonCode) => reasonCodes.has(reasonCode)) ? 1 : 0;
|
|
1581
|
+
}
|
|
1582
|
+
function countRunnerGenericHeadlessFailures(attempts, snapshotReasonCodes) {
|
|
1583
|
+
const attemptCount = attempts.filter((attempt) => {
|
|
1584
|
+
if (attempt.reason_code && RUNNER_HEADLESS_GENERIC_FAILURE_REASON_CODES.has(attempt.reason_code))
|
|
1585
|
+
return true;
|
|
1586
|
+
if (attempt.status !== 'failed')
|
|
1587
|
+
return false;
|
|
1588
|
+
return !attempt.reason_code
|
|
1589
|
+
|| (!RUNNER_HEADLESS_TRUST_FAILURE_REASON_CODES.has(attempt.reason_code)
|
|
1590
|
+
&& !RUNNER_HEADLESS_ADAPTER_UNAVAILABLE_REASON_CODES.has(attempt.reason_code)
|
|
1591
|
+
&& !RUNNER_HEADLESS_AUTH_REQUIRED_REASON_CODES.has(attempt.reason_code)
|
|
1592
|
+
&& !RUNNER_HEADLESS_BACKOFF_REASON_CODES.has(attempt.reason_code)
|
|
1593
|
+
&& !RUNNER_HEADLESS_TIMEOUT_REASON_CODES.has(attempt.reason_code));
|
|
1594
|
+
}).length;
|
|
1595
|
+
if (attemptCount > 0)
|
|
1596
|
+
return attemptCount;
|
|
1597
|
+
return snapshotReasonCodes.some((reasonCode) => RUNNER_HEADLESS_GENERIC_FAILURE_REASON_CODES.has(reasonCode)) ? 1 : 0;
|
|
1598
|
+
}
|
|
1599
|
+
function runnerHeadlessRedactionCount(attempt) {
|
|
1600
|
+
if (!attempt.redaction_summary_json)
|
|
1601
|
+
return 0;
|
|
1602
|
+
try {
|
|
1603
|
+
const parsed = JSON.parse(attempt.redaction_summary_json);
|
|
1604
|
+
return Array.isArray(parsed) ? parsed.length : 0;
|
|
1605
|
+
}
|
|
1606
|
+
catch {
|
|
1607
|
+
return 0;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
function doctorCheck(id, label, status, reasonCodes, summary, counts) {
|
|
1611
|
+
return {
|
|
1612
|
+
id,
|
|
1613
|
+
label,
|
|
1614
|
+
status,
|
|
1615
|
+
reasonCodes: unique(reasonCodes.length > 0 ? reasonCodes : [RUNNER_REASON_CODES.OK]),
|
|
1616
|
+
summary: boundPublicString(summary, 180),
|
|
1617
|
+
...(counts ? { counts } : {}),
|
|
1618
|
+
publicSafe: true,
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function boundPublicString(value, maxChars) {
|
|
1622
|
+
const redacted = UNSAFE_VALUE_PATTERN.test(value) ? '[REDACTED]' : value;
|
|
1623
|
+
return redacted.length <= maxChars ? redacted : `${redacted.slice(0, maxChars)}…[TRUNCATED:${redacted.length - maxChars}]`;
|
|
1624
|
+
}
|